Javaは分からないけどマイクラMODを作りたい #7 AIを作る

前回はもともと定義されているAIの機能をいじりましたが、今回からは自分でAIを作っていきます。

1. EntityAIBase を継承する

LittleMaidMob のカスタムAIを見てみると、どうやら EntityAIBase を継承しているようです。これを参考にAIを作ってみたいと思います。

1.1 AIの行動におけるメソッド

まずは EntityAIBase を継承して機能を記述していくにあたって必要なメソッドやパラメータなどを書いていきます。

メソッド名 return 内容
= className constructor ここにはメインの処理は書かない。あらかじめ渡されたパラメータによる初期化などを行う。メインのメソッドを呼び出す必要もない。
shouldExecute boolean 行動を開始するための条件を記述するメソッド。行動させるための条件を記述し、行動させる場合は true を返す。無条件で動作させたい場合はそのまま return true; で書けば良さそう。abstruct 修飾子がついているのでオーバーライド必須。
shouldContinueExecuting boolean 行動を続行させるかの条件を記述するメソッド。デフォルトでは return this.shouldExecute(); となっているため初期条件が true であれば続行となる。shudExecute() は初期条件であったが、これは途中条件?になる。
isInterruptible boolean 行動するタスクの優先順位を変更可能かどうかを決める。デフォルトでは true になっており、現在行動しているタスクよりも優先順位が高いタスクが来た場合は中断される。
startExecuting void AIの初回行動時に呼び出されるメソッドで、1回だけ行動させたい条件や内容を書く。メインの動作をさせるための初期化など。
resetTask void 他のタスクによって中断された場合に呼び出されるメソッドで、内部状態(行動やパラメータなど)をリセットする。主にAIが行動を終了する際の内容を記述する。
updateTask void メインとなる行動内容を記述する。このメソッドは常に更新される(繰り返される)ため、1回で終わらないような処理(例えばプレイヤーに追従するために移動し続ける)などを書く。

AIの行動に関するメソッドはこんな感じです。

1.2 AIのタスクに関するメソッド

AIのタスクは場合によっては非同期処理されます。例えば、モンスターを例に挙げてみます。水中にモンスターを落とすと水面に上がる動作をします。これにプレイヤーが近づくと攻撃されます。加えてプレイヤーを見てきます。

この場合 AI のタスクは 3つにスレッドが分かれて同時進行しています。ここで3つのタスクそれぞれ「泳ぐ」「攻撃」「睨みつけ」が動いているわけですね。で、このタスクの同時進行が可能かどうかを mutexBits というパラメータで調整します。

mutexBits は他のAIのタスクから保護(矛盾がないように)するのに使用されるといってもいいと思います。また例え話になりますが、狼なんかはいい例だと思います。普段はプレイヤーを追従していますが、モンスターとの戦闘になった場合はモンスターへの攻撃を優先します。この場合、「追従」と「攻撃」の両タスクを実行することはできないようになっています。もし両者のタスクが同時で実行されると、AIは固まります。

ちなみにどのような判定で決めているかはこちらの記事に書いてあったので載せておきます。論理積をとることで判定しているようですね。

w.atwiki.jp

これを見る限り、処理はある程度グルーピングされているようです。「攻撃系」「首の動き」「泳ぎ」などですかね。同じ属性に入るような動きは矛盾が生じてしまうため、この表を参考にパラメータを合わせたほうがよさそうです。

これらを処理するメソッドは次の通りです。C#でいうところの getter / setter でしょうか....。

メソッド名 return 内容
setMutexBits void mutex フラグ用の値を設定するためのメソッド。1 の場合はモーションなどの動作関連、2 は視線などの首の動き関連、4 は水泳、その他などに割り振られているらしい。
getMutexBits void mutex による処理で他のタスクと同時実行可能か否かを判別するための処理。競合する(AND演算が true である)とき、このAIタスクは排他的に実行される。

あとはパラメータ mutexBits があり、整数型です。この値をもとに判定されます。

2. サンプルコードで練習

こちらのサイト に実装例があったので、これを参考にします。バージョンが少し古いですが、そこは読み換えて何とかするしかなさそうです。

サイトでは「レッドストーンを持っているプレイヤーに突進」とありますが、これを「ダイヤモンドを持っているプレイヤーを追従」に変更します。
まるで宝石に群がる人みたいですw

2.1 継承したクラスの作成

まずはAIの機能を書いていきます。 EntityAIBase を継承します。

package jp.takunology.takunologymod.entity.ai;

import jp.takunology.takunologymod.entity.HumanUnitBase;
import net.minecraft.entity.ai.EntityAIBase;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.init.Items;
import net.minecraft.item.ItemStack;

public class HumanUnitAIDiamondFollow extends EntityAIBase
{
    HumanUnitBase owner; //このAIを搭載した Entity
    EntityPlayer target; //対象となるプレイヤー

    public HumanUnitAIDiamondFollow(HumanUnitBase humanUnitBase)
    {
        owner = humanUnitBase;
        setMutexBits(1); //一応動作に関する部分なので 1 にしました
    }

    @Override //AIの行動開始条件
    public boolean shouldExecute() {
        target = owner.world.getClosestPlayerToEntity(owner, 10.0D);
        if(target != null) //対象プレイヤーがいるかどうか nullチェック
        {
            if(owner.getDistance(target) < 2.0D) //すでに接近している場合
            {
                return false;
            }
            
            ItemStack itemStack = target.getHeldItemMainhand();
            if(itemStack != null) //アイテムがあるかどうか nullチェック
            {
                //ダイヤモンドを持っていた場合は実行する
                if(itemStack.getItem() == Items.DIAMOND) 
                {
                    return true;
                }        
            }
        }
        return false; //対象のプレイヤーがいない場合は何もしない
    }

    @Override //AIが行動を続けるかどうか
    public boolean shouldContinueExecuting()
    {
        return super.shouldContinueExecuting();
    }

    @Override //AI行動開始1回目に行う処理
    public void startExecuting()
    {

    }

    @Override
    public void resetTask()
    {
        target = null; //処理が終わったらターゲットを初期化して終了
    }

    @Override
    public void updateTask()
    {
        if(target != null)
        {
            //ターゲットがいる限りついていく
            owner.getNavigator().tryMoveToEntityLiving(target, 0.55F);
        }
    }
}

あとはこのAI行動を Entity のタスクに登録すれば完了です。コンストラクタの引数は this 修飾子を使ってそのオブジェクトを渡しています。

this.targetTasks.addTask(2, new HumanUnitAIDiamondFollow(this));

2.2 使用したメソッド

使用したメソッドは表の通りです。参考にしたサイトのバージョンが古く、対応していないメソッドもありましたが、ライブラリを参照しながら適当に試しました。

アイテムの比較を行う際に、参考サイトでは itemStack.itemID == Item.redstone.itemID としていましたが、これは 1.12.2 では使用できませんでした。代わりに itemStack.getItem() == Items.DIAMOND を用いています。アイテムIDや名前の管理は Json 形式になったため、変更されたのだと思います。

一度、itemStack にプレイヤーが持っているアイテムを保持しておき、これを比較しています。念のため null チェックを入れています。

メソッド名 return 内容
getClosestPlayerToEntity player Entity とプレイヤー間の距離を引数とし、引数に指定した範囲内で最も近いプレイヤーを取得する
getHeldItemMainhand item 右手に持っているアイテムを取得する。
getDistance float プレイヤーとの距離を取得する。
tryMoveToEntityLiving boolean 追従の対象となるEntityと移動速度を引数に入れ、その対象に可能な限り追従する。
getItem item ItemStack に保持されたアイテムを取得する。
Items.DIAMOND item デフォルトで登録されているアイテムへアクセスし、その名前のアイテムを取得する。

2.3 動かしている様子

ダイヤモンドを持っていて、ある条件になると追従します。

youtu.be

3. EntityAIBase のフロー

ソースコードだけでは見づらかったので、図にしてみました。(ソフトウェア工学で使うような図はよく分からないのでテキトーに。)あと、タスクが実行されるときはタスクを束ねている EntityAITasks というクラスが動くらしいです。

f:id:takunology:20200316175053p:plain

4. おわりに

とりあえず EntityAIBase クラスのメソッドと使い方は把握できたかもしれません。ここまで理解するのに2日くらいかかりましたが、今はなんとなく雰囲気で書けるようになってきました。あとは鬼のような量のライブラリを参照すれば自力で書けるかもしれません。

C#と似ている点があるので作業はしやすいです。