🎮

TypeScript で Minecraft のサバイバルを楽にするアドオンを作ってみた

2023/12/24に公開
5

こんにちは。株式会社アイデミーの清水(@meso)です。

アイデミーは、DXリテラシーからAI/機械学習の専門知識までオンラインで学べるラーニングサービス Aidemy を運営しています。

教育サービスをやっているソフトウェアエンジニアという立場なので、自分の子にもプログラミングをどう経験させようかとずっと考えてきましたが、その一つの道筋が見えてきたのでそれを共有しつつ、そのために必要なツールとして表題のアドオンをご紹介いたします。

対象読者は、普段から TypeScript や JavaScript のコードを書いていて、子供にプログラミングに親しみを持ってもらいたいと思っている Web エンジニアを想定しています。

Minecraft とプログラミング

娘は、年長さんのころからマインクラフトにハマり始めました。きっかけは YouTube で「まいぜんシスターズ」や「HIKAKIN」の動画を見始めたことなんですが、マイクラがプログラミング教育にも良いという話は以前から聞いていたので、タブレットにアプリをインストールするなど僕からも後押しした感じでした。

マインクラフトがプログラミング教育にも良いと言われる所以はいくつかあるのですが、マインクラフトで経験できる主なプログラミング的な要素を以下にまとめます。

マインクラフトには、昔からある Java 版(Java Edition) と比較的新しい 統合版(Bedrock Edition) と、統合版をベースとした 教育版(Minecraft Education) がありますが、それぞれで出来ることが微妙に異なっているためそれもまとめます。

  1. レッドストーンやコマンドブロックを用いた自動装置を作る

    • 自動で羊毛を刈り取る装置や、自動で畑を収穫する装置など
    • ゲームの延長線上で、効率化を求めるようになると自然と行き着くようになっているし、YouTube などでも作例がとても多い
    • ゲーム内アイテムであるレッドストーンを用いたレッドストーン回路は論理回路そのものであり、チューリング完全であることが証明されている
    • Java 版、統合版、教育版 問わず(多少の違いはあるものの)実行可能
  2. プログラミングができる Mod を入れる

    • Mod とは Java 版で利用可能 なゲームを拡張する非公式な拡張手法
    • 非公式ではあるもののとても多くのユーザーによって利用されている
    • Raspberry Jam Mod を導入すると、PythonScratch でワールドを操作できるようになる
  3. 教育版マインクラフト(Minecraft Education)でコードエディタを使う

    • 教育版マインクラフトは、Java 版や統合版と違って買い切りではなく年間12ドルのサブスク
    • 元々は教育機関のみが利用できたが、今は一般的な組織でも購入可能(独自ドメインさえあれば一般家庭でも)
    • ワールド内でコードエディタを開けば、いつでも Scratch ライクなブロックコーディングや Python でのコーディングが可能
  4. Mod や Addon を開発する

    • Java 版 での Mod に相当するのが 統合版 における Addon
    • Addon は Mod と違って公式にサポートされた機能
    • Addon は Script API を用いて JavaScript でコーディングが可能
    • 統合版には JavaScript エンジンとして QuickJS が組み込まれている
  5. WebSocket を使ってマインクラフトのワールドと接続する

    • 実は 統合版 マインクラフトは WebSocket クライアントの機能も備わっている
    • /connect xxx.xxx.xxx.xxx コマンドで、任意の WebSocket サーバーに接続が可能
    • ワールドで起こる様々なイベントを Listen して、リアクションを起こすことが可能

Minecraft を好きになるまでの障害

マインクラフトでプログラミングを始めるときにまず障害になるのは、サバイバルの難しさだと思っています。

マインクラフトには主に サバイバルモードクリエイティブモード の2つのゲームモードがあります(厳密には他にもありますが省略)。敵 Mob が出てきて戦いながらサバイバルしてエンダードラゴンを倒すという目標があるのがサバイバルモードで、敵はでてこず死ぬこともなく自由に空が飛べすべてのアイテムを最初から使い放題なのがクリエイティブモードです。

巨大な建築をしたり、自動化装置を試作したりするのはクリエイティブモードが向いているのですが、ゲーム性がないのでやはりまずはサバイバルモードでゲームとしての楽しさを味わってもらいたいと考えました。

サバイバルのゲーム難易度は「ピースフル」「イージー」「ノーマル」「ハード」の4つから選択可能ですが、ピースフルは敵が出てこなくて必要な素材を集めるのに苦労することがあるため、イージーでワールドを作ることが多いのですが、小学1年生になってもまだうちの娘にはイージーでも難しく、すぐ死んでしまいます。ネザーにいくことすらほとんどできない状態です。

マインクラフトを好きになってもらう
→ もっとマインクラフトを極めたい
→ マインクラフトでプログラミングしよう

というステップを目論んでいたのに最初から躓いてしまい、今はどちらかというとマインクラフトをプレイしてる時間より YouTube のプレイ動画を見てる時間の方が長い始末です。

というわけで、マインクラフトのサバイバルモードを楽にするアドオンを TypeScript で作ることにしました!!(ここまで前置き)

Minecraft のアドオンを作る

娘は主にタブレットでマインクラフトを遊んでいるため、タブレットで動作するマインクラフトを改造する必要があります。そのため、PC でしか動作しない Java 版向けの Mod ではなく、統合版向けのアドオンを開発することにしました。

統合版であれば、Realms と呼ばれる公式が提供してるサーバー(有料)にそのアドオンを導入することで、タブレットでも Switch でも PC でもアドオンが有効な状態で(友達とも一緒に)遊ぶことができます。

アドオンの作り方もググってもらうと色々情報が出てくるのですが、古かったり Script API を使わないものだったりもあるので、ここに簡単にまとめます。

アドオンの概要

マインクラフトのアドオンは、ビヘイビアーパック(behavior pack)リソースパック(resource pack) の2つから構成されています。

リソースパックは、アイテムや Mob などの見た目をいじったり、新しいアイテムの見た目を定義したりするものです。ビヘイビアーパックは、ゲームの挙動を変更したり新しいアイテムの挙動を定義したりするためのものです。

今回は、サバイバルモードを「敵 Mob は出てくるけれどイージーよりももっと簡単にする」ことを目的としたアドオンを作成するので、見た目を変える必要はないため、ビヘイビアーパックのみを作成します。

ビヘイビアーパックの構成

アドオンの開発には、統合版マインクラフトがインストールされた Windows PC を利用します。タブレット等でもできなくはないようですが、Windows PC を使うのが一番楽だと思います。

マインクラフトをインストールしていると ${home}\AppData\Local\Packages\Microsoft.MinecraftUWP_8wekyb3d8bbwe\LocalState\games\com.mojang というフォルダがあり、その中に development_behavior_packsdevelopment_resource_packs があります。今回はビヘイビアーパックを作るので development_behavior_packs の中に、適当な名前のフォルダを作りましょう。僕は veryeasy という名前で作りました。

そこでおもむろに

$ npm install @minecraft/server

を実行しましょう。もちろん、Node.js のインストールは事前に済ませておいてください。

また、TypeScript を使いたいので

$ npm install typescript --save-dev

$ npx tsc --init

をします。tsconfig.jsonoutDirscripts にして、module"ES2020" か何か(ES Modules 対応のもの)にしてください。

あとは、アドオンについての定義を書き示す manifest.json を用意します。全部説明すると長くなるので全部貼り付けて要所だけ説明します。

manifest.json
{
    "format_version": 2,
    "header": {
        "name": "サバイバルを楽にするアドオン",
        "description": "レベルが上がると強くなり、敵を倒すとリジェネがかかる",
        "uuid": "xxxxx",
        "version": [ 1, 0, 0 ],
        "min_engine_version": [ 1, 20, 50 ]
    },
    "modules": [

        {
            "description": "Script API",
            "type": "script",
            "language": "javascript",
            "uuid": "yyyyy",
            "entry": "scripts/main.js",
            "version": [1, 0, 0]
        }
    ],
    "dependencies": [
        {
            "module_name": "@minecraft/server",
            "version": "1.7.0"
        }
    ]
}

2か所ある uuid は、それぞれユニークな UUID をジェネレーターとかで生成してコピペしてください。min_engine_version は動作させたいマインクラフトのバージョンを、entry には呼び出したい JavaScript のファイルを記述します。dependenciesversion は、npm install した @minecraft/server のバージョンと一致するよう注意してください。

TypeScript のコードを書く

あとは、src/main.ts にコード書いていくだけです。今回は、レベルがあがると色んなバフがついて自身が強化されることによって、サバイバルで生き抜くのが楽になるようにします。

こちらのコードもそれほど長くないので全部見てみましょう。

src/main.ts
import { system, world, TicksPerSecond, EntityHealthComponent, Player, Entity } from "@minecraft/server";

// レベルがあがった分だけ強くなっていく
system.runInterval(() => {
    const players = world.getAllPlayers();
    for (const player of players) {
        const level = player.level;
        if (level < 1) continue;

        // LVに応じてハートを増やす
        player.addEffect("health_boost", 999 * TicksPerSecond, { amplifier: (level - 1) / 2, showParticles: false });

        // LV5ずつ攻撃力と防御力を増やす
        if (level >= 20) {
            player.addEffect("strength", 999 * TicksPerSecond, { amplifier: 3, showParticles: false });
            player.addEffect("resistance", 999 * TicksPerSecond, { amplifier: 3, showParticles: false });    
        } else if (level >= 15) {
            player.addEffect("strength", 999 * TicksPerSecond, { amplifier: 2, showParticles: false });
            player.addEffect("resistance", 999 * TicksPerSecond, { amplifier: 2, showParticles: false });    
        } else if (level >= 10) {
            player.addEffect("strength", 999 * TicksPerSecond, { amplifier: 1, showParticles: false });
            player.addEffect("resistance", 999 * TicksPerSecond, { amplifier: 1, showParticles: false });    
        } else if (level >= 5) {
            player.addEffect("strength", 999 * TicksPerSecond, { amplifier: 0, showParticles: false });
            player.addEffect("resistance", 999 * TicksPerSecond, { amplifier: 0, showParticles: false });    
        }

        // LVに応じてステータス効果を増やす
        if (level >= 20) {
            player.addEffect("night_vision", 999 * TicksPerSecond, { showParticles: false }); // 暗視効果
        }
        if (level >= 25) {
            player.addEffect("fire_resistance", 999 * TicksPerSecond, { showParticles: false }); // 耐火効果
        }
        if (level >= 30) {
            player.addEffect("water_breathing", 999 * TicksPerSecond, { showParticles: false }); // 水中呼吸
        }
    }
}, TicksPerSecond / 5);

// エンティティが死んだとき
world.afterEvents.entityDie.subscribe(event => {
    // debug メッセージ
    world.sendMessage(event.deadEntity.typeId + " was killed.");

    // プレーヤーがモブを殺したのなら
    if (event.damageSource.damagingEntity === undefined ||
        event.damageSource.damagingEntity.typeId !== "minecraft:player") {
        return;
    }
    if (event.deadEntity.typeId === "minecraft:player") {
        return;
    }

    // 10秒再生効果
    const player = event.damageSource.damagingEntity;
    player.addEffect("regeneration", 10 * TicksPerSecond, { showParticles: false });
});

// プレーヤーがスポーンしたとき
world.afterEvents.playerSpawn.subscribe(event => {
    if (event.initialSpawn) {
        // 死亡時にアイテムと経験値を保持する
        event.player.runCommandAsync("gamerule keepInventory true");
    } else {
        const healthComponent = event.player.getComponent(EntityHealthComponent.componentId) as EntityHealthComponent;
        // 死んでリスポーンしたときには、ブースト分も体力を回復する
        system.runTimeout(() => healthComponent.resetToMaxValue(), TicksPerSecond / 4);
    }
});
  1. system.runInterval(callback, tickInterval)tickInterval ごとに callback を実行します。tickInterval は秒数やミリ秒数ではなく、マインクラフトで内部的に使われてる Tick という単位で指定します。環境で変わるのかもしれませんが、現状では1秒=20Tickということになっている(TicksPerSecond という定数の値が 20)なので、TickPerSecond / 5 を指定すると 1/5 秒ごとに実行することになります。

  2. player.addEffect(effectType, ducation, options) で、プレイヤーにステータス効果を付与することができます。どのようなステータス効果があるのかは、wiki などを参照してください。duration は効果が持続する Tick 数です。ここでは無駄に 999 秒掛けてます。最後の引数は名前の通りオプションですが、showParticlesfalse にすることでバフに伴う視覚効果を表示しないようにしています。ずっと出てると目障りなので。また、amplifier はその効果の強さを指定するオプションです(0 から効果ありです)。

  3. world.afterEvents.entityDie.subscribe(callback) は、何らかのエンティティ(プレイヤーや敵 Mob など体力があるやつ(ブロックやアイテムではないもの)は大体エンティティです)が死んだときに発生するイベントをサブスクライブして、イベントが発生したら callback を実行します。この場合、デバッグメッセージを表示したあとに、ダメージを与えた主体(= damagingEntity)が誰なのか、死んだの(= deadEntity)が誰なのかを判断し、プレイヤーが mob を殺したのであれば再生効果を付与しています。これで mob を倒すたびに体力が少し回復します。

  4. world.afterEvents.playerSpawn.subscribe(callback) は、プレイヤーがスポーンもしくはリスポーンしたときに発生するイベントをサブスクライブして、イベントが発生したら callback を実行します。この場合、初めてのスポーン(=ワールドにJOINしたタイミング)の場合、死んでもアイテムと経験値をドロップしない設定にしています。また、リスポーン(=死んでスポーン)の場合、バフはいったん全部切れてしまっているので、1/5秒後に全部のバフが掛かるのを待った上で、1/4秒後にバフ分も含めて体力を全回復するようにしています。healthComponent は厳密にいえば undefined の可能性もあるので as EntityHealthComponent してるのはあまりお行儀良くはないのですが、まあ目を瞑ってください。

普段から TypeScript や JavaScript のコードを書いている皆さんにとってはとても簡単なコードだと思います。これをもとに色々いじってもらえると思い通りの挙動をさせるアドオンが開発できるんじゃないかと思います。

コードが書けたら

$ npx tsc

することで scripts フォルダ内に JavaScript ファイルを生成しましょう。

作ったアドオンを動かす

作ったアドオンを動かすには、マインクラフトを起動し、新しいワールドを作ります。その際に、ゲームモードは サバイバル で、難易度は イージー を選んでください。

そして、左メニューのビヘイビアーパックを選び、今作成したアドオンの有効化ボタンを押しましょう。

これで、アドオンが有効化されたワールドが作成されました。

ログを確認する

うまく動かない場合、何らかのエラーログが出力されてる場合があります。ログは、マインクラフトの設定画面(ワールドの設定ではない)のクリエイターメニューを選ぶと「コンテンツログファイルの有効化」や「コンテンツログGUIの有効化」がチェックできるので、これらをチェックするとファイルや画面上にログが出力されるようになります。

Alt text

Realms で動かす

アドオンを開発した Windows PC 上のマインクラフトで遊ぶ場合や、そのマインクラフトのワールドに接続して遊ぶ場合はそれでいいんですが、子供が遊ぶときにいつも Windows PC でマインクラフトを起動していなければならないのはめんどくさいので、公式で用意されている Realms というサーバーをお金を払って使います。上の画像で「Realms サーバー上の作成」ボタンを押せば良さそうに見えますが、実際にはこのまま実行してもワールドは作られますがアドオンは有効にはなっていません。

どうやら Realms には development_behavior_packs フォルダの中のアドオンはアップロードされないようです。ではどうすればいいかというと、アドオンを パッケージ としてまとめてマインクラフトに食わせることで behavior_packs フォルダの中に入れ、その状態で「Realms サーバー上で作成」ボタンを押せばいいのです。

パッケージにまとめるには development_behavior_packs 内の今開発していたフォルダ(僕の場合は veryeasy)を zip で圧縮します。圧縮してできたファイルの .zip 拡張子を .mcpack に書き換えます。これでパッケージ化は完了です。

このままダブルクリックするとマインクラフトが起動して取り込まれるのですが、development_behavior_packs に開発してたフォルダが存在したままの状態だと、どっちがどっちかわからなくなるので、一度こちらのフォルダ内の開発してたフォルダは別の場所に退避させましょう。その上でダブルクリックするとマインクラフトが起動してアドオンパッケージが取り込まれます。その状態で「ワールド新規作成」で「ビヘイビアーパック」で今取り込んだアドオンを「有効化」し、「Realmsサーバー上で作成」をすればアドオンが有効なワールドがRealms上に誕生します。

まとめ

子供にマインクラフトを好きになってもらい、その延長としてプログラミングに興味を持ってもらうために、マインクラフトのサバイバルを楽にするアドオンの開発をしました。

マインクラフトのアドオン開発は、JavaScript で(もちろん TypeScript でも)できるため、Web エンジニアなら誰でもトライできると思います。

また、先述のとおり、統合版マインクラフトには WebSocket クライアントの機能 もあるため、こちらも合わせて使うと色々面白いことができそう(たとえばゲーム実況の視聴者がゲーム世界に影響を与える、とか)なので、次回があればこちらも深堀りしてみようと思います。

そうそう、肝心の娘の反応ですが、だいぶ食いつきがよく洞窟にも探検にいくようになりました。「こういう効果つけてよ!」って言われて「はいはい」って対応してあげられるの、ソフトウェアエンジニアの特権ですね。エンダードラゴンはまだ倒せてません。

追記
友だちと一緒に無事エンドラは討伐できたようです。
また、ソースコードを GitHub で公開しました。アドオンのパッケージもリリースしているので、導入してみたい方は こちら から .mcpack ファイルをダウンロードしてください。

Aidemy Tech Blog

Discussion

Ryo TakahashiRyo Takahashi

また、先述のとおり、統合版マインクラフトには WebSocket クライアントの機能 もあるため、こちらも合わせて使うと色々面白いことができそう(たとえばゲーム実況の視聴者がゲーム世界に影響を与える、とか)なので、次回があればこちらも深堀りしてみようと思います。

まさにこれ、数ヶ月前に作って遊んでました!
https://github.com/ryo-takahashi/interactive-tiktok-live-for-minecraft

Ryo TakahashiRyo Takahashi

マイコンと組み合わせて、「物理世界でマイコンのボタンを押すとTNTが出現する」(通信はwebsocket)とかやってみても面白そうです