6. 敌人类型与AI
2026/4/13大约 12 分钟
敌人类型与AI
地图有了,但地图上空空荡荡的可不行。赤色要塞的乐趣很大一部分来自于和各种不同类型的敌人战斗——步兵、坦克、炮台、直升机……每种敌人都有不同的行为模式。这一章我们来设计和实现敌人系统。
敌人类型设计
敌人一览表
| 敌人类型 | 移动方式 | 血量 | 攻击方式 | 威胁等级 | 视觉比喻 |
|---|---|---|---|---|---|
| 步兵 | 缓慢移动 | 低(1) | 近距离射击 | ★☆☆ | 像蚂蚁——单个不致命,一群很烦人 |
| 坦克 | 中速移动 | 高(5) | 发射炮弹 | ★★★ | 像犀牛——血厚攻击高,正面硬刚 |
| 炮台 | 固定不动 | 中(3) | 远距离射击 | ★★☆ | 像蚊子——你跑它打,得优先处理 |
| 直升机 | 空中飞行 | 中(3) | 投弹/扫射 | ★★★ | 像蚊子——烦人,还不能直接撞 |
| 运兵车 | 快速移动 | 中(2) | 生成步兵 | ★★☆ | 像快递员——不断送"麻烦"过来 |
敌人节点结构
所有敌人共享一个基础结构:
EnemyBase (CharacterBody3D) ← 敌人基类
├── MeshInstance3D ← 敌人模型
├── CollisionShape3D ← 碰撞形状
├── DetectionArea (Area3D) ← 检测范围(发现玩家)
│ └── CollisionShape3D ← 检测范围形状
├── AttackArea (Area3D) ← 攻击范围
│ └── CollisionShape3D ← 攻击范围形状
├── HealthBar (SubViewport) ← 血条显示
└── RayCast3D ← 射线检测(视野内有没有障碍物挡住)敌人AI——状态机
什么是状态机?
状态机(State Machine)就是把敌人的行为分成几个"状态",每个状态下敌人做不同的事情,满足条件时切换到另一个状态。
就像红绿灯一样:红灯停、绿灯行、黄灯减速。敌人也有类似的"灯":
发现玩家
┌─────────┐ ──────────→ ┌─────────┐
│ 巡逻 │ │ 追踪 │
│ (Idle) │ ←────────── │ (Chase) │
└─────────┘ 失去目标 └────┬────┘
↑ │ 进入攻击范围
│ ↓
│ ┌─────────┐
└──────────────────│ 攻击 │
攻击完毕/远离 │(Attack) │
└─────────┘| 状态 | 行为 | 切换条件 |
|---|---|---|
| 巡逻(Patrol) | 在原地或小范围内移动 | 检测到玩家 → 切换到追踪 |
| 追踪(Chase) | 向玩家方向移动 | 进入攻击范围 → 切换到攻击 |
| 攻击(Attack) | 射击/发射炮弹 | 攻击完毕且不在范围内 → 回到追踪 |
敌人基类脚本
C
using Godot;
/// <summary>
/// 敌人基类 - 所有敌人的公共逻辑
/// </summary>
public abstract partial class EnemyBase : CharacterBody3D
{
// 基础属性
[Export] public int MaxHealth { get; set; } = 3;
[Export] public float MoveSpeed { get; set; } = 3.0f;
[Export] public float DetectionRange { get; set; } = 15.0f;
[Export] public float AttackRange { get; set; } = 10.0f;
[Export] public int ScoreValue { get; set; } = 100; // 击杀得分
// 状态枚举
public enum EnemyState { Patrol, Chase, Attack, Dead }
protected EnemyState _currentState = EnemyState.Patrol;
protected int _currentHealth;
protected Node3D _target; // 追踪目标(玩家)
protected Vector3 _spawnPosition; // 出生位置
public override void _Ready()
{
_currentHealth = MaxHealth;
_spawnPosition = GlobalPosition;
}
public override void _PhysicsProcess(double delta)
{
// 根据当前状态执行不同行为
switch (_currentState)
{
case EnemyState.Patrol:
OnPatrol((float)delta);
break;
case EnemyState.Chase:
OnChase((float)delta);
break;
case EnemyState.Attack:
OnAttack((float)delta);
break;
case EnemyState.Dead:
return; // 死了就什么都不做
}
MoveAndSlide();
}
/// <summary>
/// 受到伤害
/// </summary>
public virtual void TakeDamage(int damage)
{
_currentHealth -= damage;
// 受击反馈
Modulate = new Color(2.0f, 0.5f, 0.5f);
CreateTween().TweenProperty(this, "modulate", Colors.White, 0.1f);
if (_currentHealth <= 0)
{
Die();
}
}
/// <summary>
/// 死亡处理
/// </summary>
protected virtual void Die()
{
_currentState = EnemyState.Dead;
// 加分
var gameManager = GetNodeOrNull<Node>("/root/GameManager");
gameManager?.Call("AddScore", ScoreValue);
// 死亡动画:缩小消失
var tween = CreateTween();
tween.TweenProperty(this, "scale", Vector3.Zero, 0.3f)
.SetEase(Tween.EaseType.In);
tween.TweenCallback(Callable.From(() => QueueFree()));
}
/// <summary>
/// 查找最近的玩家
/// </summary>
protected Node3D FindPlayer()
{
var players = GetTree().GetNodesInGroup("player");
if (players.Count == 0) return null;
Node3D nearest = null;
float nearestDist = float.MaxValue;
foreach (var p in players)
{
if (p is Node3D player)
{
float dist = GlobalPosition.DistanceTo(player.GlobalPosition);
if (dist < nearestDist)
{
nearestDist = dist;
nearest = player;
}
}
}
return nearest;
}
/// <summary>
/// 巡逻状态
/// </summary>
protected virtual void OnPatrol(float delta)
{
// 默认巡逻行为:在出生点附近小范围移动
_target = FindPlayer();
if (_target != null)
{
float dist = GlobalPosition.DistanceTo(_target.GlobalPosition);
if (dist <= DetectionRange)
{
_currentState = EnemyState.Chase;
}
}
}
/// <summary>
/// 追踪状态
/// </summary>
protected virtual void OnChase(float delta)
{
if (_target == null || !IsInstanceValid(_target))
{
_currentState = EnemyState.Patrol;
return;
}
float dist = GlobalPosition.DistanceTo(_target.GlobalPosition);
// 超出检测范围,回到巡逻
if (dist > DetectionRange * 1.5f)
{
_currentState = EnemyState.Patrol;
return;
}
// 进入攻击范围,切换到攻击
if (dist <= AttackRange)
{
_currentState = EnemyState.Attack;
return;
}
// 向玩家移动
Vector3 direction = (_target.GlobalPosition - GlobalPosition).Normalized();
direction.Y = 0; // 只在水平面上移动
Velocity = Velocity.MoveToward(direction * MoveSpeed, MoveSpeed * delta * 2f);
// 朝向玩家
LookAt(new Vector3(_target.GlobalPosition.X, GlobalPosition.Y, _target.GlobalPosition.Z), Vector3.Up);
}
/// <summary>
/// 攻击状态(子类重写)
/// </summary>
protected virtual void OnAttack(float delta)
{
// 基类默认不做任何事,让子类实现具体攻击逻辑
}
}GDScript
# 敌人基类 - 所有敌人的公共逻辑
extends CharacterBody3D
# 基础属性
@export var max_health: int = 3
@export var move_speed: float = 3.0
@export var detection_range: float = 15.0
@export var attack_range: float = 10.0
@export var score_value: int = 100 # 击杀得分
# 状态枚举
enum EnemyState { PATROL, CHASE, ATTACK, DEAD }
var current_state: EnemyState = EnemyState.PATROL
var _current_health: int
var _target: Node3D # 追踪目标(玩家)
var _spawn_position: Vector3 # 出生位置
func _ready():
_current_health = max_health
_spawn_position = global_position
func _physics_process(delta):
# 根据当前状态执行不同行为
match current_state:
EnemyState.PATROL:
_on_patrol(delta)
EnemyState.CHASE:
_on_chase(delta)
EnemyState.ATTACK:
_on_attack(delta)
EnemyState.DEAD:
return # 死了就什么都不做
move_and_slide()
## 受到伤害
func take_damage(damage: int):
_current_health -= damage
# 受击反馈
modulate = Color(2.0, 0.5, 0.5)
var tween = create_tween()
tween.tween_property(self, "modulate", Color.WHITE, 0.1)
if _current_health <= 0:
_die()
## 死亡处理
func _die():
current_state = EnemyState.DEAD
# 加分
var game_manager = get_node_or_null("/root/GameManager")
if game_manager:
game_manager.add_score(score_value)
# 死亡动画:缩小消失
var tween = create_tween()
tween.tween_property(self, "scale", Vector3.ZERO, 0.3)\
.set_ease(Tween.EASE_IN)
tween.tween_callback(queue_free)
## 查找最近的玩家
func _find_player() -> Node3D:
var players = get_tree().get_nodes_in_group("player")
if players.size() == 0:
return null
var nearest: Node3D = null
var nearest_dist: float = INF
for p in players:
var player = p as Node3D
if player:
var dist = global_position.distance_to(player.global_position)
if dist < nearest_dist:
nearest_dist = dist
nearest = player
return nearest
## 巡逻状态
func _on_patrol(delta: float):
# 默认巡逻行为:检测是否有玩家靠近
_target = _find_player()
if _target:
var dist = global_position.distance_to(_target.global_position)
if dist <= detection_range:
current_state = EnemyState.CHASE
## 追踪状态
func _on_chase(delta: float):
if _target == null or not is_instance_valid(_target):
current_state = EnemyState.PATROL
return
var dist = global_position.distance_to(_target.global_position)
# 超出检测范围,回到巡逻
if dist > detection_range * 1.5:
current_state = EnemyState.PATROL
return
# 进入攻击范围,切换到攻击
if dist <= attack_range:
current_state = EnemyState.ATTACK
return
# 向玩家移动
var direction = (_target.global_position - global_position).normalized()
direction.y = 0 # 只在水平面上移动
velocity = velocity.move_toward(direction * move_speed, move_speed * delta * 2)
# 朝向玩家
look_at(Vector3(_target.global_position.x, global_position.y, _target.global_position.z), Vector3.UP)
## 攻击状态(子类重写)
func _on_attack(_delta: float):
pass # 基类默认不做任何事,让子类实现步兵实现
步兵是最简单的敌人——缓慢移动,靠近后射击。
C
using Godot;
/// <summary>
/// 步兵 - 缓慢移动,靠近后射击
/// </summary>
public partial class Infantry : EnemyBase
{
[Export] public float ShootInterval { get; set; } = 1.5f; // 射击间隔
[Export] public PackedScene BulletScene { get; set; } // 子弹场景
private float _shootCooldown;
public Infantry()
{
MaxHealth = 1;
MoveSpeed = 2.0f;
DetectionRange = 12.0f;
AttackRange = 8.0f;
ScoreValue = 100;
}
protected override void OnAttack(float delta)
{
if (_target == null) return;
// 面向玩家
LookAt(new Vector3(_target.GlobalPosition.X, GlobalPosition.Y, _target.GlobalPosition.Z), Vector3.Up);
// 射击冷却
_shootCooldown -= delta;
if (_shootCooldown <= 0)
{
Shoot();
_shootCooldown = ShootInterval;
}
// 保持距离:太近了就后退
float dist = GlobalPosition.DistanceTo(_target.GlobalPosition);
if (dist < 3.0f)
{
Vector3 awayDir = (GlobalPosition - _target.GlobalPosition).Normalized();
Velocity = awayDir * MoveSpeed * 0.5f;
}
else
{
Velocity = Velocity.MoveToward(Vector3.Zero, MoveSpeed * delta);
}
}
private void Shoot()
{
if (BulletScene == null) return;
var bullet = BulletScene.Instantiate<Area3D>();
GetTree().CurrentScene.AddChild(bullet);
bullet.GlobalPosition = GlobalPosition + Vector3.Up * 0.5f;
// 朝玩家方向射击
Vector3 shootDir = (_target.GlobalPosition - GlobalPosition).Normalized();
if (bullet.HasMethod("SetDirection"))
bullet.Call("SetDirection", shootDir);
}
}GDScript
# 步兵 - 缓慢移动,靠近后射击
extends "res://scripts/enemies/enemy_base.gd"
@export var shoot_interval: float = 1.5 # 射击间隔
@export var bullet_scene: PackedScene # 子弹场景
var _shoot_cooldown: float = 0.0
func _init():
max_health = 1
move_speed = 2.0
detection_range = 12.0
attack_range = 8.0
score_value = 100
func _on_attack(delta: float):
if _target == null:
return
# 面向玩家
look_at(Vector3(_target.global_position.x, global_position.y, _target.global_position.z), Vector3.UP)
# 射击冷却
_shoot_cooldown -= delta
if _shoot_cooldown <= 0:
_shoot()
_shoot_cooldown = shoot_interval
# 保持距离:太近了就后退
var dist = global_position.distance_to(_target.global_position)
if dist < 3.0:
var away_dir = (global_position - _target.global_position).normalized()
velocity = away_dir * move_speed * 0.5
else:
velocity = velocity.move_toward(Vector3.ZERO, move_speed * delta)
func _shoot():
if bullet_scene == null:
return
var bullet = bullet_scene.instantiate()
get_tree().current_scene.add_child(bullet)
bullet.global_position = global_position + Vector3.UP * 0.5
# 朝玩家方向射击
var shoot_dir = (_target.global_position - global_position).normalized()
if bullet.has_method("set_direction"):
bullet.set_direction(shoot_dir)坦克实现
坦克是重型敌人——血厚、攻击高、移动中速。
C
using Godot;
/// <summary>
/// 坦克 - 高血量,发射炮弹
/// </summary>
public partial class Tank : EnemyBase
{
[Export] public float ShootInterval { get; set; } = 2.5f;
[Export] public PackedScene ShellScene { get; set; } // 炮弹场景
private float _shootCooldown;
private float _turretAngle; // 炮塔角度(独立于车身旋转)
public Tank()
{
MaxHealth = 5;
MoveSpeed = 3.0f;
DetectionRange = 20.0f;
AttackRange = 15.0f;
ScoreValue = 500;
}
protected override void OnChase(float delta)
{
base.OnChase(delta);
// 坦克追踪时速度减半(更沉稳)
Velocity *= 0.6f;
}
protected override void OnAttack(float delta)
{
if (_target == null) return;
// 炮塔始终朝向玩家
LookAt(new Vector3(_target.GlobalPosition.X, GlobalPosition.Y, _target.GlobalPosition.Z), Vector3.Up);
// 缓慢移动
Velocity = Velocity.MoveToward(Vector3.Zero, MoveSpeed * delta);
// 射击
_shootCooldown -= delta;
if (_shootCooldown <= 0)
{
FireShell();
_shootCooldown = ShootInterval;
}
}
private void FireShell()
{
if (ShellScene == null) return;
var shell = ShellScene.Instantiate<RigidBody3D>();
GetTree().CurrentScene.AddChild(shell);
shell.GlobalPosition = GlobalPosition + Vector3.Up * 1.0f;
Vector3 fireDir = (_target.GlobalPosition - GlobalPosition).Normalized();
shell.ApplyCentralImpulse(fireDir * 20.0f);
}
}GDScript
# 坦克 - 高血量,发射炮弹
extends "res://scripts/enemies/enemy_base.gd"
@export var shoot_interval: float = 2.5
@export var shell_scene: PackedScene # 炮弹场景
var _shoot_cooldown: float = 0.0
var _turret_angle: float = 0.0 # 炮塔角度
func _init():
max_health = 5
move_speed = 3.0
detection_range = 20.0
attack_range = 15.0
score_value = 500
func _on_chase(delta: float):
super._on_chase(delta)
# 坦克追踪时速度减半(更沉稳)
velocity *= 0.6
func _on_attack(delta: float):
if _target == null:
return
# 炮塔始终朝向玩家
look_at(Vector3(_target.global_position.x, global_position.y, _target.global_position.z), Vector3.UP)
# 缓慢移动
velocity = velocity.move_toward(Vector3.ZERO, move_speed * delta)
# 射击
_shoot_cooldown -= delta
if _shoot_cooldown <= 0:
_fire_shell()
_shoot_cooldown = shoot_interval
func _fire_shell():
if shell_scene == null:
return
var shell = shell_scene.instantiate()
get_tree().current_scene.add_child(shell)
shell.global_position = global_position + Vector3.UP * 1.0
var fire_dir = (_target.global_position - global_position).normalized()
shell.apply_central_impulse(fire_dir * 20.0)炮台实现
炮台固定不动,但可以旋转朝向玩家并远距离射击。
C
using Godot;
/// <summary>
/// 炮台 - 固定位置,旋转射击
/// </summary>
public partial class Turret : EnemyBase
{
[Export] public float ShootInterval { get; set; } = 1.0f;
[Export] public PackedScene BulletScene { get; set; }
[Export] public float RotationSpeed { get; set; } = 3.0f; // 炮塔旋转速度
private float _shootCooldown;
public Turret()
{
MaxHealth = 3;
MoveSpeed = 0.0f; // 不会移动
DetectionRange = 18.0f;
AttackRange = 15.0f;
ScoreValue = 300;
}
protected override void OnPatrol(float delta)
{
// 炮台不移动,只检测玩家
_target = FindPlayer();
if (_target != null)
{
float dist = GlobalPosition.DistanceTo(_target.GlobalPosition);
if (dist <= DetectionRange)
_currentState = EnemyState.Attack; // 炮台直接从巡逻到攻击
}
}
// 炮台不需要追踪,重写为空
protected override void OnChase(float delta) { }
protected override void OnAttack(float delta)
{
if (_target == null)
{
_currentState = EnemyState.Patrol;
return;
}
float dist = GlobalPosition.DistanceTo(_target.GlobalPosition);
if (dist > DetectionRange * 1.2f)
{
_currentState = EnemyState.Patrol;
return;
}
// 炮塔朝向玩家
Vector3 targetPos = new(_target.GlobalPosition.X, GlobalPosition.Y, _target.GlobalPosition.Z);
float targetAngle = Mathf.Atan2(
targetPos.X - GlobalPosition.X,
targetPos.Z - GlobalPosition.Z
);
Rotation = new Vector3(0, Mathf.LerpAngle(Rotation.Y, targetAngle, RotationSpeed * delta), 0);
// 射击
_shootCooldown -= delta;
if (_shootCooldown <= 0)
{
Shoot();
_shootCooldown = ShootInterval;
}
}
private void Shoot()
{
if (BulletScene == null) return;
// 发射3发子弹,散布角度
for (int i = -1; i <= 1; i++)
{
var bullet = BulletScene.Instantiate<Area3D>();
GetTree().CurrentScene.AddChild(bullet);
bullet.GlobalPosition = GlobalPosition + Vector3.Up * 1.0f;
float spread = i * 0.15f; // 散布角度
Vector3 shootDir = -GlobalTransform.Basis.Z.Rotated(Vector3.Up, spread);
bullet.Call("SetDirection", shootDir);
}
}
}GDScript
# 炮台 - 固定位置,旋转射击
extends "res://scripts/enemies/enemy_base.gd"
@export var shoot_interval: float = 1.0
@export var bullet_scene: PackedScene
@export var rotation_speed: float = 3.0 # 炮塔旋转速度
var _shoot_cooldown: float = 0.0
func _init():
max_health = 3
move_speed = 0.0 # 不会移动
detection_range = 18.0
attack_range = 15.0
score_value = 300
func _on_patrol(delta: float):
# 炮台不移动,只检测玩家
_target = _find_player()
if _target:
var dist = global_position.distance_to(_target.global_position)
if dist <= detection_range:
current_state = EnemyState.ATTACK # 炮台直接从巡逻到攻击
# 炮台不需要追踪,重写为空
func _on_chase(_delta: float):
pass
func _on_attack(delta: float):
if _target == null:
current_state = EnemyState.PATROL
return
var dist = global_position.distance_to(_target.global_position)
if dist > detection_range * 1.2:
current_state = EnemyState.PATROL
return
# 炮塔朝向玩家
var target_pos = Vector3(_target.global_position.x, global_position.y, _target.global_position.z)
var target_angle = atan2(
target_pos.x - global_position.x,
target_pos.z - global_position.z
)
rotation.y = lerp_angle(rotation.y, target_angle, rotation_speed * delta)
# 射击
_shoot_cooldown -= delta
if _shoot_cooldown <= 0:
_shoot()
_shoot_cooldown = shoot_interval
func _shoot():
if bullet_scene == null:
return
# 发射3发子弹,散布角度
for i in range(-1, 2):
var bullet = bullet_scene.instantiate()
get_tree().current_scene.add_child(bullet)
bullet.global_position = global_position + Vector3.UP * 1.0
var spread = i * 0.15 # 散布角度
var shoot_dir = -global_transform.basis.z.rotated(Vector3.UP, spread)
bullet.set_direction(shoot_dir)运兵车实现
运兵车会定时生成步兵,需要优先击杀。
C
using Godot;
/// <summary>
/// 运兵车 - 快速移动,定时生成步兵
/// </summary>
public partial class Apoc : EnemyBase
{
[Export] public float SpawnInterval { get; set; } = 3.0f; // 生成步兵间隔
[Export] public PackedScene InfantryScene { get; set; } // 步兵场景
[Export] public int MaxSpawns { get; set; } = 5; // 最多生成几个步兵
private float _spawnCooldown;
private int _spawnedCount;
public Apoc()
{
MaxHealth = 2;
MoveSpeed = 5.0f; // 运兵车比较快
DetectionRange = 15.0f;
AttackRange = 8.0f; // 靠近后停车放人
ScoreValue = 300;
}
protected override void OnAttack(float delta)
{
if (_target == null) return;
// 面向玩家
LookAt(new Vector3(_target.GlobalPosition.X, GlobalPosition.Y, _target.GlobalPosition.Z), Vector3.Up);
// 减速停车
Velocity = Velocity.MoveToward(Vector3.Zero, MoveSpeed * delta * 3f);
// 定时生成步兵
_spawnCooldown -= delta;
if (_spawnCooldown <= 0 && _spawnedCount < MaxSpawns)
{
SpawnInfantry();
_spawnCooldown = SpawnInterval;
}
}
private void SpawnInfantry()
{
if (InfantryScene == null) return;
var infantry = InfantryScene.Instantiate<Node3D>();
GetTree().CurrentScene.AddChild(infantry);
// 在车辆附近随机位置生成
float offsetX = (float)GD.RandRange(-2.0, 2.0);
float offsetZ = (float)GD.RandRange(-2.0, 2.0);
infantry.GlobalPosition = GlobalPosition + new Vector3(offsetX, 0, offsetZ);
_spawnedCount++;
}
}GDScript
# 运兵车 - 快速移动,定时生成步兵
extends "res://scripts/enemies/enemy_base.gd"
@export var spawn_interval: float = 3.0 # 生成步兵间隔
@export var infantry_scene: PackedScene # 步兵场景
@export var max_spawns: int = 5 # 最多生成几个步兵
var _spawn_cooldown: float = 0.0
var _spawned_count: int = 0
func _init():
max_health = 2
move_speed = 5.0 # 运兵车比较快
detection_range = 15.0
attack_range = 8.0 # 靠近后停车放人
score_value = 300
func _on_attack(delta: float):
if _target == null:
return
# 面向玩家
look_at(Vector3(_target.global_position.x, global_position.y, _target.global_position.z), Vector3.UP)
# 减速停车
velocity = velocity.move_toward(Vector3.ZERO, move_speed * delta * 3)
# 定时生成步兵
_spawn_cooldown -= delta
if _spawn_cooldown <= 0 and _spawned_count < max_spawns:
_spawn_infantry()
_spawn_cooldown = spawn_interval
func _spawn_infantry():
if infantry_scene == null:
return
var infantry = infantry_scene.instantiate()
get_tree().current_scene.add_child(infantry)
# 在车辆附近随机位置生成
var offset_x = randf_range(-2.0, 2.0)
var offset_z = randf_range(-2.0, 2.0)
infantry.global_position = global_position + Vector3(offset_x, 0, offset_z)
_spawned_count += 1敌人生成管理器
不要手动在编辑器里一个个放敌人——用一个管理器根据关卡配置自动生成:
C
using Godot;
using System.Collections.Generic;
/// <summary>
/// 敌人生成管理器 - 根据区域触发器生成敌人
/// </summary>
public partial class EnemySpawner : Node
{
[Export] public PackedScene InfantryScene { get; set; }
[Export] public PackedScene TankScene { get; set; }
[Export] public PackedScene TurretScene { get; set; }
[Export] public PackedScene ApocScene { get; set; }
private readonly Dictionary<string, List<SpawnInfo>> _spawnGroups = new();
public struct SpawnInfo
{
public string EnemyType;
public Vector3 Position;
public float Delay;
}
public override void _Ready()
{
// 监听区域触发器信号
GetTree().Root.Connect("TriggerEntered", new Callable(this, nameof(OnTriggerEntered)));
}
/// <summary>
/// 注册生成组
/// </summary>
public void RegisterSpawnGroup(string triggerId, SpawnInfo[] spawns)
{
_spawnGroups[triggerId] = new List<SpawnInfo>(spawns);
}
/// <summary>
/// 触发器被激活时调用
/// </summary>
private void OnTriggerEntered(string triggerId)
{
if (!_spawnGroups.ContainsKey(triggerId)) return;
foreach (var spawn in _spawnGroups[triggerId])
{
// 使用定时器实现延迟生成
GetTree().CreateTimer(spawn.Delay).Timeout += () =>
{
SpawnEnemy(spawn.EnemyType, spawn.Position);
};
}
}
private void SpawnEnemy(string type, Vector3 position)
{
PackedScene scene = type switch
{
"infantry" => InfantryScene,
"tank" => TankScene,
"turret" => TurretScene,
"apoc" => ApocScene,
_ => null
};
if (scene == null) return;
var enemy = scene.Instantiate<Node3D>();
GetTree().CurrentScene.AddChild(enemy);
enemy.GlobalPosition = position;
}
}GDScript
# 敌人生成管理器 - 根据区域触发器生成敌人
extends Node
@export var infantry_scene: PackedScene
@export var tank_scene: PackedScene
@export var turret_scene: PackedScene
@export var apoc_scene: PackedScene
var _spawn_groups: Dictionary = {}
func _ready():
# 监听所有触发器
_connect_triggers()
func _connect_triggers():
var triggers = get_tree().get_nodes_in_group("area_trigger")
for trigger in triggers:
if trigger.has_signal("trigger_entered"):
trigger.trigger_entered.connect(_on_trigger_entered)
## 注册生成组
func register_spawn_group(trigger_id: String, spawns: Array):
_spawn_groups[trigger_id] = spawns
## 触发器被激活时调用
func _on_trigger_entered(trigger_id: String):
if not _spawn_groups.has(trigger_id):
return
for spawn in _spawn_groups[trigger_id]:
# 使用定时器实现延迟生成
var delay = spawn.get("delay", 0.0)
get_tree().create_timer(delay).timeout.connect(
func(): _spawn_enemy(spawn.type, spawn.position)
)
func _spawn_enemy(type: String, position: Vector3):
var scene = null
match type:
"infantry": scene = infantry_scene
"tank": scene = tank_scene
"turret": scene = turret_scene
"apoc": scene = apoc_scene
if scene == null:
return
var enemy = scene.instantiate()
get_tree().current_scene.add_child(enemy)
enemy.global_position = position敌人配置参数汇总
方便后续调参的速查表:
| 敌人 | 血量 | 速度 | 检测范围 | 攻击范围 | 射击间隔 | 得分 |
|---|---|---|---|---|---|---|
| 步兵 | 1 | 2.0 | 12 | 8 | 1.5s | 100 |
| 坦克 | 5 | 3.0 | 20 | 15 | 2.5s | 500 |
| 炮台 | 3 | 0 | 18 | 15 | 1.0s | 300 |
| 直升机 | 3 | 6.0 | 20 | 12 | 2.0s | 400 |
| 运兵车 | 2 | 5.0 | 15 | 8 | 3.0s | 300 |
平衡性提示
这些数值只是起点!在游戏可玩后,你需要反复试玩来调整这些数值。一个好的做法是:先让敌人"明显太弱",然后逐渐增强,直到找到"有挑战但不太难"的感觉。
本章检查清单
下一章
敌人有了,下一步来搭建关卡和任务系统。
