日常在使用电脑的时候,有时候听到机箱噪音大,这样就会影响工作和学习的心情,降低工作和学习的效率。该怎么办呢?接下来小编就来给大家介绍一下电脑...... [详细]
五花肉放进酱油里泡1周,又香又入味,比腊肉好吃,新手一次成功过年吃腊肉,是流传了多少年的传统习俗,你家今年制作的腊肉吃完了吗?我家还剩好几斤...... [详细]
今天上班遇见了不该遇见的人,怎么办...... [详细]
这里是 HelloGitHub 推出的《讲解开源项目》系列,本期为您讲解的是 80、90 后的儿时记忆,诞生于 1978 年经典街机游戏《太空侵略者》也叫“小蜜蜂”的 C 语言复刻版——si78c。
这款游戏在当时可谓是风靡一时,相信很多朋友小时候都玩过。现在长大了,不知道有多少朋友对它的源码感兴趣呢!
原版的《太空侵略者》由大约 2k 行的 8080 汇编代码写成,但汇编语言太过底层不方便阅读,今天讲解的开源项目 si78c 是按照原版汇编代码用 C 语言重写了一遍,并最大程度还原了原版街机硬件的中断、协程逻辑,在运行时其内存状态也几乎与原始版本相同 几乎达到了完美的复刻,着实让我眼前一亮!
下面就请跟着 HelloGitHub 一起抽丝剥茧,运行这个开源项目、阅读源码,穿越历史感受 40 年前游戏设计的精妙之处!
一、快速开始本文的实验环境为 Ubuntu 20.04 LTS,GCC 版本大于 GCC 3
1. 准备工作首先 si78c 使用 SDL2 绘制游戏窗口,所以需要安装依赖:
$ sudo apt-get install libsdl2-dev然后从仓库下载源码:
$ git clone https://github.com/loadzero/si78c.git此外,该项目会从原版的 ROM 中提取原版游戏的图片、字体,所以还需要下载原版的 ROM 文件
2. 文件结构在 si78c 源码文件夹中新建名为 inv1 和 bin 的文件夹
$ cd si78c-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.h3. 编译与运行使用 make 进行编译:
$ make之后会在 bin 文件夹中生成可执行文件,运行即可启动游戏:
$ ./bin/si78c游戏操控按键如下:
a LEFT(左移)d RIGHT(右移)1 1P(单人)2 2P(双人)j FIRE(射击)5 COIN(投币)t TILT(结束游戏)二、 前置知识2.1 简介《太空侵略者》原版代码运行在 8080 处理器之上,其内容全部由汇编代码写成并涉及一些硬件操作,为了模拟原版街机代码逻辑以及效果,si78c 尽最大可能将汇编代码转换为 C 语言并使用一个 Mem 的结构体模拟了原版街机的硬件,所以有些代码从纯软件的角度来讲是比较奇怪甚至是匪夷所思的,但限于篇幅原因作者无法将代码全部贴进文章进行解释,所以请读者配合本人详细注释代码阅读此文。
2.2 什么是协程si78c 使用了 ucontex 库的 协程 模拟原版街机的进程调度和中断操作。
协程:协程更加轻便快捷、节省资源,协程 对于 线程 就相当于 线程 对于 进程。
其中 ucontext 提供了 getcontext()、makecontext()、swapcontext() 以及 setcontext() 函数实现协程的创建和切换,si78c 中的初始化函数为 init_thread。下面我们直接来看源码中的例子:
如果这里不够直观可以看后面状态转移图,图文结合更加直观。
代码 2-1
// 切换协程时用的中间变量static ucontext_t frontend_ctx;// 游戏主要逻辑协程static ucontext_t main_ctx;// 游戏中断逻辑协程static ucontext_t int_ctx;// 用于切换两个协程static ucontext_t *prev_ctx;static ucontext_t *curr_ctx;// 初始化游戏协程static void init_threads(YieldReason entry_point){ // 获取当前上下文,存储在 main_ctx 中 int rc = 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 的协程,运行 run_main_ctx 函数, frontend_ctx 为后继上下文 * (run_main_ctx 运行完毕之后会接着运行 frontend_ctx 记录的上下文) * 协程 对于 线程,就相当于 线程 对于 进程 * 只是协程切换开销更小,用起来更加轻便 */ // 获取当前上下文存储在 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 的协程,运行 run_int_ctx 函数, frontend_ctx 为后继上下文 * (run_int_ctx 运行完毕之后会接着运行 frontend_ctx 记录的上下文) * 协程 对于 线程,就相当于 线程 对于 进程 * 只是协程切换开销更小,用起来更加轻便 */ // 给 pre_ctx 初始值,在第一次调用 timeslice() 时候能切换到 main_ctx 运行 prev_ctx = &main_ctx; // 给 curr_ctx 初始值,这时候 frontend_ctx 还是空的 // frontend_ctx 会在上下文切换的时候用于保存上一个协程的状态 curr_ctx = &frontend_ctx;}之后每次调用 yield() 都会使用 swapcontext() 进行两个协程间切换:
代码 2-2
static void yield(YieldReason reason){ // 调度原因 yield_reason = reason; // 调度到另一个协程上 switch_to(&frontend_ctx);}// 协程切换函数static void switch_to(ucontext_t *to){ // 给 co_switch 包装了一层,简化了代码量 co_switch(curr_ctx, to);}// 协程切换函数static void co_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 分配情况表,更多详情
本文适合有 C 语言基础的朋友这里是 HelloGitHub 推出的《讲解开源项目》系列,本期为您讲解的是 80、90 后的儿时记忆,诞生于...... [详细]
眉飞色舞 色脸色形容人得意兴奋的样子 眉高眼低 脸上的表情泛指为人处世的道理或辨貌观色的本领 眉睫之祸 眉睫眉毛和眼睫毛,比喻近在眼前。...... [详细]
每个品牌的手机都有自己的用户群体,总结了一下每个品牌用户的归类,不是100%,但绝对是同一群体的最多数人苹果用户:①外企工作者②卖房销售员③...... [详细]
4月1日,日本政府公布新年号为“令和”,现在的“平成”年号将于4月30日随着明仁天皇的退位停止使用,5月1日起,正式使用新年号。日本明治政府...... [详细]
不知你是否遇到这样一个问题:需要批量修改图片的大小和格式。如果是在职场中,你可能会按部就班地一张一张处理。但是如果你是老板你肯定需要能快速处...... [详细]
怀孕期间,每个女人都会有一个胎梦,不同的胎梦有不同的妊娠期。看完暗示生女孩的胎梦,我们再来了解一下暗示她即将怀孕的胎梦,尤其是准备怀孕的...... [详细]
现代人每天都有很长的时间面对着显示器,所以如何护眼也成了大家关注的重点,其中常见的是显示器的滤蓝光功能。那么有必要为了滤蓝光功能加钱吗?“蓝...... [详细]
英文词“Plus”,有“加”“好的”等意思,近年来被广泛应用于手机等数码产品名称上。拥有Plus后缀的产品,其性能、定价等方面均高出普通产品不少,因而也让Plus一词在消费者心中有了一丝高级感。
如今,Plus后缀命名的方式也蔓延到了汽车圈,就让我们一起来看看哪些轿车拥有Plus的名头吧。
朗逸Plus(除两厢、朗逸启航及经典款朗逸外)
厂商指导价:11.29-15.89万元
蝉联国内轿车销量榜首多年的朗逸,应当是最早在国内使用Plus字眼的车型。2018年5月,朗逸推出新车并定名为朗逸Plus,这款新车相比老款车型而言,拥有更大的车身尺寸、更强的动力输出以及全新的造型和内饰设计,Plus之名名副其实。
英文词“Plus”,有“加”“好的”等意思,近年来被广泛应用于手机等数码产品名称上。拥有Plus后缀的产品,其性能、定价等方面均高出普通产品...... [详细]
赤道是地球表面的点随地球自转产生的轨迹中周长最长的圆周线。赤道半径6378.2千米,赤道周长40075.02千米(24901英里)。它把地...... [详细]
随着A股市场退市制度的不断改革,上市公司优胜劣汰将在所难免,将来会有更多股票在A股市场上退市。而股民很容易不小心就会踩上业绩变脸的股票,所以很多新股民提出这样的问题:“如果股票退市了,我们散户的钱该怎么办?”
实际上,股票在退出之时,会给散户很多次退出机会的,我们建议散户遇到这样的股票,还是越早退出越好,越往后拖越是不利。现在A股市场的股票退市总共有以下几个步骤:第一步,如果一只股票出现风险,主板市场会给它一个ST的标识,而创业板的股票则会连续发出公告,告诉你尽量不要再买这样的股票了,现在有危险了,该退出要退出,该止损的还是要止损。
第二步,如果你还继续持有下去,而手中这只ST股票还继续亏损,甚至出现更大的危机和问题,那么就被暂停上市,而在暂停上市的时间里,股票无法交易,如果上市公司能够扭亏为盈,或者被其他公司“借壳上市”,那么还能继续回到市场来交易,如果还是一直亏损,或找不到其他企业来借壳,那么就会进入退市模式。
随着A股市场退市制度的不断改革,上市公司优胜劣汰将在所难免,将来会有更多股票在A股市场上退市。而股民很容易不小心就会踩上业绩变脸的股票,所以...... [详细]
近日,福建福州的林女士突然脾气变得暴躁稍有不顺心就骂人还时不时发出傻笑声几天几夜不睡自言自语手舞足蹈家人赶紧将她送医经过医生诊断林女士竟然得...... [详细]
寒露过后,黄花红叶,妆点秋光,重阳来到,又是一年登高望远的日子。每年的农历九月初九,是为重阳节,二九相重,称为“重九”。1重 阳 来 源农历...... [详细]
事故案例分析报告的一般格式一、事故经过(概述)二、事故性质认定和原因分析1、性质认定2、直接原因3、间接原因三、事故责任划分与处理1、直接...... [详细]
最近突然发现我的手机顶部信号栏出来"HD"字母,不知道你们的手机有没有?一开始我还以为是又开通什么业务,后来通过咨询查证,才...... [详细]
近段时间读成语故事,发现成语的形成大多在春秋战国时期以及秦汉时期,而成语故事起源最多的主人公非楚霸王项羽莫属了。1.四面楚歌、破釜沉舟、乌江...... [详细]
单机游戏需要肝的多了去了 举一些例子:怪物猎人 打怪刷素材做装备强化装备做武器强化武器 还有收集素材上交或者做弹药陷阱什么的 世界这代还需要...... [详细]
工作超负荷、学习压力大、养娃伤脑筋……最近有没有觉得心好“累”?今天,我们就请北京回龙观医院的张晓鸣医生为大家推荐六条缓解“心理疲劳”的锦囊...... [详细]