19年5月下旬,CA发行了一款经典战争策略游戏《全战三国》,发行不到一周,玩家高峰在线数达20w,IGN给出9.3分好评,我看某主播玩了几日,从画质画风到配音到游戏内容都可圈可点。游戏里面,玩家可以从12诸侯势力中选择一个,从外交到内政到建设等等全面经营,逐步到达人生的巅峰,,,,我们从游戏开发角度来着重研究一下小战场功能的实现。
小战场上,玩家要操作弓箭手、弓弩手、骑兵、投石车兵等很多个兵种作战,每次小战场投入兵力都在千位数以上,多者七八千甚至更高,虽然游戏中每个单位的兵力阵列都是百人左右的npc代替,但是整场战争下来,游戏要绘制的npc数量我估计也会有千八百,并且没有上限。如果把每个兵种封装成类,每个npc都实例化出一个对象的话,上千个类实例同时绘画,需要对性能的要求有多大?会吃多少内存?
当然对于CA开发商来说,这些优化自有其一套成熟的方案,我这样的毛头小子是无法企及的,但是如果是我去开发,我会想怎样才能尽可能的对内存做最大的优化。
我们先来分析特征,我们发现战场上的所有npc,他们的绘制行为即:哪个位置的npc向哪里攻击,如何攻击。每个npc的位置信息都不同;但是他们攻击方式就那么几种,这取决于它们的兵种,不同的兵种有不同的攻击动作,兵种就那么多,兵种可能会增加减少发生变化,但是兵种不是随环境发生变化的,而是阶段性需求变化。如果我们把npc位置信息和兵种都作为类的内部状态,那么必然,我们要对每个npc都实例化,但是如果我们把位置信息提出来,只在使用它的时候再以参数的形式传给类实例,那我们只需要每个兵种实例化一个对象就可以。例如弓箭手兵种实例化出一个对象,这个对象提供一个绘制行为接口,根据应用场景传给的位置信息(哪个位置的士兵、攻击位置)进行绘制,发射弓箭到指定位置区域。
这样我们就大大的减少了对象实例化的个数,我们把造成对象细粒度化的状态信息封装在类外部作为外部状态,在使用的时候让应用场景传给我们,同时把不变的状态信息放在类内部作为内部状态,将变化的部分参数化,这样实例化出来的对象其实并不是一个具体的对象的代表,是一坨相同(外部状态除外)对象的通用化。
那么为什仫不将兵种也封装到类外面作为外部信息呢?因为我愿意,,,,我认为将本来属于类的状态属性放在外面并在使用的时候再传递给它是影响了类的封装性的,并且一组对象变成一个通用版,用参数的形式去处理实际对象,这明显会增加类的复杂度。所以对于那些不变(可能多种,但是不变)的状态属性我觉得还是放在类内部好一些;而对于那些不随环境发生改变(可能会改变,但是改变的因素是需求)的状态属性我甚至觉得抽象出基类更好一些。例如我们把弓箭手、弓弩手、骑兵、投石车兵类做抽象,抽象出来的虚基类规范了绘制行为接口Fire(point pt,point target),所有兵种类在此基础上派生,应用场景使用虚基类引用(c++指针)调用具体的对象,这时候再增加一个长矛兵兵种,只需要再虚基类基础上派生出一个新的类即可。
上面例子描述的终极目的就是将部分状态属性封装成外部状态,使对一个对象可以共享使用,甚至维护成一个对象池,提供一批对象来支持共享操作,这就是我们要说的享元模式。
享元模式:运用共享技术有效的支持大量细粒度的对象
一、特征
1:对象,此模式针对对象,将大量可外置状态的对象进行统一,也就是说用一个实例化好的对象实现一组具体对象的行为功能,从而达到对象级别的共享。所以最后供给应用场景的绝对是一个对象。
2:细粒度,什么样的对象叫细粒度对象,就是说,这些对象只有小部分属性值不同,将这部分造成细粒度化的属性提取出来,对象可以统一成一个。
3:共享,共享一般情况下不止一个对象,而是一个对象池,这个池子里可能有一个类的多个对象,也可能有N个类实例化后的N个对象(兵种),当然复杂情况下也有可能使两者的组合,这就需要一个管理类来规划管理这个池子。
二、作用:
在某些应用场景里,我们可能会实例化出来很多很多对象,这些对象有着相似的调用方式,有着相似的属性,甚至这些对象都是由一个类实例化出来的。当这些对象的个数影响到我们的程序性能时,我们就可以考虑使用该模式。
例如过多的对象同时存在会消耗大量内存,再如初始化消耗大量资源的对象(读取文件、socket等耗时io操作)频繁创建和释放造成大量损耗。这时我们可以把造成对象不同的状态属性提出来作为外部状态,将不变的或不随环境变化的状态属性作为内部状态,将这个或这些类进行重新封装,并维护一个对象池来盛放这些对象实例,从而达到一个对象就能代替一组实际对象完成操作的目的。
三、实现
由图可知适配器模式包含以下三个角色:
1:Flayweight(享元抽象类):将应用场景对实际对象的使用方法进行抽象,同时确定有没有外部状态,以及如何组织外部状态的参数传入形式。
2:ConcreteFlyweight(具体享元类):它具体实现了对外部状态的使用,判断是否有内部状态,并组织内部状态的表现形式(直接封装到类里,还是派生成多个类,我觉得都有可能)。
3:FlyweightFactory(享元工厂类):负责享元对象池的创建(饿汉、懒汉)、维护(根据用途不同可以分为很多种,可以维护一个类的指定数量的对象作为对象池,可以将一个类的对象根据某些属性进行分组实例化来作为对象池,可以维护多个类的实例化对象来作为对象池,自己发挥)。
我们来看看小战场功能如何实现:
//位置信息
public class Point {
public int x;
public int y;
public Point(int x,int y)
{
this.x = x;
this.y = y;
}
}
//享元抽象类,pos参数为外部装填
public abstract class Flyweight {
abstract void draw(Point pos,Point target);
}
//具体享元类,弓兵
public class Bowmen extends Flyweight
@Override
void draw(Point pos, Point target) {
// TODO Auto-generated method stub
System.out.printf("坐标(%d,%d)处的弓兵,向(%d,%d)坐标放箭\n", pos.x,pos.y,target.x,target.y);
}
}
//具体享元类,骑兵
public class Cavalryman extends Flyweight{
@Override
void draw(Point pos, Point target) {
// TODO Auto-generated method stub
System.out.printf("坐标(%d,%d)处的骑兵,向(%d,%d)坐标发起冲锋\n", pos.x,pos.y,target.x,target.y);
}
}
//享元管理类
public class FlyweightFactory {
private Map
public FlyweightFactory()
{
this.myList = new HashMap();
Bowmen bowmen = new Bowmen();
Cavalryman caval = new Cavalryman();
myList.put(1, bowmen);
myList.put(2, caval);
}
Flyweight getFlyweight(int type)
{
return myList.get(type);
}
}
//应用场景
public class Client {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
FlyweightFactory fac = new FlyweightFactory();
Flyweight flyweight = null;
flyweight = fac.getFlyweight(1);
Point pt1 = new Point(1,1);
Point pt2 = new Point(100,100);
flyweight.draw(pt1,pt2);
Point pt3 = new Point(1,2);
flyweight.draw(pt3,pt2);
flyweight = fac.getFlyweight(2);
Point pt4 = new Point(200,200);
flyweight.draw(pt4,pt2);
}
}
//输出结果
坐标(1,1)处的弓兵,向(100,100)坐标放箭
坐标(1,2)处的弓兵,向(100,100)坐标放箭
坐标(200,200)处的骑兵,向(100,100)坐标发起冲锋
四、总结
1、本质,我认为享元模式的本质就是创建并维护一个对象池,这个对象池中的对象可以共享给一组对象使用,这样无论从内存占用还是资源消耗方面考虑都有优势,但是伴随的就是代码的复杂度。
2、工厂模式,两者区别在哪里?他们都有一个工厂,但是工厂的目的不一样,工厂模式目标是创建,工厂负责从一堆使用方式相同的产品中实例化出一个用户想要的产品实例,后续产品实例的维护使用它就不管了,并且工厂模式更偏向于产品类级别上的划分;而享元模式目标是使用,工厂负责创建维护一个对象池,目标是后续维护中实现对对象的复用,而创建这些享元对象只是维护中的一部分,并且享元模式更偏向于对象级别上的划分,即使这个对象池中的所有对象都是一个类实例化出来的,享元工厂也能按不同的属性对他们进行分类维护。
3、单例模式,两者区别在哪里?他们都会在每次调用的时候提供出一个实例来供应用场景使用,所以在实现两个模式的代码的时候都需要考虑多线程安全问题,考虑创建实例时候的安全,考虑类的可重入性和对象的线程安全性。但是他们是有区别的,单例模式是针对一个类去设计的,而准确的说享元模式不在乎有多少个类,它只在乎有多少相似的对象,只在乎如何根据实现环境去维护这些对象。