Minecraft 1.16.1 でのエンダードラゴンの挙動をまとめた

Minecraft 1.16.1 でのエンダードラゴンの挙動の詳細を調査した。

最近Minecraft 1.16.1のRTAにハマっている。エンダードラゴン戦をすばやく終わらせることはタイム短縮に大きくつながる行動だが、エンダードラゴンの挙動は思った以上に複雑であり、これらの状態や条件を詳しく知る必要が出てきた。今回は、Minecraftの内部実装を確認し、エンダードラゴンが実際にどのように動き、何に反応して行動を変えているのか?を調査した。

前提

コードリーディングにおいて、Fabricプロジェクトの yarn 1.16.1+build.21 を使用してマップ済み(≒難読化解除済み)のコードを生成してもらい、それを元に情報を調査した。

また、一部ステート遷移の確認には、より新しいバージョンである yarn 1.20+build.1 を使用して生成したコードを使用した。これについては、コード上に影響のある変化がないことを確認した上で調査を進めている。

動的な確認を行うために 1.16.1 向けのMODを作成している。その際のソースコードなどは下記リンクから確認できる。

なにか間違いがありそうであればXのDMかコメント欄まで。

用語集

広く使われている/使われていないに依らず、この記事内で使用されている特殊な用語の一覧。

Perch

(X: 0, Z: 0) にある最も高い位置の任意のブロックを指す。通常、エンドの出口ポータルの中心にある柱の頂点がこれに該当する。*1

エンドの出口ポータル。通常、中心の柱の頂点が (X: 0, Z: 0) において最も高いブロックとなる。

パスノード

エンダードラゴンの経路として設定される、24個の座標を指す。平面図に描画すると以下のように規則的に点在している。

高度については、基本的には同じ (X, Z) 内で最も高い場所に位置するブロック + (5 または 10) が採用される。特に、(X: 40, Z: 0) の円周にあるパスノードのうちいくつかは黒曜石の柱の上空に位置することがある。

各パスノードの位置。(X: 0, Z: 0) の位置に岩盤の柱(Perch)が存在する。

フェーズ

エンダードラゴンの挙動を区切るための状態や段階を表したもの。エンダードラゴンの挙動は内部的には11個のフェーズが存在しており、それぞれ別々のクラスで実装されている。今回は、主にこれらのクラスを読み解くことでエンダードラゴンの挙動の詳細を調査した。

ステート遷移図

まず、自分自身が理解できるようになるためにステート遷移図を書いた。書いたのだが、あまりにも見辛い図ができてしまったため参考程度に見ていただくとよいと思う。

別タブで開く

stateDiagram-v2
    [*] --> Hover : エンティティ生成時
    state 生きてる {
        state after_holding_pattern <<choice>>
        state after_sitting_scanning <<choice>>
        state after_sitting_flaming <<choice>>
        Hover --> HoldingPattern : エンティティ生成直後
        HoldingPattern --> after_holding_pattern : 目的地から遠すぎ/近すぎるとき…
        after_holding_pattern --> LandingApproach : 1 / (エンドクリスタルの数 + 3) の確率で
        after_holding_pattern --> StrafePlayer : 1 / (プレイヤーとPerchとの距離) の確率で\nまたは、1 / (エンドクリスタルの数 + 2) の確率で\nまたは、エンドクリスタルを破壊したとき
        StrafePlayer --> HoldingPattern : プレイヤーがいないとき
        LandingApproach --> Landing : 目的地から遠すぎ/近すぎる場合
        Landing --> SittingScanning : Perchの直上に来たら
        SittingScanning --> after_sitting_scanning : 近くにプレイヤーがいればその方を向く
        after_sitting_scanning --> SittingAttacking : 近くにプレイヤーがいてすこし経過している場合に
        after_sitting_scanning --> Takeoff : しばらく経っても周りに誰もいない場合に
        after_sitting_scanning --> ChargingPlayer : しばらく経過した時、そこそこの範囲にプレイヤーがいれば
        SittingAttacking --> SittingFlaming : 少ししたら
        SittingFlaming --> after_sitting_flaming : しばらくしてから…
        after_sitting_flaming --> SittingScanning : 4回未満の場合に
        after_sitting_flaming --> Takeoff : 4回以上の場合に
        Takeoff --> HoldingPattern : パス更新後、Perchに近くなったら
        ChargingPlayer --> HoldingPattern : 少ししたら
    }
    生きてる --> Dying : HPが0になったら
    Dying --> [*] : 死亡演出が終わったら

また、これを作る前に捉えられる情報を殆ど記載しているバージョンのステート遷移図を作成した。これをもとに情報を落としたバージョンが上記の図。もし、情報を確認したかったり、いくらでも拡大できるような環境なのであれば以下を見ても良いかも?

ここから確認できる

別タブで開く

stateDiagram-v2
    [*] --> Hover : エンティティ生成時に PhaseManager でセット
    state 生きてる {
        state after_holding_pattern <<choice>>
        state after_sitting_scanning <<choice>>
        state after_sitting_flaming <<choice>>
        Hover --> HoldingPattern : エンティティ生成直後の処理内部の EnderDragonEntity#createDragon() でセット
        HoldingPattern --> after_holding_pattern : 目的地から遠い(150より遠い)または近い(10より近い)とき…
        after_holding_pattern --> LandingApproach : 1 / (エンドクリスタルの数 + 3) の確率でセット
        after_holding_pattern --> StrafePlayer : 1 / int((プレイヤーとerchの距離)の2乗を512で割った数 + 2.0) の確率で\nまたは 1 / (エンドクリスタルの数 + 2) の確率で\nまたは、エンドクリスタルを破壊したとき
        StrafePlayer --> HoldingPattern : 距離が64より近ければ、現在のpathを移動を完了してから攻撃して、終了後にセット\n-> 目的地から遠い(150より遠い) または 近い(10より近い) 場合はパスの更新が発生\n-> 5tick以上プレイヤーを見たら火の玉で攻撃する\n\nまたはプレイヤーがいなければセット
        LandingApproach --> Landing : 目的地から遠い(150より遠い) または 近い(10より近い) 場合にパスを更新、パスの移動が完了した時にセット
        Landing --> SittingScanning : ターゲット(Perch)までの距離が1未満の時にセット
        SittingScanning --> after_sitting_scanning : 距離20までにプレイヤーがいれば一番近いプレイヤーの方を向く
        after_sitting_scanning --> SittingAttacking : 距離20までにプレイヤーがいて25tick以上経過している場合にセット
        after_sitting_scanning --> Takeoff : 100tick経過後、誰もいない場合にセット
        after_sitting_scanning --> ChargingPlayer : 100tick経過後、距離20より離れてて距離150までにプレイヤーがいればセット そのままプレイヤーに向かってくる
        SittingAttacking --> SittingFlaming : 40tick経過後にセット
        SittingFlaming --> after_sitting_flaming : 200tick経過後…
        after_sitting_flaming --> SittingScanning : 連続回数が4回未満の場合にセット
        after_sitting_flaming --> Takeoff : 連続回数が4回以上の場合にセット
        Takeoff --> HoldingPattern : パスを更新して移動、Perchから距離10以内になったらセット
        ChargingPlayer --> HoldingPattern : 最長10tickの間、プレイヤーに突っ込んでくる\nプレイヤーから遠い(150より遠い) または 近い(10より近い)場合はtickが短くなる
    }
    生きてる --> Dying : HPが0になった時にセット (内部的にはDYING中はHPが1になる)
    Dying --> [*] : Perchから10より近い または 150より遠い時にセット\n(HPも0になってエンティティが消滅する)

RTA走者向け

RTA走者向けには以下で充分だと思う。スポーンしてからベッド爆破で倒すまで。

別タブで開く

stateDiagram-v2
    [*] --> HoldingPattern : エンティティ生成直後
    HoldingPattern --> after_holding_pattern : 目的地から遠い(150より遠い)または近い(10より近い)とき…
    after_holding_pattern --> LandingApproach : 1 / (エンドクリスタルの数 + 3) の確率で
    after_holding_pattern --> StrafePlayer : 1 / int((プレイヤーとPerchの距離)の2乗を512で割った数 + 2.0) の確率で\nまたは 1 / (エンドクリスタルの数 + 2) の確率で\nまたは、エンドクリスタルを破壊したとき
    StrafePlayer --> HoldingPattern : プレイヤーがいないとき\nまたは 距離が64より近ければ、現在のpathを移動を完了して攻撃してからセット\n-> 目的地から遠い(150より遠い) または 近い(10より近い) 場合はパスの更新が発生\n-> 5tick以上プレイヤーを見たら火の玉で攻撃する
    LandingApproach --> Landing : 目的地から遠い(150より遠い) または 近い(10より近い) 場合にパスを更新\nパスの移動が完了した時にセット
    Landing --> Dying : Perchの直上に来たらベッド爆破で倒す
    Dying --> [*] : 死亡演出が終わったら

エンダードラゴンの挙動の詳細について

エンダードラゴンの挙動を流れで追っていく。各見出しは内部で設定されているフェーズの名前。

スポーン直後

おおよそ初期状態に近い状態。基本的には HoldingPattern のみとなる。

Hover

エンティティ生成直後のバニラではあまり見ない状態。バニラのサバイバルで通常通り生成される場合、この後の処理によって同tick内に HoldingPattern に移行する。

/summon minecraft:ender_dragon などによって生成されたエンダードラゴンはこの状態から始まり、常に同じようなY高度を保ちながらぐるぐると飛び続ける。

HoldingPattern

バニラで最初に見られる状態はこれ。最も近い位置に存在するパスノードと周辺のパスノード(詳細は後述)を選択し、そのパスノード上部(Y: +0~20)をターゲットとして向かう。ターゲットへの移動が終了した場合、同じロジックで次のターゲットを発見し、以後同じように移動を繰り返す。

エンダードラゴンとターゲットの距離が 150ブロックより遠いか、10ブロックより近い 場合に以下のようなステート移行のチャンスを得る。

パスノード選択の詳細

パスノードの選択についてもう少し詳細に記載する。エンダードラゴンは (X: 0, Y: 128, Z: 0) にスポーンし、最も近い位置に存在するパスノードを取得する。初期の状況的には、東(16)か西(12)のどちらか高い方の黒曜石の柱の上にあるパスノードが選択されることになる。その後、抽選が行われ、1/8の確率で +7 、それ以外の場合は -1 のパスノードを選択する。エンダードラゴンは最初に選択したパスノードを経由したあと、2つ目のパスノードの上空(Y: +0~20)をターゲットとして向かう。(初期状態ではより高いところにいるため、ゆっくり降下してパスノード上空(Y: +0~20)に向かうこととなる。)

向かっている最中、先程記載した条件 (エンダードラゴンとターゲットの距離が 150ブロックより遠いか、10ブロックより近い) を達成し、別のフェーズに移行できなかった場合に再度パスノードとターゲットの選択の動きを繰り返す。

1/8 の抽選に当選する度にテーブルが変動するが、詳細は以下のコードを参照すること。

ここから確認できる

if (this.dragon.getRandom().nextInt(8) == 0) {
    this.shouldFindNewPath = !this.shouldFindNewPath;
    i += 6;
}
i = this.shouldFindNewPath ? ++i : --i;

火の玉攻撃

火の玉攻撃をするための状態。

StrafePlayer

こちらにドラゴンの火の玉を吐き、攻撃してくる状態。HoldingPattern から移行してくる時に事前にターゲットとなるプレイヤーが決定されており、そのプレイヤーに対して攻撃する。エンダードラゴンは、エンダードラゴンに最も近い位置のパスノードとプレイヤーに最も近い位置のパスノードを選択しながら、プレイヤーに近づくように移動する。また、フェーズが移行する前にこのパス移動が完了した場合は HoldingPattern と同じロジックでパスノードとターゲットの選択が繰り返される。

ターゲットとの距離が64ブロックより近く、5tick以上ターゲットを視認できる場合にドラゴンの火の玉エンティティを作成し、攻撃を行う。その後、その時行っていたパスの移動を完了させると HoldingPattern に移行する。

プレイヤーのエンティティが存在しなくなった場合はすぐに HoldingPattern に移行する。

着陸

Perchに着陸するまでの状態。

LandingApproach

Perchに着陸する前のフェーズ。まず、エンダードラゴンと最も近いプレイヤーを選出する。その後、Perchを原点としてプレイヤーの反対側にあるパスノードの直上に移動する(これはおおよそ黒曜石の柱の上部や上空を指すことになる)。

移動が完了したタイミングでそのまま Landing に移行する。

Landing

Perchに着陸するフェーズ。(よほど変な地形や状況で無い限りは)高度を保ちながら (X: 0, Z: 0) に向かって進み、その後はPerchの先端を目指して周期的に回転しながら降下してくる。

Perchの先端からの距離が1ブロック未満になったタイミングで SittingScanning に移行する。また、このタイミングで Sitting 系のフェーズによる連続攻撃回数の記録はリセットされる(詳しくは後述)。

着陸後

ブレス攻撃をするための状態。ブレス攻撃は連続で4回までしか行わない。

この状態にいる時に50ダメージ以上与えると、即座に Takeoff に移行する。

SittingScanning

Perchの先端に留まり、特に大きくは動かないフェーズ。20ブロック以内にプレイヤーがいる状態では、そのプレイヤーの方向を向く。

20ブロック以内にプレイヤーがいる状態が25tickを超えた場合、 SittingAttacking に移行する。

もし20ブロック以内にプレイヤーいない状態で100tickが経過した場合、150ブロック以内のプレイヤーを捜索し、発見できれば ChargingPlayer に移行する。発見できない場合は Takeoff に移行する。

SittingAttacking

ブレス攻撃前のフェーズ。特に何もしない。クライアント側ではエンダードラゴンの咆哮が再生される。

40tick経過後に SittingFlaming に移行する。

SittingFlaming

ブレス攻撃を行うフェーズ。10tick経過時、ブレスを展開する。

200tick経過後、連続攻撃回数が4回未満の場合は再度 SttingScanning に移行する。4回以上の場合は Takeoff に移行する。

離陸

Perchから離れ、HoldingPattern に戻るまでの状態。

ChargingPlayer

突進攻撃をしてくるフェーズ。SittingScanning から移行してくる時に事前にターゲットとなる経路が決定されており、目的の座標に対して攻撃する。

フェーズ中、ターゲットがリセットされるなどすると、すぐに HoldingPattern に移行する(おそらくこのフェーズ中にワールドに入りなおすなどすると発生する?)。

ある程度の時間その方向に向かったら HoldingPattern に移行する。ある程度の時間はデフォルトでは10tickだが、エンダードラゴンとターゲットの距離が 150ブロックより遠いか、10ブロックより近い 状態になっているtickがある場合は 1tick ずつ時間が減っていく。

Takeoff

突撃をせず、そのまま HoldingPattern に戻るためのフェーズ。現在地から最も近いパスノード → おおよそ反対側の上空という経路で地上から離れる。

Perchから10ブロック以上離れたタイミングで HoldingPattern に移行する。

RTA向けのTips

自分のRTAの手腕はまだまだなので見当違いなことを言っている可能性はあるが、あくまで今回の調査から得られたTipsということで、いくつか紹介する。

StrafePlayer に移行しづらくする

RTAの最中、エンダードラゴンがすばやくPerchに着陸してもらうことが求められる。このためには、まず LandingApproach フェーズに移行してもらう必要がある。

HoldingPattern で説明したように、基本的にPerchから 150ブロック以上離れている10ブロックより近くにいる 場合に次のフェーズに移行するチャンスを得る。ただ、その際は LandingApproach の抽選と同時に StrafePlayer の抽選もされてしまう。StrafePlayer に移行すると、エンダードラゴンとプレイヤーの距離が64ブロック未満になるまで火の玉を発射せず、HoldingPattern に戻ることもない。そのため、できるだけ StrafePlayer への抽選を避けることが無難な行動だと言える。

この際、StrafePlayer の抽選にはプレイヤーの距離も抽選に関わる。

 \frac{1}{\left\lfloor \frac{(\text{Distance between Player and Perch})^2}{512} \right\rfloor + 2.0} \quad

遠ければ遠いほど確率は下がるので、StrafePlayer の抽選をできるだけ外しやすくするためには エンダードラゴンとの距離を遠くする ことが重要となる。

また、StrafePlayer が選択されてしまった場合のことも考える。エンダードラゴンは垂直方向へのスピードより、水平方向へのスピードの方が早いため、プレイヤーがPerchとの距離を稼ぐ場合は縦方向に遠ざかることでより早く火の玉を発射してもらうことに期待できる。この場合、黒曜石の柱に登り、そのまま上方向にブロックを積むことで、火の玉を避ける行動も兼ねて距離を稼ぐことができる。

また、エンドクリスタルを破壊すると、LandingApproach が当選する確率と StrafePlayer が当選する確率の両方が上がる。これも上記アプローチによってうまく回避できると言える。

 \quad \frac{1}{\text{Count of End Crystals} + 2}

Landing を素早く終わらせる

Landing の際に向かう目標とされるブロックは Landing に移行する際に決定される。そのため、移行前にPerch上にどんどんブロックを置いていき、柱を延長するような形にすることでそのブロック上に着地するようになる。これにより、通常よりもエンダードラゴンが着地するまでの時間が短縮される。

ただ、これによって短縮される時間というのはそこまで有意なものとは思えず、これをやるのであれば、もうちょっと別の技(例えば: Zero Cycle など)を練習してもらったほうが良いように思う。

おわりに

今回はエンダードラゴンの挙動を実装した各フェーズのクラスを中心にコードリーディングすることで自分が知りたい範囲で詳細を確認することができた。これにより、ある程度は自信を持って行動できるようになると思う。

やはりMinecraftのゲーム内のロジックを読み解くのはかなり楽しい。これは、Minecraft自体がとても機械的で、かつそのロジックが充分に機能していることが大きく、その裏側を見ることで自分のMinecraftに対する知識の確かな裏付けとなる感覚があるからだと思っている。表面から見て感情がなく、とても律儀に動くゲームの裏側は勿論コードによって支えられており、その実装がとてもしっかりしているからこそ、ここまで多くの人がこの箱庭に魅了されたのだ!と胸を張って言えるような気がする。

また、そのプレイ人口の数もあってエコシステムがかなりしっかりしているため、何かをいじったりするための用意が周到なのも特徴と言える。おおよそなんでもコードレベルで知ることができるような気持ちになる。

今後もなんらかMinecraftにまつわる不思議なことが現れるたびにコードを見に行って、感心したり納得したりしながら付き合っていくのだろう。

*1:この用語自体はRTAでも使われるものだが、通常のRTAでは岩盤の柱の上にブロックを積むようなことはしないため、コミュニティ内で高い位置に置かれたブロックのことまで含めてPerchと呼ぶのかは不明だが、今回は便宜上Perchと呼ぶ。