開(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è)置完成之后,如下圖

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

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

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

將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è)試一下。

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ū)域。

將上面的兩個(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。

這時(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。

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

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

選擇Canvas中Button的子物體Text,并修改其Tag為“Text”。

修改Text的Text為“Start!”

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

現(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è)圈。

選擇Button物體

點(diǎn)擊NoFunction的下拉列表,選擇我們剛寫(xiě)的OnButtonPress()函數(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之上。

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

“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í)。

當(dāng)玩家贏了和輸了的時(shí)候,球會(huì)自動(dòng)回到中央并提醒重新開(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)載。