サブティックの話

この記事は[Minecraft Command Advent Calendar 2023](https://qiita.com/advent-calendar/2023/mccmd　)の18日目の記事です。

ここでのコマンドは1.20.2付近を想定してますが、理論としては1.8でも動きます。

ついでにこの記事では基礎的な用語やらの解説はしていません。ご注意を。

## サブティックってなに?
(サブティック タイミングが正式名称な気がしますが長いのでサブティックとさせていただきます)
サブティックとは、**エンティティのTick**の隙間のことで、そこでコマンドを実行することだと思います。たぶん。
~~誰かサブティックの言葉の定義教えてください~~

エンティティのTickというのは、例えば「加速度のぶん移動する」「炎上ダメージを受ける」「敵の方を向く」「鳴き声をあげる」「敵に攻撃する」
などの、コマンドで弄ることのできないマインクラフトのソースコードに書かれた処理のことです。
クリーパーであれば「爆発する」なども含まれますね。

このエンティティのTickは召喚順に処理されます。
例えば、2体の瀕死のクリーパーが同時に爆発すると、先に生まれたクリーパーが爆発し、後に生まれたクリーパーは爆殺され爆発しません。

ここで、**コマンドブロック付きトロッコ**はアクティベーターレールの上にいるときにコマンドを実行しますし、矢やトライデントなどの**投擲物**は敵に攻撃した際、`player_hurt_entity`のadvancementからコマンドを実行できます。**エリアエフェクトクラウド**（以下AEC）も同様です。
サブティックはこれらのコマンドを実行できるエンティティを利用して、エンティティのTickの前後でコマンドを実行するやつです。

## なにができるの?
エンティティのTickの前後でコマンドを実行することができます。

これを使えば、例えばTNTのTickの前後に、「近くのガラスを岩盤に変える」「岩盤をガラスに戻す」コマンドを挟めば、TNTが爆発した際、ガラスがTNTの爆風を耐えたように見えるはずです。

![image4708.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3593091/0f0789f3-0000-5fd3-eddc-38974116295b.png)


この際、TNTやコマンドを実行したエンティティを除き、あらゆるエンティティやプレーヤーは岩盤に替えられたガラスを観測することができません。


## 試しに実装してみる
[BigPapi13](https://github.com/BigPapi13)氏作の[Delta](https://github.com/BigPapi13/Delta)ライブラリのような、プレーヤーの加速を実装してみましょう。

プレーヤーの加速の仕組みを大まかに説明すると、
① クリーパーとサブティック用のエンティティを召喚する
② プレーヤーをクリーパーの近くにtpし、クリエイティブにする
③ クリーパーが爆発する
④ プレーヤーをもとの位置に戻し、ゲームモードも戻す

というものです。クリエイティブにすることでクリーパーのダメージを無くし、爆風による加速だけを受けるのですね。

これを実現するには
```
コマンドを実行するエンティティ
クリーパー
コマンドを実行するエンティティ
```
の順にエンティティを召喚する必要があります。

以下に**コマンドブロック付きトロッコ**、**AEC**、**投擲物**を使用したプレーヤーの加速のコードを記載しますが、すべてゲームモードを変更する処理を省いたものになりますので、クリエイティブで実行してください。
### コマンドブロック付きトロッコ
```:advent_calendar:start
summon command_block_minecart x x x {Command:"execute at @p run tp @p ~ ~1000 ~"}
execute at @p run summon creeper ~ ~1000 ~ {Fuse:4,ignited:true,PersistenceRequired:true}
summon command_block_minecart x x x {Command:"execute at @p run tp @p ~ ~-1000 ~"}
summon command_block_minecart x x x {Command:"kill @e[type=command_block_minecart]"}
```
`x x x` の部分にはアクティブになっているアクティベーターレールの座標を入れてください。

### AEC

コマンドブロック付きトロッコと違うところは、コマンドを実行するのに攻撃を受けるエンティティ必要であるということです。
ここではコウモリを使うので、つまりコウモリと `instant_damage` のAECが同じ位置にあればコマンドが実行されるわけです。
```:advent_calendar:start
#サブティックのコマンド実行用エンティティ召喚
summon area_effect_cloud ~ ~10000 ~ {Duration:1,Age:-1,effects:[{id:"minecraft:instant_damage"}],Tags:["copy_owner"]}
summon bat ~ ~10000 ~ {Tags:["subtick_pre"],NoAI:true,Health:1}
#本体召喚
summon creeper ~ ~11000 ~ {Fuse:0,PersistenceRequired:true}
summon area_effect_cloud ~ ~12000 ~ {Duration:1,Age:-1,effects:[{id:"minecraft:instant_damage"}],Tags:["copy_owner"]}
summon bat ~ ~12000 ~ {Tags:["subtick_post"],NoAI:true,Health:1}

#AECをプレーヤーが放ったことにする
data modify storage : _ set from entity @s UUID
execute as @e[tag=copy_owner] run data modify entity @s Owner set from storage : _
tag @e remove copy_owner
```

<details><summary>subtick_preのタグが付いたエンティティを攻撃したときにadvent_calendar:subtick/preを呼び出す進捗</summary>

```json:advent_calendar\:subtick/pre
{
  "criteria": {
    "requirement": {
      "trigger": "minecraft:player_hurt_entity",
      "conditions": {
        "entity": [
          {
            "condition": "minecraft:entity_properties",
            "entity": "this",
            "predicate": {
              "nbt": "{Tags:[\"subtick_pre\"]}"
            }
          }
        ]
      }
    }
  },
  "rewards": {
    "function": "advent_calendar:subtick/pre"
  }
}
```
</details>

<details><summary>subtick_postのタグが付いたエンティティを攻撃したときにadvent_calendar:subtick/postを呼び出す進捗</summary>

```json:advent_calendar\:subtick/post
{
  "criteria": {
    "requirement": {
      "trigger": "minecraft:player_hurt_entity",
      "conditions": {
        "entity": [
          {
            "condition": "minecraft:entity_properties",
            "entity": "this",
            "predicate": {
              "nbt": "{Tags:[\"subtick_post\"]}"
            }
          }
        ]
      }
    }
  },
  "rewards": {
    "function": "advent_calendar:subtick/post"
  }
}
```

</details>

```:advent_calendar\:subtick/pre
advancement revoke @s only advent_calendar:subtick/pre
tp ~ ~11000 ~
```
```:advent_calendar\:subtick/post
advancement revoke @s only advent_calendar:subtick/post
tp ~ ~-11000 ~
```


### 投擲物

AECと性質が似ているため、`advent_calendar:start` 以外はAECのコードと同じです。
こちらは、コウモリのnブロック上に`Motion`値が`[0.0,-n,0.0]`の投擲物があればコマンドが実行されます。
```:advent_calendar:start
#サブティックのコマンド実行用エンティティ召喚
summon arrow ~ ~10005 ~ {Motion:[0.0,-10.0,0.0],Tags:["copy_owner"]}
summon bat ~ ~10000 ~ {Tags:["subtick_pre"],NoAI:true,Health:1}
#本体召喚
summon creeper ~ ~11000 ~ {Fuse:0,PersistenceRequired:true}
summon arrow ~ ~12005 ~ {Motion:[0.0,-10.0,0.0],Tags:["copy_owner"]}
summon bat ~ ~12000 ~ {Tags:["subtick_post"],NoAI:true,Health:1}

#矢をプレーヤーが放ったことにする
data modify storage : _ set from entity @s UUID
execute as @e[tag=copy_owner] run data modify entity @s Owner set from storage : _
tag @e remove copy_owner
```

## AECは同時実行ができない
ここで忘れてはいけないのが、AECは効果範囲内にいるすべてのエンティティに効果を付与するということです。
先ほどのコマンドでは同じTickに何度実行しても ` ~ ~10000 ~ ` の場所は変わりません。そのため、同時に何回も実行すると、同じ位置にAECとコウモリがたまることになります。

投擲物で2回同時に実行した場合、実行されたコマンドにその他のエンティティを処理順に並べると以下のようになります。
```
(プレーヤーのTick)
(矢のTick)　コウモリに攻撃
tp @s ~ ~11000 ~
(コウモリのTick)　死亡処理
(クリーパーのTick)　爆発
(矢のTick)　コウモリに攻撃
tp @s ~ ~-11000 ~
(コウモリのTick)　死亡処理

(矢のTick)　コウモリに攻撃
tp @s ~ ~11000 ~
(コウモリのTick)　死亡処理
(クリーパーのTick)　爆発
(矢のTick)　コウモリに攻撃
tp @s ~ ~-11000 ~
(コウモリのTick)　死亡処理
```
対して、AECの場合だと1回目のAECで2回目のコウモリも攻撃してしまうため、ぶっ壊れます。
```
(プレーヤーのTick)
(AECのTick)　コウモリに攻撃
tp @s ~ ~11000 ~
tp @s ~ ~11000 ~
(コウモリのTick)　死亡処理
(クリーパーのTick)　爆発
(AECのTick)　コウモリに攻撃
tp @s ~ ~-11000 ~
tp @s ~ ~-11000 ~
(コウモリのTick)　死亡処理

(AECのTick)
(コウモリのTick)　死亡処理
(クリーパーのTick)　爆発
(AECのTick)
(コウモリのTick)　死亡処理
```
そのため、多少負荷に違いは出るものの、基本的には投擲物を使用するのをおすすめします。

# 永続サブティック
これまでのサブティック用のエンティティはすべて、一度コマンドを実行したら消えていましたね。

AECは`Duration`の値が低く設定されていたのですぐに消えましたし、投擲物は矢を使用していたため消えていました。コマンドブロック付きトロッコも最後にすべてを消す処理を挟んでいたため消えていました。

では、どうしたらサブティック用のエンティティが消えずに済むのでしょうか。

AECは`Duration`の値を高く設定し、投擲物はトライデントを使用しましょう。コマンドブロック付きトロッコも最後にすべてを消す処理を挟まなければ消えません。
このようにして複数回コマンドが実行させるサブティックを永続サブティックと呼びます。
~~なぜなら私がそう呼んだからです~~

## なにができるの?
エンティティのTickの前後でコマンドを実行することができます。
しかも永続的に、1Tickの隙間もありません。

例えばハスクのTickの前後でそれぞれ、「足元に石を置く」「足元の石を消す」コマンドを挟んだ場合、ハスクは石に乗っていますが、ほかのエンティティから見ると宙に浮いているように見えます。
ほかのエンティティのTickが処理されるときには石は消えているわけですから、触ることもできません。
▼動画
![あどかれ-Trim.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3593091/c0cebf6e-dace-ae33-6681-a0e1ec2dd97d.gif)


## 作ってみましょう
宙に浮いている (ように見える) ハスクを作ってみましょう。

ここではトライデントを使います。トライデントなら、コウモリに刺さっても消えませんし、`DealtDamage`を0にすれば何度でも刺すことができます。
永続サブティックとて、作るのはそう難しくはありません。
ただ、プレーヤ加速のコマンドと大きく違う点が一つあります。

それは、サブティック用のエンティティに挟まれているエンティティを特定する必要があるということです。
プレーヤー加速のコマンドでは、挟まれているエンティティはクリーパーで、位置が常に
` ~ ~10000 ~ ` であったため、プレーヤーをそこに移動させるだけですみましたが、今度は自由に動き回るエンティティが相手です。

そのため、紐づけが必要になります。
この紐づけは、まずトライデントとコウモリに、次にコウモリとハスクに対して行います。

トライデントとコウモリを紐づけるというのは、スコア紐づけではなく、トライデントとそのトライデントに当たるコウモリの組を保つことです。

**投擲物は同時に複数のエンティティに当たった時、召喚順の一番最初のエンティティだけに当たる**という仕様があります。
コウモリがすべて同じ位置にいる場合、召喚順の一番最初のトライデントは召喚順の一番最初のコウモリに当たり、コウモリはダメージを受けて死にます。
二番目のトライデントは、一番目のコウモリがついさっき死んだため、二番目のコウモリに当たります。
三番目のトライデントも、一番目と二番目のコウモリがさっき死んだため、三番目のコウモリに当たります。
…というように、トライデントに当たったコウモリが片っ端から死んでゆけば、n番目のトライデントがn番目のコウモリに当たるようになっています。
しかしこのままでは、コウモリは当たったそばから死んでしまうため次のTickに繋がりませんし、コウモリとハスクの紐づけも使えません。

そこで、コウモリの体力を、トライデントのダメージに耐えられるくらいまで引き上げ、コウモリと紐づいているハスクを特定してから`kill`コマンドで殺します。殺した後でハスクと紐づいているコウモリを召喚すれば、次のTickにも繋がりますね。
トライデントに当たるのは常に召喚順の一番最初のコウモリなので、`limit=1`で取得することができます。

以下がコードになります。
```:advent_calendar:start
#サブティックのコマンド実行用エンティティ召喚
summon trident 0 10002 0 {Motion:[0.0,-2.0,0.0],Tags:["init","respeed"]}
summon bat 0 10000 0 {PersistenceRequired:true,AbsorptionAmount:1024f,NoAI:true,Tags:["subtick_pre","hitter","noid","init"],DeathTime:19s}
#本体召喚
summon husk ~ ~ ~ {Tags:["subtick_entity"]}
summon trident 0 10002 0 {Motion:[0.0,-2.0,0.0],Tags:["init","respeed"]}
summon bat 0 10000 0 {PersistenceRequired:true,AbsorptionAmount:1024f,NoAI:true,Tags:["subtick_post","hitter","noid","init"],DeathTime:19s}

#トライデントをプレーヤーが放ったことにする
data modify storage : _ set from entity @p UUID
execute as @e[tag=init,tag=respeed] run data modify entity @s Owner set from storage : _
#スコア紐づけ
function advent_calendar:defid
scoreboard players operation $ id = @e[tag=subtick_entity,limit=1] id
scoreboard players operation @e[tag=init] owner.id = $ id
tag @e remove init
tag @e remove subtick_entity
```
```:advent_calendar:defid
#エンティティにスコアをつける
execute as @e unless score @s id matches -2147483648..2147483647 store result score @s[tag=!noid] id run scoreboard players add $world id 1
```

<details><summary>subtick_preのタグが付いたエンティティを攻撃したときにadvent_calendar:subtick/preを呼び出す進捗</summary>

```json:advent_calendar:subtick/pre
{
  "criteria": {
    "requirement": {
      "trigger": "minecraft:player_hurt_entity",
      "conditions": {
        "entity": [
          {
            "condition": "minecraft:entity_properties",
            "entity": "this",
            "predicate": {
              "nbt": "{Tags:[\"subtick_pre\"]}"
            }
          }
        ]
      }
    }
  },
  "rewards": {
    "function": "advent_calendar:subtick/pre"
  }
}
```
</details>
<details><summary>subtick_postのタグが付いたエンティティを攻撃したときにadvent_calendar:subtick/postを呼び出す進捗</summary>

```json:advent_calendar:subtick/post
{
  "criteria": {
    "requirement": {
      "trigger": "minecraft:player_hurt_entity",
      "conditions": {
        "entity": [
          {
            "condition": "minecraft:entity_properties",
            "entity": "this",
            "predicate": {
              "nbt": "{Tags:[\"subtick_post\"]}"
            }
          }
        ]
      }
    }
  },
  "rewards": {
    "function": "advent_calendar:subtick/post"
  }
}
```
</details>

```:advent_calendar:subtick/pre
advancement revoke @s only advent_calendar:subtick/pre

#スコア紐づけをもとに本体を特定
tag @e remove owner
scoreboard players operation $ owner.id = @e[tag=hitter,limit=1] owner.id
execute as @e if score @s id = $ owner.id run tag @s add owner
#本体がいなかったら消える
execute unless entity @e[tag=owner] run function advent_calendar:removeall
execute unless entity @e[tag=owner] run return -1

execute as @e[tag=owner] at @s run function advent_calendar:pre_command

#再召喚&紐づけ
summon bat 0 10000 0 {PersistenceRequired:true,AbsorptionAmount:1024f,NoAI:true,Tags:["subtick_pre","hitter","noid","init"],DeathTime:19s}
scoreboard players operation @e[tag=init,limit=1] owner.id = @e[tag=owner,limit=1] id
tp @e[tag=hitter,limit=1] 0 0 0
kill @e[tag=hitter,limit=1]
tag @e remove init
```
```:advent_calendar:subtick/post
advancement revoke @s only advent_calendar:subtick/post

#本体がいなかったら消える
execute unless entity @e[tag=owner] run function advent_calendar:removeall
execute unless entity @e[tag=owner] run return -1

execute as @e[tag=owner] at @s run function advent_calendar:post_command

#再召喚&紐づけ
summon bat 0 10000 0 {PersistenceRequired:true,AbsorptionAmount:1024f,NoAI:true,Tags:["subtick_post","hitter","noid","init"],DeathTime:19s}
scoreboard players operation @e[tag=init,limit=1] owner.id = @e[tag=owner,limit=1] id
tp @e[tag=hitter,limit=1] 0 0 0
kill @e[tag=hitter,limit=1]
tag @e remove init
```
```:advent_calendar:removeall
#owner.idが一致しているエンティティを消す
scoreboard players operation $ owner.id = @e[tag=hitter,limit=1] owner.id
execute as @e if score @s owner.id = $ owner.id run kill @s
```
```:advent_calendar:tick
# 毎Tick実行されるコマンドです

#トライデントがまたコウモリに当たるようにする
tp @e[tag=respeed] 0 10001.2 0
execute as @e[tag=respeed] run data merge entity @s {Motion:[0.0,-0.01,0.0],DealtDamage:0b}
```

これでハスクのTickの前後でコマンドが実行する準備が整いました。
実際に実行するコマンドの内容は、 `advent_calendar:pre_command` と `advent_calendar:post_command` に書きます。
```:advent_calendar:pre_command
fill ~-1 ~-1 ~-1 ~1 ~-1 ~1 stone
```
```:advent_calendar:post_command
fill ~-2 ~-1 ~-2 ~2 ~-1 ~2 air replace stone
```
完成です。やったね。

## 既に存在するエンティティを取り込む
永続サブティックを機能させるには、召喚順で見たときにエンティティがサブティック用のエンティティに挟まれている必要があります。

既に存在するエンティティの後ろにエンティティを召喚することはできません。
そこで、皆さんご存じかご存じでないか知りませんが、とある仕様が役に立ちます。
それは「**エンティティはディメンション間を移動すると召喚順の一番最後に移動する**」というものです。
つまり、
```
summon husk ~ ~ ~ {Tags:["a"]}
summon skeleton ~ ~ ~ {Tags:["a"]}
say @e[tag=a]
#ハスク, スケルトン 
execute as @e[type=husk] in minecraft:the_nether run tp 0 0 0
say @e[tag=a]
#スケルトン, ハスク
```
こうなるということです。

これを利用すれば、既に存在するエンティティをサブティックに取り込むことができます。

追記：エンティティはディメンション間を移動した際、移動する前のエンティティを消し、移動後のディメンションに同じエンティティを召喚する、という処理が行われます。
そのため、ディメンション間の移動を挟んだ直後だと、`@s`が移動する前の、消される直前のエンティティを指すことになります。（修正前のコードは誤りです すいません）
```:advent_calendar:start
#サブティックのコマンド実行用エンティティ召喚
summon trident 0 10002 0 {Motion:[0.0,-2.0,0.0],Tags:["init","respeed"]}
summon bat 0 10000 0 {PersistenceRequired:true,AbsorptionAmount:1024f,NoAI:true,Tags:["subtick_pre","hitter","noid","init"],DeathTime:19s}
#取り込まれる
tag @s add this
execute in the_nether run tp 0 0 0
tp @e[tag=this] ~ ~ ~
tag @e remove this
summon trident 0 10002 0 {Motion:[0.0,-2.0,0.0],Tags:["init","respeed"]}
summon bat 0 10000 0 {PersistenceRequired:true,AbsorptionAmount:1024f,NoAI:true,Tags:["subtick_post","hitter","noid","init"],DeathTime:19s}

#トライデントをプレーヤーが放ったことにする
data modify storage : _ set from entity @p UUID
execute as @e[tag=init,tag=respeed] run data modify entity @s Owner set from storage : _
#スコア紐づけ
function advent_calendar:defid
scoreboard players operation $ id = @s id
scoreboard players operation @e[tag=init] owner.id = $ id
tag @e remove init
```
なお、これを使ってプレーヤーを取り込んだとしても、一切の効果が得られません。
多分これの効果があるのがサーバー側だけなのだと思われます。

## おわり
あんまり使う機会はないと思いますが、ぜひ可愛がってやってください。