本文適合有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ò)。現(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)境為Ubuntu20.04LTS,GCC版本大于GCC3
1.準(zhǔn)備工作首先si78c使用SDL2繪制游戲窗口,所以需要安裝依賴:
$sudoapt-getinstalllibsdl2-dev然后從倉(cāng)庫(kù)下載源碼:
$gitclonehttps://github.com/loadzero/si78c.git此外,該項(xiàng)目會(huì)從原版的ROM中提取原版游戲的圖片、字體,所以還需要下載原版的ROM文件
2.文件結(jié)構(gòu)在si78c源碼文件夾中新建名為inv1和bin的文件夾
$cdsi78c-master$mkdirinv1bin然后將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.h3.編譯與運(yùn)行使用make進(jìn)行編譯:
$make之后會(huì)在bin文件夾中生成可執(zhí)行文件,運(yùn)行即可啟動(dòng)游戲:
$./bin/si78c游戲操控按鍵如下:
aLEFT(左移)dRIGHT(右移)11P(單人)22P(雙人)jFIRE(射擊)5COIN(投幣)tTILT(結(jié)束游戲)二、前置知識(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
//切換協(xié)程時(shí)用的中間變量staticucontext_tfrontend_ctx;//游戲主要邏輯協(xié)程staticucontext_tmain_ctx;//游戲中斷邏輯協(xié)程staticucontext_tint_ctx;//用于切換兩個(gè)協(xié)程staticucontext_t*prev_ctx;staticucontext_t*curr_ctx;//初始化游戲協(xié)程staticvoidinit_threads(YieldReasonentry_point){//獲取當(dāng)前上下文,存儲(chǔ)在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;//設(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é)程切換開(kāi)銷更小,用起來(lái)更加輕便*///獲取當(dāng)前上下文存儲(chǔ)在init_ctx中rc=getcontext(&int_ctx);//指定棧空間int_ctx.uc_stack.ss_sp=&int_ctx_stack;//指定棧空間大小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é)程切換開(kāi)銷更小,用起來(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-2
staticvoidyield(YieldReasonreason){//調(diào)度原因yield_reason=reason;//調(diào)度到另一個(gè)協(xié)程上switch_to(&frontend_ctx);}//協(xié)程切換函數(shù)staticvoidswitch_to(ucontext_t*to){//給co_switch包裝了一層,簡(jiǎn)化了代碼量co_switch(curr_ctx,to);}//協(xié)程切換函數(shù)staticvoidco_switch(ucontext_t*prev,ucontext_t*next){prev_ctx=prev;curr_ctx=next;//切換到next指向的上下文,將當(dāng)前上下文保存在prev中swapcontext(prev,next);}具體用法請(qǐng)見(jiàn)后文
由于文章篇幅有限,下面只展示的關(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-1FFF8KROM2000-23FF1KRAM2400-3FFF7KVideoRAM4000-RAMmirror可以看出當(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ù)為000304781413081A3D68FCFC683D1A00八位一行排列和出來(lái)就是一個(gè)外星人帶著一個(gè)顛倒字母“Y”的圖片(圖中的內(nèi)容看起來(lái)像是旋轉(zhuǎn)了90度這是因?yàn)閳D片是一列一列存儲(chǔ)的,每8bit代表一列像素)。
si78c的作者在顯示圖片的時(shí)候直接將XY軸進(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)畫(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)閟i78c模擬了街機(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
intmain(intargc,char**argv){//初始化SDL和游戲窗口init_renderer();//初始化游戲init_game();intcredit=0;size_tframe=-1;//開(kāi)始游戲協(xié)程調(diào)度與模擬觸發(fā)中斷while(1){frame++;//處理按鍵輸入input();//如果退出標(biāo)志置位推出循環(huán)清理游戲內(nèi)存if(exited)break;//preservestimingcompatibilitywithMAME//保留與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();return0;}啟動(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)閥ield_rason這個(gè)變量是static類型其默認(rèn)值為零
代碼3-2
//根據(jù)游戲狀態(tài)標(biāo)志切換到相應(yīng)的上下文staticintexecute(intallowed){int64_tstart=ticks;ucontext_t*next=NULL;switch(yield_reason){//剛啟動(dòng)時(shí)yield_reason是0表示YIELD_INITcaseYIELD_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)caseYIELD_TIMESLICE:next=prev_ctx;break;caseYIELD_INTFIN://處理完中斷后讓int_ctx休眠,重新運(yùn)行main_ctxnext=&main_ctx;break;//玩家死亡、等待開(kāi)始、外星人入侵狀態(tài)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;//如果有中斷產(chǎn)生if(allowed&&interrupted()){next=&int_ctx;}switch_to(next);returnticks-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)見(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_ctx與frontend_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-1中l(wèi)oop_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
//處理中斷的函數(shù)staticvoidrun_int_ctx(){while(1){//0xcf=RST1opcode(call0x8)//0xd7=RST2opcode(call0x16)if(irq_vector==0xcf)midscreen_int();elseif(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)、開(kāi)火、碰撞等等的檢測(cè)更新與繪制(具體看函數(shù)GameObj0到4)*以及確定下一個(gè)將要繪制哪個(gè)外星人,檢測(cè)外星人是不是入侵成功了*/staticvoidmidscreen_int(){//更新vblank標(biāo)志位m.vblankStatus=BEAM_MIDDLE;//如果沒(méi)有運(yùn)動(dòng)的游戲?qū)ο螅祷豬f(m.gameTasksRunning==0)return;//在歡迎界面且沒(méi)有在演示模式,返回(只在游戲模式和demo模式下繼續(xù)運(yùn)行)if(!m.gameMode&&!(m.isrSplashTask&0x1))return;//運(yùn)行g(shù)ameobjects但是略過(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)行直到y(tǒng)ield(YIELD_INTFIN)表示協(xié)程切換回execute(),并在execute()中重新將next設(shè)定為main_ctx使main_init()能夠繼續(xù)運(yùn)行(詳情見(jiàn)代碼3-2)。
接下來(lái)是vblank_int():
代碼4-3
/***當(dāng)光擊中屏幕最后一點(diǎn)(模擬老式街機(jī)原理)時(shí)觸發(fā)*主要處理游戲結(jié)束、投幣、游戲中各種事件處理、播放演示動(dòng)畫(huà)*/staticvoidvblank_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){//xref005dif(m.waitStartLoop)return;m.waitStartLoop=1;//切換協(xié)程到等待開(kāi)始循環(huán)yield(YIELD_WAIT_FOR_START);assert(FALSE);//不會(huì)再返回了}//如果以上事情都沒(méi)發(fā)生,播放演示動(dòng)畫(huà)ISRSplTasks();}其主要作用一是檢測(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_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開(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ò),歡迎分享給更多人。