5. 子弹与碰撞
2026/4/14大约 9 分钟
5. 坦克大战——子弹与碰撞
简介
子弹是坦克的武器。当玩家按下射击键,坦克就会从炮口发射一颗子弹,子弹沿着坦克面朝的方向飞出去。如果子弹飞出屏幕边缘,就自动消失;如果子弹撞到墙壁,要么把墙打碎(砖墙),要么自己消失(钢墙);如果子弹撞到坦克,坦克就会被击中。
碰撞就是"两个东西撞在一起"的判断。在游戏中,子弹和墙壁、子弹和坦克之间都需要做碰撞检测。
为什么子弹系统很关键?
子弹系统是整个游戏的核心交互——没有子弹,坦克就只是移动的盒子。子弹系统需要处理的情况很多:
| 碰撞情况 | 结果 | 说明 |
|---|---|---|
| 玩家子弹打砖墙 | 墙消失,子弹消失 | 砖墙可以被一枪打碎 |
| 玩家子弹打钢墙 | 子弹消失,墙不动 | 钢墙不可破坏(高级子弹除外) |
| 玩家子弹打敌人 | 敌人受伤/死亡,子弹消失 | 消灭敌人得分 |
| 敌人子弹打玩家 | 玩家死亡,子弹消失 | 玩家失去一条命 |
| 子弹飞出屏幕 | 子弹消失 | 防止子弹无限飞行 |
| 两颗子弹相撞 | 两颗子弹都消失 | 可以用来防御 |
| 子弹打基地 | 基地被摧毁,游戏结束 | 保护基地很重要 |
子弹场景结构
在 Godot 中创建子弹场景 scenes/bullets/bullet.tscn:
Bullet (Area2D) # 子弹根节点,用 Area2D 检测碰撞
├── Sprite2D # 子弹图片
├── CollisionShape2D # 碰撞形状(小矩形)
├── VisibilityNotifier2D # 可见性检测(飞出屏幕时销毁)
└── Bullet.cs / bullet.gd # 子弹脚本子弹脚本
// Bullet.cs - 子弹
using Godot;
public partial class Bullet : Area2D
{
// ========== 属性 ==========
// 子弹速度
[Export] public float Speed { get; set; } = 300.0f;
// 子弹方向
public GameManager.Direction Direction { get; set; } = GameManager.Direction.Up;
// 是否是玩家发射的子弹
public bool IsPlayerBullet { get; set; } = false;
// 子弹等级(1-3),等级越高威力越大
public int BulletLevel { get; set; } = 1;
// 子弹大小
private const float BULLET_SIZE = 6.0f;
// ========== 生命周期 ==========
public override void _Ready()
{
// 连接碰撞信号
BodyEntered += OnBodyEntered;
AreaEntered += OnAreaEntered;
// 连接可见性检测信号(飞出屏幕时自动销毁)
var visibility = GetNode<VisibilityNotifier2D>("VisibilityNotifier2D");
visibility.ScreenExited += OnScreenExited;
// 设置初始朝向
UpdateBulletRotation();
}
// 每帧移动子弹
public override void _Process(double delta)
{
// 根据方向移动
Vector2 velocity = Direction 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
};
Position += velocity * (float)delta;
}
// ========== 碰撞处理 ==========
// 与物理体碰撞(墙壁、坦克等)
private void OnBodyEntered(Node2D body)
{
if (body is TileMapLayer tileMap)
{
// 子弹撞到 TileMap(地形)
HandleTileCollision(tileMap);
}
else if (body is StaticBody2D wall)
{
// 子弹撞到静态物体(独立墙壁块)
HandleWallCollision(wall);
}
else if (body is CharacterBody2D tank)
{
// 子弹撞到坦克
HandleTankCollision(tank);
}
}
// 与其他区域碰撞(其他子弹)
private void OnAreaEntered(Area2D area)
{
if (area is Bullet otherBullet)
{
// 两颗子弹相撞,都消失
CreateSmallExplosion(Position);
QueueFree();
}
}
// 飞出屏幕
private void OnScreenExited()
{
QueueFree();
}
// ========== 具体碰撞处理 ==========
// 子弹撞到 TileMap
private void HandleTileCollision(TileMapLayer tileMap)
{
// 获取子弹当前所在的格子坐标
var mapCell = tileMap.LocalToMap(Position);
// 获取该格子的地形数据
var tileData = tileMap.GetCellTileData(mapCell);
if (tileData == null) return;
// 获取自定义数据,判断地形类型
int tileType = tileData.GetCustomData("type").AsInt32();
switch (tileType)
{
case 1: // 砖墙 - 可以破坏
tileMap.EraseCell(mapCell);
CreateSmallExplosion(Position);
QueueFree();
break;
case 2: // 钢墙 - 普通子弹无法破坏
if (BulletLevel >= 3)
{
// 等级3的子弹可以破坏钢墙
tileMap.EraseCell(mapCell);
CreateSmallExplosion(Position);
}
else
{
// 普通子弹被弹开
CreateSparkEffect(Position);
}
QueueFree();
break;
default:
// 其他地形(空地、草地等),子弹穿过
break;
}
}
// 子弹撞到独立墙壁块
private void HandleWallCollision(StaticBody2D wall)
{
if (wall.IsInGroup("brick_wall"))
{
// 砖墙,可以破坏
CreateSmallExplosion(Position);
wall.QueueFree();
}
else if (wall.IsInGroup("steel_wall"))
{
// 钢墙
if (BulletLevel >= 3)
{
CreateSmallExplosion(Position);
wall.QueueFree();
}
else
{
CreateSparkEffect(Position);
}
}
QueueFree();
}
// 子弹撞到坦克
private void HandleTankCollision(CharacterBody2D tank)
{
// 玩家子弹打敌人
if (IsPlayerBullet && tank is EnemyTank enemy)
{
enemy.TakeDamage(BulletLevel);
CreateSmallExplosion(Position);
QueueFree();
}
// 敌人子弹打玩家
else if (!IsPlayerBullet && tank is PlayerTank player)
{
if (!player.IsShielded)
{
player.Destroy();
CreateSmallExplosion(Position);
}
QueueFree();
}
}
// ========== 特效 ==========
// 创建小型爆炸效果
private void CreateSmallExplosion(Vector2 pos)
{
var scene = GD.Load<PackedScene>("res://scenes/effects/explosion_small.tscn");
var effect = scene.Instantiate<Node2D>();
effect.Position = pos;
// 添加到特效容器
var container = GetNode("/root/Game/EffectsContainer");
container.AddChild(effect);
}
// 创建火花效果(打钢墙时)
private void CreateSparkEffect(Vector2 pos)
{
var scene = GD.Load<PackedScene>("res://scenes/effects/spark.tscn");
var effect = scene.Instantiate<Node2D>();
effect.Position = pos;
var container = GetNode("/root/Game/EffectsContainer");
container.AddChild(effect);
}
// 更新子弹旋转角度
private void UpdateBulletRotation()
{
float rotation = Direction switch
{
GameManager.Direction.Up => 0,
GameManager.Direction.Down => Mathf.Pi,
GameManager.Direction.Left => -Mathf.Pi / 2,
GameManager.Direction.Right => Mathf.Pi / 2,
_ => 0
};
Rotation = rotation;
}
}# bullet.gd - 子弹
extends Area2D
# ========== 属性 ==========
## 子弹速度
@export var speed: float = 300.0
## 子弹方向
var direction: int = GameManager.Direction.UP
## 是否是玩家发射的子弹
var is_player_bullet: bool = false
## 子弹等级(1-3),等级越高威力越大
var bullet_level: int = 1
# 子弹大小
const BULLET_SIZE: float = 6.0
# ========== 生命周期 ==========
func _ready() -> void:
# 连接碰撞信号
body_entered.connect(on_body_entered)
area_entered.connect(on_area_entered)
# 连接可见性检测信号(飞出屏幕时自动销毁)
var visibility = get_node("VisibilityNotifier2D") as VisibilityNotifier2D
visibility.screen_exited.connect(on_screen_exited)
# 设置初始朝向
update_bullet_rotation()
## 每帧移动子弹
func _process(delta: float) -> void:
# 根据方向移动
var velocity: Vector2
match direction:
GameManager.Direction.UP:
velocity = Vector2(0, -speed)
GameManager.Direction.DOWN:
velocity = Vector2(0, speed)
GameManager.Direction.LEFT:
velocity = Vector2(-speed, 0)
GameManager.Direction.RIGHT:
velocity = Vector2(speed, 0)
_:
velocity = Vector2.ZERO
position += velocity * delta
# ========== 碰撞处理 ==========
## 与物理体碰撞(墙壁、坦克等)
func on_body_entered(body: Node2D) -> void:
if body is TileMapLayer:
# 子弹撞到 TileMap(地形)
handle_tile_collision(body)
elif body is StaticBody2D:
# 子弹撞到静态物体(独立墙壁块)
handle_wall_collision(body)
elif body is CharacterBody2D:
# 子弹撞到坦克
handle_tank_collision(body)
## 与其他区域碰撞(其他子弹)
func on_area_entered(area: Area2D) -> void:
if area is Bullet:
# 两颗子弹相撞,都消失
create_small_explosion(position)
queue_free()
## 飞出屏幕
func on_screen_exited() -> void:
queue_free()
# ========== 具体碰撞处理 ==========
## 子弹撞到 TileMap
func handle_tile_collision(tile_map: TileMapLayer) -> void:
# 获取子弹当前所在的格子坐标
var map_cell = tile_map.local_to_map(position)
# 获取该格子的地形数据
var tile_data = tile_map.get_cell_tile_data(map_cell)
if tile_data == null:
return
# 获取自定义数据,判断地形类型
var tile_type: int = tile_data.get_custom_data("type")
match tile_type:
1: # 砖墙 - 可以破坏
tile_map.erase_cell(map_cell)
create_small_explosion(position)
queue_free()
2: # 钢墙 - 普通子弹无法破坏
if bullet_level >= 3:
# 等级3的子弹可以破坏钢墙
tile_map.erase_cell(map_cell)
create_small_explosion(position)
else:
# 普通子弹被弹开
create_spark_effect(position)
queue_free()
## 子弹撞到独立墙壁块
func handle_wall_collision(wall: StaticBody2D) -> void:
if wall.is_in_group("brick_wall"):
# 砖墙,可以破坏
create_small_explosion(position)
wall.queue_free()
elif wall.is_in_group("steel_wall"):
# 钢墙
if bullet_level >= 3:
create_small_explosion(position)
wall.queue_free()
else:
create_spark_effect(position)
queue_free()
## 子弹撞到坦克
func handle_tank_collision(tank: CharacterBody2D) -> void:
# 玩家子弹打敌人
if is_player_bullet and tank.is_class("EnemyTank"):
tank.take_damage(bullet_level)
create_small_explosion(position)
queue_free()
# 敌人子弹打玩家
elif not is_player_bullet and tank.is_class("PlayerTank"):
if not tank.is_shielded:
tank.destroy()
create_small_explosion(position)
queue_free()
# ========== 特效 ==========
## 创建小型爆炸效果
func create_small_explosion(pos: Vector2) -> void:
var scene = load("res://scenes/effects/explosion_small.tscn")
var effect = scene.instantiate()
effect.position = pos
var container = get_node("/root/Game/EffectsContainer")
container.add_child(effect)
## 创建火花效果(打钢墙时)
func create_spark_effect(pos: Vector2) -> void:
var scene = load("res://scenes/effects/spark.tscn")
var effect = scene.instantiate()
effect.position = pos
var container = get_node("/root/Game/EffectsContainer")
container.add_child(effect)
## 更新子弹旋转角度
func update_bullet_rotation() -> void:
match direction:
GameManager.Direction.UP:
rotation = 0
GameManager.Direction.DOWN:
rotation = PI
GameManager.Direction.LEFT:
rotation = -PI / 2
GameManager.Direction.RIGHT:
rotation = PI / 2砖墙系统
砖墙是可破坏的。在经典坦克大战中,一颗子弹打掉一个半格(一个砖块由4个小方块组成)。为了简化,我们让一颗子弹打掉一整格砖墙。
砖墙场景
BrickWall (StaticBody2D) # 砖墙壁块
├── Sprite2D # 砖墙图片
└── CollisionShape2D # 碰撞形状
Layer: 1 (Walls)砖墙被击中时的一半效果
如果想要更精细的效果,可以让砖墙被击中后变成"半碎"状态:
// BrickWall.cs - 可破坏的砖墙
using Godot;
public partial class BrickWall : StaticBody2D
{
// 砖墙血量(被打几次才会消失)
private int _health = 2;
// 第0帧:完好的砖墙
// 第1帧:被打过一次的砖墙(有裂纹)
private Sprite2D _sprite;
public override void _Ready()
{
_sprite = GetNode<Sprite2D>("Sprite2D");
}
// 被子弹击中
public void Hit()
{
_health--;
if (_health <= 0)
{
// 砖墙被完全摧毁
QueueFree();
}
else
{
// 砖墙变成有裂纹的状态
_sprite.Frame = 1;
}
}
}# brick_wall.gd - 可破坏的砖墙
extends StaticBody2D
# 砖墙血量(被打几次才会消失)
var health: int = 2
# Sprite2D 节点引用
@onready var sprite: Sprite2D = $Sprite2D
func _ready() -> void:
# 将砖墙加入 "brick_wall" 组
add_to_group("brick_wall")
## 被子弹击中
func hit() -> void:
health -= 1
if health <= 0:
# 砖墙被完全摧毁
queue_free()
else:
# 砖墙变成有裂纹的状态
sprite.frame = 1子弹等级系统
子弹有3个等级,通过获取"星星"道具来升级。等级越高,子弹越强:
| 等级 | 子弹速度 | 能破坏的地形 | 同时在场子弹数 |
|---|---|---|---|
| 等级1(初始) | 300 | 砖墙 | 1颗 |
| 等级2 | 350 | 砖墙 | 2颗 |
| 等级3 | 400 | 砖墙 + 钢墙 | 2颗 |
// PlayerTank 中升级子弹的方法
public void UpgradeBullet()
{
_bulletLevel = Mathf.Min(_bulletLevel + 1, 3);
// 根据等级调整属性
_shootCooldown = _bulletLevel switch
{
1 => 0.5f, // 等级1:0.5秒冷却
2 => 0.4f, // 等级2:0.4秒冷却
3 => 0.3f, // 等级3:0.3秒冷却
_ => 0.5f
};
GD.Print($"子弹升级到等级 {_bulletLevel}");
}# PlayerTank 中升级子弹的方法
func upgrade_bullet() -> void:
bullet_level = mini(bullet_level + 1, 3)
# 根据等级调整属性
match bullet_level:
1:
shoot_cooldown = 0.5 # 等级1:0.5秒冷却
2:
shoot_cooldown = 0.4 # 等级2:0.4秒冷却
3:
shoot_cooldown = 0.3 # 等级3:0.3秒冷却
print("子弹升级到等级 %d" % bullet_level)碰撞检测优化
在一个有大量子弹和墙壁的场景中,碰撞检测可能会影响性能。以下是一些优化技巧:
| 优化方法 | 说明 | 效果 |
|---|---|---|
| 限制同时在场子弹数 | 玩家最多2颗,每个敌人1颗 | 减少碰撞计算次数 |
| 使用 Area2D 而不是 RayCast | Area2D 的碰撞检测更高效 | 降低CPU占用 |
| 飞出屏幕立即销毁 | 用 VisibilityNotifier2D 检测 | 避免处理屏幕外的子弹 |
| 使用对象池 | 子弹不销毁,而是回收复用 | 减少内存分配和GC压力 |
简单的对象池实现
对象池就像一个"子弹仓库"——用过的子弹不是扔掉,而是放回仓库,下次需要时直接从仓库拿,不用重新创建。
// BulletPool.cs - 子弹对象池
using Godot;
using System.Collections.Generic;
public partial class BulletPool : Node
{
// 池子里存放的子弹列表
private readonly List<Bullet> _pool = new();
private PackedScene _bulletScene;
// 池子的最大容量
private const int MAX_POOL_SIZE = 20;
public override void _Ready()
{
_bulletScene = GD.Load<PackedScene>("res://scenes/bullets/bullet.tscn");
}
// 从池子里获取一颗子弹
public Bullet GetBullet()
{
Bullet bullet;
if (_pool.Count > 0)
{
// 池子里有现成的子弹,直接拿
bullet = _pool[0];
_pool.RemoveAt(0);
}
else
{
// 池子里没有,创建新的
bullet = _bulletScene.Instantiate<Bullet>();
}
// 重置子弹状态
bullet.SetProcess(true);
bullet.Visible = true;
return bullet;
}
// 把用完的子弹放回池子
public void ReturnBullet(Bullet bullet)
{
if (_pool.Count >= MAX_POOL_SIZE)
{
// 池子满了,直接销毁
bullet.QueueFree();
return;
}
// 停止处理,隐藏
bullet.SetProcess(false);
bullet.Visible = false;
// 从父节点移除,放回池子
if (bullet.GetParent() != null)
{
bullet.GetParent().RemoveChild(bullet);
}
_pool.Add(bullet);
}
}# bullet_pool.gd - 子弹对象池
extends Node
# 池子里存放的子弹列表
var pool: Array[Bullet] = []
var bullet_scene: PackedScene
# 池子的最大容量
const MAX_POOL_SIZE: int = 20
func _ready() -> void:
bullet_scene = load("res://scenes/bullets/bullet.tscn")
## 从池子里获取一颗子弹
func get_bullet() -> Bullet:
var bullet: Bullet
if pool.size() > 0:
# 池子里有现成的子弹,直接拿
bullet = pool.pop_front()
else:
# 池子里没有,创建新的
bullet = bullet_scene.instantiate()
# 重置子弹状态
bullet.set_process(true)
bullet.visible = true
return bullet
## 把用完的子弹放回池子
func return_bullet(bullet: Bullet) -> void:
if pool.size() >= MAX_POOL_SIZE:
# 池子满了,直接销毁
bullet.queue_free()
return
# 停止处理,隐藏
bullet.set_process(false)
bullet.visible = false
# 从父节点移除,放回池子
if bullet.get_parent() != null:
bullet.get_parent().remove_child(bullet)
pool.append(bullet)下一章预告
子弹和碰撞系统完成了!坦克可以射击,子弹可以打碎砖墙、被钢墙弹开、消灭敌人。下一章我们将实现敌人AI——让敌人坦克会自己移动、会追踪玩家、会瞄准基地。
