我一直在開發《Pizza Legacy》,這是 1994 年 DOS 遊戲《Pizza Tycoon》的開源重製版。遊戲中城市有近距離的街道視圖,當你捲動時,可以看到車輛川流不息地在街道上行駛。一次大約有 20 到 30 個小小的精靈圖,但它們會導航路網,在交叉路口排隊,總體上看起來像一個充滿活力的城市。是的,它有點小瑕疵,因為有時它們會穿過彼此,但足以讓地圖充滿生命感。這一切都發生在一個 25 MHz 的 386 CPU 上。
我在 2010 年開始這個專案時,首先實現的就是這個近距離縮放等級,但花了 14 年才終於讓車輛以我滿意的方式跑起來;這些年來我嘗試了多次,但每次遇到問題時,我都會陷入建立一個過於複雜、難以理解且工作起來不有趣的系統。
2017 年的一次嘗試是讓每個地圖塊記錄哪些位置已被佔用,每輛車在移動前都必須向網格請求許可,並在移動過程中預留和釋放位置。這基本上變成了一個共享鎖定系統,只是為了移動幾個像素,車輛和地圖塊不斷試圖保持同步。
同時,我腦海中一直有個揮之不去的想法:原始的《Pizza Tycoon》是在 25 MHz 的 CPU 上運行的,為什麼我的版本總是如此複雜?
最後,我深入研究了組合語言(我花了多年時間慢慢地更好地理解和記錄它),以了解原始遊戲是如何做的,並借助了 LLM 的幫助,這些技術(幾年前)是新穎且令人興奮的,能夠比我更好地理解組合語言。
現在我終於讓它工作起來了,我可以看到我哪裡做錯了:我帶著充滿現代概念的大腦去處理它:場景圖、尋路、碰撞偵測,當然還有充足的 CPU 來運行所有這些!
首先,讓我們看看一個城市實際上是什麼樣子的:
正如你所見,有雙向車道、T 型路口、交叉路口和轉角。在《Pizza Tycoon》中,地圖由 160x120 個地圖塊組成,每個地圖塊都來自 landsym.vga:原始的 landsym.vga 文件,增加了地圖塊之間的邊界和指示行、列偏移的文本。字節 0x54 表示第 5 列,第 4 行(倉庫的屋頂地圖塊)。
回到交通流量;讓這個系統能在如此慢的 CPU 上運行的關鍵洞察是:車輛不需要知道它們要去哪裡。每種道路地圖塊類型都有自己的方向。道路地圖塊 0x16 是水平道路的底部,意味著車輛只能從左到右行駛。類似地,道路地圖塊 0x06 僅用於右到左的交通,然後 0x26 和 0x36 相同,但用於垂直交通。
這意味著一旦車輛知道它在哪個地圖塊上,它就可以一直前進,基本上城市就是一堆單向道路。
轉角也以相同的方式工作,0x56(在我枚舉中的 CORNER_SW)是允許車輛繼續向西行駛或向南轉彎的轉角。當車輛遇到轉角時,它會拋硬幣,50% 的機會直行,50% 的機會轉彎。地圖的設計方式使得道路始終有意義,這意味著在 CORNER_SW 旁邊有另一個地圖塊,它要么是從南到北的交通(所以我們必須向南),要么是另一個允許轉彎或直行的邊緣地圖塊。
還有一個額外的規則可以讓交通看起來更自然,如果你剛剛左轉,下一個轉角會迫使你直行;連續兩次左轉是不允許的。
每種地圖塊類型的有效方向,用箭頭表示。
車輛每幀移動一個像素。每經過一個計數週期,主循環就會檢查車輛是否被阻塞,如果沒有,則根據方向增加或減少其屏幕坐標。向東增加 X。向北減少 Y。
還有一個第二個進度計數器,從 16 倒計時到 1。當它歸零時,它會重置為 16,遊戲就會運行地圖塊邊界邏輯:查找下一個地圖塊,決定新方向,更新精靈幀(以視覺上將車輛轉向新方向)。由於每個地圖塊寬 16 像素,高 16 像素,這正好在穿越每個地圖塊時運行一次。每像素移動每幀發生;更重的地圖塊邏輯僅以 1/16 的頻率運行。
當車輛首次生成時,進度被設置為 1 到 16 之間的隨機值。這會分散所有車輛,使其地圖塊邊界檢查不會同時發生,從而將工作均勻地分散開。
與我各種花哨的碰撞偵測嘗試不同,原始遊戲使用簡單的成對檢查:對於每輛車,遍歷整個車輛列表並詢問「這兩輛車在下一幀會重疊嗎?」如果會,則在被阻塞的車輛上設置一個 10 幀的等待計數器,然後繼續處理下一輛車。
但是碰撞偵測代碼的編寫方式是盡快退出。它首先做的事情是提取另一輛車的方向;因為道路是單向的,東向和西向永遠不會共享同一條道路,所以東向的車輛和西向的車輛永遠不會發生碰撞。該對立即返回,根本沒有坐標讀取。與東向和南向、西向和北向等情況相同。
假設在典型的城市視圖中有 25 輛車,每幀有 625 次成對調用。其中大約一半僅憑方向檢查就在幾條 CPU 指令內返回。大多數其餘的會失敗車道檢查(同向的車輛必須在同一條道路上,這是一個相等比較)。實際上達到任何坐標算術的對通常是個位數。
當車輛確實被阻塞時,10 幀的等待會產生自然的交通堵塞:車輛堆積起來,前面的車輛最終找到暢通的道路,隊伍就會疏散。系統中有一些錯誤(尤其是在你讓它運行一段時間並且有很多交叉路口時),但考慮到這的目的不是運行準確的駕駛模擬,而是僅僅在屏幕上顯示一些移動,它運行得非常完美且高效。碰撞偵測系統有一些怪癖;某些組合從未被檢查過(例如,東向的車輛從未與南向的車輛相交),這可能是某些錯誤的原因。
當你進入近距離縮放視圖時,遊戲會掃描視口中的所有 132 個地圖塊(12 列乘以 11 行),對於每個道路地圖塊,它會根據該區域的交通密度進行隨機判斷,以決定是否在那裡生成車輛,因此交通流量較高的區域會更繁忙。轉角地圖塊被排除在生成點之外,因此車輛僅出現在直線道路地圖塊上。
駛出屏幕邊緣的車輛會以新的(隨機)顏色車輛的形式重新生成,面向相反的方向,位於另一條相反方向的道路上。這意味著遊戲不必擔心重新生成車輛,除了每當一輛車向東駛出時,它就會在下方生成一輛新的向西行駛的車輛,依此類推。
注意看車輛駛出地圖邊緣,你會發現它們被反方向行駛的車輛取代了。
當你捲動時,新暴露的地圖塊帶會進行相同的處理,有機會在上面生成車輛。
回顧我失敗的嘗試,我是在為原始遊戲根本沒有考慮到的問題進行設計。車輛不需要尋路,因為地圖告訴它們可以去哪裡。碰撞偵測很便宜,因為早期退出邏輯使得大多數成對檢查基本上免費。沒有速度或物理學,因為每幀一個像素足以看起來令人信服。當你即將撞上東西時,只需暫停 10 幀,當你需要轉彎時,你只需移動半個地圖塊的寬度,然後再轉彎,這在任何方向的任何地圖塊上都有效。
我幾乎是緊密地按照組合語言重現了它,所以只有幾個 switch 語句,每個地圖塊類型都有不同的路由選項,你可以在 Car.cpp 中的 decide_desired_direction 方法中看到。
Pizza Legacy (代碼) 在 GNU General Public License v3 下發布。原始 Pizza Connection 遊戲素材和內容 © 1994 MicroProse。僅用於說明目的。pizzalegacy.nl - 網站內容 © cowomaly,根據 CC-BY-SA 4.0 授權。