译:Gamemaker Studio 2.3 语法详解
译: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)
GMS2.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 <name>() {<code>} 语法来区分:
/// @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(<arguments>) 语法从另一个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)++ <expr> (条件表达式的后增量) 还是 if (condition) ++<expr> (在 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日
晴
SNOWFALL DEVLOG_03
经历了大概10天的严重睡眠不足,白天大部分时间不是补觉就是打瞌睡。接着生病,不过好在一天就恢复了。
基本上这些天都在做3C,发小给的建议是花大量时间在3C上再往后推,要不然可能会有很多返工,可能还要持续一段时间。
之前的准星和移动处理,基本上和nuclear throne差不多,开始测试的时候我用的手柄(像helldivers那样,没有开启准星),忽略了键鼠操作准星可能导致的问题。在键鼠下,当鼠标(准星)固定在某一个点时,角色会在移动的时候根据瞄准角度(鼠标位置)去自动转身,会有点奇怪,就像车在漂移,这个体验很难受。就改成了helldivers那种逻辑,在非瞄准状态下,射击方向和朝向匹配,这样在键盘操作上,如果不瞄准,就是8方向射击,更复古,也可以在不减少移动速度的情况下向一个固定方向射击;如果在比较安全的距离,利用瞄准来精确射击。
精确射击本来打算给敌人设计弱点区,比如这样
但是top-down 3/4 这个视角很尴尬,比如横版卷轴或者3D TPS、FPS来说,敌人和玩家是站在地上,但在2d top-down 这个视角下,其实敌人和玩家实际上是躺在地上 =_=
后来想如果做多个hitbox,在子弹上再加raycast处理来检测hitbox,后来想到如果关联到动画上可能工作量挺大,有点嫌麻烦,暂时先用瞄准线和敌人的中心点之间的角度来处理精确瞄准。后面还是需要试一下raycast,哪怕只给boss或者比较强的敌人做这个功能,这个如果实现了,战斗会更有意思一些,对于枪法好的人来说。
另外学着用动画曲线做了一个完美换弹的试验,不过可能并不用像战争机器那样麻烦,可能Returnal那种处理更简化一些(虽然是建立在无限子弹的基础上),还没有考虑好,暂时没有扔进工程。
………………………………………………………………………………………………………………………………………………………………………………………………………………………………
在写代码和想功能之外,做美术资源的时候开始比较头疼,由于想做非像素,就没办法使用aseprite,用photoshop在做动画上折磨了大概一天,实在是受不了了,找了一些做动画的插件,看视频感觉也不是很方便,就用去年疫情时候买的优动漫的正版开始做角色粗略动画,但是做到一半……发现动画的帧数还有限制,wtf,只好去买了个破解版的Clip Studio Paint EX(功能全部开放),正版我实在是承受不起,在导出的时候CSP只能导出单帧图像,并不像aseprite那么方便,还要用Free texture packer处理成条状图再回photoshop里加颜色,不过整个流程都比只在PS里做动画心情舒畅多了。(顺便说一下,这个软件千万不要像我这样装繁体中文版,很多地方都看不懂,靠猜……还不如在安装的时候直接选择英文了)
………………………………………………………………………………………………………………………………………………………………………………………………………………………………
大前天在sprite editor里操作的时候,GMS2直接闪退(没有报错弹窗),重启后并没有任何提示信息,我也没当回事就忘了这茬儿。
前天晚上孩子睡觉以后,突然想去改一个功能,然后就出现了资源树里object索引出错的问题,无法新建任何object,只要新建,各种代码报错(注释掉相关的,其他报错,无限),只要删除新建的object,恢复,google搜了很多也没找到相关的情况。
在请教了@流贾君 之后,只能导出所有文件做yymp重开项目,成功救回来了工程。
昨晚在@流贾君的过程中还发现音频组合材质组不能改名了(搜索了一下,在reddit和gms社区也有人发帖说这个问题)………上个月新建工程的时候还可以,我换了上一次的runtime发现也不能改名,暂时只能等更新了,不敢用beta版。
………………………………………………………………………………………………………………………………………………………………………………………………………………………………
有用的连接:
GMS2 BLOG上的文章,不单单可以扩展世界,利用这个方法可以像魂系列那样来进行敌人的刷新处理
EXPANDING WORLDS: BUILDING GAMES WITH INTERCONNECTED LEVELS
由于更新2.3之后都没咋用过,看了这个教程感觉要补习很多东西,新加的constructor和static,要花时间仔细学学用法。
(上面俩视频连接都来自Youtube的SamSpadeGameDev,这个频道很不错)
………………………………………………………………………………………………………………………………………………………………………………………………………………………………
2021年11月22日周一
早上阴沉沉 这会儿大太阳
Highway
译:[GMS2] SEALS OF THE BYGONE中随机生成平台游戏关卡的方法
作者:Logan Foster
译:highway★
游戏体验的多样性是Roguelite游戏的支柱之一。算法生成的关卡,从平台游戏的角度来说,存在很多挑战,对于刚入手GMS的新手来说也比较复杂。比较折中的方式是将预制的关卡区块链接组合在一起,下面是我在Seals of the Bygone中制作随机关卡的方法,用到了GMS2的图层系统。
摘要
从本质上说,我们要为关卡创建一些不同的区块(section)。每个区块包含子区块(subsections),其中包含我们的资产(asset)。通过使用代码来动态切换子区块的开关(on/off),我们就可以随机关卡了。(译注:当然这不如dead cells那种算法加预制的随机方法效果好,不过对于刚入坑这里的朋友来说应该也是种不错的选择,而且在区块中来控制关卡设计,也相对纯算法容易一些,比如做risk of rain类型的游戏,应该是很够用了)
我们没有在代码或外部资源中存储关卡区块信息,而是在Room Editor中创建它们,并存储在图层中,就跟做其他关卡一样。这也是我创建这个系统的主要原因,我想保留现有的设计流程,该流程基于Room Editor。
配置
第1步:创建父区块
首先,我们要为每个主要结构创建空白资产层。区块的数量并没有限制,在下面的示例关卡里,我们创建了3个区块,名为"Section1","Section2","Section3"。这些资产层基本上就相当于结构文件夹,也可以在代码中引用。
第2步:创建子区块
现在,创建子区块的其他空白资产层。这些图层将保存我们构成关卡的tile和object。这里我们将其命名为"Sub1_1","Sub1_2"等。
第3步:子区块的图层
最后,我们将构成关卡的tile、asset和object层也加入到这些子区块中。上图中,子区块"Sub2_1"是打开的,并存在"Collision2_1"和"Tiles2_1"这类图层。这些就是我们构建关卡要用到的层。
现在我们完成了所有设置,你可以切换父组的可见性,以便查看子区块的不同组合。我建议你在完成设计子区块后使用锁定功能,以确保不会意外的编辑它。
代码
我们已经搞定了关卡区块,并存储在不同的层中,现在可以来试试在关卡开始的时候随机选择哪些层了。在Room Start事件中敲下面的代码。
首先,我们定义要用于区块和子区块的前缀。我们将使用它们的名字来找到我们要用的层,请确保这些命名与你在Room Editor中的层相同。然后我们循环查看有多少不同的区块。
现在我们知道有多少区块了,遍历每个区块并查看有多少个子区块。然后选择一个随机的子区块来使用。
在决定使用哪个子区块之后,我们循环并销毁其他所有子区块(在该区块内)。最后我们通过设置层的可见性进行清理,以确保一切正确。如图,这里的图层名称是"硬编码"的,所以你需要将它们更改为你的区块包含的内容。我们可以动态的执行此操作,但这里没什么必要。
结论
完事儿了,很简单的解决方案,可以为你的关卡增添一些随机性。如果你想做一些更强大的东西,可以试试用layer_x和layer_y移动一些东西来扩展这个系统。
如果你有什么问题,可以随时联系我@rologfos
谢谢阅读!
2/23/2019
H
【译文】GAMEMAKER STUDIO系列:简单状态机
作者:Nathan Ranney
翻译:highway★
按照设计,状态机一次只能处于一种状态。 由我们来定义对我们的情况有意义的状态,以及它们之间的关系。 在本文中,我们将使用状态机来控制在任何给定时间可用的玩家操作,允许我们设置角色并定义角色可以执行的操作。
大家好, 今天我想告诉你如何设置一个简单的状态机。 状态机是一种数据结构,顾名思义,它跟踪不同的状态。 例如,我们的游戏可能有三种状态:“游戏运行”,“游戏暂停”和“游戏结束”。我们可能会使用状态机来记住哪一个处于活动状态,并定义如何从一个状态转换到另一个状态(请参阅 上面的图片)。
基础设置
此条目需要比上一个篇文章(绘制精灵)多一些动画,因此在开始之前,你需要将这些动画添加到工程中。 从此链接下载精灵并将其添加到你的项目中。 我已经恰当地命名了文件,因此只要确保精灵的名称与文件名相匹配,就可以将其添加到GMS中。 继续添加所有精灵 - 甚至是敌人的精灵 - 因为我们将在以后的文章中需要这些精灵。 确保每个精灵的原点是(16,32)。
枚举,控制器和持久性
为了设置我们的状态机,我们首先确定哪些状态是可能的,以及我们如何在代码中识别它们。 由于这个例子都是关于角色动作的,所以让我们定义那些动作是什么,并给每个动作一个整数id。 最简单的方法是使用enum(枚举,Enumeration的缩写),它是在自定义变量类型下保存的常量的集合。 如果你熟悉Gamemaker中的Macros(宏),那么枚举就是这样的。 我更喜欢使用枚举,因为它们比宏更容易管理和跟踪。
创建一个脚本并将其命名为enum_init。 添加以下行。 //states enum states { normal, crouch, attack, hit }
请注意,我们不必设置每个条目的值,枚举自动将值0指定为“正常”,然后在每个条目后递增。 我们可以随时获得值
var example = states.attack; show_message(string(example)); //output: 3
实际上,你可以通过为每个条目指定值来覆盖枚举的自动编号,但重要的是要重申枚举是常量。 它们定义后无法更改!
枚举也是全局的,这意味着任何对象都可以访问它们。 这对我们的状态机来说非常完美。
现在我们有了枚举,我们在哪里实例化它? 从非持久对象调用这些变量,比如我们的oPlayer对象,不是最好的主意。 我们想要做的是创建一个持久的控制器对象(它始终存在),以管理许多对象可以访问的枚举和其他数据类型之类的东西。 继续创建一个新对象并将其命名为“con”(译注:建议还是oController这样与其他对象保持相同的命名前缀,在看代码时会比较易读。)。 我喜欢保持我的控制器名称简短,因为它更容易返回。选中新对象上的“持久(Persistent)”框。 最后,将Create事件添加到对象,并添加以下面的代码:
///init 初始化 enum_init();
将con对象放在你的房间里。 由于此对象是持久的,因此除非您明确销毁,否则它将继续存在! 无需在每个房间放置此物体。
Switch cases
既然我们已经在枚举中定义了状态,我们就可以从我们的玩家对象中访问它们了。 打开我们在上一个条目中创建的oPlayer对象,并将以下行添加到create事件中。
attack = false; //states currentState = 0; lastState = 0; //movement xSpeed = 0; ySpeed = 0; lastSprite = sprite;
将End Step事件添加到oPlayer,然后添加一些代码。
xPos = x; yPos = y; x += xSpeed; y += ySpeed; //animation frame_reset();
现在让我们跳到step事件。 我们可以删除我们在之前那篇文章中添加的几乎所有代码,因为大多数代码只是为了展示draw_sprite_ext的不同部分。 查看下面的代码,并确保你的step事件看起来完全相同。
//buttons player_buttons(); //animation frame_counter(); //state switch switch currentState { case states.normal: normal_state(); break; case states.crouch: crouch_state(); break; case states.attack: attack_state(); break; }
如果你之前从未见过switch语句,你可能会想知道到底发生了什么。 我稍后会解释,但首先我们需要创建三个新脚本:normal_state,crouch_state和attack_state。 我喜欢使用不同状态的脚本,因为它使代码更容易阅读。 你可以弹出所需的任何脚本(译注:在GMS2中,在代码中的脚本上按下鼠标中键即可弹出对应的脚本,并链接在当前对象窗口),并在该特定状态下工作。
好吧,所有这一切究竟意味着什么呢? 什么是switch语句以及它是如何工作的? 将switch语句视为if语句的更具体版本。if语句用于布尔值检查,条件满足则执行,switch语句用于根据变量的值执行代码。 看看下面的代码块。
//if statement if(currentState == states.normal) { normal_state(); }else if(currentState == states.crouch) { crouch_state(); } //switch statement switch currentState { case states.normal: normal_state(); break; case states.crouch: crouch_state(); break; }
这两段代码在功能上都是相同的。 它们都将根据currentState变量的当前值运行我们想要的脚本,但switch语句要清晰得多。 当我们添加状态时,使用if语句变得难以管理。 switch语句更容易管理。
最后,我们需要在player_buttons脚本中添加一个新的按钮变量。 打开该脚本并添加此行:
attack = keyboard_check_pressed(ord("Z"));
状态机
我们已经定义了一个可能状态的枚举,以及变量currentState来跟踪哪个状态是活动的。 现在我们知道了switch语句的工作原理,我们可以创建在每个状态下执行的代码,以及在它们之间进行转换的规则。 switch语句可以很容易地显示我们的状态机是什么以及它正在做什么。 如果我们的currentState变量等于语句中的一个case,则执行与该case相关的代码。 由于我们为每个状态创建了脚本,因此请继续打开normal_state脚本并添加以下代码
//移动 if(left) { xSpeed = -2; }else if(right) { xSpeed = 2; }else { xSpeed = 0; } //切换到下蹲状态 if(down) { currentState = states.crouch; } //切换到攻击状态 if(attack) { currentState = states.attack; }
这段代码非常简单。 对我来说,正常状态意味着角色的默认状态。 他们没有执行任何特殊操作,例如攻击或使用道具,玩家可以完全控制角色。 在这里,我们有左右移动,并转换到蹲和攻击状态。 如果你现在运行游戏,你将无法看到我们的状态机的全部效果。 如果你按下或Z,你将改变状态,不再能够移动。 接下来让我们定义蹲状态。 打开crouch_state脚本并添加以下代码:
xSpeed = 0; if(!down) { currentState = states.normal; }
蹲下时(按住向下箭头键)我们停止玩家的水平移动(xSpeed = 0)。 如果他们释放向下键,我们将返回正常状态。 这将是一个在蹲下时添加不同动作的好地方,比如爬行或者可能是蹲下的攻击。
打开我们创建的最后一个状态脚本,attack_state,并添加以下代码:
xSpeed = 0; if(frame > sprite_get_number(sprite) - 1) { currentState = states.normal; }
我们再次将水平速度归零,并且当动画结束时我们将玩家状态设置回正常。 但是......我们还没有设置我们的动画,是吗? 动画控制是状态机和switch的另一个重要用途! 创建一个新脚本并将其命名为animation_control。 添加以下代码:
xScale = approach(xScale,1,0.03); yScale = approach(yScale,1,0.03); //动画控制 switch currentState { case states.normal: if(left) { facing = -1; }else if(right) { facing = 1; } if(left || right) { sprite = sprPlayer_Run; }else { sprite = sprPlayer_Idle; } break; case states.crouch: sprite = sprPlayer_Crouch; break; case states.attack: sprite = sprPlayer_Attack; break; } //如果精灵更改,则将帧重置为0 if(lastSprite != sprite) { lastSprite = sprite; frame = 0; }
通过使用另一个switch语句,我们可以轻松控制播放器动画。 请注意,我们可以在switch case中使用if语句! 我们没有将动画控制与我们创建的初始switch语句组合在一起的原因有几个。 首先,我们希望我们的动画在所有代码的最后发生。 动画是之前发生的一切的结果! 其次,它让代码更好读。 上面代码底部的最后一个表达式会在精灵更改时将帧重置为0。 这可以防止在更改精灵时动画在错误的帧上启动。
请注意,我们将xScale和yScale代码移动到animation_control的顶部。 这对以后很重要。
在oPlayer对象中打开end step事件,并将以下行添加到代码的底部。 这将确保它在其他一切之后发生。
animation_control();
来吧,运行游戏。 你应该有一个能够左右奔跑,空闲,蹲和攻击的角色了! 我们能够根据当前状态区分角色的行为。 除了管理优势之外,设置状态机还可以更轻松地跟踪错误,添加新行为以及跟踪对象的整体结构。 我经常使用状态机和switch语句来控制比如要显示的菜单屏幕,当前的游戏模式以及定义给敌人的AI类型等内容。
谢谢阅读! 在Twitter上关注我,并在我的网站上关注更多与游戏开发相关的内容。
【译文】GAMEMAKER STUDIO系列:构建更好的动画系统
作者:Nathan Ranney
翻译:highway★
在开发Kerfuffle(译注:游戏挂了,过于追求视觉效果、没钱、再加上一些其他问题,他们现在在做Knight Club)时,我需要一个动画系统,允许我在游戏中保持任何单独的动画帧(译注:格斗游戏/动作游戏为了提升打击感,会采用帧冻结的技术),而无需手动添加或删除精灵帧。 我还需要能够根据当前动画帧触发某些动作。 使用此设置,我可以创建hitbox,播放声音或更改状态,同时完全控制屏幕上绘制的所有内容。
变量
这些是与动画系统相关的重要变量。 如果后面你感到困惑,请回头再仔细看看。
frameSpeed
The speed in FPS that the game counts through the frames to be displayed. Default is 1.游戏通过要显示的帧计算的FPS速度。 默认值为1。
frameCounter
Increases every frame by the frameSpeed.
每帧按frameSpeed递增
currentFrame
The frame of the sprite currently being drawn on the screen.
当前正在屏幕上绘制的精灵帧
frameData
Current list of frame data the game is reading from, based on the animation that needs to play. Idle, run, attack, etc.
游戏正在读取的当前帧数据列表,基于需要播放的动画。 如空闲,奔跑,攻击等
frameDuration
Total number of in game frames to display the current sprite frame.
显示当前精灵帧的游戏帧总数。
maxFrames
The total number of frames in any given sprite.
任何给定精灵中的帧总数。
animSprite
The actual name of the sprite resource in GameMaker. sprMomo_Idle, for example.
GameMaker中精灵资源的实际名称。 例如,sprMomo_Idle。
脚本
后面我们要用到的脚本。
frame_reset();
//将frameCounter和currentFrame重置为0 frameCounter = 0; currentFrame = 0;
animation_set();
该脚本接受两个参数。 首先,frameData(相关的帧数据列表)和第二个是animSprite(你想要绘制的精灵资源)
//animation_set ( argument0, argument1 ); frameData = argument0; animSprite = argument1;
帧数据
每个动画都需要一个帧数据列表。 这是一个列表,其中包含每帧动画播放的游戏帧数量。 每个数据列表都使用以下命名约定。 frameDataIdle,frameDataRun,frameDataDash等。
(译注:如果你初次接触这些并对这些内容感兴趣,可以扩展阅读一下,google搜一下街霸系列的frame data,会有很多更详细的资料。btw indienova如何插入表格呢?)
Momo(我们游戏中的一个角色)空闲动画的帧数据
请注意,所有列表和值都以0开头。因此,即使此动画有12帧,列表中的最大数字也是11。这包括你要显示的帧! 如果你希望它在游戏中显示5帧,则列表中的值应为4。
** GMS特定说明**
确保在不再使用时手动删除列表! 否则你可能会遇到内存泄漏!
帧计数器
现在我们有一个帧数据列表,我们需要实际根据该数据设置动画。 我们需要做的第一件事是弄清楚maxFrame是什么。
maxFrames = sprite_get_number( animSprite ) - 1;
然后,如果您的currentFrame恰好大于或等于最大帧,并当frameCounter大于或等于精灵帧应出现在屏幕上的最大帧数,则重置为第一帧。
if ( currentFrame >= maxFrames - 1 && frameCounter == frameDuration ) { frame_reset(); }
现在frameCounter可以完成它的工作。 它计算应该显示精灵的当前帧的帧数,然后一旦达到该最大值,将当前帧切到精灵的下一帧,并重置为0以再次开始计数。
frameCounter += frameSpeed; frameDuration = ds_list_find_value ( frameData, currentFrame ); if ( frameCounter == frameDuration ) { currentFrame ++; frameCounter = 0; }
注意:
使用maxFrames也是结束动画和更改为新动画或状态的好方法! 在整个攻击动画一直播放之后,我使用maxFrames从攻击状态切换回正常状态。
** GMS特定说明**
sprite_get_number是一个内置的GMS函数,它返回精灵中的帧数。 此函数返回确切的帧数,并且不会从0开始计数! 所以如果你有一个5帧的精灵,这将返回5! 这就是为什么在检查maxFrames时,我们这样做,同时从其值中减去1。
切换精灵
Kerfuffle中的所有内容都运行在一个相当简单的状态机上。 根据角色所处的状态,动画会发生变化。
//存储当前的动画精灵,以便我们稍后检查 currentAnim = animSprite; switch ( state ) { case normal: //如果玩家向左或向右,则改为跑步精灵 if ( left || right ) { animation_set ( frameDataRun, runSprite ); //如果玩家没有按任何按钮,则更改为空闲精灵 } else { animation_set ( frameDataIdle, idleSprite ); } break; case dash: //如果玩家状态为前冲,改为前冲精灵 animation_set ( frameDataDash, dashSprite ); break; } //针对上一个动画检查当前动画。 //如果这些动画不相同,请将frameCounter和currentFrame重置为0。 if(lastAnim != currentAnim) { frame_reset(); lastAnim = currentAnim; }