Unity 游戏实例开发集合 之 CutFruit (切水果(水果忍者)) 休闲小游戏快速实现

目录

Unity 游戏实例开发集合 之 CutFruit (切水果(水果忍者)) 休闲小游戏快速实现

一、简单介绍

二、CutFruit (切水果(水果忍者)) 游戏内容与操作

三、相关说明

四、游戏代码框架

五、知识点

六、游戏效果预览

七、实现步骤

八、工程源码地址

九、延伸扩展


一、简单介绍

Unity 游戏实例开发集合,使用简单易懂的方式,讲解常见游戏的开发实现过程,方便后期类似游戏开发的借鉴和复用。

本节介绍,CutFruit (切水果(水果忍者))休闲小游戏快速实现的方法,希望能帮到你,若有不对,请留言。

这是一个 3D 游戏,主要是使用精灵图(水果特效等)、3D 重力、3D 碰撞体,TrailRenderer (切刀效果) 实现。

二、CutFruit (切水果(水果忍者)) 游戏内容与操作

1、游戏开始,底部会自动生成向上运动的水果

2、按下鼠标,移动就会生成切水果的刀锋

3、鼠标刀锋碰到水果,水果就会被切开

4、松开鼠标,切割刀锋消失

5、当切到水果,会对应加分,切刀炸弹,会对应失去生命

6、生命没有了,则游戏结束

三、相关说明

1、这里底部生成水果位置,做了屏幕适配,x 方向是:宽度的20% - 80%,y 方向是:屏幕向下高度的 20 %

2、随机生成水果和炸弹的规则是:随机某个范围,指定某个数是炸弹,其他是随机水果

3、这里 3D 水果和炸弹,使用 Collider 3D 碰撞体,但是使用了的是 Trigger 效果,Knife 可以喝水果碰撞,触发事件,但是不会发生实际碰撞效果

4、这里水果或者水果碎片回收,使用的是 OnBecameInvisible() 函数(编辑状态下,包括编辑Scene 的窗口,不仅仅是游戏Game窗口),既是:水果出相机视野,则进行,对应的回收,对象池中继续使用,节约性能

5、脚本复刻这块建议:先复刻 Common 和 Tools 文件夹的脚本,然后 Fruit 和 Effect 文件夹的脚本,接着 Manager 的各个脚本(顺序可按实现步骤的顺序来),最后 GameManager 和 GameStart 即可

6、这里值得注意的是,水果的枚举的作用:1)枚举值是随机生成水果的随机值对应;2)枚举是对应的预制体名称一致;3)枚举是对饮水果的脚本名称一致

四、游戏代码框架

unity 水果忍者 unity水果忍者游戏报告书_游戏

unity 水果忍者 unity水果忍者游戏报告书_unity 水果忍者_02

unity 水果忍者 unity水果忍者游戏报告书_unity_03

五、知识点

1、MonoBehaviour 生命周期函数:Awake,Start,Update,Destroy

2、Input 按键的监控鼠标按键的状态

3、GameObject.Instantiate 物体的生成,GameObject.Destroy 物体的销毁

4、简单的对象池管理

5、Rigidbody 重力效果,添加 CirecleCollider ,进行 碰撞检测(Trigger)

6、简单UGUI的使用

7、简单屏幕适配(主要是UI和 水果生成位置)

8、一些数据,路径等的统一常量管理

9、Transform.Rotate 旋转使用

10、IEnumerator 协程 , StartCoroutine 开始协程 和 StopAllCoroutines 停止所有协程的使用

11、IManager 简单的接口规范 Manager 类函数

12、Action<int> OnChangeValue 属性变化中委托的使用

13、Resources.Load<GameObject>() 代码加载预制体的使用

14、简单的屏幕坐标转为世界坐标的工具类

15、 SceneManager.LoadScene 加载,和 SceneManager.GetActiveScene() 当前场景的获取

16、游戏开发的资源脚本文件夹分类管理

17、等等

六、游戏效果预览

unity 水果忍者 unity水果忍者游戏报告书_unity_04

七、实现步骤

1、打开 Unity,导入水果 等相关资源

unity 水果忍者 unity水果忍者游戏报告书_unity_05

2、然后把对应的资源做成预制体

unity 水果忍者 unity水果忍者游戏报告书_切水果_06

3、这个说明一下 Knife 的制作过程:1)图片是刀锋,可以替换图片更换刀锋;2) 创建一个拖尾的材质Trail,对应shader 为 Particles/Additive,并赋值对应刀锋图;3)创建一个 Sphere ,改名为Knife,添加 TrailRenderer 组件,并赋值 Trail 材质,TrailRenderer  Time 适当改小为 0.1,Knife scale 适当改小为 0.2(可参照实际水果大小调整)

unity 水果忍者 unity水果忍者游戏报告书_unity 水果忍者_07

unity 水果忍者 unity水果忍者游戏报告书_CutFruit_08

unity 水果忍者 unity水果忍者游戏报告书_unity 水果忍者_09

unity 水果忍者 unity水果忍者游戏报告书_unity 水果忍者_10

4、取消 Knife 的MeshRenderer 渲染,保留 Collier ,用于与水果触发碰撞事件

unity 水果忍者 unity水果忍者游戏报告书_游戏_11

5、其中水果预制体,添加 Rigibody,和 MeshCollider ,MeshCollider  勾选 Convex 和 Is Trigger(碰撞的时候只触发碰撞事件,不发生实际碰撞效果),Bomb 类似操作

unity 水果忍者 unity水果忍者游戏报告书_unity 水果忍者_12

unity 水果忍者 unity水果忍者游戏报告书_unity_13

6、BombEffect 和 Splash 是 Spriterenderer 图片动画来的

unity 水果忍者 unity水果忍者游戏报告书_CutFruit_14

unity 水果忍者 unity水果忍者游戏报告书_unity_15

7、在场景中,创建一个 GameObject,改名为 World(pos(0,0,0)),把默认的Main Camera(位置修改为(0,0,0)) 和 Direction Light 至于World 下面,类似依次创建 GameObject,改名为 SpawnFruitPos、SpawnSplashPos、SpawnKnifePos、SpawnBombPos、SpawnBombEffectPos,位置都为(0,0,0),功能如其名,用来把对应生成的游戏物体统一管理

unity 水果忍者 unity水果忍者游戏报告书_切水果_16

unity 水果忍者 unity水果忍者游戏报告书_unity_17

unity 水果忍者 unity水果忍者游戏报告书_游戏_18

unity 水果忍者 unity水果忍者游戏报告书_切水果_19

8、创建一个 GameObject,改名为UI(Pos(0,0,0)),添加一个Canvas (自动添加EventSystem),设置 Canvas Scaler 的 UI Scale Mode 为 Scale with Screen Size ,Reference Resolution 为  1080 x 1920 ,Match 滑动到 Height

unity 水果忍者 unity水果忍者游戏报告书_游戏_20

9、LifeText 放置在左边上,改变锚点,文字加粗居中显示,可以根据实际情况自行调整

unity 水果忍者 unity水果忍者游戏报告书_unity 水果忍者_21

10、ScoreText 放置在右边上,改变锚点,文字加粗居中显示,可以根据实际情况自行调整

unity 水果忍者 unity水果忍者游戏报告书_CutFruit_22

 11、GameOverImage 图片铺满屏幕,设置为黑色,增加透明度,GameOverText 文字加粗居中显示,RestartGameButton 放大,文字调大,可以根据实际情况自行调整,最终如图

unity 水果忍者 unity水果忍者游戏报告书_unity_23

unity 水果忍者 unity水果忍者游戏报告书_unity_24

unity 水果忍者 unity水果忍者游戏报告书_unity_25

12、修改屏幕为 1080x1920 ,最终 UI 效果如下

unity 水果忍者 unity水果忍者游戏报告书_游戏_26

13、在工程中添加 Enum 脚本,管理所有枚举,目前只是水果类型,和水果破碎类型枚举

unity 水果忍者 unity水果忍者游戏报告书_unity 水果忍者_27

/// <summary>
	/// 水果类型
	/// </summary>
	public enum FruitType { 
		Apple = 0,
		Lemon,
		Watermelon,

		SUM_COUNT, // 水果类型总数
	}

	/// <summary>
	/// 破碎水果类型
	/// </summary>
	public enum FruitBrokenType { 
		Apple_Broken = 0,
		Lemon_Broken,
		Watermelon_Broken,


		SUM_COUNT,  // 破碎水果类型总数
	}

14、在工程中添加 GameConfig 脚本,管理游戏中的一些数据

unity 水果忍者 unity水果忍者游戏报告书_unity 水果忍者_28

/// <summary>
	/// 游戏配置数据
	/// </summary>
	public class GameConfig 
	{
		public const string NAME_SPACE_NAME = "MGP_005CutFruit.";

		// Knife 预制体名称(用于碰撞触发判断使用)
		public const string KNIFE_NAME = "Knife";

		// 水果、炸弹、Knife 等游戏生成的统一深度距离
		public const float GAME_OBJECT_Z_VALUE = 5;	

		// 底部间隔几秒生成物体
		public const float BOTTOM_SPAWN_INTERVAL_TIME = 2;	

		// 游戏生命(可切破几次炸弹)
		public const int GAME_LIFE_LENGTH = 3;
		// 游戏剩余几条命结束
		public const int REMAIN_LIFE_IS_GAME_OVER = 0;

		// 水果 炸弹生成向上的最小最大速度
		public const float FRUIT_UP_Y_VELOSITY_MIN_VALUE = 8;
		public const float FRUIT_UP_Y_VELOSITY_MAX_VALUE = 10;

		/// <summary>
		/// 随机生成水果炸弹的数值范围
		/// 随机数多少是炸弹
		/// </summary>
		public const int FRUIT_RANDOM_MIN_VALUE = 0;
		public const int FRUIT_RANDOM_MAX_VALUE = 15;
		public const int BOMB_RANDOM_VALUE = 10;

		/// <summary>
		/// 切刀不同水果的得分
		/// </summary>
		public const int FRUIT_APPLE_SCORE = 10;
		public const int FRUIT_LEMON_SCORE = 20;
		public const int FRUIT_WATERMELON_SCORE = 30;

		//切到炸弹失去的生命值
		public const int BOMB_REDUCE_LIFE = 1;
	}

15、在工程中添加 GameObjectPathInSceneDefine 脚本,定义场景中的游戏物体路径类,ResPathDefine 定义预制体路径类

unity 水果忍者 unity水果忍者游戏报告书_unity_29

unity 水果忍者 unity水果忍者游戏报告书_unity 水果忍者_30

/// <summary>
	/// 定义场景中的游戏物体路径类
	/// </summary>
	public class GameObjectPathInSceneDefine 
	{
		public const string WORLD_PATH = "World";
		public const string UI_PATH = "UI";

		public const string SPAWN_FRUIT_POS_PATH = "SpawnFruitPos";
		public const string SPAWN_SPLASH_POS_PATH = "SpawnSplashPos";
		public const string SPAWN_KNIFE_POS_PATH = "SpawnKnifePos";
		public const string SPAWN_BMOB_POS_PATH = "SpawnBombPos";
		public const string SPAWN_BMOB_EFFECT_POS_PATH = "SpawnBombEffectPos";


		public const string UI_LIFE_TEXT_PATH = "Canvas/LifeText";
		public const string UI_SCORE_TEXT_PATH = "Canvas/ScoreText";
		public const string UI_GAME_OVER_IMAGE_PATH = "Canvas/GameOverImage";
		public const string UI_RESTART_GAME_BUTTON_PATH = "Canvas/GameOverImage/RestartGameButton";


	}

	/// <summary>
	/// 定义预制体路径类
	/// </summary>
	public class ResPathDefine 
	{
		public const string FRUIT_PREFAB_BASE_PATH = "Prefabs/CutFruits/";

		public const string SPLASH_PREFAB_PATH = "Prefabs/Splash";
		public const string KNIFE_PREFAB_PATH = "Prefabs/Knife";
		public const string BOMB_PREFAB_PATH = "Prefabs/Bomb";
		public const string BOMB_EFFECT_PREFAB_PATH = "Prefabs/BombEffect";
	}

16、在工程中添加 BaseFruit 脚本,水果基类脚本,主要管理水果和 Knife 碰撞事件(加分,生成水果碎片,特效等),以及出相机视野外的回收处理,关键代码如下

unity 水果忍者 unity水果忍者游戏报告书_unity_31

/// <summary>
        /// 碰撞事件
        /// </summary>
        /// <param name="other"></param>
        private void OnTriggerEnter(Collider other)
        {
            if (other.name.StartsWith( GameConfig.KNIFE_NAME))
            {
                //增加分数
                m_DataModelManager.Score.Value += Score;


                for (int i = 0; i < 2; i++)
                {
                    //产生破碎的水果
                    FruitBroken fruitBroken = SpawnBroken();
                    fruitBroken.Init(FruitBrokenType,m_FruitManager);
                    GameObject temp = fruitBroken.gameObject;
                    temp.SetActive(true);
                    temp.transform.position = transform.position;
                    temp.transform.eulerAngles = new Vector3(Random.Range(0, 360), Random.Range(0, 360), Random.Range(0, 360));
                    //-3 -2  2 3

                    // 随机水果碎片速度
                    float x, y;
                    int ranX = Random.Range(0, 2);
                    if (ranX == 0)
                    {
                        x = Random.Range(-3.0f, -2.0f);
                    }
                    else
                    {
                        x = Random.Range(2.0f, 3.0f);
                    }

                    int ranY = Random.Range(0, 2);
                    if (ranY == 0)
                    {
                        y = Random.Range(-3.0f, -2.0f);
                    }
                    else
                    {
                        y = Random.Range(2.0f, 3.0f);
                    }

                    temp.GetComponent<Rigidbody>().velocity = new Vector2(x, y);

                }

                //产生果汁特效
                Splash splash = SpawnSplash();
                splash.transform.position = transform.position;
                splash.transform.rotation = Quaternion.Euler(Vector3.forward * Random.Range(-180,180));
                splash.Show(FruitSplahColor, m_SplashManager.RecycleSplah);
                

                //隐藏水果物体
                m_FruitManager.RecycleFruit(FruitType,this);
                m_IsRecycle = false;
            }
            if (other.tag == "BottomCollider")
            {
                gameObject.SetActive(false);
            }
        }

        private void OnBecameVisible()
        {
            m_IsRecycle = true;
        }

        /// <summary>
        /// 相机视野外
        /// 回收水果
        /// </summary>
        private void OnBecameInvisible()
        {
            if (m_IsRecycle==true)
            {
                m_FruitManager.RecycleFruit(FruitType, this);
                m_IsRecycle = false;

            }
        }

17、在工程中添加Apple、Lemon、Watermelon 脚本,继承 BaseFruit ,实现抽象类,设置其对应水果类型,水果破碎类型,碎果Splash 颜色,和得分

unity 水果忍者 unity水果忍者游戏报告书_切水果_32

public class Apple : BaseFruit
	{
       

        public override FruitType FruitType => FruitType.Apple;

        public override FruitBrokenType FruitBrokenType => FruitBrokenType.Apple_Broken;

        public override Color FruitSplahColor => Color.red;

        public override int Score => GameConfig.FRUIT_APPLE_SCORE;
    }

18、在工程中添加 FruitManager 脚本,主要管理水果和水果碎片的预制体加载,以及水果和水果碎片对象池的初始化,并提供接口,从对象池中获取水果和水果碎片,和回收水果和水果碎片

unity 水果忍者 unity水果忍者游戏报告书_切水果_33

/// <summary>
        /// 获取水果碎片
        /// </summary>
        /// <param name="fruitBrokenType"></param>
        /// <returns></returns>
        public FruitBroken GetFruitBroken(FruitBrokenType fruitBrokenType)
        {
            Queue<FruitBroken> fruitBrokenQue = m_FruitBrokenObjectPoolDict[fruitBrokenType];
            if (fruitBrokenQue.Count > 0)
            {
                return fruitBrokenQue.Dequeue();
            }
            else
            {
                return InstantiateFruitBroken(fruitBrokenType);
            }
        }

        /// <summary>
        /// 回收水果
        /// </summary>
        /// <param name="fruitType"></param>
        /// <param name="baseFruit"></param>
        public void RecycleFruit(FruitType fruitType,BaseFruit baseFruit) {
            baseFruit.gameObject.SetActive(false);
            if (m_FruitObjectPoolDict!=null && m_FruitObjectPoolDict[fruitType]!=null)
            {
                m_FruitObjectPoolDict[fruitType].Enqueue(baseFruit);

            }
        }

        /// <summary>
        /// 回收水果碎片
        /// </summary>
        /// <param name="fruitBrokenType"></param>
        /// <param name="fruitBroken"></param>
        public void RecycleFruitBroken(FruitBrokenType fruitBrokenType, FruitBroken fruitBroken)
        {
            fruitBroken.gameObject.SetActive(false);
            m_FruitBrokenObjectPoolDict[fruitBrokenType].Enqueue(fruitBroken);
        }

        /// <summary>
        /// 加载预制体和预载水果到对象池中
        /// </summary>
        private void InitFruit() {

            // 加载预制体
            m_FruitPrefabsDict = new Dictionary<string, GameObject>();
            for (FruitType fruitType = FruitType.Apple; fruitType < FruitType.SUM_COUNT; fruitType++)
            {
                GameObject prefab = Resources.Load<GameObject>(ResPathDefine.FRUIT_PREFAB_BASE_PATH+fruitType.ToString());
                if (prefab == null)
                {
                    Debug.LogError(GetType() + "/InitFruit()/prefab is null, path = " + ResPathDefine.FRUIT_PREFAB_BASE_PATH + fruitType.ToString());
                }
                else {
                    m_FruitPrefabsDict.Add(fruitType.ToString(),prefab);
                }
            }

            // 加载预制体
            for (FruitBrokenType fruitBrokenType = FruitBrokenType.Apple_Broken; fruitBrokenType < FruitBrokenType.SUM_COUNT; fruitBrokenType++)
            {
                GameObject prefab = Resources.Load<GameObject>(ResPathDefine.FRUIT_PREFAB_BASE_PATH + fruitBrokenType.ToString());
                if (prefab == null)
                {
                    Debug.LogError(GetType() + "/InitFruit()/prefab is null, path = " + ResPathDefine.FRUIT_PREFAB_BASE_PATH + fruitBrokenType.ToString());
                }
                else
                {
                    m_FruitPrefabsDict.Add(fruitBrokenType.ToString(), prefab);
                }
            }

            // 预载水果
            m_FruitObjectPoolDict = new Dictionary<FruitType, Queue<BaseFruit>>();
            for (FruitType fruitType = FruitType.Apple; fruitType < FruitType.SUM_COUNT; fruitType++) {
                Queue<BaseFruit> fruits = new Queue<BaseFruit>();
                m_FruitObjectPoolDict.Add(fruitType, fruits);

                RecycleFruit(fruitType, InstantiateFruit(fruitType));
            }

            // 预载水果 Broken
            m_FruitBrokenObjectPoolDict = new Dictionary<FruitBrokenType, Queue<FruitBroken>>();
            for (FruitBrokenType fruitBrokenType = FruitBrokenType.Apple_Broken; fruitBrokenType < FruitBrokenType.SUM_COUNT; fruitBrokenType++)
            {
                Queue<FruitBroken> fruitBrokens = new Queue<FruitBroken>();
                m_FruitBrokenObjectPoolDict.Add(fruitBrokenType, fruitBrokens);

                RecycleFruitBroken(fruitBrokenType, InstantiateFruitBroken(fruitBrokenType));
            }
        }

        /// <summary>
        /// 生成指定水果对象
        /// </summary>
        /// <param name="fruitType"></param>
        /// <returns></returns>
        private BaseFruit InstantiateFruit(FruitType fruitType) {
            GameObject fruit = GameObject.Instantiate(m_FruitPrefabsDict[fruitType.ToString()], m_SpawnFruitPosTrans);
            // 反射获取脚本类型,添加脚本 (注意添加命名空间)
            Type type = Type.GetType( GameConfig.NAME_SPACE_NAME+ fruitType.ToString());
            return (fruit.AddComponent(type) as BaseFruit);
        }

        /// <summary>
        /// 生成破碎水果
        /// </summary>
        /// <param name="fruitBrokenType"></param>
        /// <returns></returns>
        private FruitBroken InstantiateFruitBroken(FruitBrokenType fruitBrokenType)
        {
            GameObject fruit = GameObject.Instantiate(m_FruitPrefabsDict[fruitBrokenType.ToString()], m_SpawnFruitPosTrans);
          
            return fruit.AddComponent<FruitBroken>();
        }

19、在工程中添加 Bomb 脚本,主要功能是 Bomb 与 Knife 的碰撞(减少生命值,以及生成特效),以及相机视野外后回收

unity 水果忍者 unity水果忍者游戏报告书_游戏_34

private void OnTriggerEnter(Collider other)
        {
            if (other.name.StartsWith( GameConfig.KNIFE_NAME))
            {
                //减少生命值
                m_DataModelManager.Life.Value -= GameConfig.BOMB_REDUCE_LIFE;

                //产生爆炸特效
                BombEffect bombEffect = m_BombEffectManager.GetBombEffect();
                bombEffect.Show(transform.position, m_BombEffectManager.RecycleBombEffect);

                //隐藏物体
                m_BmobManager.RecycleBomb(this);
            }
        }


        private void OnBecameVisible()
        {
            m_IsRecycle = true;
        }


        /// <summary>
        /// 相机是野外事件
        /// </summary>
        private void OnBecameInvisible()
        {
            if (m_IsRecycle == true)
            {
                m_BmobManager.RecycleBomb( this);
                m_IsRecycle = false;

            }
        }

20、在工程中添加 BombManager 脚本,主要管理 Bomb 预制体加载,对象池初始化,提供接口获取和回收 Bomb

unity 水果忍者 unity水果忍者游戏报告书_unity_35

/// <summary>
        /// 获取 Bomb 
        /// </summary>
        /// <returns></returns>
        public Bomb GetBmob()
        {
            if (m_BombQueue.Count > 0)
            {
                Bomb bomb = m_BombQueue.Dequeue();
                bomb.gameObject.SetActive(true);
                return bomb;
            }
            else
            {
                return InstantiateBomb();
            }
        }

        /// <summary>
        /// 回收 Bomb 
        /// </summary>
        /// <param name="bomb"></param>
        public void RecycleBomb(Bomb bomb)
        {
            bomb.gameObject.SetActive(false);
            m_BombQueue.Enqueue(bomb);
        }

        /// <summary>
        /// 初始化
        /// </summary>
        private void InitBmob()
        {
            // 加载预制体
            m_BombPrefab = Resources.Load<GameObject>(ResPathDefine.BOMB_PREFAB_PATH);

            // 预载 Bomb
            m_BombQueue = new Queue<Bomb>();
            RecycleBomb(InstantiateBomb());
        }

        private Bomb InstantiateBomb()
        {
            GameObject go = GameObject.Instantiate(m_BombPrefab, m_SpawnBombPosTrans);

            return go.AddComponent<Bomb>();
        }

21、在工程中添加 BombEffect 脚本,主要功能是 显示特效,并且定时回收特效

unity 水果忍者 unity水果忍者游戏报告书_切水果_36

/// <summary>
		/// 显示特效
		/// </summary>
		/// <param name="pos"></param>
		/// <param name="showAnimationEndAction"></param>
		public void Show(Vector3 pos, Action<BombEffect> showAnimationEndAction)
		{
			// 赋值位置和随机旋转,并且定时回收
			transform.position = pos;
			transform.rotation = Quaternion.Euler(Vector3.forward * UnityEngine.Random.Range(-180, 180));
			StartCoroutine(Recycle(showAnimationEndAction));

		}

		/// <summary>
		/// 协程回收特效
		/// </summary>
		/// <param name="showAnimationEndAction"></param>
		/// <returns></returns>
		IEnumerator Recycle(Action<BombEffect> showAnimationEndAction) {
			yield return new WaitForSeconds(m_AnimationLength);
            if (showAnimationEndAction!=null)
            {
				showAnimationEndAction.Invoke(this);

			}
		}

22、在工程中添加 BombEffectManager 脚本,主要管理 Bomb 预制体加载,对象池初始化,提供接口获取和回收 BombEffect

unity 水果忍者 unity水果忍者游戏报告书_切水果_37

/// <summary>
        /// 获取爆炸特效
        /// </summary>
        /// <returns></returns>
        public BombEffect GetBombEffect()
        {
            if (m_BombEffectQueue.Count > 0)
            {
                BombEffect bombEffect = m_BombEffectQueue.Dequeue();
                bombEffect.gameObject.SetActive(true);
                return bombEffect;
            }
            else
            {
                return InstantiateBombEffect();
            }
        }

        /// <summary>
        /// 回收爆炸特效
        /// </summary>
        /// <param name="bombEffect"></param>
        public void RecycleBombEffect(BombEffect bombEffect)
        {
            bombEffect.gameObject.SetActive(false);
            m_BombEffectQueue.Enqueue(bombEffect);
        }

        /// <summary>
        /// 初始化特效
        /// </summary>
        private void InitBombEffect()
        {
            // 加载预制体
            m_BombEffectPrefab = Resources.Load<GameObject>(ResPathDefine.BOMB_EFFECT_PREFAB_PATH);

            // 预载 BombEffect
            m_BombEffectQueue = new Queue<BombEffect>();
            RecycleBombEffect(InstantiateBombEffect());
        }

        private BombEffect InstantiateBombEffect()
        {
            GameObject go = GameObject.Instantiate(m_BombEffectPrefab, m_SpawnBombEffectPosTrans);

            return go.AddComponent<BombEffect>();
        }

23、在工程中添加 Splash 脚本,主要功能是 显示 Splash ,并且定时回收 Splash

unity 水果忍者 unity水果忍者游戏报告书_切水果_38

/// <summary>
		/// 显示特效
		/// </summary>
		/// <param name="color"></param>
		/// <param name="showAnimationEndAction"></param>
		public void Show(Color32 color, Action<Splash> showAnimationEndAction)
		{
			StartCoroutine(EffectAnimation(color, showAnimationEndAction));
		}

		/// <summary>
		/// 特效动画
		/// </summary>
		/// <param name="color"></param>
		/// <param name="showAnimationEndAction"></param>
		/// <returns></returns>
		IEnumerator EffectAnimation(Color color, Action<Splash> showAnimationEndAction)
		{
			m_ColorValue = 0;
			color.a = 0;
			SpriteRenderer.color = color;
			while (true)
			{
				// lerp 匀速插值处理
				m_ColorValue += 1.0f / SPEED * Time.deltaTime;
				color.a = Mathf.Lerp(color.a, 1, m_ColorValue);
				SpriteRenderer.color = color;
				if ((1 - color.a <= 0.05f))
				{
					color.a = 1;
					SpriteRenderer.color = color;

					break;
				}


				yield return new WaitForEndOfFrame();
			}
			m_ColorValue = 0;
			while (true)
			{
				// lerp 匀速插值处理
				m_ColorValue += 1.0f / SPEED * Time.deltaTime;
				color.a = Mathf.Lerp(color.a, 0, m_ColorValue);
				SpriteRenderer.color = color;
				if ((color.a - 0) <= 0.05f)
				{
					color.a = 0;
					SpriteRenderer.color = color;
					break;
				}


				yield return new WaitForEndOfFrame();
			}


			if (showAnimationEndAction != null)
			{
				showAnimationEndAction.Invoke(this);
			}
		}

24、在工程中添加 SplashManager 脚本,主要管理 Bomb 预制体加载,对象池初始化,提供接口获取和回收 Splash

unity 水果忍者 unity水果忍者游戏报告书_unity 水果忍者_39

/// <summary>
        /// 获取 Splash
        /// </summary>
        /// <returns></returns>
        public Splash GetSplash()
        {
            if (m_SplashQueue.Count > 0)
            {
                Splash splash = m_SplashQueue.Dequeue();
                splash.gameObject.SetActive(true);
                return splash;
            }
            else
            {
                return InstantiateSplash();
            }
        }

        /// <summary>
        /// 回收 Splash
        /// </summary>
        /// <param name="splash"></param>
        public void RecycleSplah(Splash splash)
        {
            splash.gameObject.SetActive(false);
            m_SplashQueue.Enqueue(splash);
        }

        /// <summary>
        /// 初始化 SPlash
        /// </summary>
        private void InitSplash()
        {
            // 加载预制体
            m_SplashPrefab = Resources.Load<GameObject>(ResPathDefine.SPLASH_PREFAB_PATH);

            // 预载Splash
            m_SplashQueue = new Queue<Splash>();
            RecycleSplah(InstantiateSplash());
        }

        private Splash InstantiateSplash()
        {
            GameObject go = GameObject.Instantiate(m_SplashPrefab, m_SpawnSplashPosTrans);

            return go.AddComponent<Splash>();
        }

25、在工程中添加 Tools 脚本,主要功能是 把屏幕坐标转为世界坐标

unity 水果忍者 unity水果忍者游戏报告书_切水果_40

public class Tools
	{
		/// <summary>
		/// 把屏幕坐标转为世界坐标
		/// </summary>
		/// <param name="refTran">对应参照对象</param>
		/// <param name="refCamera">对应参照相机</param>
		/// <param name="screenPos">屏幕位置</param>
		/// <returns>屏幕位置的世界位置</returns>
		public static Vector3 ScreenPosToWorldPos(Transform refTran, Camera refCamera, Vector2 screenPos)
		{
			//将对象坐标换成屏幕坐标
			Vector3 pos = refCamera.WorldToScreenPoint(refTran.position);
			//让鼠标的屏幕坐标与对象坐标一致
			Vector3 mousePos = new Vector3(screenPos.x, screenPos.y, pos.z);
			//将正确的鼠标屏幕坐标换成世界坐标交给物体
			return refCamera.ScreenToWorldPoint(mousePos);

		}
	}

26、在工程中添加 KnifeManager 脚本,主要管理 Knife 预制体加载,监控鼠标按下情况,进行刀锋切水果功能

unity 水果忍者 unity水果忍者游戏报告书_unity_41

/// <summary>
        /// 加载预制体
        /// </summary>
        void LoadPrefab() {
            m_KnifePrefab = Resources.Load<GameObject>(ResPathDefine.KNIFE_PREFAB_PATH);
            if (m_KnifePrefab == null)
            {
                Debug.LogError(GetType() + "/LoadPrefab()/ prefab  is  null, path = " + ResPathDefine.KNIFE_PREFAB_PATH);
            }
            else {
                m_KnifeTrans = GameObject.Instantiate(m_KnifePrefab,m_SpawnKnifePosTrans).transform;
                m_KnifeTrans.position = Vector3.forward * GameConfig.GAME_OBJECT_Z_VALUE;
                m_KnifeTrans.gameObject.SetActive(false);
            }
        }

        /// <summary>
        /// 游戏未结束时,鼠标按下更新刀锋
        /// </summary>
        void UpdateShowKnife() {
            if (m_KnifeTrans!=null && m_IsGameOver==false)
            {
                if (Input.GetMouseButtonDown(0))
                {
                    m_KnifeTrans.position = Tools.ScreenPosToWorldPos(m_KnifeTrans, Camera.main, Input.mousePosition);
                    m_KnifeTrans.gameObject.SetActive(true);
                }
                else if (Input.GetMouseButton(0))
                {
                    m_KnifeTrans.position = Tools.ScreenPosToWorldPos(m_KnifeTrans, Camera.main, Input.mousePosition);
                }
                else if (Input.GetMouseButtonUp(0))
                {
                    m_KnifeTrans.gameObject.SetActive(false);
                }
            }
            
        }

27、在工程中添加 Model 脚本,主要功能是 分数和生命数据模型,包含数值记录,和数值变化事件委托

unity 水果忍者 unity水果忍者游戏报告书_unity_42

/// <summary>
	/// 数据模型
	/// </summary>
	public class Model 
	{
		private int m_Value;
		public int Value
		{
			get { return m_Value; }
			set
			{
				if (m_Value != value)
				{
					m_Value = value;

					if (OnValueChanged != null)
					{
						OnValueChanged.Invoke(value);

					}
				}
			}
		}

		/// <summary>
		/// 数值变化事件
		/// </summary>
		public Action<int> OnValueChanged;
	}

28、在工程中添加 DataModelManager 脚本,主要管理 分数Score 和 生命Life

unity 水果忍者 unity水果忍者游戏报告书_游戏_43

public Model Score => m_Scroe;
        public Model Life => m_Life;

        public void Init(Transform rootTrans, params object[] manager)
        {
            m_Scroe = new Model();
            m_Scroe.Value = 0;
            m_Life = new Model();
            m_Life.Value = GameConfig.GAME_LIFE_LENGTH;
        }

29、在工程中添加 UIManager 脚本,主要管理 UI 相关获取,以及数据显示,游戏结束,按钮事件重新加载游戏

unity 水果忍者 unity水果忍者游戏报告书_切水果_44

private Text m_LifeText;
        private Text m_ScoreText;
        private GameObject m_GameOverImageGo;
        private Button m_RestartGameButton;

        private DataModelManager m_DataModelManager;
        public void Init(Transform rootTrans, params object[] managers)
        {
            m_LifeText = rootTrans.Find(GameObjectPathInSceneDefine.UI_LIFE_TEXT_PATH).GetComponent<Text>();
            m_ScoreText = rootTrans.Find(GameObjectPathInSceneDefine.UI_SCORE_TEXT_PATH).GetComponent<Text>();
            m_GameOverImageGo = rootTrans.Find(GameObjectPathInSceneDefine.UI_GAME_OVER_IMAGE_PATH).gameObject;
            m_RestartGameButton = rootTrans.Find(GameObjectPathInSceneDefine.UI_RESTART_GAME_BUTTON_PATH).GetComponent<Button>();

            m_DataModelManager = managers[0] as DataModelManager;

            m_GameOverImageGo.SetActive(false);
            m_LifeText.text = m_DataModelManager.Life.Value.ToString();
            m_ScoreText.text = m_DataModelManager.Score.Value.ToString();
            m_DataModelManager.Life.OnValueChanged += OnLifeValueChanged;
            m_DataModelManager.Score.OnValueChanged += OnScroeValueChanged;
            m_RestartGameButton.onClick.AddListener(OnRestartButton);
        }

        public void OnGameOver()
        {
            m_GameOverImageGo.SetActive(true);
        }

        private void OnLifeValueChanged(int life)
        {
            m_LifeText.text = life.ToString();
        }

        private void OnScroeValueChanged(int score)
        {
            m_ScoreText.text = score.ToString();
        }

        private void OnRestartButton()
        {
            SceneManager.LoadScene(SceneManager.GetActiveScene().name);
        }

30、在工程中添加 GameManager 脚本(单例脚本),主要功能:1)获取场景中相关游戏物体或者 UI,2)new 相关的 Manager 管理类,初始化Init,Update 、和Destroy;3)定时生成水果和炸弹;4)判断游戏是否结束;

unity 水果忍者 unity水果忍者游戏报告书_unity 水果忍者_45

/// <summary>
        /// 底部生成
        /// </summary>
        private void BottomSpawnFruitAndBomb()
        {
            GameObject go = null;
            // 随机数,判断生成水果还是炸弹
            int ran = Random.Range(GameConfig.FRUIT_RANDOM_MIN_VALUE, GameConfig.FRUIT_RANDOM_MAX_VALUE);
            if (ran != GameConfig.BOMB_RANDOM_VALUE)
            {
                BaseFruit fruit = m_FruitManager.GetRandomFruit();
                fruit.Init(m_FruitManager, m_SplashManager, m_DataModelManager);
                go = fruit.gameObject;
            }
            else
            {
                Bomb bomb = m_BmobManager.GetBmob();
                bomb.Init(m_BmobManager,m_BombEffectManager,m_DataModelManager);
                go = bomb.gameObject;
            }

            
            // 生成物体的位置和,向上速度
            go.transform.position = new Vector3(Random.Range(m_Bottom_Spawn_X_Min, m_Bottom_Spawn_X_Max), m_Bottom_Spawn_Y, GameConfig.GAME_OBJECT_Z_VALUE);
            go.transform.rotation = Quaternion.identity;
            go.GetComponent<Rigidbody>().velocity = new Vector2(0, Random.Range(GameConfig.FRUIT_UP_Y_VELOSITY_MIN_VALUE,
                GameConfig.FRUIT_UP_Y_VELOSITY_MAX_VALUE));

        }

        /// <summary>
        /// 判断游戏是否结束
        /// </summary>
        /// <param name="life"></param>
        void JudageGameOverByLife(int life) {
            if (life == GameConfig.REMAIN_LIFE_IS_GAME_OVER)
            {
                OnGameOver();
            }
        }

        /// <summary>
        /// 结束的操作
        /// </summary>
        void OnGameOver() {
            m_IsGameOver = true;
            m_UIManager.OnGameOver();
            m_KnifeManager.OnGameOver();
        }

31、在工程中添加 GameStart 脚本,整个游戏的入口,管理对应 GameManager 的 Awake(),Start(),Update(),OnDestroy() 对应函数功能

unity 水果忍者 unity水果忍者游戏报告书_切水果_46

/// <summary>
    /// 游戏入口
    /// </summary>
	public class GameStart : MonoBehaviour
	{
        private void Awake()
        {
            GameManager.Instance.Awake(this);
        }

        // Start is called before the first frame update
        void Start()
		{
            GameManager.Instance.Start();
        }

		// Update is called once per frame
		void Update()
		{
            GameManager.Instance.Update();
        }

        private void OnDestroy()
        {
            GameManager.Instance.Destroy();
        }
     }

32、在场景中添加 GameObject 空物体,改名为 GameStart,并且挂载 GameStart 脚本

unity 水果忍者 unity水果忍者游戏报告书_CutFruit_47

34、运行游戏,自动生水果,按下鼠标,既可以切割水果了

unity 水果忍者 unity水果忍者游戏报告书_游戏_48

unity 水果忍者 unity水果忍者游戏报告书_游戏_49

八、工程源码地址

github 地址:GitHub - XANkui/UnityMiniGameParadise: Unity 游戏开发集合代码集

的 MGP_005CutFruit 工程

九、延伸扩展

游戏的好不好玩,趣味性,视觉化等诸多因素影响,下面简单介绍几个方面拓展游戏的方向,仅做参考

1、可以根据自己需要修改游戏资源,换肤什么的等

2、可以根据需要添加加分特效,音效,背景更多的细节变化等等

3、添加 UI 面板等,美化游戏

4、Effect 可以设置不同类型的的特效,或者3D 粒子特效

5、特殊水果特殊事件,冰冻,火烧等;

6、添加最高分数保留,和游戏排行榜等;

7、炸弹的特效,可以进行设置不同炸弹,不同特效;

8、水果生成只设置底部生成,可以添加两侧生成也生成水果炸弹等

9、可以是设置,不同波数的水果数量,和间歇频次,定时游戏时间

10、音乐音效也可以对应添加上,效果更好

11、等等