C++ 版本的 行為樹的簡單實現,js簡單實現樹形結構
如果你想轉載這篇文章呢,請嚴格按照以下格式注明出處和作者
出處:http://www.cnblogs.com/anxin1225/p/4827294.html
作者:Anxin1225、Bianchx、Linker(其實他們都是一個人。。)
行為樹是一種簡潔明了的整理業務邏輯的有效方法。至於他的好處,不做贅述。
由於項目的需要,所以實現了一個非常簡單的行為樹,來應對我們的需求。之所以說簡單,是因為我並沒有實現很多控制節點,而只是實現了最基礎的業務的三個節點而已。至於其他的你覺得有用的控制節點,可以自己修改出來。
簡單說說我實現的節點:基礎節點、單條節點、列表節點、選擇節點、順序節點、取反節點。這幾個節點分為相對較為基礎的節點,和業務節點。基礎的節點包括:基礎節點、單條節點、列表節點。基礎的節點的主要作用是定義,定義最基礎的調用方法和關於子節點應該怎麼樣保存。業務節點包含選擇節點、順序節點和取反節點。他們的繼承關系如下:基礎節點是最基礎的節點;單條節點和列表節點繼承自基礎節點;選擇節點和順序節點繼承自列表節點;取反節點繼承自單條節點。
來簡單說一下各個節點的作用
基礎節點:
1、invoke函數,被調用時,返回true或者false
2、destroy函數,節點被釋放時會遞歸式的釋放所有依附於此節點的子節點和曾子節點
3、設置和獲取 Describe 的函數,用於打印AITree時的結構描述
4、設置和獲取 Name 的函數,用於打印AITree時的名稱描述和調用時,遞歸描述的打印
列表節點:
1、包含一個有序的子集列表,可以添加和獲取子集列表的引用
單條節點:
1、包含一個子集節點,可以設置和獲取子集節點
選擇節點:
1、被調用時,如果沒有子集節點則會直接返回false
2、調用時,會依次從前往後執行,任何一個子集節點返回了true,則終止循環,直接返回true
3、當所有的子集節點都沒有返回true時,則會返回false
順序節點:
1、被調用時,如果沒有子集節點則會直接返回false
2、調用時,會依次從前往後執行,任何一個子集節點返回了false,則終止循環,直接返回false
3、 當所有的子集節點都沒有返回false時,則會返回true
取反節點:
1、被調用時,如果沒有子集節點則直接返回false
2、存在子集節點時,則會調用子集節點,並且將結果取反並返回
實現了這些節點之後就可以實現以下圖示的大部分功能(手比較殘,又加上身邊沒有工具,所以用文字的表示吧)
先簡單解釋一下這個圖什麼意思,第一個是節點的名字,注入的時候寫的,可以是中文也可以是英文,這個無所謂,畢竟只有這個地方在用。第二個參數是當前實例的描述,如果是用來幫助你理解這個樹的
再簡單解釋一下,這段邏輯是什麼意思。這個是一個寵物的邏輯,如果附近有金幣呢,他就取撿金幣;如果沒有金幣呢並且很長時間沒有撿到金幣並且很長時間沒有回到主人身邊了,那就回到主人身邊,否則就隨便走走。
其實這個邏輯真的挺簡單的,如果,是按照普通的方式來寫的話。就會在各種狀態之間判斷條件然後各種跳轉執行。這樣能實現,不過後期的維護可能更加費勁一些,如果使用配置行為樹則相對簡單一些,在修改的時候只需要添加新的分支或者減掉原來的分支就可以了。邏輯也相對更加清晰。
然後簡單說明一下,怎麼在我的這個小玩意裡邊擴展自己的東西。
1、在AITreeNodeType添加一個新的枚舉,主要是用來確定Id用的,注入的時候用的(至於什麼是注入一會再說)
2、然後繼承比較基本的節點,一般情況下繼承最基礎的三個就好,最常用的就是AINodeBase,那我們就那AINodeBase來舉例
3、然後實現virtual bool invoke(int level = 0, bool isLog = false);方法,level代表從根節點開始這是第幾層調用,一般用作Log的時候前邊有幾個空格,isLog代表是否打印Log,你完全可以忽視這兩個參數不管,當然你要實現對應的功能最好遵守這兩個參數的約定,當然不遵守我也沒有意見。
4、在類中添加一個私有的static AINodeRegister<類名> reg;然後在Cpp文件中編寫AINodeRegister<類名> AINodeReleaseSkill::reg(NodeId, NodeName);來實現注入,第一個參數是之前你獲得的Id,第二個參數是對應的節點名,可以不是類名,不過我推薦你還是用類名,只有查找的時候好找
可能放上一段代碼更直觀一些
//回到主人身邊
class AINodeGotoOwnerSide : public AINodeBase
{
private:
static AINodeRegister<AINodeGotoOwnerSide> reg;
public:
virtual bool invoke(int level = 0, bool isLog = false);
};
AINodeRegister<AINodeGotoOwnerSide> AINodeGotoOwnerSide::reg(ANT_GOTO_OWNER_SIDE, "AINodeGotoOwnerSide");
bool AINodeGotoOwnerSide::invoke(int level, bool isLog)
{
return rand() % 100 > 20;
}
說完了累的擴展,應該簡單說一下什麼是注入了,簡單點說,就是我寫了一個公開的幫助函數,用來接受Id跟一個創建節點的函數指針,然後把它們保存在的字典中,你需要調用的時候,我就從字典裡邊找找當初注入的函數指針,然後調用它,給你一個實例。至於為什麼要寫一個靜態的AINodeRegister泛型類,是因為靜態的初始化實在程序啟動的時候會初始化,應用這個特性,我們就可以在初始化的時候把,想要初始化的內容注入到內存中。
其實說到這個地方,主要的邏輯已經基本上說的差不多了。還有一些其他的方面,比如說樹的組裝如何處理,如果是挨個編寫他們之間的引用應該也會很麻煩。並且,使用這種結構處理業務邏輯的時候,業務內容就會分的亂七八糟什麼地方都有,調試也可能會成為問題。
實現Id跟類型之間的關聯之後就可以通過描述類型來創建類了,最後的實現如下
AINodeDescribe des[] = {
AINodeDescribe(1, 0, ANBT_SELECT, "根節點"),
AINodeDescribe(2, 1, ANBT_SEQUENCE, "是否拾取金幣的判定節點"),
AINodeDescribe(5, 2, ANT_RELEASE_SKILL, "附近是否存在金幣"),
AINodeDescribe(6, 2, ANT_PICKING_UP_COINS, "撿取金幣節點"),
AINodeDescribe(3, 1, ANBT_SEQUENCE, "是否回到主人身邊的判定節點"),
AINodeDescribe(7, 3, ANT_RELEASE_SKILL, "是不是很長時間沒有見到金幣了"),
AINodeDescribe(8, 3, ANT_PICKING_UP_COINS, "是不是很長時間沒有回到主人身邊了"),
AINodeDescribe(9, 3, ANT_PICKING_UP_COINS, "回到主人身邊的執行節點"),
AINodeDescribe(4, 1, ANT_PICKING_UP_COINS, "沒事隨便逛逛吧"),
};
int desCount = sizeof(des) / sizeof(AINodeDescribe);
vector<AINodeDescribe> des_vtr;
for (int i = 0; i < desCount; ++i)
{
des_vtr.push_back(des[i]);
}
AINodeBase * rootNode = AINodeHelper::sharedHelper()->CreateNodeTree(des_vtr);
AINodeDescribe初始化的時候接受四個參數:當前Id,父節點Id,當前節點創建的樹節點具體類型,當前節點實例的描述。其中父節點如果是0的時候則會被當做根節點返回,這個一點要有一個哦,不然會直接返回NULL,並且申請的所有節點都會造成內存洩露。
起始這個地方可以吧參數都寫到文件中,然後通過文件來進行初始化,不過,我這個地方只是為了演示用,所以直接寫死也沒有關系,不過你在用的時候,我推薦你寫一個讀取文件配置的方法,效果會更好。(因為你可以吧這段的邏輯整理直接做一個編輯器,讓策劃來進行對應的內容的整理。)
對了,這個地方,你可能是按照自己的想法來描寫的這個文件,但是實際的執行結果可能跟你的想法並不一樣,你可以進行如下處理來進行驗證
cout << "\n狀態結構組織圖 \n" << endl;
AINodeHelper::sharedHelper()->printAITree(rootNode);
cout << "\n狀態結構組織圖 \n" << endl;
輸出的結果呢,就是最上邊那張圖了
剩下的還存在一個問題,那就是調試問題了,我不可能在這麼多內容中下斷點,那跟下毒沒啥區別。所以我們需要有一種方式來打印各個節點的運行結果。這個我的處理如下
for (int i = 0; i < 10; ++i)
{
cout << "調用樹開始" << endl;
rootNode->invoke(0, true);
cout << "調用樹結束" << endl;
}
其中invoke的第一個參數的意思為最基礎的節點的屆位,第二個參數為是否打印Log,如果不想調試的話,兩個參數都不要填就可以。
最後說一下項目的地址
http://git.oschina.net/anxin1225/AiTreeTest
可能這個描述還不是很明確,你可以給我留言,我盡量給你解答