Unity 游戏实例开发集合 之 FlappyBird (像素鸟) 休闲小游戏快速实现

Unity 游戏实例开发集合 之 FlappyBird (像素鸟) 休闲小游戏快速实现

目录

Unity 游戏实例开发集合 之 FlappyBird (像素鸟) 休闲小游戏快速实现

一、简单介绍

二、FlappyBird (像素鸟) 游戏内容与操作

三、相关说明

四、游戏代码框架

五、知识点

六、游戏效果预览

七、实现步骤

八、工程源码地址

扫描二维码关注公众号,回复: 13721786 查看本文章

九、延伸扩展


一、简单介绍

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

本节介绍,FlappyBird (像素鸟) 休闲小游戏快速实现的方法,希望能帮到你,若有不对,请留言。

这是一个 2D 游戏,主要是使用精灵图、2D 重力、2D 碰撞体,实现,游戏实现原理:

1、鸟 x 方向位置保持不变,背景天空草地,管道等在从右向左不动移动,从而实现鸟向前飞行效果

2、鸟有重力效果,通过 GetComponent<Rigidbody2D>().Velocity 的 y 方向添加速度,从而实现鸟向上飞的效果

3、背景天空草地,管道 也有Rigidbody2D,但是 Body Type 是 Kinematic ,没有重力向下的效果,但是可以 设置 GetComponent<Rigidbody2D>().Velocity 向左的速度,实现匀速向左运动的效果

4、游戏暂停实现原理是:1)暂停效果:把背景天空草地,管道 也有Rigidbody2D的Velocity 设置为0 ,暂停时记录鸟的Rigidbody2D的Velocity ,把 Rigidbody2D的 Body Type 是 Static,从而实现停止效果;2)继续游戏效果:重新设置背景天空草地,管道 也有Rigidbody2D的Velocity 设置为0之前的原值,把鸟Rigidbody2D的 Body Type 是 Dynamics,Rigidbody2D的Velocity设置为之前记录的值即可,这样就恢复现场,继续游戏了

5、无限背景实现:把背景天空草地设置两份(可以根据需要设置多份),1) 两份左右拼接

2)先是左边的背景显示,背景一起向左边运动到左边看不见,左边的看不到的背景,重新移动到右边背景的右边,这样,就实现无限循环背景了

二、FlappyBird (像素鸟) 游戏内容与操作

1、游戏开始,背景背景开始向左移动,鸟会受重力向下坠落

2、点击鼠标左键,鸟就会添加一个向上的力,避免坠落地面

3、鸟每过一个管子,就会有对应的分数增加

4、鸟撞到水管或者碰到地面,游戏则结束

三、相关说明

1、音频的枚举命名对应实际音频的名字,方便加载和指定播放音频

2、由于2D 游戏(元素都在同一平面),设置 SpriteRenderer 的 Order in Layer 来控制显示先后,Order in Layer 越大,显示就在前面,所以各个 Order in Layer 定义显示规则为 :天空预制体为 -5,草地 为 5,Bird 为 0,Pipe 下的上下管子为 2

3、Pipe 、GrassTile、SkyTile 的 Rigidbody2D, Body Type 是 Kinematic ,没有重力向下的效果

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

四、游戏代码框架

(其实可以把Manager 拆分 Server 和 Data,或者更好)

 

五、知识点

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

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

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

4、简单的对象池管理

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

6、简单UGUI的使用

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

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

9、Animation、Animator 的简单使用

10、游戏中 Tag 的使用

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

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

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

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

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

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

17、等等

六、游戏效果预览

七、实现步骤

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

2、因为是 2D 游戏,场景中设置MainCamera 的Clear Flags 为 Solid Color,Projection 为 Orthographic ,Pos(0,0,-10),大家根据需要设置即可

3、把导入的图片设置为 Sprite 精灵图

4、把两张天空图拖入场景,左右拼接两张图,一个pos(0,0,0),另一个pos(20.25,0,0),20.25很关键,将作为循环背景移动的关键参考数据

 

 5、把屏幕设置为 1920x1080 作为参考大小,天空显示如图

 6、同理,拖入草地的背景图,左右拼接,一个为pos(0,-2.5,0),另一个为pos(20.25,-2.5,0),效果如下

 

 7、把天空和草地作为预制体,其中图片显示层级做修改,SkyTile 的 Order in Layer 为 -5(显示在后面),GrassTile 的 Order in Layer 为 5(显示在前面面),两个添加 Rigidbody2D ,其中 Rigidbody2D 的 Body Type 设置为 Kinematic(不会有向下的重力,但是可以施加 Rigidbody2D.velocity 的速度效果),然后 拖到 Resources 作为预制体

8、把两张飞行的鸟图拖入场景中,然后系统会自动生成一个动画和对应动画机挂载在图片上,运行场景,鸟动画大致如下

9、把BirdHero_01 改名为 Bird,选中 Bird,在菜单栏 Window - Animation - Animation,打开动画编辑,添加 BirdDie 鸟死亡动画,选择 Add Property ,添加SpriteRenderer 的 Sprite ,然后在 0.00 和 0.01 添加死亡图片,完成BirdDie 动画

 

 

 10、选中Bird ,在Windows菜单中,选择 Animation - Animator,进入动画机,添加 Parameters参数来控制动画播放,这里添加Bool 的 IsDie 和 IsFly

 

 11、在空白处右键,添加一个空动画,作为Idle 动画,选中 Idle ,右键 设置为默认动画,选中Idle 右键,点击 Make Transition ,拖到 BirdFly,形成一个动画过渡条件,同理,添加各个动画过渡条件线如图

 

 

 

12、其中 Idle 到 BirdFly 的过渡线的条件,选择为 IsFly 为true,BirdFly 到 Idle 的过渡线为 IsFly 为 flase 

 

13、其中 BirdFly 到 BirdDie 的过渡线的条件,选择为 IsDie 为true,BirdDie 到 BirdFly 的过渡线为 IsDie 为 flase (或者可以不要这根过渡线也可以),Idle 到 BirdDie 的过渡线的条件,选择为 IsDie 为true

 

 

14、这样 Bird 的动画机完成了,整理一下相关资源,在 Bird 上,添加 Rigidbody ,和 BoxCollider2D,并整理BoxCollider2D大小适配鸟实际大小,然后把 Bird 拖入 Resources 作为预制体资源

15、把管子拖入场景,把 Bird 拖入场景中,为了适配鸟过去,添加一个空物体GameObject,改名为 Pipe(添加 Rigidbody2D,BodyType 设置为 Kinematic),把管子图片置于其下,上下一个,位置如下,DownColumnSprite 的 Pos(0,-6,0),BoxCollider2D 适配精灵图实际大小,UpColumnSprite的 Pos(0,6,0),BoxCollider2D 适配精灵图实际大小,ScoreEdgeCollider2D 位置和方向如图,刚好鸟在中间触发加分,并勾选 IsTrigger (可以碰撞触发,但没有实际碰撞效果)

 

16、把设置好的Pipe 作为预制体,拖入 Resources 文件夹下

17、在场景中添加一个空物体GameObject,改名为 World (pos(0,0,0)),然后把 Main Camera 等拖到其下,新建多个 空物体GameObject,依次命名为 SpawnSkyTilePos(天空背景的生成位置和父物体),SpawnGrassTilePos(草地背景的生成位置和父物体),SpawnBirdPos(Bird的生成位置和父物体),SpawnPipePos(管子的生成位置和父物体),AudioSourceTrans(音频播放源的挂载体),位置 都为 Pos(0,0,0)

18、  在 World 下添加一个 GameObject ,添加 EdgeCollider2D ,改名为 GroundEdgeCollider2D ,位置搞好放置到草地上(拖入草地预制体作为参考),如图,最后记得把参考的参照物删除就好

19、  在 World 下添加一个 GameObject ,添加 EdgeCollider2D ,改名为 SkyEdgeCollider2D ,添加一个Cube 作为可视化参考,位置搞好放置到天空上(拖入天空预制体作为参考),如图

 20、新建一个 GameObject,改名为UI,用来当作 UI相关部分的父物体,pos 为 (0,0,0)

21、在UI下面,添加一个Canvas (自动添加EventSystem),设置 Canvas Scaler 的 UI Scale Mode 为 Scale with Screen Size ,Reference Resolution 为  1920 x 1080  ,Match 滑动到 Width

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

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

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

 

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

26、在工程中添加 Common (一般可以游戏开发最后写,脚本中抽出来,整理统一管理),管理游戏中的一些常量,AnimatorParametersDefine 动画机的参数定义类,Enum 管理所有枚举,GameConfig 一些游戏配置,GameObjectPathInSceneDefine 场景中的游戏物体路径定义,ResPathDefine 预制体路径定义类,TagDefine Tag 定义类

27、 TagDefine Tag 定义类,其中 Tag 定义方式如下,选择一个游戏物体,选择 Tag 下拉菜单,点击 Add Tag ... 添加 Tag ,然后选择 + ,命名一个名称,save 即可,这里,我们把预制体的 Pipe的  Tag 设置为 Pipe (后面碰撞体触发用到)

 

28、在工程中添加 IManager 脚本,主要功能是定义管理类的一些基本接口

	public interface IManager
	{
		// 初始化
		void Init(Transform rootTrans, params object[] managers);
		// 帧更新
		void Update();
		// 销毁时调用(主要是释放数据等使用)
		void Destroy();

		// 游戏结束
		void GameOver();
	}

29、在工程中添加 ResLoadManager 脚本,主要功能是,加载 Resources 文件加载的预制体,这里主要涉及加载 GameObject 和 AudioClip

        /// <summary>
        /// 加载预制体 GameObject 
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        public GameObject LoadPrefab(string path) {
            if (m_PrefabsDict.ContainsKey(path) == true)
            {
                return m_PrefabsDict[path];
            }
            else {
                GameObject prefab = Load<GameObject>(path);
                if (prefab!=null)
                {
                    m_PrefabsDict.Add(path, prefab);
                }

                return prefab;
            }
        }

        /// <summary>
        /// 加载预制体 AudioClip 
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        public AudioClip LoadAudioClip(string path)
        {
            if (m_AudioClipsDict.ContainsKey(path) == true)
            {
                return m_AudioClipsDict[path];
            }
            else
            {
                AudioClip prefab = Load<AudioClip>(path);
                if (prefab != null)
                {
                    m_AudioClipsDict.Add(path, prefab);
                }

                return prefab;
            }
        }

        /// <summary>
        /// 泛型加载预制体
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="path"></param>
        /// <returns></returns>
        private T Load<T>(string path) where T:Object{
            T prefab = Resources.Load<T>(path);
            if (prefab == null)
            {
                Debug.LogError(GetType() + "/Load()/prefab is null,path = " + path);
            }

            return prefab;
        }

30、在工程中添加 Model 脚本,主要功能是定义了一个基本的数据模型,包含数值,和数值变化触发的事件

	/// <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;

31、在工程中添加 DataModelManager 脚本,主要功能是定义游戏中涉及到的数据,这里定义了 Score (既是 Model 类类型参数)的数据

        private Model m_Scroe;

        public Model Score => m_Scroe;

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

        public void Destroy()
        {
            m_Scroe.OnValueChanged = null;
            m_Scroe.Value = 0;
            m_Scroe = null;
        }

32、在工程中添加 SkyTile脚本,主要功能是 利用 Rigidbody2D.velocity,更新天空背景位置,当位置到达指定位置,进行位置左移,从而实现无限循环

		/// <summary>
		/// 初始化
		/// </summary>
		/// <param name="index">第几块天空,从 0 开始</param>
		public void Init(int index) {
			m_TargetPosX = -1 * GameConfig.BACKGROUND_SPRITE_INTERVAL_X;
			m_Velocity = Vector2.left * GameConfig.BACKGROUND_MOVE_LEFT_X;

			Vector3 curPos = transform.position;
			transform.position = new Vector3(curPos.x+(index)* GameConfig.BACKGROUND_SPRITE_INTERVAL_X,
				curPos.y, curPos.z);

			m_IsPause = false;
			Move();
		}

		public void Resume() {
			m_IsPause = false;
			Move();
		}

		public void Pause()
		{
			m_IsPause = true;
			Rigidbody2D.velocity = Vector2.zero;
		}
		public void GaomeOver()
		{
			Rigidbody2D.velocity = Vector2.zero;
		}

		private void Move()
		{
			Rigidbody2D.velocity = m_Velocity;
		}

		/// <summary>
		/// 更新天空背景位置
		/// 当位置到达指定位置,进行位置左移,从而实现无限循环
		/// </summary>
		private void UpdatePosOperation() {


			Vector3 curPos = transform.position;

			if (curPos.x <= m_TargetPosX)
            {
				// 移动到右边(以为走了,右边的右边,所以增加 2 * BACKGROUND_SPRITE_INTERVAL_X )
				curPos = new Vector3((curPos.x + 2* GameConfig.BACKGROUND_SPRITE_INTERVAL_X), curPos.y, curPos.z);
				transform.position = curPos;
			}
		}

33、在工程中添加 SkyTileManager 脚本,主要功能是加载 SkyTile 预制体,生成到指定位置,然后在游戏暂停、继续、结束控制 SkyTile 是否移动

 

        /// <summary>
        /// 初始化
        /// </summary>
        /// <param name="rootTrans"></param>
        /// <param name="managers"></param>
        public void Init(Transform rootTrans, params object[] managers)
        {
            m_SpawnSkyTilePosTrans = rootTrans.Find(GameObjectPathInSceneDefine.SPAWN_SKY_TILE_POS_PATH);
            m_ResLoadManager = managers[0] as ResLoadManager;
            m_SkyTileList = new List<GameObject>();

            LoadPrefab();
        }


        /// <summary>
        /// 加载预制体
        /// </summary>
        private void LoadPrefab() {
            GameObject prefab = m_ResLoadManager.LoadPrefab(ResPathDefine.PREFAB_SKY_TILE_PATH);
            for (int i = 0; i < GameConfig.BACKGROUND_TILE_COUNT; i++)
            {
                GameObject skyTile = GameObject.Instantiate(prefab,m_SpawnSkyTilePosTrans);
                skyTile.AddComponent<SkyTile>().Init(i);
                m_SkyTileList.Add(skyTile);
            }
        }

        public void GamePause()
        {
            foreach (GameObject item in m_SkyTileList)
            {
                item.GetComponent<SkyTile>().Pause();
            }
        }

        public void GameResume()
        {
            foreach (GameObject item in m_SkyTileList)
            {
                item.GetComponent<SkyTile>().Resume();
            }
        }

        public void GameOver()
        {
            foreach (GameObject item in m_SkyTileList)
            {
                item.GetComponent<SkyTile>().GaomeOver();
            }
        }

34、在工程中添加 GrassTile 脚本,主要功能是 利用 Rigidbody2D.velocity,更新草地背景位置,当位置到达指定位置,进行位置左移,从而实现无限循环

		/// <summary>
		/// 初始化
		/// </summary>
		/// <param name="index">第几块天空,从 0 开始</param>
		public void Init(int index)
		{
			m_TargetPosX = -1 * GameConfig.BACKGROUND_SPRITE_INTERVAL_X;
			m_Velocity = Vector2.left * GameConfig.BACKGROUND_MOVE_LEFT_X;

			Vector3 curPos = transform.position;
			transform.position = new Vector3(curPos.x + (index) * GameConfig.BACKGROUND_SPRITE_INTERVAL_X,
				curPos.y, curPos.z);

			m_IsPause = false;
			Move();
		}


		/// <summary>
		/// 更新草地背景位置
		/// 当位置到达指定位置,进行位置左移,从而实现无限循环
		/// </summary>
		private void UpdatePosOperation()
		{


			Vector3 curPos = transform.position;

			if (curPos.x <= m_TargetPosX)
			{
				// 移动到右边(以为走了,右边的右边,所以增加 2 * BACKGROUND_SPRITE_INTERVAL_X )
				curPos = new Vector3((curPos.x + 2 * GameConfig.BACKGROUND_SPRITE_INTERVAL_X), curPos.y, curPos.z);
				transform.position = curPos;
			}
		}

35、在工程中添加 GrassTileManager 脚本,主要功能是加载 GrassTile 预制体,生成到指定位置,然后在游戏暂停、继续、结束控制 GrassTile 是否移动

        /// <summary>
        /// 初始化
        /// </summary>
        /// <param name="rootTrans"></param>
        /// <param name="managers"></param>
        public void Init(Transform rootTrans, params object[] managers)
        {
            m_SpawnGrassTilePosTrans = rootTrans.Find(GameObjectPathInSceneDefine.SPAWN_GRASS_TILE_POS_PATH);
            m_ResLoadManager = managers[0] as ResLoadManager;
            m_GrassTileList = new List<GrassTile>();

            LoadPrefab();
        }


        public void GamePause()
        {
            foreach (GrassTile item in m_GrassTileList)
            {
                item.Pause();
            }
        }

        public void GameResume()
        {
            foreach (GrassTile item in m_GrassTileList)
            {
                item.Resume();
            }
        }

        public void GameOver()
        {
            foreach (GrassTile item in m_GrassTileList)
            {
                item.GaomeOver();
            }
        }

        /// <summary>
        /// 加载实例化预制体
        /// </summary>
        private void LoadPrefab()
        {
            GameObject prefab = m_ResLoadManager.LoadPrefab(ResPathDefine.PREFAB_GRASS_TILE_PATH);
            for (int i = 0; i < GameConfig.BACKGROUND_TILE_COUNT; i++)
            {
                GameObject tile = GameObject.Instantiate(prefab, m_SpawnGrassTilePosTrans);
                GrassTile grassTile = tile.AddComponent<GrassTile>();
                grassTile.Init(i);
                m_GrassTileList.Add(grassTile);
            }
        }

36、在工程中添加 ObjectPool 脚本,主要功能是 泛型对象池,继承即可使用,包含 对象获取,预载、回收、以及清空等功能

    /// <summary>
    /// 泛型对象池
    /// </summary>
    /// <typeparam name="T"></typeparam>
	public class ObjectPool<T>  where T : MonoBehaviour
	{
        private Queue<T> m_TQueue;

        /// <summary>
        /// 获取 T
        /// </summary>
        /// <returns></returns>
        public T Get(GameObject prefab,Transform parent)
        {
            if (m_TQueue!=null && m_TQueue.Count > 0)
            {
                T t = m_TQueue.Dequeue();
                t.gameObject.SetActive(true);
                return t;
            }
            else
            {
                return InstantiateT(prefab, parent);
            }
        }

        /// <summary>
        /// 回收 T
        /// </summary>
        /// <param name="t"></param>
        public void Recycle(T t)
        {
            t.gameObject.SetActive(false);
            if (m_TQueue==null)
            {
                m_TQueue = new Queue<T>();
            }
            m_TQueue.Enqueue(t);
        }

        /// <summary>
        /// 预载
        /// </summary>
        /// <param name="prefab"></param>
        /// <param name="parent"></param>
        /// <param name="preloadCount"></param>
        public void PreloadT(GameObject prefab, Transform parent,int preloadCount=1)
        {
            // 预载Splash
            if (m_TQueue==null)
            {
                m_TQueue = new Queue<T>();
            }
            for (int i = 0; i < preloadCount; i++)
            {
                Recycle(InstantiateT(prefab, parent));
            }
        }

        /// <summary>
        /// 清空对象池
        /// </summary>
        public void ClearPool() {
            if (m_TQueue!=null)
            {
                while (m_TQueue.Count > 0)
                {
                    GameObject.Destroy(m_TQueue.Dequeue().gameObject);
                }
            }

            m_TQueue = null;
        }

        /// <summary>
        /// 生成实例
        /// </summary>
        /// <param name="prefab"></param>
        /// <param name="parent"></param>
        /// <returns></returns>
        private T InstantiateT(GameObject prefab,Transform parent)
        {
            GameObject go = GameObject.Instantiate(prefab, parent);

            return go.AddComponent<T>();
        }
    }

37、在工程中添加 Bird 脚本,主要功能是 Rigidbody2D.velocity 实现鸟飞行,鸟的动画切换,以及触发碰撞体相关事件(死亡和得分事件)

		/// <summary>
		/// 初始化
		/// </summary>
		public void Init(Action onGroundCollisionEnter2D, Action onScoreCollisionEnter2D)
		{
			m_OnGroundCollisionEnter2D = onGroundCollisionEnter2D;
			m_OnScoreCollisionEnter2D = onScoreCollisionEnter2D;
			m_UpVelocity = Vector2.up * GameConfig.BIRD_MOVE_UP_Y;
			PlayFlyAnimation();
		}

		public void Fly() {
			Rigidbody2D.velocity = m_UpVelocity;
		}

		public void Resume()
		{
			
			Rigidbody2D.bodyType = RigidbodyType2D.Dynamic;
			Move(m_CurVelocity);

			PlayFlyAnimation();
		}

		public void Pause()
		{
			m_CurVelocity = Rigidbody2D.velocity;
			Rigidbody2D.bodyType = RigidbodyType2D.Static;

			PlayIdleAnimation();
		}


		public void GameOver() {
			//Rigidbody2D.bodyType = RigidbodyType2D.Static;
			PlayDieAnimation();
		}

		private void Move(Vector2 velocity)
		{
			Rigidbody2D.velocity = velocity;
		}

		private void PlayIdleAnimation() {
			Animator.SetBool(AnimatorParametersDefine.IS_FLY, false);
			Animator.SetBool(AnimatorParametersDefine.IS_DIE, false);
		}

		private void PlayFlyAnimation()
		{
			Animator.SetBool(AnimatorParametersDefine.IS_FLY, true);
			Animator.SetBool(AnimatorParametersDefine.IS_DIE, false);
		}

		private void PlayDieAnimation()
		{
			Animator.SetBool(AnimatorParametersDefine.IS_FLY, false);
			Animator.SetBool(AnimatorParametersDefine.IS_DIE, true);
		}

		/// <summary>
		/// 触发死亡碰撞
		/// </summary>
		/// <param name="collision"></param>
        private void OnCollisionEnter2D(Collision2D collision)
        {
            if (collision.collider.name.StartsWith(GameConfig.GROUND_EDGE_COLLIDER2D_NAME) 
				|| collision.collider.CompareTag(TagDefine.PIPE))
            {
                if (m_OnGroundCollisionEnter2D!=null)
                {
					m_OnGroundCollisionEnter2D.Invoke();

				}
            }

			
		}

		/// <summary>
		/// 触发加分碰撞
		/// </summary>
		/// <param name="collision"></param>
        private void OnTriggerEnter2D(Collider2D collision)
        {
			if (collision.name.StartsWith(GameConfig.SCORE_EDGE_COLLIDER2D_NAME))
			{
				if (m_OnScoreCollisionEnter2D != null)
				{
					m_OnScoreCollisionEnter2D.Invoke();

				}
			}
		}

38、在工程中添加 BirdManager脚本,主要功能是加载 BIrd预制体,生成到指定位置,然后在游戏暂停、继续、结束控制 Bird 是否可出发飞行等事件和动画,以及游戏结束和游戏得分事件

        /// <summary>
        /// 初始化
        /// </summary>
        /// <param name="rootTrans"></param>
        /// <param name="managers"></param>
        public void Init(Transform rootTrans, params object[] managers)
        {
            m_SpawnBirdPosTrans = rootTrans.Find(GameObjectPathInSceneDefine.SPAWN_BIRD_POS_PATH);
            m_ResLoadManager = managers[0] as ResLoadManager;
            m_DataModelManager = managers[1] as DataModelManager;
            m_AudioManager = managers[2] as AudioManager;

            m_IsPause = false;
            m_IsGameOver = false;

            LoadPrefab();
        }

        public void Update()
        {
            if (m_IsPause == true || m_IsGameOver == true)
            {
                return;
            }

            UpdatePosOperation();
        }

        public void Destroy()
        {
            m_SpawnBirdPosTrans = null;
            m_ResLoadManager = null;
            m_Bird = null;
        }



        public void GameResume() {
            m_IsPause = false;
            m_Bird.Resume();
        }
        public void GamePause() {
			m_IsPause = true;
            m_Bird.Pause();
        }

        public void GameOver()
        {
            m_IsGameOver = true;
            m_Bird.GameOver();
        }

        /// <summary>
        /// 加载实例化鸟
        /// </summary>
        private void LoadPrefab()
        {
            GameObject prefab = m_ResLoadManager.LoadPrefab(ResPathDefine.PREFAB_BIRD_PATH);
            GameObject bird = GameObject.Instantiate(prefab, m_SpawnBirdPosTrans);
            m_Bird =  bird.AddComponent<Bird>();

            m_Bird.Init(OnBirdGroundCollisionEnter, OnBirdScoreCollisionEnter);
        }

        /// <summary>
        /// 监听是否鼠标按下,向上飞
        /// </summary>
        private void UpdatePosOperation()
        {

            if (Input.GetMouseButtonDown(0) == true 
                && EventSystem.current.IsPointerOverGameObject() ==false) // 鼠标点击在 UI 上不触发
            {
                m_AudioManager.PlayAudio(AudioClipSet.Fly);
                m_Bird.Fly();
            }
        }

        /// <summary>
        /// 游戏结束事件
        /// </summary>
        private void OnBirdGroundCollisionEnter() {
            m_AudioManager.PlayAudio(AudioClipSet.Collider);
            m_IsGameOver = true;

        }

        /// <summary>
        /// 游戏加分事件
        /// </summary>
        private void OnBirdScoreCollisionEnter()
        {
            m_AudioManager.PlayAudio(AudioClipSet.Tip);
            m_DataModelManager.Score.Value += GameConfig.PASS_PIPE_GET_SCORE;

        }

39、在工程中添加 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);

		}

40、在工程中添加 Pipe 脚本,主要功能是 Rigidbody2D.velocity 实现 管子移动,和到达指定位置,进行对象回收

		/// <summary>
		/// 初始化
		/// </summary>
		/// <param name="spawnPosX"></param>
		/// <param name="spawnPosY"></param>
		/// <param name="movePosTargetPosX"></param>
		/// <param name="onRecycleSelfAction"></param>
		public void Init(float spawnPosX, float spawnPosY, float movePosTargetPosX, Action<Pipe> onRecycleSelfAction)
		{
			m_TargetPosX = movePosTargetPosX;
			m_Velocity = Vector2.left * GameConfig.BACKGROUND_MOVE_LEFT_X;

			Vector3 curPos = transform.position;
			transform.position = new Vector3(spawnPosX, spawnPosY, curPos.z);

			m_OnRecycleSelfAction = onRecycleSelfAction;

			m_IsPause = false;
			Move();
		}


		private void Move()
		{
			Rigidbody2D.velocity = m_Velocity;
			
		}


		/// <summary>
		/// 位置更新
		/// 判断位置是否到达指定位置,进行对象回收
		/// </summary>
		private void UpdatePosOperation()
		{


			Vector3 curPos = transform.position;

			if (curPos.x <= m_TargetPosX)
			{
				if (m_OnRecycleSelfAction != null)
                {
					m_OnRecycleSelfAction.Invoke(this);

				}
			}
		}

41、在工程中添加 PipeManager脚本(继承 ObjectPool 实现 Bird 对象池功能),主要功能是加载 Pipe 预制体,生成到指定位置(管子的Y的位置是随机的,但范围是如图确认),然后在游戏暂停、继续、结束控制 Pipe 是否运动,定时生成管子

        /// <summary>
        /// 初始化
        /// </summary>
        /// <param name="rootTrans"></param>
        /// <param name="managers"></param>
        public void Init(Transform rootTrans, params object[] managers)
        {
            m_SpawnPipePosTrans = rootTrans.Find(GameObjectPathInSceneDefine.SPAWN_PIPE_POS_PATH);
            m_ResLoadManager = managers[0] as ResLoadManager;

            m_PipeList = new List<Pipe>();
            m_IsPause = false;
            m_IsGameOver = false;

            m_SpawnTimer = GameConfig.PIPE_SPAWN_TIME_INTERVAL;

            m_SpawnPosX = Tools.ScreenPosToWorldPos(m_SpawnPipePosTrans,Camera.main,Vector2.right*(Screen.width *(1+0.1f))).x;
            m_TargetMovePosX = Tools.ScreenPosToWorldPos(m_SpawnPipePosTrans,Camera.main,Vector2.left*(Screen.width *(0.1f))).x;
            m_PipePrefab = m_ResLoadManager.LoadPrefab(ResPathDefine.PREFAB_Pipe_PATH);
            LoadPrefab(m_PipePrefab, m_SpawnPipePosTrans);
        }

        public void GamePause()
        {
            m_IsPause = true;
            if (m_PipeList != null && m_PipeList.Count > 0)
            {
                foreach (var item in m_PipeList)
                {
                    item.Pause();
                }
            }
        }

        public void GameResume()
        {
            m_IsPause = false;
            if (m_PipeList != null && m_PipeList.Count > 0)
            {
                foreach (var item in m_PipeList)
                {
                    item.Resume();
                }
            }
        }

        public void GameOver()
        {
            m_IsGameOver = true;
            if (m_PipeList!=null && m_PipeList.Count>0)
            {
                foreach (var item in m_PipeList)
                {
                    item.GaomeOver();
                }
            }
        }

        /// <summary>
        /// 预载实例化对象
        /// </summary>
        /// <param name="prefab"></param>
        /// <param name="parent"></param>
        private void LoadPrefab(GameObject prefab,Transform parent)
        {
            PreloadT(prefab, parent);
        }

        /// <summary>
        /// 计时生成管子,以及初始化管子和设置回收管子事件
        /// </summary>
        void UpdateSpawnPipe() {

            m_SpawnTimer += Time.deltaTime;
            if (m_SpawnTimer>=GameConfig.PIPE_SPAWN_TIME_INTERVAL)
            {
                m_SpawnTimer -= GameConfig.PIPE_SPAWN_TIME_INTERVAL;
                Pipe pipe = Get(m_PipePrefab, m_SpawnPipePosTrans);
                float spawnPosY = Random.Range(GameConfig.PIPE_SPAWN_POS_Y_LIMIT_MIN,GameConfig.PIPE_SPAWN_POS_Y_LIMIT_MAX);
                pipe.Init(m_SpawnPosX, spawnPosY,m_TargetMovePosX, 
                    (p)=> {
                        m_PipeList.Remove(p);
                        Recycle(p);
                    });

                m_PipeList.Add(pipe);
            }
        }

 42、在工程中添加 AudioManager 脚本,主要功能是加载指定音频,以及添加AudioSource ,播放指定音频(Sound 拖到 Resources 文件夹下,枚举命名上注意与音频文件对应)

        /// <summary>
        /// 初始化
        /// </summary>
        /// <param name="rootTrans"></param>
        /// <param name="managers"></param>
        public void Init(Transform rootTrans, params object[] managers)
        {
            m_AudioSourceTrans = rootTrans.Find(GameObjectPathInSceneDefine.AUDIO_SOURCE_TRANS_PATH);
            m_ResLoadManager = managers[0] as ResLoadManager;
            m_AudioClipDict = new Dictionary<AudioClipSet, AudioClip>();

            m_AudioSource = m_AudioSourceTrans.gameObject.AddComponent<AudioSource>();

            Load();
        }


        /// <summary>
        /// 播放指定音频
        /// </summary>
        /// <param name="audioName"></param>
        public void PlayAudio(AudioClipSet audioName) {
            if (m_AudioClipDict.ContainsKey(audioName) == true)
            {
                m_AudioSource.PlayOneShot(m_AudioClipDict[audioName]);
            }
            else {
                Debug.LogError(GetType()+ "/PlayAudio()/ audio clip is null,audioName = "+ audioName);
            }
        }
        
        /// <summary>
        /// 加载音频
        /// </summary>
        private void Load() {
            for (AudioClipSet clipPath = AudioClipSet.Collider; clipPath < AudioClipSet.SUM_COUNT; clipPath++)
            {
                AudioClip audioClip = m_ResLoadManager.LoadAudioClip(ResPathDefine.AUDIO_CLIP_BASE_PATH+ clipPath.ToString());
                m_AudioClipDict.Add(clipPath, audioClip);
            }
        }

 43、在工程中添加 UIManager 脚本,主要功能是获取 UI 元素,对应和 Score 添加对应事件,以及 游戏暂停,继续、结束按钮的事件添加

        public void Init(Transform rootTrans, params object[] managers)
        {
            m_ScoreText = rootTrans.Find(GameObjectPathInSceneDefine.UI_SCORE_TEXT_PATH).GetComponent<Text>();
            m_ResumeGameImageGo = rootTrans.Find(GameObjectPathInSceneDefine.UI_RESUME_GAME_IMAGE_PATH).gameObject;
            m_GameOverImageGo = rootTrans.Find(GameObjectPathInSceneDefine.UI_GAME_OVER_IMAGE_PATH).gameObject;
            m_ResumeGameButton = rootTrans.Find(GameObjectPathInSceneDefine.UI_RESUME_GAME_BUTTON_PATH).GetComponent<Button>();
            m_PauseGameButton = rootTrans.Find(GameObjectPathInSceneDefine.UI_PAUSE_GAME_BUTTON_PATH).GetComponent<Button>();
            m_RestartGameButton = rootTrans.Find(GameObjectPathInSceneDefine.UI_RESTART_GAME_BUTTON_PATH).GetComponent<Button>();

            m_DataModelManager = managers[0] as DataModelManager;

            m_GameOverImageGo.SetActive(false);
            m_ScoreText.text = m_DataModelManager.Score.Value.ToString();
            m_DataModelManager.Score.OnValueChanged += OnScroeValueChanged;
            m_ResumeGameButton.onClick.AddListener(OnResumeGameButton);
            m_PauseGameButton.onClick.AddListener(OnPauseGameButton);
            m_RestartGameButton.onClick.AddListener(OnRestartButton);

            m_ResumeGameImageGo.SetActive(true);
            m_IsPause = true;
        }

        public void Update()
        {

        }

        public void Destroy()
        {
            m_RestartGameButton.onClick.RemoveAllListeners();

            m_ScoreText = null;
            m_GameOverImageGo = null;
            m_RestartGameButton = null;
            m_DataModelManager = null;
        }

        public void GameOver()
        {
            m_GameOverImageGo.SetActive(true);

        }

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

        private void OnPauseGameButton()
        {
            m_IsPause = true;
            m_ResumeGameImageGo.SetActive(true);
        }

        private void OnResumeGameButton()
        {
            m_IsPause = false;
            m_ResumeGameImageGo.SetActive(false);
        }

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

44、在工程中添加 GameManager 脚本(单例),主要功能:1)获取场景中相关游戏物体或者 UI根物体,2)new 相关的 Manager 管理类,初始化Init,Update 、和Destroy,3)判断游戏状态,是否暂停、继续、结束

        public void Awake() {
            m_ResLoadManager = new ResLoadManager();
            m_AduioManager = new AudioManager();
            m_SkyTileManager = new SkyTileManager();
            m_GrassTileManager = new GrassTileManager();
            m_BirdManager = new BirdManager();
            m_PipeManager = new PipeManager();
            m_DataModelManager = new DataModelManager();
            m_UIManager = new UIManager();
        }

        public void Start() {
            m_WorldTrans = GameObject.Find(GameObjectPathInSceneDefine.WORLD_PATH).transform;
            m_UITrans = GameObject.Find(GameObjectPathInSceneDefine.UI_PATH).transform;

            Init(null);

            m_IsGameOver = false;
        }

        public void Init(Transform rootTrans, params object[] managers)
        {
            m_DataModelManager.Init(null);
            m_ResLoadManager.Init(null);
            m_AduioManager.Init(m_WorldTrans, m_ResLoadManager);
            m_SkyTileManager.Init(m_WorldTrans, m_ResLoadManager);
            m_GrassTileManager.Init(m_WorldTrans, m_ResLoadManager);
            m_BirdManager.Init(m_WorldTrans, m_ResLoadManager, m_DataModelManager, m_AduioManager);
            m_PipeManager.Init(m_WorldTrans, m_ResLoadManager);
            m_UIManager.Init(m_UITrans, m_DataModelManager);
        }

        public void Update()
        {
            JudgeGamePauseOrResume();
            JudgeGameOver();

            m_DataModelManager.Update();
            m_ResLoadManager.Update();
            m_SkyTileManager.Update();
            m_GrassTileManager.Update();
            m_BirdManager.Update();
            m_PipeManager.Update();
            m_UIManager.Update();
            m_AduioManager.Update();

        }

        public void Destroy()
        {
            m_DataModelManager.Destroy();
            m_ResLoadManager.Destroy();
            m_SkyTileManager.Destroy();
            m_GrassTileManager.Destroy();
            m_BirdManager.Destroy();
            m_PipeManager.Destroy();
            m_UIManager.Destroy();
            m_AduioManager.Destroy();
        }

45、GameStart 脚本,整个游戏的入口,管理对应 GameManager 的 Awake(),Start(),Update(),OnDestroy() ,OnGUI() 对应函数功能

	public class GameStart : MonoBehaviour
	{

        private void Awake()
        {
            GameManager.Instance.Awake();
        }

        // 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();

        }
    }

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

47、运行场景,就会自动生成场景,点击 Game ,开始游戏,Pause 暂停游戏,Bird 撞地面和管子都会游戏结束,每过一个管子都会增加分数

八、工程源码地址

github 地址:https://github.com/XANkui/UnityMiniGameParadise

的 MGP_006FlappyBird 工程

九、延伸扩展

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

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

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

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

4、Bird 得分特效添加

5、Bird 不同分数下的不同状态或者皮肤,抑或速度;

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

7、管子可以出来一些什么东西,或者也有上下动的管子;

8、天空和草地的不同组别,比如春夏秋冬,中国日本埃及不同风格,以增加新奇性

9、等等
 

猜你喜欢

转载自blog.csdn.net/u014361280/article/details/122648699