回顧和今天的計劃
今天的主要任務是完成稀疏 Unicode 支持。之前我們已經完成了所有的思考和設計工作,但代碼部分尚未完成,因為有許多內容需要調整和重構。因此,今天的目標就是把這些內容全部整理好并最終實現。
回顧當前測試資源構建器的狀態
上次的修改還沒有完全完成,尤其是**測試資產構建(test asset builder)**部分的調整仍然存在問題。因此,今天的第一步是檢查相關代碼,確保所有修改都真正完成。很可能仍有未完成的部分,需要進一步完善后再進行加載和測試。
目前的代碼結構中,我們將所有內容移動到了外部,并在 .h
文件中定義了 loaded_font
結構。在 loaded_font
里,我們存儲了一些非稀疏的數據。例如,字形(glyphs)存儲在稀疏表中,而水平方向的前進量(horizontal advance)則并非完全稀疏,而是只在某個維度上稀疏。這是因為用戶可能會添加不同數量的字形,所以結構需要靈活處理。
此外,還實現了 GlyphIndexFromCodePoint
這一轉換表,用于從特定的 Unicode 碼點(code point)映射到字形索引。這一部分是稀疏存儲的,而 HorizontalAdvance
在某個軸向上稀疏但在另一個軸向上是稠密的。至于 glyphs
數據本身是稠密存儲的,也就是說,所有實際使用的字形是緊密排列存儲的。
在數據存儲時,每個字形都會關聯其對應的 Unicode 碼點,并存入相應的結構中。目前的實現方式基本合理,接下來需要進行整體檢查,以確保代碼結構完整、邏輯清晰,至少能夠通過基本的合理性檢查。
具體到 LoadFont
這部分,它的主要作用是加載字體,并返回 loaded_font
結構。所有內容都被封裝到結構體中,以便支持多個字體加載。例如,通過 SelectObject
選擇對象,并確定 MaxGlyphCount
(最大字形數量)。
最大字形數量的設定是一個經驗值,我們假設不會有超出此數量的字形,并以此作為上限。因為這部分僅適用于資產處理階段(asset processor),而非運行時(runtime),所以可以在后續根據需要進行調整。
在進一步檢查代碼時,發現 GlyphIndexFromCodePointSize
相關邏輯可能有問題。看起來 loaded_font
不應該按照當前的方式進行存儲,因為這樣顯然是不合理的。這部分代碼可能需要調整,使 GlyphIndexFromCodePointSize
僅分配一次,而不是錯誤地為每個 loaded_font
進行分配。
關于類型安全內存分配
在代碼編寫過程中,使用特定的方式可以減少類型相關的錯誤。例如,在實際代碼中,PushArray
這種方法會進行類型轉換,并傳遞計數值,同時使用相同類型的 sizeof
,這樣可以在一定程度上增加類型安全性。這種做法類似于更強類型的語言中 new
關鍵字的作用,有助于避免某些錯誤。因此,在正式代碼中,會更加謹慎地使用這些方法,以減少潛在的 bug,而在一些臨時測試代碼中,這方面可能不會特別注意。
這次遇到的 bug 也是一個典型例子,正好可以借此機會強調這一點。這種方法并不是說直接“捕獲”錯誤,而是通過正確的大小計算和類型匹配,使得錯誤不會發生。
在代碼實現上,首先需要存儲 glyphs
,所以進行內存分配,并清除已有數據。這一步非常重要,因為需要在一開始就將所有內容清空,以確保不會受到之前數據的影響。
在檢查代碼時,發現目前尚未有 null glyph
,但最好增加一個,這樣在處理字符映射時會更靈活。
然后,開始對 Font->Glyphs
進行分配,malloc
申請存儲空間,數量取決于最大可能的字形數(MaxGlyphCount
)。
同時,對水平方向的前進量(horizontal advance
)進行類似的存儲分配。這里的 horizontal advance
其實在兩個軸向上都是稠密存儲的,但因為它是按照最大數量來分配的,而不是根據實際使用的 glyph count
,所以會有額外的空間,形成一個嵌套矩形的結構。
因此,在存儲 horizontal advance
時,會先申請一塊較大的矩形區域,并進行初始化,以確保后續計算不會受到未定義數據的影響。
為空字形預留空間
當前的實現基本上是合理的,但 glyph_count
仍然存在一些問題。需要預留一個 null glyph(空字形),因此 glyph_count
應該從 1
開始,而不是 0
。這樣,第 0 個字形永遠不會被實際使用,以確保在處理字形索引時不會意外訪問無效數據。
對于 null glyph,需要明確設置其 unicode code point
和 bitmap_id
,以確保它不會被錯誤地解析為有效字符。unicode code point
應該設置為一個不會被使用的值,而 bitmap_id
需要明確標記為無效,以避免后續渲染或解析邏輯誤用這一字形。
在 finalize_font 過程中,需要遍歷所有字形配對,檢查每個配對的有效性。目前的邏輯在處理配對時存在漏洞,因為在判斷是否為有效代碼點后,會直接執行查找操作,而不會檢查是否為 0
(即 null glyph
)。應當避免 null glyph
參與 kerning(字距調整),因為 null glyph
只是一個占位符,不應該與任何其他字符產生影響。因此,應該在處理配對關系時,確保不會向表中寫入 null glyph
相關的數據。
在 InitializeFontDC 過程中,主要確保 font dc
資源正確初始化,避免后續重復使用時出現異常。
在 LoadGlyphBitmap 中,確保對 code point 的賦值邏輯正確,當前代碼已經在加載字形時設置了 unicode code point
和 bitmap_id
,并正確維護了稀疏查找表,確保 glyph_index
的映射關系正常工作。
最后,在加載字體時,仍然需要遵循 Windows
API 的調用規則,正確傳遞 unicode
代碼點,以確保 Windows
能夠正確解析并返回相應字形數據。
當前的字體處理流程基本完善,首先進行字體加載,然后執行一系列計算和存儲操作,以確保所有字形數據正確寫入。在完成主要操作后,會計算字符寬度,并遍歷所有字形,更新相關信息。
在遍歷字形索引的過程中,避免對 OtherGlyphIndex
為 0
的情況進行多余的操作,盡量減少無意義的計算。不過,部分邏輯可能是之前用于修正 kerning(字距調整)的,涉及與 Windows 的對齊,因此暫時不修改,避免影響已有的調整邏輯。
在字體數據存儲時,嚴格使用字形索引,而不是通過轉換表進行二次查詢,以確保效率。所有的處理邏輯在當前設計下是合理的。
在最終寫入字體數據時,按照以下步驟進行:
- 遍歷所有字形,計算所需的存儲大小。
- 將所有字形數據依次寫入文件,包括每個字形的具體大小。
- 處理水平前進量(
horizontal advance
),按照行的方式寫入,只寫入有效數據部分,忽略多余的填充空間。 - 通過最大字形數量(
MaxGlyphCount
)進行偏移,以保證數據排列符合存儲格式,類似于位圖操作中的子區域拷貝(blit
)。
在檢查字體處理代碼時,確認了 initialize_font_dc
被正確調用,確保 font_dc
資源的正確初始化。此外,整體邏輯看起來是合理的,沒有發現明顯錯誤。
最后,在 bitmap_id
相關代碼中出現了一個錯誤提示,但問題不大,主要是涉及位圖 ID 處理,需要再檢查相關代碼邏輯,確保所有 glyph
對應的 bitmap
被正確關聯。總體來看,字體處理邏輯已經完成。
運行資源構建器
接下來運行 asset builder,觀察其實際執行情況,以確認字體數據的寫入是否正確。當前并沒有明確的錯誤或異常,但由于剛剛完成數據寫入,還無法確定一切是否如預期般正常工作。
執行 asset builder 后,會將數據寫入 game data 目錄。通過檢查該目錄中的數據文件,可以進行初步驗證。盡管目前沒有發現明顯的災難性問題,但由于對數據格式尚不完全確定,仍然需要進一步檢查,確保數據的正確性。
當前的調試狀態,缺乏明確的反饋信息,仍需更詳細的檢查手段,例如:
- 直接打印或查看存儲的字形數據,以確保其格式正確。
- 進一步調試,查看字體數據在加載時是否匹配預期的字形信息。
- 運行游戲實際渲染字體,觀察其顯示是否符合預期,以確認整個流程正常運行。
目前暫時沒有發現致命問題,但仍需進一步檢查,以確保字體系統能夠正確加載和使用所生成的字形數據。
回顧資源加載代碼
當前在加載 assets 時,相較于之前的流程發生了一些變化,尤其是在處理字體數據時。
位圖索引部分
位圖索引的加載方式仍然保持不變,意味著 bitmap 相關的加載邏輯沒有調整,依舊按照原有方式讀取和解析數據。
字體數據的加載
字體的加載過程發生了一些變化,需要適配新的數據格式。新格式的存儲結構如下:
GlyphCount
(字形總數)AscenderHeight
(上升高度)DescenderHeight
(下降高度)ExternalLeading
(外部行距)- 字形數據,其中包括:
UnicodeCodePoint
(Unicode 碼點):每個字形對應的 Unicode 編碼- 其他字體相關信息
新的數據格式增加了一些額外的參數,如 AscenderHeight
、DescenderHeight
和 ExternalLeading
,這些信息可能用于文本排版,確保字體的對齊和間距正確無誤。
在加載字體數據時,需要按照新的格式逐步解析每個字段,并正確映射字形索引與 Unicode 碼點的關系。接下來需要檢查加載邏輯,確保它能夠正確解析新格式的數據,并在內存中正確構建字體結構。
我們現在不直接處理Unicode碼點
在新的加載流程中,之前可以直接訪問的內容現在不能再直接訪問了。因此,加載字體時,需要更新處理方式。
更改內容
- 水平推進(Horizontal Advance):這部分內容需要重新加載,不再使用之前的方式。
- Unicode 碼點表(Code Point Table):這部分也發生了變化,不再直接使用
CodePointsSize
來表示字形數據的大小。 - 字形大小(Glyph Size):之前的
CodePointsSize
不再適用,取而代之的是新的字形大小。
更新后的加載方式
現在需要讀取的是字形的大小,而不再是每個碼點的大小。這個變化意味著需要更新代碼,確保能夠正確地加載新的字形大小,而不是依賴于原來的 CodePointsSize
。這意味著在加載時,必須根據新的格式處理這些數據。
雖然更新后的流程看起來沒有什么問題,但需要仔細調整代碼,確保正確讀取并處理新格式的數據。
解包密集字形表
當前,我們有一個密集的字形表(dense glyph table),這對我們來說是一個問題。因為,雖然我們知道每個字形的位圖 ID以及它們對應的Unicode 碼點,但我們缺乏一個能夠將用戶輸入的Unicode 碼點映射到具體字形的表格。用戶輸入通常會是 Unicode 碼點,而我們目前的表格并沒有提供這樣的映射機制。
解決方案:
為了處理這個問題,需要構建一個從 Unicode 碼點到字形的映射表。這個表格基本上是將原來的字形表(step table)進行反向處理,使其變得稀疏(sparse)。這意味著我們需要將現有的密集字形表轉換成一個可以根據 Unicode 碼點來快速查找對應字形的稀疏表格。
這種反向處理的目的是為了能夠更方便地根據用戶輸入的 Unicode 碼點查找相應的字形數據,從而支持正確的字體渲染。
暫時使用過大的Unicode映射表
在加載字體時,我們還需要創建一個Unicode 映射表。問題是,這個映射表可能會非常龐大。我們知道,如果要為每個 Unicode 碼點建立一個完整的查找表,這個表會占用大量的內存。具體來說,如果包含所有 Unicode 碼點的查找表是一個16位的索引表,那么它的大小將會達到 2MB。這個大小對于某些情況下可能會比較大。
解決方案:
-
初步方案:可以從一個 2MB 的查找表開始,這樣的查找表會包含所有 Unicode 碼點的映射。這個表是基于每個字形的 Unicode 碼點,可以直接映射到字形。
-
后續優化:不過,這個表可能會浪費大量內存。未來可以考慮采用 范圍查找(range lookup) 或 二分查找(binary search) 等更高效的方法,因為字形的 Unicode 碼點是可以排序的。如果采用排序后的查找,可能會減小內存使用和提高查詢效率。
-
表格存儲:這個映射表將會儲存在一個結構中,并且大小為16位的 Unicode 映射表,這確保了每個字體中最大不會超過 64,000 個字形(Unicode 碼點)。這將意味著映射表的大小不會超過約 128 KB,遠小于2MB的查找表大小。
-
最終決定:雖然 2MB 查找表可能是一個初步解決方案,但未來可以根據實際需要逐步優化它。初期階段可以保持使用查找表,等系統完全確認沒有問題時,再考慮做性能優化(如使用二分查找)。
最終,這個 Unicode 映射表和其他字形信息將一起存儲在文件格式中,確保每個字形都有正確的 Unicode 映射關系,幫助后續的字體渲染過程。
在資源文件中存儲使用過的最高碼點
為了優化字形加載過程,并減少映射表的大小,可以考慮一種方法,這樣做可能會解決一些潛在的問題,尤其是在處理一些特殊字符時。具體來說,可以使用一個策略來避免映射表過大。
方案:
-
引入最大 Unicode 碼點加一:我們可以通過將最大 Unicode 碼點加一的方式來簡化映射表。這意味著我們不再需要為每一個 Unicode 碼點都創建映射,而只需要使用最大碼點加一的值來設定一個“最大值”的參考點。
-
避免過大的映射表:通過這種方式,我們可以確保映射表的大小不會超過某個指定的范圍。比如,最大碼點加一可以作為新的上限,這樣就可以避免映射表過大,從而減少內存消耗。
-
具體操作:在加載字體時,可以先將該最大碼點加一的值初始化為零。然后,每次使用時,檢查當前使用的 Unicode 碼點。如果該碼點大于或等于當前設定的最大碼點加一的值,就可以將其重置為新的“有效”值。
-
確保有效性:如果在使用過程中遇到碼點大于或等于最大 Unicode 碼點加一的情況,可以將該碼點值調整為
最大碼點 + 1
,確保它不會超過最大范圍。
總結:
這種方法能夠有效地減少映射表的空間需求,避免使用過大的查找表,同時仍然能夠保證每個 Unicode 碼點都有相應的處理。雖然這種方法不會立即解決所有問題,但它為將來的優化提供了一個靈活的方案,可以根據實際需求進一步調整。
UnicodeMap
在編程的過程中,需要注意的一些細節被總結如下:
-
加載字體時的 Unicode 映射表:在加載字體時,需要確保 Unicode 映射表正確地被加載。由于在字體加載時并沒有直接包含字形(glyph),因此需要確保映射表包含正確的數據。為了支持這一點,必須先計算出 Unicode 映射表的大小,并在內存中分配相應的空間。這個過程涉及到計算 Unicode 映射表的大小,并通過
AcquireAssetMemory
函數申請內存。 -
字形數據和 Unicode 映射表的順序:在內存分配和加載過程中,首先加載字形數據,然后緊接著加載 Unicode 映射表。Unicode 映射表應該位于字形數據之后,按照一定的順序進行內存安排。
-
內存分配:在進行內存分配時,特別是涉及到 Unicode 映射表時,要確保分配的內存大小足夠,并且數據的排列順序合理。需要在內存中為字形數據和 Unicode 映射表分別分配空間。
-
內存數據結構:例如,字形數據是以 16 位大小存儲的,而 Unicode 映射表需要與字形數據緊密相連,因此要確保這兩者的內存布局是連貫的,并且能有效管理。
-
附加信息:在處理內存和數據時,還需要注意額外的調整和注意事項,比如數據的順序、內存對齊以及如何管理額外的空間。
總結來說,程序中涉及到內存管理和數據順序時,確保對 Unicode 映射表和字形數據的內存進行合理分配是非常關鍵的。這需要在內存中正確排序,確保每個部分都能被有效讀取和使用。
通常我們不會手動進行復雜的子分配
在處理內存對齊和數據分配時,采取了一些特定的策略:
-
內存對齊和子分配:在進行內存分配時,考慮到內存對齊的需求,采用了子分配的方法。通過為每個資產類型分配少量內存,確保字形和相關數據的對齊。雖然這種操作看起來簡單,但它確保了內存的有效使用,避免了不必要的浪費。
-
簡化內存分配:由于這種內存分配的操作不會頻繁發生,因此沒有專門為此創建一個系統。通常,面對需要頻繁進行內存分配的情況時,會為這些操作創建一個統一的系統來減少錯誤的發生,并提高開發效率。但是在當前情況下,認為這種操作的頻率較低,不值得額外為此構建一個復雜的內存管理系統。
-
代碼的簡化:通過這種方法,可以減少重復的代碼,也使得內存分配更加直觀和容易管理,特別是在只進行少量內存分配時。
-
字體數據的更新:加載的字體數據現在包含了 Unicode 映射表,并且字形數據也得到了更新。每個字形(glyph)現在會通過新的數據結構進行管理,確保能夠正確處理字體中的每個字符。Unicode 映射表和字形數據的內存布局已經經過了調整,以適應新的要求。
-
修改代碼結構:加載的字體數據結構中加入了新的成員,包括 Unicode 映射表和字形數據。這些變化需要在代碼中進行適當的調整,以確保能夠正確讀取和使用這些新的字段。
總結來說,通過調整內存管理和數據結構,優化了字體數據的加載過程,確保了內存的高效使用,并為未來可能的需求做出了靈活的準備。
在字體加載后添加額外的終結代碼
在處理字體加載時,發現了一個問題:目前還沒有構建 Unicode 映射表。為了解決這個問題,需要在加載字體數據時,處理和準備這些數據,確保 Unicode 映射表能夠正確生成。
-
初始化和準備:在加載字體數據時,必須在讀取文件數據并返回之后,進一步處理這些數據。如果是字體文件,需要進行額外的處理步驟。例如,可以通過在加載資產時增加一個步驟來完成這一操作。
-
額外工作項:為了解決這個問題,可以為資產加載過程增加一個“額外工作”的選項。例如,在資產加載完畢后,通過一個開關語句檢查是否需要做其他處理。如果需要處理,則可以調用相應的操作(比如生成 Unicode 映射表)。
-
狀態更新與處理:為了避免狀態不一致,在加載過程中,需要確保在加載完數據后,檢查是否需要生成或更新 Unicode 映射表。可以通過一個簡單的開關語句來判斷當前需要進行的操作,例如如果是字體類型,就生成 Unicode 映射表。
-
終結操作:對于完成加載的操作,可以通過“卸載資產”階段執行必要的終結步驟,確保所有必要的操作都已經完成。比如在卸載時,可以進行一些最終清理工作。
總結來說,為了確保 Unicode 映射表能夠正確生成,需要在加載資產后添加額外的處理步驟,通過狀態控制和終結操作來確保整個過程的順利進行。
構建Unicode映射表
為了構建 Unicode 映射表,步驟其實相當簡單:
-
確定字形數量:首先,我們知道有一些字形需要處理。第一個字形是特殊的,不需要關心它。因此,我們可以遍歷其他字形,逐一處理。
-
檢查字體是否加載:在構建映射表之前,必須確保字體已經加載。如果字體沒有加載,構建映射表是沒有意義的。因此,需要檢查字體是否已經正確加載。
-
獲取字體信息:通過獲取字體的資產信息,可以獲取加載的字體列表。加載的字體包含了所需的字形數據,包括每個字形的 Unicode 代碼點。
-
處理字形并填充映射表:對于每個字形,我們獲取它的 Unicode 代碼點,并將該字形的索引存儲在映射表中。此時,映射表中的位置對應于 Unicode 代碼點,并且存儲的是字形的索引。
-
確保映射表大小合適:映射表的初始狀態是空的,為了確保后續填充映射表時不會出錯,需要先將映射表清空。這樣可以確保每個位置從一開始就是零,避免出現未初始化的值。
-
驗證映射表的有效性:在填充映射表時,還可以通過斷言來驗證每個 Unicode 代碼點是否小于最大支持的 Unicode 代碼點,以確保填充過程中不會超出限制。
-
最終清理和填充:當所有字形的 Unicode 代碼點都填充到映射表中后,映射表就完成了。此時,映射表將變得更加稀疏,因為只有在 Unicode 代碼點存在字形時,才會有值。
總結來說,構建 Unicode 映射表的過程包括了:檢查字體是否加載、獲取字形信息、構建映射表并填充字形索引,確保映射表大小合適,并通過斷言和清理確保最終結果正確。
實現ZeroArray輔助函數
在處理字體加載和映射表構建時,決定采取的方案是:
-
清空映射表:在開始填充映射表之前,首先要確保將映射表清空。這樣可以確保每個位置都從零開始,避免未初始化的值。這是一個好主意,能確保后續填充過程的正確性。
-
Unicode 代碼點的最大值:雖然一開始考慮要對 Unicode 映射表進行大小檢查,但現在回想起來,已經不需要因為布局系統已經能夠處理這些信息。盡管如此,仍然覺得清空映射表的想法是有益的,能夠確保數據的有效性。
-
優化操作:考慮到清空映射表是一個簡單但有效的操作,決定繼續推進這個方案,并且已經準備好設置最終的操作步驟。
-
處理指針問題:在做這個操作時,涉及到一些指針轉換問題。當前需要確保資產轉換過程能夠正確進行,以避免出現無法轉換的情況。
總結來說,決定繼續清空映射表并進行后續處理,雖然有些細節最初看似多余,但最終還是認為這樣做可以提高代碼的健壯性和可維護性。
通過雙重轉換防止溢出
在構建 Unicode 映射表時,接下來需要做的幾個步驟包括:
-
斷言檢查:為了確保代碼的正確性,需要對 Glyph 索引做斷言檢查。具體來說,檢查使用的 16 位 Glyph 索引是否等于實際的 Glyph 索引,這樣可以確保在映射過程中沒有發生溢出。
-
加載后處理:在加載字體數據后,還需要添加一個最終的操作(finalize)來完成一些必要的清理或檢查。這是為了確保所有資產都在加載完成后正確處理。尤其是對于字體數據,必須在其他類型的資產加載后,單獨進行字體的最終化處理。
-
代碼整理和優化:雖然目前的代碼能順利編譯,但需要減少重復的模板代碼,因為每次都需要重復這種格式化操作,顯得非常繁瑣。這是一個優化目標,計劃在未來簡化這一部分內容。
-
代碼的穩定性:目前代碼雖然能夠編譯,但并不一定完美,需要在后續進一步測試和調整,確保它能夠穩定運行。
總結起來,當前的關鍵任務包括確保索引映射的正確性,添加加載后的處理步驟,以及優化代碼結構以提高代碼的可維護性和效率。
用字形替代碼點
接下來的步驟是修復代碼中涉及到代碼點(code point)的地方,因為我們希望這些部分處理的是 Glyph(字形)而不是代碼點。具體的處理方式包括:
-
修改代碼點為字形:所有涉及到獲取橫向推進(HorizontalAdvance)或(GetGlyphFromCodePoint)的地方,都需要改為處理字形。也就是說,當代碼中使用了“GetHorizontalAdvanceForPair”或“GetGlyphFromCodePoint”這些方法時,需要修改為處理字形的版本。
-
修改函數和方法:具體而言,將
GetClampedCodePoint 方法替換為
GetGlyphFromCodePoint`,這樣我們在傳遞代碼點時,實際處理的將是對應的字形。這一步是為了確保代碼點被正確轉換成字形,而不是僅僅作為一個代碼點存在。 -
修改代碼中涉及字體的部分:當涉及到字體的代碼點時,要確保這些代碼點被映射到字形,并且這些字形是通過相應的方法獲取的。此時,“GetGlyphFromCodePoint” 方法將被用來將代碼點轉換為字形。
-
調整偏移量:確保所有的值都按照預期的方式進行偏移,這樣可以避免出現偏差或錯誤,確保字形處理的準確性。
總結來說,主要的調整是在所有涉及到代碼點的地方進行修改,使它們改為處理字形,同時確保這些修改后的代碼能夠正確映射字形,并且字形的獲取和處理方式符合預期。這樣,代碼的邏輯將更加清晰和一致。
實現字形查找例程
現在我們需要實現 GetGlyphFromCodePoint
方法。具體的步驟如下:
-
檢查代碼點是否在范圍內:首先需要確認傳入的代碼點是否在有效的范圍內,即是否小于等于最大的有效代碼點。如果代碼點在范圍內,接著繼續執行后續步驟。
-
獲取字形索引:如果代碼點有效,字形索引將根據字體的 Unicode 映射表獲取。我們從映射表中查找該代碼點對應的字形索引。
-
確保索引有效:在獲取字形索引后,我們需要通過斷言(assert)確保該索引小于字體的字形總數。因為字形索引不能超過字體中實際存在的字形數量,這樣能防止超出范圍的錯誤。
-
返回字形索引:一旦確認字形索引有效,返回該索引以供后續使用。
總結來說,GetGlyphFromCodePoint
方法的目的是驗證代碼點的有效性,獲取對應的字形索引,并確保該索引在有效范圍內,最后返回該字形索引。這些操作保證了我們在處理字形時不會出現越界或無效的錯誤。
調試今天和昨天的更改
首先運行資產打包工具和舊的自定義資產構建器。接下來,需要進行基準測試,檢查當前的狀態,看看到底發生了什么。需要檢查程序是否會崩潰,或者是否能夠正常運行。根據結果,調整程序,確保不會發生崩潰。
沒有字體顯示。讓我們檢查字體加載代碼
首先,發現屏幕上沒有任何內容顯示,所以需要檢查加載過程。為此,我們將從進入 load font
函數開始,查看加載過程中的信息,檢查字體相關的數據和尺寸。這樣可以了解加載過程中是否存在問題,找出任何可能的錯誤。在此過程中,需要注意一些值和信息的大小,以便找出加載失敗的原因。
OnePastHighestCodepoint已設置但未保存到資源文件
在調試過程中,發現沒有顯示內容的原因是之前跟蹤了一些值,但卻沒有實際寫入它們,導致這些值沒有被正確設置。經過分析,發現字體數據已經寫出,但是加載過程并沒有正確記錄字體的相關信息,這就是導致問題的根源。
進一步調查后,發現雖然設置了一些字體數據,但并沒有正確地記錄在加載的字體對象中。由于加載過程中的順序問題,字體數據沒有被正確傳遞和使用,這使得最終的加載結果并不符合預期。
接下來,通過設置斷點來調試,驗證字體的代碼點是否被正確處理。確定了最高的代碼點值,并確認它符合預期的范圍,這使得調試進展更加順利。盡管分配的內存較大,但這是正常情況。
在此基礎上,將繼續進行內存分配和后續的調試步驟。
字體加載代碼中的另一個小錯誤
在調試過程中,發現了一個錯誤,因為之前沒有正確檢查一些條件,導致程序假設沒有任何失敗發生。現在已經修正了這個問題,確保所有的檢查都被正確執行。
接下來,開始逐個檢查字形(glyphs)是否正確,確保它們符合預期的標準。通過逐個檢查字形,確認它們的表現是準確的,而不是完全錯誤的。最終,所有的字形數據看起來都符合預期。
斷點沒進來
顯示game Owl Unicode碼點
在這個階段,開始考慮輸出一些其他的調試信息,比如重新添加一個小的“Owl”圖標作為測試元素。既然已經完成了獲取這些數據的工作,接下來可以通過調試測試一下我們的Unicode是否正常運行。
通過這種方式,進行調試文本輸出,并檢查是否一切按預期工作。之前的調試代碼已經做了一些設置,現在可以進一步驗證這些內容的正確性。
在這個過程中,決定進行一些調試操作,尤其是測試如何輸出Unicode碼點。為了這個目的,使用了一個相對不常見的方式來展示Unicode字符。首先,嘗試通過打印出對應的Unicode碼點來調試,方法是將字符轉換為十六進制表示,并通過拼接字符來實現這一功能。調試時,還進行了字符的范圍檢查,確保字符在合法的范圍內。
其中,為了處理十六進制字符,設計了一個輔助函數GetHex
,它根據字符的值來判斷并返回對應的十六進制數值。如果字符是數字字符,就會直接計算;如果字符是字母字符,則進行相應的轉換。這個過程顯得有些繁瑣,但它對于調試來說是必須的。最終通過這些步驟,成功地輸出了字符的Unicode碼點。
在測試時,使用了一個“小耳木兎”符號作為調試對象,通過不斷調整代碼,最終實現了將字符和其Unicode碼點輸出到控制臺。調試過程中,遇到了一些錯誤,比如沒有正確處理大小寫字母的問題,但通過調整代碼,逐步修復了這些問題。
測試的目的是確認字體是否能正確加載,是否能夠正確處理Unicode字符的映射,以及字體是否能夠正確顯示這些字符。通過對字符的逐個檢查,發現大部分字符都能正確顯示,調試代碼也成功運行,但仍需進一步驗證字體是否完全加載和正常顯示。
總結來說,這個過程涉及了多個調試步驟,從輸出Unicode碼點到處理字符映射,最終確保字體能正確顯示預期字符。
Kanji碼點存在,但沒有正確的字距調整
目前已經完成了大部分工作,但字體的渲染并沒有完全符合預期,存在一些問題。盡管大部分內容看起來是對的,字符的排列還是沒有按照預期的方式正確顯示。可能是字符對齊的處理沒有與當前實現方式完全兼容,或者字體表格中存在某些錯誤。盡管如此,字體的渲染和顯示問題并沒有完全出錯,因此認為整體工作還是相當成功的。
接下來可能需要進一步調整,檢查字符的對齊方式,或者探討從Windows中提取數據的方式,確保能夠精確對齊,同時保留其他功能的正確性。雖然目前的結果還不完美,但已經接近目標,可以繼續深入調試和優化。
那么,我們現在完成字體了嗎?是否可以轉向調試的其他部分?
目前字體的處理大部分已經完成,但仍然存在一些問題。具體來說,字符對齊的情況不完全正確,因此需要進一步調試和解決這些問題。可能的問題包括字符的顯示順序不對,或者是字符在處理時被錯誤解釋了。雖然問題并不復雜,但仍然需要找出具體的原因,以確保一切能夠正常運行。因此,在移動到其他部分之前,需要先解決這些字體相關的調試問題。
后面我們將轉向哪個部分?
接下來,我們將開始處理調試代碼部分。這意味著我們需要進行一些調試構建,確保我們能夠查看內存布局、線程活動等相關信息。這是非常重要的,因為這樣可以幫助我們深入了解程序的執行情況。接下來的幾周,我們將花時間做這部分工作,確保一切正常運行。同時,還需要確保能處理好調試過程中可能出現的各種問題,包括如何查看和解析字符映射等相關問題。
這可能就是字距調整的方式
在檢查當前字體時,發現它可能沒有正確顯示,盡管Windows報告了相關信息。通過查看其實際輸出,發現字體的顯示效果看起來像是空格,而不是期望的樣式。感覺這并不是我們所期望的正確顯示,可能在某些地方忽略了重要的細節或有其他問題。為了進一步確認,我們決定進行調試,特別是在數學計算和字體對齊方面,查找可能存在的問題。
但Red中的字母在邊緣看起來有點奇怪
在查看字體渲染時,注意到紅色字母的邊緣有些問題,顯示效果看起來不太對。當前的渲染過程中,我們沒有使用任何提示(hinting),所以字體的邊緣可能會稍微模糊,且可能在邊緣存在一些額外的透明像素。這可能是由于在渲染時,使用了Alpha混合導致的。
此外,也沒有確保字體的顯示是完全精確的,可能在渲染時引入了一些額外的模糊,尤其是在雙線性插值的影響下。為了進一步驗證,可以在渲染過程中進行測試,查看是否這種模糊效果是由于渲染設置引起的。
因此,雖然這看起來是個問題,但并不完全確定是否是設計問題,還是渲染時的模糊效果導致的。這個問題值得進一步調試,尤其是確認是否在渲染過程中引入了額外的模糊或其他不必要的效果。
可能是個基礎問題,為什么我們需要自己的字體渲染?Windows不是有我們可以調用的字體渲染函數嗎?
雖然Windows確實有自己的字體渲染函數,但我們不能直接依賴它們,因為這樣做會帶來跨平臺的問題。如果不自己渲染字體,當程序在其他平臺上運行時,比如Mac、Linux或Raspberry Pi等,可能會遇到沒有字體系統的情況。在某些情況下,直接啟動的系統可能根本沒有可用的字體庫,這時就沒有任何可調用的字體渲染系統。
即使在某些平臺上有字體渲染系統,我們也無法保證它們的行為完全相同。這樣會導致我們的游戲在不同平臺上看起來有所不同,而這并不是我們想要的。游戲本質上是一個藝術媒介,所有的藝術和視覺效果都是經過精心設計和調整的,我們希望游戲在所有平臺上看起來都一致。如果依賴平臺的字體渲染系統,可能會導致不同操作系統下游戲外觀的差異,或者在操作系統更新后,字體渲染出現不兼容等問題,從而影響游戲的可靠性和穩定性。因此,我們選擇自己實現字體渲染,確保跨平臺的一致性。
人們不理解為什么字體渲染系統可能會很復雜,因為他們只從像素的角度思考。對此你有什么評論嗎?(如果沒有其他問題的話)
字體渲染的復雜性常常被誤解,因為很多人只是從像素的角度來理解問題。他們可能認為字體只是由像素組成的,每個字形就是一個固定的像素點集合,但實際上字體渲染要比這復雜得多。
首先,字體不是簡單地由固定的像素組成,而是由矢量圖形(例如貝塞爾曲線)描述的。這些矢量圖形在渲染時會根據顯示設備的分辨率進行處理和轉換,通常會涉及抗鋸齒、字體平滑、縮放和不同的字體樣式等技術。這些因素共同影響了最終顯示的效果。
其次,字體的渲染不僅僅是把圖形顯示出來,還要考慮字符間距、行距、字形的可變性、不同語言的支持等問題。此外,渲染時還需要考慮顯示設備的不同特性,比如分辨率、顯示模式等,甚至同一個字體在不同平臺上可能會有不同的渲染效果。因此,字體渲染的復雜性不僅僅是處理像素,更是要處理這些背后的數學和技術細節,確保字體在各種情況下都能清晰、正確地顯示出來。
你能檢查一下你的數字是否是等寬字體嗎?例如,111111應該和999999寬度相同
我們可以很容易地追蹤這個問題,查看是否像“11111”和“99999”這樣的數字占用相同的空間。經過檢查,發現它們實際上并沒有占用相同的空間。
ZoomIt
在游戲中,1和9的對齊方式和在記事本中不一樣
在討論字體對齊的問題時,指出游戲中的字體和記事本中的字體并沒有完全對齊。雖然已經做了調整,確保盡可能接近微軟的規范,但仍然無法做到完全一致。一個可能的問題是字體的尺寸,當前使用的單位是“點”,這可能導致一些微小的偏差。
盡管如此,字體的對齊還是非常接近預期,幾乎已經非常精確了。然而,仍然存在一些小差異,特別是在嘗試讓文本與左邊緣對齊時。為了實現這一點,可能需要做一些額外的調整,因為有些部分沒有完全達到預期的效果。
你喜歡用循環還是遞歸來遍歷樹?為什么?
在遍歷樹形結構時,如果不太在意效率或者只是為了快速實現,通常會選擇遞歸,因為它寫起來比較簡潔。然而,如果這個操作很重要或者對性能有較高要求,那么更傾向于使用循環,因為循環在速度和可靠性方面比遞歸要好得多。
如果你想開始自己的游戲項目,你是從零開始,使用框架/API/SDK(SFML, SDL等),還是使用引擎(自己做的或已經存在的)?
在開始自己的游戲項目時,選擇使用框架、API、現成的引擎或自己定制的工具,應該根據項目的需求來決定。就像其他任何選擇一樣,關鍵在于你想要做的是什么。如果是想做一個類似已有的游戲,比如一個第一人稱射擊游戲,如果目標是模仿《虛幻引擎》那樣的游戲,那么使用虛幻引擎是一個非常合適的選擇。如果項目需求不同,或者有特定的問題需要解決,那么使用不同的工具或定制化的解決方案可能更合適。
它們之間的對齊不一樣
之所以不讓它們對齊,是因為我們做了左對齊的處理,而其他的系統沒有這樣做。我們特別為了實現左對齊的功能,所以在做測試時,刻意沒有讓它們對齊。這是一個有意的設計,以確保文本能夠左對齊。
有沒有計劃添加腳本或modding接口?
沒有計劃添加腳本或現代化的鉤子,因為這個項目是一個教育項目,目的是讓用戶去編輯隨項目提供的源代碼。這是項目的核心理念之一,鼓勵用戶通過修改源代碼來進行學習和實踐。
是的。有些人(合理地)把屏幕看作一個像素網格,認為字體渲染只是直接把字體貼到屏幕上。所以沒關系,他們只是還沒理解超出位圖字體的內容
大多數人將屏幕視為像素的網格,并且認為字體渲染只是將這些像素直接貼到屏幕上,這種理解大多基于位圖字體。在這種情況下,確實如他們所說,位圖字體就是簡單地將圖像(字符)顯示在屏幕上。但是,如果不使用基于字形(manas)的字體,就需要對字形的排版有更深的理解和意識,包括字符的大小、字符之間的間距等。如果只是使用基于字形的字體,那確實只是簡單地將字符圖像貼到屏幕上,但如果想要使顯示效果更好、看起來不那么糟糕,就必須考慮到排版細節,確保字符的適當排列和對齊,從而增加了復雜性。
為什么你使用GlyphIndexFromCodePoint而不是一個(大多數為空)指針數組來實現稀疏性?
使用字符點(code point)對應的字形索引(glyph index)而不是一個包含大多數為空指針的數組來實現稀疏性,是因為后者會導致大量不必要的內存浪費。每個指針占用8個字節,而16位的索引只占2個字節。當表格有三萬多條記錄時,如果使用指針數組,整個表格的內存大小將是使用字形索引的四倍多,這樣會浪費大量的內存帶寬。因此,選擇字形索引來節省內存是更高效的做法。
喜歡將函數的作用域限制在僅在使用的地方定義。你是如何繞過C語言不支持局部定義函數的限制的?你使用函數對象嗎?
對于C語言沒有局部定義函數的問題,實際上并不覺得這是個特別大的問題。盡管C語言沒有內建的功能讓函數僅在使用的地方定義,但在實際使用中,并沒有發現這會帶來太多麻煩。唯一可能遇到的問題是名稱沖突,但這種情況其實也很少。自己并不覺得這種作用域限制是個問題,雖然如果能夠像lambda函數那樣在函數內部定義局部函數,且可以訪問堆棧或外部函數的活動范圍,那會更加方便。雖然C語言最近添加了類似的功能,但實現起來比較混亂。如果C語言最初就具備這種特性,可能會更加簡潔和實用。
你是如何處理自省/元數據來構建你的編輯器檢查工具的?
在構建編輯器檢查工具時,通常會使用一種類似的系統進行自我檢測和反思。在正常的引擎中,有一個叫做“元數據系統”的機制,這個系統能夠幫助進行所需的自省工作。這個系統負責對代碼進行檢查和管理,確保所有信息都能被有效地獲取和顯示,用來支持編輯器工具的構建和調試。這種機制可以讓我們更輕松地進行自我檢查和反向操作,提高效率。