Minecraft Education で Script API を使う
マイクラ内でなるべく教材が完結するよう、あるいはヘルプを表示できるようにGUIを実装できるような試み。
Script API は簡単に言えば JavaScript(TypeScript) を使ったアドオン開発。mcfunction で実装しても変数やら条件分岐やらイベントトリガーなど、すごく面倒。なので、システム的な部分をAPIで補おうという話。
使用するには Manifest ファイルにモジュールを登録しておく必要がある。
-
マイクラ内のGUI操作に必要なモジュール
https://learn.microsoft.com/ja-jp/minecraft/creator/scriptapi/minecraft/server-ui/minecraft-server-ui?view=minecraft-bedrock-stable -
マイクラ内のワールドやエンティティなどの操作に必要なモジュール
https://learn.microsoft.com/ja-jp/minecraft/creator/scriptapi/minecraft/server/minecraft-server?view=minecraft-bedrock-stable
基本的に2点セットが揃っていれば、GUI開発は可能。
このAPI,統合版でしか使えないものだと思っていたが、統合版ベースになっている教育版でも利用できることを知った。
まずはワールドデータのディレクトリに入って、ビヘイビアパックの manifest.json
を定義しておく。下記2つが ScriptAPI を使う上で重要なやつ。
- @minecraft/server
- @minecraft/server-ui
{
"format_version": 2,
"header": {
"description": "テストパック",
"name": "takunology-001",
"uuid": "eef9aaed-455c-a1b8-2a4a-2b84fc9105bb",
"version": [1, 0, 0],
"min_engine_version": [1,20,13]
},
"modules": [
{
"description": "データ",
"type": "data",
"uuid": "078ad330-4b14-5f71-0c08-1b70627dc812",
"version": [1, 0, 0]
},
{
"type": "script",
"language": "javascript",
"uuid": "ee45cc17-4765-3898-9210-d283b45d0407",
"entry": "scripts/main.js",
"version": [1,0,0]
}
],
"dependencies": [
{
"module_name": "@minecraft/server",
"version": "1.11.0"
},
{
"module_name": "@minecraft/server-ui",
"version": "1.1.0"
}
]
}
で、modules
の中の entry
にあるのがスクリプト実行のエントリポイント。
マイクラ再起動して有効化しておく。
あとはコーディング中にシンタックスハイライトやら参照ができたほうが便利なので、npm パッケージを取得しておく。
npm install @minecraft/server
npm install @minecraft/server-ui
ソースコードはワールドディレクトリ内直下に src
フォルダを作成して、それをもとにビヘイビアパックの scripts
フォルダへ書き出せるようにしておく。マイクラ側では behavior_packs/hoge/scripts/main.js
を読み出しに行くようになっているので、scripts
フォルダは必ず必要。
続いて TypeScript 化もしていく。ワールドディレクトリ内に tsconfig.json
を作っておく。
{
"compilerOptions": {
"module": "ES2020",
"target": "ES2021",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true,
"baseUrl": "./src",
"rootDir": "./src",
"outDir": "./behavior_packs/scripting_api/scripts"
},
"exclude": [ "node_modules" ],
"include": [ "src" ]
}
そして src
ディレクトリに Main.ts
を作成し、コーディングの準備は完了。
ここまでのファイル構成はこんな感じ。一部省略。
│ level.dat
│ level.dat_old
│ levelname.txt
│ package-lock.json
│ package.json
│ tsconfig.json
│ world_behavior_packs.json
│ world_icon.jpeg
│ world_resource_packs.json
│
├─behavior_packs
│ └─scripting_api
│ manifest.json
├─db
├─node_modules
│ └─@minecraft
│ ├─common
│ ├─server
│ └─server-ui
│
├─resource_packs
└─src
Main.ts
まずは動作チェック。コードをかけたら tsc
コマンドでトランスパイルする。生成された JS ファイルはマイクラ側で自動読み込みされないので、マイクラ上でも /reload
を実行するのを忘れない。
例えば20ティックごとにメッセージを表示していくプログラム。ちょっと将棋っぽく、10秒~とか20秒~とかやってみたり。
import { world, system } from "@minecraft/server";
let x: number = 1;
system.runInterval(() => {
world.sendMessage(String(x));
if(x > 9 && x % 10 === 0) {
world.sendMessage(`§6${x}秒§r経過~`);
}
x++;
}, 20);
MakeCodeではおなじみの「アイテムが使われたとき」の動きも作れる。
import { world, ItemUseAfterEvent, Player, ItemStack } from "@minecraft/server";
world.afterEvents.itemUse.subscribe((event :ItemUseAfterEvent) => {
const itemStack = event.itemStack as ItemStack;
if(itemStack.typeId === "minecraft:stick") {
world.sendMessage("これは§a棒§rですねぇ");
} else if (itemStack.typeId === "minecraft:iron_sword") {
world.sendMessage("これは§6鉄の剣§rですねぇ");
} else if (itemStack.typeId === "minecraft:diamond") {
world.sendMessage("これは§sダイヤモンド§rですなぁ");
} else {
world.sendMessage("それ以外のものですね");
}
});
アイテム持って右クリックするとこんな感じ。
ということで、アイテムを使うとお助けメニューが表示できるような挙動も作れるわけですね。
問題を解いている生徒さんにヒントが表示できると便利です。
ダイヤモンドを右クリックすると、GUIメニューが表示される例
import { world, ItemUseAfterEvent, Player, ItemStack } from "@minecraft/server";
import { ActionFormData, ActionFormResponse } from "@minecraft/server-ui";
world.afterEvents.itemUse.subscribe((event: ItemUseAfterEvent) => {
const player = event.source as Player;
const itemStack = event.itemStack as ItemStack;
if(itemStack.typeId === "minecraft:diamond") {
showActionForm(player);
} else {
return;
}
});
function showActionForm(player: Player) {
const form = new ActionFormData();
form.title("お助けメニュー");
form.body("問題がむずかしいときは、ボタンをおしてヒントをもらおう!");
form.button("1問目のヒント");
form.button("2問目のヒント");
form.button("先生をよぶ");
form.show(player).then((response: ActionFormResponse) => {
switch (response.selection) {
case 0:
player.sendMessage("1番目のボタンが選択されたよ");
break;
case 1:
player.sendMessage("2番目のボタンが選択されたよ");
break;
case 2:
player.sendMessage("3番目のボタンが選択されたよ");
break;
default:
player.sendMessage("キャンセルされたよ");
break;
}
});
};
さて、次は先生目線です。マルチプレイでのクラスでは、生徒さんが途中からバラバラになって行動することがあります。全員を先生のもとへ集合(テレポート)させる、AさんをBさんへテレポートさせるといった操作が必要なことも。
もちろん、先生全員がコマンドを完璧に使えればいいのですが、コマンドが苦手な方や新しい先生は難しいですよね。そんなときにもGUIコントロールを使えば便利です。
import { world, system, ItemUseAfterEvent, Player, ItemStack, PaletteColor } from "@minecraft/server";
import { ActionFormData, ActionFormResponse, ModalFormData } from "@minecraft/server-ui";
world.afterEvents.itemUse.subscribe((event: ItemUseAfterEvent) => {
const player = event.source as Player;
const itemStack = event.itemStack as ItemStack;
if(itemStack.typeId === "minecraft:diamond") {
showActionForm(player); //これは先程の関数
} else if(itemStack.typeId === "minecraft:stick") {
teachersActionForm(player);
} else {
return;
}
});
function teachersActionForm(player: Player) {
const form = new ActionFormData();
form.title("先生用メニュー");
form.body("必要な操作を選んでください");
form.button("テレポートする");
form.button("アイテムを渡す");
form.button("ブロックを設置する");
form.show(player).then((response: ActionFormResponse) => {
switch (response.selection) {
case 0:
showTpModal(player);
break;
case 1:
player.sendMessage("2番目のボタンが選択されたよ");
break;
case 2:
player.sendMessage("3番目のボタンが選択されたよ");
break;
default:
break;
}
});
}
let tpSelector: string[] = ["先生", "生徒A", "生徒B"];
function showTpModal(player: Player) {
const modalForm = new ModalFormData()
modalForm.title("テレポート操作");
modalForm.dropdown("テレポート元のプレイヤー", tpSelector, 1);
modalForm.dropdown("テレポート先のプレイヤー", tpSelector);
modalForm.show(player).then(formData => {
const fromTp = tpSelector[Number(formData.formValues[0])];
const toTp = tpSelector[Number(formData.formValues[1])];
player.sendMessage(`/tp ${fromTp} ${toTp} が実行されました`);
}).catch((error: Error) => {
player.sendMessage(`エラー: ` + error);
return -1;
});
}
この例ではただメッセージを出しているだけですが、本当にテレポートさせるなら runCommand
メソッドを使用することで、任意のコマンドを実行できます。
player.runCommand(`/tp ${fromTp} ${toTp}`);
ときどきこれが表示される理由がわからん