ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Unity副本系统开发实战:雾巢终局状态机与红字预警实现

Unity副本系统开发实战:雾巢终局状态机与红字预警实现 处理“异环”项目里“雾巢终局”这个副本玩法时最容易出问题的往往不是单个 Boss 技能的数值而是整条玩法链路是否可靠玩家进入布满雾气的巢穴在虚实之间判断目标等屏幕亮起红字预警后完成躲避或打断最后在终局阶段把 Boss 血量击穿触发结算。标题里的“冲破虚妄幻梦一战散尽红字烽烟”落到工程实现上可以拆成四个模块雾效场景、幻梦阶段切换、红字技能预警、Boss 终局战斗。这篇文章就用最小可运行的 Unity 示例把“雾巢终局”做成一个可由状态机驱动的副本 DEMO。示范代码不做过度封装但会保留生产项目里常见的扩展点适合已经能写基础 C# 脚本、但还没有完整做过副本系统的开发者。1. 先理解“雾巢终局”是一个系统不是一个剧情片段1.1 用工程语言重新描述玩法一句话概括这个玩法玩家进入被雾气笼罩的巢穴在一个类似梦境的循环中寻找并击破真 BossBoss 会通过全屏红字提示释放致命技能玩家必须在预警时间内完成位移或打断否则会承受高额伤害甚至重开节点。如果只把它当成剧情名工程上很容易漏掉关键内容。真正要实现的是一套可配置的副本流程它至少包含“场景进入、幻境探索、真假目标识别、红字预警、终局战斗、结果结算”六个环节。每个环节都对应不同的脚本职责和资源生命周期所以一开始就要把“雾巢终局”当作系统来设计。下面是这个副本系统的模块映射玩法环节对应技术模块核心职责进入雾巢场景加载与气氛初始化加载副本场景启动雾效显示 HUD探索与触发触发器与阶段编排玩家到达指定区域后自动推进阶段虚妄幻梦幻象生成与真身切换生成多个 Boss 镜像只有一个是真身红字烽烟预警事件与伤害执行红字提示先行延迟后执行伤害判定终局决战Boss 状态机与血量控制管理 Boss 的施法、受击、死亡表现结算数据写入与奖励发放保存通关结果解锁下一节点这张表可以提前告诉团队每个模块的验收标准不是“看起来像”而是“在特定阶段触发、在特定条件下结束、异常时有日志可查”。1.2 玩法闭环为什么必须由代码控制玩家看到的雾巢终局是线性的进入地图、看到幻象、找到真身、应对红字、击败 Boss。但代码层面不能直接写“按顺序执行”因为副本里穿插了失败、重试、超时、多人协作、性能波动等复杂情况。例如幻梦阶段中玩家可能长时间攻击幻象也可能把雾巢里的计时器耗尽。如果没有一个明确的状态机每个模块都会自己判断“当前该干什么”最终就会变成“UI 显示错误阶段、Boss 提前进入无敌、红字播报完但伤害没结算”这类问题。正确做法是让阶段状态机成为唯一流程决策者其余系统只监听状态切换事件并响应自己的生命周期回调。1.3 技术难点分布把难点提前列出来开发时才不会陷在细节里难点出错表现建议方案多阶段切换阶段卡死、重复触发用状态机统一管理进入和退出幻象数量多生成时卡顿、内存升高对象池复用 Boss 镜像红字与伤害不同步玩家被莫名秒杀数据驱动预警与伤害执行雾效影响性能低端机帧率骤降雾效分层、可动态关闭结算结果不一致通关后没有记录使用独立结算服务或数据校验把这五个问题解决雾巢终局的可玩版本就已经具备雏形了。2. 开发环境、依赖和目录结构要提前对齐2.1 示例开发环境下面这组环境是演示 DEMO 的推荐配置不是唯一选择。实际接入前先确认项目使用的 Unity 版本和渲染管线避免示例代码因 API 差异失效。组件推荐版本说明Unity2021.3 LTS 或 2022.3 LTS使用 LTS 版本稳定性更好渲染管线URP 12 或 14演示使用 URP 做雾效与后处理开发语言C# 9.0Unity 2021 起默认支持输入系统Input System 1.7支持新输入事件UI 方案TextMeshPro红字预警需要高质量字体渲染后处理Post Processing 3.2做最终调色和泛光如果项目仍然使用内置渲染管线第 6 章雾效部分需要改成线性雾或指数雾方案其他阶段状态机、幻象对象池、红字预警逻辑都可以复用。2.2 推荐目录结构副本系统涉及场景、脚本、配置、预制体和 UI目录如果混乱后期找人、找资源都会非常痛苦。推荐按功能模块划分Assets/ Scenes/ FFA_Dungeon.unity Scripts/ Core/ # 游戏启动、服务定位 Flow/ # 副本阶段状态机 Dream/ # 幻梦阶段幻象与真身切换 Boss/ # Boss 行为、血量、技能 Warning/ # 红字预警系统 VFX/ # 雾效、屏幕特效控制 Datas/ StageConfigs/ # 阶段配置资源 WarningConfigs/ # 红字预警配置资源 RewardConfigs/ # 通关奖励配置资源 Prefabs/ Boss/ # Boss 预制体 Enemy/ # 小怪预制体 Projectiles/ # 弹道与范围预警预制体 UI/ HUD/ Warning/关键点有两个一是配置资源要单独放不能直接塞进 Prefab 里否则策划调参时每次都要打开预制体二是 Flow 脚本要与 Boss 脚本解耦状态机不直接调用 Boss 的私有方法而是通过事件分发。2.3 依赖包确认在 Unity Package Manager 中确认以下包存在。如果没有直接通过 UPM 添加{ dependencies: { com.unity.render-pipelines.universal: 14.0.10, com.unity.inputsystem: 1.7.0, com.unity.textmeshpro: 3.0.6, com.unity.postprocessing: 3.2.2 } }这里要提醒一句版本号只是示例不同 Unity 2021.3 小版本的 URP 包可能不同。正确的做法是先新建一个空项目在 Package Manager 中查看已解析版本再把项目代码迁移过去。3. 用阶段状态机先把副本流程串起来3.1 先定义副本阶段枚举阶段枚举是整套流程的“通用语言”。策划说“进入幻梦”程序说“DreamLoop”美术说“场景切幻觉滤镜”都要对应到同一个定义。public enum FogNestStage { None 0, Entering, // 进入雾巢 Exploring, // 探索与触发 DreamLoop, // 虚妄幻梦 BossFight, // 红字烽烟终局战 Ending, // 通关结算 Failed // 失败重试 }不建议从 1 开始定义公共枚举保留 0 作为未初始化状态可以用来排查“Boss 还没配置就开战”这类时序错误。3.2 最小状态机实现副本状态机不需要引入复杂框架一个 MonoBehaviour 加上进入和退出回调就能跑通using UnityEngine; public class FogNestFlow : MonoBehaviour { public static FogNestFlow Instance { get; private set; } public FogNestStage Current { get; private set; } private void Awake() { Instance this; Current FogNestStage.None; } public void ChangeStage(FogNestStage next) { if (next Current) { Debug.LogWarning($[FogNestFlow] 尝试切换到当前阶段 {next}已忽略); return; } Debug.Log($[FogNestFlow] Stage {Current} - {next}); ExitStage(Current); Current next; EnterStage(Current); } private void ExitStage(FogNestStage stage) { switch (stage) { case FogNestStage.Entering: FFAEvents.RaiseStageExited(stage); break; case FogNestStage.DreamLoop: DreamBossDirector.Instance?.StopAllCoroutines(); FFAEvents.RaiseStageExited(stage); break; } } private void EnterStage(FogNestStage stage) { switch (stage) { case FogNestStage.Entering: FFAEvents.RaiseStageEntered(stage); break; case FogNestStage.DreamLoop: FFAEvents.RaiseStageEntered(stage); break; } } }这段代码的关键不是 switch 本身而是“先 Exit 再 Enter”。很多副本卡死是因为旧阶段生成的 Boss、协程、事件监听没有清理新阶段启动后两套逻辑同时运行。所以在 ExitStage 里要停止协程、关闭 UI、销毁临时对象。3.3 为什么不用 if/else 堆逻辑如果只在 Update 里写“当前阶段等于 DreamLoop 时执行幻梦逻辑”短时间能跑但只要加入失败重试、超时复活、多人同步代码就会变成无数分支叠加。状态机的优势是强制规定每个阶段有明确的进入时机和退出时机阶段之间可以插桩日志每个系统按 EnterStage 和 ExitStage 分别处理资源。一个比较容易落地的开发习惯是状态机脚本里只写流程不写 Boss 具体技能不写 UI 文字不写伤害计算。具体表现由其他模块监听FFAEvents.RaiseStageEntered事件完成。注意不要在 Update 里散落“当前阶段 xxx”的判断。阶段切换只能通过 ChangeStage 触发否则你会很难定位“为什么 Boss 在幻梦阶段就开始放红字技能”。4. “虚妄幻梦”的落地真假 Boss 的识别与切换4.1 幻梦阶段的规则设定“虚妄幻梦”放在副本里可以设计成这样一个规则场景中同时存在多个 Boss 镜像只有一个镜像携带真身组件其余为幻象玩家攻击幻象会获得“虚妄”反馈但不会扣除真身血量幻象被击败一定数量后真身会进入可击杀状态如果超时未破梦所有幻象重置真身索引重新随机。这种规则的好处是玩家能快速理解程序也容易实现。真假 Boss 的决定权不在美术表现上而在一个可配置的索引上。4.2 用配置决定真身和幻象把配置独立成资源策划可以根据不同难度调整幻象数量和血量{ dreamLoopCount: 3, illusionCount: 4, autoSelectTrueBoss: true, trueBossIndex: -1, illusionHP: 1000, trueBossHP: 8000, timeLimit: 60 }对应 C# 配置类using System; [Serializable] public class DreamStageConfig { public int dreamLoopCount 3; // 循环轮数 public int illusionCount 4; // 每次生成的镜像数量 public bool autoSelectTrueBoss true; public int trueBossIndex -1; // 手动指定真身-1 表示自动随机 public float illusionHP 1000f; public float trueBossHP 8000f; public float timeLimit 60f; }autoSelectTrueBoss字段非常实用。测试阶段可以把它设为 false并手动指定trueBossIndex 0这样跑自动化脚本时不会因为随机结果导致测试不稳定。4.3 用对象池生成幻象幻象不是一次性生成 4 个就结束而是每一轮循环都会重新生成。为了避免每轮 Instantiate 造成卡顿使用对象池using UnityEngine; public class SimpleObjectPoolT where T : Component { private readonly T prefab; private readonly Transform parent; private readonly QueueT pool new(); public SimpleObjectPool(T prefab, Transform parent, int preload) { this.prefab prefab; this.parent parent; for (int i 0; i preload; i) { T item Create(); item.gameObject.SetActive(false); pool.Enqueue(item); } } public T Get() { T item pool.Count 0 ? pool.Dequeue() : Create(); item.gameObject.SetActive(true); return item; } public void Release(T item) { item.StopAllCoroutines(); item.gameObject.SetActive(false); pool.Enqueue(item); } private T Create() { T item Object.Instantiate(prefab, parent); return item; } }这里的Release除了把物体隐藏还要调用StopAllCoroutines。幻象身上通常挂着技能协程如果对象池复用时没有停掉旧的协程新幻象会继承上一个对象的施法状态。4.4 真假 Boss 切换逻辑真假切换由DreamBossDirector控制。它负责从对象池取幻象初始化真身索引并监听幻象被击败事件using System.Collections.Generic; using UnityEngine; public class DreamBossDirector : MonoBehaviour { [SerializeField] private FogBossController bossPrefab; [SerializeField] private Transform bossRoot; private readonly ListFogBossController activeBosses new(); private SimpleObjectPoolFogBossController pool; private DreamStageConfig config; private void Awake() { pool new SimpleObjectPoolFogBossController(bossPrefab, bossRoot, 4); } public void StartDream(DreamStageConfig cfg) { config cfg; ClearActiveBosses(); int trueIndex config.autoSelectTrueBoss ? Random.Range(0, config.illusionCount) : config.trueBossIndex; for (int i 0; i config.illusionCount; i) { FogBossController boss pool.Get(); boss.InitAsIllusion(config.illusionHP); boss.OnIllusionDefeated HandleIllusionDefeated; activeBosses.Add(boss); if (i trueIndex) { boss.InitAsTrue(config.trueBossHP); } } } private void HandleIllusionDefeated(FogBossController boss) { activeBosses.Remove(boss); pool.Release(boss); if (activeBosses.Count 0) { FFAEvents.RaiseStageCompleted(FogNestStage.DreamLoop); } } private void ClearActiveBosses() { foreach (FogBossController boss in activeBosses) { boss.OnIllusionDefeated - HandleIllusionDefeated; pool.Release(boss); } activeBosses.Clear(); } }注意activeBosses.Count 0的判断意味着所有镜像都被击败。这里有一个容易被忽略的细节真身是否也会被当成幻象击败如果玩法要求“真身最终可击杀”那么真身被击败后就进入终局战斗而不是判定为幻梦失败。这个问题需要策划明确规则代码里用boss.IsTrue分支处理即可。4.5 幻象被击败后的阶段推进幻梦阶段结束时FogNestFlow需要切换阶段。推荐使用事件让状态机只监听结果private void OnEnable() { FFAEvents.OnStageCompleted HandleStageCompleted; } private void HandleStageCompleted(FogNestStage stage) { if (stage FogNestStage.DreamLoop) { ChangeStage(FogNestStage.BossFight); } }如果幻梦超时则重置所有幻象并将trueBossIndex重新随机让玩家重新体验一轮“虚妄幻梦”。这样玩家不会因为一次误判就永远卡死。5. “红字烽烟”把必杀预警做成数据驱动系统5.1 红字预警的数据结构红字预警是终局战里最核心的表现系统。它要解决的不只是“屏幕上显示红色文字”而是让玩家在技能真正结算前有足够时间反应。推荐用一个配置类描述一次预警using System; using UnityEngine; [Serializable] public class RedWarningConfig { public string warningText 警告红字烽烟; public float duration 2f; // 预警持续时间 public float damage 30f; // 预警结束后造成的伤害 public float rangeRadius 4f; // 危险区域半径 public Vector2 offset; // 危险区域相对 Boss 的偏移 public WarningStyle style WarningStyle.Circle; } public enum WarningStyle { Circle, Rect, Dash }这里最关键的是duration。它表示“从红字出现到伤害结算”的时间间隔。如果这个值小于 1 秒玩家几乎无法反应如果太长又会拖慢战斗节奏。建议基础技能 1.5 到 2 秒高伤害技能 2.5 到 3 秒。5.2 预警触发流程正确的流程分三段预警开始显示红字生成危险区域施法中玩家可以移动、打断、格挡结算执行伤害判定关闭危险区域。using System.Collections; using UnityEngine; public class RedWarningSystem : MonoBehaviour { [SerializeField] private RedWarningUI warningUI; [SerializeField] private GameObject dangerZonePrefab; private GameObject currentDangerZone; public void ShowWarning(RedWarningConfig config) { Debug.Log($[RedWarning] Show: {config.warningText} in {config.duration}s); warningUI.Show(config.warningText, config.duration); SpawnDangerZone(config); StartCoroutine(ExecuteWarningDamage(config)); } private IEnumerator ExecuteWarningDamage(RedWarningConfig config) { yield return new WaitForSeconds(config.duration); PlayerHealth player FindFirstObjectByTypePlayerHealth(); if (player ! null player.IsInsideDangerZone(currentDangerZone.transform.position, config.rangeRadius)) { player.TakeDamage(config.damage); } DestroyDangerZone(); } }这段代码只用于演示逻辑实际项目需要把PlayerHealth改造成服务或组件引用不要频繁使用FindFirstObjectByType。注意红字预警必须遵守“预告、施法、结算”三段节奏。不要出现 UI 红字和伤害同时触发的现象那会直接破坏玩家对副本的信任感。5.3 红字 UI 的实现红字 UI 的 Canvas 要放在最上层不能被雾效、后处理和 UI 特效遮挡。使用 TextMeshPro 保证 UI 清晰。using System.Collections; using UnityEngine; using TMPro; public class RedWarningUI : MonoBehaviour { [SerializeField] private TextMeshProUGUI warningText; [SerializeField] private CanvasGroup canvasGroup; public void Show(string content, float duration) { StopAllCoroutines(); warningText.text content; StartCoroutine(DisplayFlow(duration)); } private IEnumerator DisplayFlow(float duration) { canvasGroup.alpha 1f; yield return new WaitForSeconds(duration * 0.6f); float fadeDuration duration * 0.4f; float timer fadeDuration; while (timer 0f) { timer - Time.deltaTime; canvasGroup.alpha Mathf.Clamp01(timer / fadeDuration); yield return null; } canvasGroup.alpha 0f; } }这个实现里显示阶段占 60% 时间淡出阶段占 40%。如果同一帧有多条红字要显示不要直接覆盖应该给RedWarningConfig加一个priority字段只有高优先级预警才能打断当前显示。5.4 用事件解耦 Boss 和预警系统Boss 脚本不应该直接 new 一个RedWarningConfig后调用RedWarningSystem这样会把 Boss 的技能逻辑和 UI 表现绑死。推荐做法是让 Boss 发出预警事件由全局监听者转发给预警系统。using UnityEngine; using UnityEngine.Events; public class BossWarningEvent : MonoBehaviour { public UnityEventRedWarningConfig OnWarningTriggered; }Boss 技能脚本里只需要触发事件using System.Collections; using UnityEngine; public class FogBossController : MonoBehaviour { [SerializeField] private BossWarningEvent warningEvent; public void CastRedWarningSkill(RedWarningConfig config) { warningEvent.OnWarningTriggered?.Invoke(config); StartCoroutine(SkillProcess(config)); } private IEnumerator SkillProcess(RedWarningConfig config) { yield return new WaitForSeconds(config.duration); // 此处执行真实伤害判定 DealDamage(config); } }这样写的好处是红字预警系统可以被任意 Boss 复用甚至普通精英怪也可以使用同一套红字机制只需要配置不同的文案和伤害参数。6. 雾效氛围性能、可调试性和视觉品质的平衡6.1 三种雾效方案对比雾巢最重要的视觉特征是“雾”。实现方式多种多样需要根据项目阶段选择方案实现成本视觉效果性能风险适用阶段内置指数雾低中等全场景均匀低DEMO 和生产环境都可用URP 体积雾中高有空间层次中高全屏后处理高配 PC 或主机自定义全屏雾 Shader高最高可定制取决于实现有专门 TA 团队时使用如果目标是快速验证副本流程优先使用内置指数雾。它能通过RenderSettings.fogDensity动态调整非常适合配合阶段状态机做“进入雾巢时雾变浓、终局时雾散开”的演出。6.2 用指数雾实现基础雾效新建一个FogAmbience脚本挂在副本根物体上using System.Collections; using UnityEngine; public class FogAmbience : MonoBehaviour { [SerializeField] private float targetDensity 0.06f; [SerializeField] private float transitionSpeed 1f; public IEnumerator EnterFog() { RenderSettings.fog true; while (Mathf.Abs(RenderSettings.fogDensity - targetDensity) 0.001f) { RenderSettings.fogDensity Mathf.Lerp( RenderSettings.fogDensity, targetDensity, Time.deltaTime * transitionSpeed ); yield return null; } RenderSettings.fogDensity targetDensity; } public IEnumerator ExitFog() { while (Mathf.Abs(RenderSettings.fogDensity - 0f) 0.001f) { RenderSettings.fogDensity Mathf.Lerp( RenderSettings.fogDensity, 0f, Time.deltaTime * transitionSpeed ); yield return null; } RenderSettings.fog false; } }这里使用协程而不是直接赋值是为了让雾效变化成为一段可被打断的演出。阶段状态机在退出副本或切换新阶段时可以调用StopAllCoroutines停止当前雾效过渡避免出现“雾还没散就切场景”的情况。6.3 性能预算与参数控制雾效不是越大越好。分享一张基础性能控制表控制项低配建议高配建议原因粒子雾数量50 个以内200 个以内粒子雾是性能大头后处理次数1 次最多 2 次多次全屏后处理增加带宽雾粒子距离30 米内60 米内远景粒子肉眼难分辨动态更新频率每 0.2 秒每帧雾流不需要每帧都变化如果使用全屏后处理雾需要额外关注 RenderFeature 的适用场景。不要在 HUD 界面上叠加体积雾HUD 应该使用单独的 Canvas 层级不受后处理影响。6.4 雾效调试开关开发阶段最怕雾太浓导致看不清错误布局。建议给FogAmbience加一个调试菜单using UnityEngine; public partial class FogAmbience : MonoBehaviour { [ContextMenu(Toggle Fog)] public void ToggleFog() { RenderSettings.fog !RenderSettings.fog; Debug.Log($[FogAmbience] Fog Enabled: {RenderSettings.fog}); } [ContextMenu(Set Fog Density To 0)] public void ClearFog() { RenderSettings.fogDensity 0f; RenderSettings.fog false; } }这样运行到迷雾场景时如果看不到关键节点可以直接在 Inspector 右键组件通过Toggle Fog临时关闭雾效不必重启游戏。建议先开调试开关再谈雾效品质。看不到逻辑运行过程雾效做得再好看也无法帮你定位问题。7. 运行验证与排错现象、原因、修复路径7.1 最小 DEMO 验证清单跑通副本后不要只看画面。按下面的清单逐项验证验证项操作预期结果进入雾巢从主城传送到副本雾渐浓阶段切换日志出现 Entering探索触发走到指定区域状态机进入 DreamLoop幻象生成观察 Boss 数量生成 4 个镜像日志打印真身索引攻击幻象攻击非真身显示“虚妄”反馈真身血量不变击败幻象击杀所有镜像状态机进入 BossFight红字预警触发 Boss 技能红字出现 2 秒后才有伤害终局结算Boss 血量归零状态机进入 Ending奖励写入成功7.2 常见坑以下是副本开发中经常出现的现象、原因和修复方式问题现象常见原因处理建议红字出现的同时玩家掉血预警与伤害在同一个协程里执行没有等待 duration拆分预告和结算两段协程击杀所有幻象后真身未出现真身也在activeBosses.Count 0时被当成幻象释放真身被击败时单独进入 BossFight状态机卡在 DreamLoop阶段切换事件没有订阅或者事件被重复订阅在 OnDisable 里取消事件监听对象池复用时血量异常对象池 Release 时没有重置血量、状态机和协程Release 中调用 Reset 方法UI 红字被雾效遮挡Canvas 的 Render Mode 使用了 Screen Space Camera且雾后处理影响 UI单独用最上层 Canvas或使用 Overlay 模式低端机进入雾巢后帧率骤降粒子雾和后处理过多根据设备分级动态降低雾粒子数量7.3 排查链路如果副本运行到一半出现问题按下面的链路排查通常比直接翻代码更快。第一步确认阶段切换日志。查看 Control 台是否出现[FogNestFlow] Stage DreamLoop - BossFight。如果缺少日志问题在阶段状态机或事件监听。第二步确认红字预警时间线。查看[RedWarning] Show: ...和伤害执行的时间差。如果中间没有间隔预警系统逻辑错误。第三步确认对象池状态。查看每次生成幻象时对象池是否实际创建了新的实例还是复用了旧对象。如果复用后没有重置会出现“继承了上一个对象的真身索引”的问题。第四步确认渲染设置。如果画面里雾完全透明或过于浓密检查RenderSettings.fog、fogDensity、雾效 Shader 是否与 URP 管线匹配。这四条链路基本覆盖 80% 的副本流程问题。8. 从 DEMO 走向正式副本配置服务、监控、多人化和自动化测试8.1 配置外置化DEMO 阶段可以把配置写在 C# 类里但正式副本一定要外置。推荐把DreamStageConfig、RedWarningConfig、RewardConfig做成 ScriptableObject 或 JSON 资源并允许策划在编辑器里直接调整。配置项是否可线上调整调整影响幻象数量可影响难度和性能幻象血量可影响玩家通关时长红字预警时间可影响玩家反应容错Boss 伤害数值可影响平衡性雾效浓度可影响视觉表现不建议频繁调通关奖励可影响经济系统配置标准化后测试也可以从配置驱动生成测试用例而不是每次改数值都要重新出包。8.2 日志、监控和异常恢复副本系统在正式环境不能“卡死就重开”。需要两类兜底能力第一类是监控。在状态机ChangeStage里输出结构日志并把日志接入大数据平台。出现连续长时间未切换阶段时告警触发。第二类是异常恢复。给副本状态机增加超时保护public class FogNestFlow : MonoBehaviour { [SerializeField] private float stageTimeout 120f; private void Update() { if (Current FogNestStage.None || Current FogNestStage.Ending) return; if (Time.time - stageEnterTime stageTimeout) { Debug.LogError($[FogNestFlow] Stage {Current} 超时自动进入失败处理); ChangeStage(FogNestStage.Failed); } } private void EnterStage(FogNestStage stage) { stageEnterTime Time.time; // 其他逻辑 } }超时保护不能替代表现层的 Bug但它能保证玩家不会因为客户端异常永远卡在某个阶段。正式环境里还应该提供“返回主城”的强制出口。8.3 多人联机环境下的注意事项如果副本支持多人红字预警和伤害结算必须以服务端或主机权威为准。客户端看到红字预警不代表接下来一定能造成伤害。玩家在预警范围内服务器是否判定命中取决于服务器收到的位置和 buff 状态。因此RedWarningConfig.damage、rangeRadius、offset这些参数要放到服务端配置里客户端只负责表现。状态机切换也要避免“每个客户端各自跑一套”。理想方式是主机或服务器广播当前阶段客户端收到后才能切换到对应表现。8.4 自动化测试和回放副本测试不适合全靠手工。以下用例可以做成 PlayMode 测试测试用例操作断言进入阶段调用 ChangeStage(Entering)Current 等于 Entering幻梦生成调用 StartDream场内 Boss 数量等于配置值真假标记检查真身索引只有真身 IsTrue 为 true红字延迟触发 RedWarning伤害执行时间晚于预警 2 秒通关结算调用 EndStage奖励列表非空且状态进入 Ending如果条件允许还可以把副本关键操作记录成回放数据阶段切换、红字预警、伤害结算、Boss 行为。这些数据既能用于测试回归也能用于玩家错误反馈分析。回到“雾巢终局”这个玩法本身真正复杂的并不是某一个技能有多华丽而是多个系统能否沿着同一时间轴运行。阶段状态机负责流程幻象对象池负责性能红字预警负责玩家反馈雾效负责氛围它们各司其职再通过事件连接。对新手的建议是先不要急着做完整 Boss先做一个“进入雾巢、出现幻象、击杀后真身出现、红字预警、通关”的五节点 DEMO每个节点用日志输出把流程跑通后再叠加表现层。这样无论最后项目是否叫“异环”“雾巢终局”这套副本流程控制方法都能复用到后续其他副本里。
返回列表