演算法、資料結構、與設計模式等在遊戲開發中的運用 (四):佇列(Queue)

Compasslg發表於2021-04-25

演算法、資料結構、與設計模式等在遊戲開發中的運用 (四):佇列(Queue)

作者:Compasslg

1. 什麼是佇列

如同棧(Stack)一般,佇列(Queue)也是一種抽象的資料結構(Abstract Data Structure)。所以同理的,“佇列” 這個名稱定義的是你如何從外部理解和使用這種資料結構,而非該資料型別的具體實現方式或者內部結構——你可以根據自己的需求選擇用陣列、連結串列等各種各樣的資料結構來實現佇列。關於這方面的詳情可以參考我討論Stack的那一篇博文

相反於棧的先進後出(FILO: First in, Last out),佇列是一種先進先出(FIFO) 的資料型別。就和現世生活中的排隊購物一樣,先進隊伍的人在前面,可以比後進隊伍的人先結賬並脫離隊伍;而新來的人則會加入到隊伍的末尾。

與Stack類似,佇列也有三個基本的方法:一個用於加入新的元素到尾部 (enqueue入隊),一個用於獲取並移除佇列最前端的元素 (dequeue出隊),以及一個用於檢視當前佇列是否為空。同時根據需要,佇列也常常會有用於檢視最前方元素(Front)和尾端元素(Rear)的方法,與棧的Peek/Top方法相似。

上面談到的是最基本的線性佇列的概念與結構。事實上,除了這種所謂 “傳統意義上的佇列” 以外,程式設計中也有很多功能花哨甚至可能是非線性的資料結構也號稱“佇列”。一個典型的特殊佇列的例子就是很多演算法都會運用到的優先佇列(Priority Queue),這個會在以後講Heap或者尋路演算法的時候再詳細討論。

2. 如何實現和使用佇列

佇列有很多實現方法,其中最簡單的便是利用連結串列(LinkedList)。但在實際運用中,利用連結串列往往不是最優的佇列實現方法,且大多數語言內建的Collection包裡也不會用連結串列來實現佇列。如果你對資料結構有一定的瞭解,就會知道相較於連結串列,使用陣列會有明顯的速度和空間使用效率上的優勢(尤其是佇列容量固定的時候)。但由於這篇博文重點在於介紹佇列的概念和使用,為了方便理解這裡會採用最直觀的連結串列來實現佇列的方法(其實在資料不多的情況下,連結串列的這些劣勢基本可以忽略不急)。

public class Queue<T> {
	private Node<T> front, rear;
	public Queue(){
		front = null;
		rear = null;
	}

	// 入隊
	// add a new element to the rear of the queue
	public void enqueue(T e){
		// create a node, assign to both rear and front if the queue was empty
		// 新建一個元素並插入到佇列最前方。如果佇列此前為空,那麼這個元素會同時為前端和尾端。
		if(front == null){
			front = new Node(e);
			rear = front;
		}else{
			rear.next = new Node(e);
			rear = rear.next;
		}
	}
	
	// remove and return the front element
	// 出隊
	public T dequeue(){
		// throw an error if the queue is empty
		// 佇列為空時丟擲異常
		if(front == null){
			throw new Exception("dequeue(): queue is empty.");
		}
		// save front value, remove front node
		// 獲取佇列最前端的值,並移除用於儲存該值的Node
		T e = front.value;
		front = front.next;
		
		// if the removed front is the only element in queue
		// both front and null should be null
		// 如果移除後最前端的值變為空,則將隊尾的值也設定為空
		if(front == null){
			rear = null;
		}
		return e;
	}
	
	// return true if front is null, meaning no element in Linked List
	// 檢視佇列是否為空
	public boolean isEmpty(){
		return front == null;
	}

	// an inner class representing a node in the linkedlist
	// 一個node代表連結串列/佇列中的包含一個元素的容器。
	class Node<T>{
		T value;
		Node<T> next;
		public Node(T e){
			value = e;
			next = null;
		}
	}
}

這裡要注意的是,我在上一段程式碼中使用的是Java語法,但我並沒有選擇使用Java自帶LinkedList,而是直接在佇列的類中自己實現了一個簡單的連結串列。事實上,Java的LinkedList類本身就繼承了一個Queue的介面(Interface),你可以直接將它當佇列來使用,用linkedList.add()來入隊,linkedList.remove()來出隊。因此,如果在外面再套一層Queue的話會顯得有些多此一舉;同時,我選擇直接在Queue中實現連結串列的方式實際上也和Java自帶的連結串列類裡實現Queue介面中的方法的方式大同小異,這樣也更方便理解。

3. 遊戲開發中的應用

  1. 回合制遊戲中的行動順序佇列(Turn Queue)
    回合制和半回合制遊戲中常常有速度、行動力的概念來決定場上所有單位的行動順序,這個時候可以通過佇列來安排。當然,很多遊戲中會有提升或降低速度的技能和物品,這個時候會需要重新生成佇列。

  2. 管理經營類遊戲中的生產佇列
    很多管理經營類遊戲,或者即時戰略遊戲中都會有生產佇列的概念。通過使用佇列來提前規劃之後的生產順序可以使玩家操作起來更為方便。一個典型的例子就是文明系列中在城市裡的生產列隊,提前安排之後需要生成的單位或設施,將後續需要製造的東西依次加入佇列,噹噹前生產任務完成時,從佇列中移除並獲取下一個需要生成的單位。

  3. 行動佇列
    很多及時戰略遊戲和MOBA類遊戲中都有用佇列提前安排之後的行動的功能。例如DotA中可以在TP的時候按住Shift將跳刀加到行動佇列中實現落地瞬間跳走,沙王施法前搖時將跳到放到佇列中實現跳大等操作。

  4. 劇情對話
    當劇情對話會因為玩家的選擇產生分支的時候,常常需要用樹結構來儲存,但歸根結底,這可以被當作一種非線性的佇列。實際執行的時候,還是可以用Queue來儲存和操作正在播放的劇情文字,分支產生並被選擇以後再將該分支下的對話加入到佇列中。

  5. 動畫播放,多節點移動
    遊戲動畫中,有時候會有沿著一系列節點移動的操作,這個過程可以使用佇列來完成,將所有節點按照順序加入佇列,然後用dequeue獲取並移除佇列頂端的點來實現按照相同順序移動。

  6. 訊息、事件的傳輸和分發
    一些網遊或者多程式的單機需要用接收、處理從網路或其他程式傳入的指令和訊息,而當本地在處理訊息的時候,其他陸續傳入的訊息將在佇列中等待,然後當前一個任務執行完畢後從佇列中獲取下一個需要被執行的指令或需要處理的訊息。

4. 總結

總而言之,佇列是一種非常常見且實用的抽象資料結構。其重點不在實現過程,而是概念和使用方式;很多時候,初學者都會以使用佇列的方式來利用陣列或連結串列而不自知。能夠熟練和合理的運用和理解這種極簡卻實用的抽象資料型別可以為遊戲開發提供很大的便利。

相關文章