34c - 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 架构设计与实现 |
| Cursor | AI IDE,实时状态机代码补全 | 免费-$20/月 | 日常状态机编码与重构 |
| GitHub Copilot | 编辑器内代码补全 | $10-39/月 | 状态转换逻辑快速补全 |
| LimboAI | Godot 行为树+状态机插件 | 免费(开源) | Godot 可视化状态机编辑 |
| Unity Animator | Unity 内置状态机编辑器 | Unity 订阅内含 | 动画状态机与逻辑状态机 |
| Unreal AI Module | UE 内置 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 可以帮助快速生成物理相关代码,但开发者需要理解底层原理以正确调参。
游戏物理核心组件:
| 组件 | Unity | Godot | Unreal |
|---|---|---|---|
| 刚体 | Rigidbody / Rigidbody2D | RigidBody2D / RigidBody3D | UPrimitiveComponent (Simulate Physics) |
| 运动体 | CharacterController | CharacterBody2D / CharacterBody3D | ACharacter (CharacterMovementComponent) |
| 碰撞体 | Collider2D / Collider | CollisionShape2D / CollisionShape3D | UShapeComponent |
| 触发区域 | Trigger Collider | Area2D / Area3D | Trigger Volume |
| 射线检测 | Physics.Raycast | RayCast2D / RayCast3D | LineTrace |
| 物理材质 | PhysicsMaterial2D | PhysicsMaterial | Physical Material |
| 关节 | Joint2D / Joint | Joint2D / Joint3D | Physics 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 enemies2.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 生成难度 | 低 | 中等 |
工具推荐
| 工具 | 用途 | 价格 | 适用场景 |
|---|---|---|---|
| Beehave | Godot 行为树插件 | 免费(开源) | Godot 可视化行为树编辑 |
| LimboAI | Godot 行为树+状态机 C++ 插件 | 免费(开源) | Godot 高性能 AI 系统 |
| Unity Behavior | Unity 官方行为树包 | Unity 6+ 内置 | Unity 可视化行为树 |
| Unreal Behavior Tree | UE 内置行为树编辑器 | 免费(引擎内置) | UE 标准 NPC AI |
| NodeCanvas | Unity 高级行为树/FSM 插件 | $80(一次性) | Unity 复杂 AI 系统 |
| Claude Code | 生成行为树逻辑代码 | $20-100/月 | 自定义行为树节点 |
| Cursor | AI 辅助行为树编码 | 免费-$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 Search | A* 优化,跳点搜索 | 均匀网格地图 | 很高 | 需重新计算 |
| Theta* | A* 变体,任意角度路径 | 需要平滑路径的场景 | 中等 | 需重新计算 |
工具推荐
| 工具 | 用途 | 价格 | 适用场景 |
|---|---|---|---|
| Unity NavMesh | Unity 内置导航系统 | Unity 内置 | 3D 场景导航 |
| Unity A Pathfinding Project* | 高级寻路插件 | 免费版/Pro $100 | 复杂 2D/3D 寻路 |
| Godot NavigationServer | Godot 内置导航系统 | 免费(引擎内置) | 2D/3D 导航 |
| Unreal Navigation System | UE 内置 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_positionUnity 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/月 | 自定义生成算法 |
| Cursor | PCG 代码辅助 | 免费-$20/月 | 日常 PCG 编码 |
| Unity ProBuilder | 快速关卡原型 | Unity 内置 | 关卡白盒 |
| Godot TileMap | 2D 瓦片地图 | 免费(引擎内置) | 2D 关卡生成 |
| Houdini Engine | 高级程序化建模 | $269/年(Indie) | 3D 程序化资产 |
| Dungeon Architect | Unity/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 true5.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
提供碰撞层配置说明和碰撞回调处理代码。案例分析
关键决策点:
- 状态机 vs 行为树:玩家用状态机(状态少、转换明确),敌人用行为树(行为复杂、需要模块化复用)
- BSP vs WFC:选择 BSP 因为矩形房间更适合 Roguelike,WFC 更适合自然地形
- 自定义 A vs NavMesh*:选择 Godot 内置 NavigationServer,因为地牢是动态生成的,NavMesh 可以运行时烘焙
- 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% 的人工修改才能达到生产质量。
避坑指南
❌ 常见错误
-
状态机转换爆炸
- 问题:N 个状态需要 N×(N-1) 个转换条件,代码迅速膨胀不可维护
- 正确做法:超过 8 个状态时考虑层级状态机(HFSM),将相关状态分组为子状态机。例如将 Idle/Run/Jump/Fall 归入 Movement 子状态机,Attack/Skill/Block 归入 Combat 子状态机
-
行为树每帧从根节点重新评估
- 问题:每帧都从 Root 开始 Tick,导致正在执行的 RUNNING 动作被中断
- 正确做法:实现”记忆”机制——记住上次返回 RUNNING 的节点,下次直接从该节点继续。或使用 Parallel 节点同时检查中断条件和执行动作
-
A 寻路在大地图上卡顿*
- 问题:每次寻路遍历整个网格,100x100 的地图可能需要检查 10000 个节点
- 正确做法:使用二叉堆(Binary Heap)优先队列替代线性搜索;实现分帧计算(每帧只处理 N 个节点);对远距离目标使用分层寻路(HPA*)
-
物理参数硬编码导致手感差
- 问题:AI 生成的物理参数(重力、跳跃力、加速度)是”合理”的默认值,但游戏手感需要精细调优
- 正确做法:将所有物理参数暴露为
@export/[SerializeField],在编辑器中实时调整。参考成熟游戏的参数(如 Celeste 的土狼时间 0.1s、跳跃缓冲 0.15s)
-
程序化生成不验证连通性
- 问题:AI 生成的地牢/洞穴可能存在孤立区域,玩家无法到达某些房间
- 正确做法:生成后必须执行洪水填充(Flood Fill)连通性检查,发现孤立区域时挖掘走廊连接或重新生成
-
AI 生成的碰撞层配置混乱
- 问题:AI 可能不理解碰撞层(Layer)和碰撞掩码(Mask)的区别,导致碰撞检测不生效或误触发
- 正确做法:明确区分”我是什么”(Layer)和”我检测什么”(Mask)。画一个碰撞矩阵表,逐一配置。Unity 用 Physics2D.IgnoreLayerCollision,Godot 用 collision_layer 和 collision_mask 位掩码
-
行为树黑板数据不同步
- 问题:多个行为树节点读写同一个黑板变量,导致数据竞争或过时数据
- 正确做法:明确黑板变量的”所有权”——哪个节点负责写入,哪些节点只读。使用时间戳标记数据新鲜度,过期数据触发重新感知
-
NavMesh 不支持动态障碍
- 问题:运行时添加/移除障碍物后,NPC 仍然走旧路径穿墙
- 正确做法:Unity 使用 NavMeshObstacle 组件标记动态障碍;Godot 使用 NavigationObstacle2D/3D;Unreal 使用 Dynamic Obstacle 标记。确保导航网格在障碍变化后重新烘焙或使用实时避障
✅ 最佳实践
-
状态机和行为树混合使用:玩家角色用状态机(状态少、响应快),复杂 NPC 用行为树(模块化、可复用),简单 NPC 用状态机(开发快)
-
物理参数使用 ScriptableObject / Resource:将物理参数(速度、跳跃力、重力等)存储在独立的数据文件中,方便策划调整和 A/B 测试
-
程序化生成使用种子系统:所有随机数使用可控种子,便于 Bug 复现、关卡分享和竞速模式
-
寻路使用对象池:频繁创建/销毁寻路请求会产生 GC 压力,使用对象池复用路径数据
-
行为树使用可视化调试器:Beehave 和 LimboAI 都提供运行时可视化调试,可以看到当前执行到哪个节点、每个条件的返回值
-
AI 生成代码后必做的 3 件事:
- 运行一次完整的游戏循环,检查状态转换是否正常
- 用极端输入测试(连续快速按键、同时按多个键)
- 检查内存泄漏(特别是程序化生成的对象是否正确释放)
相关资源与延伸阅读
- Game Programming Patterns - State Pattern — Robert Nystrom 的经典游戏编程模式书,状态机章节是必读内容
- Godot Beehave 行为树插件 — Godot 最流行的行为树插件,提供可视化编辑器和调试工具
- LimboAI: Behavior Trees & State Machines for Godot 4 — Godot 高性能 C++ 行为树+状态机插件,支持 GDScript 扩展
- Unity Behavior Tree 官方文档 — Unity 6+ 官方行为树包文档
- Red Blob Games - A* Pathfinding — 最佳 A* 寻路可视化教程,交互式演示
- Red Blob Games - Procedural Generation — 噪声地形生成的可视化教程
- Wave Function Collapse 算法详解 — WFC 算法原始仓库,包含大量示例和论文链接
- Unreal Engine AI 官方文档 — UE 行为树、EQS、感知系统完整文档
- Godot NavigationServer 文档 — Godot 导航系统官方教程
- AI Game Dev - Procedural Content Generation Wiki — 程序化内容生成技术百科,涵盖所有主流 PCG 算法
参考来源
- AI In Game Development: How AI Is Transforming The Game Dev Workflow (2026-02)
- The State of Vibe Coding: A 2026 Strategic Blueprint (2026-02)
- How AI is Shaping Video Games, from Smarter NPCs to Google’s Project Genie (2026-02)
- Unity says its AI tech will soon eliminate the need for coding (2026-02)
- Best Game Development Software in 2026 (2026-02)
- Implementing Behavior Trees for AI Design in GDScript (2025-12)
- Pathfinding with NavigationAgent2D (2025-06)
- LimboAI: Behavior Trees & State Machines for Godot 4.4+ (2026-01)
- AI-Generated Game Worlds Test Industry Boundaries (2026-02)
- Wave Function Collapse - Model Synthesis (2025-12)
Content was rephrased for compliance with licensing restrictions