Arrays

数组非常有用,是制作游戏的重要组成部分。它本质上是一种变量类型,可以将多个值保存为"列表"——考虑以下代码:

numbers = [ 0, 1, 2, 3, 4, 5 ];

fruits = [ "Apples", "Oranges", "Mangoes" ];

我们使用[item, item, item]语法创建一个存储在变量中的数组。稍后可以使用整数通过该变量访问存储在数组中的项目,从0开始,该整数位于[]括号内:

first_fruit = fruits[ 0 ];
second_fruit = fruits[ 1 ];
// ...and so on.

一维数组一维数组

在进一步讨论之前,让我们先澄清一下数组实际上是什么以及它的结构。数组只是分配给变量的一种数据类型,它不仅可以包含一个值,还可以包含多个值。下图显示了基本数组的示意图:

这称为1D(一维)数组,如您所见,该数组存储在变量"a"中并包含多个值。要访问该数组,您可以执行如下操作:

var _val = a[0];
show_debug_message(_val);

上面的代码从数组"a"的位置 0 获取值,然后将其输出到控制台,根据上图所示数组的内容,控制台将输出 125。如果您执行以下操作:

var _val = a[3];
show_debug_message(_val);

输出将显示"Hi!"。

正如您所看到的,您为数组指定了一个变量名称,然后在方括号[]中指定了一个值,其中该值是数组中要从中获取数据的位置。所以本质上,数组是一个容器,有多个槽来存储值,容器中的每个位置都有一个特定的数字来标识它,这就是我们放在[]中的数字。值得注意的是,数组的内容始终从 0开始,并且决不能为负数

创建数组

我们已经展示了如何检查数组中的数据,但是我们如何开始创建数组呢?首先,我们必须先对其进行初始化,然后才能使用它,否则GameMaker将会给我们带来错误。初始化数组只是意味着我们为数组的每个槽赋予一个初始值,以准备在项目代码的其他地方使用它。记住这一点很重要,因为这意味着您必须在使用数组之前进行一定量的规划,但使用重复循环初始化数组很容易,如下所示:

var i = 9;

repeat(10)
{
    array[i] = 0;
    i -= 1;
}

这个简单的代码将初始化一个十槽数组(从 0 到 9)以保存 0,即:数组中的每个槽都包含值 0。您会注意到该数组已向后初始化,最后一个槽被初始化为首先定义值。这并非绝对必要,但却是最佳方法,因为它将在内存中保留一个与数组大小完全相同的空间,而如果您从 0向上初始化数组,则必须重新分配内存-为每个添加的附加值分配(因此对于十槽数组,在循环中初始化它会改变内存分配十次)。对于较小的阵列,速度差异可以忽略不计,但较大的阵列应尽可能以这种方式进行优化。

注意:HTML5 导出是上述规则的例外,当定位时,您应该从 0 开始按连续顺序初始化数组。

您还可以使用 GML 函数array_create()初始化具有固定大小的数组,甚至可以创建不包含值的"空"数组,例如:

my_array = [];

这告诉 GameMaker 变量"my_array"是一个数组,您可以在以后随时向其中添加值。但是,如果您尝试访问空数组中的值,则会收到错误。

如果您已经知道要放入数组中的项目,则可以在声明数组时在括号之间添加逗号分隔的值:

my_array = ["Steve", 36, "ST-3V3 - Steve Street"];

数组界限

您应该始终注意仅访问有效的数组位置,因为尝试访问数组外部的值也会出错。例如,这将导致项目在运行时崩溃:

my_array = array_create(5, 0);
var _val = my_array[6];

该数组仅初始化为 5 个位置,但我们尝试获取位置 7 - 由于数组从 0 开始编号,array[6]是位置 7 - 因此游戏会生成错误并崩溃。

使用数组

现在我们如何实际使用数组呢?与我们使用普通变量完全相同,如以下示例所示:

// Add two array values together
total = array[0] + array[5];

// Check an array value
if array[9] == 10
{
    // Do something
}

// Draw an array value
draw_text(32, 32, array[3]);

由于数组是按顺序编号的,这意味着您也可以循环遍历它们来执行额外的操作,就像我们初始化它一样:

var total = 0;

for (var i = 0; i < 10; ++i)
{
    total += array[i];
    draw_text(32, 32 + (i * 32), array[i]);
}

draw_text(32, 32 + (i * 32), total);

上面的代码将把数组中的所有值相加,绘制每一个值,然后在最后绘制总值。

删除数组

关于数组最后要提到的是,您可以简单地通过将定义数组的变量"重新分配"为单个值来删除数组。这将释放与该数组的所有位置和值关联的内存。例如:

// Create an array
for (var i = 9; i > -1; --i)
{
    a[i] = i;
}

// Delete the array
a = -1;

如果数组有多个维度(见下文),它们也将被清理,并且请注意,当您在实例中创建数组时,当实例从游戏中删除时不需要清理这些数组,因为它们将被清理。在 Destroy 或 Room End 时由垃圾收集器自动删除。不过,如果任何数组位置包含对动态资产(例如粒子系统、缓冲区或数据结构)的引用,则需要在删除数组、销毁实例之前销毁这些或者房间结束。

 

多维数组多维数组

我们现在知道什么是一维数组,但在GameMaker中,您可以拥有多个维度的数组,这些数组本质上的结构为数组内数组内数组...例如,以下是2D(二维)数组:

array[0][0] = 5;

这本质上是告诉 GameMaker 该数组实际上是由各种一维数组组成的。这是一个扩展示例:

array[0][0] = 0;
array[0][1] = 1;
array[0][2] = 2;

array[1][0] = 3;
array[1][1] = 4;
array[1][2] = 5;

在上面的代码中,array[0]保存另一个数组,array[1]也是如此。

或者,要访问二维数组,您还可以使用以下语法:

array[0, 0] = 5;

注意以上语法仅适用于二维数组。

多维数组需要在使用前进行初始化,与单个一维数组相同,并且可以保存实数、字符串和任何其他数据类型,就像任何变量一样,使其成为任何游戏的理想选择需要以易于访问的方式存储大量数据(请记住,您可以轻松地循环遍历数组)。

您还可以通过嵌套一维数组在一条语句中初始化多维数组:

two_dimensional_array = 
[
    ["Apple", 10, 2],
    ["Orange", 5, 2],
    ["Mango", 15, 4],
    // ...and so on.
]

多维数组也不限于二维,您可以根据代码的需要为数组添加 3、4 或更多个维度,只需添加[n]个其他参数即可,例如:

array[0][0][0] = 1;     // A three dimensional array
array[0][0][0][0] = 1;  // A four dimensional array
// etc...

还应该注意的是,数组中每个维度的长度可以不同,因此您可以将初始数组维度的长度设置为 3,但对于第一个维度中的每个槽,第二个维度条目的长度可以不同;例如:

array[2][2] = "3";
array[2][1] = "2";
array[2][0] = "1";

array[1][3] = "four";
array[1][2] = "three";
array[1][1] = "two";
array[1][0] = "one";

array[0][1] = 2;
array[0][0] = 1;

在上述代码中,array[0]有 2 个槽,array[1]有 4 个槽,array[2]有 3 个槽。

扩展示例

这是如何在实际游戏中使用这一点的最后一个示例:假设您想根据随机值在游戏中的四个不同点生成四个不同的敌人。好吧,我们可以使用二维数组来完成此操作,并节省编写大量代码。

首先,我们应该初始化我们将在"控制器"对象的 Create 事件中使用的数组(请注意使用注释来提醒您每个数组条目的作用):

enemy[3][2] = 448;       //y position
enemy[3][1] = 32;        //x position
enemy[3][0] = obj_Slime; //Object
enemy[2][2] = 448;
enemy[2][1] = 608;
enemy[2][0] = obj_Skeleton;
enemy[1][2] = 32;
enemy[1][1] = 608;
enemy[1][0] = obj_Knight;
enemy[0][2] = 32;
enemy[0][1] = 32;
enemy[0][0] = obj_Ogre;

现在,我们有了要生成实例的对象及其在房间内相应的 x 和 y 生成坐标,所有这些都存储在我们的数组中。现在可以在控制器对象的另一个事件(例如警报或按键事件)中按如下方式使用:

//get a random number from 0 to 3, inclusive
var i = irandom(3);

//Use the array to create the object
instance_create_layer(enemy[i][1], enemy[i][2], "Enemy_Layer", enemy[i][0]);

该短代码现在将在游戏房间中生成一个随机敌人,并且它使用的代码比"if / then / else"结构甚至"switch"少得多,并且因为数组是在创建事件中一起初始化后,编辑和更改任何这些值都会变得更加容易,因为它们没有硬编码到项目代码的其余部分中。

 

另请参阅:数组函数

数组作为函数参数

您可以将数组作为参数传递到脚本函数方法变量中,并在函数内的任意位置修改这些数组。这样做也会修改原始数组。

例如,此函数只是更改传递给它的数组的前三个元素:

modify_array = function (array)
{
    array[0] = 2;
    array[1] = 4;
    array[2] = 6;
}

您现在可以创建一个数组并将其传递给此函数,该函数将修改该数组:

my_array = [100, 4, 214];

modify_array(my_array);

show_debug_message(my_array); // Prints [2, 4, 6];

在以前的 GameMaker 版本中,情况并非如此,因为修改函数内的数组会创建一个副本。如果需要,仍可以启用此已弃用的行为:请阅读下面的"写入时复制"部分以了解更多信息。

写时复制

写入时复制行为已弃用,并且仅当在常规游戏选项中启用"启用数组的写入时复制行为"时才使用。本节描述启用此选项时阵列的行为。

如上一节所述,数组可以作为参数传递给函数。为此,您只需指定数组变量(不需要每个单独的位置,也不需要[]括号),整个数组将通过引用传递到函数中:

my_array = [1, 2, 4, 8, 16];

do_something(my_array);

不过,当启用写入时复制时,更改函数内任何数组的值都会根据您的修改创建一个临时副本。原始数组没有被修改。这种行为称为写入时复制。

要实际修改传递到函数中的原始数组,您必须将其返回,或使用@访问器。

例如,上面调用的函数do_something()可能会执行如下简单操作:

do_something = function(array)
{
    array[1] = 200;
}

现在,您可能希望my_array保存值1、200、4等,这通常是正确的 - 但当启用写入时复制时,原始数组不受影响。

要解决此问题,您可以让函数返回修改后的数组副本,然后将其应用回原始变量:

my_array = [1, 2, 4, 8, 16];

my_array = do_something(my_array);

该函数本身将返回修改后的数组:

do_something = function(array)
{
    array[1] = 200;

    return array;
}

注意如果您不更改任何数组的值,而是引用它们,则不需要上述代码。引用数组不会复制它,并且解析速度会更快。

第二种解决方案是使用 @ 访问器直接更改数组值,这样可以节省必须进行临时复制的 CPU 开销。这意味着您不需要从函数返回数组,可以直接编辑它:

do_something = function(array)
{
    array[@ 1] = 200;
}

使用此访问器会绕过写入时复制行为并直接修改引用的数组。这可用于有选择地禁用特定语句的写入时复制,同时保持该选项启用。

同样,如果 禁用 ( 这是默认且推荐的选项 ) 写入时复制 ,则所有这些都是不必要的。

从以下页面了解有关访问器及其工作方式的更多信息以及数组示例: