引言:
网络游戏作为一种复杂度较高的软件,从其设计角度还是有一些共性的,比方说几乎所有的网游都会有一个主循环。由于游戏需要根据输入、游戏内状态的改变来不间断地更新游戏画面,所以游戏的主循环往往看起来像一个“死循环”,那么这个“死循环”是如何工作的?
主循环主要做什么?
1.处理游戏逻辑(输入、AI、事件处理)
2.执行渲染操作(更新游戏画面)
注:对于事件处理,可以归入游戏逻辑一类中,但是在实际中为了处理的方便,往往将事件处理独立出来。(windows环境下)
一个简单的主循环如下:
- int WinMain()
- {
- bool game_is_running = true;
- while( game_is_running )
- {
- ProcessMessage(); ///< 事件处理
- GameMain(); ///< 主循环
- }
- return 0;
- }
- int GameMain()
- {
- update_game(); ///< 更新游戏逻辑
- render_game(); ///< 执行渲染操作
- return 0;
- }
如果有必要,可以长期在GameMain中循环,而不返回到WinMain中,但是这样做游戏内将不会响应任何输入,因为控制权被GameMain()霸占了,而ProcessMessage()将没有机会执行。正常的情况应当是每次我们执行完一次游戏逻辑和渲染操作后,将程序的控制权交回WinMain,由Windows负责处理输入消息。
游戏速度
上面展示的是最简单的主循环,在真正的设计中,有一个因素不得不考虑,就是游戏的速度。游戏通常以1秒钟画面渲染的帧数(即FPS,Frame per second)来控制游戏速度,一般来说帧数在25帧以上时,肉眼不会察觉游戏帧与帧之间的切换,可以简单的将帧数理解为每秒钟render_game()被调用的次数。
一部分单机游戏会追求极致的显示效果,对于帧数的要求也更高(对显卡的要求也更好-_-!);而网络游戏由于在同一显示范围内(有些游戏会有类似9屏的概念,即将地图划分为若干个方格,以玩家为中心,服务器只向玩家下发周围9个格子内其他玩家或是AI的信息)需要实时更新更多的物体,加上网游对于硬件的低门槛,注定无法一味的追求高帧率。
如果将GameMain()至于循环中放任不管,帧率会居高不下,与此同时机器的CPU使用率高居不下。而如此此时运行其他的大型软件,如虚拟机等,又会发现游戏内的帧率会有很大程度的波动,这样波动的帧率会带来很多潜在的问题。无论开发者还是玩家当然希望游戏速度尽量保持稳定,于是产生了一种固定帧率的做法。
- const int FRAMES_PER_SECOND = 50; ///< FPS:50
- const int SKIP_TICKS = 1000 / FRAMES_PER_SECOND;
- DWORD next_game_tick = GetTickCount();
- int sleep_time = 0;
- bool game_is_running = true;
- int WinMain()
- {
- while( game_is_running )
- {
- ProcessMessage(); ///< 处理网络消息
- GameMain(); ///< 主循环
- next_game_tick += SKIP_TICKS;
- sleep_time = next_game_tick - GetTickCount();
- if( sleep_time >= 0 )
- {
- Sleep( sleep_time ); ///< 出让CPU的控制权
- }
- }
- return 0;
- }
- int GameMain()
- {
- update_game(); ///< 更新游戏逻辑
- render_game(); ///< 执行渲染操作
- return 0;
- }
总结
第二种方案比起第一种方案对于CPU的使用率高效了很多,但在这种方案中游戏逻辑与渲染操作是同时进行的,在大多数情况下游戏逻辑更新频率超过一定数值(比方说50帧),玩家已经区分不出如此细微的变化,而对于竞速类、射击类的游戏更高的帧率显然在表现高速移动的物体会带来更好的体验。
于是会有将游戏逻辑与渲染分离的做法,简单的做法是将render_game()移动到循环之外,这样对于CPU更好的机器,便可以跑出更高的帧率。而更为通用的做法是将渲染操作独立为一个线程,这样做最大的好处逻辑线程与渲染线程其中一个线程发生阻塞不会影响到另一个线程的正常运行。与此同时,程序中也需要加入线程间的同步机制,程序实现的复杂度相比单一线程复杂了很多。