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

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)

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

NickPansh

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值