回顧并為今天的內容做準備
昨天,我們解決了一些關于排序的問題,這對我們清理長期存在的Z軸排序問題很有幫助。這個問題我們一直想在開始常規游戲代碼之前解決。雖然不確定是否完全解決了問題,但我們提出了一個看起來合理的排序標準。
有兩點不確定:第一,我們不能百分之百確定這個排序標準總是適用于所有情況,盡管我們盡量考慮了大多數可能遇到的情況,但也可能有未想到的特殊精靈情況。第二點則是關于排序算法本身的問題。即使排序標準是合理的,它也可能不會產生一個完全一致的排序結果,也就是說排序標準不一定能保證產生一個全序或部分有序的關系。這樣,使用常規的排序算法,比如歸并排序,可能無法正確解決排序問題。
我們計劃用一個簡單但開銷較大的N2算法,在調試場景下檢查排序結果。具體做法是排序完成后,逐對比較所有精靈,根據排序規則確認排序是否正確。如果發現有任何排序順序錯誤,就說明確實遇到了無法用簡單排序解決的問題。
這種情況并不致命,只是意味著需要使用更復雜的圖論方法來解決排序問題。昨天提到的 Andrew Russell 寫的博客中講了類似的情況,他在對《River City Ransom Underground》的精靈排序時,發現只能用帶有語義理解的圖排序方法,因為存在不可排序的情況。
雖然目前還不確定是否必須用圖排序,但如果必須用,情況也還算好,因為可以解決問題。只不過我們更傾向于用更簡單的排序方法,比如歸并排序,因為圖排序可能效率更低。雖然我們還沒有具體測試圖排序的性能,但一般情況下簡單排序會更快。
總之,目前我們已經寫好了排序代碼,代碼可以編譯,但運行時發現排序功能并沒有被實際調用,程序仍在執行舊的排序代碼,這不是我們想要的效果。接下來會繼續調試和測試,看看排序在真實場景中的表現是否符合預期,是否會遇到上述兩種潛在問題。
game_sort.cpp:仔細檢查 IsInFrontOf() 函數的方向是否正確
今天的主要目標是繼續完成排序邏輯的最后一部分。排序系統基本已經寫好了,不過還剩下一個需要處理的問題。此前有個名叫 Steven(或四分之一Tron)的觀眾在聊天室里指出了一個潛在的錯誤。他提醒我們在處理兩個都是Z類型的精靈時,可能忘記翻轉某些符號方向。
具體來說,Z類型精靈是指帶有范圍邊界的精靈。問題涉及的判斷邏輯是:當兩個精靈都不是“扁平”的,也就是說它們的 YMin
和 YMax
不相等時,我們就認為它們都是Z類型的精靈;如果其中一個的 YMin
等于 YMax
,那么它就不是Z類型。
最初的邏輯是:如果兩個精靈的 YMin
不等于 YMax
,那它們就是Z類型;否則就是一個是Z類型,一個不是。回頭看這段判斷邏輯,還是覺得是正確的。因此也不確定聊天室里提到的錯誤是否真的存在。暫時認為這個邏輯沒問題,但也不排除可能忽略了某些細節。
此外,在這過程中還出現了一個技術問題——鍵盤暫時失靈,導致操作受阻,但很快恢復了正常。
總之,現在的重點就是繼續推進,保持代碼邏輯清晰,必要時結合聊天室里的反饋進一步修正排序判斷的邊界條件。后續會繼續觀察排序系統在實際運行中的表現,確保所有Z軸相關的排序處理都能正確執行。
game_sort.cpp:考慮如何免費實現分層排序,并將整個渲染器轉變為基于精靈邊界的排序
我們已經完成了我們需要的歸并排序(merge sort),現在它可以對 sort_sprite_bound
類型的數據進行排序。不過這個過程當中有一些有趣的細節值得深入思考,雖然暫時不確定是否要馬上利用它們。
當前排序的數據結構
除了 YMin
、YMax
、Z
和 ZMax
等用于排序的字段之外,每個 sort_sprite_bound
還帶有一個 Index
字段,這個字段指向我們最終要繪制的對象。
引入“虛擬平面”的想法
我們突然意識到一個潛在的優化思路:
可以人為插入一些“虛擬平面”,作為圖層的分隔標志,從而實現圖層內自動排序。
- 假設我們插入一個擁有“無限 Y 區間”的虛擬物體,它在 Y 軸上會始終被認為是一個 Z 精靈;
- 由于它沒有實際繪制內容,只存在于排序階段;
- 那么在排序過程中,它就可以作為一個“層分割線”,將所有 Z 值在其下方或上方的元素分組;
- 通過這種方式,我們在排序的同時,也順便將內容分隔成了不同的圖層。
當我們在之后處理排序結果、依次“退役”渲染指令時(即開始真正繪制),只要檢測到這些“虛擬平面”即可知道是否到了新的圖層。
這是一種聰明的技巧:用歸并排序的規則自動實現圖層劃分,無需額外的圖層管理邏輯。
現有排序系統中的處理
我們當前的排序系統是用于多種用途的,比如用于顯示性能計數器時也要用排序。
所以,我們不應該破壞原有 sort_entries
的那部分邏輯,它仍要繼續保留。
接下來我們的計劃是:
- 把整個渲染器的排序方式切換為基于
sort_sprite_bound
; - 將所有需要繪制的內容推入
sort_sprite_bound
列表中; - 借助排序規則自動實現合理的前后關系和圖層分隔。
實際挑戰:數據準備
實現排序不難,真正的挑戰在于:我們現在并沒有準備好所有必要的排序信息。
我們需要確定每個圖形元素的:
YMin
/YMax
:表示它在屏幕上的垂直覆蓋區域;Z
/ZMax
:表示其深度范圍,用于在 Z 精靈中判斷前后關系;
特別是我們常用的“直立卡片”類型的元素(如角色等),它們是豎直的、站立在地面上的,可以忽略 Y 區間的跨度,僅需設置一個 YMin == YMax
的值。
這樣做對我們的排序規則非常友好,因為它會優先按 YMin 排序,省去了復雜的計算。
而對于一些“地面瓷磚”類型的元素,它們是平躺的貼圖,有實際的面積覆蓋:
- 我們確實知道它們的大小,因為地面磚塊通常都是單位格;
- 所以我們可以準確地設置它們的
YMin
/YMax
,從而參與到更復雜的排序中。
理想情況
如果一切順利,我們只需要為這些“地面元素”設置正確的邊界信息,然后對所有元素調用一次排序函數即可得到正確的繪制順序,無需額外的圖層機制或特殊處理。
這會大大簡化渲染流程,而且運行效率也較高。
下一步計劃
- 替換渲染器使用
sort_sprite_bound
的方式; - 插入實際的渲染指令;
- 逐步調試排序結果是否正確;
- 如果必要,引入“虛擬平面”作為圖層隔斷;
這是當前的分析與計劃。接下來就要開始動手處理這一切了。
game_render_group.h:考慮完全從 entity_basis_p_result 中去除 SortKey
我們現在的進展是這樣的:
當前渲染流程的結構:Render Group
我們正在使用的渲染系統中,主要依賴一個叫 render_group
的結構來推送所有的渲染指令。而我們以往在這個過程中,使用的是一種叫做 SortKey
的機制來排序可見實體(entity basis)和相關的基礎繪制結果(basis result)。
但是隨著我們切換到使用 ZMax
和 YMin/YMax
為核心的新排序機制,舊的 SortKey
方案將不再適用。
可能不再需要 SortKey
從現在的排序規則來看,我們實際上只關心 ZMax
值,這是排序判斷“誰在誰前面”的核心因素之一。這個值理論上并不需要進行復雜變換:
- 它只需要相對于攝像機的位置;
- 但其實即便不做這個偏移,也可能不影響排序;
- 因為我們排序是相對的,只要所有值在同一個空間里,排序順序就仍然是有效的。
因此,如果我們足夠幸運,可能可以完全移除 SortKey
相關的邏輯,在 entity
和 basis result
等結構中都不再使用它。這樣會讓代碼變得更簡潔、邏輯更清晰。
不過我們也不能太早下結論,現在還不能確定這個設想是否完全可行,只能說這是一個很好的可能性。
接下來的目標
暫時不去管是否真的能完全擺脫 SortKey
,我們還是把重點放在:
- 將新的排序邏輯正式集成到渲染器;
- 確保所有實體和繪制項都能正確地填入
ZMax
、YMin/YMax
等關鍵數據; - 替代原來的排序流程;
- 檢查整體排序是否正確地反映了視覺前后關系。
我們將以此為方向,繼續推進渲染系統的重構和測試。
game_render_group.cpp:讓 PushRenderElement_() 接收 sort_sprite_bound 而不是 SortKey
我們目前正在重構渲染系統的排序部分,把原本使用的 SortKey
排序邏輯,全面切換為基于 sort_sprite_bound
(精靈邊界)的新排序方法。以下是我們這段時間處理的核心內容與思路:
將 SortKey 替換為 Sprite Bound 排序邏輯
之前的做法是,在推送渲染元素(Render Element)時,會生成一個 SortKey
,并將其作為 sort_entry
存儲,用于后續排序。
現在我們不再使用 SortKey
,而是使用結構體 sort_sprite_bound
,它內部包含了更豐富的空間信息:
YMin
,YMax
: 精靈的垂直邊界范圍;ZMax
: 精靈的深度排序依據;Index
: 實際渲染命令在 push buffer 中的位置。
我們開始在代碼中替換所有以 sort_entry
命名的邏輯為使用 sort_sprite_bound
。
渲染流程中的調整
在實際操作中,我們發現我們并沒有在太多地方使用 sort_entry
,這使得替換的復雜度沒我們預期的那么高。
- 我們定位到具體調用
PushRenderElement
的地方; - 然后修改邏輯,使其推送
sort_sprite_bound
; - 此結構比原來的
sort_entry
稍大(大約兩倍),但仍然在可接受范圍內。
Push 過程的關鍵細節
我們注意到 sort_sprite_bound
中的 Index
成員并不應該由調用者填充,而應由 PushRenderElement
內部計算并設置:
- 因為只有
PushRenderElement
知道渲染命令在 buffer 中的偏移量; - 所以在調用方傳入時,只需要傳入
YMin
,YMax
,ZMax
等排序信息; Index
字段應在 push 時自動設置,確保其準確對應實際的渲染命令位置。
這就帶來了一個小問題:我們不能直接傳入完整的 sort_sprite_bound
實例,因為其中的一個字段(Index)是由函數內部控制的,而不是由調用者決定的。
設計上的權衡思考
這讓我們開始思考是否需要調整結構設計:
- 是繼續保留當前設計,在外部構建
sort_sprite_bound
,再在 push 時覆蓋 Index? - 還是將
Index
從結構中剝離,讓排序信息和緩沖區位置分開管理?
目前還沒有定論,但我們傾向于保留結構的完整性,只是在使用時注意內部字段的控制歸屬。為了簡化接口與使用,未來也可能考慮封裝成構造函數或者構建器來創建這個結構。
當前目標
- 用新的
sort_sprite_bound
結構完全替代舊的排序邏輯; - 將所有
PushRenderElement
邏輯改為支持新結構; - 確保 Index 正確反映渲染命令位置;
- 在后續排序時使用新結構進行排序(YMin/YMax/ZMax)。
我們已經完成了主要的結構替換工作,接下來將開始在實際數據流中測試并調試這些變更。
game_sort.cpp:將 sort_sprite_bound 分離成 sprite_bound,并讓 IsInFrontOf() 接收 sprite_bound
我們正在進一步優化渲染排序系統的結構與接口設計,目的是讓排序相關的數據結構更加清晰、解耦,并減少潛在的優化器混淆或調試性能損耗。以下是我們目前的思路和調整內容:
結構設計上的優化思考
當前我們在推送渲染指令時,需要提供三個核心值:
YMin
:精靈底部位置YMax
:精靈頂部位置ZMax
:用于深度排序的最大 Z 值
之前我們使用 sort_sprite_bound
結構體來打包這三項加一個 Index
,但由于 Index
是在 push 渲染時才生成的,而不是調用方能確定的,所以我們在傳參時會面臨一個邏輯上的割裂。
為了解決這個問題,我們引入了一個新的中間結構,例如:
struct sprite_bound {float YMin;float YMax;float ZMax;
};
然后,我們在推送函數中傳入的是這個結構而不是完整的 sort_sprite_bound
。在內部再由系統自動設置 Index
值,構造最終的 sort_sprite_bound
,用于排序和渲染調度。
這種做法的好處在于:
- 接口更簡潔:調用方無需知道 Index 的存在;
- 結構更清晰:把用于排序的信息和控制信息分離;
- 更易優化:讓編譯器更容易理解數據結構的只讀特性。
接口重構與調用優化
由于我們現在傳入的是一個簡單的 sprite_bound
類型值:
- 所有使用
IsInFrontOf()
比較函數的地方也要相應更新; - 不再使用包含
Index
的完整結構去判斷誰在前誰在后; - 只需要在比較時傳入兩個
sprite_bound
即可。
例如:
bool32 IsInFrontOf(sprite_bound A, sprite_bound B);
傳值(pass-by-value)而非指針或引用的形式也帶來潛在好處:
- 編譯器可以明確知道這些值在函數內部不會被修改;
- 避免潛在別名(aliasing)問題;
- 有助于更好的內聯優化(尤其是在 Release 模式下)。
雖然這可能在 Debug 模式下稍微降低性能(由于結構復制),但整體來看,在 Release 編譯中能帶來更高的效率和更穩定的優化行為。
舊邏輯的遷移與簡化
我們同時對舊代碼中使用 sort_key
的部分進行替換或標記:
- 將原來的
sort_key
替換為新的sprite_bound
; - 在推送邏輯中構建最終結構并寫入渲染命令緩沖區;
- 在排序前操作中,完全使用新的邊界數據結構。
總結
我們目前的策略是在系統中引入一個更清晰、更職責單一的結構(如 sprite_bound
),作為排序用的傳參值,從而:
- 降低使用復雜度;
- 避免不必要的冗余字段傳遞;
- 減少編譯器優化上的不確定性;
- 提升后續排序、渲染調度的可維護性。
未來我們還可以考慮進一步封裝排序規則,讓調用層幾乎不需要理解排序細節,只需關注其渲染表達的語義。
game_sort.cpp:考慮傳給 PushBitmap() 的正確值
我們現在已經完成了排序邏輯內部的調整,使其接受新的排序鍵 sprite_bound
,并在合并和單元素比較的兩個關鍵環節都替換為傳入該結構體。這一變化使得整個排序流程本身準備就緒,可以從外部正式切換過來使用新的機制。但是,接下來要面對的問題是,原先所有調用排序的渲染接口現在都無法正確工作,因為它們仍舊傳入的是舊式的“排序鍵”(如簡單的 sort key 值),而我們現在需要的是 包含 YMin、YMax 和 ZMax 的完整邊界信息。
外部渲染調用的問題
主要問題集中在類似 PushBitmap()
這樣的接口調用上:
- 原先調用這些接口時只傳入了一個 Z 值或簡化的 sort key;
- 現在排序系統需要完整的邊界信息(YMin/YMax/ZMax)才能完成排序;
- 所以原有的調用點都“失效”了,必須重新構造這些值。
推測邊界信息的思路
當前我們并沒有完整的邊界數據可用,只能根據已有信息推斷,比如:
-
對象是否為“直立型”(upright)
- 這個信息我們可以從對象的 transform 結構中獲得;
- 如果是直立型精靈(例如人物立繪),我們可以假設它們是垂直延展的,Y 方向范圍很小;
- 如果不是直立的,而是“貼地面”的(如地板、道具等),那么它們會在 Y 方向上有一定的寬度。
-
位圖的尺寸
- 通過 bitmap 的高度和寬度,我們可以估算它在 Y 方向上的投影范圍;
- 將此尺寸應用在物體位置上,推算出 YMin/YMax;
- ZMax 的推算可以保持與原有邏輯一致,甚至可以直接使用對象 transform 的 Z 值。
擬定臨時方案測試效果
我們準備嘗試一種折中的臨時處理辦法:
- 使用對象 transform 中的“upright”字段來判斷 Y 方向的延展方式;
- 如果是直立型,我們可能設置一個極小的 Y 范圍(或統一的默認值);
- 如果是平鋪型,我們根據 bitmap 尺寸推導出一個 YMin/YMax;
- 然后將這些值組成
sprite_bound
,傳入新的排序系統。
這不是絕對精確的方法,但足以用于嘗試渲染流程是否能正常運作。我們會根據實際運行效果來判斷這個方法是否值得保留或進一步細化。
總結
目前的目標是將外部渲染調用過渡到新的排序系統。面臨的問題是缺乏完整的排序所需邊界數據。我們提出了以下策略應對:
- 利用 transform 中的“upright”字段來初步區分排序方式;
- 使用位圖尺寸推算 Y 方向的邊界值;
- 嘗試這種方法后觀察排序與渲染是否合理,后續再逐步精細化處理邊界估算邏輯。
這是從系統結構向實際數據落地的關鍵一步,也是排序系統徹底統一的前置條件。后續會繼續調整 bitmap 渲染等接口,確保每個調用點都能提供正確的邊界信息。
game_render_group.h 和 *.cpp:從 entity_basis_p_result 中刪除 SortKey
我們現在明確了一點:原有的 sort key(排序鍵)系統已經徹底作廢,因為我們現在采用的是新的排序機制,基于 sprite_bound
(包含 YMin、YMax、ZMax 的結構體)來完成排序。因此,所有依賴舊 sort key 的邏輯都可以直接刪除,整個代碼中與 sort key 相關的部分都將被移除。
清理舊邏輯
首先清除掉:
- 所有從 DIM Basis(如 entity basis)傳遞 sort key 的過程;
- 在
PushBitmap
或其他渲染接口中設置 sort key 的操作; - 在排序和渲染流程中依賴 sort key 的判斷或處理邏輯。
這些舊邏輯都不再有任何作用,統一刪除。
新邏輯的核心依賴:upright 狀態
新的排序方式依賴的是 sprite_bound
的三個值:YMin、YMax 和 ZMax。為了正確生成這三個值,我們需要知道一個關鍵屬性:
當前位圖是“平面貼地”還是“垂直豎立”?
這個信息由 object_transform
中的 upright
字段給出,這是我們目前判斷物體朝向的唯一依據。
-
若為
upright = true
,表示該對象是豎直朝上的,比如人物立繪、招牌等;- 此時我們可以忽略 Y 范圍,直接使用
ZMax = transform.Z
;
- 此時我們可以忽略 Y 范圍,直接使用
-
若為
upright = false
,表示該對象是鋪地的,比如地磚、地面物品等;- 這時必須設置 YMin/YMax,我們將通過 bitmap 的尺寸來推算;
- ZMax 仍然可以直接取 transform 的 Z 值。
如何將 upright 正確用于排序構建
我們目前的問題是:upright
字段存在于 object_transform
中,但未必在所有地方都方便讀取。所以我們面臨一個設計上的抉擇:
-
是否應該把
upright
提取出來作為PushBitmap
的一個明確參數?- 這可以使排序判斷邏輯更加明確清晰;
- 避免未來
object_transform
內部結構變動帶來的耦合問題。
雖然我們還沒做最終決定,但可以明確一點:**無論放在哪里,upright
的信息是不可或缺的。**因為排序邏輯離不開它。
總結
我們完成了以下幾個關鍵步驟和思考:
- 舊 sort key 全面作廢,相關代碼將被清除;
- 新的排序機制依賴
sprite_bound
,必須構造出 YMin/YMax/ZMax; - 構造方式依賴物體的
upright
屬性,必須保證其在調用PushBitmap
時可以獲取到; - 未來可能需要調整接口設計,將
upright
明確作為參數傳入,而不是隱含在 transform 中。
這一步已經為我們清晰了外部調用排序邏輯的入口條件,接下來的任務就是在所有調用點確保 sprite_bound
能夠被正確構造。這樣整個渲染系統才能正式遷移到新的排序機制上。
game_render_group.cpp:讓 PushBitmap() 為 Upright(直立)精靈設置 SpriteBound.YMin 和 .ZMax
在當前的排序邏輯重構過程中,我們面臨兩種情況:豎直物體(upright) 和 平鋪物體(lying flat)。我們分別討論了它們在構造 sprite_bound
(YMin、YMax、ZMax)時的差異。
對于豎直物體(upright)
這類物體的高度決定它在 Z 軸上的可見性,但在 Y 軸上并不需要參與排序。
- ZMax:直接取 transform 的 Z 值即可;
- YMin/YMax:可以設為任意默認值,因為在排序中不使用。
這是一個理想情況,計算簡單,不容易出錯,因此我們已經實現了這部分邏輯并通過測試。
對于平鋪物體(lying flat)
問題正好相反:
- ZMax:確定且簡單,直接使用 transform.Z,因為這類物體“貼地”,Z 值不會隨著尺寸發生變化;
- YMin/YMax:困難的地方在于我們不知道這個 sprite 的 Y 向投影范圍。
雖然我們大致知道這個 sprite 是圍繞某個 Y 值居中渲染的,但缺少精確數據,不知道這個 sprite 在 Y 軸上實際“占了多少空間”。
臨時解決辦法
我們決定采用一種簡化策略,雖然它可能不準確,但可以作為臨時手段以驗證整體流程是否可行:
- YMin = transform.y - 某個“向后”偏移(假設值);
- YMax = transform.y + 某個“向前”偏移(假設值);
這個偏移量的計算方式依賴 sprite 的寬高信息或尺寸參數,但目前我們并未做精確計算,只是粗略模擬。
我們意識到:
- 這種方式不能覆蓋所有情況;
- 很可能在游戲后續運行中,某些 sprite 會因為排序錯誤而出現穿插、遮擋等視覺問題;
- 等整體系統跑通之后,我們需要回頭對這部分邏輯進行精度提升,可能要讀取 sprite 的真實邊界、處理非居中渲染、旋轉縮放等復雜因素。
下一步任務
- 繼續推進
PushBitmap
調用鏈中sprite_bound
構造的適配; - 臨時使用上述方式填充 YMin/YMax;
- 等排序穩定后,再評估哪些錯誤是由不準確的邊界計算造成的;
- 再反過來優化這部分邏輯,引入更精細的邊界推斷方法。
總結
我們為平鋪物體臨時設計了一種不準確但可運行的 sprite_bound
生成邏輯,確保整個新排序系統能跑通一遍。雖然它會帶來一些排序誤差,但這樣可以快速驗證結構正確性,等驗證通過后再逐步替換為更精確的算法。這是一個典型的“先跑起來再精細化”的迭代策略。
game_render_group.cpp:討論 SpriteBound
我們已經完成了構造 sprite_bound
的邏輯,現在可以直接將其傳遞到后續的排序流程中去,整個排序路徑可以順利運轉了。下面是我們對邏輯的進一步整合和優化,詳細說明如下:
初步統一邏輯處理
我們識別出一些公共部分,可以簡化處理流程,減少重復代碼,提高可讀性和可維護性:
-
ZMax 值始終是 transform 的 z 分量
無論是豎直還是平鋪的 sprite,ZMax 都是基于 transform.z 進行計算,因此我們將這部分代碼抽離出來,統一處理。 -
YMin / YMax 的條件注入
- 如果是平鋪(lying flat),我們會為 YMin 和 YMax 添加推導的范圍;
- 如果是豎直(upright),則跳過 Y 軸的范圍計算,只用 ZMax 即可。
這種分離式處理結構更清晰,邏輯也更直觀。
核心計算邏輯結構
我們在代碼結構上形成如下模式:
// 統一處理 zmax
sprite_bound.zmax = transform.offset.z;// 判斷物體朝向并決定是否處理 y 范圍
if (is_upright) {// 豎直物體:Y 范圍無關,僅設置 zmax 即可// YMin/YMax 保持默認值或不處理
} else {// 平鋪物體:需計算 y 范圍sprite_bound.ymin = transform.offset.y - backward_extent;sprite_bound.ymax = transform.offset.y + forward_extent;
}
這樣的邏輯可以清晰區分不同的 sprite 類型,并提供基礎的排序依據。
下一步可擴展方向
雖然當前的邏輯可以支撐排序邏輯的正確性,但我們也意識到有不少地方仍存在提升空間:
-
對偏移量
forward_extent
/backward_extent
的來源進行精化
目前我們是基于 sprite 尺寸估算的,需要根據實際 bitmap 尺寸或 bounding box 來精確推導。 -
支持更復雜的變換
未來如果引入旋轉、非等比縮放等情況,當前的基于 transform.offset 的方式將不再足夠,需要矩陣乘法等更復雜的處理。 -
統一封裝 Y/Z 范圍構造
可以進一步封裝為一個輔助函數,根據 sprite 的朝向、尺寸等信息自動構建完整的sprite_bound
。
總結
我們完成了將 sprite_bound
構造邏輯合理地嵌入渲染流程的改造,同時抽離出 ZMax 的統一計算和 YMin/YMax 的條件處理邏輯,從而形成了清晰可控的結構。這為后續排序系統的準確性和性能優化打下了良好基礎。后續重點在于精化邊界估算邏輯,使排序更加精準。
game_render_group.cpp:考慮讓 PushRect() 做和 PushBitmap() 相同的計算,并讓 Clear() 排序在所有內容下方
我們現在推進到處理 push_rect
的部分。從邏輯上來看,我們判斷這部分很可能可以復用與 push_bitmap
相同的排序邊界計算代碼。雖然目前還不確定完全適配,但初步猜測是可以的,所以我們決定嘗試用相同的計算方式處理。
push_rect
的處理思路
我們推測 push_rect
也能沿用 push_bitmap
中的 sprite_bound
構造方法,即:
zmax
統一設定為transform.offset.z
- 根據是否“豎直”或“平鋪”來決定是否添加 Y 方向上的
ymin
/ymax
- 采用和 bitmap 一樣的判定方法來處理 rect 的排序邏輯
這意味著我們有機會提取出一段共享代碼來統一處理所有此類“可排序可繪制元素”的邊界計算。
clear
操作的排序策略
接下來處理的是 clear
操作,它并不是真正的 sprite,但我們依舊希望它能“出現在最底層”,也就是被排到所有內容之前渲染。因此我們需要構造一個特殊的排序邊界來達到這一目的。
具體做法:
-
構造一個“虛擬”的
sprite_bound
,其值如下:ymin = REAL32_MIN
ymax = REAL32_MAX
zmax = REAL32_MIN
這樣的設置方式可以確保 clear
排序值永遠小于其他任何內容,因此會被排在最前面,達到“清屏在先”的目標。
這是一個非常簡單粗暴但有效的策略。
接下來目標:提取共享邏輯
由于我們現在已經有多個地方(如 push_bitmap
、push_rect
、clear
)都需要構造 sprite_bound
,所以我們下一步的目標是提取一段共享的代碼邏輯來統一處理這部分計算。
大致目標如下:
- 編寫一個輔助函數,比如
ComputeSpriteBound(transform, size, is_upright)
,統一處理 Y/Z 方向的邊界計算 - 對于
clear
操作,單獨使用特定的常量值(不通過函數),以避免冗余計算
這樣我們就能避免重復書寫大量的邏輯分支,提高系統的整體清晰度與健壯性。
小結
push_rect
有望復用push_bitmap
的邊界構造邏輯clear
操作通過構造極端值實現最底層排序- 下一步目標是提取出共享的 sprite_bound 構造邏輯,統一用于所有繪制操作的排序準備流程
整體來看,這是一種漸進式演進思路,先統一策略,再逐步抽象與優化。
game_render_group.cpp:引入 GetBoundFor() 并將 PushBitmap() 的功能抽取到其中
我們現在開始將構造排序邊界(sprite_bound
)的邏輯進一步抽象成一個可復用的函數,以便在不同渲染路徑中(比如 push_bitmap
和 push_rect
)共享。
目標:提取出 GetBoundFor
函數
我們要做的,是將構造 sprite_bound
的邏輯提取出來,形成一個統一的工具函數,比如叫 GetBoundFor
。這個函數用于根據傳入的參數生成用于排序的邊界信息。
所需參數分析如下:
-
object_transform.offset_p
對象的位置,是排序中 Z 值和 Y 值計算的基礎。 -
upright
標志位
用來判斷當前圖像是豎立的(如角色或樹木)還是平鋪的(如地面貼圖),這影響 Y 和 Z 的排序方式。 -
offset
圖像的偏移量,在計算邊界時需要加入用于精確定位。 -
height
圖像的高,用于計算 Y 向或 Z 向的擴展邊界范圍。
只要這四項有了,就能精確地生成 sprite_bound
。
調用方式及用法示例
函數調用形式如下:
sprite_bound = GetBoundFor(offset_p, upright, offset, height);
調用中,我們傳入:
- 當前的偏移位置
offset_p
- 是否豎立的布爾標志
upright
- 圖像偏移
offset
- 圖像高度
height
然后函數內部會判斷是“平鋪”還是“豎立”,從而使用不同的邏輯構造 ymin/ymax/zmax
。
應用到 PushRect 中
在 push_rect
的情況下,只需要把 height
設置為 Y 維度大小即可。因為 push_rect
通常就是用來畫二維矩形,而我們系統中的 Y 是垂直方向,所以它也符合之前的建模方式。
因此可以直接寫成:
sprite_bound = GetBoundFor(transform.offset_p, upright, offset, rect_dim.y);
補充說明:清理舊代碼
隨著這個函數的引入,我們可以徹底刪除之前在 push_bitmap
、push_rect
等路徑中重復的排序邊界構造代碼,統一通過 GetBoundFor
處理。
總結
- 成功抽象出
GetBoundFor
方法,用于統一構建sprite_bound
- 函數依賴四個參數:位置、是否豎立、圖像偏移、高度
- 適配了
push_bitmap
和push_rect
- 提升代碼復用性和可維護性,后續如果排序邏輯要調整,也只需改動一個地方
下一步,我們可以繼續觀察運行結果是否符合預期,若發現某些排序仍不對,就再調整 GetBoundFor
的具體實現邏輯即可。
運行游戲并迅速遇到大量問題
我們目前遇到的問題是,雖然在構建排序鍵(sort key)時做了結構上的改變并完成了代碼更新,但渲染器并不知道我們已經更改了排序鍵的格式。這會導致運行時崩潰或產生嚴重渲染錯誤。
問題核心
渲染器依舊使用舊的 sort_key
格式去解釋和處理渲染命令。而我們已經將 sort_key
替換為一個新的結構(比如 sprite_bound
),并用它來進行深度和順序上的排序。
但是,在 render
過程中,渲染器仍然:
- 從渲染命令中讀取
sort_key
- 使用舊的結構去解析數據(比如使用了舊的字段、類型轉換等)
- 在
SortEntries
時將內存內容強制轉換為老式結構體
下一步調整方向
我們必須同步更新渲染器中的排序邏輯與結構定義,以匹配我們更新后的 sort_key
結構。
首先,定位到渲染器中的排序邏輯:
- 找到
SortEntries
相關函數 - 查看它是如何處理每條渲染命令的
- 重點檢查它是否對
sort_key
使用了固定偏移量、結構體強轉或硬編碼解讀方式
例如,可能看到如下邏輯:
sort_entry = (sort_entry_type *)command->sort_key;
我們需要把這一類邏輯替換成基于新結構的訪問方式。
然后,更新 sort_entry
結構定義
如果我們將 sort_key
替換為一個包含以下字段的新結構:
struct sprite_bound {float ymin;float ymax;float zmax;
};
那么渲染器中排序函數就應當改為:
sprite_bound *a = &command_a->sort_key;
sprite_bound *b = &command_b->sort_key;if (a->zmax != b->zmax)return a->zmax < b->zmax;
else if (a->ymax != b->ymax)return a->ymax < b->ymax;
elsereturn a->ymin < b->ymin;
注意這里必須確保排序邏輯保持一致,避免前后代碼不對齊導致邏輯錯亂。
小結
- 渲染器仍然使用舊格式解析排序鍵,導致嚴重問題
- 必須更新渲染器中關于
sort_key
的訪問邏輯和數據結構 - 要確保排序函數匹配我們新的排序邊界結構體(
sprite_bound
) - 所有基于舊結構體的強制轉換、偏移操作都需要移除
更新完成后,渲染系統才能理解新的排序鍵格式,整體渲染才會恢復正確排序邏輯并正常運行。
game_sort.cpp:引入 GetSortEntries() 用于將 Entries 轉換為 sort_sprite_bound
我們正在對渲染系統中使用的排序鍵結構進行結構性重構,以適配新的 sprite_bound
類型。在處理過程中,發現了代碼中存在大量直接對排序鍵內存進行類型轉換(cast)的做法,這種方式在我們更新排序結構之后將變得非常危險,因為一旦結構體發生改變,所有這些顯式轉換都會默默失效,導致渲染邏輯出錯,且很難追蹤和維護。
🚩 主要問題
目前大量地方通過如下方式讀取排序鍵數據:
sort_entry = (sort_entry_type *)command->sort_key;
這種做法的問題在于:
- 對排序結構的更改不具備可追蹤性
- 所有調用處都必須手動更新
- 易錯、難以維護
- 無法形成統一的數據訪問通路
? 解決方案
我們將這部分邏輯提取為統一的接口函數,避免在多個調用點重復手動轉換結構,提高可維護性與健壯性。
🧱 實施步驟如下:
-
定義統一的訪問接口函數
在渲染器公共模塊中新增函數(例如在
sort.cpp
中):sprite_bound* GetSortEntries(game_render_commands *commands) {return (sprite_bound *)commands->SortMemory.Base; }
此函數明確指定返回類型為
sprite_bound*
,并隱藏了具體的類型轉換過程。 -
替換所有舊的直接類型轉換調用
將所有類似以下的代碼:
sprite_bound *entries = (sprite_bound *)commands->SortMemory.Base;
替換為統一調用:
sprite_bound *entries = GetSortEntries(commands);
-
統一在各個渲染路徑中使用新接口
包括:
- 通用渲染器路徑
- OpenGL 渲染路徑
- 可能存在的調試或測試路徑
我們已經在通用路徑和 OpenGL 路徑中完成替換。
-
構建錯誤傳播鏈以驗證完整性
替換之后,編譯器將幫助我們發現所有仍然使用舊方式訪問排序鍵的位置,借助這些錯誤信息,我們可以確保整個代碼庫中排序鍵的訪問方式全部統一。
總結
-
排序鍵結構體更新后,不能再通過直接強制轉換方式訪問
-
應將所有訪問封裝進統一的函數接口中
-
函數接口具備以下優勢:
- 自動適配結構更新
- 提高代碼安全性
- 錯誤定位更清晰
- 更易維護與重構
-
當前已完成替換并確保各路徑同步更新
隨著這個結構封裝完成,我們的排序邏輯訪問就更加穩固,后續再做結構調整也會變得輕松許多。
game_render.cpp:讓 SortEntries() 調用 GetSortEntries(),不再調用 RadixSort(),改用 MergeSort()
我們還有一個地方沒有正確調用新的接口,那就是執行排序本身的部分。現在排序調用的仍然是舊的 sort_entries
概念,而不是我們新引入的 sprite_bound
。如果當初在這里就調用了正確的封裝函數,那么一開始編譯就會報錯,能立刻暴露問題。這也是我們進行接口封裝的意義所在:確保結構發生變化時,編譯器能幫我們發現所有錯誤使用的地方。
排序部分的更新與適配
-
停止使用 Radix Sort
當前的排序邏輯原本依賴于 Radix Sort,但由于我們新設計的
sprite_bound
結構體是復雜的浮點值結構,不再是適合 Radix 的整數位字段形式,因此:- Radix Sort 完全不適用
- 必須改用 Merge Sort
-
改用 Merge Sort 實現
在實際排序代碼中已經更換為 Merge Sort,并更新相關調用,確保它使用的是我們新的
sprite_bound
結構:void Sort(sprite_bound *Entries, sprite_bound *Temp, uint32 Count) {// Merge sort implementation... }
為此,我們需要為
Temp
分配等量空間,用來輔助排序操作。 -
同步修正內存分配處
在渲染初始化或排序所需內存分配中,原本是基于
sizeof(sort_entry)
進行內存分配:NeededMemorySize = MaxSortEntryCount * sizeof(sort_entry);
這必須改為:
NeededMemorySize = MaxSortEntryCount * sizeof(sprite_bound);
否則即使邏輯正確,分配的內存不夠也會導致數據越界或崩潰。
-
將大小定義和訪問統一封裝
為了統一后續處理,可以考慮將排序項的類型與大小統一封裝為函數或宏,比如:
inline size_t GetSortEntrySize() {return sizeof(sprite_bound); }inline sprite_bound* GetSortEntries(game_render_commands *Commands) {return (sprite_bound *)Commands->SortMemory.Base; }
所有關于排序項類型和大小的調用都改用這些封裝,確保修改結構體時可以集中修改邏輯。
總結
- 已完全廢棄 Radix Sort,改用 Merge Sort,以適配復雜排序鍵結構
- 所有
sort_entry
使用點替換為sprite_bound
- 對內存分配中的類型和大小做了同步修正
- 排序接口調用已封裝為統一函數,減少未來維護成本
- 通過編譯器錯誤實現錯誤定位閉環,提升可維護性與健壯性
通過這一系列的結構調整和接口統一,我們的排序系統現在更加穩健、靈活,能夠適應不同結構體下的渲染排序需求,為后續功能拓展和調試提供了良好基礎。
game_render.cpp:將 GetSortEntries() 從 game_sort.cpp 中提取出來,并引入 GetSortTempMemorySize()
我們在這里要做的,是為排序內存的大小分配增加統一的查詢函數,比如 GetSortMemorySize
或 GetSortTempMemorySize
,這樣在 Win32 層的代碼中,就不需要手動硬編碼去計算排序內存的大小,而是可以通過調用這個統一的函數來獲取準確的數值。這么做的原因并不復雜,只是希望將相關操作的邏輯盡量聚集在一起,避免因改動某一個地方而遺漏另一個地方,從而導致錯誤。
目標
我們要實現的,就是在結構層級中,創建一個獲取排序內存大小的接口函數,使得任何地方在需要知道排序內存大小時,都可以調用這個函數而不是自己計算。
做法細節
-
引入內存大小接口函數:
比如我們定義以下函數:
size_t GetSortMemorySize(uint32 ElementCount) {return ElementCount * sizeof(SpriteBound); }
或者對臨時排序緩沖區也有:
size_t GetSortTempMemorySize(uint32 ElementCount) {return ElementCount * sizeof(SpriteBound); // 假設一樣大 }
-
用于統一調用:
在 Win32 渲染初始化代碼或排序所需內存配置處,就不再是:
size_t NeededSize = ElementCount * sizeof(SpriteBound);
而是:
size_t NeededSize = GetSortMemorySize(ElementCount);
-
方便未來結構變化
如果以后
SpriteBound
的結構體變化,或者內存排列方式發生變動(比如多一個對齊字段、加了 padding、變化為指針等),我們只需修改GetSortMemorySize()
函數內部的實現,其他調用此函數的地方完全不需要修改。
整體意義
我們這樣做的核心目的,并不是追求復雜的功能,而是追求一致性和可維護性:
- 改動結構時自動聯動:通過統一的接口,任何結構變化都只需修改一個地方。
- 提高可讀性:調用者更容易知道這里的內存是做什么用途的(例如排序),不用再去猜
sizeof(...)
的意圖。 - 避免重復邏輯:減少冗余代碼和硬編碼。
總結
我們增加了統一的排序內存大小接口,意在:
- 綁定內存結構和調用邏輯的關系
- 提高代碼維護時的連貫性
- 避免因結構更改導致遺漏更新的潛在 bug
操作本身很簡單,但能極大提升項目規模變大后的可維護性,是一個典型的架構優化策略。
win32_game.cpp:讓 WinMain() 調用 GetSortTempMemorySize()
我們接下來要做的事情,是在需要排序操作的地方統一調用 GetSortTempMemorySize
來獲取所需的臨時排序內存大小。我們把這種邏輯統一封裝起來,意味著今后在任何地方需要計算排序內存時,都不再手動處理,而是調用標準接口,保證一致性和可維護性。
實施步驟詳解
-
統一替換內存大小計算邏輯
將所有之前用來計算排序內存大小的地方,例如:size_t NeededSize = ElementCount * sizeof(SpriteBound);
統一改成:
size_t NeededSize = GetSortTempMemorySize(RenderCommands);
-
實現
GetSortTempMemorySize
函數
該函數內部會從渲染命令中提取元素數量,然后乘以排序所需的數據結構大小:size_t GetSortTempMemorySize(GameRenderCommands* Commands) {return Commands->SortEntryCount * sizeof(SortSpriteBound); }
-
整理冗余代碼
原先散落在各處的排序內存分配代碼可以刪除或重構,比如臨時數組的空間分配、類型轉換等。
影響范圍覆蓋
- OpenGL 渲染器:確保在排序前使用統一的內存大小函數。
- Win32 平臺下的渲染邏輯:內存分配階段也使用新接口。
- 排序實現邏輯(如 MergeSortSpriteBounds):調用這個函數保證輸入參數合法。
- GameSlow 或調試渲染路徑:這部分如果也涉及排序內存,同樣適配。
可預期的進一步修改
在繼續推進過程中可能會發現以下情況:
- 有的模塊根本沒有走通這條路徑,可能是死代碼或者走了其他后門邏輯;
- 有的排序函數內部并未真正使用類型安全方式訪問排序內存(例如強轉未統一);
- 某些舊路徑直接寫了
sizeof(...)
,沒有調用GetSortTempMemorySize()
,需逐步替換。
最終目的
整個重構的目的并不復雜:
- 提高排序結構和渲染流程之間的耦合性可控;
- 降低維護成本,未來排序結構一旦變動(比如排序字段變化、尺寸變動、類型升級),只需改動一處;
- 減少手動計算、強制類型轉換、復制粘貼代碼等易錯操作;
- 保證渲染器中所有路徑(主流程、OpenGL、調試路徑)一致性。
總結
我們現在正將排序所需內存的處理邏輯集中封裝到 GetSortTempMemorySize
這類接口中,逐步淘汰所有手動計算與冗余邏輯,使得未來無論渲染流程怎么擴展或排序邏輯如何復雜化,我們都能通過統一入口點進行維護和升級。這是一種非常有效的工程實踐,雖然短期內修改面略大,但長遠來看將極大提升代碼質量與穩定性。
game_sort.cpp:將 SortEntries() 標記為 TIMED_FUNCTION()
我們現在正好處于一個非常合適的測試點,恰好可以驗證排序邏輯的準確性,特別是“是否在前方(is in front of)”這個判斷邏輯是否在各種情況下都能成立。
當前測試目的
我們的目標是驗證排序系統在所有相關對象上的“前后”邏輯是否準確,也就是:
確保對于所有應該在前方的對象,
is_in_front_of
函數都返回true
。
這類驗證在 game_slow
之類的調試或慢速執行路徑中尤為重要,因為可以精確觀察渲染順序的錯誤。
執行的具體內容
-
測試“我的頭”是否在其他元素前方
舉例來說,像“我的頭”這個精靈,應該被排在地面元素或身體之上。這就意味著對應的排序判斷邏輯必須能識別出其位置在前。 -
驗證
is_in_front_of
邏輯在各種數據上的正確性
包括:- 平面精靈(如地面磚塊)
- 立起的精靈(如人物、物體)
- 特殊對象(如 HUD 或 UI 元素)
-
調試構建中的意外表現
有一個情況令人驚訝,就是在 debug 模式下并沒有出現期望中的報錯或異常。這說明可能存在一些函數邏輯在調試構建下被優化掉了,或者斷言未觸發。
后續行動
- 運行調試版本觀察輸出:通過手動或自動方式檢查哪些對象被錯誤排序;
- 明確是否所有路徑都調用了
is_in_front_of
:有可能部分對象走了舊路徑或未被加入排序; - 回溯 debug 構建未報錯的原因:例如檢查是否缺失了斷言邏輯、類型轉換判斷、nullptr 檢查等;
- 利用當前測試場景增強單元測試:將當前視為基礎測試用例,逐步擴展場景(如遮擋、嵌套對象、非標準 transform)。
總結
我們正處于驗證排序邏輯核心的階段,利用現有場景可以高效測試 is_in_front_of
的正確性。尤其是像“我的頭在什么前面”這類邏輯,可以作為非常典型的判斷用例。如果這類基礎排序都不能確保準確,那將影響整個渲染管線的視覺正確性。通過集中測試這部分邏輯,同時檢查 debug 模式下的意外表現,有助于我們及時發現并修復渲染排序系統的核心問題。
調試器:單步進入 SortEntries() 并檢查 EntryA 和 EntryB 的 SortKey 值
我們現在正深入調試排序系統的問題,目標是定位為什么某些精靈在渲染順序中出現錯誤。通過逐步進入排序比較函數,開始具體分析兩個參與排序的精靈條目的數據。
目的:驗證排序比較邏輯是否符合預期
我們希望檢查排序時比較邏輯是否在某些情況下未按我們想要的行為執行,尤其是“ZMax 值較大的精靈應被排在后面”的規則。
調試過程和觀察結果
-
初步測試失敗
最開始嘗試比較兩個精靈條目時,發現它們完全相同(YMin
、YMax
、ZMax
都一樣),這類情況下排序結果無法判定優先級,即排序穩定性不保證順序——這種情況跳過處理。 -
加入斷言以縮小排查范圍
于是我們添加了判斷邏輯:- 若兩個條目的
YMin
、YMax
、ZMax
全相等,則跳過; - 若有差異,則進入斷言,確認我們是否確實在處理排序順序有誤的精靈條目。
- 若兩個條目的
-
成功抓到一個排序異常樣本
在某一對條目中:- 兩個精靈都是立起的(
YMin
、YMax
相等); - 然而其中一個的
ZMax
為 1.25,另一個為 0.75; - 根據邏輯,
ZMax = 0.75
的條目應排在前,但實際ZMax = 1.25
的條目卻被排在前,這表明排序行為有誤。
- 兩個精靈都是立起的(
-
排序狀態分析
- 當前比對的索引為 70;
- 表示在前 70 個條目中排序都是正確的;
- 這一條目是第一個排序錯誤的個例;
- 從而可知這個 bug 不是系統性全錯,而是局部在特定數據下觸發。
關鍵問題分析
- ZMax 作為深度優先排序參考值未被正確比較;
- 排序邏輯可能忽略了部分排序關鍵字或比較函數實現不符合預期;
- 排序算法本身(如 merge sort)沒有問題,但排序函數的比較邏輯存在缺陷。
后續修正方向
- 檢查排序比較函數是否嚴格實現了“ZMax 小者優先”邏輯;
- 確認所有排序輸入項在排序前已正確填充(YMin, YMax, ZMax);
- 為邊界情況(如精靈條目完全相同)添加穩定性保障邏輯;
- 擴展測試用例,專門測試排序的不同情況(同高、同位、不同位);
- 考慮調試工具中增加詳細的排序比較跟蹤輸出,便于快速發現其他潛在錯誤。
總結
通過斷言與逐步調試,我們確認排序邏輯在某些特定情況下未正確執行,具體表現為精靈的 ZMax 值未被當作排序關鍵優先級,導致視覺錯誤。這類排序錯誤可能是極偶發的,但在視覺上影響較大,因此必須盡快修正排序比較函數的實現,確保所有參與排序的字段都被嚴格處理。此輪調試也說明對排序邏輯的測試覆蓋需進一步加強,特別是針對“排序相等”與“排序不等”的邊界情況。
game_sort.cpp:用 #if 0 注釋掉 SortEntries() 中的 game_SLOW 代碼,運行游戲并無任何異常
我們當前面對的問題是,在渲染約 2000 個精靈時,屏幕顯示為空白,這意味著排序或渲染過程中存在嚴重錯誤。考慮到目前排序數據混亂、難以逐一調試所有精靈,我們決定采取更簡化、可控的方式來逐步排查問題。
當前目標
讓排序和渲染邏輯在可控的小規模場景中運行,以便我們能觀察排序效果,并驗證排序是否按照 Z
軸(深度)正確進行。
調整策略與計劃
-
放棄使用 2000 個復雜的測試精靈
當前測試場景中的精靈數量太多,視覺上混亂,排序驗證困難,同時調試復雜,效率極低。 -
簡化輸入:使用離散層級的數據場景
計劃切換到類似“過場動畫”一類的場景。這類場景通常只需要按照 Z 值進行排序,精靈數量有限,分層清晰,易于觀察排序效果。 -
過場動畫的優勢:
- 排序只依賴
Z
值,無需考慮YMin
/YMax
; - 精靈位置穩定,且視覺布局清晰;
- 是理想的驗證排序系統是否基礎正確的起點。
- 排序只依賴
-
下一步行動:
- 修改當前測試入口,加載一個過場動畫場景;
- 檢查該場景下的排序輸出是否按照 Z 值正確排列;
- 確認渲染是否成功顯示預期精靈順序;
- 若顯示正常,則逐步引入更復雜的測試數據結構(如混合排序條件);
- 若依然顯示為空白,則需要進一步排查渲染管線本身的問題。
小結
當前的大規模精靈排序場景過于復雜,不利于調試。我們決定先采用精簡、結構明確的過場動畫場景作為驗證排序系統正確性的切入點。在該場景中,只需確保精靈按照 Z 值渲染即可,驗證通過后再擴展至復雜場景。這是更科學、更高效的調試思路,有助于快速定位問題根源并逐步恢復渲染正常。
game.cpp:讓 GAME_UPDATE_AND_RENDER() 只播放過場動畫
我們現在回到項目中,準備切換到一個更簡單、更易調試的場景——過場動畫(cutscene),用以驗證排序邏輯是否正確運行。
當前目標
強制程序在啟動后直接進入過場動畫模式,便于我們在更少干擾、數據明確的條件下調試渲染與排序系統。
操作步驟與驗證流程
-
查找并確認入口設置位置:
當前推測修改是在某段代碼里設置了啟動場景為 cutscene,只是記不清具體在哪個函數內。
我們重新定位代碼段并確認該邏輯是否生效。 -
強制進入 cutscene:
找到該邏輯后,通過直接賦值或跳轉方式,程序在啟動后將直接跳入過場動畫模式,跳過主游戲流程。 -
運行程序,確認結果:
運行游戲后,觀察是否如預期那樣直接加載并進入 cutscene。 -
成功標志:
若界面上正確渲染出預期的過場動畫內容(如角色、對話背景等),說明切換成功,當前環境適合我們進行排序調試。
小結
我們已經找到并確認了強制程序進入 cutscene 的代碼位置,并成功啟動了一個只涉及離散 Z 層級的過場動畫場景。接下來可以在這個簡單環境下調試 sprite 排序邏輯,驗證渲染是否符合 Z 值排序規則。如果一切正常,將為更復雜場景提供穩定的基礎驗證參考。
調試器:中斷 SortEntries(),確認 IsInFrontOf() 和 MergeSort() 的方向正確
我們現在正在對合并排序(merge sort)與前后判斷邏輯進行逐步核查與調試,目標是確保在過場動畫(cutscene)中 sprite 的 Z 軸排序是正確的。
核查“是否在前方”的判斷邏輯
- 檢查
IsInFrontOf()
函數,語義是 “A 是否在 B 的前面”。 - 對于按 Z 軸排序的情況,若 A 的
ZMax
大于 B 的ZMax
,說明 A 更靠近觀察者,應排在前方 → 返回 true。
→ 邏輯正確。 - 若是按 Y 軸排序(即從上到下繪制),判斷方向應相反 → 此處代碼也處理了這種情況。
→ 方向處理無誤。
檢查合并排序邏輯
-
合并排序會遞歸地對數組劃分兩半,然后合并時對兩半的首項進行對比:
- 若“第二半的首項應在第一半前方”,則交換兩者順序。
- 否則保持當前順序。
-
合并階段也判斷當前讀取的是哪一半:
- 若只剩一邊,直接輸出;
- 若兩邊都有數據,則使用
IsInFrontOf()
決定先寫入哪一邊。
-
這些邏輯在代碼中被清晰實現,判斷條件也符合預期。
→ 合并排序整體邏輯正確。
Y 坐標綁定判斷
- 判斷兩個 sprite 是否為 Y 軸排序相關的 sprite,是通過
YMin
和YMax
是否不同來確認的。 - 若兩者的 Y 范圍完全重合,當前邏輯認為它們不屬于可以按 Z 排序的范圍。
- 目前對于嚴格的 Z 軸排序場景而言(如過場動畫),這個判斷應該沒問題。
準備實際運行測試
-
當前進入排序函數后,只看到了兩個 sprite → 數量過少,不具代表性。
-
稍作運行之后發現有 39 個 sprite,推測大多數是 debug sprite。
-
為減少干擾,決定 暫時關閉 debug 渲染輸出,以便只關注真正參與排序的游戲元素:
- 進入 handmade 系統模塊;
- 通過 GameInternal 標志來關閉 debug 輸出。
小結
- 我們確認了“是否在前方”的判斷邏輯完全正確;
- 合并排序的實現和調用邏輯也符合預期;
- 計劃通過關閉 debug sprite 來進一步簡化測試環境,從而專注觀察 Z 層排序的實際效果;
- 接下來的測試將著眼于 cutscene 中的 sprite 是否按照 Z 值正確前后排序,進一步驗證渲染邏輯的準確性。
調試器:中斷 SortEntries(),檢查前 8 個 Entries 的 SortKey() 值
我們當前進入了一個理想的測試環境:屏幕上不再繪制其他內容,只有用于排序測試的幾層基本圖層。這些圖層用于驗證渲染排序的最簡單情況是否表現正確。下面是詳細分析:
簡化測試環境已構建完成
- 當前只保留了八個用于測試排序的圖層;
- 這是最簡化的測試場景,有助于排查基礎邏輯問題;
- 理論上應該確保所有渲染對象僅根據 Z 值排序,且順序完全可控。
排查圖層數據
- 所有圖層的 Y 值(如
yMin
、yMax
)被設置為特定數值,但尚不清楚這些值為何如此設定; - 為了理解排序是否正常,首先需要弄清楚這些 Y 值的由來;
- 雖然當前排序主要基于 Z 值,但 Y 值設定仍可能影響進入哪一類比較路徑(例如 Z-only 排序 vs. Z+Y 排序路徑)。
排序邏輯回顧
- 所有渲染條目都有 Z 值,意味著它們應該全部走 Z-sprite 的排序路徑;
- 排序過程只會比較
ZMax
值,而不會用到YMin/YMax
; - 因此這些對象的
ZMax
值應當能完全決定其排序順序; - 正確行為:具有最大
ZMax
值的對象應排在最前。
觀察異常現象
- 實際排序結果可能存在異常;
- 排序表現與預期不符,說明可能存在邏輯錯誤或者初始數據設置不合理;
- 接下來將進一步調查 Y 值為何被設置成當前值,確認是否誤導了排序邏輯;
- 還需要逐步檢查排序比較函數是否確實只在 Z-sprite 路徑中使用 Z 值。
總結當前狀態
- 已建立純粹的圖層測試環境;
- 渲染數據簡潔、可控,有助于定位錯誤;
- 正在聚焦于
ZMax
排序路徑是否完全可靠; - 下一步需確認排序行為與設定數據是否一致,并檢查 Z 值是否確實主導排序。
在如此簡潔的場景下仍能觀察到潛在排序問題,說明排序邏輯中可能存在深層次的小錯誤。后續將重點跟蹤每個圖層的排序輸入值與實際繪制順序,確保二者一一對應。
這不是一個前到后的渲染器
當前我們確認了一個重要事實:這個渲染器并不是一個“從前到后”(front-to-back)的渲染器。這一點很明顯,意味著它并不會自動確保屏幕上距離相機更近的物體覆蓋更遠的物體。
渲染排序邏輯定位
- 當前渲染排序機制不是按照物體距離(Z 值)由近到遠進行渲染;
- 這種渲染方式會影響遮擋關系,特別是在需要深度層級呈現的場景下,可能導致前景物體被后景物體覆蓋;
- 若希望渲染器具備“從前到后”特性,必須手動控制渲染條目的順序或引入深度測試機制。
檢查恢復斷點邏輯
- 準備將一項判斷邏輯重新加入代碼中,以便觀察或驗證某些關鍵狀態;
- 此處可能是用于調試或確認某些變量、行為是否如預期進行;
- 后續可能會定位到排序錯誤具體出現在哪一環。
當前排序機制的局限
- 在現有機制下,排序更多是依賴于手動設定的 sort key(如 Z 值、Y 值等);
- 一旦 sort key 設置不合理,或比較函數存在瑕疵,就可能導致渲染結果與期望不符;
- 尤其在處理具有遮擋關系的場景時,這種機制的缺陷會被放大。
接下來的方向
- 需要進一步審查排序邏輯是否能支持類似“從前到后”的行為;
- 如果不能,需要評估是否引入新的排序標準,或者在特定場景手動控制順序;
- 重新啟用某些檢查(如斷言、日志)有助于暴露問題出現的位置;
- 總體目標是確保渲染順序與視覺邏輯一致,避免前景物體被錯誤覆蓋。
通過這一步,我們不僅確認了當前渲染器不具備從前到后的自動排序能力,也明確了改進方向:要么改進排序邏輯以適應復雜視覺層級,要么在特定情況下手動干預排序以獲得正確結果。
game_sort.cpp:讓 IsInFrontOf() 按正確方向排序
我們在檢查 is_in_front_of
函數時,發現盡管它本身返回的判斷邏輯是正確的,但使用這個判斷結果的地方,邏輯是反的。換句話說,當前的排序行為與渲染的需求相違背。
邏輯錯誤分析
is_in_front_of(A, B)
返回的是 A 在 B 前面(也就是 A 更靠近相機);- 但是在當前的排序實現中,當判斷 A 在 B 前時,A 被移動到了更靠前的位置(更早繪制);
- 然而在渲染中,更靠近相機的物體應當在后面繪制,以遮擋背景中的物體;
- 因此這個邏輯恰好應該反過來使用 —— 如果 A 在 B 前面,A 應該排在后面(晚一點繪制);
- 正確的處理應該是:如果 A 在 B 前面,就交換 A 和 B 的順序,讓 A 在 B 后繪制。
修正排序方向
- 所以之前的交換邏輯是錯誤的;
- 正確的做法是:當判斷出 A 在 B 前時,我們應該將 A 放在 B 的后面,也就是交換 A、B 的順序;
- 原本的排序使用方式是錯誤的,必須將其顛倒。
修正后的行為預期
- 修正之后,排序邏輯就會變成“后繪制的在前面”,即前景物體會蓋住背景物體;
- 視覺上會呈現符合物理遮擋規律的畫面;
- 同時邏輯也與
is_in_front_of
的語義保持一致:返回 true 意味著 A 更靠近,所以 A 應該排在后繪制的位置。
總結
我們發現了一個關鍵性的問題:盡管前置判斷邏輯(是否在前)是正確的,但其應用方式和渲染需求相違背。通過調整交換順序邏輯,確保 越靠近觀察者的物體被越晚繪制,從而正確處理遮擋關系,修復了渲染排序的根本錯誤。
運行游戲,確認基礎邏輯沒有徹底混亂
目前的目標是驗證基礎渲染排序邏輯是否正確。以下是我們當前的分析與狀態總結:
當前驗證結果
- 通過對一組 簡單的 Z 平面精靈(Z-flat sprites) 進行測試,已經確認 基礎的排序邏輯是正確的;
- 我們所實現的排序可以正確地按照 Z 值來判斷前后關系,并按從遠到近的順序渲染,確保遮擋關系正確;
- 這意味著在最基本的情形下,我們使用的合并排序與判斷前后關系的邏輯并沒有根本性錯誤,可以正常工作。
驗證目的說明
- 由于整個排序流程相對復雜,為了避免基礎部分出現問題導致后續調試困難,我們先驗證了最簡單場景下的正確性;
- 這是為了確認系統的底層機制沒有崩壞,可以為接下來更復雜情況的調試提供信心;
- 排除了“根本邏輯錯誤”的可能性,使我們可以更專注于更高級別的 bug 排查。
后續挑戰與問題
- 游戲實際運行中的渲染情況遠比簡單測試復雜,排序邏輯是否足夠強大以處理所有真實情況仍有待驗證;
- 特別是涉及 Y + Z 層混合排序、多層遮擋、交叉重疊等情況,可能無法通過簡單的線性排序解決;
- 當前排序邏輯是否能“完全適用于復雜游戲場景”這一點仍不確定;
- 后續需要驗證:當前排序策略是否本質上能解決所有精靈遮擋邏輯,還是說在某些情況下必須引入拓撲排序或其他更復雜的方法。
下一步計劃
- 回到真實游戲場景中進行調試,觀察在復雜精靈組合下的渲染是否仍然正確;
- 定位當前存在的問題;
- 確定是否有更高階的排序需求,例如無法用單一比較函數處理的排序沖突;
- 繼續改進邏輯,或調整場景數據以適應現有排序機制。
總結
我們已確認基礎排序邏輯在簡單 Z 平面精靈上是正確的,這為后續復雜場景調試打下了基礎。下一步將轉向游戲主流程中的實際排序邏輯,識別剩余的渲染 bug,同時思考是否需要更復雜的排序模型來支撐整個系統。我們準備繼續深入。
game.cpp:讓 GAME_UPDATE_AND_RENDER() 直接進入游戲,運行后確認線性掃描中沒有明顯失敗
目前已將渲染流程切換回主游戲場景,跳過了片頭序列,進入游戲后進行了初步觀察和驗證。以下是詳細總結:
當前觀察結果
-
在當前游戲場景中進行 線性遍歷檢查(linear sweep) 時,沒有觸發排序失敗的斷言,這說明:
- 在大多數情況下排序順序看起來是有效的;
- 排序算法本身并沒有在最表層出現明顯錯誤;
- 基本排序機制在真實游戲場景中的表現尚可,未立即暴露崩潰或邏輯異常。
當前存在的問題
-
屏幕上仍然存在大量 物體相互穿透(interpenetrating objects) 的情況;
- 這導致排序測試不夠明確,因為物體之間的遮擋關系不清晰;
- 穿透情況削弱了我們觀察排序正確性的能力,使調試復雜化;
-
當前測試場景的精靈排列較為混亂,不具備良好的測試條件;
- 缺乏有層次、清晰遮擋關系的對象,難以精準驗證深度排序邏輯。
下一步改進方向
-
明日的工作重點將轉向為場景補充更真實的層次結構(layering),具體包括:
- 明確設置對象的 Y、Z 軸位置,避免模糊重疊;
- 構造更合理、分層清晰的測試對象;
- 改進原有的“圖層系統”(layer system),提供可控的精靈分布;
-
此外,還將繼續優化排序測試,使其能更好地暴露潛在問題。
總結
當前初步驗證表明,在大場景中排序系統沒有表現出崩潰或明顯錯誤,基礎邏輯可用。然而由于場景中存在大量對象穿透,導致排序正確性難以確認。接下來將重點重構測試用例,增加更真實有效的分層精靈布局,以便更清楚地驗證渲染排序機制的有效性和魯棒性。
game_entity.cpp:讓 UpdateAndRenderEntities() 繪制實體時按 Z 值排序,不將它們壓平到平面上
為了討論方便,假設暫時不做之前對實體進行的截斷處理,也就是說,不將所有東西壓平到同一層級,而是直接在“世界模式”下輸出實體,并使用實體自身的真實Z值進行繪制,而不是進行相對層級變換。
具體來說:
- 目前渲染流程中,有一個“相對層級變換”步驟,會對實體的層級和深度進行調整,目的是統一處理和簡化排序。
- 現在假設不做這一步,直接用實體本身的Z值繪制。
- 這樣做的目的是觀察在不做層級變換的情況下,排序機制會產生什么效果,看看能否更準確或有何不同。
- 實際代碼中這部分被移動到了單獨的實體文件中,需要注意這一點。
- 通過這種方式,可以驗證排序系統對于真實Z值的處理是否合理,以及觀察排序結果是否符合預期。
簡而言之,就是嘗試關閉原本為了簡化排序而做的層級扁平化處理,直接用實體的原始Z值輸出,來測試排序邏輯在這種情況下的表現。
運行游戲,發現排序結果不符合預期
在這種情況下,排序結果看起來不正確,沒有達到預期的效果。具體表現為,觀察到的一些對象的位置和排序順序與預期不符,說明排序邏輯可能存在問題。
另外,因為開啟了“游戲內部調試模式”導致鼠標光標被關閉,為了更方便觀察當前畫面情況,暫時將鼠標光標重新打開。這樣可以更直觀地查看對象的位置和排序情況,幫助進一步調試和分析排序問題。
總結就是,直接用實體真實Z值繪制時,排序沒有按預期工作,需要檢查排序算法和相關邏輯。同時為了更好觀察,先恢復鼠標光標顯示。
問答環節
閱讀這篇博客讓我覺得非常有趣,尤其是關于2D游戲中如何正確排序精靈顯示的問題。雖然我平時不太做2D游戲,但對這個問題一直不是很了解,也沒意識到它其實是個很有語義復雜度的問題——畢竟這不是完全的3D,而是2D畫面中通過某種方式模擬深度感。如何讓精靈正確地表現“前后”關系,其實遠比單純按Y軸排序復雜。
博客介紹了一個很棒的解決方案,我覺得非常酷,也讓我對不同游戲中處理排序的方式產生了濃厚興趣。因為肯定有很多游戲在這方面做過有趣的嘗試和創新,可能有的做法我以前完全不知道。現在知道這不是一件簡單的事情,而是一個需要精心設計的系統,我非常想了解更多其他游戲是怎么解決排序問題的。
總體來說,我很喜歡這篇博客里的思路和方法,也期待玩這個游戲,因為我已經很久沒玩過類似“熱血雙截龍 Double Dragon”風格的游戲了,感覺很懷念那種體驗。
最后,關于坐標的使用問題,也讓我有了更多思考。
排序規則使用的是世界坐標吧?有沒有可能用屏幕空間坐標,從屏幕頂端往下排序?
排序規則是基于世界空間坐標的,另一種方法可能是根據屏幕空間坐標從屏幕頂部到底部進行排序。之前我們一直是用屏幕坐標來排序,但這樣做的問題在于,排序屏幕坐標無法處理某些情況,因為屏幕坐標本身并不包含物體之間的深度關系信息。換句話說,僅僅依賴屏幕坐標排序無法準確判斷哪個物體應該被繪制在前面或后面,特別是在有重疊或層次關系復雜的情況下,這就導致排序結果不符合預期。因此,單純通過屏幕空間坐標排序并不能滿足需求,需要結合世界空間坐標的信息來進行更合理的排序處理。
Blackboard:討論為什么不能僅用屏幕空間坐標而不考慮 Z 進行排序
問題在于,如果在繪制三維場景時,比如有一個平臺(瓦片)和一個放在平臺上的物體,從頂視圖來看,這些物體在屏幕上的投影可能會重疊,但僅憑屏幕上的二維坐標,無法判斷哪個物體實際是在前面。舉例來說,假設有兩個物體A和B,B應該先繪制,A后繪制,這樣A會遮擋B,但單純用屏幕坐標看不出來哪個先繪制。
因此,至少需要在屏幕坐標的基礎上加入Z軸深度信息,也就是說,排序時不能只用XY屏幕坐標,還要用Z坐標。但即便如此,問題依舊存在。比如,有些物體在視角下會重疊,但它們的Y坐標大小關系并不能直接決定繪制順序。舉個例子,一個物體放在四個瓦片上面,人站在物體上,這時如果只用Y坐標排序,會出現順序錯誤,因為人可能在Y坐標上小于某些瓦片,但實際應該繪制在瓦片前面。
所以,不能簡單用Y值或Z值單獨排序,而是需要使用物體在Y軸上的范圍(y-min和y-max)以及Z軸信息結合起來判斷。只有知道每個物體的Y軸上下界,才能正確判斷它們的遮擋關系,解決排序的“平局”問題。僅憑屏幕坐標沒有Z信息的排序根本不夠,因為這沒法體現物體間真實的空間關系。
綜上,要正確處理2D畫面中的深度關系,必須用物體在空間中的范圍(尤其是Y軸的最小最大值)和Z軸信息綜合考慮,而不是簡單排序Y或者Z坐標。這個結論是通過反復推敲和測試得到的,單用屏幕空間坐標無法解決實際問題。
我有個關于開發方法的問題:我沒見過你單獨寫程序或環境測試東西,總是在運行中的游戲里做,你覺得這樣“嘈雜”嗎?
關于開發方式,通常根據具體情況而定。大多數時候并不會專門創建獨立的程序或環境來測試功能,而是直接在運行中的游戲環境里進行開發和調試。只有在處理純理論算法或者需要專門測試某些算法時,才會單獨搭建一個測試環境。
曾經有過類似的經歷,比如之前做3D算法時,寫過一個叫做“math viz”的小工具,用來快速測試和可視化3D算法的效果。這個工具類似一個小型的實驗平臺,可以方便地繪制和試驗各種算法。它曾經存在于以前的開發工具源碼樹里,但現在已經找不到了,可能是機器清理文件時被刪掉了。
雖然獨立測試環境在理論上很有用,但實際操作中往往會因為切換環境而降低效率,所以通常直接在游戲內部調試更方便。不過,當預計會在某個算法上花很多時間時,會花心思制作類似的工具箱,方便快速原型開發和測試。
總體來說,制作單獨的測試程序是一個有用但不常用的手段,更多時候依賴于游戲內部的調試和開發流程。過去的這些實驗工具是內部開發用的,不是隨SDK發布的公共資源,現在基本已經丟失或無法找到。
好吧,我說謊了,Witness Wednesdays 里見過你做過類似操作
在開發過程中,雖然盡量在直播或錄制中真實展現實際的游戲開發過程,不事先準備解決方案,遇到問題時現場思考和調試,但也有些復雜的算法和工作是不會在公開環境中詳細演示的。
例如,曾經在一個復雜的算法(像是曲線擬合算法)上花費了大量時間,可能達到200小時以上,這種級別的工作幾乎相當于整個項目的大部分時間。這樣的深度研究和長時間調試,公開展示是不現實的,因為會非常枯燥且難以讓觀眾持續關注,甚至大部分時間都在沉默思考。
因此,公開的開發內容更多是中等難度或者常規的程序設計,而超難問題的研究性工作基本不會在公開內容中出現。公開內容的目的是盡量真實地表現游戲開發的常態,展現現場調試和思考的過程,但不會展示那些耗時長且枯燥的算法攻關。
總的來說,雖然有時候會做一些新算法和研究工作,但大多數難題和復雜算法的攻關不會在公開場合展示,因為這些內容既難以觀看,也不適合直播或系列內容的節奏,這也是制作公開開發內容時必須面對的現實限制。
數學可視化演示
我們曾經使用一個叫做Math Vis的程序作為開發工具,主要用于開發Granny引擎里的各種算法。這個程序包含了一個比較原始但實用的用戶界面,雖然不像現代即時模式UI那么先進,但它有一些簡單的拖拽滑塊控件,可以快速調整參數,方便調試和測試算法。
Math Vis允許我們插入各種可視化模塊,大概有十多個,能夠同時編譯進程序里,根據需求點擊切換不同的可視化界面。它支持畫箭頭、操作移動控制器等功能,類似于游戲開發中調試時常用的工具。通過這些工具,可以直觀地看到算法的執行過程,比如GJK碰撞檢測算法的具體步驟。
在GJK算法的調試中,我們用這個工具顯示了不同點構成的簡單形狀(simplex),標注并動態調整點的位置,驗證算法在不同情況下是否正確識別碰撞區域和計算結果。通過拖動數值,可以實時觀察算法對點的選擇和轉換是否合理,確保邏輯的準確性。
另外,Math Vis還實現了一些空間劃分和剔除(culling)功能的可視化,用來測試空間數據結構的效果。比如有一部分是軸對齊包圍盒(AABB)的顯示和交集測試,涉及Minkowski差分的計算,這些都是用于碰撞檢測的重要數學工具。通過顏色和圖形表示,能夠幫助判斷是否存在重疊或包含關系。
該程序支持各種可調參數,通過滑塊調整顯示內容,比如是否顯示網格,調整視角等,方便根據需要觀察不同的細節。雖然有些參數具體作用不太清楚,但整體設計非常靈活,支持快速迭代和調試。
Math Vis不僅僅用于GJK算法,也用于曲線擬合、基于姿態的信息處理等多個復雜算法的原型制作和測試,是一個綜合性的數學和算法開發平臺。在開發過程中,任何非簡單的算法都會先在這個測試環境里做原型,確保邏輯正確和性能可行,然后再集成到游戲或引擎中。
總之,Math Vis是一個非常重要的工具,通過簡單直觀的可視化界面幫助我們理解和調試復雜算法,提高開發效率,確保核心技術的可靠性。