简介:面向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 Factor | 1 | 保证鱼模型尺寸与场景单位一致 |
| Animation Type | Generic | 决定Animator Controller能否复用 |
| Loop Time | True | 游动动画必须循环 |
| Optimize Game Objects | False | 保留骨骼层级,方便挂碰撞体 |
导入后把鱼模型分别存成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,防止鱼游出水面或扎进地形。
| 参数 | 推荐范围 | 说明 |
|---|---|---|
| poolSize | 20~40 | 超过40移动端可能掉帧 |
| baseSpeed | 0.8~2.0 | 低于0.8鱼群显得死板 |
| maxSpeed | 2.0~5.0 | 逃跑时的峰值速度 |
| turnSpeed | 1.5~6.0 | 限制转弯平滑度 |
| spawnRadius | 12~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 触发器参数调校
| 参数 | 推荐值 | 备注 |
|---|---|---|
| expandTime | 0.3s | 小于0.2s玩家反应不过来 |
| maxRadius | 2.5 | 比大鱼模型半径大1.5倍 |
| 单条鱼捕获耗时 | 0 | 网碰到即捕获 |
| scoreValue | 10~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参数 | 类型 | 用途 |
|---|---|---|
| swimSpeed | Float | 驱动BlendTree里的游动速度 |
| caught | Trigger | 受击后播放捕获动画 |
| flee | Bool | 逃跑状态,切换更快的游泳动作 |
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模式即可。
| 过检项 | 标准 |
|---|---|
| 鱼停止移动后动画是否回到Idle | Play模式观察2分钟 |
| 渔网碰到鱼是否只加一次分 | 单网捕获5条,分数差扣一个分数值 |
| 真机帧率 | 20条鱼加水面透明特效,不低于40FPS |
| 工程目录无未使用素材 | Assets总大小不超过1GB |
最后补一个常用调试技巧:在NetThrower里临时加一行Debug.Log($"net at {hit.point}"),真机跑一次后通过logcat看坐标是否符合海底平面高度。如果落点浮在海床上方0.5米以上,回到ScreenToSeaPlane检查seaLevel是相对高度还是世界高度,统一用transform.position.y作参照就能解决。
本文还有配套的精品资源,点击获取