人气 312

[游戏程序] Q3 MOD制作入门教程 [复制链接]

九艺网 2017-3-10 17:01:40

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?注册

x
  
来自:www.q3acn.com

0. 前言

"blah, blah, blah..."

各位quaker晚上好,白天也好,这里是q3acn.com的article栏目,这次为你准备的内容是mod制作入门教程,在屏幕的右方你可以看到目录,其中第1节是给还没有自己编译过q3游戏部分源代码的朋友看的,第2节和第3节是游戏源代码的一些入门知识,就算你不打算做mod,只是对quake3的运行机制有兴趣,这两节也很适合你,最后两节我们就要自己动手做两个有趣的功能,不过如果你直接从第4或第5节开始看的话,有些概念可能会不大明白,建议你还是按部就班的依次阅读下去。

整篇教程比较长,你可以需要把它们保存到硬盘上离线浏览,里面有几张图片,你要选择全部html或者是mht。

在解释或分析源代码的过程中,我会尽量不直接说,这个是什么意思,而那个又是干什么用的,我会尽量把我如何知道它们的含义和用途的这个过程也写出来,一方面是为了你以后解决自己的问题提供经验,另一方面,即使你不打算继续研究这些代码,我也希望这篇入门教程能给你带来多一些DIY的乐趣。

可能你对编程不熟悉,甚至对编程一点概念没有,请不要畏缩,虽然对这些源代码进行修改需要一定的编程经验,和一段时间c语言的学习,但是如果只是阅读和理解它们,frag的经验远比编程经验有用,给一个没打过quake的编程高手这套源代码,他理解这些代码的速度绝对比你:一个quake老兵同时也是编程新手,要慢得多。在头几节对于出现C语言的地方和VC的一些技巧做出了一些解释和说明,当然,只是为了让你读起来比较流畅,想仔细研究代码和学习VC的话,你需要专门的教科书,注意,你不需要任何关于c++的知识,尽管VC又被称为VC++。

Disclaimer & Copyright

1、既然你已经点击了这篇文章,说明你对mod制作抱有兴趣,有必要在一开始说明,制作出来的mod只能被免费的通过网络发放,而不能被用于任何商业用途,这是你在安装源代码的过程中必然要同意的条款,而不只是quake社区乌托邦式的理想。(我知道进来看的人都是quaker,跟你说这个有点不尊重你,但我在生活中的确遇到过这样的人,跟我说“既然地图编辑器,游戏源代码都公开了,那做几幅地图,做几把新的枪,不就可以当任务版卖钱了嘛”,.............他们都是不打quake的,不要跟他们一般见识)

2、作者已经尽了最大努力保证文章中信息的正确性和准确性,但如果在阅读本文或依照本文进行操作的过程中造成了对你的软件/硬件/精神上的损害,与作者无关。

3、你可以在不做任何改动并注明原始出处(www.q3acn.com)和作者(ed9er)的前提下任意在网上转载全文或引用或文字图片,无须通知作者,但作者不允许传统媒体进行上述行为。








1. 运行你的第一个MOD

"Ease of use is damn important." —— John Carmack

1.1. 安装源代码

先准备好Q3_1.29h_src_fixed.zip这个文件(1.26M),这个包里我已经把原来1.29h源代码的一些编译错误改正了,另外添加了几个.bat;为了消除版本不一致可能带来的问题,你还需要有quake3 1.31。

你必须把zip中的所有文件解压到某个盘的根目录下的quake3目录,在这篇文章里我们用D:\quake3,如果你的quake3路径本身就是某个盘的根目录下的quake3目录,那么建议你不要解到这个目录下,换一个盘。

1.2. 生成qvm

你只需要简单的执行\quake3\code下的那几个bat文件,就可以生成相应的qvm文件,其中makeall.bat是生成所有三个模块的qvm文件,你可以现在就双击它,等它运行结束后,你就可以在\quake3\baseq3\vm目录下找到那些qvm文件,baseq3这个目录是q3asm.exe来建立的,我们无法更改它的名字,这也是为什么建议不要解压到quake3的运行路径。

下面我们用1.17源代码的readme里写的慢速火箭弹来做例子试试在quake3里运行我们的mod。

到game\g_missile.c的649行,把900改成300,存盘,运行code\game.bat,生成qagame.qvm,这时我们要把这个文件打包成zip:直接在\quake3\baseq3\vm这个文件夹的图标上用右键菜单来生成vm.zip,这样可以方便的保存文件的路径信息(和你的winzip的设置有关系,有可能你需要在baseq3目录上生成zip,反正最后你要确定在用winzip打开这个文件时在qagame.qvm这行的path列可以看到"vm\"),然后把vm.zip改名为vm.pk3,到你的quake3运行路径下建立mymod目录,把这个pk3拷贝过去,然后运行quake3.exe +set fs_game mymod +map q3dm17,然后吃RL,开炮。

(如果你运行的结果和预期不一致,火箭弹还是以常速飞行,请检查以上步骤,尤其是zip内目录的信息)

1.3. 用dll来实现mod。

quake对mod的支持走过了一条还算比较曲折的道路,在quake1的时候,用的是著名的quake c,其实就是个复杂的脚本解释器,在97年左右的电脑商情报上曾有比较详细的讲解,carmack本人对于众多玩家通过这个简陋的qc来开始编程之路既感到自豪又感到惭愧,于是到了quake2的时候,通过若干个.plan与玩家进行讨论后,carmack直接采用了windows的dll机制来分离引擎与游戏代码,这样就局限了quake2的mod的跨平台性,在quake3的开发过程中,java逐渐火了起来,carmack也曾流露出直接采用java/jvm来分离引擎与游戏代码的想法,但由于java的诸多不足,使得他最后放弃java,不厌其烦的自己实现了qvm,具体可参见:A chat with John Carmack,

qvm是quake virtual machine的缩写,carmack采用了lcc编译器并做了一些修改,由lcc编译生成.asm,然后q3asm.exe这些.asm再进行一次处理,生成字节码,这些字节码会被quake3.exe在运行时转换成本地的机器码,这样既可以让玩家通过标准的C语言来编写MOD,同时又可以做到跨平台。而如果你需要使用到windows提供的各种系统调用或者是其他一些机制,qvm就无能为力了,但是,你仍然可以通过dll来实现,quake3在初始化的时候会在当前fs_game目录下寻找相应的dll,如果找不到,再去寻找qvm,但有一个很重要的区别,就是qvm可以打包在pk3文件中,而dll不能打包到pk3中。(看一下bg_lib.c,这个文件里实现了很多但不是全部的标准c函数,如果你使用了额外的函数,那么为了编译成qvm,你必须在这个文件里给出你的实现,如果是平台相关的函数,我就建议你不要做qvm的打算了,因为quake3支持的平台太多了,你不大可能拥有iMac, irix等等来测试并实现你的平台相关函数)

如果你机器上安装了VC6的话,你可以直接打开\quake3\code\quake3.dsw,在编译和运行之前你需要更改一下Project Setting,如下面两图,e:\quake3是我机器上的quake3运行路径,你需要把它改成你的quake3运行路径,quake3_1.31.exe是我用的exe的名字,你也需要更改,完成后设置game为活动项目,然后按F5就可以直接进入我们刚才改好的慢速火箭弹模式了;如果你需要在程序中设置断点,或者是进行单步执行,查看变量等debug动作的话,你需要把mymod/q3config.cfg里的r_fullscreen改为0,r_mode改成2,另外,不要通过VC的结束调试命令(Shift+F5)来强行终止quake3,这样会使你的系统不稳定,每次都用quake3的菜单或者是/quit来退出。



注意:当sv_pure为1的时候quake3只允许载入pk3文件以及.cfg、.menu文件,所以当你使用dll来调试的时候,一定要把sv_pure设为0,否则quake3会在你mod目录下寻找包含qvm的pk3并使用qvm,如果没有找到,quake3会使用baseq3下的缺省的qvm,当你最终发布的时候,再编译成qvm并打包成pk3。

如果你机器上没有安装VC6,并且你决心制作MOD或者是仔细研究q3游戏源代码的话,我强烈建议你装上它,而不要只是用UltraEdit和.bat,一方面是VC6强大的browse功能可以让你迅速查找变量、结构、函数的定义及调用关系等等,更重要的,你可以真正进入debug状态来运行代码,这在qvm是不可能的。

1.4. 如何从MOD菜单LOAD我们的mymod

首先,这个MOD的子目录下一定要有至少一个pk3文件或者qagamex86.dll,否则quake3不认为这是一个合法的MOD子目录,在MOD菜单中根本就看不到,这也是为什么我们在前面把vm\qagame.qvm打包的原因,你也可以不打包,直接在mymod下再建立一个vm目录,把qagame.qvm拷贝进去,然后再拷贝一个打过包的以.pk3结尾的地图文件进mymod,这样也可以工作,但是这样在sv_pure 1的情况下可能会有问题,还是建议你把qvm老老实实打个包。

然后,在MOD菜单中你就可以看到一栏"mymod",这是我们的目录名,但不是我们的MOD描述,我们需要在mymod下新建一个文本文件description.txt,里面只写一行:"My First Mod",这行文字就会显示在MOD菜单中,当然你也可以用"^1My First Mod^7"来显示红色的字,注意,description.txt不能打包到pk3中。






2. 代码的框架结构

"Discover YOUR World" —— Discovery Channel

我们先浏览一下它的目录结构:


\quake3\binnt:编译源代码所需的可执行文件lcc.exe, q3asm.exe
\quake3\source\lcc\bin:编译源代码所需的可执行文件cpp.exe, rcc.exe
\quake3\ui:菜单的定义,你可以用文本编辑器打开那些.menu文件看个究竟


\quake3\code\cgame:客户端(cgame: client game)的源代码
\quake3\code\game: 服务器端(game)的源代码
\quake3\code\q3_ui:用户界面(ui: user interface)的源代码
\quake3\code\ui: TA的新增的界面功能的源代码

code下面这四个子目录下面各有一个dsp,和.bat,那么,我们就可以从这整个代码里编译出四个独立的模块(dll或者qvm),它们分别是:cgame、game、q3_ui和ui,在这里,id把名字弄的比较混乱,ui目录下是TeamArena里新增加的界面功能,而q3_ui目录下才是quake3的界面功能,ui目录下编译可以得到uix86_new.dll和ui.qvm,q3_ui下编译完成后可以得到uix86.dll或者q3_ui.qvm,dll的文件名是正常的,而如果你编译q3_ui目录下的东西并得到了q3_ui.qvm的话,你必须把它改名为ui.qvm才可以正常使用;这些在那几个bat里已经都处理了,你不用操心了。

ui目录我估计没人会用得到,我也一行没看过,我们来看q3_ui目录,这里面的.c文件全部都以ui加下划线开头,仔细观察一下他们的名字,你很容易就会联想到你在游戏中的菜单操作,譬如:ui_addbots.c——添加bot,ui_controls2.c——操作设置,ui_cinematics.c——选择观看过场动画,等等,随便打开一个你都可以看到一大把menu什么的结构,和UI_XXXMenu_Init之类的函数,至此,你可以很有信心的说,这个目录下面的程序负责了(也只负责)所有菜单的显示,选择,反馈。

然后我们来看cgame目录,这个目录下面文件都以cg加下划线开头,在这里我们光看文件名就看不出个所以然了,必须去看文件的内容,主要注意函数的名称就可以了,在这里只能简单的跟你说它控制了客户端HUD、比分牌的显示,以及与game的协调,对玩家移动的预测(这也是为什么一个LAG以后玩家会觉得被往后拽了一下),model的载入,音效的控制,等等。

最后是最庞大也最重要的game目录,一开始是一些以ai加下划线开头的文件,它们是关于BOT的控制,然后是bg加下划线开头的文件,它们是同时会被cgame使用到的文件(bg: both game),然后是g加下划线开头的文件,它们定义了这个世界的运行规则(quake3.exe负责把这个世界展现到你的显示器上),最后是几个以q加下划线开头的文件,它们是各个模块(包括quake3.exe和q3radiant)在编译时都要用到的文件,这几个文件你永远不需要也不能去改动它们。

至此,你至少知道要改某样东西需要从哪里下手了,下面我们来仔细看一下game模块。

首先一个最根本的问题,编译完之后得到的这个qagamex96.dll有哪些函数供quake3.exe调用,会在什么时候被调用?一个解决方法是在命令行用VC自带的工具dumpbin /exports来查看;另一个办法就是在源代码里找编译dll时指定导出函数的关键字:dllexport,你会发现找不到,他们把导出函数的定义放到了game.def文件里面,打开看就可以看到了;最后一个办法就是查看源文件里函数的调用关系,找到最顶上的那个,也就是在game模块里不被任何函数调用的函数,应该就是由quake3.exe来调用的了,通过VC的browse功能可以很方便的展开被调用树,你会发现几乎所有函数最后都归结到一个叫vmMain的函数,这和我们在game.def里面找到的是一致的,它在g_main.c的183行。(如果你真的去打开game.def文件看了的话,你会知道另外还有一个dllEntry,马上会谈到它)

在vmMain的注释里写的很清楚:这是唯一一个进入子模块的入口。它的第一个参数叫command,看下函数体就可以发现,这个参数指明了quake3.exe因为什么样的原因来调用子模块:GAME_INIT(初始化)、GAME_SHUTDOWN(结束)、GAME_CLIENT_CONNECT(有玩家连入)、GAME_CLIENT_DISCONNECT(玩家离开)、GAME_RUN_FRAME(一个server祯开始)、GAME_CONSOLE_COMMAND(控制台命令),等等。至于在每一种情况下这个game模块都干了些什么,就需要你跟着那些函数一层层进去看了。

好,我们再使用VC的Browse功能来查看函数的调用树(刚才是被调用树),你会发现所有函数最后都终止在一些sprintf等很简单的函数,以及,许多以trap_开头的函数,查看这些trap_函数的定义,它们在g_syscalls.c里面,这个文件一开始定义了一个函数指针syscall,并在dllEntry里对它进行赋值,在trap_函数里使用它,也就是说,quake3.exe负责调用dllEntry,告诉game模块它提供的函数的地址,然后game模块就可以使用这个函数(syscall,一个函数指针)取得quake3.exe提供的功能,这些功能是什么呢?

我们知道quake3的pk3文件其实就是zip文件,那么如果我们需要读取这个pk3文件中的一副贴图或者是一个wav,我们该怎么办?先解压?不用,quake3.exe内已经实现了透明的zip文件读取,并维护着一个很大的目录树,你只需要调用两个trap函数就可以读取你需要的文件,在pk3中的文件!它们是trap_FS_FOpenFile和trap_FS_Read。

再譬如,在cgame中(在game里我找不到好的例子了,cgame也有同样的函数调用机制),我们需要播放一段wav,你该不会去用windows的MCI函数来控制声卡吧,我们有trap_S_StartLocalSound。

类似地,若干个trap函数,这些函数都是三无产品:没有文档没有注释没有实现,它们都隐藏在quake3.exe内部,对于某一个特定的trap函数,如果你要弄清楚它的调用格式,以及它完成的功能,你只能看它在已有的代码中是如何被调用的,然后模仿现有的格式来使用它,千万不要妄加揣测自行其道。这里有玩家制作的一部分trap函数的文档:Q3 Documentation Project,还没有全部完成,你如果觉得你吃透了某个trap函数的用法,也可以去添一笔。

至此,知道了入口和出口,我们知道我们可以干什么了,譬如,我们无法直接通过网络传输数据,因为没有这样的trap函数,我们只能利用quake3.exe内部的传输机制(譬如server command或config string,等等,这是个很大的话题,有的在下面的内容里会讲到,但主要还是*你自己去探索)。

OK,告一段落,弄清楚模块划分和vmMain、trap_之后,你在看代码的时候就会有点方向感了。

还有,在代码中你会经常碰到:


#ifdef MISSIONPACK

凡是看到这个东西,那都是TeamArena用的,把中间的部分略过,直到#endif或者#else,并且你要注意别把你添加的东西放到了这些ifdef内部。







3. Hello, Quake World!

"Things will be done when they are done, and they should be pretty good. :)" —— John Carmack

在开始这一节之前,建议你把目前整套代码拷贝一份出来,如果改了900为300,再改回去,这样做有两个目的,一是为了你以后可以方便的从一套干净原始的代码的基础上开始修改,二是你可以保留这份代码专门做学习用,在里面添加注释,写上自己的理解,等等。

3.1 如何在屏幕上打印文字

我们马上要做的事情是在屏幕的中央打印出"Hello, Quake World!"这行字,你可能立即会想到,en...肯定有个trap函数可以往屏幕上写东西,对,一点不错,但是考虑一下有没有现成的封装的更简便函数供我们使用呢?回想一下我们在游戏中的时候会遇到哪些出现屏幕中央的信息,en...FIGHT! 好,在code目录下查找含FIGHT的文件,用VC的Find in files,选中搜索子目录,选中区分大小写和匹配全字,搜索结果只有一个:


cgame\cg_servercmds.c(445): CG_CenterPrint( "FIGHT!", 120, GIANTCHAR_WIDTH*2 );

幸福啊,我们只要替换下参数就可以打印需要的东西了,这时你不妨看一下CG_CenterPrint的实现,你就会发现它并没有直接使用trap,它只是把你要打印的东西放到了一个叫cg的结构变量的centerPrint成员变量里面,而cg是一个全局变量,那我们继续查找cg.centerPrint,这次可以找到3个,头两个就是在CG_CenterPrint里面,我们已经看到过它们了,而第三个在1837行,双击这个查找结果,我们就到了CG_DrawCenterString函数体内,一层层进去看一下吧,最后到了CG_DrawChar函数体内终于使用了trap做真正的输出,现在你该不会还想自己用trap来做了吧。

那么,我们在什么时候打印这行字呢?比较常规的办法是通过控制台命令来开始打印,(你也可以在开枪的时候打印,在看完这一节后结合你已经知道的哪个函数是发射rocket,你就可以自己完成)。我们都知道在控制台的输入命令需要以正斜杠或反斜杠开头,这些命令又分为两大类:cvar的变量名和控制台命令,而cvar又分为server端的cvar和client端的cvar,譬如g_gametype就是server端的,而cg_drawGun就是client端的,他们分别可以在g_main.c和cg_main.c里找到,还有一类cvar,是由quake3.exe来创建的,譬如r_mode,你可以在子模块中设置它的值,(尝试在code目录开始查找r_mode,你可以学习到一个trap函数的用法),这类cvar我们称之为internal cvar;控制台命令也可以做game/cgame这样的划分,也有一些命令是由quake3.exe来负责执行的,譬如vid_restart(尝试查找vid_restart,你可以学习到另一个trap函数的用法),这部分命令我们称之为internal command。

3.2 cvar和command

在这里我想费点口舌讨论下quake3是如何管理这些cvar和命令的,我不知道确切答案,但从quake2的源代码和我们现在手上的1.29h的代码是可以做出一些推测的,当然,推测不一定正确,但至少和程序的表现一致。

quake3.exe在初始化时会负责创建和初始化它内部的internal cvar和internal command,cvar结构中包含了它的名称,缺省值,属性,internal command的结构中包含了命令名称和它们对应的函数,然后quake3.exe会依次载入ui、cgame、game模块(具体顺序我不知道),在ui模块的初始化过程中,它向quake3.exe注册它模块自己的cvar,同样,在cgame模块的初始化过程中,它也要向quake3.exe注册它自己的cvar(cg_main.c/Ln308),game模块也一样,在g_main.c/Ln319。

好,这时quake3.exe就知道了所有cvar的信息,当我们在控制台敲入cvar的名字时,quake3.exe就可以找到这个cvar的值,以及它的缺省值,显示出来给你看。在这里你可能会有疑问,我在quake3的控制台中敲命令更改cvar的值后,更改的应该是由quake3.exe维护的数据,那它们是如何被反映到game/cgame子模块中的呢?也就是说game/cgame里的程序怎么知道玩家对这个值做了更改呢?这个问题就留给你自己去解决了,(提示:GAME_RUN_FRAME和CG_DRAW_ACTIVE_FRAME时的vmMain)。

而控制台命令有所不同,当你敲入的字符串不是cvar时,整个过程比较复杂,我在这里画了张流程图,你可以看一下,里面有几个地方要说明,一是playing server的意思,它就是你在非dedicated的quake3里敲map q3dm17得到的那种server,一般大家在局域网上打着玩都是开这种server; 二是5、7、8这三步它们所能解释的命令列表,你可以根据图里的vmMain的参数到程序里找到, 我在这里把它们给出来,但还是希望你自己从vmMain开始寻找它们:5: cg_consolecmds.c/Ln433,7: g_svcmds/Ln398,8: g_cmds.c/Ln1578;三是在第2步中要注意"是否可执行"这个判断,在一个dedicated server上运行vid_restart显然是不可执行的。



在这里,我们把第5步能解释的命令称为cgame console command,第7步能解释的称为game console command,第8步的称为client command,我们可以画出这样的一张表:



你可能会问,对于一个dedicated server,类似/callvote这样的client command有什么意义?在game console command处理的最后,我们可以看到,如果是dedicated server,那么所有不能识别的命令都会被作为server的广播信息发送出去,并返回true,也就是说,不再会被当作client command查找。

直到现在,一切都还很清晰,但是且慢,看到有个文件叫cg_servercmds.c没有,我们已经知道了console command和client command,可什么是server command啊?来,我们把它打开来看,用VC的Browse的函数列表功能,我们可以看到,这个文件的关键所在应该是一个叫CG_ServerCommand的函数,到它的函数体里面一看,en,又冒出一把命令,其中有一个叫cp的,它唯一做的事情就是调用CG_CenterPrint,哈哈哈,白忙乎啦,原来已经有在屏幕正中打字符的命令了,好,打开quake3,进入地图,进入控制台,敲"\cp hello",没有反应,敲"\cp",还是没有反应,但是也不显示unknown cmd,奇怪啊奇怪,好,退出quake3,继续看代码。

我们来看CG_ServerCommand在什么时候会被调用,看它的被调用树,当然最后都是vmMain了,但在下面一级,我们看到是CG_DrawActiveFrame,这个函数是在vmMain(CG_DRAW_ACTIVE_FRAME)的时候被调用的,也就是说,这些所谓的server command是在画每一祯画面的时候被执行的,和我们从console输入的东西一点关系没有,那么,刚才我们敲"/cp hello"的时候自然也就不会有东西输出到屏幕上。

那么,这些命令是从哪里来呢?根据server command这个名字,我们觉得应该是由game模块来传输给cgame模块的,到code/game目录下查找cp,我们看到了诸如此类的调用:


trap_SendServerCommand( -1, va("cp \"%s" S_COLOR_WHITE " joined....\n\"", ... );

那么我们就完全有理由相信,这里的所谓server command,只是用来供game向cgame发布指令用的,它们和前面的command的最大区别就是它们是无法通过用户输入来执行的,同样在CG_ServerCommand的函数里,我们还可以看到map_restart,但它和我们敲命令map_restart完全是两个概念,后者是由quake3.exe来识别并执行的,一定要分清楚。

同样,我们在CG_ServerCommand还可以看到chat、cs等等,如果试图通过控制台输入这些命令,就会得到unknown cmd,而cp在这里是一个特例,它不但不返回unknown cmd,甚至在sv_pure 1的时候还会把你踢回主菜单,我对我画的那个流程图还是有信心的,我怀疑是quake3.exe内部对cp做了处理,所以我在quake3.exe中查找cp,找到一个地方很有嫌疑:nextdl..download...cp..getIpAuthorize,然后我们拿download来试验,同样不会返回unknown cmd(必须要进入地图),由此可以确认cp是quake3.exe内部能识别的命令,属于internal command,但它又和server command里的在屏幕正中打印字符的命令名字相同,就象两个map_restart一样,我甚至怀疑cp在quake3.exe里的意思是拷贝文件....哦,security hole?

讨论完这个特殊的cp,我们继续回到server command的话题上来,因为它不是通过控制台输入的,所以我们不能把它添加到上面的结构图中去,你只要知道有这么个东西,这么个机制存在,不要把它和结构图中的command弄混淆,并且知道我们可以利用这个机制从game向cgame发送指令就可以了

似乎到这里我们已经可以结束关于cvar和command的讨论了,不过不知道你注意到没有,我们没有找到console command和client command的注册机制,quake3.exe只是简单的把它不能解释的命令按照流程图里的顺序一步步交给子模块,并通过检查返回值来判断是否被解释执行,那么quake3.exe是如何知道这些命令的名称并且可以让你在控制台通过TAB自动完成功能查看到所有匹配的命令呢?答案在cg_consolecmds.c/Ln520。

3.3 实现

抱歉,让你在实现个如此简单的功能之前看了那么多东西,希望你没有太着急,这些付出都是会有回报的,我们下面就可以非常轻松了。

首先我们给这个命令起个名字,我们就叫它"hello"吧,然后我们要决定把它添加到哪一种command里面,当然不可能是internal command和server command,只能是console command或者client command,为了给下一节的内容做个铺垫,我在这里采用client command,你也可以自己试着用game/cgame console command来完成它,不同的实现适用于不同的情况,你可以根据前面的流程图看出来。

看到这里,你肯定已经手痒了吧,现在我们就开始动手。

根据我们在前面找到的client command在程序中的位置,我们到g_cmds.c/Ln1578,看ClientCommand这个函数,它通过比较命令的字符串来执行不同的函数,我们要把对于"hello"的处理添加进来,但是添加在什么地方呢?在函数中间有如下语句:


// ignore all other commands when at intermission
if (level.intermissiontime) {
Cmd_Say_f (ent, qfalse, qtrue);
return;
}

intermission的意思就是一局打完了,但新的还没开始,这个时候没有活动的玩家在地图中,大家屏幕上都是比分牌。我们的HelloWorld在这个时候也不该显示在屏幕上,所以我们要把它添加到这段的后面。

我们要添加一个else if在最后一个else前面,然后在else if中做我们要做的事情,这里我们看到原来的代码都只是简单的调用一个函数,我们就不用了专门写函数了,因为我们的功能很少,反正也只有一行;我们使用前面查找cp的结果:


trap_SendServerCommand( -1, va("cp \"%s" S_COLOR_WHITE " joined....\n\"", ... );

并把要显示的东西换成我们需要的,去掉里面的变参代换,改完以后这段程序应该是这个样子:


else if (Q_stricmp (cmd, "stats") == 0)
Cmd_Stats_f( ent );
else if (Q_stricmp (cmd, "hello") == 0)
trap_SendServerCommand( -1, va("cp \"Hello, Quake World!\""));
else
trap_SendServerCommand( clientNum, va("print \"unknown cmd %s\n\"", cmd ) );

运行一下试试,进入地图后敲"\hello",如果前面一节的慢速火箭弹你已经成功运行了的话,这次的hello应该也一点问题没有,但如果屏幕上没有反应,你需要检查编译的步骤,以及目录的设置。

3.4 简单的c语言解释

这一小段是给没有任何编程知识的人来看的,你可以跳过。

如果"cp \"Hello, Quake World!\""这样的写法让你感到困惑,你可以尝试把它改成你觉得直观的形式:"cp Hello, Quake World!",并且看一下运行效果,你会发现屏幕上只显示了一个单词Hello和后面的逗号,也就是说cp这个server command只显示了跟在它后面的第一个单词,我们可以在它的实现的地方(cg_servercmds.c/Ln968)看到:


CG_CenterPrint( CG_Argv(1), SCREEN_HEIGHT * 0.30, BIGCHAR_WIDTH );

在c语言里,字符串都是用双引号包起来的,如过你要在这个字符串的内容里有双引号,那么你就需要用一种特别的方式来书写,以免这个双引号被错误的解释为字符串的结束,这个特别的方式,就是用反斜杠开头:\"

那么,其实刚才va函数得到的字符串的内容是:


cp "Hello, Quake World!"

对于这样的情况,va函数会把它原封不动的返回,从而trap函数得到也是这个字符串,然后quake3.exe会负责把这个字符串发送给客户端的quake3.exe,然后这个quake3.exe再把它交给cgame模块来作为server command解释,在968这行语句上,我们看到了CG_Argv(1)这样的调用,这个函数会返回cgame模块收到的这一串server command的命令后面的第一个单词,就好比我们在dos下使用dir *.exe一样,这里,*.exe就是第一个单词,在取出这个单词之后,就可以调用CG_CenterPrint来显示它了。

那如果你把里面的\"去掉了,那么cgame收到的是:


cp Hello, Quake World!

这时,第二个单词是Hello,,而如果有引号的话,第二个单词就是引号内的整个字符串。

到这里你可能要问,既然va函数只是原封不动的返回,那我们何必用va呢,直接这样不也可以嘛:


trap_SendServerCommand( -1, "cp \"Hello, Quake World!\"");

当然可以!但是有时候你可能会需要在屏幕上显示玩家的名字,后面才是Hello;这就就要用到刚才提到的"变参",也就是可变参数,在我们修改之前,原来的trap_SendServerCommand可能是这个样子:


trap_SendServerCommand( -1, va("cp \"%s" S_COLOR_WHITE " joined the red team.\n\"",
client->pers.netname) );

这句是显示某玩家加入了红队,那么client->pers.netname里面就是玩家的名字,中间插入S_COLOR_WHITE是为了避免有的人不以^7结束,影响到后面字体的颜色,那么我们可以把我们的语句改成:


trap_SendServerCommand( -1, va("cp \"%s" S_COLOR_WHITE ": Hello, Quake World!\n\"",
ent->client->pers.netname) );

这时,%s就被替换成了玩家的名字。

关于c语言的格式,只能写那么多了,以下的内容里不再对c语言做解释,你该考虑弄一本c语言的课本了。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

QQ|手机版|小黑屋|九艺游戏动画论坛 ( 津ICP备2022000452号-1 )

GMT+8, 2024-5-4 21:39 , Processed in 0.144525 second(s), 23 queries .

Powered by Discuz! X3.4  © 2001-2017 Discuz Team.