脚本函数和变量

脚本资源本质上是一个或多个用户定义的函数或变量的集合,您可以在脚本编辑器中将这些函数或变量自己编写为代码片段。您在脚本中定义的函数可以解析表达式、返回值或执行 GameMaker 语言允许的任何其他操作,就像内置的运行时函数一样。

如果您有在多个位置或多个对象中使用的代码块,或者您希望以模块化方式跨多个对象使用代码块,则通常应使用脚本函数。使用脚本定义函数意味着您只需在需要时更改函数一次,并且调用该函数的每个对象都会"接收"该更改。

从组织的角度来看,脚本也非常方便,因为它们允许您创建属于特定类别的函数组 - 例如,您可能有几个与游戏中实例之间的碰撞相关的函数,因此您可以创建一个"Collision_Functions"脚本并将所有这些函数一起存储在其中。

另请参阅:脚本函数与方法

创建函数

创建包含函数的脚本时,必须使用以下格式创建函数:

function name( parameter1, parameter2, ... )
{

    statement1;
    statement2;
    ...
}

or:

name = function( parameter1, parameter2, ... )
{
    statement1;
    statement2;
    ...
}

要在 GML Visual 中创建函数,请使用声明新函数。启用其"Temp"选项来创建方法变量(上面显示的第二种语法)。

不过,一般来说,您会为脚本函数使用第一个形式,因为它将将该函数定义为专门的脚本函数,这意味着它将在作用域全局,并被分配脚本索引,并且不需要全局前缀来识别它,因为编译器会将其识别为脚本函数。

使用第二种形式将生成一个全局范围的方法变量,因此它不会被 IDE 识别为脚本函数,并且需要使用global前缀在您的代码中引用时。

注意您可以通过在脚本中使用这两种表单,然后对每个表单调用运行时函数typeof()来进行检查。一个将被归类为"数字" - 因为它返回脚本索引 ID - 另一个将被归类为"方法"。

参数/参数

您可以为函数定义自己的参数/参数,这些参数/参数将作为局部变量提供给函数,并且可用于该函数内的任何目的:

function move(spd, dir)
{
    speed = spd;
    direction = dir;
}

该函数接受两个参数,并将它们的值应用于实例的速度方向变量。现在可以像任何运行时函数一样调用它,并且可以将参数传递给它:

var _mouse_dir = point_direction(x, y, mouse_x, mouse_y);

move(4, _mouse_dir);

基于索引的参数

还可以通过 argumentN 变量 (argument0、argument1 等 ) 或 argument[] 数组 (argument[0] 、参数 [1] 等 )。

您可以使用argument_count获取传递到函数中的参数数量,从而允许传入可变数量的参数。

function print(){
    var _str = "";

    for (var i = 0; i < argument_count; i ++)
    {
        _str += string(argument[i]);
    }

    show_debug_message(_str);
}

// In an object
print("Player : ", current_time, " : ", id, " : fired");

print()函数循环遍历所有传递的参数,并将它们添加到字符串变量中。然后将该字符串打印到输出日志中。

现在,您可以使用任意数量的字符串来调用此函数,这些字符串将全部连接在一起。

可选参数

如果未向函数提供参数,则其值将为未定义。您可以使用它来定义可选参数,并通过检查参数是否等于未定义来检查参数是否已传入。不过,您也可以显式定义参数的默认值,当未传入该参数时,将使用该默认值来代替undefined

您可以使用等号(=) 将此类默认值分配给参数,使其成为可选变量:

function move(spd, dir = 90)
{
    speed = spd;
    direction = dir;
}

如果调用上述函数时未传入dir参数,则其值将默认为90,将实例向上移动。

提示您可以省略函数调用中的参数,它们将默认为undefined(或函数定义的该参数的默认值)。

例如,编写my_func(0 ,,,1)与编写my_func(0, undefined, undefined, 1)相同。


可选变量的默认值可以是表达式,因此,例如,您可以在定义可选变量时使用变量和调用函数。请注意,只有在函数调用中未提供其可选参数时,才会执行此类表达式。请参阅以下日志记录功能示例:

function log(text = "Log", object = object_index, time = date_datetime_string(date_current_datetime()))
{
    var _string = "[" + string(time) + "] ";
    _string += object_get_name(object) + ": ";
    _string += text;
    
    show_debug_message(_string);
}

该函数采用三个参数,其中第一个参数默认为字符串常量,第二个参数默认为实例变量(在调用实例的范围内),第三个参数默认为调用函数以检索当前日期和时间。现在可以使用最多三个参数调用此函数,如以下示例所示:

log();
// Prints: [09-Jun-21 12:34:37 PM] Object1: Log

log("Player Shot", obj_player, 10);
// Prints: [10] obj_player: Player Shot

JSDoc

我们还建议您添加注释来定义函数的属性(有关更多详细信息,请参阅有关JSDoc 注释的部分),这样简单的脚本将如下所示:

/// @function                 log(message);
/// @param {string}  message  The message to show
/// @description              Show a message whenever the function is called.

function log(message)
{
    show_debug_message(message);
}

可以使用如上所示的相同格式在脚本资源中一个接一个地添加脚本的附加函数。

Multiple Functions In One Script

返回值

脚本中的函数也可以返回值,就像运行时函数一样,因此它们可以在表达式中使用。为此,您可以使用return语句:

return <expression>

应该注意的是,函数的执行在 return 语句处结束,这意味着return被调用之后的任何代码都不会运行。下面是一个来自名为"sqr_calc"的函数的简短示例函数,该函数计算传递给它的任何值的平方,如果给定值不是实数,它会使用return结束尽早调用该函数,因此实际计算永远不会运行:

/// @function           sqr_calc(val);
/// @param {real}  val  The value to calculate the square of
/// @description        Calculate the square of the given value

function sqr_calc(val)
{
    if (!is_real(val))
    {
        return 0;
    }

    return (val * val);
}

请注意,如果您创建的脚本函数没有返回值,那么在您的代码中检查是否有返回值,默认情况下您将获得值未定义

要从一段代码中调用脚本函数,只需使用与调用任何运行时函数相同的方式即可 - 即,在括号中写入函数名称和参数值。因此,上面的脚本将被这样调用:

if (keyboard_check_pressed(vk_enter))
{
    val = scr_sqr(amount);
}

注意当在代码编辑器中的脚本中使用您自己的函数时,您可以按F1 Icon或在函数名称上单击鼠标中键MMB Icon以打开脚本包含它以供直接编辑。

脚本名称与函数名称

请务必了解脚本名称独立于它们包含的函数,因此您可以用更"常识"的方式命名您的脚本,即:您的所有 AI 函数都可以采用脚本"Enemy_AI"并包含ai_target_pos()ai_alert_level()ai_state()等函数。

但是,您仍然可以将脚本命名为与您在其中定义的函数相同的名称并调用该脚本,例如如果您想要每个脚本一个函数(这对于制作可重用库非常有用,因此所有函数都显示在资源浏览器中)。执行此操作时,由于GameMaker存储资源引用的方式,了解脚本引用的行为方式以避免代码错误非常重要。

例如,考虑从实例的事件调用的以下代码:

function indirectCall(func, arg)
{
    func(arg);
}

indirectCall(myscript, arg);

上述代码尝试在方法内调用名为myscript的脚本,在本例中将会失败。这是因为内联函数实际上使用脚本资源的索引,而不是实际调用脚本函数 - 例如:如果脚本索引解析为"4",则该函数本质上是调用4(arg),这没有意义。

您可以采取以下两项措施来解决此问题:

  1. 将脚本索引转换为方法,然后将其作为参数传递
  2. 使用script_execute调用传递的脚本引用

以下展示了两种方法:

// 1
function indirectCall(func, arg)
{
    func(arg);
}

indirectCall(method(undefined, myscript), arg);

// 2
function indirectCall(func, arg)
{
    script_execute(func, arg);
}

indirectCall(myscript, arg);

注意请记住,如果您的脚本完全为空,则它不会加载到已编译的游戏中,并且将变得不存在;这意味着尝试引用此类脚本资源将使您的游戏崩溃。请注意,这仅适用于完全空的脚本,因此即使您的脚本仅包含注释,它仍然会包含在编译的游戏中。

脚本范围

这让我们了解了有关脚本及其包含的函数的最后也是最重要的一点:脚本在全局级别进行解析,并将在游戏一开始就进行编译。这意味着,技术上脚本中的所有函数都是"未绑定的"方法变量,并且脚本中函数外部声明的任何变量都将被视为全局变量。例如,考虑这个脚本:

function Foo()
{
    // Do something
}
blah = 10;
function Bar()
{
    // Do something else
}

在上述示例中,我们不仅定义了函数FooBar,还定义了变量blah,并且所有这些都被视为在global范围内创建。这些函数不需要global关键字来识别,因为编译器知道这些函数是脚本的一部分,但如果您想访问blah,则需要执行以下操作:

val = global.blah;

也就是说,我们建议您在脚本中创建全局变量时始终显式键入全局变量,以防止以后出现任何问题。脚本也是定义任何枚举(常量)的理想位置,因为将它们添加到函数外部的脚本中也意味着将在游戏之前创建它们以供使用代码实际上开始运行。下面是为游戏创建不同全局范围值的脚本示例:

/// Initialise All Global Scope Values And Constants
global.player_score = 0;
global.player_hp = 100;
global.pause = false;
global.music = true;
global.sound = true;

enum rainbowcolors
{
    red,
    orange,
    yellow,
    green,
    blue,
    indigo,
    violet
}

#macro weapon_num 3
#macro weapon_gun 0
#macro weapon_bomb 1
#macro weapon_knife 2

请注意所有这些常量是如何在任何函数调用之外设置的,这意味着它们将在其他所有操作之前并在全局范围内进行初始化。这意味着,如果您想使用脚本来初始化实例范围内的变量,则必须将它们包装在函数中,例如:

/// @function                init_enemy();
/// @description             Initialise enemy instance vars

function init_enemy()
{
    hp = 100;
    dmg = 5;
    mana = 50;
}

因此,脚本可用于在游戏开始之前生成宏、枚举和全局变量,以便随时使用它们,并且它们还可以用于创建可以使用的"未绑定"方法(用户定义的函数)在您的游戏中,例如 GML 运行时函数。

关于脚本函数,最后需要注意的一点是,如果您正在针对Web进行开发(即:以HTML5为目标),则在向脚本添加函数时可以使用一个附加函数协议,即在函数名称前加上gmcallback_前缀,例如:

gmcallback_create_button

使用上述函数名称意味着函数gmcallback_create_button()不会被混淆,因此可以在 JavaScript 扩展和游戏的其他区域中使用,例如,在使用clickable_*功能。

静态变量

函数还可以使用静态变量,这些变量在每次函数调用期间都保持其值。请阅读此页面了解更多信息。