GameFramework学习(一) GF_Base

前言

在我大三的时候,我第一次接触到游戏开发。通过参加几次GameJam,我对游戏开发产生了浓厚的兴趣,并迅速开始了我的实习生涯。我有幸在一家专注于二次元手游开发的公司工作了9个月。在这段时间里我真正地接触到了实际投入商业使用的游戏框架,可以说对它的框架使用已经非常熟悉,但这并不能说我真正的搞懂了框架。之前在看过公司的框架之后在网上东拼西凑自己搞了个小框架用在自己参与的独游项目上,沾沾自喜以为自己学会了框架,结果却弄得满地鸡毛。
所以框架到底是什么,这里我直接摘抄网络上的定义:
框架‌是为特定领域或问题提供基础结构、规范与可扩展性的软件或理论体系,包含预定义的设计模式、类库和工具,允许开发者通过填充具体逻辑实现定制功能。
在我看来,框架应当在它所在的领域足够泛用,它应当是该领域所有地方需要用到工具的交集。那么对于一个游戏框架,它就应当只包含任何类型游戏开发都共同需要使用到的工具,比如UI模块,资源管理模块等等。
而在社区里,有这么几款游戏框架总是被广泛讨论,它们因为其高度的规范性、健壮性和可拓展性被大家使用,甚至被投入到商业开发中,比如ET、GameFramework。
我在刚学习游戏框架的时候就听说了GameFramework的大名,工作中遇到的框架在设计上也总是能见到GameFramework的影子,但是因为之前工作过于烦忙,还要准备校招,实在没有一个好机会能够静下心来好好地去拜读GameFramework的源码。而这次,因为校招进入一家新公司,我们有为期数月的培训,而其中一个任务便是阅读GameFramework源码。这是一次绝佳的机会,让我能够去做我之前想做但是却没时间做的事。

正文

GameFramework实际上有两层,一层为GameFramework,它不依赖Unity和其它任何第三方工具,以程序集的方式提供。另一层是UnityGameFramework,它依赖GameFramework和Unity相关程序集。
那么在这一期我们先来看GameFramework层的Base部分。

GFModule管理

GameFramework的框架入口为GameFrameworkEntry,GameFramework框架的所有模块都需要实现GameFrameworkModule抽象类。

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
/// <summary>
/// 游戏框架模块抽象类。
/// </summary>
internal abstract class GameFrameworkModule
{
/// <summary>
/// 获取游戏框架模块优先级。
/// </summary>
/// <remarks>优先级较高的模块会优先轮询,并且关闭操作会后进行。</remarks>
internal virtual int Priority
{
get
{
return 0;
}
}

/// <summary>
/// 游戏框架模块轮询。
/// </summary>
/// <param name="elapseSeconds">逻辑流逝时间,以秒为单位。</param>
/// <param name="realElapseSeconds">真实流逝时间,以秒为单位。</param>
internal abstract void Update(float elapseSeconds, float realElapseSeconds);

/// <summary>
/// 关闭并清理游戏框架模块。
/// </summary>
internal abstract void Shutdown();
}

使用模块时都会使用类中的GetModule懒加载模块并加入到一个链表中,以下是部分代码

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
/// <summary>
/// 获取游戏框架模块。
/// </summary>
/// <typeparam name="T">要获取的游戏框架模块类型。</typeparam>
/// <returns>要获取的游戏框架模块。</returns>
/// <remarks>如果要获取的游戏框架模块不存在,则自动创建该游戏框架模块。</remarks>
public static T GetModule<T>() where T : class
{
Type interfaceType = typeof(T);
if (!interfaceType.IsInterface)
{
throw new GameFrameworkException(Utility.Text.Format("You must get module by interface, but '{0}' is not.", interfaceType.FullName));
}

if (!interfaceType.FullName.StartsWith("GameFramework.", StringComparison.Ordinal))
{
throw new GameFrameworkException(Utility.Text.Format("You must get a Game Framework module, but '{0}' is not.", interfaceType.FullName));
}

string moduleName = Utility.Text.Format("{0}.{1}", interfaceType.Namespace, interfaceType.Name.Substring(1));
Type moduleType = Type.GetType(moduleName);
if (moduleType == null)
{
throw new GameFrameworkException(Utility.Text.Format("Can not find Game Framework module type '{0}'.", moduleName));
}

return GetModule(moduleType) as T;
}

而对于这个链表,GF会去不断地轮询遍历Module。这样子就有了一个最基本的模块统一管理

1
2
3
4
5
6
7
8
9
10
11
12
/// <summary>
/// 所有游戏框架模块轮询。
/// </summary>
/// <param name="elapseSeconds">逻辑流逝时间,以秒为单位。</param>
/// <param name="realElapseSeconds">真实流逝时间,以秒为单位。</param>
public static void Update(float elapseSeconds, float realElapseSeconds)
{
foreach (GameFrameworkModule module in s_GameFrameworkModules)
{
module.Update(elapseSeconds, realElapseSeconds);
}
}

ReferencePool(引用池)

引用池一般用来存储C#类实例,它可以说的上是GF的基石,GF的许多模块都大量地使用到它来优化性能,比如时间模块,网络模块等,可以说,只要涉及到大量重复地创建并销毁的操作的,都可以使用它来进行优化。
它主要是通过复用之前使用过的实例,来避免频繁地创建和销毁,这种思想在游戏开发中尤其常见,也异常重要。具体做法其实也非常简单,维护一个列表(或者别的什么数据结构,最好出队入队的算法复杂度都为O(1))。因为引用池不会去做遍历,基本上只会去push和pop。

GF引用池实现

了解了引用池的原理,我们就可以更好地去理解GF对于引用池的设计。

IReference引用池接口、

首先就是引用池的接口。以下是GF的实现

1
2
3
4
5
6
7
8
9
10
/// <summary>
/// 引用接口。
/// </summary>
public interface IReference
{
/// <summary>
/// 清理引用。
/// </summary>
void Clear();
}

所有的引用池对象都需要实现这个接口,这个接口一定要实现Clear方法,来在引用池对象释放的时候调用这个方法,这样子做的精妙之处就在于可以清晰地知道引用池对象该如何清理自己,保证下一次取出的时候是全新的状态。
这一个简单的接口就给予了引用池对象巨大的安全性,保证了对象的状态重置。

ReferencePool引用池

接下来就是引用池设计,引用池是一个静态类,总体上说,它通过维护一个Key为对象类型,Value为引用池信息的自定义类的字典来达到复用的目的。

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
private static readonly Dictionary<Type, ReferenceCollection> s_ReferenceCollections = new Dictionary<Type, ReferenceCollection>();
/// <summary>
/// 获取所有引用池的信息。
/// </summary>
/// <returns>所有引用池的信息。</returns>
public static ReferencePoolInfo[] GetAllReferencePoolInfos();
/// <summary>
/// 清除所有引用池。
/// </summary>
public static void ClearAll()
/// <summary>
/// 从引用池获取引用。
/// </summary>
/// <typeparam name="T">引用类型。</typeparam>
/// <returns>引用。</returns>
public static T Acquire<T>() where T : class, IReference, new()
/// <summary>
/// 从引用池获取引用。
/// </summary>
/// <param name="referenceType">引用类型。</param>
/// <returns>引用。</returns>
public static IReference Acquire(Type referenceType)
/// <summary>
/// 将引用归还引用池。
/// </summary>
/// <param name="reference">引用。</param>
public static void Release(IReference reference)
/// <summary>
/// 向引用池中追加指定数量的引用。
/// </summary>
/// <typeparam name="T">引用类型。</typeparam>
/// <param name="count">追加数量。</param>
public static void Add<T>(int count) where T : class, IReference, new()
/// <summary>
/// 向引用池中追加指定数量的引用。
/// </summary>
/// <param name="referenceType">引用类型。</param>
/// <param name="count">追加数量。</param>
public static void Add(Type referenceType, int count)
/// <summary>
/// 从引用池中移除指定数量的引用。
/// </summary>
/// <typeparam name="T">引用类型。</typeparam>
/// <param name="count">移除数量。</param>
public static void Remove<T>(int count) where T : class, IReference
/// <summary>
/// 从引用池中移除指定数量的引用。
/// </summary>
/// <param name="referenceType">引用类型。</param>
/// <param name="count">移除数量。</param>
public static void Remove(Type referenceType, int count)
/// <summary>
/// 从引用池中移除所有的引用。
/// </summary>
/// <typeparam name="T">引用类型。</typeparam>
public static void RemoveAll<T>() where T : class, IReference
/// <summary>
/// 从引用池中移除所有的引用。
/// </summary>
/// <param name="referenceType">引用类型。</param>
public static void RemoveAll(Type referenceType)

ReferenceCollection引用池收集

这个便是对应每一个引用池对象的一个收集器,它内部实际维护一个队列,通过队列的入队出队,来实现引用池对象的复用,当需要使用对象时,从队列中出队一个对象使用,使用结束后,调用Clear方法将对象恢复原样并重新入队到队列中供其它地方复用。

private readonly Queue<IReference> m_References;
private readonly Type m_ReferenceType;
private int m_UsingReferenceCount;
private int m_AcquireReferenceCount;
private int m_ReleaseReferenceCount;
private int m_AddReferenceCount;
private int m_RemoveReferenceCount;

GF的引用池收集器不仅维护了引用池对象的队列,还维护了许多的数值,包括当前使用的对象数,获取释放的对象数,添加移除的对象数,这样子可以方便我们去了解实际的对象池性能,来做更好的优化。