算法、数据结构、与设计模式等在游戏开发中的运用 (二):栈(Stack)

算法、数据结构、与设计模式等在游戏开发中的运用 (二):栈(Stack)

作者:Compasslg 李涵威

(如果你已经了解什么是栈并且如何实现,可以直接跳到第3部分)

1. 什么是栈(Stack)

通常来说,我们认为栈(Stack)是一种抽象的数据类型(Abstract Data Type),或者说抽象的数据结构(Abstract Data Structure)。之所以说是抽象,我个人的见解是因为这种数据结构并非根据他的内部组成或者实现方式定义的,而是根据其调用方式。举一个简单的例子,链表(LinkedList)是一种由一个一个不同的节点(Node)通过指针连接而成的线性数据结构。他的使用方式和一般的ArrayList没有什么区别,但是由于他们的实现方式不同,导致我们会在不同的情况下使用它们,而他们也被视为不同的数据结构。因此,这些数据结构都是由他们的实现方式定义的。

相比上述提到的数据结构,Stack这个概念本身不会限制开发者如何实现它,而只是抽象的告诉使用者,这个结构会将数据一层一层的“堆叠”起来。你可以通过如图1所示的方法在将新的元素加入(Push)到栈的 顶端(Top),或者获取当前顶端的元素。这样每次从中获得或者移除的数据就是你最后一次加入的数据,这也就是 先进后出(First In, Last Out) 的概念。至于具体如何实现,开发者可以自行决定。很多人甚至会直接把其他的线性数据结构例如数组和链表当作Stack来使用——事实上,Stack通常也只是这两种数据结构之一的外面包一层外壳而已。

图1:Stack中元素的加入,查看,和移除
图 1:Stack中元素的加入,查看,和移除

相对于 “栈” 这个名词,我个人更喜欢港台地区对Stack的翻译 —— “堆叠”。在我学习编程之初,“堆栈”的用法对我造成了很大的困扰。因为Stack这个词本身的意思就和“堆”差不多,我在用英文学习的时候也是把它视作一种 “将东西堆起来” 的数据结构,因此我常常先入为主的把中文的“堆”(Heap)和“栈”(Stack)的含义弄反。直到很长一段时间以后才从一些中文的资料中发现我之前闹的乌龙……

在内存中,堆和栈代表着不同的意思。由于这篇文章主要讨论的是Stack作为一种抽象数据结构在游戏开发中的运用,在这里我不会讨论他们的其他含义。在之后博客中我也会谈到堆(Heap)作为一种数据结构在游戏开发中的应用,此处就不多提了。

2. 如何实现和使用 Stack (Java)

在这一部分,我将用利用Java中的数组(Array)来实现Stack。之所以使用数组而不是前面提到的链表,是因为这样可以尽量保证Stack使用的是连续的内存空间。虽说在游戏开发中用到Stack的大多数情况下这点优化都几乎可以忽略不计,但养成一个好的开发习惯总归是没错的,更何况你永远不知道什么时候这些小问题会在将来造成更大的影响。

以下便是一个利用java中的数组实现的简单的栈。要注意的是,因为使用的是最普通的数组,我们难免会要面对Stack的容量限制问题。这一点上,我们可以选择接受Stack有容量限制的事实,在超出容量时报错;也可以选择类似java中ArrayList的处理方式,在Stack满了以后扩大容量 (建一个新的数组,两倍于原来的大小,将原数组中的所有元素转移到新数组)。下面的代码选择的是第一种处理方式。

public class Stack<T>{
	private T arr[];
	private int top;
	// 构造器 1: 使用参数容量
	public Stack(int size){
		arr = new T[size];
		top = -1;
	}

	// 构造器 2:假定默认容量为 10
	public Stack(){
		arr = new T[10];
		top = -1;
	}

	// 加入新元素 element
	public void push(T element){
		// stack 已满的情况:
		if(arr.length - 1 == top){
			// 这里你可以选择扩大Stack的容量或报错
			throw new Exception("push(): stack is full")
		}
		top++;
		arr[top] = element;
	}
	
	// 移除并返回顶端元素
	public T pop(){
		// stack 为空的情况:
		if(isEmpty()){
			// 这里你可以选择报错或其他处理方式
			throw new Exception("pop(): stack is empty");
		}
		
		T element = arr[top];

		// 移除顶端
		arr[top] = null;
		top--;
		
		return element;
	}
	
	// 查看当前顶端元素
	public T peek(){
		// stack 为空的情况:
		if(isEmpty()){
			throw new Exception("peek(): stack is empty");
		}
		return arr[top]; 
	}

	// 查看是否为空
	public boolean isEmpty(){
		return top == -1;
	}
	
}

下面则是使用的范例

Stack<Integer> intStack = new Stack<Integer>();
intStack.push(3);
intStack.push(2);
intStack.push(1);
System.out.println(intStack.isEmpty()); // false
System.out.println(intStack.pop()); // 1
System.out.println(intStack.pop()); // 2
System.out.println(intStack.pop()); // 3
System.out.println(intStack.isEmpty()); // true

3. 游戏开发中的应用

在游戏开发中,Stack往往是被用来实现 “返回/撤销” 的功能。一个典型的例子就是推箱子类游戏中的撤销上一步的操作。我们可以将每一步的操作或者是坐标信息加入到Stack中,当玩家按下返回/撤销键时将上一步操作从栈中移除并根据其中的信息在游戏中反向操作,这样就可以达到想要的效果。

另一个Stack常常被用到的地方就是在游戏 “状态/窗口/场景” 管理中。在游戏中,我们往往会有多个窗口叠加在一起,或者从一个场景进入到子场景。这个时候,通过在 进入 “新场景/打开新窗口” 时将这个 “场景/窗口” 加入到Stack中,玩家选择返回时再从栈中弹出并销毁。所有的事件和操作都只对当前的顶端的窗口生效,以下是一段简单的示范代码

public class SceneManager{
	private Stack<Scene> scenes;
	
	// constructor
	public SceneManager(){
		scenes = new Stack<Scene>();
	}
	
	// 刷新当前场景
	public void update(){
		scenes.peek().update();
	}

	// 返回到上一场景
	public void popScene(){
		if(scenes.isEmpty()){
			return;
		}
		scenes.pop();
	}

	// 进入新场景
	public void pushScene(Scene scene){
		scenes.push(scene);
	}
}

在刚开始学习编程和制作游戏的时候,我使用的都是RPG Maker中用一个变量来指向当前场景,然后在每个场景中保存上一个场景的信息的方法。这样虽然简单,却时常造成了很多重复的信息存取和一些难以预料的bug,直到接触了Stack,我才从中解脱出来。

4. 总结

作为一种线性的数据结构,Stack本身并不复杂。大部分编程语言的内置包中都会有Stack, 除非有特殊需求,程序员也往往都不会需要自己编写。不过先了解Stack的原理才能更好的运用它,而且我个人觉得Stack这个数据结构是现实生活中的概念和编程的完美结合,在游戏开发的过程中,它给我带来了很多灵感; 我将来可能还会在讨论一些算法的时候再接着聊聊它的妙用。

posted @ 2021-04-23 19:59  Compasslg  阅读(305)  评论(0编辑  收藏  举报