游戏ai 行为树_游戏AI –行为树简介
游戲ai 行為樹(shù)
游戲AI是一個(gè)非常廣泛的主題,盡管有很多資料,但我找不到能以較慢且更易理解的速度緩慢介紹這些概念的東西。 本文將嘗試解釋如何基于行為樹(shù)的概念來(lái)設(shè)計(jì)一個(gè)非常簡(jiǎn)單但可擴(kuò)展的AI系統(tǒng)。
什么是AI?
人工智能是參與游戲的實(shí)體表現(xiàn)出的類似于人類的行為。 與實(shí)際的智能推理驅(qū)動(dòng)的行為相比,這更是實(shí)體對(duì)智能和周到的行動(dòng)的幻想。 目的是試圖欺騙玩家,使他們認(rèn)為其他“智能”實(shí)體是由人類而不是機(jī)器控制的。 說(shuō)起來(lái)容易做起來(lái)難,但是我們可以使用很多技巧來(lái)實(shí)現(xiàn)一些真正好的,看似隨機(jī)的“智能”行為。
一個(gè)例子
在跳入有趣的話題之前,讓我們起草一個(gè)我們想要實(shí)現(xiàn)的目標(biāo)的計(jì)劃。 同樣,我將以機(jī)器人為例。 想象一下一個(gè)競(jìng)技場(chǎng),機(jī)器人將在其中爭(zhēng)奪戰(zhàn),最后一個(gè)站位的機(jī)器人將是贏家。
競(jìng)技場(chǎng)將是一塊木板,機(jī)器人將隨機(jī)放置在上面。 我們將其制作為基于回合的游戲,以便我們可以追蹤整個(gè)AI的發(fā)展,但可以輕松將其轉(zhuǎn)變?yōu)閷?shí)時(shí)游戲。
規(guī)則很簡(jiǎn)單:
- 木板是矩形
- 機(jī)器人可以在任一方向上每轉(zhuǎn)一圈將瓷磚移動(dòng)到任何相鄰的未占用瓷磚上
- 機(jī)器人具有一定范圍,并且可以向其范圍內(nèi)的機(jī)器人發(fā)射
- 機(jī)器人將具有通常的屬性:它們?cè)斐傻膫蜕?
為了簡(jiǎn)單起見(jiàn),我們將使用非常簡(jiǎn)單的結(jié)構(gòu)。 該應(yīng)用程序?qū)⒕哂蠨roid類和Board類。 機(jī)器人將具有以下定義它的屬性:
public class Droid {final String name;int x;int y;int range;int damage;int health;Board board;public Droid(String name, int x, int y, int health, int damage, int range) {this.name = name;this.x = x;this.y = y;this.health = health;this.damage = damage;this.range = range;}public void update() {// placeholder for each turn or tick}/* ... *//* getters and setters and toString() *//* ... */ }Droid只是具有一些屬性的簡(jiǎn)單pojo。 這些屬性不言自明,但這是它們的簡(jiǎn)短摘要:
- name –機(jī)器人的唯一名稱,也可以用于ID。
- x和y –網(wǎng)??格上的坐標(biāo)。
- health , damage和range -它說(shuō)了什么。
- board –是對(duì)機(jī)器人所在的Board以及其他機(jī)器人的引用。 我們需要這樣做,因?yàn)闄C(jī)器人將通過(guò)了解其環(huán)境(即board <./ li>)來(lái)做出決策。
還有一個(gè)空的update()方法,每次droid結(jié)束旋轉(zhuǎn)時(shí)都會(huì)調(diào)用該方法。 如果是實(shí)時(shí)游戲,則從游戲循環(huán)(最好是從游戲引擎)中調(diào)用update方法。
還有一些明顯的getter和setter以及toString()方法,它們從清單中省略了。 Board類非常簡(jiǎn)單。
public class Board {final int width;final int height;private List<Droid> droids = new ArrayList<Droid>();public Board(int width, int height) {this.width = width;this.height = height;}public int getWidth() {return width;}public int getHeight() {return height;}public void addDroid(Droid droid) {if (isTileWalkable(droid.getX(), droid.getY())) {droids.add(droid);droid.setBoard(this);}}public boolean isTileWalkable(int x, int y) {for (Droid droid : droids) {if (droid.getX() == x && droid.getY() == y) {return false;}}return true;}public List<Droid> getDroids() {return droids;} }它具有width和height ,并且包含機(jī)器人列表。 它還包含一些方便的方法來(lái)檢查給定坐標(biāo)上是否已存在機(jī)器人,以及一種輕松地逐個(gè)添加機(jī)器人的方法。
到目前為止,這是相當(dāng)標(biāo)準(zhǔn)的。 我們可以在板上散布一些機(jī)器人,但它們不會(huì)做任何事情。 我們可以創(chuàng)建板,向其中添加一些機(jī)器人,然后開(kāi)始調(diào)用update() 。 它們只是一些愚蠢的機(jī)器人。
不太傻的機(jī)器人
為了使droid做某事,我們可以在其update()方法中實(shí)現(xiàn)邏輯。 這就是所謂的每一次跳動(dòng)或在我們的情況下每轉(zhuǎn)一次的方法。 例如,我們希望我們的機(jī)器人在競(jìng)技場(chǎng)(木板)上徘徊,如果他們看到射程范圍內(nèi)的其他機(jī)器人,請(qǐng)接合它們并開(kāi)始向它們射擊直到它們死亡。 這將是非?;镜腁I,但仍然是AI。
偽代碼如下所示:
if enemy in range then fire missile at it
otherwise pick a random adjacent tile and move there
這意味著,機(jī)器人之間的任何相互作用都將導(dǎo)致僵持,較弱的機(jī)器人會(huì)被破壞。 我們可能要避免這種情況。 因此,我們可以補(bǔ)充一下,如果機(jī)器人有可能丟失,請(qǐng)嘗試逃跑。 僅在無(wú)處可逃時(shí)站起來(lái)戰(zhàn)斗。
if enemy in range then
if enemy is weaker then fight escape route exists then escape fight wander
一切都很好。 機(jī)器人將開(kāi)始“智能化”地工作,但是除非我們添加更多代碼來(lái)做更多聰明的事情,否則它們?nèi)匀环浅S邢蕖?而且,它們將起到相同的作用。 想象一下,如果將它們放在更復(fù)雜的舞臺(tái)上。 在競(jìng)技場(chǎng)上,有一些道具如力量道具可以增強(qiáng)力量,可以避免障礙。 例如,當(dāng)機(jī)器人四處飛來(lái)飛去時(shí),請(qǐng)決定在拿起醫(yī)療/修理包與拿起武器加電之間。
它很快就會(huì)失控。 如果我們想要一個(gè)行為不同的機(jī)器人該怎么辦。 一個(gè)是攻擊機(jī)器人,另一個(gè)是修理機(jī)器人。 當(dāng)然,我們可以通過(guò)對(duì)象合成來(lái)實(shí)現(xiàn)這一目標(biāo) ,但是機(jī)器人的大腦將極其復(fù)雜,游戲設(shè)計(jì)的任何變化都需要付出巨大的努力才能適應(yīng)。
讓我們看看是否可以提出一個(gè)可以解決這些問(wèn)題的系統(tǒng)。
大腦來(lái)了
我們可以將機(jī)器人的AI模塊視為某種大腦。 大腦由遵循一系列規(guī)則作用于機(jī)器人的幾個(gè)例程組成。 這些規(guī)則支配著例程的執(zhí)行,因此它將生存和贏得比賽的機(jī)會(huì)最大化作為最終目標(biāo)。 如果我們想到由例程組成的人腦,并以馬斯洛的需求層次作為參考,我們可以立即識(shí)別出一些例程。
- 生理程序 –每次都需要執(zhí)行的程序,否則將沒(méi)有任何生命
- 生存程序 –一旦滿足生活條件,就必須執(zhí)行該程序,以確保長(zhǎng)期生存
- 有抱負(fù)的例程 –如果在維持生計(jì)之后還剩下時(shí)間需要再次執(zhí)行生計(jì),則將執(zhí)行此例程
讓我們分解一下人類的智慧。 人類需要呼吸才能生存。 每次呼吸都消耗能量。 一個(gè)人可以呼吸這么多,直到能量耗盡。 要補(bǔ)充能量,就需要吃飯。 一個(gè)人只有在他/她有食物可支配的情況下才能吃飯。 如果沒(méi)有可用的食物,則需要消耗更多的能量。 如果購(gòu)買食物需要很長(zhǎng)時(shí)間(例如,需要狩獵)并且獲得的食物量很少,那么在食用之后,人們需要更多食物,并且例程會(huì)立即重新開(kāi)始。 如果從超市購(gòu)買散裝的食物,吃完后還有很多剩余的空間,因此人類可以繼續(xù)做更多有趣的事情,這些都是他/她理想的部分。 例如,結(jié)交朋友,發(fā)動(dòng)戰(zhàn)爭(zhēng)或看電視之類的事情。
只要思考一下人腦中有多少東西才能使我們發(fā)揮功能并嘗試對(duì)其進(jìn)行模擬。 所有這些都忽略了我們正在獲得和響應(yīng)的大多數(shù)刺激。 為此,我們需要對(duì)人體進(jìn)行參數(shù)設(shè)置,并且由刺激觸發(fā)的每個(gè)傳感器將更新正確的參數(shù),并且執(zhí)行的例程將檢查新值并采取相應(yīng)的措施。 我現(xiàn)在不會(huì)描述它,但是您希望我有所想法。
讓我們切換回更簡(jiǎn)單的機(jī)器人。 如果我們嘗試使人類例程適應(yīng)機(jī)器人,我們將得到如下所示:
- 生理的/存在的 -在本示例中我們可以忽略這部分,因?yàn)槲覀冊(cè)O(shè)計(jì)的是機(jī)器人,并且它們是機(jī)械人。 當(dāng)然,對(duì)于它們來(lái)說(shuō),它們?nèi)匀恍枰獜碾姵鼗蚱渌赡芎谋M的能源中獲取能量(例如動(dòng)作點(diǎn))。 為了簡(jiǎn)單起見(jiàn),我們將忽略這一點(diǎn),并認(rèn)為能源是無(wú)限的。
- 生存/安全 -該例程將確保機(jī)器人在避免當(dāng)前威脅的情況下在當(dāng)前回合中存活并存活。
- 有抱負(fù) –一旦安全例行程序簽出就可以啟動(dòng),而不必激活機(jī)器人的逃逸例行程序。 機(jī)器人目前的簡(jiǎn)單愿望是殺死其他機(jī)器人。
盡管所描述的例程非常簡(jiǎn)單并且可以進(jìn)行硬編碼,但是我們將要實(shí)現(xiàn)的方法更加復(fù)雜。 我們將使用基于行為樹(shù)的方法。
首先,我們需要將機(jī)器人的所有活動(dòng)委托給它的大腦。 我將其稱為“ Routine而不是大腦。 它可以被稱為Brain或AI或其他任何東西,但我選擇Routine是因?yàn)樗鼘⒆鳛榻M成所有例程的基類。 它還將負(fù)責(zé)控制大腦中的信息流。 Routine本身是具有3個(gè)狀態(tài)的有限狀態(tài)機(jī)。
public abstract class Routine {public enum RoutineState {Success,Failure,Running}protected RoutineState state;protected Routine() { }public void start() {this.state = RoutineState.Running;}public abstract void reset();public abstract void act(Droid droid, Board board);protected void succeed() {this.state = RoutineState.Success;}protected void fail() {this.state = RoutineState.Failure;}public boolean isSuccess() {return state.equals(RoutineState.Success);}public boolean isFailure() {return state.equals(RoutineState.Failure);}public boolean isRunning() {return state.equals(RoutineState.Running);}public RoutineState getState() {return state;} }這三種狀態(tài)是:
- Running -該例程當(dāng)前正在運(yùn)行,并且將在下一回合中作用于機(jī)器人。 例如。 該例程負(fù)責(zé)將機(jī)器人移動(dòng)到某個(gè)位置,并且機(jī)器人在運(yùn)輸過(guò)程中仍然不間斷地移動(dòng)。
- Success –例行程序已經(jīng)完成,并且成功完成了應(yīng)做的工作。 例如,如果例程仍然是“移動(dòng)到位置”,則當(dāng)機(jī)器人到達(dá)目的地時(shí)例程成功。
- Failure –使用前面的示例(移至),機(jī)器人的移動(dòng)被中斷(機(jī)器人被破壞,出現(xiàn)了意外障礙或其他常規(guī)例程受到干擾)并且沒(méi)有到達(dá)目的地。
Routine類具有act(Droid droid, Board board)抽象方法。 我們需要傳入Droid和Board因?yàn)楫?dāng)例程執(zhí)行操作時(shí),它會(huì)在droid上并且在知道droid的環(huán)境即董事會(huì)上也這樣做。 例如,moveTo例程將在每轉(zhuǎn)一圈更改機(jī)器人的位置。 通常,當(dāng)例程作用于機(jī)器人時(shí),它會(huì)使用從其環(huán)境中收集的知識(shí)。 這些知識(shí)是根據(jù)實(shí)際情況建模的。 想象一下,機(jī)器人(像我們?nèi)祟愐粯?#xff09;無(wú)法看到整個(gè)世界,而只能看到它的視線范圍。 我們?nèi)祟惖囊曇按蠹s為135度,因此,如果我們要模擬人類,我們將進(jìn)入包含我們看到的部分及其中所有可見(jiàn)部分的世界切片,并讓例行程序進(jìn)行如下操作:盡其所能并得出結(jié)論。 我們也可以對(duì)機(jī)器人執(zhí)行此操作,只需傳遞range覆蓋的電路板部分,但我們現(xiàn)在將使其保持簡(jiǎn)單并使用整個(gè)電路板。 start() , succeed()和fail()方法是簡(jiǎn)單的公共可重寫方法,可以相應(yīng)地設(shè)置狀態(tài)。 另一方面, reset()方法是抽象的,必須由每個(gè)具體例程來(lái)實(shí)現(xiàn),以重置該例程專有的任何內(nèi)部狀態(tài)。 其余的是查詢例程狀態(tài)的便捷方法。
學(xué)習(xí)走路
讓我們實(shí)現(xiàn)第一個(gè)具體例程,即上面討論的MoveTo 。
public class MoveTo extends Routine {final protected int destX;final protected int destY;public MoveTo(int destX, int destY) {super();this.destX = destX;this.destY = destY;}public void reset() {start();}@Overridepublic void act(Droid droid, Board board) {if (isRunning()) {if (!droid.isAlive()) {fail();return;}if (!isDroidAtDestination(droid)) {moveDroid(droid);}}}private void moveDroid(Droid droid) {if (destY != droid.getY()) {if (destY > droid.getY()) {droid.setY(droid.getY() + 1);} else {droid.setY(droid.getY() - 1);}}if (destX != droid.getX()) {if (destX > droid.getX()) {droid.setX(droid.getX() + 1);} else {droid.setX(droid.getX() - 1);}}if (isDroidAtDestination(droid)) {succeed();}}private boolean isDroidAtDestination(Droid droid) {return destX == droid.getX() && destY == droid.getY();} } 這是一個(gè)非常基本的類,它將使機(jī)器人將一個(gè)磁貼向目的地移動(dòng),直到到達(dá)目的地。 除了機(jī)器人是否處于活動(dòng)狀態(tài)之外,它不會(huì)檢查任何其他約束。 那就是失敗的條件。 該例程具有2個(gè)參數(shù)destX和destY 。 這些是MoveTo例程將用于實(shí)現(xiàn)其目標(biāo)的最終屬性。 該例程的唯一職責(zé)是移動(dòng)機(jī)器人。 如果做不到,它將失敗。 而已。 在這里, 單一責(zé)任非常重要。 我們將看到如何將它們結(jié)合起來(lái)以實(shí)現(xiàn)更復(fù)雜的行為。 reset()方法只是將狀態(tài)設(shè)置為Running 。 它沒(méi)有其他內(nèi)部狀態(tài)或值要處理,但是需要重寫。
該例程的核心是act(Droid droid, Board board)方法,該方法執(zhí)行操作并包含邏輯。 首先,它檢查故障情況,即機(jī)器人是否死亡。 如果它已經(jīng)死了并且例程處于活動(dòng)狀態(tài)(其狀態(tài)為Running ),則該例程將無(wú)法執(zhí)行應(yīng)有的操作。 它調(diào)用超類的默認(rèn)fail()方法將狀態(tài)設(shè)置為Failure并退出該方法。
該方法的第二部分檢查成功條件。 如果機(jī)器人尚未到達(dá)目的地,則將機(jī)器人向目的地移動(dòng)一格。 如果到達(dá)目的地,則將狀態(tài)設(shè)置為Success 。 檢查isRunning()以確保該例程僅在該例程處于活動(dòng)狀態(tài)且尚未完成時(shí)才起作用。
我們還需要填寫Droid的update方法以使其使用例程。 這只是一個(gè)簡(jiǎn)單的委托。 它是這樣的:
public void update() {if (routine.getState() == null) {// hasn't started yet so we start itroutine.start();}routine.act(this, board);}它應(yīng)該僅由第6行組成,但我還要檢查一下?tīng)顟B(tài)是否為null ,如果為null ,則start例程。 這是在首次調(diào)用update啟動(dòng)例程的一種方法。 這是一種準(zhǔn)命令模式,因?yàn)樵赼ct方法中,將action命令的接收者作為參數(shù)(即機(jī)器人本身)作為參數(shù)。 我還修改了Routine類以在其中記錄不同的事件,因此我們可以看到發(fā)生了什么。
// --- omitted --- */public void start() {System.out.println(">>> Starting routine: " + this.getClass().getSimpleName());this.state = RoutineState.Running;}protected void succeed() {System.out.println(">>> Routine: " + this.getClass().getSimpleName() + " SUCCEEDED");this.state = RoutineState.Success;}protected void fail() {System.out.println(">>> Routine: " + this.getClass().getSimpleName() + " FAILED");this.state = RoutineState.Failure;}// --- omitted --- */讓我們用一個(gè)簡(jiǎn)單的Test類進(jìn)行Test 。
public class Test {public static void main(String[] args) {// SetupBoard board = new Board(10, 10);Droid droid = new Droid("MyDroid", 5, 5, 10, 1, 2);board.addDroid(droid);Routine moveTo = new MoveTo(7, 9);droid.setRoutine(moveTo);System.out.println(droid);// Execute 5 turns and print the droid outfor (int i = 0; i < 5; i++) {droid.update();System.out.println(droid);}} }這是帶有main方法的標(biāo)準(zhǔn)類,該方法首先設(shè)置一個(gè)10 x 10的正方形Board并在坐標(biāo)5,5處添加具有所提供屬性的Droid 。 在第10行上,我們創(chuàng)建了MoveTo例程,該例程將目標(biāo)設(shè)置為(7,9) 。 我們將此例程設(shè)置為機(jī)器人的唯一例程( 第11行),并打印機(jī)器人的狀態(tài)( 第12行)。 然后我們執(zhí)行5轉(zhuǎn)并在每轉(zhuǎn)之后顯示機(jī)器人的狀態(tài)。
運(yùn)行Test我們將看到以下內(nèi)容打印到sysout中:
Droid{name=MyDroid, x=5, y=5, health=10, range=2, damage=1}>>> Starting routine: MoveToDroid{name=MyDroid, x=6, y=6, health=10, range=2, damage=1}Droid{name=MyDroid, x=7, y=7, health=10, range=2, damage=1}Droid{name=MyDroid, x=7, y=8, health=10, range=2, damage=1}>>> Routine: MoveTo SUCCEEDEDDroid{name=MyDroid, x=7, y=9, health=10, range=2, damage=1}Droid{name=MyDroid, x=7, y=9, health=10, range=2, damage=1}如我們所見(jiàn),機(jī)器人按照預(yù)期從位置(5,5)開(kāi)始。 首次調(diào)用update方法,啟動(dòng)MoveTo例程。 隨后的3次對(duì)更新的調(diào)用將通過(guò)將機(jī)器人的坐標(biāo)每轉(zhuǎn)一圈將其移動(dòng)到目的地。 例程成功后,傳遞給該例程的所有調(diào)用都將被忽略,因?yàn)樗淹瓿伞?
這是第一步,但不是很有幫助。 假設(shè)我們想讓我們的機(jī)器人在板上徘徊。 為此,我們需要重復(fù)執(zhí)行MoveTo例程,但是每次重新啟動(dòng)MoveTo例程時(shí),都需要隨機(jī)選擇目的地。
徘徊
但是,讓我們從Wander例程開(kāi)始。 它不過(guò)是MoveTo不過(guò)只要我們了解棋盤,我們就會(huì)生成一個(gè)隨機(jī)目的地。
public class Wander extends Routine {private static Random random = new Random();private final Board board;private MoveTo moveTo;@Overridepublic void start() {super.start();this.moveTo.start();}public void reset() {this.moveTo = new MoveTo(random.nextInt(board.getWidth()), random.nextInt(board.getHeight()));}public Wander(Board board) {super();this.board = board;this.moveTo = new MoveTo(random.nextInt(board.getWidth()), random.nextInt(board.getHeight()));}@Overridepublic void act(Droid droid, Board board) {if (!moveTo.isRunning()) {return;}this.moveTo.act(droid, board);if (this.moveTo.isSuccess()) {succeed();} else if (this.moveTo.isFailure()) {fail();}} }遵循單一責(zé)任原則, Wander類的唯一目的是在板上選擇隨機(jī)的目的地。 然后,它使用MoveTo例程將機(jī)器人獲取到新的目的地。 reset方法將重新啟動(dòng)它,并選擇一個(gè)新的隨機(jī)目標(biāo)。 目標(biāo)是在構(gòu)造函數(shù)中設(shè)置的。 如果我們希望機(jī)器人漫游,可以將Test類更改為以下內(nèi)容:
public class Test {public static void main(String[] args) {// SetupBoard board = new Board(10, 10);Droid droid = new Droid("MyDroid", 5, 5, 10, 1, 2);board.addDroid(droid);Routine routine = new Wander(board);droid.setRoutine(routine);System.out.println(droid);for (int i = 0; i < 5; i++) {droid.update();System.out.println(droid);}} }輸出將類似于以下內(nèi)容:
Droid{name=MyDroid, x=5, y=5, health=10, range=2, damage=1}>>> Starting routine: Wander>>> Starting routine: MoveToDroid{name=MyDroid, x=6, y=6, health=10, range=2, damage=1}Droid{name=MyDroid, x=7, y=7, health=10, range=2, damage=1}Droid{name=MyDroid, x=7, y=8, health=10, range=2, damage=1}>>> Routine: MoveTo SUCCEEDED>>> Routine: Wander SUCCEEDEDDroid{name=MyDroid, x=7, y=9, health=10, range=2, damage=1}Droid{name=MyDroid, x=7, y=9, health=10, range=2, damage=1}注意Wander如何包含和委托MoveTo例程。
重復(fù),重復(fù),重復(fù)…
一切都很好,但是如果我們希望機(jī)器人反復(fù)游蕩怎么辦? 我們將創(chuàng)建一個(gè)Repeat例程,其中將包含要重復(fù)的例程。 同樣,我們將使該例程生效,以便它可以使用一個(gè)參數(shù)來(lái)指定要重復(fù)一個(gè)例程多少次。 如果不接受參數(shù),則它將永久重復(fù)包含例程,或者直到機(jī)器人死掉為止。
public class Repeat extends Routine {private final Routine routine;private int times;private int originalTimes;public Repeat(Routine routine) {super();this.routine = routine;this.times = -1; // infinitethis.originalTimes = times;}public Repeat(Routine routine, int times) {super();if (times < 1) {throw new RuntimeException("Can't repeat negative times.");}this.routine = routine;this.times = times;this.originalTimes = times;}@Overridepublic void start() {super.start();this.routine.start();}public void reset() {// reset countersthis.times = originalTimes;}@Overridepublic void act(Droid droid, Board board) {if (routine.isFailure()) {fail();} else if (routine.isSuccess()) {if (times == 0) {succeed();return;}if (times > 0 || times <= -1) {times--;routine.reset();routine.start();}}if (routine.isRunning()) {routine.act(droid, board);}} }該代碼很容易理解,但是我將解釋一些添加的內(nèi)容。 屬性routine在構(gòu)造函數(shù)中傳遞,該例程將被重復(fù)。 originalTimes是一個(gè)存儲(chǔ)變量,其中包含初始次數(shù)值,因此我們可以使用reset()調(diào)用重新啟動(dòng)例程。 這只是初始狀態(tài)的備份。 times屬性是提供的例程將被重復(fù)多少次。 如果它是-1那么它是無(wú)限的。 所有這些都在act方法內(nèi)的邏輯中進(jìn)行了編碼。 為了測(cè)試這一點(diǎn),我們需要?jiǎng)?chuàng)建一個(gè)Repeat例程并提供要重復(fù)的內(nèi)容。 例如,要使機(jī)器人不斷徘徊,我們需要這樣做:
Routine routine = new Repeat((new Wander(board)));droid.setRoutine(routine);如果我們反復(fù)調(diào)用update ,我們將看到機(jī)器人將不斷移動(dòng)。 檢查以下樣本輸出:
Droid{name=MyDroid, x=5, y=5, health=10, range=2, damage=1}>> Starting routine: Repeat>>> Starting routine: Wander>>> Starting routine: MoveToDroid{name=MyDroid, x=4, y=6, health=10, range=2, damage=1}>>> Routine: MoveTo SUCCEEDED>>> Routine: Wander SUCCEEDEDDroid{name=MyDroid, x=4, y=7, health=10, range=2, damage=1}>>> Starting routine: Wander>>> Starting routine: MoveToDroid{name=MyDroid, x=5, y=6, health=10, range=2, damage=1}Droid{name=MyDroid, x=6, y=5, health=10, range=2, damage=1}Droid{name=MyDroid, x=7, y=4, health=10, range=2, damage=1}Droid{name=MyDroid, x=8, y=3, health=10, range=2, damage=1}Droid{name=MyDroid, x=8, y=2, health=10, range=2, damage=1}>>> Routine: MoveTo SUCCEEDED>>> Routine: Wander SUCCEEDEDDroid{name=MyDroid, x=8, y=1, health=10, range=2, damage=1}>>> Starting routine: Wander>>> Starting routine: MoveToDroid{name=MyDroid, x=7, y=2, health=10, range=2, damage=1}Droid{name=MyDroid, x=6, y=3, health=10, range=2, damage=1}請(qǐng)注意Repeat例程不會(huì)結(jié)束。
組裝情報(bào)
到目前為止,我們只是在編寫行為。 但是,如果我們想對(duì)機(jī)器人進(jìn)行決策并建立更復(fù)雜的行為,該怎么辦? 輸入行為樹(shù)。 這個(gè)術(shù)語(yǔ)沒(méi)有描述它的含義,我發(fā)現(xiàn)的大多數(shù)文章也沒(méi)有描述。 我將從首先要實(shí)現(xiàn)的目標(biāo)開(kāi)始,希望這一切都是有意義的。 我想實(shí)現(xiàn)本文開(kāi)頭所述的行為。 我希望我的機(jī)器人掃描其范圍內(nèi)是否有較弱的機(jī)器人,如果有,請(qǐng)使其接合,否則請(qǐng)?zhí)与x。 看下圖。 它顯示了一棵樹(shù)。 它不過(guò)是由多個(gè)不同例程組成的例程。 每個(gè)節(jié)點(diǎn)都是一個(gè)例程,我們將必須實(shí)現(xiàn)一些特殊的例程。
Droid AI(行為樹(shù))
讓我們打破常規(guī)。
- Repeat –是較早實(shí)施的例程。 它將永遠(yuǎn)重復(fù)給定的例程,或者直到嵌入式例程失敗為止。
- Sequence –順序例程只有在其包含的所有例程都成功后才能成功。 例如,要攻擊機(jī)器人,敵方機(jī)器人必須在射程范圍內(nèi),需要裝載槍支,機(jī)器人需要拉動(dòng)扳機(jī)。 一切按此順序進(jìn)行。 因此,該序列包含例程列表并對(duì)其執(zhí)行操作,直到所有例程都成功為止。 如果槍未裝彈,則沒(méi)有必要扳動(dòng)扳機(jī),因此整個(gè)攻擊都是失敗的。
- Selector –此例程包含一個(gè)或多個(gè)例程的列表。 當(dāng)它起作用時(shí),如果列表中的例程之一成功,它將成功。 例程的執(zhí)行順序由例程的傳遞順序設(shè)置。如果我們想隨機(jī)化例程的執(zhí)行,則創(chuàng)建一個(gè)Random例程很容易,其唯一目的是隨機(jī)化例程列表通過(guò)了。
- 所有灰色例程都是樹(shù)上的葉子,這意味著它們不能再包含任何后續(xù)例程,它們是作用于接收者的機(jī)器人上的例程。
上面的樹(shù)代表了我們想要實(shí)現(xiàn)的非常基本的AI。 讓我們從根開(kāi)始。
Repeat –將無(wú)限期重復(fù)選擇器,直到兩個(gè)分支都無(wú)法成功執(zhí)行。 選擇器中的例程為: Attack a droid和Wander 。 如果兩者均失敗,則表明機(jī)器人已死。 Attack a droid例程是一系列例程,這意味著所有例程都必須成功才能使整個(gè)分支成功。 如果失敗,則后退是通過(guò)Wander選擇一個(gè)隨機(jī)目的地并將其移動(dòng)到那里。 然后重復(fù)。
我們需要做的就是實(shí)現(xiàn)例程。 例如, IsDroidInRange可能看起來(lái)像這樣:
public class IsDroidInRange extends Routine {public IsDroidInRange() {}@Overridepublic void reset() {start();}@Overridepublic void act(Droid droid, Board board) {// find droid in rangefor (Droid enemy : board.getDroids()) {if (!droid.getName().equals(enemy)) {if (isInRange(droid, enemy)) {succeed();break;}}}fail();}private boolean isInRange(Droid droid, Droid enemy) {return (Math.abs(droid.getX() - enemy.getX()) <= droid.getRange()|| Math.abs(droid.getY() - enemy.getY()) < droid.getRange());} }這是一個(gè)非?;镜膶?shí)現(xiàn)。 它確定機(jī)器人是否在范圍內(nèi)的方式是,通過(guò)遍歷板上的所有機(jī)器人,以及敵方機(jī)器人(假設(shè)名稱唯一)是否在范圍內(nèi),則成功了。 否則失敗。 當(dāng)然,我們需要以某種方式將這個(gè)機(jī)器人輸入下一個(gè)例程,即IsEnemyStronger 。 這可以通過(guò)為droid提供上下文來(lái)實(shí)現(xiàn)。 一種簡(jiǎn)單的方法可能是Droid類可以具有一個(gè)屬性nearestEnemy ,如果success ,例程將填充該字段,而失敗則將其清除。 這樣,以下例程可以訪問(wèn)droid的內(nèi)部,并使用該信息確定其成功或失敗的情況。 當(dāng)然,可以并且應(yīng)該對(duì)此進(jìn)行擴(kuò)展,以便機(jī)器人將在其范圍內(nèi)包含一系列機(jī)器人,并有例程確定機(jī)器人應(yīng)該飛行還是戰(zhàn)斗。 但這不是本介紹的范圍。
我不會(huì)實(shí)現(xiàn)本文中的所有例程,但是您將能夠在github上查看代碼: https : //github.com/obviam/behavior-trees ,我將添加越來(lái)越多的例程。
然后去哪兒?
僅僅看一下就可以做出很多改進(jìn)。 作為測(cè)試系統(tǒng)的第一步,為了方便起見(jiàn),我將例程的創(chuàng)建移至工廠。
/*** Static convenience methods to create routines*/ public class Routines {public static Routine sequence(Routine... routines) {Sequence sequence = new Sequence();for (Routine routine : routines) {sequence.addRoutine(routine);}return sequence;}public static Routine selector(Routine... routines) {Selector selector = new Selector();for (Routine routine : routines) {selector.addRoutine(routine);}return selector;}public static Routine moveTo(int x, int y) {return new MoveTo(x, y);}public static Routine repeatInfinite(Routine routine) {return new Repeat(routine);}public static Routine repeat(Routine routine, int times) {return new Repeat(routine, times);}public static Routine wander(Board board) {return new Wander(board);}public static Routine IsDroidInRange() {return new IsDroidInRange();}}這將允許以更優(yōu)雅的方式測(cè)試某些方案。 例如,要放置2個(gè)具有不同行為的機(jī)器人,您可以執(zhí)行以下操作:
public static void main(String[] args) {Board board = new Board(25, 25);Droid droid1 = new Droid("Droid_1", 2, 2, 10, 1, 3);Droid droid2 = new Droid("Droid_2", 10, 10, 10, 2, 2);Routine brain1 = Routines.sequence(Routines.moveTo(5, 10),Routines.moveTo(15, 12),Routines.moveTo(2, 4));droid1.setRoutine(brain1);Routine brain2 = Routines.sequence(Routines.repeat(Routines.wander(board), 4));droid2.setRoutine(brain2);for (int i = 0; i < 30; i++) {System.out.println(droid1.toString());System.out.println(droid2.toString());droid1.update();droid2.update();}}當(dāng)然,到目前為止,這并不是最好的解決方案,但是它比例程的不斷實(shí)例化要好。 理想情況下,應(yīng)該通過(guò)外部腳本編寫腳本或從外部加載AI,例如,通過(guò)腳本編寫,或至少以JSON形式提供,并由AI匯編程序創(chuàng)建。 這樣,每次調(diào)整AI時(shí)都不需要重新編譯游戲。 但同樣,這也不是本文的范圍。
此外,我們?nèi)绾螞Q定哪個(gè)動(dòng)作需要轉(zhuǎn)牌/勾號(hào)或立即得到評(píng)估? 一種可能的解決方案是將動(dòng)作點(diǎn)分配給機(jī)器人可以花費(fèi)一轉(zhuǎn)的動(dòng)作點(diǎn)(如果是實(shí)時(shí)的,則勾選),并為每個(gè)動(dòng)作分配一個(gè)成本。 只要機(jī)器人機(jī)器人用完了點(diǎn),我們就可以繼續(xù)前進(jìn)。 我們還需要跟蹤哪個(gè)例程是當(dāng)前例程,以便我們優(yōu)化樹(shù)的遍歷。 如果AI非常復(fù)雜,尤其是在實(shí)時(shí)游戲中,這將很有幫助。
如果您認(rèn)為本文很有用,并且想要獲取代碼,請(qǐng)檢查github存儲(chǔ)庫(kù)。 您也可以返回,因?yàn)槲掖蛩銛U(kuò)展并更新它,從而使它演變成更完整的AI示例。 因?yàn)檫@是我第一次接觸AI,所以還有很多事情需要改進(jìn),而且我始終對(duì)改進(jìn)有很多意見(jiàn)和想法。
- https://github.com/obviam/behavior-trees
翻譯自: https://www.javacodegeeks.com/2014/08/game-ai-an-introduction-to-behaviour-trees.html
游戲ai 行為樹(shù)
總結(jié)
以上是生活随笔為你收集整理的游戏ai 行为树_游戏AI –行为树简介的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 什么是发蓝处理 发蓝处理的解释
- 下一篇: 鸭嘴杯有必要买吗 鸭嘴杯有没有必要买