作者:Vadim(aka YellowAfterlife)
译:highway★
译注:从 2.3 更新之后基本没怎么碰过 GMS2,重新开启 GMS2.3 之后很多新加的东西都没咋看过对很多新东西有些恐惧,总是一拖又拖,这篇文章讲的很细致,比起看视频也节省一些时间。搬运过来,希望能对同样使用 GMS2,特别是我这种对 2.3 比较懵逼的人有些帮助。
Chained accessors(链式访问器)
长期以来,GameMaker 一直允许少量的 "访问器 ",
// 正常 array 操作: val = an_array[index]; an_array[index] = val; // 非写入时复制操作(non-copy-on-write): an_array[@index] = val; // 等同于 array_set(an_array, index, val) // ds_map: val = a_map[?key]; // 等同于 val = ds_map_find_value(a_map, key) a_map[?key] = val; // 等同于 ds_map_set(a_map, key, val) // ds_list: val = a_list[|index]; // 等同于 val = ds_list_find_value(a_list, index) a_list[|index] = val; // 等同于 ds_list_set(a_list, index, val) // ds_grid: val = a_grid[#x, y]; // 等同于 val = ds_grid_get(a_grid, x, y) a_grid[#x, y] = val; // 等同于 ds_grid_set(a_grid, x, y, val)
GMS 2.3 这些基础上稍作了扩展,允许将它们链接在一起--所以我们现在可以这样写:
list_of_maps[|i][?"hi"] = "hello";
来替代以前的写法:
ds_map_set(ds_list_find_value(list_of_maps, i), "hi", "hello");
对于嵌套数据结构和多维数组,这么写很方便。
Array 的改动
2D 数组现在只是嵌套的 1D 数组,你可以更容易地创建更高维数的数组。
array_1d[0] = "hi!"; // 没有改动 array_2d[1][0] = "hi!"; // 以前这么写 array_2d[0, 0] = "hi!" array_3d[2][1][0] = "hi!"; // 新加的! // ...等等
Structs
Structs
就像实例(instance),但没有任何事件或内置变量。非常轻便。
我们可以通过使用 {}
来创建一个空结构。
var q = {}; show_debug_message(q); // { } q.hi = "hello!"; show_debug_message(q); // { hi : "hello!" } q.one = 1; show_debug_message(q); // { hi : "hello!", one: 1 } 你也可以通过指定名称预先填入一些字段 name: value: var q = { a: 1, b: 2 }; show_debug_message(q); // { b : 2, a : 1 } q.c = 3; show_debug_message(q); // { c : 3, b : 2, a : 1 }
与 array
类似,Structs
由 GMS2 自动管理,这意味着你不必像对待实例那样明确地销毁它们。
Structs
可以像之前我们在实例上那样的用法一样,比如我们可以 with(a_struct)
,尽管我们不能以这种方式遍历 struct
中的每一个 "实例"--我们需要将它们添加到一个 array
或 list
中。
Structs as maps
与实例类似,struct
有 variable_struct_*
函数用于动态管理其变量。
这使得 struct
可以作为 ds_maps
的垃圾收集替代物:
var q = { a: 1 }; variable_struct_set(q, "b", 2); variable_struct_set(q, "$", "dollar"); show_debug_message(q); // { $ : "dollar", a : 1, b : 2 } show_debug_message(q.b); // 2 show_debug_message(variable_struct_get(q, "a")); // 1 show_debug_message(variable_struct_get(q, "$")); // dollar
为了方便,2.3.1 开始通过添加 struct[$key]
访问器进一步扩展了这一点:
var q = { a: 1 }; q[$"b"] = 2; // 等同于 variable_struct_set(q, "b", 2) var v = q[$"b"]; // 等同于 variable_struct_get(q, "b")
结合 array
,这允许复制大多数数据结构而无需明确的销毁它们。
一些注意事项:
- 直接
(a.b)
读/写比使用variable_struct_*
函数更快,可用于您确定结构具有变量的情况。否则variable_struct_*
函数的性能与ds_map
非常相似。 - 与
ds_map
不同,ds_map
几乎可以接受任何key
值,但struct
变量名称是字符串,因此variable_struct_set(q, 4, "four")
与variable_struct_set(q, "4", "four")
相同。 - Structs for JSON
- 2.3.1 增加了
json_stringify
和json_parse
函数,它们与现有的json_encode
和json_decode
很相似,但使用的是struct
和array
,而不是像之前的和map
和list
。
我们可以这样:
var o = { a_number: 4.5, a_string: "hi!", an_array: [1, 2, 3], a_struct: { x: 1, y: 2 } }; show_debug_message(json_stringify(o));
这会输出下面的信息:
{ "a_string": "hi!", "an_array": [ 1, 2, 3 ], "a_struct": { "x": 1, "y": 2 }, "a_number": 4.5 }
并将该字符串传递给 json_parse
会返回给我们一个嵌套 struct
。
Functions
以前,每个脚本资源都将包含在调用时要运行的单个代码片段。
像下面这样:
/// array_find_index(array, value) /// @param array /// @param value var _arr = argument0; var _val = argument1; var _len = array_length_1d(_arr); for (var _ind = 0; _ind < _len; _ind++) { if (_arr[_ind] == _val) return _ind; } return -1;
但现在,情况不同了 - 我们可以在同一个脚本资源中有多个独立的片段,通过使用 function () {
语法来区分:}
/// @param array /// @param value function array_find_index() { var _arr = argument0; var _val = argument1; var _len = array_length_1d(_arr); for (var _ind = 0; _ind < _len; _ind++) { if (_arr[_ind] == _val) return _ind; } return -1; } /// @param array /// @param value function array_push() { var _arr = argument0; var _val = argument1; _arr[@array_length(_arr)] = _val; }
其工作原理如下:
脚本中的 function name(){}
成为一个全局函数,这相当于 2.3 之前的工作方式
function name() { // code here }
function(){}
可以用作表达式,允许您执行
explode = function() { instance_create_layer(x, y, layer, obj_explosion); instance_destroy(); }
在 Create
事件中,甚至将其用作函数调用中的参数!
layer_script_begin("MyLayer", function() { shader_set(sh_brightness); shader_set_uniform_f(shader_get_uniform(sh_brightness, "u_bright"), 1); }); layer_script_end("MyLayer", function() { shader_reset(); });
在另一个函数中/在脚本外的 function name(){}
等效于:
self.name = function(){};
可以更方便使用。
任何在脚本内但在函数外的其他代码都将在游戏启动时运行;获取/设置变量将像 global.variable
一样工作:
show_debug_message("Hello!"); // 在创建任何实例之前显示 variable = "hi!"; // sets global.variable // ...函数定义
允许它被用于任何初始设置。
然而,请注意,这个程序在进入第一个房间之前就已经运行了,所以,如果你想生成实例,你会想使用 room_instance_add
。
作为一个令人愉快的奖励,你现在可以不用 script_execute
来调用存储在变量中的函数。
function scr_hello() { show_debug_message("Hello, " + argument0 + "!"); } /// ... var hi = scr_hello; script_execute(hi, "you"); hi("you"); // 新的! 与上面效果一样
现在,开始进行更有趣的补充。
命名参数
函数语法的引入还带来了另一个奇妙的补充--命名的参数!
以前,咱得这么写:
function array_push() { var _arr = argument0, _val = argument1; _arr[@array_length(_arr)] = _val; }
或者
function array_push() { var _arr = argument[0], _val = argument[1]; _arr[@array_length(_arr)] = _val; }
现在咱只需要这么写:
function array_push(_arr, _val) { _arr[@array_length(_arr)] = _val; }
这使得可选参数也更容易--任何没有提供给脚本的命名参数都将被设置为未定义,这意味着咱可以这样写:
function array_clear(_arr, _val) { if (_val == undefined) _val = 0; // 之前得这么写: var _val = argument_count > 1 ? argument[1] : 0; var _len = array_length(_arr); for (var _ind = 0; _ind < _len; _ind++) _arr[@_ind] = _val; return _arr; }
静态变量
这些变量类似于 C++ 中的局部静态变量。
也就是说,静态变量是持久的,但只在它所声明的函数中可见。
这对任何需要函数特定状态的情况来说都是很好的。
function create_uid() { static next = 0; return next++; } function scr_hello() { show_debug_message(create_uid()); // 0 show_debug_message(create_uid()); // 1 show_debug_message(create_uid()); // 2 }
静态变量在执行中第一次到达时被初始化。
function scr_hello() { // show_debug_message(some); // error - not defined static some = "OK!"; show_debug_message(some); // "OK!"" }
因此,静态变量通常位于其各自函数的开头。
Methods/function 绑定
这个功能与基于 ECMAScript 语言中的 Function.bind
完全相同。
一个函数可以被 "绑定 "到某个东西上,这将在该函数调用中把自己变成那个值,把原来的自己推到其他地方(就像 with
语句那样)。
这意味着,如果你有
// obj_some, Create event function locate() { show_debug_message("I'm at " + string(x) + ", " + string(y) + "!"); }
, 你可以同时进行
var inst = instance_create_depth(100, 200, 0, obj_some); inst.locate(); // 100, 200 var fn = inst.locate; fn(); // also 100, 200!
因为你得到的函数引用是与该实例绑定的。
一个函数可以被绑定到一个 struct
、一个实例 ID
,或者什么都没有(未定义)。
没有绑定到任何东西的函数会像 2.3 之前的脚本那样保留 self/other
。
然而,如果一个函数没有被绑定到任何东西上,但你以 some.myFunc
的形式调用它,它将被当作被绑定到 some
上。
自动绑定的工作原理如下:
- 在脚本中的
function name(){}
不绑定任何东西,保持与 2.3 之前版本的兼容性。 - 绑定到
self
的function name(){}
使得实例方法的定义更加简单(也就是说,你可以在Create
事件中拥有一系列的函数定义)。 static name = function(){}
也没有绑定任何东西,这很好,因为你不希望静态函数绑定到父函数被调用的第一个实例。- 任何其他使用
name = function(){}
的行为都会被绑定到self
。
函数可以使用方法内置函数进行[重新]绑定。一个已经被绑定的函数(function)在形式上被称为 "方法(method)"(因此被称为内置函数)。
总的来说,这不仅对实例/结构特定的函数很方便,而且还可以 "创建 "与一些自定义上下文绑定的函数
例如,你可以写一个函数,返回一个生成增量 ID
的函数(就像前面提到的的 static
),并且让每个这样的返回函数的 ID
是独立的。
function create_uid_factory() { var _self = { next: 0 }; var _func = function() { return self.next++; }; return method(_self, _func); } // var create_entity_uid = create_uid_factory(); var create_network_uid = create_uid_factory(); repeat (3) show_debug_message(create_entity_uid()); // 0, 1, 2 show_debug_message(create_network_uid()); // 0
函数调用
由于现在函数可以存储在任何地方,你也可以从任何地方调用它们。
scr_greet("hi!"); // 跟以前一样 other.explode(); // 这样可以 init_scripts[i](); // 这样也可以 method(other, scr_some)(); // 对'other'执行'scr_some',不用加'with'
内置函数引用
可以这样
var f = show_debug_message; f("hello!");
而且我们可以自动为内置函数建立索引。
var functions = {}; for (var i = 0; i < 10000; i++) { var name = script_get_name(i); if (string_char_at(name, 1) == "<") break; functions[$name] = method(undefined, i); show_debug_message(string(i) + ": " + name); } // `functions` now contains name->method pairs
这会输出:
0: camera_create 1: camera_create_view 2: camera_destroy ... 2862: layer_sequence_get_speedscale 2863: layer_sequence_get_length 2864: sequence_instance_exists
索引对于调试和脚本工具来说是非常方便的--例如,GMLive 现在使用这种机制,而不是有一个充满脚本的庞大文件来包装每一个已知的内置函数。
Constructor(构造函数)
Constructor
是一个标有 Constructors
后缀关键字的函数。
function Vector2(_x, _y) constructor { x = _x; y = _y; }
这使你能够做到
var v = new Vector2(4, 5); show_debug_message(v.x); // 4 show_debug_message(v); // { x: 4, y: 5 }
简而言之,new
关键字可以自动创建一个空结构,为它调用构造函数,然后返回它。就像其他编程语言中的类一样! 但还有更多。
Static variables 静态变量
GMS2 将把 constructor
中的静态变量视为存在于由它创建的 struct
实例中,只要 struct
实例没有覆盖该变量。
这类似于变量定义对对象的作用,或原型在其他编程语言中的作用(如 JavaScript 原型或 Lua 的元数据)。
这可以用于默认值(然后可以覆盖),但最重要的是,可以向 struct
添加 method
,而不需要在每个 struct
实例中实际存储:
function Vector2(_x, _y) constructor { x = _x; y = _y; static add = function(v) { x += v.x; y += v.y; } } // ... 然后 var a = new Vector2(1, 2); var b = new Vector2(3, 4); a.add(b); show_debug_message(a); // { x : 4, y : 6 }
注意:如果您想在 constructor
中直接覆盖静态变量(而不是在其中的 function
中),您需要使用 self.variable
来区分 static variable
和 new struct
的变量:
function Entity() constructor { static uid = 0; self.uid = uid++; }
(这将给每个实体一个唯一的 ID
)
Inheritance(继承)
一个 constructor
可以使用: Parent()
语法从另一个 constructor
继承:
function Element(_x, _y) constructor { static step = function() {}; static draw = function(_x, _y) {}; x = _x; y = _y; } function Label(_x, _y, _text) : Element(_x, _y) constructor { static draw = function(_ofs_x, _ofs_y) { draw_text(_ofs_x + x, _ofs_y + y, text); }; text = _text; }
这将首先调用父 constructor
,然后再调用子 constructor
。
在子 constructor
中定义的静态变量优先于在父 constructor
中定义的静态变量,这就为覆盖父字段提供了一种方法--因此,用上述方法,你可以做到
var label = new Label(100, 100, "Hello!"); label.step(); // 调用父 step 函数 label.draw(5, 5); // 调用子 draw 函数
如果你确实需要父 method
是可调用的,你可以在覆写子 method
之前存储它,比如说
function Label(_x, _y, _text) : Element(_x, _y) constructor { static __step = step; // 现在引用父 constructor 的 step 函数 static step = function(_ofs_x, _ofs_y) { __step(); // 调用父 constructor 的 step 函数 // ... }; // ... }
异常处理
GameMaker 函数的结构通常是不抛出错误的,除非它肯定是你的错——所以,例如,试图打开一个不存在的文本文件将返回一个特殊的索引 -1
,但试图从一个无效的索引读取将抛出一个错误。
不过,写允许失败的代码还是很方便的,不需要在过程的每一步插入安全检查。现在你可以了! 其工作原理如下。
try { // (可能引发错误的代码) var a = 1, b = 0; a = a div b; // 导致 "除以零 "的错误 show_debug_message("this line will not execute"); } catch (an_exception) { // 对错误信息做一些事情(或不做),这些信息是 // 现在存储在局部变量 an_exception 中。 show_debug_message(an_exception); }
"内置 "错误是带有几个变量的结构。
message
: 一个字符串,包含对错误的简短描述。例如,如果你试图做整数除以0
,它将是 "DoRem :: Divide by zero"。longMessage
: 一个对错误和callstack
有较长描述的字符串。如果你不处理这个错误,这将出现在内置的错误弹出窗口。Stacktrace
: 表示调用堆栈的字符串数组 - 导致问题点的一连串函数名。当从 IDE 或使用 YYC 运行时,行号将包含在每个函数名之后(例如gml_Script_scr_hello
(第 5 行))。script
: (技术上的)错误起源的脚本/函数的名称。这与抓取stacktrace
中的第一项没有太大区别。
你也可以抛出你自己的异常--可以通过调用 show_error
和错误文本。
try { show_error("hey", false); } catch (e) { show_debug_message(e.message); // "hey" }
或通过使用 throw
关键字(允许任意的值被 "抛出")。
try { throw { message: "hey", longMessage: "no long messages today", stacktrace: debug_get_callstack() } } catch (e) { show_debug_message(e); // 输出上述 struct }
Try-catch
块可以嵌套在同一个或不同的脚本中。
当这种情况发生时,最近的捕获块将被触发。
如果你不想处理一个异常,你可以“重新抛出”它。
try { try { return 10 / a_missing_variable; } catch (e) { if (string_pos("DoRem", e.message) != 0) { show_debug_message("Caught `" + e.message + "` in inner catch!"); } else { throw e; } } } catch (e) { show_debug_message("Caught `" + e.message + "` in outer catch!"); }
如果一个异常没有被捕获,你会得到熟悉的错误弹出窗口。除非……
exception_unhandled_handler
在可以被认为是最后一道防线的情况下,GMS2 现在还提供了一个函数,当一个异常没有被捕获,你的游戏即将关闭时,这个功能将被调用。这覆盖了默认的错误弹出窗口。
exception_unhandled_handler(function(e) { show_message("Trouble!\n" + string(e.longMessage)); }); show_error("hey", true);
正如文档所指出的,在这一点上你能做的不多,但你可以将错误文本(连同任何可能证明有用的上下文)保存到一个文件中,这样你就可以在游戏开始时加载它,并为用户提供一个报告。
较小的添加物
主要是便利功能。
String functions
增加了 string_pos_ext
、string_last_pos
和 string_last_pos_ext
,以处理从偏移量和/或从字符串末尾开始搜索子串的问题,这对解析数据很有帮助--例如,见我以前的“在分隔符上分割字符串”的帖子。
Array functions
增加了一些数组函数来处理数组。
array_resize(array, newsize)
这将一个数组的大小调整为新的大小,要么在数组的末尾添加零,要么删除元素以满足大小。
var arr = [1, 2, 3]; array_resize(arr, 5); show_debug_message(arr); // [1, 2, 3, 0, 0] array_resize(arr, 2); show_debug_message(arr); // [1, 2]
使得其他各种实用函数得以实现。
array_push(array, ...values)
将一个或多个值添加到一个数组的末端。
var arr = [1, 2, 3]; array_push(arr, 4); show_debug_message(arr); // [1, 2, 3, 4] array_push(arr, 5, 6); show_debug_message(arr); // [1, 2, 3, 4, 5, 6] array_insert(array, index, ...values)
array_insert(array, index, ...values)
在一个数组中的偏移处插入一个或多个值。
var arr = [1, 2, 3]; array_insert(arr, 1, "hi!"); show_debug_message(arr); // [1, "hi!", 2, 3]
array_pop(array)➜value
移除数组中的最后一个元素,并将其返回。
var arr = [1, 2, 3]; show_debug_message(array_pop(arr)); // 3 show_debug_message(arr); // [1, 2] array_delete(array, index, count)
array_delete(array, index, count)
删除数组中某一偏移处的元素
var arr = [1, 2, 3, 4]; array_delete(arr, 1, 2); show_debug_message(arr); // [1, 4] array_sort(array, sorttype_or_function)
array_sort(array, sorttype_or_function)
对一个数组进行升序/降序排序(就像 ds_list_sort 一样)。
var arr = [5, 3, 1, 2, 4]; array_sort(arr, true); show_debug_message(arr); // [1, 2, 3, 4, 5]
或通过提供的“comparator
”函数传递每个元素
var strings = ["plenty", "1", "three", "two"]; array_sort(strings, function(a, b) { return string_length(a) - string_length(b); }); show_debug_message(strings); // [ "1","two","three","plenty" ]
script_execute_ext
记得我们以前通过 switch
语句来根据某种情况 script_execute
么?现在不需要了。
var arr = [1, 2, 3, 4]; var test = function() { var r = ""; for (var i = 0; i < argument_count; i++) { if (i > 0) r += ", "; r += string(argument[i]); } show_debug_message(r); } script_execute_ext(test, arr); // `1, 2, 3, 4` - 整个 array script_execute_ext(test, arr, 1); // `2, 3, 4` - 从偏移量开始 script_execute_ext(test, arr, 1, 2); // `2, 3` - 偏移量和计数
数据结构检查
增加了四个函数,用于检查 ds_list
和 ds_map
项是否为 map/list
:
ds_list_is_map(id, index) ds_list_is_list(id, index) ds_map_is_map(id, key) ds_map_is_list(id, key)
这可以验证你正在访问的东西(特别是对于 json_decode
输出)确实是一个 map/list
ds_map functions
增加了两个函数用于枚举 map
的键/值:
ds_map_values_to_array(id,?array) ds_map_keys_to_array(id,?array)
这些对于迭代大型 map
来说是很方便的,特别是如果你希望在迭代过程中修改它们(这就是 ds_map_find_*
函数有未定义行为的地方)。
类型检查功能
is_struct
, is_method
已经被加入,用于检查一个值是否是一个结构或一个绑定的函数,但还有一个额外的功能--is_numeric 将检查一个值是否是任何数字类型(real
, int32
, int64
, bool
)。
突破性改变
需要注意的几件事:
2d array functions
由于 2d 数组函数现在已被废弃,它们翻译成如下。
array_length_1d(arr) ➜ array_length(arr)
array_height_2d(arr) ➜ array_length(arr)
array_length_2d(arr, ind) ➜ array_length(arr[ind])
这里的意思是 array_height_2d
并不关心你的数组是否真的是 2D 的(里面有子数组),因此在 1D 数组上使用时会返回意外的值--例如 array_height_2d([1, 2, 3])
是 3。
你可以通过以下方式来解决这个问题
function array_height_2d_fixed(arr) { var n = array_length(arr); if (n == 0) return 0; // 空/不是一个数组 for (var i = 0; i < n; i++) if (is_array(arr[i])) return n; return 1; // 里面没有数组 }
(只有当数组包含子数组时才会返回 > 1
)
但是这仍然会对包含 1d 数组的 1d 数组产生误报,因为现在 2d 数组就是这样。
默认返回值
以前,如果脚本/函数没有返回任何东西,则脚本/函数调用会返回 0
。
现在它们会返回 undefined
。
这通常是一个很好的变化,因为 GameMaker 在很多地方仍然使用数字 ID
(忘记返回一个值可能会导致你使用一个有效但不相关的结构,索引为 0
),但可能会打破旧的代码,这些代码只能通过偶然的机会真正起作用。
在 2.3.1 中,一些内置函数也同样被修改为如果它们不应该返回任何东西,则返回 undefined
(以前也是 0
)。
self/other
值
在 GameMaker≤8.1 时代,写
show_debug_message(self); show_debug_message(other);
将分别显示 -1
和 -2
,这在大多数函数中被视为一种特殊情况。
这在 GMS1 中被改变了,相当于 self.id
和 other.id
。
现在这一点又被改变了,self/other
现在给你提供了实例“structs
”--所以
hi = "hello!"; show_debug_message(self);
现在将显示 { hi : "hello!" }
。这有一些影响。
self-struct
不等于self.id
,所以依赖它的旧代码会被破坏。(在这种情况下,对self
的使用最好用self.id
代替)。- 与通过
ID
引用不同,使用实例结构,即使实例已经通过instance_destroy
从房间中移除,你也可以使用实例变量(但仍然可以使用instance_exists
检查它是否在房间中)。
Prefix-ops as then-branch
这种
if (condition) ++variable;
这种
if (condition) --variable;
由于各种新的句法结构造成的歧义,不再允许使用,这使得很难判断您的意思是 if (condition)++
(条件表达式的后增量) 还是 if (condition) ++
(在 then-branch
表达式上预增量)。
如果您想要个人看法,我宁愿禁止将 (variable)++ 等同于 variable++ - 我认为我没有看到在任何项目中有意使用这种构造。
无论如何,这很容易解决。
array[$hex]
由于 a[$b]
现在用于结构访问器(见上文),试图做 array[$A1]
(以前用 Pascal 风格的十六进制字头索引的数组访问)将不会像以前那样工作(而是试图从一个叫 A1
的变量中读取键)。
你会想把它改为 array[ $A1]
(为了清晰起见,有一个空格)或 array[0xA1]
(C 语言风格的十六进制字面)。
image_index
以前,image_index
被允许溢出 image_number
,这将使它在绘图时循环(image_index % image_number
)。
在 2.3 版本中,试图分配 image_index
超过 image_number
时,会在分配时将其循环回来,这意味着:
sprite_index = spr_3_frames; image_index = 4; show_debug_message(image_index);
将显示 1
而不是 4
。
在大多数情况下,这是无害的,并修复了一些与在游戏启动时保存越来越大的索引有关的奇怪现象,但这确实意味着,像
if (image_index >= image_number) { image_index = 0; sprite_index = spr_other_sprite; }
将不再触发,需要进行修改。
buffer_get/set_surface
当导入旧项目到 2.3.1 时,你会经常看到以下错误。
wrong number of arguments for function buffer_get_surface wrong number of arguments for function buffer_set_surface
这是因为在 2.3.1 之前,这些函数有如下签名。
buffer_get_surface(buffer, surface, mode, offset, modulo) buffer_set_surface(buffer, surface, mode, offset, modulo)
而现在他们有了以下内容。
buffer_get_surface(buffer, surface, offset) buffer_set_surface(buffer, surface, offset)
有关这方面的更多信息,请见此文。
结论和进一步阅读
请放心,2.3 的变化是非常令人兴奋的,并且拓宽了在 GML 中可以做的事情的视野。最值得注意的是,许多 JavaScript 代码现在可以很容易地被移植到 GML 中,正如用户创建的库(如 GMLodash)所展示的那样。
关于这里可能没有涵盖的细节,你可以查看
玩得开心!
2021 年 12 月 3 日
晴
依旧实用,马克后食用。
最后的2.3资源doc实在是太详细了!
@顺子:嗯,眼花缭乱