国产精品天干天干,亚洲毛片在线,日韩gay小鲜肉啪啪18禁,女同Gay自慰喷水

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

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

2021-12-08 10:58 作者:HelloGitHub  | 我要投稿

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


這里是 HelloGitHub 推出的《講解開(kāi)源項(xiàng)目》系列,本期為您講解的是 80、90 后的兒時(shí)記憶,誕生于 1978 年經(jīng)典街機(jī)游戲《太空侵略者》也叫“小蜜蜂”的 C 語(yǔ)言復(fù)刻版——si78c。


這款游戲在當(dāng)時(shí)可謂是風(fēng)靡一時(shí),相信很多朋友小時(shí)候都玩過(guò)?,F(xiàn)在長(zhǎng)大了,不知道有多少朋友對(duì)它的源碼感興趣呢!

原版的《太空侵略者》由大約 2k 行的 8080 匯編代碼寫(xiě)成,但匯編語(yǔ)言太過(guò)底層不方便閱讀,今天講解的開(kāi)源項(xiàng)目 si78c 是按照原版匯編代碼用 C 語(yǔ)言重寫(xiě)了一遍,并最大程度還原了原版街機(jī)硬件的中斷、協(xié)程邏輯,在運(yùn)行時(shí)其內(nèi)存狀態(tài)也幾乎與原始版本相同 幾乎達(dá)到了完美的復(fù)刻,著實(shí)讓我眼前一亮!

下面就請(qǐng)跟著 HelloGitHub 一起抽絲剝繭,運(yùn)行這個(gè)開(kāi)源項(xiàng)目、閱讀源碼,穿越歷史感受 40 年前游戲設(shè)計(jì)的精妙之處!


一、快速開(kāi)始

本文的實(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 文件

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


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

使用 make 進(jìn)行編譯:

$ make

之后會(huì)在 bin 文件夾中生成可執(zhí)行文件,運(yùn)行即可啟動(dòng)游戲:

$ ./bin/si78c

游戲操控按鍵如下:


二、 前置知識(shí)

2.1 簡(jiǎn)介

《太空侵略者》原版代碼運(yùn)行在 8080 處理器之上,其內(nèi)容全部由匯編代碼寫(xiě)成并涉及一些硬件操作,為了模擬原版街機(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


之后每次調(diào)用 yield() 都會(huì)使用 swapcontext() 進(jìn)行兩個(gè)協(xié)程間切換:

代碼 2-2


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

由于文章篇幅有限,下面只展示的關(guān)鍵源碼部分。

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)畫(huà)顯示原理以前,我們需要先了解一下游戲的素材是怎么存儲(chǔ)的:


圖 2-1

圖片來(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.vram0x24000x3FFF)模擬了街機(jī)的顯存,這里面每一個(gè) bit 代表一個(gè)像素的黑(0)白(1),從左下角向右上角進(jìn)行渲染,其對(duì)應(yīng)關(guān)系如圖 2-2


圖 2-2

游戲中所有跟動(dòng)畫(huà)繪制有關(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ù)開(kāi)始運(yùn)行:

代碼 3-1


啟動(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ì)被 midscreenvblank 兩個(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


需要注意的是,在 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)見(jiàn)代碼 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_ctxfrontend_ctx 間的時(shí)間片輪轉(zhuǎn)操作,其過(guò)程如下:


圖 3-4

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


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

如果只翻看 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-1loop_core() 函數(shù)被兩個(gè) irq() 分隔了開(kāi)來(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


我們先來(lái)看 midscreen_int()

代碼 4-2


在這一部分中 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)行(詳情見(jiàn)代碼 3-2)。

接下來(lái)是 vblank_int()

代碼 4-3


其主要作用一是檢測(cè)玩家是否想要退出游戲或是進(jìn)行了投幣操作,如果已經(jīng)處于游戲模式中則依次播放艦隊(duì)聲音、繪制在 midscreen_int() 中標(biāo)記出的外星人、運(yùn)行 RunGameObjs() 處理玩家和外星人開(kāi)火與移動(dòng)事件、TimeToSaucer() 隨機(jī)生成神秘飛碟。如果未在游戲模式中則進(jìn)入 ISRSplTasks() 調(diào)整當(dāng)前屏幕上應(yīng)該播放的動(dòng)畫(huà)。

我們可以注意到,如果玩家進(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_STARTYIELD_INVADED、YIELD_TILT 這四種分支中都調(diào)用了 init_threads(yield_reason),在這個(gè)函數(shù)里會(huì)重置 int_ctxmain_ctx 的堆棧并重新綁定調(diào)用 run_main_ctx 時(shí)的參數(shù)為 yield_reason,這樣在下一次執(zhí)行的時(shí)候 run_main_ctx 就會(huì)根據(jù)中斷的指示跳轉(zhuǎn)到合適的分支去運(yùn)行。

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

開(kāi)篇的時(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)了源碼的評(píng)論 (共 條)

分享到微博請(qǐng)遵守國(guó)家法律
上思县| 虎林市| 无为县| 蒲江县| 宁河县| 正蓝旗| 卢湾区| 淳化县| 忻州市| 湄潭县| 华亭县| 绥芬河市| 年辖:市辖区| 班戈县| 游戏| 泗洪县| 东辽县| 马鞍山市| 朔州市| 繁峙县| 辛集市| 宁国市| 报价| 吉木乃县| 胶州市| 高尔夫| 乌拉特后旗| 准格尔旗| 密山市| 丽水市| 潼关县| 嘉峪关市| 稻城县| 平邑县| 张北县| 呼图壁县| 竹北市| 怀化市| 利津县| 邻水| 平湖市|