在Unity中為即時戰略遊戲實現戰爭迷霧(下)

遊資網發表於2019-11-12
上一篇文章中,遊戲開發工程師Ariel Coppes分享了《鋼鐵戰隊》中戰爭迷霧效果的實現方法,本文他將介紹新的一種實現方法。

新的戰爭迷霧及視野系統目標是實現下列功能:

  • 能夠隨時渲染每個玩家的戰爭迷霧,用於進行回放和除錯。
  • 能夠結合多個玩家的視野,用於提供友方視野、實現觀眾模式和觀看回放時使用。
  • 使用不同地形高度和其它元素來阻擋視野。
  • 優化開發,使視野在移動裝置上支援同時顯示50多個單位,並在60fps的狀態下執行。
  • 該效果應該類似《星際爭霸2》和《英雄聯盟》等遊戲。


下面是《星際爭霸2》的戰爭迷霧,是我希望實現的效果。

請注意:為了讓本文的內容更簡潔,在提到“單位”時,所指的是角色、建築等影響遊戲內戰爭迷霧的結構。

邏輯

首先,我們有一個概念叫UnitVision,即單位視野,用於表示可揭開迷霧的任何物件。UnitVision在程式碼中是一種資料結構。

  1. struct UnitVision
  2. {
  3.    // 表示視野系統中玩家分組的位掩碼。
  4.    int players;

  5.    // 使用世界座標的視野範圍。
  6.    float range;

  7.    // 使用世界座標的位置。
  8.    vector2 position;

  9.    // 用於阻擋視線。
  10.    short terrainHeight;
  11. }
複製程式碼

通常在遊戲中,每個單位都有一個單位視野,有的單位會有更多視野,例如:大型單位,而有的單位甚至沒有視野。

位掩碼用於指定玩家分組,例如:如果玩家0是0001,而玩家1是0010,那麼0011表示由玩家0和玩家1組成的分組。由於這是一個int型別數值,因此它最多支援sizeof(int)大小的玩家數量。

大多數時候,該分組僅包含一個玩家,但在部分情況下,例如:在通用特效或影片效果中,該分組需要被所有玩家看到,其中一種實現方法是使用帶有多個玩家的unitVision。

欄位terrainHeight儲存當前單位的高度,我們會使用該欄位來檢測單位是否會阻擋視野。如果該單位是地面單位,該值通常是該位置世界地形的高度,但也有一些特別情況。

例如:飛行單位或可改變單位高度的特殊技能,在計算被阻擋的視野時,要考慮到這些因素。遊戲要對該欄位進行相應的更新。

我們還有一個概念名叫VisionGrid,即視野網格,表示所有玩家的視野。下面是VisionGrid的資料結構。

  1. struct VisionGrid
  2. {
  3.     // 下列變數表示網格的寬度和高度,它們需要訪問陣列。
  4.     int width, height;

  5.     // 儲存寬度乘高度得到的大小結果,每個部分都有int型別數值和位碼,表示哪個玩家的視野中有該部分。
  6.     int[] values;

  7.    // 和values陣列類似,但它只儲存玩家是否在某段時間訪問該部分。
  8.     int[] visited;

  9.     void SetVisible(i, j, players) {
  10.         values[i + j * width] |= players;
  11.         visited[i + j * width] |= players;
  12.     }

  13.     void Clear() {
  14.         values.clear(0);
  15.     }

  16.     bool IsVisible(i, j, players) {
  17.         return (values[i + j * width] & players) > 0;
  18.     }

  19.     bool WasVisible(i, j, players) {
  20.         return (visited[i + j * width] & players) > 0;
  21.     }
  22. }
複製程式碼

請注意:陣列的大小為寬度乘以高度的結果。

網格越大,計算視野的速度越慢,但它會有更多資訊用於實現單位的行為或渲染更好的迷霧效果。網格越小,效果則會相反。我們必須在一開始就確定好二者的平衡,由此來構建遊戲。

下面是遊戲世界中網格的示例。

在Unity中為即時戰略遊戲實現戰爭迷霧(下)

在獲得世界位置的網格部分後,該結構會在values陣列儲存int類數值,提供哪個玩家視野內有該位置的資料資訊。例如:如果該部分儲存了0001,那麼它表示只有玩家0能夠看到該部分,如果該部分儲存了0011,那麼玩家0和玩家1都會看到該部分。

該結構也會把玩家之前探索迷霧部分的時間儲存到visited陣列中,主要用於渲染功能,即渲染灰色迷霧,但該資料也可以用到遊戲邏輯中,例如:用於檢查玩家是否瞭解相關資訊。

如果位掩碼中的玩家能夠看到該位置,IsVisible(i, j, players)方法會返回True。

WasVisible(i, j, players)方法有類似的功能,但它會檢查visited陣列。

例如:如果玩家1和玩家2,即位碼中的0010和0100屬於同一陣營,如果玩家2希望知道敵人是否可見,以便展開進攻,則可以使用兩個玩家的位掩碼0110來呼叫isVisible方法。

計算視野

每次更新視野網格時,values陣列都會清空,然後重新計算。

下面是該演算法的虛擬碼。

  1. void CalculateVision()
  2. {
  3.    visionGrid.Clear()

  4.    for each unitVision in world {
  5.       for each gridEntry inside unitVision.range {
  6.          if (not IsBlocked(gridEntry)) {
  7.         // 設為可見時,會更新values陣列和visited陣列。
  8.             grid.SetVisible(gridEntry.i, gridEntry.j,
  9.                             unitVision.players)
  10.          }
  11.       }
  12.    }
  13. }
複製程式碼

為了在範圍內迭代網格部分,該指令碼首先會使用網格座標來計算視野的位置和範圍,相應的變數分別是gridPosition和gridRange,然後指令碼會圍繞gridPosition並以gridRange為半徑來繪製實心圓形。

在Unity中為即時戰略遊戲實現戰爭迷霧(下)


被阻擋的視野

為了檢測被阻擋的視野,我們有相同大小的另一個網格,它帶有地形的高度資訊。

下面是該資訊的資料結構。

  1. struct Terrain {
  2.     // 下列變數表示網格的寬度和高度,它們需要訪問陣列。
  3.     int width, height;

  4.     // 儲存寬度乘高度得到的大小結果,該陣列擁有網格部分的地形層級。
  5.     short[] height;

  6.     int GetHeight(i, j) {
  7.        return height[i + j * width];
  8.     }
複製程式碼

下面是網格在遊戲中的示例效果。

在Unity中為即時戰略遊戲實現戰爭迷霧(下)

在迭代unitVision範圍中視野的網格部分時,為了檢測該部分是否可見,我們的系統會檢查視野中心是否有障礙物。為此,它會從該部分的位置繪製一條直線,連線到中心的位置。

如果線條上的所有網格部分都在相同高度或較低高度,那麼該部分是可見的。下面是相應的示例,藍點表示進行計算的網格部分,白點表示連線中心的線條。

在Unity中為即時戰略遊戲實現戰爭迷霧(下)

如果線條上至少有一個部分的高度較大,那麼視線會被阻擋。

在下面的例子中,藍點表示我們希望瞭解是否可見的部分,白點表示線條上相同高度的部分,紅點表示地形較高的部分。

在Unity中為即時戰略遊戲實現戰爭迷霧(下)

在指令碼檢測到某個部分高於視野後,它不必繼續繪製連線視野中心的線條。下面是相應的虛擬碼。

  1. bool IsBlocked()
  2. {
  3.    for each entry in line to unitVision.position {
  4.       height = terrain.GetHeight(entry.position)
  5.       if (height > unitVision.height) {
  6.          return true;
  7.       }
  8.    }
  9.    return false;
  10. }
複製程式碼

優化

如果某個部分已經在迭代所有單位視野時標記為可見,則不必重新計算該部分。

減小網格的大小。

減少更新迷霧的頻率,我最近在玩《星際爭霸1》時發現,更新迷霧會有約1秒的延遲。

渲染

渲染戰爭迷霧時,我使用和網格相同大小的小型紋理FogTexture,通過使用Texture2D.SetPixels()方法,在該紋理上寫入相同大小的Color陣列。

在每一幀中,我會在每個VisionGrid部分進行迭代,通過使用values陣列和visited陣列,對陣列設定對應的顏色。

下面是這部分的虛擬碼。

  1. void Update()
  2. {
  3.    for i, j in grid {
  4.        colors[i + j * width] = black
  5.        if (visionGrid.IsVisible(i, j, activePlayers))
  6.            colors[pixel] = white
  7.        else if (visionGrid.WasVisible(i, j, activePlayers))
  8.            colors[pixel] = grey // 這裡用於處理之前的視野。
  9.    }
  10.    texture.SetPixels(colors)
  11. }
複製程式碼

欄位activePlayers包含玩家的位掩碼,用於渲染玩家的當前迷霧。通常,它只包含遊戲中主要玩家的迷霧,但是在部分情況下,例如:回放模式中,該欄位可以隨時改變,渲染不同玩家的視野。

如果有兩個玩家處於同一陣營,兩個玩家的位掩碼可用於渲染二者的共享視野。

在Unity中為即時戰略遊戲實現戰爭迷霧(下)

填補FogTexture紋理後,該紋理會使用帶有後期處理濾鏡的攝像機,在RenderTexture渲染紋理中進行渲染,濾鏡會給紋理應用模糊效果,讓迷霧的外觀效果更好。

為了實現更好的結果,在應用後期處理特效時,這裡使用的RenderTexture渲染紋理比FogTexture紋理大四倍。

在Unity中為即時戰略遊戲實現戰爭迷霧(下)

獲得RenderTexture渲染紋理後,我會使用自定義著色器在遊戲中渲染它,該著色器會把影象看作Alpha遮罩,白色表示透明部分,黑色表示不透明部分,由於我在此不需要其它顏色通道,因此使用紅色作為補充,這部分處理類似《鋼鐵戰隊》的相應過程。

畫面效果如下圖所示。

在Unity中為即時戰略遊戲實現戰爭迷霧(下)

下圖是該方法在Unity場景檢視中的效果。

在Unity中為即時戰略遊戲實現戰爭迷霧(下)

渲染流程如下圖所示。

在Unity中為即時戰略遊戲實現戰爭迷霧(下)

平緩過渡

在部分情況下,迷霧紋理會在不同幀數間大幅變化,例如:在新單位出現時,或是某個單位移動到高地時。

對於這類情況,我會給colors陣列新增平緩過渡過程,這樣陣列中每個部分會及時從原有狀態過渡到新狀態,從而最小化變化幅度。

該過程非常簡單,雖然該過程在處理紋理畫素時會增加少量效能開銷,但最終效果比我想象的更好,而且該過程也可以隨時禁用。

最初我不確定是否要直接把畫素寫入紋理,因為我擔心這項操作的速度很慢,但在移動裝置上測試後,我發現速度很快,因此這並不是一個問題。

單位可見性

為了瞭解某個單位是否可見,該系統要檢查包含單位的所有部分,大型單位會佔用多個部分,如果至少有一個部分是可見的,那麼該單位就是可見的。這個檢查能夠幫助我們瞭解某個單位是否可以被攻擊。

下面是相應的虛擬碼。

  1. bool IsVisible(players, unit)
  2. {
  3.   // 這是某個玩家的單位。
  4.   if ((unit.players & players) > 0)
  5.     return true;

  6. // 返回包含該單位的所有部分
  7.   entries = visionGrid.GetEntries(unit.position, unit.size)

  8.   for (entry in entries) {
  9.     if (visionGrid.IsVisible(entry, players))
  10.       return true;
  11.   }

  12.   return false;
  13. }
複製程式碼

哪些單位可見和渲染的迷霧有關,因此我們使用了相同的activePlayers欄位來檢查是否顯示單位。

為了避免渲染單位,我使用了類似《鋼鐵戰隊》的方法,也就是使用了遊戲物件的圖層。

如果單位是可見的,那麼我們會給該物件設定預設圖層,如果單位不可見,那麼我們會給它設定從遊戲攝像機剔除的圖層。

  1. void UpdateVisibles() {
  2.   for (unit in units) {
  3.     unit.gameObject.layer = IsVisible(activePlayers, unit) : default ? hidden;
  4.   }
  5. }
複製程式碼

小結

將整個遊戲世界簡化為網格,並開始對紋理進行思考後,我們可以輕鬆應用不同的影象演算法,例如:繪製填充圓圈或線條,它們在進行優化時非常實用。此外,還有更多影象操作可以用於遊戲邏輯和渲染。

《星際爭霸2》有很多紋理方面的資訊,不只是玩家的視野,還提供了API來訪問紋理,該API也被用到了機器學習的實驗中。我仍在開發更多相關功能,同時還計劃嘗試一些優化功能,例如:C# Job System。

給迷霧紋理使用模糊效果也存在缺點,例如:這會在不合適的時候展示一部分高地。我仍然會研究其它影象效果,尋找合適的方法。

相關閱讀:在Unity中為即時戰略遊戲實現戰爭迷霧(上)


作者:Ariel Coppes  
來源:Unity官方平臺
原地址:https://mp.weixin.qq.com/s/HfalFXTpknC-YYxuKalFwQ

相關文章