GML脚本语言的高级运用
在上一节中,我们介绍了基本的GML脚本语言及其在GameMaker Studio中的应用。本节将深入探讨GML脚本语言的高级运用,包括函数、变量、控制结构、面向对象编程和性能优化等方面。这些内容将帮助你更好地利用GML编写复杂且高效的代码,从而提升你的游戏开发能力。
1. 高级函数的使用
1.1 函数的定义与调用
函数是编程中非常重要的概念,它们可以将代码模块化,提高代码的可读性和可维护性。在GML中,函数的定义和调用与大多数编程语言相似,但有一些特定的语法和功能。
1.1.1 函数的定义
在GML中,你可以使用function
关键字来定义函数。函数可以有参数,也可以有返回值。以下是一个简单的函数定义示例:
// 定义一个简单的函数,用于计算两个数的和
function add_numbers(a, b) {
// 检查参数类型
if (is_real(a) && is_real(b)) {
return a + b; // 返回两个数的和
} else {
show_error("参数必须是实数");
return 0; // 返回0表示错误
}
}
1.1.2 函数的调用
定义好的函数可以通过函数名和参数来调用。以下是如何调用上述add_numbers
函数的示例:
// 调用add_numbers函数
var result = add_numbers(3, 5);
show_debug_message("3 + 5 = " + string(result)); // 输出: 3 + 5 = 8
// 错误调用
result = add_numbers("3", 5);
show_debug_message("结果: " + string(result)); // 输出: 结果: 0
1.2 参数传递
GML中的函数参数可以是各种数据类型,包括实数、字符串、数组、结构体等。参数传递可以是按值传递,也可以是按引用传递。
1.2.1 按值传递
按值传递是指在函数调用时,传递的是参数的副本,因此函数内部对参数的修改不会影响到外部的变量。
// 定义一个函数,按值传递参数
function modify_value(x) {
x += 10;
}
// 调用函数
var num = 5;
modify_value(num);
show_debug_message("num = " + string(num)); // 输出: num = 5
1.2.2 按引用传递
按引用传递是指在函数调用时,传递的是参数的引用,因此函数内部对参数的修改会影响到外部的变量。在GML中,数组和结构体通常按引用传递。
// 定义一个函数,按引用传递参数
function modify_array(arr) {
arr[0] += 10;
}
// 调用函数
var numbers = [1, 2, 3];
modify_array(numbers);
show_debug_message("numbers = " + string(numbers)); // 输出: numbers = [11, 2, 3]
1.3 可变参数函数
GML支持定义可变参数的函数,这使得函数可以接受任意数量的参数。使用argument_count
和argument
关键字可以实现这一点。
// 定义一个可变参数函数,用于计算所有参数的和
function sum() {
var total = 0;
for (var i = 0; i < argument_count; i++) {
total += argument[i];
}
return total;
}
// 调用函数
var result = sum(1, 2, 3, 4, 5);
show_debug_message("总和 = " + string(result)); // 输出: 总和 = 15
1.4 递归函数
递归函数是指在函数内部调用自身的函数。递归在处理复杂问题时非常有用,例如树结构的遍历、排序算法等。
// 定义一个递归函数,用于计算阶乘
function factorial(n) {
if (n == 0) {
return 1;
} else {
return n * factorial(n - 1);
}
}
// 调用函数
var result = factorial(5);
show_debug_message("5! = " + string(result)); // 输出: 5! = 120
2. 高级变量的使用
2.1 变量作用域
在GML中,变量的作用域有全局变量和局部变量之分。全局变量在整个游戏范围内都可见,而局部变量只在定义它们的函数或事件中可见。
2.1.1 全局变量
全局变量可以在任何地方定义和访问。通常,全局变量用于存储游戏的全局状态,例如得分、生命值等。
// 在创建对象事件中定义全局变量
global.score = 0;
global.lives = 3;
// 在其他对象的事件中访问全局变量
show_debug_message("当前得分: " + string(global.score)); // 输出: 当前得分: 0
show_debug_message("当前生命值: " + string(global.lives)); // 输出: 当前生命值: 3
2.1.2 局部变量
局部变量只在定义它们的函数或事件中可见。局部变量可以避免变量命名冲突,提高代码的可读性和可维护性。
// 定义一个带有局部变量的函数
function calculate_damage(attack_power, defense) {
var damage = attack_power - defense;
if (damage < 0) {
damage = 0;
}
return damage;
}
// 调用函数
var damage = calculate_damage(50, 20);
show_debug_message("伤害值: " + string(damage)); // 输出: 伤害值: 30
2.2 变量类型
GML支持多种变量类型,包括实数、字符串、数组、结构体等。了解这些变量类型及其使用方法对于编写高效且正确的代码至关重要。
2.2.1 实数和字符串
实数和字符串是最基本的变量类型,它们的使用方式与其他编程语言相似。
// 定义实数和字符串变量
var health = 100.0;
var name = "Player1";
// 输出变量
show_debug_message("玩家名称: " + name); // 输出: 玩家名称: Player1
show_debug_message("玩家生命值: " + string(health)); // 输出: 玩家生命值: 100.0
2.2.2 数组
数组是用于存储多个值的变量。GML中的数组可以是动态数组,也可以是固定大小的数组。
// 定义一个动态数组
var items = ds_list_create();
ds_list_add(items, "剑");
ds_list_add(items, "盾");
ds_list_add(items, "弓");
// 输出数组内容
for (var i = 0; i < ds_list_size(items); i++) {
show_debug_message("物品 " + string(i) + ": " + ds_list_find_value(items, i));
}
// 销毁数组
ds_list_destroy(items);
// 定义一个固定大小的数组
var numbers = [1, 2, 3, 4, 5];
// 输出数组内容
for (var i = 0; i < array_length_1d(numbers); i++) {
show_debug_message("数字 " + string(i) + ": " + string(numbers[i]));
}
2.2.3 结构体
GML中的结构体可以用来存储相关的多个值。结构体可以通过点操作符来访问和修改其成员。
// 定义一个结构体
var player = {
name: "Player1",
health: 100.0,
score: 0
};
// 修改结构体成员
player.health -= 10;
// 输出结构体成员
show_debug_message("玩家名称: " + player.name); // 输出: 玩家名称: Player1
show_debug_message("玩家生命值: " + string(player.health)); // 输出: 玩家生命值: 90.0
show_debug_message("玩家得分: " + string(player.score)); // 输出: 玩家得分: 0
2.3 变量的默认值
在GML中,你可以为变量设置默认值,这样在变量未被赋值时,会使用默认值。
// 定义一个带有默认值的函数
function greet(name = "Guest") {
show_debug_message("欢迎, " + name);
}
// 调用函数
greet("Player1"); // 输出: 欢迎, Player1
greet(); // 输出: 欢迎, Guest
3. 高级控制结构
3.1 条件语句
条件语句用于根据不同的条件执行不同的代码块。GML中的条件语句包括if
、else if
和else
。
// 定义一个函数,用于判断玩家的生命值
function check_health(health) {
if (health > 80) {
show_debug_message("玩家生命值很高");
} else if (health > 50) {
show_debug_message("玩家生命值中等");
} else if (health > 0) {
show_debug_message("玩家生命值很低");
} else {
show_debug_message("玩家已死亡");
}
}
// 调用函数
check_health(90); // 输出: 玩家生命值很高
check_health(60); // 输出: 玩家生命值中等
check_health(30); // 输出: 玩家生命值很低
check_health(0); // 输出: 玩家已死亡
3.2 循环语句
循环语句用于重复执行某段代码,直到满足特定条件为止。GML中的循环语句包括for
、while
和do...while
。
3.2.1 for
循环
for
循环是最常用的循环语句,适用于已知循环次数的情况。
// 使用for循环遍历数组
var numbers = [1, 2, 3, 4, 5];
for (var i = 0; i < array_length_1d(numbers); i++) {
show_debug_message("数字 " + string(i) + ": " + string(numbers[i]));
}
3.2.2 while
循环
while
循环适用于在满足特定条件时重复执行代码。
// 使用while循环计算阶乘
var n = 5;
var result = 1;
while (n > 0) {
result *= n;
n -= 1;
}
show_debug_message("5! = " + string(result)); // 输出: 5! = 120
3.2.3 do...while
循环
do...while
循环至少会执行一次代码块,然后在每次循环结束时检查条件。
// 使用do...while循环计算阶乘
var n = 5;
var result = 1;
do {
result *= n;
n -= 1;
} while (n > 0);
show_debug_message("5! = " + string(result)); // 输出: 5! = 120
3.3 嵌套控制结构
嵌套控制结构是指在一个控制结构内部使用另一个控制结构。这种结构可以处理更复杂的问题。
// 使用嵌套控制结构遍历二维数组
var grid = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
for (var i = 0; i < array_length_1d(grid); i++) {
for (var j = 0; j < array_length_1d(grid[i]); j++) {
show_debug_message("grid[" + string(i) + "][" + string(j) + "] = " + string(grid[i][j]));
}
}
4. 面向对象编程
4.1 对象的创建与使用
在GameMaker Studio中,对象是游戏的基本构成单元。每个对象可以有自己的变量、函数和事件。通过合理地创建和使用对象,可以提高游戏的可扩展性和可维护性。
4.1.1 创建对象
在GameMaker Studio中,可以通过创建对象来定义游戏中的角色、敌人、道具等。以下是一个创建玩家对象的示例:
-
在对象资源中创建一个名为
obj_player
的对象。 -
在该对象中定义变量和函数。
// 在创建对象事件中定义变量
health = 100.0;
score = 0;
// 定义一个函数,用于计算受到的伤害
function calculate_damage(attack_power, defense) {
var damage = attack_power - defense;
if (damage < 0) {
damage = 0;
}
health -= damage;
return damage;
}
4.1.2 使用对象
在其他对象或脚本中,可以通过对象名来访问和调用对象的变量和函数。
// 在另一个对象中使用obj_player对象
var player = instance_create_layer(x, y, "Instances", obj_player);
player.health -= 10; // 直接修改对象的变量
var damage = player.calculate_damage(50, 20); // 调用对象的函数
show_debug_message("玩家受到的伤害: " + string(damage)); // 输出: 玩家受到的伤害: 30
4.2 继承与多态
GML支持对象继承和多态,这使得你可以创建具有共同特征的对象,并通过继承来扩展这些对象的功能。
4.2.1 对象继承
通过继承,子对象可以继承父对象的变量和函数,并可以重写或添加新的功能。
-
创建一个名为
obj_enemy
的父对象。 -
创建一个名为
obj_goblin
的子对象,继承自obj_enemy
。
// 在obj_enemy对象中定义变量和函数
health = 50.0;
score = 0;
function calculate_damage(attack_power, defense) {
var damage = attack_power - defense;
if (damage < 0) {
damage = 0;
}
health -= damage;
return damage;
}
// 在obj_goblin对象中重写calculate_damage函数
function calculate_damage(attack_power, defense) {
var damage = super.calculate_damage(attack_power, defense); // 调用父对象的函数
if (damage > 0) {
show_debug_message("哥布林受到了伤害: " + string(damage));
}
return damage;
}
4.2.2 多态
多态是指子对象可以替代父对象,调用相同的方法时会执行子对象中重写的方法。
// 在另一个对象中使用多态
var enemy = instance_create_layer(x, y, "Instances", obj_enemy);
var goblin = instance_create_layer(x, y, "Instances", obj_goblin);
var damage1 = enemy.calculate_damage(30, 10);
var damage2 = goblin.calculate_damage(30, 10);
show_debug_message("普通敌人受到的伤害: " + string(damage1)); // 输出: 普通敌人受到的伤害: 20
show_debug_message("哥布林受到的伤害: " + string(damage2)); // 输出: 哥布林受到了伤害: 20
5. 性能优化
5.1 减少冗余计算
在游戏开发中,减少冗余计算可以显著提升性能。例如,可以在需要时才计算某些值,而不是在每个帧中都计算。
// 定义一个变量,用于存储最后一次计算的时间
var last_calculation_time = 0;
// 在步进事件中判断是否需要重新计算
if (current_time - last_calculation_time > 1000) { // 每秒计算一次
last_calculation_time = current_time;
var result = complex_calculation();
show_debug_message("计算结果: " + string(result));
}
function complex_calculation() {
// 模拟一个复杂的计算过程
var result = 0;
for (var i = 0; i < 1000000; i++) {
result += i;
}
return result;
}
5.2 使用缓存
缓存是指将计算结果存储起来,以便在需要时直接使用,而不是重复计算。这可以显著提升性能。
// 定义一个缓存变量
var cached_result = -1;
// 在步进事件中判断是否需要重新计算
if (cached_result == -1) {
cached_result = complex_calculation();
show_debug_message("计算结果: " + string(cached_result));
}
function complex_calculation() {
// 模拟一个复杂的计算过程
var result = 0;
for (var i = 0; i < 1000000; i++) {
result += i;
}
return result;
}
5.3 优化数组操作
数组操作是游戏开发中常见的性能瓶颈。合理地使用数组可以提高游戏的性能。
5.3.1 使用动态数组
动态数组可以在运行时动态地增加或减少大小。
// 使用ds_list作为动态数组
var items = ds_list_create();
ds_list_add(items, "剑");
ds_list_add(items, "盾");
ds_list_add(items, "弓");
// 输出动态数组内容
for (var i = 0; i < ds_list_size(items); i++) {
show_debug_message("物品 " + string(i) + ": " + ds_list_find_value(items, i));
}
// 销毁动态数组
ds_list_destroy(items);
5.3.2#### 5.3.2 使用固定大小的数组
固定大小的数组在内存分配上更高效,因为它们在创建时就分配了固定大小的内存。如果你在游戏开发中知道数组的大小不会改变,使用固定大小的数组是一个更好的选择。
// 定义一个固定大小的数组
var numbers = [1, 2, 3, 4, 5];
// 输出固定大小的数组内容
for (var i = 0; i < array_length_1d(numbers); i++) {
show_debug_message("数字 " + string(i) + ": " + string(numbers[i]));
}
5.4 避免频繁的对象实例创建和销毁
频繁创建和销毁对象实例会导致性能下降。尽量在游戏开始时一次性创建所有需要的对象,并在游戏结束时统一销毁。
5.4.1 预创建对象
在游戏的初始化阶段,一次性创建所有需要的对象实例,并将它们存储在一个数组或列表中。
// 在创建对象事件中预创建对象实例
var enemies = ds_list_create();
for (var i = 0; i < 10; i++) {
var enemy = instance_create_layer(100 + i * 50, 100, "Instances", obj_enemy);
ds_list_add(enemies, enemy);
}
// 在其他事件中使用这些对象实例
for (var i = 0; i < ds_list_size(enemies); i++) {
var enemy = ds_list_find_value(enemies, i);
enemy.health -= 10;
}
5.4.2 对象池
对象池是一种常见的性能优化技术,通过重用对象实例来避免频繁的创建和销毁。
// 定义一个对象池
var enemy_pool = ds_list_create();
// 在创建对象事件中初始化对象池
for (var i = 0; i < 10; i++) {
var enemy = instance_create_layer(-100, -100, "Instances", obj_enemy);
ds_list_add(enemy_pool, enemy);
}
// 在需要时从对象池中获取对象
function get_enemy() {
if (ds_list_size(enemy_pool) > 0) {
var enemy = ds_list_find_value(enemy_pool, 0);
ds_list_delete(enemy_pool, 0);
enemy.visible = true;
enemy.x = 100;
enemy.y = 100;
return enemy;
} else {
show_error("对象池已空");
return noone;
}
}
// 在不再需要时将对象返回到对象池
function return_enemy(enemy) {
if (enemy != noone) {
enemy.visible = false;
ds_list_add(enemy_pool, enemy);
}
}
// 使用对象池
var enemy = get_enemy();
if (enemy != noone) {
enemy.health -= 10;
return_enemy(enemy);
}
5.5 优化图形和碰撞检测
图形和碰撞检测是游戏开发中的重要部分,但也是性能瓶颈。合理地优化这些操作可以显著提升游戏性能。
5.5.1 使用碰撞对象
使用碰撞对象(如instance_place
、instance_nearest
等)可以减少碰撞检测的复杂度。
// 使用instance_nearest函数检测最近的敌人
var nearest_enemy = instance_nearest(x, y, obj_enemy);
if (nearest_enemy != noone) {
show_debug_message("最近的敌人: " + nearest_enemy.name);
}
5.5.2 减少图形渲染
减少不必要的图形渲染可以提升性能。例如,可以只在对象可见时才进行渲染。
// 在绘制事件中检查对象是否可见
if (visible) {
draw_self();
}
5.6 使用脚本和函数
合理地使用脚本和函数可以提高代码的可读性和可维护性,同时也可以优化性能。将常用的代码段封装成函数或脚本,可以避免重复代码,减少错误。
// 定义一个脚本,用于计算伤害
script_add(script_create("calculate_damage", "function calculate_damage(attack_power, defense) { var damage = attack_power - defense; if (damage < 0) { damage = 0; } return damage; }"));
// 调用脚本
var damage = calculate_damage(50, 20);
show_debug_message("伤害值: " + string(damage)); // 输出: 伤害值: 30
6. 高级调试技巧
6.1 使用调试输出
调试输出是开发过程中非常有用的工具,可以帮助你了解代码的执行情况。GML提供了show_debug_message
函数用于输出调试信息。
// 在步进事件中输出调试信息
show_debug_message("当前帧: " + string(current_time));
show_debug_message("玩家位置: (" + string(x) + ", " + string(y) + ")");
6.2 使用断点
断点是调试代码时的重要工具,可以在特定的代码行暂停执行,以便检查变量的值和程序的状态。
-
在GameMaker Studio中,右击代码行,选择“插入断点”。
-
运行游戏时,程序会在断点处暂停,你可以检查变量的值和调用栈。
6.3 使用性能分析工具
GameMaker Studio提供了性能分析工具,可以帮助你识别代码中的性能瓶颈。通过分析工具,你可以了解哪些函数或代码段消耗了较多的时间,从而进行优化。
-
在GameMaker Studio中,选择“工具” > “性能分析器”。
-
运行游戏并观察性能分析结果。
7. 总结
通过本节的学习,你已经掌握了GML脚本语言的高级运用,包括函数的定义与调用、参数传递、变量的高级使用、控制结构、面向对象编程和性能优化等方面。这些知识将帮助你在GameMaker Studio中编写更复杂、更高效的游戏代码,提升你的游戏开发能力。
希望这些内容对你有所帮助,祝你在游戏开发的道路上越走越远!
如果你有任何疑问或需要进一步的帮助,请随时提问。