算法、数据结构、与设计模式等在游戏开发中的运用 (四):队列(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. 总结

总而言之,队列是一种非常常见且实用的抽象数据结构。其重点不在实现过程,而是概念和使用方式;很多时候,初学者都会以使用队列的方式来利用数组或链表而不自知。能够熟练和合理的运用和理解这种极简却实用的抽象数据类型可以为游戏开发提供很大的便利。

04-25 18:20