4. 子弹系统
2026/4/14大约 8 分钟
4. 雷霆战机——子弹系统
4.1 为什么需要对象池?
在射击游戏中,子弹会不断地被创建和销毁。想象你每秒发射8发子弹,打一分钟就是480发。如果每发子弹都new一个新对象然后销毁,会给垃圾回收器(GC)造成很大压力,导致游戏卡顿。
对象池(Object Pool) 就是解决这个问题的方案:预先创建一批子弹对象放在"池子"里,需要用的时候从池子里取一个,用完了放回去,而不是销毁。
就像图书馆的书:你借一本来看(从池中取出),看完了还回去(放回池中),而不是每次都买一本新的看完就扔掉。
| 方式 | 创建480发子弹 | 内存影响 | 性能影响 |
|---|---|---|---|
| 每次new+销毁 | 480次创建+480次销毁 | 频繁GC | 帧率波动 |
| 对象池 | 0次创建(预分配200个) | 稳定 | 稳定 |
4.2 玩家子弹
玩家子弹的特点:速度快、直线飞行、击中敌人后消失。
C
using Godot;
/// <summary>
/// 玩家子弹
/// </summary>
public partial class PlayerBullet : Area2D
{
private Vector2 _direction;
private float _speed;
private int _damage = 1;
private bool _active = false;
// 离开屏幕自动回收
public override void _Process(double delta)
{
if (!_active) return;
Position += _direction * _speed * (float)delta;
// 超出屏幕范围就标记为不活跃
if (Position.Y < -20f || Position.Y > GameConfig.ScreenHeight + 20f
|| Position.X < -20f || Position.X > GameConfig.ScreenWidth + 20f)
{
Deactivate();
}
}
/// <summary>
/// 初始化子弹
/// </summary>
public void Initialize(Vector2 pos, Vector2 direction, float speed)
{
GlobalPosition = pos;
_direction = direction.Normalized();
_speed = speed;
_active = true;
Visible = true;
ProcessMode = ProcessModeEnum.Always;
}
/// <summary>
/// 设置伤害值
/// </summary>
public void SetDamage(int damage)
{
_damage = damage;
}
public int GetDamage() => _damage;
/// <summary>
/// 销毁子弹(交给对象池回收)
/// </summary>
public void Destroy()
{
Deactivate();
}
/// <summary>
/// 停用子弹(不销毁,放回池中)
/// </summary>
public void Deactivate()
{
_active = false;
Visible = false;
ProcessMode = ProcessModeEnum.Disabled;
}
/// <summary>
/// 检查子弹是否活跃
/// </summary>
public bool IsActive() => _active;
}GDScript
extends Area2D
## 玩家子弹
var _direction: Vector2
var _speed: float
var _damage: int = 1
var _active: bool = false
func _process(delta: float) -> void:
if not _active:
return
position += _direction * _speed * delta
# 超出屏幕范围就停用
if position.y < -20.0 or position.y > GameConfig.SCREEN_HEIGHT + 20.0 \
or position.x < -20.0 or position.x > GameConfig.SCREEN_WIDTH + 20.0:
deactivate()
## 初始化子弹
func initialize(pos: Vector2, direction: Vector2, speed: float) -> void:
global_position = pos
_direction = direction.normalized()
_speed = speed
_active = true
visible = true
process_mode = Node.PROCESS_MODE_ALWAYS
## 设置伤害值
func set_damage(damage: int) -> void:
_damage = damage
func get_damage() -> int:
return _damage
## 销毁子弹
func destroy() -> void:
deactivate()
## 停用子弹
func deactivate() -> void:
_active = false
visible = false
process_mode = Node.PROCESS_MODE_DISABLED
## 检查子弹是否活跃
func is_active() -> bool:
return _active4.3 敌人子弹
敌人子弹比玩家子弹复杂一些,因为弹幕游戏需要多种弹幕模式。
C
using Godot;
/// <summary>
/// 敌人子弹——支持多种弹幕模式
/// </summary>
public partial class EnemyBullet : Area2D
{
// 弹幕类型
public enum BulletType
{
Straight, // 直线弹
Aimed, // 瞄准弹(朝玩家方向)
Circular, // 圆形弹
Spiral, // 螺旋弹
Homing // 追踪弹
}
private Vector2 _velocity;
private float _speed;
private float _lifetime = 0f;
private bool _active = false;
private BulletType _type;
// 追踪弹参数
private Area2D _target;
private float _homingStrength = 0.5f;
public override void _Process(double delta)
{
if (!_active) return;
_lifetime += (float)delta;
switch (_type)
{
case BulletType.Straight:
Position += _velocity * (float)delta;
break;
case BulletType.Aimed:
Position += _velocity * (float)delta;
break;
case BulletType.Spiral:
// 螺旋弹:速度方向缓慢旋转
float angle = _velocity.Angle();
angle += 1.5f * (float)delta;
_velocity = new Vector2(
Mathf.Cos(angle), Mathf.Sin(angle)) * _speed;
Position += _velocity * (float)delta;
break;
case BulletType.Homing:
// 追踪弹:缓慢转向目标
if (_target != null && _lifetime < 3f)
{
var toTarget = (_target.GlobalPosition - GlobalPosition)
.Normalized();
_velocity = _velocity.Lerp(
toTarget * _speed,
_homingStrength * (float)delta);
}
Position += _velocity * (float)delta;
break;
}
// 超出屏幕或超过存活时间
if (_lifetime > GameConfig.EnemyBulletLife)
Deactivate();
if (Position.Y < -30f || Position.Y > GameConfig.ScreenHeight + 30f
|| Position.X < -30f || Position.X > GameConfig.ScreenWidth + 30f)
Deactivate();
}
/// <summary>
/// 初始化直线弹
/// </summary>
public void InitStraight(Vector2 pos, Vector2 direction, float speed)
{
GlobalPosition = pos;
_velocity = direction.Normalized() * speed;
_speed = speed;
_type = BulletType.Straight;
Activate();
}
/// <summary>
/// 初始化瞄准弹
/// </summary>
public void InitAimed(Vector2 pos, Vector2 targetPos, float speed)
{
GlobalPosition = pos;
var dir = (targetPos - pos).Normalized();
_velocity = dir * speed;
_speed = speed;
_type = BulletType.Aimed;
Activate();
}
/// <summary>
/// 初始化圆形弹
/// </summary>
public void InitCircular(Vector2 pos, float angle, float speed)
{
GlobalPosition = pos;
_velocity = new Vector2(
Mathf.Cos(angle), Mathf.Sin(angle)) * speed;
_speed = speed;
_type = BulletType.Circular;
Activate();
}
/// <summary>
/// 初始化追踪弹
/// </summary>
public void InitHoming(
Vector2 pos, Area2D target, float speed, float homingStrength)
{
GlobalPosition = pos;
_target = target;
_speed = speed;
_homingStrength = homingStrength;
_type = BulletType.Homing;
// 初始方向:朝向目标
var dir = (target.GlobalPosition - pos).Normalized();
_velocity = dir * speed;
Activate();
}
private void Activate()
{
_active = true;
_lifetime = 0f;
Visible = true;
ProcessMode = ProcessModeEnum.Always;
}
public void Deactivate()
{
_active = false;
Visible = false;
ProcessMode = ProcessModeEnum.Disabled;
}
public void Destroy() => Deactivate();
public bool IsActive() => _active;
}GDScript
extends Area2D
## 敌人子弹——支持多种弹幕模式
# 弹幕类型
enum BulletType { STRAIGHT, AIMED, CIRCULAR, SPIRAL, HOMING }
var _velocity: Vector2
var _speed: float
var _lifetime: float = 0.0
var _active: bool = false
var _type: int
# 追踪弹参数
var _target: Area2D
var _homing_strength: float = 0.5
func _process(delta: float) -> void:
if not _active:
return
_lifetime += delta
match _type:
BulletType.STRAIGHT, BulletType.AIMED, BulletType.CIRCULAR:
position += _velocity * delta
BulletType.SPIRAL:
# 螺旋弹:速度方向缓慢旋转
var angle = _velocity.angle()
angle += 1.5 * delta
_velocity = Vector2(cos(angle), sin(angle)) * _speed
position += _velocity * delta
BulletType.HOMING:
# 追踪弹
if _target != null and _lifetime < 3.0:
var to_target = (_target.global_position - global_position).normalized()
_velocity = _velocity.lerp(to_target * _speed, _homing_strength * delta)
position += _velocity * delta
# 超出屏幕或超过存活时间
if _lifetime > GameConfig.ENEMY_BULLET_LIFE:
deactivate()
if position.y < -30.0 or position.y > GameConfig.SCREEN_HEIGHT + 30.0 \
or position.x < -30.0 or position.x > GameConfig.SCREEN_WIDTH + 30.0:
deactivate()
## 初始化直线弹
func init_straight(pos: Vector2, direction: Vector2, speed: float) -> void:
global_position = pos
_velocity = direction.normalized() * speed
_speed = speed
_type = BulletType.STRAIGHT
_activate()
## 初始化瞄准弹
func init_aimed(pos: Vector2, target_pos: Vector2, speed: float) -> void:
global_position = pos
var dir = (target_pos - pos).normalized()
_velocity = dir * speed
_speed = speed
_type = BulletType.AIMED
_activate()
## 初始化圆形弹
func init_circular(pos: Vector2, angle: float, speed: float) -> void:
global_position = pos
_velocity = Vector2(cos(angle), sin(angle)) * speed
_speed = speed
_type = BulletType.CIRCULAR
_activate()
## 初始化追踪弹
func init_homing(pos: Vector2, target: Area2D, speed: float, homing: float) -> void:
global_position = pos
_target = target
_speed = speed
_homing_strength = homing
_type = BulletType.HOMING
var dir = (target.global_position - pos).normalized()
_velocity = dir * speed
_activate()
func _activate() -> void:
_active = true
_lifetime = 0.0
visible = true
process_mode = Node.PROCESS_MODE_ALWAYS
func deactivate() -> void:
_active = false
visible = false
process_mode = Node.PROCESS_MODE_DISABLED
func destroy() -> void:
deactivate()
func is_active() -> bool:
return _active4.4 子弹对象池
C
using Godot;
using System.Collections.Generic;
/// <summary>
/// 子弹对象池——避免频繁创建和销毁
/// </summary>
public partial class BulletPool : Node
{
private Node2D _container;
private PackedScene _bulletScene;
private Queue<Area2D> _pool = new Queue<Area2D>();
private List<Area2D> _active = new List<Area2D>();
private int _poolSize;
public override void _Ready()
{
_container = GetParent<Node2D>();
}
/// <summary>
/// 初始化对象池
/// </summary>
public void Initialize(PackedScene scene, int size)
{
_bulletScene = scene;
_poolSize = size;
for (int i = 0; i < size; i++)
{
var bullet = scene.Instantiate<Area2D>();
bullet.ProcessMode = ProcessModeEnum.Disabled;
bullet.Visible = false;
_container.AddChild(bullet);
_pool.Enqueue(bullet);
}
}
/// <summary>
/// 从池中获取一个子弹
/// </summary>
public Area2D GetBullet()
{
Area2D bullet;
if (_pool.Count > 0)
{
bullet = _pool.Dequeue();
}
else
{
// 池子空了,创建新的(或回收最老的)
GD.Print("警告:子弹池已空,创建新子弹");
bullet = _bulletScene.Instantiate<Area2D>();
_container.AddChild(bullet);
}
_active.Add(bullet);
return bullet;
}
/// <summary>
/// 回收子弹到池中
/// </summary>
public void ReturnBullet(Area2D bullet)
{
if (_active.Remove(bullet))
{
bullet.ProcessMode = ProcessModeEnum.Disabled;
bullet.Visible = false;
_pool.Enqueue(bullet);
}
}
/// <summary>
/// 清除所有活跃子弹(使用炸弹时调用)
/// </summary>
public void ClearAll()
{
foreach (var bullet in _active)
{
bullet.ProcessMode = ProcessModeEnum.Disabled;
bullet.Visible = false;
_pool.Enqueue(bullet);
}
_active.Clear();
}
/// <summary>
/// 获取当前活跃子弹数量
/// </summary>
public int ActiveCount => _active.Count;
}GDScript
extends Node
## 子弹对象池——避免频繁创建和销毁
var _container: Node2D
var _bullet_scene: PackedScene
var _pool: Array = []
var _active: Array = []
var _pool_size: int
func _ready() -> void:
_container = get_parent()
## 初始化对象池
func initialize(scene: PackedScene, size: int) -> void:
_bullet_scene = scene
_pool_size = size
for i in range(size):
var bullet = scene.instantiate()
bullet.process_mode = Node.PROCESS_MODE_DISABLED
bullet.visible = false
_container.add_child(bullet)
_pool.append(bullet)
## 从池中获取一个子弹
func get_bullet() -> Area2D:
var bullet: Area2D
if _pool.size() > 0:
bullet = _pool.pop_front()
else:
print("警告:子弹池已空")
bullet = _bullet_scene.instantiate()
_container.add_child(bullet)
_active.append(bullet)
return bullet
## 回收子弹到池中
func return_bullet(bullet: Area2D) -> void:
var idx = _active.find(bullet)
if idx >= 0:
_active.remove_at(idx)
bullet.process_mode = Node.PROCESS_MODE_DISABLED
bullet.visible = false
_pool.append(bullet)
## 清除所有活跃子弹(使用炸弹时调用)
func clear_all() -> void:
for bullet in _active:
bullet.process_mode = Node.PROCESS_MODE_DISABLED
bullet.visible = false
_pool.append(bullet)
_active.clear()
## 获取当前活跃子弹数量
func get_active_count() -> int:
return _active.size()4.5 碰撞检测优化
在大量子弹的情况下,逐个检查碰撞会很慢。优化方法:
- 空间分区:把屏幕分成网格,只检查同一网格或相邻网格中的对象
- 简化碰撞体:子弹用圆形碰撞体(计算快)
- 分层检测:先做粗略的距离筛选,再做精确碰撞
C
/// <summary>
/// 优化的碰撞检测——使用距离预筛选
/// </summary>
public static class CollisionHelper
{
/// <summary>
/// 快速检测两个圆形是否重叠
/// 比Godot内置碰撞体检测更快
/// </summary>
public static bool CircleOverlap(
Vector2 posA, float radiusA,
Vector2 posB, float radiusB)
{
float distSq = (posA - posB).LengthSquared();
float radiusSum = radiusA + radiusB;
return distSq < radiusSum * radiusSum;
}
/// <summary>
/// 批量碰撞检测——玩家子弹vs所有敌人
/// 使用距离预筛选减少精确检测次数
/// </summary>
public static void CheckBulletsVsEnemies(
List<Area2D> bullets,
List<Area2D> enemies,
float bulletRadius,
float enemyRadius,
System.Action<Area2D, Area2D> onHit)
{
// 预筛选:跳过屏幕外的子弹
float screenBottom = GameConfig.ScreenHeight + 20f;
foreach (var bullet in bullets)
{
if (!bullet.Visible) continue;
if (bullet.GlobalPosition.Y < -20f
|| bullet.GlobalPosition.Y > screenBottom)
continue;
foreach (var enemy in enemies)
{
if (!enemy.Visible) continue;
if (CircleOverlap(
bullet.GlobalPosition, bulletRadius,
enemy.GlobalPosition, enemyRadius))
{
onHit(bullet, enemy);
break; // 一发子弹只能命中一个敌人
}
}
}
}
}GDScript
## 优化的碰撞检测
class_name CollisionHelper
## 快速检测两个圆形是否重叠
static func circle_overlap(
pos_a: Vector2, radius_a: float,
pos_b: Vector2, radius_b: float
) -> bool:
var dist_sq = (pos_a - pos_b).length_squared()
var radius_sum = radius_a + radius_b
return dist_sq < radius_sum * radius_sum
## 批量碰撞检测
static func check_bullets_vs_enemies(
bullets: Array, enemies: Array,
bullet_radius: float, enemy_radius: float,
on_hit: Callable
) -> void:
var screen_bottom = GameConfig.SCREEN_HEIGHT + 20.0
for bullet in bullets:
if not bullet.visible:
continue
if bullet.global_position.y < -20.0 \
or bullet.global_position.y > screen_bottom:
continue
for enemy in enemies:
if not enemy.visible:
continue
if circle_overlap(
bullet.global_position, bullet_radius,
enemy.global_position, enemy_radius
):
on_hit.call(bullet, enemy)
break4.6 本章小结
| 组件 | 说明 |
|---|---|
| 对象池 | 预分配子弹,避免频繁创建销毁 |
| 玩家子弹 | 直线飞行,击中敌人后回收 |
| 敌人子弹 | 支持直线、瞄准、圆形、螺旋、追踪5种模式 |
| 碰撞优化 | 圆形碰撞预筛选,跳过屏幕外对象 |
关键设计点:
- 所有子弹离开屏幕后自动回收
- 对象池预分配200个子弹,基本够用
- 追踪弹有最大追踪时间,防止永远追着玩家
- 碰撞检测使用距离平方比较,避免开根号运算
下一章我们将实现敌人波次系统。
