参考写在前面!!!

本文是在腾讯大佬花桑和大佬猫刀刀的GF解析文章的基础上,阅读源码并尝试总结、应用、拓展的个人笔记!无其他用途,水印以示尊敬。

运行流程

启动

基于MonoBehaviour来实现。

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
public abstract class GameFrameworkComponent : MonoBehaviour { 
// 游戏框架组件初始化。
protected virtual void Awake()
{
GameEntry.RegisterComponent(this);
}
}

public static class GameEntry {
// 提供api方便外部访问这个链队,从而获取组件
static readonly GameFrameworkLinkedList<GameFrameworkComponent> s_GameFrameworkComponents = new GameFrameworkLinkedList<GameFrameworkComponent>();

static void RegisterComponent(GameFrameworkComponent gameFrameworkComponent){
Type type = gameFrameworkComponent.GetType();
... // 确保链队未添加过同type元素
s_GameFrameworkComponents.AddLast(gameFrameworkComponent);
}
}

// ------------------执行完上述base.Awake()后,回到各自的Awake()逻辑。下面用ProcedureComponent举例。------------------
ProcedureComponent.protected override void Awake()
{
base.Awake(); // 上面做的事
m_ProcedureManager = GameFrameworkEntry.GetModule<IProcedureManager>();
}

初始化

GameFrameworkModule

GameFrameworkModule(GF层中各个模块的基类)采用懒加载。在Awake中GetModule的时候时,GameFrameworkEntry会先检测内部有没有这个模块对象,没有时再调用内部的CreateModule来实例化该模块。

CreateModule是通过传入的接口去掉第一个字符I,然后反射调用构造器的:

1
2
3
4
5
6
7
8
// -----------GameFrameworkEntry.GetModule<T>() where T : class-----------

// 传入的 type 是接口 (比如 IProcedureManager)
string typeName = Utility.Text.Format<string, string>("{0}.{1}", type.Namespace, type.Name.Substring(1));
...
GameFrameworkModule instance = (GameFrameworkModule) Activator.CreateInstance(Type.GetType(typeName));
... // ①
return instance;

最后这个类(比如 ProcedureManager),就在GF框架里而不是UGF框架了。这个是一个很好的解耦。

然后我们讲讲CreateModule方法里省略的代码,①里做的事:

根据instance.Priority优先级,插入到GameFrameworkEntry。s_GameFrameworkModules全局队列中合适的节点位置。这么做是为了保证链队始终保持优先级从大到小排序,等后面Update的时候就直接foreach遍历就完事了!

Tick

Tick部分逻辑比较轻:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public abstract class GameFrameworkComponent : MonoBehaviour { }

public sealed class BaseComponent : GameFrameworkComponent {
private void Update()
{
GameFrameworkEntry.Update(Time.deltaTime, Time.unscaledDeltaTime);
}
}

public static class GameFrameworkEntry{
public static void Update(float elapseSeconds, float realElapseSeconds)
{
// 直接foreach就可以!初始化① 的时候保证了链队是按优先级顺序排的
foreach (GameFrameworkModule gameFrameworkModule in GameFrameworkEntry.s_GameFrameworkModules)
gameFrameworkModule.Update(elapseSeconds, realElapseSeconds);
}
}

ShutDown

卸载。

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
public enum ShutdownType : byte
{
// 仅关闭游戏框架。
None = 0,

// 关闭游戏框架并重启游戏。
Restart,

// 关闭游戏框架并退出游戏。
Quit,
}

GameEntry.Shutdown(ShutdownType shutdownType){
baseComponent.Shutdown();
s_GameFrameworkComponents.Clear();

if (shutdownType == ShutdownType.None) { return; }

if (shutdownType == ShutdownType.Restart)
{
SceneManager.LoadScene(GameFrameworkSceneId);
return;
}

if (shutdownType == ShutdownType.Quit)
{
Application.Quit();
return;
}
}

// -----------------------下面只讨论None:仅关闭游戏框架。-----------------------

BaseComponent.Shutdown()
{
Destroy(gameObject);
}

BaseComponent.OnDestroy()
{
GameFrameworkEntry.Shutdown();
}

GameFrameworkEntry.Shutdown()
{
for() { ... } // 从后往前遍历 s_GameFrameworkModules 所有成员执行 Shutdown();

GameFrameworkEntry.s_GameFrameworkModules.Clear();
ReferencePool.ClearAll();
Utility.Marshal.FreeCachedHGlobal();
GameFrameworkLog.SetLogHelper((GameFrameworkLog.ILogHelper) null);
}

GF书写习惯

GF整体代码的书写习惯是利用 接口 + Dictionary + List 来实现对外的api调用。

接口的实际对象正如前面所说,绝大部分都是Awake的时候反射构造 同名实现类 实现的。

这一点对于扩展来说非常开闭,就是提高了熵。

知道了这一点后,下面的探究就不再对GF的接口进行关注了,而是只看他们的实现类了。

有限状态机

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Player : MonoBehaviour
{
private IFsm<Player> fsm; //一个状态机
private List<FsmState<Player>> stateList; //状态结点List

void Start()
{
//创建状态列表
stateList = new List<FsmState<Player>>() { new IdleState(), new MoveState() };
//创建状态机,注意,对于所有持有者为Player类型的状态机的名字参数"name"不能重复
fsm = GameEntry.fsmComponent.fsmManager.CreateFsm<Player>("name", this, stateList);
//以IdleState为初始状态,启动状态机
fsm.Start<IdleState>();
}
}

FsmManager

FsmManager 和 IFsmManager。状态机最外层的存在,有Update会被循环,外部直接调用它的CreateFsm方法。

值得注意的是(下面)用了2次foreach完成1次轮询,为什么要这么做?

因为如果在第一次foreach的时候就直接调用fsm.Update很可能导致m_Fsms的更改,从而导致迭代器的损坏。

1
2
3
4
5
6
7
8
9
10
11
12
// 优先级60
sealed class FsmManager : GameFrameworkModule, IFsmManager
{
readonly Dictionary<TypeNamePair, FsmBase> m_Fsms;
readonly List<FsmBase> m_TempFsms;

void Update(){
m_TempFsms.Clear();
foreach(m_Fsms) { ... } // 将m_Fsms全部放入干净的m_TempFsms中。
foreach(m_TempFsms) { fsm.Update(); }
}
}

Fsm< T >

一个状态机。继承自FsmBase,也就是上面字典的申明类型。FsmBase抽象类 就不介绍了,是一些状态机的通用属性比如Name、IsDestory之类的属性。

T传进去的是你的数据类型,和结点FsmState<T>传的T一致。

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
sealed class Fsm<T> : FsmBase, IReference, IFsm<T> where T : class
{
readonly Dictionary<Type, FsmState<T>> m_States;
Dictionary<string, Variable> m_Datas;
private FsmState<T> m_CurrentState;

public void Start<TState>() where TState : FsmState<T>
{
FsmState<T> state = GetState<TState>();
m_CurrentState = state;
m_CurrentState.OnEnter(this);
}

void Update()
{
m_CurrentState.OnUpdate(this);
}

// 这个是对FsmState结点提供的方法!
internal void ChangeState<TState>() where TState : FsmState<T>
{
Type stateType = typeof(TState);
FsmState<T> state = GetState(stateType); // GetState 就是从m_States里查找
m_CurrentState.OnLeave(this, false);
m_CurrentState = state;
m_CurrentState.OnEnter(this);
}
}

FsmState< T >

一个状态结点。一个状态机对应多个状态结点,想象一下状态机的图就行了。

一般来说,我们需要自己实现的就只有这个类。

对上面调用过的方法进行展示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public abstract class FsmState<T> where T : class
{
// 生命周期(全是虚方法,具体行为要自己实现)
void OnInit(IFsm<T> fsm);

void OnEnter(IFsm<T> fsm);

void OnUpdate(IFsm<T> fsm, float elapseSeconds, float realElapseSeconds);

void OnLeave(IFsm<T> fsm, bool isShutdown);

void OnDestroy(IFsm<T> fsm);

// 不对外提供的方法,在自己实现的状态结点代码内互相切换状态。当然你可以在子类里选择公开它。
// 一般在OnUpdate
protected void ChangeState<TState>(IFsm<T> fsm) where TState : FsmState<T>
{
Fsm<T> fsmImplement = (Fsm<T>)fsm;
fsmImplement.ChangeState<TState>(); // 调用上面的 Fsm.ChangeState方法 来实现切换状态。
}
}

自己实现这个抽象类的时候,ChangeState调用的时机一般写在 OnUpdate 方法里。比如每帧都判断一个flag,flag为true就切换到其他状态。

思考

其实可以更深层次的拓展出状态栈:如果状态被某个状态打断,可以恢复到之前的状态。

流程

使用

在ProcedureComponent组件(下面)中提到,是通过m_AvailableProcedureTypeNames来创建实例,并以m_EntranceProcedure为起始状态,启动流程状态机,那么这两个变量是怎么来的呢。

如图所示,我们直接通过流程组件的Inspector来配置,GF会通过反射获取所有继承ProcedureBase的子类,并展示在此面板,我们只需要勾选需要流程即可把它加入到m_AvailableProcedureTypeNames中,而面板上的Entrance Procedure则代表了m_EntranceProcedure,这里我们选择了StarForce.ProcedureLaunch作为起始状态,那么ProcedureLaunch类中的OnEnter方法中的逻辑,就是我们游戏启动后最先执行的游戏业务逻辑。

代码上看,是对有限状态机的封装。功能上看,如果把游戏整体看作一部电影的话,那玩家游玩时刻总是在整个游戏的某个流程结点罢了。

常见的二次元游戏流程线:打开游戏app > 热更 > 登陆 > 主城: 各种丰富的业务(任务系统、商城系统、养成系统、etc) ,选择了副本系统 > 战斗,战斗结束回到主城

我个人认为这么划分基本就涵盖了所有的业务。在花卷的博客里写了:

一般地说,一个游戏拥有的流程数量是非常有限的,如果规划出数十个流程出来,很可能是对流程的理解有所偏差。例如一个塔防游戏有数十个关卡,每个关卡的内容都不一样,但关卡中的地图,炮塔,敌人生成等,其实都是数据驱动的,而他们的逻辑其实是一样的,只是数据不同造成表现不同,所以无论是哪个关卡,他们都应该属于同一个流程。

所以我觉得 主城系统里的各种养成系统,都是一个流程结点;战斗系统里各种副本,也属于一个流程结点。

ProcedureBase

非常简单,就是继承了一下 FsmState 并指定类型 IProcedureManager 罢了。本质就是个状态结点。

1
2
3
4
public abstract class ProcedureBase : FsmState<IProcedureManager>
{

}

ProcedureManager

内部包裹了一个 FsmManager 和 一个Type为ProcedureManager的状态机,也就是状态机的最外层。

  1. 字段m_FsmManager为有限状态机管理器,会在Initialize方法初始化时作为参数传入,m_ProcedureFsm为管理流程用的有限状态机。
  2. 方法Initialize会取得FsmManager实例和包括所有流程(继承ProcedureBase的对象)的列表,并用FsmManager创建出一个状态机实例储存于m_ProcedureFsm中。
  3. 与Fsm模块类似,流程模块提供HasProcedure、GetProcedure接口来查询和获取指定流程对象,CurrentProcedure获得当前处于的流程,CurrentProcedureTime获取当前流程持续时间。
  4. StartProcedure方法,令状态机从指定流程启动,这里是游戏框架正式启动游戏的关键入口。该方法其实就是调用了内部状态机的Start。
1
2
3
4
5
6
7
8
9
10
11
12
// 优先级-10
internal sealed class ProcedureManager : GameFrameworkModule, IProcedureManager
{
private IFsmManager m_FsmManager;
private IFsm<IProcedureManager> m_ProcedureFsm;

public void Initialize(IFsmManager fsmManager, params ProcedureBase[] procedures)
{
m_FsmManager = fsmManager;
m_ProcedureFsm = m_FsmManager.CreateFsm(this, procedures); // procedures就是所有流程!
}
}

ProcedureComponent

ProcedureManager的Initialize方法会取得FsmManager实例和包括所有流程(继承ProcedureBase的对象)的列表,并用FsmManager创建出一个状态机实例储存于m_ProcedureFsm中。这些是在ProcedureComponent里完成的:

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
public sealed class ProcedureComponent : GameFrameworkComponent{

private IEnumerator Start()
{
// 所有流程的列表
ProcedureBase[] procedures = new ProcedureBase[m_AvailableProcedureTypeNames.Length];

for (int i = 0; i < m_AvailableProcedureTypeNames.Length; i++)
{
Type procedureType = Utility.Assembly.GetType(m_AvailableProcedureTypeNames[i]);
procedures[i] = (ProcedureBase)Activator.CreateInstance(procedureType);
if (m_EntranceProcedureTypeName == m_AvailableProcedureTypeNames[i])
{
// 根据名字,寻找初始流程
m_EntranceProcedure = procedures[i];
}
}

// 调用Initialize,把 所有流程的列表procedures 传进去了
m_ProcedureManager.Initialize(GameFrameworkEntry.GetModule<IFsmManager>(), procedures);

yield return new WaitForEndOfFrame();

// 开始流程,游戏运行!
m_ProcedureManager.StartProcedure(m_EntranceProcedure.GetType());
}
}

ProcedureComponent继承了Mono类,上面的Start方法会被Unity内部主动调用,调用后会根据m_AvailableProcedureTypeNames通过反射来创建流程对象,进行一系列初始化,再以m_EntranceProcedure为起始状态,启动流程状态机。

思考

其实 流程模块 完全是状态机实例完成,那么功能上没必要单独做成一个模块,但是GF却单独提取成一个模块。很大的原因是因为状态机的状态内部对外是不希望透明的,而流程内部的流程结点是希望对外透明、可访问的。

完整的启动流程

GameFramework框架学习:应用篇

花桑启动流程博文

UI模块

数据结构

花卷博客UI篇很细,这里只简述。

UIManager

UIManager是外部访问框架UI模块的入口。

UIManager 持有 Dictionary<string, **UIGroup**>。

UIManager内部会用GF的对象池模块创建一个对象池,用于缓存UIForm对象的GameObject实例,外部调用OpenUIForm来打开UI时,会先尝试从对象池获取该界面,若对象池中有同类型的空闲实例,则直接取出使用,若没有则从资源模块加载,加载成功后,会注册到对象池中,再交给UIManager使用。而调用CloseUIForm来关闭UI时,UIForm会被加到Queue类型的字段m_RecycleQueue中,在下一次Update时,会把队列所有元素取出,回收到对象池中。

内部维护了一个私有字段m_Serial,每次调用OpenUIForm的时候,m_Serial都会自增1,他表示了每个UIForm在其生命周期内的唯一标识符,即使是同一个UIForm实例,被关闭后放回对象池,再被取出来使用,其m_Serial也会发生变化。

UIGroup

UIGroup是一系列窗口集合,内部用链队存储了一个 LinkedListNode< **UIFormInfo** >,用链表来模栈式结构(链头在最上层)。有Depth的概念。

Refresh:UIGroup的核心逻辑,根据链表顺序以及UIForm的属性,去调用UIForm的OnDepthChanged、OnCover、OnReveal、OnPause、OnResume这些方法。

UIFormInfo

UIFormInfo持有UIForm的引用。

既然UIForm是窗口,那为什么要对它再包一层Info呢?它只多了Paused和Covered两个状态,用来表示状态,提供给UIGroup或外界。

UIForm

UIForm是UI窗口类,被UIGroup直接管理,每个UI窗口都会有一个UIForm实例,同时也拥有一个UIFormLogic实例(一般是它的派生类)。有唯一标识符字段m_SerialId。

UIFormLogic

UIFormLogic为UI界面的具体逻辑类,类内有UIForm的所有生命周期方法。

游戏业务层不对UIForm做扩展,而是对UIFormLogic继承进行扩展,也就是自己的脚本继承UIFormLogic,正常写。

生命周期

引用池

就是对象池结构。

关于对象池我的一些个人理解:对象池本身的存在理由是,因为频繁创建-销毁对象,会让堆产生大量的内存碎片,导致gc的压缩更频繁。这不好,所以直接申请一大块内存,保持在游戏运行时不释放它。

但在GF中并不要求开发者在创建的时候就传入引用池capacity,默认会是0,然后动态扩容。我个人认为,可能是因为分配大量内存而不用的危害远比频繁压缩大,所以gf中对象池的核心作用只是复用对象,也就是减少mono对象和类对象的创建

每个对象的内存大小是固定的,未使用的对象会保留在内存中。

GF中池子有两种,一种叫引用池,一种叫对象池,两者原理一样,但具体实现和针对的对象不同,引用池一般用来储存普通的C#类型对象,而对象池则一般用于储存UnityEngine下的对象(如Unity中的GameObject对象)。

ReferencePool

ReferencePool 静态类,是外部访问引用池模块的入口。

内部维护了一个Dictionary<Type, **ReferenceCollection**>,这个字典就是所有的引用池,Typa对应引用池类型。

采用惰性初始化:当内部有需要获取某引用池实例(比如外界调用API获取引用,内部就先需要get到这个池子),如果在这个字典里Type的池子并不存在,则构造一个加入字典再返回。

ReferenceCollection

引用池。每一个引用池都有自己对应的Type,不同类型的对象储存在各自类型的池子中。

① 需要对构造器传入一个Type,在初期化时会保存好Type。

Queue存储引用:内部维护了一个Queue< **IReference** >,来储存池子中的对象。

池容量动态扩容:创建时不需要指定池的capacity,设置为0。容量的扩大,完全靠动态扩容(具体思考看本节开头)。

IReference

IReference接口只包含一个Clear方法,此方法会在对象回收池被调用,每一个需要被引用池储存的类型都需要实现此接口,以能清空当前状态,恢复到初始状态,供下次使用。

引用池->对象复用 的根本。

使用

引用池一般用来储存普通的C#类型对象。

比如可以实现复用状态机的状态:

  1. 状态内实现Create方法,去引用池Acquire(取)一个空闲的相同状态。
  2. 状态内实现Clear方法(IReference接口),清空所有字段变回初始值。

之后调用,只需要在new状态机的时候Create,Destory时Clear就可以了。

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
// from 花桑
public class Player : MonoBehaviour
{
private IFsm<Player> fsm;

void Start()
{
//创建状态列表(不用引用池)
//List<FsmState<Player>> stateList = new List<FsmState<Player>>() { new IdleState(), new MoveState() };

//创建状态列表(使用引用池)
List<FsmState<Player>> stateList = new List<FsmState<Player>>() { IdleState.Create(), MoveState.Create() };

// --------------------------下面和原来一样--------------------------
//创建状态机,注意,对于所有持有者为Player类型的状态机的名字参数不能重复,这里用自增ID避免重复
fsm = GameEntry.Fsm.CreateFsm<Player>((SERIAL_ID++).ToString(), this, stateList);
//以IdleState为初始状态,启动状态机
fsm.Start<IdleState>();
}

private void OnDestroy()
{
//取出状态机所有状态
FsmState<Player>[] states = fsm.GetAllStates();
//销毁状态机
GameEntry.Fsm.DestroyFsm(fsm);

//把状态实例归还引用池
foreach (var item in states)
{
ReferencePool.Release((IReference)item);
}
}
}

Inspector面板监视

Enable Strick Check:开启类型检测。对象池内部操作时,一方面检测Type是不是非抽象Class且实现了IReference接口的Class;另一方面是释放对象(也就是把对象放回对象池时),需要检查这个对象是不是已经空闲着了。如果 类型不满足、空闲还要求释放 ,就抛错。不过这个检测开启会影响性能,所以只建议测试时开。

面板用途:可以通过此面板方便地检查业务逻辑中有没有正确使用引用池,例如某个对象只会在某个流程中会使用,我们可以检测在流程循环中,这个对象的Acquire和Release是否相等,而流程结束时,Using是否为0,Unused是否与Add相等。

对象池

GF中对象池与引用池作用类似,一般用于储存UnityEngine下的对象(如Unity中的GameObject对象)。

对象池的实现我们可以把他分成3部分,上图中从上到下每一行就是一部分,分别是

  1. 物体部分(抽象类ObjectBase,Object< T >,结构体ObjectInfo),
  2. 对象池部分(抽象类ObjectPoolBase,ObjectPool< T >,委托ReleaseObjectFilterCallback),
  3. 对象池管理器部分(接口IObjectPoolManager,类ObjectPoolManager)。

其中Object和ObjectPool是ObjectPoolManager的内部私有类。

整体关系⭐

对象池:ObjectPool< T >类。它继承 ObjectPoolBase抽象类,且内部持有一个**Object< T >**一对多GF字典。

对象:Object< T > 类。它内部持有一个T对象的引用,且要求T必须继承自 ObjectBase抽象类。它实现 IReference接口,所以Create的时候是从引用池分配的。

对象抽象:ObjectBase 类,也就是上面2个类中限定的T 。内部持有一个object字段(m_Target),这才是真正的 System.Object对象。

物体部分

ObjectBase

通过Initialize方法可把目标对象传递给m_Target字段,这才是object。通过重写OnSpawn、OnUnspawn方法实现对象获取、回收时执行的逻辑。

Object< T >

它实现 IReference接口,Create的时候是从引用池分配的,Release。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static Object<T> Create(T obj, bool spawned)
{
internalObject = ReferencePool.Acquire<Object<T>>();
internalObject.m_Object = obj; // m_Object就是持有的ObjectBase对象引用
internalObject.m_SpawnCount = spawned ? 1 : 0;
...
return internalObject;
}

public void Release(bool isShutdown)
{
m_Object.Release(isShutdown);
ReferencePool.Release(m_Object);
}

对象池

ObjectPool< T >

对象池,继承自 ObjectPoolBase抽象类 和 IObjectPool接口。两者其实很相似,但是却要做成2个是因为IObjectPool是泛型接口,而ObjectPoolBase不是泛型,后者在使用起来的时候可以不需要明确ObjectBase类型。

内部持有:一个<string, Object<T>>类型类型的一对多GF字典,一个<object, Object<T>>类型的普通字典。

Spawn方法:获取对象。字典中,有空闲的就返回,没有就返回null。也就是说,并不会新建对象

Register方法:创建对象。

AutoReleaseInterval 属性:执行Release的频率。

Release方法:自动释放,频率由AutoReleaseInterval 决定,每个池可不同。Release过程会先获取可释放对象序列,然后通过委托ReleaseObjectFilterCallback对可释放物体序列进行筛选后,最后仅对筛选后的对象调用ReleaseObject进行释放。

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
public void Register(T obj, bool spawned)
{
Object<T> internalObject = Object<T>.Create(obj, spawned); // Create方法在上面一小节有展示
m_Objects.Add(obj.Name, internalObject);
m_ObjectMap.Add(obj.Target, internalObject);

if (m_ObjectMap.Count > m_Capacity)
{
Release();
}
}

public void Release()
{
toReleaseCount = Count - m_Capacity; // 当前对象池中对象的数量 - 池容量
expireTime = DateTime.UtcNow.AddSeconds(-m_ExpireTime); // 计算出过期时间点
...
GetCanReleaseObjects(m_CachedCanReleaseObjects);
List<T> toReleaseObjects = releaseObjectFilterCallback(m_CachedCanReleaseObjects, toReleaseCount, expireTime);
...
foreach (T toReleaseObject in toReleaseObjects)
{
ReleaseObject(toReleaseObject);
}
}

GetCanReleaseObjects(m_CachedCanReleaseObjects); 方法:遍历m_ObjectMap的Value值,非使用中状态、非锁定状态、以及自定义释放标记为True时被认为是可释放对象。将所有可释放对象传入m_CachedCanReleaseObjects。

releaseObjectFilterCallback(m_CachedCanReleaseObjects, toReleaseCount, expireTime) 委托方法:这个方法负责从可释放对象序列中进一步选出符合要求的对象,之后再进行释放。默认有DefaultReleaseObjectFilterCallback方法,也可以自己传入委托。

③ ReleaseObject 方法:会把从对应的Object<T>对象从m_Objects和m_ObjectMap中移除。

对象池管理

ObjectPoolManager

在内部使用字典保存所有对象池 ObjectPool< T >。

CreateSingleSpawnObjectPool方法 和 CreateMultiSpawnObjectPool方法 创建对象池,分别对应一个对象同时只能被获取一次的对象,以及一个对象能被同时获取多次两种类型的对象池。正常对象是必须用Single模式的,只有一些资源部分可以用Multi。

这一层在

使用

需要一个逻辑物体ObjectBase,以及继承Mono的表现物体GameFrameworkComponent

官方Demo的示例:

HPBarItemObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class HPBarItemObject : ObjectBase
{
public static HPBarItemObject Create(object target)
{
HPBarItemObject hpBarItemObject = ReferencePool.Acquire<HPBarItemObject>();
hpBarItemObject.Initialize(target);
return hpBarItemObject;
}

protected override void Release(bool isShutdown)
{
// 销毁血条的GameObject
HPBarItem hpBarItem = (HPBarItem)Target;
Object.Destroy(hpBarItem.gameObject);
}
}

HPBarComponent

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
public class HPBarComponent : GameFrameworkComponent
{
[SerializeField]
private HPBarItem m_HPBarItemTemplate = null;

[SerializeField]
private Transform m_HPBarInstanceRoot = null; // 设置生成物体的Root

[SerializeField]
private int m_InstancePoolCapacity = 16;

private IObjectPool<HPBarItemObject> m_HPBarItemObjectPool = null;
private List<HPBarItem> m_ActiveHPBarItems = null;
private Canvas m_CachedCanvas = null;

private void Start()
{
if (m_HPBarInstanceRoot == null)
{
Log.Error("You must set HP bar instance root first.");
return;
}

m_CachedCanvas = m_HPBarInstanceRoot.GetComponent<Canvas>();
// 传入对象池名字、对象池容量作为参数,并持有这个对象池的接口引用
m_HPBarItemObjectPool = GameEntry.ObjectPool.CreateSingleSpawnObjectPool<HPBarItemObject>("HPBarItem", m_InstancePoolCapacity);
m_ActiveHPBarItems = new List<HPBarItem>();
}

private void HideHPBar(HPBarItem hpBarItem)
{
// 不再需要该对象时
hpBarItem.Reset();
m_ActiveHPBarItems.Remove(hpBarItem);
m_HPBarItemObjectPool.Unspawn(hpBarItem);
}

private HPBarItem CreateHPBarItem(Entity entity)
{
HPBarItem hpBarItem = null;
HPBarItemObject hpBarItemObject = m_HPBarItemObjectPool.Spawn();
if (hpBarItemObject != null)
{
// 如果对象池里有空闲对象,就直接返回
hpBarItem = (HPBarItem)hpBarItemObject.Target;
}
else
{
// 如果对象池里无空闲对象,就自己在外部建好物体,然后由此创建为ObjectBase,最后丢到池里注册
hpBarItem = Instantiate(m_HPBarItemTemplate);
Transform transform = hpBarItem.GetComponent<Transform>();
transform.SetParent(m_HPBarInstanceRoot);
transform.localScale = Vector3.one;
m_HPBarItemObjectPool.Register(HPBarItemObject.Create(hpBarItem), true); // true:注册时就立马使用; false:注册时不立马使用
}

return hpBarItem;
}
}

Inspector面板监视

引用池与对象池的区别

  1. 引用池从池子内部通过默认构造方法创建对象,只适合普通的C#对象。对象池是在外部自行创建对象后再注册进去,能用于必须通过Unity API才能实例化的对象。
  2. 引用池仅提供Clear接口来清除对象状态,在移除对象时没有任何额外处理,仅仅是去掉引用,适用于受GC管理的类型。而对象池提供OnSpawn,OnUnspawn两个操作,且在移除对象时,提供Release接口,对于Unity中的GameObject需要在Release写上Destroy(gameObject)的逻辑才能销毁。
  3. 对象池提供自行释放的机制,可指定每个池子自动释放周期、物体过期时长、池子容量,并在可一定程度上自定义每个池子的释放策略。引用池没有以上机制,仅可通过Remove接口主动移除对象。
  4. 对象池提供锁定物体、自定义释放标记功能,可进一步定制释放策略。

思考

同一个对象池中,为什么还要以Name区分对象集合?

在同一个prefab上挂上相同的脚本,最后以他们的资源路径名字作为Name,则可在一个对象池中对不同外形的陨石进行区分,以实现向一个对象池取不同外型的陨石的需求。

官方Demo StarForce中的陨石对象池,虽然他们都是同一个类型,具有相同的逻辑,但他们可能有不一样的外型。我们把外型不同的陨石做成单独prefab,并在这些prefab上挂上相同的脚本,最后以他们的资源路径名字作为Name,则可在一个对象池中对不同外形的陨石进行区分,以实现向一个对象池取不同外型的陨石的需求。

在同一个对象池中以Name区分对象,与用多个对象池储存不同Name的对象有什么区别?

让开发者能更好地规划释放策略。

主要区别就在于一个对象池执行同一个释放逻辑,而多个对象池是各自执行各自的释放逻辑。继续以上面的陨石为例子,我们一共有3种陨石,我希望储存陨石的对象池总容量是60,我们随机去生成不同种类的陨石,如果随机结果不均匀,最终池子里可能有种类一40个,种类二15个,种类三5个,在我们把他们放在同一对象池下管理情况下,这没有什么问题,无论怎样它都很好地以总数量为60个的策略去管理。但如果我们把不同外形的陨石分到不同的对象池去管理,我们很难去动态调整3个池子的容量平衡,以达到总数量为60的策略。

为什么既有引用池又有对象池,全部用对象池不是就可以满足需求了吗?

对象池太繁琐,引用池使用更轻便。