游戲引擎學習第307天:排序組可視化

簡短談談直播編程的一些好處。

上次結束后,很多人都指出代碼中存在一個拼寫錯誤,因此這次我們一開始就知道有一個 bug 等待修復,省去了調試尋找錯誤的時間。
今天的任務就是修復這個已知 bug,然后繼續排查其他潛在的問題。如果短期內沒有更多 bug,我們就要著手解決我們算法中的 O(N2) 部分,因為這個性能瓶頸遲早會帶來問題。

當前的目標:

  • 修復已知拼寫錯誤;
  • 檢查其他渲染相關的 bug;
  • 若時間允許,則優化算法性能,減少不必要的平方復雜度處理邏輯。

接下來就正式開始今天的開發流程。

運行游戲并展示當前存在的排序 bug。

畫面中的問題不太容易看清,不過可以看到矩形渲染存在一些異常。問題其實在打開某些調試信息后會更明顯。我們打算先回到修改代碼之前的狀態,也就是在進行 bug 簡化和定位之前的原始狀態,這樣可以更清楚地看到錯誤在沒有任何修復之前表現得有多嚴重,尤其是排序方面的問題。

這樣做的目的是為了建立一個“基線”——即了解在當前代碼下排序錯誤的嚴重程度。之后我們再進行修復和優化時,就能更直觀地對比效果,評估修復是否有效,以及系統是否朝著更正確的方向發展。

簡而言之,現在的做法是:

  • 暫時回退對代碼的簡化和修復操作;
  • 恢復原始狀態,觀察 bug 在“未處理”情況下的完整表現;
  • 通過實際運行畫面獲取視覺反饋,建立一個判斷基準;
  • 為后續排序邏輯修復和驗證提供參考依據。
    在這里插入圖片描述

打開 game_world_mode.cpp,重新啟用 AddStandardRoom() 函數中被注釋掉的 else 分支,以便生成完整房間內容。

在添加標準房間時,我們之前把房間的所有瓦片都移除了,具體是將 else 分支中的邏輯清空了。現在如果把那段邏輯恢復,游戲中就會重新填充大量房間內容。

恢復后可以看到,僅僅是渲染一個房間的內容,就已經明顯出現了排序錯誤的問題。角色在游戲中會被一些物體(例如樹木)遮擋在后面,而這些物體原本應該出現在角色的后方。也就是說,圖層的前后渲染順序是錯誤的。

我們目前仍無法完全確認排序到底錯得有多嚴重,因為有些情況下傳遞給排序系統的數據本身就是不準確的。然而從屏幕上呈現出的效果來看,錯誤已經非常明顯了:

  • 樹木被渲染在角色前方;
  • 遮擋關系紊亂,透視不符合預期;
  • 這些錯誤在只渲染一個房間時就已經存在;
  • 如果有多個房間堆疊在一起,排序錯誤可能會更加嚴重。

這些現象表明,我們當前的渲染排序系統存在明顯問題,排序邏輯和輸入數據的準確性都需要進一步檢查和修復。我們必須在繼續開發之前解決這個渲染順序的核心錯誤。
在這里插入圖片描述

game_render_group.cpp 中修復一個錯誤:將 RectMinMax 改為 RectMinDim,用于 PushRect() 的調用。

這次出現的問題是由于一個小小的筆誤引起的。當我們在渲染組的代碼中,把使用的 RectMinMax 改為 RectMinDim 時,忘記將所有相關函數同步修改,具體是在 PushRect 的調用中仍然保留了舊的 RectMinMax,而這本應該也要改為 RectMinDim

回顧當初的修改過程,我們決定不再使用 RectMinMax,因為已經有了更合適的 RectMinDim,于是將某處替換為了新的函數,但遺漏了另一處調用,造成了不一致的問題。

由于 RectMinMax 接收的是一個最小點和一個最大點,而 RectMinDim 接收的是一個最小點和一個尺寸(寬高),所以如果傳遞了不正確的最大點,就可能導致矩形變得沒有意義。例如:

  • 如果最小點設得比最大點還大,就可能導致矩形變成負面積;
  • 這會讓該矩形在渲染過程中根本無法參與排序邏輯;
  • 或者更糟糕,參與排序時產生了非預期行為,比如錯亂的遮擋關系;
  • 實際上很多矩形可能都無法被識別或處理,因為它們從邏輯上就“不存在”。

因此,這個筆誤不僅使得渲染出來的圖形異常,還可能導致整個排序系統行為異常。這一錯誤解釋了為何部分圖形完全不顯示,或者出現在錯誤的位置。這也強調了代碼一致性的重要性,在對函數調用進行重構時必須全面同步修改相關調用點。
在這里插入圖片描述

再次運行游戲,觀察修復錯字后精靈排序的效果。

代碼在修復了筆誤之后重新運行,整體表現上確實有了一些改善。例如,一些場景元素像是臺階、地面等現在的排序已經變得正確,可以按照預期的遮擋關系來顯示。

不過,仍然存在一些奇怪的問題,并不是所有地方都已經正常排序。有幾個值得注意的現象我們逐個分析:

  1. 主角排序不正確
    主角的排序目前看起來是有問題的,但這并不會帶來太大困擾。我們現在還沒有對主角的排序方式做詳細設計。目前使用的是一些不精確的Z值,這不是未來最終的實現方式。
    預計會采用一種更精確的方式,比如利用拓撲排序,把主角的身體部分之間建立嚴格的前后關系:頭在披風上方,披風在軀干上方等等,而不需要使用Z值來強行控制順序。通過這樣的拓撲規則,我們能夠讓主角在所有情況下都能按照正確的層級顯示。

  2. 跳上臺階后出現渲染異常
    當跳躍到某些臺階或平臺上時,會發現圖像出現奇怪的重疊或交錯。這些情況的發生主要是因為相關圖形被渲染為在同一個Z平面上,且它們的矩形區域相互交叉。這種情況本身就是邏輯錯誤,不應該在正式游戲中出現。因此當前這些問題并不重要,不打算處理。

  3. 左側樹木排序錯誤
    真正令人擔憂的問題是屏幕左側的樹木排序。雖然其他區域的樹木顯示正常,但左側明顯存在排序錯亂的現象。樹的上半部分似乎被錯誤地繪制在了某些元素之下,或遮擋了本不該遮擋的內容。

    這個問題比較奇怪,因為同樣是樹,其他位置的就沒有出問題。由此推測,問題可能出現在排序算法在某些位置的判斷條件中,或者是數據輸入階段某些位置的圖形邊界不一致、坐標偏差等。

目前,雖然部分問題依舊存在,但已有初步判斷哪些是暫時可以忽略的(如主角或重疊圖形的問題),哪些是需要繼續深入排查的(如樹木的排序錯亂)。接下來的計劃應該會著重分析左側樹木的排序邏輯,以查清其不一致的根本原因。
在這里插入圖片描述

運行游戲,觀察剖析器排序仍存在問題。

我們觀察到,目前某些元素依然顯示錯誤,具體來說,有一些應該屬于Z排序精靈(Z sprites)的圖像卻沒有按照正確的順序渲染出來。

這些圖形本應擁有非常高的Z值,也就是說,在渲染排序中應該始終位于其他元素之上,因為它們代表的是場景中離相機最遠或最前景的對象。然而,在當前的畫面中,這些圖像卻沒有處于最上層,表明它們的排序出現了問題。

進一步觀察,在某些圖像顯示或被激活的時候,重疊關系變得正確,這暗示了某些圖像之間并沒有真正參與排序過程。也就是說,部分對象似乎根本沒有和其他對象發生比較排序。這種情況可能說明這些對象沒有被正確地插入到排序系統中,或者排序圖(sort graph)中沒有正確構建這些圖像之間的依賴邊關系,導致它們在拓撲排序中被孤立。

我們推測出現這種問題的可能原因有:

  1. 某些Z精靈的Z值雖然應該很大,但實際上可能因為某些錯誤,被設定成了錯誤或默認值;
  2. 排序圖在構建時未能識別這些對象之間應當存在遮擋或前后關系;
  3. 排序函數在比較這些對象時未能觸發預期的邏輯,導致排序無效;
  4. 這些對象被錯誤地標記或遺漏,未參與最終的圖像合成與顯示流程。

后續需要驗證這些精靈對象是否確實是Z精靈,并確認它們的Z值是否正確傳入了排序系統,同時也要進一步檢查排序系統是否對它們進行了正確的拓撲比較和連接。總的來說,這種缺失排序連接關系的現象是導致排序錯誤的關鍵癥結之一。
在這里插入圖片描述

考慮增強調試能力,便于觀察排序邏輯是如何工作的。

我們目前遇到的排序問題,歸根結底是由于缺乏有效的可視化手段,無法直觀了解排序系統的實際運行情況。

首先,我們在屏幕上看到了一些明顯排序錯誤的圖像對象,比如左側一列樹木,它們并未按照應有的順序從下至上正確遮擋。雖然我們可以觀察到結果錯誤,但我們無法確認具體是哪些部分出了問題。可能的原因包括對象的屏幕空間邊界(Bounds)設置錯誤、對象沒有被納入同一個排序分組(Sort Group)、或者排序依賴關系鏈(即圖中的邊)未能正確建立。

為了解決這個問題,我們決定引入一種新的可視化機制,目標是能夠:

  1. 繪制每個圖像對象的屏幕空間邊界:當前我們無法判斷某個對象在屏幕上的具體邊界是否正確。通過在渲染器中添加調試繪制功能,我們可以看到實際用于排序判斷的矩形范圍是否合理,從而發現問題所在,比如矩形是否太小、位置是否錯位,是否與視覺上對象位置不符等。

  2. 可視化排序分組(Sort Group):我們想看到哪些對象是被分在同一個排序組中的。例如屏幕左側的一整列樹木,它們應該形成一個連續的排序鏈,從下到上每一個都要知道自己遮擋上面的那個。如果這些樹之間沒有形成連續的依賴關系(比如A遮擋B,B遮擋C),就會出現排序斷裂,導致渲染錯誤。我們計劃讓調試顯示能直觀表示哪些對象屬于同一個分組,或者以顏色、編號等方式區分不同的組。

  3. 輔助判定排序鏈斷裂問題:沒有這些可視化機制,我們只能從“結果錯誤”間接推測問題來源,缺乏對“結構錯誤”的直觀判斷。通過顯示排序組結構,可以快速識別斷鏈、不完整分組、遺漏鏈接等邏輯問題。

我們預計這套可視化機制一旦建成,不僅可以幫助排查當前的問題,也將對未來進一步完善拓撲排序邏輯提供重要幫助。因為排序錯誤如果僅憑最終圖像難以溯源,而這套機制將成為我們定位排序邏輯問題的核心工具。我們將從實現邊界繪制開始,逐步加入排序組高亮顯示等功能。

修改 game_entity.cpp,禁用可行走區域的體積高亮顯示與輪廓渲染(PushRectOutline)。

我們準備開始對排序的可視化進行改進。首先要做的是關閉部分實體的高亮顯示,因為當前屏幕上高亮的元素太多,視覺上顯得雜亂,影響對排序調試的觀察效果。具體來說:

  • 關閉碰撞體積的高亮顯示,這樣就不會再突出顯示這些碰撞區域,減少干擾。
  • 關閉遍歷對象(traversable)的矩形邊框輪廓繪制,因為這部分顯示對當前調試沒有必要,甚至可以考慮徹底移除。

這樣一來,屏幕上就不會有過多矩形形狀的疊加,界面會更加簡潔,方便專注于觀察排序相關的可視化內容。我們先從世界模式(World Mode)的實體渲染部分著手,逐步減少無關的視覺元素,為后續繪制精確的排序邊界和分組效果做準備。
在這里插入圖片描述

在這里插入圖片描述

再次運行游戲,注意到渲染速度明顯提升,同時樹木的排序也正確了。

關閉了部分實體高亮后,界面變得清爽不少,運行速度也明顯提升了。推測原因是因為減少了高亮的矩形和分組,導致需要排序的元素減少,從而降低了排序算法中n2復雜度的開銷,但具體影響還有待確認。

觀察當前狀態發現,移除這些高亮之后,有些元素反而能正確排序了,這點有些奇怪,不太清楚為什么會有這么明顯的影響。尤其是在操作選擇光標時,排序表現出很怪異的現象。

發現只要加入幾個層次分明的Z軸精靈(z sprites),排序順序就會和只有一個平面的海面精靈時完全不同,說明排序邏輯受這些分層元素影響很大。這種現象很反常,所以打算進一步收集數據,嘗試在已有房間上再疊加一個房間,以觀察排序表現如何變化。

修改 game_world_mode.cpp 中的 AddStandardRoom() 函數,添加一個上層房間。

我們決定在現有場景中再疊加一個標準房間,目的是觀察新增房間中的精靈(sprites)在排序時的表現,看看多層疊加對排序系統會產生什么影響。我們準備快速查看這個變化,然后再決定是否保留或移除這個疊加房間。
在這里插入圖片描述

運行游戲,發現排序實際上還是不正確。

我們觀察到疊加房間后,排序結果依然錯誤,有些精靈顯示在上方,有些卻顯示在下方,說明排序存在比較根本性的問題。雖然界面上有些輔助顯示條可以幫助定位問題,但現在看來這些輔助工具并不是特別必要,當然如果需要排查那個bug,還是可以重新啟用它們。總之,排序問題依舊明顯,需要繼續深入調試。

修改 game_opengl.cpp,讓 OpenGLRenderCommands() 繪制精靈邊界框。

我們計劃從渲染器入手,主要是在OpenGL渲染部分進行修改,因為這是現在最常用的渲染方式。目標是在渲染過程中增加對排序組和精靈邊界的可視化展示。幸運的是,精靈邊界的數據是有序的,可以通過相同的索引訪問其他相關數據,盡管實際上不一定非得依賴索引對應。

具體做法是在繪制完所有內容后,再增加一個繪制步驟,專門繪制所有精靈的邊界。因為我們已經有了這些邊界數據,理論上沒有什么障礙。實現時,可以通過一個循環遍歷所有邊界數據,然后繪制出來。

在代碼中,使用了“sort sprite bound”結構,里面包含了邊界的詳細信息,比如邊緣情況、哪些精靈在前哪些在后等,這些信息對應的是索引到精靈邊界數組。基于這些數據,我們可以繪制出很多有用的信息,幫助理解排序的具體情況,輔助后續調試和問題定位。

game_render.hsprite_flag 枚舉中添加 Sprite_DebugBox 標志位。

在已有的“visited”和“drawn”標記基礎上,可以新增一個“sprite debug box”功能,用來實現類似遞歸的邊界繪制效果。通過這個功能,可以展示不同連接組之間的路徑關系,幫助我們直觀地看到排序組內部的連接情況和層級結構。這種可視化有助于更深入理解排序過程中的復雜關聯,從而更有效地調試和優化排序邏輯。
在這里插入圖片描述

在這里插入圖片描述

繼續完善 OpenGLRenderCommands() 的實現。

實現這個功能相對簡單,主要是寫一個遞歸繪制邊界的函數。這個函數會接受一個邊界對象,同時傳遞該邊界的索引,以便在遞歸調用時能清楚知道當前處理的是哪個邊界。通過遞歸調用,可以逐步繪制連接的排序邊界,幫助我們更清晰地理解不同排序組之間的關系。這個方法借鑒了之前的做法,但增加了傳遞邊界索引的參數,使調試時能更好地追蹤和區分不同的調用層級。
在這里插入圖片描述

game_opengl.cpp 中引入遞歸函數 OpenGLDrawBoundsRecursive() 用于繪制邊界框。

我們打算實現一個遞歸繪制邊界的功能,首先在調用該函數時,會傳入當前邊界的索引,以便能在遞歸中跟蹤訪問狀態。每次訪問一個邊界時,會設置一個調試標志,避免重復繪制。然后會遍歷與當前邊界相連的所有邊,按照從前到后的順序遞歸調用繪制函數。

在繪制時,我們計劃先畫出每個精靈的屏幕邊界區域,這樣可以直觀地看到我們為每個精靈指定的邊界范圍。同時,還想在這些邊界之間畫出連接線,連接不同邊界的中心點,方便觀察它們之間的排序關系。

為了更清晰地展示排序組的結構,我們打算給同一連通排序組中的所有邊界用同一種顏色繪制。為此,準備使用一個系統性的調試顏色表,這個顏色表是共享的,方便團隊其他成員也能用到它進行調試和可視化。

整體上,這個方案能夠幫助我們更直觀地理解和調試排序邊界及其連接關系,從而更好地定位排序中的問題。
在這里插入圖片描述

考慮還是鍵一個game_shared.h把減少文件直接的相互依賴,放一些公共的common(公共)

在這里插入圖片描述

修改 OpenGLRenderCommands(),讓不同邊界框使用不同顏色。

我們計劃在繪制遞歸邊界時,為每個排序組分配不同的顏色,具體做法是給每個組傳入一個顏色參數。整個繪制過程會用線條來表現,遞歸調用時用一個開始-結束的繪制塊包裹。為了區分不同組,每當繪制新的組時,從調試顏色表中依次取顏色。

為了避免給已經繪制過的邊界重復分配顏色,我們會先檢查該邊界的標志位,只有未被標記的邊界才會進入繪制并分配顏色。這樣就不會因為索引順序而浪費顏色資源,保證顏色表被合理使用,從而使顏色分配更有意義。

在實現中遇到了一些C++語言的細節難點,比如變量未聲明、函數參數不匹配等問題,也發現了一些模塊間的依賴和聲明順序問題,進行了調整,比如前向聲明相關數據結構。關于顏色傳遞,最后發現其實不用顯式傳遞顏色參數,因為可以直接調用OpenGL設置顏色,所有線條繪制統一顏色即可。

整體流程就是先遍歷所有邊界,根據標志位判斷是否繪制新組,分配顏色后遞歸繪制對應邊界和連接線條,過程中處理好C++代碼中的類型聲明和參數匹配問題,確保繪制邏輯順暢。這樣能直觀展現排序組及其關系,輔助調試排序問題。
在這里插入圖片描述

運行游戲,發現沒有任何效果。

目前雖然已經繪制了彩色的OpenGL框架,但由于還沒有真正發出繪制線條的指令,所以畫面上暫時不會有變化,仍然只顯示之前的內容,這樣暫時足夠用于觀察和調試。
在這里插入圖片描述

OpenGLRenderCommands() 中調用 glDisable(GL_TEXTURE_2D) 來關閉紋理,在繪制邊界框之前執行。

準備開始繪制線條時,需要回顧一下OpenGL的繪制流程。繪制時要先啟動一個圖元類型(比如三角形),然后設置頂點指針。這里繪制線條會遇到一個問題,就是默認情況下紋理功能是開啟的(通常是啟用的texture 2D),這會導致繪制的線條被紋理影響。因此,在繪制線條之前必須關閉紋理功能,確保線條不會被任何紋理污染。關閉紋理后再進行線條繪制,這樣才能正確顯示純色的調試線條。
在這里插入圖片描述

在這里插入圖片描述

OpenGLDrawBoundsRecursive() 中添加功能:在兩個精靈邊界中心之間繪制線條。

在OpenGL渲染中,只需提交一些實際頂點即可繪制線條。這里有幾種線條類型需要繪制:

首先是從當前元素指向其“排序鄰居”的線條。可以將這個鄰居稱為“neighbor”,即當前的bound加上“behind edge”的索引得到。為繪制這條線,需獲取當前元素和鄰居的屏幕空間邊界(screen area)的中心點。可以調用 get_center 函數來獲取這些矩形的中心點,如果函數未實現,可以在矩形結構中添加該方法。

獲取了兩個中心點后,就能在它們之間繪制一條線。由于這些線都使用相同顏色進行繪制,因此實際提交頂點的時機無所謂,只要在適當遞歸調用后提交就可以了。繪制時,只需提交兩個頂點,一個是當前中心點,一個是鄰居的中心點。

在繪制完連接線后,還需要繪制代表每個元素的矩形框。這個過程是通過獲取每個元素的 screen_area 的最小和最大點(min 和 max)來實現的,然后用這四個點繪制一個矩形輪廓線。系統中應該已經有一個用于繪制矩形輪廓的OpenGL函數,可以調用它完成這一步。

總結:

  • 關閉紋理繪制以避免干擾;
  • 遍歷每個sprite bound,遞歸繪制與其相連的其他bounds;
  • 每個group用不同顏色繪制;
  • 每個bound繪制其矩形框;
  • 每兩個相連的bounds之間繪制一條連接線;
  • 所有線和框都以非紋理方式純色繪制,方便調試排序關系和邊界結構。
    在這里插入圖片描述

引入 OpenGLLineVertices() 函數用于繪制線條。

接下來可以增加一個用于繪制線框矩形的功能,比如叫做 OpenGLLineVertices。這個功能的作用與之前繪制實心矩形類似,不過不是繪制填充的三角形,而是沿著矩形的邊界畫線,也就是只畫矩形的輪廓。

具體實現步驟如下:

  • 從矩形的左上角(min)開始;
  • 向右繪制至右上角(min.x 到 max.x);
  • 向下繪制至右下角(min.y 到 max.y);
  • 向左繪制至左下角(max.x 到 min.x);
  • 再向上返回起點(max.y 到 min.y);

這樣就順時針或逆時針地將矩形的四條邊都繪制出來了,共需畫四條線段。

每條邊都由兩個頂點構成,總共需要提交八個頂點來繪制出這個線框矩形。這種方式適合用于調試或可視化 sprite 的邊界信息,因為線框矩形不會遮擋其他圖形內容,可以清晰地展現出各個元素的占位情況和層級關系。

這個功能可以與之前提到的遞歸遍歷排序節點、繪制連線的流程結合使用,從而全面可視化 sprite 系統中各個對象的空間關系和排序邊界。
在這里插入圖片描述

OpenGLDrawBoundsRecursive() 中調用 OpenGLLineVertices() 進行繪制。

現在我們可以簡單地輸出矩形的頂點信息來繪制線框,只需要針對這個矩形調用對應的頂點輸出函數就可以了。希望一切順利。

不過這時候出現了一個小問題:嘗試訪問 screen_area 的時候發現了一個標識符找不到的錯誤。原來是 get_min 函數中用的是 mid_corner,而不是預期中的 min,至于為什么最初取名為 mid_corner,已經記不清了,但當時可能是覺得這樣更準確或更符合語義。

無論如何,解決這個問題的方法就是把調用處做相應調整,改成使用正確的命名(即使用 mid_corner 來訪問對應信息),這樣就能順利地提取矩形邊界并繼續完成線框繪制的工作了。

整個流程的目的,是為了在不遮擋場景的前提下,通過簡單的線段清晰可視化每一個 sprite 的屏幕占用范圍,以及它們之間的連接關系。這在調試復雜排序和遮擋邏輯時是非常有幫助的。
在這里插入圖片描述

在這里插入圖片描述

運行游戲,看到新的調試可視化效果。

為什么沒有顯示呢

在這里插入圖片描述

發現是GL_LINES的原因

在這里插入圖片描述

在這里插入圖片描述

https://registry.khronos.org/OpenGL-Refpages/gl4/html/glPolygonMode.xhtml
glPolygonMode() 函數中的 GL_LINE 模式,而不是 glBegin(GL_LINE)(后者是不存在的)
在這里插入圖片描述

在這里插入圖片描述

畫的不對

在這里插入圖片描述

在這里插入圖片描述

只留hero看看

在這里插入圖片描述

畫的hero周邊的線不怎么對

在這里插入圖片描述

ScreenArea 中的值貌似不對

在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

game_world_mode.cpp 中暫時禁用 UpdateAndRenderWorld() 中的清屏操作(Clear())。

我們決定暫時去掉清屏(clear screen)操作,以驗證當前渲染問題是否與之有關。

因為之前在屏幕上看到有些矩形區域的渲染結果明顯不正確,看起來像是“無效”或“雜亂”的區域。這讓我們懷疑是否所有圖形都被錯誤地連接到了背景或者清屏圖形對象上,從而導致視覺上的錯亂。

為此我們嘗試暫時去掉清屏邏輯,看看是否所有多余的連接線都會消失,這樣就能確認是否是由于這些區域始終連向清屏造成的。如果結果確實如此,那么這些異常現象并不表示真正的邏輯錯誤,而只是調試輔助圖像渲染中的可視化誤導。

這一步的核心目的是排查邊界框的遞歸連接繪制時是否與背景或清屏圖層存在不當的依賴。我們希望通過排除清屏圖層驗證是否真的是所有錯誤連線都是因為它在場。

簡單來說,我們采用“移除清屏,再觀察結果”的方式來調試視覺問題,確保數據結構和可視化邏輯之間的對應關系沒有出錯。
在這里插入圖片描述

運行游戲,觀察未清屏情況下的效果,并思考更合理的清屏時機。

我們確認了一個重要問題:之前所有的元素都被錯誤地歸并進一個巨大的分組,原因就在于“清屏(clear)”操作本身。因為清屏的圖像覆蓋了整個屏幕,所以其他所有元素在排序時都被強制連接到了這個清屏對象,導致它們在調試輔助圖形中被顯示為同屬一個巨大連通區域。

現在已經明確這是由清屏造成的行為之后,我們對當前的分組檢測結果感到比較滿意,看起來也相對正常合理了。

但是,保留清屏作為一個普通圖層參與排序操作其實是沒有必要的。因為清屏總是在所有渲染動作之前發生,它的存在本就不需要作為排序流程的一部分。如果繼續將它當作一個標準渲染項參與排序,會導致整個排序系統無法正確區分不同元素的邏輯關系,使所有圖層都間接連接為一個統一組,這是沒有意義的。

于是我們開始思考如何“合理地”將清屏邏輯從整個排序流程中抽離出去。有一種最直接的方式是把清屏設置為一個獨立的狀態設置項,比如只設定一個清屏顏色參數,然后在渲染流程開始時自動清除顏色緩沖,而不讓它出現在排序圖層隊列中。

我們之所以對這種方式稍有保留,是擔心未來渲染系統可能會擴展到多渲染目標(Render Target)的場景,比如有多個屏幕區域、不同緩沖區需要分別清理的情況。但仔細一想,這種擴展也可以通過設定“渲染目標 + 清屏顏色”組合的方式來處理,因此清屏操作作為單獨管理是合理且不會帶來問題的。

總的來說,我們可以將清屏操作獨立管理,從排序流程中剝離出去。這樣可以避免無意義的全局連接,提升調試可讀性,同時也不會限制未來系統的拓展性。清屏作為啟動初始化動作,并不需要成為排序邏輯的參與者,因此將其從中剔除是當前正確的優化決策。
在這里插入圖片描述

在這里插入圖片描述

修改 OpenGLRenderCommands(),直接在這里調用 glClear(),并考慮永久將清屏邏輯從 UpdateAndRenderWorld() 中移除。

我們考慮了一種更合適的做法來處理清屏(Clear)的問題:既然這是游戲內部的 OpenGL 處理,我們可以把清屏操作直接歸為“內部渲染指令”,而不是像現在這樣把它作為一種排序元素參與整個圖層排序。

具體而言,我們可以在一個合適的地方,直接使用 glClearColor 函數來設定清屏的顏色,然后在渲染流程開始時調用一次 glClear,就完成了背景清除的任務。這樣可以完全繞開將清屏邏輯納入圖層排序系統的需求。

更進一步講,我們可以在渲染流程的初始化階段,像如下方式處理:

glClearColor(R, G, B, A);
glClear(GL_COLOR_BUFFER_BIT);

這意味著清屏只執行一次,作為整個渲染管線的“背景準備步驟”,不再作為一個實際渲染元素出現。

這樣的做法有幾個明顯的好處:

  1. 避免干擾圖層排序邏輯:清屏元素覆蓋整個屏幕,如果參與排序,會讓其他所有圖層都與它發生“連接”,形成無意義的巨大連通塊,干擾調試輔助圖形的判讀。
  2. 符合渲染流程直覺:清屏本質是“初始化畫布”,和具體的圖層繪制是兩件事情,不應放在同一處理邏輯中。
  3. 提高可維護性與可擴展性:未來即使有多個渲染目標(Render Targets)或不同的清屏策略,也可以通過集中管理清屏狀態,而不污染排序流程。

綜上,我們決定將清屏作為一個獨立處理的 OpenGL 狀態設定,并在適當位置調用執行,而不是像以前那樣把它放進排序系統中去參與分組或連通性判斷。這種方式更加合理、簡潔,并為后續功能擴展留下了良好的基礎。
在這里插入圖片描述

在這里插入圖片描述

“讓我們開啟令人驚艷的粉紅色背景”α。

game_platform.hgame_render_commands 結構體中添加 ClearColor 字段,同時修改屏幕清除邏輯。

我們決定對平臺系統中的渲染命令結構進行調整。為此,我們在原有傳遞內容的基礎上,新增了一個 clear color(清屏顏色)的字段。這么做的目的,是為了讓渲染系統(如 OpenGL)可以直接讀取這個字段,作為清屏時使用的顏色,而不再通過單獨的“清屏”命令來實現。

接著我們取消了原本在渲染命令隊列中定義的 RenderEntryType_Clear 類型。也就是說,清屏操作不再通過向命令隊列中壓入一條專門的清屏命令來完成。而是在執行清屏的地方,直接從命令結構中讀取 clear color 字段的值來執行清屏動作。這樣一來,系統不需要為清屏操作維護單獨的命令數據,清理了邏輯結構。

隨后,在原本用于生成清屏命令的代碼部分,我們取消了向命令緩沖區壓入數據的操作,轉而直接在命令結構中設置 clear color 值。如此一來,整個渲染流程中不再依賴清屏命令對象,而是通過狀態值進行控制。

同時,雖然我們清除了與清屏相關的命令部分,但仍然保留了原有的 screen area(屏幕區域)數據結構,出于其他用途的考慮,暫時不對這部分結構做修改。

在調整過程中,我們還處理了一個類型轉換相關的問題:系統中某處需要從 u32 類型轉換到 real32 類型。為此,我們在渲染命令結構中為 clear color 設置了初始默認值,比如黑色(black),確保系統在未顯式設置時也有安全默認值可用。

最后,為保持渲染系統一致性,我們還移除了軟件渲染器中與原清屏命令相關的處理邏輯,避免冗余,同時確保整個渲染系統的行為仍然符合預期。通過以上修改,我們完成了從基于命令的清屏方式向基于狀態字段的清屏機制的遷移,簡化了渲染命令管理流程,并提升了結構清晰度和執行效率。
在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

運行游戲,觀察當前渲染結果。

我們現在已經成功恢復了 clear color(清屏顏色)的設置功能,這個功能重新回到了渲染流程中,說明相關機制已經恢復正常。

接著,我們觀察到了渲染命令的排序邏輯現在按預期進行了優化:各項命令被正確地分組和排序,在邏輯上被劃分為了本地的、獨立的小組,這種排序方式符合我們之前的目標。

我們還注意到,目前的排序方式是“單一排序”(single sort),這是我們希望實現的形式。它避免了原本由于復雜依賴關系帶來的排序循環問題。之前排序操作中可能存在多個依賴回路,導致排序無法有效進行,性能受限。而現在這種簡化后的排序機制更容易打破循環,提高了排序效率。

為了更準確地評估當前系統的排序性能,我們暫時關閉了調試模式(debug),以便排除調試信息對性能的干擾。我們認為,之前的排序系統由于依賴關系復雜,導致了性能瓶頸,現在通過這種更清晰的排序方式,性能應該有明顯改善。

我們下一步的計劃是嘗試啟用更多的房間(rooms),測試在更復雜場景下當前排序機制的表現。同時我們也準備研究如何讓某些特定渲染項不參與排序邏輯,作為排序系統之外的獨立存在,這將在后續進一步處理。現在我們先切換回主場景進行這些操作的驗證。
在這里插入圖片描述

修改 OpenGLRenderCommands(),增加開關控制是否繪制精靈邊界框。

我們計劃添加一個功能,用于顯示渲染排序分組(sort groups),這樣便于在調試或觀察排序邏輯時更清晰地看到各個命令的分組情況。為此,我們打算新增一個全局變量 b32 Global_ShowSortGroups,作為一個控制開關來啟用或禁用該功能。

雖然我們一開始對整個排序系統的實現細節已經有些遺忘,但大致流程仍可回憶起來。我們可以在合適的位置插入一個條件判斷,根據 Global_ShowSortGroups 的值來決定是否執行顯示排序分組的邏輯。

隨后我們意識到,這段邏輯是在平臺層(platform layer)中實現的,因此需要回顧平臺層是如何處理控制變量的。我們找到了平臺控制邏輯的定義,其中已經存在兩個控制變量,通過這部分結構,我們可以向其添加我們自己的控制變量 Global_ShowSortGroups

我們將其放置在控制結構的頂部,與已有變量并列。按理論推導,這樣一來,只要我們在平臺層正確處理這個變量的輸入(如按鍵觸發、命令行標志等),并在渲染層讀取這個變量的狀態,就能實現對排序分組顯示功能的控制。

整體上,這是為調試渲染命令排序系統所設計的輔助工具之一,目的在于進一步可視化和驗證分組邏輯是否符合預期,提升開發效率和代碼可靠性。
在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述

運行游戲,測試邊界框開關功能。

我們完成了對 Global_ShowSortGroups 控制變量的添加,接下來當然需要重新編譯整個工程,確保變量能被正確識別并參與運行時控制。雖然邏輯上還存在一些小問題,功能還不是完全正確,但從運行效果來看,整體機制已經基本起效,說明方向是正確的。

當前的實現允許我們在運行時切換是否顯示排序分組,這為我們后續進行調試和邏輯驗證提供了方便。我們可以手動啟用或禁用該選項,觀察渲染系統中不同命令被分配到的排序組,從而判斷排序策略是否按照預期執行。

盡管目前切換行為存在一些瑕疵,可能表現為狀態未及時更新或部分渲染效果異常,但至少功能上已經聯通了輸入、平臺控制邏輯以及渲染展示,這為進一步修正和完善打下了基礎。

整體來看,我們正在逐步理清整個排序顯示控制流程的結構,系統開始向可調試、可驗證的方向推進。接下來的重點將是解決當前邏輯中仍然存在的不一致問題,確保開關行為更加準確、穩定。
在這里插入圖片描述

修改 AddStandardRoom(),讓其生成多個圖層。

我們現在已經可以通過開關 Global_ShowSortGroups 來控制是否顯示排序分組。在關閉該選項后,渲染排序看起來明顯更接近預期,雖然未必完全正確,但至少邏輯上已經朝著正確的方向發展。當前的排序機制已經表現出初步有效的跡象,說明之前的問題部分已經被解決。

不過,我們也發現仍然存在一些問題。例如,在界面中某些對象的排序仍然不正確,顯示效果不符合預期。此外,目前的測試場景相對簡單,無法充分檢驗排序系統在復雜條件下的表現,因此無法全面評估排序邏輯的健壯性。

為更徹底地測試排序系統,我們打算構造一些更加復雜和極端的測試場景,例如在多個房間(rooms)疊加在一起的情況下觀察排序行為。這種“堆疊房間”的場景更容易暴露隱藏的排序問題,有助于檢驗當前排序策略是否具備足夠的通用性和正確性。

通過這些測試,我們可以更清晰地了解當前系統距離最終理想狀態還有多遠,識別出尚未解決的問題,進一步修正排序規則,朝著實現全面、準確的排序邏輯目標邁進。
在這里插入圖片描述

運行游戲,觀察當前精靈排序情況。

我們實際測試了一下當前的排序效果,結果并不算糟糕,整體表現已經有了一定改善,但顯然仍然不夠正確,存在不少問題。從渲染結果來看,很多元素的前后順序仍然混亂,視覺上無法實現我們期望的圖層疊加效果。

根據這些觀察,我們判斷問題很可能出在兩個方面:

一是排序準則(sort criteria)本身存在不足。當前的排序規則可能無法全面覆蓋所有需要考慮的層級邏輯、空間位置或優先級關系,導致在某些情況下元素排列順序不符合預期。我們需要對排序權重、比較邏輯等進行重新審視,確保排序系統具備更強的判別能力。

二是存在排序環(sort cycles)。這意味著部分渲染項之間形成了互相依賴的循環引用,導致無法建立一個穩定且無矛盾的排序順序。這種情況會嚴重干擾渲染順序的決策過程,進而使最終輸出結果錯亂。因此,我們必須引入機制來檢測并打破這些排序環,或者在邏輯上避免這種依賴關系的出現。

下一步,我們會集中精力分析當前排序邏輯中的關鍵節點,定位可能導致錯誤的比較條件,并結合實際運行時數據查找潛在的環狀依賴。只有解決了這兩個核心問題,排序系統才能真正穩定且正確地工作,實現我們對渲染秩序的預期控制目標。

在黑板上記錄:排序錯誤來源分析。

我們現在打算添加一種可視化指示器,用于在出現排序循環問題(sort cycles)時給予提示,幫助識別和調試這些邏輯錯誤。在正式處理這一問題之前,我們先回顧并整理了當前排序系統中可能導致錯誤的三大來源。

我們在畫板上做了歸納,明確了排序錯誤的三類核心來源:

  1. 代碼中的 Bug
    最常見也最直接的問題來源就是代碼實現中的錯誤。例如排序邏輯編寫不當、比較條件失效、內存越界或數據未初始化等,這類問題通常通過審查代碼或調試日志可以發現。

  2. 排序循環(Sort Cycles)
    排序過程中存在命令之間互相依賴的情況,導致形成一個閉環,使排序無法線性化。這種問題較為隱蔽,不容易通過單步調試發現,但會直接導致排序失敗或異常結果。

  3. 排序準則設計不足(Criteria Deficiency)
    排序規則本身設計不完善,比如缺少考慮某些空間關系、遮擋關系或邏輯優先級,導致某些場景下排序結果錯誤。即使沒有 Bug,也沒有循環,這類設計缺陷依然會造成結果不正確。

我們目前準備重點處理第二類問題:排序循環。為了更有效地識別這類問題,我們計劃添加圖形化或調試輔助方式,例如繪制一個實時指示器或高亮機制,用于標記當前渲染數據中是否存在循環依賴。一旦檢測到循環,可以直接通過界面上看到提醒,而無需每次都手動調試,這將極大提升排查效率。

接下來我們將專注于實現這個檢測機制,并驗證其在復雜場景下的可靠性,為后續徹底解決排序邏輯中的結構性問題打下基礎。

黑板記錄:可能存在的 bug。

我們目前在排查和處理排序系統的問題時,明確了理論上設想都是正確的,但實現過程中可能存在具體的錯誤,這成為一個主要的錯誤來源。

我們已經識別出的第一類排序錯誤來源是:實現層面的失誤
即使我們在理論設計、邏輯推導和整體架構上都沒有問題,也可能因為實際編碼中的失誤導致系統無法正常工作。例如最典型的是拼寫錯誤(type),就像我們在開發初期修復的一個拼寫錯誤那樣,看似簡單,卻足以影響整個功能的執行。這類錯誤也包括變量未初始化、數組越界、錯誤的條件判斷、遺漏的分支處理等典型代碼層面的 bug。

這種問題的特點是:不在理論邏輯中產生,而是在具體代碼實現中引入。它通常通過調試和代碼審查能較快定位和修復,但在復雜系統中,如果沒有良好的日志和驗證機制,仍可能造成大量時間的浪費。

接下來我們將繼續整理剩下幾類排序錯誤來源,并在此基礎上構建更完整的排序問題檢測和提示系統,確保無論是代碼錯誤、結構缺陷還是邏輯矛盾都能被盡早發現并加以處理。

黑板記錄:Z值比較邏輯可能有誤。

我們總結出的第二類排序錯誤來源是:比較函數設計不合理

當前使用的排序比較邏輯是我們“臨時”制定的,用于處理普通精靈(sprite)與深度精靈(Z-sprite)之間的排序關系。然而,這套比較規則從未經過嚴格驗證,也未明確地證明它適用于所有渲染場景。因此,即使排序算法本身邏輯無誤、代碼無 Bug,排序結果仍可能出現嚴重錯誤,原因就在于比較邏輯本身理論上就是不成立的

我們面臨的一個根本性問題是:在二維渲染系統中試圖模擬三維場景下的物體層級關系。當前的實現方式只是將三維對象投影為二維“卡片”(cards),但這些卡片無法準確表達三維物體之間復雜的遮擋、嵌套、交錯等空間關系。這意味著,我們目前的排序依據其實是一種近似,是人為設計的一套簡化模型,它在很多情況下無法真實反映物體間的正確前后關系。

這種錯誤的特點是:比較函數本身在理論層面無法適配所有場景,它不是由實現 bug 導致的,而是設計本身的局限性。一些具體場景中,即使比較邏輯完全按預期運行,得到的排序結果也可能不正確。

我們已經意識到這種問題是不可避免的,只要使用當前這種簡化模型,就一定會存在某些場景是無法被正確排序的。為了應對這種情況,我們需要:

  • 識別并記錄這種排序不可能正確處理的場景;
  • 嘗試設計更加穩健或多階段的排序邏輯
  • 考慮引入深度信息或其他結構性數據作為輔助判斷;
  • 提供可視化工具用于提示潛在不可靠排序結果

最終目標是盡可能在系統層面規避比較邏輯固有的不足,或者在用戶視角下提供預警與修復機制,從而提升渲染的穩定性和可靠性。

黑板記錄:圖結構中存在循環引用的問題。

最后一個排序錯誤的來源是:圖的循環問題及其解決(graph cycle resolution)

這里所說的“解決”指的是處理循環依賴關系的行為,而不是像素分辨率之類的意思。排序系統中,我們可能會遇到頂點之間形成環狀依賴的情況,也就是所謂的“循環”,這意味著沒有辦法對這些對象給出一個唯一且正確的繪制順序。

例如最簡單的循環是幾個元素按順序相互依賴,形成一個環(環狀圖),導致排序準則無法確定哪個元素應該先繪制。這種情況本質上說明排序比較函數存在不足,因為它無法為這組對象提供一個一致且線性的排序結果。

不過,也有可能是因為我們缺乏一個更加智能和合理的斷環策略。比如當前系統選擇在某個環路的特定位置斷開,導致繪制順序為 ABC,但如果換個斷開點,比如斷在 B 或 C 處,繪制順序變成 BCA,最終效果可能更好。這表明不同的斷環點會影響排序結果的合理性。

因此,除了改進比較邏輯,我們還需要研究如何更有效地檢測和打破這些排序循環,從而得到更符合實際需求的繪制順序。沒有萬能的解決方案,因為這是一個本質上復雜且難以窮盡的問題,尤其是在我們僅用二維卡片來模擬三維空間,沒有使用諸如深度緩沖(z-buffer)等硬件支持的情況下。

總結來說,這三個主要的排序錯誤來源包括:

  1. 實現上的代碼錯誤(bug);
  2. 比較函數設計本身的理論不足;
  3. 排序依賴關系中存在的循環及其斷環策略。

理解并解決這些問題,是提升渲染排序正確性和穩定性的關鍵所在。

考慮繪制圖循環結構,以及解決該循環的方法。

我們現在剩下大約五分鐘的時間,計劃快速實現一個功能,用于繪制和檢測圖中的排序循環(graph cycles),這樣就能直觀地知道排序中是否存在循環問題。

當前排序算法在遍歷精靈圖時,每訪問一個節點都會設置一個“訪問過”的標記(visited flag),用來避免重復訪問同一個節點。這雖然方便,但存在一個局限:它只能告訴我們某個節點曾經被訪問過,但無法判斷在本次特定的遍歷路徑中是否重新訪問了該節點。

也就是說,現有標記機制不能確定我們是否“繞回了自己本來的路徑”,而只有這種情況才代表真正的循環。節點可能是在別的路徑中被訪問過的,但這不代表當前路徑存在環。

為了解決這個問題,我們需要一種方式來記錄當前遍歷路徑中的訪問情況,以便發現當路徑再次訪問到自己時,確認存在循環。

實現上有兩種方案:

  1. 利用一個額外的“路徑訪問標記”,區別于全局的訪問標記。每次進入新節點時,將其標記為當前路徑中的節點,如果在路徑中再次遇到該節點,立即判定為循環。

  2. 保持原有算法大體不變,只做小修改,在遞歸進入和退出節點時分別設置和清除路徑標記,從而追蹤當前路徑狀態。

這兩種方法都可以較為簡單地集成到現有排序遍歷代碼中,不需要大幅修改算法邏輯。通過加入這種路徑跟蹤,我們能夠準確檢測排序依賴圖中存在的循環,并基于此繪制出循環結構,方便調試和進一步優化排序策略。

黑板記錄:增加 Generation Tag 用于標記圖遍歷階段。

我們現在有兩種非常簡單的方法可以檢測圖中的循環,下面是對這兩種方法的詳細總結:

第一種方法是使用生成標簽(generation tag)
這個概念之前在資產跟蹤系統中也用過,基本思路是維護一個數字標簽,每次算法運行時這個標簽會遞增。具體做法是:

  • 每個節點有一個標簽變量,表示上一次被訪問時的“代數”或“時間戳”。

  • 在每次排序遍歷開始時,我們增加全局的“當前代數”值。

  • 當遍歷節點時,檢查該節點的標簽值是否等于當前代數:

    • 如果是等于,說明這個節點在本次遍歷已經被訪問過,出現了環路(循環)。
    • 如果不是,說明節點還未在本次遍歷中被訪問,可以繼續處理,同時將節點標簽更新為當前代數。

相比傳統的用一個布爾值標記是否訪問過的方法,生成標簽能更精確地區分不同遍歷過程中的訪問狀態。布爾值只能表示是否“曾經訪問過”,但不能區分“這次遍歷”或“上次遍歷”等不同時間點。生成標簽機制則能夠準確反映本次遍歷中的訪問路徑,幫助我們判斷是否存在循環。

總結來說,生成標簽法通過一個整數遞增計數器配合每個節點的標簽值,能在每次遍歷時準確標記訪問歷史,輕松檢測出循環路徑。

第二種方法在后續繼續討論,但第一種生成標簽法已經能夠較好解決當前檢測循環的問題,且實現簡單,易于集成到現有排序代碼中。

黑板記錄:增加 Erasable Bit,用于調試/回退精靈狀態。

第二種方法是使用可擦除標簽(erasable tag)

具體來說,我們有一個“訪問過”的標記位,這個位需要是永久性的,也就是說一旦某個節點被訪問過,這個位就一直保持,防止之后的遍歷重復處理已經遍歷過的圖部分。這樣做的目的是,當新的遍歷開始時,如果碰到已經標記過的節點,可以直接跳過,認為該部分的排序已經完成,不必再次遍歷。

然而,這樣的問題是:這個永久的訪問標記無法區分當前遍歷是誰設定的。也就是說,某個節點上的訪問標記可能是之前某次遍歷留下的,而當前遍歷者無法判斷它是不是自己“走過”的路徑。所以,這種情況下,我們不能確定訪問標記的存在是否意味著當前路徑中有循環,因為它可能是由其他路徑設置的。

為了解決這個問題,我們可以增加另一個標記位,這個位是在當前遍歷過程中動態設置的。遍歷時進入節點就設置該位,如果再次訪問時發現該位已被設置,說明當前路徑出現了環(循環)。當遞歸返回時,可以擦除這個位,保持狀態的準確。

總結來說,這種方法用一個永久訪問標記來避免重復遍歷,用另一個可擦除的“當前路徑訪問標記”來檢測當前遍歷中的循環。這樣能更精準地判斷循環,同時又能避免多次重復遍歷圖的部分,提高效率。

game_render.hsprite_flag 中添加 Sprite_Cycle 枚舉值。

我們準備在節點結構中增加一個字段,稱為“sprite cycle check”(精靈循環檢查標志),用來檢測圖中的循環問題。原本考慮使用兩個標志位,一個用來檢測是否出現循環,一個用來標記節點是否處于循環中,但后來覺得其實只用一個標志就足夠了。

在具體實現時,會用這個“sprite cycle check”標志來判斷當前遍歷是否遇到了循環,一旦發現某節點的這個標志已經被設置,說明在當前路徑中再次訪問了該節點,確認存在循環。

由于時間有限,暫時決定只用這一個標志來完成循環檢測的功能,簡化實現過程。這樣可以快速開始對圖中的循環問題進行檢測和調試。
在這里插入圖片描述

game_render.cpp 中更新 RecursiveFromToBack():遍歷前設置 Sprite_Cycle,遍歷后清除。

我們準備快速把循環檢測的邏輯加進去,具體操作是:
在遍歷某個節點時,會用按位或(OR)操作把“循環標志”設置進去,表示當前路徑已經經過該節點。
當完成對該節點的所有邊遍歷后(即回溯回來時),會用按位與(AND)配合取反操作把這個循環標志清除掉,表示當前路徑已經離開該節點,不再算作正在訪問中。

這樣做的目的是在遞歸遍歷過程中,動態地標記當前訪問路徑中的節點,當遇到已標記節點時,能夠準確判斷存在循環;而在遞歸返回時清除標記,保證狀態的準確和不會誤判。

目前先把這個邏輯寫進代碼,后續再考慮如何更好地處理和利用檢測到的循環信息。
在這里插入圖片描述

game_render.h 中的 sprite_graph_walk 結構體添加 HitCycle 字段。

我們在遍歷節點時,進入節點時會設置循環標志,離開節點時會清除該標志。這樣,當到達某個節點時,如果發現該節點的循環標志已經被設置,說明我們遇到了一個循環,因為對于非循環路徑,標志會在離開節點時被清除。

因此,我們可以通過檢查這個標志來判斷是否存在循環。

為了方便表示當前是否處于循環狀態,我們計劃利用已有的“graph lock”(圖鎖)機制,增加一個變量或標志來記錄“是否在循環中”,從而在遍歷和后續處理時能及時獲知是否發生了循環情況。
在這里插入圖片描述

修改 WalkSpriteGraph()RecursiveFromToBack(),配合 HitCycle 檢測圖中的循環。

我們在開始遍歷一個組時,會先將“未檢測到循環”狀態初始化為假(即還沒發現循環)。在遍歷過程中,如果發現有循環,則將“檢測到循環”的標志設置為真。

每次遍歷節點時,會通過邏輯“或”操作,將當前節點的循環標志與整體的“檢測到循環”標志合并,從而確保只要路徑中有一個循環,整個遍歷結果都會反映出存在循環。

當遍歷過程中遇到已經處于循環狀態的節點時,會保持該循環標志不清除,保證整個組都被標記為含有循環。

在遍歷結束回退時,如果沒有檢測到循環,則清除循環標志,否則保留該標志,確保最終能標記出所有包含循環的組。

這樣我們就能識別并“標記”任何存在循環的組,方便后續處理和調試。
在這里插入圖片描述

修改 OpenGLRenderCommands(),僅繪制帶有循環標志位的精靈邊界框。

我們計劃繪制那些標記為存在循環的組,并且默認開啟這個功能。具體做法是在繪制邊界的遞歸函數中增加一個判斷:只有當組的循環標志被設置時,才進行繪制;如果組沒有循環標志,則跳過繪制。

這樣,我們就能只顯示那些在排序過程中檢測到有循環問題的組,方便我們定位和調試排序中的循環錯誤。
在這里插入圖片描述

運行游戲,確認當前無圖結構循環。

理論上,如果存在循環,我們應該能看到對應組的輪廓被繪制出來,但目前沒有看到任何輪廓,這說明要么我們系統中沒有循環存在,要么我們實現的檢測循環的算法有問題。具體算法是否有效,后續還需要繼續調試驗證,因為現在時間已經不夠了。這是接下來要做的工作重點。
在這里插入圖片描述

命中斷言了

在這里插入圖片描述

注釋掉斷言

在這里插入圖片描述

棧溢出了不會是發生循環圖了

在這里插入圖片描述

檢查一下什么原因

暈死

在這里插入圖片描述

在這里插入圖片描述

進入問答環節。

你是如何判斷一個問題是否復雜到需要可視化的?

判斷一個問題是否復雜到需要可視化,通常依賴于我們在解決問題過程中遇到的認知難度。如果我們在腦中思考問題時感到混亂,或者嘗試多種調試方式依然無法明確問題本質,通常就意味著我們可能需要借助可視化手段。

這個判斷并不總是顯而易見的,它更多地依賴于經驗積累形成的一種直覺。當我們在面對一個 bug 或一個邏輯難題時,通常會先用自己的習慣方式著手,比如逐步調試、打印日志或分析數據結構的狀態。如果這些方法能夠清晰揭示問題,那說明問題本身比較線性、簡單,不需要可視化。比如涉及簡單數學運算、明確的流程邏輯、單次執行路徑等,這些內容在調試器中逐步執行就能看明白。

但另一些問題則不同,比如涉及大量對象間相互引用的結構問題、復雜數據結構(如圖、樹、網絡)的維護、或游戲中物理對象之間的空間關系等。這類問題往往由于其包含多個元素之間的復雜關系,使得用代碼或斷點調試方式難以完全掌握其中的狀態。這時我們很容易“在腦子里看不清”,比如你寫了一個平衡二叉樹的插入函數,但結構一旦涉及幾百個節點,僅靠輸出或調試器已經很難判斷一次旋轉是否正確完成,節點指針是否指向正確的子樹等。

此時,可視化手段就顯得非常必要。我們可以把節點和指針的關系畫出來、動態更新樹結構的變化、或者在游戲中用圖形方式表示物體的包圍盒、Z序關系等等。通過圖形化的方式,可以立刻看到結構中是否有環、錯亂、偏移等異常狀態,節省大量試錯時間。

總結就是:
當問題規模較小、流程單一、調試器輸出足以支撐分析時,通常不需要可視化。
當問題涉及大量復雜對象關系、多層結構交互、狀態難以線性分析時,就應該考慮使用可視化手段幫助理解和調試。尤其是在結構錯綜復雜、需要“看清楚整體”才能發現異常的場景中,可視化幾乎是唯一可行手段。

構建圖的時間復雜度還是多少?是 O(n2) 嗎?

我們目前還沒有優化這部分邏輯。具體來說,在 SortEntries 內部,包含了兩個關鍵函數:BuildSpriteGraphWalkSpriteGraph。其中:

  • WalkSpriteGraph 的時間復雜度大致為 $O(n)$,更準確地說是 $O(2m)$,其中 $m$ 是邊的數量。這部分的效率還算可以,遍歷的是圖中的連接關系。
  • BuildSpriteGraph 則仍然是 $O(n^2)$ 的復雜度。這是因為它目前的實現方式會在構建時進行所有對象之間的兩兩比較,盡管因為只比較一次(即 (i, j) 而不重復 (j, i))導致操作次數略少于 $n^2$,比如說是 $n \times (n+1)/2$,但本質上仍然是一個與 $n^2$ 成比例的復雜度。

換句話說,雖然實際操作數量少于 $n^2$,但增長趨勢仍然是平方級別的,只要輸入數量增加,性能壓力就會迅速上升。

目前沒有使用任何索引結構(比如空間分區、BSP 樹或哈希)來減少不必要的比較,也沒有任何提前排除的機制,因此是一個典型的樸素全比較實現。這使得在大量精靈(sprites)或圖元素的場景下,這部分代碼是性能瓶頸之一。

優化的方向包括:

  • 引入空間索引結構,排除不可能發生遮擋或交互的對象對。
  • 使用排序或掃描線技術,減少比較次數。
  • 將構建與遍歷階段分離優化,結合更高效的圖分析方法。

目前來說,這段代碼仍然是一個與 $n^2$ 成比例的瓶頸模塊。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/81269.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/81269.shtml
英文地址,請注明出處:http://en.pswp.cn/web/81269.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

基于PyTorch的殘差網絡圖像分類實現指南

以下是一份超過6000字的詳細技術文檔,介紹如何在Python環境下使用PyTorch框架實現ResNet進行圖像分類任務,并部署在服務器環境運行。內容包含完整代碼實現、原理分析和工程實踐細節。 基于PyTorch的殘差網絡圖像分類實現指南 目錄 殘差網絡理論基礎服務…

(27)運動目標檢測 之 分類(如YOLO) 數據集自動劃分

(27)運動目標檢測 之 分類(如YOLO) 數據集自動劃分 目標檢測場景下有時也會遇到分類需求,比如車牌識別、顏色識別等等本文以手寫數字數據集為例,講述如何將 0~9 10個類別的數據集自動劃分,支持調整劃分比例手寫數字數據集及Python實現代碼可在此直接下載:https://downloa…

Ubuntu安裝1Panel可視化管理服務器及青龍面板及其依賴安裝教程

Ubuntu安裝1Panel可視化管理服務器及青龍面板及其依賴安裝教程 前言一、準備工作二、操作步驟1、1Panel安裝2、青龍面板安裝3、青龍面板依賴安裝 前言 1Panel 是一款現代化的開源 Linux 服務器管理面板,專注于簡化服務器運維操作,提供可視化界面管理 Web…

DataGridView中拖放帶有圖片的Excel,實現數據批量導入

1、帶有DataGridView的窗體,界面如下 2、編寫DataGridView支持拖放的代碼 Private Sub DataGridView1_DragEnter(ByVal sender As Object, ByVal e As DragEventArgs) Handles DataGridView1.DragEnterIf e.Data.GetDataPresent(DataFormats.FileDrop) ThenDim file…

創新點!貝葉斯優化、CNN與LSTM結合,實現更準預測、更快效率、更高性能!

能源與環境領域的時空數據預測面臨特征解析與參數調優雙重挑戰。CNN-LSTM成為突破口:CNN提取空間特征,LSTM捕捉時序依賴,實現時空數據的深度建模。但混合模型超參數(如卷積核數、LSTM層數)調優復雜,傳統方法…

獲取點擊點所在區域所能容納最大連續空白矩形面積及頂點坐標需求分析及相關解決方案

近日拿到一個需求,通過分析思考以及查詢資料得以解決,趁著不忙記錄一下: 需求: 頁面上放一個圖片控件,載入圖片之后,點擊圖片任何一個白色空間,找出點擊點所在區域所能容納的最大連續空白矩形…

vue-cli 構建打包優化(JeecgBoot-Vue2 配置優化篇)

項目:jeecgboot-Vue2 在項目二次開發后,在本人電腦打包時間為3分35秒左右 webpack5默認優化: Tree Shaking(搖樹優化):刪除未使用的代碼base64 內聯: 小于 8KB 的資源(圖片等&…

科學養生:解鎖現代健康生活新方式

在現代社會,熬夜加班、外賣快餐、久坐不動成了很多人的生活常態,由此引發的亞健康問題日益凸顯。其實,遵循科學的養生方式,無需復雜操作,從日常細節調整,就能顯著提升健康水平。? 飲食上,把控…

PostGIS使用小結

文章目錄 PostGIS使用小結簡介安裝配合postgres使用的操作1.python安裝gdal PostGIS使用小結 簡介 PostGIS 是 PostgreSQL 數據庫的地理空間數據擴展,通過為 PostgreSQL數據庫增加地理空間數據類型、索引、函數和操作符,使其成為功能強大的空間數據庫&…

NNG和DDS

NNG (Nanomsg Next Generation) 和 DDS (Data Distribution Service) 是兩種不同的通信協議,各自在不同場景下具有其優勢。下面我將對這兩種技術進行詳細解釋,并通過具體的例子來說明它們如何應用在實際場景中。 1. NNG (Nanomsg Next Generation) NNG簡…

自制操作系統day7(獲取按鍵編碼、FIFO緩沖區、鼠標、鍵盤控制器(Keyboard Controller, KBC)、PS/2協議)

day7 獲取按鍵編碼(hiarib04a) void inthandler21(int *esp) {struct BOOTINFO *binfo (struct BOOTINFO *) ADR_BOOTINFO; // 獲取系統啟動信息結構體指針unsigned char data, s[4]; // data: 鍵盤數據緩存&#x…

Javase 基礎加強 —— 09 IO流第二彈

本系列為筆者學習Javase的課堂筆記,視頻資源為B站黑馬程序員出品的《黑馬程序員JavaAI智能輔助編程全套視頻教程,java零基礎入門到大牛一套通關》,章節分布參考視頻教程,為同樣學習Javase系列課程的同學們提供參考。 01 緩沖字節…

服務器操作系統調優內核參數(方便查詢)

fs.aio-max-nr1048576 #此參數限制并發未完成的異步請求數目,應該設置避免I/O子系統故障 fs.file-max1048575 #該參數決定了系統中所允許的文件句柄最大數目,文件句柄設置代表linux系統中可以打開的文件的數量 fs.inotify.max_user_watches8192000 #表…

[Windows] 格式工廠 FormatFactory v5.20.便攜版 ——多功能媒體文件轉換工具

想要輕松搞定各類媒體文件格式轉換?這款 Windows 平臺的格式工廠 FormatFactory v5.20 便攜版 正是你的不二之選!無需安裝,即開即用,為你帶來高效便捷的文件處理體驗。 全能格式轉換,滿足多元需求 軟件功能覆蓋視頻、…

[AI]主流大模型、ChatGPTDeepseek、國內免費大模型API服務推薦(支持LangChain.js集成)

主流大模型特色對比表 模型核心優勢適用場景局限性DeepSeek- 數學/代碼能力卓越(GSM8K準確率82.3%)1- 開源生態完善(支持醫療/金融領域)7- 成本極低(API價格僅為ChatGPT的2%-3%)5科研輔助、代碼開發、數據…

國際薦酒師(香港)協會亮相新西蘭葡萄酒巡展深度參與趙鳳儀大師班

國際薦酒師(香港)協會率團亮相2025新西蘭葡萄酒巡展 深度參與趙鳳儀MW“百年百碧祺”大師班 廣州/上海/青島,2025年5月12-16日——國際薦酒師(香港)協會(IRWA)近日率專業代表團出席“純凈獨特&…

Node.js Express 項目現代化打包部署全指南

Node.js Express 項目現代化打包部署全指南 一、項目準備階段 1.1 依賴管理優化 # 生產依賴安裝(示例) npm install express mongoose dotenv compression helmet# 開發依賴安裝 npm install nodemon eslint types/node --save-dev1.2 環境變量配置 /…

java基礎知識回顧3(可用于Java基礎速通)考前,面試前均可用!

目錄 一、基本算數運算符 二、自增自減運算符 三、賦值運算符 四、關系運算符 五、邏輯運算符 六、三元運算符 七、 運算符的優先級 八、小案例:在程序中接收用戶通過鍵盤輸入的數據 聲明:本文章根據黑馬程序員b站教學視頻做的筆記,可…

隨機密碼生成器:原理、實現與應用(多語言實現)

在當今數字化的時代,信息安全至關重要。而密碼作為保護個人和敏感信息的第一道防線,其安全性直接關系到我們的隱私和數據安全。然而,許多人在設置密碼時往往使用簡單、易猜的組合,如生日、電話號碼或常見的單詞,這使得…

TypeScript 泛型講解

如果說 TypeScript 是一門對類型進行編程的語言,那么泛型就是這門語言里的(函數)參數。本章,我將會從多角度講解 TypeScript 中無處不在的泛型,以及它在類型別名、對象類型、函數與 Class 中的使用方式。 一、泛型的核…