- VisualStudio2022插件的安装及使用-编程手把手系列文章
- pprof-在现网场景怎么用
- C#实现的下拉多选框,下拉多选树,多级节点
- 【学习笔记】基础数据结构:猫树
之前有阵子在业余时间拓展自己的一个游戏框架,结果在实现的过程中发现一个设计问题。这个游戏框架基于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
的子类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模式和扩展方法解决面向对象设计问题的文章就讲到这里了,如果你想了解更多关于在C#中使用适配器Adapter模式和扩展方法解决面向对象设计问题的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我想了解 Ruby 方法 methods() 是如何工作的。 我尝试使用“ruby 方法”在 Google 上搜索,但这不是我需要的。 我也看过 ruby-doc.org,但我没有找到这种方法。
Test 方法 对指定的字符串执行一个正则表达式搜索,并返回一个 Boolean 值指示是否找到匹配的模式。 object.Test(string) 参数 object 必选项。总是一个
Replace 方法 替换在正则表达式查找中找到的文本。 object.Replace(string1, string2) 参数 object 必选项。总是一个 RegExp 对象的名称。
Raise 方法 生成运行时错误 object.Raise(number, source, description, helpfile, helpcontext) 参数 object 应为
Execute 方法 对指定的字符串执行正则表达式搜索。 object.Execute(string) 参数 object 必选项。总是一个 RegExp 对象的名称。 string
Clear 方法 清除 Err 对象的所有属性设置。 object.Clear object 应为 Err 对象的名称。 说明 在错误处理后,使用 Clear 显式地清除 Err 对象。此
CopyFile 方法 将一个或多个文件从某位置复制到另一位置。 object.CopyFile source, destination[, overwrite] 参数 object 必选
Copy 方法 将指定的文件或文件夹从某位置复制到另一位置。 object.Copy destination[, overwrite] 参数 object 必选项。应为 File 或 F
Close 方法 关闭打开的 TextStream 文件。 object.Close object 应为 TextStream 对象的名称。 说明 下面例子举例说明如何使用 Close 方
BuildPath 方法 向现有路径后添加名称。 object.BuildPath(path, name) 参数 object 必选项。应为 FileSystemObject 对象的名称
GetFolder 方法 返回与指定的路径中某文件夹相应的 Folder 对象。 object.GetFolder(folderspec) 参数 object 必选项。应为 FileSy
GetFileName 方法 返回指定路径(不是指定驱动器路径部分)的最后一个文件或文件夹。 object.GetFileName(pathspec) 参数 object 必选项。应为
GetFile 方法 返回与指定路径中某文件相应的 File 对象。 object.GetFile(filespec) 参数 object 必选项。应为 FileSystemObject
GetExtensionName 方法 返回字符串,该字符串包含路径最后一个组成部分的扩展名。 object.GetExtensionName(path) 参数 object 必选项。应
GetDriveName 方法 返回包含指定路径中驱动器名的字符串。 object.GetDriveName(path) 参数 object 必选项。应为 FileSystemObjec
GetDrive 方法 返回与指定的路径中驱动器相对应的 Drive 对象。 object.GetDrive drivespec 参数 object 必选项。应为 FileSystemO
GetBaseName 方法 返回字符串,其中包含文件的基本名 (不带扩展名), 或者提供的路径说明中的文件夹。 object.GetBaseName(path) 参数 object 必
GetAbsolutePathName 方法 从提供的指定路径中返回完整且含义明确的路径。 object.GetAbsolutePathName(pathspec) 参数 object
FolderExists 方法 如果指定的文件夹存在,则返回 True;否则返回 False。 object.FolderExists(folderspec) 参数 object 必选项
FileExists 方法 如果指定的文件存在返回 True;否则返回 False。 object.FileExists(filespec) 参数 object 必选项。应为 FileS
我是一名优秀的程序员,十分优秀!