在C#中使用适配器Adapter模式和扩展方法解决面向的对象设计问题
之前有阵子在业余时间拓展自己的一个游戏框架,结果在实现的过程中发现一个设计问题。这个游戏框架基于MonoGame实现,在MonoGame中,所有的材质渲染(Texture Rendering)都是通过SpriteBatch类来完成的。举个例子,假如希望在屏幕的某个地方显示一个图片材质(imageTexture),就在Game类的子类的Draw方法里,使用下面的代码来绘制图片:
protected override void Draw(GameTime gameTime)
{
// ...
spriteBatch.Draw(imageTexture, new Vector2(x, y), Color.White);
// ...
}
那么如果希望在屏幕的某个地方用某个字体来显示一个字符串,就类似地调用SpriteBatch的DrawString方法来完成:
protected override void Draw(GameTime gameTime)
{
// ...
spriteBatch.DrawString(spriteFont, "Hello World", new Vector2(x, y), Color.White);
// ...
}
暂时可以不用管这两个代码中spriteBatch对象是如何初始化的,以及Draw和DrawString两个方法的各个参数是什么意思,在本文讨论的范围中,只需要关注spriteFont这个对象即可。MonoGame使用一种叫“内容管道”(Content Pipeline)的技术,将各种资源(声音、音乐、字体、材质等等)编译成xnb文件,之后,通过ContentManager类,将这些资源读入内存,并创建相应的对象。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相关的知识就介绍这么多。接下来,就进入具体问题。由于是做游戏开发框架,那么为了能够更加方便地在屏幕上(确切地说是在当前场景里)显示字符串,我封装了一个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);
}
这样实现本身并没有什么问题,但是仔细思考不难发现,SpriteFont是从Content Pipeline读入的字体信息,而字体信息不仅包含字体名称,而且还包含字体大小(字号),并且在Pipeline编译的时候就已经确定下来了,所以,如果游戏中希望使用同一个字体的不同字号来显示不同的字符串时,就需要加载多个SpriteFont,不仅麻烦而且耗资源,灵活度也不高。
经过一番搜索,发现有一款开源的字体渲染库:FontStashSharp,它有MonoGame的扩展,可以基于字体的不同字号,动态加载字体对象(称之为“动态精灵字体(DynamicSpriteFont)”),然后使用MonoGame原生的SpriteBatch将字符串以指定的动态字体显示在场景中,比如:
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方法所接受的第一个参数为DynamicSpriteFont对象,这个DynamicSpriteFont对象是第三方库FontStashSharp提供的,它并不是标准的MonoGame里的类型,所以,这里有两种可能:
DynamicSpriteFont是MonoGame中SpriteFont的子类- FontStashSharp使用了C#扩展方法,对
SpriteBatch类型进行了扩展,使得DrawString方法可以使用DynamicSpriteFont来绘制文本
如果是第一种可能,那问题倒也简单,基本上自己开发的这个游戏框架可以不用修改,比如在创建Label实例的时候,构造函数第二个参数直接将DynamicSpriteFont对象传入即可。但不幸的是,这里属于第二种情况,也就是FontStashSharp中的DynamicSpriteFont与SpriteFont之间并没有继承关系。
现在总结一下,目前的现状是:
DynamicSpriteFont并不是SpriteFont的子类- 两者提供相似的能力:都能够被
SpriteBatch用来绘制文本,都能够基于给定的文本字符串来计算绘制区域的宽度和高度(两者都提供MeasureString方法) - 我希望在我的游戏框架中能够同时使用
SpriteFont和DynamicSpriteFont,也就是说,我希望Label可以同时兼容SpriteFont和DynamicSpriteFont的文本绘制能力
很明显,可以使用GoF95的适配器(Adapter)模式来解决目前的问题,以满足上述3的条件。为此,可以定义一个IFontAdapter接口,然后基于SpriteFont和DynamicSpriteFont来提供两种不同的适配器实现,最后,让框架里的类型(比如Label)依赖于IFontAdapter接口即可,UML类图大致如下:

DynamicSpriteFontAdapter被实现在一个独立的包(C#中的Assembly)里,这样做的目的是防止Mfx.Core项目对FontStashSharp有直接依赖,因为Mfx.Core作为整个游戏框架的核心组件,会被不同的游戏主体或者其它组件引用,而这些组件并不需要依赖FontStashSharp。
此外,同样可以使用C#的扩展方法特性,让SpriteBatch可以基于IFontAdapter进行文本绘制:
public static class SpriteBatchExtensions
{
public static void DrawString(
this SpriteBatch spriteBatch,
IFontAdapter fontAdapter,
string text) => fontAdapter.DrawString(spriteBatch, text);
}
其它相关代码类似如下:
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);
}
总结一下:本文通过对一个实际案例的分析,讨论了GoF95设计模式中的Adapter模式在实际项目中的应用,展示了如何使用面向对象设计模式来解决实际问题的方法。Adapter模式的引入也会产生一些边界效应,比如本案例中FontStashSharp的DynamicSpriteFont其实还能够提供更多更为丰富的功能特性,然而Adapter模式的使用,使得这些功能特性不能被自制的游戏框架充分使用(因为接口统一,而标准的SpriteFont并不提供这些功能),一种有效的解决方案是,扩展IAdapter接口的职责,然后使用空对象模式来补全某个适配器中不被支持的功能特性,但这种做法又会在框架设计中,让某些类型的层次结构设计变得特殊化,也就是为了迎合某个外部框架而去做抽象,使得设计变得不那么纯粹,所以,还是需要根据实际项目的需求来决定设计的方式。
在C#中使用适配器Adapter模式和扩展方法解决面向的对象设计问题的更多相关文章
- 【原】模式之-适配器Adapter模式
适配器Adapter模式 适配器模式(Adapter Pattern)把一个类的接口变换成客户端所期待的的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作. 模式所涉及的角色有 ...
- 设计模式--适配器(Adapter)模式
今天学习另一个设计模式,适配器(Adapter)模式,这是一个共同方向,但有特殊要求,就应用到此设计模式.写到这里,想起很久以前,有写过一篇<ASP.NET的适配器设计模式(Adapter)&g ...
- java演示适配器(adapter)模式
为什么要使用模式: 模式是一种做事的一种方法,也即实现某个目标的途径,或者技术. adapter模式的宗旨就是,保留现有类所提供的服务,向客户提供接口,以满足客户的需求. 类适配器:客户端定义了接口并 ...
- 设计模式C++描述----06.适配器(Adapter)模式
一. 定义 适配器模式将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类可以一起工作. Adapter 模式的两种类别:类模式和对象模式. 二. 举例说明 实际中 ...
- 适配器(Adapter)模式
适配器模式把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作. 适配器模式的一些其他名称:变压器模式.转换器模式.包装(Wrapper)模式.适 ...
- 2、适配器 adapter 模式 加个"适配器" 以便于复用 结构型设计模式
1.什么是适配器模式? 适配器如同一个常见的变压器,也如同电脑的变压器和插线板之间的电源连接线,他们虽然都是3相的,但是电脑后面的插孔却不能直接插到插线板上. 如果想让额定工作电压是直流12伏特的笔记 ...
- Java 实现适配器(Adapter)模式
平时我们会常常碰到这种情况,有了两个现成的类,它们之间没有什么联系.可是我们如今既想用当中一个类的方法.同一时候也想用另外一个类的方法.有一个解决方法是.改动它们各自的接口.可是这是我们最不愿意看到的 ...
- 漫谈设计模式(一):代理(Proxy)模式与适配器(Adapter)模式对比
1.前言 为什么要将代理模式与适配器模式放在一起来说呢?因为它们有许多的共同点,当然也有一些不同的地方.首先两者都是属于结构型模式.结构型模型是这样定义的: 结构型模式涉及到如何组合类和类以获得更大的 ...
- 关于.NET中迭代器的实现以及集合扩展方法的理解
在C#中所有的数据结构类型都实现IEnumerable或IEnumerable<T>接口(实现迭代器模式),可以实现对集合遍历(集合元素顺序访问).换句话可以这么说,只要实现上面这两个接口 ...
- C#开发学习——.net C#中页面之间传值传参的方法以及内置对象
1.QueryString是一种非常简单的传值方式,他可以将传送的值显示在浏览器的地址栏中.如果是传递一个或多个安全性要求不高或是结构简单的数值时,可以使用这个方法.但是对于传递数组或对象的话,就不能 ...
随机推荐
- Linux MySQL 服务设置开机自启动
@ 目录 前言 简介 一.准备工作 二.操作步骤 2.1 启动MySQL服务 2.2 拷贝配置 2.3 赋值权限 2.4 添加为系统服务 2.5 验证 总结 前言 请各大网友尊重本人原创知识分享,谨记 ...
- vue导入项目缺少依赖‘node_modules’
从git下载好的项目,导入vue时提示'node_modules'依赖 则需要在你的项目包下面找是否有package-lock.json文件,如: 如果有,但是依旧报错,直接删除package-loc ...
- AI开源是否应该完全开源?AI的完全开源是否可以实现?
看了一个视频: 袁进辉:零代码改动,加速AIGC 里面提到了一个完全开源的概念,感觉有些意思,虽然觉得可实现性不高,嘿嘿嘿!!! AI的完全开源: 训练数据开源.数据清洗过程开源.模型权重开源.项目代 ...
- 【转载】 在PyTorch训练一个epoch时,模型不能接着训练,Dataloader卡死——在pytorch中尽量不要使用opencv而是使用PIL
版权声明:本文为CSDN博主「时光碎了天」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明.原文链接:https://blog.csdn.net/u013289254/ ...
- 陆吾AI智能机械狗的通讯控制
陆吾AI智能机械狗现在是蛮有名的了,在YouTube上比较火的一个东西了,和波士顿机器狗不同,波士顿机器狗价格昂贵主要原因是其定位于工业领域的机械狗因此采用的是工业级的硬件,但是如果我们采用的家用环境 ...
- mybatis-plus详细的图文教程(含视频讲解)
1.课程大纲 2.目录链接 1.简介与CRUD快速使用 https://www.cnblogs.com/newAndHui/p/13938754.html 2.注解的使用 https://www.cn ...
- C#自定义快捷操作键的实现 - 开源研究系列文章
这次想到应用程序的快捷方式使用的问题. Windows已经提供了API函数能够对窗体的热键进行注册,然后就能够在窗体中使用这些注册的热键进行操作了.于是笔者就对这个操作进行了整理,将注册热键操作写成了 ...
- 用一杯星巴克的钱,训练自己私有化的ChatGPT
文章摘要:用一杯星巴克的钱,自己动手2小时的时间,就可以拥有自己训练的开源大模型,并可以根据不同的训练数据方向加强各种不同的技能,医疗.编程.炒股.恋爱,让你的大模型更"懂"你-. ...
- 推荐一个优秀的 .NET MAUI 组件库
前言 .NET MAUI 的发布,项目中可以使用这个新的跨平台 UI 框架来轻松搭建的移动和桌面应用. 为了帮助大家更快地构建美观且功能丰富的应用,本文将推荐一款优秀的 .NET MAUI 组件库MD ...
- LemurBrowser狐猴浏览器:支持插件扩展、内置免费AI工具的移动端浏览器
如何选择一款合适的浏览器? 在这个数字化时代,浏览器作为互联网的入口.然而,选择一款合适的浏览器却并不容易. 注释:狐猴浏览器是浏览器新标签页插件Wetab提供的支持在移动端安装插件,内置免费AI工具 ...