倉庫:https://gitee.com/mrxiao_com/2d_game_5
回顧上次內容并介紹今天的主題
上次留下的是一個非常簡單的任務,至少第一步是非常簡單的。我們需要在渲染器中加入排序功能,這樣我們的精靈(sprites)才能以正確的順序顯示。為此我們已經完成了一些前期準備,現在只需要實現排序功能。
我們打算首先實現一個非常簡單的排序算法,這應該會比較容易。但還有一件事需要處理:我們需要決定,并可能稍作調整,如何處理當前的數據結構,以支持改變渲染順序。
總之,我們已經準備好了排序機制的基本架構,接下來就是正式編寫排序邏輯,并在必要時對數據結構進行調整,以便支持正確的渲染順序。
先修修復一下clangd 顯示的錯誤,雖然編譯能過不過看著顯示的錯誤有點煩 (可以不做)
修復所有的頭文件
用
#ifndef GAME_RENDER_GROUP_H
#define GAME_RENDER_GROUP_H
#endif
包含
去掉之前include cpp 文件只引用頭文件
每個cpp 單獨編譯成對應的obj 文件
修復一下沒有顯示debug 字幕的情況
修復一下遞歸引用頭文件
分成多個obj編譯之后clangd 不會出現錯誤了
貌似還有錯誤
繼續修復錯誤
回到正題
運行游戲并觸發斷言錯誤
我們當前處理的是一段和渲染輸出有關的邏輯,目的是為了幫助那些可能不太清楚流程,或者已經忘記細節的人。
現在遇到的情況比較奇怪,一開始并沒有印象中包含這一塊內容。很有可能是由于某種原因導致我們之前設定的內存大小超出了預期。具體來說,這是在渲染組中生成臨時內存的部分,我們在這里為渲染輸出創建了臨時用的緩沖區。
有可能是我們在某些處理上做得太多,比如添加了過多的渲染項,導致分配的臨時內存不足。但我們之前處理相關邏輯的時候,并沒有遇到過需要更大空間的情況,所以這一點非常值得注意。
我們本來想演示另一塊內容,但因為一開始就做了某些修改,結果現在觸發了這個問題。也就是說,當我們處理地面塊(ground chunks)時,這個問題就會被觸發。
不過我們可以先臨時關閉地面塊的處理,然后繼續演示原本要展示的內容。反正后面我們也需要回過頭來討論這部分的內存分配問題。現在先暫停一下,把這個處理好。
在 game_world_mode.cpp
中關閉 FillGroundChunk
我們可以先把地面塊的處理邏輯關閉,這樣它們就不會因為內存需求過大而引發問題。當前的狀況是,地面塊的處理可能會超出分配的內存限制,導致渲染過程中出錯。
現在這些地面塊的計算似乎已經不在主游戲邏輯中了,之前我們已經把它們移動到了 game_worlds
模塊中,所以這一步的處理應該不會影響其他功能。
在當前的地面塊相關代碼中,有一部分是關于 FillGroundChunk
的邏輯。只要暫時將這部分處理邏輯移除或者注釋掉,地面塊就不會再進行實際的計算操作。這樣,我們就可以暫時忽略它們的行為,避免它們對內存造成壓力,從而順利繼續后續的開發和調試。
接下來就可以繼續進行其他部分的運行和驗證了,不需要再擔心地面塊在后臺占用過多資源或觸發異常。這樣既能保證整體流程正常進行,又為后續專門處理地面塊相關內存問題預留了空間。
運行游戲,注意當前沒有進行 sprite 排序
當前游戲運行時,并沒有等待垂直同步(Vertical Retrace),但由于渲染器速度非常快——盡管是軟件渲染器——因此整體運行速度遠高于預期。這主要是因為當前場景中只繪制了少量的精靈(sprites),所以渲染壓力極小。
在這個基礎上,當我們在世界中移動角色時,可以明顯看到精靈并沒有經過任何排序處理。也就是說,所有的精靈都是按照它們在當前區域中出現的順序直接繪制出來的,這個順序是非確定性的,因為模擬器并不關心順序,導致繪制結果很隨機。
例如,角色有時候會被錯誤地繪制在樹的后面,盡管他應該出現在前面;而當角色移動時,甚至可能因為順序變化導致繪制層級突然發生跳變。這種狀態在實際游戲完成后是完全無法接受的,因為我們需要在各種復雜的場景下確保繪制順序是合理的。
哪怕只是一個簡單的房間,并圍繞它放置一些樹,如果不進行排序,畫面表現依然是錯誤的。因此,我們必須實現某種機制來對精靈進行排序,確保在繪制時,能正確決定誰應該覆蓋誰。
例如,如果角色處于樹后面,那么樹就必須繪制在角色上方;反之如果角色站在樹前面,那么角色就應該被繪制在最上層。只有這樣才能保證視覺上的正確性和邏輯一致性。
當前的目標就是解決這個排序問題,以確保渲染結果和空間關系相匹配。接下來還會進一步分析具體遇到的問題。
在 game_world_mode.cpp
中重新打開 FillGroundChunk
,發現我們需要為排序留出空間
我們把之前關閉的邏輯重新打開后,觀察到了之前剛剛實現的一段功能:我們需要一塊額外的內存空間用于精靈排序,這就是當時的主要目的。
問題出現在這里:當我們啟用地面塊(ground chunk)的填充邏輯后,渲染過程中會因為內存不足而出錯。具體來說,是在嘗試渲染時,程序向任務內存區域請求新的內存塊,但此時內存池已經被耗盡,無法再分配出更多空間。
從表現來看,應該是在分配渲染組(render_group)時請求的內存超出了我們預設的容量。為了搞清楚具體發生了什么,我們需要查看內存分配的邏輯。推測是在調用 AllocateRenderGroup
的過程中,內存請求超出了 arena 的上限。
接下來要做的是深入查看相關實現,分析 AllocateRenderGroup
具體是如何管理內存的,以及到底是哪些部分在大量分配空間,是否是排序邏輯引起了額外開銷,或者是地面塊處理邏輯與渲染排序邏輯之間的資源競爭,造成了內存資源的枯竭。
這將有助于我們更精準地調整內存分配策略,或者限制某些功能的使用,避免在運行時出現類似崩潰或內存分配失敗的問題。
在調試器中進入 AllocateRenderGroup
函數
在執行 AllocateRenderGroup
時,我們需要關注它實際分配了多少內存。從當前觀察來看,問題出現在多線程的執行過程中。由于渲染是并發進行的,我們有多個線程同時在申請渲染組內存,尤其是那些負責填充地面塊的線程,它們正在同時執行這個內存分配操作。
可以通過線程監控面板看到,當前確實有兩個線程正在進行渲染組的分配操作,它們就是用于地面塊填充的線程。為了便于分析,我們可以臨時凍結其中一個線程,專注觀察另一個線程的行為,避免分析時被多個線程的執行干擾。
在進一步檢查中發現,每個渲染組的最大緩沖區大小被設置為 1MB。而用于這些工作的任務內存池(task arena)本身的大小也僅為 1MB。這意味著,只要分配一個渲染組的內存,就會用光整個內存池,導致無法再進行任何額外分配。
問題的本質在于:當前為輔助任務(例如地面塊填充)配置的任務內存區域過小,無法滿足實際所需內存量。這是一個需要手動調整的參數,必須根據這些任務的真實需求來設定內存池大小,確保每個線程在運行期間有足夠空間完成其工作。
如果繼續沿用當前的內存配置方案,將導致頻繁的內存分配失敗,從而引發程序錯誤或性能問題。因此,需要根據任務復雜度對內存使用策略進行調優,例如增大每個 task arena 的大小,或限制并發任務數,以保證系統在資源允許的范圍內穩定運行。
把填充地面塊代碼打開
在沒有進入到游戲在場景畫面是一個線程分配內存一直是4194304
進入游戲之后
在 game.cpp
中增加 TranState
分配的內存量
在初始化 transient 狀態時,可以看到每個任務線程被分配了固定大小的內存區域。目前的設定中,每個線程的內存大小為 1MB,但這顯然不足以同時滿足排序操作和其它渲染需求所需的空間。
解決思路是將每個線程的任務內存池(task arena)大小提升到 2MB。這樣一來,同一個線程在執行過程中就有足夠的空間進行中間排序緩沖區的分配,同時也可以執行其他所需的內存操作,避免之前由于內存不足而導致的分配失敗或渲染異常的問題。
這個改動屬于配置層面的優化,通過適當增加每個線程的內存分配,可以提高系統穩定性,避免在高負載或并發渲染時出現資源沖突。調整的關鍵點在于根據實際任務的復雜程度和內存使用情況來評估所需的內存上限,從而為每個線程預留出足夠的緩沖空間,使系統在運行中更加高效且不易出錯。
兩兆還是段錯誤
在調試器中進入 BeginRender
,查看 Work->Task->Arena
的狀態
在當前的調試過程中,我們嘗試驗證任務線程在執行時是否有足夠的內存空間可供使用。假設分配邏輯正確,那么在某些流程中應該還保留有大約 1MB 的可用空間。然而,當實際進入相關代碼路徑時,觀察到任務線程的內存區域(task arena)已經被完全占用。
這暴露出一個核心問題:不是分配內存的邏輯有誤,而是每次進行分配操作時,程序會盡可能地占用所有剩余的內存空間。這就導致了即使預期中應該有富余的空間,也會因為這種“貪婪”式的分配方式而出現空間耗盡的情況。
根本原因在于:當前內存分配策略不會限制實際使用上限,而是直接占滿可用內存。這意味著只要存在臨時排序空間或渲染中間緩沖區的請求,它們就會無視后續需要的空間,優先填滿整個區域,從而導致后續分配失敗。
因此,問題并不完全在于初始分配的總量,而是在于沒有對不同任務間的內存使用進行有效隔離或限制。要解決這個問題,就需要在內存分配器中加入更細粒度的控制策略,確保不同用途之間能夠留出合理邊界,避免彼此侵占,從而提高穩定性與資源利用效率。
在 game_world_mode.cpp
中設置 RenderGroup
使用 512KB 內存
我們目前在處理的是排序前的內存管理問題。在之前的實現中,給渲染線程分配內存時,如果不顯式指定大小,而是傳入 0
,那么系統會默認使用任務線程可用內存區域(arena)中所有剩余的空間。這種方式存在一個明顯的問題:它會占用整個剩余空間,導致之后的任務再請求內存時失敗,即使之前我們預留了足夠的空間也無法生效。
為了解決這個問題,我們意識到可以不傳 0
,而是手動設置一個更合理的上限值,例如只使用總內存的一半。通過這種方式,可以有效防止單個模塊“吞噬”全部剩余資源,從而給后續需要排序的邏輯預留空間。
目前,我們已經恢復到一個相對“正常”的狀態,任務線程不再因內存溢出而出錯。但排序邏輯本身還沒有真正實現,這也是接下來要完成的部分。我們已經為排序留出一段預設空間(sort_space
),并定義了用于排序的數據結構 tile_sort_entry
,這個結構里包括一個 sort_key
(排序依據)和一個 PushBufferOffset
(渲染指令在緩沖區中的位置)。
接下來的目標是實現實際的排序邏輯。為了保證開發過程高效,我們選擇“從最簡單的事情做起”。也就是說,先寫出一個最基礎、能跑起來的排序系統,之后再逐步優化。這樣可以更清楚地知道哪些地方是瓶頸,哪些需要改進。
為了更好地講解接下來的排序過程,我們還計劃在黑板上做一個示意圖說明整個排序原理。由于當前項目中的數據量較大,加載黑板需要一些時間。加載完成后,我們會通過圖示的方式詳細說明排序操作如何處理渲染對象在同一個瓦片中的深度關系,確保遠處的物體被正確地繪制在近處物體的后面,解決當前繪制順序混亂的問題。
黑板講解:渲染排序
我們現在要處理的是渲染排序的問題,情況非常基礎但關鍵:我們有多個圖像元素,比如 A 和 B,而已知 A 應該顯示在 B 的前面。為了實現正確的視覺效果,我們需要確保渲染時這些元素按照特定順序繪制。
如果采用前向渲染(front-to-back),我們應該先繪制靠前的 A,再繪制 B。這樣,A 的內容會被優先顯示,B 被遮擋的部分不會浪費繪制時間。而我們當前使用的是**后向渲染(back-to-front)**的方式,即先繪制 B,然后將 A 繪制在上面,從而通過 A 的像素“遮住” B 實現前后層次。
我們要達成的目標是:無論初始繪制順序如何,比如現在是 A、C、B,我們都要確保最終繪制順序是從后到前,即 C、B、A。這樣才能正確地渲染出空間關系,物體之間不會“穿幫”。
目前的基礎數據是一個 push buffer,它是一個記錄了所有繪制指令的數組,這個數組內的每項都包含了渲染所需的數據,比如位置、圖像指針等。這些數據結構體體積可能非常大,可能達到 64 字節甚至 128 字節。
一個直接的想法是對這個 push buffer 本身進行排序,即重新排列其中的數據塊,使其按照從后到前的順序排列。但是這就涉及大量的數據復制,特別是在排序過程中可能需要多次移動,代價很高。尤其是 push buffer 體積大、數據密集時,這種做法效率很低。
因此我們傾向于采用間接排序的方式:我們不直接修改 push buffer 的內容,而是額外維護一個“索引列表”,每個索引指向 push buffer 中的一項。這個索引結構比較小,只需要記錄排序用的鍵(比如深度)以及該元素在 push buffer 中的偏移地址即可。我們先對這個索引列表進行排序,然后按照這個新順序去讀取 push buffer,依次繪制,從而達到理想的繪制效果,避免對大塊數據做復制。
總之,我們的目標就是:
- 從原始繪制指令中抽取排序信息,形成簡潔的排序結構;
- 對這些排序信息進行排序,得到正確的繪制順序;
- 按照排序結果執行繪制操作,而不是直接修改原始指令數據。
這種方式既保證了排序的正確性,又兼顧了性能和內存效率,是處理實時渲染排序問題的常見方法。接下來我們會進入具體實現階段。
黑板講解:排序緩沖區
我們要進行排序,而很多排序的基本策略是:不直接對原始數據本體進行移動,而是構造一個專門用于排序的輔助緩沖區(sort buffer),該緩沖區只包含執行排序所需的最小信息。通常,這些信息包含兩個部分:
- 排序鍵(key):用于決定該條數據在排序中的位置;
- 索引(index):用于在排序完成后能回到原始數據中,準確找到對應項。
在當前的實現中,這個索引是 push buffer 中的偏移量,即一個整數值,可以讓我們定位到原始渲染指令的位置。排序鍵則是一個我們選定的、能代表渲染層級的值,比如一個整數類型的 sort key(目前使用的是 32 位的有符號整數 int32_t
,也就是 r32
類型),這個值根據物體的深度、層級或其他優先級策略設定。
因此,每條排序項結構包含兩個字段:
sort_key
:用于比較決定排序順序;PushBufferOffset
:指向原始 push buffer 的偏移地址。
這種方式帶來的好處有幾個:
- 每個排序項結構體只占用 64 位(8 字節),效率高、內存開銷小;
- 可以使用高效的排序算法(如快速排序、歸并排序、計數排序等)對這些結構體進行排序;
- 排序完成后不需要調整原始 push buffer 內容,只需要遍歷排序后的結構數組,然后按順序去 push buffer 中讀取數據即可;
- 避免了大數據結構在排序過程中的復制,提升運行效率。
我們接下來的工作可以分成兩個階段:
- 構造排序結構數組:遍歷所有 push buffer 中的渲染指令,為每一條構造一個包含排序鍵和偏移量的結構,放入 sort buffer 中。
- 執行排序邏輯:對 sort buffer 按照 sort_key 進行排序。排序完成后就可以用 offset 去 push buffer 中讀取并執行渲染,按正確的視覺層級繪制出來。
這就是我們的排序策略,清晰高效且具有可擴展性。接下來將進入具體實現階段,把這套思路落實到渲染流程中。
黑板講解:生成排序鍵
我們現在要解決的第一個任務是:如何構造一個32位的排序鍵(sort key),用于決定渲染順序。看似簡單,比如如果每個對象只有一個Z值(深度值),我們可以直接用Z值排序。但實際情況更復雜,因為我們使用的是所謂“2.5D”的渲染方式,這種方式本身就是一種近似模擬三維視覺效果的手段,而這種近似就帶來了各種排序上的問題。
存在的問題:
我們在渲染中面臨兩個關鍵的排序維度:
- Z值(Z-depth):比如角色站在樓梯上方或下方,確實存在“層級”上的區別,Z值可以很好地表達。
- Y值(屏幕位置):但在偽3D(例如等距視角)中,即使Z值一樣,Y值不同也需要不同的排序。舉個例子:
- 玩家和一棵樹都在同一Z平面上;
- 樹的Y值比玩家靠近屏幕底部,看起來更“靠前”,應該遮住玩家;
- 所以我們需要根據Y值來補充排序。
這意味著:Z值和Y值都要考慮,并組合成一個綜合排序依據。
我們采取的策略是:
將Z值與Y值組合成一個32位整數排序鍵,方式如下:
- 高位部分:Z值(用于控制“層級”先后,比如地面、二樓、地下室等等);
- 低位部分:Y值(用于控制同一層級中,誰在更前面);
- 具體方法是:
假設Z值的范圍是 -32 到 +32,我們可以將Z值乘以一個足夠大的數,比如1024(讓Z值變成高位);
然后將Y值作為低位直接加進去,得到一個 32 位的整數作為排序鍵。
即:
sort_key = Z * 1024 + Y
這樣排序時:
- 先比較Z值(高位),確保不同“層”的物體按正確順序排列;
- 再比較Y值(低位),確保同一層的物體根據在屏幕上的遠近排序(Y值越大越靠近屏幕底部,看起來更“靠前”)。
結果與意義:
這種做法雖不是完全準確(因為不是全3D),但在2.5D世界里,它已能在絕大多數情況下提供合理的渲染順序。例如:
- 玩家走到一棵大樹后面時,樹能正確遮擋玩家;
- 樓梯上下兩層的物體能保持清晰的前后關系;
- 渲染順序的一致性和視覺連貫性大大提高。
當然,如果出現像飛行、穿越結構等復雜情況,這種基于排序鍵的簡化模型可能不夠準確。但考慮到2.5D游戲的資源限制和渲染性能,這種方法已經足夠實用,能夠大幅提升渲染的視覺正確性。
接下來的任務就是:基于這個策略實際構造排序鍵并將其集成到渲染系統中,真正實現我們前面設計的排序流程。
目前我們已經明確了渲染排序的兩大任務:
第一任務:生成排序鍵(Sort Key)
我們已經確定,排序鍵將由兩個部分組成:
- 高位是 Z 值,代表物體所處的“層級”;
- 低位是 Y 值,代表屏幕上的“靠近程度”;
組合方法就是將 Z 乘上一個較大的常數(例如 1024)后,加上 Y 值,這樣可以用一個 32 位整數表示一個物體的渲染優先級。排序時,只需要根據這個整數從小到大排序即可,越小代表越“靠后”越早繪制,越大代表越“靠前”越晚繪制,從而實現從后往前的渲染模式(即 back-to-front)。
這個策略既保留了層級關系(Z),又兼顧了偽三維中屏幕垂直方向的視覺遮擋關系(Y),非常適合 2.5D 場景下的繪制需求。
第二任務:進行排序
完成排序鍵生成后,我們需要對所有待繪制對象執行一次排序。
這里的策略也已經定好:
- 我們不會直接去移動繪制數據(push buffer 中的數據體積較大,直接移動代價高);
- 我們會先創建一個額外的結構(sort buffer),每個元素占用 64 位,分別是:
- 32 位的排序鍵;
- 32 位的偏移值(用于回到原始 push buffer 中對應的數據);
- 排序操作將僅針對 sort buffer 中的條目進行,代價低且操作快速;
- 排完后,根據排序后的偏移值再去取對應的數據進行繪制。
當前目標:
現在我們有大約半個小時的時間,計劃是:
- 先完成第一部分任務:生成每個物體的排序鍵,先使用一個近似方法進行填充,用于測試。
- 后續再完成排序部分,實現一個基礎版本的排序邏輯。
- 展示整個過程是如何工作的。
- 后面可以根據效果再逐步優化,比如考慮更復雜的遮擋情況、更細致的 Z/Y 權重調整等。
總結:
當前階段的核心是:
- 搭建起一個排序體系,哪怕初步版本并不完美;
- 保證能通過組合 Z/Y 值的排序鍵來大致還原正確的繪制順序;
- 通過使用額外的 sort buffer,避免在排序階段頻繁搬移大量渲染數據,提高效率;
- 最終目標是讓渲染結果在視覺上更合理、更具層次感,符合玩家預期的空間遮擋關系。
現在正式開始動手實現第一步:為每個繪制元素分配排序鍵,并準備好數據結構。
在 game_render_group.cpp
中討論排序策略
我們當前的目標是對渲染進行排序,以便按照正確的遮擋順序進行繪制。現在進入了真正將排序邏輯整合到渲染流程中的階段。
當前情況
我們有一個函數負責把渲染組(render_group)轉換為最終輸出,這個函數叫 RenderGroupToOutput
。實際上項目中有兩個同名函數,雖然這有些混亂但我們暫時就這么處理。
當前的實現方式是:
我們遍歷了 push buffer 中的所有渲染命令,按它們加入的順序直接進行渲染。
這意味著它們在內存中是線性的,執行時也具有良好的緩存局部性,效率高。
我們要做的改動
我們的目標是:不再按 push buffer 的原始順序繪制,而是先生成排序數組(Sort Array),再按排序后的順序渲染。
也就是說,我們要做的事情包括:
-
建立 Sort Array(排序數組):
- 不再直接繪制,而是將排序用的信息(排序鍵 + 原始命令偏移地址)記錄下來;
- 這個結構中每個元素 64 位:前 32 位是排序鍵(Sort Key),后 32 位是對應 push buffer 中數據的偏移量(Offset);
-
對 Sort Array 進行排序:
- 使用排序鍵對數組從小到大排序;
- 越小的排序鍵表示越“靠后”,應該越早繪制,實現 Back-to-Front 渲染;
-
按排序結果執行繪制:
- 遍歷排序后的排序數組;
- 每次取出對應 offset,從 push buffer 中讀取原始渲染數據并執行繪制命令;
關于性能問題
我們目前這樣處理是有一些代價的:
- 原先 push buffer 是線性內存訪問,現在是隨機訪問;
- 因為排序后的順序不保證數據的物理連續性,所以內存緩存命中率會下降;
- 這會導致所謂的 scatter-gather(分散讀取)問題;
但是,這個代價是必要的,因為:
- 在 2.5D 的世界中,遮擋順序非常重要,錯誤的繪制順序會破壞整個視覺效果;
- 除非我們能在游戲邏輯層面預先輸出排序好的渲染命令,但那樣會極大復雜化邏輯,代價更高;
因此我們選擇保留這種“先推送命令、再排序執行”的機制,雖然可能會犧牲一部分緩存效率,但可以確保正確性,并保持結構清晰。
接下來要做的工作
現在我們要進入具體實現階段:
- 在
RenderGroupToOutput
函數中,不再直接繪制; - 而是遍歷所有命令,為每一個構造一個排序鍵+偏移對,并寫入排序數組;
- 對排序數組進行排序;
- 最后再遍歷排序數組,并根據 offset 訪問 push buffer 中的命令并執行繪制。
小結
- 我們從“按推入順序繪制”轉換為“先排序再繪制”;
- 引入了 Sort Buffer 存儲排序信息;
- 排序鍵是 Y 和 Z 的混合值,用來決定物體應該出現的前后關系;
- 犧牲了一些性能,但提升了視覺準確性;
- 整體框架已經搭建,接下來是逐步完善這條完整的渲染路徑。
這一步的完成意味著我們已接近一個基礎版“支持遮擋排序”的渲染器原型。
在 RenderGroupToOutput
中找到每個 Entry 的 PushBufferOffset
,修改函數以接收 SortEntryCount
和 *SortEntries
我們現在要做的是,從串行順序的渲染循環轉變為基于排序數組的跳躍式渲染流程。具體的改變和細節如下:
原始邏輯回顧
之前的渲染流程很簡單:
我們直接從 push buffer 的基礎地址開始,依次遍歷每個渲染命令,逐個處理。這種方式內存訪問是線性的,性能較好。
修改后的渲染邏輯
現在我們有了一個排序數組(Sort Entries),每個元素記錄了:
- 一個排序鍵(決定渲染順序)
- 一個 push buffer 中的偏移量(指出原始數據位置)
我們現在要:
-
用排序后的順序來進行渲染:
- 遍歷排序數組;
- 每次取出其中的 offset;
- 直接跳轉到 push buffer 的對應位置,讀取該渲染命令并執行;
-
刪除 base address 的追蹤邏輯:
- 原來循環內部是根據 base address 累加來推進;
- 現在是每條記錄都有自己的 offset,所以不再需要在循環中追蹤和更新 base address,這一部分邏輯可以刪除,變得更簡潔。
接口調整
因為排序是多線程進行的,每個 tile 會單獨進行處理,因此我們需要為每個 tile 分別傳遞:
- 該 tile 的排序數組指針;
- 該 tile 的排序數組長度;
這意味著 RenderGroupToOutput
函數需要新增兩個參數:
tile_sort_entry_count
tile_sort_entries
這樣每次調用渲染輸出時都能知道當前 tile 該使用哪部分排序數據。
存在的問題
雖然渲染函數邏輯已經修改得較為清晰,但在實際調用 RenderGroupToOutput
時還有一部分信息我們暫時缺失:
- 我們當前有完整的排序數組
entry_list
; - 但是我們還沒有建立「每個 tile 應該使用排序數組中的哪一段」的映射關系;
這意味著我們暫時還無法將排序數據精確地分配給各個 tile。
后續處理規劃
為了不把這個過程寫得太復雜,我們計劃把這個步驟拆成兩個階段:
- 當前階段完成渲染循環邏輯的重寫,使其基于排序數組而不是 base address;
- 后續階段再來處理如何為每個 tile 準備對應的排序數據并傳入。
小結
- 渲染循環現在改為基于排序數組跳轉式訪問;
- 渲染效率可能略低,但能正確處理遮擋關系;
- 接口需要調整以傳入每個 tile 的排序數據;
- 目前還需要處理排序數據和 tile 之間的映射問題;
- 后續將分階段處理,保證代碼清晰易維護。
這個重構是渲染架構向更靈活、更正確的方向邁出的關鍵一步。
黑板講解:當前屏幕渲染流程
當前我們屏幕的渲染方式是基于區域(chunk)劃分的。具體實現邏輯如下:
當前的渲染邏輯概述
我們將屏幕劃分為若干個小塊(chunk),然后使用多線程的方式分別渲染這些塊:
- 比如,線程1渲染第一塊;
- 線程2渲染第二塊;
- 線程3渲染第三塊;
- 線程4渲染第四塊;
- ……以此類推。
這樣可以加速渲染過程,因為每個線程可以并行處理不同區域的圖像數據,從而提高整體渲染效率。
接下來的目標:將排序也并行化
接下來我們希望將排序操作也變為多線程的,邏輯如下:
- 每個 chunk 對應一個獨立的排序列表;
- 每個線程在處理自己區域內的渲染數據時,同時對該區域內的數據進行排序;
- 這樣可以避免所有線程共享一個全局排序列表,避免競爭和資源沖突,也能更好地利用多核資源;
實現這一點的前提
要讓每個 chunk 自己完成排序,我們需要做到:
- 渲染命令在進入排序系統之前,根據其位置被裁剪(clipping)進對應的 chunk;
- 即在收集渲染命令的時候,就判斷這個命令應該被送入哪個 chunk 的排序隊列;
- 最終每個 chunk 會擁有自己的 push buffer 數據 + 對應的 sort entries 列表;
當前暫不處理按塊裁剪的細節
目前我們暫時不處理將渲染命令按 chunk 分發的問題,也就是說:
- 現在所有渲染命令都會進入一個統一的排序列表;
- 所有排序和渲染操作都仍然基于這個全局的排序列表執行;
- 這樣雖然還不能實現真正的多線程排序,但能先完成基本的排序系統搭建;
等到排序機制完整運行之后,我們再進一步細分,使其支持區域劃分、并行排序和多線程裁剪。
小結
- 當前使用多線程并行渲染多個區域(chunk);
- 未來目標是讓排序也支持多線程處理;
- 為此,需要將渲染命令按區域裁剪并分發到對應 chunk 的排序隊列中;
- 當前暫不做區域裁剪,先統一處理,等機制穩定后再并行優化。
這是從單線程渲染走向高性能并行渲染體系的重要一步,結構也會隨著目標不斷完善和擴展。
考慮最佳的排序實現方式
我們目前在思考排序操作的最佳實現方式,有幾個關鍵問題需要權衡:
初步決定:先統一排序,再看是否并行優化
我們決定先在主線程里統一進行一次排序,不考慮并行優化,理由如下:
- 可以先觀察排序的性能表現;
- 如果排序耗時太長,再考慮多線程處理;
- 這樣做的好處是簡化初期實現邏輯,不必在一開始就處理多線程相關的復雜結構;
- 后續如需并行處理,也可以再逐步拆分擴展。
結構上的考慮:是否保留額外的 SortSpace
如果選擇只進行一次排序,那么可能不需要額外的 SortSpace(額外的排序緩沖區),具體分析如下:
- 當前的 push buffer 并不是固定大小;
- 我們也不知道 push buffer 中每條命令所處的具體偏移;
- 但我們要生成排序所需的 tile sort entry 列表,這個列表最好是在 push 的時候就順便生成;
- 如果在 push 階段就同步構造這個排序條目數組,那么在渲染階段直接拿來用即可,無需額外處理;
- 這種方式不依賴 SortSpace,避免冗余結構,也節省內存管理的麻煩;
更進一步的優化思路
我們甚至可以將排序 entry 的數組放在 push buffer 的尾部,按順序往下寫:
- push buffer 從頭部向下寫入圖形命令;
- 排序 entry 從尾部向上寫入 key 和 offset;
- 最終 push 完成時,排序數組也已經構建完成,只需排序然后渲染即可;
- 這樣可以避免多次掃描、避免臨時數組和空間分配,效率更高;
總結當前思考:
- 目前決定:先統一排序,暫不做多線程并行排序處理;
- 有很大可能會取消 SortSpace,轉而采用 push 階段直接構造排序 entry;
- 如果按上述方式處理,結構將更緊湊、更高效;
- 后續如需支持并行排序,也可以基于該結構進行擴展,靈活性仍然在。
下一步,我們會基于這種更簡潔的模型進行實現嘗試。這個方向看起來更合理,也更容易調試。
在 game_render_group.h
中為 render_group
添加 u32 SortEntryAt
成員
在 render_group
結構中,我們本來就有一個類似 PushRenderElement
的東西,現在我們計劃加入排序支持,思路是這樣的:
在 render_group
中直接處理排序
我們希望在 render_group
內部直接生成排序條目(sort entries),而不是單獨維護一個 SortSpace
。這種方式的主要優勢是結構緊湊、效率更高、邏輯更直觀。
所以我們可能會添加一個類似以下的新字段:
SoftEntryAt
這個字段用于記錄當前要寫入的排序條目的索引或偏移。
工作原理
在每次向 push buffer 添加繪制命令的時候:
- 計算排序 key:我們根據
Z
和Y
值組合成一個 32 位的 key,用于后續排序。 - 記錄偏移位置:我們記錄當前繪制命令在 push buffer 中的偏移。
- 寫入 sort entry:將 key 和偏移(index)打包成一個
sort_entry
,寫入 sort entry 數組,并更新SoftEntryAt
指針或索引。
這樣,我們在生成 push buffer 的同時,就順手生成了對應的排序條目,不需要再遍歷一次 push buffer 來生成排序信息,節省性能開銷。
優勢總結
- 節省一次遍歷:避免在渲染階段額外掃描 push buffer;
- 不需要額外分配排序空間:直接和 push buffer 關聯;
- 邏輯更集中統一:寫入命令的地方就生成對應的排序 entry,數據生命周期和作用域也更清晰;
- 便于未來擴展:如需多線程,只需將 push 和 sort 分別放在線程專屬 buffer 中,結構照搬即可拆分并行。
接下來我們就會基于這種結構,完善排序過程和渲染流程的整合。
在 AllocateRenderGroup
中設置 SortEntryAt
,并在 PushRenderElement_
中使用它
我們在初始化 render group
時,會在分配 push buffer
的時候,將排序條目的區域安排在 push buffer
的尾部。具體做法如下:
初始化階段的布局
當我們調用 allocate_render_group
時:
- 設置
PushBufferBase
; - 計算
PushBufferBase
為:
PushBufferBase + MaxPushBufferSize
,
也就是說排序條目的寫入起點就在push buffer
的末尾。
這個做法的好處是我們不需要為排序條目單獨申請新的內存區域,直接利用原有分配空間的一部分即可。
寫入繪制元素時同步寫入排序信息
在執行 PushRenderElement
時(即將元素寫入 push buffer
):
-
計算新的寫入位置是否越界
不再僅僅判斷(Group->PushBufferSize + Size) < Group->MaxPushBufferSize
,而是根據排序區域的位置進行判斷,確保繪制元素不會覆蓋到排序區域。
判斷方式如下:(Group->PushBufferSize + Size) < (Group->SoftEntryAt - sizeof(tile_sort_entry))
-
在 push 元素的同時寫入排序條目:
即我們把當前的繪制元素信息(例如Z
值、Y
值打包成 sort key,再加上該元素在 push buffer 中的偏移地址)打包成一個sort_entry
,寫入到PushBufferBase
指向的位置。 -
遞減 PushBufferBase:
因為我們是從尾部往前寫入排序信息,每次寫完一條就向前移動一格(PushBufferBase -= sizeof(tile_sort_entry)
)。
可視化布局(邏輯結構)
我們內存布局可抽象為:
+----------------------+ ← PushBufferBase
| Push Buffer |
| (render data) |
| |
| |
| |
| |
+----------------------+ ← PushBufferBase 初始值
| Sort Entries ↓ | (向低地址寫入)
| [key + offset] |
| [key + offset] |
+----------------------+
優勢總結
- 內存利用最大化:不用額外分配排序區域;
- 保證了 push/render 和 sort 信息的一一對應性;
- 寫入邏輯集中且效率高:每次 push 時順手寫入排序;
- 簡化了后續排序階段的流程:數據已準備好直接排序使用。
這種方式結構清晰且高度集成,非常適合未來進一步擴展優化或并行處理。接下來只需要設計排序邏輯和修改渲染循環來使用排序好的條目即可。
黑板講解:從棧頂壓入 entry,從棧底壓入 sort 數據
我們當前采用了一種類似“棧-堆對頂分配”的結構來組織內存,即在一個連續內存塊中,從一端向上推入繪制元素數據(render entries),從另一端向下推入排序條目(sort entries)。
內存布局結構
- 內存區域起始地址:
PushBufferBase
- 內存區域尾部地址:
PushBufferBase + MaxPushBufferSize
- 中間是可用空間
- 結構如下:
+---------------------------+ ← PushBufferBase
| Render Entries ↑ | 每次 push 一個繪制元素,從低地址往高地址推
| |
| ... |
| |
| |
| Sort Entries ↓ | 每次 push 一個排序條目,從高地址往低地址推
+---------------------------+ ← PushBufferBase + max_size
兩端對推機制
- 每次添加一個繪制元素時,從低地址開始逐漸向高地址推進;
- 每次添加一個排序條目時,從高地址開始逐漸向低地址推進;
- 排序條目一般是 64 位(8 字節),記錄排序 key 和該元素在
push buffer
中的偏移。
空間耗盡的判定
我們判斷空間是否耗盡,不再只看是否超過 max_size
,而是判斷兩個指針是否即將“碰撞”:
if (next_push_buffer_ptr + sizeof(render_entry) > next_sort_entry_ptr - sizeof(sort_entry)) {// 空間不足,不能再推入新的 render entry 或 sort entry
}
換句話說,只要兩個區域尚未相遇(即仍有中間空隙),我們就可以繼續推入數據。
總結要點
- 使用一塊內存完成兩種數據存儲;
- 推入順序分別從頭部和尾部進行;
- 排序條目固定大小,內容為關鍵值 + 對應 render entry 的偏移;
- 空間耗盡的判斷邏輯基于兩個指針是否交叉;
- 實現非常高效、零內存浪費,無需額外分配;
這種方式結構緊湊、性能優良,非常適合低開銷渲染流水線的設計,也為后續的排序和繪制邏輯提供了堅實基礎。
在 game_render_group.cpp
中實現上述 push 操作,并使 PushRenderElement_
接收 r32 SortKey
我們現在的實現中,采用的是一個連續內存塊的對頂分配策略,也就是說:
- 繪制元素(Render Entries) 從內存塊的起始地址向上增長;
- 排序條目(Sort Entries) 從內存塊的尾部向下壓入;
- 這兩部分數據在內存中對頂推進,直到彼此相遇(即沒有可用空間)。
新的排序條目添加邏輯
每當我們向渲染組中 push 一個新的渲染元素時,會同時創建一個新的 排序條目,具體步驟如下:
-
計算剩余空間:
在添加新渲染元素之前,判斷當前 push buffer 的位置和 sort entry 的起始地址之間是否還有空間可用。如果剩余空間不足,就不能繼續 push。 -
更新 sort entry 指針:
Group->SoftEntryAt -= sizeof(tile_sort_entry);
將 sort entry 指針向下移動,為新條目騰出空間。
-
寫入排序條目數據:
- 排序條目有兩個字段:
sort_key
(排序鍵)—— 這是我們目前還沒有的,后續必須提供;PushBufferOffset
(指向渲染元素的偏移量)—— 這個我們已有,是當前PushBufferSize
值。
tile_sort_entry *entry = (tile_sort_entry *)(group.PushBufferBase + group.SoftEntryAt); entry->PushBufferOffset = group.PushBufferSize; entry->sort_key = sort_key; // 需要外部提供
- 排序條目有兩個字段:
渲染輸出邏輯的簡化
因為我們在 push 階段已經收集好了所有排序信息,因此在輸出時 RenderGroupToOutput
不再需要額外傳入 sort entry 的信息,它可以從 group 中自行獲取:
- 排序條目的總數 = 當前已推入的渲染元素數量;
- 排序條目的起始地址 =
PushBufferBase + SoftEntryAt
。
這樣一來,渲染輸出邏輯變得更加簡潔、明確,不需要依賴外部管理這些信息。
總結要點
- 我們在 push 階段同時維護了一個 sort entry 數組;
- sort entry 是在 push buffer 尾部倒序增長,避免額外內存分配;
- 排序所需信息(排序鍵、元素偏移)在 push 時即可得出;
- 渲染輸出階段不再需要額外參數即可直接執行排序渲染;
- 唯一的新增需求是:每個渲染元素必須帶有一個
sort_key
。
這種方式非常高效地將“排序”嵌入到渲染元素構建流程中,無需額外開銷、結構清晰,未來也便于擴展,比如按 tile 分片進行排序、多線程并行等。
在 game_render_group.cpp
中修復編譯錯誤并傳遞 SortKey
我們現在正在完成渲染系統中排序邏輯的基本搭建,重點是為每一個渲染元素生成一個 排序鍵(Sort Key) 并將其寫入到 push buffer 末尾的排序條目數組中,使得后續的渲染階段可以基于這些排序鍵進行繪制順序的控制。以下是整個過程的詳細整理:
排序鍵的生成與寫入邏輯
我們在渲染系統中為每個渲染元素都建立了一個對應的排序條目,其中包含兩個關鍵字段:
- 排序鍵(Sort Key):用于決定該元素在屏幕上應被繪制的順序。
- Push Buffer 偏移量:指向該渲染元素在 Push Buffer 中的存儲位置。
具體實現思路如下:
- 在調用
PushRenderElement
時,除了正常壓入渲染數據,還需要將該元素的排序信息同時記錄下來。 - 排序條目的寫入地址是
PushBufferBase + SortEntryAt
,它從 buffer 尾部向前推進。 - 為了保證不發生內存覆蓋,我們檢查當前的 buffer 使用量和剩余空間,確保排序條目和數據不會沖突。
排序鍵的來源
排序鍵的獲取依賴于渲染元素在屏幕空間中的 位置(Y軸和Z軸)。我們基于以下邏輯生成一個可用的排序值:
- 從變換結果中獲取元素的
P.z
(深度)和P.y
(垂直位置)。 - 利用如下方式組合成排序鍵:
SortKey = Z值*4096 - Y值
- Y值的方向是從上到下遞減,所以我們需要做一次翻轉(4096 - Y),使得越靠下(也即靠近屏幕前面)的元素排序值越大,從而優先繪制。
這種排序邏輯意味著:
- 更小的 Z 值(更靠近攝像機)+ 更大的 Y 值(更靠近底部) → 排序鍵越大。
- 在排序時我們可以按升序或降序排序,來控制繪制順序。
特殊元素處理:Clear 操作
對于不參與排序邏輯的元素,比如清除背景的 Clear 操作,我們人為賦予一個 最小的排序值(Real32Minimum
),使其始終被排到最底層進行繪制。這保證了不管其他元素如何排序,Clear 都不會被遮蓋或干擾。
推進整合與回收
我們將排序鍵的生成提前到了創建 Basis(變換基準)的階段:
- 所有通過
GetRenderEntityBasisP
的地方,都會生成并返回一個帶有SortKey
的 Basis。 - 所有使用該 Basis 的地方(例如
PushRect
),將直接使用該SortKey
,不需要手動重復計算。
這樣做的好處是邏輯更集中,避免在每次 push 時重復寫一段計算排序鍵的代碼。
當前成果與下一步計劃
我們已經完成了:
- 在 PushRenderElement 時自動寫入排序條目;
- 自動從 Basis 中獲取排序鍵;
- 為 Clear 等特殊操作賦予最低排序值;
- 排序信息結構已就位,RenderGroupToOutput 可以使用這些排序條目進行繪制調度。
下一步:
可以開始在 RenderGroupToOutput
中根據這些排序條目進行排序,并依照排序后的順序進行繪制調用,實現真正的“先后繪制”控制,優化圖層堆疊正確性,避免重疊錯誤。
這意味著我們的渲染系統離真正的 深度感知繪制調度 只差一步了
運行游戲,發現排序看起來不對
debug的字都沒有
黑屏呢
忘記遍歷softEntity
目前我們已經完成了排序條目的構建工作,也就是說,現在所有的渲染元素在被推入渲染隊列時,都會同時寫入一個包含排序鍵和偏移地址的排序條目,這樣理論上就具備了排序繪制的基本能力。
當前測試結果與現象分析
我們嘗試運行當前的實現并觀察渲染結果,發現以下現象:
- 畫面確實有繪制輸出,說明排序數組的構建過程在邏輯上是有效的,渲染數據成功通過排序條目被調度參與繪制。
- 但畫面顯示效果不正常,可能存在以下幾種問題:
- 層級關系錯亂,某些應被遮擋的對象卻繪制在最上層;
- 屏幕內容混亂,某些對象順序與邏輯預期完全相反。
這些現象的初步判斷如下:
初步分析
-
排序沒有生效:目前雖然排序條目數組已經準備好了,但還沒有執行實際的排序操作,渲染輸出仍然按原始 push 順序繪制。這意味著:
- 比如背景清除(Clear)操作,如果最后才 push,那么它將被繪制在所有元素之上,反而遮擋了所有內容。
- 如果前景元素先被 push,背景元素后被 push,則前景被背景蓋住,結果就會出現“畫面看上去全亂了”的情況。
-
當前行為“理論上合理”:因為我們沒有進行排序,所以它表現得像是反序繪制,這是符合邏輯的。
下一步計劃與調試方向
為了解決當前出現的繪制順序問題,我們需要完成兩個關鍵步驟:
正確執行排序
在 RenderGroupToOutput
函數中:
- 遍歷構建好的排序條目數組;
- 按照排序鍵進行升序或降序排序(取決于設定的深度與方向);
- 然后根據排序條目的 offset 去實際訪問 push buffer 中的渲染數據,并執行繪制。
驗證排序邏輯是否正確生成
在正式排序前,建議做以下調試輔助:
- 打印排序鍵值(SortKey)與對應的元素名稱/標識;
- 檢查 PushBuffer 中記錄的 offset 是否對應正確的數據段;
- 驗證排序鍵與元素在屏幕上的 Z/Y 位置是否匹配(即越近的元素,排序鍵是否越大或越小,符合預設規則);
- 重點檢查 Clear 元素是否始終具有最小排序鍵,確保在排序后最先繪制。
總結當前進度
- 排序條目數組構建完成;
- 排序鍵邏輯初步實現;
- 尚未進行排序處理;
- 由于缺少排序,渲染順序錯亂屬預期行為;
- 需要實現排序并進行渲染映射。
接下來一旦補上排序邏輯,系統應該就會表現得更接近預期的深度繪制結構。我們已經打好了基礎,現在就差最后一塊拼圖了
在 game_render_group.cpp
中反轉排序順序
我們打算做一個測試:從排序條目數組的末尾開始向前遍歷,看看是否能產生正確的渲染結果。
之所以這樣做,是因為我們在之前構建排序條目數組時,采用的是“從頭往后推”的方式將元素加入渲染隊列,而排序條目本身是從 buffer 尾部往前寫入的。這意味著——條目的排列順序和它們原始入隊順序是相反的。
所以我們現在要驗證一個假設:
假設
如果我們倒序遍歷這些排序條目(即從最后一個向前走),那么渲染順序將會和元素原始入隊順序一致,應該可以恢復正確的顯示效果。
實施目標
- 從排序條目數組末尾開始往前遍歷;
- 根據每個條目的 offset 去讀取 push buffer 中的數據;
- 渲染每個元素;
- 最終觀察是否恢復正確的層級/遮擋關系。
背后邏輯說明
- Push Buffer 是正向增長的,數據從前往后寫入;
- Sort Entry 是反向寫入的,從尾部往前;
- 所以如果我們不進行排序,也不修改邏輯,但從末尾開始按順序走,就等于還原了原始 push 順序;
- 這樣至少能讓畫面恢復我們期望的入隊順序繪制(即后加入的元素繪制在后面,前面的在前面)。
測試預期結果
如果畫面恢復正常,那么:
- 說明排序條目確實是按預期從尾部寫入的;
- 我們可以確認 buffer 的結構與構建順序是合理的;
- 接下來我們只需對這些條目進行排序處理,即可控制任意繪制順序。
當前狀態總結
- 正在進行倒序遍歷的驗證;
- 此舉將確認排序條目構建邏輯是否健壯;
- 如果成功,則下一步就是正式插入排序算法,全面控制繪制順序。
下一步我們就能朝著正確的透明排序或遮擋剔除方向邁進了。繼續推進
再次運行游戲,發現排序正確
我們剛才的問題其實非常簡單,問題的根源在于排序條目數組的方向是反的。
我們在構建排序條目數組的時候,是從 push buffer 的末尾向前寫入排序條目的——也就是說,這些排序條目在內存中排列的順序和我們實際推入渲染數據的順序是相反的。
所以,問題的核心是:
我們在遍歷排序條目時,是正向遍歷,從頭到尾一個個地去處理。但這樣一來,就會打亂原本我們希望遵循的繪制順序。
而我們真正需要的是——按照 push buffer 中元素入隊的順序來進行繪制。
解決辦法:
我們將排序條目數組倒序遍歷,也就是說:
- 從最后一個排序條目開始,一直到第一個;
- 每一個條目指向 push buffer 中的一個元素;
- 這樣,我們就還原了元素的真實入隊順序;
- 所以繪制出來的效果就恢復正常了。
總結一下
- 排序條目是從尾部向前寫入的;
- 所以遍歷時也要從尾部開始向前;
- 如果我們直接從頭開始遍歷這些條目,會得到反向的繪制順序,導致圖層關系混亂;
- 一旦使用正確的遍歷順序,一切就恢復正常了。
這個小錯誤看起來不大,但它直接導致整個屏幕的渲染順序出現了問題。現在我們已經定位并解決了這個問題,說明整體的排序條目構建和渲染機制基本是健壯的。接下來就可以開始引入真正的排序邏輯,實現更復雜的圖層控制。
黑板講解:清屏操作在所有渲染之后才執行
我們在構建排序條目數組時,是從緩沖區的末尾開始向前推入數據的,也就是說數據是逆序寫入的。由于這種寫入方式,所有進入排序數組的渲染條目的順序剛好和我們實際想要的繪制順序完全相反。
實際情況如下:
- 渲染時,第一個被推入緩沖區的條目是 清屏指令(Clear)。
- 接著是其他繪制內容,比如圖片、圖形等。
- 由于我們從緩沖區尾部往前寫排序條目,清屏操作就被放在了排序條目數組的最末尾。
出現的問題:
當我們正向遍歷這個數組時,清屏就成了最后才執行的操作,也就是說:
我們先把所有圖形渲染完了,然后執行清屏操作,結果就是整張屏幕又被清空了,看不到任何內容。
解決方式:
雖然這個問題是因為遍歷方向引起的,但我們其實不用修改這個順序,因為——
我們即將引入排序機制來重新對這些排序條目進行排序,所以當前的順序并不重要。
只要我們后面實現正確的排序邏輯(根據 sort key
),所有的繪制順序都會變得正確,包括讓清屏操作在最開始執行。
結論整理:
- 我們是從緩沖區尾部往前推排序條目的,順序是反的;
- 因此清屏條目在數組的最后,導致它在最后才被執行;
- 渲染時清屏在最后會把整個屏幕清空,導致“看不到任何圖像”;
- 我們不打算手動修改順序,而是準備通過
sort key
排序來解決這個問題; - 所以現在唯一需要關注的是:生成正確的排序 key,而不是條目的初始順序。
這樣一來,系統最終就能自動按照正確的繪制順序工作。我們當前只是在測試這個機制是否運作良好,驗證思路是否正確,下一步將進入排序邏輯的真正實現階段。
在 game_render_group.cpp
中引入 SortEntries
我們現在要開始實現排序操作。具體來說,我們準備在將渲染組輸出(RenderGroupToOutput
)之前,先對渲染條目進行排序,以確保正確的繪制順序。
當前結構分析:
- 有一個函數叫
TiledRenderGroupToOutput
,它負責將渲染組分塊處理(tile),然后調用CompleteAllWork
執行真正的繪制工作。 - 問題在于,我們必須在這之前完成排序,否則條目的繪制順序將是錯誤的。
不能依賴的機制:
- 我們之前在渲染組有一個“開始-結束”的機制(比如 begin / end),但這個機制是在
output
之后才會被調用的。 - 所以 太晚了,不能用于觸發排序邏輯。
正確的解決思路:
我們決定在 RenderGroupToOutput
和 tiled_RenderGroupToOutput
內部手動調用排序函數,例如:
sort_entries(render_group);
- 排序操作必須在進行任何輸出操作之前完成。
注意事項:
我們在排序調用處加了一個小的 TODO
備注:
“不要重復排序”。
意思是,如果一個渲染組被多次輸出,例如由于重復渲染需求,當前邏輯會多次執行排序,其實是不必要的,因為:
- 一旦排序完成,排序列表就是有效的;
- 重復排序只是浪費性能;
- 因此需要做一次性標記或緩存判斷,避免重復執行排序操作。
總結要點:
- 排序必須在渲染組輸出之前進行;
RenderGroupToOutput
和tiled_RenderGroupToOutput
中都應主動調用sort_entries
;- 當前的“begin/end”機制無法滿足需求,太晚執行;
- 后續要優化,避免重復排序;
- 排序完成后才進入線程任務
CompleteAllWork
進行最終渲染。
接下來,我們將進入具體的排序實現階段,對渲染條目數組按 sort key
進行實際排序。
黑板講解:冒泡排序
我們現在要實現排序邏輯,而我們選擇的,是最簡單、最愚蠢但最容易理解的排序方式:冒泡排序(Bubble Sort)。
冒泡排序的基本思想:
我們有一個包含若干元素的數組,例如:
5, 3, 1, 0, 2, 4, 6
我們的目標是把這些元素按一定順序排列(比如從小到大)。冒泡排序的核心思想就是:不斷比較相鄰的兩個值,如果它們順序錯了,就交換它們的位置。
具體操作過程:
- 每一輪,我們從數組開頭到結尾依次檢查相鄰的兩個元素;
- 如果前一個比后一個大,就交換它們;
- 這樣一輪下來,最大的值就會“沉”到數組最后的位置;
- 接下來再重復同樣的過程,每次排好一個“最大的”元素;
- 最多進行 N 次,就能排好整個數組。
例如:
- 第一輪后:最大元素排到最后;
- 第二輪后:第二大的排到倒數第二;
- 以此類推……
每一輪會把當前還未排序部分的最大值“推”到尾部,就像重力讓重物往下沉一樣,因此也可以把它想象成“重力排序”。
時間復雜度:
- 對于一個長度為 N 的數組,在最壞情況下,我們需要遍歷數組 N 次;
- 每次遍歷時可能最多比較 N 個相鄰的值;
- 所以總共需要進行約
N * N = N2
次操作; - 這就是一個O(N2) 的排序算法,非常低效,但非常簡單。
為什么現在選擇這種方法?
- 當前我們的目標只是驗證排序邏輯能不能跑起來;
- 并不打算現在就實現任何高級優化;
- 所以我們選擇最簡單的方法,只為了先把功能跑通;
- 真正優化排序效率的部分,后面再做。
總結關鍵詞:
- 冒泡排序: 反復交換相鄰錯誤順序的元素;
- 原理清晰: 每次移動一個元素接近其目標位置;
- 效率低下: 最壞情況為 O(N2);
- 實現簡單: 易于理解、便于測試;
- 臨時方案: 目的是先跑通系統流程,后續可換更快算法;
我們接下來會寫出這個冒泡排序的代碼,用于對渲染條目的 sort key
進行排序,一旦排好,渲染順序就會正確地體現出前后遮擋關系了。
在 game_render_group.cpp
中實現冒泡排序邏輯
我們現在正式實現了一個最基礎、最原始的冒泡排序算法,其完整過程和邏輯如下:
排序整體結構
我們采用兩層循環來進行排序處理:
- 外層循環(outer): 控制排序需要執行多少輪;
- 內層循環(inner): 每輪中比較相鄰元素是否需要交換位置;
- 這樣構成了典型的 N2 復雜度算法結構 —— 總體循環次數是
count * count
;
這種方式雖然效率低,但邏輯非常直接,便于調試和驗證。
排序核心邏輯
-
只迭代到倒數第二個元素:
- 我們每次比較一對相鄰的元素(比如第 i 和第 i+1 個);
- 所以內層循環最多到
count - 1
,防止越界;
-
判斷排序關鍵字(Sort Key):
- 每個條目都有一個
sort key
,表示其在渲染中的順序權重; - 我們希望按從小到大的順序排列這些條目;
- 所以當我們發現當前元素 A 的
sort key
大于元素 B 的,我們就交換它們;
- 每個條目都有一個
-
交換操作:
- 我們先把元素 A 保存起來;
- 再把元素 B 賦值到 A 的位置;
- 然后把剛才保存的元素 A 賦值給 B 的位置;
- 這樣實現了兩元素的交換;
-
排序方向可控:
- 判斷語句可以輕松改為從大到小;
- 只要比較
sort key
的大小關系即可; - 排序的方向完全取決于渲染需求;
示例代碼簡化描述
for (int outer = 0; outer < count; ++outer) {for (int inner = 0; inner < count - 1; ++inner) {Entry* a = &entries[inner];Entry* b = &entries[inner + 1];if (a->SortKey > b->SortKey) {Entry temp = *a;*a = *b;*b = temp;}}
}
這個結構就是我們當前使用的完整冒泡排序邏輯。
關于性能優化的后續思考:
- 當前沒有做任何早停(Early-Out)處理,即使已經排序好了也會繼續跑;
- 后續可以添加標志位,比如某一輪中沒有發生任何交換,說明數組已排好,可以提前結束;
- 當前是臨時調試和驗證邏輯的階段,未來有需要可替換為更高效的排序方式,如快速排序、歸并排序等;
總結關鍵點
- 實現了一個結構清晰、邏輯簡單的冒泡排序;
- 使用雙重循環進行相鄰元素比較與交換;
- 排序基于元素中的
Sort Key
,排序方向可自由切換; - 當前算法效率低但易于驗證;
- 后續可優化性能或更換更先進排序算法;
通過這一過程,我們能夠確保渲染條目的順序是正確的,為后續畫面正確呈現打下基礎。
運行游戲,結果看起來可能是對的
我們完成了排序代碼的編寫,理論上這個排序邏輯應該已經生效。雖然可能打字時有些地方沒完全精確,但整體排序機制是可行的。
當前的狀態和效果
- 排序已經開始起作用,這是因為每個渲染條目都帶有一個 Z 值(深度值);
- 正因為如此,條目本身的插入順序已經不重要了;
- 無論我們以什么順序將它們放入渲染列表中,最終都會根據 Z 值正確排序并呈現;
- 這是一件非常有意義的事情,讓我們可以更加靈活地組織渲染指令;
當前顯示異常但可能是“正常”的原因
- 屏幕上的某些內容看起來似乎有點“糟糕”,但這不一定是錯誤;
- 比如那些地面圖塊(ground tiles)目前并沒有被告知它們應當“在后面”;
- 所以在 Z 值排序時,它們可能會被和其他內容“平等對待”,進而出現在前景或不合適的位置;
- 這并不是排序出錯,而是 缺少明確的層級信息;
臨時思路與調整方向
- 我們意識到:目前并沒有辦法告訴渲染器,“某些對象應該站在這些地面圖塊之上”;
- 因此,接下來的關鍵在于如何為 不同的元素類型賦予合適的 Z 值(排序關鍵值);
- 例如:
- 地面圖塊應擁有較低的 Z 值;
- 人物、物體應具有更高的 Z 值;
- 這樣在排序時才能形成我們想要的前后遮擋關系;
舉例設想:設定合理的排序關鍵字
渲染內容 | Z 值(Sort Key)示例 |
---|---|
地面 | 0.0 |
建筑 | 1.0 |
人物 | 1.5 |
前景裝飾 | 2.0 |
通過這種方式,我們可以確保視覺層次結構的正確性。
總結
- 排序功能已經在運作;
- 屏幕表現雖然有些“奇怪”,但可能是因為缺少分層信息;
- 當前排序系統仍需進一步調整不同渲染元素的
SortKey
值; - 一旦這些值被合理設置,畫面將能正確呈現前后遮擋關系;
- 后續可以優化“填充階段”時對 Z 值的賦值邏輯,使不同類型的對象具備合適的深度信息;
這一步標志著渲染系統逐步開始具備可控的圖層管理能力。接下來就是通過合理賦值讓排序發揮真正作用。
在 game_world_mode.cpp
中為 GroundBuffer
添加 zBias
我們意識到需要讓地面區塊(ground chunks)在渲染時始終處于其他元素的背后,因此想要手動地為地面設置一個較低的 Z 值,從而確保它們在排序過程中總是排在靠后位置。
當前做法與目的
-
在執行 UpdateAndGameRenderWorld 中
render_ground_chunks
相關操作時,有一段代碼用來調用PushBitmap
來添加地面瓦片的渲染; -
現在希望在這一階段直接設置一個靠后的 Z 值,確保這些地面圖塊在最終渲染排序時不會出現在人物或物體之前;
-
所以嘗試在設置該瓦片的位置時,加入一個較低的 Z 值,例如:
z = zBias - 0.5f;
-
這樣做的目的是人為地給地面一個“退后”的深度,確保它們總是比默認 Z 值的其他物體更靠后;
思路與效果
- 這是通過簡單調整深度值來影響排序邏輯;
- 不用更改整個排序系統邏輯,只需在關鍵數據插入時賦予不同的 Z;
- 這樣在
bubble sort
或任何排序算法執行后,地面圖塊總會被排在人物、物品等元素之后; - 渲染出來的視覺效果就是地面在底下,其它對象在其上方,形成正確的前后遮擋層次;
當前結果的不確定性
- 盡管設置了偏移 Z 值,但目前尚未確認效果是否完全正確;
- 顯示上的問題可能仍然存在,這需要進一步驗證是否排序邏輯和數據結構中的 Z 值被正確使用;
- 也可能需要檢查
PushBitmap
內部是否支持接收并使用這個新的 Z 值參數;
下一步建議
- 檢查
PushBitmap
的實現是否會將傳入的 Z 值用于生成排序鍵; - 確認渲染條目中的 Z 值最終是否參與到排序的
sort_key
計算中; - 若一切正常,地面圖塊將正確被排在其他元素之后,實現視覺層級分明的渲染效果;
這一步主要是對渲染順序做初步的邏輯梳理和實際調整,關鍵在于從“插入數據時就設定層級”,而不是僅依賴排序算法本身,確保地面始終被渲染在底層,構建正確的視覺深度。
在 game_world_mode.cpp
中臨時關閉 GroundBuffer
我們發現地面區塊可能還沒有被正確地實現或整合進渲染流程中,所以暫時決定先把它們關閉,先專注于讓我們能清楚看到的部分正常工作。在確保主要可視元素能夠正確排序和顯示之后,再回頭處理地面區塊的集成問題。
當前決策與操作
- 臨時關閉地面區塊的渲染邏輯;
- 目標是先驗證排序系統是否確實能正常工作;
- 因為即使地面部分表現不對,但如果排序系統整體沒有問題,那么說明排序邏輯是可靠的,之后只需要確保地面區塊被正確加入排序列表即可;
排序初步驗證結果
- 從當前其他圖層或對象的渲染結果來看,排序似乎是起作用的;
- 即便我們輸入的順序是亂的,渲染后還是能看出有一定的前后遮擋關系;
- 所以初步判斷排序算法應該已經在起效,至少基本邏輯沒有錯;
下一步計劃
- 等確認排序系統確實穩定后,再恢復地面區塊相關的渲染;
- 重新檢查地面區塊的插入位置、Z 值設置是否正確;
- 保證它們進入排序隊列并且有明確的深度信息;
- 最終確保在統一排序后它們位于其他物體之后,正常顯示在底層;
通過這種逐步驗證和分階段排查的方式,我們確保基礎功能可控可靠,再逐步引入復雜內容,降低調試難度,也避免了因為多個問題疊加而導致定位困難。
運行游戲,排序邏輯看起來運行正常
現在看起來排序功能確實已經正常工作了,我們可以清楚看到渲染順序變得一致了。
排序功能驗證結果
- 渲染結果中,圖像已經能按照設定的順序進行排序,顯示的效果也符合預期;
- 比如畫面中角色的軀干部分現在錯誤地排在了人物前面,但這是因為我們尚未設定具體的排序規則來避免這種情況,并不是排序算法本身的問題;
- 此外,畫面中樹木沿邊排列的部分,也已經明顯按照正確的順序進行了層級渲染,說明排序邏輯運行良好;
當前存在的問題
- 雖然排序在技術上已經實現,但排序的**依據(sort key)**仍然非常粗糙;
- 目前我們還沒有真正設計出一個合理、統一的方式來生成這個排序鍵;
- 尤其是 Z 和 Y 的混合影響因素還沒有深入處理,這可能會導致某些視覺上的錯誤,比如角色身體部件的前后關系錯亂;
接下來的工作重點
- 重新審視并設計排序鍵(sort key)的生成邏輯;
- 需要同時考慮 Z 值(深度)和 Y 值(在畫面上的垂直位置);
- 要制定一個清晰的規則來描述哪一類元素應該在前,哪一類元素應該在后;
- 使生成的排序鍵可以穩定且明確地反映視覺層級;
- 確保角色、地形、物體等在渲染時不會因為鍵值設置錯誤而前后顛倒;
- 解決特定細節,例如人物身體部位之間的前后錯位問題;
目前我們可以確定,基礎的排序算法已經無誤,接下來的挑戰是構建一個合理的、符合視覺邏輯的排序鍵系統。這將是渲染正確性的關鍵所在。
在 game_world_mode.cpp
中增大 zBias 并在正確的位置應用
在當前的實現中,出現了排序效果不理想的問題。通過檢查,發現可能的原因在于排序鍵的計算不完全正確,特別是變換(transform)沒有正確應用到排序上。具體來說,現有的排序邏輯并沒有考慮到物體的偏移量和變換矩陣,這導致了在渲染時,物體的位置并未得到充分的更新,從而影響了渲染順序。
目前的問題
-
偏移量和變換:當前使用的偏移量并沒有考慮實際的變換,導致排序時沒有正確反映物體的實際位置。實際的變換矩陣并沒有被應用到排序邏輯中,導致渲染的結果與預期不符。
-
排序鍵計算問題:在排序過程中,可能沒有正確地處理與變換相關的數據,因此導致某些物體被錯誤地排序,特別是地面塊(ground chunk)的渲染。
解決思路
-
變換應用問題:需要在進行排序時,確保物體的變換(如偏移、旋轉、縮放等)被正確應用。具體來說,應該確保排序過程中能夠正確考慮到這些變換,而不僅僅依賴簡單的偏移量。
-
調整偏移量計算:目前偏移量的計算可能過于簡單,需要進一步優化。例如,可以引入**偏置(bias)**來處理排序中的細節問題,從而使排序邏輯更加精確。
-
重新審視排序鍵:為了使排序更加符合預期,排序鍵的計算方式需要進行重新設計,確保它能夠考慮到變換因素,以及物體在空間中的實際位置。
下一步工作
- 改進偏移量和變換的應用:在渲染地面塊等對象時,需要確保變換矩陣被正確應用,以便排序能夠反映實際的空間位置。
- 優化排序鍵計算:調整排序鍵的計算方式,確保在排序時能夠正確處理物體的空間位置和層級關系。
- 處理地面塊的排序問題:特別是地面塊的渲染順序問題,需要考慮它們的位置相對于其他物體的位置,確保它們處于正確的層次。
通過這些調整,可以提升排序的準確性,從而確保渲染的物體按預期顯示。
再次運行游戲,一切正常(除了角色上半身始終在最前)
問題的關鍵在于偏移量(bias)的處理方式不當。當前的偏移量并沒有很好地適應排序的需求,導致排序過程中未能準確反映物體的實際位置。這是導致渲染結果不正確的主要原因。
為了修正這個問題,需要在計算偏移量時加入更合理的邏輯。需要確保在排序過程中,物體的變換和偏移量能夠得到充分考慮,從而保證物體在渲染時能夠按照預期的順序正確地排列。
下一步的工作是,優化偏移量的計算方式,確保它能夠適應不同的情況,并有效地解決排序中的問題。此外,還需重新審視排序邏輯,確保它能夠正確地處理所有物體的空間位置和層次關系。
Q&A 問答環節
你能談談堆排序與快速排序的優劣嗎?或者解釋下四元數?
堆排序(Heap Sort)和快速排序(QuickSort)是兩種常見的排序算法,它們各自有優缺點。
堆排序(Heap Sort)
優點:
- 時間復雜度:堆排序的時間復雜度始終是O(n log n),即使在最壞情況下也是如此,因此它是一個時間復雜度比較穩定的排序算法。
- 不需要額外空間:堆排序是一個原地排序算法,不需要額外的空間,除了存儲堆的數據結構外,空間復雜度為O(1)。
- 穩定性:雖然堆排序本身不是穩定排序(即相等元素的相對順序可能會改變),但是在某些應用場景中可以根據需要修改堆排序來實現穩定性。
缺點:
- 性能表現不如快速排序:雖然堆排序的最壞情況是O(n log n),但在實際情況中,堆排序通常比快速排序慢,因為堆排序需要頻繁的比較和交換,且常常涉及到樹結構的調整。
- 不適合緩存友好性:堆排序使用的堆結構通常會導致緩存不友好,相比于快速排序的局部性優勢,它的內存訪問模式較差。
- 實現復雜度較高:堆排序的實現相對較為復雜,尤其是在實現堆的插入和刪除操作時。
快速排序(QuickSort)
優點:
- 平均性能:在大多數情況下,快速排序的時間復雜度是O(n log n),并且通常比堆排序和歸并排序更快。這是因為它的局部性較好,利用了分治法減少了數據的移動。
- 較少的交換操作:快速排序在某些情況下比其他排序算法(如堆排序)所需的交換操作要少,因此在實際應用中通常表現得更為高效。
- 空間復雜度低:快速排序的空間復雜度是O(log n),主要用于遞歸調用棧,這比堆排序的O(1)空間復雜度差,但在許多情況下仍然是可以接受的。
缺點:
- 最壞情況性能差:快速排序在最壞的情況下,時間復雜度可能退化為O(n^2),比如當數據已經基本有序時,或者選取的基準元素不好時。為避免這種情況,通常會采取隨機化基準或三數取中法來改善。
- 不穩定排序:快速排序不是穩定排序,意味著如果有多個相等的元素,它們的相對順序可能會改變。
- 遞歸開銷:快速排序是一個遞歸算法,盡管平均情況下性能較好,但在遞歸深度過大的情況下,可能會導致棧溢出問題。
總結:
- 堆排序適用于對最壞情況的性能有較高要求的場景,且不需要額外的空間,但通常比快速排序慢。
- 快速排序是平均情況下性能最佳的排序算法,適用于大多數排序場景,但需要處理最壞情況的性能問題,并且不穩定。
關于排序的其他算法:
在實際應用中,可以根據數據的不同特點選擇合適的排序算法。明天或者接下來的時間里,可能會討論如何改進當前的排序算法,并嘗試實現其他排序方法,以展示它們相對于冒泡排序的優劣。
你打算最終用什么排序算法替代冒泡排序?
在討論排序算法時,最終替代冒泡排序的選擇通常會傾向于使用具有 O(n log n) 時間復雜度的算法。這是因為 O(n log n) 是理論上可以達到的最優復雜度,而不像某些庫(比如 C 標準庫)中使用的 O(n2) 的排序算法那樣。雖然 O(n2) 算法在實踐中通常表現得更快,但如果考慮到最壞情況下的表現,O(n log n) 算法通常更為穩妥。
冒泡排序是最簡單的排序算法,它通過反復比較和交換相鄰的元素,直到整個數組有序。然而,冒泡排序在最壞情況下的時間復雜度為 O(n2),這意味著當數據量增大時,冒泡排序的效率會急劇下降。因此,如果考慮到數據量可能增加的情況下,使用 O(n log n) 的排序算法通常更有保障,避免了最壞情況的性能問題。
雖然 O(n2) 的算法在一些小數據集上可能表現得更快,但在處理較大的數據集時,O(n log n) 算法通常會更加高效,尤其是在數據量并不是特別大的情況下,這樣的算法表現仍然足夠好。
總的來說,選擇排序算法時,考慮到最壞情況的性能,通常更傾向于選擇 O(n log n) 算法,而不只是單純地依賴實踐中通常更快的 O(n2) 算法,尤其是在數據量并不是非常大的情況下,性能差異并不明顯。
冒泡排序是最簡單的排序嗎?我練習時做到選擇排序,所以我以為它才是最基礎的
在實踐中,使用選擇排序(Selection Sort)是為了追求簡化。選擇排序是一種非常簡單的排序算法,基本思路是每次從未排序的部分中選擇最小的元素,放到已排序部分的末尾。它的時間復雜度為 O(n2),因為每次尋找最小值時都需要遍歷未排序部分,因此其效率在處理大數據集時可能會變得非常低。
然而,盡管選擇排序簡單易懂,但其效率較低,尤其在處理大量數據時。它的時間復雜度是 O(n2),這意味著數據量較大時,它的性能會急劇下降。在某些小規模數據的排序中,選擇排序可能不會顯得太差,但當數據量增大時,可能就會影響到整體的排序速度。
選擇排序與冒泡排序類似,都是基于比較和交換的簡單排序算法。盡管兩者的工作原理相似,但選擇排序的優勢在于它減少了交換次數,而冒泡排序在每次比較時都可能發生交換。因此,選擇排序可能比冒泡排序稍微快一些,但總體來說,它們的時間復雜度都是 O(n2),對于大數據集來說都不夠高效。
綜上所述,選擇排序是一種簡單易實現的排序算法,但由于其時間復雜度較高,在數據量較大的情況下并不適用。在實際應用中,通常會考慮使用更高效的排序算法,如歸并排序(Merge Sort)或快速排序(Quick Sort)。
n2 排序在很多情況下其實更快嗎?
在排序算法的時間復雜度分析中,通常會關注最壞情況的復雜度。最壞情況是指算法在最糟糕的情況下執行的時間。例如,像歸并排序或快速排序這樣的算法,最壞情況下的時間復雜度是 O(n log n),而冒泡排序或選擇排序的最壞情況是 O(n2)。
然而,實際應用中,最壞情況并不總是發生。實際運行時,常常遇到的是“期望情況”或“常見情況”,也就是大多數情況下算法的表現。即使某個算法的最壞情況復雜度很高,如果在大多數實際情況中不會遇到最壞情況,它的效率可能反而更好。
這是因為,即使一個時間復雜度較高的算法,它的單次迭代成本可能比低復雜度算法更低。比如說,一個具有 O(n2) 時間復雜度的算法可能在每次迭代時都非常快,而一個具有 O(n log n) 的算法雖然理論上更高效,但每次迭代可能需要更多的計算資源。如果排序的數據量比較小,比如只有幾千個元素,那么低復雜度算法的優勢可能不會體現出來,反而 O(n2) 的算法由于常數因素較低,可能會運行得更快。
因此,理論上復雜度較低的算法(如 O(n log n))在大數據集上表現更好,但在實際應用中,考慮到數據量較小和常數開銷的影響,復雜度較高的算法(如 O(n2))可能在某些情況下反而更快。
這就是為什么實際排序中,某些看似最壞情況較差的算法在實際運行時,可能比復雜度較低的算法更高效。
是否可以使用二分插入排序,讓數組在插入時就已排序,這樣 RenderGroupToOutput
就不需要再排序了?
可以使用二分插入法將元素插入到正確的位置,這樣數組就會在整個過程中保持排序狀態,從而避免在渲染組輸出時再進行額外的排序操作。但這種方法并不總是最好的選擇,原因在于,如果我們一直這樣做,就會不斷地把渲染組輸出數組中的所有數據重新加載到緩存中。這是因為每次插入時,都會遍歷數組并找到合適的位置,因此需要不斷從緩存中拉取數據,這會帶來額外的性能開銷。
這種方式的問題在于它可能導致緩存的高頻率使用,這對于大數據量的排序或頻繁更新的數據來說,可能會引發性能瓶頸。雖然從理論上講,二分插入可以提高查找位置的效率,但它仍然需要移動大量的數據,這在實際應用中可能并不總是最優解。因此,雖然二分插入在某些情況下有效,但在考慮到緩存性能和操作復雜度時,可能需要尋找更合適的排序方法。
黑板講解:事后排序 vs 實時排序
無論是排序后再處理,還是邊排序邊處理,成本是相同的,因為排序本身就需要消耗相同的計算資源。即使采用二分插入排序(binary insertion),實際上它本質上也是一種排序方式。所以,無論是事后排序還是邊做邊排序,都會執行相同數量的工作,唯一的區別在于排序過程中所處理的元素量和信息量。
如果選擇在插入時排序,成本可能更高,因為在執行排序時,無法一次性看到所有的元素。因此,排序的過程會更加復雜。在排序過程中,可能需要多次來回調整元素的順序,這意味著會不斷地訪問和修改內存中的不同部分,這可能會導致緩存的高頻率使用,從而帶來性能上的額外開銷。
相比之下,選擇事后排序可以避免這種問題。事后排序時,可以將數據順序流式處理,每次只處理新的數據部分,而已經處理過的部分可以從緩存中移除,直到最后需要再次訪問時才重新加載。這樣可以有效地減少內存的頻繁訪問和緩存的占用,提升性能。
總的來說,雖然看起來邊插入邊排序可能節省了時間,但實際上在許多情況下,可能會因為頻繁訪問內存,導致性能下降。因此,在選擇是否使用邊排序邊插入時,需要特別小心,并進行實際測試,驗證這種方法是否真正帶來性能提升。
你以前做過類似這樣的項目嗎?
在進行項目時,通常是邊做邊編碼,而不是事先做準備。原因是這個項目的重點是展示如何解決編程中的問題。目的是讓觀眾看到如何面對編程挑戰并逐步解決它們,就像在實際工作中遇到問題一樣。平時工作時,雖然會有很多類似的編程經驗,但這個項目的具體內容是新的,尤其是像這個類似“塞爾達”的二維半3D探索類游戲,之前從未做過這樣的游戲。所以,盡管有很多經驗可以借鑒,面對這些新的挑戰時,還是會有很多全新的東西需要解決。
盡管如此,在進行一些代碼編寫時,還是會基于已有的游戲開發經驗,快速處理一些常見的編程問題。這也是為什么盡管是第一次嘗試這類游戲,依然能夠高效地處理一些編程任務。雖然每個項目的具體需求不同,但很多概念和技術是可以遷移到新的項目中去的,因此在編碼過程中,有些問題可以輕松解決。
在 Emacs 中如何關閉語法高亮但保留注釋和宏高亮?
在Emacs中,雖然可以禁用語法高亮,但仍保持注釋和宏的高亮,實際上并沒有完全禁用語法高亮。只是通過設置顏色使得語法高亮看起來更簡潔,減少了顏色的繁雜。通過修改Emacs配置文件中的set-face-attributes
,可以調整不同語法元素的顏色。例如,將字符串和常量的顏色設置為相同,類型和變量的顏色也設置為相同,從而讓它們看起來沒有太多顏色上的區分。這種方法并不是完全關閉語法高亮,而是讓語法高亮的效果更加簡化,不會太過干擾工作流。在設置時,雖然所有的語法高亮仍然在運行,但通過調整顏色,讓它們變得更加統一和平滑。
你的 Visual Studio 自定義主題有地方可以下載嗎?我很喜歡
使用的主題是Studio的自定義主題,實際上它是默認的深色主題,并對其做了一些簡單的調整。如果預定了游戲,相關的配置文件可以在游戲項目的misc
目錄下找到,文件名是handmade_hero_settings_msvc2013.vssettings
。這些設置文件可以直接用于項目中,從而對Emacs的主題進行自定義和調整。
添加vssettings