回顧并為今天的內容定下基調
昨天我們新增了每個元素級別的排序功能,并且采用了一種我們認為挺有意思的方法。原本計劃采用一個更復雜的實現方式,但在中途實現的過程中,突然意識到其實有個更簡單的做法,于是我們就改用了這個簡單的方式,并且運行效果也很好我們還用上了加入的循環檢測機制,確認排序邏輯里并沒有出現錯誤,因此循環檢測也派上了用場。整體來說昨天的工作還是挺順利的,對結果也很滿意。
不過,眼下我們已經到了需要繼續完善 Z 軸分層邏輯的階段了。當前我們已經實現了在單一圖層內的正確排序,但圖層之間的分層系統還沒有做好。這也就意味著如果現在運行游戲,會發現當前只有一個房間能夠按照我們期望的方式進行排序渲染,這是正常的。
但之前做的樓梯部分仍然保留了舊的透視變換邏輯,也就是物體隨著高度上升會向一側偏移或放大,而這不是我們想要的。我們希望的是:在一個房間內部,只是簡單地向上排列(也就是 Y 值變大),而不應該帶有這種透視拉伸效果。因此這部分顯然還需要修復。
另外,當嘗試添加多個房間時,也還存在明顯的問題。當前引擎的結構還沒有處理多個房間之間的圖層關系,因此我們還需要回過頭來繼續完成圖層系統的實現。這也正是接下來需要重點處理的部分。
修改 game_world_mode.cpp:讓 AddStandardRoom()
生成多層內容,運行游戲觀察排序混亂的情況
如果我們開始讓多個房間以垂直疊加的方式添加進場景中,很快就會發現當前的排序系統完全無法正常工作。因為目前的排序邏輯只是為單個房間設計的,它只考慮了一個房間內部的規則。原因是我們采用的是“二維半”的排序方式,所以為了實現簡單,它必須做出一些妥協。
但顯而易見,這樣做在當前情形下根本行不通。多個房間疊在一起時,排序混亂,完全沒有邏輯可言,物體在渲染時的前后關系也變得毫無依據。原因很清楚,就是因為我們的排序規則并沒有考慮跨層的 Z 切片。
雖然我們理論上可以嘗試去改造排序算法以適應這種多層的情況,但這并不是我們的目標。我們的目標從來都不是讓一個排序系統去處理所有情況,而是將整個世界切分成多個 Z 切片(Z slices),然后對每一個切片獨立進行排序,這樣我們就能避免跨層的復雜依賴問題。而這個方法我們已經知道是有效的,這一直是我們設定的方向。
所以現在必須做的,就是回過頭來,把這個世界切片的系統實現出來。我們要按照預定方案將場景劃分成一個個圖層切片,然后讓渲染器對這些圖層單獨處理。這樣我們才能驗證,在這種切分模式下,原有的排序規則是否真的有效。
目前的情況是,所有對象都混雜地渲染在一起,排序混亂無序,結果無法理解也無法接受。如果不實現圖層切分,那么排序邏輯就根本沒有可行性。因此我們必須暫停當前的操作,回到圖層劃分的實現上來,這是我們接下來面臨的一項艱巨工作。
鼓勵性的發言:談論探索式編程,以及順應架構演進的流程
接下來要進行的工作會比較困難。一方面是因為這類任務通常需要我們集中精力,在連續的幾小時甚至一兩天內完成,比如在那種完全沉浸的開發狀態中,把所有細節打磨到位。而現在我們是分段進行,并且一邊做一邊講解,這讓整個過程的節奏被打散,也更難專注,思路容易被打斷。
這不僅對我們來說是挑戰,對其他人理解起來也不容易。因為我們要頻繁地移動代碼,進行結構調整,很多內容是“感覺上的”,是對系統組織方式的直覺和理解過程。這種過程很難用清晰、線性的語言完全解釋清楚,我們也為此感到抱歉,但這是開發過程中不可避免的一部分。
我們一直在采用探索式架構設計的方式推進,這種方式強調的是在實踐中“摸索出”合適的架構。而不是一開始就設想出一個完美的系統結構圖,然后按圖紙建造。從來都沒有所謂“架構全知者”,任何優秀架構的形成都是在實際開發過程中不斷調整、試錯、發現問題并修正之后逐步凝練出來的。
因此,這種架構上的調整、重組,是我們必須時不時面對并接受的。它不是失敗,而是一種成長,是朝著更清晰、更合理結構演進的必經之路。越是深入項目,就越能看清哪些原來的設計不足以支撐后續需求,而哪些新的設計思路更契合當前的發展方向。
如果我們抱著一開始就要把一切都設計完美的想法去開發軟件,那最終只會得到一個僵化的、難以維護的系統。而如果我們相信架構是一個逐步“退火”與優化的過程,那么就應該樂于接受這種在開發中不斷演進的結構變化。
所以我們要做的,是保持代碼庫的靈活性,讓它能夠隨著我們對項目理解的加深而不斷演化。不要害怕重構,不要排斥改動,而是積極地擁抱這種前行中的架構演化。因為最終只有那些可以流動、可以適應、可以成長的系統,才是真正高效、優雅且長久可維護的。
修改 game_entity.cpp:讓 UpdateAndRenderEntities()
回到排序改動前的樣子,運行游戲看看當前效果
現在我們要開始處理的任務,是關于圖層系統的進一步構建。之前我們已經開始了這一部分的設計和探索,但在中途發現需要先完成排序系統,于是暫時中斷了對圖層的開發,先去實現了排序。現在排序系統已經實現并測試得差不多了,我們回到之前未完成的圖層概念。
當前的系統中,已經存在一個叫做“轉換為相對圖層”的功能,它會根據一個位置返回一個相對圖層的數值,用來表示這個元素應該被視為在“世界”的哪一層。它還會修改傳入的位置,使其變成相對于那個圖層的偏移。為了臨時跳過這個機制做測試,之前我們只是傳入一個臨時變量,沒有真正使用轉換結果,這樣就避免了偏移修改。
現在如果我們恢復回“沒有排序機制之前”所使用的圖層系統,可以看到場景中確實出現了兩個房間,但它們被渲染在了同一個平面上,圖形出現閃爍,就是因為兩個圖層疊在一起,深度沖突,沒有正確分層。這種行為其實正好說明了我們接下來要實現的目標——我們希望所有房間的圖元在渲染時都被壓扁在一個統一的空間中,但是渲染器知道它們所屬的是不同的層,然后在實際繪制時,通過不同的透視偏移把它們展示為“上下疊放”的結構。
這也正是我們設想的“圖層切片”渲染方式的核心——邏輯上世界是分層的,但物理渲染時所有圖元都壓進同一個Z空間里,之后再通過相對Z值的偏移來渲染出深度感。我們不再讓渲染器試圖在三維空間中自行解決可見性排序的問題,而是人為地提前把世界切成若干“圖層切片”,每個切片內做局部排序,然后分層繪制,從而讓渲染變得簡單明確。
黑板講解:Z軸切片與“向上”這一概念的兩種不同理解
我們現在再次澄清并統一對“圖層切片”概念的理解,以便后續開發保持一致思路。
當前的世界結構中存在多個區域(如房間或地形塊),它們可以堆疊在彼此之上。每個區域可能被樹木等裝飾物包圍,不同層之間是垂直疊加的。
我們需要引入“向上”的兩個不同概念,也就是說,“向上”這個詞在不同上下文中代表不同的意思:
第一種“向上”的概念:在同一個切片層內部的相對Z位移
在一個切片層(即一個房間或一個平臺)內部,我們希望將所有Z軸上的高度變化,僅僅表現為在Y軸上的偏移,而不再有縮放或X方向的透視偏移。這意味著:
- 例如一段樓梯,它的每一級高度不再被拉伸或放大,而是以相同的大小逐級向上排布。
- 視覺上,這種樓梯應該是垂直排列的,沒有透視效果,就像是從側面看的一條筆直的Y軸方向延伸。
- 這種處理方式,屬于一種“傾斜正交視角”:將Z的變化直接轉換為Y軸偏移,不做其他處理。
這樣可以達到我們想要的視覺風格,使得在同一個房間中,不同高度的物體看起來是平等的,不會因為視角透視而大小不同。
第二種“向上”的概念:切片之間的垂直堆疊關系
當我們從一個房間或區域走向另一個在上方的房間(即從一層走向二層),這種Z的變化代表的是不同的切片層。這里的“向上”意味著跨越了切片邊界,進入了一個新的世界切片。
對于這種“跨切片”的垂直關系,我們希望呈現出透視變化,即:
- 越高的層級,其內容會放大(因為它離相機更近),會出現X、Y軸上的偏移(體現為透視縮放和平移)。
- 整個切片作為一個整體進行變換,比如放大或偏移,而不是對其中單獨元素進行Z方向位置的直接操作。
目標總結:
我們的渲染邏輯將按如下方式進行:
- 將整個世界分割為多個“切片”(每個代表一個房間或樓層)。
- 每個切片內部,元素根據相對Z值映射到Y軸偏移,不進行縮放或透視。
- 每個切片作為整體,根據它在世界中的高度位置進行統一的縮放、偏移,體現出它在整個世界中的上下關系。
- 渲染器對切片分開排序,每個切片內部進行局部排序(之前已實現)。
通過這種方式,我們就可以得到既有清晰邏輯層次,又符合藝術風格的視覺效果,使玩家可以準確識別不同高度上的對象,同時保持畫面的整潔和層次分明。這也是我們引入“圖層切片”架構的根本目的。
黑板講解:分層的 Alpha 混合
我們在進行圖層切片和渲染重構的過程中,還需要同步修復一個關鍵的視覺問題:透明度(Alpha)混合不正確的問題。
這個問題表面上不太明顯,但實際上對整體視覺品質有著重大影響。很多游戲中都會出現這種情況,看起來很不專業。
問題描述:
當兩個半透明的圖像(例如兩棵樹)部分重疊時,我們期望看到的效果是:前面那棵樹擋住后面一部分區域,疊加的區域在視覺上保持一致的透明度(例如都設定為 50%)。但由于渲染順序和混合邏輯不當,疊加區域實際看上去比應有的更不透明。
舉例說明:
-
假設背景為 M,然后樹A在前,樹B更前,兩者都設置為 50% 透明度。
-
我們希望最后看到的像素顏色是:
0.5 × M + 0.5 × N
(N 是前景樹圖層的合成結果)
然而,由于我們當前是逐個繪制精靈(sprites),每個精靈獨立執行一次混合計算,結果就不是我們期望的那樣。
實際發生了什么:
-
首先繪制背景 M,顯存中是 M。
-
然后繪制精靈 A(透明度 0.5):
- 當前緩沖區變為:0.5 × A + 0.5 × M
-
接著繪制精靈 B(同樣透明度 0.5):
- 它會再與上一層結果混合,而不是和原始 M 或 N 整體混合。
- 計算結果為:0.5 × B + 0.25 × A + 0.25 × M
此時的最終像素顏色中:
- 來自樹圖層(N層)的貢獻是 A 和 B 加起來,共 0.75(0.5 B + 0.25 A)
- 背景(M)的貢獻只剩下 0.25
- 總透明度已經偏離了原設定的 50%
每增加一個精靈,這種偏差會更嚴重,畫面越來越不透明,不再真實。
本質問題:
多次執行的 Alpha 混合是逐層進行的,每一層都是線性疊加,這導致前層對后層產生了過多的遮擋,疊加后的透明度呈指數式變化,遠遠偏離了預期的視覺表現。
理想目標:
我們期望達到的是對某一個完整圖層進行整體混合,也就是說:
- 圖層內部的多個精靈,應該先合成為一個“圖層圖像”
- 再把這個圖層圖像作為一個整體,用透明度(例如 50%)和背景進行一次混合
只有這樣,才能實現 真正的圖層透明效果,不會因局部重復混合而產生透明度累加問題。
初步方案方向:
為了解決這個問題,我們需要:
- 將每個切片圖層的所有內容先合成為一個臨時緩沖圖像(offscreen render target)
- 對這個合成圖層應用統一的 Alpha 混合邏輯,再與背景圖層合成
- 避免圖層內精靈直接逐個向主緩沖區進行 Alpha 混合渲染
這樣才能實現像:
- 樓梯淡出
- 房間隨鏡頭遠近漸隱
- 整體場景霧化、模糊
等特效,并且保持透明度一致,畫面清晰真實,不會“越重疊越黑”。
這個修復雖然麻煩,但非常必要,是提升畫面質量、避免渲染邏輯錯誤的關鍵一步。我們將會在進行圖層切片和分層渲染的同時,把這個正確的 Alpha 混合機制也一并設計進去。
黑板講解:如何解決當前問題
我們決定采用的解決方案是將整個場景按“切片”的方式來進行渲染,從而徹底解決多精靈混合導致透明度疊加錯誤的問題。雖然這個問題理論上可以通過一些復雜的渲染技巧解決,例如使用目標幀緩沖區中的 alpha 通道、單獨的中間緩沖、深度預通道(depth pass)以及各種比較操作等,但我們不打算走那種“瘋狂 shader 技術棧”的路線。我們選擇更簡單、直接、可控的方式來處理這個問題。
解決方案:按圖層切片進行渲染
我們將整個場景劃分為不同的切片(slice),每個切片作為一個獨立的圖層進行處理。每個切片中包含多個 sprite 精靈,我們先將這些精靈全部繪制到一個離屏緩沖(offscreen buffer)中,生成該切片的完整圖像。
之后,我們再將這個切片圖像與其它切片或者背景圖層進行合成。在這個合成階段,我們只需要應用一次標準的透明度混合公式即可:
P = (1 - α) × N + α × M
其中:
N
表示之前已經繪制好的緩沖內容(背景或其它切片)M
是當前圖層(切片)渲染結果α
是整個圖層的透明度(例如用于逐漸淡出)
這個方式的關鍵點在于:
- 圖層內所有精靈先合成為一張完整的圖像,內部再怎么復雜,外部只看最終合成的圖層圖像
- 合成操作只執行一次,因此不會因為精靈之間的重疊而產生透明度疊加問題
- 保留了精靈原始的透明度(例如玻璃、幽靈等對象仍可以是半透明的),不會影響單個精靈的表現
- 可以對圖層整體應用動態效果,比如整個圖層淡入淡出、模糊、霧化等
操作流程回顧:
-
對每一個切片圖層:
- 所有屬于該圖層的 sprite 精靈全部渲染到一張臨時緩沖圖像中
- 這些精靈保留自己的 alpha 值,不進行額外的混合
-
完成圖層合成后:
- 將整個切片圖層的圖像,作為一個整體,與其它圖層按指定透明度混合
- 混合操作只進行一次,確保 alpha 計算正確
-
最終得到準確的視覺效果,無論有多少精靈,透明度表現始終如預期
這種分層合成的方式不僅解決了透明度問題,還為后續的渲染優化和效果控制打下了堅實基礎。我們可以在圖層層面做更多效果控制,而不用在精靈層面做復雜的邏輯,極大地簡化了系統的復雜度和維護成本。
黑板講解:渲染緩沖區
我們最終實現圖層渲染和合成的方式,可能在細節上還有一定的復雜度,但整體思路是可行的。如果操作得當,甚至可能不需要為每個圖層都分別渲染,只要在關鍵圖層上使用額外處理即可。即便采用最保守的實現方式,最壞情況下我們也只是需要增加一些渲染緩沖區(render buffer),并不是非常昂貴的開銷。
渲染緩沖與操作數量的關系
我們需要的渲染緩沖區數量,與我們要進行的特殊圖層混合操作的次數成正比。回顧之前的設計,我們實際上不需要太多這樣的操作,具體如下:
1. 霧效(Fog)不受該問題影響:
- 霧效是顏色效果,而不是 alpha 混合;
- 霧不是通過多個 sprite 重疊繪制出來的,而是通過顏色疊加實現;
- 所以霧效圖層不會因為重復渲染而產生透明度累積的問題;
- 因此,從角色所處的層開始,向下的所有圖層(即更底層的場景)都可以合并在一個渲染緩沖區中,統一渲染處理,無需額外操作。
2. 僅需單獨處理高于角色的那一層:
- 真正需要使用我們提出的“切片圖層單獨合成再統一混合”的處理方式的,僅有角色上方的那一層;
- 因為那一層會由于攝像機視角靠近而被淡出,所以會涉及 alpha 混合;
- 如果直接按 sprite 分別進行透明度渲染,會導致重疊區域的透明度錯誤疊加問題;
- 因此這一層必須單獨渲染,最終統一做一次合成,確保視覺正確。
總結:
- 實際上我們不需要為所有圖層都使用這種額外緩沖和混合方式;
- 只需要為角色上方的一個圖層使用特殊處理;
- 角色所在層及其下方的所有圖層,可以直接合成并渲染,無需擔心透明度錯誤;
- 霧效等純顏色操作不涉及 sprite 重疊透明度問題,因此也不需額外處理;
- 整體的資源開銷很小,僅需一到兩個額外的 render buffer 即可實現正確視覺效果;
- 保持系統簡單的同時,實現了專業級別的圖層混合表現。
這一實現方式既高效又易于維護,能為后續擴展視覺效果和圖層控制提供良好的基礎。
黑板講解:簡單估算我們可用的圖形內存
我們評估了一下在最壞情況下所需的渲染緩沖區數量,即便是這樣,整體的內存開銷也是完全可以接受的。假設我們渲染的分辨率為 1920×1080,每個像素占用 4 字節,那么單個渲染緩沖區的內存需求約為:
- 1920 × 1080 × 4 = 8.29 MB。
如果我們需要兩個這樣的緩沖區,總共也只是大約 16 MB 的顯存占用。
顯存使用的合理性:
這個顯存占用從兩個角度來看都沒有問題:
1. 內存分配層面完全可接受:
- 當前主流的顯卡大多配備至少 512MB 顯存;
- 普遍的配置是 1GB,甚至 2GB 或以上;
- 而我們只占用了 16MB,這遠低于最低配顯卡的顯存上限,基本可以忽略;
- 即使是中低端顯卡,在運行其他正常圖形任務的同時,也完全可以承受這個開銷;
- 更何況我們不是頻繁創建銷毀,而是只在初始化時分配幾塊緩沖區長期使用。
2. Steam 硬件調查也驗證了用戶硬件足以支撐:
-
從硬件調查數據可以看出:
- 擁有 512MB 顯存 的用戶比例大約在 10% 左右;
- 顯存為 1GB 的用戶占比接近 1/3;
- 顯存為 2GB 及以上 的用戶占比甚至超過 1/3;
-
也就是說,絕大多數玩家的顯卡配置都遠高于我們的最低需求;
-
我們只需 16MB,甚至最老舊的顯卡(如 256MB 顯存)也能容納;
對弱性能設備的適配考慮:
-
如果是性能極弱的平臺,比如 Raspberry Pi:
- 本身就不適合跑 1080p 分辨率的游戲;
- 因此我們可以通過降低分辨率來規避顯存問題;
- 比如運行在 720p 或 480p 下,對顯存的需求就會大幅下降;
總結:
- 即便采用最保守的策略,在視覺處理上使用兩個完整的 1080p 渲染緩沖區,顯存開銷也只是約 16MB;
- 在現代主流或中低端顯卡配置下,這個開銷幾乎可以忽略;
- 只有在極端弱性能設備上才可能需要對分辨率或處理方式進行優化;
- 所以我們完全可以放心采用這種方案,不僅效果正確,性能成本也極低。
黑板講解:簡單估算我們可用的內存帶寬
我們分析了在進行額外的圖像合成(blit)操作時所需的內存帶寬,發現這種開銷非常小,不會對整體性能產生顯著影響。
具體分析:
- 以一個 8MB 的內存拷貝為例,這樣的操作非常輕量;
- 參考一個比較低端的顯卡型號(比如GeForce 600系列的移動版),其顯存帶寬大約是 14.4 GB/s;
- 如果按 60 幀每秒計算,顯卡每幀能處理約 240 MB 的內存帶寬;
- 8MB 的額外內存拷貝只占用非常小的比例,遠遠在顯卡的帶寬承載范圍內;
對整體渲染流程影響:
-
游戲渲染本身需要讀取和寫入大量數據,包括:
- 貼圖加載
- 深度緩沖的讀寫
- 顏色緩沖的讀寫
- 各種著色器計算
-
這些操作本身已經消耗了顯卡大量內存帶寬;
-
增加一個額外的合成步驟,就是將一個緩沖區的內容拷貝到另一個緩沖區進行 alpha 混合,這實際上只增加了大約一個屏幕大小的內存拷貝(即上文的8MB);
-
這種額外的帶寬需求非常小,屬于“雞毛蒜皮”級別;
現實情況復雜性:
-
實際上的內存帶寬開銷并非精確數值,受以下多種因素影響:
- 顯卡的具體內存子系統架構
- 著色器執行和壓縮機制(如顏色壓縮、深度壓縮等)
- 是否能提前剔除不必要的像素計算(early-out)
- 驅動和硬件優化機制
-
由于這些因素的復雜性,很難用純數學公式準確計算內存帶寬消耗;
-
但從經驗和估算來看,當前的方案是合理且可行的;
性能決策的參考原則:
- 了解內存帶寬和顯存使用的基本量級,有助于在性能優化時做出合理判斷;
- 如果發現需要額外帶寬達到數百兆字節每幀(比如 300MB/幀)這種量級,就必須警惕,因為這會嚴重拖垮性能,甚至讓某些硬件完全無法運行;
- 目前的額外帶寬需求僅為幾兆字節,遠遠低于硬件承受上限;
- 這樣能保證即使在較低端硬件上,游戲運行也不會因為額外的渲染緩沖操作出現明顯的性能瓶頸;
總結:
- 額外的合成操作帶來的內存帶寬負擔極小,完全在現代顯卡的負載范圍內;
- 這種開銷對性能影響可忽略不計,屬于性能可接受的范圍;
- 保持對硬件資源的合理估算和認知,是做出性能優化決策的重要基礎;
- 因此,我們可以安心采用這種分層渲染和一次性合成的方案,既保證了視覺效果的準確,又不會顯著增加性能負擔。
思考接下來該如何推進
我們現在需要推進兩個核心問題的解決:
第一,必須理清變換(transform)的處理方式。當前我們對Z軸有兩種不同的含義,需要明確這兩種Z值如何協調運作,確保變換邏輯正確無誤。
第二,在處理變換之前,我們可以先讓渲染器支持“分層渲染”的概念,也就是把場景分成兩個不同的切片(slice),然后讓渲染器能夠正確地對這兩個切片進行alpha混合,達到預期的疊加效果。
此外,可以考慮暫時先恢復之前的方案,分別對這兩個部分進行開發和調試,先解決渲染層的切片和混合,再進一步理清變換的實現細節。這樣分步驟推進,減少復雜度,更容易把控整體流程。
修改 game_entity.cpp:在 UpdateAndRenderEntities()
中引入 TestAlpha
我們發現alpha值沒有被正確設置,首先要查明為什么alpha沒有被賦值。看起來是因為我們已經對場景進行了分區處理,導致沒有給實體(entity)單獨設置alpha。
我們需要驗證這一點,確認是否確實沒有給實體設置全局alpha。假如想要設置實體alpha,應該依據當前的“層級索引”(level index)來決定alpha值。
具體做法是,在裁剪矩形(cliprect)處理的地方,也就是推入cliprect的函數里,嘗試獲取并保存每個層級的alpha值。為了方便調試,可以新建一個“測試alpha”數組,存儲每個層級對應的alpha。
alpha值的計算方式是:如果層級處于淡出區域(fade top)之上,alpha等于1減去那個顏色通道的透明度值(T值)。保存了這個測試alpha后,在后續渲染時可以根據層級查找對應的alpha值。
在渲染時,先拿到對應層級的test alpha,然后將它應用到顏色的alpha通道上。具體就是將顏色的alpha通道乘以這個test alpha,從而實現正確的透明度混合效果。
整體思路是:先把每個層級的alpha保存下來,然后在渲染時根據層級查找這個alpha,正確設置實體的透明度,確保分層渲染時的透明處理正確無誤。
運行游戲觀察結果
當角色上樓梯時,預期應該看到逐漸淡入的效果,但實際看到的是效果瞬間切換,沒有漸變,這顯得很奇怪。為確認問題,首先驗證了當前的狀態,發現level index的變化似乎沒有按照預期工作。雖然設定了層級索引來控制透明度變化,但在實際運行中,這個索引并沒有正確導致alpha的漸變,反而是突然切換了狀態。因此,需要進一步排查為什么level index沒有正確驅動淡入效果,導致透明度沒有平滑變化。
使用調試器:中斷進入 UpdateAndRenderEntities()
并檢查 fade(淡化)值
我們檢查了當前的透明度(T值),發現當沒有任何層級超過fade top起始高度時,T值是1,符合預期,也就是完全顯示。但當角色逐漸上樓梯時,fade top高度是2.25,而當前層級的相對位置是3.0,看起來是正確的,意味著應該開始淡出。奇怪的是,當角色上樓梯時,應該看到淡入效果,但實際情況并非如此,可能是因為場景的世界坐標在某些時刻發生了偏移或滑動,導致fade效果還未生效就被位置調整所覆蓋了。我們推測淡入的觸發點需要設置得更高一點,才能在切換為新的層級之前完成淡入效果。但根據典型樓層高度的設置,fade開始和結束的高度比例看起來有些異常,似乎fade效果的時間點和層級變化的實際位置沒有很好地匹配。整體來看,當前的fade邏輯和層級切換存在時間和空間上的不協調,需要調整fade起止位置,確保淡入淡出效果能正確展現。
修改 game_entity.cpp:調整 FadeTopEndZ
和 FadeTopStartZ
值,再次使用調試器中斷查看
目前我們發現沒有任何層級超過fade top起始高度(3),具體來看層級高度依次是負12、負9、負6、負3、當前層級0、上層3,而fade top起始高度也是3。雖然fade的起始位置看似合理,應該能觸發透明度的漸變,但實際上并沒有按預期那樣工作,透明度沒有按范圍映射正常變化,表現得非常混亂,不像是正確的過渡效果。我們感覺這里一定遺漏了什么關鍵細節,整體邏輯有些模糊不清,需要進一步深入排查和分析才能找到問題的根源。
修改 game_opengl.cpp:將 Entry->PremulColor.a
傳給 OpenGLRenderCommands()
中繪制矩形的 glColor4f()
調用
我們對當前的效果不滿意,懷疑是計算邏輯出了問題,覺得自己可能犯了低級錯誤。為了更清楚地觀察,我們在繪制矩形時給它加上了邊框,這樣更容易看出問題所在。同時,我們想要確保透明度的計算更加準確,希望能夠尊重主顏色的Alpha值,這樣所有元素才能同步淡出,避免矩形邊框還在但內容已經消失的情況。現在發現元素的透明度表現異常,似乎計算出的范圍有問題,懷疑計算公式不對,尤其是關于相機相對地面Z的部分,感覺應該減去相機相對地面Z的值,但實際代碼中并沒有這么寫,所以打算修改這部分邏輯,嘗試修正這個錯誤。
我們對當前的效果不滿意,懷疑是計算邏輯出了問題,覺得自己可能犯了低級錯誤。為了更清楚地觀察,我們在繪制矩形時給它加上了邊框,這樣更容易看出問題所在。同時,我們想要確保透明度的計算更加準確,希望能夠尊重主顏色的Alpha值,這樣所有元素才能同步淡出,避免矩形邊框還在但內容已經消失的情況。現在發現元素的透明度表現異常,似乎計算出的范圍有問題,懷疑計算公式不對,尤其是關于相機相對地面Z的部分,感覺應該減去相機相對地面Z的值,但實際代碼中并沒有這么寫,所以打算修改這部分邏輯,嘗試修正這個錯誤。
修改 game_entity.cpp:在 UpdateAndRenderEntities()
中對 CameraRelativeGroundZ
減去 WorldMode->CameraOffset.z
我們意識到,如果我們真正想知道某個物體相對于相機的位置,就必須從其位置中減去相機在世界空間中的偏移量(world_mode_camera_offset_z
)。而我們之前在計算中并沒有進行這個減法操作,導致當前的代碼存在問題,是不正確的。
之前可能是因為其他部分的邏輯剛好掩蓋了這個錯誤,所以才“看起來能用”。現在我們修正了這個問題,確保在計算位置時正確地減去了相機的Z軸偏移值。這樣一來,我們得到的相對相機的高度數據才是準確的,渲染和Alpha漸變等操作才能基于正確的高度信息來執行。完成這一步后,整個系統的運行表現開始變得正常,顯示一切就緒,我們可以繼續推進其他部分的開發。
運行游戲并確認現在效果恢復正常
現在一切恢復正常,整體狀態良好。我們終于可以看到預期的淡入淡出效果重新發揮作用,視覺過渡也變得順暢自然。當我們下降到較低區域時,畫面也能正確呈現,沒有異常現象發生。整體運行看起來更加完善、令人滿意。
我們再次檢查了“開始淡入”和“開始淡出”的邏輯,發現這些看起來都沒有問題。目前這些值大致是合理的,不過之后還可以進一步調整以獲得更好的視覺體驗。雖然現在的過渡還不夠理想,不夠平滑,但這屬于后期微調的部分,留待之后再優化即可。
總之,這一階段我們已經順利完成了關鍵的修復和校正工作。現在的系統能夠正確識別不同Z層級之間的Alpha變化,并在渲染中按照相對相機高度進行合理的淡入淡出處理。后續可以更專注于美術表現上的細節調節,比如讓過渡更柔和、調整范圍更寬等,確保整體效果更自然流暢。當前這個基礎已經是可靠且可擴展的。
黑板講解:RecanonicalizeCoord
的作用
現在我們的目標是要讓物體更加正確地處于各自的層級中,但目前存在一個問題,就是圖層選擇的邏輯并不完全正確。舉個例子來說,樓梯的各個臺階理應被視為當前層的一部分,但從當前的行為來看,它們并沒有被包含在正確的圖層中。也就是說,我們希望保持在“當前樓層”的圖層中進行繪制,而現在這個邏輯顯然有偏差。
目前我們使用了 recanonicalize_coordinate
這個方法來處理坐標,但對于 Z 軸來說,這種處理是不合適的。這個函數中使用的是四舍五入的方式,它假定我們想要以坐標的幾何中心來判斷所處的圖層。然而我們現在的目標是明確每個對象屬于哪個樓層,而不是根據其中心點來推算所屬圖層。因此,這種“居中計算”的邏輯,在三維擴展之后就顯得不合時宜。
回憶我們最初的設計思路,之前我們將物體位置定義為相對于 tile 的中心點進行存儲,而不是 tile 的邊角。這兩種做法在二維中差別不大,但一旦擴展到三維空間,層級劃分問題就變得明顯了。我們現在需要的是更偏向“截斷(truncate)”的行為,而不是“居中(centroid)”行為。
換句話說,我們要做的是:如果一個物體落在某一層之下,那就把它歸入該層,而不是考慮它中心點是否穿越邊界。在這種分層邏輯下,我們就需要重新審視 tile 的放置方式。如果我們堅持繼續用“居中計算”的策略,那么地面 tile 本身的位置就需要微調(要么向下平移一些,要么向上),以便讓包圍盒的劃分符合我們想要的圖層邏輯。
當前我們的思考方向是:將底層的 tile 完全視為這一層的一部分,而將上方空間劃入下一層。如果我們繼續使用居中計算的方法,就必須人為地調整每個 tile 的放置高度,以便讓它剛好落在我們劃分圖層的規則之內。否則,我們就得徹底切換思路,采用截斷而不是居中的邏輯來決定物體屬于哪個圖層。
因此,這一問題的本質在于,我們需要明確并統一一個分層策略:是基于 tile 中心點來判定歸屬層,還是采用類似向下取整的邏輯,以確保每個 tile 和物體都出現在正確的層級中。一旦這個決定做出,其余的層級系統和渲染邏輯才會更加可靠和一致。
黑板講解:Z軸偏移
當前的問題在于我們對樓層的劃分邏輯存在不一致,特別是Z軸方向上各實體所屬樓層的判斷方式導致了一些渲染和邏輯錯誤。
具體來說,現在我們有兩層樓的結構,存在一群帶有Z軸偏移的角色或對象。當前系統的規則是這樣劃分樓層的:比如Z值在某個范圍內的屬于樓層A,而在另一個范圍內的則屬于樓層B。這個規則初看合理,但在實際中出現了問題。我們從側面來分析場景,比如樓梯間的情況:
- 一些角色位于較低位置,我們希望它們被歸類到下層;
- 另一些角色雖然視覺上應該還在下層,但由于他們的Z軸偏移,他們被錯誤地歸入了上層。
用圖形方式來理解的話,假設畫出側視圖,樓梯所在的位置有一個坡度,角色們根據其在這個坡道上的位置有不同的Z值。而我們的圖層判斷邏輯是根據對象的中心點落在哪個高度范圍內來決定它屬于哪個樓層。結果是,明明視覺上應該屬于下層的對象,卻因其Z偏移剛好超過了當前層級的上界,被系統錯誤地識別為上層成員。
為了解決這個問題,我們得調整“樓層的參考面”。目前,我們是以tile的幾何中心為參照點來定義每層的位置,但這種“居中判斷”的方式不適合當前的三維圖層劃分。正確的做法應該是“把每一層的地板整體向下平移一個固定值”,即每層的Z值基準點應當低于其中心值。
這個偏移調整的目標,是確保所有附著在某個樓層tile上的對象,即便有一定的Z軸偏移,也不會被誤判為屬于更高一層。所有的地面層也將處于層級定義范圍的上半部分,而不是居中對齊。
這其實可以通過簡單的修改來實現,例如在 world mode 中定義每層的實際Z值時,加上一個統一的負偏移,讓地板位于該樓層tile中心點之下一個適當的距離。這樣一來,角色的Z偏移仍然不會超出本樓層范圍,所有的劃分將更加合理。
這個調整不會引入復雜的邏輯,也不會改變tile或角色的世界坐標,僅僅是改變我們判斷“對象屬于哪層”的依據,從“中心對齊”變成“底邊對齊”,更貼合我們在視覺上和邏輯上對樓層的期望劃分方式。
修改 game_world_mode.cpp:在 ChunkPositionFromTilePosition()
中讓實體的 Z 軸位置向下偏移
我們目前正在處理的是樓層的Z軸偏移邏輯,重點在于如何正確地將房間或對象放置在合理的樓層范圍中,以便避免出現視覺或邏輯上的錯層錯誤。
在執行 AddStandardRoom
邏輯時,可以觀察到我們通過某個 P.offsetZ
的值來決定房間(或對象)在Z軸上的位置。而在調用 ChunkPositionFromTilePosition
時,這個函數內部默認采用的是將tile的位置直接映射為某一“樓層”的概念,它根據tile的位置直接確定了歸屬的層級。
然而,我們想要實現的效果并不是這種默認的對齊方式。我們希望的是,所有對象的Z軸偏移應該是從tile幾何中心向下有一個合適的偏移量。換句話說,每層的“可視區域”應該從其tile中心點往下延伸一個距離,這個距離應基于tile的深度(例如以tile的米數為單位),用于表示該層的地板區域。這么做的目的是讓具有Z軸偏移的對象不會輕易越界“蹦”到上一層去。
我們在代碼中嘗試實現這一點:在添加標準房間時,將其Z軸偏移值設置得更低一些,希望借此讓所有實體相對tile的Z值都往下偏移,從而避免不正確地被分類到上層。
理論上講,完成了上述偏移調整后,所有對象的位置都應該被“拉”到正確的樓層范圍中,不再跨越層級被錯誤渲染。然而,實際運行中我們發現這種調整并未完全生效。舉例來說,仍然有兩個對象顯示在不應出現的地方,也就是說,它們的Z判斷邏輯依然被歸入了錯誤的層級。
這種現象說明,盡管我們已經修改了Z軸偏移的邏輯,但渲染或分層的判斷機制中可能還有其他遺漏。可能是在計算Z層級歸屬的函數中還有未使用新的偏移規則,或者某些地方仍然使用了原本未修正的tile中心點作為判斷依據。
這說明接下來還需要進一步排查Z軸分層邏輯的完整性,確認是否所有參與渲染和層級歸屬的邏輯路徑都采用了統一的偏移準則,確保所有對象能夠在視覺上和邏輯上正確歸屬于其所在的樓層。
修改 game_world_mode.cpp:在 ChunkPositionFromTilePosition()
中以不同方式計算 TileDepthInMeters
我們遇到的問題是某段代碼無法正常工作,目前還不清楚是因為邏輯錯誤,還是我們當初設計時就犯了某種錯誤。情況相當復雜,需要仔細分析。
目前系統中存在多個“堆棧”(stacks),而在這些堆棧中的某個位置是模擬區域(simulation region)的原點。大多數情況下,這個模擬原點會剛好落在某一個切片(slice)上,也就是說它和切片是對齊的。
問題可能出現在相機的位置和模擬區域的位置之間存在偏差。雖然我們以為模擬原點就在我們觀察的位置上,但實際上相機的位置是不一樣的,相機會有自己的偏移。
模擬區域的原點是按固定步長(step)移動的,而相機卻有一個額外的偏移量,也就是說我們不能簡單地把相機的位置等同于模擬原點。相機有一個“world mode camera offset”(世界模式相機偏移),這是一個額外的因素,可能正是導致我們之前邏輯出錯的關鍵。
因此,模擬區域在移動時是按步進更新的,而相機的位置是獨立的,并且帶有額外的偏移,造成了兩者之間的不一致。這種不一致可能就是導致我們觀察到問題的根本原因。我們需要重新考慮相機和模擬區域的位置關系,明確它們在空間中的對應方式,從而修正現有的計算或渲染邏輯。
使用調試器:中斷進入 ChunkPositionFromTilePosition()
并檢查偏移值
我們正在檢查某個區域的創建過程,重點是觀察從頂部位置映射到塊(chunk)空間時的坐標變換情況,以確認是否符合預期。
首先,我們注意到Z軸方向上有一個位移是-1.2,這是符合預期的結果,因為這個值正好是某個數除以0.4得出的,表示模擬空間中的Z軸位置。接下來我們將這個值映射到塊空間中,結果也如預期一致,說明目前的映射過程本身沒有問題。
然后我們查看了一個標準房間的代碼,其中有一個offset_y + 2
的操作,這個是將整體向上移動的邏輯。我們暫時不明白這個操作是否合理,但從結果來看,它似乎確實完成了我們想要做的事情。然而也可能存在某種邏輯錯誤。
進一步分析我們發現,如果offset_y的值在-2到2之間,那么加上2之后的范圍就是0到4。這種情況下,理論上Z軸偏移(p_offset_z)應該不會越界,仍然在允許的范圍之內。這種判斷基于對偏移后的數值的理解,預期結果仍在合法范圍內。
我們隨后嘗試測試最高的一個偏移情況,具體坐標是0、1、2,并觀察此時的p_offset_z值。在這種情況下,我們看到初始偏移是-1.2,然后疊加上樓梯最高層的位移,結果得出0.8。
這個結果在數值上是合理的,因為0.8仍然會被歸類到原本的那一層chunk中,也就是說它的坐標在處理時仍會“對齊”到同一層chunk。因此,從理論上看,所有角色應該都還在同一個chunk之內。
但實際觀察發現情況并非如此——最終效果并沒有達到我們預期的“所有人都在同一個chunk”這一結果,說明某處邏輯可能仍存在問題,盡管看起來計算是正確的。可能是由于某個細節沒有被正確考慮或執行導致的偏差,還需進一步深入驗證。
使用調試器:中斷進入 ConvertToLayerRelative()
并查看 Z 值
我們當前的問題是,盡管在邏輯上將對象正確地分配到了對應的chunk中,但在結果上卻沒有得到預期的渲染效果。我們開始進一步追蹤Z軸坐標的數值,以尋找偏差的來源。
我們注意到,實際獲取到的Z值頻繁為0,這顯然不符合我們之前的偏移計算結果。推測可能是相機的位置被設置到某個特定對象上,從而抵消了我們原本對位置所做的位移。這意味著雖然對象在模擬階段正確地歸類到了chunk中,但在渲染階段這些位移被抵消了,導致顯示結果出現偏差。
理想情況下,渲染過程本應直接依據chunk信息來處理對象的坐標,而不是依賴當前相機的偏移,這也是接下來需要優化的部分。
我們進一步查看了實體變換的處理邏輯,發現其使用了默認的“直立變換”方式,并通過get_ground_point_entity
減去camera_p
的方式進行坐標換算。但是這一處理方式存在問題,因為并未使用絕對坐標。我們真正需要的應該是實體在全局空間中的絕對位置,而不是相對于當前模擬區域的偏移值。
進一步推斷,get_ground_point_entity
可能本身是相對于模擬區域(sim region)的位置,而非絕對世界坐標。這意味著如果我們只看相對坐標,在相機已經發生位置抵消的情況下,可能會導致渲染出的層級不準確。
因此,我們實際想要知道的應該是兩個chunk之間在Z軸上的差值:一個是實體所屬chunk,另一個是當前模擬區域的原點所處的chunk。這才是影響渲染排序或層級歸類的真正關鍵值。
雖然當前的處理流程中存在一些雜音和干擾因素,但從邏輯上我們已經逐步理清楚了核心問題所在。接下來需要調整的是坐標變換和渲染時所依據的基準,使其基于絕對chunk位置進行判斷,而非相對坐標,這樣才能得到一致的渲染效果。我們還在理清思路的過程中,很多細節需要逐步推敲和確認。
修改 game_entity.cpp:臨時計算實體應處于的相對圖層
我們的目標是正確判斷實體所在的層級,以用于層級之間的透明度(alpha)查找。為此,我們需要基于實體在世界中的實際位置來進行計算。
具體而言,我們需要獲取實體在世界空間中的全局坐標,尤其是其所在的chunk的Z軸索引。然后,將這個Z索引減去模擬區域(sim region)原點所在chunk的Z索引,得到它們之間的Z軸chunk差值。這個差值就是我們用于判斷實體所屬“相對層級”的關鍵數據。這個計算結果將用于渲染系統中確定透明度查找表所使用的層索引。
目前的一些舊邏輯,比如使用實體Z位置與相機Z位置之間的差值來計算層級,已經不再適用,因為現在的系統中,相機Z的差異不再影響層內的變換。每個層是獨立渲染的,內部不再有基于Z軸的比例變換。
因此,之前那些依賴相機Z距離的判斷邏輯可以被廢棄,我們只關心chunk之間的層級偏移。為了將這個思路落實,接下來的工作是:
- 建立獲取實體在世界空間中位置的基礎邏輯;
- 提取chunk Z索引并進行差值計算;
- 基于這個差值決定該實體處于哪個相對渲染層;
- 將舊的、基于相機Z差的判斷邏輯移除。
此外,我們記錄了一個待辦事項,即明確指出:層級判斷依賴的是實體和模擬區域原點在chunk Z方向上的差值。當前有一個與多行雙斜杠注釋相關的bug也已經報告,預計將在后續版本中修復。
總體而言,現在我們已經清晰了下一步的修正方向,盡管還有許多工作需要完成,但路徑已經明確,后續只需按計劃逐步推進即可。
問答環節
你預計最終游戲會有足夠多的 sprite 需要使用紋理圖集,以減少每幀的紋理綁定次數嗎?
我們討論了游戲中是否有必要使用紋理圖集(texture atlas)來減少每幀的紋理綁定次數。結論是,這主要取決于具體情況,但總體來說可能性不大。
現在硬件對紋理切換的開銷已經非常小,幾乎不存在性能懲罰。尤其是在Nvidia顯卡上,這一點非常明顯,因為它們支持紋理綁定列表(bind lists),幾乎不會有紋理切換的性能問題。換句話說,現代硬件基本不需要擔心頻繁切換紋理的性能影響。
即使是Intel顯卡,紋理切換的開銷也已經微乎其微。唯一可能有些差異的是AMD硬件,但總體來說,這不是一個普遍存在的瓶頸。
如果目標是非常老舊的硬件,那么紋理切換的開銷可能才會成為問題。但對于當前和未來的硬件平臺,完全可以忽略這點。
此外,即便游戲最終需要使用紋理圖集,這也不必在開發初期就強制實施。可以在游戲完成或發布后,再進行紋理圖集的優化。這不會影響整體架構設計,只是要求資源管理器更智能,能夠更好地處理資源的分頁加載和管理。
總結來說,紋理圖集優化是一個后期優化步驟,不必過早考慮,不同硬件平臺有不同表現,但現代主流硬件幾乎不受影響。我們的重點應放在功能和架構的實現上,性能優化可以留到后期針對具體硬件進行。
我不明白為什么每層樓的“基準”Z值是負的,這看起來太刻意了。從邏輯上講,讓 0 作為底部不是更合理嗎?是不是和渲染有關?我最近沒跟進直播內容
關于樓層的基準值為什么是負數,而不是從零開始,這其實并沒有絕對的對錯,這種設計非常靈活,可以根據需求自由決定。
有觀點認為基準值為零更合邏輯,尤其是作為樓層的最低點,但實際上這主要取決于渲染和空間劃分的需求。這個問題本質上是一個空間分區的問題,我們把場景分成了“塊”(chunk),每個chunk代表空間中的一個區域。
選擇負數作為基準值并不奇怪,也不是勉強的做法,只是空間劃分的一種合理方式。沒有所謂“唯一正確”的答案,完全可以根據具體需求調整。
另外,在調試過程中遇到了屏幕保護程序或屏幕鎖定自動啟動的問題,影響了觀察和操作,需要關閉自動鎖屏功能以便更好地查看調試信息。這是一個額外的環境配置問題,不影響核心邏輯的討論。
總結來說,空間分區的基準值設計是靈活且可調整的,沒有唯一標準。當前遇到的屏幕鎖定問題已經通過關閉自動鎖屏功能得以解決,方便后續調試和觀察。
黑板講解:區塊的規范化點
我們討論了在三維空間中如何定義一個“塊”(chunk)內的標準參考點位置的問題。雖然游戲中使用的是二維精靈,但底層的空間表示實際上是三維的。
對于一個塊來說,有幾種選擇來確定其標準參考點的位置:
- 選擇塊的一個角落作為參考點,比如左下角。
- 選擇塊的中心作為參考點。
- 選擇塊在X和Y方向的中心位置,但在Z方向是底部。
這個選擇會直接影響到相對位置的基準值,比如樓層的Z值。如果選擇角落或者中心底部作為參考點,樓層的Z值可能接近零,但實際上通常不會是整零,因為處于邊界會導致排序和舍入的問題,可能會希望偏移一點(比如0.1),以避免計算上的麻煩。
如果選擇的是立方體中心作為參考點,那么樓層的相對Z值會是一個負數,比如-0.5或者其他數值,取決于樓層距離中心的高度。這個選擇也是完全可以的,沒有絕對對錯。
但是,如果選擇樓層為零作為基準,而X和Y方向又以中心為參考點,這會導致坐標系在處理上變得不一致。因為這樣X和Y是以中心計算,而Z是以底部計算,導致計算時的參考點不統一,邏輯上會顯得混亂,需要在代碼中分開處理。
盡管渲染系統中因為某些效果必須對Z軸做特殊處理,但空間分區本身不應該引入這種人為的區別。空間分區的三維坐標系統最好保持一致性,要么全部用中心點,要么全部用角落(最小點)。
因此,如果堅持要讓樓層的Z值基準為零,那么X和Y也應該改為以塊的最小角作為基準點,而不是中心。這樣三維空間的參考點統一,代碼處理也更簡單清晰。
總結來說,定義塊內參考點的位置沒有唯一標準,完全是設計選擇,但為了代碼的簡潔和一致性,建議X、Y和Z方向采用統一的基準點,避免不同軸用不同基準點導致的復雜度增加。