15. 玩家角色创建与战斗
2026/4/14大约 14 分钟
玩家角色创建与战斗
创建玩家角色的完整流程
创建一个玩家角色就像制作一个游戏里的"演员"——需要给演员身体(模型)、大脑(控制脚本)、武器(攻击系统)和生命值(血条)。
完整流程:
- 搭建角色场景结构——创建角色的"身体骨架"
- 配置输入映射——告诉游戏"WASD 代表移动,空格代表跳跃"
- 编写移动和视角控制——让角色能动、能看
- 实现攻击系统——让角色能打怪
- 添加生命值系统——让角色能受伤、能死亡
- 实现状态机——让角色知道什么时候该做什么
角色场景结构
一个标准的第三人称角色场景包含以下节点:
每个节点的作用:
| 节点 | 作用 | 比喻 |
|---|---|---|
| CharacterBody3D | 角色的物理主体,处理移动和碰撞 | 演员的身体 |
| CollisionShape3D | 定义角色的碰撞形状(通常用胶囊形) | 演员穿的紧身衣(标记边界) |
| MeshInstance3D | 角色的外观模型 | 演员的服装 |
| Camera3D | 跟随角色的摄像机 | 观众的眼睛 |
| Area3D | 检测攻击范围内的敌人 | 武器的攻击范围 |
| Node(生命值组件) | 管理角色的血量 | 角色的"命" |
输入映射设置
在编写控制代码之前,需要在 Godot 中配置输入映射——告诉游戏哪个按键对应哪个动作。
在编辑器中设置:项目 → 项目设置 → 输入映射(Input Map)
需要添加以下映射:
| 动作名称 | 按键 | 说明 |
|---|---|---|
move_forward | W | 向前移动 |
move_backward | S | 向后移动 |
move_left | A | 向左移动 |
move_right | D | 向右移动 |
jump | Space | 跳跃 |
attack | 鼠标左键 | 攻击 |
rotate_camera | 鼠标右键拖动 | 旋转视角 |
也可以通过代码注册输入映射:
C#
// 通过代码注册输入映射(放在项目自动加载脚本中)
using Godot;
public partial class InputSetup : Node
{
public override void _Ready()
{
RegisterInput("move_forward", Key.W);
RegisterInput("move_backward", Key.S);
RegisterInput("move_left", Key.A);
RegisterInput("move_right", Key.D);
RegisterInput("jump", Key.Space);
RegisterInput("attack", Key.Unknown); // 鼠标左键需在编辑器设置
}
private static void RegisterInput(string action, Key key)
{
if (!InputMap.HasAction(action))
{
InputMap.AddAction(action);
var ev = new InputEventKey();
ev.Keycode = key;
InputMap.ActionAddEvent(action, ev);
}
}
}GDScript
# 通过代码注册输入映射(放在项目自动加载脚本中)
extends Node
func _ready():
register_input("move_forward", KEY_W)
register_input("move_backward", KEY_S)
register_input("move_left", KEY_A)
register_input("move_right", KEY_D)
register_input("jump", KEY_SPACE)
register_input("attack", KEY_UNKNOWN) # 鼠标左键需在编辑器设置
func register_input(action: String, key_code: int):
if not InputMap.has_action(action):
InputMap.add_action(action)
var ev = InputEventKey.new()
ev.keycode = key_code
InputMap.action_add_event(action, ev)第三人称角色控制器
这是整个角色系统的核心——控制角色移动和视角旋转。
C#
// 第三人称角色控制器
using Godot;
public partial class PlayerController : CharacterBody3D
{
[Export] public float MoveSpeed { get; set; } = 5.0f;
[Export] public float SprintSpeed { get; set; } = 8.0f;
[Export] public float JumpVelocity { get; set; } = 4.5f;
[Export] public float MouseSensitivity { get; set; } = 0.003f;
[Export] public float CameraDistance { get; set; } = 4.0f;
private float _gravity = ProjectSettings.GetSetting("physics/3d/default_gravity").AsSingle();
private Node3D _cameraPivot;
private Camera3D _camera;
private Vector2 _screenCenter;
public override void _Ready()
{
// 获取摄像机挂载点
_cameraPivot = GetNode<Node3D>("CameraPivot");
_camera = _cameraPivot.GetNode<Camera3D>("Camera3D");
// 设置摄像机初始位置
_camera.Position = new Vector3(0, 1.5f, CameraDistance);
// 捕获鼠标(隐藏光标)
Input.MouseMode = Input.MouseModeEnum.Captured;
_screenCenter = new Vector2(
(float)DisplayServer.ScreenGetSize().X / 2,
(float)DisplayServer.ScreenGetSize().Y / 2
);
}
public override void _UnhandledInput(InputEvent @event)
{
// 鼠标移动控制视角
if (@event is InputEventMouseMotion mouseMotion)
{
// 水平旋转:角色左右转
RotateY(-mouseMotion.Relative.X * MouseSensitivity);
// 垂直旋转:摄像机上下看
float vertAngle = -mouseMotion.Relative.Y * MouseSensitivity;
vertAngle = Mathf.Clamp(vertAngle, -0.05f, 0.05f);
_cameraPivot.RotationDegrees = new Vector3(
Mathf.Clamp(_cameraPivot.RotationDegrees.X + vertAngle * 100, -60, 60),
0, 0
);
}
// ESC 键释放鼠标
if (@event.IsActionPressed("ui_cancel"))
{
Input.MouseMode = Input.MouseModeEnum.Visible;
}
}
public override void _PhysicsProcess(double delta)
{
Vector3 velocity = Velocity;
// 重力
if (!IsOnFloor())
{
velocity.Y -= _gravity * (float)delta;
}
// 跳跃
if (Input.IsActionJustPressed("jump") && IsOnFloor())
{
velocity.Y = JumpVelocity;
}
// 获取移动输入
Vector2 inputDir = Input.GetVector(
"move_left", "move_right",
"move_forward", "move_backward"
);
// 将 2D 输入转换为 3D 方向(相对于角色朝向)
Vector3 direction = (Transform.Basis * new Vector3(inputDir.X, 0, inputDir.Y)).Normalized();
// 判断是否冲刺
bool isSprinting = Input.IsActionPressed("sprint");
float speed = isSprinting ? SprintSpeed : MoveSpeed;
if (direction != Vector3.Zero)
{
velocity.X = direction.X * speed;
velocity.Z = direction.Z * speed;
}
else
{
velocity.X = Mathf.MoveToward(velocity.X, 0, speed);
velocity.Z = Mathf.MoveToward(velocity.Z, 0, speed);
}
Velocity = velocity;
MoveAndSlide();
}
}GDScript
# 第三人称角色控制器
extends CharacterBody3D
@export var move_speed: float = 5.0
@export var sprint_speed: float = 8.0
@export var jump_velocity: float = 4.5
@export var mouse_sensitivity: float = 0.003
@export var camera_distance: float = 4.0
var _gravity: float = ProjectSettings.get_setting("physics/3d/default_gravity")
@onready var _camera_pivot: Node3D = $CameraPivot
@onready var _camera: Camera3D = $CameraPivot/Camera3D
func _ready():
# 设置摄像机初始位置
_camera.position = Vector3(0, 1.5, camera_distance)
# 捕获鼠标(隐藏光标)
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
func _unhandled_input(event: InputEvent):
# 鼠标移动控制视角
if event is InputEventMouseMotion:
var mouse_motion = event as InputEventMouseMotion
# 水平旋转:角色左右转
rotate_y(-mouse_motion.relative.x * mouse_sensitivity)
# 垂直旋转:摄像机上下看
var vert_angle = -mouse_motion.relative.y * mouse_sensitivity
vert_angle = clampf(vert_angle, -0.05, 0.05)
_camera_pivot.rotation_degrees = Vector3(
clampf(_camera_pivot.rotation_degrees.x + vert_angle * 100, -60, 60),
0, 0
)
# ESC 键释放鼠标
if event.is_action_pressed("ui_cancel"):
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
func _physics_process(delta: float):
var velocity = velocity
# 重力
if not is_on_floor():
velocity.y -= _gravity * delta
# 跳跃
if Input.is_action_just_pressed("jump") and is_on_floor():
velocity.y = jump_velocity
# 获取移动输入
var input_dir = Input.get_vector(
"move_left", "move_right",
"move_forward", "move_backward"
)
# 将 2D 输入转换为 3D 方向(相对于角色朝向)
var direction = (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
# 判断是否冲刺
var is_sprinting = Input.is_action_pressed("sprint")
var speed = sprint_speed if is_sprinting else move_speed
if direction != Vector3.ZERO:
velocity.x = direction.x * speed
velocity.z = direction.z * speed
else:
velocity.x = move_toward(velocity.x, 0, speed)
velocity.z = move_toward(velocity.z, 0, speed)
velocity = velocity
move_and_slide()攻击系统实现
攻击分为两种类型:近战(用 Area3D 检测范围内的敌人)和远程(用 RayCast3D 射线检测)。
近战攻击
C#
// 近战攻击组件
using Godot;
using Godot.Collections;
using System.Linq;
public partial class MeleeAttackComponent : Node3D
{
[Export] public float AttackRange { get; set; } = 2.0f;
[Export] public int AttackDamage { get; set; } = 10;
[Export] public float AttackCooldown { get; set; } = 0.5f;
[Export] public float AttackAngle { get; set; } = 90.0f; // 攻击扇形角度
private Area3D _attackArea;
private CollisionShape3D _attackShape;
private bool _canAttack = true;
private double _cooldownTimer;
public override void _Ready()
{
// 创建攻击检测区域
_attackArea = new Area3D();
_attackArea.Name = "AttackArea";
AddChild(_attackArea);
// 创建扇形碰撞形状(简化为圆形)
_attackShape = new CollisionShape3D();
var sphere = new SphereShape3D();
sphere.Radius = AttackRange;
_attackShape.Shape = sphere;
_attackShape.Position = Vector3.Zero;
_attackArea.AddChild(_attackShape);
}
public override void _Process(double delta)
{
if (!_canAttack)
{
_cooldownTimer -= delta;
if (_cooldownTimer <= 0)
{
_canAttack = true;
}
}
// 按攻击键
if (Input.IsActionJustPressed("attack") && _canAttack)
{
PerformAttack();
}
}
/// <summary>执行近战攻击</summary>
private void PerformAttack()
{
_canAttack = false;
_cooldownTimer = AttackCooldown;
// 获取攻击范围内所有物体
var overlappingBodies = _attackArea.GetOverlappingBodies();
foreach (var body in overlappingBodies)
{
// 排除自己
if (body == GetParent()) continue;
// 检查是否在攻击扇形角度内
Vector3 toTarget = (body.GlobalPosition - GlobalPosition).Normalized();
Vector3 forward = -GlobalTransform.Basis.Z;
float angle = Mathf.RadToDeg(forward.AngleTo(toTarget));
if (angle <= AttackAngle / 2.0f)
{
// 对目标造成伤害
if (body.HasMethod("TakeDamage"))
{
body.Call("TakeDamage", AttackDamage);
}
GD.Print($"攻击命中:{body.Name},造成 {AttackDamage} 点伤害");
}
}
}
}GDScript
# 近战攻击组件
extends Node3D
@export var attack_range: float = 2.0
@export var attack_damage: int = 10
@export var attack_cooldown: float = 0.5
@export var attack_angle: float = 90.0 # 攻击扇形角度
var _attack_area: Area3D
var _attack_shape: CollisionShape3D
var _can_attack: bool = true
var _cooldown_timer: float = 0.0
func _ready():
# 创建攻击检测区域
_attack_area = Area3D.new()
_attack_area.name = "AttackArea"
add_child(_attack_area)
# 创建扇形碰撞形状(简化为圆形)
_attack_shape = CollisionShape3D.new()
var sphere = SphereShape3D.new()
sphere.radius = attack_range
_attack_shape.shape = sphere
_attack_shape.position = Vector3.ZERO
_attack_area.add_child(_attack_shape)
func _process(delta: float):
if not _can_attack:
_cooldown_timer -= delta
if _cooldown_timer <= 0:
_can_attack = true
# 按攻击键
if Input.is_action_just_pressed("attack") and _can_attack:
perform_attack()
## 执行近战攻击
func perform_attack():
_can_attack = false
_cooldown_timer = attack_cooldown
# 获取攻击范围内所有物体
var overlapping_bodies = _attack_area.get_overlapping_bodies()
for body in overlapping_bodies:
# 排除自己
if body == get_parent():
continue
# 检查是否在攻击扇形角度内
var to_target = (body.global_position - global_position).normalized()
var forward = -global_transform.basis.z
var angle = rad_to_deg(forward.angle_to(to_target))
if angle <= attack_angle / 2.0:
# 对目标造成伤害
if body.has_method("take_damage"):
body.take_damage(attack_damage)
print("攻击命中:%s,造成 %d 点伤害" % [body.name, attack_damage])远程攻击
C#
// 远程攻击组件(射线检测)
using Godot;
public partial class RangedAttackComponent : Node3D
{
[Export] public float AttackRange { get; set; } = 50.0f;
[Export] public int AttackDamage { get; set; } = 15;
[Export] public float AttackCooldown { get; set; } = 0.3f;
private RayCast3D _raycast;
private bool _canAttack = true;
private double _cooldownTimer;
public override void _Ready()
{
// 创建射线
_raycast = new RayCast3D();
_raycast.Name = "AttackRay";
_raycast.TargetPosition = new Vector3(0, 0, -AttackRange);
_raycast.Enabled = true;
AddChild(_raycast);
}
public override void _Process(double delta)
{
if (!_canAttack)
{
_cooldownTimer -= delta;
if (_cooldownTimer <= 0) _canAttack = true;
}
if (Input.IsActionJustPressed("attack") && _canAttack)
{
PerformRangedAttack();
}
}
private void PerformRangedAttack()
{
_canAttack = false;
_cooldownTimer = AttackCooldown;
// 强制射线立即更新
_raycast.ForceRaycastUpdate();
if (_raycast.IsColliding())
{
var target = _raycast.GetCollider();
if (target != null && target.HasMethod("TakeDamage"))
{
target.Call("TakeDamage", AttackDamage);
GD.Print($"远程命中:{target},造成 {AttackDamage} 点伤害");
}
// 在命中点创建特效(简化示例)
var hitPos = _raycast.GetCollisionPoint();
GD.Print($"命中位置:{hitPos}");
}
}
}GDScript
# 远程攻击组件(射线检测)
extends Node3D
@export var attack_range: float = 50.0
@export var attack_damage: int = 15
@export var attack_cooldown: float = 0.3
var _raycast: RayCast3D
var _can_attack: bool = true
var _cooldown_timer: float = 0.0
func _ready():
# 创建射线
_raycast = RayCast3D.new()
_raycast.name = "AttackRay"
_raycast.target_position = Vector3(0, 0, -attack_range)
_raycast.enabled = true
add_child(_raycast)
func _process(delta: float):
if not _can_attack:
_cooldown_timer -= delta
if _cooldown_timer <= 0:
_can_attack = true
if Input.is_action_just_pressed("attack") and _can_attack:
perform_ranged_attack()
func perform_ranged_attack():
_can_attack = false
_cooldown_timer = attack_cooldown
# 强制射线立即更新
_raycast.force_raycast_update()
if _raycast.is_colliding():
var target = _raycast.get_collider()
if target and target.has_method("take_damage"):
target.take_damage(attack_damage)
print("远程命中:%s,造成 %d 点伤害" % [target, attack_damage])
# 在命中点创建特效(简化示例)
var hit_pos = _raycast.get_collision_point()
print("命中位置:%s" % hit_pos)生命值组件
C#
// 生命值组件
using Godot;
public partial class HealthComponent : Node
{
[Signal] public delegate void HealthChangedEventHandler(int newHealth, int maxHealth);
[Signal] public delegate void DiedEventHandler();
[Signal] public delegate void HurtEventHandler(int damage, Vector3 fromDirection);
[Export] public int MaxHealth { get; set; } = 100;
private int _currentHealth;
public int CurrentHealth => _currentHealth;
public bool IsDead => _currentHealth <= 0;
public float HealthPercent => (float)_currentHealth / MaxHealth;
public override void _Ready()
{
_currentHealth = MaxHealth;
EmitSignal(SignalName.HealthChanged, _currentHealth, MaxHealth);
}
/// <summary>受到伤害</summary>
public void TakeDamage(int damage)
{
if (IsDead) return;
_currentHealth -= damage;
_currentHealth = Mathf.Max(_currentHealth, 0);
GD.Print($"受到 {damage} 点伤害,剩余 {_currentHealth}/{MaxHealth}");
EmitSignal(SignalName.HealthChanged, _currentHealth, MaxHealth);
if (_currentHealth <= 0)
{
GD.Print("角色已死亡!");
EmitSignal(SignalName.Died);
}
}
/// <summary>受到伤害(带方向,用于击退)</summary>
public void TakeDamage(int damage, Vector3 fromDirection)
{
TakeDamage(damage);
EmitSignal(SignalName.Hurt, damage, fromDirection);
}
/// <summary>治疗</summary>
public void Heal(int amount)
{
if (IsDead) return;
_currentHealth = Mathf.Min(_currentHealth + amount, MaxHealth);
GD.Print($"恢复了 {amount} 点生命,当前 {_currentHealth}/{MaxHealth}");
EmitSignal(SignalName.HealthChanged, _currentHealth, MaxHealth);
}
/// <summary>复活</summary>
public void Revive()
{
_currentHealth = MaxHealth;
GD.Print("角色已复活!");
EmitSignal(SignalName.HealthChanged, _currentHealth, MaxHealth);
}
}GDScript
# 生命值组件
extends Node
signal health_changed(new_health: int, max_health: int)
signal died()
signal hurt(damage: int, from_direction: Vector3)
@export var max_health: int = 100
var _current_health: int
var current_health: int:
get: return _current_health
var is_dead: bool:
get: return _current_health <= 0
var health_percent: float:
get: return float(_current_health) / max_health
func _ready():
_current_health = max_health
health_changed.emit(_current_health, max_health)
## 受到伤害
func take_damage(damage: int):
if is_dead:
return
_current_health -= damage
_current_health = maxi(_current_health, 0)
print("受到 %d 点伤害,剩余 %d/%d" % [damage, _current_health, max_health])
health_changed.emit(_current_health, max_health)
if _current_health <= 0:
print("角色已死亡!")
died.emit()
## 受到伤害(带方向,用于击退)
func take_damage_with_direction(damage: int, from_direction: Vector3):
take_damage(damage)
hurt.emit(damage, from_direction)
## 治疗
func heal(amount: int):
if is_dead:
return
_current_health = mini(_current_health + amount, max_health)
print("恢复了 %d 点生命,当前 %d/%d" % [amount, _current_health, max_health])
health_changed.emit(_current_health, max_health)
## 复活
func revive():
_current_health = max_health
print("角色已复活!")
health_changed.emit(_current_health, max_health)受击反馈
C#
// 受击反馈效果
using Godot;
public partial class HurtFeedback : Node3D
{
[Export] public float FlashDuration { get; set; } = 0.15f;
[Export] public float KnockbackForce { get; set; } = 5.0f;
[Export] public Color HurtColor { get; set; } = new Color(1, 0, 0, 0.5f);
private MeshInstance3D _mesh;
private Material _originalMaterial;
private double _flashTimer;
private bool _isFlashing;
private HealthComponent _health;
private CharacterBody3D _body;
public override void _Ready()
{
_mesh = GetNode<MeshInstance3D>("MeshInstance3D");
_health = GetNode<HealthComponent>("../HealthComponent");
_body = GetParent() as CharacterBody3D;
// 监听受伤事件
_health.Hurt += OnHurt;
}
public override void _Process(double delta)
{
if (_isFlashing)
{
_flashTimer -= delta;
if (_flashTimer <= 0)
{
// 恢复原始材质
_mesh.MaterialOverride = _originalMaterial;
_isFlashing = false;
}
}
}
private void OnHurt(int damage, Vector3 fromDirection)
{
// 1. 材质闪烁
_originalMaterial = _mesh.MaterialOverride;
var flashMat = new StandardMaterial3D();
flashMat.AlbedoColor = HurtColor;
flashMat.ShadingMode = StandardMaterial3D.ShadingModeEnum.Unshaded;
_mesh.MaterialOverride = flashMat;
_isFlashing = true;
_flashTimer = FlashDuration;
// 2. 击退效果
if (_body != null)
{
Vector3 knockback = fromDirection * KnockbackForce;
knockback.Y = 2.0f; // 稍微向上弹起
_body.Velocity = knockback;
}
GD.Print("受击反馈触发!");
}
}GDScript
# 受击反馈效果
extends Node3D
@export var flash_duration: float = 0.15
@export var knockback_force: float = 5.0
@export var hurt_color: Color = Color(1, 0, 0, 0.5)
@onready var _mesh: MeshInstance3D = $MeshInstance3D
@onready var _health: HealthComponent = $"../HealthComponent"
var _original_material: Material
var _flash_timer: float = 0.0
var _is_flashing: bool = false
var _body: CharacterBody3D
func _ready():
_body = get_parent() as CharacterBody3D
# 监听受伤事件
_health.hurt.connect(_on_hurt)
func _process(delta: float):
if _is_flashing:
_flash_timer -= delta
if _flash_timer <= 0:
# 恢复原始材质
_mesh.material_override = _original_material
_is_flashing = false
func _on_hurt(damage: int, from_direction: Vector3):
# 1. 材质闪烁
_original_material = _mesh.material_override
var flash_mat = StandardMaterial3D.new()
flash_mat.albedo_color = hurt_color
flash_mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
_mesh.material_override = flash_mat
_is_flashing = true
_flash_timer = flash_duration
# 2. 击退效果
if _body:
var knockback = from_direction * knockback_force
knockback.y = 2.0 # 稍微向上弹起
_body.velocity = knockback
print("受击反馈触发!")角色状态机
状态机就像角色的"大脑"——告诉角色在不同状态下应该做什么,以及什么时候切换到另一个状态。
C#
// 角色状态机
using Godot;
public enum PlayerState
{
Idle,
Moving,
Attacking,
Hurt,
Dead
}
public partial class PlayerStateMachine : Node
{
[Export] public float HurtDuration { get; set; } = 0.3f;
[Export] public float AttackDuration { get; set; } = 0.5f;
private PlayerState _currentState = PlayerState.Idle;
private double _stateTimer;
private HealthComponent _health;
private CharacterBody3D _player;
// 状态机的状态属性(方便其他脚本查询)
public PlayerState CurrentState => _currentState;
public bool CanMove => _currentState is PlayerState.Idle or PlayerState.Moving;
public bool CanAttack => _currentState is PlayerState.Idle or PlayerState.Moving;
public override void _Ready()
{
_player = GetParent() as CharacterBody3D;
_health = _player.GetNode<HealthComponent>("HealthComponent");
// 监听生命值事件
_health.Hurt += OnHurt;
_health.Died += OnDied;
}
public override void _Process(double delta)
{
switch (_currentState)
{
case PlayerState.Idle:
ProcessIdle();
break;
case PlayerState.Moving:
ProcessMoving();
break;
case PlayerState.Attacking:
ProcessAttacking(delta);
break;
case PlayerState.Hurt:
ProcessHurt(delta);
break;
case PlayerState.Dead:
ProcessDead();
break;
}
}
private void ProcessIdle()
{
// 检查是否开始移动
Vector2 input = Input.GetVector("move_left", "move_right", "move_forward", "move_backward");
if (input != Vector2.Zero)
{
ChangeState(PlayerState.Moving);
return;
}
// 检查是否开始攻击
if (Input.IsActionJustPressed("attack"))
{
ChangeState(PlayerState.Attacking);
}
}
private void ProcessMoving()
{
// 检查是否停止移动
Vector2 input = Input.GetVector("move_left", "move_right", "move_forward", "move_backward");
if (input == Vector2.Zero)
{
ChangeState(PlayerState.Idle);
return;
}
// 检查是否开始攻击
if (Input.IsActionJustPressed("attack"))
{
ChangeState(PlayerState.Attacking);
}
}
private void ProcessAttacking(double delta)
{
_stateTimer -= delta;
if (_stateTimer <= 0)
{
ChangeState(PlayerState.Idle);
}
}
private void ProcessHurt(double delta)
{
_stateTimer -= delta;
if (_stateTimer <= 0)
{
ChangeState(PlayerState.Idle);
}
}
private void ProcessDead()
{
// 死亡状态:禁用输入,播放死亡动画
GD.Print("角色已阵亡,等待复活...");
}
private void OnHurt(int damage, Vector3 fromDirection)
{
ChangeState(PlayerState.Hurt);
}
private void OnDied()
{
ChangeState(PlayerState.Dead);
}
private void ChangeState(PlayerState newState)
{
if (_currentState == newState) return;
GD.Print($"状态切换:{_currentState} → {newState}");
_currentState = newState;
// 进入新状态时的初始化
switch (newState)
{
case PlayerState.Attacking:
_stateTimer = AttackDuration;
break;
case PlayerState.Hurt:
_stateTimer = HurtDuration;
break;
case PlayerState.Dead:
// 禁用碰撞,防止死后还能被攻击
if (_player != null)
{
_player.SetPhysicsProcess(false);
}
break;
}
}
}GDScript
# 角色状态机
extends Node
enum PlayerState {
IDLE,
MOVING,
ATTACKING,
HURT,
DEAD
}
@export var hurt_duration: float = 0.3
@export var attack_duration: float = 0.5
var _current_state: PlayerState = PlayerState.IDLE
var _state_timer: float = 0.0
var _health: HealthComponent
var _player: CharacterBody3D
# 当前状态
var current_state: PlayerState:
get: return _current_state
# 是否可以移动
var can_move: bool:
get: return _current_state in [PlayerState.IDLE, PlayerState.MOVING]
# 是否可以攻击
var can_attack: bool:
get: return _current_state in [PlayerState.IDLE, PlayerState.MOVING]
func _ready():
_player = get_parent() as CharacterBody3D
_health = _player.get_node("HealthComponent")
# 监听生命值事件
_health.hurt.connect(_on_hurt)
_health.died.connect(_on_died)
func _process(delta: float):
match _current_state:
PlayerState.IDLE:
process_idle()
PlayerState.MOVING:
process_moving()
PlayerState.ATTACKING:
process_attacking(delta)
PlayerState.HURT:
process_hurt(delta)
PlayerState.DEAD:
process_dead()
func process_idle():
# 检查是否开始移动
var input = Input.get_vector("move_left", "move_right", "move_forward", "move_backward")
if input != Vector2.ZERO:
change_state(PlayerState.MOVING)
return
# 检查是否开始攻击
if Input.is_action_just_pressed("attack"):
change_state(PlayerState.ATTACKING)
func process_moving():
# 检查是否停止移动
var input = Input.get_vector("move_left", "move_right", "move_forward", "move_backward")
if input == Vector2.ZERO:
change_state(PlayerState.IDLE)
return
# 检查是否开始攻击
if Input.is_action_just_pressed("attack"):
change_state(PlayerState.ATTACKING)
func process_attacking(delta: float):
_state_timer -= delta
if _state_timer <= 0:
change_state(PlayerState.IDLE)
func process_hurt(delta: float):
_state_timer -= delta
if _state_timer <= 0:
change_state(PlayerState.IDLE)
func process_dead():
# 死亡状态:禁用输入,播放死亡动画
pass
func _on_hurt(damage: int, from_direction: Vector3):
change_state(PlayerState.HURT)
func _on_died():
change_state(PlayerState.DEAD)
func change_state(new_state: PlayerState):
if _current_state == new_state:
return
print("状态切换:%s → %s" % [PlayerState.keys()[_current_state], PlayerState.keys()[new_state]])
_current_state = new_state
# 进入新状态时的初始化
match new_state:
PlayerState.ATTACKING:
_state_timer = attack_duration
PlayerState.HURT:
_state_timer = hurt_duration
PlayerState.DEAD:
# 禁用物理处理,防止死后还能被攻击
if _player:
_player.set_physics_process(false)完整的角色整合代码
将所有组件整合到一个角色场景中:
C#
// 完整的玩家角色(整合所有组件)
using Godot;
public partial class Player : CharacterBody3D
{
// 组件引用
private HealthComponent _health;
private PlayerStateMachine _stateMachine;
private MeleeAttackComponent _meleeAttack;
private HurtFeedback _hurtFeedback;
// 参数
[Export] public float MoveSpeed { get; set; } = 5.0f;
[Export] public float JumpVelocity { get; set; } = 4.5f;
[Export] public float MouseSensitivity { get; set; } = 0.003f;
private float _gravity;
public override void _Ready()
{
_gravity = (float)ProjectSettings.GetSetting("physics/3d/default_gravity").AsSingle();
// 获取组件引用
_health = GetNode<HealthComponent>("HealthComponent");
_stateMachine = GetNode<PlayerStateMachine>("StateMachine");
_meleeAttack = GetNode<MeleeAttackComponent>("MeleeAttack");
_hurtFeedback = GetNode<HurtFeedback>("HurtFeedback");
// 捕获鼠标
Input.MouseMode = Input.MouseModeEnum.Captured;
GD.Print("玩家角色初始化完成!");
}
public override void _UnhandledInput(InputEvent @event)
{
if (@event is InputEventMouseMotion motion)
{
RotateY(-motion.Relative.X * MouseSensitivity);
}
}
public override void _PhysicsProcess(double delta)
{
if (_stateMachine.CurrentState == PlayerState.Dead) return;
Vector3 velocity = Velocity;
// 重力
if (!IsOnFloor())
velocity.Y -= _gravity * (float)delta;
// 跳跃
if (Input.IsActionJustPressed("jump") && IsOnFloor())
velocity.Y = JumpVelocity;
// 移动
if (_stateMachine.CanMove)
{
Vector2 input = Input.GetVector("move_left", "move_right", "move_forward", "move_backward");
Vector3 dir = (Transform.Basis * new Vector3(input.X, 0, input.Y)).Normalized();
if (dir != Vector3.Zero)
{
velocity.X = dir.X * MoveSpeed;
velocity.Z = dir.Z * MoveSpeed;
}
else
{
velocity.X = Mathf.MoveToward(velocity.X, 0, MoveSpeed);
velocity.Z = Mathf.MoveToward(velocity.Z, 0, MoveSpeed);
}
}
Velocity = velocity;
MoveAndSlide();
}
/// <summary>外部调用受伤接口</summary>
public void TakeDamage(int damage)
{
_health.TakeDamage(damage);
}
}GDScript
# 完整的玩家角色(整合所有组件)
extends CharacterBody3D
# 组件引用
@onready var health: HealthComponent = $HealthComponent
@onready var state_machine: PlayerStateMachine = $StateMachine
@onready var melee_attack: MeleeAttackComponent = $MeleeAttack
@onready var hurt_feedback: HurtFeedback = $HurtFeedback
# 参数
@export var move_speed: float = 5.0
@export var jump_velocity: float = 4.5
@export var mouse_sensitivity: float = 0.003
var _gravity: float
func _ready():
_gravity = ProjectSettings.get_setting("physics/3d/default_gravity")
# 捕获鼠标
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
print("玩家角色初始化完成!")
func _unhandled_input(event: InputEvent):
if event is InputEventMouseMotion:
var motion = event as InputEventMouseMotion
rotate_y(-motion.relative.x * mouse_sensitivity)
func _physics_process(delta: float):
if state_machine.current_state == PlayerStateMachine.PlayerState.DEAD:
return
var velocity = velocity
# 重力
if not is_on_floor():
velocity.y -= _gravity * delta
# 跳跃
if Input.is_action_just_pressed("jump") and is_on_floor():
velocity.y = jump_velocity
# 移动
if state_machine.can_move:
var input = Input.get_vector("move_left", "move_right", "move_forward", "move_backward")
var dir = (transform.basis * Vector3(input.x, 0, input.y)).normalized()
if dir != Vector3.ZERO:
velocity.x = dir.x * move_speed
velocity.z = dir.z * move_speed
else:
velocity.x = move_toward(velocity.x, 0, move_speed)
velocity.z = move_toward(velocity.z, 0, move_speed)
velocity = velocity
move_and_slide()
## 外部调用受伤接口
func take_damage(damage: int):
health.take_damage(damage)本章小结
本章我们学习了玩家角色的完整创建流程:
| 组件 | 功能 | 关键节点 |
|---|---|---|
| 角色主体 | 移动和物理碰撞 | CharacterBody3D + CollisionShape3D |
| 摄像机 | 第三人称视角 | Camera3D + Node3D(挂载点) |
| 近战攻击 | 扇形范围检测 | Area3D + CollisionShape3D |
| 远程攻击 | 射线检测 | RayCast3D |
| 生命值 | 血量管理 | Node(自定义组件) |
| 受击反馈 | 闪烁和击退 | MeshInstance3D 材质切换 |
| 状态机 | 行为控制 | 枚举 + switch/match |
这些组件可以自由组合、替换。例如,把近战攻击换成远程攻击,就能让角色从战士变成射手。
