关于 unity-game-loop
面向 Unity 游戏开发者的游戏循环与进程设计技能,将设计意图转化为可扩展的代码结构。内容涵盖阶段、会话生命周期、胜负条件、局外成长、难度和节奏调控。
主要功能
- 01按序运行的游戏阶段
- 02会话状态与生命周期管理
- 03可组合的胜负条件
- 04分离会话与局外进度
- 05可调难度曲线
- 06遭遇节奏与强度曲线
适用场景
- 01为探索和制作等环节搭建主循环
- 02支持无需重载场景的会话重开
- 03按关卡配置胜负规则
- 04让设计师调整难度和遭遇节奏
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ nice-wolf-studio/ unity-claude-skills --skill unity-game-loop安装后在 Claude Code 中输入/unity-game-loop确认已加载。
附带文件references/game-loop-scaffolds.md
详细介绍
游戏循环与进度系统——设计转化模式
前置技能:
unity-state-machines(会话 FSM)、unity-game-architecture(事件、服务定位器、引导流程)、unity-data-driven(用于难度/节奏的 SO 配置)
Claude 最常见的游戏玩法失败模式,是孤立地构建功能,却没有统一的循环结构。它会创建移动、物品栏、战斗和制作系统,这些系统各自都能正常工作,却无法组成一个连贯的游戏。结果就是一个可玩的沙盒,没有进度系统、没有会话边界、没有胜负条件,也没有供设计师调整节奏或难度的接口。这些模式会将设计意图(“玩家探索、收集、制作,然后熬过夜晚”)转化为代码架构,确保阶段按顺序推进、跟踪会话状态,并为设计师提供可调整的参数。
模式:核心循环脚手架
设计意图:设计师将游戏描述为“探索、收集、制作、熬过夜晚”——需要一个代码骨架,确保这一循环按顺序执行各阶段,并支持扩展。
错误做法(Claude 默认):
// Systems built independently with no phase concept -- everything runs simultaneously.
// ExplorationManager, InventoryManager, CraftingManager, CombatManager all live in the scene
// and run their Update loops at the same time. There is no concept of "it is now the crafting
// phase" or "exploration has ended." The game is a flat sandbox.
public class GameManager : MonoBehaviour
{
[SerializeField] private ExplorationManager _exploration;
[SerializeField] private InventoryManager _inventory;
[SerializeField] private CombatManager _combat;
private void Start()
{
_exploration.Enable();
_inventory.Enable();
_combat.Enable(); // all systems active from frame 1
}
}
正确做法:
// GameLoopController manages phase transitions via IGamePhase interface.
// Each phase gets Enter/Tick/Exit. Phases are configured as SO assets.
// Only one phase is active at a time, enforcing the designer's intended cycle.
public interface IGamePhase
{
/// <summary>Name shown in debug UI and logs.</summary>
string PhaseName { get; }
/// <summary>Called when this phase becomes active. Load assets, enable systems.</summary>
Awaitable EnterAsync(CancellationToken ct);
/// <summary>Called every frame while this phase is active.</summary>
void Tick(float deltaTime);
/// <summary>Called when transitioning away. Cleanup, disable systems.</summary>
Awaitable ExitAsync(CancellationToken ct);
}
[CreateAssetMenu(menuName = "Game Loop/Phase Config")]
public class GamePhaseConfig : ScriptableObject
{
[Tooltip("Display name for this phase")]
public string phaseName;
[Tooltip("Assembly-qualified type name of the IGamePhase implementation")]
public string phaseTypeName;
[Tooltip("Duration in seconds, 0 = phase decides when to end")]
public float maxDuration;
}
脚手架(完整实现):
using System;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;
/// <summary>
/// Drives the core game loop through a sequence of phases.
/// Phases are defined by <see cref="GamePhaseConfig"/> assets and cycle in order.
/// </summary>
public class GameLoopController : MonoBehaviour
{
[SerializeField] private List<GamePhaseConfig> _phaseConfigs;
private readonly List<IGamePhase> _phases = new();
private int _currentIndex = -1;
private IGamePhase _currentPhase;
private bool _transitioning;
/// <summary>Fires when a new phase begins. Arg: phase index.</summary>
public event Action<int> OnPhaseStarted;
/// <summary>Fires when a phase ends. Arg: phase index.</summary>
public event Action<int> OnPhaseEnded;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
static void ResetStatics() { }
private async void Start()
{
foreach (var config in _phaseConfigs)
{
var type = Type.GetType(config.phaseTypeName);
if (type == null)
{
Debug.LogError($"Phase type not found: {config.phaseTypeName}");
continue;
}
_phases.Add((IGamePhase)Activator.CreateInstance(type));
}
if (_phases.Count > 0)
await TransitionToPhaseAsync(0);
}
private void Update()
{
if (_currentPhase != null && !_transitioning)
_currentPhase.Tick(Time.deltaTime);
}
/// <summary>Advance to the next phase. Wraps to index 0 after the last phase.</summary>
public async Awaitable AdvancePhaseAsync()
{
int next = (_currentIndex + 1) % _phases.Count;
await TransitionToPhaseAsync(next);
}
/// <summary>Jump to a specific phase by index.</summary>
public async Awaitable TransitionToPhaseAsync(int index)
{
if (_transitioning) return;
_transitioning = true;
var ct = destroyCancellationToken;
if (_currentPhase != null)
{
await _currentPhase.ExitAsync(ct);
OnPhaseEnded?.Invoke(_currentIndex);
}
_currentIndex = index;
_currentPhase = _phases[_currentIndex];
await _currentPhase.EnterAsync(ct);
OnPhaseStarted?.Invoke(_currentIndex);
_transitioning = false;
}
}
设计接口:添加新阶段只需创建新的 IGamePhase 类和一个 GamePhaseConfig SO 资源,无需修改 GameLoopController。设计师可以在 Inspector 中调整 SO 列表的顺序或添加阶段。
注意事项:阶段转换必须是异步的(加载资源、淡入淡出屏幕、启用/禁用系统)。使用即时切换会导致帧卡顿和竞态条件,因为系统可能会在转换过程中检查自身的启用状态。始终使用 Awaitable,绝不要使用同步调用。
模式:会话生命周期
设计意图:为游戏会话设定明确的开始/结束边界——例如 Roguelike 一轮、多人比赛或谜题关卡。玩家无需完整重新加载场景即可重新开始。
错误做法(Claude 默认):
// Gameplay starts in Awake/Start with no session concept.
// "Restarting" means reloading the entire scene, losing any cross-session state.
// Score, timer, and run-specific data are scattered across multiple MonoBehaviours.
public class GameManager : MonoBehaviour
{
public int score;
public float timer;
private void Start()
{
score = 0;
timer = 0;
}
public void Restart()
{
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex); // nuclear option
}
}
正确做法:
// SessionManager owns all session-scoped state. Explicit StartSession/EndSession
// with events. Systems subscribe to session events to init/cleanup. Restart
// without scene reload by ending then starting a new session.
using System;
using System.Threading;
using UnityEngine;
/// <summary>
/// Manages the lifecycle of a single play session (run, match, level attempt).
/// Separates session state from persistent meta progression.
/// </summary>
public class SessionManager : MonoBehaviour
{
public static SessionManager Instance { get; private set; }
[SerializeField] private SessionConfig _defaultConfig;
/// <summary>Current session data. Null when no session is active.</summary>
public SessionData CurrentSession { get; private set; }
/// <summary>True while a session is in progress.</summary>
public bool IsSessionActive => CurrentSession != null;
public event Action<SessionConfig> OnSessionStarted;
public event Action<SessionResult> OnSessionEnded;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
static void ResetStatics() => Instance = null;
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
}
/// <summary>Begin a new session with the given configuration.</summary>
public void StartSession(SessionConfig config = null)
{
config ??= _defaultConfig;
CurrentSession = new SessionData(config);
OnSessionStarted?.Invoke(config);
}
/// <summary>End the current session and publish the result.</summary>
public SessionResult EndSession(bool victory)
{
if (CurrentSession == null) return default;
var result = new SessionResult
{
Config = CurrentSession.Config,
Victory = victory,
Score = CurrentSession.Score,
ElapsedTime = CurrentSession.ElapsedTime,
EndTime = DateTime.UtcNow
};
OnSessionEnded?.Invoke(result);
CurrentSession = null;
return result;
}
/// <summary>Restart the session without reloading the scene.</summary>
public void RestartSession()
{
var config = CurrentSession?.Config ?? _defaultConfig;
EndSession(false);
StartSession(config);
}
}
脚手架(完整实现):请参阅 references/game-loop-scaffolds.md,了解 SessionConfig、SessionData、SessionResult 和完整的集成示例。
设计接口:SessionConfig SO 定义初始条件(初始生命值、时间限制、初始物品栏)。游戏玩法系统订阅 OnSessionStarted/OnSessionEnded,以初始化和清理各自的会话状态,而无需与管理器耦合。
注意事项:会话状态(分数、计时器、本轮物品栏)必须与持久状态(元进度、解锁内容、货币)严格分离。将两者混用会导致“重新开始后未重置”问题,进而出现元数据值被清除或会话值在不同轮次之间残留的情况。
模式:胜负条件架构
设计意图:设计师可以快速调整每个关卡的胜负条件——“消灭所有敌人”“存活 3 分钟”“抵达出口”——而无需每次更改都由程序员介入。
错误做法(Claude 默认):
// Hardcoded compound condition buried in a manager.
// Adding a new condition means editing this class. Cannot vary per level.
public class LevelManager : MonoBehaviour
{
private void Update()
{
if (enemies.Count == 0 && objectiveCaptured && timer > 30f)
{
WinGame(); // hardcoded, untestable, not reusable
}
if (playerHealth <= 0)
{
LoseGame();
}
}
}
正确做法:
// IWinCondition/ILoseCondition interfaces evaluated by a ConditionEvaluator.
// Conditions are SO assets dragged onto the level's evaluator in the Inspector.
// Composable AND/OR groups. Event-driven evaluation, not per-frame polling.
using UnityEngine;
/// <summary>
/// A single win condition that can be evaluated at any time.
/// Implement as a ScriptableObject for Inspector assignment.
/// </summary>
public abstract class WinConditionBase : ScriptableObject
{
/// <summary>Human-readable description for the Inspector.</summary>
public abstract string Description { get; }
/// <summary>Initialize this condition for a new session/level.</summary>
public abstract void Initialize();
/// <summary>Returns true when the condition is satisfied.</summary>
public abstract bool IsMet();
/// <summary>Cleanup when the session/level ends.</summary>
public abstract void Teardown();
}
/// <summary>
/// A single lose condition.
/// </summary>
public abstract class LoseConditionBase : ScriptableObject
{
/// <summary>Human-readable description for the Inspector.</summary>
public abstract string Description { get; }
public abstract void Initialize();
public abstract bool IsMet();
public abstract void Teardown();
}
脚手架(完整实现):
using System;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// Evaluates a set of win and lose conditions for the current level.
/// Supports AND (all must be true) and OR (any must be true) composition.
/// </summary>
public class ConditionEvaluator : MonoBehaviour
{
public enum CompositionMode { AllRequired, AnyRequired }
[SerializeField] private CompositionMode _winMode = CompositionMode.AllRequired;
[SerializeField] private List<WinConditionBase> _winConditions;
[SerializeField] private CompositionMode _loseMode = CompositionMode.AnyRequired;
[SerializeField] private List<LoseConditionBase> _loseConditions;
private bool _resolved;
/// <summary>Fires when win conditions are satisfied.</summary>
public event Action OnWin;
/// <summary>Fires when lose conditions are satisfied.</summary>
public event Action OnLose;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
static void ResetStatics() { }
private void OnEnable()
{
_resolved = false;
foreach (var c in _winConditions) c.Initialize();
foreach (var c in _loseConditions) c.Initialize();
}
private void OnDisable()
{
foreach (var c in _winConditions) c.Teardown();
foreach (var c in _loseConditions) c.Teardown();
}
/// <summary>
/// Call this from gameplay events (enemy killed, timer tick, objective captured)
/// instead of polling every frame.
/// </summary>
public void Evaluate()
{
if (_resolved) return;
if (CheckLose())
{
_resolved = true;
OnLose?.Invoke();
return;
}
if (CheckWin())
{
_resolved = true;
OnWin?.Invoke();
}
}
private bool CheckWin()
{
if (_winConditions.Count == 0) return false;
return _winMode == CompositionMode.AllRequired
? _winConditions.TrueForAll(c => c.IsMet())
: _winConditions.Exists(c => c.IsMet());
}
private bool CheckLose()
{
if (_loseConditions.Count == 0) return false;
return _loseMode == CompositionMode.AnyRequired
? _loseConditions.Exists(c => c.IsMet())
: _loseConditions.TrueForAll(c => c.IsMet());
}
}
设计接口:设计师可以在 Inspector 中将条件 SO 拖到关卡的 ConditionEvaluator 上。程序员可以通过继承 WinConditionBase 或 LoseConditionBase 来添加新的条件类型,无需修改评估器代码。
注意事项:条件必须处理后加入的玩家(多人游戏)和关卡中途的规则变更。应在游戏事件发生时评估条件(敌人被击杀、目标被占领),而不是只在某个固定时刻检查。订阅游戏事件以调用 ConditionEvaluator.Evaluate()。
模式:元循环钩子
设计意图:在两次游戏会话之间,持续积累进度——货币、解锁项、升级、账号 XP。这个元系统赋予单次游戏以意义,并推动玩家长期留存。
错误做法(Claude 默认):
// Meta progression mixed directly into session code.
// XP increments happen inside the combat script. Unlock checks are in the UI.
// Restarting a session wipes currency. Nothing persists.
public class EnemyHealth : MonoBehaviour
{
public static int totalXP; // static, no reset safety, no persistence
public void TakeDamage(int dmg)
{
hp -= dmg;
if (hp <= 0)
{
totalXP += 50; // meta progression buried in combat code
Destroy(gameObject);
}
}
}
正确做法:
// MetaProgressionService is a plain C# class registered via Service Locator.
// It persists between sessions. It listens for SessionResult events and
// dispatches IMetaReward SO assets. Save/load is delegated to unity-save-system.
using System;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// A reward granted after a session ends. Implement as ScriptableObject.
/// </summary>
public abstract class MetaRewardBase : ScriptableObject
{
/// <summary>Human-readable description.</summary>
public abstract string Description { get; }
/// <summary>Apply this reward to the meta progression state.</summary>
public abstract void Apply(MetaProgressionState state);
}
/// <summary>
/// Persistent player progression state that survives between sessions.
/// </summary>
[Serializable]
public class MetaProgressionState
{
public int Currency;
public int AccountXP;
public int AccountLevel;
public List<string> UnlockedItemIds = new();
}
/// <summary>
/// Service that manages meta progression. Registered via Service Locator,
/// not a MonoBehaviour -- survives scene transitions naturally.
/// </summary>
public class MetaProgressionService
{
private MetaProgressionState _state;
/// <summary>Fires after rewards are applied.</summary>
public event Action<MetaProgressionState> OnProgressionUpdated;
public MetaProgressionState State => _state;
public MetaProgressionService(MetaProgressionState loadedState = null)
{
_state = loadedState ?? new MetaProgressionState();
}
/// <summary>Process end-of-session rewards.</summary>
public void ProcessSessionResult(SessionResult result, List<MetaRewardBase> rewards)
{
foreach (var reward in rewards)
{
reward.Apply(_state);
}
OnProgressionUpdated?.Invoke(_state);
}
/// <summary>Check whether an item is unlocked.</summary>
public bool IsUnlocked(string itemId) => _state.UnlockedItemIds.Contains(itemId);
}
脚手架(完整实现):有关 CurrencyReward、XPReward 示例及其与 SessionManager 的集成,请参阅 references/game-loop-scaffolds.md。
设计钩子:新增奖励类型只需创建一个新的 MetaRewardBase 子类 SO。设计师通过将奖励 SO 分配给关卡/会话配置,来设置每种会话结果对应的奖励。无需更改服务代码。
注意事项:元状态必须在场景切换后保留。应使用在 Service Locator(来自 unity-game-architecture)中注册的普通 C# 类,而不是绑定场景的 MonoBehaviour。如果使用 MonoBehaviour,则必须使用 DontDestroyOnLoad,并通过 SubsystemRegistration 确保域重载安全。
模式:可调难度系统
设计意图:设计师需要针对每个关卡或根据玩家表现动态调整敌人数量、生成速率、伤害倍率和资源稀缺程度,而无需修改代码。
错误做法(Claude 默认):
// Magic numbers scattered across multiple scripts.
// Changing difficulty means finding and editing every script.
// No way for designers to tune without a programmer.
public class EnemySpawner : MonoBehaviour
{
private float spawnRate = 2f; // magic number
private int maxEnemies = 10; // magic number
private float enemyHealthMult = 1f; // magic number
private void Update()
{
// hardcoded scaling that nobody can tune
if (Time.timeSinceLevelLoad > 60f)
spawnRate = 1f;
if (Time.timeSinceLevelLoad > 120f)
spawnRate = 0.5f;
}
}
正确做法:
// DifficultyConfig SO with AnimationCurve fields for visual tuning.
// DifficultyProvider service that systems query for current values.
// Supports static (per-level SO) and dynamic (adaptive) modes.
using System;
using UnityEngine;
/// <summary>
/// Defines difficulty parameters as curves over normalized progression (0-1).
/// 0 = level start (or lowest skill), 1 = level end (or highest skill).
/// </summary>
[CreateAssetMenu(menuName = "Game Loop/Difficulty Config")]
public class DifficultyConfig : ScriptableObject
{
[Header("Enemy Scaling")]
[Tooltip("Max concurrent enemies over normalized time (0=start, 1=end)")]
public AnimationCurve maxEnemiesCurve = AnimationCurve.Linear(0, 5, 1, 20);
[Tooltip("Seconds between spawns over normalized time")]
public AnimationCurve spawnIntervalCurve = AnimationCurve.Linear(0, 3f, 1, 0.5f);
[Header("Damage Scaling")]
[Tooltip("Enemy damage multiplier over normalized time")]
public AnimationCurve enemyDamageMultCurve = AnimationCurve.Linear(0, 1f, 1, 2.5f);
[Tooltip("Player damage multiplier over normalized time")]
public AnimationCurve playerDamageMultCurve = AnimationCurve.Constant(0, 1, 1f);
[Header("Resource Scaling")]
[Tooltip("Resource drop rate multiplier over normalized time")]
public AnimationCurve resourceDropRateCurve = AnimationCurve.Linear(0, 1.5f, 1, 0.5f);
}
/// <summary>
/// Provides current difficulty values by evaluating curves at the current progression point.
/// Register via Service Locator. Systems query this instead of using magic numbers.
/// </summary>
public class DifficultyProvider
{
private DifficultyConfig _config;
private float _normalizedProgress;
public DifficultyProvider(DifficultyConfig config)
{
_config = config ?? throw new ArgumentNullException(nameof(config));
}
/// <summary>Set the current progression point (0-1).</summary>
public void SetProgress(float normalized)
{
_normalizedProgress = Mathf.Clamp01(normalized);
}
/// <summary>Current max enemies allowed.</summary>
public int MaxEnemies => Mathf.RoundToInt(_config.maxEnemiesCurve.Evaluate(_normalizedProgress));
/// <summary>Current spawn interval in seconds.</summary>
public float SpawnInterval => _config.spawnIntervalCurve.Evaluate(_normalizedProgress);
/// <summary>Current enemy damage multiplier.</summary>
public float EnemyDamageMult => _config.enemyDamageMultCurve.Evaluate(_normalizedProgress);
/// <summary>Current player damage multiplier.</summary>
public float PlayerDamageMult => _config.playerDamageMultCurve.Evaluate(_normalizedProgress);
/// <summary>Current resource drop rate multiplier.</summary>
public float ResourceDropRate => _config.resourceDropRateCurve.Evaluate(_normalizedProgress);
/// <summary>Swap the active config (e.g., for a new level or difficulty setting).</summary>
public void SetConfig(DifficultyConfig config) => _config = config;
}
脚手架(完整实现):有关与生成器和战斗系统的完整集成,请参阅 references/game-loop-scaffolds.md。
设计钩子:AnimationCurve 字段让设计师可以在 Inspector 中直观地调整难度曲线。新增难度参数只需在 SO 上添加一个曲线字段,并在提供程序上添加对应属性,无需更改使用方代码。
注意事项:AnimationCurve.Evaluate() 使用归一化时间(0-1)。必须明确说明 0 和 1 分别代表什么:关卡开始/结束、玩家死亡次数范围,还是会话已用时间比例。这里含糊不清会导致难度曲线体验不佳,且难以调试。
模式:节奏与遭遇时机
设计意图:游戏有自己的节奏——紧张/缓和循环、逐渐升级的强度、遭遇之间的休整时间。按固定间隔或随机生成敌人,会让游戏体验平淡、单调。
错误做法(Claude 默认):
// Fixed-interval spawning with no concept of intensity or pacing.
// Every 5 seconds, an enemy appears. No tension curve, no rest periods,
// no escalation. The experience is flat from start to finish.
public class EnemySpawner : MonoBehaviour
{
[SerializeField] private float _spawnInterval = 5f;
private float _timer;
private void Update()
{
_timer += Time.deltaTime;
if (_timer >= _spawnInterval)
{
SpawnEnemy();
_timer = 0;
}
}
}
正确做法:
// PacingDirector tracks a designer-authored intensity curve over session time.
// Triggers encounter events when intensity thresholds are crossed.
// Spawners and encounter systems subscribe to intensity events.
using System;
using UnityEngine;
/// <summary>
/// Defines the target intensity curve for a session.
/// X axis = normalized session time (0-1), Y axis = intensity (0-1).
/// </summary>
[CreateAssetMenu(menuName = "Game Loop/Pacing Curve Config")]
public class PacingCurveConfig : ScriptableObject
{
[Tooltip("Target intensity over normalized session time. Y: 0=calm, 1=max intensity.")]
public AnimationCurve intensityCurve = new(
new Keyframe(0f, 0.1f),
new Keyframe(0.2f, 0.5f),
new Keyframe(0.35f, 0.2f), // rest period
new Keyframe(0.6f, 0.7f),
new Keyframe(0.75f, 0.3f), // rest period
new Keyframe(0.9f, 0.9f),
new Keyframe(1f, 1f) // climax
);
[Tooltip("Total session duration in seconds for normalizing time")]
public float sessionDurationSeconds = 600f;
[Tooltip("Intensity thresholds that trigger encounter events")]
public float[] intensityThresholds = { 0.25f, 0.5f, 0.75f, 0.9f };
}
/// <summary>
/// Reads the pacing curve and fires intensity events that encounter systems consume.
/// Uses unscaled time so pausing does not break the curve.
/// </summary>
public class PacingDirector : MonoBehaviour
{
[SerializeField] private PacingCurveConfig _config;
private float _sessionStartTime;
private float _currentIntensity;
private readonly bool[] _thresholdsFired = new bool[16];
/// <summary>Current intensity value (0-1).</summary>
public float CurrentIntensity => _currentIntensity;
/// <summary>Fires every frame with the current intensity value.</summary>
public event Action<float> OnIntensityChanged;
/// <summary>Fires when an intensity threshold is crossed upward. Arg: threshold value.</summary>
public event Action<float> OnThresholdCrossed;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
static void ResetStatics() { }
/// <summary>Begin tracking pacing for a new session.</summary>
public void BeginSession()
{
_sessionStartTime = Time.realtimeSinceStartup;
_currentIntensity = 0f;
Array.Clear(_thresholdsFired, 0, _thresholdsFired.Length);
}
private void Update()
{
if (_config == null) return;
float elapsed = Time.realtimeSinceStartup - _sessionStartTime;
float normalized = Mathf.Clamp01(elapsed / _config.sessionDurationSeconds);
_currentIntensity = _config.intensityCurve.Evaluate(normalized);
OnIntensityChanged?.Invoke(_currentIntensity);
for (int i = 0; i < _config.intensityThresholds.Length && i < _thresholdsFired.Length; i++)
{
if (!_thresholdsFired[i] && _currentIntensity >= _config.intensityThresholds[i])
{
_thresholdsFired[i] = true;
OnThresholdCrossed?.Invoke(_config.intensityThresholds[i]);
}
}
}
}
脚手架(完整实现):有关包含冷却重置和遭遇生成器集成的完整 PacingDirector,请参阅 references/game-loop-scaffolds.md。
设计钩子:设计师在 Inspector 中直观地编辑节奏曲线,塑造紧张/缓和循环。遭遇生成器订阅 OnThresholdCrossed,以便在设计师定义的强度阈值处触发敌潮、Boss 生成或环境事件。
注意事项:如果游戏会暂停,节奏系统必须使用 Time.realtimeSinceStartup 或非缩放时间。使用 Time.time 或 Time.deltaTime 会导致节奏曲线在暂停菜单期间冻结,然后在取消暂停时一次性释放所有累积的强度——破坏设计师精心设计的节奏。
反模式速查表
| 反模式 | 症状 | 修复方法 |
|---|---|---|
| 没有阶段概念 | 所有系统同时运行,没有进度结构 | 使用带有 IGamePhase 的 GameLoopController |
| 通过重新加载场景来重启 | 每次重试都出现加载画面,元状态丢失 | 使用带有显式开始/结束逻辑的 SessionManager |
| 硬编码胜利条件 | 每种关卡变体都需要程序员参与 | 使用基于 SO 条件的 ConditionEvaluator |
| 将元状态放在会话代码中 | 重启会清除货币,或货币在不该保留时仍然保留 | 将 MetaProgressionService 与会话分离 |
| 难度使用魔法数字 | 调整需要修改代码并重新编译 | 使用包含 AnimationCurve 字段的 DifficultyConfig SO |
| 固定间隔生成 | 节奏平淡、单调,缺乏紧张与缓和 | 使用带有设计师自定义强度曲线的 PacingDirector |
相关技能
unity-save-system-- 在应用启动之间持久化元进度状态unity-npc-behavior-- 会响应节奏导演强度等级的敌人 AIunity-state-machines-- 会话和阶段管理所用的底层 FSM 模式unity-game-architecture-- 全项目使用的服务定位器、事件通道和引导序列unity-data-driven-- 用于阶段、难度和节奏控制的 ScriptableObject 配置模式