Skip to Content

34c - AI辅助游戏逻辑

本文是《AI Agent 实战手册》第 34 章第 3 节。 上一节:游戏引擎工作流 | 下一节:AI生成游戏资产

概述

游戏逻辑是游戏开发的核心骨架——状态机控制角色行为流转、物理系统赋予世界真实感、行为树驱动 NPC 智能决策、寻路算法让角色自主导航、程序化生成创造无限内容。2025-2026 年,AI Agent(Claude Code、Cursor、GitHub Copilot 等)已经能够根据自然语言描述生成完整的游戏逻辑代码,从简单的 FSM 到复杂的层级行为树,从 A* 寻路到波函数坍缩(WFC)地牢生成。本节提供五大核心游戏逻辑主题的 AI 辅助开发完整指南,覆盖 Unity(C#)、Godot(GDScript)和 Unreal Engine(C++)三大引擎,包含提示词模板、代码示例和实战案例。


1. 状态机(State Machines)

1.1 状态机基础概念

状态机是游戏开发中最基础也最重要的设计模式之一。它将游戏实体的行为分解为离散的”状态”,每个状态有明确的进入/退出逻辑和状态间的转换条件。

状态机类型层级:

简单 FSM(Finite State Machine) ├── 枚举 + switch 实现 ├── 适合:简单角色(3-5 个状态) └── 缺点:状态多时代码膨胀 状态模式(State Pattern) ├── 每个状态一个类/脚本 ├── 适合:中等复杂度(5-15 个状态) └── 优点:开闭原则,易扩展 层级状态机(Hierarchical State Machine / HFSM) ├── 状态可嵌套子状态机 ├── 适合:复杂角色(15+ 个状态) └── 优点:减少转换爆炸,逻辑分层 推栈状态机(Pushdown Automaton) ├── 状态栈,支持暂停/恢复 ├── 适合:需要"返回上一状态"的场景 └── 例如:暂停菜单、对话中断

工具推荐

工具用途价格适用场景
Claude Code终端 Agent,生成完整状态机系统$20-100/月复杂 FSM/HFSM 架构设计与实现
CursorAI IDE,实时状态机代码补全免费-$20/月日常状态机编码与重构
GitHub Copilot编辑器内代码补全$10-39/月状态转换逻辑快速补全
LimboAIGodot 行为树+状态机插件免费(开源)Godot 可视化状态机编辑
Unity AnimatorUnity 内置状态机编辑器Unity 订阅内含动画状态机与逻辑状态机
Unreal AI ModuleUE 内置 AI 框架免费(引擎内置)Blackboard + 状态树

1.2 AI 生成状态机代码

Unity C# — 枚举 FSM 实现

// Unity C# - 角色状态机(枚举 + switch 模式) // AI 提示词生成的典型输出 public enum PlayerState { Idle, Running, Jumping, Falling, Attacking, Dashing, Hurt, Dead } public class PlayerStateMachine : MonoBehaviour { [Header("状态机")] public PlayerState currentState = PlayerState.Idle; private PlayerState previousState; [Header("移动参数")] public float moveSpeed = 8f; public float jumpForce = 12f; public float dashSpeed = 20f; public float dashDuration = 0.2f; private Rigidbody2D rb; private Animator animator; private float dashTimer; private bool isGrounded; void Start() { rb = GetComponent<Rigidbody2D>(); animator = GetComponent<Animator>(); EnterState(PlayerState.Idle); } void Update() { UpdateState(); CheckTransitions(); } // --- 状态进入 --- void EnterState(PlayerState newState) { previousState = currentState; currentState = newState; switch (newState) { case PlayerState.Idle: rb.linearVelocity = new Vector2(0, rb.linearVelocity.y); animator.Play("Idle"); break; case PlayerState.Running: animator.Play("Run"); break; case PlayerState.Jumping: rb.linearVelocity = new Vector2(rb.linearVelocity.x, jumpForce); animator.Play("Jump"); break; case PlayerState.Dashing: dashTimer = dashDuration; float dir = transform.localScale.x > 0 ? 1 : -1; rb.linearVelocity = new Vector2(dir * dashSpeed, 0); rb.gravityScale = 0; animator.Play("Dash"); break; case PlayerState.Hurt: rb.linearVelocity = new Vector2(-transform.localScale.x * 5f, 3f); animator.Play("Hurt"); break; case PlayerState.Dead: rb.linearVelocity = Vector2.zero; rb.simulated = false; animator.Play("Death"); break; } } // --- 状态更新 --- void UpdateState() { switch (currentState) { case PlayerState.Running: float h = Input.GetAxisRaw("Horizontal"); rb.linearVelocity = new Vector2(h * moveSpeed, rb.linearVelocity.y); if (h != 0) transform.localScale = new Vector3( Mathf.Sign(h), 1, 1); break; case PlayerState.Dashing: dashTimer -= Time.deltaTime; if (dashTimer <= 0) { rb.gravityScale = 1; TransitionTo(PlayerState.Idle); } break; } } // --- 状态转换检查 --- void CheckTransitions() { if (currentState == PlayerState.Dead) return; float h = Input.GetAxisRaw("Horizontal"); switch (currentState) { case PlayerState.Idle: if (Mathf.Abs(h) > 0.1f) TransitionTo(PlayerState.Running); if (Input.GetButtonDown("Jump") && isGrounded) TransitionTo(PlayerState.Jumping); if (Input.GetButtonDown("Fire1")) TransitionTo(PlayerState.Attacking); if (Input.GetKeyDown(KeyCode.LeftShift)) TransitionTo(PlayerState.Dashing); break; case PlayerState.Running: if (Mathf.Abs(h) < 0.1f) TransitionTo(PlayerState.Idle); if (Input.GetButtonDown("Jump") && isGrounded) TransitionTo(PlayerState.Jumping); if (Input.GetKeyDown(KeyCode.LeftShift)) TransitionTo(PlayerState.Dashing); break; case PlayerState.Jumping: if (rb.linearVelocity.y < 0) TransitionTo(PlayerState.Falling); break; case PlayerState.Falling: if (isGrounded) TransitionTo(PlayerState.Idle); break; } } void TransitionTo(PlayerState newState) { ExitState(currentState); EnterState(newState); } void ExitState(PlayerState state) { switch (state) { case PlayerState.Dashing: rb.gravityScale = 1; break; } } // 外部调用:受伤 public void TakeDamage(int damage) { if (currentState == PlayerState.Dashing) return; // 冲刺无敌 TransitionTo(PlayerState.Hurt); Invoke(nameof(RecoverFromHurt), 0.5f); } void RecoverFromHurt() => TransitionTo(PlayerState.Idle); }

Godot GDScript — 状态模式实现

# Godot GDScript - 状态模式(每个状态一个脚本) # 基类:State.gd class_name State extends Node # 状态机引用,由 StateMachine 自动注入 var state_machine: StateMachine var player: CharacterBody2D # 虚方法:子类重写 func enter() -> void: pass func exit() -> void: pass func update(delta: float) -> void: pass func physics_update(delta: float) -> void: pass func handle_input(event: InputEvent) -> void: pass
# StateMachine.gd - 状态机管理器 class_name StateMachine extends Node @export var initial_state: State @export var player: CharacterBody2D var current_state: State var states: Dictionary = {} func _ready() -> void: # 注册所有子节点状态 for child in get_children(): if child is State: states[child.name.to_lower()] = child child.state_machine = self child.player = player # 进入初始状态 if initial_state: current_state = initial_state current_state.enter() func _process(delta: float) -> void: if current_state: current_state.update(delta) func _physics_process(delta: float) -> void: if current_state: current_state.physics_update(delta) func _unhandled_input(event: InputEvent) -> void: if current_state: current_state.handle_input(event) func transition_to(state_name: String) -> void: var new_state = states.get(state_name.to_lower()) if new_state == null: push_warning("状态不存在: " + state_name) return if new_state == current_state: return current_state.exit() current_state = new_state current_state.enter() print("状态转换: -> ", state_name)
# IdleState.gd - 空闲状态 extends State func enter() -> void: player.get_node("AnimationPlayer").play("idle") func physics_update(delta: float) -> void: # 应用重力 if not player.is_on_floor(): player.velocity.y += player.gravity * delta player.velocity.x = move_toward(player.velocity.x, 0, player.friction) player.move_and_slide() func handle_input(event: InputEvent) -> void: if event.is_action_pressed("move_left") or event.is_action_pressed("move_right"): state_machine.transition_to("run") elif event.is_action_pressed("jump") and player.is_on_floor(): state_machine.transition_to("jump") elif event.is_action_pressed("attack"): state_machine.transition_to("attack") elif event.is_action_pressed("dash"): state_machine.transition_to("dash")
# RunState.gd - 奔跑状态 extends State func enter() -> void: player.get_node("AnimationPlayer").play("run") func physics_update(delta: float) -> void: var direction = Input.get_axis("move_left", "move_right") if direction != 0: player.velocity.x = direction * player.move_speed player.get_node("Sprite2D").flip_h = direction < 0 else: state_machine.transition_to("idle") return if not player.is_on_floor(): player.velocity.y += player.gravity * delta state_machine.transition_to("fall") return player.move_and_slide() func handle_input(event: InputEvent) -> void: if event.is_action_pressed("jump") and player.is_on_floor(): state_machine.transition_to("jump") elif event.is_action_pressed("attack"): state_machine.transition_to("attack") elif event.is_action_pressed("dash"): state_machine.transition_to("dash")

Unreal Engine C++ — 层级状态机(HFSM)

// Unreal Engine C++ - 层级状态机框架 // GameplayState.h #pragma once #include "CoreMinimal.h" #include "GameplayState.generated.h" UCLASS(Abstract, Blueprintable) class MYGAME_API UGameplayState : public UObject { GENERATED_BODY() public: // 状态生命周期 UFUNCTION(BlueprintNativeEvent) void OnEnter(); UFUNCTION(BlueprintNativeEvent) void OnExit(); UFUNCTION(BlueprintNativeEvent) void OnUpdate(float DeltaTime); // 子状态机支持(HFSM 核心) UPROPERTY() class UHierarchicalStateMachine* SubStateMachine; UPROPERTY() class ACharacter* OwnerCharacter; // 状态名称 UPROPERTY(EditAnywhere, BlueprintReadOnly) FName StateName; }; // HierarchicalStateMachine.h UCLASS(BlueprintType) class MYGAME_API UHierarchicalStateMachine : public UObject { GENERATED_BODY() public: void Initialize(ACharacter* Owner); void Update(float DeltaTime); UFUNCTION(BlueprintCallable) void TransitionTo(FName StateName); UFUNCTION(BlueprintCallable) void AddState(FName Name, TSubclassOf<UGameplayState> StateClass); UFUNCTION(BlueprintCallable) FName GetCurrentStateName() const; private: UPROPERTY() TMap<FName, UGameplayState*> States; UPROPERTY() UGameplayState* CurrentState; UPROPERTY() ACharacter* OwnerCharacter; }; // HierarchicalStateMachine.cpp void UHierarchicalStateMachine::Initialize(ACharacter* Owner) { OwnerCharacter = Owner; } void UHierarchicalStateMachine::Update(float DeltaTime) { if (CurrentState) { CurrentState->OnUpdate(DeltaTime); // 递归更新子状态机 if (CurrentState->SubStateMachine) { CurrentState->SubStateMachine->Update(DeltaTime); } } } void UHierarchicalStateMachine::TransitionTo(FName StateName) { UGameplayState** FoundState = States.Find(StateName); if (!FoundState) return; if (CurrentState) { // 退出子状态机 if (CurrentState->SubStateMachine) { CurrentState->SubStateMachine->TransitionTo(NAME_None); } CurrentState->OnExit(); } CurrentState = *FoundState; CurrentState->OnEnter(); UE_LOG(LogTemp, Log, TEXT("HFSM 转换: -> %s"), *StateName.ToString()); } void UHierarchicalStateMachine::AddState( FName Name, TSubclassOf<UGameplayState> StateClass) { UGameplayState* NewState = NewObject<UGameplayState>( this, StateClass); NewState->StateName = Name; NewState->OwnerCharacter = OwnerCharacter; States.Add(Name, NewState); }

1.3 状态机提示词模板

模板 1:基础 FSM 生成

你是一个 [Unity C# / Godot GDScript / Unreal C++] 游戏开发专家。 请为 [角色类型,如:2D 平台跳跃角色] 生成一个完整的有限状态机(FSM)。 角色状态需求: - [列出所有状态,如:Idle, Run, Jump, Fall, Attack, Dash, Hurt, Dead] 每个状态需要: 1. 进入动作(播放动画、设置参数) 2. 更新逻辑(每帧执行的行为) 3. 退出动作(清理资源) 4. 转换条件(什么条件下转到哪个状态) 物理参数: - 移动速度:[X] 单位/秒 - 跳跃力:[Y] - 重力:[Z] 要求: - 使用 [枚举+switch / 状态模式 / 层级状态机] 架构 - 包含完整的输入处理 - 包含动画播放调用 - 代码注释使用中文

模板 2:层级状态机(HFSM)生成

请为 [RPG 角色 / 动作游戏 Boss] 设计一个层级状态机(HFSM)。 顶层状态: - Exploration(探索) - 子状态:Idle, Walk, Run, Interact - Combat(战斗) - 子状态:Engage, Attack, Defend, Dodge, Skill - Cutscene(过场) - 子状态:Dialogue, Animation 转换规则: - Exploration -> Combat:检测到敌人进入 [范围] - Combat -> Exploration:所有敌人被消灭或脱离战斗 [时间] 秒 - 任意 -> Cutscene:触发剧情事件 要求: - 顶层状态机管理大状态切换 - 每个大状态内部有独立的子状态机 - 子状态机在父状态退出时自动重置 - 提供状态转换的 Mermaid 图

模板 3:AI 审查现有状态机

请审查以下状态机代码,检查: 1. 是否存在"状态转换爆炸"(N 个状态需要 N² 个转换) 2. 是否有遗漏的状态转换(死锁风险) 3. 是否正确处理了边界情况(如:空中受伤、攻击中被打断) 4. 性能问题(每帧不必要的检查) 5. 是否适合重构为层级状态机 代码: [粘贴现有状态机代码] 请给出具体的改进建议和重构后的代码。

2. 物理系统(Physics)

2.1 游戏物理基础

游戏物理系统模拟现实世界的物理规律,包括碰撞检测、刚体动力学、射线检测等。AI Agent 可以帮助快速生成物理相关代码,但开发者需要理解底层原理以正确调参。

游戏物理核心组件:

组件UnityGodotUnreal
刚体Rigidbody / Rigidbody2DRigidBody2D / RigidBody3DUPrimitiveComponent (Simulate Physics)
运动体CharacterControllerCharacterBody2D / CharacterBody3DACharacter (CharacterMovementComponent)
碰撞体Collider2D / ColliderCollisionShape2D / CollisionShape3DUShapeComponent
触发区域Trigger ColliderArea2D / Area3DTrigger Volume
射线检测Physics.RaycastRayCast2D / RayCast3DLineTrace
物理材质PhysicsMaterial2DPhysicsMaterialPhysical Material
关节Joint2D / JointJoint2D / Joint3DPhysics Constraint

工具推荐

工具用途价格适用场景
Claude Code生成复杂物理交互代码$20-100/月自定义物理系统、碰撞响应
Cursor物理代码实时补全免费-$20/月日常物理编码
Unity Physics Debugger可视化碰撞体和射线Unity 内置物理调试
Godot Physics Debug显示碰撞形状免费(引擎内置)碰撞调试
Unreal Collision Analyzer碰撞通道分析免费(引擎内置)复杂碰撞配置

2.2 碰撞检测与响应

Unity C# — 碰撞检测系统

// Unity C# - 完整碰撞检测与响应系统 public class CollisionManager : MonoBehaviour { [Header("碰撞层配置")] public LayerMask groundLayer; public LayerMask enemyLayer; public LayerMask collectibleLayer; [Header("地面检测")] public Transform groundCheck; public float groundCheckRadius = 0.2f; [Header("墙壁检测")] public Transform wallCheck; public float wallCheckDistance = 0.5f; // 地面检测(OverlapCircle) public bool IsGrounded() { return Physics2D.OverlapCircle( groundCheck.position, groundCheckRadius, groundLayer ); } // 墙壁检测(Raycast) public bool IsTouchingWall() { float direction = transform.localScale.x; RaycastHit2D hit = Physics2D.Raycast( wallCheck.position, Vector2.right * direction, wallCheckDistance, groundLayer ); return hit.collider != null; } // 扇形范围检测(攻击判定) public Collider2D[] DetectEnemiesInArc( float radius, float angle, Vector2 direction) { Collider2D[] allEnemies = Physics2D.OverlapCircleAll( transform.position, radius, enemyLayer); return System.Array.FindAll(allEnemies, enemy => { Vector2 toEnemy = (enemy.transform.position - transform.position).normalized; float angleBetween = Vector2.Angle(direction, toEnemy); return angleBetween <= angle / 2f; }); } // 触发器碰撞回调 void OnTriggerEnter2D(Collider2D other) { // 收集物品 if (((1 << other.gameObject.layer) & collectibleLayer) != 0) { ICollectible collectible = other.GetComponent<ICollectible>(); collectible?.Collect(gameObject); } // 伤害区域 if (other.CompareTag("DamageZone")) { DamageZone zone = other.GetComponent<DamageZone>(); GetComponent<HealthSystem>()?.TakeDamage(zone.damage); } } // 碰撞体碰撞回调(物理碰撞) void OnCollisionEnter2D(Collision2D collision) { // 获取碰撞信息 ContactPoint2D contact = collision.GetContact(0); Vector2 normal = contact.normal; float impactForce = collision.relativeVelocity.magnitude; // 高速碰撞伤害 if (impactForce > 15f) { float damage = (impactForce - 15f) * 2f; GetComponent<HealthSystem>()?.TakeDamage(damage); } // 碰撞粒子效果 if (impactForce > 5f) { ParticleSystem particles = Instantiate(impactParticles, contact.point, Quaternion.LookRotation(normal)); Destroy(particles.gameObject, 2f); } } // 可视化调试 void OnDrawGizmosSelected() { if (groundCheck != null) { Gizmos.color = Color.green; Gizmos.DrawWireSphere(groundCheck.position, groundCheckRadius); } if (wallCheck != null) { Gizmos.color = Color.red; float dir = transform.localScale.x; Gizmos.DrawRay(wallCheck.position, Vector2.right * dir * wallCheckDistance); } } [SerializeField] private ParticleSystem impactParticles; } // 可收集物品接口 public interface ICollectible { void Collect(GameObject collector); }

Godot GDScript — 物理系统

# Godot GDScript - 角色物理控制器 # CharacterController.gd extends CharacterBody2D # 移动参数 @export var move_speed: float = 300.0 @export var jump_force: float = -500.0 @export var gravity: float = 1200.0 @export var max_fall_speed: float = 800.0 # 高级物理参数 @export var acceleration: float = 2000.0 @export var friction: float = 1500.0 @export var air_friction: float = 200.0 @export var coyote_time: float = 0.1 # 土狼时间(离开平台后仍可跳跃) @export var jump_buffer_time: float = 0.15 # 跳跃缓冲 # 内部状态 var coyote_timer: float = 0.0 var jump_buffer_timer: float = 0.0 var was_on_floor: bool = false # 射线检测节点 @onready var ground_ray_left: RayCast2D = $GroundRayLeft @onready var ground_ray_right: RayCast2D = $GroundRayRight @onready var wall_ray: RayCast2D = $WallRay func _physics_process(delta: float) -> void: _apply_gravity(delta) _handle_movement(delta) _handle_jump() _update_timers(delta) move_and_slide() _check_landing() func _apply_gravity(delta: float) -> void: if not is_on_floor(): velocity.y += gravity * delta velocity.y = min(velocity.y, max_fall_speed) func _handle_movement(delta: float) -> void: var direction = Input.get_axis("move_left", "move_right") if direction != 0: # 加速 velocity.x = move_toward( velocity.x, direction * move_speed, acceleration * delta) # 翻转精灵 $Sprite2D.flip_h = direction < 0 else: # 减速(地面摩擦 vs 空气阻力) var current_friction = friction if is_on_floor() else air_friction velocity.x = move_toward(velocity.x, 0, current_friction * delta) func _handle_jump() -> void: # 跳跃缓冲:提前按跳跃键 if Input.is_action_just_pressed("jump"): jump_buffer_timer = jump_buffer_time # 可以跳跃的条件:在地面 或 土狼时间内 var can_jump = is_on_floor() or coyote_timer > 0 if jump_buffer_timer > 0 and can_jump: velocity.y = jump_force jump_buffer_timer = 0 coyote_timer = 0 # 短按跳跃(松开按键时减速) if Input.is_action_just_released("jump") and velocity.y < 0: velocity.y *= 0.5 func _update_timers(delta: float) -> void: # 土狼时间计时器 if is_on_floor(): coyote_timer = coyote_time else: coyote_timer -= delta # 跳跃缓冲计时器 if jump_buffer_timer > 0: jump_buffer_timer -= delta func _check_landing() -> void: # 检测着陆(用于播放着陆效果) if is_on_floor() and not was_on_floor: _on_landed() was_on_floor = is_on_floor() func _on_landed() -> void: # 着陆粒子效果 $LandingParticles.emitting = true # 着陆音效 $LandingSFX.play() # --- 射线检测工具方法 --- func is_near_wall() -> bool: """检测是否靠近墙壁""" wall_ray.target_position.x = 20 if not $Sprite2D.flip_h else -20 wall_ray.force_raycast_update() return wall_ray.is_colliding() func get_ground_normal() -> Vector2: """获取地面法线(用于斜坡处理)""" if ground_ray_left.is_colliding(): return ground_ray_left.get_collision_normal() elif ground_ray_right.is_colliding(): return ground_ray_right.get_collision_normal() return Vector2.UP func detect_enemies_in_range(radius: float) -> Array[Node2D]: """检测范围内的敌人""" var space = get_world_2d().direct_space_state var query = PhysicsShapeQueryParameters2D.new() var circle = CircleShape2D.new() circle.radius = radius query.shape = circle query.transform = global_transform query.collision_mask = 0b0100 # 敌人层 var results = space.intersect_shape(query) var enemies: Array[Node2D] = [] for result in results: enemies.append(result.collider) return enemies

2.3 刚体物理与力的应用

Unity C# — 刚体物理交互

// Unity C# - 刚体物理交互系统 public class PhysicsInteraction : MonoBehaviour { [Header("推动物体")] public float pushForce = 5f; public float maxPushSpeed = 3f; [Header("爆炸力")] public float explosionForce = 500f; public float explosionRadius = 5f; public float explosionUpward = 1f; [Header("吸引力")] public float attractForce = 10f; public float attractRadius = 8f; // 推动可移动物体 void OnControllerColliderHit(ControllerColliderHit hit) { Rigidbody rb = hit.collider.attachedRigidbody; if (rb == null || rb.isKinematic) return; // 不推动脚下的物体 if (hit.moveDirection.y < -0.3f) return; Vector3 pushDir = new Vector3(hit.moveDirection.x, 0, hit.moveDirection.z); rb.AddForce(pushDir * pushForce, ForceMode.Impulse); // 限制最大速度 if (rb.linearVelocity.magnitude > maxPushSpeed) rb.linearVelocity = rb.linearVelocity.normalized * maxPushSpeed; } // 爆炸效果 public void CreateExplosion(Vector3 position) { Collider[] colliders = Physics.OverlapSphere( position, explosionRadius); foreach (Collider col in colliders) { Rigidbody rb = col.GetComponent<Rigidbody>(); if (rb != null) { rb.AddExplosionForce( explosionForce, position, explosionRadius, explosionUpward, ForceMode.Impulse); } // 对可破坏物体造成伤害 IDestructible destructible = col.GetComponent<IDestructible>(); if (destructible != null) { float distance = Vector3.Distance( position, col.transform.position); float damagePercent = 1f - (distance / explosionRadius); destructible.TakeDamage( explosionForce * damagePercent * 0.1f); } } } // 磁力吸引(每帧调用) public void ApplyAttraction(Vector3 center) { Collider[] colliders = Physics.OverlapSphere( center, attractRadius); foreach (Collider col in colliders) { Rigidbody rb = col.GetComponent<Rigidbody>(); if (rb == null || rb.isKinematic) return; Vector3 direction = (center - col.transform.position).normalized; float distance = Vector3.Distance( center, col.transform.position); // 距离越近,力越大(反平方律) float forceMagnitude = attractForce / (distance * distance + 0.1f); rb.AddForce(direction * forceMagnitude); } } } public interface IDestructible { void TakeDamage(float damage); }

2.4 射线检测(Raycasting)

Godot GDScript — 高级射线检测

# Godot GDScript - 射线检测工具集 # RaycastUtils.gd extends Node # --- 单射线检测 --- static func raycast_2d( space_state: PhysicsDirectSpaceState2D, from: Vector2, to: Vector2, collision_mask: int = 0xFFFFFFFF, exclude: Array[RID] = [] ) -> Dictionary: """执行 2D 射线检测,返回碰撞信息""" var query = PhysicsRayQueryParameters2D.create(from, to) query.collision_mask = collision_mask query.exclude = exclude return space_state.intersect_ray(query) # --- 扇形射线检测(视野锥) --- static func cone_cast_2d( space_state: PhysicsDirectSpaceState2D, origin: Vector2, direction: Vector2, distance: float, half_angle_deg: float, ray_count: int = 12, collision_mask: int = 0xFFFFFFFF ) -> Array[Dictionary]: """扇形射线检测,用于 NPC 视野检测""" var results: Array[Dictionary] = [] var half_angle = deg_to_rad(half_angle_deg) var base_angle = direction.angle() for i in range(ray_count): var t = float(i) / float(ray_count - 1) if ray_count > 1 else 0.5 var angle = base_angle - half_angle + t * 2.0 * half_angle var end = origin + Vector2.from_angle(angle) * distance var result = raycast_2d(space_state, origin, end, collision_mask) if result: results.append(result) return results # --- 地面法线检测(斜坡适配) --- static func get_ground_info( space_state: PhysicsDirectSpaceState2D, position: Vector2, ground_mask: int ) -> Dictionary: """获取脚下地面信息:法线、角度、材质""" var result = raycast_2d( space_state, position, position + Vector2.DOWN * 50, ground_mask) if result: var normal: Vector2 = result.normal var angle = rad_to_deg(normal.angle_to(Vector2.UP)) return { "hit": true, "point": result.position, "normal": normal, "angle": angle, "collider": result.collider, "is_slope": abs(angle) > 5.0, "is_steep": abs(angle) > 45.0 } return {"hit": false}

2.5 物理系统提示词模板

模板 4:碰撞检测系统生成

请为 [2D 平台游戏 / 3D 动作游戏] 生成完整的碰撞检测系统。 引擎:[Unity / Godot / Unreal] 需要的碰撞检测功能: 1. 地面检测(支持斜坡,角度阈值 [X] 度) 2. 墙壁检测(左右两侧) 3. 天花板检测 4. 敌人范围检测(圆形/扇形,半径 [R]) 5. 射线检测(武器瞄准线、视线检查) 碰撞层设置: - Layer 0: Default - Layer 1: Player - Layer 2: Enemy - Layer 3: Ground/Wall - Layer 4: Collectible - Layer 5: Projectile 要求: - 使用引擎原生物理 API - 包含 Gizmo/Debug 可视化 - 性能优化(避免每帧 OverlapAll) - 包含碰撞回调处理

模板 5:AI 辅助物理调参

我的 [2D 平台跳跃 / 3D 第三人称] 游戏角色物理手感不好。 当前参数: - 移动速度:[X] - 跳跃力:[Y] - 重力:[Z] - 加速度:[A] - 摩擦力:[F] 问题描述: [描述具体问题,如:跳跃感觉太飘、移动太滑、落地太硬] 参考游戏手感:[如:Celeste 的精确操控 / Hollow Knight 的沉重感] 请: 1. 分析当前参数的问题 2. 推荐调整后的参数值 3. 解释每个参数变化的原因 4. 提供额外的物理技巧(如:土狼时间、跳跃缓冲、可变跳跃高度)

3. AI 行为树(Behavior Trees)

3.1 行为树基础架构

行为树(Behavior Tree, BT)是游戏 AI 的主流实现方式,相比状态机具有更好的模块化和可扩展性。行为树由节点组成,每个节点返回三种状态之一:SUCCESS(成功)、FAILURE(失败)、RUNNING(运行中)。

行为树节点类型:

行为树节点 ├── 组合节点(Composite) │ ├── Selector(选择器):依次尝试子节点,第一个成功即返回成功 │ ├── Sequence(序列):依次执行子节点,全部成功才返回成功 │ ├── Parallel(并行):同时执行所有子节点 │ └── RandomSelector(随机选择器):随机选择一个子节点执行 ├── 装饰器节点(Decorator) │ ├── Inverter(取反):反转子节点结果 │ ├── Repeater(重复器):重复执行子节点 N 次 │ ├── Limiter(限制器):限制执行频率 │ ├── Cooldown(冷却):执行后等待一段时间 │ ├── TimeLimit(时间限制):超时返回失败 │ └── Condition(条件守卫):条件满足才执行子节点 └── 叶节点(Leaf) ├── Action(动作):执行具体行为(移动、攻击、播放动画) └── Condition(条件):检查条件(血量、距离、视线)

行为树 vs 状态机对比:

特性状态机(FSM)行为树(BT)
结构状态 + 转换树形节点层级
可读性状态少时直观始终清晰
可扩展性状态多时转换爆炸模块化,易添加分支
复用性低(状态间耦合)高(子树可复用)
调试当前状态一目了然需要可视化工具
适用场景简单角色、UI 流程复杂 NPC AI、Boss
AI 生成难度中等

工具推荐

工具用途价格适用场景
BeehaveGodot 行为树插件免费(开源)Godot 可视化行为树编辑
LimboAIGodot 行为树+状态机 C++ 插件免费(开源)Godot 高性能 AI 系统
Unity BehaviorUnity 官方行为树包Unity 6+ 内置Unity 可视化行为树
Unreal Behavior TreeUE 内置行为树编辑器免费(引擎内置)UE 标准 NPC AI
NodeCanvasUnity 高级行为树/FSM 插件$80(一次性)Unity 复杂 AI 系统
Claude Code生成行为树逻辑代码$20-100/月自定义行为树节点
CursorAI 辅助行为树编码免费-$20/月日常 AI 逻辑编码

3.2 AI 生成行为树代码

Unity C# — 行为树框架

// Unity C# - 轻量行为树框架 // 节点状态枚举 public enum NodeStatus { Success, Failure, Running } // 行为树节点基类 public abstract class BTNode { public string Name { get; set; } protected Blackboard blackboard; public BTNode(string name, Blackboard blackboard) { Name = name; this.blackboard = blackboard; } public abstract NodeStatus Tick(float deltaTime); public virtual void Reset() { } } // 黑板(共享数据) public class Blackboard { private Dictionary<string, object> data = new(); public void Set<T>(string key, T value) => data[key] = value; public T Get<T>(string key, T defaultValue = default) { if (data.TryGetValue(key, out object value)) return (T)value; return defaultValue; } public bool Has(string key) => data.ContainsKey(key); } // --- 组合节点 --- // 选择器:依次尝试,第一个成功即返回 public class Selector : BTNode { private List<BTNode> children; private int currentChild = 0; public Selector(string name, Blackboard bb, params BTNode[] children) : base(name, bb) { this.children = new List<BTNode>(children); } public override NodeStatus Tick(float deltaTime) { while (currentChild < children.Count) { var status = children[currentChild].Tick(deltaTime); switch (status) { case NodeStatus.Success: currentChild = 0; return NodeStatus.Success; case NodeStatus.Running: return NodeStatus.Running; case NodeStatus.Failure: currentChild++; break; } } currentChild = 0; return NodeStatus.Failure; } public override void Reset() { currentChild = 0; foreach (var child in children) child.Reset(); } } // 序列:依次执行,全部成功才返回成功 public class Sequence : BTNode { private List<BTNode> children; private int currentChild = 0; public Sequence(string name, Blackboard bb, params BTNode[] children) : base(name, bb) { this.children = new List<BTNode>(children); } public override NodeStatus Tick(float deltaTime) { while (currentChild < children.Count) { var status = children[currentChild].Tick(deltaTime); switch (status) { case NodeStatus.Success: currentChild++; break; case NodeStatus.Running: return NodeStatus.Running; case NodeStatus.Failure: currentChild = 0; return NodeStatus.Failure; } } currentChild = 0; return NodeStatus.Success; } public override void Reset() { currentChild = 0; foreach (var child in children) child.Reset(); } } // --- 装饰器节点 --- // 取反器 public class Inverter : BTNode { private BTNode child; public Inverter(string name, Blackboard bb, BTNode child) : base(name, bb) { this.child = child; } public override NodeStatus Tick(float deltaTime) { var status = child.Tick(deltaTime); return status switch { NodeStatus.Success => NodeStatus.Failure, NodeStatus.Failure => NodeStatus.Success, _ => NodeStatus.Running }; } } // 冷却装饰器 public class Cooldown : BTNode { private BTNode child; private float cooldownTime; private float timer; public Cooldown(string name, Blackboard bb, BTNode child, float cooldownTime) : base(name, bb) { this.child = child; this.cooldownTime = cooldownTime; } public override NodeStatus Tick(float deltaTime) { timer -= deltaTime; if (timer > 0) return NodeStatus.Failure; var status = child.Tick(deltaTime); if (status == NodeStatus.Success) timer = cooldownTime; return status; } } // --- 叶节点示例 --- // 条件:检查目标是否在范围内 public class IsTargetInRange : BTNode { private float range; public IsTargetInRange(Blackboard bb, float range) : base("IsTargetInRange", bb) { this.range = range; } public override NodeStatus Tick(float deltaTime) { var self = blackboard.Get<Transform>("self"); var target = blackboard.Get<Transform>("target"); if (self == null || target == null) return NodeStatus.Failure; float distance = Vector3.Distance( self.position, target.position); return distance <= range ? NodeStatus.Success : NodeStatus.Failure; } } // 动作:移动到目标 public class MoveToTarget : BTNode { private float speed; private float stoppingDistance; public MoveToTarget(Blackboard bb, float speed, float stoppingDistance) : base("MoveToTarget", bb) { this.speed = speed; this.stoppingDistance = stoppingDistance; } public override NodeStatus Tick(float deltaTime) { var self = blackboard.Get<Transform>("self"); var target = blackboard.Get<Transform>("target"); if (self == null || target == null) return NodeStatus.Failure; float distance = Vector3.Distance( self.position, target.position); if (distance <= stoppingDistance) return NodeStatus.Success; Vector3 direction = (target.position - self.position).normalized; self.position += direction * speed * deltaTime; // 面向目标 self.rotation = Quaternion.LookRotation(direction); return NodeStatus.Running; } } // 动作:攻击 public class AttackTarget : BTNode { private float attackDuration; private float damage; private float timer; public AttackTarget(Blackboard bb, float damage, float duration) : base("AttackTarget", bb) { this.damage = damage; this.attackDuration = duration; } public override NodeStatus Tick(float deltaTime) { timer += deltaTime; if (timer >= attackDuration) { // 造成伤害 var target = blackboard.Get<Transform>("target"); var health = target?.GetComponent<HealthSystem>(); health?.TakeDamage(damage); timer = 0; return NodeStatus.Success; } return NodeStatus.Running; } public override void Reset() => timer = 0; }
// Unity C# - 使用行为树框架构建 NPC AI public class EnemyAI : MonoBehaviour { [Header("AI 参数")] public float detectionRange = 15f; public float attackRange = 2f; public float moveSpeed = 5f; public float attackDamage = 10f; public float attackCooldown = 1.5f; public float patrolSpeed = 2f; public Transform[] patrolPoints; private BTNode behaviorTree; private Blackboard blackboard; void Start() { blackboard = new Blackboard(); blackboard.Set("self", transform); blackboard.Set("patrolPoints", patrolPoints); blackboard.Set("patrolIndex", 0); behaviorTree = BuildBehaviorTree(); } void Update() { // 更新黑板数据 UpdateBlackboard(); // 执行行为树 behaviorTree.Tick(Time.deltaTime); } void UpdateBlackboard() { // 寻找最近的玩家 GameObject player = GameObject.FindGameObjectWithTag("Player"); if (player != null) { blackboard.Set("target", player.transform); blackboard.Set("distanceToTarget", Vector3.Distance(transform.position, player.transform.position)); } } BTNode BuildBehaviorTree() { // 行为树结构: // Root (Selector) // ├── Combat (Sequence) // │ ├── IsTargetInRange(detectionRange) // │ └── Selector // │ ├── Attack (Sequence) // │ │ ├── IsTargetInRange(attackRange) // │ │ └── Cooldown(AttackTarget) // │ └── MoveToTarget // └── Patrol (Sequence) // └── PatrolBetweenPoints var bb = blackboard; return new Selector("Root", bb, // 战斗分支 new Sequence("Combat", bb, new IsTargetInRange(bb, detectionRange), new Selector("CombatAction", bb, // 攻击 new Sequence("Attack", bb, new IsTargetInRange(bb, attackRange), new Cooldown("AttackCooldown", bb, new AttackTarget(bb, attackDamage, 0.5f), attackCooldown) ), // 追击 new MoveToTarget(bb, moveSpeed, attackRange * 0.8f) ) ), // 巡逻分支 new PatrolAction(bb, patrolSpeed) ); } }

Godot GDScript — Beehave 行为树

# Godot GDScript - 使用 Beehave 插件构建 NPC AI # 安装:在 AssetLib 搜索 "Beehave" 或通过 Git 子模块 # --- 自定义条件节点 --- # IsPlayerInRange.gd class_name IsPlayerInRange extends ConditionLeaf @export var detection_range: float = 200.0 func tick(actor: Node, blackboard: Blackboard) -> int: var player = blackboard.get_value("player") if player == null: return FAILURE var distance = actor.global_position.distance_to( player.global_position) blackboard.set_value("distance_to_player", distance) if distance <= detection_range: return SUCCESS return FAILURE
# IsPlayerInAttackRange.gd class_name IsPlayerInAttackRange extends ConditionLeaf @export var attack_range: float = 50.0 func tick(actor: Node, blackboard: Blackboard) -> int: var distance = blackboard.get_value("distance_to_player", INF) if distance <= attack_range: return SUCCESS return FAILURE
# --- 自定义动作节点 --- # ChasePlayer.gd class_name ChasePlayer extends ActionLeaf @export var chase_speed: float = 150.0 func tick(actor: Node, blackboard: Blackboard) -> int: var player = blackboard.get_value("player") if player == null: return FAILURE var direction = (player.global_position - actor.global_position).normalized actor.velocity = direction * chase_speed # 翻转精灵 if direction.x != 0: actor.get_node("Sprite2D").flip_h = direction.x < 0 actor.get_node("AnimationPlayer").play("run") actor.move_and_slide() return RUNNING
# AttackPlayer.gd class_name AttackPlayer extends ActionLeaf @export var damage: float = 10.0 @export var attack_duration: float = 0.5 var timer: float = 0.0 func tick(actor: Node, blackboard: Blackboard) -> int: timer += actor.get_process_delta_time() if timer <= 0.01: # 攻击开始 actor.get_node("AnimationPlayer").play("attack") actor.velocity = Vector2.ZERO if timer >= attack_duration: # 造成伤害 var player = blackboard.get_value("player") if player and player.has_method("take_damage"): player.take_damage(damage) timer = 0.0 return SUCCESS return RUNNING func _on_reset() -> void: timer = 0.0
# PatrolAction.gd class_name PatrolAction extends ActionLeaf @export var patrol_speed: float = 80.0 @export var wait_time: float = 2.0 var current_point_index: int = 0 var waiting: bool = false var wait_timer: float = 0.0 func tick(actor: Node, blackboard: Blackboard) -> int: var patrol_points = blackboard.get_value("patrol_points", []) if patrol_points.is_empty(): return FAILURE if waiting: wait_timer += actor.get_process_delta_time() if wait_timer >= wait_time: waiting = false wait_timer = 0.0 current_point_index = (current_point_index + 1) % patrol_points.size() return RUNNING var target_pos: Vector2 = patrol_points[current_point_index].global_position var direction = (target_pos - actor.global_position).normalized var distance = actor.global_position.distance_to(target_pos) if distance < 10.0: waiting = true actor.velocity = Vector2.ZERO actor.get_node("AnimationPlayer").play("idle") return RUNNING actor.velocity = direction * patrol_speed actor.get_node("Sprite2D").flip_h = direction.x < 0 actor.get_node("AnimationPlayer").play("walk") actor.move_and_slide() return RUNNING
# EnemyAI.gd - 敌人主脚本(场景树中配置 Beehave 节点) extends CharacterBody2D @export var gravity: float = 980.0 @onready var behavior_tree: BeehaveTree = $BeehaveTree func _ready() -> void: # 初始化黑板数据 var blackboard = behavior_tree.blackboard blackboard.set_value("patrol_points", $PatrolPoints.get_children()) # 查找玩家 var player = get_tree().get_first_node_in_group("player") blackboard.set_value("player", player) func _physics_process(delta: float) -> void: # 应用重力 if not is_on_floor(): velocity.y += gravity * delta move_and_slide() func take_damage(amount: float) -> void: $HealthComponent.take_damage(amount)

Beehave 场景树结构(在编辑器中配置):

EnemyAI (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D ├── AnimationPlayer ├── PatrolPoints │ ├── Point1 (Marker2D) │ └── Point2 (Marker2D) └── BeehaveTree └── SelectorComposite (Root) ├── SequenceComposite (Combat) │ ├── IsPlayerInRange (detection_range=200) │ └── SelectorComposite (CombatAction) │ ├── SequenceComposite (Attack) │ │ ├── IsPlayerInAttackRange (attack_range=50) │ │ └── AttackPlayer (damage=10) │ └── ChasePlayer (chase_speed=150) └── PatrolAction (patrol_speed=80)

Unreal Engine C++ — 行为树任务

// Unreal Engine C++ - 自定义行为树任务(BTTask) // BTTask_FindPatrolPoint.h #pragma once #include "BehaviorTree/BTTaskNode.h" #include "BTTask_FindPatrolPoint.generated.h" UCLASS() class MYGAME_API UBTTask_FindPatrolPoint : public UBTTaskNode { GENERATED_BODY() public: UBTTask_FindPatrolPoint(); virtual EBTNodeResult::Type ExecuteTask( UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override; // 黑板键:巡逻点位置 UPROPERTY(EditAnywhere, Category = "Blackboard") FBlackboardKeySelector PatrolLocationKey; // 巡逻半径 UPROPERTY(EditAnywhere, Category = "Patrol") float PatrolRadius = 500.0f; }; // BTTask_FindPatrolPoint.cpp UBTTask_FindPatrolPoint::UBTTask_FindPatrolPoint() { NodeName = TEXT("Find Patrol Point"); } EBTNodeResult::Type UBTTask_FindPatrolPoint::ExecuteTask( UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) { AAIController* AIController = OwnerComp.GetAIOwner(); if (!AIController) return EBTNodeResult::Failed; APawn* AIPawn = AIController->GetPawn(); if (!AIPawn) return EBTNodeResult::Failed; // 在巡逻半径内随机选择一个可导航点 UNavigationSystemV1* NavSys = UNavigationSystemV1::GetCurrent(GetWorld()); if (!NavSys) return EBTNodeResult::Failed; FNavLocation RandomLocation; bool bFound = NavSys->GetRandomReachablePointInRadius( AIPawn->GetActorLocation(), PatrolRadius, RandomLocation); if (bFound) { OwnerComp.GetBlackboardComponent()->SetValueAsVector( PatrolLocationKey.SelectedKeyName, RandomLocation.Location); return EBTNodeResult::Succeeded; } return EBTNodeResult::Failed; }
// BTTask_AttackTarget.h - 攻击任务 #pragma once #include "BehaviorTree/BTTaskNode.h" #include "BTTask_AttackTarget.generated.h" UCLASS() class MYGAME_API UBTTask_AttackTarget : public UBTTaskNode { GENERATED_BODY() public: virtual EBTNodeResult::Type ExecuteTask( UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override; virtual void TickTask( UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override; UPROPERTY(EditAnywhere, Category = "Attack") float AttackDamage = 20.0f; UPROPERTY(EditAnywhere, Category = "Attack") float AttackDuration = 1.0f; UPROPERTY(EditAnywhere, Category = "Attack") UAnimMontage* AttackMontage; private: float AttackTimer = 0.0f; bool bDamageApplied = false; }; // BTTask_AttackTarget.cpp EBTNodeResult::Type UBTTask_AttackTarget::ExecuteTask( UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) { AAIController* AIController = OwnerComp.GetAIOwner(); APawn* AIPawn = AIController ? AIController->GetPawn() : nullptr; if (!AIPawn) return EBTNodeResult::Failed; // 播放攻击动画 if (AttackMontage) { UAnimInstance* AnimInstance = Cast<ACharacter>(AIPawn)->GetMesh()->GetAnimInstance(); if (AnimInstance) { AnimInstance->Montage_Play(AttackMontage); } } AttackTimer = 0.0f; bDamageApplied = false; bNotifyTick = true; return EBTNodeResult::InProgress; } void UBTTask_AttackTarget::TickTask( UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) { AttackTimer += DeltaSeconds; // 在攻击动画中间点造成伤害 if (!bDamageApplied && AttackTimer >= AttackDuration * 0.4f) { AActor* Target = Cast<AActor>( OwnerComp.GetBlackboardComponent()->GetValueAsObject( TEXT("TargetActor"))); if (Target) { UGameplayStatics::ApplyDamage( Target, AttackDamage, OwnerComp.GetAIOwner(), OwnerComp.GetAIOwner()->GetPawn(), nullptr); } bDamageApplied = true; } if (AttackTimer >= AttackDuration) { FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded); } }

3.3 NPC AI 常见模式

巡逻-追击-攻击模式(最常用)

行为树结构: Root (Selector) ├── 战斗 (Sequence) │ ├── [条件] 检测到玩家? │ └── (Selector) │ ├── 攻击 (Sequence) │ │ ├── [条件] 在攻击范围内? │ │ └── [动作] 执行攻击(带冷却) │ └── [动作] 追击玩家 ├── 调查 (Sequence) │ ├── [条件] 听到声音? │ └── [动作] 移动到声音位置 └── 巡逻 (Sequence) └── [动作] 在巡逻点间移动

Boss AI 多阶段模式

行为树结构: Root (Selector) ├── 阶段3:狂暴 (Sequence) │ ├── [条件] 血量 < 30%? │ └── (Selector) │ ├── [动作] 全屏 AOE 攻击(冷却 8s) │ ├── [动作] 连续冲刺攻击 x3 │ └── [动作] 召唤小怪 ├── 阶段2:强化 (Sequence) │ ├── [条件] 血量 < 60%? │ └── (Selector) │ ├── [动作] 远程弹幕攻击(冷却 5s) │ ├── [动作] 跳跃砸地攻击 │ └── [动作] 追击玩家 └── 阶段1:普通 (Selector) ├── 攻击 (Sequence) │ ├── [条件] 在攻击范围内? │ └── (Selector) │ ├── [动作] 重击(冷却 3s) │ └── [动作] 普通攻击 └── [动作] 缓慢接近玩家

3.4 行为树提示词模板

模板 6:NPC 行为树生成

请为 [敌人类型,如:巡逻守卫 / 远程弓箭手 / Boss] 生成完整的行为树 AI。 引擎:[Unity C# / Godot GDScript + Beehave / Unreal C++ BT] NPC 行为需求: 1. 巡逻:在 [N] 个巡逻点间移动,每个点停留 [X] 秒 2. 检测:视野范围 [R] 单位,视野角度 [A] 度 3. 追击:检测到玩家后追击,速度 [S],追击超过 [T] 秒或距离超过 [D] 放弃 4. 攻击: - 近战攻击:范围 [R1],伤害 [D1],冷却 [C1] 秒 - [可选] 远程攻击:范围 [R2],弹速 [V],冷却 [C2] 秒 5. 受伤反应:受伤后 [闪避 / 后退 / 格挡] [X] 秒 6. 死亡:播放死亡动画,掉落物品 黑板变量: - target: 当前目标 Transform - last_known_position: 最后已知目标位置 - health: 当前血量 - is_alerted: 是否处于警戒状态 要求: - 提供完整的行为树结构图(文本格式) - 提供所有自定义节点的代码实现 - 包含黑板数据管理 - 包含动画播放调用

模板 7:行为树调试与优化

我的 NPC 行为树有以下问题: [描述问题,如:NPC 在追击和巡逻之间频繁切换 / 攻击动画被打断 / 多个 NPC 同时攻击导致卡顿] 当前行为树结构: [粘贴行为树结构或代码] 请: 1. 分析问题根因 2. 提供修复方案 3. 建议性能优化(如:条件检查频率、黑板更新策略) 4. 如果适用,建议添加以下高级特性: - 冷却机制防止行为抖动 - 组 AI 协调(避免所有敌人同时攻击) - 感知系统(视觉 + 听觉 + 记忆衰减)

4. 寻路(Pathfinding)

4.1 寻路算法概述

寻路是游戏 AI 的基础能力,让角色能够在复杂环境中找到从 A 到 B 的最优路径。

主流寻路方案对比:

方案原理适用场景性能动态障碍
A*网格搜索,启发式最短路径2D 网格地图、策略游戏中等需重新计算
NavMesh导航网格,多边形区域3D 游戏、开放世界支持动态避障
Flow Field流场,全局方向向量大量单位同时寻路(RTS)高(批量)需重新生成
Jump Point SearchA* 优化,跳点搜索均匀网格地图很高需重新计算
Theta*A* 变体,任意角度路径需要平滑路径的场景中等需重新计算

工具推荐

工具用途价格适用场景
Unity NavMeshUnity 内置导航系统Unity 内置3D 场景导航
Unity A Pathfinding Project*高级寻路插件免费版/Pro $100复杂 2D/3D 寻路
Godot NavigationServerGodot 内置导航系统免费(引擎内置)2D/3D 导航
Unreal Navigation SystemUE 内置 NavMesh免费(引擎内置)UE 标准导航
Claude Code生成自定义寻路算法$20-100/月A* 实现、流场算法

4.2 A* 寻路算法实现

Unity C# — A* 网格寻路

// Unity C# - A* 寻路系统(2D 网格) using System.Collections.Generic; using UnityEngine; public class AStarPathfinding : MonoBehaviour { [Header("网格设置")] public int gridWidth = 50; public int gridHeight = 50; public float cellSize = 1f; public LayerMask obstacleLayer; private PathNode[,] grid; // 路径节点 public class PathNode { public int X, Y; public bool Walkable; public int GCost; // 起点到当前的代价 public int HCost; // 当前到终点的启发值 public int FCost => GCost + HCost; // 总代价 public PathNode Parent; public int MovementPenalty; // 地形惩罚 public PathNode(int x, int y, bool walkable, int penalty = 0) { X = x; Y = y; Walkable = walkable; MovementPenalty = penalty; } } void Start() { GenerateGrid(); } // 生成网格 void GenerateGrid() { grid = new PathNode[gridWidth, gridHeight]; for (int x = 0; x < gridWidth; x++) { for (int y = 0; y < gridHeight; y++) { Vector2 worldPos = GridToWorld(x, y); bool walkable = !Physics2D.OverlapCircle( worldPos, cellSize * 0.4f, obstacleLayer); // 地形惩罚(可选:不同地形不同代价) int penalty = 0; // 例如:沼泽地形惩罚 +5 if (Physics2D.OverlapCircle(worldPos, cellSize * 0.4f, LayerMask.GetMask("Swamp"))) penalty = 5; grid[x, y] = new PathNode(x, y, walkable, penalty); } } } // A* 寻路核心算法 public List<Vector2> FindPath(Vector2 startWorld, Vector2 endWorld) { PathNode startNode = WorldToNode(startWorld); PathNode endNode = WorldToNode(endWorld); if (startNode == null || endNode == null || !endNode.Walkable) return null; // 开放列表和关闭列表 List<PathNode> openList = new List<PathNode> { startNode }; HashSet<PathNode> closedSet = new HashSet<PathNode>(); // 初始化所有节点 for (int x = 0; x < gridWidth; x++) for (int y = 0; y < gridHeight; y++) { grid[x, y].GCost = int.MaxValue; grid[x, y].Parent = null; } startNode.GCost = 0; startNode.HCost = CalculateHeuristic(startNode, endNode); while (openList.Count > 0) { // 找 F 值最小的节点 PathNode current = GetLowestFCost(openList); if (current == endNode) return RetracePath(startNode, endNode); openList.Remove(current); closedSet.Add(current); // 遍历邻居 foreach (PathNode neighbor in GetNeighbors(current)) { if (!neighbor.Walkable || closedSet.Contains(neighbor)) continue; // 对角线移动代价 14,直线 10 int moveCost = current.GCost + CalculateDistance(current, neighbor) + neighbor.MovementPenalty; if (moveCost < neighbor.GCost) { neighbor.GCost = moveCost; neighbor.HCost = CalculateHeuristic( neighbor, endNode); neighbor.Parent = current; if (!openList.Contains(neighbor)) openList.Add(neighbor); } } } return null; // 无路径 } // 启发函数(曼哈顿距离 + 对角线优化) int CalculateHeuristic(PathNode a, PathNode b) { int dx = Mathf.Abs(a.X - b.X); int dy = Mathf.Abs(a.Y - b.Y); // 对角线距离:min * 14 + (max - min) * 10 return Mathf.Min(dx, dy) * 14 + Mathf.Abs(dx - dy) * 10; } int CalculateDistance(PathNode a, PathNode b) { int dx = Mathf.Abs(a.X - b.X); int dy = Mathf.Abs(a.Y - b.Y); return (dx + dy == 2) ? 14 : 10; } // 获取邻居节点(8 方向) List<PathNode> GetNeighbors(PathNode node) { List<PathNode> neighbors = new List<PathNode>(); for (int dx = -1; dx <= 1; dx++) { for (int dy = -1; dy <= 1; dy++) { if (dx == 0 && dy == 0) continue; int nx = node.X + dx; int ny = node.Y + dy; if (nx >= 0 && nx < gridWidth && ny >= 0 && ny < gridHeight) { // 对角线移动检查:防止穿墙角 if (dx != 0 && dy != 0) { if (!grid[node.X + dx, node.Y].Walkable || !grid[node.X, node.Y + dy].Walkable) continue; } neighbors.Add(grid[nx, ny]); } } } return neighbors; } // 回溯路径 List<Vector2> RetracePath(PathNode start, PathNode end) { List<Vector2> path = new List<Vector2>(); PathNode current = end; while (current != start) { path.Add(GridToWorld(current.X, current.Y)); current = current.Parent; } path.Reverse(); return path; } PathNode GetLowestFCost(List<PathNode> list) { PathNode lowest = list[0]; foreach (var node in list) if (node.FCost < lowest.FCost || (node.FCost == lowest.FCost && node.HCost < lowest.HCost)) lowest = node; return lowest; } // 坐标转换 Vector2 GridToWorld(int x, int y) => new Vector2(x * cellSize + cellSize / 2f, y * cellSize + cellSize / 2f); PathNode WorldToNode(Vector2 worldPos) { int x = Mathf.FloorToInt(worldPos.x / cellSize); int y = Mathf.FloorToInt(worldPos.y / cellSize); if (x >= 0 && x < gridWidth && y >= 0 && y < gridHeight) return grid[x, y]; return null; } }

4.3 NavMesh 导航系统

Godot GDScript — NavigationAgent 导航

# Godot GDScript - NavigationAgent2D 完整导航系统 # NavigationEnemy.gd extends CharacterBody2D @export var move_speed: float = 200.0 @export var chase_speed: float = 350.0 @export var patrol_speed: float = 100.0 @export var detection_range: float = 300.0 @export var attack_range: float = 50.0 @export var gravity: float = 980.0 # 导航代理 @onready var nav_agent: NavigationAgent2D = $NavigationAgent2D @onready var detection_area: Area2D = $DetectionArea @onready var sprite: Sprite2D = $Sprite2D @onready var anim: AnimationPlayer = $AnimationPlayer # 巡逻点 @export var patrol_points: Array[Marker2D] = [] var current_patrol_index: int = 0 # AI 状态 enum AIState { PATROL, CHASE, ATTACK, RETURN } var ai_state: AIState = AIState.PATROL var target: Node2D = null var home_position: Vector2 func _ready() -> void: home_position = global_position # 配置导航代理 nav_agent.path_desired_distance = 4.0 nav_agent.target_desired_distance = 4.0 nav_agent.avoidance_enabled = true # 启用避障 nav_agent.radius = 16.0 nav_agent.max_speed = chase_speed # 连接信号 nav_agent.velocity_computed.connect(_on_velocity_computed) nav_agent.navigation_finished.connect(_on_navigation_finished) # 设置初始巡逻目标 if patrol_points.size() > 0: _set_patrol_target() func _physics_process(delta: float) -> void: # 应用重力 if not is_on_floor(): velocity.y += gravity * delta match ai_state: AIState.PATROL: _patrol_update() AIState.CHASE: _chase_update() AIState.ATTACK: _attack_update() AIState.RETURN: _return_update() func _patrol_update() -> void: if nav_agent.is_navigation_finished(): return var next_pos = nav_agent.get_next_path_position() var direction = (next_pos - global_position).normalized var desired_velocity = direction * patrol_speed # 使用避障系统 if nav_agent.avoidance_enabled: nav_agent.velocity = desired_velocity else: _move(desired_velocity) sprite.flip_h = direction.x < 0 anim.play("walk") # 检测玩家 _check_for_player() func _chase_update() -> void: if target == null or not is_instance_valid(target): ai_state = AIState.RETURN nav_agent.target_position = home_position return var distance = global_position.distance_to(target.global_position) # 进入攻击范围 if distance <= attack_range: ai_state = AIState.ATTACK velocity.x = 0 return # 脱离追击范围 if distance > detection_range * 1.5: target = null ai_state = AIState.RETURN nav_agent.target_position = home_position return # 更新导航目标 nav_agent.target_position = target.global_position var next_pos = nav_agent.get_next_path_position() var direction = (next_pos - global_position).normalized var desired_velocity = direction * chase_speed if nav_agent.avoidance_enabled: nav_agent.velocity = desired_velocity else: _move(desired_velocity) sprite.flip_h = direction.x < 0 anim.play("run") func _attack_update() -> void: if target == null or not is_instance_valid(target): ai_state = AIState.RETURN return var distance = global_position.distance_to(target.global_position) if distance > attack_range * 1.5: ai_state = AIState.CHASE return # 面向目标 sprite.flip_h = target.global_position.x < global_position.x anim.play("attack") func _return_update() -> void: if nav_agent.is_navigation_finished(): ai_state = AIState.PATROL _set_patrol_target() return var next_pos = nav_agent.get_next_path_position() var direction = (next_pos - global_position).normalized _move(direction * patrol_speed) anim.play("walk") func _move(desired_velocity: Vector2) -> void: velocity.x = desired_velocity.x move_and_slide() func _on_velocity_computed(safe_velocity: Vector2) -> void: velocity.x = safe_velocity.x move_and_slide() func _on_navigation_finished() -> void: if ai_state == AIState.PATROL: # 到达巡逻点,等待后前往下一个 anim.play("idle") await get_tree().create_timer(2.0).timeout current_patrol_index = (current_patrol_index + 1) % patrol_points.size() _set_patrol_target() func _set_patrol_target() -> void: if patrol_points.size() > 0: nav_agent.target_position = patrol_points[current_patrol_index].global_position func _check_for_player() -> void: var player = get_tree().get_first_node_in_group("player") if player == null: return var distance = global_position.distance_to(player.global_position) if distance <= detection_range: # 视线检查(射线检测) var space = get_world_2d().direct_space_state var query = PhysicsRayQueryParameters2D.create( global_position, player.global_position) query.collision_mask = 0b0001 # 只检测墙壁层 query.exclude = [get_rid()] var result = space.intersect_ray(query) if result.is_empty() or result.collider == player: target = player ai_state = AIState.CHASE nav_agent.target_position = target.global_position

Unity C# — NavMesh Agent 导航

// Unity C# - NavMesh 导航系统(3D) using UnityEngine; using UnityEngine.AI; [RequireComponent(typeof(NavMeshAgent))] public class NavMeshEnemy : MonoBehaviour { [Header("导航参数")] public float patrolSpeed = 3.5f; public float chaseSpeed = 6f; public float detectionRange = 15f; public float attackRange = 2f; public float fieldOfView = 120f; [Header("巡逻")] public Transform[] patrolPoints; public float patrolWaitTime = 2f; private NavMeshAgent agent; private Transform player; private int currentPatrolIndex; private float waitTimer; private Vector3 lastKnownPlayerPos; enum State { Patrol, Investigate, Chase, Attack } private State currentState = State.Patrol; void Start() { agent = GetComponent<NavMeshAgent>(); player = GameObject.FindGameObjectWithTag("Player")?.transform; SetPatrolTarget(); } void Update() { switch (currentState) { case State.Patrol: PatrolUpdate(); break; case State.Investigate: InvestigateUpdate(); break; case State.Chase: ChaseUpdate(); break; case State.Attack: AttackUpdate(); break; } } void PatrolUpdate() { agent.speed = patrolSpeed; if (CanSeePlayer()) { currentState = State.Chase; return; } if (!agent.pathPending && agent.remainingDistance < agent.stoppingDistance) { waitTimer += Time.deltaTime; if (waitTimer >= patrolWaitTime) { waitTimer = 0; currentPatrolIndex = (currentPatrolIndex + 1) % patrolPoints.Length; SetPatrolTarget(); } } } void ChaseUpdate() { agent.speed = chaseSpeed; if (player == null) { currentState = State.Patrol; return; } float distance = Vector3.Distance( transform.position, player.position); if (distance <= attackRange) { currentState = State.Attack; agent.isStopped = true; return; } if (!CanSeePlayer() && distance > detectionRange) { // 失去目标,前往最后已知位置 lastKnownPlayerPos = player.position; currentState = State.Investigate; agent.SetDestination(lastKnownPlayerPos); return; } agent.SetDestination(player.position); lastKnownPlayerPos = player.position; } void InvestigateUpdate() { agent.speed = patrolSpeed; if (CanSeePlayer()) { currentState = State.Chase; return; } if (!agent.pathPending && agent.remainingDistance < agent.stoppingDistance) { // 到达最后已知位置,环顾四周后恢复巡逻 currentState = State.Patrol; SetPatrolTarget(); } } void AttackUpdate() { if (player == null) { currentState = State.Patrol; return; } // 面向玩家 Vector3 lookDir = player.position - transform.position; lookDir.y = 0; transform.rotation = Quaternion.LookRotation(lookDir); float distance = Vector3.Distance( transform.position, player.position); if (distance > attackRange * 1.5f) { agent.isStopped = false; currentState = State.Chase; } } // 视野检测(视锥 + 射线) bool CanSeePlayer() { if (player == null) return false; Vector3 dirToPlayer = (player.position - transform.position).normalized; float distance = Vector3.Distance( transform.position, player.position); // 距离检查 if (distance > detectionRange) return false; // 视野角度检查 float angle = Vector3.Angle(transform.forward, dirToPlayer); if (angle > fieldOfView / 2f) return false; // 射线遮挡检查 if (Physics.Raycast(transform.position + Vector3.up, dirToPlayer, out RaycastHit hit, detectionRange)) { if (hit.transform == player) return true; } return false; } void SetPatrolTarget() { if (patrolPoints.Length > 0) agent.SetDestination( patrolPoints[currentPatrolIndex].position); } // 可视化调试 void OnDrawGizmosSelected() { // 检测范围 Gizmos.color = Color.yellow; Gizmos.DrawWireSphere(transform.position, detectionRange); // 攻击范围 Gizmos.color = Color.red; Gizmos.DrawWireSphere(transform.position, attackRange); // 视野锥 Vector3 leftBound = Quaternion.Euler(0, -fieldOfView / 2, 0) * transform.forward * detectionRange; Vector3 rightBound = Quaternion.Euler(0, fieldOfView / 2, 0) * transform.forward * detectionRange; Gizmos.color = Color.cyan; Gizmos.DrawRay(transform.position, leftBound); Gizmos.DrawRay(transform.position, rightBound); } }

4.4 寻路提示词模板

模板 8:导航系统设置

请为我的 [2D / 3D] [类型] 游戏设置完整的导航系统。 引擎:[Unity / Godot / Unreal] 地图特征: - 地图类型:[开放世界 / 室内迷宫 / 多层建筑 / 2D 平台] - 地形类型:[平坦 / 有斜坡 / 有楼梯 / 有跳跃点] - 动态障碍:[是/否],类型:[可推动箱子 / 开关门 / 可破坏墙壁] NPC 导航需求: 1. 基础寻路:从 A 到 B 的最短路径 2. 避障:动态避开其他 NPC 和移动障碍 3. 区域代价:[沼泽减速 / 危险区域回避 / 捷径偏好] 4. 跳跃/攀爬:[是否需要 Off-Mesh Link / 导航链接] 要求: - 提供导航网格/导航区域的配置步骤 - 提供 NPC 导航脚本 - 包含路径平滑处理 - 包含导航失败的回退策略 - 包含调试可视化

模板 9:自定义 A* 实现

请实现一个 A* 寻路系统,用于 [2D 网格 / 六边形网格 / 加权图]。 引擎:[Unity C# / Godot GDScript / 纯 Python/TypeScript] 网格规格: - 尺寸:[W] x [H] - 移动方向:[4方向 / 8方向 / 6方向(六边形)] - 地形类型:[平地(代价1) / 森林(代价3) / 沼泽(代价5) / 墙壁(不可通行)] 优化需求: - [ ] 二叉堆优先队列(替代线性搜索) - [ ] 路径缓存 - [ ] 分帧计算(避免卡顿) - [ ] 路径平滑(去除锯齿) - [ ] 对角线移动防穿墙 要求: - 提供完整的 A* 实现代码 - 包含网格生成和障碍物标记 - 包含路径可视化(Gizmo/Debug Draw) - 包含性能基准测试方法

5. 程序化生成(Procedural Generation)

5.1 程序化生成概述

程序化生成(PCG)是通过算法自动创建游戏内容的技术,包括地牢、地形、关卡、物品等。AI Agent 可以帮助快速实现各种 PCG 算法,并根据设计需求调整参数。

主流 PCG 算法:

算法适用场景复杂度可控性AI 生成难度
BSP(二叉空间分割)矩形房间地牢
Random Walk(随机游走)洞穴、有机形状
Cellular Automata(元胞自动机)洞穴、自然地形
Wave Function Collapse(WFC)瓦片地图、关卡很高
Perlin/Simplex Noise地形高度图、纹理
L-System植物、分形结构
Voronoi Diagram区域划分、生物群落
Graph Grammar关卡结构、任务图很高

工具推荐

工具用途价格适用场景
Claude Code生成 PCG 算法代码$20-100/月自定义生成算法
CursorPCG 代码辅助免费-$20/月日常 PCG 编码
Unity ProBuilder快速关卡原型Unity 内置关卡白盒
Godot TileMap2D 瓦片地图免费(引擎内置)2D 关卡生成
Houdini Engine高级程序化建模$269/年(Indie)3D 程序化资产
Dungeon ArchitectUnity/UE 地牢生成插件$80(Unity)/ $50(UE)快速地牢原型

5.2 BSP 地牢生成

Unity C# — BSP 地牢生成器

// Unity C# - BSP(二叉空间分割)地牢生成器 using System.Collections.Generic; using UnityEngine; using UnityEngine.Tilemaps; public class BSPDungeonGenerator : MonoBehaviour { [Header("地牢参数")] public int dungeonWidth = 80; public int dungeonHeight = 60; public int minRoomSize = 8; public int maxRoomSize = 20; public int minRoomPadding = 2; public int maxSplitDepth = 5; public int corridorWidth = 3; [Header("Tilemap 引用")] public Tilemap floorTilemap; public Tilemap wallTilemap; public TileBase floorTile; public TileBase wallTile; [Header("种子")] public int seed = -1; // -1 = 随机种子 private List<RectInt> rooms = new(); private HashSet<Vector2Int> floorPositions = new(); // BSP 节点 class BSPNode { public RectInt Area; public BSPNode Left, Right; public RectInt? Room; public BSPNode(RectInt area) { Area = area; } } public void Generate() { // 清空 rooms.Clear(); floorPositions.Clear(); floorTilemap.ClearAllTiles(); wallTilemap.ClearAllTiles(); // 设置种子 if (seed >= 0) Random.InitState(seed); else Random.InitState(System.DateTime.Now.Millisecond); // 1. BSP 分割 BSPNode root = new BSPNode( new RectInt(0, 0, dungeonWidth, dungeonHeight)); SplitNode(root, 0); // 2. 在叶节点中生成房间 GenerateRooms(root); // 3. 连接房间(走廊) ConnectRooms(root); // 4. 绘制到 Tilemap PaintFloors(); PaintWalls(); Debug.Log($"地牢生成完成:{rooms.Count} 个房间"); } // BSP 递归分割 void SplitNode(BSPNode node, int depth) { if (depth >= maxSplitDepth) return; if (node.Area.width < minRoomSize * 2 && node.Area.height < minRoomSize * 2) return; // 决定分割方向 bool splitHorizontal; if (node.Area.width > node.Area.height * 1.25f) splitHorizontal = false; else if (node.Area.height > node.Area.width * 1.25f) splitHorizontal = true; else splitHorizontal = Random.value > 0.5f; if (splitHorizontal) { int splitY = Random.Range( node.Area.y + minRoomSize, node.Area.yMax - minRoomSize); node.Left = new BSPNode(new RectInt( node.Area.x, node.Area.y, node.Area.width, splitY - node.Area.y)); node.Right = new BSPNode(new RectInt( node.Area.x, splitY, node.Area.width, node.Area.yMax - splitY)); } else { int splitX = Random.Range( node.Area.x + minRoomSize, node.Area.xMax - minRoomSize); node.Left = new BSPNode(new RectInt( node.Area.x, node.Area.y, splitX - node.Area.x, node.Area.height)); node.Right = new BSPNode(new RectInt( splitX, node.Area.y, node.Area.xMax - splitX, node.Area.height)); } SplitNode(node.Left, depth + 1); SplitNode(node.Right, depth + 1); } // 在叶节点生成房间 void GenerateRooms(BSPNode node) { if (node.Left != null) GenerateRooms(node.Left); if (node.Right != null) GenerateRooms(node.Right); // 叶节点:生成房间 if (node.Left == null && node.Right == null) { int roomW = Random.Range(minRoomSize, Mathf.Min(maxRoomSize, node.Area.width - minRoomPadding * 2)); int roomH = Random.Range(minRoomSize, Mathf.Min(maxRoomSize, node.Area.height - minRoomPadding * 2)); int roomX = Random.Range( node.Area.x + minRoomPadding, node.Area.xMax - roomW - minRoomPadding); int roomY = Random.Range( node.Area.y + minRoomPadding, node.Area.yMax - roomH - minRoomPadding); RectInt room = new RectInt(roomX, roomY, roomW, roomH); node.Room = room; rooms.Add(room); // 记录地板位置 for (int x = room.x; x < room.xMax; x++) for (int y = room.y; y < room.yMax; y++) floorPositions.Add(new Vector2Int(x, y)); } } // 连接房间(L 形走廊) void ConnectRooms(BSPNode node) { if (node.Left == null || node.Right == null) return; ConnectRooms(node.Left); ConnectRooms(node.Right); // 获取左右子树的房间中心 Vector2Int leftCenter = GetNodeCenter(node.Left); Vector2Int rightCenter = GetNodeCenter(node.Right); // 生成 L 形走廊 CreateCorridor(leftCenter, rightCenter); } void CreateCorridor(Vector2Int from, Vector2Int to) { // 随机选择先水平还是先垂直 if (Random.value > 0.5f) { CreateHorizontalCorridor(from.x, to.x, from.y); CreateVerticalCorridor(from.y, to.y, to.x); } else { CreateVerticalCorridor(from.y, to.y, from.x); CreateHorizontalCorridor(from.x, to.x, to.y); } } void CreateHorizontalCorridor(int x1, int x2, int y) { int minX = Mathf.Min(x1, x2); int maxX = Mathf.Max(x1, x2); for (int x = minX; x <= maxX; x++) for (int w = 0; w < corridorWidth; w++) floorPositions.Add(new Vector2Int(x, y + w)); } void CreateVerticalCorridor(int y1, int y2, int x) { int minY = Mathf.Min(y1, y2); int maxY = Mathf.Max(y1, y2); for (int y = minY; y <= maxY; y++) for (int w = 0; w < corridorWidth; w++) floorPositions.Add(new Vector2Int(x + w, y)); } Vector2Int GetNodeCenter(BSPNode node) { if (node.Room.HasValue) { var r = node.Room.Value; return new Vector2Int(r.x + r.width / 2, r.y + r.height / 2); } if (node.Left != null) return GetNodeCenter(node.Left); if (node.Right != null) return GetNodeCenter(node.Right); return new Vector2Int( node.Area.x + node.Area.width / 2, node.Area.y + node.Area.height / 2); } // 绘制地板 void PaintFloors() { foreach (var pos in floorPositions) floorTilemap.SetTile( new Vector3Int(pos.x, pos.y, 0), floorTile); } // 绘制墙壁(地板边缘) void PaintWalls() { foreach (var pos in floorPositions) { for (int dx = -1; dx <= 1; dx++) { for (int dy = -1; dy <= 1; dy++) { Vector2Int neighbor = new Vector2Int(pos.x + dx, pos.y + dy); if (!floorPositions.Contains(neighbor)) { wallTilemap.SetTile( new Vector3Int(neighbor.x, neighbor.y, 0), wallTile); } } } } } // 获取随机房间位置(用于放置玩家、敌人、宝箱) public Vector2Int GetRandomRoomPosition() { RectInt room = rooms[Random.Range(0, rooms.Count)]; return new Vector2Int( Random.Range(room.x + 1, room.xMax - 1), Random.Range(room.y + 1, room.yMax - 1)); } // 获取最远的两个房间(用于放置入口和出口) public (RectInt start, RectInt end) GetStartAndEndRooms() { float maxDist = 0; RectInt startRoom = rooms[0], endRoom = rooms[0]; for (int i = 0; i < rooms.Count; i++) { for (int j = i + 1; j < rooms.Count; j++) { float dist = Vector2Int.Distance( new Vector2Int(rooms[i].center.x, rooms[i].center.y), new Vector2Int(rooms[j].center.x, rooms[j].center.y)); if (dist > maxDist) { maxDist = dist; startRoom = rooms[i]; endRoom = rooms[j]; } } } return (startRoom, endRoom); } }

5.3 波函数坍缩(Wave Function Collapse)

Godot GDScript — 简化 WFC 瓦片地图生成

# Godot GDScript - 简化 WFC(Wave Function Collapse)瓦片地图生成器 # WFCGenerator.gd extends Node2D # 瓦片类型定义 enum TileType { FLOOR, # 地板 WALL, # 墙壁 CORRIDOR, # 走廊 DOOR, # 门 WATER, # 水 GRASS # 草地 } # 邻接规则:每种瓦片可以与哪些瓦片相邻 # 格式:{瓦片类型: {方向: [允许的邻居类型]}} # 方向:0=上, 1=右, 2=下, 3=左 var adjacency_rules: Dictionary = { TileType.FLOOR: { 0: [TileType.FLOOR, TileType.WALL, TileType.DOOR, TileType.CORRIDOR], 1: [TileType.FLOOR, TileType.WALL, TileType.DOOR, TileType.CORRIDOR], 2: [TileType.FLOOR, TileType.WALL, TileType.DOOR, TileType.CORRIDOR], 3: [TileType.FLOOR, TileType.WALL, TileType.DOOR, TileType.CORRIDOR], }, TileType.WALL: { 0: [TileType.WALL, TileType.FLOOR, TileType.CORRIDOR], 1: [TileType.WALL, TileType.FLOOR, TileType.CORRIDOR], 2: [TileType.WALL, TileType.FLOOR, TileType.CORRIDOR], 3: [TileType.WALL, TileType.FLOOR, TileType.CORRIDOR], }, TileType.CORRIDOR: { 0: [TileType.CORRIDOR, TileType.FLOOR, TileType.DOOR, TileType.WALL], 1: [TileType.CORRIDOR, TileType.FLOOR, TileType.DOOR, TileType.WALL], 2: [TileType.CORRIDOR, TileType.FLOOR, TileType.DOOR, TileType.WALL], 3: [TileType.CORRIDOR, TileType.FLOOR, TileType.DOOR, TileType.WALL], }, TileType.DOOR: { 0: [TileType.WALL], 1: [TileType.FLOOR, TileType.CORRIDOR], 2: [TileType.WALL], 3: [TileType.FLOOR, TileType.CORRIDOR], }, TileType.WATER: { 0: [TileType.WATER, TileType.GRASS], 1: [TileType.WATER, TileType.GRASS], 2: [TileType.WATER, TileType.GRASS], 3: [TileType.WATER, TileType.GRASS], }, TileType.GRASS: { 0: [TileType.GRASS, TileType.FLOOR, TileType.WATER], 1: [TileType.GRASS, TileType.FLOOR, TileType.WATER], 2: [TileType.GRASS, TileType.FLOOR, TileType.WATER], 3: [TileType.GRASS, TileType.FLOOR, TileType.WATER], }, } ```gdscript # WFC 核心算法(续) # 瓦片权重(出现概率) var tile_weights: Dictionary = { TileType.FLOOR: 3.0, TileType.WALL: 2.0, TileType.CORRIDOR: 1.5, TileType.DOOR: 0.3, TileType.WATER: 0.5, TileType.GRASS: 1.0, } @export var grid_width: int = 30 @export var grid_height: int = 20 @export var rng_seed: int = -1 # 网格:每个单元格存储可能的瓦片类型集合 var grid: Array = [] # Array[Array[Array[TileType]]] var collapsed: Array = [] # Array[Array[int]] -1=未坍缩 # 方向偏移:上、右、下、左 var directions = [ Vector2i(0, -1), # 上 Vector2i(1, 0), # 右 Vector2i(0, 1), # 下 Vector2i(-1, 0), # 左 ] func generate() -> Array: """执行 WFC 算法,返回生成的网格""" if rng_seed >= 0: seed(rng_seed) _initialize_grid() var max_iterations = grid_width * grid_height * 10 var iteration = 0 while not _is_fully_collapsed() and iteration < max_iterations: # 1. 找到熵最低的未坍缩单元格 var cell = _find_lowest_entropy_cell() if cell == Vector2i(-1, -1): break # 2. 坍缩该单元格(根据权重随机选择) var success = _collapse_cell(cell) if not success: # 矛盾!回溯或重新开始 push_warning("WFC 矛盾,重新生成...") return generate() # 3. 传播约束 _propagate(cell) iteration += 1 return collapsed func _initialize_grid() -> void: """初始化网格,每个单元格包含所有可能的瓦片类型""" grid.clear() collapsed.clear() var all_types = TileType.values() for y in range(grid_height): var row = [] var collapsed_row = [] for x in range(grid_width): # 边界强制为墙壁 if x == 0 or x == grid_width - 1 or y == 0 or y == grid_height - 1: row.append([TileType.WALL]) collapsed_row.append(TileType.WALL) else: row.append(all_types.duplicate()) collapsed_row.append(-1) grid.append(row) collapsed.append(collapsed_row) func _find_lowest_entropy_cell() -> Vector2i: """找到可能性最少(熵最低)的未坍缩单元格""" var min_entropy = 999 var candidates: Array[Vector2i] = [] for y in range(grid_height): for x in range(grid_width): if collapsed[y][x] != -1: continue var entropy = grid[y][x].size() if entropy < min_entropy: min_entropy = entropy candidates = [Vector2i(x, y)] elif entropy == min_entropy: candidates.append(Vector2i(x, y)) if candidates.is_empty(): return Vector2i(-1, -1) # 随机选择一个(打破平局) return candidates[randi() % candidates.size()] func _collapse_cell(cell: Vector2i) -> bool: """坍缩单元格:根据权重随机选择一个瓦片类型""" var possible = grid[cell.y][cell.x] if possible.is_empty(): return false # 加权随机选择 var total_weight = 0.0 for tile_type in possible: total_weight += tile_weights.get(tile_type, 1.0) var roll = randf() * total_weight var cumulative = 0.0 var chosen = possible[0] for tile_type in possible: cumulative += tile_weights.get(tile_type, 1.0) if roll <= cumulative: chosen = tile_type break grid[cell.y][cell.x] = [chosen] collapsed[cell.y][cell.x] = chosen return true func _propagate(start: Vector2i) -> void: """约束传播:更新邻居的可能性""" var stack: Array[Vector2i] = [start] while not stack.is_empty(): var current = stack.pop_back() var current_possible = grid[current.y][current.x] for dir_idx in range(4): var dir = directions[dir_idx] var nx = current.x + dir.x var ny = current.y + dir.y if nx < 0 or nx >= grid_width or ny < 0 or ny >= grid_height: continue if collapsed[ny][nx] != -1: continue # 计算邻居允许的瓦片类型 var allowed: Array = [] for tile_type in current_possible: var rules = adjacency_rules.get(tile_type, {}) var dir_rules = rules.get(dir_idx, []) for allowed_type in dir_rules: if allowed_type not in allowed: allowed.append(allowed_type) # 与邻居当前可能性取交集 var neighbor_possible = grid[ny][nx] var new_possible: Array = [] for tile_type in neighbor_possible: if tile_type in allowed: new_possible.append(tile_type) # 如果可能性减少了,更新并继续传播 if new_possible.size() < neighbor_possible.size(): grid[ny][nx] = new_possible if new_possible.size() == 1: collapsed[ny][nx] = new_possible[0] stack.append(Vector2i(nx, ny)) func _is_fully_collapsed() -> bool: """检查是否所有单元格都已坍缩""" for y in range(grid_height): for x in range(grid_width): if collapsed[y][x] == -1: return false return true

5.4 地形生成(Perlin Noise)

Unity C# — 程序化地形生成

// Unity C# - Perlin Noise 程序化地形生成 using UnityEngine; [RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))] public class TerrainGenerator : MonoBehaviour { [Header("地形尺寸")] public int width = 256; public int depth = 256; public float heightScale = 20f; [Header("噪声参数")] public float noiseScale = 50f; public int octaves = 4; [Range(0f, 1f)] public float persistence = 0.5f; public float lacunarity = 2f; public Vector2 offset; public int seed; [Header("生物群落")] public Gradient terrainGradient; public float waterLevel = 0.3f; private Mesh mesh; private Vector3[] vertices; private int[] triangles; private Color[] colors; void Start() { GenerateTerrain(); } public void GenerateTerrain() { mesh = new Mesh(); mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32; GetComponent<MeshFilter>().mesh = mesh; GenerateVertices(); GenerateTriangles(); ApplyBiomeColors(); UpdateMesh(); GenerateCollider(); } void GenerateVertices() { vertices = new Vector3[(width + 1) * (depth + 1)]; System.Random prng = new System.Random(seed); Vector2[] octaveOffsets = new Vector2[octaves]; for (int i = 0; i < octaves; i++) { float offsetX = prng.Next(-100000, 100000) + offset.x; float offsetY = prng.Next(-100000, 100000) + offset.y; octaveOffsets[i] = new Vector2(offsetX, offsetY); } float maxHeight = float.MinValue; float minHeight = float.MaxValue; for (int z = 0; z <= depth; z++) { for (int x = 0; x <= width; x++) { float height = 0; float amplitude = 1; float frequency = 1; // 多层叠加(分形噪声) for (int o = 0; o < octaves; o++) { float sampleX = (x + octaveOffsets[o].x) / noiseScale * frequency; float sampleZ = (z + octaveOffsets[o].y) / noiseScale * frequency; float perlinValue = Mathf.PerlinNoise(sampleX, sampleZ) * 2 - 1; height += perlinValue * amplitude; amplitude *= persistence; frequency *= lacunarity; } if (height > maxHeight) maxHeight = height; if (height < minHeight) minHeight = height; int index = z * (width + 1) + x; vertices[index] = new Vector3(x, height, z); } } // 归一化高度并应用缩放 for (int i = 0; i < vertices.Length; i++) { float normalizedHeight = Mathf.InverseLerp( minHeight, maxHeight, vertices[i].y); // 水面以下压平 if (normalizedHeight < waterLevel) normalizedHeight = waterLevel; vertices[i].y = normalizedHeight * heightScale; } } void GenerateTriangles() { triangles = new int[width * depth * 6]; int triIndex = 0; for (int z = 0; z < depth; z++) { for (int x = 0; x < width; x++) { int vertIndex = z * (width + 1) + x; triangles[triIndex + 0] = vertIndex; triangles[triIndex + 1] = vertIndex + width + 1; triangles[triIndex + 2] = vertIndex + 1; triangles[triIndex + 3] = vertIndex + 1; triangles[triIndex + 4] = vertIndex + width + 1; triangles[triIndex + 5] = vertIndex + width + 2; triIndex += 6; } } } void ApplyBiomeColors() { colors = new Color[vertices.Length]; for (int i = 0; i < vertices.Length; i++) { float height = vertices[i].y / heightScale; colors[i] = terrainGradient.Evaluate(height); } } void UpdateMesh() { mesh.Clear(); mesh.vertices = vertices; mesh.triangles = triangles; mesh.colors = colors; mesh.RecalculateNormals(); mesh.RecalculateBounds(); } void GenerateCollider() { MeshCollider collider = GetComponent<MeshCollider>(); if (collider == null) collider = gameObject.AddComponent<MeshCollider>(); collider.sharedMesh = mesh; } }

5.5 元胞自动机洞穴生成

Godot GDScript — 元胞自动机

# Godot GDScript - 元胞自动机洞穴生成 # CaveGenerator.gd extends TileMapLayer @export var cave_width: int = 60 @export var cave_height: int = 40 @export var fill_percent: float = 0.45 # 初始填充比例 @export var smooth_iterations: int = 5 # 平滑迭代次数 @export var wall_threshold: int = 4 # 邻居墙壁数 >= 此值则变为墙壁 @export var floor_threshold: int = 4 # 邻居地板数 >= 此值则变为地板 @export var rng_seed: int = -1 # 0 = 地板, 1 = 墙壁 var cave_map: Array = [] # TileSet 中的瓦片 ID @export var floor_tile_id: int = 0 @export var wall_tile_id: int = 1 func generate() -> void: if rng_seed >= 0: seed(rng_seed) _initialize_map() _smooth_map() _ensure_connectivity() _paint_tilemap() func _initialize_map() -> void: """随机初始化地图""" cave_map.clear() for y in range(cave_height): var row: Array[int] = [] for x in range(cave_width): # 边界强制为墙壁 if x == 0 or x == cave_width - 1 or y == 0 or y == cave_height - 1: row.append(1) else: row.append(1 if randf() < fill_percent else 0) cave_map.append(row) func _smooth_map() -> void: """元胞自动机平滑""" for _i in range(smooth_iterations): var new_map: Array = [] for y in range(cave_height): var row: Array[int] = [] for x in range(cave_width): var wall_count = _count_wall_neighbors(x, y) if cave_map[y][x] == 1: # 当前是墙壁:邻居墙壁少则变地板 row.append(1 if wall_count >= wall_threshold else 0) else: # 当前是地板:邻居墙壁多则变墙壁 row.append(1 if wall_count > floor_threshold else 0) new_map.append(row) cave_map = new_map func _count_wall_neighbors(cx: int, cy: int) -> int: """计算 8 邻域中墙壁的数量""" var count = 0 for dy in range(-1, 2): for dx in range(-1, 2): if dx == 0 and dy == 0: continue var nx = cx + dx var ny = cy + dy if nx < 0 or nx >= cave_width or ny < 0 or ny >= cave_height: count += 1 # 边界外视为墙壁 elif cave_map[ny][nx] == 1: count += 1 return count func _ensure_connectivity() -> void: """确保所有地板区域连通(洪水填充 + 连接)""" var regions = _find_floor_regions() if regions.size() <= 1: return # 找到最大区域 var largest_idx = 0 for i in range(regions.size()): if regions[i].size() > regions[largest_idx].size(): largest_idx = i # 将小区域连接到最大区域 for i in range(regions.size()): if i == largest_idx: continue # 找到两个区域最近的两个点 var closest = _find_closest_points(regions[largest_idx], regions[i]) _carve_corridor(closest[0], closest[1]) func _find_floor_regions() -> Array: """洪水填充找到所有独立的地板区域""" var visited: Dictionary = {} var regions: Array = [] for y in range(cave_height): for x in range(cave_width): if cave_map[y][x] == 0 and not visited.has(Vector2i(x, y)): var region = _flood_fill(x, y, visited) regions.append(region) return regions func _flood_fill(start_x: int, start_y: int, visited: Dictionary) -> Array: """洪水填充""" var region: Array[Vector2i] = [] var stack: Array[Vector2i] = [Vector2i(start_x, start_y)] while not stack.is_empty(): var pos = stack.pop_back() if visited.has(pos): continue if pos.x < 0 or pos.x >= cave_width or pos.y < 0 or pos.y >= cave_height: continue if cave_map[pos.y][pos.x] != 0: continue visited[pos] = true region.append(pos) stack.append(Vector2i(pos.x + 1, pos.y)) stack.append(Vector2i(pos.x - 1, pos.y)) stack.append(Vector2i(pos.x, pos.y + 1)) stack.append(Vector2i(pos.x, pos.y - 1)) return region func _find_closest_points(region_a: Array, region_b: Array) -> Array: """找到两个区域之间最近的两个点""" var min_dist = INF var closest_a = region_a[0] var closest_b = region_b[0] # 采样优化:不遍历所有点对 var step_a = max(1, region_a.size() / 50) var step_b = max(1, region_b.size() / 50) for i in range(0, region_a.size(), step_a): for j in range(0, region_b.size(), step_b): var dist = region_a[i].distance_squared_to(region_b[j]) if dist < min_dist: min_dist = dist closest_a = region_a[i] closest_b = region_b[j] return [closest_a, closest_b] func _carve_corridor(from: Vector2i, to: Vector2i) -> void: """在两点之间挖掘走廊""" var x = from.x var y = from.y while x != to.x or y != to.y: if randi() % 2 == 0: x += sign(to.x - x) if to.x != x else 0 else: y += sign(to.y - y) if to.y != y else 0 # 挖掘 3x3 区域 for dy in range(-1, 2): for dx in range(-1, 2): var nx = x + dx var ny = y + dy if nx > 0 and nx < cave_width - 1 and ny > 0 and ny < cave_height - 1: cave_map[ny][nx] = 0 func _paint_tilemap() -> void: """将生成结果绘制到 TileMap""" clear() for y in range(cave_height): for x in range(cave_width): var tile_id = wall_tile_id if cave_map[y][x] == 1 else floor_tile_id set_cell(Vector2i(x, y), 0, Vector2i(tile_id, 0))

5.6 程序化生成提示词模板

模板 10:地牢生成器

请为 [2D Roguelike / 3D 地牢探索] 游戏生成一个程序化地牢生成器。 引擎:[Unity C# / Godot GDScript] 地牢需求: - 算法:[BSP / Random Walk / 元胞自动机 / WFC] - 地牢尺寸:[W] x [H] 格 - 房间数量:[N1]-[N2] 个 - 房间尺寸:最小 [A]x[B],最大 [C]x[D] - 走廊宽度:[W] 格 - 特殊房间:[起始房间 / Boss 房间 / 宝藏房间 / 商店] 内容放置: - 玩家出生点:距离出口最远的房间 - 敌人:每个房间 [N] 个,类型根据距离出生点的远近递增 - 宝箱:[N] 个,放在死胡同或隐藏区域 - 陷阱:[N] 个,放在走廊中 - 出口:距离出生点最远的房间 要求: - 支持种子(seed)以复现地牢 - 确保所有房间连通 - 输出到 [TileMap / 数组 / Mesh] - 包含地牢验证(连通性检查) - 包含内容放置逻辑

模板 11:地形生成器

请为 [开放世界 / 生存游戏] 生成一个程序化地形系统。 引擎:[Unity C# / Godot GDScript / Unreal C++] 地形需求: - 尺寸:[W] x [D] 单位 - 高度范围:[0] 到 [H] 单位 - 噪声类型:[Perlin / Simplex / Ridged / Billow] - 分形层数:[N] 层(octaves) 生物群落: - 水域:高度 < [X]% - 沙滩:高度 [X]%-[Y]% - 草地:高度 [Y]%-[Z]% - 森林:高度 [Z]%-[W]% - 山地:高度 [W]%-[V]% - 雪山:高度 > [V]% 要求: - 支持种子 - 包含生物群落着色 - 包含碰撞体生成 - [可选] 支持无限地形(分块加载) - [可选] 支持 LOD(远处低精度)

实战案例:2D Roguelike 地牢游戏完整 AI 辅助开发

案例背景

使用 AI Agent(Claude Code + Cursor)在 Godot 4.x 中开发一个 2D Roguelike 地牢探索游戏。本案例展示如何用 AI 辅助实现五大核心游戏逻辑系统的完整流程。

第一步:角色状态机(AI 生成 + 人工调优)

提示词:

请用 Godot GDScript 为 2D Roguelike 游戏的玩家角色生成一个状态模式状态机。 状态列表: - Idle:站立不动,可以转向 - Run:8 方向移动,速度 200 - Dash:冲刺,速度 500,持续 0.15 秒,冷却 0.8 秒,冲刺期间无敌 - Attack:近战攻击,3 段连击,每段 0.3 秒 - Hurt:受伤硬直 0.3 秒,击退力 300 - Dead:死亡,禁用所有输入 使用状态模式(每个状态一个脚本),包含 StateMachine 管理器。

AI 输出审查要点:

  • ✅ 检查状态转换是否完整(无死锁)
  • ✅ 检查冲刺无敌帧是否正确实现
  • ✅ 检查连击计时器是否在状态退出时重置
  • ⚠️ AI 可能遗漏:土狼时间、跳跃缓冲等手感优化

第二步:敌人行为树(AI 生成框架 + 人工扩展)

提示词:

请用 Godot GDScript + Beehave 插件为以下 3 种敌人生成行为树 AI: 1. 近战骷髅兵: - 巡逻 → 发现玩家 → 追击 → 近战攻击(冷却 1.5s) - 血量 < 30% 时逃跑 2. 远程弓箭手: - 固定位置 → 发现玩家 → 保持距离(150-250 单位)→ 射箭(冷却 2s) - 玩家靠近时后退 3. Boss 骑士: - 阶段 1(血量 > 50%):慢速追击 + 重击(冷却 3s) - 阶段 2(血量 ≤ 50%):加速 + 冲刺攻击 + 召唤骷髅兵 - 阶段转换时播放嘶吼动画 每种敌人提供完整的 Beehave 节点树结构和自定义节点代码。

AI 输出审查要点:

  • ✅ 检查行为树是否正确处理了状态恢复(如:追击中目标消失)
  • ✅ 检查 Boss 阶段转换是否有过渡动画保护
  • ⚠️ AI 可能遗漏:多敌人协调(避免同时攻击)、感知记忆衰减

第三步:地牢生成(AI 生成算法 + 人工调参)

提示词:

请用 Godot GDScript 实现一个 BSP 地牢生成器,配合 TileMapLayer 输出。 参数: - 地牢尺寸:80x60 - 房间数量:8-15 个 - 房间尺寸:6x6 到 16x12 - 走廊宽度:2-3 格 内容放置规则: - 玩家出生在距离出口最远的房间中心 - 出口(楼梯)在距离出生点最远的房间 - 每个房间随机放置 1-4 个敌人(距离出生点越远敌人越强) - 宝箱放在小房间或死胡同 - 每 3 个房间放一个回血泉 输出: - 生成的地牢数据(房间列表、走廊列表、内容位置) - TileMap 绘制 - 小地图数据

第四步:导航系统(AI 配置 + 人工验证)

提示词:

请为上述 BSP 地牢配置 Godot NavigationRegion2D 导航系统。 需求: 1. 根据生成的地牢地板区域自动创建导航多边形 2. 敌人使用 NavigationAgent2D 进行寻路 3. 支持动态障碍(可破坏的箱子、关闭的门) 4. 敌人之间启用避障(avoidance)防止重叠 提供: - 导航区域自动生成脚本 - 敌人导航控制器脚本 - 动态障碍更新逻辑

第五步:物理与碰撞(AI 生成 + 人工调试)

提示词:

请为 Roguelike 游戏配置完整的碰撞系统。 碰撞层: - Layer 1: Player - Layer 2: Enemy - Layer 3: Wall - Layer 4: PlayerProjectile - Layer 5: EnemyProjectile - Layer 6: Collectible - Layer 7: Interactable(宝箱、门、NPC) 碰撞矩阵: - Player ↔ Wall, Enemy, EnemyProjectile, Collectible, Interactable - Enemy ↔ Wall, Player, PlayerProjectile - PlayerProjectile ↔ Wall, Enemy - EnemyProjectile ↔ Wall, Player 提供碰撞层配置说明和碰撞回调处理代码。

案例分析

关键决策点:

  1. 状态机 vs 行为树:玩家用状态机(状态少、转换明确),敌人用行为树(行为复杂、需要模块化复用)
  2. BSP vs WFC:选择 BSP 因为矩形房间更适合 Roguelike,WFC 更适合自然地形
  3. 自定义 A vs NavMesh*:选择 Godot 内置 NavigationServer,因为地牢是动态生成的,NavMesh 可以运行时烘焙
  4. AI 生成代码的审查重点:物理参数(手感)、行为树边界条件(状态恢复)、地牢连通性验证

AI 辅助效率提升:

模块手动开发预估AI 辅助实际效率提升
角色状态机8 小时2 小时4x
敌人行为树(3 种)16 小时4 小时4x
BSP 地牢生成12 小时3 小时4x
导航系统6 小时1.5 小时4x
碰撞系统4 小时1 小时4x
总计46 小时11.5 小时4x

注意:以上时间包含 AI 生成 + 人工审查 + 调试调参。AI 生成的代码通常需要 20-30% 的人工修改才能达到生产质量。


避坑指南

❌ 常见错误

  1. 状态机转换爆炸

    • 问题:N 个状态需要 N×(N-1) 个转换条件,代码迅速膨胀不可维护
    • 正确做法:超过 8 个状态时考虑层级状态机(HFSM),将相关状态分组为子状态机。例如将 Idle/Run/Jump/Fall 归入 Movement 子状态机,Attack/Skill/Block 归入 Combat 子状态机
  2. 行为树每帧从根节点重新评估

    • 问题:每帧都从 Root 开始 Tick,导致正在执行的 RUNNING 动作被中断
    • 正确做法:实现”记忆”机制——记住上次返回 RUNNING 的节点,下次直接从该节点继续。或使用 Parallel 节点同时检查中断条件和执行动作
  3. A 寻路在大地图上卡顿*

    • 问题:每次寻路遍历整个网格,100x100 的地图可能需要检查 10000 个节点
    • 正确做法:使用二叉堆(Binary Heap)优先队列替代线性搜索;实现分帧计算(每帧只处理 N 个节点);对远距离目标使用分层寻路(HPA*)
  4. 物理参数硬编码导致手感差

    • 问题:AI 生成的物理参数(重力、跳跃力、加速度)是”合理”的默认值,但游戏手感需要精细调优
    • 正确做法:将所有物理参数暴露为 @export / [SerializeField],在编辑器中实时调整。参考成熟游戏的参数(如 Celeste 的土狼时间 0.1s、跳跃缓冲 0.15s)
  5. 程序化生成不验证连通性

    • 问题:AI 生成的地牢/洞穴可能存在孤立区域,玩家无法到达某些房间
    • 正确做法:生成后必须执行洪水填充(Flood Fill)连通性检查,发现孤立区域时挖掘走廊连接或重新生成
  6. AI 生成的碰撞层配置混乱

    • 问题:AI 可能不理解碰撞层(Layer)和碰撞掩码(Mask)的区别,导致碰撞检测不生效或误触发
    • 正确做法:明确区分”我是什么”(Layer)和”我检测什么”(Mask)。画一个碰撞矩阵表,逐一配置。Unity 用 Physics2D.IgnoreLayerCollision,Godot 用 collision_layer 和 collision_mask 位掩码
  7. 行为树黑板数据不同步

    • 问题:多个行为树节点读写同一个黑板变量,导致数据竞争或过时数据
    • 正确做法:明确黑板变量的”所有权”——哪个节点负责写入,哪些节点只读。使用时间戳标记数据新鲜度,过期数据触发重新感知
  8. NavMesh 不支持动态障碍

    • 问题:运行时添加/移除障碍物后,NPC 仍然走旧路径穿墙
    • 正确做法:Unity 使用 NavMeshObstacle 组件标记动态障碍;Godot 使用 NavigationObstacle2D/3D;Unreal 使用 Dynamic Obstacle 标记。确保导航网格在障碍变化后重新烘焙或使用实时避障

✅ 最佳实践

  1. 状态机和行为树混合使用:玩家角色用状态机(状态少、响应快),复杂 NPC 用行为树(模块化、可复用),简单 NPC 用状态机(开发快)

  2. 物理参数使用 ScriptableObject / Resource:将物理参数(速度、跳跃力、重力等)存储在独立的数据文件中,方便策划调整和 A/B 测试

  3. 程序化生成使用种子系统:所有随机数使用可控种子,便于 Bug 复现、关卡分享和竞速模式

  4. 寻路使用对象池:频繁创建/销毁寻路请求会产生 GC 压力,使用对象池复用路径数据

  5. 行为树使用可视化调试器:Beehave 和 LimboAI 都提供运行时可视化调试,可以看到当前执行到哪个节点、每个条件的返回值

  6. AI 生成代码后必做的 3 件事

    • 运行一次完整的游戏循环,检查状态转换是否正常
    • 用极端输入测试(连续快速按键、同时按多个键)
    • 检查内存泄漏(特别是程序化生成的对象是否正确释放)

相关资源与延伸阅读

  1. Game Programming Patterns - State Pattern  — Robert Nystrom 的经典游戏编程模式书,状态机章节是必读内容
  2. Godot Beehave 行为树插件  — Godot 最流行的行为树插件,提供可视化编辑器和调试工具
  3. LimboAI: Behavior Trees & State Machines for Godot 4  — Godot 高性能 C++ 行为树+状态机插件,支持 GDScript 扩展
  4. Unity Behavior Tree 官方文档  — Unity 6+ 官方行为树包文档
  5. Red Blob Games - A* Pathfinding  — 最佳 A* 寻路可视化教程,交互式演示
  6. Red Blob Games - Procedural Generation  — 噪声地形生成的可视化教程
  7. Wave Function Collapse 算法详解  — WFC 算法原始仓库,包含大量示例和论文链接
  8. Unreal Engine AI 官方文档  — UE 行为树、EQS、感知系统完整文档
  9. Godot NavigationServer 文档  — Godot 导航系统官方教程
  10. AI Game Dev - Procedural Content Generation Wiki  — 程序化内容生成技术百科,涵盖所有主流 PCG 算法

参考来源

Content was rephrased for compliance with licensing restrictions


📖 返回 总览与导航 | 上一节:游戏引擎工作流 | 下一节:AI生成游戏资产

Last updated on