4. 坦克移动与碰撞
2026/4/14大约 11 分钟
4. 坦克大战——坦克移动与碰撞
简介
坦克移动就像你玩遥控车——你按下"前进"按钮,遥控车就往前走;按"左转",车就往左拐。在坦克大战里,坦克只能在四个方向上移动:上、下、左、右。不能斜着走,也不能自由转向。
碰撞检测就是判断"这个坦克会不会穿墙"。想象你在迷宫里走路,前面有堵墙,你走不过去——这就是碰撞。程序需要每时每刻检查坦克是否要撞到墙或其他坦克。
为什么坦克移动要特殊处理?
普通的2D游戏角色可以自由朝任何方向移动,但坦克大战里的坦克有特殊限制:
| 特点 | 说明 | 原因 |
|---|---|---|
| 只能四个方向 | 上、下、左、右 | 经典FC坦克大战的设定 |
| 网格对齐 | 坦克会自动"吸附"到网格线上 | 方便对齐通道,不会卡在两堵墙之间 |
| 速度恒定 | 不能加速减速 | 经典玩法要求精确操控 |
| 转向时先对齐 | 按左右之前先自动对齐行/列 | 防止转弯时卡住 |
网格对齐是坦克大战最重要的移动特性。打个比方:想象你在一个棋盘上移动棋子,棋子只能放在格子的交叉点上,不能放在两个格子之间。坦克也一样——转弯时会自动"滑"到最近的网格线上。
坦克基类
玩家坦克和敌人坦克有很多相同的属性和行为(都能移动、都有生命值、都能发射子弹),所以先写一个基类,然后让玩家坦克和敌人坦克都继承这个基类。
你可以把"基类"理解成一个模板——就像饼干模具,用同一个模具可以压出不同口味的饼干,但形状都一样。
// TankBase.cs - 坦克基类
using Godot;
public partial class TankBase : CharacterBody2D
{
// ========== 常量 ==========
protected const float BASE_SPEED = 120.0f; // 基础移动速度(像素/秒)
protected const float GRID_SIZE = 48.0f; // 网格大小
protected const float ALIGN_THRESHOLD = 4.0f; // 网格对齐阈值(距离网格线多近就自动吸附)
protected const float TANK_SIZE = 40.0f; // 坦克的碰撞体大小
// ========== 属性 ==========
// 当前移动方向
protected GameManager.Direction _moveDirection = GameManager.Direction.Up;
// 移动速度
protected float _speed = BASE_SPEED;
// 坦克是否存活
protected bool _isAlive = true;
// 是否在冰面上(会打滑)
protected bool _isOnIce = false;
// 是否处于保护状态(出生时短暂无敌)
protected bool _isShielded = false;
// ========== 节点引用 ==========
protected Sprite2D _sprite;
protected AnimationPlayer _animPlayer;
protected CollisionShape2D _collisionShape;
protected Area2D _shieldArea;
// ========== 信号 ==========
[Signal] public delegate void TankDestroyedEventHandler();
[Signal] public delegate void DirectionChangedEventHandler(int newDirection);
// ========== 生命周期 ==========
public override void _Ready()
{
_sprite = GetNode<Sprite2D>("Sprite2D");
_animPlayer = GetNode<AnimationPlayer>("AnimationPlayer");
_collisionShape = GetNode<CollisionShape2D>("CollisionShape2D");
// 设置初始朝向
UpdateSpriteDirection();
}
// ========== 移动逻辑 ==========
// 设置移动方向(坦克大战的核心移动方法)
public virtual void SetDirection(GameManager.Direction newDirection)
{
if (!_isAlive) return;
// 如果方向没变,不需要处理
if (newDirection == _moveDirection) return;
// 转向时先对齐到网格
AlignToGrid(newDirection);
// 更新方向
_moveDirection = newDirection;
// 更新精灵朝向
UpdateSpriteDirection();
// 发出方向变化信号
EmitSignal(SignalName.DirectionChanged, (int)newDirection);
}
// 对齐到网格(转向时调用,防止卡墙)
protected void AlignToGrid(GameManager.Direction newDirection)
{
// 如果是水平移动(左/右),需要把Y坐标对齐到网格
if (newDirection == GameManager.Direction.Left ||
newDirection == GameManager.Direction.Right)
{
float nearestGridY = Mathf.Round(Position.Y / GRID_SIZE) * GRID_SIZE;
// 只有距离网格线很近时才对齐
if (Mathf.Abs(Position.Y - nearestGridY) < GRID_SIZE / 2)
{
Position = new Vector2(Position.X, nearestGridY);
}
}
// 如果是垂直移动(上/下),需要把X坐标对齐到网格
if (newDirection == GameManager.Direction.Up ||
newDirection == GameManager.Direction.Down)
{
float nearestGridX = Mathf.Round(Position.X / GRID_SIZE) * GRID_SIZE;
if (Mathf.Abs(Position.X - nearestGridX) < GRID_SIZE / 2)
{
Position = new Vector2(nearestGridX, Position.Y);
}
}
}
// 根据方向更新精灵朝向
protected void UpdateSpriteDirection()
{
// 根据方向旋转精灵
float rotation = _moveDirection switch
{
GameManager.Direction.Up => 0, // 上:不旋转
GameManager.Direction.Down => Mathf.Pi, // 下:旋转180度
GameManager.Direction.Left => -Mathf.Pi / 2, // 左:旋转-90度
GameManager.Direction.Right => Mathf.Pi / 2, // 右:旋转90度
_ => 0
};
_sprite.Rotation = rotation;
}
// 物理帧更新(处理移动和碰撞)
public override void _PhysicsProcess(double delta)
{
if (!_isAlive) return;
// 计算移动向量
Vector2 velocity = GetMovementVelocity();
// 使用 Godot 的物理引擎移动并检测碰撞
MoveAndCollide(velocity * (float)delta);
}
// 获取当前方向的移动向量
protected Vector2 GetMovementVelocity()
{
return _moveDirection switch
{
GameManager.Direction.Up => new Vector2(0, -_speed),
GameManager.Direction.Down => new Vector2(0, _speed),
GameManager.Direction.Left => new Vector2(-_speed, 0),
GameManager.Direction.Right => new Vector2(_speed, 0),
_ => Vector2.Zero
};
}
// ========== 坦克状态 ==========
// 摧毁坦克
public virtual void Destroy()
{
if (!_isAlive) return;
_isAlive = false;
// 播放爆炸动画
if (_animPlayer.HasAnimation("destroy"))
{
_animPlayer.Play("destroy");
}
// 发出信号
EmitSignal(SignalName.TankDestroyed);
}
// 设置保护状态(出生时短暂无敌)
public void SetShielded(bool shielded, float duration = 3.0f)
{
_isShielded = shielded;
var shield = GetNode<Area2D>("Shield");
if (shield != null)
{
shield.Visible = shielded;
}
if (shielded && duration > 0)
{
// 使用定时器自动取消保护
var timer = GetNode<Timer>("ShieldTimer");
timer.WaitTime = duration;
timer.Start();
}
}
// 保护时间结束
private void OnShieldTimerTimeout()
{
SetShielded(false);
}
// ========== 属性访问器 ==========
public GameManager.Direction MoveDirection => _moveDirection;
public bool IsAlive => _isAlive;
public float Speed => _speed;
public bool IsShielded => _isShielded;
}# tank_base.gd - 坦克基类
extends CharacterBody2D
# ========== 常量 ==========
const BASE_SPEED: float = 120.0 # 基础移动速度(像素/秒)
const GRID_SIZE: float = 48.0 # 网格大小
const ALIGN_THRESHOLD: float = 4.0 # 网格对齐阈值
const TANK_SIZE: float = 40.0 # 坦克的碰撞体大小
# ========== 属性 ==========
# 当前移动方向
var move_direction: int = GameManager.Direction.UP
# 移动速度
var speed: float = BASE_SPEED
# 坦克是否存活
var is_alive: bool = true
# 是否在冰面上(会打滑)
var is_on_ice: bool = false
# 是否处于保护状态
var is_shielded: bool = false
# ========== 节点引用 ==========
@onready var sprite: Sprite2D = $Sprite2D
@onready var anim_player: AnimationPlayer = $AnimationPlayer
@onready var collision_shape: CollisionShape2D = $CollisionShape2D
# ========== 信号 ==========
signal tank_destroyed
signal direction_changed(new_direction: int)
# ========== 生命周期 ==========
func _ready() -> void:
# 设置初始朝向
update_sprite_direction()
# ========== 移动逻辑 ==========
## 设置移动方向(坦克大战的核心移动方法)
func set_direction(new_direction: int) -> void:
if not is_alive:
return
# 如果方向没变,不需要处理
if new_direction == move_direction:
return
# 转向时先对齐到网格
align_to_grid(new_direction)
# 更新方向
move_direction = new_direction
# 更新精灵朝向
update_sprite_direction()
# 发出方向变化信号
direction_changed.emit(new_direction)
## 对齐到网格(转向时调用,防止卡墙)
func align_to_grid(new_direction: int) -> void:
# 如果是水平移动(左/右),需要把Y坐标对齐到网格
if new_direction == GameManager.Direction.LEFT or new_direction == GameManager.Direction.RIGHT:
var nearest_grid_y: float = roundf(position.y / GRID_SIZE) * GRID_SIZE
# 只有距离网格线很近时才对齐
if absf(position.y - nearest_grid_y) < GRID_SIZE / 2:
position = Vector2(position.x, nearest_grid_y)
# 如果是垂直移动(上/下),需要把X坐标对齐到网格
if new_direction == GameManager.Direction.UP or new_direction == GameManager.Direction.DOWN:
var nearest_grid_x: float = roundf(position.x / GRID_SIZE) * GRID_SIZE
if absf(position.x - nearest_grid_x) < GRID_SIZE / 2:
position = Vector2(nearest_grid_x, position.y)
## 根据方向更新精灵朝向
func update_sprite_direction() -> void:
# 根据方向旋转精灵
match move_direction:
GameManager.Direction.UP:
sprite.rotation = 0 # 上:不旋转
GameManager.Direction.DOWN:
sprite.rotation = PI # 下:旋转180度
GameManager.Direction.LEFT:
sprite.rotation = -PI / 2 # 左:旋转-90度
GameManager.Direction.RIGHT:
sprite.rotation = PI / 2 # 右:旋转90度
## 物理帧更新(处理移动和碰撞)
func _physics_process(delta: float) -> void:
if not is_alive:
return
# 计算移动向量
var velocity = get_movement_velocity()
# 使用 Godot 的物理引擎移动并检测碰撞
move_and_collide(velocity * delta)
## 获取当前方向的移动向量
func get_movement_velocity() -> Vector2:
match move_direction:
GameManager.Direction.UP:
return Vector2(0, -speed)
GameManager.Direction.DOWN:
return Vector2(0, speed)
GameManager.Direction.LEFT:
return Vector2(-speed, 0)
GameManager.Direction.RIGHT:
return Vector2(speed, 0)
_:
return Vector2.ZERO
# ========== 坦克状态 ==========
## 摧毁坦克
func destroy() -> void:
if not is_alive:
return
is_alive = false
# 播放爆炸动画
if anim_player.has_animation("destroy"):
anim_player.play("destroy")
# 发出信号
tank_destroyed.emit()
## 设置保护状态(出生时短暂无敌)
func set_shielded(shielded: bool, duration: float = 3.0) -> void:
is_shielded = shielded
var shield = get_node_or_null("Shield")
if shield != null:
shield.visible = shielded
if shielded and duration > 0:
# 使用定时器自动取消保护
var timer = get_node("ShieldTimer") as Timer
timer.wait_time = duration
timer.start()
## 保护时间结束
func _on_shield_timer_timeout() -> void:
set_shielded(false)
# ========== 属性访问器 ==========
# 注意:GDScript 没有 C# 的属性语法,直接用变量即可玩家坦克
玩家坦克继承自 TankBase,增加了键盘输入处理和射击功能。
// PlayerTank.cs - 玩家坦克
using Godot;
public partial class PlayerTank : TankBase
{
// ========== 射击相关 ==========
private float _shootCooldown = 0.5f; // 射击冷却时间(秒)
private float _shootTimer = 0.0f; // 射击计时器
private int _bulletLevel = 1; // 子弹等级(1-3)
// 场景引用
private PackedScene _bulletScene;
// ========== 初始化 ==========
public override void _Ready()
{
base._Ready();
// 预加载子弹场景
_bulletScene = GD.Load<PackedScene>("res://scenes/bullets/bullet.tscn");
// 出生时给予3秒保护
SetShielded(true, 3.0f);
// 连接游戏暂停信号
GameManager.Instance.StateChanged += OnGameStateChanged;
}
// ========== 输入处理 ==========
public override void _Process(double delta)
{
if (!_isAlive) return;
if (GameManager.Instance.CurrentState != GameManager.GameState.Playing) return;
// 更新射击冷却
_shootTimer -= (float)delta;
// 读取玩家输入
HandleMovementInput();
HandleShootInput();
}
// 处理移动输入
private void HandleMovementInput()
{
// 注意:同时按两个方向键时,优先响应后按的那个
if (Input.IsActionPressed("move_up"))
{
SetDirection(GameManager.Direction.Up);
}
else if (Input.IsActionPressed("move_down"))
{
SetDirection(GameManager.Direction.Down);
}
else if (Input.IsActionPressed("move_left"))
{
SetDirection(GameManager.Direction.Left);
}
else if (Input.IsActionPressed("move_right"))
{
SetDirection(GameManager.Direction.Right);
}
}
// 处理射击输入
private void HandleShootInput()
{
if (Input.IsActionPressed("shoot") && _shootTimer <= 0)
{
Shoot();
_shootTimer = _shootCooldown;
}
}
// 发射子弹
private void Shoot()
{
if (_bulletScene == null) return;
var bullet = _bulletScene.Instantiate<Bullet>();
bullet.IsPlayerBullet = true;
bullet.BulletLevel = _bulletLevel;
bullet.Position = GetBulletSpawnPosition();
bullet.Direction = _moveDirection;
// 把子弹加到子弹容器中
var container = GetNode("/root/Game/BulletsContainer");
container.AddChild(bullet);
}
// 获取子弹出生位置(坦克炮口的位置)
private Vector2 GetBulletSpawnPosition()
{
// 子弹从坦克前方生成
float offset = TANK_SIZE / 2 + 4; // 坦克半径 + 一点间距
return _moveDirection switch
{
GameManager.Direction.Up => Position + new Vector2(0, -offset),
GameManager.Direction.Down => Position + new Vector2(0, offset),
GameManager.Direction.Left => Position + new Vector2(-offset, 0),
GameManager.Direction.Right => Position + new Vector2(offset, 0),
_ => Position
};
}
// 游戏状态变化
private void OnGameStateChanged(int newState)
{
var state = (GameManager.GameState)newState;
if (state == GameManager.GameState.Paused)
{
// 暂停时停止移动
_speed = 0;
}
else if (state == GameManager.GameState.Playing)
{
// 恢复时恢复速度
_speed = BASE_SPEED;
}
}
// 升级子弹
public void UpgradeBullet()
{
_bulletLevel = Mathf.Min(_bulletLevel + 1, 3);
}
public override void Destroy()
{
base.Destroy();
GameManager.Instance.PlayerDied();
}
}# player_tank.gd - 玩家坦克
extends "res://scripts/tanks/tank_base.gd"
# ========== 射击相关 ==========
var shoot_cooldown: float = 0.5 # 射击冷却时间(秒)
var shoot_timer: float = 0.0 # 射击计时器
var bullet_level: int = 1 # 子弹等级(1-3)
# 场景引用
var bullet_scene: PackedScene
# ========== 初始化 ==========
func _ready() -> void:
super._ready()
# 预加载子弹场景
bullet_scene = load("res://scenes/bullets/bullet.tscn")
# 出生时给予3秒保护
set_shielded(true, 3.0)
# 连接游戏暂停信号
GameManager.state_changed.connect(on_game_state_changed)
# ========== 输入处理 ==========
func _process(delta: float) -> void:
if not is_alive:
return
if GameManager.current_state != GameManager.GameState.PLAYING:
return
# 更新射击冷却
shoot_timer -= delta
# 读取玩家输入
handle_movement_input()
handle_shoot_input()
## 处理移动输入
func handle_movement_input() -> void:
# 注意:同时按两个方向键时,优先响应后按的那个
if Input.is_action_pressed("move_up"):
set_direction(GameManager.Direction.UP)
elif Input.is_action_pressed("move_down"):
set_direction(GameManager.Direction.DOWN)
elif Input.is_action_pressed("move_left"):
set_direction(GameManager.Direction.LEFT)
elif Input.is_action_pressed("move_right"):
set_direction(GameManager.Direction.RIGHT)
## 处理射击输入
func handle_shoot_input() -> void:
if Input.is_action_pressed("shoot") and shoot_timer <= 0:
shoot()
shoot_timer = shoot_cooldown
## 发射子弹
func shoot() -> void:
if bullet_scene == null:
return
var bullet = bullet_scene.instantiate()
bullet.is_player_bullet = true
bullet.bullet_level = bullet_level
bullet.position = get_bullet_spawn_position()
bullet.direction = move_direction
# 把子弹加到子弹容器中
var container = get_node("/root/Game/BulletsContainer")
container.add_child(bullet)
## 获取子弹出生位置
func get_bullet_spawn_position() -> Vector2:
var offset: float = TANK_SIZE / 2 + 4 # 坦克半径 + 一点间距
match move_direction:
GameManager.Direction.UP:
return position + Vector2(0, -offset)
GameManager.Direction.DOWN:
return position + Vector2(0, offset)
GameManager.Direction.LEFT:
return position + Vector2(-offset, 0)
GameManager.Direction.RIGHT:
return position + Vector2(offset, 0)
_:
return position
## 游戏状态变化
func on_game_state_changed(new_state: int) -> void:
if new_state == GameManager.GameState.PAUSED:
speed = 0 # 暂停时停止移动
elif new_state == GameManager.GameState.PLAYING:
speed = BASE_SPEED # 恢复时恢复速度
## 升级子弹
func upgrade_bullet() -> void:
bullet_level = mini(bullet_level + 1, 3)
func destroy() -> void:
super.destroy()
GameManager.player_died()碰撞层设置
在 Godot 中,碰撞是通过碰撞层(Layer)和碰撞遮罩(Mask)来控制的。你可以把碰撞层想象成不同的"频道"——坦克在一个频道,墙壁在另一个频道,子弹又在另一个频道。
| 对象 | 碰撞层 (Layer) | 碰撞遮罩 (Mask) | 说明 |
|---|---|---|---|
| 玩家坦克 | 第2层 | 第1、4、6层 | 与墙壁(1)、其他坦克(6)、边界碰撞 |
| 敌人坦克 | 第3层 | 第1、2、4、6层 | 与墙壁(1)、玩家(2)、其他坦克(6)、边界碰撞 |
| 玩家子弹 | 第5层 | 第1、3、4层 | 与墙壁(1)、敌人(3)、基地周围的墙(4)碰撞 |
| 敌人子弹 | 第7层 | 第1、2、4层 | 与墙壁(1)、玩家(2)、基地周围的墙(4)碰撞 |
| 砖墙 | 第1层 | 无 | 被子弹和坦克检测碰撞 |
| 钢墙 | 第4层 | 无 | 被子弹和坦克检测碰撞 |
场景节点配置
在 Godot 编辑器中,为每个碰撞体设置 Layer 和 Mask:
玩家坦克 (CharacterBody2D)
├── CollisionShape2D
│ Layer: 2 (Tanks)
│ Mask: 1 (Walls), 6 (TankCollision)
│
敌人坦克 (CharacterBody2D)
├── CollisionShape2D
│ Layer: 3 (Enemies)
│ Mask: 1 (Walls), 2 (Tanks), 6 (TankCollision)
│
砖墙 (StaticBody2D)
├── CollisionShape2D
│ Layer: 1 (Walls)
│ Mask: 无地形碰撞处理
坦克碰到不同地形时,行为不同:
// TerrainCollision.cs - 地形碰撞处理
using Godot;
public partial class TerrainCollision : Node
{
// 检查指定位置是否可以通行
public static bool IsPassable(Vector2 position, GameMap gameMap)
{
var gridPos = gameMap.WorldToGrid(position);
var tileType = gameMap.GetTile(gridPos.X, gridPos.Y);
return tileType switch
{
GameMap.TileType.Empty => true, // 空地:可通行
GameMap.TileType.Brick => false, // 砖墙:不可通行
GameMap.TileType.Steel => false, // 钢墙:不可通行
GameMap.TileType.Grass => true, // 草地:可通行(但遮挡视线)
GameMap.TileType.Water => false, // 水面:不可通行
GameMap.TileType.Ice => true, // 冰面:可通行(但会打滑)
_ => false
};
}
// 检查坦克是否在冰面上
public static bool IsOnIce(Vector2 position, GameMap gameMap)
{
var gridPos = gameMap.WorldToGrid(position);
return gameMap.GetTile(gridPos.X, gridPos.Y) == GameMap.TileType.Ice;
}
}# terrain_collision.gd - 地形碰撞处理
extends Node
## 检查指定位置是否可以通行
static func is_passable(position: Vector2, game_map: Node) -> bool:
var grid_pos = game_map.world_to_grid(position)
var tile_type = game_map.get_tile(grid_pos.x, grid_pos.y)
match tile_type:
GameMap.TileType.EMPTY:
return true # 空地:可通行
GameMap.TileType.BRICK:
return false # 砖墙:不可通行
GameMap.TileType.STEEL:
return false # 钢墙:不可通行
GameMap.TileType.GRASS:
return true # 草地:可通行(但遮挡视线)
GameMap.TileType.WATER:
return false # 水面:不可通行
GameMap.TileType.ICE:
return true # 冰面:可通行(但会打滑)
_:
return false
## 检查坦克是否在冰面上
static func is_on_ice(position: Vector2, game_map: Node) -> bool:
var grid_pos = game_map.world_to_grid(position)
return game_map.get_tile(grid_pos.x, grid_pos.y) == GameMap.TileType.ICE下一章预告
坦克可以在地图上移动了!下一章我们将实现子弹系统——坦克发射的子弹如何飞行、如何打碎砖墙、如何被钢墙弹开。
