1. 程式人生 > >遊戲開發中的人工智慧(四):群聚

遊戲開發中的人工智慧(四):群聚

本文內容:群聚方法是 A-life 演算法的例項。 A-life 演算法除了可以做出效果很好的群聚行為外,也是高階群體運動的基礎。

群聚

通常在遊戲中,有些非玩家角色必須群聚移動,而不是個別行動。舉個例子,假設你在寫角色扮演遊戲,在主城鎮外有一片綿羊的草地,如果你的綿羊是一整群的在吃草,而不是毫無目的的在閒逛,看起來會更真實些。

這種群體行為的核心就是基本的群聚演算法,本章要詳談基本群聚演算法,教你如何修改演算法,用來處理諸如避開障礙物之類的情況。本章接下來將以“單位”代指組成群體的個別實體,例如:綿羊、鳥、等等。

基本群聚

基本的群聚演算法來自於Craig Reynolds在1987年發表的論文《Flocks,Herds and Schools:A Distributed Behavioral Model》。在論文中,他提出基本群聚演算法,用以模擬整群的鳥、魚或其他生物。

演算法的三個規則:

  • 凝聚:每個單位都往其鄰近單位的平均位置行動。
  • 對齊:每個單位行動時,都要把自己對齊在其鄰近單位的平均方向上。
  • 分割:每個單位行動時,要避免撞上其鄰近單位。

從這三條語句可以得知,每個單位都必須有比如運用轉向力行進的能力。此外,每個單位都必須得知其區域性的周遭情況,必須知道鄰近單位在哪裡、它們的方向如何以及它們和自身有多接近。

單位視野:

這裡寫圖片描述

圖4-1 是一個單位(圖中用粗線表示的那個)以 r 為半徑畫弧而定出其可見視野的說明。任何其他單位落入這個弧內,都能被這個單位看見。運用群聚規則時,這些可視的單位就會有用,而其他單位都會被忽略。弧由兩個引數定義:弧半徑和角度 θ,這兩個引數會影響最後的群聚行動。

弧半徑:

較大的弧半徑會讓單位看到群體中更多的夥伴,從而產生更強的群體(也更多了)。也就是說,群體沒有分裂成小群體的傾向,因為每個單位都可以看見多數鄰近單位或全部鄰近單位,再據此前進。另一方面,較小的半徑會讓整個群體分裂,形成較小群體的可能性較高。

角度 θ:

角度 θ 量定了每個單位的視野範圍。最寬廣的視野是360度,不過我們一般不這樣做,因為這樣最後得到的群聚行為可能會失真。常用的視野範圍類似於圖4-1 中,每個單位的身後都有一塊看不見的區域。一般而言,視野寬廣的話,如圖4-2 左側所示,視野角度約為270度,會得到組織良好的群體。視野較窄的話,如圖4-2 右側所示,視野角度約為45度,得到的群體像螞蟻那樣沿著單一路徑行進。

這裡寫圖片描述

寬視野和窄視野都有其作用。例如,如果你正在模擬一群噴射戰機,可能會用寬視野,如果模擬一支軍隊鬼鬼祟祟地跟蹤某人時,你也許會用窄視野,使其前後排成一條線。

群聚例項

我們打算模擬大約20個單位,以群聚的方式移動,避開圓形的物體,群聚中的諸多單位和玩家(另一個飛行器)的互動就是去追玩家。

行進模式

這個例項考慮的是以物理機制為基礎的範例,把每個單位視為剛體,通過在每個單位的前端施加轉向力,來保證群聚的行進模式。每條規則都會影響施加的力,最終施加的力和方向是這些規則影響的綜合。另外,需要考慮兩件事:首先,要控制好每條規則貢獻的轉向力;其次,要調整行進模式,以確保每個單位都獲得平衡。

對於避開規則:為了讓單位不會彼此撞上,且單位根據對齊和凝聚規則而靠在一起。當單位彼此間距離夠寬時,避開規則的轉向力貢獻就要小一點;反之,避開規則的轉向力貢獻就要大一些。對於避開用的反向力,一般使用反函式就夠用了,分隔距離越大,得出的避開用轉向力越小;分隔距離越小,得出的避開用轉向力越大。

對於對齊規則:考慮當前單位的當前方向,與其鄰近單位間平均方向間的角度。如果該角度較小,我們只對其方向做小幅度調整,然而,如果角度較大,就需要較大的調整。為了完成這樣的任務,可以把對齊用的轉向力貢獻,設定成和該單位方向及其鄰近單位平均方向間的角度成正比。

鄰近單位

凝聚,對齊,分隔三個規則要起作用的前提是偵測每個當前單位的鄰近單位。鄰近單位就是當前單位視野範圍內的單位,需要從圖4-1所示的視野角度和視野半徑兩方面進行判斷。

由於群體中單位所形成的排列會隨時變動,因此,遊戲迴圈每執行一輪時,每個單位都必須更新其視野。

在示例 AIDemo4-1中,你會發現一個名為 UpdateSimulation( ) 的函式,每次走過遊戲迴圈或模擬運算迴圈時,就會被呼叫。這個函式的責任是更新每個單位的位置並把每個單位畫到畫面顯示緩衝區內。

例4-1 是此例的 UpdateSimulation( ) 函式。

//例4-1:UpdateSimulation()函式

void    UpdateSimulation(void)
{
    double  dt = _TIMESTEP;
    int     i;

    // 初始化後端緩衝區
    if(FrameCounter >= _RENDER_FRAME_COUNT)
    {
        ClearBackBuffer();
        DrawObstacles();
    }

    // 更新玩家控制的單位(Units[0])
    Units[0].SetThrusters(false, false, 1);
    Units[0].SetThrusters(false, false, 1);

    if (IsKeyDown(VK_RIGHT))
        Units[0].SetThrusters(true, false, 0.5);

    if (IsKeyDown(VK_LEFT))
        Units[0].SetThrusters(false, true, 0.5);

    Units[0].UpdateBodyEuler(dt);
    if(FrameCounter >= _RENDER_FRAME_COUNT)
        DrawCraft(Units[0], RGB(0, 255, 0));

    if(Units[0].vPosition.x > _WINWIDTH) Units[0].vPosition.x = 0;
    if(Units[0].vPosition.x < 0) Units[0].vPosition.x = _WINWIDTH;
    if(Units[0].vPosition.y > _WINHEIGHT) Units[0].vPosition.y = 0;
    if(Units[0].vPosition.y < 0) Units[0].vPosition.y = _WINHEIGHT;

    // 更新計算機控制的單位
    for(i=1; i<_MAX_NUM_UNITS; i++)
    {       
        DoUnitAI(i);

        Units[i].UpdateBodyEuler(dt);

        if(FrameCounter >= _RENDER_FRAME_COUNT)
        {
            if(Units[i].Leader)
                DrawCraft(Units[i], RGB(255,0,0));
            else {
                if(Units[i].Interceptor)
                    DrawCraft(Units[i], RGB(255,0,255));        
                else
                    DrawCraft(Units[i], RGB(0,0,255));
            }
        }

        if(Units[i].vPosition.x > _WINWIDTH) Units[i].vPosition.x = 0;
        if(Units[i].vPosition.x < 0) Units[i].vPosition.x = _WINWIDTH;
        if(Units[i].vPosition.y > _WINHEIGHT) Units[i].vPosition.y = 0;
        if(Units[i].vPosition.y < 0) Units[i].vPosition.y = _WINHEIGHT;     
    } 

    //把後端緩衝區複製到螢幕上
    if(FrameCounter >= _RENDER_FRAME_COUNT) {
        CopyBackBufferToWindow();
        FrameCounter = 0;
    }  else
        FrameCounter++;
}

UpdateSimulation( ) 完成的是平常的工作,清除即將繪製圖像的後端緩衝區,處理玩家控制的單位的互動行為,更新計算機控制的單位,把一切都繪製進後端緩衝區,做好之後,再把後端緩衝區複製到螢幕上。UpdateSimulation( ) 會以迴圈走遍計算機控制單位的陣列,對每個單位而言,都會呼叫另一個名為 DoUnitAI( ) 的函式。

DoUnitAI( ) 函式處理一切和計算機控制單位的移動有關的事。所有群聚規則都在此函式內實現。例4-2 是 DoUnitAI( ) 開頭的一小部分。

//例4-2:DoUnitAI() 初始化

void    DoUnitAI(int i)
{

        int     j;
        int     N;     //鄰近單位數量
        Vector  Pave;  //平均位置向量
        Vector  Vave;  //平均速度向量
        Vector  Fs;    //總轉向力
        Vector  Pfs;   //Fs施加的位置
        Vector  d, u, v, w;
        double  m;
        int     Nf;
        bool    InView;
        bool    DoFlock = WideView || LimitedView || NarrowView;
        int     RadiusFactor;

        // 初始化
        Fs.x = Fs.y = Fs.z = 0;
        Pave.x = Pave.y = Pave.z = 0;
        Vave.x = Vave.y = Vave.z = 0;
        N = 0;
        Pfs.x = 0;
        Pfs.y = Units[i].fLength / 2.0f;
        Nf = 0;

        …

引數 i 代表當前正在處理的單位的陣列索引值,我們要收集這個單位所有鄰近單位的資料,然後再實現群聚規則。變數 j 代表 Units 陣列中,其他單位的陣列索引值。這些是 Units[i] 潛在的鄰近單位。

N 代表鄰近單位的數目,這些數目包含在當前正在處理的單位的視野內。Pave 和 Vave 分別存放的是 N 個鄰近單位的平均位置和速度向量。Fs 代表施加到處理中單位的總轉向力。Pfs代表轉向力施加的位置,以固定於個體上的座標表示。

d、u、v 以及 w 用來儲存計算函式時的各種向量值。向量值包含全域性座標系和區域性座標系的相對位置向量和方向向量。m 是乘數變數,不是 +1 就是 -1,用來指出我們所需的轉向力施加點的方向,即目前處理的單位的右側或是左側。

InView 是個標號,指出特定單位是否位於處理中單位的視野內。DoFlock 也是個標號,指出是否使用群聚規則。此例中,你可以開啟或關閉群聚規則,也可以實現三種不同的可見視野模式,以觀察群聚行為。這些可見視野模式叫做 WideView(寬廣視野)、LimitedView(有限視野) 以及 NarrowView(狹窄視野)。最後,RadiusFactor 代表的是圖4-1 中的 r引數(即弧半徑),每種可見視野模式的 r 值都不同,而且視野角度 θ 也不同。

完成初始化後,DoUnitAI( ) 就會進入一個迴圈,收集當前單位周遭的鄰近單位。

例4-3 是 DoUnitAI( ) 中的一端,會檢查所有的鄰近單位並收集資料。到此時,會進入一個迴圈,即 j 迴圈中,在這個迴圈裡面,Units 陣列的每個單位(Units[0] 除外,這是玩家控制的單位(即被追逐的單位),另外,Units[1] 也除外,這是當前單位,現在要找的是該單位的鄰近單位)都會接受測試,以確認該單位是否在當前單位的視野內。如果是,其資料將被收集起來。

//例4-3:檢查鄰近單位並收集資料(DoUnitAI()中的一部分程式碼)

…

    for(j=1; j<_MAX_NUM_UNITS; j++)
        {
            if(i!=j)
            {
                InView = false;
                d = Units[j].vPosition - Units[i].vPosition;
                w = VRotate2D(-Units[i].fOrientation, d);

                if(((w.y > 0) && (fabs(w.x) < fabs(w.y)*_FRONT_VIEW_ANGLE_FACTOR))) 
                    if(d.Magnitude() <= (Units[i].fLength * _NARROWVIEW_RADIUS_FACTOR))
                        Nf++;

                if(WideView)
                {
                    InView = ((w.y > 0) || ((w.y < 0) && (fabs(w.x) > fabs(w.y)*_BACK_VIEW_ANGLE_FACTOR)));
                    RadiusFactor = _WIDEVIEW_RADIUS_FACTOR;
                }

                if(LimitedView)
                {
                    InView = (w.y > 0);
                    RadiusFactor = _LIMITEDVIEW_RADIUS_FACTOR;
                }

                if(NarrowView)
                {
                    InView = (((w.y > 0) && (fabs(w.x) < fabs(w.y)*_FRONT_VIEW_ANGLE_FACTOR)));
                    RadiusFactor = _NARROWVIEW_RADIUS_FACTOR;
                }

                if(InView && (Units[i].Interceptor == Units[j].Interceptor))            
                {
                    if(d.Magnitude() <= (Units[i].fLength * RadiusFactor))
                    {
                        Pave += Units[j].vPosition;
                        Vave += Units[j].vVelocity;
                        N++;
                    }
                }

                …

            }
        }

…

確定 i 不等於 j 之後(即不檢查當前的單位),這個函式會計算當前單位 Units[i] 以及 Units[j] 之間的距離向量,即兩者間位置向量的差值。所得結果會儲存在區域性變數 d 中。接著,d 會從全域性座標轉換成固定於 Units[i] 之上的區域性座標,所得結果會儲存在向量 w 之中。

接著,這個函式會檢查 Units[j] 是否位於 Units[i] 的視野內。這項檢查是依據視野角度 θ 的檢查。(我們後面也會檢查 弧半徑 r,但前提是視野角度 θ 的檢查已經通過)

寬廣視野:

這裡寫圖片描述

例4-4:寬廣視野的檢查(依據視野角度θ)

//例4-4:寬廣視野的檢查(依據視野角度θ)

…

                if(WideView)
                {
                    InView = ((w.y > 0) || ((w.y < 0) && (fabs(w.x) > fabs(w.y)*_BACK_VIEW_ANGLE_FACTOR)));
                    RadiusFactor = _WIDEVIEW_RADIUS_FACTOR;
                }

…

在此程式程式碼內,_BACK_VIEW_ANGLE_FACTOR 就是視野角度係數。如果設為1,則連線視野弧線和 x 軸的夾角就是45度。如果該係數大於1,則這兩條線會靠近 x 軸,相當於不可見區域比較大。相反的,如果該係數小於1,則這兩條線靠近 y 軸,相當於不可見區域比較小。

有限視野

這裡寫圖片描述

在有限視野模式中,可見視野弧線被限制在該單位區域性的+y座標內。也就是說,每個單位都無法看到身後的任何單位。我們只需要確定,從 Units[i] 的區域性座標系來看,Units[j] 的 y 座標是否為正值。

例4-5:有限視野的檢查(依據視野角度θ)

//例4-5:有限視野的檢查(依據視野角度θ)if(LimitedView)
                {
                    InView = (w.y > 0);
                    RadiusFactor = _LIMITEDVIEW_RADIUS_FACTOR;
                }

…

狹窄視野

這裡寫圖片描述

狹窄視野把每個單位可見的範圍限制在正前方。

例4-6:狹窄視野的檢查(依據視野角度θ)

//例4-6:狹窄視野的檢查(依據視野角度θ)if(NarrowView)
                {
                    InView = (((w.y > 0) && (fabs(w.x) < fabs(w.y)*_FRONT_VIEW_ANGLE_FACTOR)));
                    RadiusFactor = _NARROWVIEW_RADIUS_FACTOR;
                }

…

此例中,係數 _FRONT_VIEW_ANGLE_FACTOR控制了該單位前方的視野。如果此係數等於1,則構成視野錐的兩條線和 x 軸的夾角就是 45度。如果係數大於1,折兩條線會靠近 x 軸,也就是說可見區域比較大。如果係數小於1,則這兩條線靠近 y 軸,也就是說可見區域比較小。

如果上述測試都過關了(即依據視野角度 θ 的檢查已通關),那麼接下來則是 依據弧半徑 r 的檢查。

例4-3 的最後一個 if 區塊就是測試此距離。如果向量 d 的數值小於 Units[i] 的長度乘以 RadiusFactor,則表示 Units[j] 和 Units[i] 夠接近。

凝聚

凝聚指的是我們想讓所有單位都待在同一個群體中,我們不要每個單位和其群體分開,各走各的路。

如前所述,為了滿足這項規則,每個單位都應該朝其鄰近單位的平均位置前進。圖4-6 是某單位與其鄰近單位的說明。

這裡寫圖片描述

鄰近位置的平均位置很容易計算。只要找出鄰近單位後,其平均位置就是其各個位置的向量總和再除以總鄰近單位數。

例4-7:鄰近單位位置總和

//例4-7:鄰近單位位置總和

…

                if(InView && (Units[i].Interceptor == Units[j].Interceptor))            
                {
                    if(d.Magnitude() <= (Units[i].fLength * RadiusFactor))
                    {
                        Pave += Units[j].vPosition;
                        Vave += Units[j].vVelocity;
                        N++;
                    }
                }

…

Pave += Units[j].vPosition;這一行將所有鄰近單位的位置向量相加。Pave 和 vPosition 是 Vector 類的變數,過載運算子會替我們做向量加法。

DoUnitAI( ) 找出鄰近單位並收集資訊後,就能使用群聚規則了。第一個處理的就是凝聚規則,程式程式碼如例4-8所示。

//4-8:凝聚規則if(DoFlock && (N>0)) 
{ 
   // DoFlock=true:啟用凝聚規則,N>0:鄰近單位數量大於零  
   Pave = Pave / N;        // 鄰近單位的平均位置向量  
   v = Units[i].vVelocity; // 當前單位的速度向量  
   v.Normalize();   
   u = Pave-Units[i].vPosition; // 鄰近單位平均位置向量與當前單位向量的差值,相對位置向量  
   u.Normalize();  
   w.VRotate2D(-Units[i].fOrientation, u);  
   if(w.x < 0) m = -1; // 相對位置向量(即u)在當前單位的右邊,需要右轉當前單位  
   if(w.x > 0) m = 1;  // 相對位置向量(即u)在當前單位的左邊,需要左轉當前單位  
   if(fabs(v*u) < 1)   // 確保反餘弦函式可以正常執行  
     Fs.x += m * _STEERINGFORCE * acos(v * u) / pi;   
   // Fs.x使用的座標系是應該也是前面剛剛轉化過的座標系  
   // acos(v*u):計算相對位置向量與當前單位的速度向量之間的夾角,除以pi是為了把弧度數值轉化為標量  
}  

…

對齊

對齊的意思是指,我們想讓群聚中的所有單位都大致朝相同的方向前進。為了滿足這條規則,每個單位都應該在行進時,試著以等同於其鄰近單位平均方向的方向來前進。

這裡寫圖片描述

參見圖4-6,中間以粗線表示的單位是沿著和其相連的粗箭頭方向行進的。另外和其相連的虛箭頭則代表其鄰近單位的平均方向。因此,就此例而言,以粗線表示的單位必須朝右側行進。

我們可以利用每個單位的速度向量求出其方向。把每個單位的速度向量換算成單位向量,就可以得出其方位向量。例4-7 顯示出收集某單位鄰近單位方向資料的過程。那一行 Vave += Units[j].vVelocity;會把每個鄰近單位的速度向量累加到 Vave 中。

例4-9說明了如何計算每個單位的對齊轉向力。

//例4-9:對齊規則

…

if(DoFlock && (N>0)) 
{  
    Vave = Vave / N;  
    u = Vave; // 鄰近單位的平均速度向量  
    u.Normalize();  
    v = Units[i].vVelocity;  
    v.Normalize();  
    w.VRotate2D(-Units[i].fOrientation, u);  
    if(w.x < 0) m = -1;  
    if(w.x > 0) m = 1;  
    if(fabs(v*u) < 1)  
      Fs.x += m * _STEERINGFORCE * acos(v * u) / pi;  
}  

…

接著,當前單位 Units[i] 的方向,可以將其速度向量換算成單位向量而求出。所得結果儲存在 v 中。

分隔

分隔是指我們想讓每個單位彼此間保持著最小距離,即使根據凝聚和對齊規則,它們會試著靠近一點。

因此,我們要採用分隔手段,讓每個單位和其視野內的鄰近單位保持某一預定的最小分隔距離。

這裡寫圖片描述

圖4-7 有個單位和那個粗線表示的單位靠的太近了。以粗線表示的單位為中央的外層弧線是可見視野弧線。內層弧線代表的就是最小分隔距離。任何單位只要移動進最小分隔弧線內,則粗線表示的單位就會離它遠一點。

處理分隔和處理凝聚與對齊只有一點不同,因為就分隔而言,求適當的轉向力校正值時,我們必須逐一檢視每個鄰近單位,而不是使用所有鄰近單位的某個平均值。

把分隔程式程式碼放在例4-3 的那個 j 迴圈裡很方便,因為鄰近單位就是在那裡找出來的。需要在新的 j 迴圈加上分隔規則的操作程式程式碼,如例4-10所示。

//例4-10:鄰近單位分隔if(InView) 
    { 
    // 如果在視野內  
        if(d.Magnitude() <= Units[i].fLength * _SEPARATION_FACTOR) 
        {  
            if(w.x < 0) m = 1;    // 這裡是分隔,方向與凝聚和對齊規則正好相反  
            if(w.x > 0) m = -1;  
            Fs.x += m * _STEERINGFOCE * (Units[i].fLength*_SEPARATION_FACTOR) / d.Magnitude(); // 分隔越小,力越大    
        }  
    }  

…

避開障礙物

加入避開障礙物的行為很簡單,我們要做的就是提供某種機制給那些單位使用,讓他們能看到前方的障礙物,再施加適當的轉向力,使其能避開路徑中的障礙物。

為了檢測障礙物是否在某單位的路徑內,我們要藉助機器人學,替我們的單位安裝虛擬觸角(feeler)。基本上,這些觸角會處在單位的前方,如果觸角碰到某種東西,就是那些單位要轉向的時候了。模型的形式很多,比如可以裝上三個觸角,分別位於三個不同方向,不但能檢測出是否有障礙物,而且檢測該障礙物位於單位的那一側。寬廣的單位需要一個以上的觸角,才能確保單位不會和障礙物碰撞。在3D遊戲中,可以使用虛擬體積,以測定是否即將和某障礙物碰撞。

觀察圖4-8,可以瞭解我們的虛擬觸角如何在幾何條件下進行操作。向量 v 代表的就是觸角。這個觸角有某個預定的固定長度和該單位的方向在同一直線上。那個又大又暗的圓圈代表障礙物。

這裡寫圖片描述

為了求出觸角是否和障礙物在某點相交,我們得用上向量數學知識。

首先,我們計算向量 a,即該單位和障礙物位置間的差值。接著,我們取 a 和 v 的內積,將 a 投射到 v 上,可得向量 p。把向量 p 減去 向量 a,可以得到向量 b。

現在,要測試 v 是否和圓的某處相交,包含兩種情況。首先,P 的數值必須小於 v 的數值。其次,b 的數值必須小於該障礙物的半徑 r。如果兩者都滿足,則需要校正轉向力,否則,該單位可以繼續沿著當前方向前進。

例4-12 避開障礙物的程式碼 必須加進 DoUnitAI( )中,以執行避開障礙物。注意,校正的轉向力也會累加在 Fs.x 成員變數中,和其他群聚規則的轉向力加在一起。

4-12:避開障礙物

…

        // 避開障礙物
        Vector  a, p, b;        

        for(j=0; j<_NUM_OBSTACLES; j++)
        {
            u = Units[i].vVelocity;
            u.Normalize();
            v = u * _COLLISION_VISIBILITY_FACTOR * Units[i].fLength;

            a = Obstacles[j] - Units[i].vPosition;
            p = (a * u) * u;
            b = p - a;

            if((b.Magnitude() < _OBSTACLE_RADIUS) && (p.Magnitude() < v.Magnitude()))
            {
                // 即將碰撞,要避開
                w = VRotate2D(-Units[i].fOrientation, a);
                w.Normalize();
                if(w.x < 0) m = 1;
                if(w.x > 0) m = -1;
                Fs.x += m * _STEERINGFORCE * (_COLLISION_VISIBILITY_FACTOR * Units[i].fLength)/a.Magnitude();
            }
        }

…

跟隨領頭者

基本群聚演算法的三條規則,似乎讓群體在遊戲世界中隨處閒逛,如果在其中加入領頭者,就能讓群體的移動更有目的性,或者看起來比較有智慧。

比如:戰爭模擬遊戲中,計算機控制一群飛機追擊玩家。可以讓其中的一架作為領頭者,其他飛機採用基本群聚規則跟著領頭者跑。在和玩家發生混戰時,可以適時關閉群聚規則,讓飛機分散進攻。另一個例項,模擬一支軍隊,指定其中某個單位為領頭者,可以讓他們成橫隊或者縱隊,採用寬廣視野或有限視野模式,使其他單位採取群聚行為。

我們不會指定任何一個單位作為領頭者,而是使用一些簡單的規則,找出誰應該或足以擔任領頭者。這樣一來,任何單位在任何時刻都有可能成為領頭者。這種做法的好處是,當領頭者被除掉,或者因為某種原因而脫離其群體時。整個群體不會因此而失去領導。

例4-13 是幾行必須加進例4-3 的語句,例4-13 是求出給定單位所有鄰近單位資料的程式區塊。

4-13:檢查誰當領頭者

…

    if(((w.y > 0) && (fabs(w.x) < fabs(w.y)*_FRONT_VIEW_ANGLE_FACTOR))) 
                    if(d.Magnitude() <= (Units[i].fLength * _NARROWVIEW_RADIUS_FACTOR))
                        Nf++;

…

if(InView && (Units[i].Interceptor == Units[j].Interceptor))            
                {

…

第一個 if 區塊是做檢查,使用我們之前談過的狹窄視野模式,求出當前處理單位前方視野內的單位數量。接著,這些資訊將被用來確認當前單位是不是領頭者。如果給定單位的前方沒有其他單位,那麼這個單位就是領頭者,其他單位就得跟著他以群聚行為行動。如果該單位前方至少有一個單位位於其視野內,則當前單位就不是領頭者,而只能遵循群聚規則行動。

第二個 if 區塊是對 InView 測試做簡單的修改。外加的程式程式碼所作的檢查,是確保當前單位和 Units[j] 的型別相同,使得屬於攔截者的單位和其他屬於攔截者的單位一起群聚行動。而普通單位則和其他普通單位一起群聚行動。這樣一來,這兩種型別的單位就不會混在同一個群體中了。

因此,如果你下載了範例程式,把其中一種群聚模式開啟,至少會看見兩組群體:一個是普通單位的群體,另一個是攔截者(追逐著)單位構成的群體。玩家控制的單位會以綠色顯示,可以用鍵盤方向鍵進行控制。

例4-14 是這兩類計算機控制的單位領頭者規則的操作內容

4-14:領頭者、追逐和攔截

…

        // 如果該單位是領頭者,就去追逐目標。Nf是當前單位前方的單位數目
        if(Chase)
        {
            if(Nf == 0) 
                Units[i].Leader = true;
            else
                Units[i].Leader = false;

            if((Units[i].Leader || !DoFlock))
            {               
                if(!Units[i].Interceptor)
                {
                    // 追逐                       
                    u = Units[0].vPosition;
                    d = u - Units[i].vPosition;
                    w = VRotate2D(-Units[i].fOrientation, d);
                    if(w.x < 0) m = -1;
                    if(w.x > 0) m = 1;
                    Fs.x += m*_STEERINGFORCE;
                } else
                 {
                    // 攔截       
                    Vector  s1, s2, s12;
                    double  tClose; 
                    Vector  Vr12;

                    Vr12 = Units[0].vVelocity-Units[i].vVelocity; // closing velocity
                    s12 = Units[0].vPosition - Units[i].vPosition; // range to close
                    tClose = s12.Magnitude() / Vr12.Magnitude(); // time to close

                    s1 = Units[0].vPosition + (Units[0].vVelocity * tClose);
                    Target = s1;
                    s2 = s1 - Units[i].vPosition;   
                    w = VRotate2D(-Units[i].fOrientation, s2);  
                    if(w.x < 0) m = -1;
                    if(w.x > 0) m = 1;
                    Fs.x += m*_STEERINGFORCE;   
                }
            }
        }

…

如果你開啟範例程式的追逐選項,則 Chase 變數會賦值為 true,而這裡列出的程式區塊就會被執行。在此區塊內,會檢查當前單位前方視野內的單位數目 Nf,以確定當前單位是否可以成為領頭者。如果 Nf 為 0,則表示當前單位前方無其他單位,因此可以成為領頭者。

領頭者在示例中是紅色顯示的。

示例原始碼 下載

在VC 6++ 環境下可執行。

程式碼:AIDemo4-1