Unity 游戏实例开发集合 之 FlappyBird (像素鸟) 休闲小游戏快速实现
目录
Unity 游戏实例开发集合 之 FlappyBird (像素鸟) 休闲小游戏快速实现
一、简单介绍
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、把屏幕设置为 1920×1080 作为参考大小,天空显示如图
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、等等