游戏角色的产生--工厂方法模式

思考并回答以下问题:

  • 工厂模式的官方定义是什么?
  • 工厂模式是必须要使用的设计模式。为什么?
  • 我理解的简单工厂模式就是为了方便改名,然后为了维持开闭原则变成了工厂模式,陈述一下。
  • 能通过客户端的代码看出使用了工厂模式吗?
  • 为什么下面这几种情况需要使用工厂模式?需要复杂的流程;需要加载外部资源,如从网络、存储设备、数据库;有对象上限;可重复使用。
  • 属性工厂是什么意思?
  • 工厂类太多也需要管理,怎么管理?
  • 怎么解决工厂子类太多的问题?如何使用泛型?

本章涵盖:

  • 产生角色
  • 工厂方法模式
    • 工厂方法模式的定义
    • 工厂方法模式的说明
    • 工厂方法模式的实现范例
  • 使用工厂方法模式产生角色对象
    • 角色工厂类
    • 实现说明
    • 使用工厂方法模式的优点
    • 工厂方法模式的实现说明
  • 工厂方法模式面对变化时
  • 结论

产生角色

按照之前的游戏需求说明可以得知:玩家通过兵营接口决定训练角色后,玩家角色就会从所属的3个兵营中产生出来;而敌方角色对象则是由关卡系统(StageSystem)负责产生,关卡系统则是根据策划人员的设置,在不同进度条件下,产生不同的敌方角色对象,如图1所示。

图1 两方阵营兵种的产生方式

如果用比较直观的设计方式,我们可以在兵营类中实现下列程序代码,用来产生玩家角色对象:

Listing1 Soldier兵营类中可以产生所有的玩家角色单位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
public class SoldierCamp
{
// 训练Rookie单位
public ISoldier TrainRookie(ENUM_Weapon emWeapon, int Lv)
{
// 产生对象
SoldierRookie theSoldier = new SoldierRookie();

// 设置模型
GameObject tmpGameObject = CreateGameObject("RookieGameObjectName");
tmpGameObject.gameObject.name = "SoldierRookie";
theSoldier.SetGameObject( tmpGameObject );

// 加入武器
IWeapon Weapon = CreateWeapon(emWeapon);
theSoldier.SetWeapon( Weapon );

// 获取Soldier的属性,设置给角色
SoldierAttr theSoldierAttr = CreateSoliderAttr(1);
theSoldierAttr.SetSoldierLv(Lv);
theSoldier.SetCharacterAttr(theSoldierAttr);

// 加入AI
SoldierAI theAI = CreateSoldierAI();
theSoldier.SetAI( theAI );

// 加入管理器
PBaseDefenseGame.Instance.AddSoldier( theSoldier as ISoldier );

return theSoldier as ISoldier;
}

// 训练Sergeant单位
public ISoldier TrainSergeant(ENUM_Weapon emWeapon,int Lv)
{
// 产生对象
SoldierSergeant theSoldier = new SoldierSergeant();

// 设置模型
GameObject tmpGameObject = CreateGameObject("SergeantGameObjectName");
tmpGameObject.gameObject.name = "SoldierSergeant";
theSoldier.SetGameObject( tmpGameObject );

// 加入武器
IWeapon Weapon = CreateWeapon(emWeapon);
theSoldier.SetWeapon( Weapon );

// 获取Soldier的属性,设置给角色
SoldierAttr theSoldierAttr = CreateSoliderAttr(2);
theSoldierAttr.SetSoldierLv(Lv);
theSoldier.SetCharacterAttr(theSoldierAttr);

// 加入AI
SoldierAI theAI = CreateSoldierAI();
theSoldier.SetAI( theAI );

// 加入管理器
PBaseDefenseGame.Instance.AddSoldier( theSoldier as ISoldier );

return theSoldier as ISoldier;
}

// 训练Caption单位
public ISoldier TrainCaption(ENUM_Weapon emWeapon,int Lv)
{
// 产生对象
SoldierCaptain theSoldier = new SoldierCaptain();

// 设置模型
GameObject tmpGameObject = CreateGameObject("CaptainGameObjectName");
tmpGameObject.gameObject.name = "SoldierCaptain";
theSoldier.SetGameObject( tmpGameObject );

// 加入武器
IWeapon Weapon = CreateWeapon(emWeapon);
theSoldier.SetWeapon( Weapon );

// 获取Soldier的属性,设置给角色
SoldierAttr theSoldierAttr = CreateSoliderAttr(3);
theSoldierAttr.SetSoldierLv(Lv);
theSoldier.SetCharacterAttr(theSoldierAttr);

// 加入AI
SoldierAI theAI = CreateSoldierAI();
theSoldier.SetAI( theAI );

// 加入管理器
PBaseDefenseGame.Instance.AddSoldier( theSoldier as ISoldier );

return theSoldier as ISoldier;
}
}

在兵营类中,针对三种玩家角色类实现了3个方法,每个方法中都会先产生对应的玩家角色对象,之后再根据需求产生Unity3D模型、武器、角色属性、角色AI等功能的对象,产生后的对象都逐一设置给角色对象。

敌方角色对象的产生方式与玩家角色相似,不同的是,敌方角色是从关卡系统(StageSystem)产生的:

Listing2 在关卡控制系统中产生所有的敌方角色对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
public class StageSystem
{
// 加入Elf单位
public IEnemy AddElf(ENUM_Weapon emWeapon)
{
// 产生对象
EnemyElf theEnmey = new EnemyElf();

// 设置模型
GameObject tmpGameObject = CreateGameObject("ElfGameObjectName");
tmpGameObject.gameObject.name = "EnemyElf";
theEnmey.SetGameObject( tmpGameObject );

// 加入武器
IWeapon Weapon = CreateWeapon(emWeapon);
theEnmey.SetWeapon( Weapon );

// 获取Enemy的属性,设置给角色
EnemyAttr theEnemyAttr = CreateEnemyAttr(1);
theEnmey.SetCharacterAttr(theEnemyAttr);

// 加入AI
EnemyAI theAI = CreateEnemyAI();
theEnmey.SetAI( theAI );

// 加入管理器
PBaseDefenseGame.Instance.AddEnemy( theEnmey as IEnemy );

return theEnmey as IEnemy;
}

// 加入Ogre单位
public IEnemy AddOgre(ENUM_Weapon emWeapon)
{
// 产生对象
EnemyOgre theEnmey = new EnemyOgre();

// 设置模型
GameObject tmpGameObject = CreateGameObject("OgreGameObjectName");
tmpGameObject.gameObject.name = "EnemyOgre";
theEnmey.SetGameObject( tmpGameObject );

// 加入武器
IWeapon Weapon = CreateWeapon(emWeapon);
theEnmey.SetWeapon( Weapon );

// 获取Enemy的属性,设置给角色
EnemyAttr theEnemyAttr = CreateEnemyAttr(2);
theEnmey.SetCharacterAttr(theEnemyAttr);

// 加入AI
EnemyAI theAI = CreateEnemyAI();
theEnmey.SetAI( theAI );

// 加入管理器
PBaseDefenseGame.Instance.AddEnemy( theEnmey as IEnemy );

return theEnmey as IEnemy;
}

// 加入Troll单位
public IEnemy AddTroll(ENUM_Weapon emWeapon)
{
// 产生对象
EnemyTroll theEnmey = new EnemyTroll();

// 设置模型
GameObject tmpGameObject = CreateGameObject("TrollGameObjectName");
tmpGameObject.gameObject.name = "EnemyTroll";
theEnmey.SetGameObject( tmpGameObject );

// 加入武器
IWeapon Weapon = CreateWeapon(emWeapon);
theEnmey.SetWeapon( Weapon );

// 获取Enemy的属性,设置给角色
EnemyAttr theEnemyAttr = CreateEnemyAttr(3);
theEnmey.SetCharacterAttr(theEnemyAttr);

// 加入AI
EnemyAI theAI = CreateEnemyAI();
theEnmey.SetAI( theAI );

// 加入管理器
PBaseDefenseGame.Instance.AddEnemy( theEnmey as IEnemy );

return theEnmey as IEnemy;
}
}

同样地,3个方法中都会先产生对应的敌方角色对象,之后再按顺序产生Unity3D模型、武器、角色属性、角色AI等功能对象并设置给敌方角色。

在两个类中,共声明了6个方法来产生不同的角色对象。在实践中,声明功能相似性过高的方法会有不易管理的问题,而且这一次实现的6个方法中,每个角色对象的组装流程重复性太高。此外,将产生相同类群组对象的实现,分散在不同的游戏功能下不易管理和维护。

所以,是否可以将这些方法都集合在一个类下实现,并且以更灵活的方式来决定产生对象的类呢?GoF的工厂方法模式为上述问题提供了答案。

工厂方法模式

提到“工厂”,大多数人的概念可能是可以大量生产东西的地方,并且是以有组织、有规则的方式来生产东西。它会有多条生产线,每一条生产线都有特殊的配置,专门用来生产特定的东西。没错,工厂方法模式就是用来搭建专门生产软件对象的地方,而且这样的软件工厂,也能针对特定的类配置特定的组装流程,来满足客户端的要求。

工厂方法模式的定义

GoF对工厂方法模式(Factory Method)的解释是:

1
定义一个可以产生对象的接口,但是让子类决定要产生哪一个类的对象。工厂方法模式让类的实例化程序延迟到子类中实施。

工厂方法模式就是将类“产生对象的流程”集合管理的模式。集合管理带来的好处是:

  • ①能针对对象产生的流程制定规则;
  • ②减少客户端参与对象生成的过程,尤其是对于那种类对象生产过程过于复杂的,如果让客户端操作对象的组装过程,将使得客户端与该类的耦合度(即依赖度)过高,不利于后续的项目维护。

工厂方法模式是先定义一个产生对象的接口,之后让它的子类去决定产生哪一种对象,这有助于将庞大的类群组进行分类。例如一家生产汽车的公司,生产各种房车、小货车、大卡车等等,而每一个大类下又有品牌和不同功能之分。

因此,生产部门可以先定义一个“生产车”的接口,从这个接口可以获取生产部门所产生的“车”。之后这个接口会衍生3个子类,每一个子类负责生产这家公司的一款车型,分别为:房车工厂、小货车工厂、大卡车工厂,如图2所示。当业务部门接到50辆房车的订单后,只要获取“房车工厂”对象,之后就能对房车工厂下达生产的命令。

图2 各种类型的汽车与工厂对应,并由业务部门下单的示意图

最后,业务部门就能获取50辆房车,至于这50辆房车是怎么在生产线上进行组装的,业务部门不需要知道。

工厂方法模式的说明

定义一个可以产生对象的接口,让子类决定要产生哪一个类的对象,其基本架构如图3所示。

图3 运用工厂方法模式的类结构图

参与者的说明如下:

  • Product(产品类)
    • 定义产品类的操作接口,而这个产品将由工厂生产。
  • ConcreteProduct(产品实现)
    • 实现产品功能的类,可以不只定义一个产品实现类,这些产品实现类的对象都会由ConcreteCreator(工厂实现类)产生。
  • Creator(工厂类)
    • 定义能产生Product(产品类)的方法:FactoryMethod。
  • ConcreteCreator(工厂实现类)
    • 实现FactoryMethod,并产生指定的ConcreteProduct(产品实现)。

工厂方法模式的实现范例

在实现工厂方法模式的选择上并非是固定的,而是按照程序设计语言的特性来决定有多少种实现方式。因为C#支持泛型程序设计,所以有4种实现方式。

第一种方式:由子类产生

定义一个可以产生对象的接口,让子类决定要产生哪一个类的对象,实现上并不会太复杂:

Listing3 声明Factory类(FactoryMethod.cs)

1
2
3
4
5
public abstract class Creator
{
// 子类返回对应的Product类型之对象
public abstract Product FactoryMethod();
}

FactoryMethod方法负责产生Product类的对象,Product类及其子类的实现如下:

Listing4 产生对象类及子类(FactoryMethod.cs)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public abstract class Product
{}

// 产品对象类A
public class ConcreteProductA : Product
{
public ConcreteProductA()
{
Debug.Log("生成对象类A");
}
}

// 产品对象类B
public class ConcreteProductB : Product
{
public ConcreteProductB()
{
Debug.Log("生成对象类B");
}
}

之后,让分别继承自Creator的子类产生对应的产品类对象:

Listing5 实现能产生产品的工厂(FactoryMethod.cs)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 产生ProductA的工厂
public class ConcreteCreatorProductA : Creator
{
public ConcreteCreatorProductA()
{
Debug.Log("产生工厂:ConcreteCreatorProductA");
}

public override Product FactoryMethod()
{
return new ConcreteProductA();
}
}

// 产生ProductB的工厂
public class ConcreteCreatorProductB : Creator
{
public ConcreteCreatorProductB()
{
Debug.Log("产生工厂:ConcreteCreatorProductB");
}

public override Product FactoryMethod()
{
return new ConcreteProductB();
}
}

第一个子类:ConcreteCreatorProductA,它的FactoryMethod方法负责产生ConcreteProductA的对象;第二个子类:ConcreteCreatorProduceB,它的FactoryMethod方法负责产生ConcreteProductB的对象。

测试方法如下:

Listing6 测试工厂模式(FactoryMethodTest.cs)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void UnitTest () 
{
// 产品
Product theProduct = null;

// 工厂接口
Creator theCreator = null;

// 设置为负责ProductA的工厂
theCreator = new ConcreteCreatorProductA();
theProduct = theCreator.FactoryMethod();

// 设置为负责ProductB的工厂
theCreator = new ConcreteCreatorProductB();
theProduct = theCreator.FactoryMethod();
}

要获取ProductA对象时,工厂接口要指定为能生产ProductA的ConcreteCreatorProductA工厂类,之后调用FactoryMethod来获取ProductA对象。接下来的ProductB也是一样的流程。输出的信息也反应两个工厂类产生了不同的Product子类的对象:

执行结果

1
2
3
4
产生工厂:ConcreteCreatorProductA
生成对象类A
产生工厂:ConcretecreatorProductB
生成对象类B

第二种方式:在FactoryMethod增加参数

由不同的子类工厂产生不同的产品类对象,在遇到产品类对象非常多的时候,很容易造成“工厂子类暴增”的情况,这对于后续维护来说,是比较辛苦的。所以,当有上述情况时,可以改成由单一FactoryMethod方法配合传入参数的方式,来决定要产生的产品类对象是哪一个:

Listing7 声明FactoryMethod,它会按照参数Type的提示返回对应Product类对象(FactoryMethod.cs)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public abstract class Creator_MethodType
{
public abstract Product FactoryMethod(int Type);
}

// 重新实现FactoryMethod,以返回Product类之对象
public class ConcreteCreator_MethodType: Creator_MethodType
{
public ConcreteCreator_MethodType()
{
Debug.Log("产生工厂:ConcreteCreator_MethodType");
}

public override Product FactoryMethod(int Type)
{
switch( Type )
{
case 1:
return new ConcreteProductA();

case 2:
return new ConcreteProductB();

default:
Debug.Log("Type["+Type+"]无法产生对象");
break;
}
return null;
}
}

子类在实现FactoryMethod时,会按照传入的Type,使用switch case语句来决定要产生的产品类对象。在测试程序中,直接产生子类工厂对象后,就能利用不同的参数来产生对应的产品类对象:

Listing8 测试FactoryMethod(FactoryMethodTest.cs)

1
2
3
4
5
6
7
8
9
void UnitTest () 
{
// 工厂接口
Creator_MethodType theCreatorMethodType = new ConcreteCreator_MethodType();

// 获取两个产品
theProduct = theCreatorMethodType.FactoryMethod(1);
theProduct = theCreatorMethodType.FactoryMethod(2);
}

输出的信息如下:

执行结果

1
2
3
产生工厂:ConcreteCreator_MethodType
生成对象类A
生成对象类B

FactoryMethod是比较常用的实现方式,但是对于switch case语句带来的缺点,则必须加以衡量。就笔者的经验来说,如果选择了FactoryMethod的方式,那么就会在switch case语句的最后加上default区段,区段中加上警告信息,提醒有忽略的Type被传入,以避免新增产品类时,忘记要修改这一段程序代码。

不过,是否存在既可以产生对应的产品类,又不想用太多的工厂子类去实现,也不想用switch case语句来列出所有产品类的方式呢?答案是有的,只是需要程序设计语言本身支持“相关语句”即可。这里所说的“相关语句”指的是程序设计语言具备“泛型程序设计”的语句。

“泛型程序设计”在C++语句中,指的是template相关语句,而在Unity3D使用的C#语句中,指的是Generic相关语句。所以,既然C#提供了语句,那就可以使用泛型语句来实现工厂类。一般还可以分为两种实现方式:泛型类(Generic Class)和泛型方法(Generic Method)。

第三种方式:Creator泛型类

首先是采用泛型类(Generic Class)的实现,与第一种实现方式比较起来,可省去继承的实现方式,改用指定“T类类型”的方式,产生对应类的对象:

Listing9 声明Generic Factory类(FactoryMethod.cs)

1
2
3
4
5
6
7
8
9
10
11
12
public class Creator_GenericClass<T> where T : Product, new()
{
public Creator_GenericClass()
{
Debug.Log("产生工厂:Creator_GenericClass<"+typeof(T).ToString()+">");
}

public Product FactoryMethod()
{
return new T();
}
}

使用泛型类(Generic Class)实现时很简洁,只有一个类需要实现。另外,可以使用public class Creator_GenericClass\ where T : Product的语句来限定T类类型,只可以带入Product群组内的类。

在客户端使用时,与第一种实现方式(由子类产生)一样,要先获取能产生特定产品类的工厂对象,之后再调用工厂对象的FactoryMethod来产生对象:

Listing10 测试泛型类(FactoryMethodTest.cs)

1
2
3
4
5
6
7
8
9
10
11
void UnitTest () 
{
// 使用Generic Class
// 负责ProductA的工厂
Creator_GenericClass<ConcreteProductA> Creator_ProductA = new Creator_GenericClass<ConcreteProductA>();
theProduct = Creator_ProductA.FactoryMethod();

// 负责ProductA的工厂
Creator_GenericClass<ConcreteProductB> Creator_ProductB = new Creator_GenericClass<ConcreteProductB>();
theProduct = Creator_ProductB.FactoryMethod();
}

输出的信息如下:

执行结果

1
2
3
4
产生工厂:Creator_GenericClass<DesignPattern_FactoryMethod.ConcreteProductA>
生成对象类A
产生工厂:Creator_GenericClass<DesignPattern_FactoryMethod.ConcreteProductB>
生成对象类B

第四种方式:FactoryMethod泛型方法

因为泛型类(Generic Class)不使用继承的方式实现,客户端无法获取“工厂接口”,所以当需要获取工厂接口时,则可改用泛型方法(Generic Method)来实现工厂方法模式:

Listing11 声明FactoryMethod接口,并使用Generic定义方法(FactoryMethod.cs)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface Creator_GenericMethod
{
Product FactoryMethod<T>() where T: Product, new();
}

// 重新实现FactoryMethod,以返回Product类之对象
public class ConcreteCreator_GenericMethod : Creator_GenericMethod
{
public ConcreteCreator_GenericMethod()
{
Debug.Log("产生工厂:ConcreteCreator_GenericMethod");
}

public Product FactoryMethod<T>() where T: Product, new()
{
return new T();
}
}

使用C# interface语句声明一个接口Creator_GenericMethod,并定义一个泛型方法FactoryMethod\。客户端可以指定要产生的产品类T,实现的类就会将T类的对象产生出来并返回。而T类在声明时,必须指定为Product类,且能使用new的方式产生。

在测试程序中,通过传入不同的T类型,就能产生对应的产品类对象:

Listing12 泛型方法的测试(FactoryMethodTest.cs)

1
2
3
4
5
6
7
void UnitTest () 
{
// 使用Generic Method
ConcreteCreator_GenericMethod theCreatorGM = new ConcreteCreator_GenericMethod();
theProduct = theCreatorGM.FactoryMethod<ConcreteProductA>();
theProduct = theCreatorGM.FactoryMethod<ConcreteProductB>();
}

执行结果

1
2
3
产生工厂:ConcreteCreator_MethodType
生成对象类A
生成对象类B

使用Generic Method的方法实现,除了拥有“工厂接口”之外,还能免去使用switch case语句带来的缺点。另外,可以限定传入T的类型,必须是Product类,所以当有不属于Product群组的类被传入时,C#在编译阶段就能发现错误。

4种实现方式的选择,一般会按实际情况,分析工厂类与其他游戏系统、客户端的互动情况来决定。不过,在不知选择哪种方式时,笔者建议可以先选择第二种:“利用传入参数来决定要产生的类对象”的方式,因为它能避免产生过多的工厂子类,也不必去编写较复杂的泛型语句。但唯一要忍受不便的是,其中switch case语句所带来的缺点,而这也是项目实现中少数可能出现switch case语句的地方。

使用工厂方法模式产生角色对象

当类的对象产生时,若出现下列情况:

  • 需要复杂的流程;
  • 需要加载外部资源,如从网络、存储设备、数据库;
  • 有对象上限;
  • 可重复使用。

建议使用工厂方法模式来实现一个工厂类,而这个工厂类内还可以搭配其他的设计模式,让对象的产生与管理更有效率。

角色工厂类

在《P级阵地》中,将角色类ICharacter的对象产生地点,全部整合在同一个角色工厂类下,有助于后续游戏项目的维护,类结构图如图4所示。

图4 角色工厂类的类结构图

参与者的说明如下:

  • ICharacterFactory:负责产生角色类ICharacter的工厂接口,并提供两个工厂方法来产生不同阵营的角色对象:CharacterSoldier负责产生玩家阵营的角色对象;CharacterEnemy负责产生敌方阵营的角色对象。
  • CharacterFactory:继承并实现ICharacter工厂接口的类,其中实现的工厂方法是实际产生对象的地方。
  • ISoldier、SoldierCaptain…:工厂类产生的“产品”,在《P级阵地》中为玩家角色。
  • IEnemy、EnemyElf…:工厂类产生的另一项“产品”,在《P级阵地》中为敌方角色。

实现说明

ICharacterFactory为抽象类,定义了两个可产生双方阵营角色的工厂方法:

Listing13 产生游戏角色的工厂接口(ICharacterFactory.cs)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 产生游戏角色工厂接口
public abstract class ICharacterFactory
{
// 产生Soldier
public abstract ISoldier CreateSoldier( ENUM_Soldier emSoldier,
ENUM_Weapon emWeapon,
int Lv,
Vector3 SpawnPosition);

// 产生Enemy
public abstract IEnemy CreateEnemy( ENUM_Enemy emEnemy,
ENUM_Weapon emWeapon,
Vector3 SpawnPosition,
Vector3 AttackPosition);
}

在声明的方法中,除了将要产生的角色类型使用枚举(enum)语句加以指定外,也将对象产生时所需要的额外信息,如武器类型、等级、集合点等一起传递给工厂方法。CharacterFactory为实现上述接口的类:

Listing14 实现产生游戏角色的工厂(CharacterFactory.cs)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
public class CharacterFactory : ICharacterFactory
{
// 产生Soldier
public override ISoldier CreateSoldier( ENUM_Soldier emSoldier,
ENUM_Weapon emWeapon,
int Lv,
Vector3 SpawnPosition)
{
// 产生对应的Character
ISoldier theSoldier = null;

switch( emSoldier)
{
case ENUM_Soldier.Rookie:
theSoldier = new SoldierRookie();
break;

case ENUM_Soldier.Sergeant:
theSoldier = new SoldierSergeant();
break;

case ENUM_Soldier.Captain:
theSoldier = new SoldierCaptain();
break;

default:
Debug.LogWarning("CreateSoldier:无法产生[" + emSoldier + "]");
return null;
}

// 设置模型
GameObject tmpGameObject = CreateGameObject("CaptainGameObjectName");
tmpGameObject.gameObject.name = "Soldier" + emSoldier.ToString();
theSoldier.SetGameObject( tmpGameObject );

// 加入武器
IWeapon Weapon = CreateWeapon(emWeapon);
theSoldier.SetWeapon( Weapon );

// 获取Soldier的属性,设置给角色
SoldierAttr theSoldierAttr = CreateSoliderAttr(theSoldier.GetAttrID());
theSoldierAttr.SetSoldierLv(Lv);
theSoldier.SetCharacterAttr(theSoldierAttr);

// 加入AI
SoldierAI theAI = CreateSoldierAI();
theSoldier.SetAI( theAI );

// 加入管理器
PBaseDefenseGame.Instance.AddSoldier( theSoldier );

return theSoldier;
}

// 产生Enemy
public override IEnemy CreateEnemy( ENUM_Enemy emEnemy,
ENUM_Weapon emWeapon,
Vector3 SpawnPosition,
Vector3 AttackPosition)
{
// 产生对应的Character
IEnemy theEnmey = null;

switch( emEnemy)
{
case ENUM_Enemy.Elf:
theEnmey = new EnemyElf();
break;

case ENUM_Enemy.Troll:
theEnmey = new EnemyTroll();
break;

case ENUM_Enemy.Ogre:
theEnmey = new EnemyOgre();
break;

default:
Debug.LogWarning("无法产生["+emEnemy+"]");
return null;
}

// 设置模型
GameObject tmpGameObject = CreateGameObject("OgreGameObjectName");
tmpGameObject.gameObject.name = "Enemy" + emEnemy.ToString();
theEnmey.SetGameObject( tmpGameObject );

// 加入武器
IWeapon Weapon = CreateWeapon(emWeapon);
theEnmey.SetWeapon( Weapon );

// 获取Enemy的属性,设置给角色
EnemyAttr theEnemyAttr = CreateEnemyAttr(theEnmey.GetAttrID());
theEnmey.SetCharacterAttr(theEnemyAttr);

// 加入AI
EnemyAI theAI = CreateEnemyAI();
theEnmey.SetAI( theAI );

// 加入管理器
PBaseDefenseGame.Instance.AddEnemy( theEnmey );

return theEnmey;
}
}

两个工厂方法都包含角色类型列举的参数,使用switch case语句来产生不同的角色对象。为了减少因为switch case语句产生的缺失,在switch语句的最后加上了default区段,用来提示列举项目无法产生对应角色对象的情况。除了产生对应的类对象,工厂方法也将对象后续所需的功能设置程序一并整合进来。最后将新增的角色通过PBaseDefenseGame.Instance.AddEnemy方法添加到游戏角色管理系统(CharacterSystem)中。

使用工厂方法模式的优点

角色工厂类CharacterFactory将“角色类群组”产生对象的实现,都整合到两个工厂方法下,并将有关的程序从客户端删除,同时降低了客户端与“角色产生过程”的耦合度(或称为依赖度)。此外,角色生成后的后续设置功能(加入武器、设置属性、设置AI等),也都在同一个地方实现,让开发人员能快速了解类之间的关联性及设置的先后顺序。

话虽如此,但这两个工厂方法对于对象产生之后的相关功能设置,其实还有改进的空间。这一部分的重构,将在角色的组装-建造者模式一章中进行说明。

工厂方法模式的实现说明

在上一小节中,我们将角色的产生运用了工厂方法模式来产生对象,事实上,在《P级阵地》中还有一些实现方面的考虑与延伸应用,简述如下。

使用泛型方法Generic Method来实现

在本章第四种方式:FactoryMethod泛型方法曾提及,可使用泛型程序设计中的“泛型方法(Generic Method)”来减少使用switch case语句。如果要在《P级阵地》中使用泛型方法来实现工厂方法模式,可能会增加其他系统在调用泛型方法时的负担。

因为调用泛型方法的系统,必须知道可以传入泛型方法的T类是哪一个,但知道越多的T类,对于系统的独立性就越不利。所以,权衡之下, 《P级阵地》还是使用了switch case语句,让调用的系统只需要知道枚举类型(ENUM_Soldier, ENUM_Enemy),以减少耦合度(即依赖度)。

不过,笔者还是将以泛型方法实现的角色工厂列出,供读者引用。而这些程序代码在实际游戏运行中,是不会被执行的:

Listing15 产生游戏角色工厂接口(Generic Method)(TCharacterFactory.cs )

1
2
3
4
5
6
7
8
9
10
public interface TCharacterFactory_Generic
{
// 产生Soldier(Generic版)
ISoldier CreateSoldier<T>(ENUM_Weapon emWeapon, int Lv,
Vector3 SpawnPosition) where T: ISoldier, new();

// 产生Enemy(Generic版)
IEnemy CreateEnemy<T>(ENUM_Weapon emWeapon, Vector3 SpawnPosition,
Vector3 AttackPosition) where T: IEnemy, new();
}

Listing16 产生游戏角色工厂Generic版(CharacterFactory_Generic.cs )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public class CharacterFactory_Generic : TCharacterFactory_Generic
{
// 产生Soldier(Generic版)
public ISoldier CreateSoldier<T>(ENUM_Weapon emWeapon, int Lv,
Vector3 SpawnPosition) where T: ISoldier, new()
{
// 产生对应的T类
ISoldier theSoldier = new T();
if(theSoldier == null)
return null;

// 设置模型
GameObject tmpGameObject = CreateGameObject("CaptainGameObjectName");
tmpGameObject.gameObject.name = "Solider" + typeof(T).ToString();
theSoldier.SetGameObject( tmpGameObject );

// 加入武器
IWeapon Weapon = CreateWeapon(emWeapon);
theSoldier.SetWeapon( Weapon );

// 获取Soldier的属性,设置给角色
SoldierAttr theSoldierAttr = CreateSoldierAttr(theSoldier.GetAttrID());
theSoldier.SetCharacterAttr(theSoldierAttr);

// 加入AI
SoldierAI theAI = CreateSoldierAI();
theSoldier.SetAI( theAI );

// 加入管理器
PBaseDefenseGame.Instance.AddSoldier( theSoldier as ISoldier );

return theSoldier;
}

// 产生Enemy(Generice版)
public IEnemy CreateEnemy<T>(ENUM_Weapon emWeapon, Vector3 SpawnPosition,
Vector3 AttackPosition) where T: IEnemy, new()
{
// 产生对应的Character
IEnemy theEnmey = null;
if(theEnmey == null)
return null;

// 设置模型
GameObject tmpGameObject = CreateGameObject("OgreGameObjectName");
tmpGameObject.gameObject.name = "Enemy" + typeof(T).ToString();
theEnmey.SetGameObject( tmpGameObject );

// 加入武器
IWeapon Weapon = CreateWeapon(emWeapon);
theEnmey.SetWeapon( Weapon );

// 获取Enemy的属性,设置给角色
EnemyAttr theEnemyAttr = CreateEnemyAttr(theEnmey.GetAttrID());
theEnmey.SetCharacterAttr(theEnemyAttr);

// 加入AI
EnemyAI theAI = CreateEnemyAI();
theEnmey.SetAI( theAI );

// 加入管理器
PBaseDefenseGame.Instance.AddEnemy( theEnmey );

return theEnmey;
}
}

其他的工厂

在《P级阵地》中采用“将类对象的产生,都以一个工厂类来实现”这种概念来实现的,不只角色工厂一个。以下是《P级阵地》中的各种工厂:

  • IAssetFactory:资源加载工厂,负责将放置在文件目录下的Unity3D资源Asset实例化的工厂,这些资源包含3D模型、2D图文件、音效音乐文件等。因为Unity3D在Asset加载时有些策略和步骤是具有选择性或可进行优化,并且也能减少客户端直接获取Unity3D资源的依赖度。所以在《P级阵地》中,会将资源实例化的工作交给IAssetFactory工厂来实现。
  • IWeaponFactory:武器工厂,负责产生角色单位使用的武器。虽然当前在游戏的设置上只有3种武器,但产生过程也需要多个步骤才能完成,所以也集中在一个工厂中实现。
  • IAttrFactory:属性产生工厂,双方角色都必须使用属性来代表能力(生命力、移动速度)。而这些属性组合在游戏设计过程中,是需要被量化和能事先计算设计的,所以“属性”往往以一组一组的方式被记录和初始化,并以指定编号的方式将特定的属性指定给角色。IAttrFactory属性产生工厂,即是通过指定编号的方式产生属性组合,其中也包含了可以优化的操作,这部分将在游戏属性管理功能-享元模式中进行说明。

工厂类对象的管理

在《P级阵地》中存在4个工厂,而这些工厂都是“接口类”,所以一定会在项目的某个地方进行“产生对象new”的操作,让这些工厂能通过对象进行运行。因为资源优化的需求,也希望整个项目中的每个工厂类都只产生一个对象,所以《P级阵地》特别设计了一个“静态类”PBDFactory来管理这些工厂:

Listing17 获取PBaseDefenseGame中所使用的工厂(PBDFactory.cs)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public static class PBDFactory
{
private static bool m_bLoadFromResource = true;
private static ICharacterFactory m_CharacterFactory = null;
private static IAssetFactory m_AssetFactory = null;
private static IWeaponFactory m_WeaponFactory = null;
private static IAttrFactory m_AttrFactory = null;

private static TCharacterFactory_Generic m_TCharacterFactory = null;

// 获取将Unity Asset实例化的工厂
public static IAssetFactory GetAssetFactory()
{
if( m_AssetFactory == null)
{
if( m_bLoadFromResource)
m_AssetFactory = new ResourceAssetFactory();
else
m_AssetFactory = new RemoteAssetFactory();
}
return m_AssetFactory;
}

// 游戏角色工厂
public static ICharacterFactory GetCharacterFactory()
{
if( m_CharacterFactory == null)
m_CharacterFactory = new CharacterFactory();
return m_CharacterFactory;
}

// 游戏角色工厂(Generic版)
public static TCharacterFactory_Generic GetTCharacterFactory()
{
if( m_TCharacterFactory == null)
m_TCharacterFactory = new CharacterFactory_Generic();
return m_TCharacterFactory;
}

// 武器工厂
public static IWeaponFactory GetWeaponFactory()
{
if( m_WeaponFactory == null)
m_WeaponFactory = new WeaponFactory();
return m_WeaponFactory;
}

// 属性工厂
public static IAttrFactory GetAttrFactory()
{
if( m_AttrFactory == null)
m_AttrFactory = new AttrFactory();
return m_AttrFactory;
}
}

因为PBDFactory是使用“静态类”设计的,所以它的类成员也是以“静态成员”的方式来声明。当调用该类的方法获取对应的工厂时,PBDFactory类可以确保静态成员只会被产生一次,而且返回的是各工厂的“接口”,正好呼应获取游戏服务的唯一对象--单例模式中所描述的需求——可以使用静态类的静态方法来获取某个类的唯一对象,而不必使用单例模式来完成。

游戏程序代码中的客户端获取工厂之后,可用下列方式来获得所需要的类对象:

Listing18 执行训练Soldier(TrainSoldierExecute.cs)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class TrainSoldierExecute
{
...
public void Action(TrainSoldierCommand Command)
{
// 获取角色工厂
ICharacterFactory Factory = PBDFactory.GetCharacterFactory();

// 产生Soldier
ISoldier Soldier = Factory.CreateSoldier(Command.emSoldier,
Command.emWeapon,
Command.Lv,
Command.Position);
...
}
}

工厂方法模式面对变化时

当把对象的产生交给各类工厂负责之后,对于项目后期的变更要求来说,修改会更有效率,例如:针对某一个工厂方法想要新增一个参数设置时,虽然这个变更的修改会改变接口规则,必须同时修改所有客户端,但对于修改的程度和影响范围而言,仍比较容易预估。因为使用程序的集成开发环境工具(IDE)的“查找被引用”功能,能快速找出所有方法被使用的地方,所以当发现修改的范围过大时,就能对于修改的方式做出其他决定,甚至是变更需求。另外,当想要变更对象的产生流程及功能组装的规则时,也只需要修改工厂方法内的实现程序代码,将修改范围局限在一个地方即可。

当工厂是以“接口”的形式存在时,代表有机会更换不同的“实现工厂类”来满足不同的设计需求,如《P级阵地》中的IAssetFactory资源加载工厂,负责Unity3D的资源加载。对于一个Unity3D资源而言,它可以存在于不同的物理位置中,例如:

  • 项目的Resource目录下。
  • 可以使用目录符号C:\xxx\xxx获取的文件资源,包含本地计算机目录和局域网中的计算机目录。
  • 使用UnityEngine.WWW类获取放在网页服务器(Web Server)上的AssetBundle资源。

为了应用不同的资源获取方式,《P级阵地》中的IAssetFactory资源加载工厂有3个子类,分别负责不同资源的获取方式,如图5所示。

图5 IAssetFactory类及其3个子类的类结构图

  • ResourceAssetFactory:从项目的Resource中,将Unity3D Asset实例化成GameObject。
  • LocalAssetFactory:从本地(存储设备)中,将Unity3D Asset实例化成GameObject。
  • RemoteAssetFactory:从远程(网络WebServer)中,将Unity3D Asset实例化成GameObject。

IAssetFactory资源加载工厂的实现如下:

Listing19 获取将Unity Asset实例化的工厂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static IAssetFactory GetAssetFactory(int type = 1)
{
if( m_AssetFactory == null)
{
switch(type)
{
case 1:
m_AssetFactory = new ResourceAssetFactory();
break;

case 2:
m_AssetFactory = new LocalAssetFactory();
break;

case 3:
m_AssetFactory = new RemoteAssetFactory();
break;
}
}
return m_AssetFactory;
}

由上述的程序代码可知,在获取工厂时,可因项目的需求返回不同的工厂,这有助于面对未来的变化。

结论

工厂方法模式的优点是,将类群组对象的产生流程整合于同一个类下实现,并提供唯一的工厂方法,让项目内的“对象产生流程”更加独立。不过,当类群组过多时,无论使用哪种方式,都会出现工厂子类爆量或switch case语句过长的问题,这是美中不足的地方。

与其他模式的合作

  • 角色工厂(CharacterFactory)中,产生不同阵营的角色时,会搭配建造者模式(Builder)的需求,将需要的参数设置给各角色的建造者。
  • 本地资源加载工厂(ResourceAssetFactor)若同时要求系统性能的优化,可使用代理者模式(Proxy)来优化加载性能。
  • 属性产生工厂(AttrFactory)可使用享元模式(Flyweight)来减少重复对象的产生。
  • 其他应用方式。

就如同本章的重点,如果系统实现人员想要将对象的产生及相关的初始化工作集中在一个地方完成,那么都可以使用工厂方法模式来完成,换句话来说,就是工厂方法模式的应用层面非常广泛。

0%