Unity制作FlappyBird教程1

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

FlappyBirdGame.gif

在開始本教程前,你至少要對(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。

Tags&Layers

當(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ī):
image.png

然后我們將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)。


Hierarchy中的樣子

Scene中的樣子

這樣,我們的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)組件

image.png

添加腳本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ì)稱。

我們創(chuàng)建一個(gè)Pipes空對(duì)象,來作為兩根管的父對(duì)象。并設(shè)置位置和BoxCollider2D。
Hierarchy中的樣子

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

pipe1的Inspector

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


Pipes1的Inspector

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

然后我們添加碰撞檢測(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中的UI對(duì)象

Scene

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


音效

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


image.png
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)論。

FlappyBird 教程2

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容