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