【Minecraft BE Addon講座#15】スクリプトの活用例～キルカウントを作ってみよう～

本ページは **アドオン講座#15、「スクリプト編、スクリプトの活用例」** です。
目次や、この講座については[#1の記事](https://qiita.com/pencilya/items/7b7e68265151f42a51ac)をご覧ください。

# スクリプト編で理解してほしいこと！

:::note alert
2022年4月19日のアップデート、[Minecraft 1.18.30(Bedrock)](https://feedback.minecraft.net/hc/en-us/articles/5520890863245)（ベータ版では2022年1月27日の[Minecraft Beta 1.18.20.21](https://feedback.minecraft.net/hc/en-us/articles/4423151445901)）をもって **ScriptingAPIは削除されました。**
本Addon講座#14、#15で解説されているScriptingAPIは、これ以降のバージョンで使用できません。
スクリプトの実行は、代わりにGameTestFrameworkが担っていくようです。
:::

:::note warn
ScriptingAPIは執筆現在、Minecraft Bedrock Edition for Windows10(通称：win10版)でしか動作しません。（でした）
条件を満たさない場合、iOS版、Android版などでは実行できないのでご注意ください。
この制限について、またこの制限の対策について、詳しくは#14｢サーバーとクライアントの違い｣をご覧ください。
:::

スクリプト編はこのアドオン講座の他の応用編と異なり、`.json`ファイルではなく、Javascriptを使用することもあり、**レベルとしては応用編の中でも発展寄り**になります。
しかし、Javascriptに少しでも触れたことがある方にとってはスクリプト編の方が簡単に感じるかもしれません。

スクリプト編は2ページからなり、**前半#14はスクリプトの書き方(記法)と仕組みの理解**について扱い、**後半#15ではどのように使うのか実際にアドオンを作って**みます。

このスクリプト編では、Javascriptを使うことで、**`.json`などの限られた機能だけではできなかった検知や、それを利用したシステムの製作ができるようになる**ことを目的としています。

* (前提#3~#4)基礎編
* (前提#8)ファンクション編復習、コマンドの実行主の理解
* (#14)`ScriptingAPI`とは？
* (#14)サーバーとクライアントの違い
* (#15)[実践]キルカウントを作ってみよう

# [実践]キルカウントを作ってみよう
Java版にはスコアボードの機能としてキルカウントがありますが、BEには`dummy`型しかありません。
再現するには「武器を1確(一回当たったら倒れる)にしてスコアやらタグやらでmobやらを経由して`@p`」という誤検知しやすく制限の多い方法を使うしかないのです。

しかし、`ScriptingAPI`では**攻撃したときの検知、エンティティが倒されたときの検知**ができ、**攻撃者と被害者のプレイヤーの両方の情報(コンポネ)が取得できる**ため、**スクリプト完結で誤判定のないキルカウントを作ることができます**。

一応解説のためのアドオン講座なので、解説を挟みながら作っていきます。

### ①フォルダ・ファイルの用意
まずはフォルダ・ファイルの用意です。
フォルダは`behavior/scripts/server`のように用意します。
これはバニラにもあるフォルダなので分かると思います。
`behavior/scripts/client`は使用しないので、コピーしてきた場合は消しておきましょう。

ファイルは**名前自由で拡張子`.js`**です。
今回は`server.js`としましょう。

### ②お膳立て(毎回コピペするやつ)
#### ②-1.スクリプト登録
エディタで`server.js`を開きましょう。
開いたらまず1行目に

```behavior/scripts/server/server.js
const system = server.registerSystem(0,0);
```

を書きます。
これは、マイクラ側に **「このファイルはスクリプトのサーバーのファイルです」と登録する文**[^1]になります。

[^1]: 引数の`(0, 0)`はスクリプトエンジンのバージョンらしいです。どの例を見ても`(0, 0)`なので、あまり気にせず使いましょう。

ちなみに、サーバーではなく、クライアントの場合は、

```behavior/scripts/client/client.js
const clientSystem = client.registerSystem(0,0);
```

です。
（変数、定数ってクライアントとサーバーで連動するんでしょうか…？有識者の方いましたら教えてください…！）

#### ②-2.組み込み関数
次に、大元の関数(**組み込み関数**)を書きます。

```behavior/scripts/server/server.js
//ワールド始動時実行
system.initialize = function() {
  //イベントの登録、コンポーネントの登録、クエリの登録、イベントの検知登録
};

//更新されるたび(=tickごと？)実行
system.update = function() {
  //更新
};

//ワールド終了時？実行
system.shutdown = function() {
  //片付け
};
```

この3つのどれかを起点に実行は始まります。
`update`と`shutdown`は使ったことがないので、どういう用途なのか、どういう挙動なのかよく分かりません。。。
`update`はワールドに入った後で、エンティティを出したり動かしたりするなど、イベントの検知以外のタイミングで実行するときに使うようです。
`shutdown`に関しては例がありませんでした。

今回は「エンティティが倒された」というイベントの検知で実行をするので、`initialize`のみ使用します。

#### ②-3.イベントの検知登録(listenForEvent)
```behavior/scripts/server/server.js
system.initialize = function() {
  //エンティティが攻撃を受けたら関数「system.entity_hurt」を実行する
  this.listenForEvent ("minecraft:entity_hurt", (eventData) => this.entity_hurt(eventData));
  //エンティティが倒されたら関数「system.entity_death」を実行する
  this.listenForEvent("minecraft:entity_death", (eventData) => this.entity_death(eventData));
};
```

このように書きます。
エンティティが倒されたときのイベント`"minecraft:entity_death"`をlisten(検知)しています。

参考に、攻撃を受けたときの`listenForEvent`も書いてみました。
検知したときに実行する関数名は自由です[^2]。
今回は`"minecraft:entity_death"`の方しか使わないので、`"minecraft:entity_hurt"`の方は消しておきましょう。

[^2]: `Javascript`的には関数名は`entityDeath`の方がしっくりくるかもしれないですね。今回はイベント名にそろえています。

### ③実行する処理
```behavior/scripts/server/server.js
//ここまでのまとめ
const system = server.registerSystem(0,0);
system.initialize = function() {
  this.listenForEvent("minecraft:entity_death", (eventData) => this.entity_death(eventData));
};
system.entity_death = function(eventData) {
  //ここにエンティティが倒されたときに実行する処理を書いていくよ
}
```
#### ③-1.コンポーネントのデータを得る
`ScriptingAPI`ではエンティティのコンポネの値を取得することができます。
[コンポーネントについて#5](https://qiita.com/pencilya/items/89d0fdf6d575baea79c2)でプレイヤーの名前表示を出したり消したりしましたが、そのときに使った`"minecraft:nameable"`というコンポーネントを使います。

```behavior/scripts/server/server.js
let hurter = this.getComponent(eventData.data.entity, "minecraft:nameable").data.name;
```

`"minecraft:nameable"`の`name`には名前が入っています[^3]。
これで変数`hurter`に「eventDataのentity」の名前が代入されました。

[^3]: 参考：https://bedrock.dev/docs/stable/Scripting#minecraft%3Anameable

「eventDataのentity(`eventData.data.entity`)」はイベントを起こしたエンティティ、つまり今回は倒されたエンティティを指します。
じゃあ倒したエンティティの名前はどうやって取得するかというと、「eventDataの中のkiller(`eventData.data.killer`)」から取得します[^4]。

[^4]: 参考：https://bedrock.dev/docs/stable/Scripting#minecraft%3Aentity_death

```behavior/scripts/server/server.js
let hurter = this.getComponent(eventData.data.entity, "minecraft:nameable").data.name;
let killer = this.getComponent(eventData.data.killer, "minecraft:nameable").data.name;
```

これで変数`killer`に「eventDataのkiller」の名前、つまり倒したエンティティの名前が代入されました。

#### ③-2.コマンドの実行
次はスクリプト内でのコマンドの実行です。
コマンドの実行は`"minecraft:execute_command"`というイベントで行われます[^5]。
①イベントデータを作り、②イベントの内容を設定(=実行するコマンドを記述)し、③イベントを実行する、という3stepを踏みます。

[^5]: 参考：https://bedrock.dev/docs/stable/Scripting#minecraft%3Aexecute_command

```behavior/scripts/server/server.js
let execute_command = this.createEventData("minecraft:execute_command");
execute_command.data.command = `/scoreboard players add @a[name="${hurter}"] deathcount 1`;
this.broadcastEvent("minecraft:execute_command", execute_command);
```

それぞれの行が3stepに対応しています。
複数コマンドを実行する場合は②③を繰り返します。

```behavior/scripts/server/server.js
let execute_command = this.createEventData("minecraft:execute_command");
execute_command.data.command = `/scoreboard players add @a[name="${hurter}"] deathcount 1`;
this.broadcastEvent("minecraft:execute_command", execute_command);
execute_command.data.command = `/scoreboard players add @a[name="${killer}"] killcount 1`;
this.broadcastEvent("minecraft:execute_command", execute_command);
```

①はコマンドの文を代入できるようにイベントの型を作っている、っていう感覚ですかね。
作った型は`execute_command.data.command`に再代入すれば使いまわせるって感覚です。

もちろん作ったイベントデータを代入する変数名は`execute_command`ではなく`executeCommand`でも`commandEvent`でもなんでも大丈夫です。
ただしそのイベントデータを使うところでは最初に代入した変数名と同じ名前にしなければならないので注意。
プログラムの変数やマイクラのスコアボードと考え方は同じです。

:::note warn
＜注意＞
<strong>コマンドに使うセレクタの名前指定は<code>@a[name="名前"]</code>という方法で行ってください</strong>。
<code>/scoreboard players add 名前 deathcount 1</code>や<code>/scoreboard players add @p[name="名前"] deathcount 1</code>ではできません。
理由はよく分からないのですが、エンティティが倒された瞬間に実行するコマンドなので、<code>名前</code>や<code>@p[name="名前"]</code>では、倒されたエンティティが見つからないようです(これに気付くのに2か月くらいかかりました)。
:::

:::note info
＜追加情報＞
<code>ScriptingAPI</code>は座標<code>(x,y,z)=(0,0,0)</code>を中心に実行されるようです。
<code>@p</code>や相対座標(チルダ<code>~~~</code>)を使う場合、この影響を受けることになります。
:::

### ④まとめ
```behavior/scripts/server/server.js
//スクリプトの登録
const system = server.registerSystem(0,0);

//ワールドを開いたとき実行
system.initialize = function() {
  //イベント検知の登録
  this.listenForEvent("minecraft:entity_death", (eventData) => this.entity_death(eventData));
};

//エンティティが倒されたとき実行
system.entity_death = function(eventData) {
  //名前の取得
  let hurter = this.getComponent(eventData.data.entity, "minecraft:nameable").data.name;
  let killer = this.getComponent(eventData.data.killer, "minecraft:nameable").data.name;
  //コマンドの実行
  let execute_command = this.createEventData("minecraft:execute_command");
  execute_command.data.command = `/scoreboard players add @a[name="${hurter}"] deathcount 1`;
  this.broadcastEvent("minecraft:execute_command", execute_command);
  execute_command.data.command = `/scoreboard players add @a[name="${killer}"] killcount 1`;
  this.broadcastEvent("minecraft:execute_command", execute_command);
}
```

ワールドに入ったら、
`/scoreboard objectives add killcount dummy`
`/scoreboard objectives add deathcount dummy`
を実行します。
`/scoreboard objectives setdisplay sidebar [表示したいobject名]`
も実行して、倒したり、倒されたりしてみてください。
倒せば`killcount`が増え、倒されれば`deathcount`が増えるのが分かると思います。

これで一応完成です！

### ⑤さらに極めてみる
ついでにキルデス通知も作ってみましょう。

```behavior/scripts/server/server.js
const system = server.registerSystem(0,0);

system.initialize = function() {
  this.listenForEvent("minecraft:entity_death", (eventData) => this.entity_death(eventData));
};

system.entity_death = function(eventData) {
  let hurter = this.getComponent(eventData.data.entity, "minecraft:nameable").data.name;
  let execute_command = this.createEventData("minecraft:execute_command");
  execute_command.data.command = `/scoreboard players add @a[name="${hurter}"] deathcount 1`;
  this.broadcastEvent("minecraft:execute_command", execute_command);

  //倒したエンティティがいるなら
  if(eventData.data.killer){
    let killer = this.getComponent(eventData.data.killer, "minecraft:nameable").data.name;
    execute_command.data.command = `/scoreboard players add @a[name="${killer}"] killcount 1`;
    this.broadcastEvent("minecraft:execute_command", execute_command);
    
    execute_command.data.command = `/execute @a[name="${hurter}"] ~~~ playsound note.didgeridoo @s`;
    this.broadcastEvent("minecraft:execute_command", execute_command);
    execute_command.data.command = `/execute @a[name="${killer}"] ~~~ playsound note.harp @s`;
    this.broadcastEvent("minecraft:execute_command", execute_command);
    execute_command.data.command = `/titleraw @a[name="${hurter}"] actionbar {"rawtext":[{"text":"§l§b${killer}§r§lに倒された"}]}`;
    this.broadcastEvent("minecraft:execute_command", execute_command);
    execute_command.data.command = `/titleraw @a[name="${killer}"] actionbar {"rawtext":[{"text":"§l§c${hurter}§r§lを倒した"}]}`;
    this.broadcastEvent("minecraft:execute_command", execute_command);
  }
}
```

`if`を使うことで、溶岩などでHPがゼロになったときに、(「名無しに倒された」という)通知が出ないようになっています。
アクションバーに倒した/倒されたプレイヤー名が出るほか、倒した/倒されたときにSE(効果音)がなるようにしました。

# スクリプト編終了！
応用編の中でも難易度の高いスクリプト編でしたが、どうでしたか？
難しいと言いつつ2回分で全内容が終了してしまいました。

ここまで複数の応用編で、学びながら実際にアドオンを作っていくセクションがありましたが、それらは**私が今までに作ってきたアドオンを簡易化したもの**です。

後半の回(今回#15)で作成した機能に、チームの概念を導入して、FF表示やチームメンバーのキル/デス通知、キルレの計算などを実装したものが、配布している「ExtraDeathMessage」というアドオンになります。

http://pencilya.blog.jp/articles/20200530.html

さらにこれを発展させてPvPに特化したものが「JustBattlePvP」というアドオンになり、

http://pencilya.blog.jp/articles/20201206.html

これをベースに雪合戦に特化したものが

http://pencilya.blog.jp/articles/20210212.html

爆弾設置/防衛ゲームに特化したものが

https://twitter.com/pencilya_/status/1433832266451480590?s=20

(未配布)

**これらの企画の基礎には、アドオンの力があるのです！**
そしてこのアドオン講座で解説したことは、**実際に配布できるアドオンの技術につながっています！**

このアドオン講座で得た知識で、よりおもしろいアドオンが生まれたらいいな！と思っています。

最後まで読んでくれたみなさんが、作りたいものが作れますように！
お疲れ様でした！！

↓目次#1へ↓

https://qiita.com/pencilya/items/7b7e68265151f42a51ac