1. 程式人生 > 其它 >用Unity製作一個簡單的大魚吃小魚遊戲

用Unity製作一個簡單的大魚吃小魚遊戲

完成效果:

 

0.一些準備工作

簡單準備了一些魚的圖片素材和一張海底的背景圖

將這些魚的圖片拖動到遊戲中,建立一個擁有簡單動畫效果的遊戲物件。將它們的z軸都設為0。

將背景也拖入遊戲,並將z軸改為1。

將攝像機的Projection改為Orthographic。

 

1.控制魚的移動

接下來我們需要控制魚的移動。新建指令碼Fish,在update裡獲得玩家的輸入輸出。

    void Update()
    {
        Vector3 playerInput = new Vector3(0, 0, 0);
        playerInput.x = Input.GetAxis("Horizontal");
        playerInput.y = Input.GetAxis("Vertical");

        transform.localPosition += playerInput;
    }

將指令碼掛在一條魚上,我們發現我們已經可以控制魚的移動了,但此時我們的移動過快了,需要做一些修改。

給Fish新增屬性speed,再將playerInput乘上speed和Time.deltaTime。

public float speed = 5f;

void Update()
{
    Vector3 playerInput = new Vector3(0, 0, 0);
    playerInput.x = Input.GetAxis("Horizontal");
    playerInput.y = Input.GetAxis("Vertical");
    transform.localPosition += playerInput * speed * Time.deltaTime;
}

現在我們可以移動的更慢一些,每一幀不再移動那麼多的距離。同時,我們現在可以通過調節speed來控制移動速度。

再為我們的移動增加一個加速的過程。

增加兩個屬性velocity和desiredVelocity,表示魚當前的速度和我們期望的速度,並在awake裡將它們初始化。

Vector3 velocity;
Vector3 desiredVelocity;

void Awake()
{
    velocity = new Vector3(0, 0, 0);
    desiredVelocity = new Vector3(0, 0, 0);
}

增加屬性acceleration,表示魚的加速度。

public float acceleration = 1f;

 修改魚的移動,將transform.localPosition += playerInput * speed * Time.deltaTime;改為

desiredVelocity = playerInput * speed;
velocity = Vector3.MoveTowards(velocity, desiredVelocity, acceleration);
transform.localPosition += velocity * Time.deltaTime;

現在魚雖然已經可以移動,但還無法切換移動方向,會出現向後移動的情況。我們可以通過尤拉角改變魚的方向。將剛剛的移動修改為

desiredVelocity = playerInput * speed;
velocity = Vector3.MoveTowards(velocity, desiredVelocity, acceleration);

if (velocity.x > 0) transform.localEulerAngles = new Vector3(0, 0, 0);
else if (velocity.x < 0) transform.localEulerAngles = new Vector3(0, 180, 0);

transform.localPosition += velocity * Time.deltaTime;

 魚的移動基本完成了。

 

2.創造更多的魚

在這個遊戲中,我們需要源源不斷的生成新的不同的魚,因此讓我們建立一個新的指令碼FishFactory,來為我們生產魚類。

FishFactory需要繼承ScriptableObject而不是MonoBehaviour,我們還要在類前面加上[CreateAssetMenu]。

[CreateAssetMenu]
public class FishFactory : ScriptableObject

現在,我們可以在Assets裡,右鍵->Create找到Fish Factory並建立它。

新增一個屬性fishes,來存放生產的魚類的預製體。

public Fish[] fishes;

新增get方法,輸入序號來獲得例項化的魚類。

public Fish Get(int id) {
    Fish fish = Instantiate(fishes[id]);
    return fish;
}

回到場景,將我們之前用素材製作的魚全部加上Fish指令碼,並新增碰撞體。我這裡是用的Box Collider 2D,並將邊緣調小了點。

當兩隻魚正面相遇時,我們希望它們穿過對方繼續向前(不考慮吃掉的情況下),所以我們將這些碰撞體都勾上Is Trigger。

為所有的魚新增剛體。將BodyType改為Kinematic,使其不受重力影響。

將所有的魚拖入Assets,儲存為預製體。

將其按大小從小到大拖入到Fish Factory中。

建立一個新的指令碼Control,來控制遊戲。

首先要新增一個FishFactory屬性。

public FishFactory fishFactory;

新增lastTime和delayTime屬性,來使魚能隔一段時間生成一次。

public float delayTime = 3f;
float lastTime = 0f;

先填寫update方法,來讓遊戲在(0,0,0)的地方生成新的魚。

void Update()
{
    lastTime += Time.deltaTime;
    if (lastTime >= delayTime){
        lastTime = 0f;
        Fish fish = fishFactory.Get(0);
        fish.transform.localPosition = new Vector3(0, 0, 0);
    }
}

建立空物體GameControl,並將Control指令碼掛在上面,將之前建立的FishFactory掛在指令碼上。

此時我們運行遊戲,已經可以看到螢幕中間不斷出現新的魚了。

 

3.魚的活動範圍

接下來,我們要把我們的魚的移動限制在我們的攝像機視野內。

修改Fish指令碼,我們要新增兩個屬性leftBottomPoint, rightTopPoint,來記錄攝像機投射到螢幕上的左下角和右上角的座標。

Vector3 leftBottomPoint, rightTopPoint;

在Awake中,使用ViewportToWorldPoint函式來將視口座標轉換為世界座標。

void Awake()
{
    ......
        
    leftBottomPoint = Camera.main.ViewportToWorldPoint(new Vector3(0f, 0f, 
            Mathf.Abs(Camera.main.transform.position.z)));
    rightTopPoint = Camera.main.ViewportToWorldPoint(new Vector3(1f, 1f,
            Mathf.Abs(Camera.main.transform.position.z)));
}

編寫一個新的函式Clamp,來約束魚的移動。當魚的座標要超出邊框時,將魚的座標移到邊框上並把相應速度置0。

void Clamp() {
    Vector3 position = transform.localPosition;
    if (position.x > rightTopPoint.x) {
        position.x = rightTopPoint.x;
        velocity.x = 0;
    }
    else if (position.x < leftBottomPoint.x) {
        position.x = leftBottomPoint.x;
        velocity.x = 0;
    }
    if (position.y > rightTopPoint.y) {
        position.y = rightTopPoint.y;
        velocity.y = 0;
    }
    else if (position.y < leftBottomPoint.y) {
        position.y = leftBottomPoint.y;
        velocity.y = 0;
    }
    transform.localPosition = position;
}

在魚移動後,呼叫這個函式來約束魚的位置。

transform.localPosition += velocity * Time.deltaTime;
Clamp();

現在魚已經不能超出攝像機的視野範圍了。

 

4.魚的生成方式

在這之前,遊戲已經可以源源不斷的生成新的魚在螢幕中央了,並都由我們控制,但這顯然不是我們所期望的。我們希望能夠有魚在外界不斷生成並游到我們的視野範圍,而我們只需要控制一條魚來和它們互動。

所以,讓我們先移除Fish中的玩家輸入部分。將所有和playerInput有關的語句刪掉。

//Vector3 playerInput = new Vector3(0, 0, 0);
//playerInput.x = Input.GetAxis("Horizontal");
//playerInput.y = Input.GetAxis("Vertical");

//desiredVelocity = playerInput * speed;

給Fish新增一個SetDesiredVelocity函式,讓我們在外部僅給魚一個移動方向,魚就可以向該方向移動。

public void SetDesiredVelocity(Vector3 Input){
    desiredVelocity = Input * speed;
}

如前文所說,這個輸入應該是一個方向。所以我們用ClampMagnitude函式約束它,讓它不大於1。

Vector3.ClampMagnitude(Input, 1f);
desiredVelocity = Input * speed;

在Control中新增屬性player,用來控制玩家操作的魚

Fish player;

在Awake函式中例項化它。

void Awake()
{
    player = fishFactory.Get(1);
    player.transform.localPosition = new Vector3(0, 0, 0);
}

新增PlayerUpdate函式,來讓玩家操控魚。

void PlayerUpdate() {
    Vector3 playerInput = new Vector3(0, 0, 0);
    playerInput.x = Input.GetAxis("Horizontal");
    playerInput.y = Input.GetAxis("Vertical");
    player.SetDesiredVelocity(playerInput);
}

將PlayerUpdate放入update函式中。

void Update()
{
    PlayerUpdate();
    ...
}

現在我們只能控制遊戲最開始生成在螢幕中央的魚了。

接下來我們要修改其他魚的生成方式,首先刪掉原來測試時生成魚的程式碼。

if (lastTime >= delayTime) {
    lastTime = 0f;
    //Fish fish = fishFactory.Get(0);
    //fish.transform.localPosition = new Vector3(0, 0, 0);
}

新增新的函式MakeFish

void MakeFish() {
    Fish fish = fishFactory.Get(0);
}

之前說過我們希望其他魚一開始生成在邊框外。因此Control也需要獲得螢幕邊框的位置。和上面一樣,給Control新增leftBottomPoint, rightTopPoint屬性,並在Awake裡用ViewportToWorldPoint函式獲取座標。這裡就不貼程式碼了,畢竟和上面一樣。

回到MakeFish函式,我們用Random.Range函式來隨機決定新的魚是在螢幕左邊還是右邊,以及魚的座標。

Fish fish = fishFactory.Get(0);
if (Random.Range(0, 2) == 0) {
    fish.transform.position = new Vector3(leftBottomPoint.x - 2f,
                Random.Range(leftBottomPoint.y, rightTopPoint.y), 0f);
}
else {
    fish.transform.position = new Vector3(rightTopPoint.x + 2f,
                Random.Range(leftBottomPoint.y, rightTopPoint.y), 0f);
}

給左邊生成的魚一個向右的方向,給右邊生成的魚一個向左的方向。

Fish fish = fishFactory.Get(0);
if (Random.Range(0, 2) == 0) {
    fish.transform.position = new Vector3(leftBottomPoint.x - 2f,
                Random.Range(leftBottomPoint.y, rightTopPoint.y), 0f);
    fish.SetDesiredVelocity(Vector3.right);
}
else {
    fish.transform.position = new Vector3(rightTopPoint.x + 2f,
                Random.Range(leftBottomPoint.y, rightTopPoint.y), 0f);
    fish.SetDesiredVelocity(Vector3.left);
}

在update函式中呼叫MakeFish函式。

if (lastTime >= delayTime) {
    lastTime = 0f;
    MakeFish();
}

但還有一個問題。我們剛剛限制了魚的活動範圍,但現在我們又希望不由我們控制的魚可以從螢幕外進來並離開螢幕。我們可以利用Tag來區分它們。

在Awake中,將我們生成的魚的Tag設為“Player”

player = fishFactory.Get(1);
player.transform.localPosition = new Vector3(0, 0, 0);
player.tag = "Player";

來到Fish指令碼,在update函式裡的Clamp();前面,新增限制條件

if(this.tag == "Player") Clamp();

我們還需要讓其他的魚在離開屏幕後死亡,防止生成的魚過多。因為這些魚只會往一個方向移動,方法有很多。我這裡的處理方法是讓魚在進入螢幕時標記,在離開螢幕時死亡。

使用一個屬性isInScreen,記錄是否進入螢幕。

bool isInScreen = false;

建立新的函式Die。當魚進入螢幕時,將isInScreen的值變為true。

void Die() {
    float x = transform.position.x;
    float y = transform.position.y;
    if ((x >= leftBottomPoint.x && x <= rightTopPoint.x) &&
        (y >= leftBottomPoint.y && y <= rightTopPoint.y)) {
        isInScreen = true;
    }
}

當魚進入螢幕並離開時,刪去它。

else {
    if (isInScreen) {
        Destroy(this.gameObject);
    }
}

在update裡呼叫Die函式

if (this.tag == "Player") Clamp();
else Die();

現在這些魚會被刪去了,但它們在中心觸碰邊框後就立即消失了。我們不希望這樣。修改Die函式,讓它在2s後消失。

Destroy(this.gameObject, 2f);

這樣,魚的生成邏輯基本完成了。

 

5.魚的等級與互動

現在,我們要實現這個遊戲最關鍵的部分——“大魚吃小魚”。

首先,給Fish指令碼增加一個屬性level,表示魚的大小等級。

int level;

新增函式SetLevel,設定魚的等級。

public void SetLevel(int level) {
    this.level = level;
}

新增函式GetLevel,獲得魚的等級。

public int GetLevel() {
    return this.level;
}

來到FishFactory指令碼。之前放進這些魚的預製體時,我們是按照從小到大的順序放的,所以陣列中的編號就是魚的等級。我們在Get函式裡直接修改魚的等級。

public Fish Get(int id) {
    Fish fish = Instantiate(fishes[id]);
    fish.SetLevel(id);
    return fish;
}

回到Fish。用OnTriggerEnter2D函式檢測碰撞。我們設定當玩家的等級小於等於這條魚的等級時,玩家會被吃掉;當玩家的等級大於這條魚的等級時,這條魚會被玩家吃掉。

private void OnTriggerEnter2D(Collider2D other)
{
    if (other.tag == "Player") {
        Fish player = other.GetComponent<Fish>();
        if (player.GetLevel() <= level) {
            Destroy(other.gameObject);
        }
        else {
            Destroy(this.gameObject);
        }
    }
}

接下來,我們要新增魚的升級機制。

首先,在Control腳本里新增一個屬性score,來記錄吃魚的量。使用public和static識別符號,我們之後可以直接在Fish腳本里更改得分。

public static int score = 0;

我們還要新增一個屬性playerLevel,表示玩家等級。這個等級我們初始化為1,因為按我們的規則,玩家控制的魚只能吃比他小的魚,所以玩家的魚不能是最小的。

int playerLevel = 1;

新增一個屬性maxLevel,表示最大等級。它和FishFactory裡陣列的大小有關,我們可以在Awake裡初始化它。

int maxLevel;

void Awake() {
    maxLevel = fishFactory.fishes.Length - 1;
    ...
}

建立新的函式MakePlayerFish,來根據玩家等級生成對應的魚。

首先我們要刪去舊的魚,刪之前要保留下它的位置和方向。

void MakePlayerFish() {
    Vector3 playerPosition = new Vector3(0f, 0f, 0f);
    Vector3 playerEulerAngles = new Vector3(0f, 0f, 0f);
    if (player != null) {
        playerPosition = player.transform.localPosition;
        playerEulerAngles = player.transform.localEulerAngles;
        Destroy(player.gameObject);
    }
}

 之後再根據等級生成新的魚。

player = fishFactory.Get(playerLevel);
player.transform.localPosition = playerPosition;
player.transform.localEulerAngles = playerEulerAngles;
player.tag = "Player";

 現在可以將Awake裡原來生成玩家魚的程式碼換成MakePlayerFish函式。

//player = fishFactory.Get(0);
//player.transform.localPosition = new Vector3(0, 0, 0);
//player.tag = "Player";
MakePlayerFish();

新增屬性levelLine,表示每次升級需要的吃魚數。

public int levelLine = 20;

新增函式LevelUpdate,我們將(score/levelLine)與當前的playerLevel相比較,檢測是否需要升級。因為playerLevel開始是1,所以我們給(score/levelLine)+1。

void LevelUpdate() {
    if ((score / levelLine + 1) > playerLevel) {
        playerLevel++;
        MakePlayerFish();
    }
}

將LevelUpdate函式新增到update裡。

void Update()
{
    LevelUpdate();
    PlayerUpdate();
}

更改Fish腳本里的OnTriggerEnter2D函式。當魚被吃掉時,加一得分。

else {
    Control.score++;
    Destroy(this.gameObject);
}

現在魚已經可以升級了。但是還有一個隱患。玩家的等級可能超過最大等級。回到LevelUpdate函式,做一下修改。

void LevelUpdate() {
    if ((score / levelLine + 1) > playerLevel && (playerLevel < maxLevel)) {
        playerLevel++;
        MakePlayerFish();
    }
}

我們已經完成了玩家魚的部分,但隨機生成的魚依然是固定的。因此我們還需要修改MakeFish。

刪掉原來的Fish fish = fishFactory.Get(0);,根據概率生成大魚小魚。我這邊設定是三分之二生成比玩家小一級的魚,六分之一生成比玩家大一級的魚(如果有的話),剩下的概率生成和玩家同級的魚。

//Fish fish = fishFactory.Get(0);
Fish fish;
if (Random.Range(0, 3) != 0) {
    fish = fishFactory.Get(playerLevel - 1);
}
else if (Random.Range(0, 2) == 0 && playerLevel < maxLevel) {
    fish = fishFactory.Get(playerLevel + 1);
}
else {
    fish = fishFactory.Get(playerLevel);
}

 

至此,一個簡單的大魚吃小魚就完成了。雖然這裡只有三種魚,但只要素材足夠,按大小放入FishFactory中,就可以給遊戲加入更多等級的魚。