From 1fa2be19f7f607a494b935d17c915c5d19b86148 Mon Sep 17 00:00:00 2001 From: panw Date: Tue, 24 Mar 2026 14:40:04 +0800 Subject: [PATCH] =?UTF-8?q?refactor(hero):=20=E4=BC=98=E5=8C=96=E8=8B=B1?= =?UTF-8?q?=E9=9B=84=E4=B8=8E=E6=80=AA=E7=89=A9=E5=AE=9E=E4=BD=93=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD=E4=B8=8E=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 统一英雄与怪物实体加载流程,增强代码可读性与维护性 - 为 Hero.ts 与 Mon.ts 添加详细方法级注释,说明组件职责与关键逻辑 - 在配置文件中将攻击速度(as)注释更新为技能冷却(skills[0].cd),使配置项含义更清晰 - 修复怪物技能等级未随怪物等级提升的问题,使其与英雄逻辑保持一致 - 优化对象池管理,增加容量统计与调试信息 --- assets/script/game/common/config/heroSet.ts | 12 +-- assets/script/game/hero/Hero.ts | 74 ++++++++++++------ assets/script/game/hero/Mon.ts | 83 +++++++++++++++------ 3 files changed, 120 insertions(+), 49 deletions(-) diff --git a/assets/script/game/common/config/heroSet.ts b/assets/script/game/common/config/heroSet.ts index 56fb9f4f..33fce2bb 100644 --- a/assets/script/game/common/config/heroSet.ts +++ b/assets/script/game/common/config/heroSet.ts @@ -86,12 +86,12 @@ export interface HSkillInfo { * skills[0]是普通攻击技能 * skills[1]是等级1时的技能,skills[2]是等级2时的技能,skills[3]是等级3时的技能,最多3级 * - * 属性基准(cards_lv:1,lv:1) : SPEED:120,AP:30 |HP:300|as:1 - * 坦克(cards_lv:1,lv:1) : SPEED:180,AP:25 |HP:450|as:0.75 - * 近战dps(cards_lv:1,lv:1) : SPEED:180,AP:50 |HP:250|as:1.1 - * 远程dps(cards_lv:1,lv:1) : SPEED:120,AP:60 |HP:150|as:1.3 - *远程法dps(cards_lv:1,lv:1) : SPEED:100,AP:60 |HP:150|as:1.4 - * 远程辅助(cards_lv:1,lv:1) : SPEED:100,AP:20 |HP:150|as:1 + * 属性基准(cards_lv:1,lv:1) : SPEED:120,AP:30 | HP:300 | skills[0].cd=1 + * 坦克(cards_lv:1,lv:1) : SPEED:180,AP:25 | HP:450 | skills[0].cd=0.75 + * 近战dps(cards_lv:1,lv:1) : SPEED:180,AP:50 | HP:250 | skills[0].cd=1.1 + * 远程dps(cards_lv:1,lv:1) : SPEED:120,AP:60 | HP:150 | skills[0].cd=1.3 + *远程法dps(cards_lv:1,lv:1) : SPEED:100,AP:60 | HP:150 | skills[0].cd=1.4 + * 远程辅助(cards_lv:1,lv:1) : SPEED:100,AP:20 | HP:150 | skills[0].cd=1 */ export const HeroInfo: Record = { diff --git a/assets/script/game/hero/Hero.ts b/assets/script/game/hero/Hero.ts index 4dbf3023..ca71cf72 100644 --- a/assets/script/game/hero/Hero.ts +++ b/assets/script/game/hero/Hero.ts @@ -10,14 +10,20 @@ import { GameEvent } from "../common/config/GameEvent"; import { Attrs} from "../common/config/HeroAttrs"; import { MoveComp } from "./MoveComp"; import { mLogger } from "../common/Logger"; -/** 角色实体 */ +/** 英雄实体:负责英雄节点创建、属性初始化、入场动画与销毁流程 */ @ecs.register(`Hero`) export class Hero extends ecs.Entity { + /** 英雄数据组件引用 */ HeroModel!: HeroAttrsComp; + /** 英雄表现组件引用 */ View!: HeroViewComp; + /** 英雄移动组件引用 */ HeroMove!: MoveComp; - debugMode: boolean = false; // 是否启用调试模式 + /** 调试开关,开启后输出实体层级等调试信息 */ + debugMode: boolean = false; + + /** 注册实体必需组件:移动 + 属性 */ protected init() { this.addComponents( MoveComp, @@ -25,88 +31,111 @@ export class Hero extends ecs.Entity { ); } + /** 销毁实体并释放视图节点,防止残留碰撞体与显示对象 */ destroy(): void { - // 销毁节点,防止视觉残留 + // 优先销毁节点,避免实体销毁后场景仍残留可见对象 const view = this.get(HeroViewComp); if (view && view.node && view.node.isValid) { view.node.destroy(); } + // 手动移除组件,确保 ecs 侧引用及时释放 this.remove(HeroViewComp); this.remove(HeroAttrsComp); super.destroy(); } - /** 加载角色 */ + /** + * 加载并初始化英雄 + * 1) 创建节点并挂到 HERO 层 + * 2) 初始化表现与属性数据 + * 3) 播放下落入场并在落地后启用碰撞与移动 + */ load(pos: Vec3 = Vec3.ZERO,scale:number = 1,uuid:number=1001, dropToY:number = pos.y,hero_lv:number=1) { + // 英雄始终朝右,表现缩放固定为正向 scale = 1 + // 英雄等级在当前规则下上限为 3,避免超配表范围 if(hero_lv>3) hero_lv=3 - // 查找空闲英雄槽位 + // 英雄尺寸随等级做轻量放大,强化成长反馈 let size=1+0.1*hero_lv + // 根据配置路径加载英雄预制体 var path = "game/heros/"+HeroInfo[uuid].path; var prefab: Prefab = oops.res.get(path, Prefab)!; var node = instantiate(prefab); var scene = smc.map.MapView.scene; + // 统一挂到实体显示层 HERO 节点下 node.parent = scene.entityLayer!.node!.getChildByName("HERO")!; const collider = node.getComponent(BoxCollider2D); - if (collider) collider.enabled = false; // 先禁用 + // 入场过程暂不参与碰撞,防止半空触发战斗逻辑 + if (collider) collider.enabled = false; node.setScale(size*node.scale.x,size*node.scale.y); node.setPosition(pos) - // 🔥 设置初始 SiblingIndex - 英雄基础层级 + 位置偏移 - + // 输出节点层级信息,便于排查遮挡与渲染顺序问题 mLogger.log(this.debugMode,"hero",node.getSiblingIndex()); var hv = node.getComponent(HeroViewComp)!; const model = this.get(HeroAttrsComp); - let hero = HeroInfo[uuid]; // 共用英雄数据 + // 从配置中读取英雄静态数据 + let hero = HeroInfo[uuid]; - // 设置 View 层属性(表现相关) + // 视图层参数:朝向与碰撞阵营 hv.scale = 1; hv.box_group = BoxSet.HERO; - // 设置 Model 层属性(数据相关) + // 模型层参数:身份、阵营、等级、职业 model.hero_uuid = uuid; model.hero_name = hero.name; model.lv = hero_lv; model.type = hero.type; model.fac = FacSet.HERO; - // 只有主角才挂载天赋组件 - - // ✅ 初始化技能数据(迁移到 HeroSkillsComp) - // 设置基础属性 + // 基础属性按等级倍率初始化 model.ap = hero.ap*model.lv; model.hp= model.hp_max = hero.hp*model.lv; model.speed = hero.speed; + + // 构建技能表并注入运行时冷却字段 ccd model.skills = {}; for (const key in hero.skills) { const skill = hero.skills[key]; if (!skill) continue; - //用于增量 计算最终技能等级:英雄等级与技能初始等级均从1开始,需各减1抵消,故-2,最低等级时0 + // 最终技能等级 = 初始技能等级 + 英雄等级增量,且下限为 0 model.skills[skill.uuid] = { ...skill, lv: Math.max(0,skill.lv + hero_lv - 2), ccd: 0 }; } + // 缓存技能射程等派生数据,减少战斗帧内重复计算 model.updateSkillDistanceCache(); - // 初始化 buff/debuff 系统 + // 初始化属性系统(buff/debuff 等动态属性容器) model.initAttrs(); + + // 将视图组件注册到实体,打通逻辑与表现 this.add(hv); + // 广播主角召唤事件,触发外部系统监听逻辑 oops.message.dispatchEvent(GameEvent.MasterCalled,{uuid:uuid}) + + // 初始化移动组件:方向、目标 X、站位基准 Y const move = this.get(MoveComp); - move.direction = 1; // 向右移动 + move.direction = 1; move.targetX = resolveFormationTargetX(model.fac, model.type); move.baseY = dropToY; move.moving = false; + + // 依据下落距离自适应入场时长,保证手感稳定 const dropDistance = Math.abs(pos.y - dropToY); const dropDuration = Math.max(0.18, Math.min(0.38, dropDistance / 1200)); + + // 停止旧动画后执行下落 tween,避免复用节点时动画叠加 Tween.stopAllByTarget(node); tween(node) .to(dropDuration, { position: v3(pos.x, dropToY, 0) }) .call(() => { if (!node || !node.isValid) return; + // 落地后锁定最终位置,切换到落地完成状态 node.setPosition(pos.x, dropToY, 0); hv.playEnd("down"); move.moving = true; + // 落地后再启用碰撞,避免空中阶段触发伤害结算 if (collider) { collider.enabled = true; collider.group = BoxSet.HERO; @@ -114,26 +143,29 @@ export class Hero extends ecs.Entity { } }) .start(); + // 维护关卡内英雄数量统计 smc.vmdata.mission_data.hero_num++ } + /** 重置入口:复用 destroy 的释放流程 */ reset() { - // 注: 自定义释放逻辑,视图层实现 ecs.IComp 接口的 ecs 组件需要手动释放 super.destroy(); } } +/** 英雄生命周期系统:监听实体进入与移除并输出调试日志 */ @ecs.register('HeroLifecycleSystem') export class HeroLifecycleSystem extends ecs.ComblockSystem implements ecs.IEntityEnterSystem, ecs.IEntityRemoveSystem { + /** 仅处理拥有 MoveComp 的实体 */ filter() { return ecs.allOf(MoveComp); } + /** 实体进入世界时记录日志 */ entityEnter(e: ecs.Entity): void { - // 英雄实体创建时的特殊处理 const heroAttrs = e.get(HeroAttrsComp); if (heroAttrs) { mLogger.log(heroAttrs.debugMode, 'HeroLifecycle', `英雄进入世界: ${heroAttrs.hero_name}`); @@ -142,8 +174,8 @@ export class HeroLifecycleSystem extends ecs.ComblockSystem } } + /** 实体离开世界时记录日志 */ entityRemove(e: ecs.Entity): void { - // 英雄实体销毁时的清理工作 const heroAttrs = e.get(HeroAttrsComp); if (heroAttrs) { mLogger.log(heroAttrs.debugMode, 'HeroLifecycle', `英雄离开世界: ${heroAttrs.hero_name}`); diff --git a/assets/script/game/hero/Mon.ts b/assets/script/game/hero/Mon.ts index f9fb74a1..e14c8b7d 100644 --- a/assets/script/game/hero/Mon.ts +++ b/assets/script/game/hero/Mon.ts @@ -8,18 +8,26 @@ import { HeroAttrsComp } from "./HeroAttrsComp"; import { HeroViewComp } from "./HeroViewComp"; import { MoveComp } from "./MoveComp"; import { mLogger } from "../common/Logger"; -/** 角色实体 */ +/** 怪物实体:负责怪物对象池复用、属性初始化、入场动画与回收 */ @ecs.register(`Monster`) export class Monster extends ecs.Entity { + /** 怪物数据组件引用 */ HeroModel!: HeroAttrsComp; + /** 怪物表现组件引用 */ HeroView!: HeroViewComp; + /** 怪物移动组件引用 */ MonMove!: MoveComp; - private debugMode: boolean = false; // 是否启用调试模式 + /** 调试开关,控制生命周期日志输出 */ + private debugMode: boolean = false; - // 多键对象池:Map + /** 多键对象池:key 为 prefab 路径,value 为对应节点池 */ static pools: Map = new Map(); + /** 单个路径的池容量上限 */ static readonly MAX_POOL_SIZE: number = 12; + /** 所有路径合计池容量上限 */ static readonly MAX_POOL_TOTAL: number = 60; + + /** 计算当前所有对象池节点总量 */ private static totalPoolSize(): number { let total = 0; this.pools.forEach((pool) => { @@ -28,6 +36,7 @@ export class Monster extends ecs.Entity { return total; } + /** 从指定路径对象池取可用节点,取不到返回 null */ static getFromPool(path: string): Node | null { if (this.pools.has(path)) { const pool = this.pools.get(path)!; @@ -41,6 +50,7 @@ export class Monster extends ecs.Entity { return null; } + /** 节点回收到对象池,超上限则直接销毁 */ static putToPool(path: string, node: Node) { if (!node || !node.isValid) return; if (!this.pools.has(path)) { @@ -54,6 +64,7 @@ export class Monster extends ecs.Entity { pool.put(node); } + /** 清空所有对象池并销毁池内节点 */ static clearPools() { this.pools.forEach((pool) => { while (pool.size() > 0) { @@ -67,6 +78,7 @@ export class Monster extends ecs.Entity { this.pools.clear(); } + /** 获取对象池统计信息,用于调试与容量监控 */ static getPoolStats() { let total = 0; this.pools.forEach((pool) => { @@ -80,6 +92,7 @@ export class Monster extends ecs.Entity { }; } + /** 注册实体必需组件:移动 + 属性 */ protected init() { this.addComponents( MoveComp, @@ -87,8 +100,9 @@ export class Monster extends ecs.Entity { ); } + /** 销毁实体:优先回收节点,然后释放组件 */ destroy(): void { - // 回收节点到对象池 + // 按英雄路径回收到对象池,提升高频刷怪性能 const model = this.get(HeroAttrsComp); const view = this.get(HeroViewComp); if (model && view && view.node && view.node.isValid) { @@ -96,28 +110,39 @@ export class Monster extends ecs.Entity { Monster.putToPool(path, view.node); } + // 手动移除组件,避免 ecs 引用滞留 this.remove(HeroViewComp); this.remove(HeroAttrsComp); super.destroy(); } - /** 加载角色 */ - load(pos: Vec3 = Vec3.ZERO,scale:number = 1,uuid:number=1001, is_boss:boolean=false, dropToY:number = pos.y) { + /** + * 加载并初始化怪物 + * 1) 优先对象池复用节点,减少实例化开销 + * 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) { + // 怪物默认朝左,表现缩放固定为负向 scale=-1 + // 当前怪物尺寸固定,保留变量便于后续扩展 let size=1 var scene = smc.map.MapView.scene; + // 根据配置读取怪物预制体路径 var path = "game/heros/"+HeroInfo[uuid].path; - // 尝试从池中获取 + // 优先从对象池取节点,未命中时再实例化 let node = Monster.getFromPool(path); if (!node) { var prefab: Prefab = oops.res.get(path, Prefab)!; node = instantiate(prefab); } + // 统一挂到实体显示层 HERO 节点下 node.parent = scene.entityLayer!.node!.getChildByName("HERO")!; var view = node.getComponent(HeroViewComp)!; const collider = node.getComponent(BoxCollider2D); + // 入场期间关闭碰撞,防止下落时提前参与战斗 if (collider) { collider.enabled = false; } @@ -125,54 +150,64 @@ export class Monster extends ecs.Entity { node.setScale(size*node.scale.x,size*node.scale.y); node.setPosition(pos) const model = this.get(HeroAttrsComp); - let hero = HeroInfo[uuid]; // 共用英雄数据 - // 设置 View 层属性(表现相关) + // 从配置表获取怪物静态数据 + let hero = HeroInfo[uuid]; + // 视图层参数:朝向与碰撞阵营 view.scale = scale; view.box_group = BoxSet.MONSTER; - // 设置 Model 层属性 基础属性 + // 模型层参数:身份、阵营、基础数值 model.hero_uuid = uuid; model.hero_name = hero.name; model.hp = model.hp_max = hero.hp; model.ap = hero.ap; - model.speed = hero.speed; // 使用成长后的速度 + model.speed = hero.speed; model.type = hero.type; model.fac = FacSet.MON; + // 标记是否 Boss,非 Boss 默认记作杂兵 model.is_boss =is_boss if(!model.is_boss){ model.is_kalami = true; } + + // 构建技能表并注入运行时冷却字段 ccd model.skills = {}; for (const key in hero.skills) { const skill = hero.skills[key]; if (!skill) continue; - model.skills[skill.uuid] = { ...skill, ccd: 0 }; + // 最终技能等级 = 初始技能等级 + 怪物等级增量,且下限为 0 + model.skills[skill.uuid] = { ...skill, lv: Math.max(0,skill.lv + mon_lv - 2), ccd: 0 }; } + // 缓存技能射程等派生数据,减少战斗帧内重复计算 model.updateSkillDistanceCache(); - //根据刷怪控制脚本对ap和hp进行加强 - - + // 注册视图组件并重置对象池复用状态 this.add(view); - // 重置视图状态(对象池复用时必须) view.init(); + // 广播怪物加载事件,供刷怪与战斗系统联动 oops.message.dispatchEvent("monster_load",this) - // 初始化移动参数,包括线路和生成顺序 + // 初始化移动参数:方向、目标 X、站位基准 Y const move = this.get(MoveComp); move.reset(); - move.direction = -1; // 向左移动 + move.direction = -1; move.targetX = Math.max(-320, Math.min(320, pos.x)); move.baseY = dropToY; move.moving = false; + + // 依据下落距离自适应入场时长,确保观感一致 const dropDistance = Math.abs(pos.y - dropToY); const dropDuration = Math.max(0.18, Math.min(0.38, dropDistance / 1200)); + + // 停止旧动画后执行下落 tween,避免复用节点时动画叠加 Tween.stopAllByTarget(node); tween(node) .to(dropDuration, { position: v3(pos.x, dropToY, 0) }) .call(() => { if (!node || !node.isValid) return; + // 落地后锁定最终位置,切换到落地完成状态 node.setPosition(pos.x, dropToY, 0); view.playEnd("down"); move.moving = true; + // 落地后启用怪物碰撞分组 if (collider) { collider.enabled = true; collider.group = BoxSet.MONSTER; @@ -180,27 +215,31 @@ export class Monster extends ecs.Entity { } }) .start(); + // 维护关卡内怪物数量统计 smc.vmdata.mission_data.mon_num++ } + /** 重置入口:复用 destroy 的释放流程 */ reset() { - // 注: 自定义释放逻辑,视图层实现 ecs.IComp 接口的 ecs 组件需要手动释放 super.destroy(); } } +/** 怪物生命周期系统:监听实体进入与移除并输出调试日志 */ @ecs.register('MonLifecycleSystem') export class MonLifecycleSystem extends ecs.ComblockSystem implements ecs.IEntityEnterSystem, ecs.IEntityRemoveSystem { - debugMode: boolean = false; // 是否启用调试模式 + /** 调试开关,控制系统日志输出 */ + debugMode: boolean = false; + /** 仅处理拥有 MoveComp 的实体 */ filter() { return ecs.allOf(MoveComp); } + /** 实体进入世界时记录日志 */ entityEnter(e: ecs.Entity): void { - // 怪物实体创建时的特殊处理 const heroAttrs = e.get(HeroAttrsComp); if (heroAttrs) { mLogger.log(this.debugMode, 'MonLifecycleSystem', `怪物进入世界: ${heroAttrs.hero_name}`); @@ -209,8 +248,8 @@ export class MonLifecycleSystem extends ecs.ComblockSystem } } + /** 实体离开世界时记录日志 */ entityRemove(e: ecs.Entity): void { - // 怪物实体销毁时的清理工作 const heroAttrs = e.get(HeroAttrsComp); if (heroAttrs) { mLogger.log(this.debugMode, 'MonLifecycleSystem', `怪物离开世界: ${heroAttrs.hero_name}`);