refactor(battle): 重构战斗目标查找与位置管理逻辑

新增全局位置网格系统,用于按索引存储敌我单位实体ID:
-  在SingletonModuleComp添加heroGrid与monGrid数组
-  为HeroAttrsComp新增posIndex字段记录位置索引并初始化

优化战斗核心流程:
-  重构MissionHeroComp的位置选择逻辑,拆分方法返回位置索引而非直接坐标,优化位置占用检测
-  重构SCastSystem的目标查找与收集逻辑,改用网格遍历替代全量实体查询,大幅提升性能
-  统一三路单位的查找优先级,简化代码提升可维护性
-  完善Hero与Monster的创建销毁流程,同步更新网格的单位注册与注销信息
This commit is contained in:
pan
2026-06-17 09:45:46 +08:00
parent 06a47842dd
commit b6b2dff986
7 changed files with 144 additions and 142 deletions

View File

@@ -45,6 +45,8 @@ export class SingletonModuleComp extends ecs.Comp {
in_select: false,
in_fight: false,
stop_mon_action: false,
heroGrid: [-1, -1, -1, -1, -1, -1],
monGrid: [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1],
};
finish_guides: number[] = [0]
data: any = {

View File

@@ -42,6 +42,13 @@ export class Hero extends ecs.Entity {
view.node.destroy();
}
const model = this.get(HeroAttrsComp);
if (model && model.posIndex >= 0) {
if (smc.mission.heroGrid[model.posIndex] === this.eid) {
smc.mission.heroGrid[model.posIndex] = -1;
}
}
// 手动移除组件,确保 ecs 侧引用及时释放
this.remove(HeroViewComp);
this.remove(HeroAttrsComp);
@@ -66,7 +73,7 @@ export class Hero extends ecs.Entity {
* 2) 初始化表现与属性数据
* 3) 播放下落入场并在落地后启用碰撞与移动
*/
load(pos: Vec3 = Vec3.ZERO,scale:number = 1,uuid:number=1001, dropToY:number = pos.y,hero_lv:number=1, pool_lv:number=1) {
load(pos: Vec3 = Vec3.ZERO,scale:number = 1,uuid:number=1001, dropToY:number = pos.y,hero_lv:number=1, pool_lv:number=1, posIndex: number = -1) {
// 英雄始终朝右,表现缩放固定为正向
scale = 1
// 英雄等级在当前规则下上限为 3避免超配表范围
@@ -114,6 +121,10 @@ export class Hero extends ecs.Entity {
model.type = hero.type;
model.fac = FacSet.HERO;
model.dis = hero.dis ?? 720;
model.posIndex = posIndex;
if (posIndex >= 0) {
smc.mission.heroGrid[posIndex] = this.eid;
}
// 复制触发技能配置
model.call = hero.call;

View File

@@ -76,6 +76,7 @@ export class HeroAttrsComp extends ecs.Comp {
minSkillDistance: number = 0; // 最近技能攻击距离缓存不受MP影响用于停止位置判断
// ==================== 阵型位置 ====================
posIndex: number = -1;
// ==================== 标记状态 ====================
is_dead: boolean = false;
@@ -408,6 +409,7 @@ export class HeroAttrsComp extends ecs.Comp {
this.maxSkillDistance = 0;
this.minSkillDistance = 0;
this.posIndex = -1;
this.is_dead = false;
this.is_count_dead = false;

View File

@@ -113,6 +113,12 @@ export class Monster extends ecs.Entity {
Monster.putToPool(path, view.node);
}
if (model && model.posIndex >= 0) {
if (smc.mission.monGrid[model.posIndex] === this.eid) {
smc.mission.monGrid[model.posIndex] = -1;
}
}
// 手动移除组件,避免 ecs 引用滞留
this.remove(HeroViewComp);
this.remove(HeroAttrsComp);
@@ -170,6 +176,10 @@ export class Monster extends ecs.Entity {
model.type = hero.type;
model.fac = FacSet.MON;
model.dis = hero.dis ?? 720;
model.posIndex = laneIndex;
if (laneIndex >= 0) {
smc.mission.monGrid[laneIndex] = this.eid;
}
// 复制触发技能配置
model.call = hero.call;

View File

@@ -100,12 +100,17 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
let highestAp = baseAp;
// 2. 获取场上最高攻击力的英雄,保证后期奶量/增益绝对够用
ecs.query(ecs.allOf(HeroAttrsComp)).forEach(e => {
const attr = e.get(HeroAttrsComp);
if (attr && attr.fac === FacSet.HERO && !attr.is_dead && attr.ap > highestAp) {
highestAp = attr.ap;
for (const eid of smc.mission.heroGrid) {
if (eid >= 0) {
const entity = ecs.getEntityByEid(eid);
if (entity) {
const attr = entity.get(HeroAttrsComp);
if (attr && !attr.is_dead && attr.ap > highestAp) {
highestAp = attr.ap;
}
}
}
});
}
mockAttrs.ap = highestAp;
mockAttrs.critical = 0;
@@ -562,16 +567,21 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
*/
private collectFriendlyTargetEids(fac: number, selfEid: number | undefined, includeSelf: boolean): number[] {
const eids: number[] = [];
ecs.query(this.getHeroMatcher()).forEach(entity => {
const model = entity.get(HeroAttrsComp);
const view = entity.get(HeroViewComp);
if (!model || !view?.node || !view.ent) return;
if (model.fac !== fac) return;
if (model.is_dead || model.is_reviving) return;
const eid = view.ent.eid;
if (!includeSelf && typeof selfEid === "number" && eid === selfEid) return;
eids.push(eid);
});
const grid = fac === FacSet.HERO ? smc.mission.heroGrid : smc.mission.monGrid;
for (const eid of grid) {
if (eid >= 0) {
if (!includeSelf && typeof selfEid === "number" && eid === selfEid) continue;
const entity = ecs.getEntityByEid(eid);
if (entity) {
const model = entity.get(HeroAttrsComp);
if (model && !model.is_dead && !model.is_reviving) {
eids.push(eid);
}
}
}
}
return eids;
}
@@ -592,119 +602,85 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
return group === TGroup.Self;
}
/**
* 根据网格快速查找最近的敌人
* 英雄查找怪物按列从前往后列0 -> 列3列内优先同排
* 怪物查找英雄按列从前往后列0 -> 列1列内优先中路或同排
*/
private findNearestEnemyByGrid(heroAttrs: HeroAttrsComp, heroView: HeroViewComp, maxRange: number): HeroViewComp | null {
if (!heroView.node) return null;
const isHero = heroAttrs.fac === FacSet.HERO;
const myPosIndex = heroAttrs.posIndex;
let myRow = myPosIndex >= 0 ? myPosIndex % 3 : 1; // 默认中路
const currentX = heroView.node.position.x;
let targetView: HeroViewComp | null = null;
let minCol = -1;
if (isHero) {
// 英雄找怪物
for (let col = 0; col < 4; col++) {
// 列内顺序:优先同排,其次中路,再次其他
const rowOrder = myRow === 1 ? [1, 0, 2] : [myRow, 1, myRow === 0 ? 2 : 0];
for (const row of rowOrder) {
const idx = col * 3 + row;
const eid = smc.mission.monGrid[idx];
if (eid >= 0) {
const target = ecs.getEntityByEid(eid);
if (target) {
const tModel = target.get(HeroAttrsComp);
const tView = target.get(HeroViewComp);
if (tModel && !tModel.is_dead && !tModel.is_reviving && tView && tView.node) {
const dist = Math.abs(currentX - tView.node.position.x);
if (dist <= maxRange) {
return tView; // 找到列内最优且在射程内的目标,直接返回
}
}
}
}
}
}
} else {
// 怪物找英雄
for (let col = 0; col < 2; col++) {
// 列内顺序:怪物配置优先中路
const rowOrder = [1, myRow, myRow === 0 ? 2 : 0];
for (const row of rowOrder) {
const idx = col * 3 + row;
const eid = smc.mission.heroGrid[idx];
if (eid >= 0) {
const target = ecs.getEntityByEid(eid);
if (target) {
const tModel = target.get(HeroAttrsComp);
const tView = target.get(HeroViewComp);
if (tModel && !tModel.is_dead && !tModel.is_reviving && tView && tView.node) {
const dist = Math.abs(currentX - tView.node.position.x);
if (dist <= maxRange) {
return tView;
}
}
}
}
}
}
}
return null;
}
/**
* 在施法距离内查找最近敌人。
* 用于单体技能与基础目标参考
* 考虑三路设计同路Y差较小优先如果同路没有目标再考虑跨路
* 替换为网格化查找,大幅提升性能并解决同排优先问题
*/
private findNearestEnemyInRange(heroAttrs: HeroAttrsComp, heroView: HeroViewComp, maxRange: number): HeroViewComp | null {
if (!heroView.node) return null;
const currentX = heroView.node.position.x;
const currentY = heroView.node.position.y;
let nearest: HeroViewComp | null = null;
let minDist = Infinity;
let foundPreferredLane = false;
const isHero = heroAttrs.fac === FacSet.HERO;
ecs.query(this.getHeroMatcher()).forEach(entity => {
const attrs = entity.get(HeroAttrsComp);
const view = entity.get(HeroViewComp);
if (!attrs || !view?.node) return;
if (attrs.fac === heroAttrs.fac) return;
if (attrs.is_dead || attrs.is_reviving) return;
if (this.isOutOfBattleBounds(view.node.position.x)) return;
const distX = Math.abs(currentX - view.node.position.x);
if (distX > maxRange) return;
if (isHero) {
// 英雄单纯找X轴最近无视路线
if (distX < minDist) {
minDist = distX;
nearest = view;
}
} else {
// 怪物:优先找中路目标
const isMidLane = Math.abs(view.node.position.y - BoxSet.GAME_LINE) < 30; // BoxSet.GAME_LINE(100) 是中路
if (foundPreferredLane && !isMidLane) return;
if (isMidLane && !foundPreferredLane) {
foundPreferredLane = true;
minDist = distX;
nearest = view;
return;
}
if (distX >= minDist) return;
minDist = distX;
nearest = view;
}
});
return nearest;
return this.findNearestEnemyByGrid(heroAttrs, heroView, maxRange);
}
/**
* 在施法距离内查找“最前排”敌人。
* 依据施法者面向方向选择 x 轴上更前的目标
* 考虑三路设计:英雄找最前排,怪物优先找中路最前排
* 依据网格排布列0天然是最前排因此直接复用网格查找即可
*/
private findFrontEnemyInRange(heroAttrs: HeroAttrsComp, heroView: HeroViewComp, maxRange: number, nearestEnemy: HeroViewComp): HeroViewComp | null {
if (!heroView.node || !nearestEnemy.node) return null;
const currentX = heroView.node.position.x;
const currentY = heroView.node.position.y;
const direction = nearestEnemy.node.position.x >= currentX ? 1 : -1;
let frontEnemy: HeroViewComp | null = null;
let edgeX = direction > 0 ? Infinity : -Infinity;
let foundPreferredLane = false;
const isHero = heroAttrs.fac === FacSet.HERO;
ecs.query(this.getHeroMatcher()).forEach(entity => {
const attrs = entity.get(HeroAttrsComp);
const view = entity.get(HeroViewComp);
if (!attrs || !view?.node) return;
if (attrs.fac === heroAttrs.fac) return;
if (attrs.is_dead || attrs.is_reviving) return;
const enemyX = view.node.position.x;
if (this.isOutOfBattleBounds(enemyX)) return;
const dist = Math.abs(currentX - enemyX);
if (dist > maxRange) return;
if (isHero) {
// 英雄无视路线找X轴最前的
if (direction > 0) {
if (enemyX >= edgeX) return;
edgeX = enemyX;
} else {
if (enemyX <= edgeX) return;
edgeX = enemyX;
}
frontEnemy = view;
} else {
// 怪物:优先找中路最前排的
const isMidLane = Math.abs(view.node.position.y - BoxSet.GAME_LINE) < 30; // BoxSet.GAME_LINE(100) 是中路
if (foundPreferredLane && !isMidLane) return;
if (isMidLane && !foundPreferredLane) {
foundPreferredLane = true;
edgeX = enemyX;
frontEnemy = view;
return;
}
if (direction > 0) {
if (enemyX >= edgeX) return;
edgeX = enemyX;
} else {
if (enemyX <= edgeX) return;
edgeX = enemyX;
}
frontEnemy = view;
}
});
return frontEnemy;
return this.findNearestEnemyByGrid(heroAttrs, heroView, maxRange);
}
/**

View File

@@ -132,11 +132,14 @@ export class MissionHeroComp extends CCComp {
if (model && view) {
if (model.is_dead) {
view.alive();
const landingPos = this.pickPositionForHero([hero.eid]);
const posIndex = this.pickPositionIndexForHero([hero.eid]);
const landingPos = MissionHeroComp.HERO_POSITIONS[posIndex];
// 不再直接设置位置,而是播放下落入场动画
// 计算出出生点(空中)
const spawnPos: Vec3 = v3(landingPos.x, landingPos.y + MissionHeroComp.HERO_DROP_HEIGHT, 0);
view.node.setPosition(spawnPos);
model.posIndex = posIndex;
if (posIndex >= 0) smc.mission.heroGrid[posIndex] = hero.eid;
hero.playDropAnim(spawnPos, landingPos.y);
}
model.dirty_hp = true;
@@ -171,7 +174,7 @@ export class MissionHeroComp extends CCComp {
* 动态分配英雄上场的位置
* @param excludeEids 排除计算的实体ID数组避免复活或合成时把自己算成占据的位置
*/
private pickPositionForHero(excludeEids: number[] = []): Vec3 {
private pickPositionIndexForHero(excludeEids: number[] = []): number {
const heroes = this.getAllHeroes().filter(h => {
const m = h.get(HeroAttrsComp);
return m && !m.is_dead && !excludeEids.includes(h.eid);
@@ -179,28 +182,20 @@ export class MissionHeroComp extends CCComp {
const occupied = new Set<number>();
for (const h of heroes) {
const move = h.get(MoveComp); // MoveComp 记录了英雄当前的目标位置
if (move) {
for (let i = 0; i < MissionHeroComp.HERO_POSITIONS.length; i++) {
const pos = MissionHeroComp.HERO_POSITIONS[i];
if (Math.abs(move.targetX - pos.x) < 2 && Math.abs(move.baseY - pos.y) < 2) {
occupied.add(i);
break;
}
}
}
const m = h.get(HeroAttrsComp);
if (m && m.posIndex >= 0) occupied.add(m.posIndex);
}
// 优先中前(1) -> 上前(0) -> 下前(2) -> 中后(4) -> 上后(3) -> 下后(5)
const slotPriority = [1, 0, 2, 4, 3, 5];
for (const idx of slotPriority) {
if (!occupied.has(idx)) {
return MissionHeroComp.HERO_POSITIONS[idx];
return idx;
}
}
// 溢出:默认中前
return MissionHeroComp.HERO_POSITIONS[1];
return 1;
}
/**
@@ -217,9 +212,10 @@ export class MissionHeroComp extends CCComp {
console.log("addHero uuid:",uuid)
let hero = ecs.getEntity<Hero>(Hero);
let scale = 1
const landingPos = this.pickPositionForHero();
const posIndex = this.pickPositionIndexForHero();
const landingPos = MissionHeroComp.HERO_POSITIONS[posIndex];
let spawnPos:Vec3 = v3(landingPos.x, landingPos.y + MissionHeroComp.HERO_DROP_HEIGHT, 0);
hero.load(spawnPos,scale,uuid,landingPos.y,hero_lv,pool_lv);
hero.load(spawnPos,scale,uuid,landingPos.y,hero_lv,pool_lv,posIndex);
// 召唤完成后,派发事件以更新英雄面板
const model = hero.get(HeroAttrsComp);
@@ -246,14 +242,19 @@ export class MissionHeroComp extends CCComp {
* @param targetPos 指定生成位置
* @returns 实际生成的英雄等级
*/
private addMergedHero(uuid:number, hero_lv:number, pool_lv:number, ap:number, hp_max:number, targetPos?: Vec3): number {
private addMergedHero(uuid:number, hero_lv:number, pool_lv:number, ap:number, hp_max:number, targetPosIndex?: number, targetPos?: Vec3): number {
console.log("addMergedHero uuid:",uuid)
let hero = ecs.getEntity<Hero>(Hero);
let scale = 1
const landingPos = targetPos || this.pickPositionForHero();
let posIndex = targetPosIndex;
let landingPos = targetPos;
if (posIndex === undefined || posIndex < 0 || !landingPos) {
posIndex = this.pickPositionIndexForHero();
landingPos = MissionHeroComp.HERO_POSITIONS[posIndex];
}
let spawnPos:Vec3 = v3(landingPos.x, landingPos.y + MissionHeroComp.HERO_DROP_HEIGHT, 0);
hero.load(spawnPos,scale,uuid,landingPos.y,hero_lv,pool_lv);
hero.load(spawnPos,scale,uuid,landingPos.y,hero_lv,pool_lv,posIndex);
// 召唤完成后,派发事件以更新英雄面板
const model = hero.get(HeroAttrsComp);
@@ -481,14 +482,14 @@ export class MissionHeroComp extends CCComp {
sumHpMax += model.hp_max;
}
// 计算目标出生点(提前排除素材英雄所占的位置)
const landingPos = this.pickPositionForHero(mergeEids);
const posIndex = this.pickPositionIndexForHero(mergeEids);
const landingPos = MissionHeroComp.HERO_POSITIONS[posIndex];
const spawnPos:Vec3 = v3(landingPos.x, landingPos.y + MissionHeroComp.HERO_DROP_HEIGHT, 0);
// 汇聚 → 特效 → 生成
await this.mergeDestroyAtBirth(mergeHeroes, spawnPos);
await this.playMergeBoomFx(spawnPos);
return this.addMergedHero(uuid, Math.min(this.merge_max_lv, hero_lv + 1), pool_lv, sumAp, sumHpMax, landingPos);
return this.addMergedHero(uuid, Math.min(this.merge_max_lv, hero_lv + 1), pool_lv, sumAp, sumHpMax, posIndex, landingPos);
}
/**

View File

@@ -544,7 +544,7 @@ export const InfiniteModeConfig = {
*/
export const TestModeConfig = {
/** 是否开启单挑测试模式 */
enable: true,
enable: false,
/** 测试模式中生成怪物的基础生命值 (对应 1级 英雄) */
baseHp: 150,
/** 测试模式中生成怪物的基础攻击力 (对应 1级 英雄) */