最近一階段重溫了一些關於遊戲人工智能方面的書籍。 增強了對influence map的認知。想要親自動手實現一下。node
正如文章標題所示,這篇文章講的是:如何將influence map的機制融入到當前較火的unity尋路插件A* pathfinding project裏。算法
influence map中文名:勢力圖或影響圖。如下稱勢力圖。 勢力圖是基於空間的,某些空間歸屬A,另一些空間歸屬B,等等。安全
把問題規模縮小到一場遊戲戰役,每一個兵種單位都佔據並影響着必定的空間,且相同勢力的單位對同一空間的影響能夠疊加,影響值隨傳播距離遞減。多線程
1,進攻方,能夠根據勢力圖選擇率先攻擊敵人薄弱的地方.防護方,能夠根據勢力圖選擇一個較爲安全的撤退地點。編輯器
2,進一步,統計分析,好比採起某種戰略以後,觀察勢力圖變化,能夠分析以前戰略效果。
ide
3,更進一步,經過對一段時間的勢力圖進行對比,能夠大體預測敵軍的部署動向。函數
1,定義各單位的勢力值傳播範圍,形狀,特性(這是Gameplay)因爲每一個兵種的特性和能力值不一樣,故每一個兵種單位的影響半徑與程度不盡相同。性能
好比:一個坦克能夠影響3km以內空間,3km以內都保持較高的影響。而一個機槍兵只能影響1km之內的空間,而且超出500m以後,士兵的影響十分微弱。優化
坦克相比機槍兵更具影響力,因此想要抵消掉坦克的影響,咱們可能須要更多的機槍兵與之對抗。這些數值根據具體的遊戲邏輯來設定。ui
2,實現傳播算法,以什麼樣的方式傳播,各勢力影響值得疊加邏輯。
3,實現衰減算法,以什麼樣的方式衰減,常見如影響隨距離線性衰減。
1,肯定傳播區域,獲取傳播區域內node,從center node開始以廣度優先遍歷區域內node,更新influence值。
2,influence值隨傳播距離線性衰減。
這是最簡單的方法,還有一些提升性能的方法,有興趣同窗能夠google之。
經過以上的總結,咱們已經知道了勢力圖對於戰略的做用。那麼對於通常的遊戲,咱們是否用的上呢?
我如今的想法是,Influence map能夠和尋路系統進行融合。好比,NPC在尋路的時候,不是選擇一條最短的路徑,而是選擇一條最安全的路徑。
只需想象一下便可,咱們須要到達A點,但最短路徑上有一個敵方炮塔,咱們沒法對抗炮塔的攻擊,那麼咱們須要捨近求遠,繞道一個炮塔沒法攻擊的地點,最終到達A點。
截圖體現了咱們以前總結出的規律:
1,影響的傳播,紅色區域乃是影響的傳播範圍。
2,影響的衰減,隨着遠離中心區域,紅色逐漸變淺。
3,障礙物會阻礙影響的傳播。
4,尋路小機器人,尋路時試圖躲避高危的紅色區域。
尋路機器人會躲避敵方靜止的機器人,而且雙方相互影響。
編輯器擴展涉及到的文件以下:
Base.cs AStarPath.cs AStarPathEditor.cs astarclassess.cs 核心代碼 Color NodeColor (GraphNode node, PathHandler data)
勢力圖的邏輯涉及到的文件以下:
astarclassess.cs InfluenceUpdateObject這是一個新的類,表示那部分導航圖須要更新。 可參考GraphUpdateObject
GridNode.cs / GraphNode.cs 添加node的influence信息。
GridGenerator.cs 添加node的influence信息更新邏輯。
Seeker.cs 添加使用AInfluencePath尋路的邏輯。
AInfluencePath.cs AInfluencePath : ABPath這是一個新的類,用A*算法求取的influence路徑。
須要重定義public override uint GetTraversalCost (GraphNode node)
主要優化了性能。由於A* pathfinding project 使用多線程。因此,在更新graph的Influence信息時,須要blockpathfinding thread.不然會出現尋路異常。
在更新完地圖後 unblock pathfinding thread,爲了防止頻繁的block and unblock pathfinding thread,新建一個更新隊列,批處理多個agent的更新請求。
若是有須要更新的Influence請求,就會請求block pathfinding thread,但該函數不會一直等待而是當即返回,下一幀查看pathfinding thread是否block。
若是後面某一幀 pathfinding thread block 那麼當即批處理更新隊列中的請求。 咱們能夠設置每幀更新請求數量的最大值,以避免致使某幀會耗時過長。
沒有位置和信息變化的agent不須要請求刷新Influence信息,而且同一個agent新的Influence信息會覆蓋後面的信息。這樣能夠保證queue不會過分膨脹。
InfluenceUpdateObject 描述更新區域,更新勢力,以及後續還原influence值。
public class InfluenceUpdateObject { public Bounds bounds; public List<GraphNode> changedNodes; public float deltaInfluence; public AInfluencePath.Faction faction; private List<float> backupData; public InfluenceUpdateObject(Bounds b, uint delta,Pathfinding.AInfluencePath.Faction f){ this.bounds = b; this.deltaInfluence = delta; this.faction = f; } public virtual void WillUpdateNode (InfluenceNode node) { if ( node != null) { if (changedNodes == null) { changedNodes = ListPool<GraphNode>.Claim(); backupData = ListPool<float>.Claim(); } changedNodes.Add(node.node); backupData.Add(node.deltaInfluence); } } public virtual void RevertFromBackup () { if (changedNodes == null) return; for (int i = 0; i < changedNodes.Count; i++) { if (faction == AInfluencePath.Faction.BLACK) { changedNodes [i].influenceOfBlack -= backupData [i]; if (changedNodes [i].influenceOfBlack < 0.01) changedNodes [i].influenceOfBlack = 0; } if (faction == AInfluencePath.Faction.WHITE) { changedNodes [i].influenceOfWhite -= backupData [i]; if (changedNodes [i].influenceOfWhite < 0.01) changedNodes [i].influenceOfWhite = 0; } } ListPool<GraphNode>.Release(changedNodes); ListPool<float>.Release(backupData); changedNodes = null; } }
InfluenceUpdater 該類負責更新Navgraph的influence 值。
using System.Collections; using System.Collections.Generic; using UnityEngine; using Pathfinding; public class InfluenceUpdater : MonoBehaviour { private Queue<InfluenceUpdateObject> m_queue; public uint maxHandleCount; public void AddWorkItem(InfluenceUpdateObject o){ if(!m_queue.Contains(o)) m_queue.Enqueue (o); } private void ProcessWorkItems(){ if (m_queue.Count == 0) return; AstarPath.active.BlockPathQueueNotWait (); if (!AstarPath.active.IsAllPathThreadBlocked ()) return; AstarPath.active.ReturnPaths (false); uint count = 0; while (m_queue.Count > 0 && count < maxHandleCount) { InfluenceUpdateObject iuo = m_queue.Dequeue (); foreach (IUpdatableGraph g in AstarPath.active.astarData.GetUpdateableGraphs()) { GridGraph gr = g as GridGraph; gr.UpdateInfluenceInBounds (iuo); } count++; } AstarPath.active.FlushWorkItems(); } void Awake(){ if(m_queue == null) m_queue = new Queue<InfluenceUpdateObject> (); } void LateUpdate(){ ProcessWorkItems (); } }
AInfluencePath 該類用來表示使用influence 做爲cost的路徑。
using UnityEngine; namespace Pathfinding { public class AInfluencePath : ABPath { public enum Faction {BLACK,WHITE}; private Faction faction; public AInfluencePath () { } public static AInfluencePath Construct (Vector3 start, Vector3 end, OnPathDelegate callback = null,Faction f = Faction.BLACK) { var p = PathPool.GetPath<AInfluencePath>(); p.Setup(start, end, callback); p.faction = f; return p; } public override uint GetTraversalCost (GraphNode node) { GridNode gNode = node as GridNode; if (gNode != null) { if (AInfluencePath.Faction.BLACK == faction) return gNode.influenceOfWhite <= gNode.influenceOfBlack? 0 : (uint)(gNode.influenceOfWhite - gNode.influenceOfBlack); if (AInfluencePath.Faction.WHITE == faction) return gNode.influenceOfBlack <= gNode.influenceOfWhite ? 0 :(uint)( gNode.influenceOfBlack - gNode.influenceOfWhite); } return 0; } } }
Base.cs 在編輯模式下設置graphNode的顏色
public virtual Color NodeColor (GraphNode node, PathHandler data) { Color c = AstarColor.NodeConnection; switch (AstarPath.active.debugMode) { case GraphDebugMode.Areas: c = AstarColor.GetAreaColor(node.Area); break; case GraphDebugMode.Penalty: c = Color.Lerp(AstarColor.ConnectionLowLerp, AstarColor.ConnectionHighLerp, ((float)node.Penalty-AstarPath.active.debugFloor) / (AstarPath.active.debugRoof-AstarPath.active.debugFloor)); break; case GraphDebugMode.Tags: c = AstarColor.GetAreaColor(node.Tag); break; case GraphDebugMode.Influence: if (Mathf.Abs(node.influenceOfBlack - node.influenceOfWhite) <= 0.00001) c = new Color (1, 1, 1); else { if (node.influenceOfBlack > node.influenceOfWhite) { c = Color.Lerp(AstarColor.ConnectionLowRedLerp, AstarColor.ConnectionHighRedLerp, (Mathf.Abs(node.influenceOfBlack - node.influenceOfWhite)-AstarPath.active.debugFloor) / (AstarPath.active.debugRoof-AstarPath.active.debugFloor)); } if (node.influenceOfBlack < node.influenceOfWhite) { c = Color.Lerp(AstarColor.ConnectionLowGreenLerp, AstarColor.ConnectionHighGreenLerp, (Mathf.Abs(node.influenceOfBlack - node.influenceOfWhite)-AstarPath.active.debugFloor) / (AstarPath.active.debugRoof-AstarPath.active.debugFloor)); } } break; default: if (data == null) return AstarColor.NodeConnection; PathNode nodeR = data.GetPathNode(node); switch (AstarPath.active.debugMode) { case GraphDebugMode.G: c = Color.Lerp(AstarColor.ConnectionLowLerp, AstarColor.ConnectionHighLerp, ((float)nodeR.G-AstarPath.active.debugFloor) / (AstarPath.active.debugRoof-AstarPath.active.debugFloor)); break; case GraphDebugMode.H: c = Color.Lerp(AstarColor.ConnectionLowLerp, AstarColor.ConnectionHighLerp, ((float)nodeR.H-AstarPath.active.debugFloor) / (AstarPath.active.debugRoof-AstarPath.active.debugFloor)); break; case GraphDebugMode.F: c = Color.Lerp(AstarColor.ConnectionLowLerp, AstarColor.ConnectionHighLerp, ((float)nodeR.F-AstarPath.active.debugFloor) / (AstarPath.active.debugRoof-AstarPath.active.debugFloor)); break; } break; } c.a *= 0.5F; return c; }
GridGenerators.cs 私有函數更新graph的influence值,爲防止不斷開闢內存和釋放內存以及重複計算節點距離,採起了優化措施,影響了閱讀性。
private void UpdateInfluenceInternal(InfluenceUpdateObject o){ var nodeCenter = GetNearest(o.bounds.center).node as GridNode; var radius = o.bounds.size.x <= o.bounds.size.z ? o.bounds.size.x / 2 : o.bounds.size.z / 2; nodeSet.Clear(); var decay = o.deltaInfluence / radius; int popIndex = 0; int pushIndex = popIndex; if (nodeList.Count == 0) { nodeList.Add (new InfluenceNode (nodeCenter, o.deltaInfluence)); } else { nodeList[pushIndex].node = nodeCenter; nodeList[pushIndex].deltaInfluence = o.deltaInfluence; } pushIndex++; while(popIndex < pushIndex) { InfluenceNode iNode = nodeList[popIndex]; popIndex++; GridNode curNode = iNode.node as GridNode; nodeSet.Add (curNode); o.WillUpdateNode (iNode); if (o.faction == AInfluencePath.Faction.BLACK) { curNode.influenceOfBlack += iNode.deltaInfluence; } if (o.faction == AInfluencePath.Faction.WHITE) { curNode.influenceOfWhite += iNode.deltaInfluence; } for (int i = 0; i < 8; i++) { if (curNode.GetConnectionInternal (i)) { GridNode other = nodes [curNode.NodeInGridIndex + neighbourOffsets [i]]; if (other!= null && !nodeSet.Contains(other)) { if (o.bounds.Contains ((Vector3)other.position)) { var decayDis = 0.5f; if (i >= 4) decayDis = 0.7f; float tmpDelta; if (iNode.deltaInfluence < decayDis * decay ) tmpDelta = 0; else tmpDelta = iNode.deltaInfluence - (decayDis * decay ); if (tmpDelta != 0) { if (nodeList.Count <= pushIndex) { nodeList.Add (new InfluenceNode(other,tmpDelta)); } else{ nodeList[pushIndex].node = other; nodeList[pushIndex].deltaInfluence = tmpDelta; } nodeSet.Add (other); pushIndex++; } } } } } } }
上述爲關鍵代碼,要啓用該功能,還須要自定義 Seeker.cs 以及 調用 Seeker的 AgentAI腳本。
有園友提問InfluenceNode在A* Project裏面找不到,這是咱們本身添加的新Node,因此找不到,附上
public class InfluenceNode{ public InfluenceNode(GraphNode node, float delta) { this.node = node; this.deltaInfluence = delta; } public GraphNode node; public float deltaInfluence; }