3. 玩家移动与攻击
2026/4/14大约 5 分钟
3. 玩家移动与攻击
3.1 玩家角色的节点结构
在 Godot 中,每个玩家角色是一个 CharacterBody2D 节点:
Player (CharacterBody2D)
├── Sprite2D # 角色图片
├── AnimationPlayer # 动画控制器
├── CollisionShape2D # 碰撞体(胶囊形)
├── AttackHitbox (Area2D) # 攻击判定区域
│ └── CollisionShape2D
└── HurtBox (Area2D) # 受击判定区域
└── CollisionShape2DCharacterBody2D:Godot 专门为"受玩家控制的角色"设计的节点,内置了移动和碰撞检测功能。
3.2 玩家基类
所有职业共用同一套移动逻辑,我们用一个基类来实现:
C
using Godot;
/// <summary>
/// 玩家基类——所有职业共用的移动和状态逻辑
/// </summary>
public partial class PlayerBase : CharacterBody2D
{
// ===== 导出属性(在编辑器中可调整)=====
[Export] public int PlayerId { get; set; } = 1;
[Export] public float MoveSpeed { get; set; } = 200f;
[Export] public float JumpForce { get; set; } = 400f;
[Export] public int MaxHp { get; set; } = 100;
[Export] public int MaxMp { get; set; } = 50;
// ===== 运行时状态 =====
protected int _hp;
protected int _mp;
protected bool _isKnockedDown; // 是否倒地
protected float _knockDownTimer; // 倒地计时器
protected bool _isInvincible; // 是否无敌(翻滚时)
protected float _invincibleTimer;
// 重力加速度(像素/秒²)
private const float Gravity = 980f;
public bool IsAlive => _hp > 0 && !_isKnockedDown;
public int Hp => _hp;
public int Mp => _mp;
public override void _Ready()
{
_hp = MaxHp;
_mp = MaxMp;
}
public override void _PhysicsProcess(double delta)
{
if (_isKnockedDown)
{
HandleKnockDown((float)delta);
return;
}
ApplyGravity((float)delta);
HandleMovement();
HandleJump();
HandleRoll((float)delta);
HandleAttackInput();
MoveAndSlide();
}
/// <summary>
/// 应用重力——让角色在空中会下落
/// </summary>
private void ApplyGravity(float delta)
{
if (!IsOnFloor())
{
var vel = Velocity;
vel.Y += Gravity * delta;
Velocity = vel;
}
}
/// <summary>
/// 处理左右移动
/// </summary>
private void HandleMovement()
{
var input = PlayerInput.GetMovement(PlayerId);
var vel = Velocity;
vel.X = input.X * MoveSpeed;
Velocity = vel;
// 翻转图片朝向
if (input.X != 0)
Scale = new Vector2(input.X > 0 ? 1 : -1, 1);
}
/// <summary>
/// 处理跳跃
/// </summary>
private void HandleJump()
{
if (IsOnFloor() && PlayerInput.IsAttackJustPressed(PlayerId))
{
// 这里只是示例,实际跳跃用专门的跳跃键
}
// 简化:用"上"键跳跃
if (IsOnFloor() && Input.IsActionJustPressed($"p{PlayerId}_up"))
{
var vel = Velocity;
vel.Y = -JumpForce;
Velocity = vel;
}
}
/// <summary>
/// 处理翻滚(短暂无敌)
/// </summary>
private void HandleRoll(float delta)
{
if (_isInvincible)
{
_invincibleTimer -= delta;
if (_invincibleTimer <= 0f)
_isInvincible = false;
}
if (PlayerInput.IsRollJustPressed(PlayerId) && !_isInvincible)
{
// 翻滚:向当前朝向快速位移,持续0.3秒无敌
var vel = Velocity;
vel.X = Scale.X * MoveSpeed * 2f; // 翻滚速度是移动速度的2倍
Velocity = vel;
_isInvincible = true;
_invincibleTimer = 0.3f;
}
}
/// <summary>
/// 处理攻击输入(子类重写实现不同职业的攻击)
/// </summary>
protected virtual void HandleAttackInput()
{
if (PlayerInput.IsAttackJustPressed(PlayerId))
PerformAttack();
if (PlayerInput.IsSkillJustPressed(PlayerId))
PerformSkill();
}
/// <summary>
/// 执行普通攻击(子类重写)
/// </summary>
protected virtual void PerformAttack() { }
/// <summary>
/// 执行技能(子类重写)
/// </summary>
protected virtual void PerformSkill() { }
/// <summary>
/// 受到伤害
/// </summary>
public void TakeDamage(int damage)
{
if (_isInvincible) return; // 无敌帧期间免疫伤害
_hp = Mathf.Max(_hp - damage, 0);
if (_hp <= 0)
StartKnockDown();
}
/// <summary>
/// 开始倒地状态
/// </summary>
private void StartKnockDown()
{
_isKnockedDown = true;
_knockDownTimer = 5f; // 5秒内可被救援
}
/// <summary>
/// 处理倒地计时
/// </summary>
private void HandleKnockDown(float delta)
{
_knockDownTimer -= delta;
if (_knockDownTimer <= 0f)
{
// 超时,失去一条命(由GameManager处理)
QueueFree();
}
}
/// <summary>
/// 被队友救援
/// </summary>
public void Revive()
{
_isKnockedDown = false;
_hp = MaxHp / 3; // 救援后恢复1/3血量
}
}GDScript
extends CharacterBody2D
class_name PlayerBase
## 玩家基类——所有职业共用的移动和状态逻辑
@export var player_id: int = 1
@export var move_speed: float = 200.0
@export var jump_force: float = 400.0
@export var max_hp: int = 100
@export var max_mp: int = 50
var _hp: int
var _mp: int
var _is_knocked_down: bool = false
var _knock_down_timer: float = 0.0
var _is_invincible: bool = false
var _invincible_timer: float = 0.0
const GRAVITY: float = 980.0
var is_alive: bool:
get: return _hp > 0 and not _is_knocked_down
func _ready() -> void:
_hp = max_hp
_mp = max_mp
func _physics_process(delta: float) -> void:
if _is_knocked_down:
_handle_knock_down(delta)
return
_apply_gravity(delta)
_handle_movement()
_handle_jump()
_handle_roll(delta)
_handle_attack_input()
move_and_slide()
## 应用重力
func _apply_gravity(delta: float) -> void:
if not is_on_floor():
velocity.y += GRAVITY * delta
## 处理左右移动
func _handle_movement() -> void:
var input: Vector2 = PlayerInput.get_movement(player_id)
velocity.x = input.x * move_speed
if input.x != 0:
scale.x = 1.0 if input.x > 0 else -1.0
## 处理跳跃
func _handle_jump() -> void:
if is_on_floor() and Input.is_action_just_pressed("p%d_up" % player_id):
velocity.y = -jump_force
## 处理翻滚(短暂无敌)
func _handle_roll(delta: float) -> void:
if _is_invincible:
_invincible_timer -= delta
if _invincible_timer <= 0.0:
_is_invincible = false
if PlayerInput.is_roll_just_pressed(player_id) and not _is_invincible:
velocity.x = scale.x * move_speed * 2.0
_is_invincible = true
_invincible_timer = 0.3
## 处理攻击输入(子类重写)
func _handle_attack_input() -> void:
if PlayerInput.is_attack_just_pressed(player_id):
_perform_attack()
if PlayerInput.is_skill_just_pressed(player_id):
_perform_skill()
## 执行普通攻击(子类重写)
func _perform_attack() -> void:
pass
## 执行技能(子类重写)
func _perform_skill() -> void:
pass
## 受到伤害
func take_damage(damage: int) -> void:
if _is_invincible:
return
_hp = maxi(_hp - damage, 0)
if _hp <= 0:
_start_knock_down()
## 开始倒地状态
func _start_knock_down() -> void:
_is_knocked_down = true
_knock_down_timer = 5.0
## 处理倒地计时
func _handle_knock_down(delta: float) -> void:
_knock_down_timer -= delta
if _knock_down_timer <= 0.0:
queue_free()
## 被队友救援
func revive() -> void:
_is_knocked_down = false
_hp = max_hp / 33.3 战士职业示例
战士是最简单的职业,近战四连击:
C
using Godot;
/// <summary>
/// 战士——近战四连击,最后一击击飞敌人
/// </summary>
public partial class Warrior : PlayerBase
{
private int _comboCount = 0; // 当前连击数
private float _comboResetTimer = 0f;
private const float ComboWindow = 0.5f; // 0.5秒内继续按攻击才算连击
[Export] public int AttackDamage { get; set; } = 20;
public override void _PhysicsProcess(double delta)
{
base._PhysicsProcess(delta);
// 连击重置计时
if (_comboCount > 0)
{
_comboResetTimer -= (float)delta;
if (_comboResetTimer <= 0f)
_comboCount = 0;
}
}
protected override void PerformAttack()
{
_comboCount = (_comboCount % 4) + 1;
_comboResetTimer = ComboWindow;
// 第4击:击飞效果
bool isLaunch = _comboCount == 4;
int damage = isLaunch ? AttackDamage * 2 : AttackDamage;
// 激活攻击判定区域
ActivateHitbox(damage, isLaunch);
}
protected override void PerformSkill()
{
// 旋风斩:360度范围攻击
ActivateHitbox(AttackDamage * 3, false, isAoe: true);
}
private void ActivateHitbox(int damage, bool launch, bool isAoe = false)
{
// 实际项目中:激活 AttackHitbox 的 Area2D,设置伤害参数
// 这里用信号通知,具体实现在第4章战斗系统中
GD.Print($"战士攻击:伤害={damage}, 击飞={launch}, 范围={isAoe}");
}
}GDScript
extends PlayerBase
class_name Warrior
## 战士——近战四连击,最后一击击飞敌人
var _combo_count: int = 0
var _combo_reset_timer: float = 0.0
const COMBO_WINDOW: float = 0.5
@export var attack_damage: int = 20
func _physics_process(delta: float) -> void:
super._physics_process(delta)
if _combo_count > 0:
_combo_reset_timer -= delta
if _combo_reset_timer <= 0.0:
_combo_count = 0
func _perform_attack() -> void:
_combo_count = (_combo_count % 4) + 1
_combo_reset_timer = COMBO_WINDOW
var is_launch: bool = _combo_count == 4
var damage: int = attack_damage * 2 if is_launch else attack_damage
_activate_hitbox(damage, is_launch)
func _perform_skill() -> void:
# 旋风斩:360度范围攻击
_activate_hitbox(attack_damage * 3, false, true)
func _activate_hitbox(damage: int, launch: bool, is_aoe: bool = false) -> void:
print("战士攻击:伤害=%d, 击飞=%s, 范围=%s" % [damage, launch, is_aoe])3.4 本章小结
| 概念 | 说明 |
|---|---|
| CharacterBody2D | 玩家角色的基础节点,内置移动和碰撞 |
| 重力 | 每帧给 velocity.Y 加速,模拟下落 |
| 翻滚无敌帧 | 翻滚时0.3秒内免疫伤害 |
| 倒地救援 | 血量归零后倒地5秒,队友可救援 |
| 连招系统 | 计时窗口内连续按攻击键触发连招 |
| 职业继承 | 所有职业继承 PlayerBase,重写攻击方法 |
下一章我们将实现完整的战斗系统,包括伤害判定和击退效果。
