目次
弾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にあるデフォルトの物しか呼ばれませんし、当たっても何も起こらないし消えもしないんじゃないかと思います。
Update処理を作る
Update処理というのは、ワールドの読み込みチャンク内に存在している間、毎Tick呼ばれ続ける更新処理です。BlockやTileEntityにも備わっており、Entityの場合はonUpdate()メソッドのオーバーライドで内容を追加してやることで作成できます。
注意点として、必ずsuper.onUpdate();で継承元の処理を呼ぶこと。そうでないと必要な処理が行われなくなってしまいます。(代用の処理を自作できる猛者ならこの限りではありませんが…)
// Tick処理部分 @Override public void onUpdate() { super.onUpdate(); // ここに処理を入れる }
- 移動や消滅条件などをつくる
まずは移動部分を作ります。攻撃能力はまだありません。
- 一定時間経過、ブロック内部に一定時間埋まっている、一定以下の速度に減速する、のどれかの条件で消滅する
- 移動中に常に前方に正面を向ける
- 水に入ると消火され、減速する
/* 地中判定に使うもの */ protected int xTile = -1; protected int yTile = -1; protected int zTile = -1; protected Block inTile; protected int inData; protected boolean inGround;
コンストラクタの前あたりに追加しておきます
- onUpdateに移動や消滅の処理を足す
// Tick処理部分 @Override public void onUpdate() { super.onUpdate(); // 60tick(3sec)経過すると自然消滅する livingTimeCount++; if (livingTimeCount > 60) this.setDead(); // 激突したブロックを確認している // xyzTileには、前回処理時に当たったBlockの座標が記録されているので、継続して埋まったままか確認している。 Block i = this.worldObj.getBlock(this.xTile, this.yTile, this.zTile); boolean air = this.worldObj.isAirBlock(xTile, yTile, zTile); // 空気じゃないブロックだった時(継続して埋まっている) if (i != null && i.getMaterial() != Material.air) { // 非固形ブロックなど、当たり判定が立方体でないブロックのAABBに接触しているかチェックしている i.setBlockBoundsBasedOnState(this.worldObj, this.xTile, this.yTile, this.zTile); AxisAlignedBB axisalignedbb = i.getCollisionBoundingBoxFromPool(this.worldObj, this.xTile, this.yTile, this.zTile); // 当たり判定に接触しているかどうか if (axisalignedbb != null && axisalignedbb.isVecInside(Vec3.createVectorHelper(this.posX, this.posY, this.posZ))) { // 埋まり判定をtrueに this.inGround = true; } } // 空気じゃないブロックに当たった if (this.inGround) { Block j = this.worldObj.getBlock(this.xTile, this.yTile, this.zTile); int k = this.worldObj.getBlockMetadata(this.xTile, this.yTile, this.zTile); /* * 前のTickに確認した埋まりブロックのIDとメタを照合している。違ったら埋まり状態を解除、一致したら埋まり状態を継続。 * /* 埋まり状態2tick継続でこのエンティティを消す */ if (j == this.inTile && k == this.inData) { ++this.ticksInGround; // ブロック貫通の場合、20tick(1秒間)はブロック中にあっても消えないようになる。 int limit = 20; if (this.ticksInGround > limit) { this.setDead(); } } else// 埋まり状態の解除処理 { this.inGround = false; this.ticksInGround = 0; this.ticksInAir = 0; } } else { // ここは次の段階で書き込むところ! } // ポジションに速度を加算してEntityを移動させる処理。向きも更新。 this.posX += this.motionX; this.posY += this.motionY; this.posZ += this.motionZ; float f2 = MathHelper.sqrt_double(this.motionX * this.motionX + this.motionZ * this.motionZ); this.rotationYaw = (float) (Math.atan2(this.motionX, this.motionZ) * 180.0D / Math.PI); this.rotationPitch = (float) (Math.atan2(this.motionY, f2) * 180.0D / Math.PI); while (this.rotationPitch - this.prevRotationPitch < -180.0F) { this.prevRotationPitch -= 360.0F; } while (this.rotationPitch - this.prevRotationPitch >= 180.0F) { this.prevRotationPitch += 360.0F; } while (this.rotationYaw - this.prevRotationYaw < -180.0F) { this.prevRotationYaw -= 360.0F; } while (this.rotationYaw - this.prevRotationYaw >= 180.0F) { this.prevRotationYaw += 360.0F; } this.rotationPitch = this.prevRotationPitch + (this.rotationPitch - this.prevRotationPitch) * 0.2F; this.rotationYaw = this.prevRotationYaw + (this.rotationYaw - this.prevRotationYaw) * 0.2F; // 徐々に減速する float f4 = 0.9F; // 水中に有る if (this.isInWater()) { // 着火状態が解除される if (this.isBurning()) { this.extinguish(); } // 減速も大きくなる f4 = 0.1F; } this.motionX *= f4; this.motionY *= f4; this.motionZ *= f4; // 一定以上遅くなったら消える if (this.worldObj.isRemote && (this.motionX * this.motionX + this.motionZ * this.motionZ) < 0.001D) { this.setDead(); } this.setPosition(this.posX, this.posY, this.posZ); this.func_145775_I(); }