refactor(monster&hero): 重构三路分层逻辑与渲染排序

1. 移除飞行怪特殊判定,统一按Y轴高度处理三路渲染
2. 重命名飞行层相关变量为更准确的路次命名
3. 新增英雄自动分路均衡分配逻辑
4. 调整渲染排序规则,按Y轴高度决定上下层显示顺序
5. 修复怪物入场动画与刷怪分路逻辑
This commit is contained in:
panw
2026-05-12 16:32:25 +08:00
parent 86363f50b0
commit 20e9b1d484
5 changed files with 147 additions and 52 deletions

View File

@@ -123,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, flyLaneIndex:number = 0) {
load(pos: Vec3 = Vec3.ZERO,scale:number = 1,uuid:number=1001, is_boss:boolean=false, dropToY:number = pos.y,mon_lv:number=1, laneIndex:number = 0) {
// 怪物默认朝左,表现缩放固定为负向
scale=-1
// 当前怪物尺寸固定,保留变量便于后续扩展
@@ -220,18 +220,7 @@ 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;
// 落地后启用怪物碰撞分组

View File

@@ -139,10 +139,8 @@ export class MonMoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpda
const inRange = this.isEnemyInAttackRange(model, selfX, enemyX);
// 接触判定距离,只有接触英雄才停止移动
// 飞行怪忽略接触碰撞,不被阻挡
const isFly = move.baseY > 50;
const touchDistance = 50;
const isTouching = !isFly && (dist <= touchDistance);
const isTouching = dist <= touchDistance;
// 攻击判定
if (inRange) {

View File

@@ -65,7 +65,7 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
private lastRenderSortAt = 0;
private heroMoveMatcher: ecs.IMatcher | null = null;
private heroViewMatcher: ecs.IMatcher | null = null;
private readonly renderEntries: { node: Node; bossPriority: number; frontScore: number; spawnOrder: number; eid: number }[] = [];
private readonly renderEntries: { node: Node; bossPriority: number; frontScore: number; spawnOrder: number; eid: number; laneScore: number }[] = [];
private renderEntryCount = 0;
/**
@@ -391,18 +391,21 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
}
}
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);
// 按 Y 轴计算渲染层级权重Y 越小,说明越靠近屏幕下方,应该渲染在越前面)
// 之前的 isFly 逻辑已被移除,统一按 Y 轴处理三路渲染
const laneScore = -actorView.node.position.y;
// X 轴权重:站在前排的(交战处)优先渲染
const frontScore = attrs.fac === FacSet.HERO ? actorView.node.position.x : -actorView.node.position.x;
const entryIndex = this.renderEntryCount;
let entry = this.renderEntries[entryIndex];
if (!entry) {
entry = { node: actorView.node, bossPriority: 0, frontScore: 0, spawnOrder: 0, eid: 0 };
entry = { node: actorView.node, bossPriority: 0, frontScore: 0, spawnOrder: 0, eid: 0, laneScore: 0 };
this.renderEntries.push(entry);
}
entry.node = actorView.node;
entry.bossPriority = attrs.is_boss ? 1 : 0;
entry.laneScore = laneScore;
entry.frontScore = frontScore;
entry.spawnOrder = spawnOrder;
entry.eid = e.eid;
@@ -410,9 +413,11 @@ export class MoveSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
});
this.renderEntries.length = this.renderEntryCount;
/** 定时重排同层节点Boss 优先级 -> 前后位置 -> 出生序 -> eid */
/** 定时重排同层节点Boss 优先级 -> Y 轴路次 -> 前后位置 -> 出生序 -> eid */
this.renderEntries.sort((a, b) => {
if (a.bossPriority !== b.bossPriority) return a.bossPriority - b.bossPriority;
// Y轴靠下的laneScore 较大)应该在后面,从而渲染在上面
if (Math.abs(a.laneScore - b.laneScore) > 10) return a.laneScore - b.laneScore;
if (a.frontScore !== b.frontScore) return a.frontScore - b.frontScore;
if (a.spawnOrder !== b.spawnOrder) return a.spawnOrder - b.spawnOrder;
return a.eid - b.eid;

View File

@@ -58,6 +58,12 @@ export class MissionHeroCompComp extends CCComp {
private static readonly HERO_SPAWN_START_MELEE_X = -320
/** 远程(含中程)英雄起始出生 X 坐标 */
private static readonly HERO_SPAWN_START_RANGED_X = -320
/** 三路高度偏移(上路, 中路, 下路) */
private static readonly HERO_LANE_Y_OFFSETS = [100, 0, -100]
/** 每路前排容量 */
private static readonly HERO_LANE_CAP = 3
/** 同路内 X 间距 */
private static readonly HERO_GAP_X = 100
// ======================== 运行时属性 ========================
@@ -124,7 +130,8 @@ export class MissionHeroCompComp extends CCComp {
if (model && view) {
if (model.is_dead) {
view.alive();
const landingPos = this.resolveHeroLandingPos(model.hero_uuid);
const { lane, indexInLane } = this.pickLaneForHero(model.hero_uuid, [hero.eid]);
const landingPos = this.resolveHeroLandingPos(model.hero_uuid, lane, indexInLane);
// 不再直接设置位置,而是播放下落入场动画
// 计算出出生点(空中)
const spawnPos: Vec3 = v3(landingPos.x, landingPos.y + MissionHeroCompComp.HERO_DROP_HEIGHT, 0);
@@ -159,6 +166,53 @@ export class MissionHeroCompComp extends CCComp {
// ======================== 英雄生成 ========================
/**
* 动态分配英雄上场的路和排位(优先中路 -> 上路 -> 下路)
* @param uuid 英雄 UUID
* @param excludeEids 排除计算的实体ID数组避免复活或合成时把自己算成占据的位置
*/
private pickLaneForHero(uuid: number, excludeEids: number[] = []): { lane: number; indexInLane: number } {
const heroes = this.getAllHeroes().filter(h => {
const m = h.get(HeroAttrsComp);
return m && !m.is_dead && !excludeEids.includes(h.eid);
});
const counts = [0, 0, 0];
const baseY = HeroPos[0].pos.y;
for (const h of heroes) {
const view = h.get(HeroViewComp);
if (!view || !view.node) continue;
let y = view.node.position.y;
// 处理正在掉落状态的英雄y 值偏高)
if (y > baseY + 150) {
y -= MissionHeroCompComp.HERO_DROP_HEIGHT;
}
let nearest = 1, best = Infinity;
for (let i = 0; i < 3; i++) {
const d = Math.abs(y - (baseY + MissionHeroCompComp.HERO_LANE_Y_OFFSETS[i]));
if (d < best) {
best = d;
nearest = i;
}
}
counts[nearest]++;
}
// 优先中路(1) -> 上路(0) -> 下路(2)
const priority = [1, 0, 2];
for (const lane of priority) {
if (counts[lane] < MissionHeroCompComp.HERO_LANE_CAP) {
return { lane, indexInLane: counts[lane] };
}
}
// 溢出:仍放中路,沿 X 继续排
return { lane: 1, indexInLane: counts[1] };
}
/**
* 生成一个英雄 ECS 实体:
* - 计算出生点(空中)和落点(地面)。
@@ -173,7 +227,8 @@ export class MissionHeroCompComp extends CCComp {
console.log("addHero uuid:",uuid)
let hero = ecs.getEntity<Hero>(Hero);
let scale = 1
const landingPos = this.resolveHeroLandingPos(uuid);
const { lane, indexInLane } = this.pickLaneForHero(uuid);
const landingPos = this.resolveHeroLandingPos(uuid, lane, indexInLane);
let spawnPos:Vec3 = v3(landingPos.x, landingPos.y + MissionHeroCompComp.HERO_DROP_HEIGHT, 0);
hero.load(spawnPos,scale,uuid,landingPos.y,hero_lv,pool_lv);
@@ -193,14 +248,16 @@ export class MissionHeroCompComp extends CCComp {
* 计算英雄落点位置。
* Y 坐标来自 HeroPos 配置X 坐标根据英雄类型(近战/远程)决定。
*
* @param uuid 英雄 UUID
* @returns 落点 Vec3
* @param uuid 英雄 UUID
* @param lane 分配到的路 (0: 上, 1: 中, 2: 下)
* @param indexInLane 该路排位
* @returns 落点 Vec3
*/
private resolveHeroLandingPos(uuid: number): Vec3 {
private resolveHeroLandingPos(uuid: number, lane: number, indexInLane: number): Vec3 {
const hero_pos = 0;
const baseY = HeroPos[hero_pos].pos.y;
const baseY = HeroPos[hero_pos].pos.y + MissionHeroCompComp.HERO_LANE_Y_OFFSETS[lane];
const startX = this.resolveSpawnStartX(uuid);
return v3(startX, baseY, 0);
return v3(startX + indexInLane * MissionHeroCompComp.HERO_GAP_X, baseY, 0);
}
/**
@@ -223,17 +280,42 @@ export class MissionHeroCompComp extends CCComp {
* @param pool_lv 卡池等级
* @param ap 聚合后攻击力
* @param hp_max 聚合后最大生命值
* @param targetLane 指定生成路
* @param targetIndex 指定该路排位
* @returns 实际生成的英雄等级
*/
private addMergedHero(uuid:number, hero_lv:number, pool_lv:number, ap:number, hp_max:number): number {
const hero = this.addHero(uuid, hero_lv, pool_lv);
private addMergedHero(uuid:number, hero_lv:number, pool_lv:number, ap:number, hp_max:number, targetLane?: number, targetIndex?: number): number {
console.log("addMergedHero uuid:",uuid)
let hero = ecs.getEntity<Hero>(Hero);
let scale = 1
// 如果未指定路,则按普通添加英雄处理
let lane = targetLane;
let indexInLane = targetIndex;
if (lane === undefined || indexInLane === undefined) {
const res = this.pickLaneForHero(uuid);
lane = res.lane;
indexInLane = res.indexInLane;
}
const landingPos = this.resolveHeroLandingPos(uuid, lane, indexInLane);
let spawnPos:Vec3 = v3(landingPos.x, landingPos.y + MissionHeroCompComp.HERO_DROP_HEIGHT, 0);
hero.load(spawnPos,scale,uuid,landingPos.y,hero_lv,pool_lv);
// 召唤完成后,派发事件以更新英雄面板
const model = hero.get(HeroAttrsComp);
if (!model) return hero_lv;
model.ap = Math.max(0, ap);
model.hp_max = Math.max(1, hp_max);
model.hp = model.hp_max;
model.dirty_hp = true;
return model.lv;
if (model) {
model.ap = Math.max(0, ap);
model.hp_max = Math.max(1, hp_max);
model.hp = model.hp_max;
model.dirty_hp = true;
oops.message.dispatchEvent(GameEvent.MasterCalled, {
eid: hero.eid,
model: model
});
return model.lv;
}
return hero_lv;
}
// ======================== 英雄查询 ========================
@@ -448,19 +530,24 @@ export class MissionHeroCompComp extends CCComp {
// 聚合属性
let sumAp = 0;
let sumHpMax = 0;
const mergeEids = [];
for (let i = 0; i < mergeHeroes.length; i++) {
const model = mergeHeroes[i].get(HeroAttrsComp);
mergeEids.push(mergeHeroes[i].eid);
if (!model) continue;
sumAp += model.ap;
sumHpMax += model.hp_max;
}
// 计算出生点
const landingPos = this.resolveHeroLandingPos(uuid);
// 计算目标出生点(提前排除素材英雄所占的位置)
const { lane, indexInLane } = this.pickLaneForHero(uuid, mergeEids);
const landingPos = this.resolveHeroLandingPos(uuid, lane, indexInLane);
const spawnPos:Vec3 = v3(landingPos.x, landingPos.y + MissionHeroCompComp.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);
return this.addMergedHero(uuid, Math.min(this.merge_max_lv, hero_lv + 1), pool_lv, sumAp, sumHpMax, lane, indexInLane);
}
/**

View File

@@ -57,8 +57,8 @@ export class MissionMonCompComp extends CCComp {
private static readonly MON_SPAWN_GAP_X = 50;
/** 怪物出生掉落高度 */
private static readonly MON_DROP_HEIGHT = 280;
/** 飞行层高度偏移(地面, 空中1, 空中2 */
private static readonly FLY_LANE_Y_OFFSETS = [0, 120, 240];
/** 三路高度偏移(上路, 中路, 下路 */
private static readonly LANE_Y_OFFSETS = [100, 0, -100];
// ======================== 编辑器属性 ========================
@@ -172,9 +172,22 @@ export class MissionMonCompComp extends CCComp {
// ======================== 插队刷怪 ========================
/** 选取当前排数最少的路(均衡分配) */
private pickBalancedLane(): number {
let min = this.laneIndices[0];
let lane = 0;
for (let i = 1; i < 3; i++) {
if (this.laneIndices[i] < min) {
min = this.laneIndices[i];
lane = i;
}
}
return lane;
}
/**
* 处理插队刷怪队列(每 0.15 秒尝试消费一个):
* 1. 根据飞行层排号
* 1. 根据指定路或均衡分路
* 2. 找到后从队列中移除并生成怪物。
*/
private updateSpecialQueue(dt: number) {
@@ -190,7 +203,7 @@ export class MissionMonCompComp extends CCComp {
(MonList[MonType.FlyBoss] && MonList[MonType.FlyBoss].includes(item.uuid));
const upType = this.getRandomUpType();
const lane = item.flyLane >= 0 && item.flyLane <= 2 ? item.flyLane : 0;
const lane = item.flyLane !== undefined && item.flyLane >= 0 && item.flyLane <= 2 ? item.flyLane : this.pickBalancedLane();
this.addMonsterAt(lane, this.laneIndices[lane], item.uuid, isBoss, upType, Math.max(1, Number(item.level ?? 1)));
this.laneIndices[lane]++;
@@ -305,24 +318,27 @@ export class MissionMonCompComp extends CCComp {
// 解析配置
for (const slot of config) {
const isBoss = slot.type === MonType.MeleeBoss || slot.type === MonType.LongBoss || slot.type === MonType.FlyBoss;
// 判断分配到的飞行层
let lane: number = slot.flyLane !== undefined ? slot.flyLane : 0;
if (slot.type === MonType.Fly || slot.type === MonType.FlyBoss) {
lane = slot.flyLane !== undefined ? slot.flyLane : 1; // 飞行怪默认在第一层
}
lane = Math.max(0, Math.min(2, lane)); // 约束在 0,1,2
for (let i = 0; i < slot.count; i++) {
const uuid = this.getRandomUuidByType(slot.type);
const upType = this.getRandomUpType();
// 优先使用配置的 lane否则均衡分配
let lane = slot.flyLane !== undefined ? slot.flyLane : this.pickBalancedLane();
lane = Math.max(0, Math.min(2, lane));
const req = { uuid, isBoss, upType, monLv: wave, lane };
allMons.push(req);
// 提前累加 laneIndices以便本波内的均衡分配能正确计算
this.laneIndices[lane]++;
}
}
this.waveTargetCount = allMons.length;
this.waveSpawnedCount = 0;
// 由于上面循环中已经累加了 laneIndices这里需要重置以便下面真正生成时再累加或者直接利用 allMons 生成)
this.laneIndices = [0, 0, 0];
// 4. 立即生成本波所有怪物
for (const req of allMons) {
this.addMonsterAt(req.lane, this.laneIndices[req.lane], req.uuid, req.isBoss, req.upType, req.monLv);
@@ -335,8 +351,8 @@ export class MissionMonCompComp extends CCComp {
/**
* 在指定层级、指定索引处生成一个怪物:
*
* @param laneIndex 飞行层索引 (0, 1, 2)
* @param monIndex 该级的第几个怪 (0, 1, 2...)
* @param laneIndex 三路索引 (0, 1, 2)
* @param monIndex 该级的第几个怪 (0, 1, 2...)
* @param uuid 怪物 UUID
* @param isBoss 是否为 Boss
* @param upType 属性成长类型
@@ -355,7 +371,7 @@ export class MissionMonCompComp extends CCComp {
// 计算坐标
const spawnX = MissionMonCompComp.MON_SPAWN_START_X + monIndex * MissionMonCompComp.MON_SPAWN_GAP_X;
const landingY = BoxSet.GAME_LINE + MissionMonCompComp.FLY_LANE_Y_OFFSETS[laneIndex] + (isBoss ? 6 : 0);
const landingY = BoxSet.GAME_LINE + MissionMonCompComp.LANE_Y_OFFSETS[laneIndex] + (isBoss ? 6 : 0);
const spawnPos: Vec3 = v3(spawnX, landingY + MissionMonCompComp.MON_DROP_HEIGHT, 0);
this.globalSpawnOrder = (this.globalSpawnOrder + 1) % 999;