弾Entityの作成
IGNISから発射する弾をEntityとして作成します。
EntityはBlockのように座標で固定されない、動きを持たせることが出来るオブジェクトです。ただし生成するだけではその場から動かないし何もしません。動きや向きの制御、接触判定、接触後の処理など、必要なことは自分で追加しないといけません。
(そのため、チュートリアル等ではアイテムやブロックに比べてやや上級向けとして扱われます。初心者が作りたくなるアイデアを最も理想に近い形で実装できる道でもあり、理解不足の初心者Modderを叩き落とす罠でもあります。)
私の場合、自作チュートリアルからソースを引っ張ってきて、改良しつつ作っていきます。チュートリアル(村人波動砲)のほうは、バニラの矢Entityを参考にしつつ作っています。
弾Entityの作成にあたり、どんなふうに作るのか考えてみます。
- Entityを貫通する
- Blockを貫通するが、貫通可能距離には限界がある
- ダメージを与える対象を選択する(火炎放射なので、火炎耐性を持っているとダメージを与えられない、など)
- 武器のエンチャントに応じた効果の強化
銃アイテムはEntityに座標や方角などの初期パラメータを与えて生成する程度のことしかしません。 実際の攻撃処理は、Entity側で行います。
実際に実装されているクラス
なお、内容はVer1.1aのものです。 記事が冗長になるため、クラス全文は別ページに移します。
Entityクラスを作る
Entityクラスを継承する
Entityを作るには、Entity.classを継承します。また、今回は弾を作るため、バニラの発射体用インターフェイスであるIProjectileを実装します。
Entity.class、IProjectileインターフェイスそれぞれ、いくつかの"必ず作らなければならないメソッド"があります。以下の状態が、弾Entityの最低限の状態です。
以下のように生成した場合、継承しているEntity.classに備わっているデフォルトの機能のみ持った状態になります。座標移動や当たり判定周りの機能は備わっていますが自力移動は出来ず、デフォルトの見た目(灰色の無地のボックス)の高さ1.8F、幅0.6F、当たり判定無しの四角い物体が、(おそらく)初期地点を選べない状態で生成されます。
package defeatedcrow.flamethrower.entity; import net.minecraft.entity.Entity; import net.minecraft.entity.IProjectile; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.world.World; public class EntityFlame extends Entity implements IProjectile { // コンストラクタ public EntityFlame(World world) { super(world); } /* Entityから実装を求められるメソッド */ @Override protected void entityInit() { } @Override protected void readEntityFromNBT(NBTTagCompound tag) { } @Override protected void writeEntityToNBT(NBTTagCompound tag) { } /* IProjectileから実装を求められるメソッド */ @Override public void setThrowableHeading(double par1, double par3, double par5, float par7, float par8) { } }
- コンストラクタ
public EntityFlame(World world) { super(world); }
BlockやItemは起動時の一度だけしかインスタンスが生成されないため、通常は一種のBlock、Itemにつきコンストラクタが呼ばれるのは一度だけです。しかし、Entityはいくつでもnew Entity(world~)のように、インスタンスを生成出来ます。なので、コンストラクタもEntityの生成の度に呼ばれます。
ですから、Entityのコンストラクタの場合はBlock等と違って、個体ごとに異なる値を入れることを考えて作成します。このMODの場合、現在地や射撃の方向、ダメージ量などを生成時にEntityに渡します。
- Entityクラスから求められるメソッド
/* Entityから実装を求められるメソッド */ @Override protected void entityInit() { } @Override protected void readEntityFromNBT(NBTTagCompound tag) { } @Override protected void writeEntityToNBT(NBTTagCompound tag) { }
メソッドの中身は空でもクラッシュはしません。必要な場合のみ中身を作成します。このMODの場合は、NBTタグを読み書きしたいのでNBTのメソッド2つは中身を作っています。
- IProjectile[から求められるメソッド
@Override public void setThrowableHeading(double par1, double par3, double par5, float par7, float par8) { }
発射体が飛んでいる間の速度や向きの制御。このMODの場合はXYZ方向速度から進行方向を計算して、Entityの向きを設定しています。
初期パラメータを入れてみる
上記のメソッドに必要なパラメータを入れてみます。
この弾Entityの生成座標、射撃したEntity(EntityLivingBase)、与えるダメージや弾速、サイズなどの初期値をここで入れます。
今回はそれに加えて、Entityクラスのメソッドの一部をオーバーライドして内容を変えてみます。
/* この弾を撃ったエンティティ */ public Entity shootingEntity; /* 地中・空中にいる時間 */ protected int ticksInGround; protected int ticksInAir; protected int livingTimeCount = 0; /* ダメージの大きさ */ protected final double damage; /* 前進速度 */ protected double speed = 1.0D; /* 幅 */ protected final double range; /* ノックバックの大きさ */ protected final int coolTime; // コンストラクタ public EntityFlame2(World par1World) { super(par1World); this.renderDistanceWeight = 10.0D; this.setSize(0.5F, 0.5F); this.damage = 10.0D; this.range = 4.0D; this.coolTime = 10; } public EntityFlame2(World par1World, float size, double thisdamage, double thisrange, int thisCoolTime) { super(par1World); this.setSize(size, size); this.range = thisrange; this.damage = thisdamage; this.coolTime = thisCoolTime; } public EntityFlame2(World par1World, EntityLivingBase par2EntityLivingBase, float speed, float fluctuations, double damage, double range, int cool) { this(par1World, 0.5F, damage, range, cool); this.shootingEntity = par2EntityLivingBase; this.yOffset = 0.0F; // 初期状態での向きの決定 - (1) this.setLocationAndAngles(par2EntityLivingBase.posX, par2EntityLivingBase.posY + par2EntityLivingBase.getEyeHeight() - 0.6D, par2EntityLivingBase.posZ, par2EntityLivingBase.rotationYaw, par2EntityLivingBase.rotationPitch); // 位置の調整 - (2) this.posX += (-MathHelper.sin(this.rotationYaw / 180.0F * (float) Math.PI) * MathHelper.cos(this.rotationPitch / 180.0F * (float) Math.PI)); this.posZ += (MathHelper.cos(this.rotationYaw / 180.0F * (float) Math.PI) * MathHelper.cos(this.rotationPitch / 180.0F * (float) Math.PI)); this.setPosition(this.posX, this.posY, this.posZ); float f1 = worldObj.rand.nextFloat() * 0.1F - 0.05F; float f2 = worldObj.rand.nextFloat() * 0.1F - 0.05F; float f3 = worldObj.rand.nextFloat() * 0.1F - 0.05F; // 初速度 - (3) this.motionX = f1 + ((double) (-MathHelper.sin(this.rotationYaw / 180.0F * (float) Math.PI) * MathHelper .cos(this.rotationPitch / 180.0F * (float) Math.PI))); this.motionZ = f2 + ((double) (MathHelper.cos(this.rotationYaw / 180.0F * (float) Math.PI) * MathHelper .cos(this.rotationPitch / 180.0F * (float) Math.PI))); this.motionY = f3 + ((double) (-MathHelper.sin(this.rotationPitch / 180.0F * (float) Math.PI))); this.setThrowableHeading(this.motionX, this.motionY, this.motionZ, speed, fluctuations); } /* Entityから実装を求められるメソッド */ @Override protected void entityInit() { } @Override protected void readEntityFromNBT(NBTTagCompound tag) { } @Override protected void writeEntityToNBT(NBTTagCompound tag) { } /* IProjectileから実装を求められるメソッド */ @Override public void setThrowableHeading(double par1, double par3, double par5, float par7, float par8) { float f2 = MathHelper.sqrt_double(par1 * par1 + par3 * par3 + par5 * par5); par1 /= f2; par3 /= f2; par5 /= f2; par1 += this.rand.nextGaussian() * (this.rand.nextBoolean() ? -1 : 1) * 0.007499999832361937D * par8; par3 += this.rand.nextGaussian() * (this.rand.nextBoolean() ? -1 : 1) * 0.007499999832361937D * par8; par5 += this.rand.nextGaussian() * (this.rand.nextBoolean() ? -1 : 1) * 0.007499999832361937D * par8; par1 *= par7; par3 *= par7; par5 *= par7; this.motionX = par1; this.motionY = par3; this.motionZ = par5; float f3 = MathHelper.sqrt_double(par1 * par1 + par5 * par5); this.prevRotationYaw = this.rotationYaw = (float) (Math.atan2(par1, par5) * 180.0D / Math.PI); this.prevRotationPitch = this.rotationPitch = (float) (Math.atan2(par3, f3) * 180.0D / Math.PI); this.ticksInGround = 0; } /* 以下はEntity.classの処理のオーバーライド */ // レンダー用に使われる、位置や向き情報などのメソッド @Override @SideOnly(Side.CLIENT) public void setPositionAndRotation2(double par1, double par3, double par5, float par7, float par8, int par9) { this.setPosition(par1, par3, par5); this.setRotation(par7, par8); } @Override @SideOnly(Side.CLIENT) public void setVelocity(double par1, double par3, double par5) { this.motionX = par1; this.motionY = par3; this.motionZ = par5; if (this.prevRotationPitch == 0.0F && this.prevRotationYaw == 0.0F) { float f = MathHelper.sqrt_double(par1 * par1 + par5 * par5); this.prevRotationYaw = this.rotationYaw = (float) (Math.atan2(par1, par5) * 180.0D / Math.PI); this.prevRotationPitch = this.rotationPitch = (float) (Math.atan2(par3, f) * 180.0D / Math.PI); this.prevRotationPitch = this.rotationPitch; this.prevRotationYaw = this.rotationYaw; this.setLocationAndAngles(this.posX, this.posY, this.posZ, this.rotationYaw, this.rotationPitch); this.ticksInGround = 0; } } // 接地したとき、ブロックが「Entityに踏み荒らされた」判定にするかどうか @Override protected boolean canTriggerWalking() { return false; } // 影の描画サイズ これは影を無しにする @Override @SideOnly(Side.CLIENT) public float getShadowSize() { return 0.0F; } // Itemを使ってこのEntityを攻撃できるか @Override public boolean canAttackWithItem() { return false; }
処理の流れは、
- ItemIgnis.classの右クリック処理から、このクラスのコンストラクタpublic EntityFlame2(World par1World, EntityLivingBase par2EntityLivingBase, float speed, float speed2, double damage, double range, int cool)を呼ぶ
- ShootingEntityに引数で渡したEntityLivingBase(射撃手)を入れ、これのpos(座標)、yaw(水平方向の向き)、pitch(垂直方向の向き)から初期座標と向きを算出 - (1)
- 射撃手の前から弾が発生しているように見えるよう、射撃手の少し手前に位置を調整 - (2)
- 引数のSpeedにあわせ、setThrowableHeading(double par1, double par3, double par5, float par7, float par8)メソッドを利用しつつ初速度を算出 - (3)
このような形です。とりあえず、初速度だけ与えてあるので飛んでは行くと思われますが、アップデート処理(onUpdate()メソッド)はEntity.classにあるデフォルトの物しか呼ばれませんし、当たっても何も起こらないし消えもしないんじゃないかと思います。