让XNA显示中文
最近在研究XNA。XNA有一個(我們不太用得上的)招牌特性,就是它可以用于制作跨平臺的游戲。這個跨平臺允許你的游戲運行在Windows、Xbox360和Zune HD上。聽起來是一個不錯的主意,不過實現平臺兼容性往往意味著要舍棄特定平臺上的專屬功能,比如我們今天要說的話題:字體。
雖然我們可以說在Windows平臺上,XNA用DirectX實現,但是XNA沒有使用任何DX中有關字體的功能(D3DFont),原因很簡單,X360沒有這玩意兒,Zune HD也沒有。XNA內建的字體支持,是通過事先把文字渲染到貼圖上來實現的。在開發階段,你需要在你的游戲工程下的Content子工程里編寫一個spritefont文件,指定使用的字體、字號以及要導入的字符范圍等信息,大概如下所示:
<?xml version="1.0" encoding="utf-8"?> <XnaContent xmlns:Graphics="Microsoft.Xna.Framework.Content.Pipeline.Graphics"><Asset Type="Graphics:FontDescription"><FontName>Kootenay</FontName><Size>14</Size><Spacing>0</Spacing><UseKerning>true</UseKerning><Style>Regular</Style><CharacterRegions><CharacterRegion><Start> </Start><End>~</End></CharacterRegion></CharacterRegions></Asset> </XnaContent>這一段是XNA自動生成的代碼,只需要在Content子工程中通過添加項對話框添加一個SpriteFont文件就可以了。這個文件導入了Kootenay字體的部分內容,從 到~。這是基本拉丁字符集的范圍,顯示英文是足夠了。
這個spritefont會在工程編譯之時被“編譯”成一個xnb文件,也就是XNA資源的最常見格式——雖然我們很難看到里面究竟存的是什么東西。之后只需要將這個xnb加載為SpriteFont對象,就可以用它來寫字了。給你的Game類增加一個私有成員:
private SpriteFont Font;接下來在LoadContents方法中加上:
this.Font = Content.Load<SpriteFont>("SpriteFont1");然后就可以在Draw方法里寫字了:
spriteBatch.Begin(); spriteBatch.DrawString(this.Font, "Good day commander!", Vector2.Zero, Color.White); spriteBatch.End();運行你的程序??雌饋聿诲e是嗎?我們來試試把Good day commander!改成中文。我很確信你的程序一定會拋出一個ArgumentException異常,告訴你某某字符在這個SpriteFont中沒有定義。很顯然,因為我們之前在編寫spritefont文件的時候,那個文件只定義了一些基本的拉丁字符。
好吧,我們來試試看,改一改spritefont文件。找到這么一段:
<CharacterRegion><Start> </Start><End>~</End></CharacterRegion>把它改成
<CharacterRegion><Start> </Start><End></End></CharacterRegion>在Unicode字符集中,編碼為32到65536的字符都被囊括進來了,看起來很棒!按下F5,我們來看看效果……
我不知道你是不是足夠有耐心,如果是的話,大概六個小時還是七個小時還是其他的一個什么時間之后,你的程序應該會帶著一個好幾十MB的xnb資源跑了起來。
XNA編譯字體就是這么效率低下,而且那個巨大的資源文件一定會吃掉你大量的顯存。雖然這樣做給顯示中文提供了可能,但這必然不是什么好辦法,我們得另尋出路。
既然XNA給出的解決方案不能滿足我們,我們能否求助于其他方案?
最先出現在腦海中的方法是使用GDI+。GDI+是絕大多數Windows用戶界面的繪制引擎,顯示幾個漢字必然不在話下。但是我們不太好直接讓GDI+向我們的游戲窗口寫字,最靠譜的方式是用它把我們要寫的文字畫到一張貼圖上,然后在游戲中渲染這張貼圖。Clayman老師在博客上詳細探討了這種方案,有興趣可以去看一看。
第二種方案是使用DirectX,假設我們可以拿到我們的游戲的DeviceContext,然后我們可以利用DX在上面寫字……嗯,我沒有試過,不過我覺得從裹得嚴嚴實實的XNA中拿到DeviceContext是一件很困難的事情。
以上兩種方式,雖然都有一定的可行性,可是它們還是違背了XNA的初衷,做出了一個重大犧牲:無論是使用GDI+還是DX,我們的游戲都失去了跨平臺的能力。不過如果你壓根就不打算考慮這一點,那倒是很無所謂的了。
所以,如果要保留跨平臺的特性,我們恐怕還是只有繞回原來的路子,畢竟如果要DIY從讀取字體到渲染文字的全過程,工作量是非常大的。
讓我們來明確一下目標。我們的目標是渲染中文文字,而且這一次,要具體到在不犧牲跨平臺的特性下,以盡可能簡便、盡可能高效的方式渲染中文文字。
無疑,XNA提供的方式是高效的,因為字型都被預先畫到了貼圖上,再需要渲染相應的文字時,無需再圍繞著字體做更多的處理。但是把那么多字符全都放到一張貼圖上,顯然是極其低效的,這樣不僅加載速度慢、要占用大量顯存,還會讓找字的過程變得復雜:每當要渲染一個字符時,都要從包含60,000多個字符的貼圖中尋找特定的那一小塊,那得多慢啊。不如我們來拆解一下渲染文字的整個過程:
可以看出,性能的瓶頸在于第1步和第3步。對于第1步,我們必須想辦法減小要加載的字體貼圖文件的尺寸。減小字號是一個辦法,字號越小,每個字符在貼圖上占據的面積也就越小,貼圖也就相應變小了。不過字號再小,貼圖文件也逃脫不了幾十MB的命運。那么,如果我們把這張大貼圖打散成很多張小貼圖,比如說每個字符一張貼圖(這樣就有60,000多張貼圖了……),會怎么樣?
這樣問題也很明顯。第一是小文件的讀寫性能肯定是要比大文件低的,每渲染一個文字就要讀一個貼圖文件,效率會相當低。第二,我們尚不知曉XNA的內部實現機制,不過一般來說,在渲染的過程中,切換貼圖總是一個效率很低的操作。每渲染一個字符就要切換一張貼圖,這也會產生新的性能瓶頸。如果多個文字處在同一張貼圖上,渲染每個文字時,只需要切換紋理坐標到相應字符的位置,而不需要切換貼圖,效率會得到很大提升。
所以我們的問題變成了在一張貼圖和幾萬張貼圖中間尋找一個平衡點。一方面,我們希望貼圖的尺寸盡可能的小;另一方面,我們希望加載貼圖和切換貼圖的次數盡可能的少。
在Unicode字符集中,為漢字準備的位置有幾萬個。不過顯然,這幾萬個漢字中,只有相當少的一部分會被經常用到,剩下的幾乎只會在火星文中出現。那么,如果我們把常用漢字整理出來,然后再加上標點符號、拉丁字符之類的常用字符,放在一張貼圖上,就可以在一個足夠小的貼圖上實現渲染多數字符的時候不需要切換貼圖的效果了。這個主意看起來不錯,所以我們馬上發動Google,找到了一張漢字字頻表。這張字頻表來自北大CCL(漢語語言學研究中心),應該是比較權威的。它包括了九千余漢字的字頻,應該是很夠用了。我們就挑選這個表中的前3,000個漢字,定義為常用漢字吧(排在第3000位的“雍”字,在整個統計中的出現率已經低至萬分之0.085了)。
怎么把它們編譯進xnb文件?相信這個難不倒你。你當然不會傻兮兮地一個字一個字去查它們的Unicode編碼,然后填到spritefont文件里啦。寫一個程序,很快就可以搞定這個問題,這里就不贅述了。提示一下,在spritefont文件的<CharacterRegions>中,可以包含無數個<CharacterRegion>標簽。
新的spritefont文件的尺寸有了顯著的提升,由于有3,000多個字符,所以編譯起來還是有點慢,不過編出來的xnb文件只有2MB(Droid Sans Fallback字體,14號),完全可以接受。用這個字體來渲染漢字,基本上是沒有問題了。不過在字頻表中3,000名開外的地方的字也還是蠻常見的,而且萬一我們真的要渲染火星文怎么辦?
我的解決方法是建立一個多層緩存結構。當需要渲染一個字符時,先從最常用的3,000個漢字里面找;找不到再從次常用的漢字里面找,還找不到就把整個Unicode字符集分塊建立成若干個spritefont,逐個查找:
| 1級 | 最常用的字符 | 3000個常用漢字、標點符號、拉丁字符 |
| 2級 | 次常用的漢字 | 3000個次常用漢字 |
| 3級 | 不常用漢字 | 3000個不常用漢字 |
| 4級 | 拉丁字符全集 | 包括帶有調號的拉丁字符集,支持法語、德語等 |
| 4級 | CJK雜項字符 | 平假名、片假名、制表符等等中日韓字符元素 |
| 5級 | 漢字全集(5個文件) | 將U+4E00到U+9FFF中的所有漢字字符等分成5份。 |
| 6級 | 希臘和西里爾語字符 | ? |
| 6級 | 其他 | 各種一輩子都用不上的字符 |
當然你可以根據你的需要調整這個結構。關于這個結構的優化,也有很多文章可做。我封裝了一個CachedSpriteFont類,并對查找字符的過程做了充足的優化。比如說,我們要渲染一個很火星的字符,從1級查到4級都沒有發現它的蹤影,最后它出現在了5級中的最后一個文件。這樣前前后后大概要遍歷兩三萬個字符才能找到它,效率很是低下。我的解決辦法是在整個緩存結構建立好之后,構建一張哈希表(SortedList<char, SpriteFont>),把應該使用哪個貼圖來渲染某個特定字符的信息寫進這張表里,之后只需要查表就好辦了。不過,構建這張表也是比較緩慢的,在我這里大概需要8秒鐘。所以我選擇在構建好表之后把它序列化進一個磁盤文件中,下次只需要從這個文件中加載就可以了。
需要優化的地方還很多,不過這次我們可以完美地在XNA中顯示中文了。如果完全使用我的方式,Droid Sans Fallback字體在14號下編譯出的xnb貼圖文件的總大小是18.5MB,不過我們幾乎不會需要完全加載它們;而且經過充分的優化,渲染的速度是很快的,比起一個肆無忌憚地揮霍系統資源的游戲來說,完全可以忽略不計。
轉載于:https://www.cnblogs.com/gdev/archive/2012/08/22/display-chinese-in-xna.html
超強干貨來襲 云風專訪:近40年碼齡,通宵達旦的技術人生總結