intent
一次通知一堆独立对象处理一帧的行为
motivation
玩家控制的角色正在执行一个任务,任务内容是偷取死了很久的巫师王骸骨上的珠宝。它犹豫地靠近地穴的入口。诅咒的雕塑并没有向他雷电攻击。没有不死士卫巡逻门口。它直接走进去,拿到战利品,游戏结束。你赢了。
好吧。那永远不会出现。
这个地穴需要几个守卫-我们的英雄可以与之搏斗的敌人。首先,我们需要一个守卫在门口巡逻。如果你忽略一些细节,最简单的实现守卫来回巡逻的代码可能就像这样:
while (true) { // Patrol right. for (double x = 0; x < 100; x++) { skeleton.setX(x); } // Patrol left. for (double x = 100; x > 0; x--) { skeleton.setX(x); } }
问题是,虽然角色左右移动了,但是玩家看不到。程序被卡在无限循环中,这不是个好的体验。我们真正想要的是每一帧移动一步。
我们必须移除那些循环,并使用外部循环。这保证游戏在守卫巡逻时,依然可以响应输入,可以渲染。像:
Entity skeleton; bool patrollingLeft = false; double x = 0; // Main game loop: while (true) { if (patrollingLeft) { x--; if (x == 0) patrollingLeft = false; } else { x++; if (x == 100) patrollingLeft = true; } skeleton.setX(x); // Handle user input and render game... }
后一种比前一种代码更复杂。左右巡逻上面是两个简单的for循环。现在我们使用外部循环,每一帧都从离开的位置重新开始,并使用patrollingLeft标记方向。
但是这个大约可以工作,我们继续。这些骷髅守卫没什么更多动作,接下来我们添加魔法塑像。他们射出箭状闪电,阻挡角色。
继续我们的“最简单的实现方式”,像:
// Skeleton variables... Entity leftStatue; Entity rightStatue; int leftStatueFrames = 0; int rightStatueFrames = 0; // Main game loop: while (true) { // Skeleton code... if (++leftStatueFrames == 90) { leftStatueFrames = 0; leftStatue.shootLightning(); } if (++rightStatueFrames == 80) { rightStatueFrames = 0; rightStatue.shootLightning(); } // Handle user input and render game... }
你可以分辨出这不是我们喜欢维护的代码。我们会将每一个游戏实体的大量变量和必要的代码放在游戏循环中。为了使他们一起工作,我们把代码搅在一起。
解决这个模式非常简单,可能你已经知道了:每一个实体应该封装自己的行为。这样可以使游戏循环代码整洁,而且很容易添加和删除实体。
为此,我们需要一个抽象层,通过定义一个update方法创建它。游戏循环维护一个对象的集合,但是不知道具体的类型。它知道的是它们都能update。这使得每个对象的行为与游戏循环、其它对象的行为分离开来。
每一帧,游戏循环遍历对象调用update。通过调用update,所有对象同时表现行为。
游戏循环有一个动态对象集合,所以添加和删除都是很简单的-就是直接从集合中添加和删除即可。不再有硬编码了,我们甚至可以使用文件来填充关卡,这正是关卡设计师想要的。
the pattern
游戏世界维护一个对象的集合。每一个对象都实现了update方法,模拟一帧的行为。每一帧,游戏循环更新每一个对象。
when to use it
如果把Game Loop比作面包,那么update就是黄油。与玩家交互的相当多的游戏对象采用这个模式或相似的模式。如果一个游戏有太空陆战队,龙,火星人,鬼,还有运动员,那么这是使用这个模式的好机会。
然而,如果一个游戏更加抽象,游戏对象不怎么活动更像是棋盘的棋子,那么这个模式不适合。像象棋这种游戏,不必模拟所有的棋子,甚至不必每一帧都更新一个卒子。
更新方法当这些情况时好用:
当游戏同时运行大量对象或系统时。
每一个对象的行为都是独立的。
对象行为随时间流逝来模拟
keep in mind
这个模式相当简单,没有什么令人惊讶的内容。
但是,每一行代码都有衍生版本。
sliptting code into single frame slices makes it more complex
当你比较前两段代码,你会发现第二段更复杂。都是简单地使守卫左右巡逻,但是后者把这个过程分散到每一帧进行。
这个修改是必须的,因为要处理用户输入,渲染还有其他的事务,所以第一个不实际。但是记住将代码分散会增加很大的复杂性。
you have to store state to resume where you left off each frame
在第一个例子中,我们并没有一个变量表示向左向右。它隐式取决于哪段代码正在执行。
当我们把它改成“一帧一次”的形式,我们不得不创建一个patrolLeft变量追踪方向。当我们执行完代码,执行位置丢失了,所以我们不得不显式存储足够多的数据以便下一帧恢复。