在C#中使用适配器Adapter模式和扩展方法解决面向对象设计问题

news2024/10/8 17:39:41

之前有阵子在业余时间拓展自己的一个游戏框架,结果在实现的过程中发现一个设计问题。这个游戏框架基于MonoGame实现,在MonoGame中,所有的材质渲染(Texture Rendering)都是通过SpriteBatch类来完成的。举个例子,假如希望在屏幕的某个地方显示一个图片材质(imageTexture),就在Game类的子类的Draw方法里,使用下面的代码来绘制图片:

protected override void Draw(GameTime gameTime)
{
    // ...
    spriteBatch.Draw(imageTexture, new Vector2(x, y), Color.White);
    // ...
}

那么如果希望在屏幕的某个地方用某个字体来显示一个字符串,就类似地调用SpriteBatchDrawString方法来完成:

protected override void Draw(GameTime gameTime)
{
    // ...
    spriteBatch.DrawString(spriteFont, "Hello World", new Vector2(x, y), Color.White);
    // ...
}

暂时可以不用管这两个代码中spriteBatch对象是如何初始化的,以及DrawDrawString两个方法的各个参数是什么意思,在本文讨论的范围中,只需要关注spriteFont这个对象即可。MonoGame使用一种叫“内容管道”(Content Pipeline)的技术,将各种资源(声音、音乐、字体、材质等等)编译成xnb文件,之后,通过ContentManager类,将这些资源读入内存,并创建相应的对象。SpriteFont就是其中一种资源(字体)对象,在GameLoad方法中,可以通过指定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里的类型,所以,这里有两种可能:

  1. DynamicSpriteFont是MonoGame中SpriteFont的子类

  2. FontStashSharp使用了C#扩展方法,对SpriteBatch类型进行了扩展,使得DrawString方法可以使用DynamicSpriteFont来绘制文本

如果是第一种可能,那问题倒也简单,基本上自己开发的这个游戏框架可以不用修改,比如在创建Label实例的时候,构造函数第二个参数直接将DynamicSpriteFont对象传入即可。但不幸的是,这里属于第二种情况,也就是FontStashSharp中的DynamicSpriteFontSpriteFont之间并没有继承关系。

现在总结一下,目前的现状是:

  1. DynamicSpriteFont并不是SpriteFont的子类

  2. 两者提供相似的能力:都能够被SpriteBatch用来绘制文本,都能够基于给定的文本字符串来计算绘制区域的宽度和高度(两者都提供MeasureString方法)

  3. 我希望在我的游戏框架中能够同时使用SpriteFontDynamicSpriteFont,也就是说,我希望Label可以同时兼容SpriteFontDynamicSpriteFont的文本绘制能力

很明显,可以使用GoF95的适配器(Adapter)模式来解决目前的问题,以满足上述3的条件。为此,可以定义一个IFontAdapter接口,然后基于SpriteFontDynamicSpriteFont来提供两种不同的适配器实现,最后,让框架里的类型(比如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接口的职责,然后使用空对象模式来补全某个适配器中不被支持的功能特性,但这种做法又会在框架设计中,让某些类型的层次结构设计变得特殊化,也就是为了迎合某个外部框架而去做抽象,使得设计变得不那么纯粹,所以,还是需要根据实际项目的需求来决定设计的方式。

文章转载自:dax.net

原文链接:https://www.cnblogs.com/daxnet/p/18346121

体验地址:引迈 - JNPF快速开发平台_低代码开发平台_零代码开发平台_流程设计器_表单引擎_工作流引擎_软件架构

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2196860.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

新书速览|你好,C++

《你好&#xff0c;C》 本书内容 《你好&#xff0c;C》主要介绍C开发环境的搭建、基础语法知识、面向对象编程思想以及标准模板库的应用&#xff0c;特别针对初学者在学习C过程中可能遇到的难点提供了解决方案。全书共分13章&#xff0c;以一个工资程序的不断优化和完善为线索…

ChatGPT助力文献综述写作:提升效率与写作技巧!

文献综述在论文写作中占有举足轻重的地位。它不仅帮助我们梳理已有的研究成果&#xff0c;还能为自己的研究奠定基础。许多同学在撰写文献综述时常常感到头疼&#xff1a;如何处理海量的信息&#xff1f;如何将不同的观点有条理地整合起来&#xff1f;再加上学术语言的高要求&a…

定时任务。

引入 1.启动类上加上注解 2.新建一个定时任务的管理类&#xff0c;交给Spring管理 案例 案例1&#xff1a;fixedRate //上次任务开始到下次任务开始的时间间隔为5秒 //每隔5秒执行一次,不需要等上个任务执行完 Scheduled(fixedRate 5000) public void mask01() throws Inte…

python:web自动化工具selenium安装和配置(1)

UI自动化测试 UI自动化测试&#xff08;User Interface Automation Testing&#xff09;是一种通过编写脚本或使用自动化测试工具&#xff0c;对界面&#xff08;UI&#xff09;进行自动化测试的方法。原理主要是模拟用户打开客户端或网页的UI界面&#xff0c;自动化执行用户界…

【Java 问题】基础——泛型

接上文 泛型 47.Java 泛型了解么&#xff1f;什么是类型擦除&#xff1f;介绍一下常用的通配符&#xff1f; 47.Java 泛型了解么&#xff1f;什么是类型擦除&#xff1f;介绍一下常用的通配符&#xff1f; 什么是泛型&#xff1f; Java 泛型&#xff08;generics&#xff09;是…

REINFORCEMENT LEARNING THROUGH ACTIVE INFERENCE

摘要 强化学习&#xff08;RL&#xff09;的核心原则是智能体寻求最大化累积奖励之和。相比之下&#xff0c;主动推理&#xff0c;认知和计算神经科学中的一个新兴框架&#xff0c;提出代理人采取行动&#xff0c;以最大限度地提高有偏见的生成模型的证据。在这里&#xff0c;…

上门安装维修系统小程序开发详解及源码示例

随着智能家居和设备的普及&#xff0c;消费者对上门安装和维修服务的需求日益增加。为了满足这一市场需求&#xff0c;开发一款上门安装维修系统小程序成为了一种有效的解决方案。本文将详细介绍上门安装维修系统小程序的开发过程&#xff0c;并提供一个简单的源码示例&#xf…

人工智能的未来

引言 人工智能的未来发展将是科技与人类社会深度融合的过程。随着技术的不断进步&#xff0c;AI将在全球经济、文化、政治及道德伦理等领域产生深远影响。本文将探讨人工智能在未来可能的技术进步、应用领域、社会影响、伦理挑战&#xff0c;以及对全球未来的展望。 一、技术前…

数据结构之——二叉树

一、二叉树的基本概念 二叉树是数据结构中的重要概念&#xff0c;每个节点最多有两个子树&#xff0c;分别为左子树和右子树。这种结构具有明确的层次性和特定的性质。 二叉树有五种基本形态&#xff1a; 空二叉树&#xff1a;没有任何节点。只有一个根结点的二叉树&#xff…

【HTTPS】深入解析 https

我的主页&#xff1a;2的n次方_ 1. 背景介绍 在使用 http 协议的时候是不安全的&#xff0c;可能会出现运营商劫持等安全问题&#xff0c;运营商通过劫持 http 流量&#xff0c;篡改返回的网页内容&#xff0c;例如广告业务&#xff0c;可能会通过 Referer 字段 来统计是…

kubernetes get pods的STATUS字段显示ImagePullBackOff 的解决办法

问题&#xff1a; [rootmaster ingress]# kubectl -n ingress-nginx get pods NAME READY STATUS RESTARTS AGE ingress-nginx-admission-create-mcrc6 0/1 ImagePullBackOff 0 37m ingress-…

掌握RocketMQ——基本概念和系统架构

简述RcoketMQ 概念&#xff1a;RocketMQ是一个开源的分布式消息中间件&#xff0c;由阿里巴巴开发并贡献给Apache软件基金会。它用于处理高吞吐量、低延迟的消息传递&#xff0c;并广泛应用于现代分布式系统中。 1 基本概念 1.1 消息 (Message) 概念&#xff1a;消息是信息传…

自定义协议以及序列化和反序列化

我们知道TCP是全双工的&#xff0c;可以同时进行发收&#xff0c;因为他有一个发送缓冲区和一个接收缓冲区 我们使用write其实是把数据拷贝到发送缓冲区&#xff0c;使用read接收缓冲区的数据&#xff0c;其实是把数据拷贝到文件缓冲区里&#xff0c;发送的过程中&#xff0c;我…

脸书(Facebook)高效开发国外客户的6个技巧

Facebook作为全球使用人数最多的社媒平台&#xff0c;全球三分之一的人都在用。做外贸的话基本上是必须要去掌握的一个平台&#xff0c;因为通过Facebook是可以开发到很多其他渠道平时开发不到的优质客户的。 Facebook跟LinkedIn不同&#xff0c;LinkedIn比较偏向于大B的客户&…

传热学一些“数”和意义

物体单位面积上的导热热阻/单位表面积上的对流换热热阻 无量纲时间 Nu与Bi的表达式相同&#xff0c;但是意义是无量纲的h。它们表达式里的长度取值不同&#xff0c;比如同样一个平板&#xff0c;Bi的L是厚度&#xff0c;Nu是长度&#xff0c;因为Bi面向固体&#xff0c;λ为固…

八种基本服务器类型,看这篇完全够了

号主&#xff1a;老杨丨11年资深网络工程师&#xff0c;更多网工提升干货&#xff0c;请关注公众号&#xff1a;网络工程师俱乐部 上午好&#xff0c;我的网工朋友。 服务器作为网络基础设施的核心组件&#xff0c;其重要性不言而喻。 无论是个人空间还是大型企业的数据中心&…

激波是什么?

你肯定能听懂。激波&#xff0c;激烈的波&#xff0c;代表特征&#xff1a;激波扫过你时&#xff0c;重则五脏震动&#xff0c;支离破碎。轻则耳膜震动&#xff0c;隆隆作响&#xff0c;当然也有相对你而言尺度很小的激波&#xff0c;没啥伤害。 所以激波&#xff0c;和相对于…

【VScode】VScode如何离线安装扩展

VScode如何离线安装扩展 一&#xff0c;简介二&#xff0c;操作步骤2.1 扩展下载2.2 扩展安装 三&#xff0c;总结 一&#xff0c;简介 本文以“C/C Extension Pack”扩展为例&#xff0c;介绍如何在没有网络的环境下给VScode安装扩展&#xff0c;供参考。 二&#xff0c;操作…

gradle.properties的注释乱码的解决方案

问题描述&#xff1a; gradle项目的配置脚本的注解出现乱码&#xff1a;&#xff08;#&#xff1f;&#xff1f;&#xff1f;&#xff1f;&#xff1f;&#xff09; gradle.properties #??? PRODSERVER2193.168.0.22 解决方案&#xff1a;&#xff08;3步&#xff09; 增…

OpenHarmony(鸿蒙南向开发)——标准系统方案之瑞芯微RK3568移植案例(上)

往期知识点记录&#xff1a; 鸿蒙&#xff08;HarmonyOS&#xff09;应用层开发&#xff08;北向&#xff09;知识点汇总 鸿蒙&#xff08;OpenHarmony&#xff09;南向开发保姆级知识点汇总~ 持续更新中…… 本文章是基于瑞芯微RK3568芯片的DAYU200开发板&#xff0c;进行标准…