作者 | Robin Allen
譯者 | 彎月? ? ? ?責(zé)編 | 屠敏
(資料圖)
出品 | CSDN(ID:CSDNnews)
2020 年 Adobe 結(jié)束了 Flash Player 的生命,但我不想讓我的 Flash 游戲就此消失。
我一直在斷斷續(xù)續(xù)地開發(fā)游戲,最受歡迎的是一款名為 Hapland 的游戲,所以我覺得把它移植到 Steam 上應(yīng)該會不錯。我可以繪制更好的圖像,改進(jìn)刷新率和分辨率,并添加一些額外的隱藏任務(wù)。
Hapland 2
但問題是,這款游戲本身是 Flash 游戲。圖像是用 Flash 畫的,代碼是用 Flash 編寫的,所有動畫都是用 Flash 的時間線動畫制作的。可以說整個游戲完全建立在 Flash 之上。
該怎么辦呢?
失敗的嘗試
首先,我試著用 Flash 導(dǎo)出游戲,生成可執(zhí)行文件。如果成功的話,本文到這里就可以結(jié)束了,但很不幸的是我并沒有成功,因為導(dǎo)出后游戲的性能一夜回到 2005 年。我想按照現(xiàn)代的幀速率來運(yùn)行,同時也希望擺脫 Flash 播放器。
其次,我花了很多時間研究 Adobe AIR。這是 Flash 和 Starling 的桌面運(yùn)行時,后者是在 GPU 上繪制 Flash 場景的庫。
最后我放棄了,部分原因是 AIR 有很多 bug,而且很難用,但主要原因是我不想用一個奇怪的 Adobe 軟件作為最終解決方案,我希望能完全用我自己的技術(shù)來代替??紤]到以后可能會移植到 Linux,我不想受 Adobe 的限制。
所以目標(biāo)就很明確了:我需要編寫自己的 Flash 播放器。
計劃
下面,我簡要介紹一下這款游戲的構(gòu)建。所有精靈以樹形結(jié)構(gòu)組織。在 Flash 中,動畫精靈可以在某些幀上添加代碼,當(dāng)播放到該幀時就會運(yùn)行這段代碼。這款游戲使用了許多這種代碼。游戲角色的行走路線只不過是時間線很長的動畫而已,角色上經(jīng)常會有幀動作,比如當(dāng)?shù)竭_(dá)門口時,如果門是關(guān)著的,就把門打開,或者當(dāng)遇到地雷時,如果地雷還沒有爆炸,就引爆。
時間線上的“a”標(biāo)記表示幀動作
幸運(yùn)的是,.fla文件只不過是XML而已。我需要解析該文件,將相關(guān)的數(shù)據(jù)導(dǎo)出到一個自定義的格式中,然后編寫播放器來讀取、描繪場景、處理輸入,然后運(yùn)行動畫。我還需要處理ActionScript。
最終這依然是一個Flash項目,用Flash編輯器編寫和維護(hù),只是更換了Flash播放器而已。
向量圖柵格化
Flash中的圖形都是向量圖。雖然 Flash 支持位圖,但本身是為向量圖設(shè)計的。因此Flash動畫在當(dāng)年的撥號連接上都能快速加載。這款游戲的所有圖形都是向量圖。
但是GPU不喜歡向量圖。它們擅長處理大量的帶有材質(zhì)的三角形。所以,我需要將向量圖柵格化。
我決定進(jìn)行離線柵格化,然后將柵格化后的文件打包到游戲中。在運(yùn)行時進(jìn)行柵格化應(yīng)該很有意思,而且能保持小體積的可執(zhí)行文件,但我不想在游戲中加入額外的處理。我希望盡可能多地讓代碼在我自己的機(jī)器上運(yùn)行,這樣可以保證它們不出問題。
Flash的向量圖保存在XML文件中。你也許會說,XML并不適合保存圖形數(shù)據(jù),但畢竟這是Macromedia的產(chǎn)品設(shè)計師決定的。
.fla文件中保存的向量數(shù)據(jù)
我并不是在抱怨,相反,我的工作因此更輕松了一些。
即使我沒有具體的規(guī)格,對其進(jìn)行柵格化也并不困難。向量圖的貝賽爾曲線模型從PostScript誕生以來就沒有變過。所有API的工作方式也和當(dāng)年一樣。經(jīng)過一番嘗試后,我弄清楚了 ! 和 [ 等符號的意義,于是寫了一個程序來解析這些形狀定義,并利用Mac的CoreGraphics將其渲染成PNG圖像。
選擇CoreGraphics讓我頗為猶豫了一番。我選擇它主要是因為我在Mac上開發(fā),而Mac恰好提供了這個庫,我又不想麻煩使用其他的依賴。但這個選擇導(dǎo)致只能在Mac上進(jìn)行柵格化,即使是構(gòu)建Windows版也不得不這樣做。如果可以重來一次,我會選擇一個跨平臺的庫。
在渲染好PNG之后,導(dǎo)出程序?qū)⑦@些PNG圖像拼成一張精靈圖。方法很簡單,只是將所有圖像按照尺寸排序,然后逐行排列在一起而已。這遠(yuǎn)遠(yuǎn)不是最佳方案,但已經(jīng)足夠好了。
為了保持簡單,精靈圖的尺寸為2048x2048像素,是OpenGL 3.2支持的最小材質(zhì)尺寸。
游戲中的一張精靈圖
光柵化非常慢,所以為了保證構(gòu)建時間不會太長,我需要跳過沒有變化的部分。Flash使用的壓縮后的XML文件中有一個字段表示最終變更時間,但Flash似乎并未正確使用該字段,所以無法依靠它。
于是我計算了每個圖形的XML的哈希值,僅在哈希值發(fā)生變化的時候進(jìn)行構(gòu)建。即使是這樣,有時也會出問題,因為Flash有時候會重新排列XML標(biāo)簽,即使圖像沒有任何變化。但同樣,這樣做已經(jīng)足夠好了。
使用組裝程序?qū)懭攵M(jìn)制文件
導(dǎo)出程序?qū)赢嫈?shù)據(jù)寫入到一個自定義的二進(jìn)制格式中。該程序只是逐幀遍歷時間線,然后將每幀的變化都寫出來。
這里,我想出的一個方法是將數(shù)據(jù)導(dǎo)出成匯編代碼,而不是二進(jìn)制文件。當(dāng)然匯編代碼并不是CPU指令,只是數(shù)據(jù)而已。這樣調(diào)試可以更容易一些,因為我可以翻閱匯編文件,查看生成的內(nèi)容,而不需要通過二進(jìn)制編輯器來查看每個字節(jié)。
哪個更容易調(diào)試?
我可以選擇讓導(dǎo)出程序?qū)⒆止?jié)碼寫到一個文件中,同時將文本代碼寫到另一個文件中,但我并沒有這樣做,而是選擇了匯編器,因為(1)我已經(jīng)有匯編器了;(2)不需要再調(diào)試匯編器;(3)匯編器支持標(biāo)簽。
導(dǎo)出程序的其余部分就沒什么意思了,只不過是遍歷整個樹,然后轉(zhuǎn)換變換矩陣、顏色效果等數(shù)據(jù)。最后再進(jìn)一步轉(zhuǎn)換游戲程序本身。我選擇C++編寫導(dǎo)出程序的原因只是我熟悉C++而已。
場景圖
這款游戲非常適合使用場景圖。場景圖是Flash采用的模型,游戲就圍繞著場景圖設(shè)計,所以沒有道理換成其他模型。
我將場景作為樹的節(jié)點(diǎn)保存在內(nèi)存中,每個節(jié)點(diǎn)有一個變換,可以繪制自己,并接受鼠標(biāo)點(diǎn)擊事件。每個游戲?qū)ο蠖紦碛凶约旱男袨?,是自身的類的一個實例,這些類是從Node類繼承而來的。雖然在游戲設(shè)計圈,面向?qū)ο笠呀?jīng)不流行,但因為我用的是Flash,所以并不在乎。
游戲使用的Flash特性(如顏色變換、遮罩等)都是現(xiàn)成的,盡管我實現(xiàn)的遮罩并不支持任意形狀的遮罩,而只是矩形裁切而已,然后我將所有圖形都編輯成了矩形,這樣所有遮罩都用矩形就可以了。
幀腳本
這款游戲幾乎所有的邏輯都是用ActionScript編寫的附著在時間線上的幀腳本。這些要怎么導(dǎo)出?我不想在游戲中包含ActionScript解釋器。
一個簡單的幀動作
將所有的幀腳本轉(zhuǎn)換成C++之后,就可以在編譯時將其提取出來,變成每個符號的Node子類中的方法。同時還會生成一個負(fù)責(zé)分發(fā)的方法,負(fù)責(zé)在正確的時機(jī)調(diào)用這些方法。這個分發(fā)方法大致如下:
我想說明的最后一點(diǎn)是,最后生成的腳本是靜態(tài)類型的,這一點(diǎn)很好,因為ActionScript本身并沒有類型。而導(dǎo)出程序生成的游戲?qū)ο蟠笾氯缦拢?/p>
所以,雖然底層依然是一大堆字符串查找,但這一層類型安全能夠防止在錯誤的對象上調(diào)用錯誤的方法,避免了一大堆在動態(tài)類型語言中由于輸入錯誤而導(dǎo)致的煩人的bug。
長寬比
有過將舊媒體文件轉(zhuǎn)成新格式經(jīng)驗的人對此應(yīng)該不陌生。原來的游戲在瀏覽器上運(yùn)行,根本沒有考慮到全屏運(yùn)行,所以長寬比是隨意選取的。每個游戲都不一樣,但大致都在3:2左右。
現(xiàn)在最常見的長寬比是16:9,一些筆記本上也有16:10的長寬比。我希望游戲能在這兩種長寬比上正常運(yùn)行,不會出現(xiàn)黑條,也不會拉伸圖像。唯一的做法就是將原圖切掉一部分,或加上一部分。
所以,我在每個游戲上畫了兩個矩形,一個是16:9,一個是16:10。然后游戲會根據(jù)屏幕分辨率計算矩形大小,然后用計算出的矩形作為相機(jī)的視圖邊界。只要所有重要的游戲元素都在這兩個矩形的相交部分,且相交部分的邊界不會超出場景邊緣,就沒問題。
16:10和16:9的矩形框。原始比例為3:2
唯一的難題是讓場景本身適應(yīng)額外的寬度,這需要重新繪制許多圖形并重新排列,以適應(yīng)新的長寬比。盡管有一點(diǎn)痛苦,但最后還是搞定了。
痛苦的色彩空間
經(jīng)過一番測試后,我發(fā)現(xiàn)Flash的阿爾法混合和顏色變換不是在線性空間內(nèi)進(jìn)行,而是在感知空間內(nèi)進(jìn)行的。從數(shù)學(xué)角度而言這樣做是否正確還有待商榷,但我能理解為什么,因為許多繪畫程序都是這樣做的,它們希望能按照人們期待的方式工作,而不會考慮那些不懂商業(yè)的數(shù)學(xué)家的想法。但我還是要說,這樣做是錯誤的!這樣會導(dǎo)致抗鋸齒等功能出現(xiàn)問題。
在對向量圖進(jìn)行光柵化時,你需要產(chǎn)生抗鋸齒的輸出,此時光柵化程序會產(chǎn)生一些阿爾法值,叫做“覆蓋值”,意思是,如果某個像素在向量圖中被蓋住了一半,那么該像素的alpha值就是0.5。
但在Flash中,alpha等于0.5的意思是說它的顏色介于前景色和背景色之間的一半。
這兩者完全不一樣!
在不透明的黑色像素上繪制一個半覆蓋的白色像素,其結(jié)果不應(yīng)該是50%的灰色。光的原理并非如此,向量圖的柵格化也不是這樣工作的。(沒有背景色,柵格化過程做不到“該像素的顏色應(yīng)該位于前景色和背景色之間的 x%”。)
圖:感知空間(sRGB)內(nèi)的顏色混合。上:黑色上方的透明白色;中:白色上方的透明黑色;下:灰色
圖:線性空間中的同樣的混合(物理上準(zhǔn)確的結(jié)果)。注意50%覆蓋率看上去與50%灰并不一樣。
現(xiàn)在,經(jīng)過抗鋸齒柵格化后的圖形使用的是阿爾法混合模式,而Flash導(dǎo)出的阿爾法透明度、漸變和顏色變換使用的是另一種混合模式。但是渲染流水線中只有一個阿爾法通道。那么,渲染器應(yīng)該怎樣解釋阿爾法值呢?如果按照感知混合的方式解釋,那么半透明的物體是正確的,但抗鋸齒邊緣和其他效果就是錯誤的。如果按照覆蓋值來解釋,那么結(jié)果正相反。總會有一些是錯誤的!
我只想到了兩個方法來解決這個問題:1) 設(shè)置兩個阿爾法通道,一個保存覆蓋值,一個用于感知混合值;2) 在柵格化圖形時不加抗鋸齒,而是將圖形繪制在一個非常大的幀緩沖區(qū)中,然后利用過濾算法將其縮小。
但最后我沒有采用任何一種方法,我接受了半透明效果在Flash和游戲中不同這一事實,然后不斷調(diào)整圖形直到在游戲中的效果滿意。透明物體在Flash中永遠(yuǎn)不會和實際效果一致,但幸好透明物體不是太多,所以不是太大的問題。
為了確保其他效果是正確的,我做了一個“顏色測試”圖,其中包含了多種濃度的多個顏色,以及色相切換效果等,然后在游戲中顯示,確保它在游戲中和Flash中顯示效果一樣。
圖:顯示效果是一致的!
幀速率
原來的Flash游戲的速率為24fps,但實際上,其幀速率取決于Flash播放器的心情。在Flash中,24fps的實際速率可能只有15fps,30fps的實際速率是24fps……非常不靠譜。
我希望重制后能達(dá)到60fps,這意味著我需要對動畫做一些處理,因為游戲的動畫是按照24fps設(shè)計的。(Flash的動畫工具只能在離散幀上創(chuàng)作,不能處理連續(xù)的時間。)
首先,我嘗試讓導(dǎo)出器將幀數(shù)加倍。也就是說,將時間線上的每一幀導(dǎo)出成兩幀。這樣很容易就能獲得48fps,但距離60fps還有一定距離,所以動畫速度還是快了25%。最后的解決方案很樸實——我玩了一遍游戲,然后把動畫過快的地方手動加上幾幀。
現(xiàn)在,我有了一個很不錯的C++版本的游戲,能在現(xiàn)代電腦上至少再穩(wěn)定運(yùn)行一二十年。不過我覺得還能再加一些額外的功能,所以除了重畫許多圖形、改進(jìn)動畫之外,我還做了下面這些改進(jìn)。
保存游戲狀態(tài)
在制作這款游戲時,我想減小玩家的壓力,所以想出了這個想法。整個游戲的流程很長,而且有許多很容易失敗而不得不重來的地方。也許在2006年這不算什么,但現(xiàn)在我們都長大了,沒有時間玩如此硬核的游戲。
許多模擬器都有保存游戲狀態(tài)的功能。按下“保存游戲狀態(tài)”后,模擬器會將整個模擬游戲機(jī)的內(nèi)存保存到文件中。這樣,如果失敗,只要按下“加載游戲狀態(tài)”,就可以從保存的地方重新開始。
在原始的Flash游戲中沒辦法實現(xiàn)該功能,因為Flash沒有給開發(fā)者提供任何訪問整個游戲狀態(tài)的接口。但現(xiàn)在我的游戲引擎是自己寫的,所以可以實現(xiàn)了。
我將這個功能稱為Zone,它只是一個分配器,將內(nèi)存按照固定大小進(jìn)行分配。場景中的所有節(jié)點(diǎn)都分配到當(dāng)前的Zone中。
要實現(xiàn)保存和加載,只需要實現(xiàn)兩個Zone,一個是活躍的Zone,另一個是“保存狀態(tài)”的zone。要保存狀態(tài)時,只需將活躍zone memcpy到保存狀態(tài)zone中。加載狀態(tài)時按照相反方向memcpy即可。
副任務(wù)
這款游戲本身的流程不算長,有三個任務(wù),不過我還是想讓人們再多玩幾個小時。于是我給每個游戲增加了一個“副任務(wù)”——一個游戲的修改版本,其布局和謎題略有不同。制作這種副任務(wù)的工作量比制作新游戲小得多,但能達(dá)到不錯的效果。
制作副任務(wù)意味著我要回去修改15年前做的Flash游戲,不過我還挺享受這一過程的。
Flash的界面很不錯。按鈕有邊緣,圖標(biāo)是寫實風(fēng)格的,空間利用率也非常棒。使用舊的界面感覺就像是考古學(xué)家在探索被遺忘的羅馬科技一樣。失落的界面設(shè)計。
這些是什么魔法?
而且,雖然Flash有很多bug,還很慢,缺乏非?;镜墓δ?,但我并不討厭使用它。我也沒找到什么更好用的現(xiàn)代程序。
為了避免副任務(wù)與主任務(wù)雷同,我為它們畫了新的背景,整個場景也水平翻轉(zhuǎn)了一下。
Hapland 3
Hapland 3 副任務(wù)
音樂
我給每個游戲都添加了一個背景音樂,用的是我自己錄制的一些音樂和現(xiàn)成的音效。有一次我去日本旅游,突發(fā)奇想去了某座山上的錄音棚里錄了一些東西。我在網(wǎng)上找了個音樂人幫我錄了片頭音樂,然后自己給片尾錄了一些吉他和弦,還加了一些特效,免得讓別人聽出我的吉他水平很差。
我會根據(jù)情況使用Logic或Live編輯音樂。我覺得Logic更適合錄音,而Live更適合設(shè)計音效。
成就
玩家們都很喜歡Steam游戲中的成就系統(tǒng)。雖然成就系統(tǒng)給游戲設(shè)計師加重了負(fù)擔(dān),但也不算太麻煩。
將成就上傳到Steam非常麻煩。你沒辦法定義一個成就列表,然后用命令行工具上傳,只能通過又慢又破的PHP網(wǎng)站一個個上傳。
我覺得Steam可能會為大型游戲工作室提供一個批量導(dǎo)入工具,但我沒有這種工具,所以我分析了HTTP請求,保存下cookie然后自己寫了一個導(dǎo)入工具。
在反復(fù)推敲了幾遍之后,我找到了一套還算不錯的成就方案:完成每個游戲獲得一個成就,完成每個副任務(wù)獲得一個成就,然后幾個重要的隱藏要素都有成就。正常玩家找不到的奇怪的隱藏要素沒有設(shè)置成就,玩家只能獲得發(fā)現(xiàn)的快感。
Steamworks的成就上傳界面
應(yīng)用公證
雖然我主要在Mac上開發(fā)游戲,但蘋果的“公證”機(jī)制非常令人頭疼。每當(dāng)運(yùn)行MacOS應(yīng)用時,蘋果都會檢查開發(fā)者是否繳納了年費(fèi)。如果開發(fā)者沒有付年費(fèi),MacOS就會強(qiáng)烈地暗示該應(yīng)用是個病毒,并且拒絕啟動。
出于這個原因,Windows將是我的首選平臺,而且以后可能只發(fā)布Windows版。
使用的庫
發(fā)布給最終用戶的軟件我會盡可能減少依賴,但我也不介意使用一些高品質(zhì)的庫。除了OpenGL和一些操作系統(tǒng)標(biāo)準(zhǔn)庫之外,整個游戲系列采用了下面幾個庫:
Steam SDK
cute_sound
stb_vorbis
stb_image
結(jié)束語
整個過程很有意思。只要技術(shù)實現(xiàn)得正確,玩家?guī)缀踝⒁獠坏饺魏螀^(qū)別。
如果想支持我,可以在Steam上購買Hapland Trilogy,或我在2020年后做的另一個游戲 Blackshift。我在foon.uk上也有一些基于瀏覽器的免費(fèi)游戲。比較新的游戲是用JavaScript或WASM寫的,而較舊的游戲(包括原版的Hapland)是AS2 Flash,由于有Ruffle支持,運(yùn)行得還不錯。后面的AS3的游戲無法運(yùn)行了。
關(guān)鍵詞: Adobe Flash 我嘗試在游戲上復(fù)活了它 中小微企業(yè)融資辦法