在C#中使用適配器Adapter模式和擴(kuò)展方法解決面向?qū)ο笤O(shè)計(jì)問(wèn)題記錄
之前有陣子在業(yè)余時(shí)間拓展自己的一個(gè)游戲框架,結(jié)果在實(shí)現(xiàn)的過(guò)程中發(fā)現(xiàn)一個(gè)設(shè)計(jì)問(wèn)題。這個(gè)游戲框架基于MonoGame實(shí)現(xiàn),在MonoGame中,所有的材質(zhì)渲染(Texture Rendering)都是通過(guò)SpriteBatch類來(lái)完成的。舉個(gè)例子,假如希望在屏幕的某個(gè)地方顯示一個(gè)圖片材質(zhì)(imageTexture),就在Game類的子類的Draw方法里,使用下面的代碼來(lái)繪制圖片:
protected override void Draw(GameTime gameTime)
{
// ...
spriteBatch.Draw(imageTexture, new Vector2(x, y), Color.White);
// ...
}那么如果希望在屏幕的某個(gè)地方用某個(gè)字體來(lái)顯示一個(gè)字符串,就類似地調(diào)用SpriteBatch的DrawString方法來(lái)完成:
protected override void Draw(GameTime gameTime)
{
// ...
spriteBatch.DrawString(spriteFont, "Hello World", new Vector2(x, y), Color.White);
// ...
}暫時(shí)可以不用管這兩個(gè)代碼中spriteBatch對(duì)象是如何初始化的,以及Draw和DrawString兩個(gè)方法的各個(gè)參數(shù)是什么意思,在本文討論的范圍中,只需要關(guān)注spriteFont這個(gè)對(duì)象即可。MonoGame使用一種叫“內(nèi)容管道”(Content Pipeline)的技術(shù),將各種資源(聲音、音樂(lè)、字體、材質(zhì)等等)編譯成xnb文件,之后,通過(guò)ContentManager類,將這些資源讀入內(nèi)存,并創(chuàng)建相應(yīng)的對(duì)象。SpriteFont就是其中一種資源(字體)對(duì)象,在Game的Load方法中,可以通過(guò)指定xnb文件名的方式,從ContentManager獲取字體信息:
private SpriteFont? spriteFont;
protected override void LoadContent()
{
// ...
spriteFont = Content.Load<SpriteFont>("fonts\\arial"); // Load from fonts\\arial.xnb
// ...
}OK,與MonoGame相關(guān)的知識(shí)就介紹這么多。接下來(lái),就進(jìn)入具體問(wèn)題。由于是做游戲開(kāi)發(fā)框架,那么為了能夠更加方便地在屏幕上(確切地說(shuō)是在當(dāng)前場(chǎng)景里)顯示字符串,我封裝了一個(gè)Label類,這個(gè)類大致如下所示:
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);
}這樣實(shí)現(xiàn)本身并沒(méi)有什么問(wèn)題,但是仔細(xì)思考不難發(fā)現(xiàn),SpriteFont是從Content Pipeline讀入的字體信息,而字體信息不僅包含字體名稱,而且還包含字體大?。ㄗ痔?hào)),并且在Pipeline編譯的時(shí)候就已經(jīng)確定下來(lái)了,所以,如果游戲中希望使用同一個(gè)字體的不同字號(hào)來(lái)顯示不同的字符串時(shí),就需要加載多個(gè)SpriteFont,不僅麻煩而且耗資源,靈活度也不高。
經(jīng)過(guò)一番搜索,發(fā)現(xiàn)有一款開(kāi)源的字體渲染庫(kù):FontStashSharp,它有MonoGame的擴(kuò)展,可以基于字體的不同字號(hào),動(dòng)態(tài)加載字體對(duì)象(稱之為“動(dòng)態(tài)精靈字體(DynamicSpriteFont)”),然后使用MonoGame原生的SpriteBatch將字符串以指定的動(dòng)態(tài)字體顯示在場(chǎng)景中,比如:
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方法來(lái)顯示字符串,不同的地方是,這個(gè)DrawString方法所接受的第一個(gè)參數(shù)為DynamicSpriteFont對(duì)象,這個(gè)DynamicSpriteFont對(duì)象是第三方庫(kù)FontStashSharp提供的,它并不是標(biāo)準(zhǔn)的MonoGame里的類型,所以,這里有兩種可能:
DynamicSpriteFont是MonoGame中SpriteFont的子類- FontStashSharp使用了C#擴(kuò)展方法,對(duì)
SpriteBatch類型進(jìn)行了擴(kuò)展,使得DrawString方法可以使用DynamicSpriteFont來(lái)繪制文本
如果是第一種可能,那問(wèn)題倒也簡(jiǎn)單,基本上自己開(kāi)發(fā)的這個(gè)游戲框架可以不用修改,比如在創(chuàng)建Label實(shí)例的時(shí)候,構(gòu)造函數(shù)第二個(gè)參數(shù)直接將DynamicSpriteFont對(duì)象傳入即可。但不幸的是,這里屬于第二種情況,也就是FontStashSharp中的DynamicSpriteFont與SpriteFont之間并沒(méi)有繼承關(guān)系。
現(xiàn)在總結(jié)一下,目前的現(xiàn)狀是:
DynamicSpriteFont并不是SpriteFont的子類- 兩者提供相似的能力:都能夠被
SpriteBatch用來(lái)繪制文本,都能夠基于給定的文本字符串來(lái)計(jì)算繪制區(qū)域的寬度和高度(兩者都提供MeasureString方法) - 我希望在我的游戲框架中能夠同時(shí)使用
SpriteFont和DynamicSpriteFont,也就是說(shuō),我希望Label可以同時(shí)兼容SpriteFont和DynamicSpriteFont的文本繪制能力
很明顯,可以使用GoF95的適配器(Adapter)模式來(lái)解決目前的問(wèn)題,以滿足上述3的條件。為此,可以定義一個(gè)IFontAdapter接口,然后基于SpriteFont和DynamicSpriteFont來(lái)提供兩種不同的適配器實(shí)現(xiàn),最后,讓框架里的類型(比如Label)依賴于IFontAdapter接口即可,UML類圖大致如下:

DynamicSpriteFontAdapter被實(shí)現(xiàn)在一個(gè)獨(dú)立的包(C#中的Assembly)里,這樣做的目的是防止Mfx.Core項(xiàng)目對(duì)FontStashSharp有直接依賴,因?yàn)镸fx.Core作為整個(gè)游戲框架的核心組件,會(huì)被不同的游戲主體或者其它組件引用,而這些組件并不需要依賴FontStashSharp。
此外,同樣可以使用C#的擴(kuò)展方法特性,讓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é)一下:本文通過(guò)對(duì)一個(gè)實(shí)際案例的分析,討論了GoF95設(shè)計(jì)模式中的Adapter模式在實(shí)際項(xiàng)目中的應(yīng)用,展示了如何使用面向?qū)ο笤O(shè)計(jì)模式來(lái)解決實(shí)際問(wèn)題的方法。Adapter模式的引入也會(huì)產(chǎn)生一些邊界效應(yīng),比如本案例中FontStashSharp的DynamicSpriteFont其實(shí)還能夠提供更多更為豐富的功能特性,然而Adapter模式的使用,使得這些功能特性不能被自制的游戲框架充分使用(因?yàn)榻涌诮y(tǒng)一,而標(biāo)準(zhǔn)的SpriteFont并不提供這些功能),一種有效的解決方案是,擴(kuò)展IAdapter接口的職責(zé),然后使用空對(duì)象模式來(lái)補(bǔ)全某個(gè)適配器中不被支持的功能特性,但這種做法又會(huì)在框架設(shè)計(jì)中,讓某些類型的層次結(jié)構(gòu)設(shè)計(jì)變得特殊化,也就是為了迎合某個(gè)外部框架而去做抽象,使得設(shè)計(jì)變得不那么純粹,所以,還是需要根據(jù)實(shí)際項(xiàng)目的需求來(lái)決定設(shè)計(jì)的方式。
到此這篇關(guān)于在C#中使用適配器Adapter模式和擴(kuò)展方法解決面向?qū)ο笤O(shè)計(jì)問(wèn)題記錄的文章就介紹到這了,更多相關(guān)C#面向?qū)ο笤O(shè)計(jì)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C#中將dateTimePicker初始值設(shè)置為空
本文主要介紹了C#中將dateTimePicker初始值設(shè)置為空,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-02-02
WPF自定義控件實(shí)現(xiàn)ItemsControl魚(yú)眼效果
這篇文章主要為大家詳細(xì)介紹了WPF如何通過(guò)自定義控件實(shí)現(xiàn)ItemsControl魚(yú)眼效果,文中的示例代碼講解詳細(xì),需要的可以參考一下2024-01-01
C#使用Spire.XLS for .NET將Markdown轉(zhuǎn)換為PDF和Excel的完整指南
在當(dāng)今數(shù)字化的世界中,Markdown已成為開(kāi)發(fā)者、作者和內(nèi)容創(chuàng)作者的首選標(biāo)記語(yǔ)言,然而,當(dāng)我們需要將這些內(nèi)容進(jìn)行分發(fā)、歸檔或進(jìn)行數(shù)據(jù)分析時(shí),Markdown的純文本格式便顯得力不從心了,本文將深入探討如何在C# .NET環(huán)境中將Markdown內(nèi)容轉(zhuǎn)換為PDF和Excel格式2025-12-12

