为游戏地图模块的脚本文件添加全面的注释,说明每个组件的职责、关键设计、依赖关系和使用方式。注释覆盖了英雄信息面板、技能卡槽位管理器、排行榜弹窗、卡牌控制器、背景滚动组件等核心功能模块,提高了代码的可读性和维护性。 同时修复了英雄预制体的激活状态和技能效果预制体的尺寸参数。
636 lines
23 KiB
TypeScript
636 lines
23 KiB
TypeScript
/**
|
||
* @file MissionComp.ts
|
||
* @description 任务(关卡)核心控制组件(UI + 逻辑层)
|
||
*
|
||
* 职责:
|
||
* 1. 管理单局游戏的 **完整生命周期**:初始化 → 准备阶段 → 战斗阶段 → 结算。
|
||
* 2. 在战斗阶段每帧更新战斗计时器、同步怪物数量、检测英雄全灭。
|
||
* 3. 管理怪物数量阈值(暂停 / 恢复刷怪的上下限)。
|
||
* 4. 处理新一波事件(NewWave),进入准备阶段并发放金币奖励。
|
||
* 5. 提供战斗结束后的结算弹窗入口(VictoryComp)。
|
||
* 6. (可选)内建性能监控面板,显示内存、帧率、实体数量等开发信息。
|
||
*
|
||
* 关键设计:
|
||
* - mission_start() 初始化所有游戏数据 → 进入准备阶段 → 显示 loading。
|
||
* - 准备阶段(enterPreparePhase):停止刷怪,显示开始按钮。
|
||
* - 战斗阶段(to_fight):开始刷怪,隐藏按钮,由 update 驱动。
|
||
* - 怪物数量管理采用 max/resume 双阈值:
|
||
* * 超过 max → 暂停刷怪(stop_spawn_mon=true)
|
||
* * 降至 resume 以下 → 恢复刷怪
|
||
* - cleanComponents() 在任务开始/结束时销毁所有英雄和技能 ECS 实体。
|
||
* - clearBattlePools() 回收对象池(Monster / Skill / Tooltip)。
|
||
*
|
||
* 依赖:
|
||
* - smc.mission —— 全局任务运行状态(play / pause / in_fight / stop_spawn_mon 等)
|
||
* - smc.vmdata.mission_data —— 局内数据(金币 / 波数 / 怪物数量等)
|
||
* - FightSet —— 战斗常量配置
|
||
* - CardInitCoins —— 初始金币数
|
||
* - UIID.Victory —— 结算弹窗
|
||
*/
|
||
import { _decorator, Vec3,Animation, instantiate, Prefab, Node, NodeEventType, ProgressBar, Label } from "cc";
|
||
import { ecs } from "../../../../extensions/oops-plugin-framework/assets/libs/ecs/ECS";
|
||
import { CCComp } from "../../../../extensions/oops-plugin-framework/assets/module/common/CCComp";
|
||
import { smc } from "../common/SingletonModuleComp";
|
||
import { oops } from "../../../../extensions/oops-plugin-framework/assets/core/Oops";
|
||
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
|
||
import { GameEvent } from "../common/config/GameEvent";
|
||
import { HeroViewComp } from "../hero/HeroViewComp";
|
||
import { UIID } from "../common/config/GameUIConfig";
|
||
import { SkillView } from "../skill/SkillView";
|
||
import { FacSet, FightSet } from "../common/config/GameSet";
|
||
import { mLogger } from "../common/Logger";
|
||
import { Monster } from "../hero/Mon";
|
||
import { Skill } from "../skill/Skill";
|
||
import { Tooltip } from "../skill/Tooltip";
|
||
import { CardInitCoins } from "../common/config/CardSet";
|
||
const { ccclass, property } = _decorator;
|
||
|
||
|
||
//@todo 需要关注 当boss死亡的时候的动画播放完成后,需要触发事件,通知 MissionComp 进行奖励处理
|
||
|
||
/**
|
||
* MissionComp —— 任务(关卡)核心控制器
|
||
*
|
||
* 驱动单局游戏的完整流程:准备 → 战斗 → 结算。
|
||
* 管理战斗计时、怪物数量控制、英雄全灭检测和金币奖励发放。
|
||
*/
|
||
@ccclass('MissionComp')
|
||
@ecs.register('MissionComp', false)
|
||
export class MissionComp extends CCComp {
|
||
@property({ tooltip: "是否启用调试日志" })
|
||
private debugMode: boolean = false;
|
||
@property({ tooltip: "是否显示战斗内存观测面板" })
|
||
private showMemoryPanel: boolean = false;
|
||
|
||
// ======================== 配置参数 ========================
|
||
|
||
/** 怪物数量上限(超过后暂停刷怪) */
|
||
private maxMonsterCount: number = 5;
|
||
/** 怪物数量恢复阈值(降至此值以下恢复刷怪) */
|
||
private resumeMonsterCount: number = 3;
|
||
/** 新一波金币奖励基础值 */
|
||
private prepareBaseCoinReward: number = 100;
|
||
/** 每一波金币增长值 */
|
||
private prepareCoinWaveGrow: number = 1;
|
||
/** 金币奖励上限 */
|
||
private prepareCoinRewardCap: number = 500;
|
||
|
||
// ======================== 编辑器绑定节点 ========================
|
||
|
||
/** 开始战斗按钮 */
|
||
@property(Node)
|
||
start_btn:Node = null!
|
||
/** 时间/波数显示节点 */
|
||
@property(Node)
|
||
time_node:Node = null!
|
||
|
||
// ======================== 运行时状态 ========================
|
||
|
||
/** 战斗倒计时(秒) */
|
||
FightTime:number = FightSet.FiIGHT_TIME
|
||
/** 剩余复活次数 */
|
||
revive_times: number = 1;
|
||
/** 掉落奖励列表 */
|
||
rewards:any[]=[]
|
||
/** 累计游戏数据 */
|
||
game_data:any={
|
||
exp:0,
|
||
gold:0,
|
||
diamond:0
|
||
}
|
||
|
||
/** 上一次显示的时间字符串(避免重复设置) */
|
||
private lastTimeStr: string = "";
|
||
/** 上一次显示的秒数(避免重复计算) */
|
||
private lastTimeSecond: number = -1;
|
||
/** 性能监控面板 Label 引用 */
|
||
private memoryLabel: Label | null = null;
|
||
/** 性能监控刷新计时器 */
|
||
private memoryRefreshTimer: number = 0;
|
||
/** 上一次性能文本(避免重复渲染) */
|
||
private lastMemoryText: string = "";
|
||
/** 帧间隔累加(用于计算平均 FPS) */
|
||
private perfDtAcc: number = 0;
|
||
/** 帧数计数 */
|
||
private perfFrameCount: number = 0;
|
||
/** 初始堆内存基准值(MB) */
|
||
private heapBaseMB: number = -1;
|
||
/** 堆内存峰值(MB) */
|
||
private heapPeakMB: number = 0;
|
||
/** 堆内存增长趋势(MB/分钟) */
|
||
private heapTrendPerMinMB: number = 0;
|
||
/** 趋势计算计时器 */
|
||
private heapTrendTimer: number = 0;
|
||
/** 趋势计算基准(MB) */
|
||
private heapTrendBaseMB: number = -1;
|
||
/** 怪物数量同步计时器(降低同步频率) */
|
||
private monsterCountSyncTimer: number = 0;
|
||
/** 当前波数 */
|
||
private currentWave: number = 0;
|
||
/** 上一次发放金币奖励的波数(防止重复发放) */
|
||
private lastPrepareCoinWave: number = 0;
|
||
|
||
// ======================== ECS 查询匹配器(预缓存) ========================
|
||
|
||
/** 匹配拥有 HeroViewComp 的实体(英雄/怪物视图) */
|
||
private readonly heroViewMatcher = ecs.allOf(HeroViewComp);
|
||
/** 匹配拥有 SkillView 的实体(技能视图) */
|
||
private readonly skillViewMatcher = ecs.allOf(SkillView);
|
||
/** 匹配拥有 HeroAttrsComp 的实体(英雄/怪物属性) */
|
||
private readonly heroAttrsMatcher = ecs.allOf(HeroAttrsComp);
|
||
|
||
// ======================== 生命周期 ========================
|
||
|
||
onLoad(){
|
||
this.showMemoryPanel = false
|
||
// 注册生命周期事件
|
||
this.on(GameEvent.MissionStart,this.mission_start,this)
|
||
this.on(GameEvent.MissionEnd,this.mission_end,this)
|
||
this.on(GameEvent.NewWave,this.onNewWave,this)
|
||
this.on(GameEvent.DO_AD_BACK,this.do_ad,this)
|
||
this.start_btn?.on(NodeEventType.TOUCH_END, this.onStartFightBtnClick, this)
|
||
this.removeMemoryPanel()
|
||
}
|
||
|
||
onDestroy(){
|
||
this.start_btn?.off(NodeEventType.TOUCH_END, this.onStartFightBtnClick, this)
|
||
}
|
||
|
||
/**
|
||
* 帧更新:
|
||
* - 非播放 / 暂停状态 → 跳过
|
||
* - 战斗中 → 同步怪物状态、更新计时器
|
||
*/
|
||
protected update(dt: number): void {
|
||
if(!smc.mission.play) return
|
||
if(smc.mission.pause) return
|
||
if(smc.mission.in_fight){
|
||
this.syncMonsterSpawnState(dt)
|
||
if(smc.mission.stop_mon_action) return
|
||
smc.vmdata.mission_data.fight_time+=dt
|
||
this.FightTime-=dt
|
||
this.update_time();
|
||
}
|
||
}
|
||
|
||
// ======================== 时间显示 ========================
|
||
|
||
/** 更新时间/波数显示(仅在秒数变化时更新以减少 Label 操作) */
|
||
update_time(){
|
||
const time = Math.max(0, this.FightTime);
|
||
const remainSecond = Math.floor(time);
|
||
if (remainSecond === this.lastTimeSecond) return;
|
||
this.lastTimeSecond = remainSecond;
|
||
let m = Math.floor(remainSecond / 60);
|
||
let s = remainSecond % 60;
|
||
const wave = Math.max(1, this.currentWave || smc.vmdata.mission_data.level || 1);
|
||
let str = `W${wave.toString().padStart(2, '0')} ${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
||
if(str != this.lastTimeStr){
|
||
this.time_node.getChildByName("time").getComponent(Label).string = str;
|
||
this.lastTimeStr = str;
|
||
}
|
||
}
|
||
|
||
// ======================== 奖励与广告 ========================
|
||
|
||
/** 奖励发放(预留) */
|
||
do_reward(){
|
||
}
|
||
|
||
/**
|
||
* 广告回调处理:
|
||
* 成功 → 增加刷新次数;失败 → 分发失败事件。
|
||
*/
|
||
do_ad(){
|
||
if(this.ad_back()){
|
||
oops.message.dispatchEvent(GameEvent.AD_BACK_TRUE)
|
||
smc.vmdata.mission_data.refresh_count+=FightSet.MORE_RC
|
||
}else{
|
||
oops.message.dispatchEvent(GameEvent.AD_BACK_FALSE)
|
||
}
|
||
}
|
||
|
||
/** 广告观看结果(预留,默认返回 true) */
|
||
ad_back(){
|
||
return true
|
||
}
|
||
|
||
// ======================== 任务生命周期 ========================
|
||
|
||
/**
|
||
* 任务开始:
|
||
* 1. 取消上一局延迟回调。
|
||
* 2. 清理残留实体。
|
||
* 3. 初始化全部局内数据。
|
||
* 4. 分发 FightReady 事件。
|
||
* 5. 进入准备阶段并显示 loading。
|
||
*/
|
||
async mission_start(){
|
||
this.unscheduleAllCallbacks();
|
||
this.cleanComponents();
|
||
this.node.active=true
|
||
this.data_init()
|
||
oops.message.dispatchEvent(GameEvent.FightReady)
|
||
this.enterPreparePhase()
|
||
let loading=this.node.parent.getChildByName("loading")
|
||
loading.active=true
|
||
this.scheduleOnce(()=>{
|
||
loading.active=false
|
||
},0.5)
|
||
}
|
||
|
||
/**
|
||
* 进入战斗:
|
||
* - 恢复刷怪
|
||
* - 标记战斗中
|
||
* - 隐藏开始按钮
|
||
* - 分发 FightStart 事件
|
||
*/
|
||
to_fight(){
|
||
smc.mission.stop_spawn_mon = false;
|
||
smc.mission.in_fight=true
|
||
smc.vmdata.mission_data.in_fight = true
|
||
if (this.start_btn && this.start_btn.isValid) this.start_btn.active = false;
|
||
oops.message.dispatchEvent(GameEvent.FightStart)
|
||
}
|
||
|
||
/**
|
||
* 进入准备阶段:
|
||
* - 标记非战斗
|
||
* - 暂停刷怪
|
||
* - 显示开始按钮
|
||
*/
|
||
private enterPreparePhase() {
|
||
smc.mission.in_fight = false;
|
||
smc.vmdata.mission_data.in_fight = false
|
||
smc.mission.stop_spawn_mon = true;
|
||
if (this.start_btn && this.start_btn.isValid) this.start_btn.active = true;
|
||
}
|
||
|
||
/** 开始战斗按钮点击回调 */
|
||
private onStartFightBtnClick() {
|
||
if (!smc.mission.play) return;
|
||
if (smc.mission.pause) return;
|
||
if (smc.mission.in_fight) return;
|
||
this.to_fight();
|
||
}
|
||
|
||
|
||
/**
|
||
* 打开结算弹窗:
|
||
* - 暂停游戏
|
||
* - 打开 VictoryComp 弹窗
|
||
*
|
||
* @param e 事件对象(未使用)
|
||
* @param is_hero_dead 是否因英雄全灭触发
|
||
*/
|
||
open_Victory(e:any,is_hero_dead: boolean = false){
|
||
smc.mission.pause = true;
|
||
mLogger.log(this.debugMode, 'MissionComp', " open_Victory",is_hero_dead,this.revive_times)
|
||
oops.gui.open(UIID.Victory,{
|
||
victory:false,
|
||
rewards:this.rewards,
|
||
game_data:this.game_data,
|
||
can_revive: is_hero_dead && this.revive_times > 0
|
||
})
|
||
}
|
||
|
||
|
||
/** 战斗结束:延迟清理组件和对象池 */
|
||
fight_end(){
|
||
this.scheduleOnce(() => {
|
||
smc.mission.play=false
|
||
this.cleanComponents()
|
||
this.clearBattlePools()
|
||
}, 0.5)
|
||
}
|
||
|
||
/**
|
||
* 任务结束(完全退出关卡):
|
||
* - 取消所有延迟回调
|
||
* - 重置全局标志
|
||
* - 清理组件和对象池
|
||
* - 隐藏节点
|
||
*/
|
||
mission_end(){
|
||
this.unscheduleAllCallbacks();
|
||
smc.mission.play=false
|
||
smc.mission.pause = false;
|
||
smc.mission.in_fight = false;
|
||
smc.vmdata.mission_data.in_fight = false
|
||
if (this.start_btn && this.start_btn.isValid) this.start_btn.active = false;
|
||
this.cleanComponents()
|
||
this.clearBattlePools()
|
||
this.node.active=false
|
||
}
|
||
|
||
/**
|
||
* 初始化全部局内数据:
|
||
* - 全局运行标志
|
||
* - 战斗时间 / 怪物数量 / 金币 / 波数
|
||
* - 奖励列表 / 复活次数
|
||
* - 性能监控基准值
|
||
*/
|
||
data_init(){
|
||
smc.mission.play = true;
|
||
smc.mission.pause = false;
|
||
smc.mission.stop_mon_action = false;
|
||
smc.mission.stop_spawn_mon = false;
|
||
smc.vmdata.mission_data.in_fight=false
|
||
smc.vmdata.mission_data.fight_time=0
|
||
smc.vmdata.mission_data.mon_num=0
|
||
smc.vmdata.mission_data.level = 1
|
||
smc.vmdata.mission_data.mon_max = Math.max(1, Math.floor(this.maxMonsterCount))
|
||
this.currentWave = 1;
|
||
this.FightTime=FightSet.FiIGHT_TIME
|
||
this.rewards=[]
|
||
this.revive_times = 1;
|
||
this.lastTimeStr = "";
|
||
this.lastTimeSecond = -1;
|
||
this.memoryRefreshTimer = 0;
|
||
this.lastMemoryText = "";
|
||
this.perfDtAcc = 0;
|
||
this.perfFrameCount = 0;
|
||
this.heapBaseMB = -1;
|
||
this.heapPeakMB = 0;
|
||
this.heapTrendPerMinMB = 0;
|
||
this.heapTrendTimer = 0;
|
||
this.heapTrendBaseMB = -1;
|
||
this.monsterCountSyncTimer = 0;
|
||
this.lastPrepareCoinWave = 0;
|
||
smc.vmdata.mission_data.coin = Math.max(0, Math.floor(CardInitCoins));
|
||
}
|
||
|
||
// ======================== 波次管理 ========================
|
||
|
||
/**
|
||
* 新一波事件回调:
|
||
* 1. 进入准备阶段。
|
||
* 2. 更新当前波数。
|
||
* 3. 发放本波金币奖励。
|
||
* 4. 刷新时间显示。
|
||
*
|
||
* @param event 事件名
|
||
* @param data { wave: number }
|
||
*/
|
||
private onNewWave(event: string, data: any) {
|
||
const wave = Number(data?.wave ?? 0);
|
||
if (wave <= 0) return;
|
||
this.enterPreparePhase();
|
||
this.currentWave = wave;
|
||
smc.vmdata.mission_data.level = wave;
|
||
this.grantPrepareCoinByWave(wave);
|
||
this.lastTimeSecond = -1;
|
||
this.update_time();
|
||
}
|
||
|
||
/**
|
||
* 按波数发放金币奖励:
|
||
* reward = min(cap, base + (wave - 1) × grow)
|
||
* 仅在波数首次到达时发放,防止重复。
|
||
*
|
||
* @param wave 当前波数
|
||
*/
|
||
private grantPrepareCoinByWave(wave: number) {
|
||
if (wave <= 0) return;
|
||
if (wave <= this.lastPrepareCoinWave) return;
|
||
const base = Math.max(0, Math.floor(this.prepareBaseCoinReward));
|
||
const grow = Math.max(0, Math.floor(this.prepareCoinWaveGrow));
|
||
const cap = Math.max(0, Math.floor(this.prepareCoinRewardCap));
|
||
const reward = Math.min(cap, base + (wave - 1) * grow);
|
||
if (reward <= 0) {
|
||
this.lastPrepareCoinWave = wave;
|
||
return;
|
||
}
|
||
smc.vmdata.mission_data.coin = Math.max(0, Math.floor((smc.vmdata.mission_data.coin ?? 0) + reward));
|
||
this.lastPrepareCoinWave = wave;
|
||
oops.message.dispatchEvent(GameEvent.CoinAdd, { delta: reward, syncOnly: true });
|
||
mLogger.log(this.debugMode, 'MissionComp', "prepare coin reward", { wave, reward, coin: smc.vmdata.mission_data.coin });
|
||
}
|
||
|
||
// ======================== 怪物数量管理 ========================
|
||
|
||
/**
|
||
* 获取怪物数量阈值配置。
|
||
* @returns { max: 刷怪上限, resume: 恢复刷怪阈值 }
|
||
*/
|
||
private getMonsterThresholds(): { max: number; resume: number } {
|
||
const max = Math.max(1, Math.floor(this.maxMonsterCount));
|
||
const resume = Math.min(max - 1, Math.max(0, Math.floor(this.resumeMonsterCount)));
|
||
return { max, resume };
|
||
}
|
||
|
||
/**
|
||
* 同步怪物刷新状态(降频执行,每 0.2 秒一次):
|
||
* 1. 遍历所有 HeroAttrsComp 实体,统计怪物和英雄数量。
|
||
* 2. 检测英雄全灭。
|
||
* 3. 根据 max/resume 阈值切换 stop_spawn_mon 状态。
|
||
*
|
||
* @param dt 帧间隔
|
||
*/
|
||
private syncMonsterSpawnState(dt: number) {
|
||
this.monsterCountSyncTimer += dt;
|
||
if (dt > 0 && this.monsterCountSyncTimer < 0.2) return;
|
||
this.monsterCountSyncTimer = 0;
|
||
let monsterCount = 0;
|
||
let heroCount = 0;
|
||
ecs.query(this.heroAttrsMatcher).forEach(entity => {
|
||
const attrs = entity.get(HeroAttrsComp);
|
||
if (!attrs || attrs.is_dead) return;
|
||
if (attrs.fac === FacSet.MON) {
|
||
monsterCount += 1;
|
||
return;
|
||
}
|
||
if (attrs.fac === FacSet.HERO) {
|
||
heroCount += 1;
|
||
}
|
||
});
|
||
this.handleHeroWipe(heroCount);
|
||
smc.vmdata.mission_data.mon_num = monsterCount;
|
||
const { max, resume } = this.getMonsterThresholds();
|
||
smc.vmdata.mission_data.mon_max = max;
|
||
const stopSpawn = !!smc.mission.stop_spawn_mon;
|
||
if (stopSpawn) {
|
||
// 降至恢复阈值以下 → 恢复刷怪
|
||
if (monsterCount <= resume) smc.mission.stop_spawn_mon = false;
|
||
return;
|
||
}
|
||
// 超过上限 → 暂停刷怪
|
||
if (monsterCount >= max) smc.mission.stop_spawn_mon = true;
|
||
}
|
||
|
||
/**
|
||
* 英雄全灭检测:若场上无存活英雄且处于战斗中,触发结算弹窗。
|
||
* @param heroCount 当前存活英雄数量
|
||
*/
|
||
private handleHeroWipe(heroCount: number) {
|
||
if (heroCount > 0) return;
|
||
if (!smc.mission.play || smc.mission.pause) return;
|
||
if (!smc.mission.in_fight) return;
|
||
smc.mission.in_fight = false;
|
||
smc.vmdata.mission_data.in_fight = false;
|
||
this.open_Victory(null, true);
|
||
}
|
||
|
||
// ======================== 清理 ========================
|
||
|
||
/** 清理所有英雄和技能 ECS 实体 */
|
||
private cleanComponents() {
|
||
const heroEntities: ecs.Entity[] = [];
|
||
ecs.query(this.heroViewMatcher).forEach(entity => {
|
||
heroEntities.push(entity);
|
||
});
|
||
heroEntities.forEach(entity => {
|
||
entity.destroy();
|
||
});
|
||
const skillEntities: ecs.Entity[] = [];
|
||
ecs.query(this.skillViewMatcher).forEach(entity => {
|
||
skillEntities.push(entity);
|
||
});
|
||
skillEntities.forEach(entity => {
|
||
entity.destroy();
|
||
});
|
||
}
|
||
|
||
/** 回收所有战斗对象池(Monster / Skill / Tooltip)并清理场景节点 */
|
||
private clearBattlePools() {
|
||
Monster.clearPools();
|
||
Skill.clearPools();
|
||
Tooltip.clearPool();
|
||
this.clearBattleSceneNodes();
|
||
}
|
||
|
||
/** 清理战斗场景中的 HERO 和 SKILL 根节点下的所有子节点 */
|
||
private clearBattleSceneNodes() {
|
||
const scene = smc.map?.MapView?.scene;
|
||
const layer = scene?.entityLayer?.node;
|
||
if (!layer) return;
|
||
const heroRoot = layer.getChildByName("HERO");
|
||
const skillRoot = layer.getChildByName("SKILL");
|
||
if (heroRoot) {
|
||
for (let i = heroRoot.children.length - 1; i >= 0; i--) {
|
||
heroRoot.children[i].destroy();
|
||
}
|
||
}
|
||
if (skillRoot) {
|
||
for (let i = skillRoot.children.length - 1; i >= 0; i--) {
|
||
skillRoot.children[i].destroy();
|
||
}
|
||
}
|
||
}
|
||
|
||
/** 获取战斗层的英雄和技能节点数量(用于性能监控) */
|
||
private getBattleLayerNodeCount() {
|
||
const scene = smc.map?.MapView?.scene;
|
||
const layer = scene?.entityLayer?.node;
|
||
if (!layer) return { heroNodes: 0, skillNodes: 0 };
|
||
const heroRoot = layer.getChildByName("HERO");
|
||
const skillRoot = layer.getChildByName("SKILL");
|
||
return {
|
||
heroNodes: heroRoot?.children.length || 0,
|
||
skillNodes: skillRoot?.children.length || 0
|
||
};
|
||
}
|
||
|
||
// ======================== 性能监控面板 ========================
|
||
|
||
/** 性能监控相关代码 */
|
||
|
||
/** 初始化性能监控面板:在 time_node 下创建 Label */
|
||
private initMemoryPanel() {
|
||
if (!this.showMemoryPanel || !this.time_node) return;
|
||
let panel = this.time_node.getChildByName("mem_panel");
|
||
if (!panel) {
|
||
panel = new Node("mem_panel");
|
||
panel.parent = this.time_node;
|
||
panel.setPosition(0, -32, 0);
|
||
}
|
||
let label = panel.getComponent(Label);
|
||
if (!label) {
|
||
label = panel.addComponent(Label);
|
||
}
|
||
label.fontSize = 16;
|
||
label.lineHeight = 20;
|
||
this.memoryLabel = label;
|
||
}
|
||
|
||
/** 移除性能监控面板 */
|
||
private removeMemoryPanel() {
|
||
const panel = this.time_node?.getChildByName("mem_panel");
|
||
if (panel) {
|
||
panel.destroy();
|
||
}
|
||
this.memoryLabel = null;
|
||
this.lastMemoryText = "";
|
||
}
|
||
|
||
|
||
/**
|
||
* 更新性能监控面板内容(每 0.5 秒一次):
|
||
* 显示 堆内存 / 增长趋势 / 帧率 / 实体数量 / 对象池状态 等信息。
|
||
*/
|
||
private updateMemoryPanel(dt: number) {
|
||
if (!this.showMemoryPanel || !this.memoryLabel) return;
|
||
this.perfDtAcc += dt;
|
||
this.perfFrameCount += 1;
|
||
this.memoryRefreshTimer += dt;
|
||
if (this.memoryRefreshTimer < 0.5) return;
|
||
this.memoryRefreshTimer = 0;
|
||
let heroCount = 0;
|
||
ecs.query(this.heroViewMatcher).forEach(() => {
|
||
heroCount++;
|
||
});
|
||
let skillCount = 0;
|
||
ecs.query(this.skillViewMatcher).forEach(() => {
|
||
skillCount++;
|
||
});
|
||
const monPool = Monster.getPoolStats();
|
||
const skillPool = Skill.getPoolStats();
|
||
const tooltipPool = Tooltip.getPoolStats();
|
||
const layerNodes = this.getBattleLayerNodeCount();
|
||
const perf = (globalThis as any).performance;
|
||
const heapBytes = perf && perf.memory ? perf.memory.usedJSHeapSize : 0;
|
||
let heapMB = heapBytes > 0 ? heapBytes / 1024 / 1024 : -1;
|
||
if (heapMB > 0 && this.heapBaseMB < 0) {
|
||
this.heapBaseMB = heapMB;
|
||
this.heapPeakMB = heapMB;
|
||
this.heapTrendBaseMB = heapMB;
|
||
this.heapTrendTimer = 0;
|
||
}
|
||
if (heapMB > this.heapPeakMB) {
|
||
this.heapPeakMB = heapMB;
|
||
}
|
||
this.heapTrendTimer += 0.5;
|
||
if (heapMB > 0 && this.heapTrendBaseMB > 0 && this.heapTrendTimer >= 10) {
|
||
const deltaMB = heapMB - this.heapTrendBaseMB;
|
||
this.heapTrendPerMinMB = (deltaMB / this.heapTrendTimer) * 60;
|
||
this.heapTrendBaseMB = heapMB;
|
||
this.heapTrendTimer = 0;
|
||
}
|
||
const heapText = heapMB > 0 ? heapMB.toFixed(1) : "N/A";
|
||
const heapDeltaText = this.heapBaseMB > 0 && heapMB > 0 ? (heapMB - this.heapBaseMB).toFixed(1) : "N/A";
|
||
const heapPeakText = this.heapPeakMB > 0 ? this.heapPeakMB.toFixed(1) : "N/A";
|
||
const avgDt = this.perfFrameCount > 0 ? this.perfDtAcc / this.perfFrameCount : 0;
|
||
const fps = avgDt > 0 ? 1 / avgDt : 0;
|
||
this.perfDtAcc = 0;
|
||
this.perfFrameCount = 0;
|
||
const text =
|
||
`Heap:${heapText}MB Δ:${heapDeltaText} Peak:${heapPeakText}\n` +
|
||
`Trend:${this.heapTrendPerMinMB.toFixed(2)}MB/min\n` +
|
||
`Perf dt:${(avgDt * 1000).toFixed(1)}ms fps:${fps.toFixed(1)}\n` +
|
||
`Ent H:${heroCount} S:${skillCount} N:${layerNodes.heroNodes}/${layerNodes.skillNodes}\n` +
|
||
`Pool M:${monPool.total}(${monPool.paths}) K:${skillPool.total}(${skillPool.paths}) T:${tooltipPool.total}`;
|
||
if (text === this.lastMemoryText) return;
|
||
this.lastMemoryText = text;
|
||
this.memoryLabel.string = text;
|
||
}
|
||
|
||
|
||
|
||
|
||
/** ECS 组件移除时销毁节点 */
|
||
reset() {
|
||
this.node.destroy();
|
||
}
|
||
}
|