在游戏开发中,尤其是FPS或动作类游戏,投掷物(如手雷、烟雾弹、闪光弹)系统是提升战斗策略性和操作手感的关键模块。然而,当项目迭代到新版本,或需要复用、分析一个基于老版本引擎(如Unity 2.0、Unreal Engine 4早期版本)或特定框架(如某些2.0版本的游戏框架)构建的投掷物系统时,开发者常常会面临一系列挑战:代码结构可能不符合现代设计模式,物理交互逻辑耦合严重,触发器(Trigger)的使用方式陈旧,且由于“老版系统未修改”,直接阅读和理解原始代码成为难题。
本文将围绕“基于老版本2.0触发器等操作的投掷物系统”进行深度拆解与重构实战。我们将从一个典型的、结构略显陈旧但功能完整的投掷物系统出发,逐步分析其核心逻辑,特别是触发器(Trigger)在碰撞检测、爆炸范围判断中的关键作用。然后,我们将动手实践,在不破坏原有系统核心流程的前提下,对其进行现代化重构,引入更清晰的状态模式、事件驱动架构,并优化物理与逻辑的分离。无论你是正在维护遗留代码,还是想学习如何从零设计一个健壮的投掷物系统,这篇文章都将提供从原理到代码的完整路径。
1. 投掷物系统核心概念与老版本设计解析
在深入代码之前,我们需要明确几个核心概念,并理解在老版本设计中常见的实现方式。
1.1 投掷物系统的核心组件
一个完整的投掷物系统通常包含以下逻辑链条:
- 投掷行为:玩家输入、投掷力度与角度计算、初始速度赋予。
- 飞行轨迹:受重力、空气阻力(简化)影响的抛物线运动,通常由物理引擎(如Rigidbody)或手动模拟(Transform位移)驱动。
- 碰撞检测:投掷物与场景(地面、墙壁)或其他实体(玩家、敌人)的碰撞。这是触发器(Trigger)大显身手的地方。
- 触发与效果:碰撞或到达预定位置(如定时爆炸)后,触发爆炸、燃烧、烟雾等效果。这包括伤害计算、范围判断、视觉特效(VFX)和音效(SFX)播放。
- 范围判断:对于爆炸类投掷物,需要检测爆炸范围内所有受影响的目标,并应用相应效果(如伤害、眩晕)。触发器常被用于简化这一范围的检测。
1.2 “老版本2.0”系统常见特征与挑战
结合“触发器”和“老版本”等关键词,我们可以推断这类系统可能具备以下特征,这些也是我们重构时需要重点关注的点:
- 高度耦合的MonoBehaviour脚本:所有逻辑(移动、碰撞、伤害计算)可能都塞在一个庞大的
Grenade.cs脚本中,违反了单一职责原则。 - 基于
OnTriggerEnter的简单检测:爆炸范围判断可能直接依赖于一个球形或盒形碰撞体(Collider)设置为触发器(Is Trigger = true),在OnTriggerEnter中直接处理伤害应用。这种方式简单但缺乏灵活性,难以处理如穿透伤害衰减、友军误伤过滤等复杂逻辑。 - 硬编码的参数与效果:爆炸伤害、半径、特效预制体名称可能直接以常量形式写在代码里,难以通过策划配置表进行调整。
- 有限的物理交互:可能仅使用简单的
Rigidbody.AddForce进行投掷,缺乏对复杂地形(水面、斜坡)反弹、滚动等行为的精细控制。 - 状态管理混乱:投掷物的“待投掷”、“飞行中”、“已碰撞”、“爆炸中”、“销毁”等状态可能通过一堆布尔变量(
isThrown,hasCollided,isExploding)来管理,可读性和可维护性差。
理解这些特征是理解旧代码和进行有效重构的第一步。我们的目标不是全盘否定旧系统,而是将其核心逻辑剥离、优化并封装,使其更健壮、易扩展。
2. 环境准备与项目结构说明
为了进行本次实战,我们需要一个基础的开发环境。本例将以Unity引擎为例进行讲解,因为其组件化和物理系统非常适合演示触发器相关的逻辑。许多原理同样适用于其他游戏引擎或框架。
环境准备:
- 引擎/工具:Unity 2021.3 LTS 或更高版本(兼容性较好)。老版本“2.0”可能指代某个具体的项目框架版本,此处我们将其理解为一种设计模式的代称,我们的重构思路是通用的。
- 编程语言:C#。
- IDE:Visual Studio 2022 或 Rider。
- 预期成果:一个结构清晰、可配置、易扩展的投掷物系统,包含手雷(爆炸)、烟雾弹(区域遮蔽)两种类型。
初始项目结构(模拟老版本):在开始重构前,我们先模拟一个典型的老版本投掷物脚本,了解其初始形态。
// 文件:Assets/Scripts/OldGrenadeSystem.cs (模拟的老版本脚本) using UnityEngine; public class OldGrenadeSystem : MonoBehaviour { public float throwForce = 10f; public float explosionRadius = 5f; public int explosionDamage = 100; public GameObject explosionEffect; private Rigidbody rb; private bool hasExploded = false; void Start() { rb = GetComponent<Rigidbody>(); rb.AddForce(Camera.main.transform.forward * throwForce, ForceMode.Impulse); } void OnTriggerEnter(Collider other) { // 老版本常见问题1:碰撞检测条件简单,可能误触发 if (other.CompareTag("Ground") || other.CompareTag("Wall")) { Explode(); } // 老版本常见问题2:可能试图在碰撞时直接伤害玩家,逻辑混乱 // if (other.CompareTag("Player")) { /* ... */ } } void Explode() { if (hasExploded) return; hasExploded = true; // 老版本常见问题3:范围检测与伤害应用耦合严重 Collider[] colliders = Physics.OverlapSphere(transform.position, explosionRadius); foreach (Collider hit in colliders) { // 假设有一个Health组件 Health health = hit.GetComponent<Health>(); if (health != null) { health.TakeDamage(explosionDamage); } } // 实例化特效 if (explosionEffect != null) { Instantiate(explosionEffect, transform.position, Quaternion.identity); } // 销毁自身 Destroy(gameObject); } }这个简单的脚本暴露了之前提到的问题:状态管理脆弱(hasExploded)、碰撞触发逻辑僵硬、爆炸伤害计算与表现逻辑混杂在一起。接下来,我们将对其进行系统性重构。
3. 核心重构:状态模式与事件驱动
重构的第一步是引入明确的状态机来管理投掷物的生命周期,并使用事件来解耦各个系统模块。
3.1 定义投掷物状态枚举
// 文件:Assets/Scripts/ThrowableSystem/Core/ThrowableState.cs namespace ThrowableSystem.Core { public enum ThrowableState { Idle, // 闲置(在背包或手中) Thrown, // 已投出,飞行中 Collided, // 已碰撞场景(未触发效果) Activated, // 效果激活中(如爆炸、烟雾扩散) Finished // 效果结束,等待清理 } }3.2 创建基础的投掷物抽象类
我们将创建一个抽象基类,定义投掷物的通用行为和状态转换骨架。
// 文件:Assets/Scripts/ThrowableSystem/Core/BaseThrowable.cs using UnityEngine; using System; namespace ThrowableSystem.Core { public abstract class BaseThrowable : MonoBehaviour { public event Action<ThrowableState> OnStateChanged; // 状态变更事件 [Header("Base Settings")] [SerializeField] protected float destroyDelayAfterFinish = 2f; protected ThrowableState currentState = ThrowableState.Idle; protected Rigidbody rb; public ThrowableState CurrentState => currentState; protected virtual void Awake() { rb = GetComponent<Rigidbody>(); if (rb == null) { rb = gameObject.AddComponent<Rigidbody>(); rb.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic; // 提高碰撞检测精度 } rb.useGravity = true; rb.isKinematic = false; } /// <summary> /// 外部调用,投掷投掷物 /// </summary> public virtual void Throw(Vector3 direction, float force) { if (currentState != ThrowableState.Idle) return; rb.AddForce(direction * force, ForceMode.Impulse); ChangeState(ThrowableState.Thrown); } /// <summary> /// 改变状态,触发事件 /// </summary> protected void ChangeState(ThrowableState newState) { if (currentState == newState) return; ThrowableState oldState = currentState; currentState = newState; OnStateChanged?.Invoke(newState); // 状态特定的进入逻辑 OnEnterState(newState, oldState); } /// <summary> /// 子类重写此方法以实现状态进入时的具体逻辑 /// </summary> protected virtual void OnEnterState(ThrowableState newState, ThrowableState oldState) { } protected virtual void OnCollisionEnter(Collision collision) { if (currentState == ThrowableState.Thrown) { // 碰撞到场景物体,进入Collided状态 // 这里可以添加碰撞音效、粒子等 ChangeState(ThrowableState.Collided); } } /// <summary> /// 子类需要实现的激活效果方法(如爆炸、释放烟雾) /// </summary> protected abstract void ActivateEffect(); /// <summary> /// 清理资源,销毁对象 /// </summary> protected virtual void CleanUp() { // 可以在这里回收对象到对象池,而不是直接Destroy Destroy(gameObject, destroyDelayAfterFinish); } } }这个基类将投掷物的通用物理行为、状态管理和生命周期框架定义好了。具体的激活效果(ActivateEffect)由子类实现。
4. 实战:重构爆炸手雷(使用触发器进行范围检测)
现在,我们基于新的架构,创建一个爆炸手雷类。我们将展示两种范围检测方式:1) 老版本常用的Physics.OverlapSphere,2) 使用**触发器(Trigger)**进行持续范围检测(适用于需要持续伤害或效果的区域)。
4.1 创建爆炸手雷数据脚本(ScriptableObject)
为了提升可配置性,我们使用ScriptableObject来存储手雷的配置数据。
// 文件:Assets/Scripts/ThrowableSystem/Data/ExplosiveGrenadeData.cs using UnityEngine; namespace ThrowableSystem.Data { [CreateAssetMenu(fileName = "NewGrenadeData", menuName = "Throwable System/Explosive Grenade Data")] public class ExplosiveGrenadeData : ScriptableObject { [Header("Explosion Settings")] public float explosionRadius = 5f; public int baseDamage = 80; public AnimationCurve damageFalloff = AnimationCurve.Linear(0f, 1f, 1f, 0.2f); // 伤害衰减曲线 public LayerMask damageableLayerMask; // 可以受到伤害的层 public LayerMask obstacleLayerMask; // 会阻挡爆炸的层(用于射线检测) [Header("Effects")] public GameObject explosionVFXPrefab; public AudioClip explosionSFX; public float explosionForce = 500f; // 爆炸冲击力 public float upwardsModifier = 0.5f; [Header("Timing")] public float activationDelayAfterCollision = 0.1f; // 碰撞后延迟爆炸(模拟弹跳) public float effectDuration = 0.5f; // 爆炸效果持续时间 } }4.2 实现爆炸手雷逻辑(方法一:瞬时检测)
// 文件:Assets/Scripts/ThrowableSystem/Throwables/ExplosiveGrenade_Instant.cs using ThrowableSystem.Core; using ThrowableSystem.Data; using UnityEngine; namespace ThrowableSystem.Throwables { public class ExplosiveGrenade_Instant : BaseThrowable { [SerializeField] private ExplosiveGrenadeData grenadeData; private float stateTimer = 0f; private bool hasAppliedDamage = false; protected override void OnEnterState(ThrowableState newState, ThrowableState oldState) { base.OnEnterState(newState, oldState); stateTimer = 0f; // 每次状态切换重置计时器 switch (newState) { case ThrowableState.Collided: // 碰撞后,启动一个短暂的延迟再激活爆炸 // 这比老版本直接爆炸更真实,允许手雷弹跳 break; case ThrowableState.Activated: ActivateEffect(); // 进入激活状态,立即执行爆炸效果 break; case ThrowableState.Finished: CleanUp(); break; } } protected override void Update() { base.Update(); stateTimer += Time.deltaTime; switch (currentState) { case ThrowableState.Collided: // 延迟后激活爆炸 if (stateTimer >= grenadeData.activationDelayAfterCollision) { ChangeState(ThrowableState.Activated); } break; case ThrowableState.Activated: // 爆炸效果持续一段时间后结束 if (stateTimer >= grenadeData.effectDuration) { ChangeState(ThrowableState.Finished); } break; } } protected override void ActivateEffect() { if (hasAppliedDamage) return; hasAppliedDamage = true; // 1. 视觉与音效 if (grenadeData.explosionVFXPrefab != null) { Instantiate(grenadeData.explosionVFXPrefab, transform.position, Quaternion.identity); } // AudioSource.PlayClipAtPoint(grenadeData.explosionSFX, transform.position); // 2. 核心:瞬时范围检测与伤害计算(类似老版本,但更优化) ApplyExplosionDamage(); // 3. 物理冲击力(可选) ApplyExplosionForce(); // 4. 隐藏投掷物模型,但暂时不销毁,等状态结束 GetComponent<MeshRenderer>().enabled = false; GetComponent<Collider>().enabled = false; if (rb != null) rb.isKinematic = true; } private void ApplyExplosionDamage() { Collider[] hitColliders = Physics.OverlapSphere(transform.position, grenadeData.explosionRadius, grenadeData.damageableLayerMask); foreach (Collider hitCollider in hitColliders) { // 检查是否有障碍物阻挡(如墙壁后的敌人不受伤害) Vector3 directionToTarget = (hitCollider.transform.position - transform.position).normalized; float distanceToTarget = Vector3.Distance(transform.position, hitCollider.transform.position); if (!Physics.Raycast(transform.position, directionToTarget, distanceToTarget, grenadeData.obstacleLayerMask)) { // 无阻挡,计算伤害 float distanceNormalized = distanceToTarget / grenadeData.explosionRadius; float damageMultiplier = grenadeData.damageFalloff.Evaluate(distanceNormalized); int finalDamage = Mathf.RoundToInt(grenadeData.baseDamage * damageMultiplier); Health health = hitCollider.GetComponent<Health>(); if (health != null) { health.TakeDamage(finalDamage, DamageType.Explosive); } // 还可以在这里触发其他效果,如击退、眩晕等 } } } private void ApplyExplosionForce() { Collider[] hitColliders = Physics.OverlapSphere(transform.position, grenadeData.explosionRadius); foreach (Collider hitCollider in hitColliders) { Rigidbody targetRb = hitCollider.GetComponent<Rigidbody>(); if (targetRb != null) { targetRb.AddExplosionForce(grenadeData.explosionForce, transform.position, grenadeData.explosionRadius, grenadeData.upwardsModifier); } } } } }这个版本已经比老版本强大很多,支持伤害衰减、视线阻挡判断和可配置参数。但它仍然是瞬时检测。
4.3 实现爆炸手雷逻辑(方法二:触发器持续检测)
对于燃烧弹、毒气弹等需要持续作用的投掷物,或者希望爆炸效果在极短时间内有“持续判定”(多帧伤害),可以使用触发器。我们在手雷激活后,生成一个触发器区域来进行持续检测。
首先,创建一个用于触发器区域检测的组件:
// 文件:Assets/Scripts/ThrowableSystem/Core/TriggerDamageZone.cs using UnityEngine; using System.Collections.Generic; namespace ThrowableSystem.Core { public class TriggerDamageZone : MonoBehaviour { public System.Action<Collider> OnTargetEnteredZone; // 目标进入区域事件 public System.Action<Collider> OnTargetExitedZone; // 目标离开区域事件 [SerializeField] private LayerMask targetLayerMask; private HashSet<Collider> targetsInZone = new HashSet<Collider>(); private void OnTriggerEnter(Collider other) { // 检查层掩码 if (((1 << other.gameObject.layer) & targetLayerMask) != 0) { targetsInZone.Add(other); OnTargetEnteredZone?.Invoke(other); } } private void OnTriggerExit(Collider other) { if (targetsInZone.Contains(other)) { targetsInZone.Remove(other); OnTargetExitedZone?.Invoke(other); } } // 获取当前区域内的所有目标 public HashSet<Collider> GetTargetsInZone() { return new HashSet<Collider>(targetsInZone); } // 每帧对区域内目标造成伤害(由外部调用) public void ApplyDamageOverTime(int damagePerTick, DamageType damageType) { foreach (var target in targetsInZone) { Health health = target.GetComponent<Health>(); if (health != null) { health.TakeDamage(damagePerTick, damageType); } } } private void OnDestroy() { targetsInZone.Clear(); } } }然后,修改或创建另一个手雷变体,在激活时生成这个触发器区域:
// 文件:Assets/Scripts/ThrowableSystem/Throwables/IncendiaryGrenade_Trigger.cs using ThrowableSystem.Core; using ThrowableSystem.Data; using UnityEngine; namespace ThrowableSystem.Throwables { public class IncendiaryGrenade_Trigger : BaseThrowable { [SerializeField] private ExplosiveGrenadeData grenadeData; // 复用或创建新的数据 [SerializeField] private GameObject triggerZonePrefab; // 一个带有Sphere Collider (IsTrigger=true)和TriggerDamageZone的空物体预制体 private float stateTimer = 0f; private TriggerDamageZone activeDamageZone; private GameObject zoneGameObject; protected override void OnEnterState(ThrowableState newState, ThrowableState oldState) { base.OnEnterState(newState, oldState); stateTimer = 0f; switch (newState) { case ThrowableState.Activated: ActivateEffect(); break; case ThrowableState.Finished: if (zoneGameObject != null) { Destroy(zoneGameObject); } CleanUp(); break; } } protected override void Update() { base.Update(); stateTimer += Time.deltaTime; switch (currentState) { case ThrowableState.Collided: if (stateTimer >= grenadeData.activationDelayAfterCollision) { ChangeState(ThrowableState.Activated); } break; case ThrowableState.Activated: // 持续效果期间,每帧对触发器区域内的目标造成伤害 if (activeDamageZone != null) { activeDamageZone.ApplyDamageOverTime(10, DamageType.Fire); // 每秒10点火焰伤害 } if (stateTimer >= grenadeData.effectDuration) { ChangeState(ThrowableState.Finished); } break; } } protected override void ActivateEffect() { // 生成视觉特效(火焰区域) if (grenadeData.explosionVFXPrefab != null) { Instantiate(grenadeData.explosionVFXPrefab, transform.position, Quaternion.identity); } // 生成触发器区域 if (triggerZonePrefab != null) { zoneGameObject = Instantiate(triggerZonePrefab, transform.position, Quaternion.identity); activeDamageZone = zoneGameObject.GetComponent<TriggerDamageZone>(); if (activeDamageZone != null) { // 可以在这里配置触发器区域的大小(匹配爆炸半径) SphereCollider zoneCollider = zoneGameObject.GetComponent<SphereCollider>(); if (zoneCollider != null) { zoneCollider.radius = grenadeData.explosionRadius; } // 订阅事件,用于处理目标进入/离开时的特殊效果(如点燃、减速) // activeDamageZone.OnTargetEnteredZone += OnTargetEnteredFire; } } // 隐藏手雷本体 GetComponent<MeshRenderer>().enabled = false; GetComponent<Collider>().enabled = false; if (rb != null) rb.isKinematic = true; } } }这种基于触发器的设计,完美实现了持续性的区域效果,逻辑清晰且易于扩展。
5. 投掷物管理器与使用示例
为了在游戏中方便地创建和使用投掷物,我们可以创建一个简单的管理器。
// 文件:Assets/Scripts/ThrowableSystem/Management/ThrowableManager.cs using ThrowableSystem.Core; using UnityEngine; namespace ThrowableSystem.Management { public class ThrowableManager : MonoBehaviour { public static ThrowableManager Instance { get; private set; } [SerializeField] private Transform throwableSpawnPoint; // 玩家手部或摄像机前方 [SerializeField] private BaseThrowable grenadePrefab; [SerializeField] private BaseThrowable smokeGrenadePrefab; private void Awake() { if (Instance != null && Instance != this) { Destroy(this); } else { Instance = this; } } public BaseThrowable SpawnAndThrow(GameObject throwablePrefab, Vector3 direction, float force) { if (throwablePrefab == null || throwableSpawnPoint == null) return null; GameObject grenadeObj = Instantiate(throwablePrefab, throwableSpawnPoint.position, Quaternion.identity); BaseThrowable throwable = grenadeObj.GetComponent<BaseThrowable>(); if (throwable != null) { throwable.Throw(direction, force); // 可以订阅状态事件,用于UI更新、音效等 throwable.OnStateChanged += HandleThrowableStateChange; } return throwable; } // 示例:投掷标准手雷 public void ThrowGrenade() { SpawnAndThrow(grenadePrefab.gameObject, Camera.main.transform.forward, 15f); } // 示例:投掷烟雾弹 public void ThrowSmokeGrenade() { SpawnAndThrow(smokeGrenadePrefab.gameObject, Camera.main.transform.forward, 12f); } private void HandleThrowableStateChange(ThrowableState newState) { // 根据状态做全局处理,例如:爆炸时屏幕震动 if (newState == ThrowableState.Activated) { // CameraShake.Instance.Shake(0.3f, 0.7f); } } private void Update() { // 示例输入 if (Input.GetKeyDown(KeyCode.G)) { ThrowGrenade(); } if (Input.GetKeyDown(KeyCode.H)) { ThrowSmokeGrenade(); } } } }6. 常见问题与排查思路
在重构或使用新的投掷物系统时,你可能会遇到以下问题:
| 问题现象 | 可能原因 | 排查与解决思路 |
|---|---|---|
| 投掷物不爆炸/不触发效果 | 1. 状态未正确转换到Activated。2. ActivateEffect方法未被调用或提前返回。3. 碰撞体(Collider)未正确设置(IsTrigger? IsKinematic?)。 4. 层(Layer)设置错误,碰撞检测失败。 | 1. 使用Debug.Log输出状态转换日志,检查OnCollisionEnter或延迟计时器是否生效。2. 在 ActivateEffect方法开始处添加日志,确认执行。3. 检查投掷物及其子物体上的所有Collider组件设置,确保非触发器碰撞体能触发 OnCollisionEnter。4. 在Edit -> Project Settings -> Physics中查看碰撞矩阵,确保投掷物层与地面/墙壁层可以交互。 |
| 爆炸伤害无法应用到目标 | 1. 目标物体没有Health组件或标签不匹配。2. 伤害计算逻辑错误(如距离计算、衰减曲线)。 3. 射线遮挡检测过于严格,误判为被阻挡。 4. damageableLayerMask设置错误,未包含目标所在层。 | 1. 给目标物体添加Health组件,并确保其Tag或Layer在检测范围内。2. 使用 Debug.DrawRay或Gizmos.DrawWireSphere可视化爆炸半径和射线,检查距离和遮挡关系。3. 调整 obstacleLayerMask,确保只包含真正会阻挡爆炸的物体(如厚墙),忽略栏杆、玻璃等。4. 在Inspector中仔细检查 ExplosiveGrenadeData资产上的LayerMask设置。 |
| 触发器区域检测不稳定(目标时而在内时而在外) | 1. 目标Collider是动态的且移动速度快,触发器可能在一帧内错过进入/退出事件。 2. 触发器区域Collider的 Rigidbody设置问题(最好挂在带有Kinematic Rigidbody的物体上以保证物理更新)。3. 多线程或帧率问题导致事件丢失(罕见)。 | 1. 考虑在TriggerDamageZone的Update中使用OverlapSphere进行补充检测,而不仅仅依赖事件。2. 确保触发器区域GameObject有一个 Rigidbody组件,并设置为IsKinematic = true。3. 简化逻辑,确保 OnTriggerEnter/Exit中的操作尽可能轻量。 |
| 性能问题(大量投掷物时卡顿) | 1. 每帧在Update或FixedUpdate中进行大量的Physics.OverlapSphere或射线检测。2. 频繁实例化/销毁特效和触发器区域预制体。 3. 未使用对象池管理投掷物实例。 | 1. 优化检测频率,例如爆炸伤害只在激活瞬间计算一次,持续伤害区域可以每0.1-0.2秒检测一次,而不是每帧。 2. **强烈建议使用对象池(Object Pooling)**来管理投掷物、爆炸特效和触发器区域的生成与回收。这是解决性能问题的关键。 3. 使用 Physics.SphereCastNonAlloc或OverlapSphereNonAlloc来避免GC分配。 |
| 老版本代码无法直接替换 | 老系统可能深度耦合了其他游戏系统(如UI、任务、存档)。 | 1.不要一次性重写全部。采用“绞杀者模式”,先在新系统中实现核心功能,通过适配器(Adapter)模式让新系统与老接口兼容。 2. 逐步迁移,例如先让新的手雷类继承老系统的某个接口,在测试场景中验证,再逐步替换游戏中的使用点。 |
7. 最佳实践与工程化建议
将投掷物系统从老版本重构为现代化设计后,遵循以下最佳实践可以确保其长期稳定和易于维护:
- 配置数据驱动:始终坚持使用ScriptableObject或外部配置文件(如JSON、Addressables)来管理投掷物的所有参数。这允许策划人员在不修改代码的情况下调整游戏平衡。
- 对象池是必须的:对于高频创建和销毁的游戏对象(如投掷物、子弹、特效),对象池能极大提升性能。Unity 2021后可以使用内置的
ObjectPool类,或自行实现一个简单的池。 - 逻辑与表现分离:将伤害计算、状态机等核心逻辑与视觉特效(VFX)、音效(SFX)、屏幕抖动等表现层代码分离。可以通过事件(如
OnStateChanged、OnExplosion)来驱动表现,便于单独调试和替换资源。 - 使用Layer和Tag进行高效过滤:合理规划项目的Layer,为“玩家”、“敌人”、“可破坏物”、“地形”等分配不同的层,并在检测时使用
LayerMask进行过滤,避免不必要的物理检测和组件查找。 - 为复杂效果设计独立的组件:例如,“燃烧”效果应该是一个独立的
BurnEffect组件,可以附加到任何单位上,管理燃烧持续时间、每秒伤害和结束回调。这样,手雷只需要调用target.AddComponent<BurnEffect>()或通过一个效果管理器来应用,而不是在手雷脚本里硬编码燃烧逻辑。 - 网络同步考虑(如需):如果项目是多人游戏,投掷物系统需要做权威服务器验证。所有关键的投掷、碰撞、爆炸事件都应由服务器计算并广播给客户端,客户端只负责表现预测和插值。状态机在客户端和服务器端应保持一致。
- 编写单元测试:为核心的状态转换、伤害计算函数编写单元测试。例如,测试在爆炸半径边缘的目标是否受到正确的衰减伤害,测试触发器区域是否正确添加和移除目标。这能有效防止后续修改引入回归错误。
- 提供详细的日志和调试工具:在开发阶段,为投掷物系统添加可开关的调试绘制(如
Gizmos.DrawWireSphere显示爆炸半径)和日志输出。可以创建一个ThrowableDebugger单例来统一管理这些可视化信息。
通过以上步骤,我们成功将一个基于老版本触发器思路的、结构模糊的投掷物系统,重构为一个模块化、可配置、易扩展的现代游戏功能模块。这个新系统不仅解决了老代码的可维护性问题,还为添加更多类型的投掷物(如电磁脉冲弹、侦察无人机)奠定了坚实的基础。记住,重构的核心思想是“分离关注点”和“面向接口编程”,让每一段代码只做好一件事。下次当你面对遗留代码时,不妨从定义清晰的状态和抽取可配置数据开始,一步步将其驯服。