Unity设计模式——享元模式(附源码)
享元Flyweight模式是什么
享元模式是一种结构型设计模式, 它摒弃了在每个对象中保存所有数据的方式, 通过共享多个对象所共有的相同状态, 让你能在有限的内存容量中载入更多对象。
具体不展开,本篇主旨是在Unity中实现与演示命令模式,有关命令模式的详细介绍可以看:
结构
具体需求
考虑这么一个需求:
在我的游戏中,同时有一万个敌人。
暂时不考虑渲染压力,单纯设计一个敌人属性的实现。
敌人的属性包括:
- 敌人的血量
- 敌人的身高
- 敌人的职级有四种:新兵,中士,上士,上尉
- 敌人的血量上限(根据职级不同而变,依次是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