{“content”:”---\nname: game-programming-patterns\ndescription: 游戏编程模式 — 引擎无关的 2D RPG 开发设计模式。来源:munificent/game-programming-patterns (4.5k ⭐)。引擎:Godot / Unity / 自制引擎皆适用。\nversion: 1.0.0\nauthor: Hermes (based on Robert Nystrom’s “Game Programming Patterns”)\nlicense: MIT\nmetadata:\n hermes:\n tags: [game-dev, design-patterns, architecture, gamedev-theory]\n trigger: “任何游戏开发任务时加载 — 设计模式、架构规划、代码组织”\n source: https://github.com/munificent/game-programming-patterns\n book: https://gameprogrammingpatterns.com\n---\n\n# Game Programming Patterns\n引擎无关 · 适用 Godot / Unity / 自制引擎 · 2D RPG 专用\n\n来源:Game Programming Patterns by Robert Nystrom (4.5k ⭐ GitHub)\n\n---\n\n## 核心原则\n\n> 游戏代码有四大挑战:① 时间与顺序 ② 团队快速迭代 ③ 系统交互 ④ 性能优化\n\n模式是可重用的解决方案。不要死记——理解问题再选模式。\n\n---\n\n## 四大分类\n\n| 分类 | 解决的问题 |\n|------|-----------|\n| Sequencing | 时间与执行顺序 |\n| Behavioral | 对象行为组织 |\n| Decoupling | 系统间解耦 |\n| Optimization | 性能与内存 |\n\n---\n\n## 1. Sequencing Patterns(时间与顺序)\n\n### 🔄 Game Loop — 游戏心跳\n\nIntent: 将游戏时间推进与用户输入、处理器速度解耦。\n\n为什么重要: 游戏必须以稳定帧率运行,同时处理快速/慢速硬件差异。\n\npython\n# Godot GDScript — 最基础的 Game Loop\nextends Node\n\nvar accumulator = 0.0\nconst FIXED_DT = 1.0 / 60.0 # 固定时间步长\n\nfunc _process(delta: float) -> void:\n accumulator += delta\n while accumulator >= FIXED_DT:\n physics_update(FIXED_DT) # 固定频率物理\n accumulator -= FIXED_DT\n render(accumulator / FIXED_DT) # 插值渲染\n\n\ncsharp\n// Unity C# — 经典 Update Loop\nvoid Update() {\n float delta = Time.deltaTime;\n ProcessInput(delta);\n UpdateEntities(delta);\n UpdatePhysics(delta);\n}\n\nvoid FixedUpdate() {\n // 固定物理更新 (默认 50Hz)\n Physics.Update();\n}\n\n\nRPG 用途: 控制战斗回合计时、动画帧同步、AI 决策间隔\n\n---\n\n### 📋 Update Method — 帧更新\n\nIntent: 告诉每个对象”处理一帧行为”,模拟独立对象集合。\n\n核心: 每个 Entity 有 update() 方法,游戏循环统一调用。\n\npython\n# GDScript — RPG 角色 Update Method\nclass_name Actor\nextends Node2D\n\nvar velocity := Vector2.ZERO\nvar state := \"idle\"\n\nfunc _physics_process(delta: float) -> void:\n update() # 每帧调用\n\nfunc update() -> void:\n match state:\n \"idle\": idle_update()\n \"walk\": walk_update()\n \"attack\": attack_update()\n \"hurt\": hurt_update()\n\nfunc idle_update() -> void:\n velocity = Vector2.ZERO\n if input_direction != Vector2.ZERO:\n state = \"walk\"\n\nfunc walk_update() -> void:\n velocity = input_direction * speed\n if input_direction == Vector2.ZERO:\n state = \"idle\"\n\n\nRPG 用途: 角色状态机、敌人 AI 巡逻、背包物品状态\n\n---\n\n### 🖥️ Double Buffer — 双缓冲\n\nIntent: 让一系列顺序操作看起来是瞬间或同时完成的。\n\n场景: 渲染时防止闪烁——先画到后台缓冲,再一次性交换。\n\npython\n# GDScript — 双缓冲渲染(Godot Viewport 内置实现)\n# 但理解原理很重要:\n# 缓冲A: 当前显示的帧\n# 缓冲B: 正在绘制的新帧\n# swap(): 瞬间交换 A↔B,用户看不到绘制过程\n\n# 适用场景:粒子系统、地图瓦片渲染、伤害数字显示\nclass_name DoubleBuffer\nextends Node\n\nvar current_buffer = 0 # 0 或 1\nvar buffers := [[], []] # 两个缓冲\n\nfunc write(frame: int, data) -> void:\n buffers[frame % 2].append(data) # 写入当前缓冲\n\nfunc swap() -> int:\n current_buffer = 1 - current_buffer # 切换缓冲\n return current_buffer\n\n\nRPG 用途: 战斗特效、弹幕游戏、伤害数字叠加\n\n---\n\n## 2. Behavioral Patterns(行为组织)\n\n### 🎭 State — 状态机\n\nIntent: 对象行为随内部状态变化而改变,且状态之间切换清晰。\n\n最关键的 RPG 模式之一。 角色站立/行走/攻击/受伤都可用状态机。\n\npython\n# GDScript — 完整状态机\nclass_name StateMachine\nextends Node\n\nsignal state_changed(from: String, to: String)\n\n@export var initial_state: State\n\nvar current_state: State\n\nfunc _ready() -> void:\n current_state = initial_state\n current_state.enter()\n\nfunc _physics_process(delta: float) -> void:\n var next = current_state.get_transition()\n if next:\n switch_to(next)\n\nfunc switch_to(next: State) -> void:\n state_changed.emit(current_state.name, next.name)\n current_state.exit()\n current_state = next\n current_state.enter()\n\n# 具体状态\nclass State:\n extends Node\n var fsm: StateMachine\n \n func enter() -> void: pass\n func exit() -> void: pass\n func get_transition() -> State: return null\n func _physics_process(delta: float) -> void: pass\n\n\npython\n# IdleState — 待机状态\nclass_name IdleState\nextends State\n\nfunc get_transition() -> State:\n if fsm.actor.health <= 0:\n return fsm.get_node(\"Dead\")\n if fsm.actor.input_direction != Vector2.ZERO:\n return fsm.get_node(\"Walk\")\n if fsm.actor.is_attacking:\n return fsm.get_node(\"Attack\")\n return null\n\n# WalkState — 行走状态\nclass_name WalkState\nextends State\n\nfunc _physics_process(delta: float) -> void:\n fsm.actor.velocity = fsm.actor.input_direction * fsm.actor.speed\n fsm.actor.move_and_slide()\n\n\nRPG 用途: 角色状态(待机/行走/攻击/死亡)、UI 状态(菜单打开/关闭)、对话进度\n\n---\n\n### 📜 Bytecode — 字节码\n\nIntent: 把行为编码为虚拟机的指令,用数据而不是代码定义行为。\n\n场景: RPG 剧情脚本、NPC 对话、任务条件、AI 行为树——让策划不用写代码也能编辑。\n\npython\n# GDScript — 简化字节码解释器\nclass_name DialogueVM\nextends Node\n\nenum Opcode { SAY, CHOICE, IF, GOTO, END }\n\nvar ip := 0 # instruction pointer\nvar call_stack := []\nvar variables := {}\n\nvar program: Array # [(opcode, args...), ...]\n\nfunc run() -> void:\n ip = 0\n while ip < program.size():\n var op = program[ip]\n match op[0]:\n Opcode.SAY:\n show_text(op[1])\n ip += 1\n Opcode.CHOICE:\n show_choices(op[1]) # [text, goto_ip, ...]\n return # 暂停,等待玩家选择\n Opcode.IF:\n if variables.get(op[1], 0) == op[2]:\n ip = op[3] # goto\n else:\n ip += 1\n Opcode.GOTO:\n ip = op[1]\n Opcode.END:\n end_dialogue()\n return\n _:\n ip += 1\n\n# 示例程序\nfunc example_quest_dialogue():\n program = [\n (Opcode.SAY, \"你好,冒险者!\"),\n (Opcode.CHOICE, [\"我有任务\", 4, \"再见\", 7]),\n (Opcode.SAY, \"去帮我杀10只史莱姆!\"),\n (Opcode.SET, \"quest_slime\", 1), # 触发任务\n (Opcode.END,),\n ]\n\n\nRPG 用途: 剧情脚本编辑器(类 RPG Maker 事件)、商店系统、任务条件\n\n---\n\n### 🏗️ Subclass Sandbox — 子类沙盒\n\nIntent: 用基类提供的一组操作来定义子类行为,避免子类直接依赖系统细节。\n\npython\n# GDScript — RPG 技能基类\nclass_name Skill\nextends Node\n\n# 基类提供的基础操作(沙盒)\nfunc play_effect(effect_path: String) -> void:\n var effect = load(effect_path).instantiate()\n get_tree().root.add_child(effect)\n\nfunc spawn_projectile(scene: PackedScene, from: Vector2, to: Vector2) -> void:\n var p = scene.instantiate()\n p.position = from\n p.direction = (to - from).normalized()\n get_parent().add_child(p)\n\nfunc deal_damage(target: Node2D, amount: int) -> void:\n target.take_damage(amount)\n\nfunc play_sound(sfx_path: String) -> void:\n $AudioStreamPlayer.stream = load(sfx_path)\n $AudioStreamPlayer.play()\n\n# 具体技能子类 — 只需要组合基类操作\nclass_name FireballSkill\nextends Skill\n\nfunc execute() -> void:\n play_sound(\"res://audio/fireball.wav\")\n spawn_projectile(preload(\"fireball.tscn\"), caster_pos, target_pos)\n play_effect(\"res://effects/fire_explosion.tscn\")\n deal_damage(target, 50)\n\nclass_name HealSkill\nextends Skill\n\nfunc execute() -> void:\n play_sound(\"res://audio/heal.wav\")\n caster.heal(30)\n play_effect(\"res://effects/heal_glow.tscn\")\n\n\nRPG 用途: 技能系统、buff/debuff、物品效果、装备属性\n\n---\n\n### 🗃️ Object Pool — 对象池\n\nIntent: 复用固定池中的对象,而不是单独分配和释放,提升性能。\n\n场景: RPG 中大量子弹、粒子、特效、伤害数字——频繁生成/销毁很慢。\n\npython\n# GDScript — 简单对象池\nclass_name ObjectPool\nextends Node\n\n@export var scene: PackedScene # 要复用的对象\n@export var initial_size: int = 10\n\nvar available := []\nvar active := []\n\nfunc _ready() -> void:\n for i in initial_size:\n available.append(_create_instance())\n\nfunc _create_instance() -> Node:\n var obj = scene.instantiate()\n obj.set_process(false)\n obj.visible = false\n add_child(obj)\n return obj\n\nfunc get() -> Node:\n var obj\n if available.is_empty():\n obj = _create_instance()\n else:\n obj = available.pop_back()\n obj.set_process(true)\n obj.visible = true\n active.append(obj)\n return obj\n\nfunc release(obj: Node) -> void:\n obj.set_process(false)\n obj.visible = false\n active.erase(obj)\n available.append(obj)\n\n\npython\n# 使用\nvar damage_numbers: ObjectPool\n\nfunc _on_enemy_hit(damage: int) -> void:\n var label = damage_numbers.get()\n label.text = str(damage)\n label.global_position = enemy.global_position + Vector2(0, -50)\n label.release() # 0.5秒后放回池\n\n\nRPG 用途: 子弹/魔法弹道、击中特效、伤害数字、掉落物品图标\n\n---\n\n## 3. Decoupling Patterns(解耦)\n\n### 🧩 Component — 组件模式\n\nIntent: 让单个实体跨越多个领域(渲染/物理/AI),但不耦合各领域。\n\n这是 Godot 引擎的核心理念! Godot 的 Node = Entity,Component = 挂载的 Node。\n\npython\n# GDScript — Godot 的 Component 模式天然支持\n# Player.tscn 场景树:\n# Player (CharacterBody2D)\n# ├── SpriteComponent (处理动画)\n# ├── HealthComponent (管理血量)\n# ├── InventoryComponent (背包)\n# ├── HitboxComponent (受击判定)\n# └── NavigationComponent (寻路)\n\n# HealthComponent — 独立血量逻辑\nclass_name HealthComponent\nextends Node\n\nsignal health_changed(current: int, max: int)\nsignal died\n\n@export var max_health: int = 100\nvar current_health: int\n\nfunc _ready() -> void:\n current_health = max_health\n\nfunc take_damage(amount: int) -> void:\n current_health = max(0, current_health - amount)\n health_changed.emit(current_health, max_health)\n if current_health <= 0:\n died.emit()\n\nfunc heal(amount: int) -> void:\n current_health = min(max_health, current_health + amount)\n health_changed.emit(current_health, max_health)\n\n\npython\n# 使用:在 Player 上挂载 HealthComponent\nfunc _ready() -> void:\n var health = $HealthComponent # 获取组件\n health.died.connect(_on_player_died)\n\nfunc _on_player_died() -> void:\n state_machine.switch_to(\"Dead\")\n $SpriteComponent.play(\"death\")\n\n\nRPG 用途: 角色由 HealthComponent + InventoryComponent + SpriteComponent 组成,组件之间不直接引用\n\n---\n\n### 📬 Event Queue — 事件队列\n\nIntent: 解耦事件发送处理的时机。\n\n场景: 音效延迟播放、异步奖励、UI 响应不阻塞游戏逻辑。\n\npython\n# GDScript — 简单事件队列\nclass_name EventQueue\nextends Node\n\nsignal event_emitted(event_type: String, data: Dictionary)\n\nvar queue: Array = []\n\nfunc emit(event_type: String, data: Dictionary = {}) -> void:\n queue.append({type=event_type, data=data, time=Time.get_ticks_msec()})\n\nfunc process_events() -> void:\n while not queue.is_empty():\n var event = queue.pop_front()\n event_emitted.emit(event.type, event.data)\n\n\npython\n# 使用\nvar events := EventQueue.new()\n\nfunc _ready() -> void:\n events.event_emitted.connect(_on_event)\n # 攻击时不立即播放音效,而是排队\n func attack() -> void:\n emit_signal(\"hit_enemy\", enemy) # 同步伤害\n events.emit(\"play_sound\", {sfx=\"hit.wav\"}) # 延迟音效\n\nfunc _physics_process(delta: float) -> void:\n events.process_events() # 在帧末尾统一处理\n\n\nRPG 用途: 攻击触发多个系统(扣血、播音效、震屏、加经验)不互相阻塞\n\n---\n\n### 🔍 Service Locator — 服务定位器\n\nIntent: 提供全局访问点访问服务,但不耦合具体实现类。\n\npython\n# GDScript — 全局服务定位器\nclass_name ServiceLocator\nextends Node\n\nstatic var services := {}\n\nstatic func register(name: String, service: Object) -> void:\n services[name] = service\n\nstatic func get_service(name: String) -> Object:\n return services.get(name)\n\nstatic func clear() -> void:\n services.clear()\n\n# 注册\nfunc _ready() -> void:\n ServiceLocator.register(\"audio\", $AudioManager)\n ServiceLocator.register(\"save\", $SaveSystem)\n ServiceLocator.register(\"inventory\", $Inventory)\n\n# 使用 — 不依赖具体类\nfunc play_sound(path: String) -> void:\n var audio = ServiceLocator.get_service(\"audio\")\n if audio:\n audio.play(path)\n\n\nRPG 用途: 管理 AudioManager、SaveSystem、QuestLog 等全局服务,支持运行时替换(比如 mock 测试)\n\n---\n\n## 4. Optimization Patterns(性能)\n\n### 📍 Data Locality — 数据局部性\n\nIntent: 通过合理排列数据利用 CPU 缓存加速访问。\n\n核心: 连续内存访问 >> 随机内存访问。数据组织比算法更重要。\n\npython\n# ❌ 不好:实体属性分散在不同数组\npositions = [] # AOS: Array of Structures\nhealths = []\nvelocities = []\n\n# 每次遍历所有敌人,随机访问 3 个数组\n\n# ✅ 好:结构数组(SoA)\nclass Entity:\n pass\n\nentities = [] # Structure of Arrays 紧凑结构\n# entities[i] 包含 position, health, velocity 连续访问\n\n# Godot 实践:使用 FlatBuffer 存储同质数据\nclass_name EntityBuffer\nextends Node\n\nvar positions: PackedVector2Array\nvar velocities: PackedVector2Array\nvar healths: PackedInt32Array\nvar count := 0\n\nfunc add(pos: Vector2, vel: Vector2, hp: int) -> void:\n positions.resize(count + 1)\n velocities.resize(count + 1)\n healths.resize(count + 1)\n positions[count] = pos\n velocities[count] = vel\n healths[count] = hp\n count += 1\n\n\nRPG 用途: 大量同类型敌人/子弹时,用 PackedByteArray / 紧凑数组替代 Node 列表\n\n---\n\n### 🏴 Dirty Flag — 脏标记\n\nIntent: 避免重复计算,只在数据真正改变时才重新计算。\n\npython\n# GDScript — RPG 伤害计算脏标记\nclass_name DamageCalculator\nextends Node\n\nvar base_stats: Dictionary # 基础属性\nvar cached_damage := 0\nvar is_dirty := true\n\nfunc modify_base_stat(stat: String, delta: int) -> void:\n base_stats[stat] = base_stats.get(stat, 0) + delta\n is_dirty = true # 标记脏\n\nfunc recalculate_if_dirty() -> int:\n if is_dirty:\n recalculate()\n is_dirty = false\n return cached_damage\n\nfunc recalculate() -> void:\n var atk = base_stats.get(\"attack\", 10)\n var def = base_stats.get(\"defense\", 5)\n cached_damage = max(1, atk - def)\n\n\nRPG 用途: 装备变更时属性重算、路径finding结果缓存、UI 数据刷新\n\n---\n\n### 🗺️ Spatial Partition — 空间分区\n\nIntent: 通过位置组织数据,高效查询”附近有哪些对象”。\n\nRPG 场景: 碰撞检测、范围技能(治疗/光环)、视野检测。\n\npython\n# GDScript — 简单 Grid Partition(Godot 有内置 TileMap)\nclass_name SpatialHash\nextends Node\n\nvar cell_size := 64.0\nvar grid: Dictionary = {} # key: (cx, cy), value: [entities]\n\nfunc _to_key(pos: Vector2) -> Vector2i:\n return Vector2i(floor(pos.x / cell_size), floor(pos.y / cell_size))\n\nfunc insert(entity: Node2D) -> void:\n var key = _to_key(entity.global_position)\n if not grid.has(key):\n grid[key] = []\n grid[key].append(entity)\n\nfunc query_range(pos: Vector2, radius: float) -> Array:\n var results := []\n var cell_radius = int(ceil(radius / cell_size))\n var center = _to_key(pos)\n \n for cx in range(center.x - cell_radius, center.x + cell_radius + 1):\n for cy in range(center.y - cell_radius, center.y + cell_radius + 1):\n var cell = grid.get(Vector2i(cx, cy), [])\n for entity in cell:\n if entity.global_position.distance_to(pos) <= radius:\n results.append(entity)\n return results\n\n\npython\n# 使用:范围治疗技能\nfunc heal_allies_in_range(caster: Node2D, range_radius: float, amount: int) -> void:\n var nearby = spatial_hash.query_range(caster.global_position, range_radius)\n for ally in nearby:\n if ally.has_node(\"HealthComponent\"):\n ally.get_node(\"HealthComponent\").heal(amount)\n\n\nRPG 用途: 范围攻击碰撞、召唤物管理、NPC AI 视野\n\n---\n\n## 快速索引\n\n| 需求 | 模式 |\n|------|------|\n| 角色状态(站立/攻击/受伤) | State |\n| 技能/物品效果定义 | Bytecode + Subclass Sandbox |\n| 渲染不闪烁 | Double Buffer |\n| 大量子弹/特效 | Object Pool |\n| 组件化角色(血量/背包分离) | Component |\n| 音效/特效不阻塞逻辑 | Event Queue |\n| 范围技能碰撞检测 | Spatial Partition |\n| 装备变更属性重算 | Dirty Flag |\n| 帧率无关的物理/AI | Game Loop |\n| 全局服务访问 | Service Locator |\n\n---\n\n## 参考文献\n\n- 原书:https://gameprogrammingpatterns.com\n- GitHub: https://github.com/munificent/game-programming-patterns\n- 代码示例:C++(可迁移到任何 OOP 语言)\n”}