游戏AI –行为树简介
游戲AI是一個非常廣泛的主題,盡管有很多資料,但我找不到能以較慢,更容易理解的速度緩慢介紹這些概念的東西。 本文將嘗試解釋如何基于行為樹的概念來設計一個非常簡單但可擴展的AI系統。
什么是AI?
人工智能是參與游戲的實體表現出的類似于人類的行為。 這是實體對智能和周到動作的幻想,而不是實際的智能推理驅動的行為。 目的是試圖欺騙玩家,使他們認為其他“智能”實體是由人類而不是機器控制的。 說起來容易做起來難,但是我們可以使用很多技巧來實現一些真正好的,看似隨機和“智能”的行為。
一個例子
在跳入有趣的話題之前,讓我們起草一個我們想要實現的目標的計劃。 同樣,我將以機器人為例。 想象一下一個競技場,機器人將在其中爭奪戰,最后一個站位的機器人都是贏家。
競技場將是一塊木板,機器人將隨機放置在上面。 我們將其制作為基于回合的游戲,以便我們可以追蹤整個AI的發展,但可以輕松地將其轉變為實時游戲。
規則很簡單:
- 木板是矩形
- 機器人可以在任一方向上每轉一圈將瓷磚移動到任何相鄰的未占用瓷磚上
- 機器人具有一定范圍,并且可以向其范圍內的機器人發射
- 機器人將具有通常的屬性:它們造成的傷害和生命值
為了簡單起見,我們將使用非常簡單的結構。 該應用程序將具有Droid類和Board類。 機器人將具有以下定義它的屬性:
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只是具有一些屬性的簡單pojo。 這些屬性不言自明,但這是它們的簡短摘要:
- name -這個機器人的唯一名稱,可用于ID,以及。
- x和y –網??格上的坐標。
- health , damage和range -它說了什么。
- board –是機器人所在的Board以及其他機器人的參考。 我們需要這樣做,因為機器人將通過了解其環境(即board <./ li>)來做出決策。
還有一個空的update()方法,每次droid結束旋轉時都會調用該方法。 如果是實時游戲,則從游戲循環(最好是從游戲引擎)中調用update方法。
還有一些明顯的getter和setter以及toString()方法,它們從清單中省略了。 Board類非常簡單。
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 ,并且包含機器人列表。 它還包含一些方便的方法來檢查給定坐標上是否已存在機器人,以及一種輕松地逐個添加機器人的方法。
到目前為止,這是相當標準的。 我們可以在板上散布一些機器人,但它們不會做任何事情。 我們可以創建板,向其中添加一些機器人,然后開始調用update() 。 它們只是一些愚蠢的機器人。
不太傻的機器人
為了使droid做某事,我們可以在其update()方法中實現邏輯。 這就是所謂的每一次跳動或在我們的情況下每轉一次的方法。 例如,我們希望我們的機器人在競技場(木板)上徘徊,如果他們看到射程范圍內的其他機器人,請接合它們并開始向它們射擊直到它們死亡。 這將是非常基本的AI,但仍然是AI。
偽代碼如下所示:
if enemy in range then fire missile at it
otherwise pick a random adjacent tile and move there
這意味著,機器人之間的任何相互作用都將導致僵持,較弱的機器人會被破壞。 我們可能要避免這種情況。 因此,我們可以補充一下,如果機器人有可能丟失,請嘗試逃跑。 僅在無處可逃時站起來戰斗。
if enemy in range then
if enemy is weaker then fight escape route exists then escape fight wander
一切都很好。 機器人將開始“智能化”地行動,但除非我們添加更多代碼來做更多聰明的事情,否則它們仍然非常有限。 而且,它們將起到相同的作用。 想象一下,如果將它們放在更復雜的舞臺上。 在競技場上,有一些道具如力量道具可以增強力量,可以避免障礙。 例如,當機器人四處飛來飛去時,請決定在拿起醫療/修理包與拿起武器加電之間。
它很快就會失控。 如果我們想要一個行為不同的機器人該怎么辦。 一個是攻擊機器人,另一個是修理機器人。 我們當然可以通過對象合成來實現這一目標 ,但是機器人的大腦將極其復雜,游戲設計的任何變化都需要付出巨大的努力才能適應。
讓我們看看是否可以提出一個可以解決這些問題的系統。
大腦來了
我們可以將機器人的AI模塊視為某種大腦。 大腦由遵循一系列規則作用于機器人的幾個例程組成。 這些規則支配著例程的執行,因此它將生存和贏得比賽的機會最大化作為最終目標。 如果我們想到由例程組成的人類大腦,并以馬斯洛的需求層次作為參考,我們可以立即識別出一些例程。
- 生理程序 –每次都需要執行的程序,否則將沒有任何生命
- 生存例程 –一旦滿足生活條件,就必須執行此例程,以確保長期生存
- 有抱負的例程 –如果在維持生計后仍需要再次執行生計時還剩下時間,將執行此例程
讓我們分解一下人類的智慧。 人類需要呼吸才能生存。 每次呼吸都消耗能量。 一個人可以呼吸這么多,直到能量耗盡。 要補充能量,就需要吃飯。 一個人只有在他/她有食物可支配的情況下才能吃飯。 如果沒有可用的食物,則需要購買它消耗更多的能量。 如果購買食物需要很長時間(例如,需要狩獵)并且獲得的食物量很少,那么在食用之后,人們需要更多食物,并且例程會立即重新開始。 如果從超市購買散裝食品,那么吃完后還有很多剩余的空間,因此人類可以繼續做更多有趣的事情,這些都是他/她理想的部分。 例如,結交朋友,發動戰爭或看電視之類的事情。
只要思考一下人腦中有多少東西才能使我們發揮功能并嘗試對其進行模擬。 所有這些都忽略了我們正在獲得和響應的大多數刺激。 為此,我們需要對人體進行參數設置,并且由刺激觸發的每個傳感器將更新正確的參數,并且執行的例程將檢查新值并采取相應措施。 我現在不會描述它,但是您希望我有所想法。
讓我們切換回更簡單的機器人。 如果我們嘗試使人類例程適應機器人,我們將得到如下所示:
- 生理的/存在的 –在這個例子中我們可以忽略這部分,因為我們設計的是機器人,它們是機械的。 當然,對于它們來說,它們仍然需要從電池或其他可能耗盡的能源中獲取能量(例如動作點)。 為了簡單起見,我們將忽略這一點,并認為能源是無限的。
- 生存/安全 -該例程將確保機器人在避免當前威脅的情況下在當前回合中存活并存活。
- 有抱負 –一旦安全例行程序簽出就可以啟動,并且不必激活機器人的逃逸例行程序。 機器人目前的簡單愿望是殺死其他機器人。
盡管所描述的例程非常簡單并且可以進行硬編碼,但是我們將要實現的方法更加復雜。 我們將使用基于行為樹的方法。
首先,我們需要將機器人的所有活動委托給它的大腦。 我將其稱為“ Routine而不是大腦。 它可以被稱為Brain或AI或其他任何東西,但是我選擇Routine是因為它將作為組成所有例程的基類。 它還將負責控制大腦中的信息流。 Routine本身是具有3個狀態的有限狀態機。
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;} }這三種狀態是:
- Running -該例程當前正在運行,并且將在下一回合中作用于機器人。 例如。 該例程負責將機器人移動到某個位置,并且機器人在運輸過程中仍然不間斷地移動。
- Success –例行程序已經完成,并且成功完成了應做的工作。 例如,如果例程仍然是“移動到位置”,則當機器人到達目的地時例程成功。
- Failure –使用前面的示例(移至),機器人的移動被中斷(機器人被破壞,出現了意外障礙或其他常規例程受到干擾)并且沒有到達目的地。
Routine類具有act(Droid droid, Board board)抽象方法。 我們需要傳入Droid和Board因為當例程執行操作時,它會在droid上并且在知道droid的環境即董事會上也這樣做。 例如,moveTo例程將在每轉一圈更改機器人的位置。 通常,當例程對機器人執行操作時,它將使用從其環境中收集的知識。 這些知識是根據實際情況建模的。 想象一下,機器人(像我們人類一樣)無法看到整個世界,而只能看到它的視線范圍。 美國人類的視野約為135度,因此,如果我們要模擬人類,我們將穿越世界的一部分,其中包含我們看到的部分以及其中的所有可見組件,并讓例行程序進行如下操作:盡其所能并得出結論。 我們也可以對機器人執行此操作,只需傳遞該range所覆蓋的電路板部分,但我們現在將使其保持簡單并使用整個電路板。 start() , succeed()和fail()方法是簡單的公共可重寫方法,它們會相應地設置狀態。 另一方面, reset()方法是抽象的,必須由每個具體例程來實現,以重置該例程專有的任何內部狀態。 其余的是查詢例程狀態的便捷方法。
學習走路
讓我們實現第一個具體例程,即上面討論的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();} } 這是一個非常基本的類,它將使機器人將一個磁貼向目的地移動,直到到達目的地。 除了機器人是否處于活動狀態之外,它不會檢查任何其他約束。 那就是失敗的條件。 該例程具有2個參數destX和destY 。 這些是MoveTo例程將用來實現其目標的最終屬性。 該例程的唯一職責是移動機器人。 如果無法做到,它將失敗。 而已。 在這里, 單一責任非常重要。 我們將看到如何將這些結合起來以實現更復雜的行為。 reset()方法只是將狀態設置為Running 。 它沒有其他要處理的內部狀態或值,但是需要重寫。
該例程的核心是act(Droid droid, Board board)方法,該方法執行操作并包含邏輯。 首先,它檢查故障情況,即機器人是否死亡。 如果它已死并且例程處于活動狀態(其狀態為Running ),則例程無法執行應有的操作。 它調用超類的默認fail()方法將狀態設置為Failure并退出該方法。 該方法的第二部分檢查成功條件。 如果機器人尚未到達目的地,則將機器人向目標位置移動一格。 如果到達目的地,則將狀態設置為Success 。 檢查isRunning()以確保該例程僅在該例程處于活動狀態且尚未完成時才起作用。
我們還需要填寫Droid的update方法以使其使用例程。 這只是一個簡單的委托。 它是這樣的:
public void update() {if (routine.getState() == null) {// hasn't started yet so we start itroutine.start();}routine.act(this, board);}它應該僅由第6行組成,但我還要檢查一下狀態是否為null ,如果為null ,則start例程。 這是在首次調用update啟動例程的一種方法。 這是一種準命令模式,因為在act方法中,將action命令的接收者作為參數(即機器人本身)作為參數。 我還修改了Routine類以在其中記錄不同的事件,因此我們可以看到正在發生的事情。
// --- 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 --- */讓我們用一個簡單的Test類進行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方法的標準類,該方法首先設置一個10 x 10的正方形Board并在坐標5,5處添加具有所提供屬性的Droid 。 在第10行上,我們創建了MoveTo例程,該例程將目標設置為(7,9) 。 我們將此例程設置為機器人的唯一例程( 第11行),并打印機器人的狀態( 第12行)。 然后我們執行5轉并在每轉之后顯示機器人的狀態。
運行Test我們將看到以下內容打印到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}如我們所見,機器人按照預期從位置(5,5)開始。 首次調用update方法,啟動MoveTo例程。 隨后的3次對更新的調用將通過將機器人的坐標每轉一圈將其移動到目的地。 例程成功后,將忽略傳遞給該例程的所有調用,因為它已完成。
這是第一步,但不是很有幫助。 假設我們想讓我們的機器人在板上徘徊。 為此,我們將需要重復執行MoveTo例程,但是每次重新啟動MoveTo例程時,都需要隨機選擇目的地。
徘徊
但是,讓我們從Wander例程開始。 它不過是MoveTo不過只要我們知道棋盤,我們就會生成一個隨機目的地。
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();}} }遵循單一責任原則, Wander類的唯一目的是在板上選擇隨機的目的地。 然后,它使用MoveTo例程將機器人獲取到新的目的地。 reset方法將重新啟動它并選擇一個新的隨機目標。 目標是在構造函數中設置的。 如果我們希望機器人漫游,可以將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 routine = new Wander(board);droid.setRoutine(routine);System.out.println(droid);for (int i = 0; i < 5; i++) {droid.update();System.out.println(droid);}} }輸出將類似于以下內容:
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例程。
重復,重復,重復...
一切都很好,但是如果我們希望機器人反復游蕩怎么辦? 我們將創建一個Repeat例程,其中將包含要重復的例程。 另外,我們將使該例程生效,以便它可以接受一個參數來指定我們要重復該例程多少次。 如果不接受參數,則它將永久重復包含例程,或者直到機器人死掉為止。
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);}} }該代碼很容易理解,但是我將解釋一些添加的內容。 屬性routine在構造函數中傳遞,該例程將被重復。 originalTimes是一個存儲變量,其中包含初始次數值,因此我們可以使用reset()調用重新啟動例程。 這只是初始狀態的備份。 times屬性是提供的例程將被重復多少次。 如果它是-1那么它是無限的。 所有這些都編碼在act方法內部的邏輯中。 為了測試這一點,我們需要創建一個Repeat例程并提供要重復的內容。 例如,要使機器人不斷徘徊,我們需要這樣做:
Routine routine = new Repeat((new Wander(board)));droid.setRoutine(routine);如果我們反復調用update ,我們將看到機器人將不斷移動。 檢查以下樣本輸出:
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}請注意Repeat例程不會結束。
組裝情報
到目前為止,我們只是在編寫行為。 但是,如果我們想對機器人進行決策并建立更復雜的行為,該怎么辦? 輸入行為樹。 這個術語沒有描述它的含義,我發現的大多數文章也沒有描述。 我將從首先要實現的目標開始,希望這一切都會有意義。 我想實現本文開頭所述的行為。 我希望我的機器人掃描其范圍內是否有較弱的機器人,如果是,請使其接合,否則請逃離。 看下圖。 它顯示了一棵樹。 它不過是由多個不同例程組成的例程。 每個節點都是一個例程,我們將必須實現一些特殊的例程。
Droid AI(行為樹)
讓我們打破常規。
- Repeat –是較早實施的例程。 它將永遠重復給定的例程,或者直到嵌入式例程失敗為止。
- Sequence –順序例程只有在其包含的所有例程都成功后才能成功。 例如,要攻擊機器人,敵方機器人必須在射程范圍內,需要裝載槍支,機器人需要拉動扳機。 一切按此順序進行。 因此,該序列包含例程列表并對其執行操作,直到所有例程都成功為止。 如果槍未裝彈,則沒有必要扳動扳機,因此整個攻擊都是失敗的。
- Selector –此例程包含一個或多個例程的列表。 當它起作用時,如果列表中的例程之一成功,它將成功。 例程的執行順序由例程的傳入順序設置。如果我們想隨機化例程的執行,則創建一個Random例程很容易,其唯一目的是隨機化例程列表通過了。
- 所有灰色例程都是樹上的葉子,這意味著它們不能再包含任何后續例程,而這些例程將作用于作為接收者的droid上。
上面的樹代表了我們想要實現的非常基本的AI。 讓我們從根開始。
Repeat -無限期重復選擇器,直到兩個分支都無法成功執行。 選擇器中的例程是: Attack a droid and Wander 。 如果兩者均失敗,則表明機器人已死。 Attack a droid例程是一系列例程,這意味著所有例程都必須成功才能使整個分支成功。 如果失敗,則后退是通過Wander選擇一個隨機目的地并將其移動到那里。 然后重復。
我們需要做的就是實現例程。 例如, IsDroidInRange可能看起來像這樣:
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());} }這是一個非常基本的實現。 它確定機器人是否在范圍內的方法是,通過遍歷板上的所有機器人,以及敵方機器人(假設名稱唯一)是否在范圍內,便成功了。 否則失敗。 當然,我們需要以某種方式將這個機器人輸入下一個例程,即IsEnemyStronger 。 這可以通過為droid提供上下文來實現。 一個簡單的方法可以是: Droid類可以有一個屬性nearestEnemy和success的例程將填充該字段,失敗時它會清除它。 這樣,以下例程可以訪問droid的內部,并使用該信息確定其成功或失敗的情況。 當然,可以并且應該對此進行擴展,以便機器人將在其范圍內包含一系列機器人,并有例程確定機器人應該飛行還是戰斗。 但這不是本介紹的范圍。
我不會實現本文中的所有例程,但是您將能夠在github上查看代碼: https : //github.com/obviam/behavior-trees ,我將添加越來越多的例程。
然后去哪兒?
只看一下就可以做出很多改進。 作為測試系統的第一步,為了方便起見,我將例程的創建移至工廠。
/*** 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();}}這將允許以更優雅的方式測試某些方案。 例如,要放置2個具有不同行為的機器人,您可以執行以下操作:
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();}}當然,到目前為止,這并不是最好的解決方案,但是它比例程的不斷實例化要好。 理想情況下,應該使用腳本編寫腳本并從外部源加載AI,例如通過腳本編寫,或至少以JSON形式提供,并由AI匯編程序創建。 這樣,每次調整AI時都不需要重新編譯游戲。 但同樣,這也不是本文的范圍。
另外,我們將如何決定哪個動作需要轉牌/勾號或立即得到評估? 一種可能的解決方案是將動作點分配到機器人可以花費一轉的動作點(如果是實時的,則勾選),并為每個動作分配一個成本。 只要機器人機器人用完點,我們就可以繼續前進。 我們還需要跟蹤哪個例程是當前例程,以便我們優化樹的遍歷。 如果AI非常復雜(特別是在實時游戲中),這將很有幫助。
如果您認為本文很有用,并且想要獲取代碼,請檢查github存儲庫。 您也可以返回,因為我打算對其進行擴展和更新,以便使其演變為更完整的AI示例。 因為這是我與AI的第一次接觸,所以還有很多事情需要改進,而且我始終對改進有很多意見和想法。
- https://github.com/obviam/behavior-trees
翻譯自: https://www.javacodegeeks.com/2014/08/game-ai-an-introduction-to-behaviour-trees.html
總結
以上是生活随笔為你收集整理的游戏AI –行为树简介的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 水库泄洪是什么意思 水库泄洪的解释
- 下一篇: WAI-ARIA对自动完成小部件的支持