Files
pixelheros/assets/script/game/map/MissionComp.ts
walkpan e880613f8f docs: 为游戏地图模块添加详细的代码注释
为游戏地图模块的脚本文件添加全面的注释,说明每个组件的职责、关键设计、依赖关系和使用方式。注释覆盖了英雄信息面板、技能卡槽位管理器、排行榜弹窗、卡牌控制器、背景滚动组件等核心功能模块,提高了代码的可读性和维护性。

同时修复了英雄预制体的激活状态和技能效果预制体的尺寸参数。
2026-04-07 19:00:30 +08:00

636 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @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();
}
}