From 20e9b1d484da34f452a7d881ec05a26f15eba914 Mon Sep 17 00:00:00 2001 From: panw Date: Tue, 12 May 2026 16:32:25 +0800 Subject: [PATCH] =?UTF-8?q?refactor(monster&hero):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E4=B8=89=E8=B7=AF=E5=88=86=E5=B1=82=E9=80=BB=E8=BE=91=E4=B8=8E?= =?UTF-8?q?=E6=B8=B2=E6=9F=93=E6=8E=92=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 移除飞行怪特殊判定,统一按Y轴高度处理三路渲染 2. 重命名飞行层相关变量为更准确的路次命名 3. 新增英雄自动分路均衡分配逻辑 4. 调整渲染排序规则,按Y轴高度决定上下层显示顺序 5. 修复怪物入场动画与刷怪分路逻辑 --- assets/script/game/hero/Mon.ts | 13 +-- assets/script/game/hero/MonMoveComp.ts | 4 +- assets/script/game/hero/MoveComp.ts | 17 +-- assets/script/game/map/MissionHeroComp.ts | 123 ++++++++++++++++++---- assets/script/game/map/MissionMonComp.ts | 42 +++++--- 5 files changed, 147 insertions(+), 52 deletions(-) diff --git a/assets/script/game/hero/Mon.ts b/assets/script/game/hero/Mon.ts index 0451cd62..f0cf5138 100644 --- a/assets/script/game/hero/Mon.ts +++ b/assets/script/game/hero/Mon.ts @@ -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; // 落地后启用怪物碰撞分组 diff --git a/assets/script/game/hero/MonMoveComp.ts b/assets/script/game/hero/MonMoveComp.ts index 4f5dcf20..8a36334b 100644 --- a/assets/script/game/hero/MonMoveComp.ts +++ b/assets/script/game/hero/MonMoveComp.ts @@ -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) { diff --git a/assets/script/game/hero/MoveComp.ts b/assets/script/game/hero/MoveComp.ts index b5a9615c..40085bf9 100644 --- a/assets/script/game/hero/MoveComp.ts +++ b/assets/script/game/hero/MoveComp.ts @@ -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; diff --git a/assets/script/game/map/MissionHeroComp.ts b/assets/script/game/map/MissionHeroComp.ts index 453f8a9e..a5785605 100644 --- a/assets/script/game/map/MissionHeroComp.ts +++ b/assets/script/game/map/MissionHeroComp.ts @@ -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); 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); + 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); } /** diff --git a/assets/script/game/map/MissionMonComp.ts b/assets/script/game/map/MissionMonComp.ts index 0b40e969..1ba10c4e 100644 --- a/assets/script/game/map/MissionMonComp.ts +++ b/assets/script/game/map/MissionMonComp.ts @@ -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;