公司动态

Unity捕鱼游戏开发实战:鱼群AI与碰撞检测核心技术解析

📅 2026/8/5 23:01:49
Unity捕鱼游戏开发实战:鱼群AI与碰撞检测核心技术解析
1. 项目概述与核心价值最近几年休闲游戏市场持续火热其中捕鱼类游戏凭借其简单的操作、绚丽的特效和即时的反馈一直占据着重要的份额。很多刚入行或者想拓展技能树的开发者都会把做一个自己的捕鱼游戏作为练手项目。但真动起手来你会发现从一条鱼怎么“游”得自然到一颗子弹怎么“打”得准确中间隔着不少坑。网上教程要么只讲动画要么只讲碰撞很难找到一个把整个流程串起来、并且把“为什么这么做”讲清楚的实战指南。这正是我写这篇指南的初衷。它不是一份简单的功能罗列文档而是我基于多个上线项目复盘后提炼出的一套从零到一构建捕鱼游戏核心玩法的开发心法。我们将聚焦两个最核心也最体现技术功底的模块鱼群的智能游动与子弹的精确碰撞。你会看到一个看似随机的鱼群背后是行为树与状态机的精妙配合一次爽快的命中反馈离不开物理引擎与自定义碰撞检测的深度优化。无论你是Unity新手想找个综合项目练手还是有一定经验的开发者想优化自己的游戏手感这篇文章都能给你带来可以直接“抄作业”的代码和更重要的——设计思路。2. 开发环境准备与项目框架搭建工欲善其事必先利其器。一个清晰的项目结构是高效开发的基础也能避免后期陷入“屎山”代码的困境。2.1 Unity版本与关键Package选择我强烈推荐使用Unity 2022.3 LTS版本。长期支持版意味着更高的稳定性对于商业项目或深度学习来说至关重要。避免使用过于前沿的版本以免遇到未知的兼容性问题。在Package Manager中有几个包是必装的2D Sprite/Universal RP如果你的捕鱼游戏是2D风格确保2D Sprite包已安装。如果是3D或者2.5D2D精灵在3D空间中可以考虑使用轻量级的Universal RP通用渲染管线来获得更好的光影和后期效果控制。Input System新的输入系统。它比传统的Input类更强大、更灵活尤其适合处理多平台PC、手机的输入适配。我们将用它来处理玩家的开火操作。Cinemachine虽然捕鱼游戏镜头通常固定但Cinemachine的虚拟相机能非常方便地实现击中特效时的轻微震动、镜头跟随等电影化效果极大提升表现力。Unity UI用于构建游戏内的UI如分数、金币、炮台等级等。注意在导入这些Package时特别是Input SystemUnity可能会提示你启用新的输入后端并禁用旧的。请务必按照提示操作并在Player Settings中确认输入系统已正确切换否则代码无法工作。2.2 项目目录结构设计一个合理的文件夹结构能让你和你的队友未来的你快速找到任何资源。以下是我常用的结构你可以直接参考Assets/ ├── 01_Scenes/ // 存放所有场景文件 ├── 02_Scripts/ // 所有C#脚本 │ ├── Core/ // 游戏管理器、数据管理器等核心单例 │ ├── Fish/ // 鱼相关的脚本生成、移动、行为 │ ├── Bullet/ // 子弹相关的脚本发射、移动、碰撞 │ ├── Player/ // 玩家控制、炮台逻辑 │ ├── UI/ // UI控制脚本 │ └── Utilities/ // 工具类、扩展方法 ├── 03_Prefabs/ // 预制体按类型分子文件夹 ├── 04_Art/ // 美术资源Sprites, Models, Animations等 ├── 05_Audio/ // 音效和背景音乐 ├── 06_Materials/ // 材质球 ├── 07_PhysicsMaterials // 物理材质 └── 08_Resources/ // 需要动态加载的资源谨慎使用关键点解析Scripts文件夹按功能模块划分而不是按“场景”或“对象”划分。这是因为“鱼”的行为逻辑可能很复杂集中管理便于维护和复用。Prefabs文件夹也同理一个Fish子文件夹里可能包含“小鱼”、“中鱼”、“大鱼”等不同预制体。2.3 核心管理类GameManager与DataManager在开始具体功能前我们先搭建两个基石性的管理器。GameManager.cs这是一个单例类负责游戏的整体流程控制如游戏开始、暂停、结束、分数更新等。它就像游戏的大脑中枢。using UnityEngine; public class GameManager : MonoBehaviour { public static GameManager Instance { get; private set; } public int CurrentScore { get; private set; } public System.Actionint OnScoreUpdated; // 事件用于UI更新 private void Awake() { if (Instance ! null Instance ! this) { Destroy(this.gameObject); } else { Instance this; DontDestroyOnLoad(this.gameObject); // 跨场景不销毁 } CurrentScore 0; } public void AddScore(int value) { CurrentScore value; OnScoreUpdated?.Invoke(CurrentScore); // 触发分数更新事件 Debug.Log($得分增加 {value}, 当前总分: {CurrentScore}); } // 其他方法游戏开始/结束逻辑等 }DataManager.cs或使用ScriptableObject负责管理游戏配置数据。将鱼的血量、速度、分数子弹的伤害、速度等数据从代码中剥离出来用ScriptableObject或JSON文件管理。这样做的好处是策划可以独立调整数值无需程序员重新编译游戏。例如创建一个FishDataScriptableObject[CreateAssetMenu(fileName NewFishData, menuName GameData/FishData)] public class FishData : ScriptableObject { public string fishName; public int hp; // 生命值 public int scoreValue; // 击落后获得的分数 public float moveSpeed; // 移动速度基数 public float size; // 用于碰撞检测的尺寸 // 可以添加更多如对应的预制体、死亡特效等 }然后在DataManager中持有一个FishData的数组或字典根据鱼的种类来获取相应数据。这种方式让数值平衡变得非常高效。3. 鱼群智能游动系统实现让鱼“活”起来是捕鱼游戏沉浸感的第一关。我们不仅要让鱼动还要让它们动得自然、有群体感。3.1 基础移动与动画控制首先为鱼创建预制体。通常包含以下组件SpriteRenderer显示鱼的图片。Animator控制鱼的游动动画摆尾、张嘴等。Rigidbody2D(如果2D) /Rigidbody(如果3D)用于物理移动和碰撞。这里我们选择Rigidbody2D并设置Body Type为Kinematic。为什么用Kinematic因为鱼的移动我们希望完全通过代码控制而不是完全交由物理引擎模拟Dynamic这样可以避免不必要的物理计算性能更好控制也更精确。同时它仍然能参与碰撞检测。CircleCollider2D/PolygonCollider2D碰撞体用于被子弹击中。一个自定义的FishController脚本。FishController的基础移动非常简单public class FishController : MonoBehaviour { public FishData data; private Rigidbody2D rb; private Vector2 moveDirection; public float currentSpeed; void Start() { rb GetComponentRigidbody2D(); currentSpeed data.moveSpeed; // 初始化一个随机方向 moveDirection Random.insideUnitCircle.normalized; } void FixedUpdate() { // 使用Rigidbody的MovePosition进行移动更加平滑 Vector2 newPosition rb.position moveDirection * currentSpeed * Time.fixedDeltaTime; rb.MovePosition(newPosition); // 根据移动方向旋转鱼身让鱼头朝向移动方向 if (moveDirection ! Vector2.zero) { float angle Mathf.Atan2(moveDirection.y, moveDirection.x) * Mathf.Rad2Deg; rb.rotation angle; } } }3.2 高级鱼群行为状态机与行为树如果所有鱼都只是直线游动游戏会非常枯燥。我们需要更复杂的行为遇到边界转向、被惊吓时加速逃离、与其他鱼保持距离避免重叠、偶尔改变方向等。一个清晰的方法是使用状态机State Machine。我们可以定义鱼的几个状态游动Swim、逃离Flee、死亡Die。public enum FishState { Swim, Flee, Die } public class FishController : MonoBehaviour { // ... 之前的基础变量 private FishState currentState FishState.Swim; private Transform threatSource; // 威胁源如靠近的子弹 void Update() { switch (currentState) { case FishState.Swim: UpdateSwimState(); break; case FishState.Flee: UpdateFleeState(); break; case FishState.Die: // 播放死亡动画等待销毁 break; } } void UpdateSwimState() { // 1. 边界检测与转向 if (IsApproachingBorder()) { moveDirection CalculateReflectionDirection(); } // 2. 随机改变方向增加不可预测性 if (Random.Range(0f, 1f) 0.005f) // 每帧很小概率 { moveDirection (moveDirection Random.insideUnitCircle * 0.3f).normalized; } // 3. 简单的群体分离避免堆叠 AvoidOtherFish(); } void UpdateFleeState() { if (threatSource null) { currentState FishState.Swim; return; } // 计算远离威胁源的方向 Vector2 fleeDirection (transform.position - threatSource.position).normalized; moveDirection Vector2.Lerp(moveDirection, fleeDirection, 0.1f); // 平滑转向 currentSpeed data.moveSpeed * 2.0f; // 逃离时加速 // 逃离一段时间或威胁消失后返回游动状态 // ... 计时逻辑 } public void TriggerFlee(Transform threat) { if (currentState FishState.Die) return; currentState FishState.Flee; threatSource threat; } }对于更复杂、更多变的行为比如鱼群整体队形变换可以考虑引入轻量级的行为树Behavior Tree插件或自己实现一个简单的版本。但对于大多数捕鱼游戏一个精心设计的状态机加上一些辅助行为如上面UpdateSwimState中的分离、聚集已经完全够用。3.3 鱼群生成器与动态难度鱼不能只出现一次。我们需要一个FishSpawner来持续、智能地生成鱼群。public class FishSpawner : MonoBehaviour { public ListFishData fishTypes; // 可生成的鱼类型 public Transform[] spawnPoints; // 生成区域通常是屏幕外的几个点 public float baseSpawnInterval 2.0f; private float timer; void Update() { timer - Time.deltaTime; if (timer 0) { SpawnFish(); timer baseSpawnInterval Random.Range(-0.5f, 0.5f); // 加入随机性 } } void SpawnFish() { // 1. 根据当前分数/难度决定生成鱼的种类和数量 FishData fishToSpawn ChooseFishBasedOnDifficulty(); int fishCount Random.Range(1, 4); // 可能生成小鱼群 for (int i 0; i fishCount; i) { // 2. 随机选择一个生成点 Transform sp spawnPoints[Random.Range(0, spawnPoints.Length)]; // 3. 实例化鱼预制体 GameObject fishObj Instantiate(fishToSpawn.prefab, sp.position, Quaternion.identity); FishController fish fishObj.GetComponentFishController(); fish.data fishToSpawn; // 4. 可选为鱼群设置一个共同的初始移动方向 if (fishCount 1) { // ... 设置相似的移动方向形成群体感 } } } FishData ChooseFishBasedOnDifficulty() { // 简单的加权随机算法 // 随着游戏进行提高大鱼高分数、高血量的生成权重 float totalWeight 0; Listfloat weights new Listfloat(); foreach (var fish in fishTypes) { float weight fish.scoreValue * (1 GameManager.Instance.CurrentScore * 0.001f); // 分数越高大鱼权重越高 weights.Add(weight); totalWeight weight; } float randomPoint Random.Range(0, totalWeight); for (int i 0; i fishTypes.Count; i) { if (randomPoint weights[i]) return fishTypes[i]; randomPoint - weights[i]; } return fishTypes[0]; } }实操心得生成点的位置设计很有讲究。通常设置在屏幕四边之外并且让鱼从屏幕一侧游向另一侧。可以设计多种游动路线如从左上到右下从右到左水平等让鱼群的出场方式更丰富。动态难度是留住玩家的关键通过ChooseFishBasedOnDifficulty这样的函数让游戏体验张弛有度。4. 子弹发射与碰撞检测系统捕鱼游戏的另一大核心就是“捕”的体验。子弹的发射、飞行轨迹、与鱼的碰撞每一个环节都直接影响手感。4.1 炮台控制与子弹发射首先实现玩家控制。我们使用新的Input System。创建Input Actions在项目中右键 Create - Input Actions命名为PlayerControls。打开后创建一个Action Map叫Gameplay在里面添加一个Action命名为Fire绑定鼠标左键Mouse/leftButton和手机屏幕触摸Touch/primaryTouch。编写PlayerControllerusing UnityEngine; using UnityEngine.InputSystem; public class PlayerController : MonoBehaviour { public Transform cannonPivot; // 炮台旋转的支点 public GameObject bulletPrefab; public Transform firePoint; // 子弹生成位置 public float fireCooldown 0.2f; private float currentCooldown; private Camera mainCamera; private PlayerInput playerInput; private InputAction fireAction; void Start() { mainCamera Camera.main; playerInput GetComponentPlayerInput(); fireAction playerInput.actions[Fire]; } void Update() { // 1. 炮台瞄准跟随鼠标或触摸位置 AimCannon(); // 2. 冷却计时 if (currentCooldown 0) { currentCooldown - Time.deltaTime; } // 3. 检测开火输入 if (fireAction.IsPressed() currentCooldown 0) { FireBullet(); currentCooldown fireCooldown; } } void AimCannon() { // 获取鼠标或触摸在世界空间中的位置假设是2D游戏Z轴固定 Vector2 screenPos Mouse.current.position.ReadValue(); // 如果是手机可以用 Touchscreen.current.primaryTouch.position.ReadValue() Vector3 worldPos mainCamera.ScreenToWorldPoint(new Vector3(screenPos.x, screenPos.y, mainCamera.nearClipPlane)); worldPos.z 0; // 锁定Z轴 // 计算炮台指向目标的方向 Vector2 direction (worldPos - cannonPivot.position).normalized; // 计算旋转角度 float angle Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg; // 应用旋转 cannonPivot.rotation Quaternion.Euler(0, 0, angle); } void FireBullet() { if (bulletPrefab null || firePoint null) return; GameObject bullet Instantiate(bulletPrefab, firePoint.position, firePoint.rotation); BulletController bc bullet.GetComponentBulletController(); if (bc ! null) { bc.SetDirection(firePoint.right); // 假设炮台向右为初始方向 } // 播放开火音效、特效等 // AudioManager.Instance.PlaySound(Fire); // Instantiate(muzzleFlashPrefab, firePoint.position, firePoint.rotation); } }4.2 子弹物理与运动轨迹子弹预制体通常包含SpriteRenderer、Rigidbody2DBody Type设为Kinematic或Dynamic根据需求和BulletController脚本。运动方式选择直线运动最简单通过Rigidbody2D.MovePosition或直接修改Transform.position实现。抛物线运动模拟重力需要给子弹一个初始速度并受到向下的重力加速度。这更适合一些特殊的“炮弹”武器。追踪运动子弹发射后会自动追踪最近的鱼。这需要每帧计算方向对性能有一定影响慎用。这里以最常用的直线运动为例public class BulletController : MonoBehaviour { public float speed 20f; public int damage 1; public float lifeTime 5f; // 避免子弹永远存在 private Vector2 direction; private Rigidbody2D rb; private float spawnTime; void Start() { rb GetComponentRigidbody2D(); spawnTime Time.time; } public void SetDirection(Vector2 dir) { direction dir.normalized; // 让子弹 sprite 也朝向移动方向 float angle Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg; transform.rotation Quaternion.Euler(0, 0, angle); } void FixedUpdate() { // 移动 Vector2 newPos rb.position direction * speed * Time.fixedDeltaTime; rb.MovePosition(newPos); // 生命周期检查 if (Time.time - spawnTime lifeTime) { Destroy(gameObject); } } }4.3 碰撞检测的三种方式与深度优化这是捕鱼游戏性能与手感的关键。Unity提供了多种碰撞检测方式我们需要根据场景选择。方式一Unity物理引擎碰撞OnTriggerEnter2D/OnCollisionEnter2D这是最常用、最简单的方式。在子弹和鱼上都有Collider2D并至少有一个是Trigger并挂载Rigidbody2D。// 在 BulletController 中 void OnTriggerEnter2D(Collider2D other) { FishController fish other.GetComponentFishController(); if (fish ! null) { // 命中鱼 OnHitFish(fish); // 子弹命中后通常消失或播放命中特效 Destroy(gameObject); } else if (other.CompareTag(Boundary)) { // 击中边界屏幕边缘也销毁 Destroy(gameObject); } } void OnHitFish(FishController fish) { // 这里调用鱼的受伤方法传递伤害值 fish.TakeDamage(damage); // 播放命中音效和特效 // Instantiate(hitEffectPrefab, transform.position, Quaternion.identity); }方式二射线检测Raycast适用于需要极高性能或特殊判定如激光、穿透弹的场景。在子弹每一帧向前发射一条很短的射线检测是否击中目标。void FixedUpdate() { float distanceThisFrame speed * Time.fixedDeltaTime; RaycastHit2D hit Physics2D.Raycast(transform.position, direction, distanceThisFrame, fishLayerMask); if (hit.collider ! null) { FishController fish hit.collider.GetComponentFishController(); if (fish ! null) { OnHitFish(fish); } Destroy(gameObject); } // ... 移动逻辑 }方式三自定义几何检测如圆形、矩形在极简2D游戏或需要非常特定判定规则时使用。例如判断子弹一个点是否在鱼的碰撞范围内。// 在 FishController 的 Update 中 void CheckBulletHits() { // 假设有一个全局的子弹管理器记录了所有活跃子弹 foreach (var bullet in BulletManager.Instance.activeBullets) { float distance Vector2.Distance(transform.position, bullet.transform.position); if (distance myCollider.radius) // 假设是圆形碰撞体 { TakeDamage(bullet.damage); bullet.OnHit(); } } }性能优化实战分层碰撞矩阵Layer Collision Matrix在Edit - Project Settings - Physics 2D中精心设置哪些层Layer之间会发生碰撞。确保子弹只和“Fish”层碰撞鱼只和“Bullet”层以及“Boundary”层碰撞。这是减少不必要的物理计算最有效的手段之一。使用对象池Object Pool子弹和鱼的生成与销毁非常频繁。频繁的Instantiate和Destroy会引发GC垃圾回收导致卡顿。对象池预先创建一批对象使用时激活不用时禁用并放回池中。简化碰撞体尽量使用CircleCollider2D或BoxCollider2D避免使用复杂的PolygonCollider2D除非形状非常不规则。复杂的碰撞体计算开销大。按需检测对于距离玩家很远的鱼可以降低其碰撞检测的频率例如每2帧检测一次或者直接禁用其碰撞体直到它们进入屏幕一定范围。踩坑记录曾经在一个项目里没有设置碰撞矩阵导致上千颗子弹和上百条鱼之间两两进行碰撞检测游戏瞬间掉到10帧以下。设置好碰撞层后帧率直接回到60。对象池的引入又将频繁开火时的瞬时卡顿消除了。这两个优化是捕鱼类游戏的必做项。5. 伤害计算、反馈与游戏循环击中之后如何让玩家感到“爽”这关乎伤害计算、视觉和听觉反馈。5.1 伤害系统与鱼的生命周期在FishController中添加受伤和死亡逻辑public class FishController : MonoBehaviour { // ... 已有变量 private int currentHp; void Start() { currentHp data.hp; // ... } public void TakeDamage(int damage) { if (currentState FishState.Die) return; currentHp - damage; // 显示伤害数字UI DamagePopupManager.Instance.ShowPopup(transform.position, damage); // 受伤反馈闪烁、音效 StartCoroutine(FlashRed()); if (currentHp 0) { Die(); } else { // 受伤但未死可能触发逃离状态 // TriggerFlee(...); } } IEnumerator FlashRed() { SpriteRenderer sr GetComponentSpriteRenderer(); Color original sr.color; sr.color Color.red; yield return new WaitForSeconds(0.1f); sr.color original; } void Die() { currentState FishState.Die; // 1. 停止所有移动和行为 // 2. 播放死亡动画 // 3. 生成死亡特效和金币/分数掉落物 Instantiate(data.deathEffectPrefab, transform.position, Quaternion.identity); // 4. 加分 GameManager.Instance.AddScore(data.scoreValue); // 5. 播放死亡音效 // AudioManager.Instance.PlaySound(FishDie); // 6. 延迟销毁或放回对象池 Destroy(gameObject, 0.5f); // 或者 FishPool.Instance.ReturnToPool(this); } }5.2 命中反馈与特效系统命中反馈是游戏感的灵魂。视觉除了鱼的闪烁子弹命中点应生成一个爆炸或水花特效。使用Particle System粒子系统来实现。可以创建不同大小的特效预制体根据子弹威力或鱼的大小来实例化。听觉不同的命中声音打中小鱼、大鱼、打空非常重要。使用AudioSource和AudioClip并通过一个AudioManager单例类来统一播放避免场景中存在过多AudioSource。屏幕震动击中大鱼时配合一个轻微的屏幕震动能极大增强打击感。可以用Cinemachine的Impulse Source或者写一个简单的相机抖动协程。// 简单的相机抖动 public IEnumerator ShakeCamera(float duration, float magnitude) { Vector3 originalPos mainCamera.transform.localPosition; float elapsed 0.0f; while (elapsed duration) { float x Random.Range(-1f, 1f) * magnitude; float y Random.Range(-1f, 1f) * magnitude; mainCamera.transform.localPosition new Vector3(x, y, originalPos.z); elapsed Time.deltaTime; yield return null; } mainCamera.transform.localPosition originalPos; } // 在击中大鱼时调用StartCoroutine(ShakeCamera(0.1f, 0.05f));5.3 游戏循环与数值平衡一个基本的捕鱼游戏循环是发射子弹 - 消耗金币 - 击中鱼 - 获得金币/分数 - 升级炮台 - 挑战更大的鱼。你需要设计几个关键数值曲线炮台升级每级提升伤害、射速、或子弹速度。升级所需金币呈指数增长。鱼的血量与奖励鱼的HP和其奖励分数应成非线性正比。例如一条血量是2倍的鱼奖励可能是3倍鼓励玩家挑战高价值目标。子弹成本每次发射消耗的金币数。确保高级炮台发射虽然消耗大但击杀高效率鱼的净收益更高。将这些数值放在ScriptableObject或配置表中方便反复调整。测试时重点关注“正反馈循环”——玩家是否感觉自己的成长炮台升级有效以及击杀的爽感是否足够。6. 高级优化与扩展思路当核心玩法跑通后可以考虑以下进阶内容来提升游戏品质。6.1 性能深度优化策略Draw Call合并确保鱼的Sprite使用同一张图集Sprite Atlas。Unity的Sprite Atlas功能可以将大量小图打包成一张大图从而将多次绘制调用合并为一次这是提升2D游戏渲染性能最有效的方法之一。LODLevel of Detail对于距离屏幕中心很远、或者特别小的鱼可以使用分辨率更低的Sprite甚至减少其动画帧率。逻辑更新频率分离不是所有鱼都需要每帧更新复杂的行为逻辑。可以将鱼分为“活跃区”和“非活跃区”只对屏幕内及附近的鱼进行高频率更新远处的鱼降低更新频率如每秒更新几次位置。避免在Update中使用Find、GetComponent这些函数开销较大。在Start或Awake中缓存引用。6.2 网络功能与数据持久化如果你想做排行榜或者云存档排行榜可以使用Unity的SocialAPI如GameCenter, Google Play Games或接入第三方SDK。核心是在玩家得分时提交分数到排行榜服务。数据存档使用PlayerPrefs存储本地数据金币、炮台等级。对于复杂数据可以序列化为JSON或Binary格式保存。注意PlayerPrefs容易被修改重要数据应考虑加密或使用服务器验证。6.3 扩展玩法设计特殊技能冰冻、全屏炸弹、锁定追踪等丰富游戏策略。Boss战定时出现巨型Boss鱼有独特的行动模式和多个弱点部位击败后获得大量奖励。任务系统每日任务、成就系统提供长期游玩目标。多种场景与背景不同的海底场景搭配不同的鱼群保持视觉新鲜感。开发捕鱼游戏是一个系统工程从一条鱼的游动到一片弹幕的碰撞每一个细节都影响着最终体验。这套指南提供了从框架到细节从原理到实操的完整路径。最关键的是理解每个设计背后的“为什么”然后根据自己项目的需求去调整和优化。记住优化永无止境手感需要反复打磨。拿起你的Unity从创建第一条会转弯的鱼开始吧。