34e - 游戏 Steering 规则与反模式
本文是《AI Agent 实战手册》第 34 章第 5 节。 上一节:AI生成游戏资产 | 下一节:AI辅助嵌入式开发概览
概述
游戏开发是 AI 代码生成最容易产生”隐性 Bug”的领域——AI 生成的代码在编辑器中编译通过、在低负载下运行正常,但一旦进入实际游戏场景就会暴露物理抖动、帧率骤降、内存泄漏、输入丢失、渲染卡顿等严重问题。2025-2026 年的实践数据表明,AI 生成的游戏代码中约 50-65% 存在性能或正确性问题,远高于 Web 应用的 30-40%,根本原因在于游戏开发对实时性、确定性和资源效率有极其严格的要求,而这些约束很难通过自然语言 prompt 完整传达。本节提供完整的游戏开发 Steering 规则模板(覆盖 Unity C#、Godot GDScript、Unreal C++)、五大核心反模式的深度剖析(含 AI 生成的问题代码 vs 正确代码对比)、引擎专用 Steering 规则,以及实战案例和避坑指南,帮助开发者在使用 AI 辅助游戏开发时避免常见陷阱。
1. 游戏开发 Steering 规则概述
1.1 为什么游戏开发需要专用 Steering 规则
游戏开发与传统应用开发有本质区别,AI 在以下方面特别容易犯错:
| 维度 | Web/应用开发 | 游戏开发 | AI 常见失误 |
|---|---|---|---|
| 时间模型 | 事件驱动,无严格时序 | 固定帧率循环,物理需确定性 | 在 Update 中写物理逻辑、不用 delta time |
| 内存管理 | GC 友好,分配自由 | 每帧预算严格,GC 暂停致命 | Update 中 new 对象、字符串拼接、LINQ |
| 性能预算 | 响应时间 < 200ms 即可 | 每帧 16.67ms(60fps)硬性约束 | 不考虑 draw call、overdraw、shader 复杂度 |
| 对象生命周期 | 创建-使用-GC 回收 | 对象池、延迟销毁、场景切换 | 不检查 null/freed、事件未取消订阅 |
| 输入处理 | 表单提交、点击事件 | 每帧轮询、缓冲、预测 | 在渲染帧处理物理输入、缺少输入缓冲 |
| 渲染管线 | 浏览器/框架处理 | 开发者需主动优化 | 不合批、过多材质、复杂 shader |
| 多线程 | 异步/await 即可 | 主线程/渲染线程/物理线程分离 | 跨线程访问游戏对象、竞态条件 |
| 平台差异 | 浏览器差异较小 | PC/主机/移动端性能差异巨大 | 只在编辑器测试、不考虑低端设备 |
1.2 Steering 规则工具对比
| 工具 | 规则文件位置 | 价格 | 游戏开发适用性 | 适用场景 |
|---|---|---|---|---|
| Claude Code | CLAUDE.md(项目根目录) | Max $100/月起 / API 按量 | ★★★★★ | 大型游戏项目,多文件重构 |
| Kiro | .kiro/steering/*.md | 免费(预览期) | ★★★★★ | 分层规则,按引擎/模块分区 |
| Cursor | .cursor/rules/*.mdc | 免费 / Pro $20/月 | ★★★★☆ | 日常游戏脚本编写 |
| GitHub Copilot | .github/copilot-instructions.md | $10/月(Individual) | ★★★☆☆ | 代码补全级辅助 |
| Windsurf | .windsurfrules | 免费 / Pro $15/月 | ★★★☆☆ | 轻量级游戏开发 |
| JetBrains AI | .jb-ai-rules.md | $8.33/月起(AI Pro) | ★★★★☆ | Rider(Unity C#)/ CLion(UE C++) |
1.3 游戏开发 Steering 规则架构
游戏项目根目录/
├── CLAUDE.md # 全局规则(通用游戏开发约束)
├── .kiro/steering/
│ ├── game-general.md # 游戏通用规则(always)
│ ├── unity-csharp.md # Unity C# 专用规则(auto: *.cs)
│ ├── godot-gdscript.md # Godot GDScript 专用规则(auto: *.gd)
│ ├── unreal-cpp.md # Unreal C++ 专用规则(auto: *.cpp, *.h)
│ ├── physics-rules.md # 物理系统规则(manual)
│ └── rendering-rules.md # 渲染优化规则(manual)
├── .cursor/rules/
│ ├── game-dev-rules.mdc # Cursor 游戏开发规则
│ └── engine-specific.mdc # 引擎特定规则
└── .github/
└── copilot-instructions.md # Copilot 游戏开发指令2. 完整 Steering 规则模板
2.1 CLAUDE.md 游戏开发通用规则模板
以下是适用于多引擎游戏项目的完整 CLAUDE.md 模板:
# CLAUDE.md — 游戏开发项目规则
## 项目概述
- 引擎:[Unity 6 LTS / Godot 4.4 / Unreal Engine 5.5]
- 语言:[C# 12 / GDScript / C++20]
- 目标平台:[PC + Mobile / Console / WebGL]
- 目标帧率:60fps(移动端可降至 30fps)
- 最低硬件:[具体 GPU/内存要求]
## 🔴 绝对禁止(违反即回退)
1. 禁止在 Update/Tick/_process 中使用 new 分配堆内存(使用对象池或预分配)
2. 禁止在 Update 中使用 Find/GetNode 等搜索方法(在 Start/Ready 中缓存引用)
3. 禁止在 Update 中进行字符串拼接(使用 StringBuilder 或预分配 char[])
4. 禁止在渲染帧回调中写物理逻辑(物理必须在 FixedUpdate/_physics_process 中)
5. 禁止直接销毁正在被引用的对象(先取消所有引用和事件订阅)
6. 禁止使用 LINQ 查询热路径代码(LINQ 产生大量 GC 分配)
7. 禁止在游戏循环中使用 async/await(使用协程或状态机)
8. 禁止硬编码魔法数字(使用 ScriptableObject/Resource/DataTable)
## 🟡 物理系统规则
- 所有物理计算必须在 FixedUpdate/_physics_process/Tick 中执行
- 移动刚体必须使用 MovePosition/Velocity,禁止直接修改 Transform
- 物理射线检测结果必须缓存,禁止每帧重复检测相同目标
- 碰撞检测必须使用层级过滤,禁止全层级检测
- 连续碰撞检测(CCD)必须对高速物体启用,防止穿透
## 🟢 内存管理规则
- 频繁创建/销毁的对象(子弹、特效、敌人)必须使用对象池
- 协程/Tween 必须在对象销毁前停止
- 事件/信号订阅必须在 OnDestroy/_exit_tree 中取消
- 纹理/音频资源必须按需加载,不用时释放
- 字符串操作使用 StringBuilder(C#)或 StringName(Godot)
## 🔵 输入处理规则
- 输入检测在 Update/_process/_input 中执行(不在物理帧中)
- 动作类游戏必须实现输入缓冲(3-5 帧窗口)
- 平台跳跃必须实现 Coyote Time(离开平台后 0.1-0.15s 仍可跳跃)
- 所有输入必须通过 Input Action 抽象层,禁止硬编码按键
- 移动端必须支持触摸和手柄双输入方案
## 🟣 渲染优化规则
- 单场景 draw call 不超过 [200(移动端)/ 2000(PC)]
- 使用 GPU Instancing / MultiMesh 渲染大量相同物体
- UI 元素必须分层(静态层 + 动态层),避免整体重绘
- Shader 禁止在片元着色器中使用分支语句(if/else)
- 透明物体数量严格控制,避免 overdraw
- LOD 系统必须对 3D 模型启用(至少 3 级)
## 📁 代码组织规则
- 每个脚本/节点只负责一个职责(单一职责原则)
- 游戏状态使用状态机管理,禁止嵌套 if-else 判断游戏状态
- 配置数据与逻辑分离(ScriptableObject / Resource / DataTable)
- 公共接口使用事件/信号解耦,禁止直接引用其他系统的内部状态2.2 Kiro Steering 游戏通用规则模板
文件路径:.kiro/steering/game-general.md
---
trigger: always
---
# 游戏开发通用 Steering 规则
## 帧率预算意识
- 目标:60fps = 每帧 16.67ms 预算
- 脚本逻辑预算:< 5ms
- 物理预算:< 4ms
- 渲染预算:< 8ms
- 任何单个函数执行时间不得超过 2ms
## 热路径零分配原则
以下函数被视为"热路径",禁止在其中分配堆内存:
- Update / _process / Tick(每帧调用)
- FixedUpdate / _physics_process(每物理帧调用)
- LateUpdate(每帧调用)
- OnCollision / OnTrigger / _on_body_entered(高频回调)
- 动画事件回调
- 输入处理回调
## 对象生命周期管理
- 创建:优先从对象池获取,池空时才实例化
- 使用:检查对象有效性(IsValid / is_instance_valid / IsValidLowLevel)
- 回收:归还对象池而非销毁,重置状态
- 销毁:先断开所有信号/事件连接,再销毁
## 场景切换安全
- 切换场景前:停止所有协程/Tween、取消所有异步操作、保存必要状态
- 切换场景时:使用加载屏幕,异步加载下一场景
- 切换场景后:验证所有单例引用有效、重新初始化输入状态2.3 Cursor Rules 游戏开发模板
文件路径:.cursor/rules/game-dev-rules.mdc
---
description: 游戏开发核心规则,适用于所有游戏引擎项目
globs: ["*.cs", "*.gd", "*.cpp", "*.h", "*.hpp"]
---
# 游戏开发 Cursor 规则
## 代码生成约束
1. 生成游戏脚本时,必须区分"每帧逻辑"和"一次性初始化"
2. 所有组件引用必须在初始化函数中缓存,禁止运行时查找
3. 生成物理相关代码时,必须放在固定时间步函数中
4. 生成对象创建代码时,必须提供对象池版本
5. 生成 UI 代码时,必须考虑 draw call 合批
## 性能审查清单
生成代码后,自动检查以下项目:
- [ ] 热路径中是否有 new/Instantiate/instance()
- [ ] 是否有未缓存的 GetComponent/get_node 调用
- [ ] 物理逻辑是否在正确的回调函数中
- [ ] 事件订阅是否有对应的取消订阅
- [ ] 是否有字符串拼接在热路径中
- [ ] 是否有 LINQ/lambda 在热路径中3. 反模式一:物理不稳定(Physics Instability)
3.1 问题概述
物理不稳定是 AI 生成游戏代码中最常见也最难调试的问题。表现为:角色移动抖动(jitter)、物体穿透碰撞体(tunneling)、不同帧率下行为不一致、物理模拟结果不可复现。根本原因是 AI 不理解游戏引擎的时间模型——渲染帧(可变时间步)和物理帧(固定时间步)是两个独立的循环。
问题严重程度: 🔴 致命(直接影响游戏可玩性)
影响范围:
- 角色控制器:移动速度随帧率变化,高帧率移动快、低帧率移动慢
- 碰撞检测:高速物体穿透墙壁和地面
- 物理模拟:弹道、爆炸、布料等效果不可预测
- 网络同步:不同客户端物理结果不一致
3.2 AI 生成的问题代码
Unity C# — 错误示例
// ❌ AI 常见错误:在 Update 中直接修改物理对象位置
public class PlayerMovement : MonoBehaviour
{
public float speed = 5f;
public float jumpForce = 10f;
private Rigidbody rb;
void Start()
{
rb = GetComponent<Rigidbody>();
}
// 错误 1:物理移动放在 Update 中(帧率依赖)
void Update()
{
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
// 错误 2:直接修改 Transform 而非通过 Rigidbody
transform.position += new Vector3(h, 0, v) * speed * Time.deltaTime;
// 错误 3:每帧都检测跳跃,没有地面检测缓存
if (Input.GetKeyDown(KeyCode.Space))
{
// 错误 4:使用 ForceMode.Force 而非 Impulse 做跳跃
rb.AddForce(Vector3.up * jumpForce, ForceMode.Force);
}
// 错误 5:每帧创建新的 RaycastHit(GC 分配)
RaycastHit hit;
if (Physics.Raycast(transform.position, Vector3.down, out hit, 1.1f))
{
Debug.Log("Grounded: " + hit.collider.name); // 错误 6:热路径字符串拼接
}
}
}问题分析:
| 错误编号 | 问题 | 后果 |
|---|---|---|
| 1 | 物理逻辑在 Update 中 | 帧率不同导致移动速度不一致 |
| 2 | 直接修改 Transform | 绕过物理引擎,碰撞检测失效 |
| 3 | 无地面检测缓存 | 每帧射线检测浪费性能 |
| 4 | ForceMode.Force 做跳跃 | 跳跃力度随物理帧率变化 |
| 5 | 热路径 GC 分配 | 频繁 GC 导致帧率卡顿 |
| 6 | 热路径字符串拼接 | 每帧产生垃圾字符串 |
Unity C# — 正确示例
// ✅ 正确:物理逻辑在 FixedUpdate,输入在 Update,引用预缓存
public class PlayerMovement : MonoBehaviour
{
[SerializeField] private float speed = 5f;
[SerializeField] private float jumpForce = 10f;
[SerializeField] private LayerMask groundLayer;
[SerializeField] private float groundCheckDistance = 0.2f;
private Rigidbody _rb;
private CapsuleCollider _collider;
private Vector3 _moveInput;
private bool _jumpRequested;
private bool _isGrounded;
private readonly RaycastHit[] _groundHits = new RaycastHit[1]; // 预分配
void Awake()
{
_rb = GetComponent<Rigidbody>();
_collider = GetComponent<CapsuleCollider>();
// 启用连续碰撞检测,防止高速穿透
_rb.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic;
_rb.interpolation = RigidbodyInterpolation.Interpolate; // 平滑渲染
}
// 输入在 Update 中采集(响应性最佳)
void Update()
{
_moveInput = new Vector3(
Input.GetAxis("Horizontal"),
0f,
Input.GetAxis("Vertical")
);
// 跳跃请求缓存,在 FixedUpdate 中消费
if (Input.GetButtonDown("Jump") && _isGrounded)
{
_jumpRequested = true;
}
}
// 物理逻辑在 FixedUpdate 中执行(固定时间步,确定性)
void FixedUpdate()
{
// 地面检测:使用 NonAlloc 版本避免 GC
float radius = _collider.radius * 0.9f;
Vector3 origin = transform.position + Vector3.up * radius;
_isGrounded = Physics.SphereCastNonAlloc(
origin, radius, Vector3.down, _groundHits,
groundCheckDistance, groundLayer
) > 0;
// 通过 Rigidbody 移动(物理引擎处理碰撞)
Vector3 targetVelocity = _moveInput.normalized * speed;
targetVelocity.y = _rb.linearVelocity.y; // 保持垂直速度
_rb.linearVelocity = targetVelocity;
// 消费跳跃请求
if (_jumpRequested)
{
_rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
_jumpRequested = false;
}
}
}Godot GDScript — 错误示例
# ❌ AI 常见错误:在 _process 中写物理移动
extends CharacterBody3D
var speed = 5.0
var jump_force = 10.0
# 错误 1:物理移动放在 _process 中(帧率依赖)
func _process(delta):
var direction = Vector3.ZERO
# 错误 2:硬编码按键而非使用 Input Map
if Input.is_key_pressed(KEY_W):
direction.z -= 1
if Input.is_key_pressed(KEY_S):
direction.z += 1
if Input.is_key_pressed(KEY_A):
direction.x -= 1
if Input.is_key_pressed(KEY_D):
direction.x += 1
# 错误 3:没有归一化方向向量(对角线移动更快)
velocity = direction * speed
# 错误 4:重力处理不正确(没有累积)
if not is_on_floor():
velocity.y = -9.8
# 错误 5:跳跃没有地面检测保护
if Input.is_key_pressed(KEY_SPACE):
velocity.y = jump_force
move_and_slide()Godot GDScript — 正确示例
# ✅ 正确:物理移动在 _physics_process,输入使用 Input Map
extends CharacterBody3D
@export var speed: float = 5.0
@export var jump_force: float = 10.0
@export var gravity: float = 20.0
@export var coyote_time: float = 0.12 # 土狼时间
var _coyote_timer: float = 0.0
var _jump_buffered: bool = false
var _jump_buffer_timer: float = 0.0
const JUMP_BUFFER_TIME: float = 0.1
# 输入采集在 _unhandled_input 中(事件驱动,更高效)
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("jump"):
_jump_buffered = true
_jump_buffer_timer = JUMP_BUFFER_TIME
# 物理逻辑在 _physics_process 中(固定时间步)
func _physics_process(delta: float) -> void:
# 重力累积(正确的物理模拟)
if not is_on_floor():
velocity.y -= gravity * delta
_coyote_timer -= delta
else:
_coyote_timer = coyote_time
# 跳跃缓冲计时器
if _jump_buffered:
_jump_buffer_timer -= delta
if _jump_buffer_timer <= 0.0:
_jump_buffered = false
# 跳跃:支持 Coyote Time + 输入缓冲
if _jump_buffered and _coyote_timer > 0.0:
velocity.y = jump_force
_jump_buffered = false
_coyote_timer = 0.0
# 使用 Input Map 的 Action(可重映射)
var input_dir := Input.get_vector("move_left", "move_right", "move_forward", "move_back")
var direction := (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
velocity.x = direction.x * speed
velocity.z = direction.z * speed
move_and_slide()Unreal C++ — 错误示例
// ❌ AI 常见错误:在 Tick 中直接设置 Actor 位置
void AMyCharacter::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
// 错误 1:直接修改位置,绕过物理
FVector NewLocation = GetActorLocation();
NewLocation += GetActorForwardVector() * Speed * DeltaTime;
SetActorLocation(NewLocation); // 穿透碰撞体!
// 错误 2:每帧创建 FString(堆分配)
FString DebugMsg = FString::Printf(TEXT("Pos: %s"), *NewLocation.ToString());
GEngine->AddOnScreenDebugMessage(-1, 0.f, FColor::Green, DebugMsg);
}Unreal C++ — 正确示例
// ✅ 正确:使用 CharacterMovementComponent,物理由引擎管理
void AMyCharacter::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
// 通过 MovementComponent 移动(自动处理碰撞和物理)
if (InputVector.SizeSquared() > KINDA_SMALL_NUMBER)
{
FVector WorldDirection = FRotationMatrix(GetControlRotation())
.GetScaledAxis(EAxis::X) * InputVector.X
+ FRotationMatrix(GetControlRotation())
.GetScaledAxis(EAxis::Y) * InputVector.Y;
AddMovementInput(WorldDirection.GetSafeNormal());
}
// 调试信息仅在 Debug 构建中输出
#if !UE_BUILD_SHIPPING
if (GEngine && bShowDebug)
{
GEngine->AddOnScreenDebugMessage(1, 0.f, FColor::Green,
FString::Printf(TEXT("Speed: %.1f"), GetVelocity().Size()));
}
#endif
}
// 跳跃使用 CharacterMovementComponent 内置功能
void AMyCharacter::Jump()
{
if (GetCharacterMovement()->IsMovingOnGround())
{
ACharacter::Jump();
}
}3.3 物理不稳定 Steering 规则
## 物理系统 Steering 规则
### 时间步规则
- 物理计算必须在固定时间步回调中:
- Unity: FixedUpdate()
- Godot: _physics_process(delta)
- Unreal: 使用 CharacterMovementComponent 或 Physics Tick
- 渲染相关(摄像机跟随、UI 更新)在可变时间步回调中:
- Unity: Update() / LateUpdate()
- Godot: _process(delta)
- Unreal: Tick(DeltaTime)
### 碰撞检测规则
- 高速物体(子弹、投射物)必须启用 CCD:
- Unity: CollisionDetectionMode.ContinuousDynamic
- Godot: CCD 模式启用
- Unreal: bUseCCD = true
- 射线检测使用 NonAlloc/预分配数组版本
- 碰撞层级必须正确配置,避免不必要的碰撞对
### 刚体操作规则
- 禁止直接修改 Transform/Position 移动物理对象
- 使用引擎提供的物理移动方法:
- Unity: Rigidbody.MovePosition / velocity
- Godot: CharacterBody.move_and_slide / velocity
- Unreal: AddMovementInput / LaunchCharacter
- 启用插值(Interpolation)平滑渲染表现4. 反模式二:Update 循环中分配内存(Allocation in Update Loops)
4.1 问题概述
在游戏的每帧更新循环中分配堆内存是 AI 生成代码中最普遍的性能杀手。在托管语言(C#/GDScript)中,频繁的堆分配会触发垃圾回收(GC),导致不可预测的帧率卡顿(GC spike);在 C++ 中,频繁的 malloc/free 会导致内存碎片化和缓存未命中。一次 GC 暂停可能持续 5-50ms,在 60fps 游戏中意味着丢失 1-3 帧,玩家会明显感受到”卡顿”。
问题严重程度: 🔴 致命(直接导致帧率不稳定)
常见 GC 分配来源(AI 最容易犯的错误):
| 分配来源 | 每帧分配量 | 累积影响 |
|---|---|---|
new 创建对象 | 24-100+ bytes | 每秒 60 次 = 1.4-6 KB/s |
字符串拼接 "HP: " + hp | 40-200 bytes | UI 更新时极其频繁 |
LINQ 查询 .Where().ToList() | 100-500 bytes | 迭代器 + 列表分配 |
| Lambda/闭包 | 32-64 bytes | 每次创建新委托对象 |
foreach 在某些集合上 | 32 bytes | 装箱的枚举器 |
GetComponent<T>() 每帧调用 | 间接分配 | 反射查找开销 |
ToString() | 20-100 bytes | 数值转字符串 |
| 数组/列表创建 | 40+ bytes | new List<T>() |
4.2 AI 生成的问题代码
Unity C# — 错误示例
// ❌ AI 常见错误:Update 中充满 GC 分配
public class EnemyManager : MonoBehaviour
{
public GameObject enemyPrefab;
public Text scoreText;
private int score = 0;
void Update()
{
// 错误 1:每帧用 LINQ 查找存活敌人(分配迭代器 + 列表)
var aliveEnemies = FindObjectsOfType<Enemy>()
.Where(e => e.IsAlive)
.OrderBy(e => Vector3.Distance(transform.position, e.transform.position))
.ToList();
// 错误 2:每帧创建新列表存储结果
List<Enemy> nearbyEnemies = new List<Enemy>();
foreach (var enemy in aliveEnemies)
{
if (Vector3.Distance(transform.position, enemy.transform.position) < 10f)
{
nearbyEnemies.Add(enemy);
}
}
// 错误 3:字符串拼接更新 UI(每帧产生垃圾字符串)
scoreText.text = "Score: " + score.ToString() + " | Enemies: " + nearbyEnemies.Count.ToString();
// 错误 4:每帧实例化和销毁粒子(应该用对象池)
if (nearbyEnemies.Count > 0)
{
foreach (var enemy in nearbyEnemies)
{
// 每帧为每个敌人创建新的伤害指示器
var indicator = Instantiate(enemyPrefab, enemy.transform.position, Quaternion.identity);
Destroy(indicator, 1f);
}
}
// 错误 5:Lambda 在热路径中(每次创建新委托)
nearbyEnemies.ForEach(e => e.TakeDamage(1));
}
}Unity C# — 正确示例
// ✅ 正确:零分配 Update,对象池,缓存引用
public class EnemyManager : MonoBehaviour
{
[SerializeField] private GameObject indicatorPrefab;
[SerializeField] private TMP_Text scoreText; // TextMeshPro 更高效
// 预分配集合(避免每帧 new)
private readonly List<Enemy> _aliveEnemies = new(64);
private readonly List<Enemy> _nearbyEnemies = new(32);
// 对象池
private ObjectPool<GameObject> _indicatorPool;
// UI 更新节流
private int _lastScore = -1;
private int _lastEnemyCount = -1;
private readonly StringBuilder _sb = new(64);
// 缓存的敌人注册表(避免 FindObjectsOfType)
private static readonly HashSet<Enemy> _registeredEnemies = new(128);
public static void RegisterEnemy(Enemy enemy) => _registeredEnemies.Add(enemy);
public static void UnregisterEnemy(Enemy enemy) => _registeredEnemies.Remove(enemy);
private int _score;
void Awake()
{
_indicatorPool = new ObjectPool<GameObject>(
createFunc: () => Instantiate(indicatorPrefab),
actionOnGet: obj => obj.SetActive(true),
actionOnRelease: obj => obj.SetActive(false),
defaultCapacity: 20,
maxSize: 50
);
}
void Update()
{
// 零分配:复用预分配列表
_aliveEnemies.Clear();
_nearbyEnemies.Clear();
Vector3 myPos = transform.position;
float nearbyDistSq = 10f * 10f; // 预计算平方距离(避免 sqrt)
// 零分配遍历:直接遍历 HashSet
foreach (Enemy enemy in _registeredEnemies)
{
if (!enemy.IsAlive) continue;
_aliveEnemies.Add(enemy);
// 使用平方距离比较(避免 Vector3.Distance 的 sqrt)
float distSq = (myPos - enemy.transform.position).sqrMagnitude;
if (distSq < nearbyDistSq)
{
_nearbyEnemies.Add(enemy);
}
}
// UI 更新:仅在数据变化时更新(避免每帧字符串操作)
if (_score != _lastScore || _nearbyEnemies.Count != _lastEnemyCount)
{
_sb.Clear();
_sb.Append("Score: ").Append(_score)
.Append(" | Enemies: ").Append(_nearbyEnemies.Count);
scoreText.SetText(_sb);
_lastScore = _score;
_lastEnemyCount = _nearbyEnemies.Count;
}
// 对象池:获取而非实例化
for (int i = 0; i < _nearbyEnemies.Count; i++)
{
Enemy enemy = _nearbyEnemies[i];
GameObject indicator = _indicatorPool.Get();
indicator.transform.position = enemy.transform.position;
// 延迟归还对象池
StartCoroutine(ReturnToPoolAfter(indicator, 1f));
enemy.TakeDamage(1); // 直接调用,无 Lambda
}
}
private System.Collections.IEnumerator ReturnToPoolAfter(GameObject obj, float delay)
{
yield return new WaitForSeconds(delay);
_indicatorPool.Release(obj);
}
void OnDestroy()
{
_indicatorPool?.Dispose();
}
}Godot GDScript — 错误示例
# ❌ AI 常见错误:_process 中频繁创建临时对象
extends Node2D
@onready var score_label = $ScoreLabel
var score: int = 0
func _process(delta):
# 错误 1:每帧获取所有节点(遍历整棵树)
var enemies = get_tree().get_nodes_in_group("enemies")
# 错误 2:每帧创建新数组
var nearby = []
for enemy in enemies:
# 错误 3:每帧计算距离(未使用平方距离)
var dist = global_position.distance_to(enemy.global_position)
if dist < 200.0:
nearby.append(enemy)
# 错误 4:每帧排序(O(n log n) 每帧)
nearby.sort_custom(func(a, b):
return global_position.distance_to(a.global_position) < global_position.distance_to(b.global_position)
)
# 错误 5:每帧更新 UI 文本(即使没变化)
score_label.text = "Score: " + str(score) + " | Nearby: " + str(nearby.size())
# 错误 6:每帧实例化场景
for enemy in nearby:
var bullet = preload("res://bullet.tscn").instantiate()
add_child(bullet)
bullet.global_position = global_positionGodot GDScript — 正确示例
# ✅ 正确:预分配、对象池、节流更新
extends Node2D
@onready var score_label: Label = $ScoreLabel
@export var nearby_distance: float = 200.0
@export var pool_size: int = 20
var score: int = 0
var _nearby_dist_sq: float # 预计算平方距离
var _bullet_pool: Array[Node2D] = []
var _pool_index: int = 0
var _last_score: int = -1
var _last_nearby_count: int = -1
# 预加载场景(避免每帧 preload)
var _bullet_scene: PackedScene = preload("res://bullet.tscn")
func _ready() -> void:
_nearby_dist_sq = nearby_distance * nearby_distance
# 预创建对象池
for i in pool_size:
var bullet := _bullet_scene.instantiate() as Node2D
bullet.visible = false
bullet.set_process(false)
bullet.set_physics_process(false)
add_child(bullet)
_bullet_pool.append(bullet)
func _process(delta: float) -> void:
var my_pos := global_position
var nearby_count := 0
# 使用 group 遍历(引擎内部优化,比 get_nodes_in_group 更高效)
for enemy in get_tree().get_nodes_in_group(&"enemies"):
if not enemy is Node2D:
continue
# 平方距离比较(避免 sqrt)
var diff: Vector2 = my_pos - (enemy as Node2D).global_position
if diff.length_squared() < _nearby_dist_sq:
nearby_count += 1
_fire_at(enemy as Node2D)
# 仅在数据变化时更新 UI
if score != _last_score or nearby_count != _last_nearby_count:
score_label.text = "Score: %d | Nearby: %d" % [score, nearby_count]
_last_score = score
_last_nearby_count = nearby_count
func _fire_at(target: Node2D) -> void:
# 从对象池获取
var bullet := _bullet_pool[_pool_index]
_pool_index = (_pool_index + 1) % _bullet_pool.size()
bullet.global_position = global_position
bullet.visible = true
bullet.set_process(true)
bullet.set_physics_process(true)
# bullet 脚本负责自己的回收逻辑4.3 内存分配 Steering 规则
## 内存管理 Steering 规则
### 零分配热路径原则
以下函数中禁止任何堆内存分配:
- Update / _process / Tick
- FixedUpdate / _physics_process
- LateUpdate
- 碰撞/触发回调
- 动画事件回调
### 允许的替代方案
| 禁止操作 | 替代方案 |
|---------|---------|
| `new List<T>()` | 预分配 + Clear() 复用 |
| `Instantiate()` / `.instantiate()` | 对象池 |
| `"str" + var` | StringBuilder / `%d` 格式化 |
| `FindObjectsOfType()` | 注册表模式(Register/Unregister) |
| `.Where().ToList()` | for 循环 + 预分配列表 |
| `GetComponent<T>()` 每帧 | Awake/Start 中缓存 |
| `Vector3.Distance()` | sqrMagnitude 比较 |
| `new WaitForSeconds()` | 缓存 YieldInstruction |
### UI 更新规则
- UI 文本仅在数据变化时更新(脏标记模式)
- 使用 TextMeshPro(Unity)/ RichTextLabel(Godot)
- 数值显示使用预格式化,避免每帧 ToString()5. 反模式三:事件生命周期错误(Event Lifecycle Errors)
5.1 问题概述
事件生命周期错误是 AI 生成游戏代码中最隐蔽的 Bug 类型。AI 非常擅长订阅事件和连接信号,但几乎从不生成对应的取消订阅代码。这导致三类严重问题:
- 悬空引用(Dangling References):对象已销毁,但事件回调仍然引用它,触发时导致空引用异常或访问已释放内存
- 未取消订阅的事件(Unsubscribed Events):对象销毁后事件仍在触发,导致内存泄漏和幽灵行为
- 已销毁对象的回调(Destroyed Object Callbacks):协程、Tween、定时器在对象销毁后继续执行
问题严重程度: 🔴 致命(导致崩溃、内存泄漏、不可预测行为)
AI 生成代码中事件生命周期错误的典型场景:
对象 A 订阅了对象 B 的事件
↓
对象 B 被销毁(场景切换/对象池回收/游戏逻辑)
↓
事件触发 → 回调尝试访问已销毁的对象 B
↓
💥 NullReferenceException / 访问已释放内存 / 幽灵行为5.2 AI 生成的问题代码
Unity C# — 错误示例
// ❌ AI 常见错误:订阅事件但从不取消订阅
public class HealthBar : MonoBehaviour
{
private Enemy _targetEnemy;
private Slider _slider;
void Start()
{
_slider = GetComponent<Slider>();
_targetEnemy = GetComponentInParent<Enemy>();
// 错误 1:订阅事件但没有对应的取消订阅
_targetEnemy.OnHealthChanged += UpdateHealthBar;
_targetEnemy.OnDeath += HandleEnemyDeath;
// 错误 2:订阅全局事件管理器
GameEvents.OnGamePaused += HandlePause;
GameEvents.OnGameResumed += HandleResume;
}
void UpdateHealthBar(float healthPercent)
{
// 错误 3:没有检查对象是否已销毁
_slider.value = healthPercent;
}
void HandleEnemyDeath()
{
// 错误 4:销毁自己但没有先取消事件订阅
Destroy(gameObject);
// 此时 OnHealthChanged 仍然引用着已销毁的 UpdateHealthBar
// 下次触发时 → NullReferenceException
}
void HandlePause() { /* ... */ }
void HandleResume() { /* ... */ }
// 错误 5:完全没有 OnDestroy 清理逻辑
}
// ❌ AI 常见错误:协程在对象销毁后继续执行
public class SpawnManager : MonoBehaviour
{
void Start()
{
// 错误 6:启动协程但不跟踪引用
StartCoroutine(SpawnEnemiesForever());
InvokeRepeating("CheckSpawnConditions", 0f, 1f);
}
IEnumerator SpawnEnemiesForever()
{
while (true)
{
// 错误 7:协程中引用可能已销毁的对象
var spawnPoint = GameObject.Find("SpawnPoint"); // 每次查找!
if (spawnPoint != null)
{
Instantiate(enemyPrefab, spawnPoint.transform.position, Quaternion.identity);
}
yield return new WaitForSeconds(2f); // 错误 8:每次创建新 WaitForSeconds
}
}
}Unity C# — 正确示例
// ✅ 正确:完整的事件生命周期管理
public class HealthBar : MonoBehaviour
{
private Enemy _targetEnemy;
private Slider _slider;
private bool _isSubscribed;
void Start()
{
_slider = GetComponent<Slider>();
_targetEnemy = GetComponentInParent<Enemy>();
if (_targetEnemy != null)
{
SubscribeEvents();
}
}
private void SubscribeEvents()
{
if (_isSubscribed || _targetEnemy == null) return;
_targetEnemy.OnHealthChanged += UpdateHealthBar;
_targetEnemy.OnDeath += HandleEnemyDeath;
GameEvents.OnGamePaused += HandlePause;
GameEvents.OnGameResumed += HandleResume;
_isSubscribed = true;
}
private void UnsubscribeEvents()
{
if (!_isSubscribed) return;
// 安全取消订阅(即使目标已销毁,-= 不会抛异常)
if (_targetEnemy != null)
{
_targetEnemy.OnHealthChanged -= UpdateHealthBar;
_targetEnemy.OnDeath -= HandleEnemyDeath;
}
GameEvents.OnGamePaused -= HandlePause;
GameEvents.OnGameResumed -= HandleResume;
_isSubscribed = false;
}
void UpdateHealthBar(float healthPercent)
{
// 安全检查:确保自身和 UI 组件仍然有效
if (this == null || _slider == null) return;
_slider.value = healthPercent;
}
void HandleEnemyDeath()
{
UnsubscribeEvents(); // 先取消订阅
Destroy(gameObject); // 再销毁
}
void HandlePause() { /* ... */ }
void HandleResume() { /* ... */ }
// 关键:OnDestroy 中清理所有订阅
void OnDestroy()
{
UnsubscribeEvents();
}
// 额外安全:OnDisable 时也取消订阅(对象池场景)
void OnDisable()
{
UnsubscribeEvents();
}
void OnEnable()
{
SubscribeEvents();
}
}
// ✅ 正确:协程生命周期管理
public class SpawnManager : MonoBehaviour
{
[SerializeField] private Transform _spawnPoint; // 编辑器引用,不运行时查找
[SerializeField] private GameObject _enemyPrefab;
private Coroutine _spawnCoroutine;
private static readonly WaitForSeconds _spawnDelay = new(2f); // 缓存 YieldInstruction
void OnEnable()
{
_spawnCoroutine = StartCoroutine(SpawnLoop());
}
void OnDisable()
{
// 停止协程,防止对象禁用后继续执行
if (_spawnCoroutine != null)
{
StopCoroutine(_spawnCoroutine);
_spawnCoroutine = null;
}
CancelInvoke(); // 停止所有 InvokeRepeating
}
private IEnumerator SpawnLoop()
{
while (true)
{
if (_spawnPoint != null) // 安全检查
{
// 使用对象池而非 Instantiate
var enemy = EnemyPool.Instance.Get();
enemy.transform.position = _spawnPoint.position;
}
yield return _spawnDelay; // 复用缓存的 WaitForSeconds
}
}
}Godot GDScript — 错误示例
# ❌ AI 常见错误:信号连接但不断开
extends Control
var target_enemy: Node
func _ready():
target_enemy = get_parent()
# 错误 1:连接信号但不在 _exit_tree 中断开
target_enemy.health_changed.connect(_on_health_changed)
target_enemy.died.connect(_on_enemy_died)
# 错误 2:连接全局信号
GameEvents.game_paused.connect(_on_game_paused)
# 错误 3:创建 Tween 但不跟踪
var tween = create_tween()
tween.tween_property(self, "modulate:a", 1.0, 0.5)
func _on_health_changed(percent: float):
# 错误 4:没有检查节点是否仍在树中
$HealthBar.value = percent
func _on_enemy_died():
# 错误 5:直接 queue_free 但信号仍然连接
queue_free()
func _on_game_paused():
passGodot GDScript — 正确示例
# ✅ 正确:完整的信号生命周期管理
extends Control
var _target_enemy: Node
var _active_tween: Tween
func _ready() -> void:
_target_enemy = get_parent()
_subscribe_signals()
func _subscribe_signals() -> void:
if _target_enemy == null:
return
# 使用 CONNECT_ONE_SHOT 或手动管理
if not _target_enemy.health_changed.is_connected(_on_health_changed):
_target_enemy.health_changed.connect(_on_health_changed)
if not _target_enemy.died.is_connected(_on_enemy_died):
_target_enemy.died.connect(_on_enemy_died)
if not GameEvents.game_paused.is_connected(_on_game_paused):
GameEvents.game_paused.connect(_on_game_paused)
func _unsubscribe_signals() -> void:
# 安全断开:先检查连接是否存在
if _target_enemy != null and is_instance_valid(_target_enemy):
if _target_enemy.health_changed.is_connected(_on_health_changed):
_target_enemy.health_changed.disconnect(_on_health_changed)
if _target_enemy.died.is_connected(_on_enemy_died):
_target_enemy.died.disconnect(_on_enemy_died)
if GameEvents.game_paused.is_connected(_on_game_paused):
GameEvents.game_paused.disconnect(_on_game_paused)
func _on_health_changed(percent: float) -> void:
# 安全检查:确保节点仍在场景树中
if not is_inside_tree():
return
var health_bar := get_node_or_null("HealthBar") as ProgressBar
if health_bar != null:
health_bar.value = percent
func _on_enemy_died() -> void:
_unsubscribe_signals() # 先断开信号
_kill_tweens() # 停止所有 Tween
queue_free() # 再释放
func _kill_tweens() -> void:
if _active_tween != null and _active_tween.is_valid():
_active_tween.kill()
_active_tween = null
func _on_game_paused() -> void:
pass
# 关键:退出场景树时清理
func _exit_tree() -> void:
_unsubscribe_signals()
_kill_tweens()Unreal C++ — 错误示例
// ❌ AI 常见错误:绑定委托但不解绑
void AHealthWidget::BeginPlay()
{
Super::BeginPlay();
// 错误 1:绑定但没有在 EndPlay 中解绑
if (AActor* Owner = GetOwner())
{
Owner->OnTakeAnyDamage.AddDynamic(this, &AHealthWidget::OnDamageTaken);
}
// 错误 2:绑定到全局委托
UGameplayStatics::GetGameMode(this)->OnGamePaused.AddDynamic(
this, &AHealthWidget::OnPaused);
// 错误 3:使用 Timer 但不清理
GetWorldTimerManager().SetTimer(
UpdateHandle, this, &AHealthWidget::UpdateDisplay, 0.1f, true);
}
// 错误 4:没有 EndPlay / BeginDestroy 清理Unreal C++ — 正确示例
// ✅ 正确:完整的委托生命周期管理
void AHealthWidget::BeginPlay()
{
Super::BeginPlay();
OwnerRef = GetOwner(); // 使用弱引用跟踪
if (OwnerRef.IsValid())
{
OwnerRef->OnTakeAnyDamage.AddDynamic(this, &AHealthWidget::OnDamageTaken);
}
GetWorldTimerManager().SetTimer(
UpdateTimerHandle, this, &AHealthWidget::UpdateDisplay, 0.1f, true);
}
void AHealthWidget::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
// 清理所有委托绑定
if (OwnerRef.IsValid())
{
OwnerRef->OnTakeAnyDamage.RemoveDynamic(this, &AHealthWidget::OnDamageTaken);
}
// 清理定时器
GetWorldTimerManager().ClearTimer(UpdateTimerHandle);
Super::EndPlay(EndPlayReason);
}
void AHealthWidget::OnDamageTaken(AActor* DamagedActor, float Damage,
const UDamageType* DamageType, AController* InstigatedBy, AActor* DamageCauser)
{
// 安全检查
if (!IsValid(this) || !IsValid(DamagedActor)) return;
UpdateDisplay();
}5.3 事件生命周期 Steering 规则
## 事件生命周期 Steering 规则
### 订阅/取消订阅配对原则
每一个事件订阅必须有对应的取消订阅:
- Unity: OnEnable ↔ OnDisable,或 Start ↔ OnDestroy
- Godot: _ready ↔ _exit_tree
- Unreal: BeginPlay ↔ EndPlay
### 安全检查规则
- 回调函数第一行必须检查 this/self 是否有效
- 访问其他对象前必须检查引用有效性:
- Unity: obj != null(Unity 重载了 == 运算符)
- Godot: is_instance_valid(obj) 和 is_inside_tree()
- Unreal: IsValid(obj) 或 TWeakObjectPtr
### 协程/Tween/Timer 清理规则
- 所有协程引用必须保存,在 OnDisable/OnDestroy 中停止
- 所有 Tween 必须在对象销毁前 Kill/kill
- 所有 Timer 必须在 EndPlay/_exit_tree 中清除
- 禁止使用"fire and forget"模式的异步操作
### 对象池场景的特殊规则
- 对象归还池时必须断开所有信号/事件连接
- 对象从池中取出时重新建立连接
- 使用 OnEnable/OnDisable(Unity)或自定义 activate/deactivate 方法6. 反模式四:输入处理差(Poor Input Handling)
6.1 问题概述
输入处理是游戏”手感”的核心,也是 AI 生成代码中最容易被忽视的领域。AI 通常能生成”功能正确”的输入代码——按下按键角色会移动——但缺少让游戏”感觉好”的关键细节:输入缓冲、Coyote Time、输入预测、平台适配、可重映射等。差的输入处理会让玩家觉得游戏”不跟手”、“迟钝”、“不精确”,即使其他方面都很优秀。
问题严重程度: 🟡 严重(直接影响玩家体验和留存率)
AI 输入处理常见问题清单:
| 问题 | 表现 | 影响 |
|---|---|---|
| 无输入缓冲 | 玩家必须精确到帧按键 | 动作游戏不可玩 |
| 无 Coyote Time | 平台边缘跳跃失败 | 平台跳跃体验极差 |
| 硬编码按键 | 无法重映射 | 无障碍性差,手柄不支持 |
| 输入在错误的回调中 | 物理帧中检测输入 | 输入丢失或延迟 |
| 无死区处理 | 摇杆漂移 | 角色自动移动 |
| 无平台适配 | 只支持键鼠 | 手柄/触屏用户无法游玩 |
| 输入与帧率耦合 | 高帧率输入过快 | 不同设备体验不一致 |
6.2 AI 生成的问题代码
Unity C# — 错误示例
// ❌ AI 常见错误:最基础的输入处理,缺少所有"手感"优化
public class PlayerController : MonoBehaviour
{
public float moveSpeed = 5f;
public float jumpForce = 10f;
private Rigidbody2D rb;
private bool isGrounded;
void Start()
{
rb = GetComponent<Rigidbody2D>();
}
void Update()
{
// 错误 1:硬编码按键(无法重映射,不支持手柄)
float moveX = 0;
if (Input.GetKey(KeyCode.A)) moveX = -1;
if (Input.GetKey(KeyCode.D)) moveX = 1;
// 错误 2:没有加速/减速曲线(移动感觉生硬)
rb.linearVelocity = new Vector2(moveX * moveSpeed, rb.linearVelocity.y);
// 错误 3:简单的地面检测 + 跳跃(无 Coyote Time,无输入缓冲)
isGrounded = Physics2D.Raycast(transform.position, Vector2.down, 1.1f);
if (Input.GetKeyDown(KeyCode.Space) && isGrounded)
{
rb.linearVelocity = new Vector2(rb.linearVelocity.x, jumpForce);
}
// 错误 4:没有处理跳跃高度可变(短按 vs 长按)
// 错误 5:没有处理摇杆死区
// 错误 6:没有输入缓冲(玩家稍早按跳跃就无效)
}
}Unity C# — 正确示例
// ✅ 正确:完整的输入处理系统(缓冲、Coyote Time、可变跳跃高度)
public class PlayerController : MonoBehaviour
{
[Header("Movement")]
[SerializeField] private float maxSpeed = 8f;
[SerializeField] private float acceleration = 50f;
[SerializeField] private float deceleration = 40f;
[SerializeField] private float airAcceleration = 25f;
[Header("Jump")]
[SerializeField] private float jumpForce = 12f;
[SerializeField] private float jumpCutMultiplier = 0.4f; // 短按跳跃高度衰减
[SerializeField] private float coyoteTime = 0.12f;
[SerializeField] private float jumpBufferTime = 0.1f;
[SerializeField] private float fallGravityMultiplier = 1.5f;
[Header("Ground Check")]
[SerializeField] private LayerMask groundLayer;
[SerializeField] private Vector2 groundCheckSize = new(0.8f, 0.05f);
[SerializeField] private Transform groundCheckPoint;
private Rigidbody2D _rb;
private PlayerInputActions _input; // 使用 Input System(可重映射)
// 状态
private float _coyoteTimer;
private float _jumpBufferTimer;
private bool _isGrounded;
private bool _isJumping;
private float _moveInput;
void Awake()
{
_rb = GetComponent<Rigidbody2D>();
_input = new PlayerInputActions();
}
void OnEnable()
{
_input.Enable();
_input.Gameplay.Jump.started += OnJumpStarted;
_input.Gameplay.Jump.canceled += OnJumpCanceled;
}
void OnDisable()
{
_input.Gameplay.Jump.started -= OnJumpStarted;
_input.Gameplay.Jump.canceled -= OnJumpCanceled;
_input.Disable();
}
// 输入事件回调(Input System 自动处理平台差异)
private void OnJumpStarted(UnityEngine.InputSystem.InputAction.CallbackContext ctx)
{
_jumpBufferTimer = jumpBufferTime; // 缓冲跳跃请求
}
private void OnJumpCanceled(UnityEngine.InputSystem.InputAction.CallbackContext ctx)
{
// 短按跳跃:释放按键时削减上升速度
if (_isJumping && _rb.linearVelocity.y > 0)
{
_rb.linearVelocity = new Vector2(_rb.linearVelocity.x,
_rb.linearVelocity.y * jumpCutMultiplier);
}
}
void Update()
{
// 读取移动输入(Input System 自动处理死区)
_moveInput = _input.Gameplay.Move.ReadValue<Vector2>().x;
// 更新计时器
_jumpBufferTimer -= Time.deltaTime;
// 地面检测
_isGrounded = Physics2D.OverlapBox(
groundCheckPoint.position, groundCheckSize, 0f, groundLayer);
if (_isGrounded)
{
_coyoteTimer = coyoteTime;
_isJumping = false;
}
else
{
_coyoteTimer -= Time.deltaTime;
}
}
void FixedUpdate()
{
// 水平移动:带加速/减速曲线
float targetSpeed = _moveInput * maxSpeed;
float accelRate = _isGrounded
? (Mathf.Abs(targetSpeed) > 0.01f ? acceleration : deceleration)
: airAcceleration;
float speedDiff = targetSpeed - _rb.linearVelocity.x;
float movement = speedDiff * accelRate * Time.fixedDeltaTime;
_rb.AddForce(Vector2.right * movement, ForceMode2D.Force);
// 跳跃:Coyote Time + 输入缓冲
if (_jumpBufferTimer > 0 && _coyoteTimer > 0)
{
_rb.linearVelocity = new Vector2(_rb.linearVelocity.x, jumpForce);
_jumpBufferTimer = 0;
_coyoteTimer = 0;
_isJumping = true;
}
// 下落加速(更好的跳跃手感)
if (_rb.linearVelocity.y < 0)
{
_rb.gravityScale = fallGravityMultiplier;
}
else
{
_rb.gravityScale = 1f;
}
}
}Godot GDScript — 错误示例
# ❌ AI 常见错误:最基础的输入,无手感优化
extends CharacterBody2D
var speed = 200.0
var jump_force = -400.0
func _physics_process(delta):
# 错误 1:硬编码按键
var direction = 0
if Input.is_key_pressed(KEY_LEFT):
direction = -1
if Input.is_key_pressed(KEY_RIGHT):
direction = 1
# 错误 2:瞬间达到最大速度(无加速曲线)
velocity.x = direction * speed
# 错误 3:简单重力,无下落加速
if not is_on_floor():
velocity.y += 980 * delta
# 错误 4:无 Coyote Time,无输入缓冲
if Input.is_key_pressed(KEY_SPACE) and is_on_floor():
velocity.y = jump_force
move_and_slide()Godot GDScript — 正确示例
# ✅ 正确:完整输入系统(Input Map + 缓冲 + Coyote Time + 加速曲线)
extends CharacterBody2D
@export_group("Movement")
@export var max_speed: float = 200.0
@export var acceleration: float = 1200.0
@export var deceleration: float = 1000.0
@export var air_acceleration: float = 600.0
@export_group("Jump")
@export var jump_force: float = 400.0
@export var jump_cut_multiplier: float = 0.4
@export var coyote_time: float = 0.12
@export var jump_buffer_time: float = 0.1
@export var fall_gravity_multiplier: float = 1.5
@export var base_gravity: float = 980.0
var _coyote_timer: float = 0.0
var _jump_buffer_timer: float = 0.0
var _is_jumping: bool = false
func _unhandled_input(event: InputEvent) -> void:
# 使用 Input Map Action(可在项目设置中重映射)
if event.is_action_pressed("jump"):
_jump_buffer_timer = jump_buffer_time
# 短按跳跃:释放时削减上升速度
if event.is_action_released("jump") and _is_jumping and velocity.y < 0:
velocity.y *= jump_cut_multiplier
func _physics_process(delta: float) -> void:
# 使用 Input Map 的 Action(自动支持键盘 + 手柄)
var input_dir := Input.get_axis("move_left", "move_right")
# 加速/减速曲线
var target_speed := input_dir * max_speed
var accel_rate: float
if is_on_floor():
accel_rate = acceleration if abs(input_dir) > 0.01 else deceleration
else:
accel_rate = air_acceleration
velocity.x = move_toward(velocity.x, target_speed, accel_rate * delta)
# 重力:下落时加速
if not is_on_floor():
var gravity_mult := fall_gravity_multiplier if velocity.y > 0 else 1.0
velocity.y += base_gravity * gravity_mult * delta
_coyote_timer -= delta
else:
_coyote_timer = coyote_time
_is_jumping = false
# 跳跃缓冲计时
_jump_buffer_timer -= delta
# 跳跃:Coyote Time + 输入缓冲
if _jump_buffer_timer > 0.0 and _coyote_timer > 0.0:
velocity.y = -jump_force
_jump_buffer_timer = 0.0
_coyote_timer = 0.0
_is_jumping = true
move_and_slide()6.3 输入处理 Steering 规则
## 输入处理 Steering 规则
### 输入抽象层规则
- 所有输入必须通过引擎的 Action 系统:
- Unity: Input System Package(InputAction)
- Godot: Input Map(项目设置中配置)
- Unreal: Enhanced Input System
- 禁止硬编码 KeyCode/KEY_* 常量
- 必须支持至少两种输入设备(键鼠 + 手柄)
### 手感优化必选项
动作类/平台跳跃类游戏必须实现:
1. **输入缓冲**:3-6 帧窗口(0.05-0.1s),缓存玩家的提前输入
2. **Coyote Time**:离开平台后 0.08-0.15s 仍可跳跃
3. **可变跳跃高度**:短按 vs 长按产生不同跳跃高度
4. **加速/减速曲线**:移动不应瞬间达到最大速度
5. **下落加速**:下落时重力倍增,提升跳跃手感
### 摇杆处理规则
- 必须实现死区(deadzone):内死区 0.1-0.2,外死区 0.9-0.95
- 死区内的输入值归零,死区外重新映射到 0-1
- 支持径向死区(radial deadzone)而非轴向死区
### 输入检测位置规则
- 按键检测(GetButtonDown/is_action_pressed):Update/_process/_unhandled_input
- 持续输入(GetAxis/get_axis):Update/_process 中读取,FixedUpdate/_physics_process 中应用
- 禁止在 FixedUpdate/_physics_process 中检测按键按下事件(可能丢失)7. 反模式五:渲染未优化(Unoptimized Rendering)
7.1 问题概述
渲染优化是游戏性能的最大瓶颈,也是 AI 最难自动处理的领域。AI 生成的场景代码通常”看起来正确”——物体显示在正确的位置——但完全不考虑 GPU 性能预算。常见问题包括:过多的 draw call(每个物体单独渲染)、严重的 overdraw(透明物体层层叠加)、复杂的 shader(片元着色器中使用分支和循环)、未使用 LOD(远处物体和近处物体使用相同精度)。
问题严重程度: 🔴 致命(直接导致帧率不达标)
渲染性能预算参考:
| 平台 | 目标帧率 | Draw Call 预算 | 三角形预算 | 纹理内存 |
|---|---|---|---|---|
| 高端 PC | 60-144fps | 2000-5000 | 2-10M | 4-8 GB |
| 中端 PC | 60fps | 1000-2000 | 1-3M | 2-4 GB |
| 移动端(高端) | 60fps | 100-300 | 100-500K | 512MB-1GB |
| 移动端(中端) | 30fps | 50-150 | 50-200K | 256-512MB |
| WebGL | 30-60fps | 100-500 | 100K-1M | 256MB-1GB |
| Switch | 30fps | 200-500 | 200K-1M | 1-2 GB |
7.2 AI 生成的问题代码
Unity C# — 错误示例
// ❌ AI 常见错误:不考虑渲染性能的场景生成
public class ForestGenerator : MonoBehaviour
{
public GameObject treePrefab;
public GameObject grassPrefab;
public Material[] treeMaterials; // 10 种不同材质
void Start()
{
// 错误 1:每棵树使用不同材质(无法合批,每棵树 = 1 draw call)
for (int i = 0; i < 1000; i++)
{
Vector3 pos = new Vector3(
Random.Range(-100f, 100f), 0,
Random.Range(-100f, 100f));
GameObject tree = Instantiate(treePrefab, pos, Quaternion.identity);
// 每棵树随机材质 → 1000 个 draw call!
tree.GetComponent<Renderer>().material = treeMaterials[Random.Range(0, treeMaterials.Length)];
}
// 错误 2:大量小物体单独实例化(草地)
for (int i = 0; i < 10000; i++)
{
Vector3 pos = new Vector3(
Random.Range(-100f, 100f), 0,
Random.Range(-100f, 100f));
// 10000 个独立 GameObject = 10000 draw calls!
Instantiate(grassPrefab, pos, Quaternion.identity);
}
// 错误 3:没有 LOD,没有遮挡剔除,没有视锥剔除
// 错误 4:没有静态合批标记
}
}
// ❌ AI 常见错误:低效的 Shader
// 错误 5:片元着色器中使用 if-else 分支
Shader "Custom/BadShader"
{
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
fixed4 frag(v2f i) : SV_Target
{
// 错误:GPU 分支导致 warp 分歧
if (i.uv.x > 0.5)
{
return tex2D(_MainTex, i.uv) * _Color1;
}
else
{
return tex2D(_MainTex, i.uv) * _Color2;
}
}
ENDCG
}
}
}Unity C# — 正确示例
// ✅ 正确:GPU Instancing + LOD + 合批优化
public class ForestGenerator : MonoBehaviour
{
[SerializeField] private Mesh treeMesh;
[SerializeField] private Material treeMaterial; // 单一材质,启用 GPU Instancing
[SerializeField] private Mesh[] treeLODMeshes; // LOD 0/1/2
[SerializeField] private Mesh grassMesh;
[SerializeField] private Material grassMaterial;
private Matrix4x4[][] _treeMatrices; // 分批(每批最多 1023 个)
private Matrix4x4[][] _grassMatrices;
void Start()
{
GenerateTrees();
GenerateGrass();
}
void GenerateTrees()
{
// 使用 GPU Instancing:1000 棵树 = 1 draw call!
const int batchSize = 1023; // Unity GPU Instancing 单批上限
const int treeCount = 1000;
int batchCount = Mathf.CeilToInt((float)treeCount / batchSize);
_treeMatrices = new Matrix4x4[batchCount][];
for (int batch = 0; batch < batchCount; batch++)
{
int count = Mathf.Min(batchSize, treeCount - batch * batchSize);
_treeMatrices[batch] = new Matrix4x4[count];
for (int i = 0; i < count; i++)
{
Vector3 pos = new Vector3(
Random.Range(-100f, 100f), 0,
Random.Range(-100f, 100f));
float scale = Random.Range(0.8f, 1.2f);
float rotation = Random.Range(0f, 360f);
_treeMatrices[batch][i] = Matrix4x4.TRS(
pos,
Quaternion.Euler(0, rotation, 0),
Vector3.one * scale
);
}
}
}
void GenerateGrass()
{
// 草地使用 GPU Instancing(10000 根草 = ~10 draw calls)
const int batchSize = 1023;
const int grassCount = 10000;
int batchCount = Mathf.CeilToInt((float)grassCount / batchSize);
_grassMatrices = new Matrix4x4[batchCount][];
for (int batch = 0; batch < batchCount; batch++)
{
int count = Mathf.Min(batchSize, grassCount - batch * batchSize);
_grassMatrices[batch] = new Matrix4x4[count];
for (int i = 0; i < count; i++)
{
Vector3 pos = new Vector3(
Random.Range(-100f, 100f), 0,
Random.Range(-100f, 100f));
_grassMatrices[batch][i] = Matrix4x4.TRS(
pos, Quaternion.identity, Vector3.one * 0.5f);
}
}
}
void Update()
{
// GPU Instancing 渲染:极少的 draw call
foreach (var batch in _treeMatrices)
{
Graphics.DrawMeshInstanced(treeMesh, 0, treeMaterial, batch);
}
foreach (var batch in _grassMatrices)
{
Graphics.DrawMeshInstanced(grassMesh, 0, grassMaterial, batch);
}
}
}
// ✅ 正确:无分支 Shader(使用 step/lerp 替代 if-else)
Shader "Custom/GoodShader"
{
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_instancing // 启用 GPU Instancing
fixed4 frag(v2f i) : SV_Target
{
fixed4 texColor = tex2D(_MainTex, i.uv);
// 无分支:使用 step + lerp 替代 if-else
float mask = step(0.5, i.uv.x);
return texColor * lerp(_Color1, _Color2, mask);
}
ENDCG
}
}
}Godot GDScript — 错误示例
# ❌ AI 常见错误:大量独立节点,无合批
extends Node3D
func _ready():
# 错误 1:1000 个独立 MeshInstance3D(1000 draw calls)
for i in 1000:
var mesh_instance = MeshInstance3D.new()
mesh_instance.mesh = preload("res://tree.tres")
# 错误 2:每个实例使用独立材质副本
var mat = preload("res://tree_material.tres").duplicate()
mat.albedo_color = Color(randf(), randf(), randf())
mesh_instance.material_override = mat
mesh_instance.position = Vector3(
randf_range(-100, 100), 0, randf_range(-100, 100))
add_child(mesh_instance)
# 错误 3:透明粒子过多(overdraw 爆炸)
for i in 50:
var particles = GPUParticles3D.new()
particles.amount = 200 # 50 * 200 = 10000 透明粒子!
particles.position = Vector3(randf_range(-50, 50), 5, randf_range(-50, 50))
add_child(particles)Godot GDScript — 正确示例
# ✅ 正确:MultiMesh + LOD + 粒子优化
extends Node3D
@export var tree_mesh: Mesh
@export var tree_material: Material # 单一共享材质
@export var tree_count: int = 1000
func _ready():
_generate_trees()
_setup_particles()
func _generate_trees() -> void:
# MultiMeshInstance3D:1000 棵树 = 1 draw call!
var multi_mesh := MultiMesh.new()
multi_mesh.transform_format = MultiMesh.TRANSFORM_3D
multi_mesh.instance_count = tree_count
multi_mesh.mesh = tree_mesh
# 启用颜色变化(无需独立材质)
multi_mesh.use_colors = true
for i in tree_count:
var pos := Vector3(
randf_range(-100, 100), 0, randf_range(-100, 100))
var scale := randf_range(0.8, 1.2)
var rotation := randf_range(0, TAU)
var transform := Transform3D()
transform = transform.rotated(Vector3.UP, rotation)
transform = transform.scaled(Vector3.ONE * scale)
transform.origin = pos
multi_mesh.set_instance_transform(i, transform)
# 颜色变化通过实例颜色实现(零额外 draw call)
multi_mesh.set_instance_color(i,
Color(randf_range(0.7, 1.0), randf_range(0.8, 1.0), randf_range(0.7, 1.0)))
var mmi := MultiMeshInstance3D.new()
mmi.multimesh = multi_mesh
mmi.material_override = tree_material
# 启用 LOD
mmi.lod_bias = 1.0
mmi.visibility_range_end = 150.0
mmi.visibility_range_fade_mode = GeometryInstance3D.VISIBILITY_RANGE_FADE_SELF
add_child(mmi)
func _setup_particles() -> void:
# 合并粒子系统:用少量高效粒子替代大量低效粒子
var particles := GPUParticles3D.new()
particles.amount = 500 # 单个系统,合理数量
particles.visibility_aabb = AABB(Vector3(-60, 0, -60), Vector3(120, 20, 120))
# 设置 LOD:远处减少粒子
particles.visibility_range_end = 80.0
add_child(particles)Unreal C++ — 错误示例
// ❌ AI 常见错误:每个物体独立 Actor
void AForestGenerator::BeginPlay()
{
Super::BeginPlay();
// 错误:1000 个独立 Actor(巨大的 CPU 开销 + draw call)
for (int32 i = 0; i < 1000; i++)
{
FVector Location(FMath::RandRange(-10000.f, 10000.f),
FMath::RandRange(-10000.f, 10000.f), 0.f);
FActorSpawnParameters Params;
GetWorld()->SpawnActor<ATreeActor>(TreeActorClass, Location,
FRotator::ZeroRotator, Params);
}
}Unreal C++ — 正确示例
// ✅ 正确:使用 Instanced Static Mesh / HISM
void AForestGenerator::BeginPlay()
{
Super::BeginPlay();
// Hierarchical Instanced Static Mesh:自动 LOD + 遮挡剔除
UHierarchicalInstancedStaticMeshComponent* HISM =
NewObject<UHierarchicalInstancedStaticMeshComponent>(this);
HISM->SetStaticMesh(TreeMesh);
HISM->SetMaterial(0, TreeMaterial);
HISM->SetCullDistances(5000.f, 15000.f); // LOD 距离
HISM->RegisterComponent();
TArray<FTransform> Transforms;
Transforms.Reserve(1000);
for (int32 i = 0; i < 1000; i++)
{
FVector Location(FMath::RandRange(-10000.f, 10000.f),
FMath::RandRange(-10000.f, 10000.f), 0.f);
float Scale = FMath::RandRange(0.8f, 1.2f);
float Rotation = FMath::RandRange(0.f, 360.f);
FTransform Transform;
Transform.SetLocation(Location);
Transform.SetRotation(FQuat(FRotator(0.f, Rotation, 0.f)));
Transform.SetScale3D(FVector(Scale));
Transforms.Add(Transform);
}
// 批量添加实例(比逐个添加高效得多)
HISM->AddInstances(Transforms, false);
}7.3 渲染优化 Steering 规则
## 渲染优化 Steering 规则
### Draw Call 控制规则
- 大量相同物体必须使用 Instancing:
- Unity: GPU Instancing / Graphics.DrawMeshInstanced
- Godot: MultiMeshInstance3D
- Unreal: (H)ISM Component
- 静态物体必须标记为静态(启用静态合批/烘焙)
- 共享材质:相同外观的物体必须使用同一材质实例
- 禁止在运行时 .material(Unity)创建材质副本(使用 .sharedMaterial)
### Overdraw 控制规则
- 透明物体数量严格限制(移动端 < 20 层)
- 粒子系统必须设置合理的 maxParticles 和可见范围
- UI 必须分层:静态背景层 + 动态内容层
- 全屏后处理效果在移动端最多 2-3 个
### LOD 规则
- 所有 3D 模型必须配置 LOD(至少 3 级):
- LOD0:近距离,完整细节
- LOD1:中距离,50% 三角形
- LOD2:远距离,10-25% 三角形
- 粒子系统必须设置可见范围(远处不渲染)
- 阴影必须设置级联距离和分辨率
### Shader 规则
- 片元着色器禁止 if-else 分支(使用 step/lerp/smoothstep)
- 禁止在片元着色器中采样超过 4 张纹理(移动端)
- 使用 shader_feature / multi_compile 替代运行时分支
- 移动端优先使用 half/fixed 精度而非 float8. 引擎专用 Steering 规则
8.1 Unity C# 专用 Steering 规则
文件路径:.kiro/steering/unity-csharp.md
---
trigger: auto
match: "**/*.cs"
---
# Unity C# Steering 规则
## 项目配置
- Unity 版本:6000.x LTS
- 渲染管线:URP(Universal Render Pipeline)
- 脚本后端:IL2CPP(发布构建)
- .NET 版本:.NET Standard 2.1 / .NET 6+
- 输入系统:Input System Package(非旧版 Input Manager)
## MonoBehaviour 生命周期规则
- Awake():获取自身组件引用(GetComponent)
- OnEnable():订阅事件、注册到管理器
- Start():获取外部引用、初始化状态
- Update():输入检测、UI 更新、非物理逻辑
- FixedUpdate():物理计算、刚体操作
- LateUpdate():摄像机跟随、最终位置调整
- OnDisable():取消事件订阅、从管理器注销
- OnDestroy():释放非托管资源、清理对象池
## 禁止清单(Unity 特有)
- 禁止 FindObjectOfType / FindObjectsOfType(使用注册表模式)
- 禁止 SendMessage / BroadcastMessage(使用直接引用或事件)
- 禁止 GameObject.Find(使用序列化引用或依赖注入)
- 禁止 Resources.Load(使用 Addressables)
- 禁止 .material 属性(使用 .sharedMaterial 或 MaterialPropertyBlock)
- 禁止 Camera.main 每帧调用(缓存引用)
- 禁止 CompareTag 以外的 tag 比较(.tag == "xxx" 产生 GC)
- 禁止 foreach 遍历 Dictionary(使用 for + GetEnumerator 或直接遍历 Keys/Values)
## 推荐模式
- 对象池:UnityEngine.Pool.ObjectPool<T>(Unity 内置)
- 事件系统:C# event / UnityEvent(简单场景)/ ScriptableObject 事件通道(解耦场景)
- 单例:使用 ScriptableObject 单例模式(非 MonoBehaviour 单例)
- 序列化:[SerializeField] private 替代 public 字段
- 协程替代:UniTask(零 GC 异步)或 Awaitable(Unity 6 内置)
## 性能分析命令
- 打开 Profiler:Window > Analysis > Profiler
- 打开 Frame Debugger:Window > Analysis > Frame Debugger
- 内存快照:Window > Analysis > Memory Profiler
- GPU 分析:Profiler > GPU 模块
## 测试规则
- 单元测试:使用 Unity Test Framework(NUnit)
- 测试文件放在 Tests/EditMode/ 和 Tests/PlayMode/
- 禁止在测试中使用 MonoBehaviour(使用纯 C# 类测试逻辑)
- PlayMode 测试用于集成测试(需要 Unity 生命周期的场景)8.2 Godot GDScript 专用 Steering 规则
文件路径:.kiro/steering/godot-gdscript.md
---
trigger: auto
match: "**/*.gd"
---
# Godot GDScript Steering 规则
## 项目配置
- Godot 版本:4.4+
- 渲染器:Forward+(PC)/ Mobile(移动端)/ Compatibility(WebGL)
- GDScript 版本:2.0(静态类型推荐)
- 物理引擎:Godot Physics / Jolt Physics
## 类型标注规则(强制)
- 所有函数参数必须标注类型
- 所有返回值必须标注类型
- 所有成员变量必须标注类型
- 使用 @export 替代 export(GDScript 2.0 语法)
- 使用 := 进行类型推断赋值
```gdscript
# ✅ 正确:完整类型标注
var _speed: float = 5.0
var _health: int = 100
var _enemies: Array[Enemy] = []
func take_damage(amount: int, source: Node3D) -> bool:
_health -= amount
return _health <= 0
# ❌ 错误:无类型标注
var speed = 5.0
var health = 100
func take_damage(amount, source):
health -= amount
return health <= 0节点生命周期规则
- _ready():初始化、获取子节点引用、连接信号
- _enter_tree():注册到全局系统
- _exit_tree():断开所有信号、停止 Tween、从全局系统注销
- _process(delta):非物理逻辑、输入检测、UI 更新
- _physics_process(delta):物理移动、碰撞响应
- _unhandled_input(event):游戏输入处理(优先于 _input)
禁止清单(Godot 特有)
- 禁止 get_node() 使用字符串路径访问远距离节点(使用 @export 或 group)
- 禁止 $NodeName 访问可能不存在的节点(使用 get_node_or_null)
- 禁止在 _process 中调用 get_tree().get_nodes_in_group()(缓存结果)
- 禁止 preload() 大型资源在脚本顶部(使用 ResourceLoader.load_threaded_request)
- 禁止 queue_free() 前不断开信号连接
- 禁止使用 String 作为信号名(使用 &“signal_name” StringName)
推荐模式
- 信号:自定义 signal 用于组件间通信
- 自动加载:全局管理器使用 AutoLoad(项目设置)
- 资源:使用 Resource 类存储配置数据(类似 Unity ScriptableObject)
- 对象池:自定义 Array 池 + visible/process 控制
- 场景切换:使用 ResourceLoader 异步加载 + 加载屏幕
性能分析
- 内置 Profiler:Debugger > Profiler
- 监视器:Debugger > Monitors(FPS、物理、内存)
- 远程场景树:Debugger > Remote(运行时检查节点树)
### 8.3 Unreal C++ 专用 Steering 规则
文件路径:`.kiro/steering/unreal-cpp.md`
```markdown
---
trigger: auto
match: ["**/*.cpp", "**/*.h", "**/*.hpp"]
---
# Unreal Engine C++ Steering 规则
## 项目配置
- Unreal 版本:5.5+
- 构建系统:UnrealBuildTool
- C++ 标准:C++20(UE5 默认)
- 输入系统:Enhanced Input System
- 网络:Unreal Replication System
## 内存管理规则(UE 特有)
- 使用 UPROPERTY() 标记所有 UObject 指针(GC 追踪)
- 非 UObject 使用 TSharedPtr / TWeakPtr / TUniquePtr
- 禁止 raw new/delete 管理 UObject(使用 NewObject / CreateDefaultSubobject)
- 使用 TWeakObjectPtr 引用可能被销毁的 Actor
- 大型数组使用 TArray::Reserve 预分配
- 字符串操作使用 FName(哈希比较)而非 FString(字符比较)
## Actor 生命周期规则
- Constructor:创建组件(CreateDefaultSubobject),设置默认值
- BeginPlay():初始化运行时状态、绑定委托、获取外部引用
- Tick(DeltaTime):每帧逻辑(尽量少用,优先用 Timer)
- EndPlay():清理委托绑定、停止 Timer、释放资源
- BeginDestroy():释放非 UObject 资源
## 禁止清单(UE 特有)
- 禁止在 Tick 中使用 FindObject / GetAllActorsOfClass(缓存或使用 Subsystem)
- 禁止 FString 在热路径中拼接(使用 FName 或 FText::Format)
- 禁止 SpawnActor 大量相同 Actor(使用 HISM / ISM)
- 禁止在游戏线程外访问 UObject(使用 AsyncTask(ENamedThreads::GameThread, ...))
- 禁止忽略 UPROPERTY() 标记(导致 GC 提前回收)
- 禁止在 Header 中 #include 大型头文件(使用前向声明)
## 推荐模式
- 组件模式:功能拆分到 UActorComponent / USceneComponent
- 委托:FMulticastDelegate / Dynamic Delegate(蓝图兼容)
- 数据驱动:UDataAsset / UDataTable 存储配置
- 对象池:自定义 TArray 池 + Activate/Deactivate
- 异步加载:FStreamableManager::RequestAsyncLoad
- Subsystem:UGameInstanceSubsystem / UWorldSubsystem(全局服务)
## 构建规则
- 开发构建:DebugGame Editor
- 性能测试:Development(非 Debug,Debug 禁用优化)
- 发布构建:Shipping(移除所有调试代码)
- 使用 SHIPPING_BUILD 宏保护调试代码
## 性能分析
- Unreal Insights:运行时性能追踪
- Stat 命令:stat fps / stat unit / stat scenerendering
- GPU Visualizer:ProfileGPU 命令
- 内存:stat memory / memreport -full9. 完整 Steering 规则提示词模板
9.1 AI 编码助手游戏开发 Prompt 模板
以下提示词模板用于在与 AI 编码助手交互时,确保生成的游戏代码符合最佳实践:
模板 1:新功能开发
你正在为一个 [Unity 6 / Godot 4.4 / Unreal 5.5] 项目编写代码。
## 技术约束
- 目标帧率:[60fps]
- 目标平台:[PC + Mobile]
- 当前场景 draw call 预算:[剩余 200]
## 功能需求
[描述你需要的功能]
## 必须遵守的规则
1. 物理逻辑必须在 [FixedUpdate / _physics_process / Tick] 中
2. 禁止在每帧回调中分配堆内存(使用对象池/预分配)
3. 所有事件订阅必须有对应的取消订阅
4. 输入必须通过 [Input System / Input Map / Enhanced Input] 抽象层
5. 大量相同物体必须使用 [GPU Instancing / MultiMesh / HISM]
## 输出要求
- 提供完整代码,包含初始化和清理逻辑
- 标注哪些代码在热路径中
- 说明 draw call 和内存影响模板 2:性能审查
请审查以下 [Unity C# / GDScript / UE C++] 游戏代码的性能问题:
```[语言]
[粘贴代码]审查清单
- 热路径中是否有堆内存分配?(new、字符串拼接、LINQ、Lambda)
- 物理逻辑是否在正确的回调函数中?
- 组件/节点引用是否已缓存?
- 事件/信号是否有完整的订阅-取消订阅配对?
- 是否有不必要的每帧计算?(可以用脏标记/节流替代)
- 渲染方面:draw call 是否可以通过合批/Instancing 减少?
- 是否有 GC 友好的替代方案?
输出格式
对每个问题:
- 🔴 严重 / 🟡 警告 / 🟢 建议
- 问题代码行号
- 修复方案(含代码)
- 预估性能影响
#### 模板 3:反模式修复
以下代码存在 [物理不稳定 / 内存泄漏 / 事件生命周期错误 / 输入问题 / 渲染性能] 问题。
[粘贴问题代码]请修复这段代码,要求:
- 保持功能不变
- 修复所有性能和正确性问题
- 添加必要的清理逻辑
- 使用引擎推荐的最佳实践
- 在注释中标注每处修改的原因
引擎版本
[Unity 6 / Godot 4.4 / Unreal 5.5]
目标平台
[PC / Mobile / Console]
### 9.2 AI 代码审查自动化 Steering 规则
文件路径:`.kiro/steering/game-code-review.md`
```markdown
---
trigger: manual
---
# 游戏代码审查 Steering 规则
当审查游戏代码时,按以下优先级检查:
## P0 — 必须修复(阻塞合并)
- [ ] 物理逻辑在错误的回调函数中(Update 而非 FixedUpdate)
- [ ] 热路径中有堆内存分配(new、字符串拼接、LINQ)
- [ ] 事件订阅没有对应的取消订阅
- [ ] 直接修改 Transform 移动物理对象
- [ ] 对象销毁前没有清理引用和回调
- [ ] 每帧调用 Find/GetNode 等搜索方法
## P1 — 应该修复(下个迭代)
- [ ] 缺少对象池(频繁创建/销毁的对象)
- [ ] 缺少输入缓冲和 Coyote Time(动作游戏)
- [ ] 大量相同物体未使用 Instancing
- [ ] 缺少 LOD 配置
- [ ] UI 每帧更新(无脏标记)
- [ ] 硬编码按键(未使用 Input Action)
## P2 — 建议优化(有空再做)
- [ ] 使用平方距离替代 Distance()
- [ ] 射线检测使用 NonAlloc 版本
- [ ] 协程 WaitForSeconds 未缓存
- [ ] 可以用 StringName 替代 String
- [ ] 可以用 MaterialPropertyBlock 替代材质副本10. 实战案例:用 AI 开发 2D 平台跳跃游戏的 Steering 规则实践
10.1 项目背景
项目:一款 2D 像素风平台跳跃游戏(类似 Celeste 风格) 引擎:Godot 4.4 团队:1 人独立开发者 + AI 编码助手(Claude Code + Kiro) 目标平台:PC + Nintendo Switch + Mobile 开发周期:8 周
10.2 Steering 规则配置
开发者在项目初期配置了以下 Steering 规则结构:
pixel-platformer/
├── CLAUDE.md # 全局规则
├── .kiro/
│ ├── steering/
│ │ ├── game-general.md # 通用游戏规则(always)
│ │ ├── godot-gdscript.md # GDScript 规则(auto: *.gd)
│ │ ├── physics-platformer.md # 平台跳跃物理规则(manual)
│ │ └── mobile-optimization.md # 移动端优化规则(manual)
│ └── specs/
│ └── player-controller/
│ ├── requirements.md
│ ├── design.md
│ └── tasks.md
├── project.godot
└── ...10.3 开发过程中遇到的问题与解决
问题 1:AI 生成的角色控制器物理抖动
AI 第一次生成的代码:
# AI 生成 v1 — 存在物理抖动
extends CharacterBody2D
func _process(delta): # ← 错误:应该用 _physics_process
var direction = Input.get_axis("move_left", "move_right")
velocity.x = direction * 200.0
if not is_on_floor():
velocity.y += 980 * delta
if Input.is_action_just_pressed("jump") and is_on_floor():
velocity.y = -400
move_and_slide()Steering 规则拦截后 AI 重新生成:
开发者在 physics-platformer.md 中添加了明确规则后,AI 自动修正:
# AI 生成 v2 — Steering 规则生效后
extends CharacterBody2D
@export_group("Movement")
@export var max_speed: float = 200.0
@export var acceleration: float = 1500.0
@export var friction: float = 1200.0
@export var air_friction: float = 600.0
@export_group("Jump")
@export var jump_velocity: float = -400.0
@export var jump_cut_multiplier: float = 0.4
@export var coyote_time: float = 0.1
@export var jump_buffer_time: float = 0.08
@export var gravity: float = 980.0
@export var fall_gravity_multiplier: float = 1.6
@export var max_fall_speed: float = 600.0
var _coyote_timer: float = 0.0
var _jump_buffer_timer: float = 0.0
var _was_on_floor: bool = false
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("jump"):
_jump_buffer_timer = jump_buffer_time
if event.is_action_released("jump") and velocity.y < 0:
velocity.y *= jump_cut_multiplier
func _physics_process(delta: float) -> void:
var was_on_floor := is_on_floor()
# 重力(下落加速)
if not is_on_floor():
var grav_mult := fall_gravity_multiplier if velocity.y > 0 else 1.0
velocity.y = minf(velocity.y + gravity * grav_mult * delta, max_fall_speed)
# Coyote Time
if was_on_floor and not is_on_floor() and velocity.y >= 0:
_coyote_timer = coyote_time
if is_on_floor():
_coyote_timer = coyote_time
else:
_coyote_timer -= delta
# 跳跃缓冲
_jump_buffer_timer -= delta
# 跳跃执行
if _jump_buffer_timer > 0 and _coyote_timer > 0:
velocity.y = jump_velocity
_jump_buffer_timer = 0.0
_coyote_timer = 0.0
# 水平移动(加速/减速曲线)
var direction := Input.get_axis("move_left", "move_right")
if abs(direction) > 0.01:
var accel := acceleration if is_on_floor() else air_friction
velocity.x = move_toward(velocity.x, direction * max_speed, accel * delta)
else:
var fric := friction if is_on_floor() else air_friction
velocity.x = move_toward(velocity.x, 0.0, fric * delta)
move_and_slide()问题 2:敌人生成系统 GC 卡顿
AI 第一次生成的代码导致每 3-5 秒出现一次明显卡顿(GC spike)。
开发者使用 Godot Profiler 发现问题后,在 Steering 规则中添加了对象池要求。AI 重新生成了使用对象池的版本,GC 卡顿完全消除。
性能对比:
| 指标 | 无 Steering 规则 | 有 Steering 规则 |
|---|---|---|
| 平均帧率 | 45fps(移动端) | 60fps(稳定) |
| GC 频率 | 每 3-5 秒一次 | 几乎无 GC |
| Draw Call | 850(50 个敌人) | 12(MultiMesh) |
| 内存增长 | 2MB/分钟 | 稳定 |
| 输入延迟 | 2-3 帧 | < 1 帧 |
问题 3:场景切换崩溃
AI 生成的场景切换代码没有清理事件订阅,导致切换到新场景后旧场景的回调仍在触发,引发空引用崩溃。
解决方案:在 Steering 规则中添加了”场景切换安全”章节,要求所有节点在 _exit_tree() 中断开信号连接。AI 后续生成的代码自动包含了完整的清理逻辑。
10.4 Steering 规则效果总结
| 维度 | 无 Steering 规则 | 有 Steering 规则 | 改善幅度 |
|---|---|---|---|
| AI 生成代码一次通过率 | ~35% | ~80% | +45% |
| 物理相关 Bug | 12 个/周 | 1-2 个/周 | -85% |
| 性能问题 | 8 个/周 | 1 个/周 | -87% |
| 崩溃/空引用 | 5 个/周 | 0-1 个/周 | -90% |
| 代码审查修改量 | 60% 需重写 | 15% 需微调 | -75% |
| 开发速度 | 基准 | 2.5x | +150% |
10.5 关键经验
- Steering 规则越具体越好:不要写”注意性能”,要写”禁止在 _process 中使用 new”
- 分层规则比单一大文件更有效:通用规则 + 引擎规则 + 功能规则的三层结构
- 包含代码示例:在 Steering 规则中直接给出正确和错误的代码对比
- 持续迭代:每次发现 AI 犯的新错误,立即添加到 Steering 规则中
- 平台差异规则单独管理:移动端优化规则作为 manual trigger,在需要时激活
11. 额外反模式补充
11.1 反模式六:状态管理混乱
AI 生成的游戏代码经常使用嵌套 if-else 管理游戏状态,导致代码难以维护和扩展。
错误示例(Unity C#)
// ❌ 嵌套 if-else 管理状态
void Update()
{
if (isAlive)
{
if (isAttacking)
{
if (isGrounded)
{
// 地面攻击逻辑
}
else
{
// 空中攻击逻辑
}
}
else if (isDashing)
{
// 冲刺逻辑
}
else
{
if (isGrounded)
{
// 地面移动
}
else
{
// 空中移动
}
}
}
else
{
// 死亡逻辑
}
}正确示例(Unity C# — 状态机)
// ✅ 使用状态机管理
public abstract class PlayerState
{
protected PlayerController Player;
public virtual void Enter() { }
public virtual void Exit() { }
public virtual void Update() { }
public virtual void FixedUpdate() { }
public virtual PlayerState CheckTransitions() => null;
}
public class IdleState : PlayerState
{
public override void Update()
{
// 仅处理 Idle 状态的逻辑
}
public override PlayerState CheckTransitions()
{
if (!Player.IsGrounded) return new FallState();
if (Player.MoveInput.sqrMagnitude > 0.01f) return new RunState();
if (Player.JumpRequested) return new JumpState();
return null;
}
}
// PlayerController 中:
void Update()
{
_currentState.Update();
var newState = _currentState.CheckTransitions();
if (newState != null)
{
_currentState.Exit();
_currentState = newState;
_currentState.Enter();
}
}正确示例(Godot GDScript — 状态机)
# ✅ Godot 状态机模式
# state_machine.gd
extends Node
class_name StateMachine
@export var initial_state: State
var current_state: State
func _ready() -> void:
for child in get_children():
if child is State:
child.state_machine = self
if initial_state:
initial_state.enter()
current_state = initial_state
func _unhandled_input(event: InputEvent) -> void:
current_state.handle_input(event)
func _process(delta: float) -> void:
current_state.update(delta)
func _physics_process(delta: float) -> void:
current_state.physics_update(delta)
func transition_to(target_state: State) -> void:
current_state.exit()
current_state = target_state
current_state.enter()
# state.gd
extends Node
class_name State
var state_machine: StateMachine
func enter() -> void: pass
func exit() -> void: pass
func handle_input(_event: InputEvent) -> void: pass
func update(_delta: float) -> void: pass
func physics_update(_delta: float) -> void: pass11.2 反模式七:音频管理缺失
AI 几乎从不主动生成音频管理代码,导致音效重叠播放、音量不可控、音频资源未释放。
错误示例
// ❌ 每次播放都创建新的 AudioSource
void OnCollisionEnter(Collision collision)
{
// 错误:可能同时播放 100 个相同音效
AudioSource.PlayClipAtPoint(hitSound, transform.position);
}正确示例
// ✅ 音频池 + 冷却时间 + 音量管理
public class SFXManager : MonoBehaviour
{
public static SFXManager Instance { get; private set; }
[SerializeField] private int poolSize = 16;
private AudioSource[] _pool;
private int _poolIndex;
private readonly Dictionary<AudioClip, float> _lastPlayTime = new();
private const float MIN_INTERVAL = 0.05f; // 同一音效最小间隔
void Awake()
{
Instance = this;
_pool = new AudioSource[poolSize];
for (int i = 0; i < poolSize; i++)
{
var go = new GameObject($"SFX_{i}");
go.transform.SetParent(transform);
_pool[i] = go.AddComponent<AudioSource>();
_pool[i].playOnAwake = false;
}
}
public void Play(AudioClip clip, Vector3 position, float volume = 1f)
{
if (clip == null) return;
// 冷却检查:防止同一音效重叠
if (_lastPlayTime.TryGetValue(clip, out float lastTime)
&& Time.unscaledTime - lastTime < MIN_INTERVAL)
return;
var source = _pool[_poolIndex];
_poolIndex = (_poolIndex + 1) % poolSize;
source.transform.position = position;
source.clip = clip;
source.volume = volume;
source.Play();
_lastPlayTime[clip] = Time.unscaledTime;
}
}11.3 反模式八:缺少平台适配考虑
AI 生成的代码通常只在编辑器/PC 上测试通过,不考虑移动端和主机的差异。
常见平台适配问题:
| 问题 | PC 表现 | 移动端表现 | 解决方案 |
|---|---|---|---|
| 高分辨率纹理 | 正常 | 内存溢出崩溃 | 纹理压缩 + 分辨率适配 |
| 复杂 Shader | 流畅 | 严重掉帧 | 移动端专用简化 Shader |
| 大量粒子 | 正常 | GPU 过载 | 平台差异化粒子数量 |
| 后处理效果 | 流畅 | 帧率减半 | 移动端禁用/简化后处理 |
| 物理精度 | 正常 | 浮点精度问题 | 使用 fixed-point 或限制世界大小 |
| 输入方式 | 键鼠 | 触屏 | 抽象输入层 + 虚拟摇杆 |
平台适配 Steering 规则片段:
## 平台适配规则
- 所有资源必须提供多分辨率版本(1x/2x/4x)
- Shader 必须提供移动端变体(使用 shader_feature)
- 粒子数量必须根据平台动态调整:
- PC: 100%
- Mobile High: 50%
- Mobile Low: 25%
- 后处理效果必须可按平台开关
- 世界坐标不超过 ±10000 单位(浮点精度)避坑指南
❌ 常见错误
-
在 Update/_process 中写物理逻辑
- 问题:帧率不同导致物理行为不一致,高帧率移动快、低帧率移动慢
- 正确做法:物理逻辑放在 FixedUpdate/_physics_process 中,使用固定时间步
-
每帧分配堆内存(new、字符串拼接、LINQ)
- 问题:频繁触发 GC,导致不可预测的帧率卡顿(5-50ms 暂停)
- 正确做法:预分配集合、使用对象池、StringBuilder、避免 LINQ 热路径
-
订阅事件/信号但不取消订阅
- 问题:对象销毁后回调仍触发,导致空引用崩溃或内存泄漏
- 正确做法:OnEnable/OnDisable 配对订阅,OnDestroy/_exit_tree 中清理
-
硬编码按键,缺少输入缓冲和 Coyote Time
- 问题:不支持手柄、无法重映射、动作游戏手感极差
- 正确做法:使用 Input Action 抽象层,实现输入缓冲(0.1s)和 Coyote Time(0.12s)
-
大量相同物体使用独立 GameObject/Node
- 问题:每个物体 = 1 draw call,1000 棵树 = 1000 draw calls
- 正确做法:GPU Instancing / MultiMesh / HISM,1000 棵树 = 1-2 draw calls
-
直接修改 Transform 移动物理对象
- 问题:绕过物理引擎,碰撞检测失效,物体穿透
- 正确做法:通过 Rigidbody.MovePosition / velocity / CharacterBody.move_and_slide
-
每帧调用 Find/GetNode/GetComponent 搜索方法
- 问题:遍历整个场景树/组件列表,性能极差
- 正确做法:在 Awake/Start/_ready 中缓存引用,使用注册表模式
-
不配置 Steering 规则就让 AI 写游戏代码
- 问题:AI 生成的代码功能正确但性能极差,需要大量返工
- 正确做法:项目初期就配置完整的 Steering 规则,持续迭代更新
-
Shader 中使用 if-else 分支
- 问题:GPU warp 分歧导致性能下降,移动端尤其严重
- 正确做法:使用 step/lerp/smoothstep 等数学函数替代分支
-
忽略平台差异,只在编辑器中测试
- 问题:PC 上流畅的游戏在移动端可能完全不可玩
- 正确做法:早期就在目标平台上测试,配置平台差异化规则
✅ 最佳实践
- 项目第一天就配置 Steering 规则:在写第一行游戏代码之前,先配置好 CLAUDE.md / .kiro/steering / .cursorrules
- 分层规则架构:通用规则(always)+ 引擎规则(auto)+ 功能规则(manual)三层结构
- 规则中包含代码示例:直接在 Steering 规则中给出正确和错误的代码对比,AI 理解更准确
- 持续迭代规则:每次发现 AI 犯的新错误,立即添加到 Steering 规则中
- 使用 Profiler 验证:不要相信”看起来流畅”,用引擎内置 Profiler 验证帧率、draw call、GC
- 对象池优先:任何需要频繁创建/销毁的对象(子弹、特效、敌人、UI 元素)都使用对象池
- 输入手感是核心:动作游戏的输入缓冲、Coyote Time、可变跳跃高度不是”锦上添花”,是”必须有”
- 早期多平台测试:不要等到最后才在目标平台上测试,每周至少在真机上跑一次
- 数据驱动配置:游戏参数(速度、跳跃力、重力)使用 ScriptableObject/Resource/DataTable,不硬编码
- 状态机管理复杂逻辑:角色状态、游戏流程、AI 行为都使用状态机,避免嵌套 if-else
相关资源与延伸阅读
游戏开发最佳实践
- Fix Your Timestep! — Glenn Fiedler 的经典文章,详解游戏物理时间步的正确实现方式,是理解固定时间步 vs 可变时间步的必读资料
- Game Programming Patterns — Robert Nystrom 的免费在线书籍,覆盖对象池、状态机、观察者等游戏开发核心设计模式
- Unity Performance Best Practices — Unity 官方性能优化指南,覆盖脚本、渲染、内存、物理等各方面
- Godot Performance Documentation — Godot 官方性能优化文档,包含 GDScript 优化、渲染优化、物理优化
- Unreal Engine Performance Guidelines — Epic 官方性能指南,覆盖 CPU/GPU 优化、内存管理、Profiling 工具
AI 辅助游戏开发
- Cursor Rules Guide — Cursor IDE 规则配置完整指南,适用于游戏开发项目的 AI 规则配置参考(2025)
- Vibe Coding Complete Guide — Vibe Coding 完整指南,包含游戏开发场景下的 AI 编码最佳实践(2025)
- Game Design Patterns Complete Guide — 游戏设计模式完整指南,包含 Unity C# 和 Godot GDScript 双引擎实现(2025)
输入系统与手感优化
- Game Input Systems Complete Guide — 游戏输入系统完整指南,覆盖跨平台控制器支持、输入缓冲、无障碍功能(2025)
- Why Input Buffering Makes or Breaks Action Games — 深入解析输入缓冲对动作游戏手感的关键影响(2025)
参考来源
- Fix Your Timestep! — Glenn Fiedler(经典文章,持续更新)
- Game Programming Patterns — Robert Nystrom(免费在线书籍)
- Unity Documentation: Performance Best Practices (2025 年持续更新)
- Godot Documentation: Performance (2025 年持续更新)
- Unreal Engine Performance Guidelines (2025 年持续更新)
- Godot Maintainers Battle Surge of AI Slop Pull Requests (2025 年 6 月)
- AI In Game Development: How AI Is Transforming The Game Dev Workflow (2025 年 6 月)
- Best Game Development Software in 2026 (2025 年 6 月)
- Cursor Rules Guide (2025 年 6 月)
- Vibe Coding Complete Guide (2025 年 5 月)
- Game Design Patterns Complete Guide (2025 年 1 月)
- Game Input Systems Complete Guide (2025 年 1 月)
- Unity Update vs. FixedUpdate: Solving Physics Jitter (2025 年 6 月)
- Proper Usage of _process and _physics_process in Godot (2025 年 6 月)
- Taming Time in Game Engines (2025 年 5 月)
- How to Reduce Draw Calls in Unity UI (2025 年 6 月)
📖 返回 总览与导航 | 上一节:AI生成游戏资产 | 下一节:AI辅助嵌入式开发概览