GameMaker Studio 2 在 GML 中有一系列功能来处理 缓冲区。大多数人应该熟悉这个术语,因为它在处理计算机和编程时一直被使用,但是知道这个词并不意味着你实际上知道它代表什么。所以这个页面的目的是解释缓冲区是什么以及如何在 GameMaker Studio 2 编程环境中使用它们,尽管它们的工作方式无论语言还是技术都是一样的,这也是它们如此重要的原因之一。
缓冲区(在编程中)基本上是系统内存中的一个空间,用于存储几乎任何东西(例如数据传输、碰撞、颜色数据等等)的小数据 包。由于它保存在系统内存中,访问速度非常快,而且缓冲区通常用于非常短期的存储,比如在处理之前接收网络信息,或者存储游戏中的检查点(在页面后面给出的示例中对此进行了解释)。缓冲区是通过在系统内存中分配空间来创建的,以 字节 为单位计算,然后在游戏运行或使用适当的函数删除缓冲区之前为游戏保留空间。这意味着,即使你的游戏没有获得焦点(例如,在移动设备上,当你打电话时,游戏会被放到后台),缓冲区仍然存在,但是如果游戏关闭或重新启动,缓冲区就会丢失。
注意: 重新启动游戏将不会清除或删除缓冲区!但它将防止对先前创建的缓冲区的任何进一步访问,因为 ID 句柄 已经丢失,导致内存泄漏,最终导致游戏崩溃。所以,当重新启动游戏时,记得先删除缓冲区。
GameMaker Studio 2 允许创建四种不同的缓冲区类型原因是缓冲区被设计成一个高度优化临时存储介质,因此你应该创建一个适用于你所期望的数据类型的缓冲区,否则你可能会在代码中造成错误或遇到 瓶颈。在进一步解释这一点之前,让我们看看四种可用的缓冲区类型(在 GML 中定义为 常量):
常量 描述 buffer_fixed
以字节为单位的固定大小的缓冲区。缓冲区的大小是在创建时设置的,不能再次更改。
buffer_grow
在添加数据时将动态 增长 的缓冲区。你使用初始大小创建它(它应该是预期存储的数据的近似大小),然后它将扩展以接受溢出初始大小的更多数据。
buffer_wrap
数据会被 覆盖 的缓冲区。当要添加的数据达到缓冲区大小的限制时,写入将回到缓冲区的开始位置,从那里将继续进行进一步的写入。
buffer_fast
这是一个特殊的“剥离”缓冲区,读写速度非常快。然而,它只能与 buffer_u8 数据类型一起使用,并且必须是 1 字节对齐的。(关于 数据类型 和 字节对齐 的信息可以在本页下面找到)。
这些是在使用 GameMaker Studio 2 时可用的缓冲区类型,你选择哪种类型将在很大程度上取决于你希望使用哪种类型。例如,增长 缓冲将用于存储数据的“快照”来创建一个保存游戏,因为你不知道实际所需的数据量;快速 缓冲在当你知道你正在使用的值都是 0 到 255 或 -128 到 127 之间时使用,例如在处理 RGB 图像数据。当创建一个缓冲区时,你应该总是试图创建一个大小合适的类型,一般规则是,它应该适应最大大小创建数据存储,如果不确定,请使用 增长 缓冲以防止错误覆盖数据。
创建缓冲区的实际代码是这样的:
player_buffer = buffer_create(16384, buffer_fixed, 2);
这将创建一个 16384 字节和 2 字节对齐 的固定缓冲区,函数返回一个惟一的 id 值,该值存储在一个变量中,以便以后引用该缓冲区。现在我们已经了解了缓冲区的基本概念,你应该了解 数据类型 和前面提到的 字节对齐 方式。
当你将数据读写到缓冲区时,你将在指定“数据类型”的“数据块”中执行此操作。“数据类型”设置了在缓冲区中为正在写入的值分配的字节数,并且必须正确,否则你的代码会得到一些非常奇怪的结果(甚至错误)。
缓冲区是 按顺序 写入(和读取)的,在这种情况下,数据块是一个接一个写入的,每个数据块都是一种类型。这意味着,理想情况下,你应该知道你一直在向缓冲区写入什么数据。这些 数据类型 在 GML 中由以下 常量 定义:
缓冲区类型常量 字节数 描述 buffer_u8 1
无符号 8 位整型。值域为 0 到 255。
buffer_s8 1
有符号 8 位整型。值域为 -128 到 127(0为正数)。
buffer_u16 2
无符号 16 位整型。值域为 0 到 65535 。
buffer_s16 2
有符号 16 位整型。值域为 -32768 到 32767(0为正数)。
buffer_f16 2
16 位浮点型。值域为 -65504 到 +65504。(目前不支持!)
buffer_u32 4
无符号 32 位整型。值域为 0 到 4,294,967,295。
buffer_s32 4
有符号 32 位整形。值域为 -2,147,483,648 到 2,147,483,647(0为正数)。
buffer_f32 4
32 位浮点型。值域为 -16777216 到 +16777216。
buffer_u64 8
无符号 64 位整型。(当前不支持所有缓冲函数!)
buffer_f64 8
64 位浮点型。
buffer_bool 1
布尔类型。只能为 1 或 0 (true 或 false)
buffer_string N/A(无返回值)
这是一个以 UTF-8 null 结尾的字符串(0x00)。最基本的,一个 GameMaker 字符串被转储在缓冲区中时是以 0 结尾的。
因此,假设你创建了一个缓冲区,你可以使用以下代码向它写入信息:
buffer_write(buff, buffer_bool, global.Sound);
buffer_write(buff, buffer_bool, global.Music);
buffer_write(buff, buffer_s16, obj_Player.x);
buffer_write(buff, buffer_s16, obj_Player.y);
buffer_write(buff, buffer_string, global.Player_Name);
从上面的例子可以看到,你可以编写不同数据类型的缓冲区(你在使用 快速 缓冲类型时限于一个特定的数据类型),这个数据将被添加到连续的缓冲区(尽管其实际位置在缓冲将取决于其 字节对齐,见下面的解释)。从缓冲区读取信息的顺序也是一样的,在上面给出的例子中,你将按照编写数据的顺序从缓冲区读取信息,检查相同的数据类型,例如:
global.Sound = buffer_read(buff, buffer_bool);
global.Music = buffer_read(buff, buffer_bool);
obj_Player.x = buffer_read(buff, buffer_s16);
obj_Player.y = buffer_read(buff, buffer_s16);
global.Player_Name = buffer_read(buff, buffer_string);
如你所见,你读取信息的顺序与读取缓冲区的顺序相同。有关如何从缓冲区中添加和删除数据的详细信息,请参阅下面的 示例 部分。
如果你一直阅读此页,你将看到对缓冲区 字节对齐的引用。这基本上是指新数据将存储在给定缓冲区中的 位置。它是如何运作的?对于一个按字节排列的缓冲区,每个数据块是按顺序写入缓冲区的,每个新数据块是在前一个数据块之后直接添加的。然而,一个 2 字节对齐的缓冲区会将每段数据以 2 字节为间隔写入,因此即使你的初始写入是 1 字节的数据,下一个 写入也会被移动,以对齐到两个字节。所以,如果你的字节对齐方式设置为,比如说,4个字节,你写一段 1 个字节大小的数据然后做一个 缓冲告知(告知 使得当前位置为读/写缓冲区),你会得到 1 字节的 偏移量(抵消在这种情况下从开始的缓冲区到当前读写位置的字节数)。
然而,如果你写入另一块数据,仍然是 1 个字节大小, 然后 做一个缓冲告知,你会得到一个 5 字节的偏移量(即使你只写入 2 个字节的数据),因为对齐已经 填充 了数据,将它与 4 字节缓冲区对齐。
基本上,这意味着对齐只会影响到写入对象的位置,所以如果在写入内容后执行缓冲区告知,它将返回当前写入位置,该位置紧跟在先前写入的数据之后。但是,请注意,如果你随后编写另一个数据片段,那么在实际编写该数据片段之前,缓冲区会在内部将写入位置移动到对齐大小的下一个倍数。
下面是一些缓冲区的常用用法示例。
一个关于如何在任何平台的 GameMaker Studio 2 游戏中使用缓冲区的简单例子是 game_save_buffer 函数。此函数将获取当前游戏状态的“快照”,并将其保存到预定义的缓冲区中,然后可以从缓冲区中读取该缓冲区,以便在此时再次加载游戏。
注意:这个函数是非常有限的,这是为初学者设计的一个快速启动并运行的检查点系统,但是更高级的用户可能更愿意使用 文件函数 代码创建自己的系统,因为这个游戏不会保存任何的动态资源,你可以创建在运行时的数据结构、表面、添加背景和精灵等等……
我们需要做的第一件事是创建一个新物体来控制保存和加载,你可以创建这样一个物体并给它添加 创建事件。在这种情况下,你可以编写以下代码:
SaveBuffer = buffer_create(1024, buffer_grow, 1);
StateSaved = false;
第一行创建 1024 字节的 增长缓冲区(因为我们不知道保存数据的最终大小),并对齐到 1 字节。然后创建一个变量来检查是否保存了游戏(这将用于加载)。
接下来,我们将添加一个 按键事件(例如),其中我们将保存当前游戏状态到创建的缓冲区:
StateSaved = true;
buffer_seek(SaveBuffer, buffer_seek_start, 0);
game_save_buffer(SaveBuffer);
以上代码首先将控制变量设置为 true(以便在将游戏保存到缓冲区时保存),然后在将当前保存状态写入缓冲区之前 查找 缓冲区的开始位置。为什么要使用 buffer_seek?正如前面的 缓冲区数据类型 部分中提到的,读取和写入缓冲将从我们添加数据的最后位置开始。这意味着,如果不将缓冲区 告知 回到开始,那么在保存时,将在当前缓冲区的读/写位置向缓冲区添加数据,因此我们使用 buffer_seek 函数将 告知 移动到缓冲区起点。
我们现在已经将当前游戏状态保存到一个缓冲区中。下一步是编写代码如何加载它,可能在另一个 按键事件:
if StateSaved
{
buffer_seek(SaveBuffer, buffer_seek_start, 0);
game_load_buffer(SaveBuffer);
}
然后,游戏将在你放置上述代码的事件结束时加载。
注意: 这只能在同一个房间使用,而不是用于在你的游戏关闭或重新启动后获取完整保存的游戏状态。
最后一件事是向控制器物体添加“清理”代码。缓冲区存储在内存中,因此,如果在使用缓冲区时没有进行清理,就会出现内存泄漏,最终导致游戏延迟并崩溃。因此,你可能需要添加一个房间结束事件(来自 其他 事件类别):
buffer_delete(SaveBuffer);
这个物体现在可以被放置到一个房间中,并且从缓冲区一键保存或加载房间状态。
在使用 GameMaker Studio 2 连网功能时,必须使用缓冲区创建通过网络连接发送的数据 包。本示例旨在展示如何完成此任务,但由于联网的领域非常大,因此仅设计此示例来展示如何使用缓冲区本身,而不是完整的联网系统。
我们将展示的第一件事是为网络连接的客户端创建和使用缓冲区。这个缓冲区将用于创建小的数据包,然后这些数据包可以被发送到服务器,所以在实例的 创建事件 中,我们将分配一个这样的缓冲区:
send_buff = buffer_create(256, buffer_grow, 1);
我们将缓冲区设置为较小(256 字节),因为它不是用来保存大量数据的,我们将它设置为一个 增长 缓冲区,以确保在任何时候需要添加更多发送数据时不会出现错误,并且为了方便起见,将对齐设置为 1 字节。
现在,假设我们希望客户机向服务器发送数据。为此,我们需要创建一个缓冲区“包”,在本例中,我们将发送一个 按键按下事件,比如当玩家按下 左箭头 在游戏中移动时。为此,我们先将必要的数据写入缓冲区,然后发送出去:
buffer_seek(buff, buffer_seek_start, 0);
buffer_write(buff, buffer_u8, 1);
buffer_write(buff, buffer_s16, vk_left);
buffer_write(buff, buffer_bool, true);
network_send_packet(client, buff, buffer_tell(buff));
在写入缓冲区之前,我们将“告知”设置为缓冲区的开始,因为网络 总是 从缓冲区的 开始 获取数据。然后写入 检查 值(服务器将使用该值来确定要处理的事件的类型)、使用的键以及键的状态(在本例中为 true)。然后,网络函数将该缓冲区作为数据包发送。注意,我们 没有 发送整个缓冲区!我们只发送了写入的数据,使用 buffer_tell 函数返回缓冲区的当前读/写位置(记住写入缓冲区会将“告知”移动到已写入的内容的末尾。)。
如何接收服务器上的数据?接收到的数据包必须写入服务器上的缓冲区,然后用来更新游戏。为此,我们将在服务器的网络控制器对象中使用 网络异步事件,如下面的简化代码所示:
var buff = ds_map_find_value(async_load, "buffer");
if cmd == buffer_read(buff, buffer_u8);
{
key = buffer_read(buff, buffer_s16 );
key_state = buffer_read(buff, buffer_bool);
}
异步事件将包含一个特殊的临时 ds_map(它将在事件结束时自动删除),根据来自网络的传入数据的类型,它将包含不同的信息。在本例中,我们假设已经检查了映射,发现它是一个从客户机发送的缓冲区数据包。我们现在检查的第一条数据缓冲区,看看会发送什么样的事件——在这种情况下,值 “1” 代表了一个 按键 事件,然而当编码这些事情你应该定义 常量 保存这些值来简化,然后存储键被按下的状态(true = 按下,false = 松开)。然后,此信息将用于使用玩家发送客户端的新状态更新所有客户端。
注意:从 ds_map 创建的缓冲区在网络异步事件结束时自动删除,因此这里不需要使用 buffer_delete。