最近我正在開發一款 Android 手機遊戲,以塔防的概念為主,名叫 30Days - The Village Defense。 其中,AI 角色的行為 (接受任務、執行任務、殺敵...etc),是遊戲中主要的功能,在程式碼方面來看,要是每個角色都要寫一次走路、死亡、攻擊、閒置,會爆炸,因此重新整理了角色的架構圖。

*綠色開始往上算的 (綠、紅、藍) 是繼承的意思,綠色繼承紅色,紅色繼承藍色的類別。

終端行為的程式寫法

這是村民在遊戲中的行為腳本,只包含移動習慣、執行任務(砍樹任務)。 目前程式碼長的樣子:

using UnityEngine;  
using System.Collections;

public class VillagerAI : Character { //繼承 Character 角色類別

    public int homePosition = 0;
    private Vector2 HOME;
    public override void setup()
    {
        this.blood = 100; //血量
        this.attackPower = 0; //攻擊力
        this.character_type = "villager"; //設定該物件 Prefab 為村民

        HOME = new Vector2(homePosition, transform.position.y); //設定村落位置
    }


    public override IEnumerator behaviors() //覆寫行為執行緒
    {
        int num = Random.Range(1,3); //隨機左右走路
        if(num == 1)
        {
            right(); //父物件的方法
            yield return new WaitForSeconds(Random.Range(0.8f,8)); //重點1: 隨機等待時間
            stopRight();
        }
        else
        {
            left();
            yield return new WaitForSeconds(Random.Range(0.8f, 8));
            stopLeft();
        }

        yield return new WaitForSeconds(Random.Range(2, 8));

        // 隨機機率,回家
        int home_chance = Random.Range(1, 6);
        if(home_chance == 4)
        {
            while (Mathf.Abs((int)transform.position.x) > Mathf.Abs((int)HOME.x + 5))
            { //重複執行到回到家,誤差值 5,誤差值是因為 yield 等待的時間不一定,
                string way = homeDirection();
                if (way == "left")
                {
                    left();
                }
                else
                {
                    right();
                }
                yield return new WaitForSeconds(Random.Range(2, 4));
                veryStop();

                yield return new WaitForSeconds(Random.Range(1, 4));
            }
            veryStop();
        }

        loop(); //重複執行這個執行緒
    }

    public string homeDirection() { //家在我的左方還是右方
        if(transform.position.x > HOME.x)
        {
            return "left";
        }else
        {
            return "right";
        }
    }


    public override IEnumerator executeTarget(string targetType, GameObject _obj)
    { //執行砍樹任務
        string targetDirection = null;
        while (!base.isRayCastTargetDetect(_obj.GetInstanceID()) && base.isTargetAlivable())
        {
            targetDirection = base.targetDirection();
            if (targetDirection == "left")
            {
                left();
                yield return new WaitForSeconds(1);
                stopLeft();
            }
            if (targetDirection == "right")
            {
                right();
                yield return new WaitForSeconds(1);
                stopRight();
            }
        }
        veryStop();
        yield return new WaitForSeconds(0.2f);
        //walk to forward
        if (targetDirection == "left")
        {
            left();
            yield return new WaitForSeconds(0.8f);
            stopLeft();
        }
        if (targetDirection == "right")
        {
            right();
            yield return new WaitForSeconds(0.8f);
            stopRight();
        }
        if(targetType == "Tree")
        {
            attack();
            yield return new WaitForSeconds(5);
            _obj.GetComponent<ObjectBehaviour>().dead();
            yield return new WaitForSeconds(1);
            stopAttack();
            clearTarget();
        }
    }
}

我不可以把腳本寫在 Update,那樣會更難控制角色,所以 Update 在父物件就已經做了,那是座移動的開關而已,也就是我只利用開關來移動角色左或右和動畫,那會使得更有效率一些。

因此,在執行緒被呼叫的時候,不會隨著 Update 的速率來控制程式碼,我只需要控制開關就可以了,如此簡單的做法,搭配執行緒還可以做很多次的 yield 轉讓時間 yield return new WaitSeconds() 來讓程式碼完全同步化,所以可以直接在行緒裡頭寫 while 來重複執行一件事,太棒了。

啟動與中斷執行緒

Unity 引擎中的時間線大致上有這幾種,每種的時間速率控制都不一樣。
我這裡則是使用 Coroutine 的時間線,不會隨著 Update 改變,所以要控制 Update 中的變數,只能靠全域變數改變來實現。

但若需要中斷執行緒,要從下一次 Update 被執行時,開關才會被偵測,就可以關閉目前的任務,執行新任務。 不過這也需要將所有的角色移動開關都關掉,否則執行新任務時,可能左走路跟右走路開關一起打開了。

執行緒中的迴圈陷阱

這裡已經固定執行緒是完全同步的行為,因此要執行一個砍樹的任務,執行緒也不會重複被呼叫 loop(),而是在執行緒中寫了 while(isHome()) 類似的概念,其中如果 while 不是計算,那你最好寫等待 yield return new WaitForSeconds(),否則你可能要重新啟動 Unity 了。

而我剛剛談到的 yield return new WaitForSeconds() 則是一個很大的陷阱,要是你的遊戲中寫了 RayCast 射線,而要在迴圈中判斷是否打到物體,你可要小心了,因為要是等待時間過長,角色很可能就會直接走超過目標的物體,造成不斷來回走,射線永遠打不到物體的 Bug。

解決這個問題,只要把等待時間變短就可以了,考慮 RPG 遊戲走路速度的話,最好不要超過 1秒。 yield return new WaitForSeconds(0.5f)

總結

至於標題打上 像Arduino 是因為這串程式寫起來,令我十分聯想到要在 Arduino IDE 中,要不斷地寫非同步的判斷達到需求,而這裡的腳本則強化了很多模式。

總之,還是繼續肝遊戲開發吧。

測試錄影

錄影是用 GC550 LGX 擷取盒。