多年前看的一个demo使我第一次完全明白了游戏中的不同状态。这个demo能够从一个特效平滑的过渡到另一个特效,并能从2D漩涡效果直接过渡到3D渲染环境,就像是几个不同的程序写成的。
多状态不仅在demo中重要,在游戏中也同样重要。每个游戏程序基本上都是从介绍开始,显示菜单,然后进行游戏。当被击败时,游戏就结束了(通常返回到菜单界面)。大多数游戏中,很可能会同时出现多个状态——比如游戏过程中弹出菜单。
传统的处理多状态的方式是使用一大堆的if,switch 和loop语句。程序从介绍状态开始一直循环,直到按下一个键;然后菜单就显示了,直到完成了选择操作;游戏开始了,就一直循环,直到游戏结束。每一次游戏循环,程序还得检查是应该显示菜单还是简单的绘制下一帧。而且,处理事件的那部分程序还必须判断输入究竟是菜单来相应还是游戏来相应。所有的这些组合起来就形成了一个主循环,可以发现这个大杂烩很难理解,并且调试和维护起来也不方便。
状态是什么呢?(What’s a state?)
游戏状态就像是游戏的一个单独的程序。每个状态需要以不同的方式处理事件,需要在屏幕上绘制不同的内容。每个状态还需要处理它自己的事件、更新游戏场景、绘制下一帧等等。因此,我们可以确定状态应该至少拥有三个方法。
状态又应该能够加载图形、自初始化、释放资源。并且有时状态还需要暂停,还要能够恢复。
到此为止,状态类就应该看起来如下所示:
class CGameState{public: void Init(); void Cleanup();
void Pause(); void Resume();
void HandleEvents(); void Update(); void Draw();};
上面的状态类设计应该能够满足游戏的状态需求。把它当作基类,从它可以派生出游戏所需要的每一种状态——介绍状态(intro state)、菜单状态(menu state)、游戏状态(play state)等等。
状态管理器(The State Manager)
接下来,我们需要一个管理状态的方法——状态管理器。状态管理器是游戏引擎本身的一部分。别人可能会选择创建一个单独的状态管理类,而我只是把它直接加到引擎中。我们先来看看一个游戏引擎能做什么,然后再设计一个类来完成这些功能。
我们的这个例子很简单,整个引擎需要做到是初始化SDL和完成后的清理工作。由于在主循环需要用到这个引擎,因此需要检查引擎是否仍然在运行、是否退出,以及处理事件、更新游戏、绘制帧序列。
状态管理器实际上很简单。我们用 “状态栈”来管理各种不同的游戏状态。在此用STL的vector来实现状态栈。除此之外,还需要改变状态的方法,以及入栈和出栈方法。
至此,游戏引擎类就如下所示:
class CGameEngine{public:
void Init(); void Cleanup();
void ChangeState(CGameState* state); void PushState(CGameState* state); void PopState();
void HandleEvents(); void Update(); void Draw();
bool Running() { return m_running; } void Quit() { m_running = false; }
private: // the stack of states vector<CGameState*> states;
bool m_running;};
这些成员函数中有几个非常简单的——HandleEvents(), Update() 和Draw()。它们只需简单的调用栈顶状态对象相应的成员函数即可。由于需要经常访问游戏引擎中的数据,因此需要在以上三个成员函数中加一个指向引擎指针的参数。
最后需要考虑的是状态之间的切换。引擎如何知道状态何时切换到另一个状态?答案是:它不知道。只有当前的状态知道什么时候转向下一个状态。于是,我们又需要为状态类加一个改变状态的成员函数。
对于状态类时,我们把它设计为一个抽象基类——它的大部分成员函数设计为纯虚函数。这就保证了它的子类都必须得实现这些方法。经过这些更改后,最后的游戏状态类如下所示:
class CGameState{public: virtual void Init() = 0; virtual void Cleanup() = 0;
virtual void Pause() = 0; virtual void Resume() = 0;
virtual void HandleEvents(CGameEngine* game) = 0; virtual void Update(CGameEngine* game) = 0; virtual void Draw(CGameEngine* game) = 0;
void ChangeState(CGameEngine* game, CGameState* state) { game->ChangeState(state); }
protected: CGameState() { }};
现在要为游戏增加状态就非常简单了,只需从基类继承一个子类,实现那几个纯虚函数就可以了。对于某种状态,在游戏中只需一个实例,因此把它们设计成Singleton模式是个不错的选择。
这种方法究竟能够把游戏简化到什么程度,如下所示的main.cpp中包含的所有代码可以说明:
int main ( int argc, char *argv[] ){ CGameEngine game;
// initialize the engine game.Init( "Engine Test v1.0" );
// load the intro game.ChangeState( CIntroState::Instance() );
// main loop while ( game.Running() ) { game.HandleEvents(); game.Update(); game.Draw(); }
// cleanup the engine game.Cleanup();
return 0;}
下载(Downloads)
这个例子里包含了三个不同的状态——introduction state,playing state,pauses state。每种状态都用一幅背景图片来表示。
· stateman.zip - Tutorial Source, Graphics, and Project files for Visual C++ 6.
· stateman.tar.gz - Tutorial Source, Graphics, and Makefile for Linux.
这个例子的代码用的是SDL。如果你不熟悉,可以看一下这篇文章:Getting Started with SDL。
任务组取消
新的Beta1 PPL的其中一个功能就是可以取消正在运行中的任务组。在task_group类型上加入run 和 wait方法是一个新的cancel方法。还有一个相应的is_canceling方法让你可以检查在进程中是否完成取消。task_group_status列举也有一个新的值叫做canceled,让你检查取消是否发生。以下的代码展示了这些新的功能:
//declare tasks and run tasks
task_group tg;
tg.run(([]{printf("consoleWrite0\n");}));
tg.run(([]{printf("consoleWrite1\n");}));
//cancel tasks tg.cancel();
//check whether tasks are being cancelled
bool taskGroupIsCanceling = tg.is_canceling();
//check on status of task group
task_group_status status = tg.wait();
if (status == completed){
printf("Tasks were completed successfully\n");
}
else if (status == canceled){
printf("Tasks were canceled during task execution\n");
}
else{
printf("An exception occured during task execution\n");
}
Combinable 类型
达到并行执行编程任务的最大利益的有效形式之一是工作在本地拷贝或是子数据上的每一个并行分支被处理,然后当处理结束后,将结果合并。这个形式可以最大限度的减少资源争用,而且减少潜在的死锁和当并行线程试图更新相同的内存位置时所发生的数据不一致的问题。
使用这个形式没有那么复杂,但是需要很乏味的为每一个使用进行手动编码。为了简化形式使用,Visual C++ 2010 Beta 1 添加了combinable模版类型。这个模版参数传递到combinable是每个任务将要操作的对象类型。这个类型必须有一个默认的构造器和一个拷贝构造器。通过使用local方法,每个任务访问它自己的combinable管理资源的拷贝。 当所有任务结束后,通过使用combine 或是 combine_each方法,你可以将所有结果合并到一个结果集中。
以下的代码用三个独立的任务添加一些元素给一个矢量,然后用两个合并的方法将结果合并成一个矢量。
//declare a combinable vector of integers
combinable< vector< int>> v;
//add an element to the vector using three separate tasks
parallel_invoke( [&]{ v.local().push_back(1); },
[&]{ v.local().push_back(2); },
[&]{ v.local().push_back(3); }
);
//merge the task-local copies using combine_each
vector< int> result1;
v.combine_each(
[&](vector< int>& local)
{
result1.insert(result1.end(),
local.begin(), local.end());
}
);
//merge the task-local copies using combine
vector< int> result2 = v.combine(
[](vector< int> left, vector< int> right)->vector< int>{
left.insert(left.end(), right.begin(), right.end());
return left;
});
要注意的是在最后一行的代码语句中的lambda表达式的明确返回类型声明的使用。在lambda表达式中的两个语句阻止编译器正确推断返回类型,所以需要手动声明。
也可以为没有默认构造器(或是在很多情况下默认构造器的使用是不合适的)的类型使用combinable,通过使用combinable构造器,它是采用了一个发生器功能创建了一些模版参数类型的对象。上面代码的前面几行是使用超载发生器构造器而重新编写的。在这种情况下,发生器功能返回已经包含一个元素的矢量。
//declare a combinable vector of integers
inable< vector< int>> v([]{return vector< int>(1, 0);});
同步
Combinable的使用取决于不需要作用在其他任务上的结果的任务。在多重任务的情况下需要分享一个对象,你必须使用更传统的同步策略。PLL本身带有三个同步原语——critical_section, reader_writer_lock 和 event。critical_section锁住内存位置防止没有锁的所有任务的访问。对于内存位置将有很多同时读取者和少数编写者,更优化的reader_writer_lock是可用的,允许多重读取者获取锁并同时访问内存位置。最后的原语是event,用于在任务和线程之间发信号。
同步原语在concrt.h标题文件(是PPL的基本标题文件,包含在ppl.h中)中被定义。在concrt.h中定义的大多数类型的目标是图书馆作者,而不是应用程序的开发者,但是任何对深层并行开发感兴趣的人都可以自由的调查和使用恰当的来自暴露在外的类型的功能。
critical_section类型的API是非常简单的;你使用一个blocking lock方法来获取这个锁,non-blocking try_lock会试图去获取这个锁,如果它可用的话,而且unlock会释放一个锁住的critical_section。
reader_writer_lock是有一点点的复杂。它添加了两个方法,lock_read 和try_lock_read,可以支持获取一个reader lock。Unlock方法和critical_section是一样的,而且将释放恰当的基于锁类型的锁。
最后的同步原语是event,这是手动复位事件(即:事件保持设置直到用外部代码进行手动复位)。代码可以通过调用实例wait方法(也是支持一个可选的timeout值),等待一个单一事件被设置。当没有timeout被定义的时候,等待的时间是无限的。你可以等待多重事件,通过使用静态wait_for_multiple,它接受C-style的event pointers。wait_for_multiple方法等待一个单一事件或是所有的事件传递到方法调用。以下的代码等待这两个事件被设置:
event* events[2];
events[0] = new event();
events[1] = new event();
parallel_invoke(
[&]{ events[0]->set(); },
[&]{ events[1]->set(); }
);
bool waitForAllEvents = true;
event::wait_for_multiple(events, 2, waitForAllEvents);
当同时执行很多任务的时候,处理状态管理是非常艰巨的工作。PPL提供对状态管理的一种形式的支持,就是每个线程在本地版本的分享对象上进行操作,在程序结束的时候将这些结果合并起来。对于分离状态管理的情况是不恰当的,PPL以critical_section, reader_writer_lock 和 event的形式提供传统的同步原语。