用Unity制作游戲確實(shí)“多快好省”。作為一個(gè)新手,需要大量的練習(xí)制作游戲,才能以最快速度熟悉Unity。本項(xiàng)目的代碼可以在 Github 上歡迎下載。PS: 本教程的代碼位置在 Tag “v1.0”。

在開始本教程前,你至少要對(duì)Unity2D有一個(gè)初步了解。至少要學(xué)習(xí)過了官方的UFO 2D項(xiàng)目。如果沒有學(xué)習(xí)過,強(qiáng)烈建議先學(xué)習(xí)一下。官網(wǎng)上的視頻被墻了,但是目在B站上有搬運(yùn)。
準(zhǔn)備工作
首先新建一個(gè)2D項(xiàng)目,命名為FlappyBird。
然后創(chuàng)建好如下文件夾:
下載好資源,放入我們項(xiàng)目中。我們需要的資源在工程相關(guān)目錄下可以找到。


實(shí)現(xiàn)游戲背景環(huán)境
游戲看起來是小鳥在飛,實(shí)際上,為了方便實(shí)現(xiàn),我們可以理解為小鳥不動(dòng),是背景在往后移動(dòng)。
先找到background資源導(dǎo)入時(shí)修改為Sprite,Sprite Mode: Single,Pixels Per Unit: 32。然后 Apply。
同樣對(duì)land,pipe1,pipe2執(zhí)行類似的操作,知識(shí)注意Pivot錨點(diǎn)的位置。land為Top,pipe1為Top, pipe2為Bottom。更改錨點(diǎn)是為了之后布局更方便。
創(chuàng)建背景和地面
Hierarchy 下創(chuàng)建一個(gè)空游戲?qū)ο?。Hierarchy->Create->Create Empty。命名為Ground,再在其子對(duì)象中創(chuàng)建一個(gè)MovingGround1。將background, land拖入。

接著我們要設(shè)置position: background:(0,2,0)、land(0,-3,0)。
設(shè)置SortLayer: 默認(rèn)情況下,我們的Sprite加入后都是Default層,故我們要給land更高一些。點(diǎn)擊 SortingLayer -> Add Sorting Layer 來增加四個(gè)SortLayer: Background,Pipes,F(xiàn)oreground, Player。然后給background設(shè)為Background,land設(shè)置為Foreground。也可以在菜單Eidt->ProjectSettings->Tags&Layers去設(shè)置。所以我們的圖層排序就是從下到上: Background->Pipes->Foreground->Player。

當(dāng)然你可以都使用Default,然后去更改Order in Layer。在我們這個(gè)小游戲中并沒什么關(guān)系,但是如果游戲復(fù)雜以后,最好使用Sorting Layer。
給MovingGround1添加物理系統(tǒng)組件 :Box Collider 2D組件,Rigidbody 2D組件。

添加Box Collider 2D時(shí),要正好把land上部包含,以便之后能正確的和小鳥發(fā)生碰撞檢測(cè)。
讓我們把背景動(dòng)起來吧!在Scripts下創(chuàng)建MovingGround.cs。并將它們綁定到MovingGround1中。
// MovingGround.cs
public class MovingGround : MonoBehaviour {
public float speed = 2;
private Rigidbody2D rb2D;
void Awake() {
rb2D = GetComponent<Rigidbody2D>();
rb2D.velocity = new Vector2(-speed, 0);
}
void Update() {
//if (GameController.instance.isGameOver) {
// rb2D.velocity = Vector2.zero;
//}
}
}
顧名思義,這個(gè)腳本會(huì)讓物體向后移動(dòng)起來。它之后同樣可以用到我們的其他“不動(dòng)的物體”例如:管道。
現(xiàn)在運(yùn)行一下,看看,我們的背景開始往后移動(dòng)了。但是好像很糟,因?yàn)閳D片比較窄,并沒有把屏幕包含滿。
接下我們?cè)O(shè)置下攝像機(jī):
然后我們將MovingGround1拖入到 Project->Prefabs,作為預(yù)制體。然后Ctrl+D(Mac: CMD+D)復(fù)制另外三個(gè)出來。調(diào)整Transform.Position讓他們橫排排列好。最終MovingGround1,2,3,4的位置為:(-7,-1.7,0)、(2,-1.7,0)、(11,-1.7,0)、(20,-1.7,0)。


這樣,我們的4個(gè)背景形成一個(gè)整體,一起往后移動(dòng)。接下來只要讓他們移動(dòng)到看不見的位置時(shí),又重新移動(dòng)到開始的位置即可。這樣我們的環(huán)境就可以不斷的移動(dòng),永遠(yuǎn)不會(huì)停下來了。
給MovingGround1添加腳本LoopGround.cs,并要Apply一下,讓其他三個(gè)同樣擁有這個(gè)腳本組件。
// LoopGround.cs
public class LoopGround : MonoBehaviour {
private BoxCollider2D groundCollider;
private float boxColliderWidth;
void Start() {
groundCollider = GetComponent<BoxCollider2D>();
boxColliderWidth = groundCollider.size.x;
}
void Update () {
// 到看不見的位置后,立馬移動(dòng)到最初的位置
if (transform.position.x < -2 * boxColliderWidth) {
transform.position = new Vector3(
transform.position.x + 4 * boxColliderWidth,
transform.position.y,
transform.position.z);
}
}
}
很好,我們的背景和地面就完成了。
小鳥
首先同樣我們將資源導(dǎo)入。注意,我們這個(gè)圖片是一個(gè)幀動(dòng)畫的序列圖,需要分割一下。點(diǎn)擊 Sprite Editor。

完成后,點(diǎn)擊Apply,圖片就切好了。

制作玩家對(duì)象。我們將birds_0拖入Hierarchy,并修改為Player,設(shè)置Tag為Player,并設(shè)置位置,設(shè)置Sorting Layer為Player,否則將看不到。

添加動(dòng)畫。選中birds_0,birds_1,birds_2,一起拖到Player上,這時(shí)彈出保存為動(dòng)畫的選項(xiàng)框,保存為BirdFly.anim。并將其存到Assets/Animations下。這時(shí)Animations下多出兩個(gè)文件,一個(gè)是birds_0的AnimatorController文件,一個(gè)BirdFly的Animation文件。我們修改birds_0為Bird,然后雙擊,這時(shí)就會(huì)打開Animator的窗體。默認(rèn)情況下,我們的精靈Player就會(huì)自動(dòng)執(zhí)行BirdFly的動(dòng)畫。
為小鳥添加物體系統(tǒng)組件。

添加腳本Bird.cs
// Brid.cs
public class Bird : MonoBehaviour {
public float upBounce = 300;
private Rigidbody2D rb2D;
private void Awake() {
rb2D = GetComponent<Rigidbody2D>();
anim = GetComponent<Animator>();
}
private void Update() {
if (Input.GetKeyDown(KeyCode.Space) || Input.GetMouseButtonDown(0)) {
Fly();
}
}
private void OnCollisionEnter2D(Collision2D collision) {
Debug.Log("Bird Dead, Game Over!");
}
private void Fly() {
rb2D.velocity = Vector2.zero;
// 添加一個(gè)向上的力,使得小鳥向上飛
Vector2 upForce = Vector2.up * upBounce;
rb2D.AddForce(upForce);
//SoundManager.instance.PlayFly();
}
private void Die() {
rb2D.velocity = Vector2.zero;
//GameController.instance.GameOver();
}
}
這時(shí),我們運(yùn)行看看,我們的小鳥可以自由自在的飛翔了。就下來就只差管道了。
障礙物:管道
管道在之前已經(jīng)被導(dǎo)入了,由于我們的圖片資源一個(gè)向下,一個(gè)向上,所以特地設(shè)定了Pivot為Bottom和Top。這樣我們將它們組成一個(gè)整體時(shí),可以以中心對(duì)稱。

這里記得將MovingGround.cs腳本組件添加到Pipes上,這樣管道才會(huì)跟著背景一起移動(dòng)。注意,我們要為pipe1,pipe2的SortLayer設(shè)置為Background。


我們要為Pipes父對(duì)象添加碰撞檢測(cè)區(qū)域和剛體組件。

最后的樣子如下,其中標(biāo)記的1就是小鳥在通過時(shí),可以檢測(cè)到小鳥通過了,可以進(jìn)行加分的功能。

然后我們添加碰撞檢測(cè)的邏輯,實(shí)現(xiàn)這個(gè)加分吧。給Pipes添加自定義腳本Pipes.cs
// Pipes.cs
public class Pipes : MonoBehaviour {
private void OnTriggerEnter2D(Collider2D collision) {
// 管道的中間位置的探測(cè)區(qū)域,如果碰撞物體為小鳥(被標(biāo)記為了Player),計(jì)分
if (collision.tag == "Player") {
Debug.Log("You are good!");
//GameController.instance.PassOnePip();
//SoundManager.instance.PlayPass();
}
}
}
好的,完成了,運(yùn)行一下。(好吧,我看著總覺得會(huì)有馬里奧從管道里爬出來。:)
游戲管理
項(xiàng)目到這里為止,我們還只有一個(gè)管道。隨著場景的移動(dòng),我們當(dāng)然需要不停的添加管道。這里我們添加一個(gè)GameController。
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class GameController : MonoBehaviour {
public static GameController instance = null;
// 管道預(yù)制件
public GameObject pipesPrefabs;
public Text countText;
public GameObject gameOverTips;
// 管道產(chǎn)生的頻率,每幾秒產(chǎn)生一個(gè)
public float createPipesRate = 3f;
// 管道中心位置的y最小值
public float minPipPosY = -1f;
// 管道中心位置的y最大值
public float maxPipPosY = 4f;
// 初始化管道的位置,x最好為負(fù)數(shù)不可見位置
public Vector2 startPipPos = new Vector2(-12f, 0f);
// 統(tǒng)計(jì)已經(jīng)成功過了幾個(gè)管道
private int count = 0;
// 小鳥是否已經(jīng)死了
[HideInInspector] public bool isGameOver;
// 上一次創(chuàng)建出管道的時(shí)間
private float lastCreatePipTime = float.NegativeInfinity;
// 緩存管道的鏈表,用來復(fù)用管道
private List<GameObject> pipes = new List<GameObject>();
// 管道緩存的個(gè)數(shù)
private const int PIPESTOTAL = 8;
// 當(dāng)前管道下標(biāo),用來更新管道
private int currPipesIndex = 0;
private void Awake() {
if (instance == null) {
instance = this;
} else if (instance != this) {
Destroy(gameObject);
}
}
private void Start() {
isGameOver = false;
gameOverTips.SetActive(false);
// 開始時(shí),創(chuàng)建出管道緩存
InitPipesPool();
}
private void Update() {
// 當(dāng)可以創(chuàng)建出管道時(shí),拿出緩存中一個(gè)管道來更新位置
if (!isGameOver && lastCreatePipTime + createPipesRate < Time.time) {
lastCreatePipTime = Time.time;
UpdatePipesPosition();
currPipesIndex = (currPipesIndex + 1) % PIPESTOTAL;
}
if (isGameOver && Input.GetKeyDown(KeyCode.Space)) {
GameRestart();
}
}
public void PassOnePip() {
count++;
countText.text = "Count: " + count.ToString();
}
public void GameOver() {
if (isGameOver) return;
isGameOver = true;
gameOverTips.SetActive(true);
}
private void GameRestart() {
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
}
// 初始化管道緩存池
private void InitPipesPool() {
for (int i = 0; i < PIPESTOTAL; ++i) {
GameObject obj = Instantiate(pipesPrefabs, startPipPos, Quaternion.identity);
pipes.Add(obj);
}
}
// 更新當(dāng)前管道的位置
private void UpdatePipesPosition() {
float randomPosY = Random.Range(minPipPosY, maxPipPosY);
Vector2 position = new Vector2(10f, randomPosY);
pipes[currPipesIndex].transform.position = position;
}
}
GameController管理了一個(gè)Pipes的管道池來復(fù)用管道,就不用不停的創(chuàng)建和銷毀了。通過一次創(chuàng)建,之后每隔createPipesRate秒鐘就刷新一個(gè)管道的位置,讓他移動(dòng)到適合的位置。
然后我們需要將Hierarchy中的Pipes拖到Project/Prefabs作為預(yù)制體。Hierarchy中的需要?jiǎng)h掉。之后管道就都由GameController來管理了。
添加基本UI。我們像 UFO項(xiàng)目一樣添加基本的UI。這里就不詳細(xì)解釋了。


Hierarchy創(chuàng)建一個(gè)空游戲?qū)ο?,命名為GameController。并將上面GameController.cs腳本組件關(guān)聯(lián)上。并將內(nèi)容補(bǔ)充完整。

音效
最后,我們添加一些簡單的音效吧。新建一個(gè)SoundManager的空游戲?qū)ο?,添加AudioSource組件,并創(chuàng)建腳本SoundManager.cs。

using UnityEngine;
public class SoundManager : MonoBehaviour {
public static SoundManager instance = null;
public AudioClip flyClip;
public AudioClip dieClip;
public AudioClip pointClip;
private AudioSource audioSource;
void Awake() {
if (instance == null) {
instance = this;
} else if (instance != this) {
Destroy(gameObject);
}
audioSource = GetComponent<AudioSource>();
}
public void PlayFly() {
audioSource.clip = flyClip;
audioSource.Play();
}
public void PlayDie() {
audioSource.clip = dieClip;
audioSource.Play();
}
public void PlayPass() {
audioSource.clip = pointClip;
audioSource.Play();
}
}

將之前代碼里SoundManager有關(guān)代碼反注釋。最后,我們運(yùn)行。
到這里,我們的FlappyBird就基本完成了。給自己點(diǎn)一個(gè)大大的贊吧!:)。如果有什么問題,歡迎在下面評(píng)論。