1. 程式人生 > >A*尋路演算法之解決目標點不可達問題

A*尋路演算法之解決目標點不可達問題

在遊戲世界的尋路中,通常會有這樣一種情況:在小地圖上點選目標點時,點選到了障礙物或者建築上,然後遊戲會提示我們目標地點無法到達。玩家必須非常小心的在小地圖上點選目標區域的空白部分,才能移動到目標地點。那麼,有沒有辦法來改進一下這種不友好的體驗呢?

下面給出兩種方法:

  1. 最近可達點替代:當目標點S不可達時,在S點周圍尋找一個最近的可達點R,讓R替代S作為目標點尋路。
  2. 最近點檢測法:設定一個最小距離,尋路過程中,不斷檢測每個加入openList的點,如果該點到目標點S的距離小於最小距離,儲存當前點為N,並修改最小距離。在尋路結束時,如果目標點S無法到達,那麼將最接近點N作為尋路的終點。

1.最近可達點替代

最近可達點替代方法是在尋路之前的一個預處理,首先需要檢測目標點是否可達,如果不可達則需檢測最近的一個可達的點作為目標點。其情形如下:

  

從A點到S的尋路中,發現S點不可達時,在S點周圍一圈的點中搜尋到R,使用R點作為替代目標點。如果A點需要移動到S2點呢?我們在檢測附近可達點的時候,引入一個變數depth,表示不可達點的深度,意為從S到最近的可達點需要搜尋的圈數。

程式碼延續前一篇文章A*尋路演算法之解決路徑多拐點問題中的A*尋路演算法,搜尋替換點方法實現如下:

	public Node getNearestReachableNode(Node node, int maxDepth) {
		if (maxDepth <= 0 || map.canReach(node.x, node.y)) {
			return node;
		}
		
		for (int depth = 1; depth < maxDepth ; depth++) {
			for (int i = node.x - depth; i <= node.x + depth; i++) {
				// 左
				if (map.canReach(i, node.y - depth)) {
					return new Node(i, node.y  - depth);
				}
				// 右
				if (map.canReach(i, node.y + depth)) {
					return new Node(i, node.y  + depth);
				}
			}
			for (int i = node.y - depth + 1; i < node.y + depth; i++) {
				// 上
				if (map.canReach(node.x - depth, i)) {
					return new Node(node.x - depth, i);
				}
				// 下
				if (map.canReach(node.x + depth, i)) {
					return new Node(node.x + depth, i);
				}
			}
		}
		return node;
	}

2.最近點檢測法

最近點檢測,基本的想法就是在尋路過程中不斷檢測加入到openList中的點與目標點的距離。當無法尋路到目標點時,可以移動到這個最近點上。下面直接上程式碼:

	public List<Node> findPath(Node startNode, Node endNode) {
		int minDistance = Integer.MAX_VALUE;
		Node nearestNode = startNode;
		newOpenList.add(startNode);

		Node currNode = null;
		while ((currNode = newOpenList.poll()) != null) {
			removeKey(openSet, currNode.x, currNode.y);
			addKey(closeSet, currNode.x, currNode.y);

			ArrayList<Node> neighborNodes = findNeighborNodes(currNode);
			for (Node nextNode : neighborNodes) {
				// G + H + E
				int gCost = 10 * calcNodeCost(currNode, nextNode) + currNode.gCost 
						+ calcNodeExtraCost(currNode, nextNode, endNode);
				if (contains(openSet, nextNode.x, nextNode.y)) {
					if (gCost < nextNode.gCost) {
						nextNode.parent = currNode;
						nextNode.gCost = gCost;
						nextNode.fCost = nextNode.gCost + nextNode.hCost;
					}
				} else {
					nextNode.parent = currNode;
					nextNode.gCost = gCost;
					nextNode.hCost = 10 * calcNodeCost(nextNode, endNode);
					nextNode.fCost = nextNode.gCost + nextNode.hCost;
					newOpenList.add(nextNode);
					
					addKey(openSet, nextNode.x, nextNode.y);
					
					// 檢測是否是當前最近點
					int distance = Math.abs(nextNode.x - endNode.x) + Math.abs(nextNode.y - endNode.y);
					if (distance < minDistance) {
						minDistance = distance;
						nearestNode = nextNode;
					}
				}
			}
			
			if (contains(openSet, endNode.x, endNode.y)) {
				Node node = findOpenList(newOpenList, endNode);
				return getPathList(node != null ? node : nearestNode);
			}
		}

		Node node = findOpenList(newOpenList, endNode);
		return getPathList(node != null ? node : nearestNode);
	}

大概修改10行左右的程式碼,就可以在尋路的過程中解決目標點不可達的問題。

3.兩種方法對比

1.目標點替代法無法解決尋路到封閉地形的問題,而最近點檢測方法可以移動到附近的點R( 如左圖)。因此,最近點檢測方法一定會生成一條路徑,而目標點替代法不一定會有。

2.一般情況下,當目標點無法移動過去時,最近點檢測方法往往會搜尋地圖上的所有點,而目標點替代法通過找最近可達點可以解決這個問題。不過在如左圖所示回字形地圖上,標點替代法同樣會搜尋地圖上的所有點。

3.目標點替代法搜尋到的可達點,可能不是在起點和中間之間,可能使最終生成路徑變得稍微有點怪異。而最近點檢測方法生成的路徑則很自然。如右圖所示,由於目標點替代法查詢周圍的可達點時,是按照一定的順序去搜索的。那麼就可能出現目標點是S,卻走到R1上去了,而很自然的路徑應該是走到R點上。這一點在格子大小很小的地圖中表現不是非常明顯,但是在格子很大的地圖上表現的差異感就很強烈了,甚至不能接受了。不過,我們可以通過額外的方法消除這個問題-- 在搜尋每一圈的可達點的時候,計算該圈上每一點到起始點的距離大小,選擇距離最小的可達點作為替代點。