こちらはPCゲーム「Minecraft」のMod作製の解説記事です。
Minecraft.1.12.2、
Forge-1.12.2-14.23.2.2611での解説になります。
Minecraftの世界に存在する動物やモンスターなどのMob。これらに加え弓から放たれた矢、ブレイズの火球などがEntityに含まれます。
ここでは「Entity」クラスに含まれる生物に関するクラス「EntityLivingBase」の型階層やEntityを作製する際に使う可能性の高い処理などのEntityの作製に関する情報を載せています。この記事を書いている私自身が元々はJavaを勉強したことが無い状態からMod作製を始めたので、説明内容などはプロのプログラマーの人が見たら「既に知っている」事や「当たり前」の事などもあると思います。この記事は私の様にJavaに初めて触れる人や、これからMod作製に挑戦したい人やMod作製の初心者の人の為に書いているものなので、専門用語を使う時と使わない時があります。また、記載内容に誤りや私自身の理解が誤っている可能性がある事をご了承ください。
記事の内容は徐々に追加していく予定です。
EntityLivingBase
型階層
EntityLivingBaseからの型階層。eclipseでは型階層は対象のクラスを指定して右クリックで表示されるメニューから「型階層を開く」もしくはクラスを指定した状態で「F4」キーを押す事で確認出来る。eclipseで簡単に確認できるので、クラス継承などの確認だけであればeclipseの方が早いかも知れない。
EntityLivingBaseの型階層
EntityLivingBase | EntityArmorStand |
EntityLiving | |
EntityPlayer |
EntityLivingBaseは上の表のように3つに分かれ、生物に関するメソッドや変数などが記載されている。アーマースタンドは体力も無く、生物とは言えないかもしれないが、内部的にはEntityLivingBaseの型階層に含まれる。これはアーマースタンドがプレイヤーと同様に鎧などの装備品に関わる「handInventory」と「armorArray」を実装しているからである。それ以外はプレイヤーかプレイヤー以外の生物かで2つのクラスに分かれる。
EntityLivingの型階層
EntityLiving | EntityAmbientCreature | EntityBat |
EntityCreature | ||
EntityDragon | ||
EntityFlying | EntityGhast | |
EntitySlime | EntityMagmaCube | |
EntityWaterMob | EntitySquid |
EntityLivingからは上の表にように分かれる。EntityFlyingにはガストのみが含まれ、ブレイズなどはEntityCreatureの先のEntityMob(敵対的な生物)に含まれる。スライムも敵対的な生物ではあるが他のMobと実装している内容が違うので分かれている。スライム、ガスト、シュルカー、エンダードラゴンはインターフェースの「IMob」を実装している為、「これらを含む敵対的な生物」を取得する時は「IMob」を使用すると良い。
例:if文
if (this.getAttackTarget instanceof IMob)
ほとんどの生物はEntityLivingの先のEntityCreatureに含まれるので、型階層から探す場合はEntityCreatureから探すと良い。
EntityCreatureの型階層
EntityCreature | EntityAgeable |
EntityGolem | |
EntityMob |
EntityCreatureからは上の3つに分かれる。EntityAgeableからは動物と村人に、EntityGolemからはアイアンゴーレムとスノーゴーレムとシュルカー、EntityMobからは殆どの敵対的な生物に分かれる。
EntityAgeable ➡ EntityAnimalの型階層
EntityAnimal | AbstractHorse |
EntityChicken | |
EntityCow | |
EntityPig | |
EntityPolarBear | |
EntityRabbit | |
EntitySheep | |
EntityTameable |
EntityAnimalからは上の表のように分かれる。EntityTameableには馬、ロバ、ラバ、ラマを除く「手懐け可能な動物」のオオカミ、ヤマネコ(ネコ)、オウムが含まれる。
よく使用されるメソッド
Entityを右クリックした時
public boolean processInteract(EntityPlayer player, EnumHand hand)
EntityLivingクラスのprocessInitialInteract内で呼び出される。processInitialInteractはEntityを右クリックした際の処理で、Entityクラスで実装されているが処理内容はfalseを返すのみで実際にはEntityLiving内でオーバーライドされ処理内容が実装されている。初めにEntityへのリードの付け外しの処理を実行するかの判定が行われ、それが行われなかった場合に、processInteractの処理が実行される。processInitialInteractはfinalの修飾子が付いているのでオーバーライドが出来ない。その為、「プレイヤーがリードを持っている」状態か、「Entityにリードが付いている」状態を条件とする処理は実装できない事になる。
processInteractの処理の流れは、継承先のクラスでの(右クリックした時の)処理内容の判定を行ってから、処理が行われずに最後までいった時には継承元のprocessInteractが実行される。以下はEntityWolf(オオカミ)クラス内の「processInteract」。
public boolean processInteract(EntityPlayer player, EnumHand hand)
{
ItemStack itemstack = player.getHeldItem(hand);
if (this.isTamed())
{
if (!itemstack.isEmpty())
{
if (itemstack.getItem() instanceof ItemFood)
{
ItemFood itemfood = (ItemFood)itemstack.getItem();
if (itemfood.isWolfsFavoriteMeat() && ((Float)this.dataManager.get(DATA_HEALTH_ID)).floatValue() < 20.0F)
{
if (!player.capabilities.isCreativeMode)
{
itemstack.shrink(1);
}
this.heal((float)itemfood.getHealAmount(itemstack));
return true;
}
}
else if (itemstack.getItem() == Items.DYE)
{
EnumDyeColor enumdyecolor = EnumDyeColor.byDyeDamage(itemstack.getMetadata());
if (enumdyecolor != this.getCollarColor())
{
this.setCollarColor(enumdyecolor);
if (!player.capabilities.isCreativeMode)
{
itemstack.shrink(1);
}
return true;
}
}
}
if (this.isOwner(player) && !this.world.isRemote && !this.isBreedingItem(itemstack))
{
this.aiSit.setSitting(!this.isSitting());
this.isJumping = false;
this.navigator.clearPath();
this.setAttackTarget((EntityLivingBase)null);
}
}
else if (itemstack.getItem() == Items.BONE && !this.isAngry())
{
if (!player.capabilities.isCreativeMode)
{
itemstack.shrink(1);
}
if (!this.world.isRemote)
{
if (this.rand.nextInt(3) == 0 && !net.minecraftforge.event.ForgeEventFactory.onAnimalTame(this, player))
{
this.setTamedBy(player);
this.navigator.clearPath();
this.setAttackTarget((EntityLivingBase)null);
this.aiSit.setSitting(true);
this.setHealth(20.0F);
this.playTameEffect(true);
this.world.setEntityState(this, (byte)7);
}
else
{
this.playTameEffect(false);
this.world.setEntityState(this, (byte)6);
}
}
return true;
}
return super.processInteract(player, hand);
}
上のコードの最後にある「return super.processInteract(player, hand);」で継承元(親クラス)のprocessInteractを呼び出している。このコード(EntityWolfの)をコピーして間の処理内容を自分の作りたい処理に書き換えれば、右クリックした時の処理が実装または変更が出来る。
継承先のクラスの処理を先に判定する場合
public boolean processInteract(EntityPlayer player, EnumHand hand)
{
//ここに処理内容を記述
//処理が実行される場合はreturn true;を記述
return super.processInteract(player, hand);
}
親クラスの処理を先に判定する場合
public boolean processInteract(EntityPlayer player, EnumHand hand)
{
if (super.processInteract(player, hand))
{
return true;
}
//ここに処理内容を記述
//処理が実行される場合はreturn true;を記述
return false;
}
processInteractは戻り値がboolanなので、当然ながらtrueかfalseを返す必要がある。オリジナルで作った処理が実行された後にreturn (true)を記述しておかないと、「継承先の処理を優先する場合」は継承元(親クラス)の処理も実行されてしまい、「継承元の処理を優先する場合」はプレイヤーが手にアイテムを持っていた場合は、そのアイテムを右クリックで使用した際の処理も実行されてしまう。
EntityWolfクラスのprocessInteractの
ItemStack itemstack = player.getHeldItem(hand);
ここに記載されている引数handは「プレイヤーが手に持っているアイテム」なので、どちらの手に持っているかは関係しない。なので、持っている手を指定したい場合は・・・
player.getHeldItemMainhand();メインハンド
player.getHeldItemOffhand();オフハンド
に変更すれば良い。
EntityのAI
EntityAIBase
Entityの行動を決定するAIクラスの元となるabstractクラスで、バニラのEntityAIはこれを継承して作成されている。なのでオリジナルのAIを作る場合も、これを継承したAIクラスを作ってEntityクラスで使用する事になる。
※abstractクラスは抽象クラスと呼ばれ、継承して使用する為のクラス。これを継承したクラスは、必要なメソッドの実装がされていないと、エラーを出してくれるので、「必要なメソッドの実装漏れが起きない」と、ここでは思って欲しい(詳しい説明は省くため)。
実際に動作する時のAIの流れは、
・shouldExecute():AIの起動条件をここで記述する
・shouldContinueExecuting():AIの継続条件をここで記述する
・isInterruptible():このAIより優先度(Priority)の高いAIが起動する時に、このAIを止めて起動させられるかどうか(バニラのAIはすべてtrue)
・startExecuting():AIが起動する時に呼び出される処理
・resetTask():AIが中断された時に呼び出される処理
・updateTask():AIが起動してから継続している間に断続的に呼び出される処理
・setMutexBits(int mutexBitsIn):同時に動作できる他のAIとの競合をビット演算子で設定する
・getMutexBits():mutexBitsの値を取得
package net.minecraft.entity.ai;
public abstract class EntityAIBase
{
/**
* A bitmask telling which other tasks may not run concurrently. The test is a simple bitwise AND - if it yields
* zero, the two tasks may run concurrently, if not - they must run exclusively from each other.
*/
private int mutexBits;
/**
* Returns whether the EntityAIBase should begin execution.
*/
public abstract boolean shouldExecute();
/**
* Returns whether an in-progress EntityAIBase should continue executing
*/
public boolean shouldContinueExecuting()
{
return this.shouldExecute();
}
/**
* Determine if this AI Task is interruptible by a higher (= lower value) priority task. All vanilla AITask have
* this value set to true.
*/
public boolean isInterruptible()
{
return true;
}
/**
* Execute a one shot task or start executing a continuous task
*/
public void startExecuting()
{
}
/**
* Reset the task's internal state. Called when this task is interrupted by another one
*/
public void resetTask()
{
}
/**
* Keep ticking a continuous task that has already been started
*/
public void updateTask()
{
}
/**
* Sets the mutex bitflags, see getMutexBits. Flag 1 for motion, flag 2 for look/head movement, flag 4 for
* swimming/misc. Flags can be OR'ed.
*/
public void setMutexBits(int mutexBitsIn)
{
this.mutexBits = mutexBitsIn;
}
/**
* Get what actions this task will take that may potentially conflict with other tasks. The test is a simple bitwise
* AND - if it yields zero, the two tasks may run concurrently, if not - they must run exclusively from each other.
* See setMutextBits.
*/
public int getMutexBits()
{
return this.mutexBits;
}
}
各メソッドの動作は、
shouldExecute()
AIの起動条件でtrueの場合、このAIが起動する。例えば体力が0になって死亡処理中(赤くなって倒れてから消える)のMobに近寄っても攻撃されないようにするなど。
shouldContinueExecuting()
AIの継続条件でtrueの場合はAIを継続。falseの場合はAIがresetTask()が呼び出される。例えば攻撃対象が何らかの理由で居なくなった時など。
isInterruptible()
バニラのAIはすべてtureを返すので、Entityクラスで実装する時に設定するPriorityの値で優先度が決まる。オリジナルのAIで継続中に他のAIに割り込ませたくない場合はfalseを返す様にするか、返す条件を記述すれば良い。
startExecuting()
AIが起動する時に呼び出される処理。抽象メソッドではないので、実装しなくても良い。攻撃対象を決定するAIでshouldExecute()で対象に対しての正負を設定して、trueの場合にはEntityのattackTargetに対象のEntityを設定するなど、オオカミが周囲の羊を攻撃する時のターゲット用のAIで使用されている。
resetTask()
AIが中断された時に呼び出される処理で、抽象メソッドではないので、実装しなくても良い。shouldContinueExecuting()がfalseの時に呼び出される。EntityAIAttackMeleeなどで攻撃後に対象が死亡した場合や、オオカミなどが戦闘中にプレイヤーによる待機命令を受けた時などに、attackTargetを初期化するなど。
updateTask()
AIが起動してから継続している間に断続的に呼び出される処理でEntityAIAttackMeleeなどでは、攻撃対象との距離に応じて移動の目的地の再設定までの期間を設定したり、攻撃の届く距離まで近付いた場合に、近接攻撃のメソッドを呼び出す(attackEntityAsMob)。
setMutexBits(int mutexBitsIn)
同時に動作できる他のAIとの競合をビット演算子で設定する。他のAIを見るとわかるようにコンストラクタの中に必要に応じて設定する。shouldExecutingで複数のAIがtrueを返した場合で、それらのAIの中で、このAIクラスよりPriorityが低いAIの同時実行を許すかを決定する。ビット演算の知識が無いと設定を理解するのが難しいので、それらを解説しているサイトを見るか、既存のAIクラス同士を見て比べたり、設定してみてテストする事になるが、後者の場合はエラーの確認をしっかりと行う必要がある。
メソッドの説明にもある通り、
・1 = motion:移動
・2 = look/head motion:頭の動き
・4 = swim:泳ぐ
例えばEntityAIAttackMeleeクラスは値が3なので1~5の中では1、2、3、5とANDになるので、他のAIでこのAIよりPriorityが低いAIでも0か4を設定したAIには割り込まれる事になる。オオカミの場合、EntityAIFollowOwnerも値が3でPriorityがEntityAIAttackMeleeより低いので、プレイヤーに追従時に戦闘に参加した後、プレイヤーから離れても攻撃対象を追いかけ続ける。
ここで使用されるビット演算はANDなので、整数の2進数表記のリストを見ながら設定すると良い。比較する二つの整数(int値)を2進数に変換したもので、同じ桁同士で両方が1の場合は競合する事になる。例えば(1)は00000001で(3)は00000011なので、双方の1桁目が1なので比較の際に呼び出されるisControlFlagDisabled()ではtrueを返す。
ビット演算が苦手な人の為に、1~5の競合を記載
(1):3、5
(2):3
(3):1、2、5
(4):5
(5):1、3、4