【GameMaker】GML v3 的现行提案
最近群友在问,就顺手写篇文章总结吧。主要还是Github,论坛和Discord上说的那些。
提案列表
- 1. 重用数组元素
- 2.颜色字面量
- 3. 二进制字面量
- 4. 不可能增加更多3D内容
- 5. 模板字符串
- 6. 多返回值
- 7. 可选链操作符
- 8. 移除旧的关键词
- 9. 异步事件数组
- 10. 多线程支持
- 11. const 关键字
- 12. super 关键词
1. 重用数组元素
添加一种方法,以获取现有数组中指定范围内元素组成的数组或数组兼容值,而无需为其元素分配新内存。
有时,能够仅对数组的特定元素范围运行代码非常有用。虽然我们可以通过 []
访问符自由索引数组的任何元素,并轻松实现此类行为,但许多函数(如 array_sort()
)不接受范围参数,始终对整个数组进行操作。我们可以使用 array_copy()
获取数组任意范围的副本,但副本会分配额外的内存(在游戏开发中可能成本较高),且原始数组不会反映新数组的任何修改。此外,要引用数组中的特定元素范围,还需要手动管理 start
和 end
索引,或 start
索引和 length
值。
新增函数 array_slice(_array, _slice_at)
和 array_weld(_array1, _array2, ...)
。
函数 array_slice(_array, _slice_at)
将返回一个新数组,其长度为 array_length(_array) - _slice_at
,并指向 _array
从 _slice_at
索引开始的元素,同时将 _array
的长度缩减为 _slice_at
。该函数不会为返回的数组分配新内存。
var _a = [10, 20, 30, 40, 50, 60, 70];
var _b = array_slice(_a, 3);
show_debug_message(_a); // [10, 20, 30]
show_debug_message(_b); // [40, 50, 60, 70]
由于 GML 数组在需要扩展时不会使现有引用失效,这表明实现该函数所需的间接层级已经存在。
伪 C++ 代码示例:
struct internalArrayRepresentation { int length; T* elements; };
任何表示此类 internalArrayRepresentation*
类型的值(即数组)在仅修改 length
、elements
或 *(elements+i)
时仍将保持有效,与当前行为一致。
函数 array_weld(_array1, _array2, ...)
会将 _array1
的长度恢复为 array_length(_array1) + array_length(_array2)
,并将 _array2
的长度设为 0
。仅当 _array1
和 _array2
的元素在内存中仍然连续且无需为数组元素分配内存时,该函数才会返回 true
;否则返回 false
,但仍会执行相同的语义操作(即使需要内存分配)。该函数可以接收任意数量(≥2)的数组,将所有数组的元素合并到 _array1
中。
2.颜色字面量
添加使用 #RRGGBB 十六进制格式的颜色字面量。
目前定义颜色的唯一方法是使用 make_colour_rgb
/hsv
或十六进制字面量 0xFF0000
。然而,make_colour_...
函数在定义简单颜色时显得过于繁琐,而十六进制字面量由于 GML 以 ABGR 格式解析颜色,会导致红色和蓝色通道互换。此外,标准的十六进制字面量无法在 Feather 中提供良好的颜色预览。
因此,应引入一种类似于 CSS 颜色代码的新十六进制字面量格式,支持从 RGB 到 BGR 的转换。
#RRGGBB
格式的颜色可以:
- 直接解析为数字(例如
#AF0A09 = 0xAF | 0x0A << 8 | 0x09 << 16
),或 - 在预处理阶段重新解释为等效的
0xBBGGRR
字面量。
该语法符合常见规范(CSS 采用),且广泛适用(多款图像编辑软件均使用类似颜色代码)。
理论上,可以通过自定义 colour
函数在运行时从字符串解析颜色字面量(如 colour("#Fb0Aa9")
),但这会显著增加运行时性能开销,并增加使用门槛,导致用户仍倾向于使用十六进制字面量 0xa90AFb
。此外,此方案也无法实现 Feather 的颜色预览功能。
此外,我们应当:
- 支持
#RGB
、#RGBA
和#RRGGBBAA
格式。 - 在调试器中查看颜色的功能。
3. 二进制字面量
支持使用 0b 前缀表示二进制字面量,类似于 0x 前缀表示十六进制字面量。
在 GML 中,位掩码常用于实现自动拼接(auto-tiling)或缓冲区的数据读写。由于 GML 缺乏直接表示这些掩码的方式,开发者不得不使用一些不够直观的替代方案,例如:(1 << 4)
、0x10
或直接使用数字 16。
实现方式与十六进制字面量类似,但使用二进制数字和 0b
前缀。同时,允许前缀大小写不敏感,即 0b
和 0B
均可使用。
二进制字面量在大多数现代语言中都很常见,例如 C++、Rust 和 JavaScript。本提案的语法规则与这些语言一致。
理论上,可以通过自定义 bin2dec
函数在运行时从字符串解析二进制字面量(如 bin2dec("0b0110")
),但这会增加运行时性能开销,并提高使用门槛,导致开发者仍倾向于使用前文提到的"不够直观的替代方案"。
4. 不可能增加更多3D内容
讲个笑话,3D请求一开始是他们内部提出来的,然后刚一提出就被干掉了,其实想想也是,他们现在连2D的都忙活不过来呢。
5. 模板字符串
实现模板字符串:$“今天是{weekday},气温是{temp}度!”
var s = "今天是 " + string(weekday) + ",气温是 " + string(temp) + " 度!";
引入字符串插值功能可避免这种重复性代码:
var s = $"今天是{weekday},气温是{temp}度!";
模板字符串与普通字符串("
)和原始字符串(@"
或 @'
)不同,使用 $
前缀:
var player = "杰德";
var planet = "LOFAF";var result = $"你好,{player}!欢迎来到{planet}!";
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// 语法糖,等价于:
// string("你好,{0}!欢迎来到{1}!", player, planet)
格式参数包裹在 {
和 }
之间。
由于模板字面量语法会被转换为函数调用,求值顺序未明确指定(尽管在虚拟机中可能是从左到右)。
此语法可能与结构体访问语法冲突(例如 x = save_data[$"phase"]
)
格式化参数支持(如日期/数字格式化)。
6. 多返回值
支持函数返回多个值
目前GML(v2)中要实现函数返回多个值需要这样写:
function test() {return [1, 2, 3];
}testValues = test();
show_debug_message(testValues[0]);
这种方式既不实用也不优雅。许多主流编程语言如Go、Rust和Python都原生支持多返回值功能。
建议语法:
function test() {return 1, 2, 3; // 用逗号分隔返回值
}val1, val2, val3 = test(); // 依次接收返回值
show_debug_message(val1);
7. 可选链操作符
支持可选链操作符
?.
,当左侧表达式为undefined
或右侧标识符在左侧不存在时,解析为undefined
。
某些场景需要处理高度嵌套的结构,其中包含许多可选或可能缺失的变量。例如,配置系统可能涉及许多可选变量,而加载系统或其他处理外部文件的系统可能涉及许多可能缺失的变量。
以下是一个同时使用可选链和空值合并操作符的示例,用于从配置中获取变量或在变量未定义时设置默认值:
some_variable = configuration?.some_category?.some_variable ?? 123;
在此示例中,假设 configuration
的值为 undefined
、{ }
、{ some_category: undefined }
或 { some_category: { some_variable: 456 } }
,这些情况均不会导致程序崩溃。最后一种情况会解析为 456
,而其他情况会使可选链访问解析为 undefined
,并将 some_variable
设置为默认值 123
。
相比之下,当前 GML 中实现类似功能需要如下代码:
if (is_undefined(configuration) || is_undefined(configuration[$ "some_category"]) || is_undefined(configuration.some_category)) some_variable = configuration.some_category[$ "some_variable"] ?? 123;
else some_variable = 123;
首先,定义“访问链”为一个表达式,它以常量/字面量/变量/括号表达式开头,并仅链式调用变量访问(如 x.y
)、索引访问(如 x[]
、x[? ]
、x[| ]
、x[# ,]
、x[$ ]
)或函数调用(如 x()
)。
在判断访问链时,可选链操作符被视为一种变量访问,可选索引访问被视为一种索引访问,可选方法调用被视为一种函数调用。
“完整访问链”是指未被后续变量访问、索引访问或调用直接跟随的访问链表达式。例如,在表达式 get_item_data(inventory[i]?.identifier)?.attack ?? 0
中:
inventory[i]
是一个访问链,但不是完整访问链(因为它后面跟着?.identifier
)。inventory[i]?.identifier
是一个完整访问链(它后面直接跟的是右括号,而非另一个访问)。get_item_data(...)
是一个访问链,但不是完整的。get_item_data(...)?.attack
是一个完整访问链。get_item_data(...)?.attack ?? 0
不是一个访问链,因为它包含了空值合并操作符,而该操作符不能直接作为访问链的一部分。
每个可选链/索引访问/调用操作符必须是某个完整访问链的一部分。
假设:
<expression>
是可选链操作符前的访问链;<identifier>
是操作符后紧跟的变量标识符;<rest>
是该操作符所属的完整访问链的剩余部分。
形如 <expression>?.<identifier><rest>
的可选链将按以下方式求值:
- 如果
<expression>
求值为空值(pointer_null
或undefined
),则包含该操作符的完整访问链解析为undefined
,且<rest>
的任何部分都不会被求值(整个表达式会短路)。 - 如果
<expression>
求值为非空值,但<identifier>
在其结果中不存在,则整个表达式同样会短路并返回undefined
。 - 如果
<expression>
为非空且<identifier>
存在于其结果中,则<expression>?.<identifier>
会求值为<expression>.<identifier>
,并继续处理<rest>
部分的访问链。
可选链操作符的优先级与常规变量访问、索引访问和调用相同(C# 规范称这一层为“主要”)。由于它具有短路行为,因此必须从左到右求值(不能从右侧开始求值,因为甚至无法确定右侧是否会被求值)。
如配置示例所示,可选链操作符与空值合并操作符可以很好地配合使用。
在大多数情况下,可选链操作符不会对其他语言特性产生负面影响。唯一的例外是它与内联条件操作符(即 <condition> ? <then_value> : <else_value>
)存在轻微歧义,这使得两者的解析稍微复杂一些。
具体来说,?.
操作符可能与内联条件操作符 ?
后跟以点开头的 <then_value>
操作数混淆。据我所知,唯一可能以点开头的操作数是小数,例如 .5
(等同于 0.5
)。此外,数字字面量不能包含空格。
另一方面,可选链操作符后只能跟有效的标识符(以字母或下划线开头)或空格。因此,可以通过查看点后的字符来确定表达式的含义——如果直接跟数字,则必须是内联条件表达式中的数字操作数;否则,它是可选链操作符。
为了保持代码清晰,可选链操作符本身不能包含空格(例如 ? .
)。
在常规变量访问方面,我倾向于 GameMaker 的严格性,而非 JavaScript 的宽容性。但对于可选链操作符,我倾向于在左侧可能为空值或右侧可能未设置时均采取宽容处理。
另一种处理右侧的方法是设计可选索引访问,并结合结构访问符,例如:
some_variable = configuration?.[$ "some_category"]?.[$ "some_variable"] ?? 123;
然而,这种方式会引入额外的符号和难以智能感知(或几乎无法智能感知)的字符串,使代码更难以阅读和编写。
C# 还支持空条件数组访问 ?[]
,而 JavaScript 支持可选数组访问 ?.[]
和可选调用 ?.()
。我认为这些功能值得考虑,特别是:
- 用于处理右侧可能越界的数组索引;
- 用于轻松调用可能未定义的回调(例如
on_init?.()
)。
如果实现这些功能,我更倾向于采用类似于 JavaScript 的 ?.[
和 ?.(
操作符,而非 C# 的 ?[
和 ?(
操作符。因为 ?[
可能与内联条件后跟数组字面量混淆,而 ?(
可能与内联条件后跟括号表达式混淆。相比之下,?.[
和 ?.(
在 GML 中似乎不会与其他任何语法产生歧义。
8. 移除旧的关键词
字面意思,移除 score,lives和health关键词。这玩意纯纯积木时代的残留,在如今完全是阻碍了。
9. 异步事件数组
新增通过集中式
async_events[]
数组处理异步事件的支持,使用数字标识符(类似于闹钟或用户事件:0-11 或 0-15)。
理想情况下,只读变量 async_events
可同时用于实例和结构体。
当前的异步系统分散在多个命名事件中,并绑定到每种事件类型的独立 async_load
块。这使得管理多个异步调用变得混乱且重复——尤其是在扩展或通用系统中。
采用更简洁的索引系统(如 alarms[]
)将使异步事件处理更加一致和易于管理。
开发者可通过数字访问事件,或根据需要定义自定义异步 ID 常量(最大合理值,如 11 或 15):
enum MY_ASYNC { IMAGE_LOADED, DATA_READY, LOGIN_COMPLETE
}
当异步事件触发时,其数据会写入 async_events[<id>]
并持续一帧:
if (async_events[MY_ASYNC.DATA_READY] != undefined) { var data = async_events[MY_ASYNC.DATA_READY];
}
async_events[]
在每帧结束时自动清除。- 内置辅助函数:
get_async_event_time(id); // 获取事件触发时间
get_async_event_count(); // 获取本帧触发的事件数量
异步事件常量可沿用现有数值,但不再使用特定名称,而是直接使用数字。
原方式:
EVENT_OTHER_WEB_IMAGE_LOAD \\ (60)
EVENT_OTHER_WEB_SOUND_LOAD \\ (61)
新方式:
EVENT_ASYNC_0
EVENT_ASYNC_1
(以此类推,供外部访问使用。)
底层实现上,异步结果会以如下格式存入 async_events[]
:
async_events[id] = { time: current_time, data: <any>
};
开发者访问方式:
var event = async_events[MY_ASYNC.IMAGE_LOADED];
if (event != undefined) { var payload = event.data;
}
Game Maker 的许多内置函数使用基于名称匹配的异步事件(例如音频队列函数 -> Async - Audio Playback)。
一种可能的解决方案是利用扩展原有的内部常量(如前所述),例如 EVENT_OTHER_WEB_IMAGE_LOAD
,以访问事件数组中的较高索引值,同时保留 0-11 或 0-15 供用户自定义,更高数值仅限 Game Maker 内部使用。
10. 多线程支持
字面意思,添加 async / await 关键词。但是这个提案大概率会被毙掉,理由和上面的3D提案一样。
11. const 关键字
在GML中引入
const
关键字(或等效机制)以定义编译时计算并传播的不可变引用。
与var
、static
或#macro
不同,const
既不会产生可重新赋值的变量,也不会在每次使用时生成新实例——它将作为针对任何对象、数组、结构体或原始值的固定持久化内存指针。
该结构通过将标识符在代码生成阶段视为内联指针或字面量,确保运行时零赋值开销和最高访问速度,同时相较于魔数和一次性静态变量,其可读性显著增强。
12. super 关键词
在构造脚本中使用 super 关键字访问父类属性。