Unity设计模式——享元模式(附源码)

享元Flyweight模式是什么

享元模式是一种结构型设计模式, 它摒弃了在每个对象中保存所有数据的方式, 通过共享多个对象所共有的相同状态, 让你能在有限的内存容量中载入更多对象。

具体不展开,本篇主旨是在Unity中实现与演示命令模式,有关命令模式的详细介绍可以看:

Refactoring.Guru

结构

具体需求

考虑这么一个需求:

在我的游戏中,同时有一万个敌人。

暂时不考虑渲染压力,单纯设计一个敌人属性的实现。

敌人的属性包括:

  • 敌人的血量
  • 敌人的身高
  • 敌人的职级有四种:新兵,中士,上士,上尉
  • 敌人的血量上限(根据职级不同而变,依次是100,200,500,1000)
  • 敌人的移动速度( 根据职级不同而变,依次是 1.0f , 2.0f , 3.0f , 4.0f )
  • 敌人的职级对应名字(根据职级不同而变,依次是新兵,中士,上士,上尉)

如果不考虑任何模式,你会怎么做?有什么样的问题?


最原始的方案

最原始的做法就是声明一个Attr类,它包含hp,hpMax,mp,mpMax,name。

带来的问题是内存浪费,每一个属性对象都同时开辟了hp,hpMax,mp,mpMax,name的内存空间。

改进方案—使用枚举与字典

把职级做成枚举,职级对应的属性定义一个字典。

其实这个方案非常好,类似的写法我用的非常多。代码大概是这样:

public enum AttrType : uint
{// 新兵Recruit,// 中士StaffSergeant,// 上士Sergeant,// 上尉Captian,
}public static readonly Dictionary<AttrType, Dictionary<string, float>> BaseAttrDict = new Dictionary<AttrType, Dictionary<string, float>>
{{AttrType.Recruit , new Dictionary<string, float>{{"MaxHp",100.0f},{"MoveSpeed",1.0f},}},{AttrType.Recruit , new Dictionary<string, float>{{"MaxHp",200.0f},{"MoveSpeed",2.0f},}},{AttrType.Recruit , new Dictionary<string, float>{{"MaxHp",500.0f},{"MoveSpeed",3.0f},}},  {AttrType.Recruit , new Dictionary<string, float>{{"MaxHp",1000.0f},{"MoveSpeed",4.0f},}},
};

如果需要获取属性,只需要这么调用:

public float GetMapHp()
{return Const.BaseAttrDict[baseAttrType]["MaxHp"];
}public float GetMoveSpeed()
{return Const.BaseAttrDict[baseAttrType]["MoveSpeed"];
}

代码量又少,使用又方便。但是我们的需求里有一项,要求能访问到职级对应的中文名,它的类型是string。

为了实现需求,字典的value不能是float了,而应该是object了。

于是我们把代码改写成这样:

 public static readonly Dictionary<AttrType, Dictionary<string, object>> BaseAttrDict = new Dictionary<AttrType, Dictionary<string, object>>{{AttrType.Recruit , new Dictionary<string, object>{{"MaxHp",100},{"MoveSpeed",1.0f},{"Name","新兵"}}},{AttrType.Recruit , new Dictionary<string, object>{{"MaxHp",200},{"MoveSpeed",2.0f},{"Name","中士"}}},{AttrType.Recruit , new Dictionary<string, object>{{"MaxHp",500},{"MoveSpeed",3.0f},{"Name","上士"}}},  {AttrType.Recruit , new Dictionary<string, object>{{"MaxHp",1000},{"MoveSpeed",4.0f},{"Name","上尉"}}},};

调用的代码就变成这样:

public string GetName()
{return (string)Const.BaseAttrDict[baseAttrType]["Name"];
}public int GetMapHp()
{return (int)Const.BaseAttrDict[baseAttrType]["MaxHp"];
}public float GetMoveSpeed()
{return (float)Const.BaseAttrDict[baseAttrType]["MoveSpeed"];
}

看起来也没什么问题对吧?很简洁清晰。

虽然它不像我们平常看到的巷元模式一样定义一个FlyWeight类,但讲白了这个也是一种FlyWeight的思路:把公有的部分提取到枚举字典里,对象只需要持有一个对枚举对象。

但是这种写法有一个致命的问题:拆箱带来的性能开销。

我们想在一个字典里存下各种类型的变量,被迫使用了object,但是再使用的时候,又要把object拆箱成对应的实际类型。

可别小瞧了这部分开销,如果你在Update里去调用这些属性,情况将变得非常糟糕!

所以这个配置字典方案只有在你不会频繁调用这些属性的情况下才会用。


这里还有个小的变种—引入多个字典来避免拆箱。

即字典还是<enum,float>baseFloatAttr里面包含float的属性,再配置一个<enum,string>baseStringAttr里面再配置字符串的属性。这样就能避免额外的开销。

这么做的问题是代码不好看,本应该是一个BaseAttr的属性,被拆开了,七零八落散在文件里。

找一些属性去一个字典,找另一个属性要去另一个字典,难看。

使用享元

好了终于可以切入正题了,如何用享元实现这个需求。

首先我们在设计上要找到”可共享“的属性,在这个例子中是:maxHp,moveSpeed,name。

我们把这三个属性提取出来,放到我们的FlyweightAttr里

public class FlyweightAttr
{public int maxHp { get; set; }public float moveSpeed { get; set; }public string name { get; set; }public FlyweightAttr(string name, int maxHp, float moveSpeed){this.name = name;this.maxHp = maxHp;this.moveSpeed = moveSpeed;}
}

士兵的属性类SoldierAttr持有FlyweightAttr这个引用,并包含不可共享的属性:hp和height。

public class SoldierAttr
{public int hp { get; set; }public float height { get; set; }public FlyweightAttr flyweightAttr { get; }// 构造函数public SoldierAttr(FlyweightAttr flyweightAttr, int hp, float height){this.flyweightAttr = flyweightAttr;this.hp = hp;this.height = height;}public int GetMaxHp(){return flyweightAttr.maxHp;}public float GetMoveSpeed(){return flyweightAttr.moveSpeed;}public string GetName(){return flyweightAttr.name;}
}

属性类都创建好了,我们再增加一个工厂类来让外部容易获取到属性。

public class AttrFactory
{/// <summary>/// 属性类型枚举/// </summary>public enum AttrType : uint{// 新兵Recruit = 0,// 中士StaffSergeant,// 上士Sergeant,// 上尉Captian,}/// <summary>/// 基础属性缓存/// </summary>private Dictionary<AttrType, FlyweightAttr> _flyweightAttrDB = null;public AttrFactory(){_flyweightAttrDB = new Dictionary<AttrType, FlyweightAttr>();_flyweightAttrDB.Add(AttrType.Recruit, new FlyweightAttr("士兵", 100, 1.0f));_flyweightAttrDB.Add(AttrType.StaffSergeant, new FlyweightAttr("中士", 200, 2.0f));_flyweightAttrDB.Add(AttrType.Sergeant, new FlyweightAttr("上士", 500, 3.0f));_flyweightAttrDB.Add(AttrType.Captian, new FlyweightAttr("上尉", 1000, 4.0f));}/// <summary>/// 获取角色属性/// </summary>/// <param name="type">类型</param>/// <param name="hp">血量</param>/// <param name="height">身高</param>/// <returns></returns>public SoldierAttr GetSoldierAttr(AttrType type, int hp, float height){if (!_flyweightAttrDB.ContainsKey(type)){Debug.LogErrorFormat("{0}属性不存在", type);return null;}FlyweightAttr flyweightAttr = _flyweightAttrDB[type];SoldierAttr attr = new SoldierAttr(flyweightAttr, hp, height);return attr;}
}

演示代码:

通过生成大量(这里是10000个)的SoldierAttr,看看它的内存是怎么样

AttrFactory factory = new AttrFactory();
for (int i = 0; i < _enemy_max; i++)
{var values = Enum.GetValues(typeof(AttrFactory.AttrType));AttrFactory.AttrType attrType = (AttrFactory.AttrType)values.GetValue(UnityEngine.Random.Range(0, 3));SoldierAttr soldierAttr = factory.GetSoldierAttr(attrType, UnityEngine.Random.Range(0, 100), UnityEngine.Random.Range(155.0f, 190.0f));objectsUseFlyweight.Add(soldierAttr);
}

可以看到总共是3.1MB,每个是32B。


为了对比,这里也创建一个最原始的属性类,不用享元模式,所有的属性都是一个字段。

public class HeavySoldierAttr : MonoBehaviour
{public int hp { get; set; }public float height { get; set; }public int maxHp { get; set; }public float moveSpeed { get; set; }public string name { get; set; }public HeavySoldierAttr(int hp, float height, int maxHp, float moveSpeed, string name){this.hp = hp;this.height = height;this.maxHp = maxHp;this.moveSpeed = moveSpeed;this.name = name;}
}

再看看它的内存占用是多少?

for (int i = 0; i < _enemy_max; i++)
{// 这一行代码和上面的享元模式代码不完全相等,没有随机去生成基础类型及相关属性,但随即与否不影响最终的内存分配,懒得写了。HeavySoldierAttr heavySoldierAttr = new HeavySoldierAttr(UnityEngine.Random.Range(0, 100), UnityEngine.Random.Range(155.0f, 190.0f), 1000, 4.0f, "上尉");objectsHeavy.Add(heavySoldierAttr);
}

可以看到总共占用6.1MB,每个占用64B。

当场景里有大量可以共享部分属性的对象时,使用享元模式,很好地降低了内存。

上面的例子是提升了一倍,如果可共享的属性更多,这个差距也会越的更大。

Unity中的享元

Unity中的sharedMesh和sharedMaterial就用了享元模式。

代码

完整代码已上传至nickpansh/Unity-Design-Pattern | GitHub

参考

Habrador/Unity-Programming-Patterns: A collection of open source programming patterns in Unity with examples when to use them (github.com)

Game Programming Patterns

Refactoring.Guru

C#设计模式(第2版) (豆瓣) (douban.com)

设计模式与游戏完美开发 (豆瓣) (douban.com)

Unity设计模式——享元模式(附代码)相关推荐

  1. Python设计模式-享元模式

    Python设计模式-享元模式 基于Python3.5.2,代码如下 #coding:utf-8class Coffee:name = ""price = 0def __init_ ...

  2. 10-Python与设计模式--享元模式

    10-Python与设计模式--享元模式 一.网上咖啡选购平台 假设有一个网上咖啡选购平台,客户可以在该平台上下订单订购咖啡,平台会根据用户位置进行线下配送.假设其咖啡对象构造如下: class Co ...

  3. 设计模式--享元模式实现C++

    /********************************* *设计模式--享元模式实现 *C++语言 *Author:WangYong *Blog:http://www.cnblogs.co ...

  4. 【设计模式】Java设计模式 - 享元模式

    [设计模式]Java设计模式 - 享元模式

  5. 第二十二章 Caché 设计模式 享元模式

    文章目录 第二十二章 Caché 设计模式 享元模式 定义 优点 使用场景 结构图 描述 完整示例 实体类 抽象享元角色 实现享元角色 享元工厂 调用 思考 第二十二章 Caché 设计模式 享元模式 ...

  6. JavaScript设计模式-享元模式

    JavaScript设计模式-享元模式 概念 例子 内部状态与外部状态 享元模式的通用结构 例子 总结 github仓库地址:点击 [设计模式例子](https://github.com/fanhua ...

  7. 设计模式——享元模式(附代码示例)

    一. 传统方式 以网站项目展示为例,有的要求以新闻形式发布,有的要求以博客形式发布,有的要求以微信公众号形式发布 1. 传统方式解决 给每个网站租用一个空间,直接复制粘贴一份,然后根据客户不同要求,进 ...

  8. JAVA 设计模式 享元模式

    用途 享元模式 (Flyweight) 运用共享技术有效地支持大量细粒度的对象. 享元模式是一种结构型模式. 结构 图-享元模式结构图 Flyweight : 它是所有具体享元类的超类或接口,通过这个 ...

  9. java设计模式---享元模式

    Java深入到一定程度,就不可避免的碰到设计模式这一概念,了解设计模式,将使自己 对java中的接口或抽象类应用有更深的理解.设计模式在java的中型系统中应用广 泛,遵循一定的编程模式,才能使自己的 ...

最新文章

  1. Node.js连接MySQL
  2. CGAffineTransform的使用大概:
  3. .project sturcture和Project Structure 无论是按快捷键或者是从files中打开都不显示
  4. 执行srvctl报错 :error while loading shared libraries: libpthread.so.0:
  5. cuDNN version incompatibility: PyTorch was compiled against 7005 but linked against 6021 问题解决...
  6. Excel找出两列相同部分
  7. vmos安卓虚拟手机系统x86_VMOSPro下载-VMOSPro下载v 1.1.26 安卓版-西西软件下载
  8. 数据载入过慢?这里有一份TensorFlow加速指南
  9. Linux下查看和设置环境变量
  10. 关于数学公式的软件和markdown
  11. 计算机cpu的型号与参数,怎么查看CPU型号和CPU核数
  12. 企业邮箱账号,如何在foxmail上创建
  13. 用计算机计算数学公式,Formula Calculator公式计算器
  14. Bootstrap 3 学习
  15. idea破解(到2100年)
  16. YouBank数字钱包
  17. Switchport详细用法
  18. week06_task_二分, 排序
  19. 华为eNSP模拟器软件介绍和基础命令详解
  20. Uncaught ReferenceError: xxx is not defined 解决办法

热门文章

  1. PCB生产企业自动化立体仓库/智能仓库库系统WMS/WCS解决方案
  2. 反三角函数在matlab中怎样定义
  3. 程序猿生存指南-22 分手快乐
  4. 弘辽科技:拼多多推广单元是什么意思?怎么做推广?
  5. jQuery 遍历 - 后代 【后代是子、孙、曾孙等等。children()查找下一个子元素; find()查找所有后代】
  6. Leetcode May Challenge - 05/07: Cousins in Binary Tree(Python)
  7. java clip_java – 使用Clip对象播放多个声音片段
  8. opa example
  9. SAP 财务月结与年结过程
  10. H5获取手机型号,获取具体手机型号