二叉樹演算法應用案例
阿新 • • 發佈:2019-02-14
筆者在1月4號將在CSDN學院開設一門公開課《演算法與遊戲實戰》,在這裡先把課程內容透露一部分給讀者。首先講述二叉樹演算法,二叉樹在IT領域應用是非常廣泛的,它不僅在遊戲開發中,在當前比較火的人工智慧上也得到了廣泛的應用。作為使用者,首先要清楚二叉樹的特性:它是n(n≥0)個結點的有限集;它的孩子節點做多是2個;它的遍歷有先序,中序,後序;它的儲存結構分為線性和鏈式儲存等等;還有一種是最優二叉樹也稱為哈夫曼樹,下面開始案例的分享。
在遊戲開發中美術會製作很多圖片,這些圖片一方面是用於UI介面,另一方面是用於模型的材質。大部分網路遊戲使用的圖片數量是非常多的,圖片要展示出來,它首先要載入到記憶體中,記憶體大小是有限制的,它除了載入圖片還需要載入資料或者是模型。當跟隨玩家的攝像機在場景中移動時,場景會根據攝像機的移動一一展現出來,這就需要不斷的把不同的場景加入到記憶體中,這無疑會增加記憶體的吞吐負擔,如果我們把圖片歸類把它們做成一張大的圖片,這樣一旦加入到記憶體中,就不用頻繁的載入了,提高了效率。
現在大家都使用Unity開發或者使用虛幻開發,它自己實現了一個打成圖集的功能,或者使用TexturePack工具也可以將其打包成圖集。雖然我們看不到它們的程式碼實現,但是我們自己可以使用二叉樹將其打包成圖集,給讀者展示利用二叉樹實現的UI打成圖集的效果圖:
下面給讀者展示核心程式碼,首先是建立二叉樹,目的是將圖片插入到二叉樹的結點中,包括判斷二叉樹結點是否為空,程式碼中採用遞迴的方式,程式碼如下所示:
在遊戲開發中美術會製作很多圖片,這些圖片一方面是用於UI介面,另一方面是用於模型的材質。大部分網路遊戲使用的圖片數量是非常多的,圖片要展示出來,它首先要載入到記憶體中,記憶體大小是有限制的,它除了載入圖片還需要載入資料或者是模型。當跟隨玩家的攝像機在場景中移動時,場景會根據攝像機的移動一一展現出來,這就需要不斷的把不同的場景加入到記憶體中,這無疑會增加記憶體的吞吐負擔,如果我們把圖片歸類把它們做成一張大的圖片,這樣一旦加入到記憶體中,就不用頻繁的載入了,提高了效率。
現在大家都使用Unity開發或者使用虛幻開發,它自己實現了一個打成圖集的功能,或者使用TexturePack工具也可以將其打包成圖集。雖然我們看不到它們的程式碼實現,但是我們自己可以使用二叉樹將其打包成圖集,給讀者展示利用二叉樹實現的UI打成圖集的效果圖:
下面給讀者展示核心程式碼,首先是建立二叉樹,目的是將圖片插入到二叉樹的結點中,包括判斷二叉樹結點是否為空,程式碼中採用遞迴的方式,程式碼如下所示:
public AtlasNode Insert(Texture2D image, int index) {
if (image == null) // Obviously an error!
return null;
if (child != null) {// If this node is not a leaf, try inserting into first child.
AtlasNode newNode = child[0].Insert(image, index);
if (newNode != null)
return newNode;
// No more room in first child, insert into second child!
return child[1].Insert(image, index);
}
else {
// If there is already a lightmap in this node, early out
if (hasImage)
return null;
// If this node is too small for the image, return
if (!ImageFits(image, rc))
return null;
// If the image is perfect, accept!
if (PerfectFit(image, rc)) {
hasImage = true;
imageRef = image;
name = imageRef.name;
sortIndex = index;
return this;
}
// If we made it this far, this node must be split.
child = new AtlasNode[2];
child[0] = new AtlasNode();
child[1] = new AtlasNode();
// Decide which way to split image
float deltaW = rc.width - image.width;
float deltaH = rc.height - image.height;
if (deltaW > deltaH) {
child[0].rc = new Rect(rc.xMin, rc.yMin, image.width, rc.height);
child[1].rc = new Rect(rc.xMin + image.width + TEXTURE_PADDING, rc.yMin, rc.width - (image.width + TEXTURE_PADDING), rc.height);
}
else {
child[0].rc = new Rect(rc.xMin, rc.yMin, rc.width, image.height);
child[1].rc = new Rect(rc.xMin, rc.yMin + image.height + TEXTURE_PADDING, rc.width, rc.height - (image.height + TEXTURE_PADDING));
}
// Lets try inserting into first child, eh?
return child[0].Insert(image, index);
}
}
最後一步就是建立圖集了,核心程式碼如下所示:
public static Atlas[] CreateAtlas(string name, Texture2D[] textures, Atlas startWith = null) {
List<Texture2D> toProcess = new List<Texture2D>();
toProcess.AddRange(textures);
int index = toProcess.Count - 1;
toProcess.Reverse(); // Because we index backwards
List<Atlas> result = new List<Atlas>();
int insertIndex = 0;
if (startWith != null) {
insertIndex = startWith.root.sortIndex;
}
while(index >= 0) {
Atlas _atlas = startWith;
if (_atlas == null) {
_atlas = new Atlas();
_atlas.texture = new Texture2D(AtlasSize, AtlasSize, TextureFormat.RGBA32, false);
_atlas.root = new AtlasNode();
_atlas.root.rc = new Rect(0, 0, AtlasSize, AtlasSize);
}
startWith = null;
while (index >= 0 && (_atlas.root.Contains(toProcess[index].name) || _atlas.root.Insert(toProcess[index], insertIndex++) != null)) {
index -= 1;
}
result.Add(_atlas);
_atlas.root.sortIndex = insertIndex;
insertIndex = 0;
_atlas = null;
}
foreach(Atlas atlas in result) {
atlas.root.Build(atlas.texture);
List<AtlasNode> nodes = new List<AtlasNode>();
atlas.root.GetBounds(ref nodes);
nodes.Sort(delegate (AtlasNode x, AtlasNode y) {
if (x.sortIndex == y.sortIndex) return 0;
if (y.sortIndex > x.sortIndex) return -1;
return 1;
});
List<Rect> rects = new List<Rect>();
foreach(AtlasNode node in nodes) {
Rect normalized = new Rect(node.rc.xMin / atlas.root.rc.width, node.rc.yMin / atlas.root.rc.height, node.rc.width / atlas.root.rc.width,
node.rc.height / atlas.root.rc.height);
// bunp everything over by half a pixel to avoid floating errors
normalized.x += 0.5f / atlas.root.rc.width;
normalized.width -= 1.0f / atlas.root.rc.width;
normalized.y += 0.5f / atlas.root.rc.height;
normalized.height -= 1.0f / atlas.root.rc.height;
rects.Add(normalized);
}
atlas.uvRects = new AtlasDescriptor[rects.Count];
for (int i = 0; i < rects.Count; i++) {
atlas.uvRects[i] = new AtlasDescriptor();
atlas.uvRects[i].width = (int)nodes[i].rc.width;
atlas.uvRects[i].height = (int)nodes[i].rc.height;
atlas.uvRects[i].name = nodes[i].name;
atlas.uvRects[i].uvRect = rects[i];
}
atlas.root.Clear();
#if DEBUG_ATLASES
atlas.texture.Apply(false, false);
SaveAtlas(atlas, name);
#else
if (atlas != result[result.Count - 1])
atlas.texture.Apply(false, true);
else
atlas.texture.Apply(false, false);
#endif
}
return result.ToArray();
}
當然這種技術也可以使用3D模型材質的處理,只是在製作的過程中要儲存其圖片的UV值也就是圖片在圖集中的座標,這樣程式在載入時可以“對號入座”,避免模型的材質出現“張冠李戴”。
二叉樹另一種形式是-哈夫曼樹,哈夫曼樹定義:在權為wl,w2,…,wn的n個葉子所構成的所有二叉樹中,帶權路徑長度最小(即代價最小)的二叉樹稱為最優二叉樹或哈夫曼樹。我們利用哈夫曼樹的特性可以幫助我們優化程式程式碼,特別適用於遊戲中怪物面對玩家的AI表現,在網上比較流行的案例,遊戲中也會使用到:設主角的生命值d,在省略其他條件後,有這樣的條件判定:當怪物碰到主角後,怪物的反應遵從下規則:
根據條件,我們可以用如下普通演算法來判定怪物的反應:
if(d<100) state=嘲笑,單挑;
else if(d<200) state=單挑;
else if(d<300) state=嗜血魔法;
else if(d<400) state=呼喚同伴;
else state=逃跑;
上面的演算法適用大多數情況,但其時間效能不高,我們可以通過判定樹來提高其時間效能。首先,分析主角生命值通常的特點,即預測出每種條件佔總條件的百分比,將這些比值作為權值來構造最優二叉樹(哈夫曼樹),作為判定樹來設定演算法。假設這些百分比為:
構造好的哈夫曼樹為:
對應演算法如下:
if(d>=200)&&(d<300) state=嗜血魔法;
else if(d>=300)&&(d<500) state=呼喚同伴;
else if(d>=100)&&(d<200) state=單挑;
else if(d<100) state=嘲笑,單挑;
else state=逃跑;
通過計算,兩種演算法的效率大約是2:3,很明顯,改進的演算法在時間效能上提高不少。這種改進也可以歸結到程式碼重構或者說是優化程式,它雖然沒有使用二叉樹的儲存節點,但是我們可以使用二叉樹的思想解決問題。
在人工智慧中,二叉樹使用也是非常廣泛的,不同的分支指令對應的是不同的動作等等,在遇到AI方面的問題時可以優先考慮二叉樹演算法。
總結
在使用演算法解決問題時,並不是照搬硬套,其思想是最重要的,程式碼只是程式設計工具,語言不是重點,思路才是最重要的,萬變不離其宗。