1. 程式人生 > >二叉樹演算法應用案例

二叉樹演算法應用案例

筆者在1月4號將在CSDN學院開設一門公開課《演算法與遊戲實戰》,在這裡先把課程內容透露一部分給讀者。首先講述二叉樹演算法,二叉樹在IT領域應用是非常廣泛的,它不僅在遊戲開發中,在當前比較火的人工智慧上也得到了廣泛的應用。作為使用者,首先要清楚二叉樹的特性:它是n(n≥0)個結點的有限集;它的孩子節點做多是2個;它的遍歷有先序,中序,後序;它的儲存結構分為線性和鏈式儲存等等;還有一種是最優二叉樹也稱為哈夫曼樹,下面開始案例的分享。
在遊戲開發中美術會製作很多圖片,這些圖片一方面是用於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方面的問題時可以優先考慮二叉樹演算法。

總結

在使用演算法解決問題時,並不是照搬硬套,其思想是最重要的,程式碼只是程式設計工具,語言不是重點,思路才是最重要的,萬變不離其宗。