Open10

Minecraft Education で Script API を使う

たくのろじぃ | Takumi Okawaたくのろじぃ | Takumi Okawa

マイクラ内でなるべく教材が完結するよう、あるいはヘルプを表示できるようにGUIを実装できるような試み。

Script API は簡単に言えば JavaScript(TypeScript) を使ったアドオン開発。mcfunction で実装しても変数やら条件分岐やらイベントトリガーなど、すごく面倒。なので、システム的な部分をAPIで補おうという話。

使用するには Manifest ファイルにモジュールを登録しておく必要がある。

  1. マイクラ内のGUI操作に必要なモジュール
    https://learn.microsoft.com/ja-jp/minecraft/creator/scriptapi/minecraft/server-ui/minecraft-server-ui?view=minecraft-bedrock-stable

  2. マイクラ内のワールドやエンティティなどの操作に必要なモジュール
    https://learn.microsoft.com/ja-jp/minecraft/creator/scriptapi/minecraft/server/minecraft-server?view=minecraft-bedrock-stable

基本的に2点セットが揃っていれば、GUI開発は可能。

このAPI,統合版でしか使えないものだと思っていたが、統合版ベースになっている教育版でも利用できることを知った。

たくのろじぃ | Takumi Okawaたくのろじぃ | Takumi Okawa

まずはワールドデータのディレクトリに入って、ビヘイビアパックの 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 にあるのがスクリプト実行のエントリポイント。

マイクラ再起動して有効化しておく。

たくのろじぃ | Takumi Okawaたくのろじぃ | Takumi Okawa

あとはコーディング中にシンタックスハイライトやら参照ができたほうが便利なので、npm パッケージを取得しておく。

npm install @minecraft/server
npm install @minecraft/server-ui
たくのろじぃ | Takumi Okawaたくのろじぃ | Takumi Okawa

ソースコードはワールドディレクトリ内直下に 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 を作成し、コーディングの準備は完了。

たくのろじぃ | Takumi Okawaたくのろじぃ | Takumi Okawa

ここまでのファイル構成はこんな感じ。一部省略。

│  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
たくのろじぃ | Takumi Okawaたくのろじぃ | Takumi Okawa

まずは動作チェック。コードをかけたら 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);

たくのろじぃ | Takumi Okawaたくのろじぃ | Takumi Okawa

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("それ以外のものですね");
    }
});

アイテム持って右クリックするとこんな感じ。

たくのろじぃ | Takumi Okawaたくのろじぃ | Takumi Okawa

ということで、アイテムを使うとお助けメニューが表示できるような挙動も作れるわけですね。
問題を解いている生徒さんにヒントが表示できると便利です。

ダイヤモンドを右クリックすると、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;
        }
    });
};

たくのろじぃ | Takumi Okawaたくのろじぃ | Takumi Okawa

さて、次は先生目線です。マルチプレイでのクラスでは、生徒さんが途中からバラバラになって行動することがあります。全員を先生のもとへ集合(テレポート)させる、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}`);