网络游戏综合编程(上)
技术与开发
前面几期我们分别讲解了C++语言、数据结构与算法、Windows程序开发、DirectX、网络编程。在最后这两期,我们将会把以上的知识串起来完成一个简单的网络游戏,希望大家能跟我一起做,通过实战练习,大家一定能巩固以前学到的知识,也更能加深对网游开发的认识。当然,由于篇幅限制,我们不可能给出每一行代码和作出太详细的讲解,只会给出一些伪代码和开发的思路,大家在明白了这些开发思路后就可以自己动手试一试。
我们的游戏是一个非常小型的2D MMORPG游戏,只实现多个游戏角色在同一个游戏场景里走动。程序分为游戏客户端与游戏服务器两部分。这一期我们只实现单机部分的功能
创建游戏地图
在创建场景之前首先我们要弄清楚场景究竟是由哪些东西组成的。简单的讲,场景通常由地面(如草地、泥路),物体(如树木、房子)和生物(如人类、动物、怪兽)所组成。通常把场景分成若干单元格,方便于场景的管理。例如可以把整个场景分成若干个(32×32)像素的单元格。每个单元格就是场景中的一个坐标。
一些早期的游戏,如《仙剑奇侠传》、《暗黑破坏神》,游戏的地图是由许多的Tile所组成的,图1是一个草地的Tile。

图2是由Tile所组成的一个游戏地图:

这种地图的优点很明显,它所占的资源很小;但缺点也很明显,由上图可以看出来,由Tile所拼成的地面看起来比较呆板。最近几年,由于计算机硬件发展很快,玩家也不在乎那么一点资源了。如果大家玩过《轩辕剑》系列,那么就肯定留意到自从《轩辕剑》第三集以来,它的2D画面绚丽了许多。这是由于游戏地图不是由一块块的Tile所组成的,而是一幅大图片。这种大图片的缺点是资源占用会比较大,而优点是画面可以做得很漂亮,美术人员有很大的空间去发挥。现在的2D网络游戏很多都是采用整幅图像作为地图,如《梦幻西游》(如图3)。

读者要使用哪种形式的地图就取决于个人的爱好了。无论是采用哪种形式的地图,都要把地图以二维坐标分成X×Y个单元格。每个单元格需要储存一些数据,如Tile的ID号,是否为障碍物,物体(如树木,房子)的ID等。地图数据通常以文件的形式储存,游戏开发者一般都会开发一个地图编辑器来编辑地图数据。
让游戏动起来——游戏引擎
所谓“麻雀虽小、五脏俱全”,我们的游戏虽然只是最简单的MMORPG游戏,但同样要拥有游戏引擎、状态机等开发的要素。正是因为具备了这些要素,我们的游戏程序才有了强大的运行动力,才能“动起来”。下面,就让我们先来认识一下它们。
整个游戏的流程如图4所示。

游戏的主要工作都在运行场景这部分完成,所以像游戏中场景的调入和人物的走动等都在这部分工作。而游戏的核心,游戏引擎工作的地方也在这里。那什么是游戏引擎呢?
小知识:游戏引擎
游戏引擎是游戏开发中一个有趣的概念。大家知道汽车有引擎、摩托有引擎。所以简单地讲,游戏的引擎和它们类似,也是为了让游戏跑起来和动起来的东西。而且,游戏引擎还应该是一个可以复用的事物,就像汽车引擎可以装在不同的汽车上使用一样。我们的RPG游戏虽然简单,但同样也会有一个简单的游戏引擎。
我们游戏引擎类CEngine的结构如图5。

控制游戏运行——状态机
什么是状态机呢?它在游戏开发中有什么作用呢?
“状态机”(也叫有限状态机,有限是指状态有限)简单讲在这里就是参照人的特性,使游戏在不同的情况下做出不同的行为。好比人在累的时候就会睡觉,肚子饿的时候就会吃东西。这也是实现人工智能最简单的原理。
状态机用于游戏中,就是把一个游戏当成一台状态机,根据游戏中不同的状态来调用不同的一个或多个函数,从而实现不同的功能。如图4,游戏有三个状态:GAMESTATE_BEGIN、GAMESTATE_RUN、GAMESTATE_EXIT,分别调用游戏的开始界面、运行场景(这是游戏的核心)、结束游戏三个函数。一个简单的状态机结构如图6。

那我们如何在程序中实现状态机呢?请看下面简单的示例:
enum GAMESTATE{ GAMESTATE_BEGIN, GAMESTATE_RUN, GAMESTATE_EXIT };
GAMESTATE GameState;
Switch(GameState)
{
case GAMESTATE_BEGIN: //处于游戏开始界面状态
开始界面;
break;
case GAMESTATE_RUN: //处于运行场景状态
运行场景;
break;
case GAMESTATE_EXIT: //处于结束游戏状态
结束游戏;
break;
}
那么如何去改变游戏的状态呢?这很简单,一开始把游戏状态设为GAMESTATE_BEGIN,那样就会进入游戏的开始界面;在游戏的开始界面点击“开始游戏”时就把游戏状态改为GAMESTATE_RUN,这时就进入游戏的正题了;点击“退出游戏”时游戏状态设为GAMESTATE_EXIT。
创造游戏角色
我们先来看一幅图片(图7),图片把游戏角色行走动画的每个方向的每一帧都组织起来。这一幅图一共有八行人物,从上到下的八行我们分别编号为0到7,对应的角色走动的方向分别是:上方,右上方,右方,右下方,下方,左下方,左方,左上方。

我们的角色控制方式是按住鼠标的右键角色就往鼠标的方向走去。
前面为大家讲解了状态机的概念,状态机用在角色状态上就更直观了。角色可以有很多种状态,如处于“被敌人攻击”状态就会进行反攻或逃走,处于“肚子饿”状态就会去找食物或饿死,处于“生气”状态就会摆出生气的样子等等。这些都可以利用状态机设置合理的多种状态来实现。
当然这里不需要那么多的状态,只要简单的“站立”和“走动”两个状态就行了,示例如下:
enum ROLESTATE{ ROLESTATE_STAND, ROLESTATE_MOVE };
我们可以使用函数用于检测角色现在处于的状态,它的实现方法很简单:当鼠标的右键按下时角色便处于“走动”状态,否则角色处于“站立”状态。添加一个角色状态机,它用于实现角色的状态机控制。在刚开始运行时确定角色初始的状态,然后就根据角色每个时刻的状态执行相应的行为。
滚屏技术
除非一个游戏场景的大小刚好和你的屏幕一般大,否则在人物走动中必然要用到滚屏技术。那滚屏是什么?又如何利用呢?
大家在玩游戏的时候有没有发现游戏的主角一直都在屏幕的中间舞动脚步,而场景在不停地动,看上去就好像是主角在场景中走动一样。其实游戏的主角完全没有走动过,动的只是场景,这也可以说是相对论的应用吧。我们可以想象成摄影师拿着摄影机一直跟着主角跑。这种滚动屏幕从而相对的实现人物走动的技术就是滚屏技术,它是根据主角的位置来决定应在屏幕上显示场景中的某个部分(如图8)。

图8就是一个例子,假设图中黑色方框才是屏幕能显示的大小,那如何让人物从场景的左上角走到场景的中央呢?请看下面的示例:
设屏幕的宽度和高度分别为:
const int SCREEN_WIDTH = 800; //屏幕的宽度
const int SCREEN_HEIGHT = 600; //屏幕的高度
主角的坐标为(RoleX , RoleY)。
RECT rt;
rt.left = RoleX – SCREEN_WIDTH/2; //让角色的X坐标在屏幕中间
rt.top = RoleY – SCREEN_HEIGHT/2; //让角色的Y坐标在屏幕中间
rt.right = rt.left + SCREEN_WIDTH;
rt.bottom = rt.top + SCREEN_HEIGHT;
上面的代码就实现了屏幕相对角色的移动。我们只要在角色走动的时候进行以上的计算就可以方便地实现滚屏了。