VR游戲開(kāi)發(fā)303 開(kāi)發(fā)一款VR彈球游戲(下)

開(kāi)發(fā)一款VR彈球游戲(下)

作者按:感謝大家對(duì)我的支持,甚至有出版社的主任跟我聯(lián)系要出書(shū),尤為受寵若驚。這個(gè)系列的文章我會(huì)一直寫(xiě)下去,一直更新下去的。

在上一節(jié)的學(xué)習(xí)中,我們基本上完成了這個(gè)游戲,可以實(shí)現(xiàn)基本的游戲核心玩法。但還缺少的一個(gè)主要環(huán)節(jié)就是你的對(duì)手。那么這一節(jié)我們將主要講解如何實(shí)現(xiàn)一個(gè)簡(jiǎn)單的AI對(duì)手,和一些游戲后期的處理問(wèn)題。

思考:對(duì)手和你有什么不同

這個(gè)問(wèn)題很值得商榷,只有搞清對(duì)手和你的不同才能快速的通過(guò)已有的玩家來(lái)創(chuàng)建一個(gè)對(duì)手。
首先,對(duì)手和玩家的操控方式不同。玩家是通過(guò)用戶的輸入來(lái)驅(qū)動(dòng),而你的對(duì)手是通過(guò)編寫(xiě)的AI腳本,或是網(wǎng)絡(luò)中的對(duì)手來(lái)控制的(以后確實(shí)有編寫(xiě)網(wǎng)絡(luò)對(duì)戰(zhàn)的意圖)。
其次,由于一些設(shè)計(jì)上的問(wèn)題,在這個(gè)應(yīng)用場(chǎng)景中,我們的碰撞體的反射設(shè)定并沒(méi)有做到設(shè)計(jì)的完美,造成需要對(duì)player的一些參數(shù)進(jìn)行調(diào)整。
(由于這篇教程是邊做邊寫(xiě),在上一篇的教程里就遇到了推翻以前的部分設(shè)計(jì)的問(wèn)題,所以希望和大家交流一下。)
那么明白了以上的區(qū)別之后,我們便可以開(kāi)始對(duì)手AI的設(shè)計(jì)了。

創(chuàng)建一個(gè)你的敵人

首先,我們?cè)趐roject面板中找到_Prefab文件夾下的player這個(gè)預(yù)設(shè),將它復(fù)制一份(快捷鍵:Ctrl+D),并重命名為Enemy。將Enemy拖到場(chǎng)景中,放到合適的位置。設(shè)置完成之后,如下圖


敵人的設(shè)置

為了保持Enemy的碰撞體的設(shè)置,我們將Enemy這個(gè)物體繞著Y軸旋轉(zhuǎn)180°,即將它的Rotate設(shè)置為(0,180,0)。
選中Enemy物體,點(diǎn)擊右上方檢視面板中的Tag下拉列表,點(diǎn)擊AddTag。


添加Tag

點(diǎn)擊下方的“+號(hào)”新建一個(gè)Tag叫Enemy
給新的Tag重命名

建立完成之后,在階層面板中再次選中Enemy,在Tag中選擇Enemy這一項(xiàng)。


將Tag賦予你的Enemy物體

將Enemy上的Player Movement腳本刪除。新建一個(gè)腳本EnemyMove.cs并賦予Enemy物體。
EnemyMove.cs
public class EnemyMove : MonoBehaviour {
    private GameObject ball;
    private float thisX;
    // Use this for initialization
    void Start () {
        ball = GameObject.FindGameObjectWithTag("Ball");
        thisX = this.transform.position.x;
    }
    
    // Update is called once per frame
    void Update () {
    
    }

    void FixedUpdate()
    {
        transform.position = new Vector3(ball.transform.position.x + thisX, transform.position.y, transform.position.z);
    }
}

點(diǎn)擊Play測(cè)試一下。


測(cè)試畫(huà)面

WOW,敵人好強(qiáng)!簡(jiǎn)直就是一面墻一樣,完全沒(méi)法戰(zhàn)勝!而且當(dāng)球飛出去的時(shí)候,敵人也跟著飛出去了啊。好吧,很明顯這樣的對(duì)手AI是十分不合理的。(這里根本就沒(méi)有AI好吧)。既然這樣,我們需要給敵人設(shè)置一個(gè)移動(dòng)速度,這樣他就不會(huì)飛一般地去接球了。
修改以上腳本,修復(fù)這兩個(gè)問(wèn)題。

EnemyMove.cs
public class EnemyMove : MonoBehaviour {
    public float speed=0.15f;

    private GameObject ball;
    private float thisX;
    // Use this for initialization
    void Start () {
        ball = GameObject.FindGameObjectWithTag("Ball");
        thisX = this.transform.position.x;
    }
    
    // Update is called once per frame
    void Update () {
    
    }

    void FixedUpdate()
    {
        if(ball.transform.position.x<5.4&&ball.transform.position.x>-5.4)//判斷球在場(chǎng)地中
        {
            if(ball.transform.position.x-transform.position.x+thisX>0)//球在左邊
            {
                transform.Translate(Vector3.left * speed);
            }
            else if(ball.transform.position.x - transform.position.x + thisX < 0)
            {
                transform.Translate(Vector3.right * speed);
            }
        }
    }
}

再次點(diǎn)擊Play按鈕運(yùn)行測(cè)試,怎么樣,我可以贏了啊!但是為什么對(duì)手的動(dòng)作看起來(lái)那么別扭呢,一直在跟著球在運(yùn)動(dòng)。正常情況下玩游戲的話,我應(yīng)該是將球擊出去之后就回到中間去,這樣當(dāng)球回來(lái)的時(shí)候,擊球才方便,這是一個(gè)策略。還有的就是預(yù)判,對(duì)球的軌跡進(jìn)行預(yù)先的估計(jì),估計(jì)球快要到你的位置的時(shí)候球在哪,然后事先去哪個(gè)位置。還有,當(dāng)球的速度設(shè)置的比對(duì)手移動(dòng)的速度要小的時(shí)候,對(duì)手就成了無(wú)敵的了?
以上的問(wèn)題,就不一一解決了,這里先提出一個(gè)思路。
首先是解決一直跟著球走的問(wèn)題,這個(gè)問(wèn)題可以使用Unity的Broadcast系統(tǒng),在每次玩家擊球和對(duì)手擊球的時(shí)候使用Collider上的腳本Send一條消息,然后AI再根據(jù)這條消息判斷是誰(shuí)的擊球,如果是對(duì)手(Enemy物體)的擊球的話,Enemy物體回到平臺(tái)中間(相對(duì)的只移動(dòng)X的坐標(biāo)),在玩家擊球之后,Enemy的AI再進(jìn)行相應(yīng)的判斷?!?br> 關(guān)于預(yù)判的問(wèn)題,可以在擊球的一瞬間從球沿著球的移動(dòng)方向使用Raycast來(lái)發(fā)射一條射線,當(dāng)射線碰撞到Edge的時(shí)候,在碰撞點(diǎn)沿著反射角再發(fā)射一條射線,直到射線Hit到Enemy一側(cè)的Collider為止(這個(gè)Collider跟我們一開(kāi)始設(shè)置的那面墻很相似)。至此,我們的AI可以完美的預(yù)判球?qū)⒁降奈恢昧耍@樣又和開(kāi)掛有什么區(qū)別了呢?人用的是肉眼和經(jīng)驗(yàn)來(lái)預(yù)判碰撞位置,而AI則是直接通過(guò)游戲引擎來(lái)獲取碰撞位置,這絕對(duì)是作弊!所以,我們要開(kāi)始弱化AI。通過(guò)給碰撞點(diǎn)加一個(gè)隨機(jī)數(shù)范圍,如(-10到10)來(lái)表示判斷的誤差,然后再在判斷的誤差上面加上失誤的誤差如(-5到5)來(lái)調(diào)整游戲的平衡。
如果讀者們有興趣來(lái)實(shí)現(xiàn)這些的話,完全可以動(dòng)手一試,聽(tīng)起來(lái)不是很復(fù)雜,不是么?

判斷輸贏的DeadZone

莫名其妙地球

每次看到球就這樣地飛向那遙遠(yuǎn)的天邊,是否有種莫名其妙地傷感呢。對(duì)啊,這是因?yàn)槲覀冞€沒(méi)有寫(xiě)輸贏的判定!按照以往的設(shè)計(jì),玩家的這邊和對(duì)手的那邊都應(yīng)該加上兩個(gè)碰撞體來(lái)判斷球是否越過(guò)了平臺(tái)。于是依照這個(gè)實(shí)戰(zhàn)的第一個(gè)教程,我們開(kāi)始寫(xiě)輸贏的判定。
首先,在玩家的一方和對(duì)手的一方分別建立兩個(gè)Cube,并拉伸到覆蓋住球后方的位置,確保當(dāng)球飛過(guò)了平臺(tái)之后一定會(huì)進(jìn)入Cube所圍成的Trigger區(qū)域。


Trigger區(qū)域即DeadZone

將上面的兩個(gè)Cube分別重命名為PlayerDeadZone和EnemyDeadZone。
新建一個(gè)腳本DeadZone.cs

public class DeadZone : MonoBehaviour {
    public string type;
    GameObject gm;
    GameStatus gs;
    // Use this for initialization
    void Start () {
        gm = GameObject.FindGameObjectWithTag("GameManager");
        gs = gm.GetComponent<GameStatus>();
    }

    void OnTriggerEnter(Collider other)
    {
        if (other.tag == "Ball")
        {
            if (type == "Player")
            {
                gs.setGameStatus("lose");
                Debug.Log("You lose");
            }
            else if (type == "Enemy")
            {
                gs.setGameStatus("win");
                Debug.Log("You win");
            }
        }
    }
}

這里的DeadZone主要是用于判斷輸贏,具體的對(duì)于輸贏之后的處理要交給GameManager來(lái)做。這樣做雖然有些麻煩,但是條理清晰,以后要在項(xiàng)目中進(jìn)行升級(jí)也方便。
這里我們選用Debug.Log(String)的方式來(lái)進(jìn)行測(cè)試,結(jié)果出乎意料。沒(méi)想到球碰到玩家后方的DeadZone的時(shí)候球停止不動(dòng)了。于是回到Ball上面的腳本,發(fā)現(xiàn)以前寫(xiě)過(guò)這樣的幾句話。

if(gs.getGameStatus()!="win" && gs.getGameStatus() != "lose")

當(dāng)游戲不是Win或Lose的狀態(tài)的時(shí)候才會(huì)對(duì)球進(jìn)行移動(dòng)。哈,這就是編寫(xiě)GameManager的好處嘛。

在畫(huà)布上畫(huà)東西(UI的制作)

由于在游戲中我們總不能讓玩家去查看控制臺(tái)的信息來(lái)知道自己的輸贏,我們要把輸贏的信息在屏幕上顯示出來(lái),這樣,就需要制作一個(gè)UI界面。
在Unity3D 4.6之后更新的UGUI系統(tǒng)里面,使用的是Canvas系統(tǒng)。UI中的所有內(nèi)容都存在于一個(gè)Canvas上。這樣一來(lái)
![Uploading 8_953659.png . . .],UI的布局十分方便。
首先,我們創(chuàng)建一個(gè)Canvas。在菜單欄選中GameObject->UI->Canvas。


新建一個(gè)畫(huà)布

這時(shí),場(chǎng)景中會(huì)多出一個(gè)Canvas的物體。我們選中Canvas,再用同樣的方法在其上添加一個(gè)UI->Text的子物體。并且給Canvas加上一個(gè)名叫Canvas的Tag。
點(diǎn)擊Play按鈕進(jìn)行測(cè)試。


文字跑哪去了

我的天哪!文字在左下角!而且只有一個(gè)鏡頭有!等等,這樣可不行。我們做的是VR游戲,要做到左右兩個(gè)鏡頭都能顯示,而且,還得有視覺(jué)差!
別急,因?yàn)閁GUI默認(rèn)的UI使用的是Screen Space Overlay,參照的是整個(gè)屏幕,既然如此,肯定不會(huì)參照兩個(gè)相機(jī)的。我們選中Canvas物體,在檢視面板中點(diǎn)擊Screen Space Overlay將其更改為World Space。
更改畫(huà)布的渲染模式

World Space,顧名思義就是以世界為參考,這樣的話這個(gè)Canvas畫(huà)布就是一個(gè)名副其實(shí)的物體了??梢詫?duì)它像操作一個(gè)Cube一樣進(jìn)行操作。那么下面我們就調(diào)整Canvas的Scale和位置來(lái)將其移動(dòng)到Cardboard攝像機(jī)前正確的位置。
再次測(cè)試一下。
再次測(cè)試

完美!但是文字的背景好像有點(diǎn)白。文字設(shè)置成黑色的,下面的看不見(jiàn),白色的上面的看不清,灰色的上下都看不清,尷尬。好吧,下面我們選中Text物體,在他的檢視面板中搜索Outline這個(gè)組件,并添加。


添加一個(gè)描邊

添加之后還可以同理添加Shadow這個(gè)組件。
添加陰影之后的效果

看起來(lái)應(yīng)該是好多了。可是每次一開(kāi)局都看到Y(jié)ou Win,玩起來(lái)倒是挺不好意思的。
另外由于我們?cè)谝婚_(kāi)始不想沒(méi)時(shí)間將手機(jī)放入Cardboard中而導(dǎo)致球早早地飛了起來(lái),所以要增加一個(gè)按鈕來(lái)作為開(kāi)始的按鈕。所以,我們?cè)俅位氐紺anvas物體上,右鍵點(diǎn)擊Canvas物體,選擇UI->Button
添加按鈕

將Button重命名為Start,并調(diào)整它在畫(huà)布上的大小至合適。并修改其Tag為Button。
修改按鈕的參數(shù)

選擇Canvas中Button的子物體Text,并修改其Tag為“Text”。
找到這個(gè)Text

修改Text的Text為“Start!”
將按鈕的Text改為Start!

給Button新建一個(gè)名為StartGame的腳本
Button上的腳本

現(xiàn)在進(jìn)行測(cè)試,所以腳本的內(nèi)容如下:
public class StartGame : MonoBehaviour {

    // Use this for initialization
    void Start () {
    
    }
    
    // Update is called once per frame
    void Update () {
    
    }

    public void onButtonPress()
    {
        Debug.Log("WOW!You Press ME With The Force!");
    }
}

回到Unity3D的窗口中,在階層面板選中Button,查看檢視面板,在Button的Button組件下找到On Click()這個(gè)域,點(diǎn)擊下面的那個(gè)中間有點(diǎn)點(diǎn)的一個(gè)圈。


添加響應(yīng)事件

選擇Button物體


選擇對(duì)應(yīng)的物體

點(diǎn)擊NoFunction的下拉列表,選擇我們剛寫(xiě)的OnButtonPress()函數(shù)
選擇要響應(yīng)的函數(shù)

在階層面板中重新選中Canvas,找到EventCamera這一項(xiàng),點(diǎn)擊右方的選擇按鈕,在場(chǎng)景中選擇我們的MainCamera。
這時(shí)你的場(chǎng)景中應(yīng)該自動(dòng)新建了一個(gè)EventSystem的物體,你可以在階層面板中找到。如果沒(méi)有也不要緊,可以自己新建一個(gè)嘛。新建的方法是新建一個(gè)空物體,添加EventSystem的組件即可。
在階層面板中找到Event System物體。添加Gaze Input Module組件。確保他的排序是在Touch Input Module之上。


添加凝視的事件系統(tǒng)

測(cè)試之前先將你的Quad設(shè)置為Disable,就是選中Quad物體,在檢視面板中將Quad這個(gè)單詞左邊的那個(gè)鉤鉤去掉。
點(diǎn)擊Play,當(dāng)你的視線對(duì)準(zhǔn)Start!按鈕的時(shí)候是不是有神奇的事情發(fā)生呢?
迷之Log

“WOW!你剛剛用原力按了我!”

備注:這里也許由于Cardboard的SDK的一些問(wèn)題或者是我本地的問(wèn)題,導(dǎo)致Gaze的Input不能生效,只能點(diǎn)擊屏幕來(lái)按按鈕。但當(dāng)視線不對(duì)準(zhǔn)按鈕的時(shí)候,在屏幕上點(diǎn)擊按鈕無(wú)效,視線對(duì)準(zhǔn)按鈕的時(shí)候點(diǎn)擊屏幕才有效。

開(kāi)始游戲

做了這么多的工作,還是沒(méi)有完成開(kāi)始游戲的這個(gè)功能。那么下面我們就著手寫(xiě)開(kāi)始游戲的這個(gè)重要的腳本吧。

public class StartGame : MonoBehaviour {
    public float waitTime = 10f;
    GameObject gm, quad, canvas,text;
    GameStatus gs;
    Text tx;
    private bool start;
    float time = 0f;
    // Use this for initialization
    void Start () {
        gm = GameObject.FindGameObjectWithTag("GameManager");
        gs = gm.GetComponent<GameStatus>();
        quad = GameObject.FindGameObjectWithTag("Quad");
        canvas = GameObject.FindGameObjectWithTag("Canvas");
        text = GameObject.FindGameObjectWithTag("Text");
        tx = text.GetComponent<Text>();
        time = 0f;

        start = false;
    }
    
    void Awake()
    {
        start = false;
        time = 0f;
    }

    void Update () {
        if(start)
        {
            time += Time.deltaTime;
            tx.text = (((int)(waitTime - time)).ToString());
            if(time > waitTime)
            {
                gs.setGameStatus("begin");
                canvas.SetActive(false);
                start = false;
            }
        }
        
    }

    public void onButtonPress()
    {
        start = true;
    }
}

修改GameStatus.cs腳本

public class GameStatus : MonoBehaviour {
    string gameStatus;
    GameObject canvas, text;

    public string getGameStatus()
    {
        return gameStatus;
    }

    public void setGameStatus(string gs)
    {
        gameStatus = gs;
    }
    // Use this for initialization
    void Start () {
        gameStatus = "wait";

        canvas = GameObject.FindGameObjectWithTag("Canvas");
        text = GameObject.FindGameObjectWithTag("Text");
    }

    void FixedUpdate()
    {
        if(gameStatus=="win")
        {
            canvas.SetActive(true);
            text.GetComponent<Text>().text= "You Win!Play Again!";
        }
        else if(gameStatus=="lose")
        {
            canvas.SetActive(true);
            text.GetComponent<Text>().text = "You Lose!Play Again!";
        }
    }
}

修改BallMove.cs腳本

public class ballMove : MonoBehaviour {
    Vector3 direction;
    GameObject gm;
    GameStatus gs;
    public float speed = 0.1f;
    Vector3 originPos;
    //用于設(shè)置球移動(dòng)的方向
    public void setDirection(Vector3 dir)
    {
        direction = dir;
    }
    //外部用于獲取球移動(dòng)的方向
    public Vector3 getDirection()
    {
        return this.direction;
    }
    //為了節(jié)約性能 不適用Update函數(shù)而使用FixedUpdate函數(shù)
    void FixedUpdate()
    {
        if(gs.getGameStatus()!="win" && gs.getGameStatus() != "lose")
        {
            if(gs.getGameStatus() == "begin")
            {
                transform.Translate(direction * speed);
            }   
        }
        if (gs.getGameStatus() == "win" || gs.getGameStatus() == "lose")
        {
            this.transform.position = originPos;
        }
        //Debug.Log(direction);
    }
    // Use this for initialization
    void Start () {
        gm = GameObject.FindGameObjectWithTag("GameManager");
        gs = gm.GetComponent<GameStatus>();

        originPos = this.transform.position;

        directionVector dv=new directionVector();
        direction =dv.returnVector(250,1);
    }
}

開(kāi)始點(diǎn)擊Play測(cè)試吧!
首先,點(diǎn)擊Start,下方會(huì)倒計(jì)時(shí)。


倒計(jì)時(shí)

當(dāng)玩家贏了和輸了的時(shí)候,球會(huì)自動(dòng)回到中央并提醒重新開(kāi)始。


重新開(kāi)始

至此,我們已經(jīng)完成了這個(gè)游戲的所有腳本功能。后面可以根據(jù)文章中提出的問(wèn)題進(jìn)行一些優(yōu)化和發(fā)揮等。
下面的幾節(jié),我們將對(duì)VR游戲開(kāi)發(fā)中的UI、輸入方式和優(yōu)化和美工等進(jìn)行講解。
本文作者沈慶陽(yáng)擁有著作權(quán),未經(jīng)允許不得轉(zhuǎn)載。
最后編輯于
?著作權(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),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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