跳到主要内容

1.3 函数与事件处理

前言:让代码学会"重复利用"

在前两节中,我们学会了存储数据和做条件判断。但如果你仔细观察我们写过的代码,会发现一个问题:每次需要检查玩家血量、输出状态信息,我们都要把相同的逻辑重新写一遍。

想象一下,你在 Minecraft 里建造了一个非常漂亮的路灯。如果你想在城市的每条街道都放上这种路灯,你不会每次都从零开始建造——你会把它做成一个蓝图,然后按照蓝图批量建造。

函数就是代码的蓝图。 你把一段逻辑写进函数里,之后在任何需要的地方,只需要一行代码就能调用它。

这一节我们还会接触事件处理的概念,这是 Minecraft Script API 中最核心的机制之一。


1.3.1 为什么需要函数?

先来看一个没有使用函数的例子,感受一下问题所在:

// 玩家1加入游戏
let player1Name = "Steve";
let player1Health = 20;
console.log(`欢迎,${player1Name}`);
console.log(`${player1Name} 的血量:${player1Health} / 20`);
console.log(`${player1Name} 已准备就绪。`);

// 玩家2加入游戏
let player2Name = "Alex";
let player2Health = 18;
console.log(`欢迎,${player2Name}`);
console.log(`${player2Name} 的血量:${player2Health} / 20`);
console.log(`${player2Name} 已准备就绪。`);

// 玩家3加入游戏
let player3Name = "Herobrine";
let player3Health = 20;
console.log(`欢迎,${player3Name}`);
console.log(`${player3Name} 的血量:${player3Health} / 20`);
console.log(`${player3Name} 已准备就绪。`);

你发现了什么?同样的逻辑被复制粘贴了三次。如果有100个玩家,这段代码会长得离谱。更糟糕的是,如果你想修改欢迎信息的格式,就需要改100个地方,而且很容易漏改。

用函数来改写,只需要这样:

function greetPlayer(name, health) {
console.log(`欢迎,${name}`);
console.log(`${name} 的血量:${health} / 20`);
console.log(`${name} 已准备就绪。`);
}

greetPlayer("Steve", 20);
greetPlayer("Alex", 18);
greetPlayer("Herobrine", 20);

代码量少了一半,逻辑只写了一次,修改时也只需要改一个地方。这就是函数的价值。


1.3.2 定义函数:function 关键字

定义一个函数,使用 function 关键字:

function 函数名称() {
// 这里写函数要执行的代码
}

定义完函数之后,函数里的代码不会自动执行。你需要调用它,才会真正运行:

// 定义函数
function checkTime() {
console.log("正在检查当前时间...");
console.log("检查完毕。");
}

// 调用函数
checkTime(); // 执行这行,函数里的代码才会运行
checkTime(); // 可以调用任意多次

输出结果:

正在检查当前时间...
检查完毕。
正在检查当前时间...
检查完毕。

函数名的命名规则和变量相同,也推荐使用驼峰命名法。函数通常用动词开头来命名,因为函数是用来"做某件事"的:

function checkPlayerHealth() { ... } // 检查玩家血量
function sendWelcomeMessage() { ... } // 发送欢迎消息
function spawnMonster() { ... } // 生成怪物
function calculateDamage() { ... } // 计算伤害

1.3.3 参数:给函数传递信息

很多时候,函数需要接收一些外部信息才能工作。比如"欢迎玩家"这个函数,需要知道玩家叫什么名字。

我们通过参数来给函数传递这些信息。参数写在函数名后面的括号里:

function greetPlayer(playerName) {
console.log(`欢迎来到服务器,${playerName}`);
}

greetPlayer("Steve"); // 输出:欢迎来到服务器,Steve!
greetPlayer("Alex"); // 输出:欢迎来到服务器,Alex!
greetPlayer("Herobrine"); // 输出:欢迎来到服务器,Herobrine!

playerName 就是参数,它就像函数专属的一个临时变量,每次调用函数时赋予不同的值。

函数可以有多个参数,用逗号分隔:

function showPlayerStatus(name, health, level) {
console.log(`--- ${name} 的状态 ---`);
console.log(`血量:${health} / 20`);
console.log(`等级:${level}`);
}

showPlayerStatus("Steve", 20, 30);
showPlayerStatus("Alex", 14, 12);

输出结果:

--- Steve 的状态 ---
血量:20 / 20
等级:30
--- Alex 的状态 ---
血量:14 / 20
等级:12
备注

调用函数时传入的值叫做实参(argument),函数定义时括号里的占位名字叫做形参(parameter)。在日常交流中,大家通常都统称为"参数",不用特别纠结这个区别。

但有一点要记住:调用函数时,实参的顺序必须和形参一致。showPlayerStatus("Steve", 20, 30) 中,"Steve" 对应 name20 对应 health30 对应 level,顺序不能搞错。

参数的默认值:

如果调用函数时某个参数没有传入,你可以为它设置一个默认值:

function greetPlayer(playerName, greeting = "欢迎来到服务器") {
console.log(`${greeting}${playerName}`);
}

greetPlayer("Steve"); // 使用默认问候语
greetPlayer("Alex", "很高兴再次见到你"); // 使用自定义问候语

输出结果:

欢迎来到服务器,Steve!
很高兴再次见到你,Alex!

1.3.4 返回值:让函数给你一个结果

函数不只是用来执行操作的,它还可以计算一个结果并把结果交还给你。用 return 关键字来返回值:

function calculateDamage(baseDamage, multiplier) {
let finalDamage = baseDamage * multiplier;
return finalDamage;
}

let damage = calculateDamage(5, 1.5);
console.log(`最终伤害:${damage}`); // 输出:最终伤害:7.5

return 做了两件事:

  1. finalDamage 的值"送出"函数
  2. 立即结束函数的执行return 之后的代码不会运行

因为有了返回值,函数的结果可以直接参与运算或存入变量:

function getHealthPercentage(currentHealth, maxHealth) {
return (currentHealth / maxHealth) * 100;
}

let healthPercent = getHealthPercentage(14, 20);
console.log(`血量百分比:${healthPercent}%`); // 输出:血量百分比:70%

// 也可以直接把函数调用放进模板字符串
console.log(`当前血量:${getHealthPercentage(8, 20)}%`); // 输出:当前血量:40%

return 也可以提前结束函数,用于处理不合法的输入:

function checkPlayerLevel(level) {
if (level < 0) {
console.log("错误:等级不能为负数。");
return; // 直接结束函数,下面的代码不会执行
}

if (level >= 30) {
console.log("你已达到高级玩家标准,可以挑战末地。");
} else {
console.log(`当前等级 ${level},距离挑战末地还需 ${30 - level} 级。`);
}
}

checkPlayerLevel(-5); // 输出:错误:等级不能为负数。
checkPlayerLevel(15); // 输出:当前等级 15,距离挑战末地还需 15 级。
checkPlayerLevel(35); // 输出:你已达到高级玩家标准,可以挑战末地。
提示

return 提前结束函数来处理非法输入,是一种非常常见且推荐的编程技巧,有时被称为"提前返回"或"守卫子句"(Guard Clause)。它的好处是让代码的主要逻辑不被大量的 if...else 嵌套包裹,使代码更清晰易读。

在 Script API 的实际开发中,你会频繁用到这个技巧,比如在事件处理函数里首先判断触发事件的是不是玩家,不是的话直接 return


1.3.5 函数表达式与箭头函数

在 JavaScript 里,除了用 function 关键字定义函数,还有另外两种写法,它们在 Minecraft Script API 的代码中极为常见,必须认识。

函数表达式

把一个函数直接赋值给一个变量:

const greetPlayer = function(playerName) {
console.log(`欢迎,${playerName}`);
};

greetPlayer("Steve"); // 输出:欢迎,Steve!

这里的函数被存进了 greetPlayer 这个变量里。调用方式和普通函数完全相同。

箭头函数(Arrow Function)

箭头函数是 ES6 引入的更简洁的函数写法,在现代 JavaScript 中非常普遍(在Script API中,其甚至比直接的 function 申明更为普遍):

const greetPlayer = (playerName) => {
console.log(`欢迎,${playerName}`);
};

greetPlayer("Steve"); // 输出:欢迎,Steve!

和普通函数相比,箭头函数用 => 取代了 function 关键字。

当函数体只有一行,且需要返回一个值时,可以省略大括号和 return,进一步简化:

// 完整写法
const double = (n) => {
return n * 2;
};

// 简化写法(只有一行 return 时)
const double = (n) => n * 2;

console.log(double(5)); // 输出:10
console.log(double(14)); // 输出:28

当参数只有一个时,还可以省略括号:

const double = n => n * 2; // 参数只有一个,括号也可以省略

下面是几种函数写法的对照,它们的效果完全相同:

// 写法一:function 声明
function calculateDamage(base, multiplier) {
return base * multiplier;
}

// 写法二:函数表达式
const calculateDamage = function(base, multiplier) {
return base * multiplier;
};

// 写法三:箭头函数
const calculateDamage = (base, multiplier) => {
return base * multiplier;
};

// 写法四:箭头函数简化版
const calculateDamage = (base, multiplier) => base * multiplier;
备注

你可能会问:这么多种写法,我该用哪个?

对于初学者,建议先用 function 关键字写法,逻辑最清晰。但因为 Minecraft Script API 的文档和绝大多数社区代码都大量使用箭头函数,你必须能够读懂箭头函数的写法。

在本教程后续涉及事件处理的地方,我们会使用箭头函数,因为那是 API 代码中最自然的写法。


1.3.6 作用域:变量的"生效范围"

理解函数,就必须理解作用域。作用域决定了一个变量在哪些地方可以被访问到。

全局作用域

在所有函数外面定义的变量,拥有全局作用域,在代码的任何地方都可以访问:

let serverName = "我的MC服务器"; // 全局变量

function showServerInfo() {
console.log(`服务器名称:${serverName}`); // 可以访问全局变量
}

showServerInfo(); // 输出:服务器名称:我的MC服务器

局部作用域(函数作用域)

在函数内部定义的变量,只在这个函数内部有效,函数外面无法访问:

function calculateDamage() {
let damage = 10; // 局部变量,只在函数内有效
console.log(`函数内部:${damage}`);
}

calculateDamage();
console.log(damage); // 报错!damage 在函数外面不存在

这就好比你在一个房间里放了一件东西,你知道这是什么,而房间外没有任何人知道这是什么。

下面这个例子可以帮你更直观地理解:

let playerName = "Steve"; // 全局变量

function testScope() {
let playerName = "Alex"; // 局部变量,和全局的 playerName 是两个不同的变量
console.log(`函数内部:${playerName}`); // 输出:Alex(用的是局部变量)
}

testScope();
console.log(`函数外部:${playerName}`); // 输出:Steve(用的是全局变量)
注意

虽然全局变量在任何地方都能访问,听起来很方便,但请不要滥用全局变量

如果你的代码里到处都是全局变量,当程序变得复杂,你会很难追踪某个变量是在哪里被修改的,这会让调试变成噩梦。

一个好的原则是:变量的作用范围越小越好。能用局部变量解决的,就不要用全局变量。在 Script API 开发中,我们通常只把真正需要跨函数共享的数据放在全局范围。


1.3.7 事件处理:Script API 的核心机制

现在我们来到这一节最重要的部分,也是 Minecraft Script API 的核心概念之一:事件处理

首先,什么是事件?

在 Minecraft 中,每时每刻都在发生各种各样的"事情":玩家跳跃、玩家受伤、方块被破坏、实体被生成……这些"事情"在编程中叫做事件

Script API 允许你"监听"这些事件:一旦某个事件发生,你预先准备好的函数就会自动被调用,执行你想要的操作。

这个"预先准备好的函数"有一个专门的名字:事件处理函数(Event Handler),也叫回调函数(Callback Function)。

用一个生活中的比喻来理解:

你在煮饭的时候,可以做其他事情,但是你告诉自己"一旦计时器响了,就去关火"。这里,"计时器响了"是事件,"关火"这个动作就是回调函数,它不是立刻执行的,而是等到事件发生时才执行。

回调函数的概念

在学习 API 的事件处理之前,先用纯 JavaScript 来理解回调函数的概念:

// 定义一个函数,它接受另一个函数作为参数
function doSomethingLater(callback) {
console.log("准备执行操作...");
callback(); // 在适当的时机,调用传入的函数
console.log("操作完成。");
}

// 把一个函数作为参数传进去
doSomethingLater(function() {
console.log("这是在适当时机被调用的代码!");
});

输出结果:

准备执行操作...
这是在适当时机被调用的代码!
操作完成。

函数可以像变量一样被传来传去,这是 JavaScript 的一个重要特性。正是这个特性,让事件处理成为可能。

在 Script API 中订阅事件

在 Minecraft Script API 中,监听一个事件用 .subscribe() 方法,你需要给它传入一个回调函数:

import { world } from "@minecraft/server";

// 监听"玩家生成"事件
// 当有玩家进入游戏时,传入的这个箭头函数会自动被调用
world.afterEvents.playerSpawn.subscribe((event) => {
const player = event.player;
const playerName = player.name;

player.sendMessage(`欢迎回来,${playerName}`);
world.sendMessage(`${playerName} 加入了游戏。`);
});

让我们把这段代码拆解开来看:

world.afterEvents.playerSpawn.subscribe(
// 这里传入了一个箭头函数作为回调
(event) => {
// event 是事件对象,里面包含了这次事件的所有相关信息
// 比如是哪个玩家触发了这个事件
const player = event.player;
player.sendMessage("欢迎!");
}
);
  • world.afterEvents.playerSpawn 是"玩家生成"这个事件的入口
  • .subscribe(...) 表示"我要订阅这个事件,请在它发生的时候通知我"
  • 传入的箭头函数就是"通知我之后要执行的操作"
  • event 参数是 API 自动传入的,包含了事件的详细信息

一个更完整的事件处理示例

import { world } from "@minecraft/server";

// 定义一个函数,处理玩家受伤的逻辑
function handlePlayerHurt(event) {
const player = event.hurtEntity;

// 如果受伤的不是玩家,直接结束
if (player.typeId !== "minecraft:player") {
return;
}

const health = player.getComponent("minecraft:health").currentValue;
const playerName = player.name;

// 根据血量情况发出不同的提示
if (health <= 4) {
player.sendMessage("危险!你的血量极低!");
world.sendMessage(`警告:${playerName} 处于危险状态!`);
} else if (health <= 10) {
player.sendMessage("你受伤了,注意补血。");
}
}

// 订阅事件,把处理函数传入
world.afterEvents.entityHurt.subscribe(handlePlayerHurt);

注意这里的写法:我们先把处理逻辑单独写成了一个函数 handlePlayerHurt,然后把这个函数的名字传给 .subscribe()。这和直接在 .subscribe() 里写箭头函数效果完全相同,但当逻辑比较复杂时,这种写法会更清晰。


1.3.8 函数的实际组合运用

在真实的开发中,我们会把复杂的逻辑拆分成多个小函数,然后在事件处理里组合调用它们:

import { world } from "@minecraft/server";

// 判断玩家血量等级,返回一个描述字符串
function getHealthStatus(health) {
if (health <= 2) return "极度危险";
if (health <= 6) return "血量偏低";
if (health <= 14) return "状态一般";
return "状态良好";
}

// 根据血量状态构建提示信息
function buildWarningMessage(playerName, health) {
const status = getHealthStatus(health);
return `${playerName} 的状态:${status}${health} / 20)`;
}

// 判断是否需要向全服广播警告
function shouldBroadcast(health) {
return health <= 4;
}

// 事件处理函数,组合调用上面的小函数
function handlePlayerHurt(event) {
const player = event.hurtEntity;

if (player.typeId !== "minecraft:player") {
return;
}

const health = player.getComponent("minecraft:health").currentValue;
const playerName = player.name;
const message = buildWarningMessage(playerName, health);

player.sendMessage(message);

if (shouldBroadcast(health)) {
world.sendMessage(`[服务器警告] ${message}`);
}
}

world.afterEvents.entityHurt.subscribe(handlePlayerHurt);

这种把代码拆分成小函数、各司其职的写法,是专业开发者普遍遵循的原则。每个函数只做一件事,逻辑清晰,也很容易单独测试和修改。


1.3.9 实战练习:玩家加入服务器的欢迎系统

综合运用这一节学到的所有知识,搭建一个完整的欢迎系统:

import { world } from "@minecraft/server";

// 记录服务器已接待的玩家总数(全局变量,需要跨事件共享)
let totalJoinCount = 0;

// 根据加入次数生成不同的欢迎语
function getWelcomeMessage(playerName, joinCount) {
if (joinCount === 1) {
return `欢迎 ${playerName} 首次加入服务器!希望你玩得开心!`;
} else if (joinCount <= 5) {
return `欢迎回来,${playerName}!你是本次开服后第 ${joinCount} 位加入的玩家。`;
} else {
return `${playerName} 回来了!`;
}
}

// 向玩家发送私人提示
function sendPrivateTips(player) {
player.sendMessage("--- 服务器提示 ---");
player.sendMessage("输入 /help 查看可用指令");
player.sendMessage("祝你游戏愉快!");
}

// 事件处理主函数
function handlePlayerJoin(event) {
const player = event.player;
const playerName = player.name;

// 更新计数
totalJoinCount++;

// 生成欢迎消息并全服广播
const welcomeMsg = getWelcomeMessage(playerName, totalJoinCount);
world.sendMessage(welcomeMsg);

// 向该玩家单独发送私人提示
sendPrivateTips(player);

// 在控制台记录日志
console.log(`[日志] ${playerName} 加入了游戏,今日第 ${totalJoinCount} 位玩家。`);
}

// 订阅玩家生成事件
world.afterEvents.playerSpawn.subscribe(handlePlayerJoin);

本节知识总结

概念要点示例
函数定义function 关键字定义function greet() {...}
函数调用函数名加括号greet()
参数向函数传递信息function greet(name) {...}
默认参数参数未传入时的备用值function greet(name = "玩家") {...}
返回值return 把结果送出函数return damage * 2;
提前返回return 终止函数执行if (!player) return;
函数表达式把函数赋值给变量const greet = function() {...}
箭头函数更简洁的函数写法const greet = (name) => {...}
全局作用域函数外的变量,全局可访问let count = 0;(写在函数外)
局部作用域函数内的变量,外部不可访问函数内部的 let
回调函数被作为参数传入的函数.subscribe(handleEvent)
事件订阅监听 API 事件,事件发生时调用回调world.afterEvents.xxx.subscribe(fn)

课后练习

练习1: 写一个函数 calculateDistance,接受两个参数 x1x2(代表一维坐标),返回它们之间的距离(两数之差的绝对值)。提示:JavaScript 中取绝对值可以使用 Math.abs(数字)

练习2: 写一个函数 describeItem,接受物品名称(itemName)、数量(count)和稀有度(rarity,可以是 "普通""稀有""史诗")三个参数,返回一段描述字符串,例如 "[稀有] 钻石剑 x1"。为 rarity 设置默认值 "普通"

练习3(思考题): 回顾 1.3.8 中把逻辑拆分成多个小函数的写法。思考一下,如果不拆分,把所有逻辑都写在 handlePlayerHurt 一个函数里,会有什么缺点?拆分成多个小函数又有哪些好处?


下一节预告:1.4 循环与批量操作

现在你已经能定义函数、处理事件了。但还有一个非常常见的需求我们还没有解决:对一批数据执行相同的操作。比如,服务器里有20个玩家,你想给每个人都发一条消息;或者你想检查100个方块坐标中哪些位置是空的。手动写20遍、100遍显然不现实。下一节,我们将学习循环,让程序自动重复执行操作,这将大大拓展你能处理的问题规模。