在C#中使用適配器Adapter模式和擴展方法解決面向?qū)ο笤O(shè)計問題記錄
之前有陣子在業(yè)余時間拓展自己的一個游戲框架,結(jié)果在實現(xiàn)的過程中發(fā)現(xiàn)一個設(shè)計問題。這個游戲框架基于MonoGame實現(xiàn),在MonoGame中,所有的材質(zhì)渲染(Texture Rendering)都是通過SpriteBatch
類來完成的。舉個例子,假如希望在屏幕的某個地方顯示一個圖片材質(zhì)(imageTexture),就在Game
類的子類的Draw
方法里,使用下面的代碼來繪制圖片:
protected override void Draw(GameTime gameTime) { // ... spriteBatch.Draw(imageTexture, new Vector2(x, y), Color.White); // ... }
那么如果希望在屏幕的某個地方用某個字體來顯示一個字符串,就類似地調(diào)用SpriteBatch
的DrawString
方法來完成:
protected override void Draw(GameTime gameTime) { // ... spriteBatch.DrawString(spriteFont, "Hello World", new Vector2(x, y), Color.White); // ... }
暫時可以不用管這兩個代碼中spriteBatch
對象是如何初始化的,以及Draw
和DrawString
兩個方法的各個參數(shù)是什么意思,在本文討論的范圍中,只需要關(guān)注spriteFont
這個對象即可。MonoGame使用一種叫“內(nèi)容管道”(Content Pipeline)的技術(shù),將各種資源(聲音、音樂、字體、材質(zhì)等等)編譯成xnb
文件,之后,通過ContentManager
類,將這些資源讀入內(nèi)存,并創(chuàng)建相應(yīng)的對象。SpriteFont
就是其中一種資源(字體)對象,在Game
的Load
方法中,可以通過指定xnb
文件名的方式,從ContentManager
獲取字體信息:
private SpriteFont? spriteFont; protected override void LoadContent() { // ... spriteFont = Content.Load<SpriteFont>("fonts\\arial"); // Load from fonts\\arial.xnb // ... }
OK,與MonoGame相關(guān)的知識就介紹這么多。接下來,就進(jìn)入具體問題。由于是做游戲開發(fā)框架,那么為了能夠更加方便地在屏幕上(確切地說是在當(dāng)前場景里)顯示字符串,我封裝了一個Label
類,這個類大致如下所示:
public class Label : VisibleComponent { private readonly SpriteFont _spriteFont; public Label(string text, SpriteFont spriteFont, Vector2 pos, Color color) { Text = text; _spriteFont = spriteFont; Position = pos; TextColor = color; } public string Text { get; set; } public Vector2 Position { get; set; } public Color TextColor { get; set; } protected override void ExecuteDraw(GameTime gameTime, SpriteBatch spriteBatch) => spriteBatch.DrawString(_spriteFont, Text, Position, TextColor); }
這樣實現(xiàn)本身并沒有什么問題,但是仔細(xì)思考不難發(fā)現(xiàn),SpriteFont
是從Content Pipeline讀入的字體信息,而字體信息不僅包含字體名稱,而且還包含字體大?。ㄗ痔枺⑶以赑ipeline編譯的時候就已經(jīng)確定下來了,所以,如果游戲中希望使用同一個字體的不同字號來顯示不同的字符串時,就需要加載多個SpriteFont,不僅麻煩而且耗資源,靈活度也不高。
經(jīng)過一番搜索,發(fā)現(xiàn)有一款開源的字體渲染庫:FontStashSharp,它有MonoGame的擴展,可以基于字體的不同字號,動態(tài)加載字體對象(稱之為“動態(tài)精靈字體(DynamicSpriteFont
)”),然后使用MonoGame原生的SpriteBatch
將字符串以指定的動態(tài)字體顯示在場景中,比如:
private readonly FontSystem _fontSystem = new(); private DynamicSpriteFont? _menuFont; public override void Load(ContentManager contentManager) { // Fonts _fontSystem.AddFont(File.ReadAllBytes("res/main.ttf")); _menuFont = _fontSystem.GetFont(30); } public override void Draw(GameTime gameTime, SpriteBatch spriteBatch) { spriteBatch.DrawString(_menuFont, "Hello World", new Vector2(100, 100), Color.Red); }
在上面的Draw
方法中,仍然是使用了SpriteBatch.DrawString
方法來顯示字符串,不同的地方是,這個DrawString
方法所接受的第一個參數(shù)為DynamicSpriteFont
對象,這個DynamicSpriteFont
對象是第三方庫FontStashSharp提供的,它并不是標(biāo)準(zhǔn)的MonoGame里的類型,所以,這里有兩種可能:
DynamicSpriteFont
是MonoGame中SpriteFont
的子類- FontStashSharp使用了C#擴展方法,對
SpriteBatch
類型進(jìn)行了擴展,使得DrawString
方法可以使用DynamicSpriteFont
來繪制文本
如果是第一種可能,那問題倒也簡單,基本上自己開發(fā)的這個游戲框架可以不用修改,比如在創(chuàng)建Label
實例的時候,構(gòu)造函數(shù)第二個參數(shù)直接將DynamicSpriteFont
對象傳入即可。但不幸的是,這里屬于第二種情況,也就是FontStashSharp中的DynamicSpriteFont
與SpriteFont
之間并沒有繼承關(guān)系。
現(xiàn)在總結(jié)一下,目前的現(xiàn)狀是:
DynamicSpriteFont
并不是SpriteFont
的子類- 兩者提供相似的能力:都能夠被
SpriteBatch
用來繪制文本,都能夠基于給定的文本字符串來計算繪制區(qū)域的寬度和高度(兩者都提供MeasureString
方法) - 我希望在我的游戲框架中能夠同時使用
SpriteFont
和DynamicSpriteFont
,也就是說,我希望Label可以同時兼容SpriteFont
和DynamicSpriteFont
的文本繪制能力
很明顯,可以使用GoF95的適配器(Adapter)模式來解決目前的問題,以滿足上述3的條件。為此,可以定義一個IFontAdapter
接口,然后基于SpriteFont
和DynamicSpriteFont
來提供兩種不同的適配器實現(xiàn),最后,讓框架里的類型(比如Label
)依賴于IFontAdapter
接口即可,UML類圖大致如下:
DynamicSpriteFontAdapter
被實現(xiàn)在一個獨立的包(C#中的Assembly)里,這樣做的目的是防止Mfx.Core項目對FontStashSharp有直接依賴,因為Mfx.Core作為整個游戲框架的核心組件,會被不同的游戲主體或者其它組件引用,而這些組件并不需要依賴FontStashSharp。
此外,同樣可以使用C#的擴展方法特性,讓SpriteBatch
可以基于IFontAdapter
進(jìn)行文本繪制:
public static class SpriteBatchExtensions { public static void DrawString( this SpriteBatch spriteBatch, IFontAdapter fontAdapter, string text) => fontAdapter.DrawString(spriteBatch, text); }
其它相關(guān)代碼類似如下:
public interface IFontAdapter { void DrawString(SpriteBatch spriteBatch, string text); Vector2 MeasureString(string text); } public sealed class SpriteFontAdapter(SpriteFont spriteFont) : IFontAdapter { public Vector2 MeasureString(string text) => spriteFont.MeasureString(text); public void DrawString(SpriteBatch spriteBatch, string text) => spriteBatch.DrawString(spriteFont, text); } public sealed class FontStashSharpAdapter(DynamicSpriteFont spriteFont) : IFontAdapter { public void DrawString(SpriteBatch spriteBatch, string text) => spriteBatch.DrawString(spriteFont, text); public Vector2 MeasureString(string text) => spriteFont.MeasureString(text); } public class Label(string text, IFontAdapter fontAdapter) : VisibleComponent { // 其它成員忽略 public string Text { get; set; } = text; protected override void ExecuteDraw(GameTime gameTime, SpriteBatch spriteBatch) => spriteBatch.DrawString(fontAdapter, Text); }
總結(jié)一下:本文通過對一個實際案例的分析,討論了GoF95設(shè)計模式中的Adapter模式在實際項目中的應(yīng)用,展示了如何使用面向?qū)ο笤O(shè)計模式來解決實際問題的方法。Adapter模式的引入也會產(chǎn)生一些邊界效應(yīng),比如本案例中FontStashSharp的DynamicSpriteFont
其實還能夠提供更多更為豐富的功能特性,然而Adapter模式的使用,使得這些功能特性不能被自制的游戲框架充分使用(因為接口統(tǒng)一,而標(biāo)準(zhǔn)的SpriteFont并不提供這些功能),一種有效的解決方案是,擴展IAdapter
接口的職責(zé),然后使用空對象模式來補全某個適配器中不被支持的功能特性,但這種做法又會在框架設(shè)計中,讓某些類型的層次結(jié)構(gòu)設(shè)計變得特殊化,也就是為了迎合某個外部框架而去做抽象,使得設(shè)計變得不那么純粹,所以,還是需要根據(jù)實際項目的需求來決定設(shè)計的方式。
到此這篇關(guān)于在C#中使用適配器Adapter模式和擴展方法解決面向?qū)ο笤O(shè)計問題記錄的文章就介紹到這了,更多相關(guān)C#面向?qū)ο笤O(shè)計內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
利用C#實現(xiàn)獲取當(dāng)前設(shè)備硬件信息
這篇文章主要為大家詳細(xì)介紹了如何利用C#實現(xiàn)獲取當(dāng)前設(shè)備硬件信息的功能,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起了解一下2023-03-03WPF實現(xiàn)在線預(yù)覽和顯示W(wǎng)ord和PDF文件
這篇文章主要為大家詳細(xì)介紹了如何使用WPF實現(xiàn)在線預(yù)覽和顯示W(wǎng)ord和PDF文件,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-02-02asp.net core項目mvc權(quán)限控制:分配權(quán)限
學(xué)習(xí)的最好方法就是動手去做,這里以開發(fā)一個普通的權(quán)限管理系統(tǒng)的方式來從零體驗和學(xué)習(xí)Asp.net Core。項目的整體規(guī)劃大致如下2017-02-02Unity的IPostBuildPlayerScriptDLLs實用案例深入解析
這篇文章主要為大家介紹了Unity的IPostBuildPlayerScriptDLLs實用案例深入解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05