news 2026/9/15 7:26:30

Unity3D捕鱼游戏开发全流程:对象池、鱼群AI与碰撞交互实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Unity3D捕鱼游戏开发全流程:对象池、鱼群AI与碰撞交互实战

简介:面向Unity3D初学者的捕鱼游戏期末作业工程,内含完整源程序,可直接运行体验捕鱼玩法。项目覆盖C#游戏逻辑、UI界面、音频管理、难度选择等模块,并配有清晰的代码结构与说明文档,适合课程设计、自学练手或二次开发。压缩包约210.94MB,包含约2000个文件,以C#脚本(744个)、Prefab预制体(92个)、PNG贴图(1188张)、Anim动画(81个)、Controller状态机(80个)、WAV音频(41个)为主,同时带有多套场景、材质、UI样式和Markdown文档,便于对照学习。已有1094人学习下载,对新手有较高参考价值。通过学习这份工程,可以掌握炮台发射、碰撞检测、计分、按钮交互、背景音乐与音量调节、加载进度条、难度切换等常见游戏功能的实现思路,也能借助预制体、动画和音频资源熟悉Unity3D的资源组织与复用方式,为后续独立开发完整游戏打下基础。

1. unity3D期末项目捕鱼游戏的开发定位与成品结构

捕鱼游戏常年占据unity3d期末作业选题的热门位置,因为场景好看、玩法直接,模型、动画、物理、UI、音频覆盖得全,稍加改造就能提交答辩。这个项目最常见的坑是一上来先写开火逻辑,结果场景里只有一条鱼在那里抽搐。我的建议是把“鱼群在水里游起来”当成最重要的一步,等鱼群自己会沿着曲线巡航、遇到干扰知道逃跑,再往上加点击撒网和计分,整个游戏才立得住。下面按真题项目的开发顺序,从场景搭建、鱼群对象池、捕鱼交互、动画AI到打包验证逐段讲,代码层级以单场景为主,适合初学者拿到源程序后一条线读完。

2. 用unity3d搭建海洋海底场景与模型资源组织

2.1 导入海洋海底鱼模型与动画资源时的检查项

场景构成很简单:一个半透明水面、一个海底地形、若干个鱼群预制体、一个粒子系统做气泡。地形不一定要用Heightmap,用一个Plane拆成几个小块高低错开摆放,再刷上岩石贴图即可。鱼模型在unity3d资源目录里搜“海洋海底鱼模型”能拿到一批带骨骼和待机、游动动画的FBX,不要直接拖进Hierarchy,先检查导入设置:

检查项推荐值作用
Scale Factor1保证鱼模型尺寸与场景单位一致
Animation TypeGeneric决定Animator Controller能否复用
Loop TimeTrue游动动画必须循环
Optimize Game ObjectsFalse保留骨骼层级,方便挂碰撞体

导入后把鱼模型分别存成Fish_01、Fish_02这样的Prefab,Collider统一用Box并包住身体主体,避免用Mesh Collider在碰撞检测上浪费性能。初学者容易忽略的是FBX默认的Scale Factor如果被改成0.01,模型会缩到极小,在Scene视图里不明显,放进对象池后子弹怎么都瞄不准,多半就是缩放问题。

2.2 水面材质参数与海底灯光布置

半透明水面直接用Standard的Transparent模式做,比用Water Shader简单,期末项目不会因为水面波动特效加分,反而容易掉帧。水面材质需要调整三个参数:

  • Rendering Mode设为Transparent
  • 颜色Alpha值设在0.18到0.35之间,能透过水面看到下面的鱼群
  • Smoothness设在0.6左右,让光源在水面形成一层高光

场景照明方面,主方向光用Soft Shadows,颜色偏白,Intensity约1.2;海底微弱的蓝紫光由一个Point Light补足,Range取视距的两倍,Intensity不超过1.2。这样鱼模型表面的漫反射会保留层次,不会像被灯光直射一样发白。

2.2.1 用C#脚本调整水面的半透明颜色

在unity3d里修改透明度需要走Material.SetColor链路。可以用这段最简代码在游戏启动时让水面颜色慢慢变到目标值:

using UnityEngine; public class WaterAlphaController : MonoBehaviour { public Material waterMaterial; [Range(0.1f, 0.8f)] public float targetAlpha = 0.25f; private float currentAlpha = 0.5f; void Update() { currentAlpha = Mathf.Lerp(currentAlpha, targetAlpha, Time.deltaTime * 0.8f); Color color = waterMaterial.GetColor("_Color"); color.a = currentAlpha; waterMaterial.SetColor("_Color", color); } }

这段代码每帧访问材质并修改alpha,会产生少量SetPass开销,不适合多水面场景,但期末单相机项目完全可以接受。targetAlpha越低越能看到鱼,但水面高光会丢失,0.25配合蓝绿色背景观感最自然。

气泡用粒子系统实现,Start Lifetime取3到5秒,Start Size取0.02到0.08,Shape选Sphere并设半径0.1,Simulation Space设为World。气泡数量超过200个时,把Max Particles改成100,否则底部粒子会挤成一片白雾。测试运行时能看到鱼群在水面下巡游、水面透明,就可以进入下一阶段。

3. 用对象池控制unity3d鱼群游动逻辑

3.1 为什么写FishPool而不直接Instantiate

直接Instantiate鱼在Scene视图里看效果最直观,但捕鱼游戏里每条鱼都要反复经历产生、受击、消失的过程。如果一分钟内创建上百条鱼,GC Alloc会让移动端帧率明显下降。对象池的处理方式是游戏启动时创建固定数量的鱼,捕获后停用而不是销毁,等鱼重新激活后再回到池子。这样源程序所有文件里不需要额外插件,纯C#就能完成。

FishPool挂在场景空物体上,做成单例:

using System.Collections.Generic; using UnityEngine; public class FishPool : MonoBehaviour { public static FishPool Instance { get; private set; } [SerializeField] private FishAgent fishPrefab; [SerializeField] private int poolSize = 36; [SerializeField] private float spawnRadius = 18f; private readonly List<FishAgent> pool = new List<FishAgent>(); void Awake() { Instance = this; } void Start() { for (int i = 0; i < poolSize; i++) { FishAgent fish = Instantiate(fishPrefab, transform); fish.gameObject.SetActive(false); pool.Add(fish); } SpawnBatch(poolSize / 3); } public FishAgent Take(Vector3 pos) { FishAgent fish = pool.Find(f => !f.gameObject.activeSelf); if (fish == null) return null; fish.transform.position = pos; fish.gameObject.SetActive(true); return fish; } public void Release(FishAgent fish) { fish.StopAllCoroutines(); fish.gameObject.SetActive(false); } private void SpawnBatch(int count) { for (int i = 0; i < count; i++) { Vector2 dir = Random.insideUnitCircle * spawnRadius; FishAgent f = Take(new Vector3(dir.x, 0, dir.y)); f?.StartCruise(); } } }

poolSize决定同屏鱼的数量,推荐20到40,低端机超过40会有可见的状态切换卡顿。Take方法里的pool.Find是线性查找,池子上百条时建议换成Queue;期末项目36条远不到性能瓶颈,不必过度设计。SpawnBatch只在开局生成三分之一,剩下的鱼在后续捕鱼消耗中再取用,避免开局画面里鱼群过于密集。

3.2 鱼群的巡航移动与转身逻辑

单条鱼的移动脚本挂在鱼预制体上,使用“目标点 + 速度Lerp”实现连续转向。这是捕鱼小游戏项目里最常见、也最容易控制的移动方式:

using UnityEngine; public class FishAgent : MonoBehaviour { public enum FishState { Cruise, Flee, Caught } public FishState state = FishState.Cruise; public float baseSpeed = 1.2f; public float maxSpeed = 3f; public float turnSpeed = 2.5f; public int scoreValue = 10; private Vector3 targetPoint; private Vector3 currentVelocity; private float fleeTimer; void Start() { PickNewTarget(); } void Update() { if (state == FishState.Caught) return; Vector3 steer; if (state == FishState.Cruise) { if (Vector3.Distance(transform.position, targetPoint) < 0.8f) { PickNewTarget(); } steer = (targetPoint - transform.position).normalized * baseSpeed; } else if (state == FishState.Flee) { fleeTimer -= Time.deltaTime; if (fleeTimer <= 0f) { state = FishState.Cruise; PickNewTarget(); } Vector3 away = (transform.position - Camera.main.transform.position).normalized; steer = away * maxSpeed; } else { steer = Vector3.zero; } currentVelocity = Vector3.Lerp(currentVelocity, steer, turnSpeed * Time.deltaTime); transform.position += currentVelocity * Time.deltaTime; if (currentVelocity.sqrMagnitude > 0.01f) { transform.forward = Vector3.Slerp(transform.forward, currentVelocity.normalized, turnSpeed * Time.deltaTime); } Animator animator = GetComponent<Animator>(); if (animator != null) { animator.SetFloat("swimSpeed", currentVelocity.magnitude); } } public void PickNewTarget() { Vector2 r = Random.insideUnitCircle * 12f; targetPoint = new Vector3(r.x, Random.Range(-0.6f, 0.6f), r.y); } public void StartCruise() { state = FishState.Cruise; PickNewTarget(); gameObject.SetActive(true); } }
3.2.1 游动参数与转向时的坐标限制

baseSpeed为1.2时,鱼在画面上大约每秒前进12个场景单位,适合普通水下场景;turnSpeed取2.5到4之间,视角上最接近真实鱼转身,小于1则鱼头长时间不朝前进方向,大于6则会出现抖动。PickNewTarget里的Y范围限制在±0.6,防止鱼游出水面或扎进地形。

参数推荐范围说明
poolSize20~40超过40移动端可能掉帧
baseSpeed0.8~2.0低于0.8鱼群显得死板
maxSpeed2.0~5.0逃跑时的峰值速度
turnSpeed1.5~6.0限制转弯平滑度
spawnRadius12~30开局鱼的分散程度

如果希望鱼群像捕鱼达人那样结群游动,比较朴素的做法是在场景里放一个空对象作为Leader,让每条鱼的目标点相对Leader偏移,再让Leader按矩形路径移动。前一种随机换点代码更少,够交期末项目;后一种巡游效果更自然,但多一个偏移参数需要调。

4. unity3d捕鱼交互系统:点击、渔网与碰撞计分

4.1 从摄像机射线到渔网生成

捕鱼玩法的操作方式通常有两种:瞄准鱼发射子弹和点击撒网。期末项目推荐撒网实现,代码量更少,碰撞反馈也更直观,整群鱼被同时网住的效果比单发子弹好得多。

玩家点击屏幕后,先用Camera.main的ScreenPointToRay生成一条三维射线,再用Physics.Raycast求出落点:

using UnityEngine; public class NetThrower : MonoBehaviour { public GameObject netPrefab; public float throwRange = 100f; public Camera mainCamera; void Update() { if (!Input.GetMouseButtonDown(0)) return; Ray ray = mainCamera.ScreenPointToRay(Input.mousePosition); if (Physics.Raycast(ray, out RaycastHit hit, throwRange)) { GameObject net = Instantiate(netPrefab, hit.point, Quaternion.identity); net.GetComponent<NetEffect>().Initialize(); } } }

throwRange取100即可覆盖水下可见范围。注意Physics.Raycast只识别带Collider的物体,水面Plane即使透明也会挡射线,所以水面不要加碰撞器,或者把碰撞器设成Trigger;海底地形则必须加Collider,否则渔网落不到正确深度。

4.2 渔网展开与碰撞判定

渔网预制体上放一个Sphere Collider,从极小的半径在0.3秒内扩大到2.5,勾选Is Trigger。这样网不需要写移动逻辑,只在点击点原地展开:

using System.Collections; using UnityEngine; public class NetEffect : MonoBehaviour { public float expandTime = 0.3f; public float maxRadius = 2.5f; private SphereCollider col; void Awake() { col = GetComponent<SphereCollider>(); col.isTrigger = true; col.radius = 0.01f; } public void Initialize() { StartCoroutine(Expand()); } IEnumerator Expand() { float t = 0f; while (t < expandTime) { t += Time.deltaTime; col.radius = Mathf.Lerp(0.01f, maxRadius, t / expandTime); yield return null; } yield return new WaitForSeconds(0.15f); Destroy(gameObject); } void OnTriggerEnter(Collider other) { FishAgent fish = other.GetComponent<FishAgent>(); if (fish == null || fish.state == FishAgent.FishState.Caught) return; fish.state = FishAgent.FishState.Caught; fish.GetComponent<Animator>().SetTrigger("caught"); GameUI.Instance.AddScore(fish.scoreValue); FishPool.Instance.Release(fish); // 可选:在这里显示飘字加分 // UIManager.Instance.ShowFloatingScore(fish.scoreValue, transform.position); } }

OnTriggerEnter的触发条件是渔网碰撞体与鱼碰撞体至少一方为触发器,并且另一方的Rigidbody存在。给鱼预制体加Rigidbody比较稳妥,Use Gravity项设为False,Interpolate设为Interpolate。这样渔网碰到鱼时,捕获响应基本不会延迟。

4.2.1 触发器参数调校
参数推荐值备注
expandTime0.3s小于0.2s玩家反应不过来
maxRadius2.5比大鱼模型半径大1.5倍
单条鱼捕获耗时0网碰到即捕获
scoreValue10~50小鱼10,大鱼50

渔网展开期间连续碰到两条鱼会触发两次OnTriggerEnter,所以用fish.state == Caught做守卫,防止同一鱼被重复计分。鱼被Release后停用,但渔网协程还会继续运行直到Destroy,这段残留时间正好播放渔网收拢动画。

4.3 用数学平面替代地形射线定位撒网点

如果不想让渔网被海底地形的高低起伏影响,可以绕开物理射线,用数学平面求交:

public static Vector3 ScreenToSeaPlane(Camera cam, Vector3 screenPos, float seaLevel = 0f) { Plane plane = new Plane(Vector3.up, new Vector3(0, seaLevel, 0)); Ray ray = cam.ScreenPointToRay(screenPos); plane.Raycast(ray, out float enter); return ray.GetPoint(enter); }

这种方式的优点是渔网生成位置和鱼碰撞完全分离,点击水面任意一点都会生成渔网,不需要地形Collider参与。把seaLevel设为海底地形高度加0.4,网就在离海底半路空中展开,视觉效果比直接贴地自然。

提示:如果点击后渔网出现在相机背后,检查取的是不是mainCamera;场景里存在第二个投影相机时,ScreenPointToRay拿到的是depth最大的相机视图。

5. 鱼类的动画状态机与逃跑AI实现

5.1 鱼受击动画状态切换

鱼模型一般自带idle、swim、die三段动画。把这三段放进Animator Controller后,控制器参数建议这样设计:

Animator参数类型用途
swimSpeedFloat驱动BlendTree里的游动速度
caughtTrigger受击后播放捕获动画
fleeBool逃跑状态,切换更快的游泳动作

BlendTree设置成idle在0速度、swim在1速度、fleeRun在3速度。FishAgent在Update里把currentVelocity.magnitude传给swimSpeed,这样转向和加速过程里动画是连续过渡的,不会突然切段。

捕获动画从触发到消失之间要留出播放时间,用协程管理生命周期比较清晰:

public IEnumerator CaptureSequence() { state = FishState.Caught; Animator animator = GetComponent<Animator>(); animator.SetTrigger("caught"); yield return new WaitForSeconds(0.25f); gameObject.SetActive(false); FishPool.Instance.Release(this); }

让鱼自己管理受击函数,好处是动画播放、回池整套逻辑都在FishAgent内部完成,NetEffect不需要关心鱼的动画状态,只需要设置状态和加分。

5.2 玩家靠近时的逃跑逻辑

鱼AI做到“被玩家近距离惊扰就逃跑”就够期末展示。状态机维持三个状态:Cruise巡航、Flee逃跑、Caught被捕获。逃跑逻辑的关键是远离玩家方向的计算:

public float fleeRadius = 8f; public float fleeDuration = 2f; void Update() { if (state == FishState.Caught) return; if (state == FishState.Cruise) { float distance = Vector3.Distance(transform.position, Camera.main.transform.position); if (distance < fleeRadius) { state = FishState.Flee; fleeTimer = fleeDuration; Animator animator = GetComponent<Animator>(); animator.SetBool("flee", true); } } else if (state == FishState.Flee) { fleeTimer -= Time.deltaTime; if (fleeTimer <= 0f) { state = FishState.Cruise; PickNewTarget(); Animator animator = GetComponent<Animator>(); animator.SetBool("flee", false); } } }

Flee状态下的移动方向和Cruise不同,不要再用PickNewTarget产生的随机点,而是让鱼沿玩家反方向加速。做法是把目标点替换成当前朝向的反向延长位置,原第3章的移动逻辑会自动让鱼转向并加速离开。

5.2.1 fleeRadius的边界设置

fleeRadius取8表示以玩家相机为球心、半径8的球体内鱼会逃跑。小于4鱼群毫无反应,难度过低;大于12几乎所有鱼都在跑,渔网失去目标。Flee持续时间设2到3秒,太短会出现鱼回头游进渔网的现象。

逃跑到边界时还需要限制鱼的移动范围,最简单的方法是在FishAgent里加一个半公开的HomeCenter字段,PickNewTarget生成的点如果距离原点超过20,就沿原点方向压回18。这个限制要放在移动逻辑外,否则鱼会在边界反复折返。

6. 期末项目交付前:源程序结构、性能验证与打包要点

6.1 用Profiler定位帧率瓶颈

在unity3d里打开Window > Analysis > Profiler,Play状态下跑两分钟,重点看CPU Usage里PlayerLoop的Physics.Simulate和Animation.Update各占多少。捕鱼项目最常见的瓶颈有两个:鱼群同时开启实时阴影,Shadow Casting开销飙升;渔网粒子特效没有限制总量。对策是把鱼Prefab的Cast Shadows设为Off,节省0.5到1毫秒的CPU时间。

第4章最简版本的渔网是Instantiate加Destroy,期末场景同屏渔网不会超过6个,GC影响有限,可以保留;如果改成子弹式射击,就需要为渔网单独做对象池。验证标准以20条鱼加撒网20次为基准,编辑器里帧率不低于60fps,真机不低于40fps,就可以提交。

6.2 打包前检查和Android构建参数

构建前清理输出目录是机械操作,但经常被忽略:

rm -rf Build/ mkdir -p Build/Android

在File > Build Settings里切到Android平台,包名填com.school.unity3dfish,Minimum API Level选Android 7.0,Scripting Backend用IL2CPP,Target Architecture勾选ARM64。Texture Compression选ASTC后包体更小,且不会出现部分机型纹理偏红的问题。

构建完成后用命令行看Unity日志:

adb logcat -s Unity

重点看两类信息:着色器编译警告和GC Alloc。日志中出现“Unable to find shader”时,说明水面材质引用了旧版Shader,直接换成Standard的Transparent模式即可。

过检项标准
鱼停止移动后动画是否回到IdlePlay模式观察2分钟
渔网碰到鱼是否只加一次分单网捕获5条,分数差扣一个分数值
真机帧率20条鱼加水面透明特效,不低于40FPS
工程目录无未使用素材Assets总大小不超过1GB

最后补一个常用调试技巧:在NetThrower里临时加一行Debug.Log($"net at {hit.point}"),真机跑一次后通过logcat看坐标是否符合海底平面高度。如果落点浮在海床上方0.5米以上,回到ScreenToSeaPlane检查seaLevel是相对高度还是世界高度,统一用transform.position.y作参照就能解决。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 7:25:43

11-01-Unity-CSharp语言与API支持边界验证指南

Unity 的 C# 语言与 API 支持边界&#xff1a;从编译器到 IL2CPP 的验证方法系列&#xff1a;C# 与常用数据结构源码剖析 Unity 实战篇 阅读时间&#xff1a;约 55 分钟 前置知识&#xff1a;C# 编译执行链路、泛型、反射、托管集合 结论边界&#xff1a;Unity 的工具链会随 E…

作者头像 李华
网站建设 2026/9/15 7:25:35

NetPad详解:C#开发者必备的跨平台即时脚本执行环境

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 7:25:20

phpStudy部署ThinkPHP 3.2 CRM实战教程

1. 项目概述&#xff1a;为什么用 phpStudy 部署符号象 CRM 是一条务实的落地路径“phpStudy 如何部署符号象CRM客户关系管理系统教程”——这个标题背后藏着一个非常典型的中小团队真实困境&#xff1a;没有专职运维&#xff0c;没有云服务器预算&#xff0c;甚至没有Linux命令…

作者头像 李华
网站建设 2026/9/15 7:23:41

MediaPipe+时空Transformer:动作识别项目从数据到部署的完整实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 7:23:24

Vue3 + Cesium三维GIS工程化实践:避坑指南与可复用模块

简介&#xff1a;本资源是一套基于Vue框架集成Cesium三维地理信息可视化能力的完整训练项目源码&#xff0c;面向计算机、地理信息、遥感、测绘等相关专业在校学生及初学者&#xff0c;解决Web端三维GIS开发入门与实践难题。项目已通过实际运行测试&#xff0c;涵盖底图加载&am…

作者头像 李华