最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會(huì)員登陸 & 注冊(cè)

如何在游戲里當(dāng)好一個(gè)反派——用Unity簡(jiǎn)單復(fù)刻《勇者別囂張》(下)

2018-12-27 14:00 作者:皮皮關(guān)做游戲  | 我要投稿

作者:沈琰


本篇難度:★★☆☆☆

前言

接著上期開(kāi)始下一階段的內(nèi)容。

上期傳送門(mén):如何在游戲里當(dāng)好一個(gè)反派——用Unity簡(jiǎn)單復(fù)刻《勇者別囂張》(上)

地圖搭建完畢后就應(yīng)該是游戲的核心玩法:魔物和勇者的行為邏輯實(shí)現(xiàn)了。

(上期評(píng)論說(shuō)美少女別囂張的,你們賠我的童年記憶啊,我現(xiàn)在已經(jīng)沒(méi)辦法直視這個(gè)游戲了......)

功能分析

魔物行為邏輯

這個(gè)看似休閑的小游戲某種意義上來(lái)說(shuō)相當(dāng)硬核,怎么說(shuō)?

原版游戲里的魔物生態(tài)規(guī)則看似簡(jiǎn)單,實(shí)則復(fù)雜而嚴(yán)謹(jǐn),小時(shí)候因?yàn)闆](méi)有漢化,玩得糊里糊涂,基本是很難撐到5關(guān)以后,到后來(lái)看了攻略才明白。

簡(jiǎn)單來(lái)說(shuō)魔物之間除了簡(jiǎn)單的移動(dòng)攻擊行動(dòng)外還存在著一條生態(tài)鏈,高級(jí)魔物吃低級(jí)魔物除了補(bǔ)充生命值外還能促進(jìn)繁殖,魔物的生命值即使沒(méi)有遭受攻擊也會(huì)隨著時(shí)間慢慢減少。

生成魔物的種類又與磚塊吸收的養(yǎng)分和魔份有關(guān)系,處于魔物生態(tài)鏈底端的史萊姆和鬼火負(fù)責(zé)養(yǎng)分和魔份的供給。所以想要能抵御越來(lái)越強(qiáng)的勇者進(jìn)攻,較為穩(wěn)定的生態(tài)鏈?zhǔn)且粋€(gè)金字塔形狀:

至于其他更復(fù)雜的養(yǎng)殖催生魔物進(jìn)化等等就不展開(kāi)說(shuō)了,都快變成游戲攻略了。

總之就是游戲的魔物行為邏輯沒(méi)有看起來(lái)那么簡(jiǎn)單,加上缺少動(dòng)畫(huà)素材,所以復(fù)刻時(shí)只選擇了一些重點(diǎn)功能(主要是因?yàn)閼校?。歸納一下就是:

1.移動(dòng)

2.養(yǎng)分運(yùn)送

3.攻擊

4.食物鏈(大怪物吃小怪物)

5.隨時(shí)間減少的生命力

勇者行為邏輯

相較來(lái)說(shuō)勇者的行為邏輯就簡(jiǎn)單多了,就是找魔王->干掉路上遇到的魔物->找到魔王->捆回家這么個(gè)過(guò)程。關(guān)鍵就在這個(gè)找魔王上,有經(jīng)驗(yàn)的同學(xué)大概能想到這里可能會(huì)用到尋路算法,但具體用哪一種以及怎么用?

基于觀察原版游戲勇者的尋找魔王得出的規(guī)律:

1.勇者會(huì)走到岔路里。

2.如果有多個(gè)勇者,在分岔路口會(huì)主動(dòng)分路尋找。

3.如果進(jìn)入死路,會(huì)沿著原路返回到上一個(gè)擁有其他未探尋岔路的節(jié)點(diǎn)繼續(xù)尋找。

其實(shí)到這結(jié)論已經(jīng)呼之欲出了,這種一條道走到黑的尋路方法與之最接近的是深度優(yōu)先搜索算法(Depth-First-Search),接下來(lái)所要做的就是把尋路的過(guò)程通過(guò)勇者的行動(dòng)顯示在游戲界面中。


角色移動(dòng)邏輯

先寫(xiě)移動(dòng)邏輯是因?yàn)檫@是魔物與勇者通用的方法,在上期文章實(shí)現(xiàn)鼠標(biāo)點(diǎn)擊邏輯時(shí)賣(mài)了一個(gè)關(guān)子:為什么選擇了一個(gè)坐標(biāo)系轉(zhuǎn)換自定義二維數(shù)組的方法?因?yàn)橐苿?dòng)邏輯同樣也以此作為基礎(chǔ)。

因?yàn)橛螒蚶飯?chǎng)景內(nèi)的物體都是遵循TileMap里網(wǎng)格大小的正方形,所以把世界坐標(biāo)轉(zhuǎn)換為我們自定義的二維數(shù)組坐標(biāo)后,移動(dòng)相關(guān)的邏輯就變成一個(gè)類似控制臺(tái)小游戲的移動(dòng)邏輯了。

假設(shè)現(xiàn)在要從游戲地圖里的藍(lán)點(diǎn)沿著最短曼哈頓距離移動(dòng)到紅點(diǎn),如果沒(méi)有這個(gè)自定義的二維坐標(biāo),計(jì)算起來(lái)會(huì)非常麻煩。而現(xiàn)在就簡(jiǎn)單了,設(shè)藍(lán)點(diǎn)為(0,0),右和上分別為X,Y的正方向,那么移動(dòng)到紅點(diǎn)的路徑則可以表示為:(0,0)->(0,-1)->(0,-2)->(0,-3)->(1,-3)->(2,-3),再用之前寫(xiě)好的轉(zhuǎn)換函數(shù)得到場(chǎng)景內(nèi)的實(shí)際坐標(biāo),兩點(diǎn)之間用差值計(jì)算移動(dòng)過(guò)程。


實(shí)現(xiàn)代碼:

protected IEnumerator Move(Vector2 dir)

??? {

??????? Vector2 correctionPos = Pos.Pos2Vector2(Pos.Float2IntPos(transform.position));

??????? Vector2 endPos = correctionPos + (dir * 0.18f);

??????? if (!isMoving)

??????????? yield return SmoothMovement(endPos);

??? }

?

??? protected IEnumerator MoveTo(Vector2 pos)

??? {

??????? if (!isMoving)

??????????? yield return SmoothMovement(pos);

??? }

?

IEnumerator SmoothMovement(Vector2 endPos)

??? {

??????? isMoving = true;

?

??????? float Distance = Vector2.Distance(new Vector2(transform.position.x, transform.position.y), endPos);

?

?

??????? while (Distance > float.Epsilon)

??????? {

??????????? Vector2 newPos = Vector2.MoveTowards(new Vector2(transform.position.x, transform.position.y), endPos, MoveSpeed * Time.deltaTime);

??????????

??????????? transform.position = newPos;

?

??????????? Distance = Vector2.Distance(new Vector2(transform.position.x, transform.position.y), endPos);

??????? }

??????

??????? isMoving = false;

??????? yield return null;

??? }

?

以上是純移動(dòng)邏輯,在之后加上動(dòng)畫(huà)狀態(tài)機(jī)運(yùn)行效果如下:

相當(dāng)于每個(gè)每個(gè)網(wǎng)格的中心就代表一個(gè)唯一的二維數(shù)組坐標(biāo),所以移動(dòng)的路徑也一定是呈網(wǎng)格狀的

魔物狀態(tài)機(jī)

在上面的分析中已經(jīng)把魔物所需要的的功能都羅列出來(lái)了,其中移動(dòng)已經(jīng)實(shí)現(xiàn)了?,F(xiàn)在要做的就是把剩下的功能實(shí)現(xiàn)并用狀態(tài)機(jī)組裝成魔物的AI。

狀態(tài)機(jī)大家應(yīng)該不陌生了,不熟悉的同學(xué)可以先看看專欄里關(guān)于AI狀態(tài)機(jī)的教程。

傳送門(mén):給貓看的游戲AI實(shí)戰(zhàn)(三)基于狀態(tài)機(jī)的AI系統(tǒng)

首先把魔物的動(dòng)畫(huà)狀態(tài)機(jī)做出來(lái):

2D游戲四方向動(dòng)畫(huà)里,左右方向一般是用一套素材。因?yàn)閯?dòng)畫(huà)本身沒(méi)變,只是朝向改了。在代碼里旋轉(zhuǎn)角色或者Scale改成-1都行。這里特地在畫(huà)圖軟件里鏡像了一份相反朝向的素材是錯(cuò)誤的做法,主要因?yàn)閺?qiáng)迫癥犯了...

很簡(jiǎn)單的四方向動(dòng)畫(huà),在待機(jī)->移動(dòng)->攻擊之間切換,然后思考一下魔物狀態(tài)機(jī)的流程:

大概流程就是這樣,這一步大家自己實(shí)現(xiàn)時(shí)不必完全一樣。

表現(xiàn)到代碼里其實(shí)很簡(jiǎn)單,首先是單方向的障礙檢測(cè)函數(shù),根據(jù)layer返回檢測(cè)結(jié)果:

??Vector2 Direction2Vector2(Direction dir)

??? {

??????? Vector2 direction = Vector2.zero;

??????? switch (dir)

??????? {

??????????? case Direction.Up:

??????????????? direction = Vector2.up;

??????????????? break;

??????????? case Direction.Down:

?????? ?????????direction = Vector2.down;

??????????????? break;

??????????? case Direction.Left:

??????????????? direction = Vector2.left;

??????????????? break;

??????????? case Direction.Right:

??????????????? direction = Vector2.right;

??????????????? break;

??????? }

??????? return direction;

??? }

? protected bool ObstacleCheck(Direction dir, LayerMask layer)

??? {

??????? RaycastHit2D[] hit;

??????? hit = Physics2D.RaycastAll(transform.position, Direction2Vector2(dir), 0.18f, layer);

??????? return hit.Length > 0;

??? }

?

然后是各個(gè)行為函數(shù):

? ? protected IEnumerator CheckAndAttackEnemyAround()

??? {

??????? Collider2D[] arounds = Physics2D.OverlapCircleAll(transform.position, 0.09f, Enemylayer);

?

??????? if(arounds.Length>0)

??????? {

???????????

??????????? targetDir = GetDirection(transform.position, arounds[0].transform.position);

??????????? yield return Attack(targetDir);

?? ???????

??????? }

??????

??? }

?protected IEnumerator Attack(Direction dir)

??? {

??????? RaycastHit2D[] hit;

?

???????

??????? while (ObstacleCheck(dir, Enemylayer, out hit))

??????? {

??????????? _animator.SetBool("MoveState", false);

?

??????????? _animator.SetInteger("Dir", (int)dir);

??????????? yield return null;

?

??????????? _animator.SetTrigger("Attack");

?

??????????? for (int i = 0; i < hit.Length; i++)

??????????? {

??????????????? Character c = hit[i].transform.GetComponent<Character>();

?? ?????????????if (c)

??????????????? {

??????????????????? c.OnBeHit(damage);

??????????????? }

??????????? }

??????????? yield return AttackInterval;

??????? }

??? }

?IEnumerator Idle()

??? {

??????? float time = idleTime;

??????? while(time>float.Epsilon)

??????? {

??????????? time -= Time.deltaTime;

??????????? CheckAndAttackEnemyAround();

??????????? yield return null;

??????? }

??? }

??? protected virtual IEnumerator TransportNutrients()

??? {

??????? RaycastHit2D[] hit;

??????? if (ObstacleCheck(CurDir, WallLayer, out hit))

??????? {

??????????? _animator.SetTrigger("Attack");

?

??????????? Block script = hit[0].transform.GetComponent<Block>();

??????????? if (script)

??????????? {

??????????????? script.ChangeNutrient(nutrient);

??????????? }

?

??????? }

??????? yield return IdleTime;

??? }

?

然后把流程邏輯寫(xiě)在一整個(gè)協(xié)程函數(shù)中:


protected virtual IEnumerator Action()

??? {

??????? while (true)

??????? {

??????????? RandomDir();

?

??????????? if (Random.Range(0, 4) > 2)

??????????? {

??????????????? yield return Idle();

??????????? }

????????? ??else

??????????? {

??????????????? yield return null;

??????????????? if (ObstacleCheck(CurDir, TempLayer))

??????????????? {

??????????????????? yield return Behaviour();

?

??????????????????? continue;

?

??????????????? }

??????????????? yield return Move(CurDir);

??????????? }

??????? }

??? }

?

應(yīng)該有人注意到了所有行為邏輯的代碼都是協(xié)程,這里為什么要用到協(xié)程?

因?yàn)椴还苁且苿?dòng)過(guò)程也好,動(dòng)畫(huà)播放也好都要涉及到時(shí)間,也就是每個(gè)單獨(dú)的邏輯函數(shù)可能執(zhí)行時(shí)間都不一樣,全部用協(xié)程實(shí)現(xiàn)會(huì)讓狀態(tài)轉(zhuǎn)移的時(shí)候更加方便。

所以整個(gè)AI本質(zhì)上是一個(gè)協(xié)程狀態(tài)機(jī),然后把魔物的預(yù)制體填入磚塊生成中,運(yùn)行試一試效果:

勇者尋路邏輯

與魔物不同,勇者的AI其實(shí)就是一個(gè)尋路的過(guò)程,在移動(dòng)過(guò)程中如果遇到了魔物就會(huì)暫時(shí)中斷并與魔物展開(kāi)戰(zhàn)斗,所以攻擊的行為是寫(xiě)在移動(dòng)過(guò)程里的。剩下要做的事就是把這個(gè)尋找的過(guò)程表現(xiàn)出來(lái)。

之前分析的時(shí)候說(shuō)最接近這個(gè)表現(xiàn)形式的尋路算法是DFS,為什么這么說(shuō)呢?把每個(gè)格子看做一個(gè)節(jié)點(diǎn),當(dāng)前格子能去往下一個(gè)格子的路線看做分支,那么整個(gè)地圖可以看做是一張無(wú)向圖:

再看看《算法導(dǎo)論》中關(guān)于DFS的說(shuō)明:

深度優(yōu)先搜索算法所使用的策略就像其名字所隱含的:只要可能,就在圖中盡量“深入”。深度優(yōu)先搜索總是對(duì)最近才發(fā)現(xiàn)的結(jié)點(diǎn)v的出發(fā)邊進(jìn)行探索,直到該結(jié)點(diǎn)的所有出發(fā)邊都被發(fā)現(xiàn)為止。
一旦節(jié)點(diǎn)v的所有出發(fā)邊都被發(fā)現(xiàn),搜索則"回溯"到v的前驅(qū)結(jié)點(diǎn)(v是經(jīng)過(guò)該節(jié)點(diǎn)才被發(fā)現(xiàn)的),來(lái)搜索該前驅(qū)結(jié)點(diǎn)的出發(fā)邊。該過(guò)程一直持續(xù)到從源節(jié)點(diǎn)可以達(dá)到的所有結(jié)點(diǎn)都被發(fā)現(xiàn)為止。
如果還存在尚未發(fā)現(xiàn)的結(jié)點(diǎn),深度優(yōu)先搜索將從這些未被發(fā)現(xiàn)的結(jié)點(diǎn)中任選一個(gè)作為新的源節(jié)點(diǎn),并重復(fù)同樣的搜索過(guò)程。該算法重復(fù)整個(gè)過(guò)程,直到圖中的所有結(jié)點(diǎn)都被發(fā)現(xiàn)為止。

簡(jiǎn)單點(diǎn)來(lái)說(shuō)就是在每個(gè)結(jié)點(diǎn)探尋到新的路,都以那條最后找到的路為最優(yōu)先級(jí)走到黑,一旦無(wú)路可走再返回到之前還有未探尋路徑的結(jié)點(diǎn)繼續(xù)走到黑,這恰好與分析中原版游戲中的勇者尋路規(guī)律一模一樣。

把游戲里的地圖轉(zhuǎn)換成結(jié)點(diǎn)圖來(lái)看如下:

假如現(xiàn)在起點(diǎn)是A,終點(diǎn)是B,在結(jié)點(diǎn)1的位置進(jìn)入向下的岔路,則會(huì)一直走到C結(jié)點(diǎn)。然后原路返回至1,再探尋其他的路直到找到B為止。

把尋路算法用步驟用代碼表示出來(lái)如下:

using System.Collections;

using System.Collections.Generic;

using UnityEngine;

?

public class Hero : Character

{

?

??? Dictionary<Pos, bool> map;

??? Pos target;

?

??? bool IsFindTarget;

?

? IEnumerator HeroSearchRoad(Pos next)

??? {

??????? //把當(dāng)前世界坐標(biāo)轉(zhuǎn)換為二維數(shù)組坐標(biāo)

??????? Pos cur = Pos.Float2IntPos(transform.position);

???????

?????

??????? //如果找到目標(biāo),跳出遞歸

??????? if (next.Equals(target))

??????? {

??????????? IsFindTarget = true;

??????????? yield return new WaitForSeconds(2f);

??????????? yield break;

??????? }

??????? //移動(dòng)到下一個(gè)目標(biāo)點(diǎn)

??????? yield return MoveTo(Pos.Pos2Vector2(next));

?

??????? //字典中標(biāo)記下一個(gè)坐標(biāo)點(diǎn)為已探尋,防止回旋繞路

??????? map[next] = false;

?

??????? //獲取當(dāng)前位置能移動(dòng)到的其他節(jié)點(diǎn)坐標(biāo)

??????? List<Pos> curNode = GetCurrentNode();

?

??????? //根據(jù)與目標(biāo)距離對(duì)節(jié)點(diǎn)目標(biāo)進(jìn)行排序(可選)

??????? if (curNode.Count > 1)

??????? {

??????????? curNode.Sort((a, b) => Pos.GetManhattanDistance(a, target).CompareTo(Pos.GetManhattanDistance(b, target)));

??????? }

?

???????

??????? //沿著所有開(kāi)辟出的新節(jié)點(diǎn)尋路

??????? for (int i = 0; i < curNode.Count; i++)

??????? {

??????????? //找到目標(biāo)就不繼續(xù)其他節(jié)點(diǎn)的探索了

??????????? if (IsFindTarget)

??????????? {

??????????????? break;

??????????? }

??????????? yield return HeroSearchRoad(curNode[i]);

?

??????? }

?

??????? //所有能走的節(jié)點(diǎn)都走完了,只能回頭

??????? //回溯

??????? yield return MoveTo(Pos.Pos2Vector2(cur));

??? }

?

?

?

List<Pos> GetCurrentNode()

??? {

??????? List<Pos> list = new List<Pos>();

?

??????? List<Pos> resut = new List<Pos>();

?

??????? Pos cur = Pos.Float2IntPos(transform.position);

?

??????? if (!ObstacleCheck(Direction.Up, WallLayer))

??????? {

??????????? list.Add(new Pos(cur.x, cur.y + 1));

?

??????? }

??????? if (!ObstacleCheck(Direction.Down, WallLayer))

??????? {

??????????? list.Add(new Pos(cur.x, cur.y - 1));

?

???? ???}

??????? if (!ObstacleCheck(Direction.Right, WallLayer))

??????? {

??????????? list.Add(new Pos(cur.x + 1, cur.y));

?

??????? }

??????? if (!ObstacleCheck(Direction.Left, WallLayer))

??????? {

??????????? list.Add(new Pos(cur.x - 1, cur.y));

?

??????? }

??????? for (int i = 0; i < list.Count; i++)

??????? {

??????????? if (!map.ContainsKey(list[i]))

??????????? {

??????????????

??????????????? resut.Add(list[i]);

??????????????? map.Add(list[i], true);

??????????? }

??????????? else

??????????? {

?????? ????????

??????????????? if (map[list[i]] == true)

??????????????? {

??????????????????? resut.Add(list[i]);

??????????????? }

??????????? }

??????? }

??????? return resut;

??? }

}

?

代碼里除了原本的邏輯,額外多加了一步:在每次探尋下一個(gè)結(jié)點(diǎn)時(shí)優(yōu)先選擇相對(duì)目標(biāo)點(diǎn)最近的那一個(gè),所以最后勇者的尋路算法就變成了有距離指導(dǎo)的DFS。

弄個(gè)稍微復(fù)雜點(diǎn)的地圖,運(yùn)行的效果如下:

關(guān)于尋路算法更詳細(xì)的教程可以參考專欄里的另一篇文章。

給貓看的游戲AI實(shí)戰(zhàn)(四)眼見(jiàn)為實(shí)——讓AI的思考過(guò)程可視化

結(jié)尾

至此游戲雖不完整,但主體邏輯已經(jīng)還原的七七八八了。限于篇幅,還有一些參數(shù)調(diào)整和提升表現(xiàn)力的工作就由大家自己去嘗試,或者更改邏輯,定制出專屬自己的規(guī)則。

通過(guò)這兩期的簡(jiǎn)單復(fù)刻,可以發(fā)現(xiàn)這樣簡(jiǎn)單的小游戲其實(shí)蘊(yùn)含了相當(dāng)多的細(xì)節(jié)在里面,雖然整體算不上太難,但積累起來(lái)就很可觀了。

我們只是把最基本的核心邏輯還原就費(fèi)了不少功夫,更別說(shuō)原版游戲還有更多游戲性上的細(xì)節(jié)工作。現(xiàn)在的游戲畫(huà)面日趨精良,但像這樣設(shè)計(jì)師的心思都花在玩法上的游戲反而少了。

扯得有些遠(yuǎn)了,但愿能有更多像這樣能帶給玩家最本質(zhì)的樂(lè)趣的游戲吧,感謝觀看至此。

本期工程地址:https://github.com/tank1018702/unity-005


最后想系統(tǒng)學(xué)習(xí)游戲開(kāi)發(fā)的童鞋,歡迎訪問(wèn)?http://levelpp.com/????

游戲開(kāi)發(fā)攪基QQ群:869551769????

微信公眾號(hào):皮皮關(guān)

如何在游戲里當(dāng)好一個(gè)反派——用Unity簡(jiǎn)單復(fù)刻《勇者別囂張》(下)的評(píng)論 (共 條)

分享到微博請(qǐng)遵守國(guó)家法律
科技| 泰安市| 泽普县| 乐平市| 上饶县| 乐陵市| 胶南市| 彝良县| 沽源县| 隆化县| 庆城县| 丰宁| 来凤县| 扬州市| 枣强县| 青田县| 金堂县| 万年县| 崇左市| 依兰县| 泽普县| 台前县| 大化| 当阳市| 当涂县| 垦利县| 华安县| 黑河市| 城口县| 弋阳县| 绵阳市| 韩城市| 剑河县| 怀集县| 交城县| 彭山县| 包头市| 宁阳县| 平和县| 宣汉县| 华亭县|