refactor: 合并英雄与怪物移动组件为通用 MoveComp

重构移动系统,将 HeroMoveComp 和 MonMoveComp 合并为通用的 MoveComp 组件,统一移动逻辑。
- 移除 HeroMasterComp 相关代码,简化实体查询
- 统一战斗范围计算和阵型回归逻辑
- 调整移动边界和撤退范围配置
- 优化敌人查找算法,提高性能
This commit is contained in:
walkpan
2026-03-13 15:54:12 +08:00
parent 5ab5a51752
commit b12b421823
7 changed files with 330 additions and 47 deletions

View File

@@ -186,49 +186,49 @@ export const HeroInfo: Record<number, heroInfo> = {
// 1. 基础近战型
5201:{uuid:5201,name:"兽人战士",icon:"1001",path:"mo1", fac:FacSet.MON, kind:1,as:3.0,ss:10,
type:HType.warrior,lv:1,hp:60,ap:8,speed:180,skills:[6003],
rangeType: SkillRange.Long,
rangeType: SkillRange.Melee,
buff:[],info:"标准炮灰确保英雄能完成3次普攻积累天赋计数"},
// 2. 快速突击型
5301:{uuid:5301,name:"兽人斥候",icon:"1001",path:"mo1", fac:FacSet.MON, kind:1,as:1.2,ss:10,
type:HType.assassin,lv:1,hp:40,ap:12,speed:400,skills:[6003],
rangeType: SkillRange.Long,
rangeType: SkillRange.Melee,
buff:[],info:"快速突击:极高移速贴脸,检测护盾(7102)刷新率"},
// 3. 重型坦克型
5401:{uuid:5401,name:"兽人卫士",icon:"1001",path:"mo3", fac:FacSet.MON, kind:1,as:5.0,ss:10,
type:HType.warrior,lv:1,hp:200,ap:15,speed:60,skills:[6003],
rangeType: SkillRange.Long,
rangeType: SkillRange.Melee,
buff:[],info:"重型坦克:数值墙,检测玩家破甲(7008)与持续输出"},
// 4. 远程骚扰型
5501:{uuid:5501,name:"兽人射手",icon:"1001",path:"mo1", fac:FacSet.MON, kind:1,as:3.0,ss:10,
type:HType.remote,lv:1,hp:50,ap:10,speed:90,skills:[6203],
rangeType: SkillRange.Long,
rangeType: SkillRange.Melee,
buff:[],info:"远程骚扰:跨屏打击,迫使阵地分散或移动英雄"},
// 5. 特殊机制型
5601:{uuid:5601,name:"兽人自爆兵",icon:"1001",path:"mo1", fac:FacSet.MON, kind:1,as:3.0,ss:10,
type:HType.assassin,lv:1,hp:80,ap:200,speed:220,skills:[6003],
rangeType: SkillRange.Long,
rangeType: SkillRange.Melee,
buff:[],info:"特殊机制:极端伤害,漏怪即秒杀,检测减伤(7103)"},
// 召唤师:持续召唤小怪(后续可在技能系统中实现 SType.zhaohuan
5602:{uuid:5602,name:"兽人召唤师",icon:"1001",path:"mo1", fac:FacSet.MON, kind:1,as:3.0,ss:10,
type:HType.mage,lv:1,hp:150,ap:10,speed:100,skills:[6003],
rangeType: SkillRange.Long,
rangeType: SkillRange.Melee,
buff:[],info:"战术目标:持续召唤小怪,检测英雄大招清场频率"},
// 治疗者:为周围怪物回血(此处以提升治疗效果和生命回复为基础被动)
5603:{uuid:5603,name:"兽人祭司",icon:"1001",path:"mo1", fac:FacSet.MON, kind:1,as:3.0,ss:10,
type:HType.support,lv:1,hp:150,ap:10,speed:105,skills:[6003],
rangeType: SkillRange.Long,
rangeType: SkillRange.Melee,
buff:[],info:"战术目标:为怪群回血,检测玩家沉默(7006)覆盖率"},
// 光环怪为周围怪物提供增益此处以Buff效果提升与移动速度提升为基础被动
// Attrs.BUFF_UP=60 (RATIO=1)Attrs.SPEED=63 (RATIO=1)
5604:{uuid:5604,name:"兽人图腾师",icon:"1001",path:"mo1", fac:FacSet.MON, kind:1,as:3.0,ss:10,
type:HType.support,lv:1,hp:150,ap:10,speed:110,skills:[6003],
rangeType: SkillRange.Long,
rangeType: SkillRange.Melee,
buff:[],info:"战术目标:提供加速光环,改变怪群推进节奏"},
// 6. 精英/BOSS型
5701:{uuid:5701,name:"兽人首领(BOSS)",icon:"1001",path:"mo4", fac:FacSet.MON, kind:1,as:2.5,ss:10,
type:HType.warrior,lv:3,hp:2000,ap:60,speed:120,skills:[6003],
rangeType: SkillRange.Long,
rangeType: SkillRange.Melee,
buff:[],info:"终极考验极高HP检测大招重置与辐射协同输出"},
};

View File

@@ -8,20 +8,19 @@ import { BoxSet, FacSet, FightSet, IndexSet } from "../common/config/GameSet";
import { HeroInfo, HeroPos, HType } from "../common/config/heroSet";
import { GameEvent } from "../common/config/GameEvent";
import { Attrs} from "../common/config/HeroAttrs";
import { HeroMoveComp } from "./HeroMove";
import { MoveComp } from "./MoveComp";
import { mLogger } from "../common/Logger";
import { HeroMasterComp } from "./HeroMasterComp";
/** 角色实体 */
@ecs.register(`Hero`)
export class Hero extends ecs.Entity {
HeroModel!: HeroAttrsComp;
View!: HeroViewComp;
HeroMove!: HeroMoveComp;
HeroMove!: MoveComp;
debugMode: boolean = false; // 是否启用调试模式
protected init() {
this.addComponents<ecs.Comp>(
HeroMoveComp,
MoveComp,
HeroAttrsComp,
);
}
@@ -35,7 +34,6 @@ export class Hero extends ecs.Entity {
this.remove(HeroViewComp);
this.remove(HeroAttrsComp);
this.remove(HeroMasterComp)
super.destroy();
}
@@ -98,9 +96,10 @@ export class Hero extends ecs.Entity {
model.back_chance=FightSet.BACK_CHANCE
this.add(hv);
oops.message.dispatchEvent(GameEvent.MasterCalled,{uuid:uuid})
const move = this.get(HeroMoveComp);
const move = this.get(MoveComp);
move.direction = 1; // 向右移动
move.targetX = 0; // 右边界'
move.baseY = pos.y;
if(HeroInfo[uuid].type==HType.remote){
move.targetX = -100; // 右边界'
}
@@ -122,7 +121,7 @@ export class HeroLifecycleSystem extends ecs.ComblockSystem
implements ecs.IEntityEnterSystem, ecs.IEntityRemoveSystem {
filter() {
return ecs.allOf(HeroMoveComp);
return ecs.allOf(MoveComp);
}
entityEnter(e: ecs.Entity): void {

View File

@@ -8,14 +8,14 @@ import { HeroAttrsComp } from "./HeroAttrsComp";
import { BuffConf, SkillSet } from "../common/config/SkillSet";
import { getMonAttr, MonType } from "../map/RogueConfig";
import { HeroViewComp } from "./HeroViewComp";
import { MonMoveComp } from "./MonMove";
import { MoveComp } from "./MoveComp";
import { mLogger } from "../common/Logger";
/** 角色实体 */
@ecs.register(`Monster`)
export class Monster extends ecs.Entity {
HeroModel!: HeroAttrsComp;
HeroView!: HeroViewComp;
MonMove!: MonMoveComp;
MonMove!: MoveComp;
private debugMode: boolean = false; // 是否启用调试模式
// 多键对象池Map<prefabPath, NodePool>
@@ -40,7 +40,7 @@ export class Monster extends ecs.Entity {
protected init() {
this.addComponents<ecs.Comp>(
MonMoveComp,
MoveComp,
HeroAttrsComp,
);
}
@@ -135,10 +135,11 @@ export class Monster extends ecs.Entity {
oops.message.dispatchEvent("monster_load",this)
// 初始化移动参数,包括线路和生成顺序
const move = this.get(MonMoveComp);
const move = this.get(MoveComp);
move.reset();
move.direction = -1; // 向左移动
move.targetX = -800; // 左边界
move.targetX = Math.max(-320, Math.min(320, pos.x));
move.baseY = pos.y;
move.lane = lane; // 设置线路标识
move.spawnOrder = spawnOrder; // 设置生成顺序
smc.vmdata.mission_data.mon_num++
@@ -157,7 +158,7 @@ export class MonLifecycleSystem extends ecs.ComblockSystem
debugMode: boolean = false; // 是否启用调试模式
filter() {
return ecs.allOf(MonMoveComp);
return ecs.allOf(MoveComp);
}
entityEnter(e: ecs.Entity): void {

View File

@@ -77,8 +77,9 @@ export class MonMoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpda
// 战斗状态根据职业类型和rangeType执行智能战术
this.processCombatLogic(e, move, view, model, nearestEnemy);
} else {
// 非战斗状态:向目标移动
this.processMarchLogic(e, move, view, model);
// 非战斗状态:回归阵型
move.targetY = 0;
this.processReturnFormation(e, move, view, model);
model.is_atking = false;
}
}
@@ -204,8 +205,7 @@ export class MonMoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpda
/** 执行后撤逻辑 */
private performRetreat(view: HeroViewComp, move: MonMoveComp, model: HeroAttrsComp, currentX: number) {
const safeRetreatX = currentX - move.direction * 50;
// 怪物活动范围通常宽一些
if (safeRetreatX >= -450 && safeRetreatX <= 450) {
if (safeRetreatX >= -300 && safeRetreatX <= 300) {
const retreatSpeed = (model.speed / 3) * 0.8;
this.moveEntity(view, -move.direction, retreatSpeed);
model.is_atking = false;
@@ -216,11 +216,8 @@ export class MonMoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpda
}
}
/**
* 进军逻辑 (无敌人时)
* 策略向目标X移动 (通常是屏幕左侧)
*/
private processMarchLogic(e: ecs.Entity, move: MonMoveComp, view: HeroViewComp, model: HeroAttrsComp) {
/** 回归阵型逻辑 */
private processReturnFormation(e: ecs.Entity, move: MonMoveComp, view: HeroViewComp, model: HeroAttrsComp) {
const currentX = view.node.position.x;
const targetX = move.targetX;
@@ -228,7 +225,6 @@ export class MonMoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpda
const dir = targetX > currentX ? 1 : -1;
const speed = model.speed / 3;
// 修正朝向
move.direction = dir;
this.moveEntity(view, dir, speed);
@@ -255,7 +251,7 @@ export class MonMoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpda
}
// 地图边界限制
if (newX >= -450 && newX <= 450) {
if (newX >= -320 && newX <= 320) {
view.node.setPosition(newX, newY, 0);
view.status_change("move");
} else {

View File

@@ -0,0 +1,291 @@
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
import { HeroViewComp } from "./HeroViewComp";
import { HeroAttrsComp } from "./HeroAttrsComp";
import { smc } from "../common/SingletonModuleComp";
import { FacSet } from "../common/config/GameSet";
import { HType } from "../common/config/heroSet";
import { SkillRange } from "../common/config/SkillSet";
@ecs.register('MoveComp')
export class MoveComp extends ecs.Comp {
direction: number = 1;
targetX: number = 0;
moving: boolean = true;
targetY: number = 0;
baseY: number = 0;
lane: number = 0;
spawnOrder: number = 0;
reset() {
this.direction = 1;
this.targetX = 0;
this.moving = true;
this.targetY = 0;
this.baseY = 0;
this.lane = 0;
this.spawnOrder = 0;
}
}
interface MoveFacConfig {
moveMinX: number;
moveMaxX: number;
retreatMinX: number;
retreatMaxX: number;
}
@ecs.register('MoveSystem')
export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate {
private readonly facConfigs: Record<number, MoveFacConfig> = {
[FacSet.HERO]: {
moveMinX: -320,
moveMaxX: 320,
retreatMinX: -300,
retreatMaxX: 300,
},
[FacSet.MON]: {
moveMinX: -320,
moveMaxX: 320,
retreatMinX: -300,
retreatMaxX: 300,
}
};
filter(): ecs.IMatcher {
return ecs.allOf(MoveComp, HeroViewComp, HeroAttrsComp);
}
update(e: ecs.Entity) {
if (!smc.mission.play || smc.mission.pause) return;
const model = e.get(HeroAttrsComp);
const move = e.get(MoveComp);
const view = e.get(HeroViewComp);
if (!model || !move || !view || !view.node) return;
if (model.fac !== FacSet.HERO && model.fac !== FacSet.MON) return;
if (!move.moving) return;
if (model.fac === FacSet.MON && smc.mission.stop_mon_action) {
view.status_change("idle");
return;
}
if (model.is_stop || model.is_dead || model.is_reviving || model.in_stun || model.in_frost) {
if (!model.is_reviving) view.status_change("idle");
return;
}
this.updateRenderOrder(e);
const nearestEnemy = this.findNearestEnemy(e);
if (nearestEnemy) {
this.processCombatLogic(e, move, view, model, nearestEnemy);
} else {
move.targetY = 0;
this.processReturnFormation(e, move, view, model);
model.is_atking = false;
}
}
private processCombatLogic(e: ecs.Entity, move: MoveComp, view: HeroViewComp, model: HeroAttrsComp, enemy: HeroViewComp) {
let rangeType = model.rangeType;
if (rangeType === undefined) {
if (model.type === HType.warrior || model.type === HType.assassin) {
rangeType = SkillRange.Melee;
} else if (model.type === HType.remote) {
rangeType = SkillRange.Long;
} else {
rangeType = SkillRange.Mid;
}
}
switch (rangeType) {
case SkillRange.Melee:
this.processMeleeLogic(e, move, view, model, enemy);
break;
case SkillRange.Mid:
this.processMidLogic(e, move, view, model, enemy);
break;
case SkillRange.Long:
this.processLongLogic(e, move, view, model, enemy);
break;
default:
this.processMidLogic(e, move, view, model, enemy); // 默认中程
break;
}
}
private processMeleeLogic(e: ecs.Entity, move: MoveComp, view: HeroViewComp, model: HeroAttrsComp, enemy: HeroViewComp) {
const currentX = view.node.position.x;
const enemyX = enemy.node.position.x;
const dist = Math.abs(currentX - enemyX);
const [minRange, maxRange] = this.resolveCombatRange(model, 0, 75);
move.direction = enemyX > currentX ? 1 : -1;
if (dist < minRange) {
this.performRetreat(view, move, model, currentX);
} else if (dist <= maxRange) {
view.status_change("idle");
model.is_atking = true;
} else {
const speed = model.speed / 3;
this.moveEntity(view, move.direction, speed);
model.is_atking = false;
}
}
private processMidLogic(e: ecs.Entity, move: MoveComp, view: HeroViewComp, model: HeroAttrsComp, enemy: HeroViewComp) {
const currentX = view.node.position.x;
const enemyX = enemy.node.position.x;
const dist = Math.abs(currentX - enemyX);
const [minRange, maxRange] = this.resolveCombatRange(model, 120, 360);
move.direction = enemyX > currentX ? 1 : -1;
if (dist < minRange) {
// 太近了,后撤
this.performRetreat(view, move, model, currentX);
} else if (dist > maxRange) {
const speed = model.speed / 3;
this.moveEntity(view, move.direction, speed);
model.is_atking = false;
} else {
view.status_change("idle");
model.is_atking = true;
}
}
private processLongLogic(e: ecs.Entity, move: MoveComp, view: HeroViewComp, model: HeroAttrsComp, enemy: HeroViewComp) {
const currentX = view.node.position.x;
const enemyX = enemy.node.position.x;
const dist = Math.abs(currentX - enemyX);
const [minRange, maxRange] = this.resolveCombatRange(model, 360, 720);
move.direction = enemyX > currentX ? 1 : -1;
if (dist < minRange) {
// 太近了,后撤 (远程单位对距离更敏感)
this.performRetreat(view, move, model, currentX);
} else if (dist > maxRange) {
const speed = model.speed / 3;
this.moveEntity(view, move.direction, speed);
model.is_atking = false;
} else {
view.status_change("idle");
model.is_atking = true;
}
}
private performRetreat(view: HeroViewComp, move: MoveComp, model: HeroAttrsComp, currentX: number) {
const cfg = this.facConfigs[model.fac] || this.facConfigs[FacSet.HERO];
const safeRetreatX = currentX - move.direction * 50;
if (safeRetreatX >= cfg.retreatMinX && safeRetreatX <= cfg.retreatMaxX) {
const retreatSpeed = (model.speed / 3) * 0.8;
this.moveEntity(view, -move.direction, retreatSpeed);
model.is_atking = false;
} else {
view.status_change("idle");
model.is_atking = true;
}
}
private processReturnFormation(e: ecs.Entity, move: MoveComp, view: HeroViewComp, model: HeroAttrsComp) {
const currentX = view.node.position.x;
const targetX = this.getFixedFormationX(model);
move.targetX = targetX;
if (Math.abs(currentX - targetX) > 5) {
const dir = targetX > currentX ? 1 : -1;
const speed = model.speed / 3;
move.direction = dir;
this.moveEntity(view, dir, speed);
const newX = view.node.position.x;
if ((dir === 1 && newX > targetX) || (dir === -1 && newX < targetX)) {
view.node.setPosition(targetX, view.node.position.y, 0);
}
} else {
view.status_change("idle");
}
}
private getFixedFormationX(model: HeroAttrsComp): number {
let rangeType = model.rangeType;
if (rangeType === undefined || rangeType === null) {
if (model.type === HType.remote) {
rangeType = SkillRange.Long;
} else if (model.type === HType.mage || model.type === HType.support) {
rangeType = SkillRange.Mid;
} else {
rangeType = SkillRange.Melee;
}
}
const side = model.fac === FacSet.MON ? 1 : -1;
if (rangeType === SkillRange.Long) {
return 240 * side;
}
if (rangeType === SkillRange.Mid) {
return 200 * side;
}
return 0;
}
private moveEntity(view: HeroViewComp, direction: number, speed: number) {
const model = view.ent.get(HeroAttrsComp);
const move = view.ent.get(MoveComp);
if (!model || !move) return;
const cfg = this.facConfigs[model.fac] || this.facConfigs[FacSet.HERO];
const currentX = view.node.position.x;
const delta = speed * this.dt * direction;
let newX = view.node.position.x + delta;
if (currentX < cfg.moveMinX && direction < 0) {
view.status_change("idle");
return;
}
if (currentX > cfg.moveMaxX && direction > 0) {
view.status_change("idle");
return;
}
newX = Math.max(cfg.moveMinX, Math.min(cfg.moveMaxX, newX));
const newY = view.node.position.y;
view.node.setPosition(newX, newY, 0);
view.status_change("move");
}
private resolveCombatRange(model: HeroAttrsComp, defaultMin: number, defaultMax: number): [number, number] {
const minRange = model.getCachedMinSkillDistance();
const maxRange = model.getCachedMaxSkillDistance();
if (maxRange <= 0) return [defaultMin, defaultMax];
const safeMin = Math.max(0, Math.min(minRange, maxRange - 20));
return [safeMin, maxRange];
}
private findNearestEnemy(entity: ecs.Entity): HeroViewComp | null {
const currentView = entity.get(HeroViewComp);
if (!currentView?.node) return null;
const currentPos = currentView.node.position;
const myFac = entity.get(HeroAttrsComp).fac;
let nearest: HeroViewComp | null = null;
let minDis = Infinity;
// 优化查询:一次遍历
ecs.query(ecs.allOf(HeroAttrsComp, HeroViewComp)).forEach(e => {
const m = e.get(HeroAttrsComp);
if (m.fac !== myFac && !m.is_dead) {
const v = e.get(HeroViewComp);
if (v?.node) {
const d = Math.abs(currentPos.x - v.node.position.x);
if (d < minDis) {
minDis = d;
nearest = v;
}
}
}
});
return nearest;
}
private updateRenderOrder(entity: ecs.Entity) {
return;
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "836ec21a-d77d-4830-92bd-1e65887c5927",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -9,7 +9,6 @@ import { CardType, FightSet, CardKind } from "../common/config/GameSet";
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
import { HeroMasterComp } from "../hero/HeroMasterComp";
const { ccclass, property } = _decorator;
@@ -252,16 +251,7 @@ export class MissionCardComp extends CCComp {
*/
fetchCards(level: number, forcedType?: CardType){
// 获取主角已有的属性倾向 (已拥有的永久属性Buff)
let preferredAttrs: number[] = [];
// @ts-ignore
const entities = ecs.query(ecs.allOf(HeroMasterComp));
if (entities.length > 0) {
const role = entities[0];
const heroAttrs = role.get(HeroAttrsComp);
if (heroAttrs && heroAttrs.BUFFS) {
preferredAttrs = Object.keys(heroAttrs.BUFFS).map(Number);
}
}
// 使用 CardSet 的 getCardOptions 获取卡牌
// 这里我们要获取 4 张卡牌
@@ -441,13 +431,10 @@ export class MissionCardComp extends CCComp {
.to(0.1, { scale: new Vec3(1, 1, 1) })
.delay(0.5)
// .call(() => {
// // 使用 HeroMasterComp 查找主角实体
// // @ts-ignore
// const entities = ecs.query(ecs.allOf(HeroMasterComp));
// let role = entities.length > 0 ? entities[0] : null;
// if (!role) {
// mLogger.log(this.debugMode, 'MissionCard', `[MissionCard] 未找到挂载 HeroMasterComp 的主角实体`);
// } else {
// mLogger.log(this.debugMode, 'MissionCard', `[MissionCard] 成功定位主角实体: ${role.eid}`);
// }