Unity 2D弹道系统实战:霰弹枪、追踪与反弹弹幕实现
1. 项目概述从标题拆解核心玩法与实现路径看到这个标题很多Unity 2D游戏开发者尤其是正在制作横版射击、Roguelike或者弹幕游戏的朋友眼睛肯定会一亮。“霰弹枪”和“Boss弹幕”几乎是这类游戏里提升爽感和挑战性的标配。但标题里真正吸引人的是后面那几个词“子弹反弹”和“追踪弹”。这不仅仅是两个功能它们代表了两种截然不同的弹道逻辑组合起来能瞬间让游戏的战斗系统丰富好几个层级。霰弹枪在游戏里通常意味着近距离的高爆发伤害和视觉冲击力它的核心是一次发射多发子弹并形成一个扇形的散布区域。而Boss弹幕则是大规模、有规律的子弹阵列旨在给玩家制造躲避的压力其核心在于子弹的生成逻辑、运动模式与视觉表现。当我们将“子弹反弹”机制引入意味着子弹不再是打出去就消失的一次性消耗品它可能与场景边界、特定障碍物发生碰撞并改变方向这极大地增加了流弹的不可预测性和关卡设计的维度。而“追踪弹”则为Boss战或精英怪增添了智能化的压迫感子弹会“思考”并尝试追击玩家考验玩家的走位和瞬时决策。所以这个项目标题本质上是在挑战我们快速实现一套复合型、可交互的2D弹道系统。它要求我们不仅要处理基础的子弹生成与移动还要深入物理碰撞、向量数学以及简单的AI行为逻辑。下面我们就抛开空泛的理论直接进入实战用代码和思路拆解如何一步步构建这个系统。我会基于最常见的Unity 2D项目设置使用Rigidbody 2D和Collider 2D进行物理交互来展开并提供可直接复用的完整代码块。2. 核心系统设计与基础组件搭建在动手写一行关于霰弹或追踪的代码之前我们必须把地基打牢。这个地基就是一颗普普通通的、能飞出去、能打中东西的子弹。很多教程会忽略这一步的健壮性导致后续添加复杂功能时bug频出。2.1 子弹预制体Bullet Prefab的标准化创建一颗标准的2D子弹预制体通常包含以下组件Sprite Renderer用于显示子弹贴图。一个简单的圆形或方形Sprite即可。Rigidbody 2D这是物理核心。设置Body Type为Kinematic。这里有一个关键抉择为什么用Kinematic而不是Dynamic对于大量高速运动的子弹使用Kinematic体并由脚本完全控制其移动性能更优且运动轨迹更精确避免了物理引擎对高速小物体模拟的不稳定。同时记得勾选Collision Detection为Continuous以防止子弹因速度过快而穿透碰撞体。Collider 2D用于检测碰撞。根据子弹形状选择Circle Collider 2D或Box Collider 2D。重要技巧将Collider的Is Trigger勾选上。这意味着我们将使用触发器Trigger事件来处理伤害逻辑而不是物理碰撞Collision事件。这样做的好处是子弹不会因为物理碰撞而被“弹开”物理反弹我们后面用代码单独控制伤害逻辑和物理反应逻辑可以解耦。自定义脚本如Bullet.cs挂载在上面负责移动、生命周期管理和伤害触发。创建一个名为Bullet.cs的基础脚本这是所有子弹类型的父类或模板。using UnityEngine; public class Bullet : MonoBehaviour { public float speed 10f; // 子弹基础速度 public float lifeTime 2f; // 子弹存活时间避免无限飞行 public int damage 1; // 子弹伤害值 protected Rigidbody2D rb; protected Vector2 moveDirection; protected virtual void Start() { rb GetComponentRigidbody2D(); // 生命周期结束后自动销毁 Destroy(gameObject, lifeTime); } // 初始化子弹方向和速度可由发射器调用 public virtual void Initialize(Vector2 direction) { moveDirection direction.normalized; // 确保方向向量为单位向量 } protected virtual void Update() { // Kinematic模式下的移动控制 if (rb ! null) { rb.velocity moveDirection * speed; } } // 触发器事件处理击中目标 protected virtual void OnTriggerEnter2D(Collider2D other) { // 示例击中“Enemy”标签物体时造成伤害 if (other.CompareTag(Enemy)) { // 这里可以调用敌人的受伤接口例如 // EnemyHealth enemyHealth other.GetComponentEnemyHealth(); // if (enemyHealth ! null) enemyHealth.TakeDamage(damage); Debug.Log($Bullet hit: {other.name}); Destroy(gameObject); // 击中后销毁自身 } // 注意我们暂时不在这里处理反弹反弹逻辑在物理碰撞中处理 } }这个基础类提供了子弹最根本的功能沿指定方向运动、定时自毁、触发伤害。Initialize方法是一个很好的设计它将发射逻辑与子弹自身逻辑分离。2.2 发射器Shooter的抽象与霰弹枪实现发射器负责在正确的位置、以正确的参数生成子弹。我们首先创建一个基础的Shooter脚本然后派生出ShotgunShooter。BaseShooter.cs (可选但推荐)using UnityEngine; public abstract class BaseShooter : MonoBehaviour { public GameObject bulletPrefab; // 子弹预制体 public Transform firePoint; // 子弹生成位置枪口 public abstract void Shoot(Vector2 direction); }ShotgunShooter.cs霰弹枪的核心在于一次生成多颗子弹并为每颗子弹赋予一个在总散布角内略微不同的方向。using UnityEngine; public class ShotgunShooter : BaseShooter { public int pelletCount 5; // 霰弹数量 public float spreadAngle 30f; // 总散布角度度 public override void Shoot(Vector2 direction) { if (bulletPrefab null || firePoint null) { Debug.LogWarning(ShotgunShooter: Bullet prefab or fire point not set!); return; } // 计算散布角的一半和起始角度 float halfSpread spreadAngle / 2f; float angleStep spreadAngle / (pelletCount - 1); // 子弹间的角度间隔 // 如果只有一颗子弹就朝正前方发射 if (pelletCount 1) { SpawnBullet(direction); return; } // 循环生成每一颗霰弹 for (int i 0; i pelletCount; i) { // 计算当前子弹相对于中心方向的偏移角度 // 例如5发总30度间隔7.5度。角度序列为-15, -7.5, 0, 7.5, 15 float currentAngle -halfSpread (angleStep * i); // 将方向向量旋转 currentAngle 度 Quaternion rotation Quaternion.AngleAxis(currentAngle, Vector3.forward); // 绕Z轴旋转 Vector2 pelletDirection rotation * direction; SpawnBullet(pelletDirection); } } private void SpawnBullet(Vector2 dir) { GameObject bulletObj Instantiate(bulletPrefab, firePoint.position, firePoint.rotation); Bullet bullet bulletObj.GetComponentBullet(); if (bullet ! null) { bullet.Initialize(dir); } else { Debug.LogError(Spawned bullet prefab does not have a Bullet component!); } } }关键点解析spreadAngle定义了子弹锥形的宽度。30f是一个比较典型的霰弹枪值。Quaternion.AngleAxis是核心函数它创建一个绕指定轴这里是屏幕朝前的Z轴旋转指定角度的四元数然后用它来旋转原始方向向量。注意事项Instantiate时使用firePoint.rotation作为初始旋转这意味着子弹预制体自身的Sprite朝向需要事先调整好通常是朝右。如果你的美术资源朝向不同可能需要在这里或Initialize方法中额外调整子弹实例的transform.rotation以匹配运动方向这样子弹贴图才会正确朝向飞行方向。将ShotgunShooter挂载到玩家或武器对象上在Update中检测输入如鼠标点击并调用Shoot方法传入发射方向如(Input.mousePosition - Camera.main.WorldToScreenPoint(transform.position)).normalized。你立刻就能看到扇形散射的霰弹效果。3. 高级弹道机制反弹与追踪的实现基础发射系统完成后我们进入标题中最吸引人的部分让子弹“活”起来。3.1 物理反弹Physics-Based Ricochet的实现子弹反弹不是简单的触发器事件它需要与物理碰撞体Collider发生实际的物理碰撞并依据物理法则改变方向。因此我们需要为需要反弹的子弹使用OnCollisionEnter2D事件并可能调整Rigidbody 2D的设置。首先修改子弹预制体将Rigidbody 2D的Body Type暂时改回Dynamic或保持Kinematic但用代码模拟反弹并取消勾选Collider 2D的Is Trigger。我们创建一个新的子弹脚本RicochetBullet.cs它继承自Bullet但重写碰撞行为。更常见的、性能更好的做法是保持Rigidbody 2D为KinematicCollider为Trigger用于伤害检测但同时用射线检测Raycast或物理查询来模拟与墙壁的碰撞反弹。这里介绍基于射线检测的精确反弹方法它不依赖物理引擎的实时碰撞性能可控。RicochetBullet.csusing UnityEngine; public class RicochetBullet : Bullet { public int maxBounces 3; // 最大反弹次数 private int currentBounces 0; public LayerMask bounceLayerMask; // 指定可以反弹的图层如“Wall”、“Ground” protected override void Update() { // 在移动前进行射线检测预测碰撞 float distanceThisFrame speed * Time.deltaTime; RaycastHit2D hit Physics2D.Raycast(transform.position, moveDirection, distanceThisFrame, bounceLayerMask); if (hit.collider ! null) { // 发生碰撞处理反弹 Bounce(hit); // 移动子弹到碰撞点避免穿透 transform.position hit.point; } // 基类移动逻辑使用Kinematic velocity或Transform平移 base.Update(); } private void Bounce(RaycastHit2D hit) { if (currentBounces maxBounces) { Destroy(gameObject); return; } // 计算反射方向入射方向相对于法线的反射 // Vector2.Reflect 是Unity内置函数完美计算反射向量 moveDirection Vector2.Reflect(moveDirection, hit.normal).normalized; // 可选可以在这里添加反弹后的效果如播放声音、火花粒子 // AudioSource.PlayClipAtPoint(bounceSound, transform.position); currentBounces; Debug.Log($Bullet bounced! Remaining: {maxBounces - currentBounces}); } // 重写触发器方法反弹子弹也需要造成伤害 protected override void OnTriggerEnter2D(Collider2D other) { // 例如只对非墙壁层如Enemy, Player造成伤害 if (((1 other.gameObject.layer) bounceLayerMask) 0) { base.OnTriggerEnter2D(other); // 调用基类伤害逻辑 } // 如果是墙壁层什么都不做反弹逻辑已在Update中处理 } }核心原理与避坑指南Vector2.Reflect(inDirection, inNormal)这是实现反弹的数学核心。给定入射方向和碰撞表面的法线向量它返回反射方向。hit.normal是射线检测碰撞点后返回的表面法线它垂直于碰撞表面指向外部。LayerMask的使用这是关键你必须在Unity编辑器中设置图层Layer例如创建一个“Wall”层并将所有墙壁对象的Layer设为“Wall”。然后在子弹的bounceLayerMask属性中选择“Wall”层。这样子弹只会与墙壁反弹而不会与敌人或玩家反弹。常见错误忘记设置LayerMask导致子弹与所有物体发生不可预测的交互。性能考量每一帧对每一颗子弹进行射线检测。如果屏幕上同时存在数百颗反弹子弹可能会有性能压力。优化方法包括使用Physics2D.CircleCast或BoxCast代替Raycast以应对子弹有一定体积的情况或者对子弹进行对象池管理。视觉同步由于我们是在Update中先检测、后移动视觉上可能会有单帧的延迟感。对于高速子弹可以增加Raycast的distance或者使用Physics2D.CircleCast并适当设置半径来提前检测。3.2 智能追踪弹Homing Projectile的实现追踪弹的逻辑与反弹弹完全不同它的目标是动态的。我们需要在每帧更新时调整子弹的飞行方向使其逐渐转向目标。HomingBullet.csusing UnityEngine; public class HomingBullet : Bullet { public Transform target; // 追踪目标 public float rotateSpeed 200f; // 转向速度度/秒 public float homingStartDelay 0.2f; // 发射后开始追踪的延迟避免原地转弯 private float delayTimer 0f; private bool isHomingActive false; protected override void Start() { base.Start(); delayTimer homingStartDelay; // 如果未指定目标尝试寻找标签为“Player”的目标假设子弹是敌人发射的 if (target null) { GameObject player GameObject.FindGameObjectWithTag(Player); if (player ! null) target player.transform; } } protected override void Update() { // 延迟计时 if (delayTimer 0) { delayTimer - Time.deltaTime; if (delayTimer 0) isHomingActive true; } // 执行追踪逻辑 if (isHomingActive target ! null target.gameObject.activeInHierarchy) { Homing(); } // 基类移动逻辑 base.Update(); } private void Homing() { // 计算指向目标的向量 Vector2 directionToTarget (target.position - transform.position).normalized; // 计算当前前进方向与目标方向之间的角度差带符号 float angle Vector2.SignedAngle(moveDirection, directionToTarget); // 根据转向速度限制单帧最大旋转角度 float maxRotationThisFrame rotateSpeed * Time.deltaTime; float rotationThisFrame Mathf.Clamp(angle, -maxRotationThisFrame, maxRotationThisFrame); // 旋转当前移动方向 Quaternion rotation Quaternion.AngleAxis(rotationThisFrame, Vector3.forward); moveDirection rotation * moveDirection; // 可选让子弹的Sprite也旋转朝向移动方向视觉效果更好 float lookAngle Mathf.Atan2(moveDirection.y, moveDirection.x) * Mathf.Rad2Deg; transform.rotation Quaternion.AngleAxis(lookAngle, Vector3.forward); } public void SetTarget(Transform newTarget) { target newTarget; } }实现细节与调参心得Vector2.SignedAngle(from, to)这个函数非常有用它返回从from向量到to向量的有符号角度-180 到 180 度。正数表示需要逆时针旋转负数表示顺时针旋转。这让我们知道应该朝哪个方向转。rotateSpeed这是追踪弹的“灵敏度”。值太低子弹转弯笨拙可能永远追不上灵活的目标值太高子弹会像锁链一样紧紧咬住目标轨迹显得不自然。一个实用的技巧让rotateSpeed随着子弹与目标的距离动态变化距离远时转向慢接近时转向快或者加入一定的随机扰动会让运动轨迹更真实。homingStartDelay极其重要的参数如果没有这个延迟子弹在生成的一瞬间就会立刻转向离它最近的目标。如果发射点离目标很近子弹可能会做出一个180度的急转弯甚至原地打转视觉效果非常糟糕。0.2-0.5秒的延迟可以让子弹先沿着初始方向“冲”出去一段距离再开始追踪看起来更像是发射后有了一段初始惯性。目标丢失处理当前脚本在目标被销毁或失活后会停止追踪并直线飞行。你可以根据需要增强逻辑例如寻找新目标或在一定时间后自毁。4. Boss弹幕系统的模式化设计Boss弹幕不是随机乱射它是有规律的、有模式的甚至是美丽的弹幕游戏常称其为“弹幕艺术”。实现弹幕的关键在于将发射行为模式化和参数化。我们不再一颗颗地发射子弹而是通过协程Coroutine或定时器按照特定规律批量生成。4.1 弹幕模式抽象与环形弹幕实现我们设计一个BossPatternController来管理多种弹幕模式。首先定义一个弹幕模式的基类。BossPattern.cs (抽象基类)using UnityEngine; using System.Collections; public abstract class BossPattern : MonoBehaviour { public GameObject bulletPrefab; public Transform[] firePoints; // 多个发射点 public float patternDuration 5f; // 该模式持续时间 public float timeBetweenShots 0.1f; // 每次发射间隔 protected bool isPatternRunning false; public IEnumerator ExecutePattern() { isPatternRunning true; float timer 0f; while (timer patternDuration) { ShootPattern(); // 由子类实现具体的发射逻辑 yield return new WaitForSeconds(timeBetweenShots); timer timeBetweenShots; } isPatternRunning false; } protected abstract void ShootPattern(); }RingPattern.cs (环形弹幕)using UnityEngine; public class RingPattern : BossPattern { public int bulletsPerRing 12; // 一圈子弹数量 public float ringSpeed 5f; protected override void ShootPattern() { if (bulletPrefab null || firePoints.Length 0) return; foreach (Transform firePoint in firePoints) { for (int i 0; i bulletsPerRing; i) { // 计算圆环上每个子弹的方向 float angle i * (360f / bulletsPerRing); Quaternion rotation Quaternion.AngleAxis(angle, Vector3.forward); Vector2 direction rotation * firePoint.right; // 假设firePoint.right是初始前向 GameObject bulletObj Instantiate(bulletPrefab, firePoint.position, Quaternion.identity); Bullet bullet bulletObj.GetComponentBullet(); if (bullet ! null) { bullet.speed ringSpeed; bullet.Initialize(direction); } } } } }BossPatternController.csusing UnityEngine; using System.Collections.Generic; public class BossPatternController : MonoBehaviour { public ListBossPattern patterns; public float timeBetweenPatterns 2f; private int currentPatternIndex 0; private bool isCycling false; void Start() { if (patterns.Count 0) { StartCoroutine(CyclePatterns()); } } IEnumerator CyclePatterns() { isCycling true; while (isCycling) { // 执行当前模式 BossPattern currentPattern patterns[currentPatternIndex]; yield return StartCoroutine(currentPattern.ExecutePattern()); // 模式间隔 yield return new WaitForSeconds(timeBetweenPatterns); // 切换到下一个模式 currentPatternIndex (currentPatternIndex 1) % patterns.Count; } } // 可以从外部调用强制切换或停止模式 public void StopAllPatterns() { StopAllCoroutines(); isCycling false; } }4.2 组合与进阶反弹追踪弹幕现在让我们把之前的高级弹道组合起来创造一个让玩家头皮发麻的Boss技能发射会追踪、碰到墙壁还会反弹的弹幕。你只需要创建一个新的子弹预制体挂载上同时包含HomingBullet和RicochetBullet功能的脚本或者创建一个继承两者的新脚本。然后在Boss的弹幕模式中使用这个预制体。HomingRicochetBullet.cs (组合功能示例)using UnityEngine; public class HomingRicochetBullet : HomingBullet // 继承自追踪弹 { // 复用反弹逻辑需要重写Update方法以整合两者 public int maxBounces 2; private int currentBounces 0; public LayerMask bounceLayerMask; private Vector2 previousPosition; // 用于更精确的射线检测 protected override void Start() { base.Start(); previousPosition rb.position; } protected override void Update() { // 1. 处理反弹检测基于上一帧到当前位置的线段 Vector2 currentPosition rb.position; Vector2 moveVector currentPosition - previousPosition; float distance moveVector.magnitude; if (distance 0.01f) // 避免零向量 { RaycastHit2D hit Physics2D.Raycast(previousPosition, moveDirection, distance, bounceLayerMask); if (hit.collider ! null currentBounces maxBounces) { moveDirection Vector2.Reflect(moveDirection, hit.normal).normalized; currentBounces; // 重置目标反弹后可以取消追踪或者切换追踪目标增加随机性 // target null; } } previousPosition currentPosition; // 2. 处理追踪调用基类HomingBullet的Update base.Update(); // 这里面包含了延迟判断和Homing逻辑 } // 注意OnTriggerEnter2D需要小心处理避免与反弹逻辑冲突 protected override void OnTriggerEnter2D(Collider2D other) { // 可能只对非墙壁层且非友军的单位造成伤害 if (target ! null other.transform target) { base.OnTriggerEnter2D(other); } // 其他情况比如碰到其他敌人可以忽略或触发其他效果 } }设计思路这种子弹首先会直线飞行一小段homingStartDelay然后开始追踪玩家。如果中途碰到墙壁它会根据物理法则反弹反弹后继续追踪。maxBounces限制了它的反弹次数避免一颗子弹在封闭空间里无限弹跳。你可以通过调整rotateSpeed和maxBounces来创造不同难度的弹幕高转向多反弹 高难度追踪弹幕低转向无反弹 慢速导向弹幕。5. 性能优化、调试与实战心得当屏幕上同时有数十个发射器、上百颗带有复杂逻辑的子弹时性能问题会立刻凸显。此外调试弹道和碰撞也是一大挑战。5.1 对象池Object Pooling必备的性能优化手段绝对不要频繁地Instantiate和Destroy子弹。对象池是解决这个问题的标准答案。它的核心思想是游戏开始时预先创建一堆子弹对象并禁用它们需要时从池中取用并激活子弹完成任务后不是销毁而是禁用并放回池中。简易对象池实现BulletPool.csusing System.Collections.Generic; using UnityEngine; public class BulletPool : MonoBehaviour { public static BulletPool Instance; // 单例模式方便全局访问 public GameObject bulletPrefab; public int poolSize 50; private QueueGameObject bulletPool new QueueGameObject(); void Awake() { if (Instance null) { Instance this; } else { Destroy(gameObject); } InitializePool(); } void InitializePool() { for (int i 0; i poolSize; i) { GameObject bullet Instantiate(bulletPrefab); bullet.SetActive(false); bulletPool.Enqueue(bullet); } } public GameObject GetBullet() { if (bulletPool.Count 0) { GameObject bullet bulletPool.Dequeue(); bullet.SetActive(true); return bullet; } else { // 池空了动态扩容可选 GameObject bullet Instantiate(bulletPrefab); return bullet; } } public void ReturnBullet(GameObject bullet) { bullet.SetActive(false); bulletPool.Enqueue(bullet); } }然后修改你的Shooter和BossPattern脚本不再使用Instantiate而是调用BulletPool.Instance.GetBullet()。在子弹的OnTriggerEnter2D或OnBecameInvisible当子弹飞出摄像机视口方法中不再调用Destroy而是调用BulletPool.Instance.ReturnBullet(gameObject)。注意事项对象池中的子弹被复用前必须重置其状态这包括位置、旋转、速度、生命值、反弹计数、追踪目标等所有脚本变量。最好在子弹脚本中创建一个ResetBullet()方法在OnEnable从池中取出时和OnDisable放回池中时调用。5.2 可视化调试与Gizmos调试弹道尤其是反弹和追踪逻辑光靠看运行效果是不够的。Unity的Gizmos和Debug.DrawRay是你的眼睛。绘制子弹方向线在Bullet类的OnDrawGizmos或OnDrawGizmosSelected中private void OnDrawGizmosSelected() { Gizmos.color Color.red; Gizmos.DrawRay(transform.position, moveDirection * 2); // 绘制2个单位长度的方向线 }绘制射线检测在RicochetBullet的Update中在Raycast调用后Debug.DrawRay(transform.position, moveDirection * distanceThisFrame, Color.yellow); if (hit.collider ! null) { Debug.DrawLine(hit.point, hit.point hit.normal, Color.green); // 绘制法线 }绘制追踪目标线在HomingBullet的Update中if (target ! null) { Debug.DrawLine(transform.position, target.position, Color.cyan); }在Scene视图中这些彩色的线会清晰地展示出子弹的意图、碰撞点和追踪关系对于调整参数和排查“为什么子弹不转弯”、“为什么反弹角度奇怪”这类问题有奇效。5.3 参数调优与手感打磨数值设计直接决定了游戏的“手感”。霰弹枪pelletCount弹片数、spreadAngle散布角、speed速度、damage单颗伤害。你需要平衡高伤害、高散布、低射速的“喷子”感还是较低伤害、低散布、高射速的“连喷”感。反弹弹maxBounces反弹次数、物理材质的Bounciness如果你用物理碰撞、speed衰减系数每次反弹后速度是否降低。一个有趣的设定是反弹后伤害增加或附加特效。追踪弹rotateSpeed转向速度、homingStartDelay启动延迟、追踪目标更新频率可以每N帧计算一次而不是每帧以节省性能。让追踪带有一定的预测性计算目标速度朝其未来位置引导或加入随机扰动可以大大增加躲避的趣味性而不是简单的“被锁死”。弹幕timeBetweenShots发射频率、patternDuration模式时长、子弹的speed和加速度。好的弹幕不是无法躲避的墙而是有节奏、有缝隙的“舞蹈”。通过改变这些参数你可以设计出“扩散圆”、“自机狙”、“螺旋线”等经典弹幕图案。最后所有这些系统都需要放在一起进行大量测试。观察在不同帧率下子弹的运动是否平滑对象池在压力测试下是否会导致卡顿复杂弹幕是否会导致DrawCall激增。使用Unity的Profiler工具监控CPU和GPU时间确保你的弹幕盛宴不仅是华丽的也是流畅的。记住最酷的技术效果永远是为最终的游戏体验服务的。

相关新闻