缓冲区使用指南

缓冲(在编程中)基本上是系统内存中的一个空间,用于存储几乎任何东西的小数据包(例如:数据传输、冲突、颜色数据等)。由于它被保存在系统存储器中,所以访问它非常快,并且缓冲器通常用于非常短期的存储,比如在处理网络信息之前接收网络信息,或者用于在游戏中存储检查点(这在页面后面的示例中进行了解释)。

Buffer Memory通过在系统内存中分配空间(以字节计算)来创建缓冲区,然后只要游戏运行或直到您使用适当的函数删除缓冲区,该空间就会保留给您的游戏(你可以在这里找到所有列出的GML缓冲区函数)。这意味着即使你的游戏没有焦点,(例如,在移动终端上,当您接听电话时,游戏将被置于后台)缓冲区将仍然存在,但是,如果游戏被关闭或重新启动,则缓冲区将丢失。

注意重新启动游戏不会清除或删除缓冲区!但它会阻止对先前创建的缓冲区的任何进一步访问,因为ID句柄将丢失,导致内存泄漏,最终会导致游戏崩溃。因此,当重新启动游戏时,请记住首先删除所有缓冲区。

GameMaker允许创建四种不同的缓冲区类型。原因是缓冲区被设计为高度优化的临时存储介质,因此您应该创建适合您希望存储的数据类型的缓冲区,否则您可能会出错或导致代码瓶颈。在进一步解释之前,让我们看看四种可用的缓冲区类型(在GML中定义为常量):

 

常量描述
buffer_fixed
A buffer of a fixed size in bytes. The size is set when the buffer is created and cannot be changed again.
buffer_grow
A buffer that will grow dynamically as data is added. You create it with an initial size (which should be an approximation of the size of the data expected to be stored), and then it will expand to accept further data that overflows this initial size.
buffer_wrap
A buffer where the data will wrap. When the data being added reaches the limit of the buffer size, the overwrite will be placed back at the start of the buffer, and further writing will continue from that point.
buffer_fast
This is a special "stripped down" buffer that is extremely fast to read/write to. However it can only be used with buffer_u8 data types, and must be 1 byte aligned. (Information on data types and byte alignment can be found further down this page).

 

这些是使用GameMaker时可用的缓冲区类型,您选择哪一种将在很大程度上取决于您希望将其用于的用途。例如,增长缓冲区将用于存储数据的"快照"以创建保存游戏,因为您不知道要放置在其中的实际数据量,或者,当您知道正在处理的值都在0到255或-128到127之间时,例如在处理图像中的ARGB数据时,将使用快速缓冲区。

Buffer Types在创建缓冲区时,您应该始终尝试将其创建为适合类型的大小,一般规则是应创建它以容纳要存储的最大数据大小,如果有疑问,请使用增长缓冲区以防止覆盖错误。

创建缓冲区的实际代码如下所示:

player_buffer = buffer_create(16384, buffer_fixed, 2);


这将创建一个16384字节的固定缓冲区,字节对齐为2,函数返回一个唯一的ID值,该值存储在一个变量中,供以后引用此缓冲区。

当阅读和写数据到一个缓冲区时,你在"数据类型"定义的"数据块"中进行操作。"数据类型"设置了缓冲区内为写入的值分配的字节数,这一点很重要,否则你的代码会得到一些非常奇怪的结果(甚至错误)。

缓冲区是按顺序写入(和读取)的,因为一段数据是一段接一段地写入的,每段数据都是一个集合类型。这意味着您应该始终知道您正在写入缓冲区的数据。这些数据类型在GML中由以下常量定义:

缓冲区数据类型常量
常量描述返回的数据类型
buffer_u8An unsigned, 8bit integer. This is a positive value from 0 to 255.int32
buffer_s8A signed, 8bit integer. This can be a positive or negative value from -128 to 127 (0 is classed as positive).int32
buffer_u16An unsigned, 16bit integer. This is a positive value from 0 - 65,535.int32
buffer_s16A signed, 16bit integer. This can be a positive or negative value from -32,768 to 32,767 (0 is classed as positive).int32
buffer_u32An unsigned, 32bit integer. This is a positive value from 0 to 4,294,967,295.int64
buffer_s32A signed, 32bit integer. This can be a positive or negative value from -2,147,483,648 to 2,147,483,647 (0 is classed as positive).int32
buffer_u64An unsigned 64bit integer. This is a positive value from 0 to 18,446,744,073,709,551,615.int64
buffer_f16A 16bit float. This can be a positive or negative value within the range of +/- 65504.number (real)
buffer_f32A 32bit float. This can be a positive or negative value within the range of +/-16777216.number (real)
buffer_f64A 64bit float.number (real)
buffer_boolA boolean value, can only be either 1 or 0 (true or false). It is stored in a single byte (8bit)int32
buffer_stringA string of any size, including a final null terminating characterstring
buffer_textA string of any size, without the final null terminating characterstring

所以,假设你已经创建了一个缓冲区,你想向它写入信息,那么你会使用类似下面的代码:

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字节的数据,下一次写入将被移动到对齐两个字节:

Buffer Byte Alignment所以,如果字节对齐设置为4字节,并且您写入一个大小为1字节的数据,然后执行buffertell(tell获取缓冲区的当前阅读/写位置),则您将获得1字节的offset(本例中的offset是从缓冲区开始到当前读/写位置的字节数)。

但是,如果您写入另一段数据,也是1字节大小,然后执行缓冲区告诉,您将获得5字节的偏移量(即使您只写入了2字节的数据),因为对齐已填充数据以将其与4字节缓冲区对齐对齐。

基本上,这意味着对齐只会影响写入内容的位置,因此,如果您在写入内容后执行缓冲区通知,它将返回紧随您之前写入的数据之后的当前写入位置 书面。 但请注意,如果您随后写入另一条数据,则在实际写入该数据之前,缓冲区内部会将写入位置移动到对齐大小的下一个倍数。

下面我们有几个关于如何在项目中使用缓冲区的例子:

缓冲检查点缓冲检查点

一个简单的例子是如何在任何平台的任何GameMaker游戏中使用缓冲区,函数game_save_buffer()。该函数将获取当前游戏状态的"快照"并将其保存到预定义的缓冲区,然后可以从该缓冲区读取以再次加载游戏。

注意此功能非常有限,它是为初学者设计的,用于快速启动和运行检查点系统,但更高级的用户可能更喜欢使用File功能编写自己的系统,因为游戏不会保存任何您可以在运行时创建的动态资源,如数据结构,表面,添加的精灵等。

我们需要做的第一件事是创建一个新的对象来控制保存和加载,所以你可以创建一个并给予它一个Create Event。在这个事件中,你可以放置以下代码:

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()将tell移动到缓冲区的起始位置。

我们现在已经将当前游戏状态保存到了一个缓冲区中。下一步将是编写如何加载它的代码,可能是在另一个Keypress Event中:

if (StateSaved)
{
    buffer_seek(SaveBuffer, buffer_seek_start, 0);
    game_load_buffer(SaveBuffer);
}

然后,游戏将在放置上述代码的事件结束时加载。

注意这仅适用于在同一房间内使用,而不是用于在游戏关闭或重新启动后生成完整的已保存游戏!

最后要添加到控制器对象的是一些"清理"代码。缓冲区存储在内存中,因此,如果您在使用完它们后不清理它们,您可能会得到内存泄漏,最终会延迟并导致游戏崩溃。因此,您可能会添加房间结束事件(来自其他事件类别):

buffer_delete(SaveBuffer);

这个对象现在可以被放置到一个房间里,然后按下保存键,从一个缓冲区加载房间状态。

 

网络缓存网络缓存

当使用GameMaker网络功能时,您必须使用缓冲区来创建通过网络连接发送的数据包。本示例旨在展示如何完成此操作,但由于网络可能性的范围,它仅旨在展示如何使用缓冲区本身,而不是完整的网络系统。

我们首先要展示的是为网络连接的客户端创建和使用缓冲区。此缓冲区将用于创建小数据包,然后将其发送到服务器,因此在实例的Create Event中,我们将像这样分配缓冲区:

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));

在写入缓冲区之前,我们已将“tell”设置为缓冲区的开头,因为网络始终从缓冲区的开头获取数据。 然后我们写入检查值(服务器将使用它来确定要处理的事件类型),然后写入正在使用的键,然后写入键的状态(在本例中为按下状态)。 然后,该缓冲区由网络功能作为数据包发送。 请注意,我们不会发送整个缓冲区! 我们只发送写入的数据,使用 buffer_tell 函数返回缓冲区的当前读/写位置(请记住,写入缓冲区会将“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 映射 async_load(它会在事件结束时自动从内存中删除),其中包含不同的信息,具体取决于来自网络的传入数据的类型。 在这种情况下,我们假设该地图已被检查并发现是从客户端发送的缓冲区数据包。 我们现在检查缓冲区中的第一条数据,以查看已发送的事件类型 - 在本例中,值“1”代表关键事件,但是在编码这些内容时,您应该定义常量来保存这些值 简化操作 - 然后存储按下的按键及其状态(true = 按下,false = 释放)。 然后,该信息将用于向所有客户端更新发送客户端玩家的新状态。

注意从 DS 映射创建的缓冲区会在网络异步事件结束时自动删除,因此此处无需使用 buffer_delete()