3. 玩家移动与射击
2026/4/14大约 7 分钟
3. 雷霆战机——玩家移动与射击
3.1 玩家移动的两种模式
在弹幕射击游戏中,玩家通常有两种移动速度:
- 正常模式:速度快,用于躲避和移动到有利位置
- 低速模式(Focus):按住Shift键后速度降低,但会显示判定点位置
为什么需要低速模式?因为在密集的弹幕中,正常速度太快了,很容易"滑"进子弹里。低速模式让你能精确地穿过子弹之间的缝隙。
就像开车:高速公路上你开120km/h,但在拥挤的停车场你需要降到5km/h慢慢挪。
C
using Godot;
/// <summary>
/// 玩家战机——移动和射击控制
/// </summary>
public partial class Player : Area2D
{
// ===== 节点引用 =====
private Sprite2D _sprite;
private CollisionShape2D _collisionShape;
private Marker2D _hitboxMarker; // 判定点位置标记
private Sprite2D _hitboxVisual; // 判定点可视化
// ===== 状态 =====
private bool _isInvincible = false;
private float _invincibleTimer = 0f;
private bool _isFocusing = false; // 是否在低速模式
private float _fireTimer = 0f;
private bool _alive = true;
// ===== 引用 =====
private RaidenGameManager _gameManager;
public override void _Ready()
{
_sprite = GetNode<Sprite2D>("Sprite");
_collisionShape = GetNode<CollisionShape2D>("CollisionShape");
_hitboxMarker = GetNode<Marker2D>("HitboxMarker");
_hitboxVisual = GetNode<Sprite2D>("HitboxVisual");
_gameManager = GetNode<RaidenGameManager>("/root/RaidenGameManager");
// 初始位置:屏幕底部中间
Position = new Vector2(
GameConfig.ScreenWidth / 2f,
GameConfig.ScreenHeight - 60f
);
// 默认隐藏判定点
_hitboxVisual.Visible = false;
}
public override void _Process(double delta)
{
if (!_alive) return;
HandleMovement((float)delta);
HandleShooting((float)delta);
HandleInvincibility((float)delta);
}
/// <summary>
/// 处理玩家移动
/// </summary>
private void HandleMovement(float delta)
{
// 读取输入方向
var input = Vector2.Zero;
if (Input.IsActionPressed("move_up")) input.Y -= 1;
if (Input.IsActionPressed("move_down")) input.Y += 1;
if (Input.IsActionPressed("move_left")) input.X -= 1;
if (Input.IsActionPressed("move_right")) input.X += 1;
// 归一化(防止斜向移动更快)
if (input.Length() > 0)
input = input.Normalized();
// 检查是否按住低速键(Focus)
_isFocusing = Input.IsActionPressed("focus");
// 计算速度
float speed = GameConfig.PlayerSpeed;
if (_isFocusing)
speed *= GameConfig.FocusSpeedMult;
// 移动
Position += input * speed * delta;
// 显示/隐藏判定点
_hitboxVisual.Visible = _isFocusing;
// 限制移动范围
ClampPosition();
}
/// <summary>
/// 限制玩家在屏幕范围内移动
/// </summary>
private void ClampPosition()
{
float margin = 10f;
float minX = margin;
float maxX = GameConfig.ScreenWidth - margin;
float minY = GameConfig.ScreenHeight * 0.4f; // 不能超过屏幕40%
float maxY = GameConfig.ScreenHeight - margin;
Position = new Vector2(
Mathf.Clamp(Position.X, minX, maxX),
Mathf.Clamp(Position.Y, minY, maxY)
);
}
/// <summary>
/// 处理自动射击
/// </summary>
private void HandleShooting(float delta)
{
_fireTimer -= delta;
if (_fireTimer <= 0f)
{
_fireTimer = GameConfig.PlayerFireRate;
Fire();
}
}
/// <summary>
/// 发射子弹
/// </summary>
private void Fire()
{
var bulletContainer = GetParent().GetNode<Node2D>("PlayerBullets");
int level = _gameManager.WeaponLevel;
// 根据武器等级发射不同数量和模式的子弹
switch (level)
{
case 1:
FireSingleBullet(bulletContainer, Position, Vector2.Up);
break;
case 2:
FireSingleBullet(bulletContainer,
Position + new Vector2(-8, 0), Vector2.Up);
FireSingleBullet(bulletContainer,
Position + new Vector2(8, 0), Vector2.Up);
break;
case 3:
FireSingleBullet(bulletContainer, Position, Vector2.Up);
FireAngledBullet(bulletContainer, Position, -10f);
FireAngledBullet(bulletContainer, Position, 10f);
break;
case 4:
FireSingleBullet(bulletContainer,
Position + new Vector2(-8, 0), Vector2.Up);
FireSingleBullet(bulletContainer,
Position + new Vector2(8, 0), Vector2.Up);
FireAngledBullet(bulletContainer, Position, -15f);
FireAngledBullet(bulletContainer, Position, 15f);
break;
case 5:
FireSingleBullet(bulletContainer, Position, Vector2.Up);
FireSingleBullet(bulletContainer,
Position + new Vector2(-10, 0), Vector2.Up);
FireSingleBullet(bulletContainer,
Position + new Vector2(10, 0), Vector2.Up);
FireAngledBullet(bulletContainer, Position, -20f);
FireAngledBullet(bulletContainer, Position, 20f);
break;
}
}
private void FireSingleBullet(
Node2D container, Vector2 pos, Vector2 direction)
{
var bullet = CreateBullet(container, pos, direction);
bullet.SetDamage(1);
}
private void FireAngledBullet(
Node2D container, Vector2 pos, float angleDegrees)
{
float rad = Mathf.DegToRad(angleDegrees - 90f);
var dir = new Vector2(Mathf.Cos(rad), Mathf.Sin(rad));
var bullet = CreateBullet(container, pos, dir);
bullet.SetDamage(1);
}
private PlayerBullet CreateBullet(
Node2D container, Vector2 pos, Vector2 direction)
{
// 使用对象池(第4章详细实现)
var scene = GD.Load<PackedScene>(
"res://scenes/bullets/player_bullet.tscn");
var bullet = scene.Instantiate<PlayerBullet>();
bullet.Initialize(pos, direction, GameConfig.PlayerBulletSpeed);
container.AddChild(bullet);
return bullet;
}
/// <summary>
/// 处理无敌状态
/// </summary>
private void HandleInvincibility(float delta)
{
if (!_isInvincible) return;
_invincibleTimer -= delta;
// 闪烁效果
float blink = Mathf.Sin(_invincibleTimer * 30f);
_sprite.Visible = blink > 0;
if (_invincibleTimer <= 0f)
{
_isInvincible = false;
_sprite.Visible = true;
}
}
/// <summary>
/// 被击中时调用
/// </summary>
public void OnHit()
{
if (_isInvincible) return;
_gameManager.LoseLife();
if (_gameManager.Lives > 0)
{
// 进入无敌状态
_isInvincible = true;
_invincibleTimer = GameConfig.InvincibleTime;
}
else
{
// 死亡
Die();
}
}
/// <summary>
/// 玩家死亡
/// </summary>
private void Die()
{
_alive = false;
Visible = false;
// 播放爆炸特效
var explosionScene = GD.Load<PackedScene>(
"res://scenes/effects/explosion.tscn");
var explosion = explosionScene.Instantiate<Node2D>();
explosion.GlobalPosition = GlobalPosition;
GetParent().GetNode<Node2D>("Effects").AddChild(explosion);
}
/// <summary>
/// 检查是否处于无敌状态
/// </summary>
public bool IsInvincible() => _isInvincible;
}GDScript
extends Area2D
## 玩家战机——移动和射击控制
# ===== 节点引用 =====
@onready var _sprite = $Sprite
@onready var _collision_shape = $CollisionShape
@onready var _hitbox_marker = $HitboxMarker
@onready var _hitbox_visual = $HitboxVisual
# ===== 状态 =====
var _is_invincible: bool = false
var _invincible_timer: float = 0.0
var _is_focusing: bool = false
var _fire_timer: float = 0.0
var _alive: bool = true
# ===== 引用 =====
var _game_manager: RaidenGameManager
func _ready() -> void:
_game_manager = get_node("/root/RaidenGameManager")
# 初始位置:屏幕底部中间
position = Vector2(
GameConfig.SCREEN_WIDTH / 2.0,
GameConfig.SCREEN_HEIGHT - 60.0
)
# 默认隐藏判定点
_hitbox_visual.visible = false
func _process(delta: float) -> void:
if not _alive:
return
_handle_movement(delta)
_handle_shooting(delta)
_handle_invincibility(delta)
## 处理玩家移动
func _handle_movement(delta: float) -> void:
# 读取输入方向
var input = Vector2.ZERO
if Input.is_action_pressed("move_up"):
input.y -= 1
if Input.is_action_pressed("move_down"):
input.y += 1
if Input.is_action_pressed("move_left"):
input.x -= 1
if Input.is_action_pressed("move_right"):
input.x += 1
# 归一化(防止斜向移动更快)
if input.length() > 0:
input = input.normalized()
# 检查是否按住低速键(Focus)
_is_focusing = Input.is_action_pressed("focus")
# 计算速度
var speed = GameConfig.PLAYER_SPEED
if _is_focusing:
speed *= GameConfig.FOCUS_SPEED_MULT
# 移动
position += input * speed * delta
# 显示/隐藏判定点
_hitbox_visual.visible = _is_focusing
# 限制移动范围
_clamp_position()
## 限制玩家在屏幕范围内移动
func _clamp_position() -> void:
var margin = 10.0
var min_x = margin
var max_x = GameConfig.SCREEN_WIDTH - margin
var min_y = GameConfig.SCREEN_HEIGHT * 0.4
var max_y = GameConfig.SCREEN_HEIGHT - margin
position = Vector2(
clampf(position.x, min_x, max_x),
clampf(position.y, min_y, max_y)
)
## 处理自动射击
func _handle_shooting(delta: float) -> void:
_fire_timer -= delta
if _fire_timer <= 0.0:
_fire_timer = GameConfig.PLAYER_FIRE_RATE
_fire()
## 发射子弹
func _fire() -> void:
var bullet_container = get_parent().get_node("PlayerBullets")
var level = _game_manager.weapon_level
# 根据武器等级发射不同模式的子弹
match level:
1:
_fire_single_bullet(bullet_container, position, Vector2.UP)
2:
_fire_single_bullet(bullet_container,
position + Vector2(-8, 0), Vector2.UP)
_fire_single_bullet(bullet_container,
position + Vector2(8, 0), Vector2.UP)
3:
_fire_single_bullet(bullet_container, position, Vector2.UP)
_fire_angled_bullet(bullet_container, position, -10.0)
_fire_angled_bullet(bullet_container, position, 10.0)
4:
_fire_single_bullet(bullet_container,
position + Vector2(-8, 0), Vector2.UP)
_fire_single_bullet(bullet_container,
position + Vector2(8, 0), Vector2.UP)
_fire_angled_bullet(bullet_container, position, -15.0)
_fire_angled_bullet(bullet_container, position, 15.0)
5:
_fire_single_bullet(bullet_container, position, Vector2.UP)
_fire_single_bullet(bullet_container,
position + Vector2(-10, 0), Vector2.UP)
_fire_single_bullet(bullet_container,
position + Vector2(10, 0), Vector2.UP)
_fire_angled_bullet(bullet_container, position, -20.0)
_fire_angled_bullet(bullet_container, position, 20.0)
func _fire_single_bullet(container: Node2D, pos: Vector2, dir: Vector2) -> void:
var scene = load("res://scenes/bullets/player_bullet.tscn")
var bullet = scene.instantiate()
bullet.initialize(pos, dir, GameConfig.PLAYER_BULLET_SPEED)
container.add_child(bullet)
func _fire_angled_bullet(container: Node2D, pos: Vector2, angle_deg: float) -> void:
var rad = deg_to_rad(angle_deg - 90.0)
var dir = Vector2(cos(rad), sin(rad))
_fire_single_bullet(container, pos, dir)
## 处理无敌状态
func _handle_invincibility(delta: float) -> void:
if not _is_invincible:
return
_invincible_timer -= delta
# 闪烁效果
var blink = sin(_invincible_timer * 30.0)
_sprite.visible = blink > 0
if _invincible_timer <= 0.0:
_is_invincible = false
_sprite.visible = true
## 被击中时调用
func on_hit() -> void:
if _is_invincible:
return
_game_manager.lose_life()
if _game_manager.lives > 0:
_is_invincible = true
_invincible_timer = GameConfig.INVINCIBLE_TIME
else:
_die()
## 玩家死亡
func _die() -> void:
_alive = false
visible = false
# 播放爆炸特效
var explosion_scene = load("res://scenes/effects/explosion.tscn")
var explosion = explosion_scene.instantiate()
explosion.global_position = global_position
get_parent().get_node("Effects").add_child(explosion)
## 检查是否处于无敌状态
func is_invincible() -> bool:
return _is_invincible3.2 触摸控制支持
移动端需要触摸控制。我们实现一个虚拟摇杆和自动射击。
| 操作 | 移动端 | PC端 |
|---|---|---|
| 移动 | 手指拖拽 | 方向键/WASD |
| 射击 | 自动射击 | 自动射击 |
| 低速 | 双指触摸 | Shift |
| 炸弹 | 双击屏幕 | X/B |
C
using Godot;
/// <summary>
/// 触摸控制——虚拟摇杆
/// </summary>
public partial class TouchControls : Control
{
private Vector2 _touchStart = Vector2.Zero;
private bool _isDragging = false;
private Vector2 _moveDirection = Vector2.Zero;
// 信号:移动方向变化
[Signal] public delegate void MoveDirectionChangedEventHandler(
Vector2 direction);
// 信号:使用炸弹
[Signal] public delegate void BombUsedEventHandler();
public override void _Ready()
{
// 仅在移动端显示
Visible = OS.GetName() == "Android" || OS.GetName() == "iOS";
}
public override void _Input(InputEvent @event)
{
if (@event is InputEventScreenTouch touchEvent)
{
if (touchEvent.Pressed && !_isDragging)
{
_touchStart = touchEvent.Position;
_isDragging = true;
}
else if (!touchEvent.Pressed && _isDragging)
{
_isDragging = false;
_moveDirection = Vector2.Zero;
EmitSignal(SignalName.MoveDirectionChanged, _moveDirection);
}
}
if (@event is InputEventScreenDrag dragEvent && _isDragging)
{
var offset = dragEvent.Position - _touchStart;
// 死区(防止微小移动就触发)
if (offset.Length() > 10f)
{
_moveDirection = offset.Normalized();
EmitSignal(SignalName.MoveDirectionChanged, _moveDirection);
}
}
}
/// <summary>
/// 获取当前移动方向
/// </summary>
public Vector2 GetMoveDirection() => _moveDirection;
}GDScript
extends Control
## 触摸控制——虚拟摇杆
var _touch_start = Vector2.ZERO
var _is_dragging = bool = false
var _move_direction = Vector2.ZERO
# 信号
signal move_direction_changed(direction: Vector2)
signal bomb_used()
func _ready() -> void:
# 仅在移动端显示
visible = OS.get_name() == "Android" or OS.get_name() == "iOS"
func _input(event: InputEvent) -> void:
if event is InputEventScreenTouch:
var touch = event as InputEventScreenTouch
if touch.pressed and not _is_dragging:
_touch_start = touch.position
_is_dragging = true
elif not touch.pressed and _is_dragging:
_is_dragging = false
_move_direction = Vector2.ZERO
move_direction_changed.emit(_move_direction)
if event is InputEventScreenDrag and _is_dragging:
var drag = event as InputEventScreenDrag
var offset = drag.position - _touch_start
# 死区
if offset.length() > 10.0:
_move_direction = offset.normalized()
move_direction_changed.emit(_move_direction)
## 获取当前移动方向
func get_move_direction() -> Vector2:
return _move_direction3.3 武器等级系统
不同武器等级的子弹布局:
| 等级 | 子弹数量 | 布局 |
|---|---|---|
| 1 | 1发 | 正中间 |
| 2 | 2发 | 左右各一发 |
| 3 | 3发 | 中间+两侧斜射 |
| 4 | 4发 | 双直射+双斜射 |
| 5 | 5发 | 三直射+双斜射 |
3.4 本章小结
| 功能 | 说明 |
|---|---|
| 双速移动 | 正常/低速模式,低速时显示判定点 |
| 自动射击 | 每隔固定时间自动发射子弹 |
| 武器等级 | 1-5级,子弹数量和布局递增 |
| 无敌闪烁 | 被击中后2秒无敌+闪烁效果 |
| 触摸控制 | 移动端虚拟摇杆 |
| 边界限制 | 玩家只能在屏幕下方40%区域内移动 |
下一章我们将实现子弹系统,包括对象池优化。
