feat(monster&spawn): 新增飞行怪物支持,重构怪物移动与刷怪系统
抽离MonMoveComp拆分怪物移动逻辑,让MoveComp仅负责英雄移动 新增Fly和FlyBoss怪物类型,配置三层飞行轨道支持空中怪物 重写波次刷怪逻辑,移除固定5槽限制,按轨道自由排布怪物 将怪物生成上限与恢复阈值从5/3调整为50/30 优化渲染排序逻辑,为飞行怪添加持续浮动动画 移除跨波怪物属性继承,波次切换时自动清理残留怪物
This commit is contained in:
@@ -7,6 +7,7 @@ import { HeroInfo } from "../common/config/heroSet";
|
||||
import { HeroAttrsComp } from "./HeroAttrsComp";
|
||||
import { HeroViewComp } from "./HeroViewComp";
|
||||
import { MoveComp } from "./MoveComp";
|
||||
import { MonMoveComp } from "./MonMoveComp";
|
||||
import { GameEvent } from "../common/config/GameEvent";
|
||||
/** 怪物实体:负责怪物对象池复用、属性初始化、入场动画与回收 */
|
||||
@ecs.register(`Monster`)
|
||||
@@ -16,7 +17,7 @@ export class Monster extends ecs.Entity {
|
||||
/** 怪物表现组件引用 */
|
||||
HeroView!: HeroViewComp;
|
||||
/** 怪物移动组件引用 */
|
||||
MonMove!: MoveComp;
|
||||
MonMove!: MonMoveComp;
|
||||
/** 调试开关,控制生命周期日志输出 */
|
||||
private debugMode: boolean = false;
|
||||
|
||||
@@ -95,7 +96,7 @@ export class Monster extends ecs.Entity {
|
||||
/** 注册实体必需组件:移动 + 属性 */
|
||||
protected init() {
|
||||
this.addComponents<ecs.Comp>(
|
||||
MoveComp,
|
||||
MonMoveComp,
|
||||
HeroAttrsComp,
|
||||
);
|
||||
}
|
||||
@@ -122,7 +123,7 @@ export class Monster extends ecs.Entity {
|
||||
* 2) 初始化表现、属性、技能与阵营
|
||||
* 3) 播放下落入场并在落地后启用碰撞与移动
|
||||
*/
|
||||
load(pos: Vec3 = Vec3.ZERO,scale:number = 1,uuid:number=1001, is_boss:boolean=false, dropToY:number = pos.y,mon_lv:number=1) {
|
||||
load(pos: Vec3 = Vec3.ZERO,scale:number = 1,uuid:number=1001, is_boss:boolean=false, dropToY:number = pos.y,mon_lv:number=1, flyLaneIndex:number = 0) {
|
||||
// 怪物默认朝左,表现缩放固定为负向
|
||||
scale=-1
|
||||
// 当前怪物尺寸固定,保留变量便于后续扩展
|
||||
@@ -200,7 +201,7 @@ export class Monster extends ecs.Entity {
|
||||
// 广播怪物加载事件,供刷怪与战斗系统联动
|
||||
oops.message.dispatchEvent("monster_load",this)
|
||||
// 初始化移动参数:方向、目标 X、站位基准 Y
|
||||
const move = this.get(MoveComp);
|
||||
const move = this.get(MonMoveComp);
|
||||
move.reset();
|
||||
move.direction = -1;
|
||||
move.targetX = pos.x;
|
||||
@@ -219,7 +220,19 @@ export class Monster extends ecs.Entity {
|
||||
if (!node || !node.isValid) return;
|
||||
// 落地后锁定最终位置,切换到落地完成状态
|
||||
node.setPosition(pos.x, dropToY, 0);
|
||||
// 如果是飞行怪,可以保持空中状态,这里依然调用 down
|
||||
view.playEnd("down");
|
||||
|
||||
// 飞行怪加一个轻微的上下浮动动效
|
||||
if (flyLaneIndex > 0) {
|
||||
tween(node)
|
||||
.by(1.5, { position: v3(0, 15, 0) }, { easing: "sineOut" })
|
||||
.by(1.5, { position: v3(0, -15, 0) }, { easing: "sineIn" })
|
||||
.union()
|
||||
.repeatForever()
|
||||
.start();
|
||||
}
|
||||
|
||||
move.moving = true;
|
||||
// 落地后启用怪物碰撞分组
|
||||
if (collider) {
|
||||
|
||||
229
assets/script/game/hero/MonMoveComp.ts
Normal file
229
assets/script/game/hero/MonMoveComp.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
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 { HeroDisVal, HType } from "../common/config/heroSet";
|
||||
import { Node } from "cc";
|
||||
|
||||
@ecs.register('MonMoveComp')
|
||||
export class MonMoveComp extends ecs.Comp {
|
||||
/** 朝向:1=向右,-1=向左 */
|
||||
direction: number = -1;
|
||||
/** 当前移动目标 X */
|
||||
targetX: number = 0;
|
||||
/** 是否允许移动(出生落地前会短暂关闭) */
|
||||
moving: boolean = true;
|
||||
/** 站位基准 Y */
|
||||
baseY: number = 0;
|
||||
/** 出生序,用于同条件渲染排序稳定 */
|
||||
spawnOrder: number = 0;
|
||||
|
||||
reset() {
|
||||
this.direction = -1;
|
||||
this.targetX = 0;
|
||||
this.moving = true;
|
||||
this.baseY = 0;
|
||||
this.spawnOrder = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ecs.register('MonMoveSystem')
|
||||
export class MonMoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate {
|
||||
/** 近战判定射程 */
|
||||
private readonly meleeAttackRange = 250;
|
||||
/** 远程判定射程 */
|
||||
private readonly longAttackRange = 600;
|
||||
/** 渲染层级重排节流,避免每帧排序 */
|
||||
private readonly renderSortInterval = 0.05;
|
||||
private lastRenderSortAt = 0;
|
||||
private monMoveMatcher: ecs.IMatcher | null = null;
|
||||
private heroViewMatcher: ecs.IMatcher | null = null;
|
||||
private readonly renderEntries: { node: Node; bossPriority: number; frontScore: number; spawnOrder: number; eid: number }[] = [];
|
||||
private renderEntryCount = 0;
|
||||
|
||||
private getMonMoveMatcher(): ecs.IMatcher {
|
||||
if (!this.monMoveMatcher) {
|
||||
this.monMoveMatcher = ecs.allOf(HeroAttrsComp, HeroViewComp, MonMoveComp);
|
||||
}
|
||||
return this.monMoveMatcher;
|
||||
}
|
||||
|
||||
private getHeroViewMatcher(): ecs.IMatcher {
|
||||
if (!this.heroViewMatcher) {
|
||||
this.heroViewMatcher = ecs.allOf(HeroAttrsComp, HeroViewComp);
|
||||
}
|
||||
return this.heroViewMatcher;
|
||||
}
|
||||
|
||||
filter(): ecs.IMatcher {
|
||||
return ecs.allOf(MonMoveComp, HeroViewComp, HeroAttrsComp);
|
||||
}
|
||||
|
||||
update(e: ecs.Entity) {
|
||||
/** 战斗未开始/暂停时不驱动移动 */
|
||||
if (!smc.mission.play || smc.mission.pause) return;
|
||||
|
||||
const model = e.get(HeroAttrsComp);
|
||||
const move = e.get(MonMoveComp);
|
||||
const view = e.get(HeroViewComp);
|
||||
if (!model || !move || !view || !view.node) return;
|
||||
if (model.fac !== FacSet.MON) return;
|
||||
if (!move.moving) return;
|
||||
|
||||
/** 关卡阶段性冻结怪物行为 */
|
||||
if (smc.mission.stop_mon_action) {
|
||||
this.clearCombatTarget(model);
|
||||
view.status_change("idle");
|
||||
return;
|
||||
}
|
||||
|
||||
if (model.is_stop || model.is_dead || model.is_reviving || model.isFrost()) {
|
||||
this.clearCombatTarget(model);
|
||||
if (!model.is_reviving) view.status_change("idle");
|
||||
return;
|
||||
}
|
||||
|
||||
/** 所有移动都锁定在 baseY,避免出现“漂移” */
|
||||
if (view.node.position.y !== move.baseY) {
|
||||
view.node.setPosition(view.node.position.x, move.baseY, 0);
|
||||
}
|
||||
// 渲染层级统交由 MoveSystem 统一处理,避免两个 System 争抢 setSiblingIndex
|
||||
|
||||
// 仅在战斗中才处理索敌和移动
|
||||
if (!smc.mission.in_fight) return;
|
||||
|
||||
const nearestEnemy = this.findNearestEnemy(e);
|
||||
if (nearestEnemy) {
|
||||
/** 有敌人:进入战斗位移逻辑 */
|
||||
this.processCombatLogic(e, move, view, model, nearestEnemy);
|
||||
this.syncCombatTarget(model, view, nearestEnemy);
|
||||
} else {
|
||||
/** 无敌人:继续向左推进 */
|
||||
this.clearCombatTarget(model);
|
||||
model.is_atking = false;
|
||||
this.moveEntity(view, -1, model.speed / 3);
|
||||
}
|
||||
}
|
||||
|
||||
private clearCombatTarget(model: HeroAttrsComp): void {
|
||||
model.combat_target_eid = -1;
|
||||
model.enemy_in_cast_range = false;
|
||||
}
|
||||
|
||||
private syncCombatTarget(model: HeroAttrsComp, selfView: HeroViewComp, enemyView: HeroViewComp): void {
|
||||
if (!enemyView || !enemyView.node || !enemyView.ent) {
|
||||
this.clearCombatTarget(model);
|
||||
return;
|
||||
}
|
||||
const enemyAttrs = enemyView.ent.get(HeroAttrsComp);
|
||||
if (!enemyAttrs || enemyAttrs.is_dead || enemyAttrs.is_reviving || enemyAttrs.fac === model.fac) {
|
||||
this.clearCombatTarget(model);
|
||||
return;
|
||||
}
|
||||
model.combat_target_eid = enemyView.ent.eid;
|
||||
model.enemy_in_cast_range = this.isEnemyInAttackRange(model, selfView.node.position.x, enemyView.node.position.x);
|
||||
}
|
||||
|
||||
private isEnemyInAttackRange(model: HeroAttrsComp, selfX: number, enemyX: number): boolean {
|
||||
const dist = Math.abs(selfX - enemyX);
|
||||
const rangeType = model.type as HType.Melee | HType.Mid | HType.Long;
|
||||
if (rangeType === HType.Melee) return dist <= this.meleeAttackRange;
|
||||
return dist <= this.longAttackRange;
|
||||
}
|
||||
|
||||
private processCombatLogic(e: ecs.Entity, move: MonMoveComp, view: HeroViewComp, model: HeroAttrsComp, enemy: HeroViewComp) {
|
||||
const selfX = view.node.position.x;
|
||||
const enemyX = enemy.node.position.x;
|
||||
const dist = Math.abs(selfX - enemyX);
|
||||
const inRange = this.isEnemyInAttackRange(model, selfX, enemyX);
|
||||
|
||||
// 接触判定距离,只有接触英雄才停止移动
|
||||
// 飞行怪忽略接触碰撞,不被阻挡
|
||||
const isFly = move.baseY > 50;
|
||||
const touchDistance = 50;
|
||||
const isTouching = !isFly && (dist <= touchDistance);
|
||||
|
||||
// 攻击判定
|
||||
if (inRange) {
|
||||
model.is_atking = true;
|
||||
} else {
|
||||
model.is_atking = false;
|
||||
}
|
||||
|
||||
// 移动判定:只有接触了英雄才停止移动,否则继续向英雄方向移动
|
||||
if (isTouching) {
|
||||
view.status_change("idle");
|
||||
} else {
|
||||
const dir = enemyX > selfX ? 1 : -1;
|
||||
this.moveEntity(view, dir, model.speed / 3);
|
||||
}
|
||||
}
|
||||
|
||||
private moveEntity(view: HeroViewComp, direction: number, speed: number) {
|
||||
const model = view.ent.get(HeroAttrsComp);
|
||||
const move = view.ent.get(MonMoveComp);
|
||||
if (!model || !move) return;
|
||||
|
||||
// 简化的边界限制(怪物主要往左走,英雄防线在左侧,-999999 代表左侧尽头)
|
||||
const moveMinX = -999999;
|
||||
const moveMaxX = 999999;
|
||||
|
||||
const currentX = view.node.position.x;
|
||||
const delta = speed * this.dt * direction;
|
||||
let newX = currentX + delta;
|
||||
|
||||
newX = Math.max(moveMinX, Math.min(moveMaxX, newX));
|
||||
if (Math.abs(newX - currentX) < 0.01) {
|
||||
view.status_change("idle");
|
||||
return;
|
||||
}
|
||||
|
||||
view.node.setPosition(newX, move.baseY, 0);
|
||||
move.direction = direction;
|
||||
|
||||
// 确保怪物的朝向表现,向左走 scale=-1,向右走 scale=1
|
||||
if (direction < 0) {
|
||||
view.scale = -1;
|
||||
} else if (direction > 0) {
|
||||
view.scale = 1;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
/** 一次遍历筛出最近敌人(仅比较 x 轴距离,忽略 Y,飞行和地面都能互相攻击) */
|
||||
ecs.query(this.getHeroViewMatcher()).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;
|
||||
}
|
||||
}
|
||||
9
assets/script/game/hero/MonMoveComp.ts.meta
Normal file
9
assets/script/game/hero/MonMoveComp.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "a3113d69-645e-4a4b-bf68-a434fcbd6d8d",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { smc } from "../common/SingletonModuleComp";
|
||||
import { FacSet } from "../common/config/GameSet";
|
||||
import { HeroDisVal, HType } from "../common/config/heroSet";
|
||||
import { BoxCollider2D, Node } from "cc";
|
||||
import { MonMoveComp } from "./MonMoveComp";
|
||||
|
||||
@ecs.register('MoveComp')
|
||||
export class MoveComp extends ecs.Comp {
|
||||
@@ -47,8 +48,10 @@ interface MoveFacConfig {
|
||||
|
||||
@ecs.register('MoveSystem')
|
||||
export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate {
|
||||
/** 近战判定射程(来自 heroSet) */
|
||||
private readonly meleeAttackRange = HeroDisVal[HType.Melee];
|
||||
/** 近战判定射程 */
|
||||
private readonly meleeAttackRange = 250;
|
||||
/** 远程判定射程 */
|
||||
private readonly longAttackRange = 600;
|
||||
private readonly heroFrontAnchorX = -100;
|
||||
private readonly monFrontAnchorX = 0;
|
||||
/** 常规同阵营横向最小间距(英雄) */
|
||||
@@ -106,21 +109,13 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
||||
/** 战斗未开始/暂停时不驱动移动 */
|
||||
if (!smc.mission.play || smc.mission.pause) return;
|
||||
|
||||
/** 在战斗阶段,英雄和怪物不移动 */
|
||||
if (smc.mission.in_fight) 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 (model.fac !== FacSet.HERO) return; // 只处理英雄移动
|
||||
if (!move.moving) return;
|
||||
/** 关卡阶段性冻结怪物行为 */
|
||||
if (model.fac === FacSet.MON && smc.mission.stop_mon_action) {
|
||||
this.clearCombatTarget(model);
|
||||
view.status_change("idle");
|
||||
return;
|
||||
}
|
||||
|
||||
if (model.is_stop || model.is_dead || model.is_reviving || model.isFrost()) {
|
||||
this.clearCombatTarget(model);
|
||||
if (!model.is_reviving) view.status_change("idle");
|
||||
@@ -131,7 +126,10 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
||||
if (view.node.position.y !== move.baseY) {
|
||||
view.node.setPosition(view.node.position.x, move.baseY, 0);
|
||||
}
|
||||
this.updateRenderOrder(view);
|
||||
|
||||
// 渲染层级重排放在独立的系统或这里统筹,这里我们让它处理所有英雄和怪物的排序
|
||||
this.updateRenderOrder();
|
||||
|
||||
const nearestEnemy = this.findNearestEnemy(e);
|
||||
if (nearestEnemy) {
|
||||
/** 有敌人:进入战斗位移逻辑 */
|
||||
@@ -169,12 +167,7 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
||||
const dist = Math.abs(selfX - enemyX);
|
||||
const rangeType = model.type as HType.Melee | HType.Mid | HType.Long;
|
||||
if (rangeType === HType.Melee) return dist <= this.meleeAttackRange;
|
||||
if (rangeType === HType.Long) {
|
||||
const [, maxRange] = this.resolveCombatRange(model, 360, 720);
|
||||
return dist <= maxRange;
|
||||
}
|
||||
const [, maxRange] = this.resolveCombatRange(model, 120, 720);
|
||||
return dist <= maxRange;
|
||||
return dist <= this.longAttackRange;
|
||||
}
|
||||
|
||||
private processCombatLogic(e: ecs.Entity, move: MoveComp, view: HeroViewComp, model: HeroAttrsComp, enemy: HeroViewComp) {
|
||||
@@ -366,30 +359,42 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
||||
return nearest;
|
||||
}
|
||||
|
||||
private updateRenderOrder(view: HeroViewComp) {
|
||||
private updateRenderOrder() {
|
||||
const scene = smc.map?.MapView?.scene;
|
||||
const actorRoot = scene?.entityLayer?.node?.getChildByName("HERO");
|
||||
if (!actorRoot) return;
|
||||
|
||||
if (view.node.parent !== actorRoot) {
|
||||
view.node.parent = actorRoot;
|
||||
}
|
||||
|
||||
const now = Date.now() / 1000;
|
||||
if (now - this.lastRenderSortAt < this.renderSortInterval) return;
|
||||
this.lastRenderSortAt = now;
|
||||
|
||||
this.renderEntryCount = 0;
|
||||
ecs.query(this.getHeroMoveMatcher()).forEach(e => {
|
||||
ecs.query(this.getHeroViewMatcher()).forEach(e => {
|
||||
const attrs = e.get(HeroAttrsComp);
|
||||
const actorView = e.get(HeroViewComp);
|
||||
const actorMove = e.get(MoveComp);
|
||||
if (!attrs || !actorView?.node || !actorMove || attrs.is_dead) return;
|
||||
if (!attrs || !actorView?.node || attrs.is_dead) return;
|
||||
if (attrs.fac !== FacSet.HERO && attrs.fac !== FacSet.MON) return;
|
||||
|
||||
if (actorView.node.parent !== actorRoot) {
|
||||
actorView.node.parent = actorRoot;
|
||||
}
|
||||
const frontScore = attrs.fac === FacSet.HERO ? actorView.node.position.x : -actorView.node.position.x;
|
||||
|
||||
// 获取 spawnOrder,由于可能挂载 MoveComp 或 MonMoveComp,我们需要动态获取
|
||||
let spawnOrder = 0;
|
||||
const heroMove = e.get(MoveComp);
|
||||
if (heroMove) {
|
||||
spawnOrder = heroMove.spawnOrder;
|
||||
} else {
|
||||
const monMove = e.get(MonMoveComp);
|
||||
if (monMove) {
|
||||
spawnOrder = monMove.spawnOrder;
|
||||
}
|
||||
}
|
||||
|
||||
const isFly = actorView.node.position.y > 50;
|
||||
// 飞行怪在最前,其次是地面的 X 坐标
|
||||
const frontScore = isFly ? 999999 - actorView.node.position.x : (attrs.fac === FacSet.HERO ? actorView.node.position.x : -actorView.node.position.x);
|
||||
|
||||
const entryIndex = this.renderEntryCount;
|
||||
let entry = this.renderEntries[entryIndex];
|
||||
if (!entry) {
|
||||
@@ -399,7 +404,7 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
|
||||
entry.node = actorView.node;
|
||||
entry.bossPriority = attrs.is_boss ? 1 : 0;
|
||||
entry.frontScore = frontScore;
|
||||
entry.spawnOrder = actorMove.spawnOrder;
|
||||
entry.spawnOrder = spawnOrder;
|
||||
entry.eid = e.eid;
|
||||
this.renderEntryCount += 1;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user