手册:场景异步加载与对象池(加载画面/游戏对象池)

关于本手册

本手册讲解加载画面与对象池的设置方法。它们用于减少由读取和生成引起的卡顿,例如「切换场景时会瞬间卡住」「黑屏散开之后对象才零零散散地冒出来」「一次性发射大量子弹时那一下会掉帧」等问题。

两者都是以错开读取时机为目的的功能,而且加载画面的进度中也包含了对象池的预先生成。一起设置的话效果会更明显。

ACTION GAME MAKER 中读取机制的概要

与读取相关的机制有 3 种,所需的设置量各不相同。

机制 做什么 所需设置
场景的异步加载 在后台推进场景的读取,不让主处理停下来 无(始终有效,没有开关设置)
加载画面 在读取过程中显示你自己做的画面 做一个场景,然后在设置中选中它
对象池 事先创建好对象的实体,消失时不丢弃而是循环利用 只需放置 1 个节点

异步加载本身不需要任何设置就在工作。 正因为读取在后台推进,加载画面才能在读取期间持续播放动画,并且要等目标场景真正进入可绘制的状态后画面才会切换。

对象池的目标是削减实例生成所花费的时间,而不是节省内存或防止碎片化。


加载画面

设置方法

  1. 点击地图一览的「+ 新建」,在类型中选择「加载场景」并命名。它会被创建在 res://loadingscenes 中,可直接编辑。
  2. 打开「设置」菜单 →「基本设置」。
  3. 打开「加载场景」的「启用」。
  4. 从「场景路径」的下拉菜单中选择第 1 步创建的场景。

就这些。不需要设置撤除的时机,也不需要按转换种类分别设置。

下拉菜单中会列出项目内的加载场景(会自动拾取根节点为 LoadingScene 的场景)。改回「未指定」就恢复为未设置状态。

把「启用」关掉,就会回到不使用加载画面的传统行为。

想更改存放位置时,可以在同一个「基本设置」的素材文件夹栏中更改「加载场景」的文件夹。

编辑方法

创建好的加载场景,与地图和菜单一样在地图制作视图中编辑。把地图一览的种类下拉菜单切换到「加载场景」就会出现在一览中。

图层停靠面板的根行是「加载设置」,从它的「+」可以放置以下 6 种。由于加载期间游戏并未运行,所以只限定为显示物和动画。

精灵 / 纯色矩形 / 动画 / 标签 / 富文本 / 九宫格矩形

与地图不同,这里没有图层,各元素直接排列在根节点下。

根节点的类型

什么类型都可以。 CanvasLayer / Control / Node2D 等都能直接使用。显示在最前面的处理由引擎完成,因此也不需要调整 layer。从地图一览创建时会是 LoadingScene。

唯独以 GameScene / MenuScene / CoreScene 为根的场景不能指定。这些是由游戏侧管理的场景,无法同时存在两个。指定后日志会报错,加载画面也不会显示。

如果想接收读取的开始・完成通知,或者想放置由可视化脚本驱动的角色,就要使用专用的 LoadingScene(后述)。

只要放上去就能生效的东西

只要放进加载画面的场景中,引擎就会代为处理,不需要做任何连接设置。

放置的东西 会怎样
简易计量条 / 图像计量条 / ProgressBar 引擎会更新其数值。它直接使用节点的最小值/最大值,所以保持默认的 0〜100 即可工作
AnimationPlayer 中名为 hide 的动画 撤除时会播放,并在播放结束之后画面才消失(名为 loading_out 也是一样的)
AnimationPlayer 的自动播放 在整个读取期间持续运行。即使游戏处于暂停状态也不会停

进度条只要是继承自 Range 的节点都可以使用。ACTG 的简易计量条/图像计量条也包含在内,直接放上去就会显示进度(未指定显示变量的计量条,会原样显示外部写入的数值)。

不过,只有按树顺序最先找到的那 1 个会被更新。如果想把进度分发给多个进度条,请在根节点的脚本中接收 set_progress(参数为 0.0〜1.0)。一旦实现了 set_progress,计量条的自动更新就会停止(为了避免重复操作)。

如果没有放置 hide 动画,引擎会用自带的黑幕自动淡出。

请关闭 hide 的循环。 如果处于循环状态,动画永远不会结束,完成通知也就不会到来。引擎检测到循环设置时会在日志中提示,并改用黑幕来撤除。

只有想在加载画面中放置由可视化脚本驱动的角色时,才需要把根节点的类型设为 LoadingScene(参见下一节)。

把根节点设为 LoadingScene 后会增加什么

加载画面的根节点可以指定为 LoadingScene。指定是可选的,它本身没有设置项。指定之后可以做到以下 2 件事。

能做到的事 内容
接收读取的通知 可以通过 signal 接收进度变化、读取开始、读取完成
让可视化脚本的角色动起来 作为子节点放置的、带有可视化脚本的对象,会像在普通场景中一样运作

第 2 项是由 LoadingScene 遍历子孙节点并进行注册实现的,因此根节点仍是 CanvasLayer 之类时不会生效。 想放置在加载期间四处走动的角色时,请设为 LoadingScene。

反过来说,上一节列举的那些(进度条的自动更新、hide 的撤除演出、自动播放、黑幕、消失时机、set_progress)与根节点的类型无关,都会生效。如果只是「想自己加工进度的显示方式」,实现 set_progress 就够了,不需要 LoadingScene。

消失的时机

以下条件全部满足后才会消失。没有设置项。

  1. 目标场景的读取已完成
  2. 目标场景已被放入画面并开始运行
  3. 放入后已绘制了 2 帧
  4. 对象池的预先生成已完成
  5. 已经过最短显示时间(0.3 秒)

第 3 项是针对「黑屏散开之后对象才冒出来」这一现象的对策。在加载画面的背后把场景预热好再消失,所以画面一消失就能立刻游玩。

目标场景在确定撤除之前不会开始运行。 这是为了避免在预先生成的等待时间里,游戏在看不见的地方擅自推进。它开始运行的时点是撤除演出(黑幕闭合,或 hide)开始的那一刻,因此首帧是在画面被遮住的期间跑的,等到能看见时显示内容已经确定了。

在游戏场景中设置的 BGM 也会等到撤除时。 不会出现画面还在、BGM 却先响起来的情况。

第 5 项是为了防止读取瞬间完成时加载画面一闪而过。这 0.3 秒是从黑幕完全打开后开始计时的,所以不会出现「刚出现就消失」的情况。

出现与消失的淡入淡出(黑幕)

如果加载画面突然出现又突然消失会显得很闪,因此引擎会开合黑幕,让切换的瞬间画面必定为纯色。

动作 时长 颜色
加载画面出现 0.2 秒 与转换前演出的颜色一致
加载画面消失 0.2 秒 与转换后演出的颜色一致
目标场景出现 转换后演出的时长(最短 0.2 秒) 与转换后演出的颜色一致

最后一项「目标场景出现」会直接使用在场景转换编辑器中设置的转换后演出(亮转)的时长。即使把转换后演出设为「无」,为了不变成硬切,也至少会淡入 0.2 秒。

如果把转换后演出设为滑动类(SLIDE_UP 等),则会变成黑幕朝指定方向退去的擦除效果。原本那种「新场景的画面滑动进来」的滑动无法再现(因为场景中带有摄像机,即使移动场景,摄像机也会一起移动,画面上看不出变化)。方向会按设置得到尊重。

如果你自行放置了 hide 动画,消失之后的演出就交给作者自己负责(不使用黑幕),这是为了避免重复淡出。

关于进度条的运动

进度条表示的是距离完成的预估,并不是严格的百分比。

区间 内容
0〜60% 资源读取中
60〜85% 从读取完成到场景放置
100% 撤除已确定

它会朝着各区间的上限平滑地增长,阶段推进时会加速。不会倒退,也不会在 100% 停住干等。

存在对象池的预先生成时,从那里到 99% 由预先生成的进度填充。 这一段是实际创建数量的比例,因此不是随时间平滑增长,而是直接反映生成的推进情况。预先生成开始的位置最低为 30%,不会比这更早开始(为了避免读取期间进度条不动看起来像卡住了)。

无法做到严格数值是引擎侧的限制。既取不到读取的中途进度,而且目标场景往往已作为邻近场景被预读,读取本身一瞬就结束了。

启动时的流程

启动后,首先会显示 1 秒 ACTION GAME MAKER 的启动画面,之后进入加载画面。

显示启动画面期间读取也在后台推进,所以不会因为显示启动画面而拖慢启动。 如果第一个场景很轻,在启动画面显示期间读取就完成了,那就不显示加载画面直接进入游戏(防闪烁)。

启动画面会直接反映「应用程序」→「Boot Splash」的设置(图像/背景色/缩放/过滤)。未指定时则使用 ACTION GAME MAKER 内置的启动画面。

不过,如果启动时还有对象池的预先生成未完成,则会优先显示加载画面而非防闪烁(因为需要有一个等待预先生成完成的对象)。

连接滑动转换时不显示

在场景转换编辑器中指定了连接滑动(SLIDE_UP_CONNECT 等)的转换,不会显示加载画面。连接滑动是为了「无缝移动到隔壁房间」的演出,若插入加载画面,连接滑动和玩家的移动演出就全被遮住了。

只要转换前演出・转换后演出中任意一方指定了连接滑动,就不会显示。


对象池

什么情况下有效

新建对象的处理是要花时间的。一个一个创建时感觉不出来,但在同时发射 20 发子弹这类批量创建的场合,那一瞬间就会掉帧。

对象池的机制是:在读取期间先把这些实体创建好,消失时也不丢弃而是让它休眠,下次生成时循环利用。由于没有了生成的等待时间,批量出现也不会卡顿。

设置方法

  1. 在地图制作视图的图层停靠面板中点击「地图设置」的「+」,选择「游戏对象池」。
  2. 向「池的一览」中添加元素。
  3. 在「对象路径」中指定想要纳入池管理的对象场景。
  4. 根据需要调整「预先生成的数量」等项目。

就这些。设置只需在这一个节点上完成。 对象侧、子弹数据侧、可视化脚本侧都没有需要设置的项目。登记到一览中的对象,无论从哪条生成路径都会经由对象池。

添加位置是地图设置的正下方(因为预先生成的计划是在场景读取时点进行的,所以不放在图层里面)。

游戏对象池的设置项

「池的一览」的每 1 个元素都有以下项目。

项目 含义 默认值
池的显示名 用于在一览中区分的名称。不用于引用 空
对象路径 作为生成来源的对象场景。该场景即为池的标识符 空
仅进行预读取 只提前完成读取,不持有池 关
预先生成的数量 事先创建好的数量 8
不足时自动追加生成 库存耗尽时是否追加创建 开

把「不足时自动追加生成」关闭后,库存耗尽的部分会照常新建,而且这些个体不会归还到池中。这不会报错,只是变回使用池之前的行为而已。

预先生成是跨帧一点点进行的(对每帧所占的时间设有上限),不会因为一口气创建完而卡住帧。进度会传递给加载画面,在预先生成结束前加载画面不会消失。

节点的放置位置

休眠中的库存由这个节点持有,因此节点一旦被销毁,库存也会随之丢失。放置位置不同,行为也不同。

放置位置 行为
不会被场景转换销毁的位置 库存会跨场景保留。转换时出借中的部分也会被归还,所以转换后可以直接继续使用
游戏场景之中 转换时节点连同库存一起被销毁。目标场景中的节点会重新进行预先生成

想让库存跨场景循环利用时,请让它常驻。 简便的做法是使用自动加载:创建一个只放了 1 个游戏对象池的场景,在项目设置的「全局」→「自动加载」中注册。注册的节点会在游戏开始时创建一次,且不会被场景转换销毁。

也可以从别的地图复制过来。 粘贴位置会在地图设置的正下方。

不要把它放在运行时生成的游戏对象的子级下。 因为预先生成的计划必须在场景读取时点就已完成,请作为专用节点直接放置到场景中。

仅进行预读取

打开「仅进行预读取」后,只会提前完成读取,不持有库存。

  • 在生成来源场景读取完成后只创建 1 个并立即丢弃。场景及其素材会保持已读取状态,因此实际生成时的卡顿会减轻
  • 该场景的对象会照常生成(不从池中分发,消失时也就直接消失)
  • 不限制根节点的类型。 非游戏对象的场景、UI 的场景也能登记
  • 由于不使用「预先生成的数量」「不足时自动追加生成」,这两项会变为不可编辑

它适合那些「不想循环利用,但想消除出现瞬间卡顿」的对象。

特效和粒子适用这个设置。 EffectObject / ParticleObject 不在循环利用的对象范围内(后述),但「仅进行预读取」不限制类型,所以可以登记。由于能提前完成首次显示时的读取,首次出现时的卡顿会减轻。 建议把打算使用的特效统一登记进来。

适用的对象

能够循环利用的只有游戏对象和轻量对象。 因为归还处理是从这两种类型的消失流程中执行的。

由物理运算驱动的对象(PhysicsGameObject)、特效(EffectObject)、粒子(ParticleObject)以及其他节点都不在范围内,因为它们没有在消失时归还到池中的路径。「对象路径」虽然可以指定任意 .tscn,但根节点不属于适用种类的场景不会创建池(运行时会在日志中提示)。

「仅进行预读取」的情况不受此限制。 特效和粒子请用它来登记,提前完成首次读取即可。

生效的生成路径

只要登记到一览中,以下全部都会自动经由池。 没有按路径分别设置的项目。

路径 示例
子弹的发射 发射子弹的动作
对象的生成 「生成对象」动作
连接对象 作为子级生成的对象
对象的变更 「变更对象」动作
复活 已消失对象的复活、从摄像机外的返回、场景切换后的复原

归还的时机

对象消失时,会代替删除而返回到池中。 消失动作、对象的变更、场景转换,任意一种情况下都会归还。

返回的个体会在归还的时点被还原为场景中保存的状态。运行时变更过的属性会回到保存值,运行时追加的节点会被删除,可视化脚本会回到从初始动作重新运行的状态。由于是在归还时而非取出时整理,所以批量取出的场合不会增加开销。

场景中一开始就配置好的对象不在归还范围内(因为它们不是从池中分发的,会直接被删除)。

编辑器的警告与日志

设置上的遗漏可以通过场景树上的警告察觉。

  • 「对象路径」未设置
  • 「对象路径」重复(同一场景登记多次时,靠后的不会被使用)

运行时会在日志中提示预先生成的失败(路径未设置/路径无法解析/重复/读取失败/根节点不属于适用种类)。

「没有池」这件事本身不会输出到日志。 未登记到一览中的对象只是照常生成,并非设置失误。

仅仅是库存耗尽的情况(「不足时自动追加生成」为关闭),也只是转为普通生成,不会输出任何内容。


实用示例

同时发射 20 发子弹时那一下会卡顿

  1. 放置游戏对象池,在「对象路径」中指定子弹的场景。
  2. 把「预先生成的数量」设为比同时出现在画面上的最大数量略多一些(20 发的话设为 24 等)。
  3. 子弹数据侧什么都不用改,直接运行。

想知道库存是否足够,可以把「不足时自动追加生成」关掉试试。如果不够,中途的子弹就会卡顿,据此增加「预先生成的数量」即可。确认完成后再把它打开,这样在意料之外的场合也不至于打不出子弹。

在加载画面上显示「Now Loading」

  1. 用地图一览的「+ 新建」→ 类型「加载场景」创建。

此时它已经带有黑色背景、计量条、「NOW LOADING …」的标签,以及让点号增减的动画。之后只要在「设置」→「基本设置」中启用,并从下拉菜单中选择这个场景就能运作。

想改变外观时,可以从图层停靠面板的「+」添加元素,调整标签的文字和计量条的颜色。若要自己制作消失时的演出,请在 AnimationPlayer 中添加名为 hide 的动画(请关闭循环)。

只把 Boss 的场景提前读取好

适用于不想循环利用、但想消除出现时卡顿的情况。

  1. 向游戏对象池的「池的一览」中添加元素。
  2. 在「对象路径」中指定 Boss 的场景。
  3. 打开「仅进行预读取」。

Boss 会照常生成,但场景和素材已读取完毕,所以出现时会更轻快。

让库存跨关卡循环利用

  1. 创建一个只放了 1 个游戏对象池的场景,保存到 res://。
  2. 在项目设置的「全局」→「自动加载」中注册该场景。
  3. 池的设置就排列在那个场景的节点上。

预先生成只会在游戏开始时跑一次,之后的场景转换不会重新创建。与按关卡分别放置的区别仅在于库存是否保留,设置项是相同的。


注意事项

  • 异步加载没有开关设置。 场景的读取始终在后台推进。需要设置的只有加载画面和对象池。
  • 加载场景的设置位于「设置」→「基本设置」。 如果要从项目设置直接修改,它在「高级设置」一侧(application/loading_scene/*),请打开开关或在搜索栏中输入 loading。
  • 加载画面的根节点不能指定 GameScene / MenuScene / CoreScene。 指定后日志会报错且不会显示。
  • 若要在加载画面中放置可视化脚本的角色,请把根节点设为 LoadingScene。 其他类型不会被注册,放上去也不会动。既不报错也不警告,是很难察觉的地方。
  • 请关闭 hide 动画的循环。 处于循环状态时完成通知不会到来。检测到时会在日志中提示,并改用黑幕撤除。
  • 若迟迟无法满足撤除条件,会强制撤除。 到底在等什么会输出到日志。预先生成的数量极多时也会在此被中断,但剩余部分会在取出时创建,不影响运作。
  • 只有 1 个进度条会被自动更新。 想显示多个时,请在根节点的脚本中接收 set_progress。一旦实现,自动更新就会停止。
  • 连接滑动转换时不显示加载画面。 只要转换前・转换后中任意一方指定了连接滑动就不显示(这是规格)。
  • 游戏对象池的节点被销毁时,库存也会丢失。 想跨场景循环利用时,请用自动加载等方式让它常驻。
  • 请勿把同一场景登记到多个池中。 由于生成来源场景就是池的标识符,靠后的那个不会被使用。可以通过场景树的警告察觉。
  • 池的适用对象只有游戏对象和轻量对象。 在「对象路径」中指定其他类型也不会创建池(使用「仅进行预读取」时则不限类型)。
  • 库存耗尽时的行为是静默的。 「不足时自动追加生成」为关闭时只是转为普通生成,也不会输出日志。想确认库存是否足够时,请故意关掉它试试。
  • 循环利用只是「不重新创建消失后返回的个体」而已。 对象的状态会在归还时还原为场景的保存值,不会从上次的中途继续。反过来说,也无法实现「让运行时添加的子节点下次也保留」这类用法。
  • 请勿把游戏对象池放在运行时生成的对象的子级下。 预先生成的计划必须在场景读取时就已完成。
  • 对象池的目标是削减生成时间。内存使用量会按预先生成的数量相应增加。
  • 预先生成的数量会直接变成加载时间。 每个个体的生成时间取决于对象的规模,但带可视化脚本的子弹要花上数毫秒。设成数百个的话就会拉长到数秒级别,请根据实际同时出现的数量来决定。
1 个赞

从 GDScript 设置 GameObjectPools

对象池(GameObjectPools)通常是通过在场景中放置节点来使用的,
但也可以在运行时从 GDScript 组装。

对象池的使用(取出・归还)由引擎自动完成,因此没有可从脚本调用的 API。
无论是子弹、「生成对象」动作还是连接对象,任何生成路径都会透明地使用对象池。
在 GDScript 中要做的只有对象池的设置与状态确认。


1. 参数(与 GameObjectPoolData 相同)

属性 类型 默认值 内容
pool_name String "" 用于一览显示的名称。不用于引用(不填也能运作)
object_path String "" 生成来源对象的场景文件路径。res://...tscn / uid://... 均可。仅限根节点为 GameObject / Area2DGameObject 的场景(is_prewarm_only 时无此限制,参见第 5 章)
is_prewarm_only bool false 仅进行预读取,不持有对象池(见下文)
create_count int 8 预先生成的数量。也可指定 0(此时只进行归还用的准备)。is_prewarm_only 打开时会被忽略
auto_expand bool true 休眠中的对象耗尽时是否追加生成

is_prewarm_only(仅进行预读取)

打开后只提前完成加载,不持有对象池。

  • 生成来源场景加载完成后,只实例化 1 个并立即销毁。场景及其依赖资源会保持已加载状态,因此实际生成时的卡顿会减轻
  • 该场景的对象会照常生成(不从池中分发,消失时也直接释放)
  • 由于不会复用,不限制根节点的类型
  • 也不受池对象类别的限制。 无论根节点是什么类型,任何场景都可以指定
  • create_count 会被忽略,在检查器中也无法编辑

对象池由 object_path 所指的场景来识别。pool_name 只是用于区分的显示名。


2. 最简示例

extends Node

func _ready() -> void:
	var data := GameObjectPoolData.new()
	data.pool_name = "敌人的子弹"
	data.object_path = "res://objects/enemy_bullet.tscn"
	data.create_count = 64
	data.auto_expand = true

	var data_list: Array[GameObjectPoolData] = [data]

	var pools := GameObjectPools.new()
	# 必须在进入树之前设置(进入树的那一刻就会开始预先生成)
	pools.set_pool_data_list(data_list)

	add_child(pools)

3. 多个对象池 + 等待完成 + 库存确认

extends Node

# 对象池设置(与 GameObjectPoolData 的参数相同)
const POOL_SETTINGS := [
	{
		"pool_name": "敌人的子弹",
		"object_path": "res://objects/enemy_bullet.tscn",
		"create_count": 64,
		"auto_expand": true,
	},
	{
		"pool_name": "爆炸特效",
		"object_path": "res://objects/explosion.tscn",
		"create_count": 8,
		"auto_expand": false,
	},
]

var _pools: GameObjectPools = null


func _ready() -> void:
	_pools = _create_pools(POOL_SETTINGS)


## 从设置列表组装 GameObjectPools 并添加到树中
func _create_pools(p_settings: Array) -> GameObjectPools:
	var data_list: Array[GameObjectPoolData] = []

	for setting in p_settings:
		var data := GameObjectPoolData.new()
		data.pool_name = setting.get("pool_name", "")
		data.object_path = setting.get("object_path", "")
		data.create_count = setting.get("create_count", 8)
		data.auto_expand = setting.get("auto_expand", true)
		data_list.append(data)

	var pools := GameObjectPools.new()
	pools.name = "GameObjectPools"

	# 预先生成会在进入树时开始,因此设置要在 add_child 之前完成
	pools.set_pool_data_list(data_list)
	pools.prewarm_finished.connect(_on_prewarm_finished)

	add_child(pools)

	return pools


## 预先生成的完成通知(该节点负责的部分全部准备好时只调用 1 次)
func _on_prewarm_finished() -> void:
	for setting in POOL_SETTINGS:
		var object_path: String = setting["object_path"]
		print("%s : 休眠 %d 个" % [object_path, _pools.get_dormant_count(object_path)])

4. 从检查器进行设置

使用 @export_custom 可以在检查器上编辑对象池设置。由于不必把设置值写进脚本,
想在项目一侧调整数量时用这种方式更方便。

extends Node

@export_custom(PROPERTY_HINT_ARRAY_TYPE, "GameObjectPoolData") var list : Array

var _pools: GameObjectPools = null


func _ready() -> void:
	_pools = _create_pools(list)


## 从设置列表组装 GameObjectPools 并添加到树中
func _create_pools(p_settings: Array) -> GameObjectPools:
	var pools := GameObjectPools.new()
	pools.name = "GameObjectPools"

	# 预先生成会在进入树时开始,因此设置要在 add_child 之前完成
	pools.set_pool_data_list(p_settings)
	pools.prewarm_finished.connect(_on_prewarm_finished)

	add_child(pools)

	return pools


## 预先生成的完成通知(该节点负责的部分全部准备好时只调用 1 次)
func _on_prewarm_finished() -> void:
	for setting in list:
		var object_path: String = setting.object_path
		print("%s : 休眠 %d 个" % [object_path, _pools.get_dormant_count(object_path)])

把这个脚本附加到常驻节点(不会被场景转换销毁的节点)上,检查器中就会出现数组,
可以添加・编辑 GameObjectPoolData 作为元素。object_path 可以通过文件选择来指定。

补充

  • 把元素的类名作为 @export_custom 的第 2 个参数(hint_string)传入,元素的类型就会传达给检查器
  • 得到的是无类型的 Array,但只要元素是 GameObjectPoolData,set_pool_data_list() 就能直接接收
  • 数组的内容会保存到附加了脚本的场景(.tscn)中。更改设置后需要保存场景
  • 把同一个脚本附加到多个节点时,请注意不要把同一场景注册到多个对象池(只有先注册的那个会被使用)

5. 注意事项

设置要在「进入树之前」完成

预先生成会在节点进入树的那一刻开始。在 add_child() 之后调用
set_pool_data_list() 是不会生效的。

数组要带类型传入

set_pool_data_list() 接收的是 Array[GameObjectPoolData]。在脚本中组装时请加上类型
(否则可能被 GDScript 的类型检查拦下)。

var data_list: Array[GameObjectPoolData] = []   # ← 加上类型

第 4 章的 @export_custom 得到的是无类型的 Array,但只要元素是 GameObjectPoolData
就可以直接传入。

对象仅限 GameObject / Area2DGameObject

可以放入对象池的是 GameObject 和 Area2DGameObject。因为只有这两个类持有归还路径,
而归还也是从这两个类的销毁路径中执行的。

PhysicsGameObject 以及普通的 Node2D 派生类不在支持范围内
(即使指定到 object_path 也不会创建对象池)。

启用 is_prewarm_only 时不受此限制。 由于不作为对象池使用,
无论根节点是什么类型,任何场景都可以指定(UI 场景等也可以)。适合只想提前把加载完成的场合。

需要目标对象一侧的设置

是否使用对象池,仅由是否登记到一览中决定。 目标场景的根节点必须是受支持的类别,
否则不会创建对象池,并会出现以下错误。

[GameObjectPools] The root node is not a GameObject or an Area2DGameObject, so the pool was not created.

节点的放置位置

  • 休眠中的对象由 GameObjectPools 节点持有。节点被销毁时,休眠中的库存也会被销毁
  • 想跨场景复用时,请把它放在场景转换不会销毁的位置(常驻节点之下)。出借中的对象会在转换时归还到池中,因此转换后可以直接继续使用
  • 放在游戏场景中时,转换会连节点一起销毁,库存也随之丢失(目标场景中的节点会重新进行预先生成)
  • 请不要把它附加在运行时生成的游戏对象之下(会被那个对象的存亡所牵连)

使其常驻的方法(自动加载)

在 项目设置 → 全局 → 自动加载 → Select Script/Scene 中注册。可以指定的有两种。

注册的内容 用途
放置了 GameObjectPools 节点的场景(.tscn) 不必编写 GDScript。只需做一个放了 1 个节点的场景,在检查器中排列对象池设置即可
附加了组装 GameObjectPools 的 GDScript 的节点(如第 4 章的脚本) 想在运行时决定设置,或想在预先生成完成后进行处理时

注册到自动加载的节点会在游戏开始时创建一次,且不会被场景转换销毁。
因此可以直接当作常驻的 GameObjectPools 使用。库存会跨场景留存,转换时
出借中的对象也会被归还。

使用 GDScript 时,若指定单独的脚本(.gd),会自动创建一个 Node 并附加上去。
想从检查器编辑对象池设置时,请做一个附加了该脚本的场景(.tscn)再注册。

预先生成会跨帧推进

为了不让加载画面卡住,预先生成会在每帧的时间预算内一点点推进。
add_child() 之后库存仍然为 0。想确认数量时,请等待 prewarm_finished。

预先生成进行到一半时也可以取出(会当场生成剩余部分并交付)。

把同一场景注册到多个节点时

先注册的 GameObjectPools 会生效,后注册一侧的对象池不会被使用
(其预先生成的部分就白费了)。开发版构建中会输出以下日志。

[ACTG][GameObjectPools] ... pool is already registered by another node. (scene:res://...)

auto_expand = false 并不是对象总数的上限

设为 false 后对象池不会增长,但库存耗尽时会回退到普通生成
(=并不是不会创建对象)。回退创建的部分不受对象池管理,
消失时会直接释放。


6. 把对象池的状态显示到 Label 上

GameObjectPools.get_all_pool_status_list() 是静态方法,因此不需要节点的引用。
树中所有 GameObjectPools 的对象池,一次调用就能全部返回。

extends Label

## 更新间隔(秒)
@export var update_interval: float = 0.2

var _elapsed: float = 0.0


func _process(delta: float) -> void:
	# 没有取出/归还的通知,所以用轮询来更新
	_elapsed += delta
	if _elapsed < update_interval:
		return
	_elapsed = 0.0
	_update_text()


func _update_text() -> void:
	var lines: PackedStringArray = PackedStringArray()

	# 预先生成的进度(x:已生成 / y:计划)
	var prewarm: Vector2i = GameObjectPools.get_all_prewarm_count()
	lines.append("预先生成 %d/%d %s" % [
		prewarm.x, prewarm.y,
		"完成" if GameObjectPools.is_all_prewarm_finished() else "生成中",
	])

	for status in GameObjectPools.get_all_pool_status_list():
		var pool_name: String = status["pool_name"]
		if pool_name.is_empty():
			pool_name = String(status["scene_path"]).get_file()

		lines.append("%s  使用 %d / 休眠 %d / 计划 %d(剩余 %d)%s%s" % [
			pool_name,
			status["lending"],
			status["dormant"],
			status["create_count"],
			status["pending_create"],
			"" if status["auto_expand"] else " [不自动追加]",
			"" if status["is_loaded"] else " [加载中]",
		])

	text = "\n".join(lines)

返回的 Dictionary 的键如下。

键 类型 内容
owner_path String 持有该对象池的 GameObjectPools 的路径(把所有节点的内容一并取得时用于区分)
pool_name String 所设置的显示名
scene_path String 生成来源场景的路径(对象池的标识符)
dormant int 休眠中(库存)的数量
lending int 出借中(使用中)的数量。也包含等待归还处理的
create_count int 预先生成的计划数量
pending_create int 预先生成剩余的数量
auto_expand bool 耗尽时是否追加生成
is_loaded bool 生成来源场景是否已加载完成、处于可用状态

返回的不是设置列表,而是实际持有的对象池。因加载失败或不属于支持类别而未能创建的对象池
(即使还留在设置中)不会被包含。

对象是树中的 GameObjectPools。同一场景被多个节点注册时,会列出相应的条数
(用于取出的只有先注册的那个,可以通过 owner_path 区分)。

由于没有取出/归还的通知信号,请用轮询来更新。即使是像子弹那样进出频繁的
对象池,把更新间隔设为 0.1〜0.5 秒,显示的开销也不会成为问题。


7. 可从 GDScript 使用的 API

API 内容
GameObjectPoolData.new() 生成设置数据。属性如第 1 章所示
GameObjectPools.new() 生成对象池节点
set_pool_data_list(list) / get_pool_data_list() 设置・获取对象池设置列表
get_dormant_count(packed_scene_path) 指定场景的休眠对象数量
prewarm_finished(信号) 该节点负责的预先生成的完成通知(仅 1 次)
GameObjectPools.get_all_pool_status_list() 静态。树内所有对象池的状态(不需要节点引用・第 6 章)
GameObjectPools.get_all_prewarm_count() 静态。预先生成的进度(x:已生成 / y:计划)
GameObjectPools.is_all_prewarm_finished() 静态。预先生成是否已完成

状态的获取只提供不需要指定节点的静态方法(无需为了显示而去查找节点,
也不必用 @export 去指向它)。

取出・归还・预先生成的驱动都在引擎内部进行,未向 GDScript 公开。