一、项目声明
本博客使用的所有素材(包括但不限于图片、视频、音频、文字等)已购买版权,仅供个人学习、交流、参考和研究使用,同学们如果想要用于个人项目或商业游戏请到https://itch.io/购买版权进行使用,否则需承担因使用该素材而产生的全部责任。本博客不承担任何直接或间接的法律责任。
二、项目演示
捕鱼达人测试
三、前期准备
1.创建2D核心模版项目

2.导入项目资源(创建文件夹进行分类保存)


四、游戏场景UI设计——经验条UI设计
1.创建2个场景GameScene和MainMenuScene

2.选中GameScene,创建背景2DSprite——Square,拖入背景素材,调整大小(1.3倍即可)。调整Game视图分辨率为1280*720


3.创建Image,设置锚点为左上角,拖入素材,设置合适大小和位置(保证后续便于计算取整数)

4.创建1个slider滑条作为经验条,删除滑条的滑头,给fill换成经验条素材

5.创建1个Text,作为等级文本,调整合适的范围以及大小

6.创建1个Text,作为经验等级称号文本,设置合适的大小和范围

五、游戏场景UI设计——倒计时和金币UI设计
1.倒计时UI设计
(1)创建1个Image,用来作为UI背景
(2)创建1个Text,作为时间计数

2.金币UI设计
(1)创建1个Image作为背景

(2)创建一个image,作为金币图标

(3)创建1个text,作为金币的数量

六、水波纹设计
水波测试
1.水波纹材质效果设计
(1)创建一个材质,选择Shader按照图中所找,然后拖入Texture即可生成水波纹材质
(2)创建3D物体Plane,旋转并且调整大小,使其在UI和UIBG中间,然后把材质拖入给Plane身上


2.实现水循环循环播放的效果
效果演示如下:
(1)创建脚本控制Texture进行贴图轮换从而实现效果

(2)把素材全部拖入数组中

(3)代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Effect : MonoBehaviour
{
public Material WaterWave_Material;
public Texture[] WaterWave_Textures;
private int index = 0;
private void Start()
{
WaterWave_Material=this.GetComponent<MeshRenderer>().material;
InvokeRepeating("ChangeWaterWaveTexture",0,0.1f);
}
public void ChangeWaterWaveTexture()
{
WaterWave_Material.mainTexture = WaterWave_Textures[index];
index=(index+1)% WaterWave_Textures.Length;
}
}
七、制作炮架和炮台
1.制作炮架
(1)创建1个空物体用来管理炮相关的物体
(2)创建3个2D Sprite 方形,添加木板素材
(3)调整合适的大小

2.制作炮台
(1)创建4个2D Sprite 方形,添加炮台素材
(2)创建一个空物体,作为后面子弹发射的位置

(3)把炮台做成预制体保存起来

3.让炮台跟着鼠标旋转
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RotateGun : MonoBehaviour
{
void Update()
{
ChangeGunDir();
}
private void ChangeGunDir()
{
Vector3 mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
mousePos.z = 0;
Vector3 dir=mousePos-transform.position;
float angle=Mathf.Atan2 (dir.y,dir.x)*Mathf.Rad2Deg;
transform.rotation = Quaternion.Euler(0, 0, angle - 90);
}
}
八、生成鱼
1. Fish 脚本:单条鱼的移动控制

public class Fish : MonoBehaviour
{
public float moveSpeed = 0f; // 移动速度(可在Inspector面板设置)
public int fishNum = 0; // 该品种鱼群的生成数量(每个预制体独立配置)
private void Update()
{
// 沿自身右方向移动(受旋转影响,实现不同方向游动)
transform.Translate(Vector3.right * Time.deltaTime * moveSpeed);
}
}
2.关键逻辑
moveSpeed控制移动快慢,fishNum定义该品种一次生成多少条鱼(由预制体本身决定,比如 “小丑鱼” 一次生成 5 条,“鲨鱼” 一次生成 2 条)。Update中通过Translate实现移动,Vector3.right是 “自身局部坐标系的右方向”,因此鱼的移动方向会受其旋转角度影响(这与生成时的旋转设置呼应)。
3. GrowFish 脚本:鱼群生成逻辑

public class GrowFish : MonoBehaviour
{
// 生成点数组(在Inspector拖入场景中的生成点对象)
public Transform[] GrowPoints;
// 鱼预制体数组(拖入不同品种的鱼预制体)
public GameObject[] FishPrefabs;
private void Start()
{
// 启动重复调用:从0秒开始,每0.5秒执行一次GrowFishs()
InvokeRepeating("GrowFishs", 0, 0.5f);
}
public void GrowFishs()
{
// 随机选择一个生成点
int PointIndex = Random.Range(0, GrowPoints.Length);
// 随机选择一种鱼预制体
int FishIndex = Random.Range(0, FishPrefabs.Length);
// 随机旋转角度(-45°到45°,让鱼群方向有轻微差异)
float randomAngle = Random.Range(-45f, 45f);
Quaternion randomRotation = Quaternion.Euler(0, 0, randomAngle);
// 最终旋转 = 生成点自身旋转 + 随机角度(生成点可预设基础方向)
Quaternion finalRotation = GrowPoints[PointIndex].rotation * randomRotation;
// 获取该品种鱼的生成数量(从预制体的Fish组件中读取)
int num = FishPrefabs[FishIndex].GetComponent<Fish>().fishNum;
// 启动协程生成鱼群
StartCoroutine(MakeOnePeciesFish(
FishPrefabs[FishIndex], // 选中的鱼预制体
GrowPoints[PointIndex], // 选中的生成点
finalRotation, // 最终旋转角度
num // 生成数量
));
}
// 协程:按间隔生成单种鱼群的多条鱼
IEnumerator MakeOnePeciesFish(GameObject fishPrefab, Transform growPoint,
Quaternion rotation, int num)
{
for (int i = 0; i < num; i++)
{
// 实例化鱼:位置=生成点位置,旋转=计算好的角度,父物体=生成点(方便管理)
GameObject fish = Instantiate(fishPrefab, growPoint.position, rotation, growPoint);
// 调整层级:每条鱼的排序值+1,避免重叠时完全遮挡
fish.GetComponent<SpriteRenderer>().sortingOrder += i;
// 每条鱼生成间隔1秒(可调整)
yield return new WaitForSeconds(1f);
}
}
}
4.关键逻辑
- 生成点与预制体管理:通过数组
GrowPoints和FishPrefabs灵活配置生成位置和鱼种类,方便在编辑器中调整,无需改代码。 - 随机化设计:随机选择生成点、鱼种类、旋转角度,让鱼群生成更自然,避免机械重复。
- 协程控制间隔:用
MakeOnePeciesFish协程实现 “同一种鱼群中每条鱼的生成间隔”(当前是 1 秒),避免瞬间生成一堆鱼导致重叠。 - 层级管理:通过
sortingOrder += i让同群鱼按生成顺序轻微错开层级,解决 2D 渲染中完全重叠的问题。
5.鱼超出边界消失
(1)创建边界碰撞器,同时给鱼添加碰撞器(修改为触发器)和刚体组件

(2)代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Fish : MonoBehaviour
{
public float moveSpeed = 0f;
public int fishNum=0;
private void Update()
{
transform.Translate(Vector3.right*Time.deltaTime*moveSpeed);
}
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.tag == "Wall")
{
Destroy(gameObject);
}
}
}
九、制作切换炮台的按钮
1.制作炮台的预制体
(1)拖入素材,创建炮台
(2)挂载鼠标跟随脚本,制作所有炮台的预制体

2.炮台切换脚本
(1)创建2个button,然后拖入素材
(2)创建炮台脚本UIManger

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UIManager : MonoBehaviour
{
public GameObject[] ShootPrefebs;
private int index = 0;
private void Start()
{
ShootPrefebs[index].SetActive(true);
}
public void Btn_Shoot_Add()
{
ShootPrefebs[index].SetActive(false);
index =(index+1)% ShootPrefebs.Length;
if (index == ShootPrefebs.Length)
{
index=0;
}
ShootPrefebs[index].SetActive(true);
}
public void Btn_Shoot_Sub()
{
ShootPrefebs[index].SetActive(false);
index = (index - 1) % ShootPrefebs.Length;
if (index == -1)
{
index = ShootPrefebs.Length-1;
}
ShootPrefebs[index].SetActive(true);
}
}
十、实现炮台发射子弹
1.实现鼠标生成子弹
(1)创建脚本挂在到炮台上,实现可在指定位置生成子弹预制体
(2)实现随机生成不同的子弹
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BulletShoot : MonoBehaviour
{
public GameObject[] Bullets;
public GameObject BulletGrow;
private int index;
private void Update()
{
if (Input.GetMouseButtonDown(0))
{
ShootBullet();
}
}
public void ShootBullet()
{
index=Random.Range(0, Bullets.Length);
Instantiate(Bullets[index], BulletGrow.transform.position, BulletGrow.transform.rotation);
}
}
2.实现子弹本身移动和销毁功能
(1)创建脚本,挂载到子弹预制体上,并设置合适的速度
(2)给子弹添加碰撞器,鱼和墙添加标签,墙也添加刚体(因为子弹给刚体,我担心发射太多子弹降低性能)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Bullet : MonoBehaviour
{
public float Speed_bullet=0f;
private void Update()
{
transform.Translate(Vector3.up * Speed_bullet * Time.deltaTime);
}
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.tag == "Fish")
{
Destroy(gameObject);
//鱼处理逻辑
}
if (collision.tag == "Wall")
{
Destroy(gameObject);
}
}
}
十一、捕网制作以及销毁
1.捕网制作
(1)修改鱼的碰撞体(使得子弹确保打到鱼)

(2)创建2D Sprite ,添加网的素材。制作四种网并添加圆形碰撞器,制成预制体

(3)同时网的层级应该在鱼的上面(我的鱼最高位270),炮台和木板的下面(这个最低是1000)

(4)将网的生成与子弹碰到鱼联系到一起
表示子弹销毁的同时,生成网的预制体

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Bullet : MonoBehaviour
{
public float Speed_bullet=0f;
public GameObject WebPrefeb;
private void Update()
{
transform.Translate(Vector3.up * Speed_bullet * Time.deltaTime);
}
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.tag == "Fish")
{
GameObject Web=Instantiate(WebPrefeb,gameObject.transform.position,Quaternion.identity);
Destroy(gameObject);
//鱼处理逻辑
}
if (collision.tag == "Wall")
{
Destroy(gameObject);
}
}
}
2.捕网销毁
(1)创建一个新的脚本控制网本身
(2)定时销毁
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Web : MonoBehaviour
{
public float Web_Destroy_Time = 0f;
private void Start()
{
Destroy(this,Web_Destroy_Time);
}
}
3.被捕的鱼销毁以及释放动画
(1)添加鱼碰到网销毁
(2)为了避免后续鱼移动到网里面不需要销毁,我写了获取碰撞器并让他失活
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Fish : MonoBehaviour
{
public float moveSpeed = 0f;
public int fishNum=0;
private void Update()
{
transform.Translate(Vector3.right*Time.deltaTime*moveSpeed);
}
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.tag == "Wall")
{
Destroy(gameObject);
}
if (collision.tag == "Web")
{
Destroy(gameObject);
collision.gameObject.GetComponent<CircleCollider2D>().enabled = false;
}
}
}
(3)获取鱼的动画并播放动画然后再次销毁
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Fish : MonoBehaviour
{
public float moveSpeed = 0f;
public int fishNum=0;
public GameObject fish_Die_Anim;
private void Update()
{
transform.Translate(Vector3.right*Time.deltaTime*moveSpeed);
}
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.tag == "Wall")
{
Destroy(gameObject);
}
if (collision.tag == "Web")
{
GameObject DieAnim = Instantiate(fish_Die_Anim,gameObject.transform.position, gameObject.transform.rotation);
Destroy(gameObject);
collision.gameObject.GetComponent<CircleCollider2D>().enabled = false;
Destroy(DieAnim,1f);
}
}
}
十二、UI制作
注意:这部分重在思路,代码也比较分散,所以一定要理解思路,自己写
1.等级更新以及相应称号更新
// 等级与称号相关变量
private int GradeValue = 0; // 当前等级
private string[] TitleText = { "菜鸟", "新手", "熟练", "大师", "渔王" };
// 等级计算方法
public void Grade_Calculate()
{
GradeValue = (int)(LV_Value / Grade_Need_LV);
Grade.text = GradeValue.ToString();
}
// 称号更新方法
public void Update_Title_Text()
{
if (GradeValue >= 0 && GradeValue < 20) Title.text = TitleText[0];
else if (GradeValue >= 20 && GradeValue < 40) Title.text = TitleText[1];
else if (GradeValue >= 40 && GradeValue < 60) Title.text = TitleText[2];
else if (GradeValue >= 60 && GradeValue < 80) Title.text = TitleText[3];
else if (GradeValue >= 80 && GradeValue < 100) Title.text = TitleText[4];
}
核心逻辑:通过鱼被捕获获得经验,经验累积到阈值后升级,等级决定称号。
-
经验机制:鱼被捕获时(
Fish脚本的OnTriggerEnter2D),通过单例UIManager.Instance.LV_Value += fish_Admire_LV添加经验。每次升级后,经验值会扣除当前等级所需经验(实现 “归零” 效果),同时下一级所需经验增加(Grade_Need_LV += LevelUpExpIncrease),保证后期升级难度递增。 -
等级计算:在
Grade_Calculate中使用while循环处理(而非if),避免一次获得大量经验时只升一级的问题(例如一次获得 1000 经验,直接从 0 级升到 2 级)。 -
称号系统:采用 “等级区间映射” 逻辑:每 20 级对应一个称号(0-19 级→菜鸟,20-39 级→新手...),通过
GradeValue / 20计算称号索引,简洁且易扩展。
2.经验条UI制作
// 经验条相关变量
public Slider LVSlider; // 经验条组件
public float LV_Value = 0; // 当前经验值
public float Grade_Need_LV = 500; // 升级所需经验
// 经验条更新方法
public void Update_LVSlider()
{
LVSlider.value = LV_Value % Grade_Need_LV / Grade_Need_LV;
}
实现方式:使用 Unity 的Slider组件作为经验条容器。
-
进度计算:经验条的
value属性(0-1 之间)由 “当前经验 / 升级所需经验” 计算得出(LVSlider.value = LV_Value / Grade_Need_LV),实时反映升级进度。
3.金币UI制作
// 金币相关变量
public Text Coin; // 金币文本组件
public float CoinCapcity = 500; // 当前金币数量
// 金币更新方法
public void Update_Coin_Text()
{
Coin.text = CoinCapcity.ToString();
}
// 金币增减逻辑(分布在其他脚本)
// 1. 发射子弹消耗金币(BulletShoot.cs)
if (UIManager.Instance.CoinCapcity >= Bullet_Cost_Coin)
UIManager.Instance.CoinCapcity -= Bullet_Cost_Coin;
// 2. 捕获鱼获得金币(Fish.cs)
UIManager.Instance.CoinCapcity += fish_Admire_Coin;
核心逻辑:金币通过捕获鱼增加(Fish脚本),发射子弹消耗(BulletShoot脚本),UI 实时显示当前金币数。
-
性能优化:原代码中
Update_Coin_Text在Update中每帧调用,优化后改为 “数值变化时外部调用”(例如捕获鱼或发射子弹后调用),减少不必要的 UI 刷新。 -
显示格式:使用
ToString("F0")确保金币显示为整数(避免因浮点数精度问题出现小数,如 “500” 而非 “500.0”)。
4.倒计时UI制作
// 倒计时相关变量
public Text CountTime; // 倒计时文本组件
public float Time_Sum = 300; // 总倒计时(秒)
private float Timerjishu = 0; // 时间累加器
// 倒计时更新方法
public void Update_CountTime_Text()
{
Timerjishu += Time.deltaTime;
if (Timerjishu >= 1f)
{
Time_Sum--;
Timerjishu = 0;
}
CountTime.text = Time_Sum.ToString();
}
实现方式:通过累计时间差实现整数秒递减,避免小数显示。
-
核心逻辑:用
Timerjishu累计每帧的Time.deltaTime,当累计满 1 秒时,Time_Sum减 1 并重置累加器,确保每秒只减 1 次。 -
边界处理:当
Time_Sum <= 0时停止更新,避免出现负数;可扩展添加倒计时结束逻辑(如游戏结束弹窗)。
5.设置按钮打开暂停界面
(1)功能说明
通过点击按钮触发暂停界面的显示,同时冻结游戏时间;暂停状态下可通过 “继续游戏” 按钮恢复正常运行。
(2)代码
// 暂停相关变量
public GameObject PausePanel; // 暂停面板UI
public AudioSource keySound; // 按钮音效
// 打开暂停面板
public void Open_PausePanel()
{
keySound.Play(); // 播放按钮点击音效
PausePanel.SetActive(true); // 显示暂停面板
Time.timeScale = 0; // 冻结游戏时间(暂停Update逻辑)
}
// 继续游戏(关闭暂停面板)
public void Continue_Game()
{
keySound.Play(); // 播放按钮点击音效
PausePanel.SetActive(false); // 隐藏暂停面板
Time.timeScale = 1; // 恢复游戏时间
}
(3)实现逻辑分析
-
面板显示与隐藏:
- 暂停面板(
PausePanel)默认处于隐藏状态(SetActive(false)),点击 “暂停” 按钮时调用Open_PausePanel(),通过SetActive(true)显示面板。 - 点击 “继续游戏” 按钮时调用
Continue_Game(),隐藏面板(SetActive(false)),恢复游戏界面恢复游戏视图。
- 暂停面板(
-
时间冻结机制:
- 调用
Time.timeScale = 0冻结时间,使依赖Time.deltaTime的逻辑(如鱼的移动、倒计时)暂停,同时配合前文对RotateGun和BulletShoot的修改,确保暂停时无法操作枪支和射击。 - 恢复时设置
Time.timeScale = 1,游戏逻辑正常运行。
- 调用
-
交互反馈:按钮点击时播放
keySound音效,增强用户操作的听觉反馈,提升交互体验。
6.游戏结束界面
(1)金币为0
// 金币与结束面板变量
public Text Coin; // 金币显示文本
public float CoinCapcity = 500; // 当前金币数量
public GameObject OverPanel; // 结束面板UI
// Update中检测金币是否耗尽
private void Update()
{
// 其他UI更新逻辑...
if (CoinCapcity <= 0 || Time_Sum == 0)
{
Open_overPanel(); // 触发结束界面
}
}
// 打开结束面板
public void Open_overPanel()
{
OverPanel.SetActive(true); // 显示结束面板
Time.timeScale = 0; // 冻结时间
}
- 金币通过射击消耗(
BulletShoot脚本中CoinCapcity -= Bullet_Cost_Coin),当CoinCapcity减至 0 或负数时,Update函数检测到条件并调用Open_overPanel(),显示结束界面。 - 结束后时间冻结(
Time.timeScale = 0),防止后续逻辑继续执行。
(2)时间为0
// 倒计时相关变量
public Text CountTime; // 倒计时显示文本
public float Time_Sum = 300; // 总倒计时(秒)
private float Timerjishu = 0; // 时间累加器
// 倒计时更新
public void Update_CountTime_Text()
{
Timerjishu += Time.deltaTime;
if (Timerjishu >= 1f)
{
Time_Sum--; // 每秒减1
Timerjishu = 0;
}
CountTime.text = Time_Sum.ToString(); // 显示剩余时间
}
// Update中检测时间是否为0
private void Update()
{
// 其他UI更新逻辑...
if (CoinCapcity <= 0 || Time_Sum == 0)
{
Open_overPanel(); // 触发结束界面
}
}
- 倒计时通过
Update_CountTime_Text()每秒递减 1,当Time_Sum减至 0 时,Update函数检测到条件并调用Open_overPanel(),显示结束界面。 - 与金币耗尽逻辑共用同一个结束面板,统一游戏结束入口。
十三、开始界面制作
(1)添加素材,个性化设置

(2)创建脚本,挂载代码

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class UIManagerMainMenu : MonoBehaviour
{
public void Btn_EnterGame()
{
SceneManager.LoadScene(1);
}
public void Btn_Return_Menu()
{
SceneManager.LoadScene(0);
}
public void Btn_ExitGame()
{
Application.Quit();
}
}
(3)设置场景

十四、总结以及完整项目
通过网盘分享的文件:CatchFish.zip
链接: https://pan.baidu.com/s/13wpkdy_NoGG60b5TntnoOg?pwd=1111 提取码: 1111
--来自百度网盘超级会员v5的分享
回顾这篇项目记录更像一份一份 “从 0 到 1” 的解谜日志 —— 没有一开始始就追求完美,而是在 “发现问题 - 解决问题” 的循环中逐步搭建起完整的游戏框架。回头看,最核心的收获并非最终实现了多少功能,而是学会了如何将一个复杂的游戏需求拆解为可落地的小目标,并用逻辑串联起来。
希望大家自己根据项目和素材完善其他功能和玩法,一定不要被上面的过程禁锢,按照自己的想法来
2597

被折叠的 条评论
为什么被折叠?



