最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會(huì)員登陸 & 注冊(cè)

給貓看的游戲AI實(shí)戰(zhàn)(四)眼見為實(shí)——讓AI的思考過程可視化

2017-09-14 19:32 作者:皮皮關(guān)做游戲  | 我要投稿

上一節(jié)我們學(xué)習(xí)了AI狀態(tài)機(jī)的基本使用方法,讓AI具有了進(jìn)攻、追擊、回退等基本功能。本來今天是想繼續(xù)深入狀態(tài)機(jī)的,但是其實(shí)未來狀態(tài)機(jī)AI相對(duì)來說不再是技術(shù)發(fā)展的主流,在復(fù)雜AI的項(xiàng)目中,它的地位被行為樹取代了許多。

不久之后我們會(huì)用行為樹的方法再看看復(fù)雜AI行為的設(shè)計(jì),由于行為樹的設(shè)計(jì)比較復(fù)雜,對(duì)初學(xué)者很不友好 ╮(╯▽╰)╭。所以今天我們先換一個(gè)方向突破——AI尋路,但是本文的重點(diǎn)并不在尋路算法本身。先展示一下今天要做的最終效果:


1、算法可視化

圖像可以幫助理解抽象的概念,這是顯然的。但是如果只把圖像理解為幫助理解的工具,就太膚淺了。舉個(gè)例子:

一個(gè)物體的速度從10開始,一直勻速降低。先降低到0,繼續(xù)降低到-10為止。這個(gè)圖像表示了一個(gè)怎樣的情景呢?放一個(gè)動(dòng)圖:

如圖,就是簡(jiǎn)單的拋一個(gè)硬幣而已,只要定義速度的方向向上為正,向下為負(fù),那么硬幣的速度-時(shí)間曲線,就是上面的直線了。想一想,拋個(gè)硬幣試試,是不是很反直覺呢?( 不要問我和炮姐有什么關(guān)系,只是扔個(gè)硬幣而已  ( ̄y▽ ̄)~* 。)

所以說,圖像并不僅僅是一種輔助工具,它可以幫助我們換一個(gè)角度理解世界。很多時(shí)候,我們實(shí)現(xiàn)了一個(gè)很牛的算法,A*尋路、碰撞檢測(cè)、三角剖分等等,但是也僅僅是按照前人的方法實(shí)現(xiàn)了而已,而對(duì)算法的理解,還停留在很低的層面上。如果這時(shí)候從不同的角度讓算法直觀地表現(xiàn)出來,那么算法存在的缺陷、優(yōu)化的方法、適用的范圍等等一系列深度問題,都會(huì)顯而易見了。


如果你還是不知道這個(gè)方法好玩在哪,建議翻到本文最后第5段,里面有很多好玩的例子。本文最后面有工程下載地址,也可以先運(yùn)行工程試試。

那么我們先來畫個(gè)迷宮。


2、圖形化顯示數(shù)組內(nèi)容

1、定義地圖大小,并創(chuàng)建一個(gè)二維數(shù)組map代表地圖里的元素(空地、墻、起點(diǎn)、終點(diǎn)等等)


const int Height = 20;
const int Width = 30;
int[,] map = new int[Height, Width];

// 在map里填寫以下數(shù)字,代表開始點(diǎn)、結(jié)束點(diǎn)、墻
const int START = 8;
const int END = 9;
const int WALL = 1;


2、在Unity中放置一個(gè)平面,代表地面,大小和位置可以在我們畫出地圖后再調(diào)節(jié)也可以。

3、以下代碼可以將數(shù)組里的墻顯示出來,map的前一個(gè)元素是Y坐標(biāo),第二個(gè)元素是X坐標(biāo):


void InitMap0()
   {        // 新建一個(gè)GameObject walls作為所有墻的父節(jié)點(diǎn)
        var walls = new GameObject();
        walls.name = "walls";
        for (int i = 0; i < H; i++)
        {
            for (int j = 0; j < W; j++)
            {
                if (map[i, j] == WALL)
                {
                    var go = Instantiate(prefab_wall, new Vector3(j * 1, 0.5f, i * 1), Quaternion.identity, walls.transform);
                }
            }
        }
    }


4、現(xiàn)在我們的數(shù)組內(nèi)容都是默認(rèn)的0,我們可以用很多方法初始化數(shù)組的值,這里提供一個(gè)直接編寫文本文件來定義地圖的方法:


public void ReadMapFile()
    {
        string path = Application.dataPath + "//" + "map.txt";
        if (!File.Exists(path))
        {
            return;
        }
        FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read);
        StreamReader read = new StreamReader(fs, Encoding.Default);

        string strReadline = "";
        int y = 0;

        // 跳過第一行
        read.ReadLine();
        strReadline = read.ReadLine();

        while (strReadline!=null && y<H)
        {
            for (int x=0; x<W && x<strReadline.Length; ++x)
            {
                int t;
                switch(strReadline[x])
                {
                    case '1':
                        t = 1;
                        break;
                    case '8':
                        t = 8;
                        break;
                    case '9':
                        t = 9;
                        break;
                    default:
                        t = 0;
                        break;
                }
                // Debug.Log("x, y"+ x +" " + y);
                map[y, x] = t;
            }
            y += 1;
            strReadline = read.ReadLine();
        }
        read.Dispose();//文件流釋放  
        fs.Close();
    }


在Unity的Assets目錄中新建一個(gè)map.txt文件,可以用記事本直接畫地圖,空格代表空白,1代表墻,第一行不包含在內(nèi)。注意行數(shù)、列數(shù)和代碼中的定義一致。類似下面這樣:


----+----+----+----+----+----+
          1      11111111     $
          1                   $
          1                   $
          1                   $
                              $
1111111111111 11111111        $
              1               $
              1               $
              1               $
              1               $
              1               $
              1               $
              1               $
              1               $
        11111111111111        $
                              $
                              $
                              $
                              $
                              $


$用來標(biāo)記每行末尾,沒有特別的意思。這樣初始化地圖就變得很簡(jiǎn)單了。注意編輯器一定要用記事本之類的等寬字體編輯哦,否則字符寬度不同就麻煩了 ( ̄工 ̄lll)。

5、測(cè)試看看。如果地面對(duì)的不齊,只要調(diào)整地面位置就可以了。


3、廣度優(yōu)先搜索(BFS)

廣度優(yōu)先搜索是尋路算法的基礎(chǔ)。所謂廣度優(yōu)先,就是在搜索時(shí),先盡可能把一定距離內(nèi)的點(diǎn)全部探索完畢,然后再探索稍微遠(yuǎn)一點(diǎn)的所有點(diǎn),以此類推,直到達(dá)到足夠遠(yuǎn)的地方發(fā)現(xiàn)終點(diǎn)。

如圖,數(shù)字代表探索的順序。探索是從入口開始,先探索所有附近1格的節(jié)點(diǎn),然后是2格、3格,依次類推,和本文開頭的動(dòng)圖是一致的。

1、本文不再一步一步講解BFS細(xì)節(jié)的實(shí)現(xiàn),只是提示一些細(xì)節(jié),你就可以實(shí)現(xiàn)自己的BFS方法。先說數(shù)據(jù)結(jié)構(gòu):

  1. 關(guān)鍵的數(shù)據(jù)結(jié)構(gòu)共有三個(gè)。

  2. 首先是地圖map二維數(shù)組本身,這個(gè)畫地圖時(shí)已經(jīng)用到了。

  3. 其次是保存每格里的步數(shù)的二維數(shù)組 int bfs[Height, Width],前面的標(biāo)記了數(shù)字的圖就是。

  4. 最后是關(guān)鍵性的,一個(gè)任務(wù)隊(duì)列,用List表示即可。List<Pos> queue = new List<Pos>();

  5. Pos代表每一個(gè)格子,由于格子坐標(biāo)是整數(shù)(整數(shù)可以直接做數(shù)組索引),不能用Vector2,定義如下:


// Pos代替Vector2,代表位置。整數(shù)
public class Pos
{
    public int x = 0;
    public int y = 0;
// 多個(gè)初始化函數(shù)是為了方便使用
    public Pos()
    {
    }

    public Pos(Pos p)
    {
        x = p.x;
        y = p.y;
    }

    public Pos(int x, int y)
    {
        this.x = x;
        this.y = y;
    }
// 定義了Equals函數(shù),判斷相等時(shí)比較方便。p.Equals(q),就可以判斷p和q是否相等了。
    public bool Equals(Pos p)
    {
        return x == p.x && y == p.y;
    }
}


2、數(shù)據(jù)結(jié)構(gòu)之后,描述一下算法:

  1. 初始化bfs數(shù)組全部為short.MaxValue,初始值是0會(huì)帶來很多麻煩。

  2. 初始點(diǎn)步數(shù)為0,設(shè)置bfs[start.y, start.x] = 0,然后將start這個(gè)點(diǎn)加入到queue最后面去。queue.Add(start);

  3. 下面開始循環(huán)。從queue中取出第一個(gè)元素p,并從queue中刪除它。

  4. 如果p就是終點(diǎn),那么我們的任務(wù)就完成了。設(shè)置終點(diǎn)的步數(shù)后退出循環(huán)。

  5. 依次判斷p的上、下、左、右四個(gè)相鄰元素。相鄰的元素q不能超出地圖范圍,不能是墻;如果q已經(jīng)被探索過了,那么也不再考慮它(這是BFS特有的處理方式)。

  6. 對(duì)上一步發(fā)現(xiàn)的新的合理的格子q,設(shè)置bfs[q.y, q.x] = bfs[p.y, p.x] + 1; 即步數(shù)+1。并將q插入隊(duì)列queue。

  7. 回到第3步,如果隊(duì)列為空就代表探索完畢,沒有發(fā)現(xiàn)終點(diǎn)(比如終點(diǎn)被擋死了)。

重點(diǎn)是第5步中,如果某個(gè)點(diǎn)已經(jīng)被探索過、標(biāo)記過步數(shù),那么后來再探索到它的時(shí)候,步數(shù)一定大于等于原來的值,也就不需要再考慮它了,只有BFS才有這個(gè)性質(zhì)。未來做其他尋路算法時(shí),要注意如果新的步數(shù)小于原來標(biāo)記的步數(shù),就要執(zhí)行第6步(設(shè)置新的步數(shù)并把該點(diǎn)加入queue)。

3、最后描述一下得到了bfs表之后,怎樣獲得最短路徑。

  1. 初始化一個(gè)path列表代表路徑,List<Pos> path = new List<Pos>();

  2. 從終點(diǎn)開始,設(shè)置p為終點(diǎn),步數(shù)為n。

  3. 從p的上下左右四個(gè)點(diǎn)中找到任意一個(gè)步數(shù)為n-1的點(diǎn),加入到path中,將p賦值為這個(gè)點(diǎn)。重復(fù)本步驟即可,直至p是起點(diǎn)。

  4. 完畢,path即代表最短路徑。

4、探索過程的可視化

一般來說,函數(shù)的執(zhí)行要么是一次執(zhí)行完畢,要么是每幀執(zhí)行一次。在算法做可視化的時(shí)候,這是個(gè)很頭疼的問題。

1、Unity提供的協(xié)程機(jī)制可以很方便地讓函數(shù)變?yōu)槊繋瑘?zhí)行1到n步,可以隨意控制。為了說明方便,這里寫一個(gè)BFS的偽代碼:


void BFS()
{    bfs = new int[map.GetLength(0),map.GetLength(1)];
    bfs每個(gè)元素賦值為int.MaxValue;  bfs_search[i, j] = short.MaxValue;
    List<Pos> queue = new List<Pos>();
    bfs[startPos.y, startPos.x] = 0;
    queue.Add(startPos);

    while (queue.Count > 0)
    {
        Pos cur = queue[0];    // cur是當(dāng)前方格
        queue.RemoveAt(0);
        處理上方方格;
        處理下方方格;
        處理左邊方格;
        處理右邊方格;
    }
}


如果要每探索出一個(gè)方格,都停頓1幀或者零點(diǎn)幾秒,可以改成如下形式:


IEnumerator BFS()
{
    bfs = new int[map.GetLength(0),map.GetLength(1)];
    bfs每個(gè)元素賦值為int.MaxValue;  bfs_search[i, j] = short.MaxValue;
    List<Pos> queue = new List<Pos>();
    bfs[startPos.y, startPos.x] = 0;
    queue.Add(startPos);

    while (queue.Count > 0)
    {
        Pos cur = queue[0];    // cur是當(dāng)前方格
        queue.RemoveAt(0);
        處理上方方格;
        處理下方方格;
        處理左邊方格;
        處理右邊方格;
        RefreshPath(bfs);      // 刷新場(chǎng)景
        yield return new WaitForSeconds(0.05f);  // 暫停0.05
    }
    yield return null;
}


只加了三句話,一個(gè)RefreshPath調(diào)用,兩個(gè)yield分別代表暫停和結(jié)束;還修改了函數(shù)返回值為IEnumerator。這個(gè)被改寫過的函數(shù)調(diào)用時(shí)要這么寫:

StartCoroutine(BFS());

關(guān)于協(xié)程就這樣簡(jiǎn)單講解一下。協(xié)程在Unity中非常有用,可以把順序邏輯改為分時(shí)間執(zhí)行。編寫時(shí)參考示例工程,或者看一下更詳細(xì)的Unity入門文章吧  ̄? ̄。

2、顯示搜索過的路線。


void Refresh()
    {
        // 刪除所有格子
        GameObject[] all_go = GameObject.FindGameObjectsWithTag("Path");
        foreach (var go in all_go)
        {
            Destroy(go);
        }

        // 創(chuàng)建起點(diǎn)和終點(diǎn)
        for (int i = 0; i < H; i++)
        {
            for (int j = 0; j < W; j++)
            {
                if (map[i, j] == START)
                {
                    Debug.Log("START "+ prefab_start);
                    var go = Instantiate(prefab_start, new Vector3(j * 1, 0.5f, i * 1), Quaternion.identity, pathParent.transform);
                    go.tag = "Path";
                }
                if (map[i, j] == END)
                {
                    var go = Instantiate(prefab_end, new Vector3(j * 1, 0.5f, i * 1), Quaternion.identity, pathParent.transform);
                    go.tag = "Path";
                }
            }
        }
    }

    void RefreshPath(short[,] temp_map)
    {
        Refresh();
        // 顯示探索過的部分
        for (int i = 0; i < H; i++)
        {
            string line = "";
            for (int j = 0; j < W; j++)
            {
                line += temp_map[i, j] + " ";
                if (map[i,j]==0 && temp_map[i, j] != short.MaxValue)
                {
                    var go = Instantiate(prefab_path, new Vector3(j * 1, 0.1f, i * 1), Quaternion.identity, pathParent.transform);
                    go.tag = "Path";
                }
            }
        }
    }


我的這個(gè)做法效率很低,先刪除所有格子,然后把數(shù)組里的起點(diǎn)、終點(diǎn)、探索過的部分都重新生成Cube顯示出來。但是對(duì)“算法可視化”的目標(biāo)來說,這種方法極其方便,通用性很強(qiáng),可以讓畫圖的過程和算法無關(guān)。學(xué)習(xí)過程中不要考慮效率,記住我們的目標(biāo)是為了更好的理解算法。



5、修改并理解更多算法

寫了這么多功能,只用來學(xué)習(xí)BFS不覺得很虧嗎?咱們?cè)偌狱c(diǎn)料。

1、在BFS中,只要改變從queue中取元素的順序,就可以實(shí)現(xiàn)各種666的算法:

  1. 如果從queue的后面開始取,就是類似深度優(yōu)先搜索的算法(另類的DFS)。

  2. 如果從queue中選擇離終點(diǎn)最近的元素,就實(shí)現(xiàn)了有指導(dǎo)的DFS,這種算法在某些特定地圖上比AStar還快。

口說無憑,看動(dòng)圖:

1、輕微修改BFS做成的DFS,實(shí)際效果和DFS完全一致。


2、有距離指導(dǎo)的DFS,在路線直接的情況下非??欤?/p>


3、AStar,可以看到AStar并不能說絕對(duì)的“快”,但是兼顧了廣度和深度,絕大多數(shù)情況下表現(xiàn)更好:


4、福利。連連看算法,起點(diǎn)和終點(diǎn)之間最多只能有兩次折線。這個(gè)算法和尋路完全不一樣,是用十字射線相交的方法實(shí)現(xiàn)的:

以上算法在示例工程中都有。


6、總結(jié)

本章主要就是演示算法可視化,讓大家體會(huì)這種方法的樂趣。算法實(shí)現(xiàn)的過程雖然辛苦,但是滿足感也是巨大的。

尋路算法做出來之后,怎樣將它融合到AI的移動(dòng)過程中,又是一個(gè)新的問題了,好在這個(gè)問題并不難,會(huì)在未來的文章中一筆帶過。游戲開發(fā)中,很少有一招鮮、吃遍天的情況,任何算法都有它的適應(yīng)范圍,為了讓游戲有更好的體驗(yàn),AI算法也必須要相應(yīng)修正。

現(xiàn)在流行的即時(shí)戰(zhàn)略游戲比如《星際爭(zhēng)霸2》中,AI已經(jīng)有了長(zhǎng)足的進(jìn)步,不僅能在進(jìn)攻時(shí)自動(dòng)排成隊(duì)形,盡可能讓所有人都能輸出傷害,還能在多個(gè)友軍部隊(duì)向不同方向前進(jìn)時(shí),自動(dòng)錯(cuò)開位置躲避對(duì)方。這都是多種算法的綜合應(yīng)用,不用覺得這些方法有多么復(fù)雜,關(guān)鍵是深入理解基本算法,在實(shí)際項(xiàng)目中做出關(guān)鍵性的調(diào)整,才是AI學(xué)習(xí)的重點(diǎn)。

就啰嗦這么多吧,下次咱們?cè)傺芯啃碌膯栴},再見 (′?ω?`) 。


GameMap工程地址:

https://github.com/mayao11/PracticalGameAI/tree/master/GameMap



————————————————————————————————————

對(duì)游戲開發(fā)感興趣的同學(xué),歡迎圍觀我們:【皮皮關(guān)游戲開發(fā)教育】 ,會(huì)定期更新各種教程干貨,更有別具一格的線下小班教育~

我們的官網(wǎng)地址:levelpp.com/

我們的游戲開發(fā)技術(shù)交流群:610475807

我們的微信公眾號(hào):皮皮關(guān)


給貓看的游戲AI實(shí)戰(zhàn)(四)眼見為實(shí)——讓AI的思考過程可視化的評(píng)論 (共 條)

分享到微博請(qǐng)遵守國(guó)家法律
大宁县| 成安县| 图片| 且末县| 出国| 邵阳市| 蕉岭县| 庄浪县| 沾化县| 白河县| 图们市| 南澳县| 湾仔区| 临漳县| 无极县| 石狮市| 内丘县| 临夏县| 青河县| 琼中| 甘肃省| 高碑店市| 鄂托克前旗| 满城县| 谢通门县| 玛纳斯县| 安西县| 县级市| 乐昌市| 保靖县| 封丘县| 廊坊市| 泾川县| 藁城市| 衡南县| 安岳县| 肥西县| 三门县| 白城市| 上思县| 任丘市|