燃值

正正考核期 0 做的世界进度玩法:燃值积累条 + 燃值点(FirePoint)。 积累条是团队协作数值——三层加成(团队人数/战斗人数/安全区人数)全走配置表; 燃值点消耗完全在战斗服(Buff 体系),客户端零扣减逻辑。 页面虽小,但把「数值配置化 + 服务器权威边界」两件事做得很干净。

0 到 1 三层加成全配置化 双通道刷新 客户端零扣减

1. 工作清单

内容性质
燃值系统 0 到 1:积累协议对接、三层加成计算、进度 UI(主界面火焰条)、燃值点属性通知 0 建

2. 理论:三层加成模型与职责边界

2.1 积累速率:三层加成的叠加结构

层驱动公式上限
① 团队人数层isAllTeamMemberInSameLevel 且 playerCount > 1min(playerCount, param0)max_fire_layer_limit_of_team_member
② 战斗成员层队友 battleState 为 BATTLE 的数量battleCount * param1(随总钳制)
③ 安全区成员层队友在安全区的数量min(safeCount * param2, param7)max_fire_layer_limit_of_safe_zone_member
总钳制三层相加后AddLayer = min(AddLayer, param3)fire_point_gather_max_layer

加成层数 → 每跳增加量:查 fire_point_gather_layer_value 数组表(层号即下标)。 客户端每 3 秒一跳把增加量累进 fireGatherValue,到 fire_point_max_gather_value 封顶。

数值全部配置化的收益 七张 commonParam 配置(param0-param7)没有一个魔法数字写死在代码里: 策划调「安全区每人加几层」「团队最多算几人」全部改表; 数值策划调整节奏不用发版。「层数表」还顺带把非线性留了口子—— 第 3 层加 15、第 4 层加 30,数值曲线自由。

2.2 职责切分:客户端算速率,服务器管一切权威值

1
服务器推送权威值
WorldFirePointGatherValueSCNotify:fireGatherValue / banFirePointCount / isAllTeamMemberInSameLevel
2
客户端平滑推进
面板 3 秒定时器跑 UpdateFirePointGatherValue,按三层加成逐跳累进(表现插值)
3
UI 双通道刷新
定时器跳一拍刷一次 + 服务器通知到达立刻刷(Emit FirePointGatherValueChange)
「客户端零扣减」的边界设计 客户端敢自己加 fireGatherValue(积累条可以客户端先走), 但燃值点的消耗(banFirePointCount、扣点)一行逻辑都没有—— 消耗发生在战斗服 Buff 体系(燃值点挂 GameplayTag 的 Buff 上), 客户端只收 ATTRIBUTE_TYPE_FIRE_POINT 属性通知打日志。 判断标准:积累是表现(客户端可乐观推进),消耗是裁定(必须服务器)。 混了这个边界的模块,回放/断线重连时数值必对不上。

3. 代码片段

3.1 三层加成计算:每层独立钳制 + 总钳制

Src\Game\Module\FirePoint\FirePointModel.ts:95-131(节选)

public UpdateFirePointGatherValue(): void {
    if (this.FirePointInfo === undefined) { return; }

    // 人数加成:团队人数越多,加成层数越高(最多4层)
    const playerCount = ModelManager.TeamModel?.GetTeamMemberNum();
    if (playerCount === undefined) { return; }

    this.AddLayer = 0; // 重置加成层数

    if (this.FirePointInfo.isAllTeamMemberInSameLevel) {
        if (playerCount > 1) {
            const param0 = configCommonParamById.GetIntConfig('max_fire_layer_limit_of_team_member')!;
            this.AddLayer += Math.min(playerCount, param0);
        }

        // 战斗中玩家加成:每个战斗中的玩家提供额外加成
        const battleMemberCount = this.CountOfBattleMember();
        const param1 = configCommonParamById.GetIntConfig('fire_point_battle_member_add_layer')!;
        this.AddLayer += battleMemberCount * param1;

        // 安全区中玩家加成:每个安全区中的玩家提供额外加成(限制最大4层)
        const safeZoneMemberCount = this.CountOfSafeZoneMember();
        const param2 = configCommonParamById.GetIntConfig('fire_point_safe_zone_member_add_layer')!;
        const param7 = configCommonParamById.GetIntConfig('max_fire_layer_limit_of_safe_zone_member')!;
        const safeZoneAddition = safeZoneMemberCount * param2;
        this.AddLayer += Math.min(safeZoneAddition, param7);
    }

    // 最终加成层数不能超过配置上限
    const param3 = configCommonParamById.GetIntConfig('fire_point_gather_max_layer')!;
    this.AddLayer = Math.min(this.AddLayer, param3);
    ...
}
钳制的层次感 团队层 min(人数封顶)、安全区层 min(乘积封顶)、最后总 min(全层封顶)—— 每层先各自钳,总和再钳一次。 这样任何一层配置改到离谱,AddLayer 都不会穿透 fire_point_gather_max_layer, 查表下标永远安全(见下一段的越界防御)。

3.2 层数查表 + 上限封顶

Src\Game\Module\FirePoint\FirePointModel.ts:133-167

    // 根据加成层数增加燃值积累
    const param4 = configCommonParamById.GetIntArrayConfig('fire_point_gather_layer_value')!;
    if (this.FirePointInfo && this.AddLayer < param4.length) {
        const add = param4[this.AddLayer];
        this.FirePointInfo.fireGatherValue += add; //速率
    }

    // 检查是否达到燃值上限,达到则重置并标记需要分配燃值点
    const param5 = configCommonParamById.GetIntConfig('fire_point_max_gather_value')!;
    if (this.FirePointInfo && this.FirePointInfo.fireGatherValue >= param5) {
        this.FirePointInfo.fireGatherValue = param5; // 重置燃值积累到上限
    }
}

public GetFirePointGatherValue(): number {
    if (!this.FirePointInfo) { return 0; }
    const param5 = configCommonParamById.GetIntConfig('fire_point_max_gather_value')!;
    if (this.FirePointInfo.fireGatherValue >= param5) {
        return 1; // 返回上限值
    }
    return this.FirePointInfo.fireGatherValue / param5;
}
UI 读的是归一化进度 AddLayer < param4.length 的越界检查保证查表安全(配置表删行不会越界读 undefined); GetFirePointGatherValue 返回 0-1 归一化进度而不是裸值—— UI 层 SetFillAmount 直接吃,表现和数值上限解耦(改上限不用动 UI)。

3.3 通知正发:Controller 薄得只有正发

Src\Game\Module\FirePoint\FirePointController.ts:31-48

// 燃值累计值更新通知
private readonly OnFirePointGatherValueNotify = (notify: Game.Protocol.WorldFirePointGatherValueSCNotify): void => {
    const firePointInfo = ModelManager.FirePointModel!.GetInfo();
    if (firePointInfo) {
        firePointInfo.fireGatherValue = notify.fireGatherValue;
        firePointInfo.banFirePointCount = notify.banFirePointCount;
        firePointInfo.isAllTeamMemberInSameLevel = notify.isAllTeamMemberInSameLevel;
        EventSystem.Emit(EEventName.FirePointGatherValueChange);
    }
};

private readonly OnFirePointTeamMemberInfoNotify = (
    notify: Game.Protocol.WorldFirePointTeamMemberInfoSCNotify,
): void => {
    ModelManager.FirePointModel?.SetMemberInfo(notify.fireTeamMemberInfo);
};
MVC 分层的教科书样例 61 行的 Controller 只做三件事:注册两条通知、把权威值灌进 Model、Emit 事件。 没有一行数值计算。 服务器通知一到直接覆盖 fireGatherValue—— 客户端 3 秒一跳的乐观推进随时被权威值校准, 两条通道(客户端速率 + 服务器校准)最终收敛。

3.4 UI 面板:3 秒定时器 + 事件双驱动

Src\Game\Module\MainUi\Views\ChildView\BattleUiFirePanel.ts:47-60, 94-100(节选)

protected override OnStart(): void {
    super.OnStart();
    this.Refresh();
    this.RefreshTimer = this.OwnedTimers.Forever(this.OnTimerRefresh, 3000, 'FirePanelView.OnStart');
}

private readonly OnTimerRefresh = (): void => {
    if (ModelManager.FirePointModel === undefined) { return; }
    ModelManager.FirePointModel.UpdateFirePointGatherValue();   // 通道1:定时器推进积累
    this.Refresh();
};

private readonly FirePointGatherValueChange = (): void => {
    this.Refresh();                                                    // 通道2:服务器通知立刻刷
};

private Refresh(): void {
    if (ModelManager.FirePointModel === undefined) { return; }
    const firePointValue = ModelManager.FirePointModel.GetFirePointGatherValue();
    this.BarFlame?.SetFillAmount(firePointValue);
}
双通道刷新的取舍 只有服务器通知:积累条 3 秒一跳的服务器节奏推送太浪费; 只有客户端定时器:服务器改值(比如 ban 掉燃值点)UI 不同步。 两条并存:定时器管平滑推进,通知管权威校准。 定时器挂在 OwnedTimers 上(面板销毁自动回收),不会泄漏。

4. 面试怎么讲

燃值系统的加成模型是怎么设计的设计
三层加成叠加:团队人数层(全员同关卡才生效,min 人数封顶)、 战斗成员层(每个战斗中的队友加固定层)、安全区成员层(乘积后 min 封顶)。 三层各自钳制后再做一次总钳制。
所有数字全部走 commonParam 配置表,含一张「层数 → 每跳增量」数组表—— 数值曲线非线性也支持,策划调参零代码改动。 总钳制同时保证了查表下标永远不越界。
客户端和服务端的数值边界怎么划关联
一句话:积累是表现,消耗是裁定。 积累条客户端每 3 秒按加成速率乐观推进(UI 平滑), 服务器随时推权威值直接覆盖校准(两条通道收敛); 燃值点的消耗在战斗服 Buff 体系(ATTRIBUTE_TYPE_FIRE_POINT 属性通知), 客户端一行扣减逻辑都没有,banFirePointCount 只读不写。
这个边界混了的代价很具体:断线重连/回放时数值必对不上, 多人场景里每个人的燃值点会各算各的。