关于 unity-level-design
面向 Unity 关卡设计与玩法开发的技能,提供将设计意图转化为数据配置和可复用系统的模式,涵盖触发器、遭遇战、检查点、演出及交互。
主要功能
- 01可配置触发器与事件通道
- 02数据驱动的遭遇战波次
- 03检查点状态保存与恢复
- 04可组合的异步演出序列
- 05统一的物体交互模式
适用场景
- 01在 Inspector 中配置关卡触发行为
- 02无需改代码调整敌人波次
- 03玩家死亡后恢复关卡状态
- 04在编辑器中编排可跳过的演出
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ nice-wolf-studio/ unity-claude-skills --skill unity-level-design安装后在 Claude Code 中输入/unity-level-design确认已加载。
附带文件references/level-system-scaffolds.md
详细介绍
关卡设计 —— 设计转换模式
前置技能:
unity-scene-assets(叠加场景、Addressables)、unity-async-patterns(异步加载、取消)、unity-game-architecture(SO 事件、引导)
Claude 将关卡视为静态场景文件,关卡设计师与游戏程序员之间没有契约层。触发器是一次性脚本,游戏逻辑全塞进 OnTriggerEnter;遭遇战是硬编码的生成序列;检查点在场景重新加载后失效,因为没人考虑过哪些状态需要保留。这些模式建立了转换层:设计师通过数据资源和 Inspector 配置表达意图,程序员提供遵循这些意图的系统。
模式 1:触发器与事件架构
设计意图:关卡设计师在编辑器中放置触发区域,以触发游戏事件——生成敌人、播放对话、打开门。触发器是通用、可复用的组件;响应则通过 ScriptableObject 事件通道在 Inspector 中连接。
错误做法:
// Each trigger is a bespoke script -- 47 of these in the project
public class Door3Trigger : MonoBehaviour
{
public GameObject door;
void OnTriggerEnter(Collider other)
{
if (other.CompareTag("Player"))
door.GetComponent<Animator>().SetTrigger("Open");
}
}
每个触发器都是独特的 MonoBehaviour。无法复用,也没有设计师控制能力,更无法在不修改代码的情况下禁用或重新配置。
正确做法:使用通用的 TriggerZone 组件,并配置激活规则和 SO 事件通道输出。设计师在 Inspector 中连接事件——无需为每个触发器编写代码。
using UnityEngine;
/// <summary>
/// Generic trigger volume that raises a GameEvent SO channel on activation.
/// Configurable activation count, tag filter, cooldown, and delay.
/// </summary>
[RequireComponent(typeof(Collider), typeof(Rigidbody))]
public class TriggerZone : MonoBehaviour
{
public enum ActivationMode { Once, Repeating, NTimes }
[Header("Activation Rules")]
[SerializeField] ActivationMode _mode = ActivationMode.Once;
[SerializeField] int _maxActivations = 1;
[SerializeField] float _cooldownSeconds;
[SerializeField] float _activationDelay;
[SerializeField] string _requiredTag = "Player";
[SerializeField] LayerMask _requiredLayer = ~0;
[Header("Event Output")]
[SerializeField] GameEvent _onActivated;
int _activationCount;
float _lastActivationTime = float.NegativeInfinity;
// See unity-game-architecture for GameEvent SO channel pattern
void Reset()
{
// Guarantee Rigidbody is kinematic and collider is trigger
var rb = GetComponent<Rigidbody>();
rb.isKinematic = true;
GetComponent<Collider>().isTrigger = true;
}
void OnTriggerEnter(Collider other)
{
if (!PassesFilter(other)) return;
if (!CanActivate()) return;
_ = ActivateAsync();
}
bool PassesFilter(Collider other)
{
if (!string.IsNullOrEmpty(_requiredTag) && !other.CompareTag(_requiredTag))
return false;
if ((_requiredLayer & (1 << other.gameObject.layer)) == 0)
return false;
return true;
}
bool CanActivate()
{
if (_mode == ActivationMode.Once && _activationCount >= 1) return false;
if (_mode == ActivationMode.NTimes && _activationCount >= _maxActivations) return false;
if (Time.time - _lastActivationTime < _cooldownSeconds) return false;
return true;
}
async Awaitable ActivateAsync()
{
if (_activationDelay > 0f)
await Awaitable.WaitForSecondsAsync(_activationDelay, destroyCancellationToken);
_activationCount++;
_lastActivationTime = Time.time;
_onActivated?.Raise();
}
}
脚手架:
TriggerZoneMonoBehaviour(见上文)——通用触发器组件GameEventScriptableObject 通道——参见unity-game-architectureTriggerConfig字段——激活模式、次数、冷却时间、延迟时间、标签/层过滤器
设计挂钩:设计师在场景中放置触发区域,在 Inspector 中分配 GameEvent SO,并配置激活规则。无需为每个触发器编写代码。新增触发器行为 = 新增 GameEvent 监听器,而不是编写新的触发器脚本。
注意事项:OnTriggerEnter 要求一方碰撞体的 isTrigger = true,且这对碰撞体中至少有一个 Rigidbody。TriggerZone 通过 [RequireComponent] 添加自身的运动学 Rigidbody,并在 Reset() 中进行设置,以确保无论进入对象是什么,都能正常工作。否则,当进入对象没有 Rigidbody 时,触发器会悄无声息地失效。
模式 2:遭遇战设计契约
设计意图:设计师将遭遇战(敌人波次、竞技场封锁、谜题)定义为数据资源,并通过预制件将其放置到关卡中。遭遇战定义与遭遇战实例彼此分离。
错误做法:
// Hardcoded encounter -- can't reuse, can't tune without code changes
public class ArenaEncounter : MonoBehaviour
{
public GameObject zombiePrefab;
public GameObject skeletonPrefab;
async Awaitable Start()
{
// Wave 1
Instantiate(zombiePrefab, transform.position, Quaternion.identity);
Instantiate(zombiePrefab, transform.position + Vector3.right * 3, Quaternion.identity);
await Awaitable.WaitForSecondsAsync(10f);
// Wave 2
Instantiate(skeletonPrefab, transform.position, Quaternion.identity);
}
}
每场遭遇战都是一个独特脚本。波次时间、敌人类型和生成位置全部硬编码。
正确做法:EncounterConfig SO 将波次定义为数据。EncounterController 预制件读取配置并管理生成。
using System;
using UnityEngine;
/// <summary>
/// Defines a single wave within an encounter: enemy types, counts, and timing.
/// </summary>
[Serializable]
public struct WaveDefinition
{
/// <summary>Enemy prefab to spawn in this wave.</summary>
public GameObject EnemyPrefab;
/// <summary>Number of enemies to spawn.</summary>
public int Count;
/// <summary>Seconds to wait before starting this wave (after previous completes).</summary>
public float DelayBeforeWave;
/// <summary>If true, all enemies must be defeated before next wave starts.</summary>
public bool WaitForClear;
}
/// <summary>
/// Data asset defining an entire encounter: sequence of waves and completion rules.
/// </summary>
[CreateAssetMenu(menuName = "Level Design/Encounter Config")]
public class EncounterConfig : ScriptableObject
{
/// <summary>Ordered list of waves in this encounter.</summary>
public WaveDefinition[] Waves;
/// <summary>Event raised when the encounter begins.</summary>
public GameEvent OnEncounterStart;
/// <summary>Event raised when all waves are cleared.</summary>
public GameEvent OnEncounterComplete;
}
using System.Collections.Generic;
using System.Threading;
using UnityEngine;
/// <summary>
/// Runs an encounter defined by an EncounterConfig SO.
/// Place in scene with spawn point transforms as children.
/// </summary>
public class EncounterController : MonoBehaviour
{
[SerializeField] EncounterConfig _config;
[SerializeField] Transform[] _spawnPoints;
[SerializeField] GameEvent _triggerEvent; // Listens for this to start
readonly List<GameObject> _activeEnemies = new();
// Full implementation in references/level-system-scaffolds.md
}
脚手架:
EncounterConfigSO——波次定义、完成事件WaveDefinition可序列化结构体——敌人预制件、数量、时间安排、清除条件EncounterControllerMonoBehaviour——异步推进波次、管理生成
设计挂钩:设计师为每场遭遇战创建 EncounterConfig SO,并在遭遇战位置放置 EncounterController 预制件。生成点是控制器的子 Transform。调整波次数量、时间安排和敌人类型无需修改代码。
注意事项:生成点必须在场景中按遭遇战单独定义(控制器上的 Transform 数组),而不能放在 SO 中。ScriptableObject 无法引用场景对象,因为它们是项目级资源。如果在 SO 中放入 Transform 引用,它们在运行时将为 null。
模式 3:检查点与重生
设计意图:玩家死亡后会在最近激活的检查点重生。检查点状态会在多次尝试间保留——门保持开启,钥匙保持已收集,敌人保持死亡。
错误示例:
// Full scene reload on death -- all progress lost
public class DeathHandler : MonoBehaviour
{
public void OnPlayerDied()
{
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
}
}
重新加载场景会重置所有内容。门会重新上锁,敌人会重生,谜题会重置。玩家必须重新完成整个关卡。
正确做法:CheckpointSystem 在激活时捕获一个 CheckpointSnapshot。玩家死亡时,恢复快照,而不重新加载场景。需要保存和恢复的对象需实现 ICheckpointable。
using UnityEngine;
/// <summary>
/// Implement on any object that needs to save/restore state at checkpoints.
/// </summary>
public interface ICheckpointable
{
/// <summary>Unique ID for this object (use a serialized GUID).</summary>
string CheckpointId { get; }
/// <summary>Capture current state as serializable data.</summary>
object CaptureState();
/// <summary>Restore state from previously captured data.</summary>
void RestoreState(object state);
}
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// Snapshot of all checkpointable state at a specific checkpoint.
/// </summary>
public class CheckpointSnapshot
{
/// <summary>Player position at checkpoint activation.</summary>
public Vector3 PlayerPosition;
/// <summary>Player rotation at checkpoint activation.</summary>
public Quaternion PlayerRotation;
/// <summary>Saved state per checkpointable object, keyed by CheckpointId.</summary>
public Dictionary<string, object> ObjectStates = new();
}
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
/// <summary>
/// Manages checkpoint save/restore. Singleton, lives in persistent scene.
/// </summary>
public class CheckpointSystem : MonoBehaviour
{
public static CheckpointSystem Instance { get; private set; }
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
static void ResetStatics() => Instance = null;
CheckpointSnapshot _currentSnapshot;
void Awake()
{
if (Instance != null && Instance != this) { Destroy(gameObject); return; }
Instance = this;
}
/// <summary>
/// Captures a checkpoint snapshot from the player and all ICheckpointable objects.
/// </summary>
public void SaveCheckpoint(Transform player)
{
_currentSnapshot = new CheckpointSnapshot
{
PlayerPosition = player.position,
PlayerRotation = player.rotation
};
foreach (var obj in FindCheckpointables())
_currentSnapshot.ObjectStates[obj.CheckpointId] = obj.CaptureState();
}
/// <summary>
/// Restores the last checkpoint snapshot. Returns false if no checkpoint exists.
/// </summary>
public bool RestoreCheckpoint(Transform player)
{
if (_currentSnapshot == null) return false;
player.position = _currentSnapshot.PlayerPosition;
player.rotation = _currentSnapshot.PlayerRotation;
foreach (var obj in FindCheckpointables())
{
if (_currentSnapshot.ObjectStates.TryGetValue(obj.CheckpointId, out var state))
obj.RestoreState(state);
}
return true;
}
IEnumerable<ICheckpointable> FindCheckpointables() =>
FindObjectsByType<MonoBehaviour>(FindObjectsSortMode.None).OfType<ICheckpointable>();
}
脚手架:
CheckpointSystem管理器(单例,持久场景)——协调保存与恢复CheckpointSnapshot——玩家变换信息 + 对象状态字典ICheckpointable接口——CaptureState()/RestoreState(object)CheckpointTrigger——扩展TriggerZone模式(模式 1),调用CheckpointSystem.SaveCheckpoint
设计扩展点:对象通过实现 ICheckpointable 来参与检查点的保存与恢复(门、开关、可破坏物)。设计师在场景中放置检查点触发区域,其余工作由系统处理。
注意事项:检查点必须保存所有可变状态。如果某扇门在两个检查点之间被打开,那么开启状态必须包含在快照中。检查每个游戏对象是否实现了 ICheckpointable——只要有一个对象漏掉实现,该对象就会在死亡时重置,而其他对象都会恢复,从而导致状态不可能出现。另请注意:FindObjectsByType 开销很大——如果检查点触发频繁,请缓存结果。
模式 4:过场动画与脚本化序列
设计意图:将游戏内过场动画、镜头移动、NPC 对话和定时事件制作成可编排的序列,让设计师无需编写代码,就能在 Inspector 中进行组合。
错误示例:
// Coroutine chain -- fragile, not reusable, not skippable
IEnumerator PlayCutscene()
{
camera.transform.position = new Vector3(10, 5, 0);
yield return new WaitForSeconds(2f);
npc.GetComponent<Animator>().SetTrigger("Talk");
dialogueUI.Show("Welcome, hero!");
yield return new WaitForSeconds(3f);
door.SetActive(false);
}
位置硬编码、无法取消、不能跳过,并使用了已弃用的协程模式。
正确做法:使用 ISequenceStep 接口及异步方法 ExecuteAsync(CancellationToken)。SequencePlayer 按顺序运行各个步骤。通过 [SerializeReference] 在 Inspector 中组合步骤。
using System.Threading;
/// <summary>
/// A single step in a scripted sequence. Implement for each step type.
/// </summary>
public interface ISequenceStep
{
/// <summary>
/// Execute this step. Must respect cancellation for skip support.
/// When cancelled, the step should immediately apply its end state.
/// </summary>
Awaitable ExecuteAsync(CancellationToken ct);
}
using System;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;
/// <summary>
/// Plays a sequence of ISequenceStep instances in order.
/// Steps are composed via [SerializeReference] in the Inspector.
/// </summary>
public class SequencePlayer : MonoBehaviour
{
[SerializeReference] List<ISequenceStep> _steps = new();
[SerializeField] bool _playOnStart;
[SerializeField] GameEvent _onSequenceComplete;
bool _isPlaying;
/// <summary>Play the full sequence. Cancellation skips remaining steps.</summary>
public async Awaitable PlayAsync(CancellationToken ct = default)
{
if (_isPlaying) return;
_isPlaying = true;
using var linked = CancellationTokenSource.CreateLinkedTokenSource(
ct, destroyCancellationToken);
foreach (var step in _steps)
{
if (linked.Token.IsCancellationRequested) break;
try
{
await step.ExecuteAsync(linked.Token);
}
catch (OperationCanceledException)
{
break;
}
}
_isPlaying = false;
_onSequenceComplete?.Raise();
}
async void Start()
{
if (_playOnStart)
await PlayAsync(destroyCancellationToken);
}
}
步骤实现示例:
using System;
using System.Threading;
using UnityEngine;
/// <summary>Wait for a specified duration. Skippable via cancellation.</summary>
[Serializable]
public class WaitStep : ISequenceStep
{
[SerializeField] float _duration = 1f;
public async Awaitable ExecuteAsync(CancellationToken ct)
{
await Awaitable.WaitForSecondsAsync(_duration, ct);
}
}
/// <summary>Enable or disable a GameObject.</summary>
[Serializable]
public class SetActiveStep : ISequenceStep
{
[SerializeField] GameObject _target;
[SerializeField] bool _active = true;
public Awaitable ExecuteAsync(CancellationToken ct)
{
if (_target != null) _target.SetActive(_active);
return Awaitable.NextFrameAsync(ct);
}
}
脚手架:
ISequenceStep接口——ExecuteAsync(CancellationToken)SequencePlayerMonoBehaviour——按顺序运行[SerializeReference]步骤列表- 步骤类型:
CameraMoveStep、DialogueStep、WaitStep、SetActiveStep
设计扩展点:新增序列步骤 = 实现 ISequenceStep。设计师可以在 Inspector 中通过向 [SerializeReference] 列表添加、移除或重新排序步骤来组合序列。创建新的过场动画无需修改代码。
注意事项:[SerializeReference] 要求具体类型与接口位于同一程序集,或位于引用接口所在程序集的程序集中。Unity 默认的 Inspector 不会为 [SerializeReference] 提供友好的 UI——你需要自定义 PropertyDrawer,或使用 Odin Inspector / SerializeReferenceDropdown 等包。步骤必须妥善处理取消操作:取消(跳过)时,应立即应用最终状态(例如,将镜头直接切换到最终位置),而不是让步骤停在未完成的状态。
模式 5:环境叙事钩子
设计意图:物体会对玩家的靠近或互动做出反应——可阅读的便笺、可播放的音频日志、可触发的环境动画。所有可交互物体都采用一致的 UX 模式。
错误示例:
// Each interactive object is a unique script with bespoke logic
public class Note47 : MonoBehaviour
{
void OnTriggerEnter(Collider other)
{
if (other.CompareTag("Player") && Input.GetKeyDown(KeyCode.E))
UIManager.Instance.ShowNote("Note 47 text...");
}
}
没有通用的交互 UX。每个物体都以不同方式检测输入。提示显示不一致。新增交互类型时都需要从头编写脚本。
正确示例:使用带有 IInteraction 接口的 Interactable 组件。玩家身上的通用 InteractionDetector 负责处理距离检测和输入提示。
using UnityEngine;
/// <summary>
/// Interface for interaction behaviors. Implement per interaction type.
/// </summary>
public interface IInteraction
{
/// <summary>Text shown on the interaction prompt (e.g., "Read Note").</summary>
string PromptText { get; }
/// <summary>Execute the interaction.</summary>
void Execute(GameObject interactor);
}
using UnityEngine;
/// <summary>
/// Marks a GameObject as interactable. Holds an IInteraction and detection range.
/// </summary>
public class Interactable : MonoBehaviour
{
[SerializeField] float _interactionRange = 2f;
[SerializeReference] IInteraction _interaction;
/// <summary>Maximum distance for interaction.</summary>
public float InteractionRange => _interactionRange;
/// <summary>The interaction behavior attached to this object.</summary>
public IInteraction Interaction => _interaction;
}
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
/// <summary>
/// Player-side component. Detects nearby Interactables and handles input prompting.
/// Cross-ref unity-input-correctness for input handling.
/// </summary>
public class InteractionDetector : MonoBehaviour
{
[SerializeField] float _detectionRadius = 5f;
[SerializeField] LayerMask _interactableLayer;
Interactable _currentTarget;
readonly Collider[] _overlapBuffer = new Collider[16];
void Update()
{
_currentTarget = FindClosestInteractable();
if (_currentTarget != null)
{
ShowPrompt(_currentTarget.Interaction.PromptText);
// Input check -- see unity-input-correctness for Input System usage
}
else
{
HidePrompt();
}
}
Interactable FindClosestInteractable()
{
int count = Physics.OverlapSphereNonAlloc(
transform.position, _detectionRadius, _overlapBuffer, _interactableLayer);
Interactable closest = null;
float closestDist = float.MaxValue;
for (int i = 0; i < count; i++)
{
if (!_overlapBuffer[i].TryGetComponent<Interactable>(out var interactable))
continue;
float dist = Vector3.Distance(transform.position, interactable.transform.position);
if (dist > interactable.InteractionRange) continue;
if (dist < closestDist)
{
closest = interactable;
closestDist = dist;
}
}
return closest;
}
void ShowPrompt(string text) { /* UI Toolkit prompt -- see implementation in scaffolds */ }
void HidePrompt() { /* Hide prompt UI */ }
}
脚手架:
InteractableMonoBehaviour——通过[SerializeReference]持有IInteraction,并设置检测范围IInteraction接口——包含PromptText、Execute(GameObject interactor)- 玩家身上的
InteractionDetector——负责距离检测、提示显示和输入处理 - 示例:
ReadNoteInteraction实现IInteraction
设计钩子:新增交互类型 = 实现 IInteraction。设计师可在预制件上配置提示文本、范围和交互类型。所有可交互物体的 UX 保持一致。
注意事项:交互提示必须使用世界坐标到屏幕坐标的转换(Camera.main.WorldToScreenPoint),并处理屏幕外钳制。使用 Physics.OverlapSphereNonAlloc 和预分配的缓冲区——不要每帧都分配内存。Camera.main 底层会调用 FindObjectWithTag;请缓存相机引用。
模式 6:关卡流式加载与加载接缝
设计意图:玩家移动时,大型关卡会加载/卸载各个区域——开放世界、长走廊、由相连区域构成的枢纽。玩家在游戏过程中不会看到加载画面。
错误示例:
// Entire world in one scene
// 500 MB memory usage, 45-second load time, untestable
所有内容都在同一个场景中。美术人员无法并行工作(会产生合并冲突)。内存使用量没有上限。初始加载时间会不断增长。
正确示例:使用 StreamingVolume 触发区域,通过 Addressables 增量加载/卸载场景区块(交叉参考 unity-scene-assets)。根据玩家距离预加载。使用滞后机制防止频繁切换。
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.ResourceProviders;
/// <summary>
/// Configuration for a streamable level chunk.
/// </summary>
[CreateAssetMenu(menuName = "Level Design/Level Chunk Config")]
public class LevelChunkConfig : ScriptableObject
{
/// <summary>Addressable scene asset reference for this chunk.</summary>
public AssetReference SceneReference;
/// <summary>Distance at which to begin preloading this chunk.</summary>
public float PreloadDistance = 50f;
/// <summary>Distance at which to unload this chunk (must be > PreloadDistance).</summary>
public float UnloadDistance = 80f;
}
using System.Threading;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.ResourceManagement.ResourceProviders;
using UnityEngine.SceneManagement;
/// <summary>
/// Manages loading/unloading a level chunk based on player proximity.
/// Place at the seam between level sections.
/// Cross-ref unity-scene-assets for Addressable scene loading patterns.
/// </summary>
public class StreamingVolume : MonoBehaviour
{
[SerializeField] LevelChunkConfig _chunkConfig;
[SerializeField] Transform _playerTransform;
AsyncOperationHandle<SceneInstance> _loadHandle;
bool _isLoaded;
bool _isLoading;
void Update()
{
if (_playerTransform == null) return;
float distance = Vector3.Distance(transform.position, _playerTransform.position);
if (!_isLoaded && !_isLoading && distance < _chunkConfig.PreloadDistance)
_ = LoadChunkAsync(destroyCancellationToken);
else if (_isLoaded && distance > _chunkConfig.UnloadDistance)
_ = UnloadChunkAsync();
}
async Awaitable LoadChunkAsync(CancellationToken ct)
{
_isLoading = true;
_loadHandle = Addressables.LoadSceneAsync(
_chunkConfig.SceneReference, LoadSceneMode.Additive);
while (!_loadHandle.IsDone)
{
ct.ThrowIfCancellationRequested();
await Awaitable.NextFrameAsync(ct);
}
_isLoaded = true;
_isLoading = false;
}
async Awaitable UnloadChunkAsync()
{
if (!_loadHandle.IsValid()) return;
var unload = Addressables.UnloadSceneAsync(_loadHandle);
while (!unload.IsDone)
await Awaitable.NextFrameAsync(destroyCancellationToken);
_isLoaded = false;
}
void OnDestroy()
{
if (_isLoaded && _loadHandle.IsValid())
Addressables.UnloadSceneAsync(_loadHandle);
}
}
脚手架:
StreamingVolumeMonoBehaviour——根据距离加载/卸载,并采用滞后机制LevelChunkConfigSO——场景引用、预加载距离、卸载距离- 异步加载/卸载 Addressable 场景(交叉参考
unity-scene-assets)
设计钩子:设计师在关卡接缝处放置流式加载区域,并分配区块配置。区块配置定义要加载哪个 Addressable 场景以及加载距离。预加载距离 < 卸载距离可实现滞后机制。
注意事项:相邻区块必须有一小段重叠区域。接缝处的物体(例如连接两个区域的桥梁)必须同时存在于两个区块中,或放在永不卸载的持久场景中。卸载距离必须严格大于预加载距离(滞后缓冲区),否则系统会每帧都在加载和卸载之间反复切换。使用 Profiler.GetTotalAllocatedMemoryLong() 测试,确认卸载时区块确实释放了内存。
反模式总结
| 反模式 | 问题 | 模式修复方案 |
|---|---|---|
| 定制触发器脚本 | 无法复用,设计师无法控制 | 模式 1:通用 TriggerZone + SO 事件 |
| 硬编码生成序列 | 无法在不修改代码的情况下调整 | 模式 2:EncounterConfig SO + EncounterController |
| 死亡时重新加载场景 | 所有进度都会丢失 | 模式 3:CheckpointSystem + ICheckpointable |
| 协程过场动画链 | 无法跳过,也无法组合 | 模式 4:ISequenceStep + SequencePlayer |
| 一次性可交互物体 | UX 不一致,无法复用 | 模式 5:Interactable + IInteraction |
| 单体式单场景 | 内存、性能和协作方面的问题 | 模式 6:StreamingVolume + Addressables |
相关技能
unity-game-architecture— GameEvent SO 通道模式、引导场景、基于 SO 的配置unity-scene-assets— Addressable 资源加载、叠加场景管理unity-async-patterns— Awaitable 用法、取消令牌、异步生命周期unity-input-correctness— 用于交互提示的 Input System 集成unity-save-system— 持久化存档数据(检查点快照是临时的;存档系统是永久的)unity-physics— 触发碰撞体设置、基于图层的筛选、Rigidbody 要求
其他资源
- Unity 手册:场景加载
- Unity 手册:Addressables
- Unity 手册:SerializeReference
- Unity 手册:ScriptableObject