最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會(huì)員登陸 & 注冊(cè)

重玩 40 年前的經(jīng)典游戲小蜜蜂,這次通關(guān)了源碼(轉(zhuǎn)載)

2021-12-28 17:58 作者:V3庫(kù)洛洛老師  | 我要投稿

轉(zhuǎn)載公眾號(hào),Github喵

本文適合有 C 語(yǔ)言基礎(chǔ)的朋友

作者:HelloGitHub-Anthony大家好,我是喵哥。今天在Anthony兄的筆下,帶大家看看80、90 后兒時(shí)的記憶,誕生于 1978 年的經(jīng)典街機(jī)游戲《太空侵略者》也叫“小蜜蜂”的 C 語(yǔ)言復(fù)刻版——si78c

項(xiàng)目:https://github.com/loadzero/si78c

這款游戲在當(dāng)時(shí)可謂是風(fēng)靡一時(shí),相信很多朋友小時(shí)候都玩過(guò)。現(xiàn)在長(zhǎng)大了,不知道有多少朋友對(duì)它的源碼感興趣呢!原版的《太空侵略者》由大約 2k 行的 8080 匯編代碼寫成,但匯編語(yǔ)言太過(guò)底層不方便閱讀,今天講解的開源項(xiàng)目 si78c 是按照原版匯編代碼用 C 語(yǔ)言重寫了一遍,并最大程度還原了原版街機(jī)硬件的中斷、協(xié)程邏輯,在運(yùn)行時(shí)其內(nèi)存狀態(tài)也幾乎與原始版本相同?幾乎達(dá)到了完美的復(fù)刻,著實(shí)讓我眼前一亮!下面就請(qǐng)跟著我一起抽絲剝繭,運(yùn)行這個(gè)開源項(xiàng)目、閱讀源碼,穿越歷史感受 40 年前游戲設(shè)計(jì)的精妙之處!

一、快速開始

本文的實(shí)驗(yàn)環(huán)境為 Ubuntu 20.04 LTS,GCC 版本大于 GCC 3

1. 準(zhǔn)備工作

首先?si78c?使用?SDL2?繪制游戲窗口,所以需要安裝依賴:$?sudo?apt-get?install?libsdl2-dev
然后從倉(cāng)庫(kù)下載源碼:$?git?clone?https://github.com/loadzero/si78c.git
此外,該項(xiàng)目會(huì)從原版的 ROM 中提取原版游戲的圖片、字體,所以還需要下載原版的 ROM 文件

《太空侵略者》原版 ROM 文件:https://archive.org/download/MAME_078/invaders.zip

2. 文件結(jié)構(gòu)

在 si78c 源碼文件夾中新建名為?inv1?和?bin?的文件夾$?cd?si78c-master
$?mkdir?inv1?bin
然后將?invaders.zip?中的內(nèi)容解壓到?inv1?中,最后目錄結(jié)構(gòu)如下:si78c-master
├──?bin
├──?inv1
│???├──?invaders.e
│???├──?invaders.f
│???├──?invaders.g
│???└──?invaders.h
├──?Makefile
├──?README.md
├──?si78c.c
└──?si78c_proto.h

3. 編譯與運(yùn)行

使用?make?進(jìn)行編譯:$?make
之后會(huì)在?bin?文件夾中生成可執(zhí)行文件,運(yùn)行即可啟動(dòng)游戲:$?./bin/si78c?
游戲操控按鍵如下:a???LEFT(左移)
d???RIGHT(右移)
1???1P(單人)
2???2P(雙人)
j???FIRE(射擊)
5???COIN(投幣)
t???TILT(結(jié)束游戲)

二、 前置知識(shí)

2.1 ?簡(jiǎn)介

《太空侵略者》原版代碼運(yùn)行在 8080 處理器之上,其內(nèi)容全部由匯編代碼寫成并涉及一些硬件操作,為了模擬原版街機(jī)代碼邏輯以及效果,si78c 盡最大可能將匯編代碼轉(zhuǎn)換為 C 語(yǔ)言并使用一個(gè)?Mem?的結(jié)構(gòu)體模擬了原版街機(jī)的硬件,所以有些代碼從純軟件的角度來(lái)講是比較奇怪甚至是匪夷所思的,但限于篇幅原因作者無(wú)法將代碼全部貼進(jìn)文章進(jìn)行解釋,所以請(qǐng)讀者配合本人詳細(xì)注釋代碼閱讀此文。

2.2 什么是協(xié)程

si78c?使用了?ucontex?庫(kù)的?協(xié)程?模擬原版街機(jī)的進(jìn)程調(diào)度和中斷操作。

協(xié)程:協(xié)程更加輕便快捷、節(jié)省資源,協(xié)程 對(duì)于 線程 就相當(dāng)于 線程 對(duì)于 進(jìn)程。

其中?ucontext?提供了?getcontext()、makecontext()、swapcontext()?以及?setcontext()?函數(shù)實(shí)現(xiàn)協(xié)程的創(chuàng)建和切換,si78c?中的初始化函數(shù)為?init_thread。下面我們直接來(lái)看源碼中的例子:

如果這里不夠直觀可以看后面狀態(tài)轉(zhuǎn)移圖,圖文結(jié)合更加直觀。

代碼 2-1//?切換協(xié)程時(shí)用的中間變量
static?ucontext_t?frontend_ctx;
//?游戲主要邏輯協(xié)程
static?ucontext_t?main_ctx;
//?游戲中斷邏輯協(xié)程
static?ucontext_t?int_ctx;


//?用于切換兩個(gè)協(xié)程
static?ucontext_t?*prev_ctx;
static?ucontext_t?*curr_ctx;

//?初始化游戲協(xié)程
static?void?init_threads(YieldReason?entry_point)
{
????//?獲取當(dāng)前上下文,存儲(chǔ)在?main_ctx?中
????int?rc?=?getcontext(&main_ctx);
????assert(rc?==?0);

????//?指定棧空間
????main_ctx.uc_stack.ss_sp?=?main_ctx_stack;
????//?指定??臻g大小
????main_ctx.uc_stack.ss_size?=?STACK_SIZE;
????//?設(shè)置后繼上下文
????main_ctx.uc_link?=?&frontend_ctx;

????//?修改?main_ctx?上下文指向?run_main_ctx?函數(shù)
????makecontext(&main_ctx,?(void?(*)())run_main_ctx,?1,?entry_point);

????/**?以上內(nèi)容相當(dāng)于新建了一個(gè)叫?main_cxt?的協(xié)程,運(yùn)行?run_main_ctx?函數(shù),?frontend_ctx?為后繼上下文
?????*?(run_main_ctx?運(yùn)行完畢之后會(huì)接著運(yùn)行?frontend_ctx?記錄的上下文)
?????*?協(xié)程?對(duì)于?線程,就相當(dāng)于?線程?對(duì)于?進(jìn)程
?????*?只是協(xié)程切換開銷更小,用起來(lái)更加輕便
?????*/

????//?獲取當(dāng)前上下文存儲(chǔ)在?init_ctx?中
????rc?=?getcontext(&int_ctx);

????//?指定??臻g
????int_ctx.uc_stack.ss_sp?=?&int_ctx_stack;
????//?指定??臻g大小
????int_ctx.uc_stack.ss_size?=?STACK_SIZE;
????//?設(shè)置后繼上下文
????int_ctx.uc_link?=?&frontend_ctx;

????//?修改上下文指向?run_init_ctx?函數(shù)
????makecontext(&int_ctx,?run_int_ctx,?0);

????/**?以上內(nèi)容相當(dāng)于新建了一個(gè)叫?int_ctx?的協(xié)程,運(yùn)行?run_int_ctx?函數(shù),?frontend_ctx?為后繼上下文
?????*?(run_int_ctx?運(yùn)行完畢之后會(huì)接著運(yùn)行?frontend_ctx?記錄的上下文)
?????*?協(xié)程?對(duì)于?線程,就相當(dāng)于?線程?對(duì)于?進(jìn)程
?????*?只是協(xié)程切換開銷更小,用起來(lái)更加輕便
?????*/

????//?給?pre_ctx?初始值,在第一次調(diào)用?timeslice()?時(shí)候能切換到?main_ctx?運(yùn)行
????prev_ctx?=?&main_ctx;
????//?給?curr_ctx?初始值,這時(shí)候?frontend_ctx?還是空的
????//?frontend_ctx?會(huì)在上下文切換的時(shí)候用于保存上一個(gè)協(xié)程的狀態(tài)
????curr_ctx?=?&frontend_ctx;
}
之后每次調(diào)用?yield()?都會(huì)使用?swapcontext()?進(jìn)行兩個(gè)協(xié)程間切換:代碼 2-2static?void?yield(YieldReason?reason)
{
????//?調(diào)度原因
????yield_reason?=?reason;
????//?調(diào)度到另一個(gè)協(xié)程上
????switch_to(&frontend_ctx);
}

//?協(xié)程切換函數(shù)
static?void?switch_to(ucontext_t?*to)
{
????//?給?co_switch?包裝了一層,簡(jiǎn)化了代碼量
????co_switch(curr_ctx,?to);
}

//?協(xié)程切換函數(shù)
static?void?co_switch(ucontext_t?*prev,?ucontext_t?*next)
{
????prev_ctx?=?prev;
????curr_ctx?=?next;

????//?切換到?next?指向的上下文,將當(dāng)前上下文保存在?prev?中
????swapcontext(prev,?next);
}

具體用法請(qǐng)見后文

由于文章篇幅有限,下面只展示的關(guān)鍵源碼部分。更詳細(xì)的源碼逐行中文注釋:

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

2.3 模擬硬件

前文講過(guò),si78c 是原版街機(jī)游戲像素級(jí)的復(fù)刻,甚至大部分的內(nèi)存數(shù)據(jù)也是相等的,為了做到這一點(diǎn) si78c 模擬了街機(jī)的一部分硬件:RAM、ROM 和 顯存,它們?cè)诖a中被封裝成了一個(gè)名為?Mem?的大結(jié)構(gòu)體,內(nèi)存分配如下:

  • 0000-1FFF 8K ROM

  • 2000-23FF 1K RAM

  • 2400-3FFF 7K Video RAM

  • 4000- RAM mirror

可以看出當(dāng)年機(jī)器的 RAM 只有可憐的 1kb 大小,每一個(gè)比特都彌足珍貴需要程序認(rèn)真規(guī)劃。這里有張 RAM 分配情況表,更多詳情


2.4 從模擬顯存到屏幕

在詳細(xì)解釋游戲動(dòng)畫顯示原理以前,我們需要先了解一下游戲的素材是怎么存儲(chǔ)的:


圖片來(lái)自于街機(jī)匯編代碼解讀

在街機(jī)原版 ROM 中,游戲素材直接以二進(jìn)制格式保存在內(nèi)存中,其中每一位二進(jìn)制表示當(dāng)前位置像素是黑還是白比如?圖 2-1?中顯示?0x1BA0?位置的內(nèi)存數(shù)據(jù)為?00 03 04 78 14 13 08 1A 3D 68 FC FC 68 3D 1A 00?八位一行?排列和出來(lái)就是一個(gè)外星人帶著一個(gè)顛倒字母 “Y” 的圖片(圖中的內(nèi)容看起來(lái)像是旋轉(zhuǎn)了 90 度這是因?yàn)閳D片是一列一列存儲(chǔ)的,每 8 bit 代表一列像素)。

si78c 的作者在顯示圖片的時(shí)候直接將 X Y 軸進(jìn)行了交換以達(dá)到旋轉(zhuǎn)圖片的效果。

我們可以找到名為?Mem?的結(jié)構(gòu)體,其中的?m.vram?(0x2400?到?0x3FFF)模擬了街機(jī)的顯存,這里面每一個(gè) bit 代表一個(gè)像素的黑(0)白(1),從左下角向右上角進(jìn)行渲染,其對(duì)應(yīng)關(guān)系如圖 2-2

圖 2-2游戲中所有跟動(dòng)畫繪制有關(guān)的代碼都是在修改這部分區(qū)域的數(shù)據(jù),例如?DrawChar()、ClearPlayField()、?DrawSimpSprite()?等等。那么怎么讓模擬現(xiàn)存的內(nèi)容顯示到玩家的屏幕上呢?注意看代碼 3-1?中在循環(huán)的末尾調(diào)用了?render()?函數(shù),它負(fù)責(zé)的就挨個(gè)讀取模擬顯存中的內(nèi)容并在窗口上有像素塊的地方渲染一個(gè)像素塊。

仔細(xì)想想不難發(fā)現(xiàn),這種先修改模擬顯存再統(tǒng)一繪制的方法其實(shí)沒(méi)有多省事,甚至有些怪異。這是因?yàn)?si78c 模擬了街機(jī)硬件的顯示過(guò)程:修改相應(yīng)的顯存然后硬件會(huì)自動(dòng)將顯存中的內(nèi)容顯示到屏幕上。

2.5 按鍵檢測(cè)

代碼 3-1?中的?input()?函數(shù)負(fù)責(zé)檢測(cè)并存儲(chǔ)用戶的按鍵信息,其底層依賴?SDL?庫(kù)。

三、首次啟動(dòng)

si78c?和所有的 C 程序一樣,都是從?main()?函數(shù)開始運(yùn)行:代碼 3-1int?main(int?argc,?char?**argv)
{
????//?初始化?SDL?和?游戲窗口
????init_renderer();
????//?初始化游戲
????init_game();
????int?credit?=?0;
????size_t?frame?=?-1;
????//?開始游戲協(xié)程調(diào)度與模擬觸發(fā)中斷
????while?(1)
????{
????????frame++;
????????//?處理按鍵輸入
????????input();
????????//?如果退出標(biāo)志置位推出循環(huán)清理游戲內(nèi)存
????????if?(exited)
????????????break;
????????//?preserves?timing?compatibility?with?MAME
????????//?保留與?MAME(一種街機(jī))?的時(shí)序兼容性
????????if?(frame?==?1)
????????????credit--;
????????/**
?????????*??執(zhí)行其他進(jìn)程大概?CRED1?的時(shí)間
?????????*?(為什么是這個(gè)數(shù)我也不知道,應(yīng)該是估計(jì)值)
?????????*?(原作者也說(shuō)這種定時(shí)方法不是很準(zhǔn)確但不影響游戲效果)
?????????*/
????????credit?+=?CRED1;
????????loop_core(&credit);
????????//?設(shè)置場(chǎng)中間中斷標(biāo)志位,在下面的?loop_core()?中會(huì)切換到?int_ctx?執(zhí)行一次,然后清除標(biāo)志位
????????irq(0xcf);
????????//?道理同上
????????credit?+=?CRED2;
????????loop_core(&credit);
????????//?設(shè)置垂直消隱中斷標(biāo)志位,下個(gè)循環(huán)時(shí)候?loop_core()?中會(huì)切換到?int_ctx?執(zhí)行一次,然后清除標(biāo)志位
????????irq(0xd7);
????????//?繪制游戲界面
????????render();
????}
????fini_game();
????fini_renderer();
????return?0;
}
啟動(dòng)過(guò)程如圖所示:

圖 3-1游戲原版代碼(8080 匯編)使用的是中斷驅(qū)動(dòng)(這種編程方式和硬件有關(guān),具體內(nèi)容可以自行了解什么是?中斷)配合協(xié)程多任務(wù)操作。為了模擬原版游戲邏輯作者以?main()?中大循環(huán)作為硬件行為模擬中心(實(shí)現(xiàn)中斷管理、協(xié)程切換、屏幕渲染)。游戲大約三分之一的時(shí)間在運(yùn)行?主線程,主線程?會(huì)被?midscreen?和?vblank?兩個(gè)中斷搶占,代碼 3-1?中兩個(gè)?irq()?就實(shí)現(xiàn)了對(duì)中斷的模擬(設(shè)置對(duì)應(yīng)的變量作為標(biāo)志位)。在?第一次?進(jìn)入?loop_core()?時(shí)其流程如下:

圖 3-2




因?yàn)?yield_rason?這個(gè)變量是?static?類型其默認(rèn)值為零

代碼 3-2//?根據(jù)游戲狀態(tài)標(biāo)志切換到相應(yīng)的上下文
static?int?execute(int?allowed)
{
????int64_t?start?=?ticks;
????ucontext_t?*next?=?NULL;
????switch?(yield_reason)
????{
????//?剛啟動(dòng)時(shí)?yield_reason?是?0?表示?YIELD_INIT
????case?YIELD_INIT:
????//?當(dāng)需要延遲的時(shí)候會(huì)調(diào)用?timeslice()?將?yield_reason?切換為?YIELD_TIMESLICE
????//?模擬時(shí)間片輪轉(zhuǎn),這個(gè)時(shí)候會(huì)切換回上一個(gè)運(yùn)行的任務(wù)(統(tǒng)共就倆協(xié)程),實(shí)現(xiàn)時(shí)間片輪轉(zhuǎn)
????case?YIELD_TIMESLICE:
????????next?=?prev_ctx;
????????break;
????case?YIELD_INTFIN:
????????//?處理完中斷后讓?int_ctx?休眠,重新運(yùn)行?main_ctx
????????next?=?&main_ctx;
????????break;
????//?玩家死亡、等待開始、外星人入侵狀態(tài)
????case?YIELD_PLAYER_DEATH:
????case?YIELD_WAIT_FOR_START:
????case?YIELD_INVADED:
????????init_threads(yield_reason);
????????enable_interrupts();
????????next?=?&main_ctx;
????????break;
????//?退出游戲
????case?YIELD_TILT:
????????init_threads(yield_reason);
????????next?=?&main_ctx;
????????break;
????default:
????????assert(FALSE);
????}
????yield_reason?=?YIELD_UNKNOWN;
????//?如果有中斷產(chǎn)生
????if?(allowed?&&?interrupted())
????{
????????next?=?&int_ctx;
????}
????switch_to(next);
????return?ticks?-?start;
}
需要注意的是,在?execute()?中進(jìn)行了協(xié)程的切換,這個(gè)時(shí)候?execute()?的運(yùn)行狀態(tài)就被保存在了變量?frontend_ctx?之中,指針?prev_ctx?更新為指向?frontend_ctx,指針?curr_ctx?更新為指向?main_ctx,其過(guò)程如圖所示:

圖 3-3



實(shí)現(xiàn)解釋請(qǐng)見代碼 2-2

當(dāng)?execute()?返回時(shí)他會(huì)按照正常的執(zhí)行流程返回到?loop_core(),就像它從未被暫停過(guò)一樣。仔細(xì)觀察?main_init?中主循環(huán)我們可以發(fā)現(xiàn)其多次調(diào)用?timeslice()?函數(shù)(例如?OneSecDelay()?中),通過(guò)這個(gè)函數(shù)我們就可以實(shí)現(xiàn)?main_ctx?與?frontend_ctx?間的時(shí)間片輪轉(zhuǎn)操作,其過(guò)程如下:


圖 3-4在?main_init()?中主要做了如下事情:


在玩家投幣前,游戲會(huì)依靠?main_init()?循環(huán)播放動(dòng)畫吸引玩家

如果只翻看?main_init()?中出現(xiàn)的函數(shù)我們會(huì)發(fā)現(xiàn)代碼中并未涉及太多的游戲邏輯,例如外星人移動(dòng)、射擊,玩家投幣檢查等內(nèi)容好像根本不存在一樣,更多的時(shí)候是在操縱內(nèi)存、設(shè)置標(biāo)志位。那么有關(guān)游戲游戲邏輯處理相關(guān)的函數(shù)又在哪里呢?這部分內(nèi)容將在下面揭秘。

四、模擬中斷

在?代碼 3-1?中?loop_core()?函數(shù)被兩個(gè)?irq()?分隔了開來(lái)。我們之前提到?main()?中的大循環(huán)本質(zhì)上是在模擬街機(jī)的硬件行為,在真實(shí)的機(jī)器上中斷是只有在觸發(fā)時(shí)才會(huì)執(zhí)行,但在 si78c 上我們只能通過(guò)在?loop_core()?之間調(diào)用?irq()?來(lái)模擬產(chǎn)生中斷并在?execute()?中輪詢中斷狀態(tài)來(lái)判斷是不是進(jìn)入中斷處理函數(shù),過(guò)程如下:


這時(shí)它的協(xié)程狀態(tài)如下:



有兩種中斷:midscreen_int()?與?vblank_int()?這兩種中斷會(huì)輪流出現(xiàn)。代碼 4-1//?處理中斷的函數(shù)
static?void?run_int_ctx()
{
????while?(1)
????{
????????//?0xcf?=?RST?1?opcode?(call?0x8)
????????//?0xd7?=?RST?2?opcode?(call?0x16)
????????if?(irq_vector?==?0xcf)
????????????midscreen_int();
????????else?if?(irq_vector?==?0xd7)
????????????vblank_int();
????????//?使能中斷
????????enable_interrupts();
????????yield(YIELD_INTFIN);
????}
}
我們先來(lái)看?midscreen_int()代碼 4-2/**
?*?在光將要擊中屏幕中間(應(yīng)該是模擬老式街機(jī)的現(xiàn)實(shí)原理)時(shí)由中斷觸發(fā)
?*?主要處理游戲?qū)ο蟮囊苿?dòng)、開火、碰撞等等的檢測(cè)更新與繪制(具體看函數(shù)?GameObj0到4)
?*?以及確定下一個(gè)將要繪制哪個(gè)外星人,檢測(cè)外星人是不是入侵成功了
?*/
static?void?midscreen_int()
{
????//?更新?vblank?標(biāo)志位
????m.vblankStatus?=?BEAM_MIDDLE;
????//?如果沒(méi)有運(yùn)動(dòng)的游戲?qū)ο螅祷?br>????if?(m.gameTasksRunning?==?0)
????????return;
????//?在歡迎界面?且?沒(méi)有在演示模式,返回(只在游戲模式?和?demo模式下繼續(xù)運(yùn)行)
????if?(!m.gameMode?&&?!(m.isrSplashTask?&?0x1))
????????return;
????//?運(yùn)行?game?objects?但是略過(guò)第一個(gè)入口(玩家)
????RunGameObjs(u16_to_ptr(PLAYER_SHOT_ADDR));
????//?確定下一個(gè)將要繪制的外星人
????CursorNextAlien();
}
在這一部分中?RunGameObjs()?函數(shù)基本上包括了玩家的移動(dòng)和繪制,玩家子彈和外星人子彈的移動(dòng)、碰撞檢測(cè)、繪制等等所有游戲邏輯的處理,CursorNextAlien()?則找到要繪制的下一個(gè)活著的外星人設(shè)置標(biāo)志位等待繪制,并且檢測(cè)外星飛船是否碰到了屏幕底端。運(yùn)行結(jié)束后會(huì)返回到?run_int_ctx()?繼續(xù)運(yùn)行直到?yield(YIELD_INTFIN)?表示協(xié)程切換回?execute(),并在?execute()?中重新將?next?設(shè)定為?main_ctx?使?main_init()?能夠繼續(xù)運(yùn)行(詳情見代碼 3-2)。接下來(lái)是?vblank_int()代碼 4-3/**?
?*?當(dāng)光擊中屏幕最后一點(diǎn)(模擬老式街機(jī)原理)時(shí)觸發(fā)
?*?主要處理游戲結(jié)束、投幣、游戲中各種事件處理、播放演示動(dòng)畫
?*/
static?void?vblank_int()
{
????//?更新標(biāo)志位
????m.vblankStatus?=?BEAM_VBLANK;
????//?計(jì)時(shí)器減少
????m.isrDelay--;
????//?看看是不是結(jié)束游戲
????CheckHandleTilt();
????//?看看是不是投幣了
????vblank_coins();
????//?如果游戲任務(wù)沒(méi)有運(yùn)行,返回
????if?(m.gameTasksRunning?==?0)
????????return;
????//?如果在游戲中的話
????if?(m.gameMode)
????{
????????TimeFleetSound();
????????m.shotSync?=?m.rolShotHeader.TimerExtra;
????????DrawAlien();
????????RunGameObjs(u16_to_ptr(PLAYER_ADDR));
????????TimeToSaucer();
????????return;
????}
????//?如果投幣過(guò)了
????if?(m.numCoins?!=?0)
????{
????????//?xref?005d
????????if?(m.waitStartLoop)
????????????return;
????????m.waitStartLoop?=?1;
????????//?切換協(xié)程到等待開始循環(huán)
????????yield(YIELD_WAIT_FOR_START);
????????assert(FALSE);?//?不會(huì)再返回了
????}
????//?如果以上事情都沒(méi)發(fā)生,播放演示動(dòng)畫
????ISRSplTasks();
}
其主要作用一是檢測(cè)玩家是否想要退出游戲或是進(jìn)行了投幣操作,如果已經(jīng)處于游戲模式中則依次播放艦隊(duì)聲音、繪制在?midscreen_int()?中標(biāo)記出的外星人、運(yùn)行?RunGameObjs()?處理玩家和外星人開火與移動(dòng)事件、TimeToSaucer()?隨機(jī)生成神秘飛碟。如果未在游戲模式中則進(jìn)入?ISRSplTasks()?調(diào)整當(dāng)前屏幕上應(yīng)該播放的動(dòng)畫。我們可以注意到,如果玩家進(jìn)行了投幣會(huì)進(jìn)入?if (m.numCoins != 0)?里,并調(diào)用?yield(YIELD_WAIT_FOR_START)?后面會(huì)提示這個(gè)函數(shù)不會(huì)再返回。在 si78c 的代碼中許多地方都會(huì)有這樣的提示,這里并不是簡(jiǎn)單的調(diào)用一個(gè)不會(huì)返回的函數(shù)進(jìn)行套娃。觀察?代碼 3-2?可以發(fā)現(xiàn)在?YIELD_PLAYER_DEATH、YIELD_WAIT_FOR_START、YIELD_INVADED、YIELD_TILT?這四種分支中都調(diào)用了?init_threads(yield_reason),在這個(gè)函數(shù)里會(huì)重置?int_ctx?與?main_ctx?的堆棧并重新綁定調(diào)用?run_main_ctx?時(shí)的參數(shù)為?yield_reason,這樣在下一次執(zhí)行的時(shí)候 ?run_main_ctx?就會(huì)根據(jù)中斷的指示跳轉(zhuǎn)到合適的分支去運(yùn)行。

五、巧妙地節(jié)省 RAM

開篇的時(shí)候提到過(guò),當(dāng)年街機(jī)的 RAM 只有可憐的 1kb 大小,這樣小的地方必定無(wú)法讓我們存儲(chǔ)屏幕上每個(gè)對(duì)象的信息,但是玩家的位置、外星人的位置以及它們的子彈、屏幕上的盾牌損壞情況都是會(huì)實(shí)時(shí)更新的,如何做到這一點(diǎn)呢?我發(fā)現(xiàn)《太空侵略者》游戲區(qū)域內(nèi)容分布還是很有規(guī)律的,特殊飛船(飛碟)只會(huì)出現(xiàn)在屏幕上端,盾牌和玩家的位置不會(huì)改變,只有子彈的位置不好把握,所以仔細(xì)研讀代碼,從?DrawSpriteGeneric()?可以看出,游戲?qū)τ谂鲎驳臋z測(cè)只是簡(jiǎn)單的判斷像素塊是否重合,對(duì)于玩家子彈到底擊中了什么在?PlayerShotHit()?函數(shù)進(jìn)行判斷時(shí),則只需要判斷子彈垂直方向坐標(biāo)(Y坐標(biāo)),如果 >= 216 則是撞到上頂,>=206 則是擊中神秘飛碟,其他則是擊中護(hù)盾或者外星人的子彈。且由于外星飛船的是成組一起運(yùn)動(dòng),只需要記住其中一個(gè)的位置就能推算出整體每一個(gè)外星飛船的坐標(biāo)。這樣算下來(lái),程序只需要保存外星飛船的存活狀態(tài)、當(dāng)前艦隊(duì)的相對(duì)移動(dòng)位置、玩家和外星人子彈信息,在需要檢測(cè)碰撞時(shí)則去讀取顯存中的像素信息進(jìn)行對(duì)比然后反推當(dāng)前時(shí)哪兩樣物體發(fā)生了碰撞即可,這種方法相比存儲(chǔ)每一個(gè)對(duì)象的信息節(jié)省了不少資源。

六、結(jié)語(yǔ)

si78c?不同于其他代碼,它本質(zhì)上是對(duì)硬件和匯編代碼的仿真,希望通過(guò)本文的源碼講解,讓更多人看到當(dāng)年程序員們?cè)谟邢拶Y源下制作出優(yōu)秀游戲的困難,還有代碼設(shè)計(jì)的精妙。最后,感謝本項(xiàng)目作者所做的一切,沒(méi)有他的付出也就不會(huì)有這篇文章。如果您覺(jué)得這篇文章還不錯(cuò),歡迎分享給更多人。


重玩 40 年前的經(jīng)典游戲小蜜蜂,這次通關(guān)了源碼(轉(zhuǎn)載)的評(píng)論 (共 條)

分享到微博請(qǐng)遵守國(guó)家法律
南靖县| 浑源县| 河间市| 萝北县| 盈江县| 合阳县| 当涂县| 盈江县| 九寨沟县| 绍兴市| 湟中县| 文登市| 澄城县| 松潘县| 靖州| 溆浦县| 延庆县| 安泽县| 三门峡市| 丹阳市| 舟曲县| 宿迁市| 土默特左旗| 五家渠市| 湘潭县| 固镇县| 云浮市| 远安县| 沁源县| 玛沁县| 大英县| 延川县| 华安县| 汨罗市| 滨州市| 桐梓县| 吉木萨尔县| 盐山县| 榆社县| 通州市| 县级市|