日文版: 1997年8月。
OEM版: 1997年9月。
韩文版: 1997年11月。
繁体版: 1997年12月。
增强版: 1997年12月。
需要说明一点:那就是我们的所有程序全部都是自己完成的,没有使用任何其他人其他公司提供给我们的代码。要知道,我们公司当时还没有能力去购买国外的游戏引擎,而我个人连SoftICE都不会使用,更不要说跟踪研究别人的代码了。我们对别人的学习方式非常简单和直接,就是观察。通过观察猜测它所使用的方法,然后考虑自己如何把它实现。这可能是我想到的最笨的一种方法,如果一个程序员能力强,在制作游戏以前,详细分析了解别的游戏的算法我想一定是非常有用的,他在制作游戏时一定可以节约不少走弯路的时间。
另外,我们对<赤壁>的测试时间也是很短的,在程序基本稳定之后,我们大概只剩下两个星期左右的时间。所以有很多Bug。
2.2 模块划分
赤壁的程序分成五个大的部分和19个模块:
显示模块
战场显示模块
分为通用显示底层和游戏战场显示。
通用显示底层是基于DirectDraw的一套函数。
有关内容请详见DDApi.h, DDCompo.h。
游戏战场显示是根据游戏单元的类型,位置,状态,动画帧等数据将单元位图以适当形式显示在战场的适当的位置上。要显示的内容有:地形,单元(士兵/建筑/将领),攻击效果,魔法效果,远程武器物体,阴影等。
主要功能有:对图素的压缩和读取,图像的显示,单元归属颜色的转换,边界剪裁,遮挡关系,缩略图显示和响应,屏幕移动,阴影遮挡判断,攻击效果,魔法效果,远程武器物体的显示和移动。
有关内容请详见CBDraw.h, 显示单元的位图
CBDrawM.h, 显示特殊效果,比如水。
CBDrawOT.h, 显示特殊物体。
CBMini.h, 显示缩略图。
CBMap.h, 图素的操作。
CBShadow.h, 阴影的计算和显示。
CBOther.h, 特殊效果的显示和计算。
界面显示模块
根据游戏设计需要,在显示器相应的位置上显示游戏的各层界面。
它分为两个部分:界面底层部分和游戏界面部分。
界面底层是属于底层部分的通用函数库。它包括按钮,对话框,滑块,检查框等界面元素的实现。
有关内容请详见DDBitmap.h, 显示位图的基类。
DDButton.h, 按钮。
DDCheck.h, 检查框。
DDList.h, 列表框。
DDMenu.h, 菜单。
DDScroll.h, 滚动条。
DDText.h, 文字。
游戏界面主要有:游戏主菜单,新游戏菜单,读取进度菜单,保存进度菜单,网络选择菜单,系统设置菜单,任务提示菜单,结束菜单,错误处理对话框等。
有关内容请详见Interfac.h, 提供基本的游戏界面接口函数。
Interfa1.h, 处理所有的按钮Button信息。
Interfa2.h, 处理所有鼠标操作发出的消息。
net_face.h, 网络部分界面。
Marco.h, 所有按钮的消息ID。
CBprompt.h, 游戏战场中屏幕右方信息的显示。
CBAarray.h, 对游戏元素的查询。
DDComUn.h, 针对游戏中下达命令时的命令组。
过场动画模块
显示公司标志,制作群,历时回顾,片头,片尾和过场动画。
它有两个部分,第一是播放视频图像,第二是调用其它进程。
有关内容请详见CBAvi.h, 视频图像的播放。
Mciapi.h, 播放AVI文件的底层函数。
VCMApi.h, 高效率的播放AVI的底层函数。
CBGame.h, 播放结尾。
Interface.h, 程序状态的转换。
单挑显示模块
武将单挑时出现的专门画面。本部分与原始设计有出入,原始设计中可以对单挑进行操作,后来删减称为只播放一段Video。有关内容请详见CBAvi.h。
控制模块
鼠标控制模块
根据鼠标的位置设置鼠标的形状,对鼠标的操作对响应单元发出命令。
主要内容有:鼠标点击检测,目的地模式,命令构造,可建造区域判定,鼠标形状转换。
鼠标点击检测,主要判断鼠标点击的位置是否在某个单元上或地形上。详见CBMouse.h。
目的地模式,主要控制鼠标选择了单元(命令主体)后,可能对单元下达的命令的模式,根据不同的命令可能需要不同的目的地类型和命令参数。详见CBCtrl.h。
命令构造,通过鼠标的点击选择或按下某个,命令按钮,构造出具有命令主体,命令ID和命令客体的命令,放到命令队列中,供执行。详见CBCtrl.h。
可建造区域判定,在建造建筑时需要判断哪里可以建造哪里不可以, 并且显示出来。详见CBBuild.h。
鼠标形状转换,鼠标移动到某个界面或某个单元上时,或处于命令构造阶段时,需要对鼠标的形状做一定的改变,以显示当前的操作状态。详见CBMouse.h。
命令处理模块
根据鼠标和人工智能发出的命令,传送给对象单元,并将其转化成为单元的相应状态序列。
根据单元状态,判断单元状况,更改单元的状态。
详见CBCtrl.h, 命令的构造保存和传递。
CBRun.h, 命令的执行。
CBRDelay.h, 命令执行时需要的一些变量。
攻击计算模块
根据敌我双方的攻防力量,计谋的实施和阵型计算每一次打击敌人生命的损失。
详见CBRun.h, 攻击计算。
CBZhenFa.h, 阵法计算。
行军控制模块
根据单元的位置,速度,目的地和地形数据,计算行军路线,设置单元的下一步。详见March_n.h。
网络控制模块
游戏数据在网络上的传递,纠错。模拟机的建立和管理。详见Network.h。
策略模块
君主策略模块
计算机一方根据战场双方的力量对比和战斗模式计算对单元的生产,对敌人的攻击,产生命令。
详见TEmperor.h。
将领策略模块
每一支部队根据将领的属性,士兵的状况调整战斗的方式或判断撤退。
详见TGeneral.h。
本能策略模块
士兵单元面对周围的情况产生固定的基本反应。
详见Tai.h。
Tbnbase.h, 人工智能中需要的数据结构。
CBEyes.h, 人工智能与游戏主体结构之间的接口。
文件模块
资料数据文件模块
地形和单元图素的图像文件,相应控制数据文件。单元的各项属性数据文件,操作用数据文件,界面位置数据文件。
详见CBData.h, 游戏中使用的单元属性全局数据结构。
CBMap.h, 游戏中使用的地图图素,单元图素全局数据结构。
CBGame.h, 游戏中使用的单元全局数据结构。
存储数据文件模块
存盘用数据,记录战场上的所有单元的状态和思考数据。
详见CBGame.h, 存盘。
其它模块
文件封装模块
为减少程序使用的文件的数量,增加程序的安全性,将大量的图像文件和数据文件封装起来,供程序调用。
详见L_Allbmp.h, L_Image.h, L_Save.h, L_Scan.h, L_Text.h, Tools.h。
声音模块
背景音乐和音效。有混音和音量控制。
详见DsWave.h, 播放WAV文件。
文字模块
在非中文系统下显示汉字,日文,繁体汉字和韩文。详见puthz.h。
地图编辑器
为使美术人员方便快捷规范地制做战场地图,提供专门的地图编辑器。同时为整个游戏的文件系统,显示系统做技术上的准备。
这是一个单独运行的程序,详见Mapedit。exe。
安装程序
将游戏安装和卸载。详见Setup。exe。
2.3 游戏引擎
说到游戏的引擎,很多人都不知道它是什么,以为制作它有多么困难。引擎的概念也很混乱,至少现在我还不知道它的确切定义。但我想如果一个东西要被称作引擎,它应该具有这样一些特点:
它应该是由函数组成的。
它应该实现某项具体功能。
它应该是完整的。
它应该可以被重新使用。
从上面的要求可以看出,其实这就是作为底层程序的要求。我想没有必要把引擎认为是游戏的现成编写工具,只要2改一下美术就是另一个游戏了。只要这些程序代码将会被我们应用在以后的游戏中,我们就已经节约了很多的时间和精力。
下面我会说一下在<赤壁>的代码里,哪些将被看作我们的引擎。实际上,这些部分经过一些修改后正在被我们应用到新的产品中。
显示底层:
这是一套包裹在DirectDraw外面的函数。为了简化在调用DirectDraw函数时的复杂度我们使用了一些缺省参数和内部错误处理函数。建立了一个CDDSurface类库,使得对位图的使用更加简单。详见DDApi.h
在DDCompo.h中我们有关于游戏鼠标的一套操作。在屏幕独占模式中,Windows标准鼠标有时显示会不正常。于是我们自己制作了鼠标的显示方法。方法很简单,在每帧读取鼠标的位置,然后在该位置上显示一张位图。
在赤壁里面,我们没有使用双缓冲区的模式,而是只更新某个特定的区域。它的优点是当需要更新哪里的时侯就更新哪里,对于哪些在每帧中都只有小面积图像需要更新时是非常高效的。比如在486上,<赤壁>的主游戏界面里的鼠标移动仍然是十分流畅的。可惜的是,在<赤壁>的战场部分,它并没有优势,因为基本上是需要全屏刷新的。
在未来的游戏制作中,因为计算机的速度越来越快,所以我们当时所使用的模式恐怕变得不太适用,双缓冲区模式应该是主流方向。
多媒体底层:
主要包括声音和视频。我们使用了MCI设备来播放AVI,WAV,MIDI,CD AUDIO等内容。那曾经是我们在做上一个游戏的时侯完成的部分。但是它有很多缺点,比如不能同时播放多个WAV文件,这对于我们制作游戏音效是很重要的内容。
所以我们又使用DirectSound来播放声音。这里的难点在于当我们需要播放很长的文件时,不能一次读入,而需要建立新的线程按时装入声音。好在现在大部分游戏都使用CD Audio作为背景音乐,不需要WAV。
界面底层:
基于显示底层之上的界面元素其实并不好做。因为我们总希望它的响应方式与Windows95中相同。而大家在<赤壁>里看到的内容就与Windows95有些不一样。比如滚动条(ScrollBar)对鼠标的响应就非常简单,按钮(Button)的反应也有所不同。但是好在它比较简单,易于使用。
在每做完一个游戏之后,我们都习惯要把某些东西整理一下,看看它是否可以在以后被使用起来。而往往这些东西也都是需要不断修改的。因为程序运行的平台不一样了,它的用途也不一样了,而我们的编程水平也不一样了。但总之这些代码被较为完整地保留了下来,它必将是我们今后编程的基石。
2.4 关键讨论
我刚开始编写游戏的时侯总有一个想法,只要游戏的主要部分写完了,游戏也就差不多了。我也遇到过一些游戏制作组的成员,他们也大都是这样的想法,认为只要把游戏的演示版拿给别人一看,然后只要再投资让美工画一些画,游戏就可以做完了。其实事情并不想想象中那样简单。
在我看来,把游戏的大概样子做完了,顶多占整个游戏的三分之一。另外的两个三分之一分别是整个游戏的制作和测试。
举个简单的例子,比如我们在演示版中通常只有一个兵种和一个战场。游戏的显示效果可能很不错。但是,真正在游戏中不会只有一个兵种的,每方都会有大概十种兵,又会有三四方的敌人,这时侯你的显示底层是否能够胜任呢?内存是否会占用太多呢?这时侯还需要我们对其进行优化和修改。连游戏的底层显示部分都可能需要修改,更何况游戏中还有更多的内容呢?
下面我举一些<赤壁>中的例子,这些都可能是极小的问题,但都是我们需要仔细考虑的问题。在你准备开始制作一个即时战略游戏之前,你是否曾经考虑过这些问题呢?假如你对这些问题有所了解,那么你就应该可以非常有把握地马上开始制作游戏。如果没有,也没有关系,因为这些问题我也没有全都事先考虑过。
假如你有时间,可以对你自己的游戏多多考虑一下,这个游戏距离一个真正的产品,到底还缺什么?还有哪些模块和部分没有做完?当你对两者之间的差距有了一个明确的认识后,也就不会担心了。任何东西都是一点一点做出来的,只要按照你想做的内容去做就可以了。
程序状态的转换
我们在写DirectX程序的时侯,总有一种偏见,那就是不希望Windows界面出现在我们的游戏里。于是什么都需要我们自己做。比如说窗口。因为窗口的刷新需要我们自己管理,就觉得没有必要生成多个窗口了。这样所有的窗口消息就必须在唯一的一个函数里实现。可是我们的游戏里有很多种不同的操作,比如界面,系统菜单,播放视频等,这些内容就都必须在这个地方处理。所以我们就引入了程序状态这个概念。我们定义了一系列的状态,在每个状态里,有固定的操作和响应,状态之间的转换也在特定的时侯进行。这样我们就很容易把一些关系不大的内容独立开来,降低程序的复杂性。
其实我们在实现这一部分的时侯是很混乱的。你很难在代码中找到所有状态转换的地方。但是它的实现很简单,一般的规则就是程序的对称性。有专门的装入函数和释放函数,然后有显示函数,计算函数,鼠标消息响应函数,热键响应函数。在内部,需要结束本状态时就发出一个状态转换的消息。在外部,只要在主程序的主循环和消息响应函数处针对不同的状态执行它们不同的函数就可以了。
这里的关键在于状态的转换。因为状态在转换中一定会释放和申请大量内存,如果有的内存没有释放,转换次数一多就会出现问题。如果我们把状态转换的地方写好了,程序看起来也非常干净整齐,Bug也会比较少。
现在编写游戏,需要装入的图量非常大,很有必要制作一个装入时的画面,并且显示百分比。这是我们可能需要一个装入中的状态。遗憾的是<赤壁>并没有实现这个,它的淡入淡出效果仅仅是效果,程序在执行这部分时停止在这里。
有关内容请详见CBGame.cpp和Interfac.cpp。
执行任务
一个士兵在接到我的命令之后,便开始了它的行动。它察看了一下它的命令,这是由两个部分组成的。它先取出第一个部分,是行走。于是根据自己现在的位置和目的地位置先计算了一下路线,把自己当前的状态设置为正在移动,然后根据计算好的方向和自己的速度把自己移动到一个位置。如果这个位置已经到达目的地了,它就停止行走。又察看了一下剩余命令的部分,是攻击。于是它拔出刀砍向附近的一个敌人。敌人死了么?它不停地问自己。如果敌人真的"哇"的一声倒下了,它就得意地站在一旁,微笑着。
这就是我们在战场的一个角落所看到的一个士兵的表现。而实事上,在程序里我们也是这样做的。
我们把每个士兵做为一个单元,独立地处理自己的事物。我们把每个命令划分得更加细致,而称其为状态,执行每个状态时所需要的参数的执行的步骤是最简化的。每个命令都是由一个或多个状态组成的。一个状态满足后就自动转向下一个状态。我们为每个状态都编写专门的开始,运行中,和结束代码。这样每个士兵都在独立地按照顺序处理自己的事务,一个个复杂的单元行动就变得有条有理了。
有关内容请详见CBRun.cpp。
阴影
有人说Warcraft做得好,有人说C&C做得好。我属于前者一派,这不仅仅因为我对Warcraft的观察多一些,而且有一个理由足以说明Warcraft在程序上比C&C更高一筹。那就是阴影。大家都知道Warcraft是双层阴影,而C&C是单层。双层阴影的好处是更加真实。在我们去过一个地方之后,虽然我已经看清楚了地形,但是这里敌人的活动,我们不应该永远知道。可是要把单层阴影变成双层,并不是只要加上一层显示就可以解决问题的。
首先,增加一层阴影就增加了需要显示的时间。时间对于即时战略来讲是至关重要的。它直接影响到游戏的表现效果和操作速度。多加一层阴影就意味着减少了我们增加效果的机会。在我们这里,双层阴影占用了大约5-10%的显示时间。
其次,增加一层阴影就增加了特殊的计算。一层阴影只需要一个二进制数组记录哪里被打开,哪里被隐藏就可以了。而现在则不同,我们需要把那些已经打开的阴影再关上。我们的做法是生成一个二维数组,每位表示一个图素格子。当有一个士兵的视野可以打开这个格子上的阴影时,就把那里的计数器加一,离开时减一。当为0时则这块地形被重新隐藏。
第三,阴影下的单元需要特殊的处理。当我的士兵离开敌人阵地的时侯,敌人附近被半透明的阴影重新覆盖了,这时我们需要把正在那里活动的敌人士兵隐藏起来,而建筑不动。
这就是双层阴影所带来的。可是为了效果,我们不得不如此。看起来还是达到了效果。
除了双层阴影以外,阴影的边界也是很重要的问题。我们不可能把阴影做得和刀切的一样,而让它必须和打开的地面相结合。于是我们必须要有一套贴图,用于阴影边界的各个方向。好在我们在这里利用了一个偷懒的办法(当然是很巧的办法,是Onefish想出来的)。我们采用了一个椭圆的贴图,让相邻的椭圆相切,从而造成边界。因为椭圆是没有方向之分的,我们也就节省了一些贴图的内存和对使用哪张贴图的复杂计算。
有关内容请详见CBShadow.cpp。
选中谁了
<赤壁>战场里的格子是菱形的。这一点有多少人发现了?又有多少人知道它的代价?或许有人认为菱形的格子并不是个很好的注意,但我们恰巧在这里使用了它。当鼠标按下时我们需要知道它放在哪个格子里了。过去的经验是只要知道该点的位置和每个格子的宽度我们就可以很快地算出格子的序号。但是在这里不行,因为格子是菱形的,在一个矩形区域里的点不能被确定它到底属于哪个格子。
怎么办呢?我制作了一个二进制二维数组,大小是格子的宽和高。在这个区域里,如果是属于矩形内接菱形区域内的点设置为1,否则为0。当鼠标在某个矩形区域里时,我就用这个栅格去判断该点的位置上的数值是1还是0。是1则这个点在这个菱形格子里,否则不在。这个办法是我遇到的比较迅速的一个办法。
命令的产生
一个由游戏者发出的命令,一般有三个部分:命令主体,命令本身和命令客体。命令主体就是执行命令的人。我想对这支部队下达命令,这支部队就是命令的主体,在游戏里就是我们用鼠标选中的一群士兵。命令本身是命令的标识。我想下达的是什么样的命令,在游戏里,可能是我按下了某个命令按钮。命令客体是命令结果的接受者,如果我选择了攻击命令之后,再选择了一群敌人,那敌人就是命令客体。
如果所有的命令都这样下达,那编起程序就简单了,玩游戏的人也会累死了。我们常用的玩法是选择了命令的主体后直接用鼠标右键点击命令客体。而命令的产生就由命令主体和命令客体的类别来确定了。
这才是需要我们考虑的东西。
首先是选择命令主体。当我们在战场上鼠标一点,或圈了一个框,然后一松手,这时侯程序就开始算了。这里我要介绍一下<赤壁>的单元类型。我们把单元分成士兵,将领和建筑。士兵又有已经组建成部队的和尚未组建成部队的。士兵的种类可以分成一般士兵,工人和船。我们的选择规则是:组队的士兵优先于建筑。将领优先于组队的士兵。未组队的一般士兵优先于将领,未组队的船优先于一般士兵,未组队的工人优先于船。另外还有特殊处理。按下Ctrl键则选中该组队士兵的将领,按下Shift键则把刚选中的士兵添加到已选中的士兵中。
其次,是选择命令的客体。命令客体可分为地点和单元。地点有普通地点和资源地点。单元又分为敌人的和我方的。单元的种类有士兵,将领和建筑。士兵分为未组队的和组队的。士兵的种类有工人和一般士兵。建筑又分为一般建筑和资源建筑。我们需要根据命令主体和客体共同来判断命令是什么。不同的组合所产生的命令可能可能是不同的。比如,如果我选择了组队的工人作为命令主体,又选择了一个建筑作为命令客体。如果这个建筑是铁矿,那么命令就是采矿。如果这个建筑是我方的建筑,则这个命令是修理。如果这个建筑是敌人的建筑,那这个命令就是攻击了。
有关内容请详见CBCtrl.cpp。
眼睛函数
在游戏策划编写人工智能的时侯,总喜欢写成这样:“当匪兵甲发现敌人就在不远处时就去攻击。”而要把这句话变成程序,还需要不少的路要走。比如匪兵甲是如何发现敌人就在附近呢?首先必须要知道匪兵甲自己在什么地方,其次匪兵甲的视野有多大,也就是说多远算作附近,第三敌人在哪里。这三样东西匪兵甲是如何知道的呢?依靠的就是眼睛函数。
一般我们在编写人工智能时都会把它作为一个比较独立的模块。因为它的主要功能就是判断。它的出口同游戏者的鼠标操作一样,发布命令。如果它的入口也很明确和简单则这部分内容的难度就非常小了。可是人工智能需要得到大量的信息作为判断的依据,而我们的主体数据结构又可能写得不很好,这时候就需要我们另外制作一套程序把游戏内核与人工智能分开。这就是眼睛函数的用处。
编写人工智能的程序员只需要写出他想知道什么,然后交给游戏核心程序员,由游戏核心程序员从游戏核心数据中找到相应的信息,传递给人工智能。这样编写人工智能的程序员就可以根本不知道游戏核心数据是什么样子就可以开始工作了。但是这也是临时的办法,
如果我们可以把核心数据结构写得风雨不透那也不必如此劳神了。
有关内容请详见CBEyes.cpp。
命令队列与网络
本以为网络编程很简单,一写下来才知道头大。虽然这部分代码不是我写的,但是我也知道跟踪一个10兆大的Log文件是什么滋味。网络上的游戏通讯一般有两个办法:交换所有信息和交换重要信息。第一种方法是随时把自己的所有数据都传递到另一端,另一端根据这些数据进行显示和操作。它的优点是不需要同步,对网络状况要求低,计算也非常简单。缺点则是数据量太大,只传递少量数据还可以接受。所以这种方法多用于RPG游戏并多在Internet上使用。第二种方法则只传递关键的数据,计算机接到这些数据后对其进行计算,因为计算的结果是唯一的,所以可以保证网络两端一致。这种方法适用于那些数据量大的游戏,但它的编写和调试就比较复杂,而且对同步的要求较高。我们的<赤壁>就是采用的这种方法(<西游记>用的是第一种方法)。
在这种网络的联接方式中,最重要的问题一个是同步,一个是结果唯一。只有同步我们才能保证在每一轮的计算中所有计算机上的初始状况是统一的,只有结果唯一我们才能保证在执行了一段时间后所有计算机上的状态是统一的。这样才能保证网络游戏的正确性。
除了初始化等必要得操作以外,我们把游戏中的命令作为关键信息在网络上传递。游戏者对战场上部队的任何操作,下达的任何命令都会被传递给其它的计算机。如果游戏者没有操作,则我们制3在每一轮循环中传递同步信号。在这里我们遇到了一个DirectPlay中的问题,那就是信号并不是每次都会正确传递到对方那里的,尽管我们在DirectPlay中使用的参数要求系统必须在确认对方收到后才返回,可是它返回后对方依然没有收到。关于这个问题我没有查看有关文档,也许有更好的解决办法。所以我们自己制作了一套校验确认的方法。
但是我们在本机上发布的命令传递到另一台计算机上时一定会有延迟,这就不能保证初始状态的一致。所以我们采用了轮回制。任何人发布的命令并不会马上被执行,而是先存储到一个命令队列中。在每次循环中对曾经下达的所有命令统一进行发送,侦听。当所有计算机都收到了所有计算机本轮要执行的命令后,所有的命令开始被顺序执行。这样在任何一台计算机上本轮中所有命令的内容和顺序都是相同的,执行时才能保证其结果的一致。
有关内容请详见CBCtrl.cpp, CBRun.cpp。
兵种的颜色
这大概是即时战略游戏里最隐蔽的一个问题了。因为不做的人不知道,而做的人都心照不宣。在我们的游戏里,不同的君主用不同的颜色代表,属于该君主的士兵则在它的衣服上赋予相应的颜色。这样虽然兵种相同但是他们的颜色不同,这样就可以区分敌我了。但是如果我为每一个兵种都制作一套图素,那么就显得有些浪费了,这会占用过多的内存(我们这里所有兵种的图素量约有10MB,如果有四种颜色就需要40MB的图素)。谁都希望找到一个好的办法节省这些内容。
我们的方法很简单,抽色。我们在调色板里的留出固定的位置,需要改变的颜色就按照顺序存储在这里。在显示时,我们会根据传递进来的君主号为每张图素的颜色进行判断,如果颜色在这些区域里就根据规则偏移。这样各种颜色的士兵就会显示出来,而只需要一份图素。
这种方法的缺点就是速度慢,毕竟需要对每个点进行判断和处理。所以这部分也是我们针对MMX优化的主要部分之一。
怎么样?奇怪么?有关内容请详见CBDraw.cpp。
遮挡
我在设计游戏开始时就考虑过单元的遮挡问题,因为这曾经是<官渡>遇到的问题之一。游戏单元在屏幕上的位置会经常有所变动,但我们必须保证一点,即下面的物体总要把上面的物体遮住。但是我发现这个问题实际上在<赤壁>里几乎不存在。因为在每一帧都是重新显示一屏内的所有信息,只要我每次从上向下画图就可以了。当然这是个极为极端的例子。我们仍然需要考虑遮挡的问题。比如,人被山挡住的问题。我们通常为了在显示函数里显得结构化一些,都是先显示地形,再显示人物。那么当人物走到了山的上部时,就需要山峰挡住人,可是这时侯山都已经显示完了,我们必须再次显示山。而显示山的时侯又不能够显示整个的山,我们必须划分出显示的区域,让它可以显示又不会遮挡别的地形。
调整这部分内容可能占据了我制作显示部分的绝大部分的时间。好在最后实现了,但我仍然在想是否有更好的办法解决这个问题。
有关内容请详见CBDraw.cpp。
行军算法
我们在制作<官渡>时没有给士兵以一套完整的行军算法,让它去寻找一条到达目的地最近的路线,而在<赤壁>里想做一下尝试。就自制了这样一套算法。首先找到一条直线,然后围绕挡住这条直线的障碍物搜索一条可以行走的路线。说起来简单,做起来可很困难。这部分代码写了大约有四五个月。
当然它也仍然有缺点,比如如果在路上的障碍物内部有空洞,那么就会卡住。但好在它的速度还算可以,一百个人同时走路时才会感觉到一点停顿。
本来我们打算在<荆州>里有所改变的,我们采用了世界上比较流行的行军算法:A*算法。并且为它专门制作了一套底层,使它可以挂在任何一个地图结构里。
有关内容请详见March_n.cpp。
为什么有数量限制
又回到了老生常谈的话题。编程序的人一看就可以知道,对士兵数量进行限止的唯一理由就是没有使用链表。的确是我胆小的缘故。我没有在游戏的核心数据结构中使用链表,原因很简单,怕出错。我为每个游戏者开了200个单元的空间,也就是说每方游戏者都可以制造200个士兵和建筑。我想这个数量应该是够了,因为如果真的每个游戏者都生产出这么多数量的单元,游戏也跑不动了。
使用数组不仅出错的机会少一些,而且它对于查错也很有帮助。在我们的早期测试版中,我让游戏每隔10轮就自动存一次盘。存盘的内容就是简单的把这些数组内的内容写到文件中。如果程序跳出,马上调出存盘很快就会跟踪出错误的位置。后来,在<赤壁>标准版发行后,也有玩家反映有异常退出的错误,我就让他们把存盘文件寄给我,我就能很快发现问题。如果使用指针链表恐怕就没那么容易了,但也有一个好处。如果玩家玩出一个错误,只要他再重新玩很可能这个错误就不出现了。
有关内容请详见CBCtrl.cpp。
镂空贴图
我估计有不少初学编游戏的人都会有这个疑问,如何让一个人出现在地形上而身上没有黑边。要知道计算机的显示一般都是以矩形为单位的,一个边界不规则的图形该如何显示呢?
假如我们按点去操作,那么就可以判断如果人的图像上是黑色则跳过去,否则显示到背景上。但是谁都知道这样是很慢的。于是我们想到了使用逻辑操作来贴图。我们有一个真人的图像,边界是黑色的,我们叫源码。然后我们制作一个掩码,人的内部是黑色的实心,外部是白色。然后我们做两次逻辑操作,先把人的掩码与背景做“与”操作,这样背景就被扣出了一个黑色的轮廓,然后把人的源码与背景作“或”操作,人就呆在背景里了。
如果你看到我们的地图编辑器,就会发现我们就是这样做的。这是<官渡>的做法,在编写编辑器时我还不会使用DirectDraw呢。但是在DirectDraw环境下,我想大家就可以省事很多了,只要一个ColorKey的变量设置,DirectDraw就可以替你完成镂空贴图的操作。
从这里你也可以看到我们也许不会一开始就把游戏做好,但只要坚持下去,我们一定会越做越好。
有关内容请详见DDApi.cpp。
2.5 希望
有人说即时战略游戏已经过时了,或者已经做到头了。我的观点截然相反。
我认为它是目前唯一能够胜任大规模集团作战的游戏类型,它所能够带给人的东西我们还远远没有做到。我是很喜欢看电影的,尤其是战争电影,那些电影里的场面和效果在游戏里出现了么?有,但是还远远不够。
我承认我们的制作能力与国外的水平还有很大的差距,但是即时战略依然会有它的发展。
比如,即时战略已经开始慢慢向战棋式的战略游戏靠拢,采用多层战略地图,多级控制的方式。这就是一个可能的发展方向。即时战略也可以不采用采矿的方式,而利用其它(比如经营)方式来获得资源,这也是一个可能的发展方向。
我总是觉得,制作游戏就是在创造世界,虽然我们的这个世界还无法与真实的世界相比。但是,只要我们做了,就会有进步,就会离梦想近一点,也许我等不到那一天,可是也许某个看到我的文章的人等得到那一天,这我已经很高兴了。制作游戏的路刚刚开始,对于我们而言,我们希望能够继续做下去,可是上帝毕竟是广大的玩家,如果大家不认可,那我们还是趁早改行为好。但我总是有些不死心,总希望能够有人坚持下来,如果这个人不是我。我真心地希望能有更多的人了解游戏的制作,了解制作游戏的人,也希望有更多的人加入到这个充满了艰辛的事业来。因为美好的明天需要我们去创造,我们为游戏付出了很多,它也会同样地回报我们的。
以上内容仅是我个人的观点,如果有任何异议请与我联系,但是我不会与各位争论什么,因为只有做才是最重要的。如果有什么问题,也可以来信,但是我可能不会很快或很详细地回答,因为我现在正忙于制作新的游戏。
我的E-mail地址是liu-gang@263。net。
假如你已经整装待发,那么就开始吧,不要犹豫,人的一生只有一次,只要去生活就是了。