[Unity] A* pathfinding project integrated with influence map

簡介

最近一階段重溫了一些關於遊戲人工智能方面的書籍。 增強了對influence map的認知。想要親自動手實現一下。node

正如文章標題所示,這篇文章講的是:如何將influence map的機制融入到當前較火的unity尋路插件A* pathfinding project裏。算法

先科普一下Influence Map基本概念:

influence map中文名:勢力圖或影響圖。如下稱勢力圖。 勢力圖是基於空間的,某些空間歸屬A,另一些空間歸屬B,等等。安全

把問題規模縮小到一場遊戲戰役,每一個兵種單位都佔據並影響着必定的空間,且相同勢力的單位對同一空間的影響能夠疊加,影響值隨傳播距離遞減。多線程

勢力圖除了告訴咱們某塊空間的歸屬以外,還能告訴咱們什麼呢?

1,進攻方,能夠根據勢力圖選擇率先攻擊敵人薄弱的地方.防護方,能夠根據勢力圖選擇一個較爲安全的撤退地點。編輯器

2,進一步,統計分析,好比採起某種戰略以後,觀察勢力圖變化,能夠分析以前戰略效果。
ide

3,更進一步,經過對一段時間的勢力圖進行對比,能夠大體預測敵軍的部署動向。函數

 

實現InfluenceMap的要點

1,定義各單位的勢力值傳播範圍,形狀,特性(這是Gameplay)因爲每一個兵種的特性和能力值不一樣,故每一個兵種單位的影響半徑與程度不盡相同。性能

好比:一個坦克能夠影響3km以內空間,3km以內都保持較高的影響。而一個機槍兵只能影響1km之內的空間,而且超出500m以後,士兵的影響十分微弱。優化

坦克相比機槍兵更具影響力,因此想要抵消掉坦克的影響,咱們可能須要更多的機槍兵與之對抗。這些數值根據具體的遊戲邏輯來設定。ui

 

2,實現傳播算法,以什麼樣的方式傳播,各勢力影響值得疊加邏輯。

3,實現衰減算法,以什麼樣的方式衰減,常見如影響隨距離線性衰減。

 

本文使用的算法

1,肯定傳播區域,獲取傳播區域內node,從center node開始以廣度優先遍歷區域內node,更新influence值。

2,influence值隨傳播距離線性衰減。

這是最簡單的方法,還有一些提升性能的方法,有興趣同窗能夠google之。

 

尋路與InfluenceMap結合

經過以上的總結,咱們已經知道了勢力圖對於戰略的做用。那麼對於通常的遊戲,咱們是否用的上呢?

我如今的想法是,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;
    }
相關文章
相關標籤/搜索