责任编辑适宜有 C 词汇此基础的好友

著实40天前的经典之作格斗游戏濶濑,此次报检了源代码插图

这儿是 HelloGitHub 面世的《传授开放源码工程项目》系列产品,下期为您传授的是 80、90 后的儿时梦境,问世于 1978 年经典之作红白机格斗游戏《外太空入侵者》也叫濶濑的 C 词汇重制版——si78c

著实40天前的经典之作格斗游戏濶濑,此次报检了源代码插图1

这款格斗游戏在彼时虽说是红极一时,坚信许多好友儿时都玩过。那时长大成人了,不晓得有啥好友对它的源标识符钟爱呢!

美版的《外太空入侵者》由约 2k 行的 8080 编订标识符写出,但编订词汇太过下层不方便写作,传授的开放源码工程项目 si78c 是依照美版编订标识符用 C 词汇改写了两遍,并某种程度还原成了美版红白机硬体的受阻、PulseAudio方法论,在运转时其缓存状况也基本上与原初版完全相同基本上达至了轻松的重制,虽说让我耳目一新!

上面请跟著 HelloGitHub 一同解明,运转那个开放源码工程项目、写作源标识符,横越发展史体会 40 天前格斗游戏结构设计的绝妙含意!

著实40天前的经典之作格斗游戏濶濑,此次报检了源代码插图2

一、加速已经开始

责任编辑的试验自然环境为 Ubuntu 20.04 LTS,GCC 版小于 GCC 3

1. 预备组织工作

具体来说si78c采用SDL2绘出格斗游戏询问处,因此须要加装倚赖:

$ sudo apt-getinstall libsdl2-dev

然后从仓库下载源标识符:

$ gitclonehttps://github.com/loadzero/si78c.git

此外,该工程项目会从美版的 ROM 中提取美版格斗游戏的图片、字体,因此还须要下载美版的 ROM 文件

2. 文件结构

在 si78c 源标识符文件夹中新建名为inv1bin的文件夹

$cdsi78c-master$mkdir inv1 bin

然后将invaders.zip中的内容解压到inv1中,后目录结构如下:

si78c-master├──bin├──inv1│ ├──invaders.e│ ├──invaders.f│ ├──invaders.g│ └──invaders.h├──Makefile├──README.md├──si78c.c└──si78c_proto.h

3. 编译与运转

采用make进行编译:

$make

之后会在bin文件夹中生成可执行文件,运转即可启动格斗游戏:

$ ./bin/si78c

格斗游戏操控按键如下:

aLEFT(左移)dRIGHT(右移)11P(单人)22P(双人)jFIRE(射击)5COIN(投币)tTILT(结束格斗游戏)

二、 前置知识

2.1 简介

《外太空入侵者》美版标识符运转在 8080 处理器之上,其内容全部由编订标识符写出并涉及一些硬体操作,为了模拟美版红白机标识符方法论以及效果,si78c 尽大可能将编订标识符转换为 C 词汇并采用一个Mem的结构体模拟了美版红白机的硬体,因此有些标识符从纯软件的角度来讲是比较奇怪甚至是匪夷所思的,但限于篇幅原因作者无法将标识符全部贴进文章进行解释,因此请读者配合本人详细注释标识符写作此文

2.2 什么是PulseAudio

si78c采用了ucontex库的PulseAudio模拟美版红白机的进程调度和受阻操作。

PulseAudio:PulseAudio更加轻便快捷、节省资源,PulseAudio 对于 线程 相当于 线程 对于 进程。

其中ucontext提供了getcontext()makecontext()swapcontext()以及setcontext()函数实现PulseAudio的创建和切换,si78c中的初始化函数为init_thread。上面我们直接来看源标识符中的例子:

如果这儿不够直观可以看后面状况转移图,图文结合更加直观。

标识符 2-1

// 切换PulseAudio时用的中间变量staticucontext_tfrontend_ctx;// 格斗游戏主要方法论PulseAudiostaticucontext_tmain_ctx;// 格斗游戏受阻方法论PulseAudiostaticucontext_tint_ctx;// 用于切换两个PulseAudiostaticucontext_t*prev_ctx;staticucontext_t*curr_ctx;// 初始化格斗游戏PulseAudiostaticvoidinit_threads(YieldReason entry_point){// 获取当前上下文,存储在 main_ctx 中intrc = getcontext(&main_ctx); assert(rc ==0);// 指定栈空间main_ctx.uc_stack.ss_sp = main_ctx_stack;// 指定栈空间大小main_ctx.uc_stack.ss_size = STACK_SIZE;// 设置后继上下文main_ctx.uc_link = &frontend_ctx;// 修改 main_ctx 上下文指向 run_main_ctx 函数makecontext(&main_ctx, (void(*)())run_main_ctx,1, entry_point);/** 以上内容相当于新建了一个叫 main_cxt 的PulseAudio,运转 run_main_ctx 函数, frontend_ctx 为后继上下文 * (run_main_ctx 运转完毕之后会接着运转 frontend_ctx 记录的上下文) * PulseAudio 对于 线程,就相当于 线程 对于 进程 * 只是PulseAudio切换开销更小,用起来更加轻便 */// 获取当前上下文存储在 init_ctx 中rc = getcontext(&int_ctx);// 指定栈空间int_ctx.uc_stack.ss_sp = &int_ctx_stack;// 指定栈空间大小int_ctx.uc_stack.ss_size = STACK_SIZE;// 设置后继上下文int_ctx.uc_link = &frontend_ctx;// 修改上下文指向 run_init_ctx 函数makecontext(&int_ctx, run_int_ctx,0);/** 以上内容相当于新建了一个叫 int_ctx 的PulseAudio,运转 run_int_ctx 函数, frontend_ctx 为后继上下文 * (run_int_ctx 运转完毕之后会接着运转 frontend_ctx 记录的上下文) * PulseAudio 对于 线程,就相当于 线程 对于 进程 * 只是PulseAudio切换开销更小,用起来更加轻便 */// 给 pre_ctx 初始值,在第一次调用 timeslice() 时候能切换到 main_ctx 运转prev_ctx = &main_ctx;// 给 curr_ctx 初始值,这时候 frontend_ctx 还是空的// frontend_ctx 会在上下文切换的时候用于保存上一个PulseAudio的状况curr_ctx = &frontend_ctx; }

之后每次调用yield()都会采用swapcontext()进行两个PulseAudio间切换:

标识符 2-2

staticvoidyield(YieldReason reason){// 调度原因yield_reason = reason;// 调度到另一个PulseAudio上switch_to(&frontend_ctx); }// PulseAudio切换函数staticvoidswitch_to(ucontext_t*to){// 给 co_switch 包装了一层,简化了标识符量co_switch(curr_ctx, to); }// PulseAudio切换函数staticvoidco_switch(ucontext_t*prev,ucontext_t*next){ prev_ctx = prev; curr_ctx = next;// 切换到 next 指向的上下文,将当前上下文保存在 prev 中swapcontext(prev, next); }

具体用法请见后文

由于文章篇幅有限,上面只展示的关键源标识符部分。更详细的源标识符逐行中文注释:

地址:https://github.com/AnthonySun256/easy_games

2.3 模拟硬体

前文讲过,si78c 是美版红白机格斗游戏像素级的重制,甚至大部分的缓存数据也是相等的,为了做到这一点 si78c 模拟了红白机的一部分硬体:RAM、ROM 和 显存,它们在标识符中被封装成了一个名为Mem的大结构体,缓存分配如下:

0000-1FFF 8K ROM2000-23FF 1K RAM2400-3FFF 7K Video RAM4000- RAM mirror

可以看出当年机器的 RAM 只有可怜的 1kb 大小,每一个比特都弥足珍贵须要程序认真规划。这儿有张 RAM 分配情况表,更多详情

著实40天前的经典之作格斗游戏濶濑,此次报检了源代码插图3

2.4 从模拟显存到屏幕

在详细解释格斗游戏动画显示原理以前,我们须要先了解一下格斗游戏的素材是怎么存储的:

著实40天前的经典之作格斗游戏濶濑,此次报检了源代码插图4

图 2-1

图片来自于红白机编订标识符解读

在红白机美版 ROM 中,格斗游戏素材直接以二进制格式保存在缓存中,其中每一位二进制表示当前位置像素是黑还是白

比如图 2-1中显示0x1BA0位置的缓存数据为00 03 04 78 14 13 08 1A 3D 68 FC FC 68 3D 1A 00八位一行排列和出来是一个外星人带着一个颠倒字母 Y 的图片(图中的内容看起来像是旋转了 90 度这是因为图片是一列一列存储的,每 8 bit 代表一列像素)。

si78c 的作者在显示图片的时候直接将 X Y 轴进行了交换以达至旋转图片的效果。

我们可以找到名为Mem的结构体,其中的m.vram0x24000x3FFF)模拟了红白机的显存,这儿面每一个 bit 代表一个像素的黑(0)白(1),从左下角向右上角进行渲染,其对应关系如图 2-2

著实40天前的经典之作格斗游戏濶濑,此次报检了源代码插图5

图 2-2

格斗游戏中所有跟动画绘出有关的标识符都是在修改这部分区域的数据,例如DrawChar()ClearPlayField()DrawSimpSprite()等等。那么怎么让模拟现存的内容显示到玩家的屏幕上呢?注意看标识符 3-1中在循环的末尾调用了render()函数,它负责的挨个读取模拟显存中的内容并在询问处上有像素块的地方渲染一个像素块。

仔细想想不难发现,这种先修改模拟显存再统一绘出的方法其实没有多省事,甚至有些怪异。这是因为 si78c 模拟了红白机硬体的显示过程:修改相应的显存然后硬体会自动将显存中的内容显示到屏幕上。

2.5 按键检测

标识符 3-1中的input()函数负责检测并存储用户的按键信息,其下层倚赖SDL库。

三、首次启动

si78c和所有的 C 程序一样,都是从main()函数已经开始运转:

标识符 3-1

intmain(intargc,char**argv){// 初始化 SDL 和 格斗游戏询问处init_renderer();// 初始化格斗游戏init_game();intcredit =0;size_tframe =-1;// 已经开始格斗游戏PulseAudio调度与模拟触发受阻while(1) { frame++;// 处理按键输入input();// 如果退出标志置位面世循环清理格斗游戏缓存if(exited)break;// preserves timing compatibility with MAME// 保留与 MAME(一种红白机) 的时序兼容性if(frame ==1) credit–;/** * 执行其他进程大概 CRED1 的时间 * (为什么是那个数我也不晓得,应该是估计值) * (原作者也说这种定时方法不是很准确但不影响格斗游戏效果) */credit += CRED1; loop_core(&credit);// 设置场中间受阻标志位,在上面的 loop_core() 中会切换到 int_ctx 执行一次,然后清除标志位irq(0xcf);// 道理同上credit += CRED2; loop_core(&credit);// 设置垂直消隐受阻标志位,下个循环时候 loop_core() 中会切换到 int_ctx 执行一次,然后清除标志位irq(0xd7);// 绘出格斗游戏界面render(); } fini_game(); fini_renderer();return0; }

启动过程如图所示:

著实40天前的经典之作格斗游戏濶濑,此次报检了源代码插图6

图 3-1

格斗游戏美版标识符(8080 编订)采用的是受阻驱动(这种编程方式和硬体有关,具体内容可以自行了解什么是受阻)配合PulseAudio多任务操作。为了模拟美版格斗游戏方法论作者以main()中大循环作为硬体行为模拟中心(实现受阻管理、PulseAudio切换、屏幕渲染)。格斗游戏约三分之一的时间在运转主线程主线程会被midscreenvblank两个受阻抢占,标识符 3-1中两个irq()实现了对受阻的模拟(设置对应的变量作为标志位)。

进入loop_core()时其流程如下:

著实40天前的经典之作格斗游戏濶濑,此次报检了源代码插图7

图 3-2

因为yield_rason那个变量是static类型其默认值为零

标识符 3-2

// 根据格斗游戏状况标志切换到相应的上下文staticintexecute(intallowed){int64_tstart = ticks;ucontext_t*next =NULL;switch(yield_reason) {// 刚启动时 yield_reason 是 0 表示 YIELD_INITcaseYIELD_INIT:// 当须要延迟的时候会调用 timeslice() 将 yield_reason 切换为 YIELD_TIMESLICE// 模拟时间片轮转,那个时候会切换回上一个运转的任务(统共就俩PulseAudio),实现时间片轮转caseYIELD_TIMESLICE: next = prev_ctx;break;caseYIELD_INTFIN:// 处理完受阻后让 int_ctx 休眠,重新运转 main_ctxnext = &main_ctx;break;// 玩家死亡、等待已经开始、外星人入侵状况caseYIELD_PLAYER_DEATH:caseYIELD_WAIT_FOR_START:caseYIELD_INVADED: init_threads(yield_reason); enable_interrupts(); next = &main_ctx;break;// 退出格斗游戏caseYIELD_TILT: init_threads(yield_reason); next = &main_ctx;break;default: assert(FALSE); } yield_reason = YIELD_UNKNOWN;// 如果有受阻产生if(allowed && interrupted()) { next = &int_ctx; } switch_to(next);returnticks – start; }

须要注意的是,在execute()中进行了PulseAudio的切换,那个时候execute()的运转状况被保存在了变量frontend_ctx之中,指针prev_ctx更新为指向frontend_ctx,指针curr_ctx更新为指向main_ctx,其过程如图所示:

著实40天前的经典之作格斗游戏濶濑,此次报检了源代码插图8

图 3-3

实现解释请见标识符 2-2

execute()返回时他会依照正常的执行流程返回到loop_core(),像它从未被暂停过一样。

仔细观察main_init中主循环我们可以发现其多次调用timeslice()函数(例如OneSecDelay()中),通过那个函数我们可以实现main_ctxfrontend_ctx间的时间片轮转操作,其过程如下:

著实40天前的经典之作格斗游戏濶濑,此次报检了源代码插图9

图 3-4

main_init()中主要做了如下事情:

著实40天前的经典之作格斗游戏濶濑,此次报检了源代码插图10

在玩家投币前,格斗游戏会依靠main_init()循环播放动画吸引玩家

如果只翻看main_init()中出现的函数我们会发现标识符中并未涉及太多的格斗游戏方法论,例如外星人移动、射击,玩家投币检查等内容好像根本不存在一样,更多的时候是在操纵缓存、设置标志位。那么有关格斗游戏格斗游戏方法论处理相关的函数又在哪里呢?这部分内容将在上面揭秘。

四、模拟受阻

标识符 3-1loop_core()函数被两个irq()分隔了开来。我们之前提到main()中的大循环本质上是在模拟红白机的硬体行为,在真实的机器上受阻是只有在触发时才会执行,但在 si78c 上我们只能通过在loop_core()之间调用irq()模拟产生受阻并在execute()中轮询受阻状况来判断是不是进入受阻处理函数,过程如下:

著实40天前的经典之作格斗游戏濶濑,此次报检了源代码插图11

这时它的PulseAudio状况如下:

著实40天前的经典之作格斗游戏濶濑,此次报检了源代码插图12

有两种受阻:midscreen_int()vblank_int()这两种受阻会轮流出现。

标识符 4-1

// 处理受阻的函数staticvoidrun_int_ctx() {while(1) {// 0xcf = RST 1 opcode (call 0x8)// 0xd7 = RST 2 opcode (call 0x16)if(irq_vector ==0xcf)midscreen_int();elseif(irq_vector ==0xd7)vblank_int();// 使能受阻enable_interrupts();yield(YIELD_INTFIN); } }

我们先来看midscreen_int()

标识符 4-2

/** * 在光将要击中屏幕中间(应该是模拟老式红白机的现实原理)时由受阻触发 * 主要处理格斗游戏对象的移动、开火、碰撞等等的检测更新与绘出(具体看函数 GameObj0到4) * 以及确定下一个将要绘出哪个外星人,检测外星人是不是入侵成功了 */staticvoidmidscreen_int(){// 更新 vblank 标志位m.vblankStatus = BEAM_MIDDLE;// 如果没有运动的格斗游戏对象,返回if(m.gameTasksRunning ==0)return;// 在欢迎界面 且 没有在演示模式,返回(只在格斗游戏模式 和 demo模式下继续运转)if(!m.gameMode && !(m.isrSplashTask &0x1))return;// 运转 game objects 但是略过第一个入口(玩家)RunGameObjs(u16_to_ptr(PLAYER_SHOT_ADDR));// 确定下一个将要绘出的外星人CursorNextAlien(); }

在这一部分中RunGameObjs()函数基本上包括了玩家的移动和绘出,玩家子弹和外星人子弹的移动、碰撞检测、绘出等等所有格斗游戏方法论的处理,CursorNextAlien()则找到要绘出的下一个活着的外星人设置标志位等待绘出,并且检测外星飞船是否碰到了屏幕底端。

运转结束后会返回到run_int_ctx()继续运转直到yield(YIELD_INTFIN)表示PulseAudio切换回execute(),并在execute()中重新将next设定为main_ctx使main_init()能够继续运转(详情见标识符 3-2)。

接下来是vblank_int()

标识符 4-3

/** * 当光击中屏幕最后一点(模拟老式红白机原理)时触发 * 主要处理格斗游戏结束、投币、格斗游戏中各种事件处理、播放演示动画 */staticvoidvblank_int(){// 更新标志位m.vblankStatus = BEAM_VBLANK;// 计时器减少m.isrDelay–;// 看看是不是结束格斗游戏CheckHandleTilt();// 看看是不是投币了vblank_coins();// 如果格斗游戏任务没有运转,返回if(m.gameTasksRunning ==0)return;// 如果在格斗游戏中的话if(m.gameMode) { TimeFleetSound(); m.shotSync = m.rolShotHeader.TimerExtra; DrawAlien(); RunGameObjs(u16_to_ptr(PLAYER_ADDR)); TimeToSaucer();return; }// 如果投币过了if(m.numCoins !=0) {// xref 005dif(m.waitStartLoop)return; m.waitStartLoop =1;// 切换PulseAudio到等待已经开始循环yield(YIELD_WAIT_FOR_START); assert(FALSE);// 不会再返回了}// 如果以上事情都没发生,播放演示动画ISRSplTasks(); }

其主要作用一是检测玩家是否想要退出格斗游戏或是进行了投币操作,如果已经处于格斗游戏模式中则依次播放舰队声音、绘出在midscreen_int()中标记出的外星人、运转RunGameObjs()处理玩家和外星人开火与移动事件、TimeToSaucer()随机生成神秘飞碟。如果未在格斗游戏模式中则进入ISRSplTasks()调整当前屏幕上应该播放的动画。

我们可以注意到,如果玩家进行了投币会进入if (m.numCoins != 0)里,并调用yield(YIELD_WAIT_FOR_START)后面会提示那个函数不会再返回。在 si78c 的标识符中许多地方都会有这样的提示,这儿并不是简单的调用一个不会返回的函数进行套娃。

观察标识符 3-2可以发那时YIELD_PLAYER_DEATHYIELD_WAIT_FOR_STARTYIELD_INVADEDYIELD_TILT这四种分支中都调用了init_threads(yield_reason),在那个函数里会重置int_ctxmain_ctx的堆栈并重新绑定调用run_main_ctx时的参数为yield_reason,这样在下一次执行的时候run_main_ctx会根据受阻的指示跳转到合适的分支去运转。

五、巧妙地节省 RAM

开篇的时候提到过,当年红白机的 RAM 只有可怜的 1kb 大小,这样小的地方必定无法让我们存储屏幕上每个对象的信息,但是玩家的位置、外星人的位置以及它们的子弹、屏幕上的盾牌损坏情况都是会实时更新的,如何做到这一点呢?

我发现《外太空入侵者》格斗游戏区域内容分布还是很有规律的,特殊飞船(飞碟)只会出那时屏幕上端,盾牌和玩家的位置不会改变,只有子弹的位置不好把握,因此仔细研读标识符,从DrawSpriteGeneric()可以看出,格斗游戏对于碰撞的检测只是简单的判断像素块是否重合,对于玩家子弹到底击中了什么在PlayerShotHit()函数进行判断时,则只须要判断子弹垂直方向坐标(Y坐标),如果 >= 216 则是撞到上顶,>=206 则是击中神秘飞碟,其他则是击中护盾或者外星人的子弹。且由于外星飞船的是成组一同运动,只须要记住其中一个的位置能推算出整体每一个外星飞船的坐标。

这样算下来,程序只须要保存外星飞船的存活状况、当前舰队的相对移动位置、玩家和外星人子弹信息,在须要检测碰撞时则去读取显存中的像素信息进行对比然后反推当前时哪两样物体发生了碰撞即可,这种方法相比存储每一个对象的信息节省了不少资源。

六、结语

si78c不同于其他标识符,它本质上是对硬体和编订标识符的仿真,希望通过责任编辑的源标识符传授,让更多人看到当年程序员们在有限资源下制作出优秀格斗游戏的困难,还有标识符结构设计的绝妙。

后,感谢本工程项目作者所做的一切,没有他的付出也不会有这篇文章。如果您觉得这篇文章还不错,欢迎分享给更多人。

作者 nasiapp

在线客服
官方客服
我们将24小时内回复。
12:01
您好,有任何疑问请与我们联系!

选择聊天工具: