倉庫:
https://gitee.com/mrxiao_com/2d_game_7(已滿)
新倉庫:
https://gitee.com/mrxiao_com/2d_game_8
回顧并為今天的內容定下基調
我們接下來要繼續完成之前開始的工作,上周五開始的部分內容,雖然當時對最終效果還不太確定,但現在主要任務是讓渲染器的前端能夠正常工作,確保我們想要實現的所有Z軸處理效果能夠正確呈現。
目前我們已經實現了排序算法,精靈的排序基本上是正常工作的,但還沒有實現我們想要的分層切片機制。我們打算把精靈分到不同的堆棧里,這些堆棧是基于圖層的,并且希望這些圖層能夠相對獨立地發生變化,不受屏幕上其他內容的影響。因此,現在必須回到之前想完成的工作——確保Z軸排序從頭到腳都正確。
下一步要搞清楚這種分層切片的具體表現形式。因為現在我們有真正的游戲內容,比如怪物行為和渲染邏輯,我們希望這些代碼能夠說同一種“語言”,也就是說,讓實體能夠簡單明了地表達自己想要渲染什么、渲染在哪里,不希望有太多零碎的代碼引起問題。
雖然聽起來不算大事,但這實際上是大量的工作。我們需要對代碼進行組織和調整,把內容合理劃分成切片。第一步是確定怎么劃分這些切片,第二步是確定切片如何經過渲染系統和排序階段,最后是這些切片怎么通過后端渲染器(軟件渲染或者OpenGL)輸出。
這里面涉及很多復雜的決策,而且有很多相互競爭的需求。舉個簡單例子,現在排序主要是在渲染流程的最后完成,但我們可能需要把排序往上移一點,提前到游戲邏輯層處理。原因是我們在游戲代碼中已經知道每個切片包含的內容,沒必要把所有這些細節都暴露給渲染層。渲染層本身需要在軟件渲染和OpenGL兩套系統中都實現,并且在底層要保證簡潔高效,避免復雜難控的代碼。
此外,我們還要傳遞和存儲排序鍵(sort keys),這可能會造成資源浪費或者效率低下,也需要仔細權衡。
總的來說,這是一件非常復雜的事情,需要我們深入思考,制定合理的設計方案,才能正確地實現這些目標。接下來會花不少時間在這方面的工作上。
計劃實現一個功能:對精靈集合進行排序
接下來可能會從明天開始著手處理這個問題。今天的計劃是回到上周五查看的那段代碼,繼續實驗和探索如何實現這樣一個功能:讓一個角色或對象擁有一組已經預先排序好的精靈集合,然后讓這個集合整體參與排序,而不是把集合中的每個精靈單獨拿出來,通過各自的排序鍵逐個排序。
針對這個問題,我們有幾種不同的實現方式,但目前還不確定哪種方式最簡單可行。上次我們慢慢地開始探索這條思路。這里還涉及之前一次討論中的一些觀點,所以需要結合之前的想法一起考慮。總體來說,就是嘗試設計一個系統,使得預先排序的精靈組可以作為一個整體被排序,從而簡化排序流程和提高效率。
黑板討論:我們可以使用哪些方法來對精靈集合排序
我們在討論手動排序覆蓋的問題,考慮了幾種不同的處理方式。首先,有一種方案是將一組相關的精靈合并成一個整體的“盒子”,這樣排序器就可以簡單地把它們當成一個單元來處理,這樣做看起來挺簡單。但問題是,這樣合并的盒子可能會包含過多重疊區域,導致排序時出現不必要的復雜情況和錯誤,因為把多個元素合成一個大盒子,會讓排序區域變得很寬泛,包含了很多本不應該被包含的部分。
因此,更安全的做法可能是保持每個精靈都有自己的盒子,同時提供一種機制來告訴排序器它們應該被視為一組一起排序。也就是說,不是物理合并它們的空間區域,而是邏輯上把它們關聯起來。
另一種相對簡單的方式是依賴排序鍵(SortKey)機制。如果我們有一組精靈需要按預定順序排列,這些精靈可以賦予相同的排序鍵。比如,精靈A、B、C都給出完全相同的排序鍵,這樣排序系統在排序時,會根據它們在緩沖區中出現的順序來決定繪制順序。因為排序過程通常是穩定排序,順序相同的鍵值時會保持它們原來的排列順序。
這種方法相對簡單且已有部分實現。不同的精靈雖然有各自的屏幕區域,但排序鍵相同,就能保證它們保持在預期順序里。具體排序鍵的數值該怎么確定呢?如果是Y軸精靈(Y-sprite),排序鍵可以用其最大Z值和某個固定的Y值(可能是平均Y或者第一個Y坐標),這些數值可以由用戶指定。如果是Z軸精靈(Z-sprite),則可能需要更復雜的Y最小值和最大值區間來確定排序,這部分還不完全確定。
相較于之前考慮的“串聯鏈表(daisy chaining)”的復雜方案,這種統一排序鍵的方案看起來更簡單,更易于實現。把“串聯鏈表”功能移除后,可以讓這些相關的精靈按照正常順序依次加入排序器,再讓排序器依據排序鍵自動處理。
簡而言之,盡量減少系統中的復雜特殊處理邏輯,保持排序系統盡可能簡單,是有利于整體效率和可維護性的。即使這種方法在某些情況下可能略微降低效率,但減少復雜性帶來的整體優化和代碼簡潔性會帶來更大的好處。
總體來說,希望嘗試這種用相同排序鍵,通過緩沖區順序實現多精靈有序繪制的方式,簡化整個排序系統的設計,使它既滿足需求又不增加不必要的復雜度。
運行游戲,觀察當前的排序效果
當前運行的是一個標準的游戲版本。我們進行了測試,加載并運行了最新版本后,觀察游戲中角色的表現情況。現在關注的重點是主角的Sprite渲染順序。
當前的Sprite渲染邏輯中,角色在Y軸向后移動時,會被排序系統判斷為應該“更靠后”渲染,導致角色頭部被衣服遮擋。雖然這種Y軸深度排序在大多數情況下是正確的,但這并不適用于主角的頭部和身體之間的關系。
我們希望無論主角在Y軸上的位置如何變化,頭部始終渲染在衣服之上,避免出現視覺上的遮擋錯誤。換句話說,我們要調整渲染層級,讓頭部始終顯示在最上層,而不是被自動排序邏輯所影響。
當前的測試目的就是驗證是否能夠實現這個目標。我們計劃通過調整渲染優先級、分層邏輯等方式,確保主角的頭部在任何時候都不會被衣服遮擋,從而提升游戲的視覺一致性與表現力。
修改 game_render_group.cpp
:移除 PushRenderElement_()
中的 NewElement
,引入“寫回”機制,允許使用新的信息覆蓋已有的 SortKey
我們正在優化游戲渲染系統中的排序機制,目標是更好地控制不同Sprite(精靈)之間的渲染順序,特別是主角Sprite的部分遮擋問題。
目前在render group
中,我們開始初步設計新的排序方法。以往的邏輯是每個元素在渲染時都會生成一個新的排序元素(sort field),這在大多數情況下是有效的,但現在我們需要一種更加靈活的機制,以支持多個Sprite按照特定規則進行整體排序。
我們不再希望保留原有的通用排序邏輯,而是要引入“排序回寫(write-back)”的概念,也就是說,在將多個Sprite渲染后,我們需要用一個“聚合后的排序鍵(aggregate sort key)”來覆蓋原始的排序鍵。這就需要我們記錄下多個Sprite組合后的邊界信息,形成一個“聚合邊界(aggregate bound)”,供后續使用。
為此,我們計劃在render group
中新增一個字段,例如SpriteBound
或類似的變量,用來存儲聚合邊界信息。這個字段會在每次添加新Sprite時,通過一系列min
和max
操作來不斷更新,確保包含所有相關Sprite的完整范圍。
值得注意的是,我們不打算更改每個Sprite自身的屏幕區域(screen area)信息,這部分仍然保持獨立,確保它們各自的繪制區域正確分離。我們只聚合邊界信息,用來影響排序邏輯。
通過這種方式,在render group
中我們就能為多個Sprite提供統一的排序依據,使它們作為一個整體進行渲染排序。接下來,我們可能還會設計一個小型API,供調用者使用,方便地將Sprite打包為一個排序單元,從而提升整體的渲染控制能力和視覺一致性。
繼續在 game_render_group.cpp
中考慮:是否讓 PushBitmap()
返回一個值,以供 PushRenderElement_()
修改
我們正在進一步改進渲染排序機制,重點在于處理 push_bitmap
操作的行為變化。
以往在調用 push_bitmap
(推送一個Sprite貼圖)時,并不會返回任何值。但現在由于新的排序策略引入,我們需要讓它返回某些信息。原因在于:我們需要在推送完一系列Sprite之后,重新寫入(overwrite)它們的排序鍵(sort key)。這是因為直到我們完成多個Sprite的處理之后,我們才知道它們真正的聚合排序信息,進而能確定應該使用的最終排序鍵。
因此,我們必須引入一種機制,使調用者能夠在事后回頭修改這些已推送Sprite的排序信息。這也意味著我們要為每個 push_bitmap
操作返回某種可追蹤的數據結構,這樣在之后可以使用新的排序信息覆蓋已有內容。
解決這個問題的一種思路是,在開始推送Sprite之前,顯式調用一個“重置聚合邊界(reset sort bound)”的函數。這樣就能確保我們記錄的是這批Sprite的統一邊界。隨后,在完成這些Sprite的所有 push_bitmap
操作之后,我們便擁有一個聚合邊界(aggregate bound),其中包含我們想要用來計算最終排序鍵的數據。
核心問題就變成了:如何獲取之前已經推送過的Sprite的排序鍵?因為只有拿到這些鍵,我們才能將新的聚合排序鍵寫回去。
所以,下一步的設計方向是:
- 修改
push_bitmap
接口:返回可以被追蹤和訪問的結構,比如一個記錄了該Sprite在渲染隊列中的索引或句柄。 - 支持排序鍵覆蓋:引入一個機制,允許我們在事后使用新的排序鍵覆蓋這些記錄中對應的Sprite。
- 提供聚合邊界API:通過
reset_sort_bound
開始記錄一個新的聚合過程,結束后使用聚合結果統一更新一組Sprite的排序鍵。
這種設計將使得多個相關的Sprite能夠被當作一個整體進行排序,同時又不會破壞它們各自獨立的屏幕區域信息。整體目標是實現更精確的視覺層級控制,特別是在角色頭部與身體等部位之間,保持正確的渲染前后關系。
修改 game_entity.cpp
:在 UpdateAndRenderEntities()
中加入 BeginAggregateSortKey()
和 EndAggregateSortKey()
,標記哪些實體共享一個排序鍵
我們正在繼續完善渲染排序系統的結構,當前的重點是解決多個Sprite(例如一個角色的身體部位)之間如何共享統一的排序鍵(sort key)的問題。
以往每次調用 push_bitmap
(推送一個Sprite)時,并不會返回任何數據,也不追蹤這些調用的位置。但在當前的設計中,我們需要將這些Sprite當作一個整體來處理,因此必須追蹤這些操作,以便在最后用一個統一的排序鍵來覆蓋它們。也就是說,我們需要一種機制來記錄在某段時間內推送了哪些Sprite,并在它們全部被推送后統一更新它們的排序信息。
在查看push_bitmap
調用的位置時,我們注意到Sprite的推送邏輯已經被移到entity
系統中。在遍歷piece index
并進行Bitmap推送的地方,是一個合適的切入點來標記這些Sprite將共享同一個排序鍵。
但問題在于:如何標記和覆蓋這些排序鍵?這是當前遇到的主要挑戰。
我們不希望引入太多復雜的內存復制或指針操作,因此在考慮如何以簡單清晰的方式實現這一功能。設想如下:
- 開始和結束一個聚合排序階段:通過調用類似
begin_aggregate_sort_key()
和end_aggregate_sort_key()
的函數,標志著接下來的一批push_bitmap
調用將屬于同一個聚合排序單元。 - 在這個階段內推送的所有Sprite會記錄索引位置(例如在渲染緩沖區中的位置),而不是返回復雜的結構或指針。
- 在
end_aggregate_sort_key()
被調用時,我們將計算出一個聚合的排序邊界(aggregate bounds),并統一將該排序鍵寫入到之前記錄的Sprite中。
這一思路的好處是:
- 不需要外部干預,用戶只需明確開始和結束聚合區域即可;
- 內部結構高度有序,可以輕松基于索引范圍更新排序鍵;
- 不依賴額外指針,避免復雜的內存管理。
目前還有一個技術上的難點,就是聚合排序鍵的類型推斷。例如:
- 如果推入的是Y軸排序類型的Sprite(y-sprite),那么聚合后也應該保留為y-sprite;
- 但使用簡單的min-max方式進行聚合,可能意外將其“擴展”為Z軸類型,從而改變渲染邏輯;
因此,需要決定:
- 是否允許用戶在
end_aggregate_sort_key()
中手動提供排序類型信息; - 或者是否從某個代表性Sprite(比如第一個)中提取排序類型作為基準;
- 又或者直接在內部通過某種策略來保持最初的排序類型。
為簡化初期實現,我們將先做一個完全自動化版本,即用戶無需傳遞任何信息,系統會自動追蹤聚合邊界并更新排序鍵。后續如果發現自動推斷帶來問題,再考慮添加更細化的用戶控制接口。
總體而言,當前的設計逐漸清晰,將支持更強大而靈活的排序控制能力,尤其適用于多個Sprite需要共同排序但又要保持各自獨立屏幕區域的復雜渲染場景。
在 game_render_group.cpp
中實現 BeginAggregateSortKey()
和 EndAggregateSortKey()
我們目前正在實現一套聚合排序鍵(Aggregate Sort Key)機制,用于在渲染流程中統一管理一批被推入的 Sprite,使它們擁有一致的排序行為。核心目標是確保在調用 begin_aggregate_sort_key()
到 end_aggregate_sort_key()
之間的所有 Sprite 都共享一個統一的排序邊界(Sort Bound)。
以下是詳細設計與實現邏輯:
初始化聚合邊界(Aggregate Bound)
在 begin_aggregate_sort_key()
被調用時,我們首先需要初始化聚合邊界數據:
- 最小值設為最大可能值,最大值設為最小可能值。這樣可以確保后續任何一個 Sprite 推入時,它的邊界信息都會“擴展”當前聚合邊界,保證邊界最終包含所有 Sprite 的范圍。
- 這種初始化策略是通用技巧,可以確保聚合邊界在遍歷過程中被正確更新。
記錄 Sprite 推入的位置
我們在每次調用 push_bitmap
時,都會生成一條排序命令并推入渲染命令緩沖區。為了在 end_aggregate_sort_key()
時能夠回頭修改這些命令的排序鍵,我們必須知道它們在緩沖區中的確切位置。
解決方案:
- 在調用
begin_aggregate_sort_key()
時記錄當前緩沖區的起始位置,命名為first_aggregate_at
。 - 隨后每推入一個 Sprite,就自增一個聚合計數器
aggregate_count
。 - 在
end_aggregate_sort_key()
中,我們使用first_aggregate_at
和aggregate_count
計算出所有參與聚合的命令位置,然后統一修改它們的排序鍵為聚合邊界所生成的排序鍵。
結束聚合并覆蓋排序鍵
在 end_aggregate_sort_key()
中:
- 遍歷從
first_aggregate_at
開始的所有aggregate_count
條渲染命令。 - 每一條命令的排序鍵字段被替換為聚合邊界生成的統一排序鍵。
這個操作確保所有聚合內的 Sprite 都按照這個統一的排序邏輯參與最終渲染。
排序鍵計算中的注意事項
我們還需要考慮排序類型的保持,例如:
- 若起始 Sprite 是一個 Y-Sprite,則不希望聚合后變成 Z-Sprite。
- 使用邊界 Min/Max 來自動生成排序鍵時,可能會誤將 Y-Sprite 變為 Z-Sprite,因為邊界擴大了。
為此我們考慮兩種方案:
- 完全自動推斷:在聚合過程中自動提取最初 Sprite 的排序類型,并在結束時應用到統一排序鍵上;
- 用戶手動指定:允許
end_aggregate_sort_key()
接收一個明確的排序類型參數,避免誤推斷。
當前階段我們先采用全自動模式,在必要時再引入手動控制的選項。
技術實現細節
- 使用 push buffer 的索引計算 Sprite 的實際位置;
- 渲染命令結構通過已有字段(如
SortEntryAt
)與渲染組內的 Sprite 數組結構對齊,因此只需要計算偏移量即可精準訪問; - 所有聚合 Sprite 的排序鍵都通過統一聚合邊界生成,避免了冗余數據復制或不必要的內存操作;
- 編譯時也需調整 include 順序,確保相關的結構體如
render_group
和render_doh
在使用前已正確定義。
這一機制的設計簡潔、直接,避免了復雜的內存管理或返回值處理,同時具備高擴展性,為后續更高級的渲染排序策略(例如動態層級分組)打下了堅實基礎。我們下一步要處理的問題,是如何在結束聚合時確保類型標識的一致性。
讓 PushRenderElement_()
能夠正確設置 Y/Z 精靈的聚合邊界(AggregateBound
)
我們正在解決的問題是:在執行聚合排序(Aggregate Sort Key)操作的過程中,如何根據 Sprite 的類型(Y Sprite 或 Z Sprite)合理地處理它們的排序邊界,尤其是在多次 push_bitmap
的情況下正確擴展或設置聚合邊界信息。
基本邏輯流程
每當 push_bitmap
被調用,我們都會做以下幾件事:
-
遞增聚合計數器
aggregate_count
。 -
判斷當前是否為聚合的第一個 Sprite:
- 如果是第一個(
aggregate_count == 0
),則將其排序邊界直接復制給聚合邊界。 - 如果不是第一個,則進入邊界合并邏輯。
- 如果是第一個(
關鍵判斷:Y Sprite vs Z Sprite
在第一個 Sprite 被推入后,后續的每一個都要判斷是否應該合并進同一個聚合邊界,而這個判斷的核心在于 Sprite 類型:
- Y Sprite 通常根據 Y 坐標進行排序。
- Z Sprite 通常根據 Z 坐標進行排序。
我們必須確保所有被聚合的 Sprite 類型保持一致,否則排序邏輯會出錯。
我們引入一個輔助條件判斷機制來處理這個:
if (is_y_sprite) {// 聚合邏輯:更新 Y 邊界
} else {// 聚合邏輯:更新 Z 邊界
}
判斷是否是 Y Sprite 或 Z Sprite 的方法,我們已有代碼邏輯可用,例如通過 Sprite 的排序鍵中的某個位來判斷是否為 Y Sprite。
聚合邊界的處理策略
-
第一個 Sprite:
- 聚合邊界直接等于當前 Sprite 的排序邊界;
-
后續 Sprite:
- 如果是 Z Sprite:通常對 Z 方向的 min/max 值做合并(擴展范圍);
- 如果是 Y Sprite:則可能只對 YMin/YMax 做擴展,且不能改變 Sprite 類型(仍需標記為 Y Sprite);
- 類型一致的情況下,我們只需對邊界進行標準 min/max 擴展。
這個機制確保了聚合排序鍵能保持一致性并且不意外改變原始的排序邏輯。
總結當前策略的優勢
- 自動處理邏輯簡潔明確: 使用
aggregate_count == 0
判定首個 Sprite,后續擴展邊界; - 類型判斷嚴謹: 根據 Sprite 類型(Y/Z)決定如何合并邊界;
- 數據結構利用高效: 不需要引入額外的復雜數據結構或指針操作;
- 行為可擴展: 未來可添加如平均邊界值等策略,僅需在此邏輯中擴展分支判斷即可。
下一步要繼續完善的是聚合結束時是否允許外部指定排序類型,或者完全依賴首個 Sprite 類型作為基準并自動傳播,這還需要進一步驗證更復雜使用場景中的表現。
修改 game_render.cpp
:新增 IsYSprite()
函數用于判斷是否為 Y 向精靈
我們正在實現一個聚合排序系統,并進一步完善其中的邏輯處理,尤其是針對 Y Sprite 和 Z Sprite 的處理方式。
目標與背景
我們希望支持在一段時間內連續推送多個 Sprite(比如多個 tile、物體或圖層片段),在邏輯上把它們作為一個整體進行排序。因此需要構建一個聚合邊界(Aggregate Bound),這個邊界能夠代表所有 Sprite 的聯合排序信息。
但由于不同類型的 Sprite(Y Sprite 與 Z Sprite)在排序方式上是完全不同的,我們必須確保一個聚合內部的所有元素類型是統一的,否則無法正確排序。
主要處理邏輯細化
1. 判斷 Sprite 類型
我們通過排序鍵(Sort Key)中的某些字段是否相等來判斷 Sprite 的類型:
- 如果某兩個特定字段 不相等,那么這是一個 Z Sprite;
- 否則就是一個 Y Sprite。
于是我們實現了一個函數 is_z_sprite()
來封裝這個判斷邏輯。
2. 對聚合中的 Sprite 做類型一致性檢查
我們加入斷言邏輯:
- 若聚合中第一個 Sprite 是 Z Sprite,后續所有必須也是 Z Sprite;
- 若聚合中第一個 Sprite 是 Y Sprite,后續也必須是 Y Sprite;
- 否則立即觸發斷言,阻止繼續聚合不同類型的 Sprite。
這保證了聚合排序的合法性和行為一致性。
3. 聚合邊界更新方式的差異
-
Z Sprite 的聚合:
需要不斷擴展聚合邊界的 Z 值區間(例如 zMin/zMax),以涵蓋所有 Z Sprite 的實際可見排序范圍。 -
Y Sprite 的聚合:
當前策略下我們選擇不更新邊界,即保持第一個 Sprite 的 Y 值作為聚合邊界。這是因為 Y Sprite 的排序往往固定在起始值即可。
若后續需求復雜,可以考慮改為擴展 YMin/YMax。
聚合狀態的控制機制
為避免錯誤地在非聚合期間執行聚合邏輯,我們增加了一個標志變量:
bool aggregating;
在 Render Group 內部使用該變量表示當前是否處于聚合狀態。
控制邏輯:
-
調用
begin_aggregate_sort_key()
時:aggregating
必須為 false;- 設置為 true;
- 初始化聚合狀態;
- 斷言防止重復 begin;
-
調用
end_aggregate_sort_key()
時:aggregating
必須為 true;- 設置為 false;
- 寫入聚合鍵;
- 斷言防止漏掉 begin 或嵌套聚合;
這樣可以有效防止多次 begin 或 end 調用造成的狀態混亂,也防止在非聚合狀態下執行聚合邊界擴展邏輯。
當前實現的優勢
-
強一致性保證:
類型判斷+斷言確保不會錯誤聚合 Y 和 Z Sprite。 -
邏輯清晰且可擴展:
分支結構處理不同 Sprite 類型聚合行為,后續可擴展更多行為策略(如平均值聚合、特殊 Y 擴展等)。 -
狀態機制明確可靠:
使用aggregating
標志位嚴格限制聚合邊界的執行范圍,避免誤操作。 -
調試友好性高:
合理使用斷言讓調試過程中的邏輯錯誤快速暴露,減少潛在 bug。
下一步計劃
- 檢查調用聚合 API 的地方,確保
begin
和end
成對出現; - 驗證所有 Y/Z Sprite 的聚合邏輯在實際渲染流程中的正確性;
- 如有必要,進一步引入聚合類型枚舉或配置,以便支持更復雜的排序需求。
再次運行游戲,發現當前效果其實還不錯
現在我們需要確認之前實現的聚合排序邏輯是否真的生效。雖然結果乍一看好像不太對,但仔細觀察之后發現,可能其實是工作正常的,只是視覺表現讓我們誤以為出了問題。
觀察現象分析
我們看到屏幕上有三個元素順序發生了變化,本以為這是錯誤的表現。但之后發現這三個元素實際上并不是被作為一個聚合體傳遞的,它們來自兩個獨立的實體(entity),所以目前還不能用它們的排序變化來判斷聚合是否正確工作。
進一步檢查后發現軀干(torso)和披風(cape)在排序中并未發生變化,說明當前聚合機制可能已經部分起效,只是頭部(head)尚未被納入同一聚合序列中。
當前階段判斷
因此,在當前的測試中:
- 軀干與披風作為一個實體,沒有顯示出異常行為;
- 頭部暫時未聚合,表現正常;
- 暫無明顯排序錯誤或斷言觸發。
所以可以推測:雖然還沒有正式完成全部聚合邏輯的使用,但當前的聚合排序機制本身在底層已經能夠正常運作。
現有方案的價值與意義
目前的方案不僅基本可行,還有以下幾個額外優勢:
1. 結構清晰
聚合邏輯通過明確的 begin_aggregate_sort_key()
與 end_aggregate_sort_key()
來界定使用范圍,便于維護與閱讀。
2. 不強制依賴結構完整性
即便沒有把所有相關 Sprite(比如頭部)立即聚合進去,現有邏輯仍然穩健,不會導致渲染失敗或邏輯崩壞。
3. 允許逐步集成
我們可以先把軀干、披風等部分納入聚合,確認其工作機制,再逐步引入更多 Sprite,比如頭部。每一步都是可測試、可觀察的,降低了調試難度。
4. 支持動態可變組合
當我們在使用聚合機制時,不需要所有 Sprite 都一開始就被靜態組合,可以根據實際情況動態加入聚合區域。只要確保聚合邊界正確,系統就能正確處理。
后續改進方向
- 添加頭部 Sprite 到聚合結構中,驗證整個聚合單元能否被正確排序;
- 確保在所有需要聚合的地方都使用了
begin
和end
包裹; - 增加可視化調試機制,比如調試打印聚合索引、Sprite 類型,以確認實際聚合行為;
- 最終目標是:整個人物(頭部、軀干、披風等)作為一個聚合單元,始終保持視覺與邏輯排序一致性。
總結來說,目前的聚合排序系統已經基本具備了功能,并且具備良好的可擴展性和測試分層能力,雖然還未完全應用到所有部件,但已有良好基礎。接下來將逐步整合更多組件驗證完整性。
修改 game_render_group.h
:移除 render_group_entry_header
結構中的鏈式指針(daisy-chaining)
我們在實現聚合機制的過程中,發現了一種非常實用的“后門”方式。通過聚合排序鍵(aggregate key),我們不僅能夠在一段連續的 Sprite 推送中完成聚合,還可以保存這個聚合鍵,并在稍后的時刻直接將其他元素加入同一聚合組,只要它們使用相同的聚合鍵即可。這個設計思路讓我們的系統更靈活,不再依賴原來的順序式推送,增強了結構的可控性。
優化方向:刪除多余的鏈式結構
原先我們使用了一種“鏈式”的方式進行聚合管理,通過 next
字段將多個 render_entry
串聯起來,以便構建聚合單元。但在新的聚合鍵機制下,這種鏈式結構顯得不再必要,甚至變得多余與復雜。因此,我們決定徹底移除這部分功能以簡化系統。
具體修改操作
1. 移除 next
字段
在 render_entry_header
結構體中,曾經存在一個 next_offset
字段用于鏈式鏈接。現在我們將其徹底刪除。這樣,每個渲染條目只保存自己,不再擁有對下一個條目的鏈接信息。
2. 修改相關邏輯
所有使用 next_offset
或鏈表邏輯的地方都需要進行調整:
- 刪除相關偏移量的處理;
- 移除遍歷鏈表的循環;
- 確保每次只處理一個條目,不再期望自動進入下一個條目。
3. 清理游戲渲染路徑中的遺留代碼
在游戲渲染函數中,曾經存在一個基于 next_offset
的遍歷循環,這在鏈式結構下是合理的,但在當前邏輯下則會導致死循環或錯誤渲染。我們識別出這段代碼是個潛在的無限循環,并及時清除,避免運行錯誤。
當前優化的好處
- 簡化數據結構:去除了不再必要的字段,讓
render_entry
更輕量; - 提升邏輯清晰度:每個條目獨立存在,通過聚合鍵統一識別歸類,邏輯更直接;
- 提高靈活性:可以在不同時間、不同位置將元素歸入相同聚合組;
- 避免錯誤或復雜度膨脹:減少出錯點,降低維護成本。
后續可行方向
- 增加聚合鍵注冊和回收機制,避免重復或錯誤使用鍵值;
- 增強對聚合鍵渲染區域的可視化調試;
- 驗證“延遲加入聚合組”的實際使用效果,確認渲染輸出符合預期。
最終,我們通過刪除鏈式聚合結構、引入更靈活的聚合鍵機制,使聚合渲染系統變得更簡潔、高效、易維護,同時也更符合現代實時渲染系統對靈活性與穩定性的要求。
再次運行游戲,開始思考主角精靈的繪制順序問題
我們現在面臨的問題是:到底以什么順序來處理這些渲染對象。對此,我們采取了一個非常實際的策略:按照最利于渲染系統當前實現的順序來處理,也就是說,我們不會去強行定義某種理想順序,而是直接順從現有邏輯。
順序處理策略
我們決定:采用和對象被推入的順序相反的繪制順序。也就是說:
- 如果某個對象最后一個被加入到渲染隊列中,它就會第一個被繪制;
- 最先加入的則最后繪制;
- 這種策略可以簡化當前的渲染邏輯,減少對排序機制的額外負擔。
這種做法實際上在很多渲染隊列中非常常見,特別是在基于棧式結構的處理方式里,后入先出(LIFO)非常自然。
對當前結構的適配
在具體代碼邏輯中,我們查看了 world
相關的渲染部分,確認了當前渲染順序的處理模式。因此,決定不打亂已有順序,只要反向讀取就能滿足渲染需求:
- 不需要對聚合組內部再做額外排序;
- 渲染系統只需按原順序反向處理渲染指令;
- 保證聚合組整體的渲染順序依舊合理,且性能不會受影響。
好處總結
- 簡潔清晰:直接復用已有的推入順序,省去了額外排序邏輯;
- 性能友好:避免在渲染階段增加計算負擔;
- 行為可控:由于是固定規則,開發者對對象的渲染順序擁有明確的預期;
- 易于調試:出問題時能根據對象推入順序快速定位渲染順序問題。
后續優化可能
雖然目前我們使用的是反向順序策略,但未來如果渲染需求變化,比如加入了深度層級控制、多層視差背景、半透明圖層等復雜情況,可能需要引入:
- 顯式排序字段;
- 按需自定義渲染層級;
- 動態優先級調整機制。
但在當前架構下,這種“推入順序反向渲染”的方案既高效又簡單,足以滿足我們現階段的所有需求。我們可以基于這個規則繼續推進聚合渲染系統的其他部分構建與測試。
修改 game_world_mode.cpp
:調整 AddPlayer()
中 AddPiece()
的調用順序
我們接下來查看了角色(hero)創建的部分邏輯,尤其是在添加身體部件(piece)的時候的順序安排。通過檢查與處理代碼,我們確認了一件事情:
角色部件添加順序的控制
我們在構造角色時,對各個渲染片段(如頭部、軀干、披風等)的添加順序進行了檢查和整理:
- 每一個角色的部件都是按特定順序進行添加的;
- 例如:先添加軀干,再添加披風,最后添加頭部;
- 這個順序是有意設定的,以確保在渲染階段,繪制順序也是合理的(后添加者先繪制);
通過當前渲染系統中采用的“推入順序反向繪制”策略,這種添加順序就直接轉化為了視覺上的前后關系:
- 最后添加的頭部先繪制 → 顯示在最上層;
- 最先添加的軀干最后繪制 → 顯示在最底層。
這一點在我們實際測試時也得到了驗證,繪制出來的角色結構清晰,遮擋關系正確。
渲染結構與添加邏輯對齊
我們通過這種策略達成了一個非常重要的目標:
- 添加邏輯和渲染結構完全一致;
- 不需要額外的“排序”過程來處理遮擋順序;
- 簡化了代碼邏輯,也降低了維護成本。
效果驗證
從現有的運行效果來看:
- 渲染順序完全符合我們的預期;
- 部件之間的覆蓋和疊加關系是正確的;
- 沒有出現錯亂或者不一致的問題;
因此可以認為,這部分邏輯目前是穩定并且可靠的。
下一步方向
在角色的部件渲染順序確認無誤之后,我們就可以繼續推進:
- 實現更多角色、物體或場景的聚合渲染;
- 將這個邏輯通用于所有類似需要排序控制的渲染元素;
- 開始進一步測試聚合與非聚合對象之間的排序關系是否穩定;
我們當前的基礎框架已經奠定,后續就是逐步擴展與穩固整體的渲染系統結構。
修改 AddPiece()
調用的數值參數,不再使用難以理解的偏移值
我們接下來的問題,更多是實體系統層面的問題,而不僅僅是渲染排序本身。問題在于:如何表達一種非標準但又非常關鍵的規則,比如“如果頭部和身體同時存在,那么頭部始終應該繪制在身體之上”。
問題背景與癥狀
我們發現了一個渲染問題:
- 當角色頭部在 Z 值上“進入”身體的后方時,會出現一種視覺閃爍的偽影;
- 這是一種由于 Z 值排序引起的不一致視覺現象;
- 用戶看到的就是角色部件突然在前后之間“跳動”,非常不自然;
這個問題并不是簡單地靠現有排序邏輯可以解決的,因為:
- 我們的排序是通用性的;
- 而這個需求是特例化的邏輯關系:頭部必須蓋在身體上,不管 Z 值如何。
潛在的解決方向
我們需要在實體系統中找到一種方式來表達這類“特殊部件關系”:
-
一種可能是:添加一個渲染順序的附加規則表;
- 比如:當某個實體中包含“頭部”和“軀干”時,強制讓“頭部”渲染在“軀干”之上;
-
這種規則要能夠在 render group API 中傳遞下去,并影響最終的渲染排序邏輯;
-
這需要我們在設計 API 時,就考慮到這類語義性的關系排序需求。
雖然目前我們主要在處理排序系統的核心邏輯,但這類附加規則對 API 設計有重要影響,所以我們提前考慮是有必要的。
清理臨時 hack 和冗余代碼
同時,我們也注意到以前為了臨時解決排序問題添加的一些“亂七八糟”的 tweak:
- 有些實體被硬編碼了不合理的 Z 值,僅僅是為了讓排序“看起來”對;
- 現在由于我們有了更合理、語義化的排序方式,這些 hack 就變得多余了;
- 我們可以清除這些值,避免系統邏輯混亂或后續維護困難。
這樣,整個渲染系統就能更加“干凈”,不再依賴硬編碼技巧,而是真正基于邏輯語義來驅動繪制順序。
下一步計劃
- 回到實體系統,探索在哪一層可以表達這種“部件優先級”的規則;
- 確保這些規則能夠通過 API 向渲染層傳遞;
- 清理掉不再需要的臨時代碼;
- 保證排序系統的核心邏輯和特殊規則都能良好共存。
這不僅讓渲染行為更可控,也為后續擴展復雜角色(更多可組合部件)提供了基礎。
探討:如何明確表達一個 sprite(比如頭部)始終要繪制在另一個 sprite(比如身體)之上的意圖
我們現在面臨的問題,是如何表達一種部件之間的前后繪制關系約束,尤其是像“頭部必須始終繪制在身體前面”這種需求。這個問題的復雜之處在于:頭部和身體作為獨立實體存在,它們在渲染時并不知道彼此的存在,也無法預知彼此的渲染順序。
問題分析
-
實體之間相互獨立
- 頭部和身體是兩個不同的實體;
- 渲染順序依賴于它們被推入渲染隊列的時間順序;
- 我們當前沒有機制可以讓一個實體提前聲明“后續還會有另一個需要與我有關聯排序的實體”。
-
無法前向引用
- 如果身體先被推入渲染隊列,它無法告知渲染器:“等一下還會來一個頭部,那個要畫在我前面”;
- 如果頭部先進入渲染隊列,也無法修改身體的渲染順序;
- 渲染器的排序是“事后統一進行”的,但缺少跨實體排序依賴關系的輸入。
-
不希望強制規定推送順序
- 雖然可以通過讓調用方嚴格規定“先推身體再推頭”來解決部分問題;
- 但這會讓系統變得脆弱、不通用,稍有疏漏就出錯,屬于不可靠設計。
可能的解決方向
-
引入“排序依賴邊”機制
- 可以在渲染系統內部構建一個圖結構,表示實體之間的“繪制先后依賴關系”;
- 每個渲染實體可以顯式指定“我必須繪制在另一個實體之后(或之前)”;
- 在最終排序階段,基于這些依賴邊執行拓撲排序,生成最終繪制順序;
- 這種方式不依賴實體被推入渲染隊列的順序,允許前向或后向引用。
-
在 render group 中添加接口
-
我們可以在 render group 中增加一個方法,比如:
PushRenderOrderingDependency(entityA, entityB); // 表示 A 應該繪制在 B 之后
-
每個實體在推送時(或腦系統中),可以檢查自身是否有關聯的依賴;
-
如果有,就把該關系推送到渲染系統內部的依賴圖中。
-
-
在腦系統中分析依賴
- 比如在 Hero 的腦模塊中,我們知道頭部和身體是成對存在的;
- 可以在這里收集渲染順序關系;
- 把這些邏輯從渲染系統中剝離出來,保持渲染系統本身的“純粹性”。
對已有系統的影響
- 渲染系統中的 Sprite 圖構建階段,需要支持讀取并處理這些依賴邊;
- 排序邏輯要從純 Z 值或層級排序,轉變為有向圖排序;
- 清理掉原來依賴 Z 值的“排序 hack”和偽造位置數據;
- 所有這些操作應保持模塊化,避免在一般情況下影響性能。
當前策略總結
-
不建議強行規定推送順序,太脆弱;
-
不建議使用臨時 Z 值偏移做排序,維護成本高;
-
最佳方式是:
- 實體系統中收集排序依賴;
- 在 render group 中注冊這些關系;
- 渲染系統在排序階段執行拓撲排序處理。
這種機制不僅能解決當前“頭部-身體”問題,也為未來擴展更復雜角色結構打下基礎,比如裝備疊加、坐騎、多人動作交錯等場景。最終我們可以獲得一個語義清晰、易于維護的 2D 渲染排序系統。
黑板討論:手動指定排序邊緣(Manual Edge Specification)
我們考慮采用手動邊(Manual Edge)指定機制來解決某些實體之間的繪制順序問題,比如“頭部必須繪制在身體之前”這種情況。以下是該策略的完整思路與分析:
核心想法:構建手動排序邊 Sideband
- 在構造 Sprite 渲染圖(Edge Graph)時,額外附加一份手動邊的列表;
- 每條邊表示一個明確的“繪制優先關系”,例如:“Sprite A 應該在 Sprite B 前面繪制”;
- 這個“誰在誰前”是我們代碼中明確知道的,并且可由我們主動指定;
- 我們只需在前面那個 Sprite上指定即可,因為渲染系統處理的是“前在后之上”的邏輯。
邊的定義方式
-
使用的形式大致如下:
is_in_front_of(SpriteA, SpriteB)
-
表達的含義是:SpriteA 應該在 SpriteB 上方渲染;
-
因為邊是從“前面那一位”發出的,我們只需要關注那個前面的對象;
-
所以我們可以在“頭部”那一邊,記錄說“我要在身體前面”。
標識目標對象(如何找到要依附的對象)
- 遇到的難點是:我們不知道 SpriteB 是哪個對象,因為它可能稍后才出現;
- 解決方案是:為需要關聯的對象打上標記(Tag),并記錄 Tag 編號;
- 具體操作流程如下:
- 每個需要被引用的 Sprite(比如身體)被賦予一個 tag 編號,比如 tag = 3;
- 在頭部 Sprite 被推入渲染隊列時,附加一條邊信息:我在 tag 3 的前面;
- 渲染排序階段,遍歷 Sprite 列表時將所有帶 tag 的節點加入到對應 tag 的桶(bin)中;
- 隨后根據手動邊的描述,從前者找到對應 tag 后者,在圖上添加邊;
總結該機制的優勢
特性 | 描述 |
---|---|
靈活 | 可以處理前向或后向引用關系,無需控制實體推送順序 |
精確 | 明確指定“誰在誰前”,不依賴 Z 值或偏移等臨時手段 |
通用 | 不局限于頭部-身體關系,也可擴展到任意其他視覺層疊需求 |
兼容原有機制 | 不影響原有自動排序系統,僅在有特殊關系時才使用手動邊 |
實現細節建議
- 維護一個
manual_edges[]
列表,包含:{ front_tag, back_tag }; - 每個 Sprite 允許指定一個 tag,并注冊到
tag_bin[]
中; - 渲染排序圖構建時,先將所有節點加入圖;
- 遍歷
manual_edges[]
,根據 tag 在圖中查找目標節點,添加方向邊; - 執行拓撲排序,完成最終繪制順序計算。
這個機制設計雖然多了一些小的結構和步驟,但邏輯清晰、結構穩定、可擴展性強,是目前看來最合理、最通用的處理方式。我們決定采用這個方案,并作為后續處理實體層級繪制關系的標準模式。