この記事では、マインクラフトにおけるMod開発について解説しますが、
Mod開発環境の構築などの説明は行いません。
あらかじめご了承ください。
はじめに
こんにちは!
マイクラのMod開発をやっているはたこーというものです!
今回はModに関する記事を書きます!
マイクラって?
あまり説明する必要もないかと思いますが、今回解説するMod開発の基盤となる「マインクラフト」というゲームについて簡単に解説します。
マインクラフト(以下マイクラ)は2011年にスウェーデンに本部を置くMojang Studios(通称モヤン)が開発したサンドボックスゲームです。
プレイヤーはブロックで構成された3次元の世界を舞台に、木や石から色々な道具を作って探検したり、色々なブロックを使って建築などを行うことができます。
2014年に販売本数が6000万本を突破し、「世界で最も売れたインディーゲーム」に認定されました。現在の販売本数は3億5000万本を突破しており、PCだけでなく、SwitchやPS機、スマートフォンなどの多岐にわたるプラットフォームでプレイすることが可能です。
ソフトウェアとしてはJavaという言語で記述されており、2017年に登場した統合版(Bedrock Edition)はC++で書かれています。
Modって?
ModはModification(改造)を略したゲーム用語で、既存のゲームのデータを書き換えて新しいアイテムやNPCを追加したりすることを指します。つまり、Modはマイクラだけではなく色々なゲームに存在するものになります。
本来、第三者のゲーム製品を改造することは違法行為ですが、マインクラフトの場合はJava Editionに限り、規約によってModを開発することが認められています。
本題
前置きが長くなりましたが、早速本筋に入りましょう。
何を作るのか
マイクラには爆発という概念が実装されています。
メンバー(イベントに参加してくれる中高生)は「爆発」が大好き。
よくコマンドで大量にTNTを出して爆発させている姿を目にしますし、「切ったら爆発する剣」はModの成果物として人気があります。
かく言う僕も爆発は好きです。
しかし、爆発をModを組み込むにあたって一つ課題が見えてきました。
それは....
爆発でプレイヤー自身もダメージを受けてしまうことです。
「敵を攻撃したら爆発する剣」を例にとれば、敵を爆破して倒したくても、自分も爆発に巻き込まれてしまうわけです。
これを回避する手段として、例えば切った時に耐性のポーション効果(バフ的なやつ)をつけて物理的に爆発に耐えることで合ったり、そもそも最初からクリエイティブモード(無敵状態みたいなやつ)にしてプレイする、ことなどが挙げられますが、正直無理矢理感が否めません。
そこで私は思いました。
「プレイヤーだけダメージを受けない爆発を実装すればいいじゃん」、と。
後それだけでは物足りないので「爆発に指向性を持たせて、視線方向だけ爆発力を強くしよう」と言うことも考えました。
実装
早速作っていきましょう。まず、マイクラにおいて爆発はどう実装されているのでしょうか?
答えとしてはそのまんまExplosionと言うクラスが存在し、爆破範囲の探索と、爆破ダメージの計算などが制御されています。(内部実装のソースは権利的よく分かんないので載せないことにします。すみません。)
こちらを継承し、CustomExplosionというクラスを作っていきます。
public class CustomExplosion extends Explosion
{
}
当然これだけではエラーになるので、コンストラクタを追加していきましょう。
親クラスではコンストラクタが多重定義されていますが、
public Explosion(Level pLevel, @Nullable Entity pSource, double pToBlowX, double pToBlowY, double pToBlowZ, float pRadius, boolean pFire, Explosion.BlockInteraction pBlockInteraction) {
this(pLevel, pSource, (DamageSource)null, (ExplosionDamageCalculator)null, pToBlowX, pToBlowY, pToBlowZ, pRadius, pFire, pBlockInteraction);
}
こちらを使うこととし、追加で欲しいパラメータとして
float pPowerFactor
@Nullable Player player
を設定します。
あとは親クラスではprivateなフィールドになっていてアクセスできないパラメータを子クラスで保持しておくため、以下の変数宣言&メソッド作成を行います。
private final float radius;
private final ExplosionDamageCalculator damageCalculator;
private final float powerFactor;
@Nullable private final Player player;
private static final ExplosionDamageCalculator EXPLOSION_DAMAGE_CALCULATOR = new ExplosionDamageCalculator();
private ExplosionDamageCalculator makeDamageCalculator(@Nullable Entity pEntity)
{
return pEntity == null ? EXPLOSION_DAMAGE_CALCULATOR : new EntityBasedExplosionDamageCalculator(pEntity);
}
ここまできたらコンストラクタを設定できます!
public CustomExplosion(Level pLevel, @Nullable Entity pSource, @Nullable DamageSource pDamageSource, @Nullable ExplosionDamageCalculator pDamageCalculator, double pToBlowX, double pToBlowY, double pToBlowZ, float pRadius, boolean pFire, Explosion.BlockInteraction pBlockInteraction,float pPowerFactor, @Nullable Player player){
super(pLevel,pSource,pDamageSource,pDamageCalculator,pToBlowX,pToBlowY,pToBlowZ,pRadius,pFire,pBlockInteraction);
this.radius = pRadius;
this.damageCalculator = pDamageCalculator == null ? this.makeDamageCalculator(pSource) : pDamageCalculator;
this.powerFactor=pPowerFactor;
this.player=player;
}
ここまでは下準備です。いよいよ本格的な実装に入っていきます。
実際に爆発の処理を行なっているのはExplosionクラスのexplodeメソッドです。
これをオーバーライドして、再定義していきます。
一度一気に完成品をお見せします。
@Override
public void explode() {
Vec3 playerLookVec= Objects.requireNonNull(player).getLookAngle();
Level level= Objects.requireNonNull(this.getExploder()).getLevel();
Entity source=this.getExploder();
double x=this.getPosition().x;
double y=this.getPosition().y;
double z=this.getPosition().z;
level.gameEvent(source, GameEvent.EXPLODE,new BlockPos(x,y,z));
Set<BlockPos> set= Sets.newHashSet();
for(int j=0; j<16; j++){
for(int k=0; k<16; k++){
for(int l=0; l<16; l++){
if(j==0||j==15||k==0||k==15||l==0||l==15){
double d0=(double) j/15.0D*2.0D-1.0D;
double d1=(double) k/15.0D*2.0D-1.0D;
double d2=(double) l/15.0D*2.0D-1.0D;
double d3=Math.sqrt(d0*d0+d1*d1+d2*d2);
d0/=d3;
d1/=d3;
d2/=d3;
float initialStrength=this.radius*(0.7F+level.random.nextFloat()*0.6F);
float f=initialStrength;
Vec3 rayVector=new Vec3(d0,d1,d2);
double dotProduct=playerLookVec.dot(rayVector);
if(dotProduct>0.7){
f=initialStrength*3.0F;
}
double d4=x;
double d6=y;
double d8=z;
for(float f1=0.3F; f>0.0F; f-=0.22500001F){
BlockPos blockPos=new BlockPos(d4,d6,d8);
BlockState blockState=level.getBlockState(blockPos);
FluidState fluidState=level.getFluidState(blockPos);
if(!level.isInWorldBounds(blockPos)){
break;
}
Optional<Float> optional=this.damageCalculator.getBlockExplosionResistance(this,level,blockPos,blockState,fluidState);
if(optional.isPresent()){
float resistance=optional.get();
f-=(resistance/powerFactor+0.3F)*0.3F;
}
if(f>0.0F&&this.damageCalculator.shouldBlockExplode(this,level,blockPos,blockState,f)){
set.add(blockPos);
}
d4 += d0 * (double)0.3F;
d6 += d1 * (double)0.3F;
d8 += d2 * (double)0.3F;
}
}
}
}
}
this.getToBlow().addAll(set);
float f2=this.radius*2.0F;
int k1= Mth.floor(x-(double) f2-1.0D);
int l1 = Mth.floor(x + (double)f2 + 1.0D);
int i2 = Mth.floor(y - (double)f2 - 1.0D);
int i1 = Mth.floor(y + (double)f2 + 1.0D);
int j2 = Mth.floor(z - (double)f2 - 1.0D);
int j1 = Mth.floor(z + (double)f2 + 1.0D);
List<Entity> list=level.getEntities(source, new AABB(k1, i2, j2, l1, i1, j1));
net.minecraftforge.event.ForgeEventFactory.onExplosionDetonate(level, this, list, f2);
Vec3 vec3 = new Vec3(x, y, z);
for (Entity entity : list) {
if (entity == this.player) {
continue;
}
if (!entity.ignoreExplosion()) {
double d12 = Math.sqrt(entity.distanceToSqr(vec3)) / f2;
if (d12 < 1.0D) {
double d5 = entity.getX() - x;
double d7 = (entity instanceof PrimedTnt ? entity.getY() : entity.getEyeY()) - y;
double d9 = entity.getZ() - z;
double d13 = Math.sqrt(d5 * d5 + d7 * d7 + d9 * d9);
if (d13 != 0.0D) {
d5 /= d13;
d7 /= d13;
d9 /= d13;
double d14 = getSeenPercent(vec3, entity);
double d10 = (1.0D - d12) * d14;
entity.hurt(this.getDamageSource(), (float) ((int) ((d10 * d10 + d10) / 2.0D * 7.0D * (double) f2 + 1.0D)));
double d11 = d10;
if (entity instanceof LivingEntity) {
d11 = ProtectionEnchantment.getExplosionKnockbackAfterDampener((LivingEntity) entity, d10);
}
entity.setDeltaMovement(entity.getDeltaMovement().add(d5 * d11, d7 * d11, d9 * d11));
}
}
}
}
}
まあ何かウダウダと書いてありますが、大きく「爆発させる範囲の探索」と「爆発した後の処理」に分けることができます。早速見ていきましょう。
Vec3 playerLookVec= Objects.requireNonNull(player).getLookAngle();
Level level= Objects.requireNonNull(this.getExploder()).getLevel();
Entity source=this.getExploder();
double x=this.getPosition().x;
double y=this.getPosition().y;
double z=this.getPosition().z;
level.gameEvent(source, GameEvent.EXPLODE,new BlockPos(x,y,z));
Set<BlockPos> set= Sets.newHashSet();
これは、このメソッドの中で用いていく変数を定義している部分になります。
playerLookVecはプレイヤーの視線方向のベクトルを取得しています。
levelは乱数を生成したり、ブロックの状態を取得したりするのに用います。
sourceは爆発源となるエンティティで親クラスのヘルパーメソッドから取得しており、nullにすることもできます。
x, y, zはそれぞれ座標を表し、こちらも親クラスのヘルパーメソッドから取得しています。
level.gameEventで上で取得したx, y, zの座標に爆発源と爆発のイベントを割り当てます。
setは爆発範囲にあるブロックを格納するために使います。
for(int j=0; j<16; j++){
for(int k=0; k<16; k++){
for(int l=0; l<16; l++){
if(j==0||j==15||k==0||k==15||l==0||l==15){
double d0=(double) j/15.0D*2.0D-1.0D;
double d1=(double) k/15.0D*2.0D-1.0D;
double d2=(double) l/15.0D*2.0D-1.0D;
double d3=Math.sqrt(d0*d0+d1*d1+d2*d2);
d0/=d3;
d1/=d3;
d2/=d3;
float initialStrength=this.radius*(0.7F+level.random.nextFloat()*0.6F);
float f=initialStrength;
Vec3 rayVector=new Vec3(d0,d1,d2);
double dotProduct=playerLookVec.dot(rayVector);
if(dotProduct>0.7){
f=initialStrength*3.0F;
}
double d4=x;
double d6=y;
double d8=z;
for(float f1=0.3F; f>0.0F; f-=0.22500001F){
BlockPos blockPos=new BlockPos(d4,d6,d8);
BlockState blockState=level.getBlockState(blockPos);
FluidState fluidState=level.getFluidState(blockPos);
if(!level.isInWorldBounds(blockPos)){
break;
}
Optional<Float> optional=this.damageCalculator.getBlockExplosionResistance(this,level,blockPos,blockState,fluidState);
if(optional.isPresent()){
float resistance=optional.get();
f-=(resistance/powerFactor+0.3F)*0.3F;
}
if(f>0.0F&&this.damageCalculator.shouldBlockExplode(this,level,blockPos,blockState,f)){
set.add(blockPos);
}
d4 += d0 * (double)0.3F;
d6 += d1 * (double)0.3F;
d8 += d2 * (double)0.3F;
}
}
}
}
}
ここら辺は爆破範囲の探索となっています。
大体内部実装そのままなのですが、処理の追加を行っているところは、
float initialStrength=this.radius*(0.7F+level.random.nextFloat()*0.6F);
float f=initialStrength;
Vec3 rayVector=new Vec3(d0,d1,d2);
double dotProduct=playerLookVec.dot(rayVector);
if(dotProduct>0.7){
f=initialStrength*3.0F;
}
になっています。具体的には、実現したい「視線方向の爆発力を大きくする」部分を実装しており、float型のinitialStrengthを通常の威力として、rayVectorで色々な方向を持つベクトルを作り、
double dotProduct=playerLookVec.dot(rayVector);
によってプレイヤーの視線方向のベクトルと内積をとります。
内積は2、3次元上では2つのベクトルのなす角の情報を持つので、
if(dotProduct>0.7){
f=initialStrength*3.0F;
}
というif文で、内積の値が0.7より大きい場合、つまり2つのベクトルの方向が大体一致している(具体的には45度より小さい)時は威力を3倍にする処理を実装します。
後の処理は指定された座標のブロックの状態や、流体としての状態、ワールドの境界にないか、爆発の耐性はどれくらいかなどを総合して、爆発させるブロックを上で定義しておいたsetに追加していっています。
this.getToBlow().addAll(set);
このコードで先ほど加工したsetを親クラスに存在する、最終的に爆発させるブロックが格納される場所に送ります。
続いてダメージ処理を与える部分を見ていきましょう。
float f2=this.radius*2.0F;
int k1= Mth.floor(x-(double) f2-1.0D);
int l1 = Mth.floor(x + (double)f2 + 1.0D);
int i2 = Mth.floor(y - (double)f2 - 1.0D);
int i1 = Mth.floor(y + (double)f2 + 1.0D);
int j2 = Mth.floor(z - (double)f2 - 1.0D);
int j1 = Mth.floor(z + (double)f2 + 1.0D);
List<Entity> list=level.getEntities(source, new AABB(k1, i2, j2, l1, i1, j1));
net.minecraftforge.event.ForgeEventFactory.onExplosionDetonate(level, this, list, f2);
Vec3 vec3 = new Vec3(x, y, z);
for (Entity entity : list) {
if (entity == this.player) {
continue;
}
if (!entity.ignoreExplosion()) {
double d12 = Math.sqrt(entity.distanceToSqr(vec3)) / f2;
if (d12 < 1.0D) {
double d5 = entity.getX() - x;
double d7 = (entity instanceof PrimedTnt ? entity.getY() : entity.getEyeY()) - y;
double d9 = entity.getZ() - z;
double d13 = Math.sqrt(d5 * d5 + d7 * d7 + d9 * d9);
if (d13 != 0.0D) {
d5 /= d13;
d7 /= d13;
d9 /= d13;
double d14 = getSeenPercent(vec3, entity);
double d10 = (1.0D - d12) * d14;
entity.hurt(this.getDamageSource(), (float) ((int) ((d10 * d10 + d10) / 2.0D * 7.0D * (double) f2 + 1.0D)));
double d11 = d10;
if (entity instanceof LivingEntity) {
d11 = ProtectionEnchantment.getExplosionKnockbackAfterDampener((LivingEntity) entity, d10);
}
entity.setDeltaMovement(entity.getDeltaMovement().add(d5 * d11, d7 * d11, d9 * d11));
}
}
}
}
}
また何かゴチャゴチャと書いてありますが、やっていることはシンプルです。
k, l, jは範囲を指定するパラメータで、
List<Entity> list=level.getEntities(source, new AABB(k1, i2, j2, l1, i1, j1));
によって範囲内にいるすべてのエンティティをリストに格納します。
ここで取得したエンティティを
for (Entity entity : list) {
}
の拡張for文で1つずつ持ってきて、それぞれに処理を加えていくわけですが、
ここでこの記事の最大の目的である「プレイヤーだけダメージを受けない」機能を割り込ませます。
if (entity == this.player) {
continue;
}
これにより、for文を回す中でPlayerを取得した場合は、それ以降の処理をキャンセルして、次のエンティティの処理に移させることで、ダメージの処理を素通りさせることができるわけです。
continueとかあんまりに何に使うのか分からなかったけど、「プレイヤーだけダメージを受けない爆発を作る」ためにあったんですね〜
後のコードはダメージ処理に加えて、着火されたTNTが範囲にいた場合の処理とかも書いてあったり、エンティティがダメージ軽減のエンチャントを持っている場合や、爆発耐性を持っている場合などを総合して、そのエンティティをどれくっらい吹き飛ばすかなどの処理も書いてあります。
時間がある人は見てみてください。
爆発したブロックを壊したり、アイテムをドロップさせる処理は親クラスにある他のメソッドがやってくれるので書かなくて大丈夫です!
オブジェクト指向最高!
どこかでインスタンスを作成して実際に見てみよう
クラスを作っただけでは意味ないので実際に「敵を攻撃したら爆発する剣」を作ってみましょう!
ExplosionSwordクラスの作成
package com.example.examplemod.mc_05_mysword;
import com.example.examplemod.explosion.CustomExplosion;
import net.minecraft.world.damagesource.DamageSource;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.*;
import net.minecraft.world.level.Explosion;
import org.jetbrains.annotations.NotNull;
public class ExplosionSword extends SwordItem {
public ExplosionSword()
{
super(
Tiers.NETHERITE,
10,
-2.7F,
new Item.Properties().tab(CreativeModeTab.TAB_COMBAT).rarity(Rarity.EPIC)
);
}
@Override
public boolean hurtEnemy(@NotNull ItemStack itemStack, @NotNull LivingEntity target, @NotNull LivingEntity attacker) {
CustomExplosion customExplosion=new CustomExplosion(
target.level,
target,
DamageSource.explosion(target),
null,
target.getX(),
target.getY(),
target.getZ(),
2.0F,
false,
Explosion.BlockInteraction.NONE,
3,
(Player) attacker
);
customExplosion.explode();
customExplosion.finalizeExplosion(true);
if(target.level instanceof ServerLevel serverLevel && attacker instanceof Player){
serverLevel.sendParticles(ParticleTypes.EXPLOSION_EMITTER,target.getX(),target.getY(),target.getZ(),1,0.0D,0.0D,0.0D,0.0D);
serverLevel.sendParticles(ParticleTypes.EXPLOSION,target.getX(),target.getY(),target.getZ(),1,0.0D,0.0D,0.0D,0.0D);
target.level.playSound(null,target.blockPosition(), SoundEvents.GENERIC_EXPLODE, SoundSource.BLOCKS,1.0F,1.0F);
}
return super.hurtEnemy(itemStack, target, attacker);
}
}
剣のテクスチャは下のようににしました。TNTっぽさを目指したつもり。

こんな感じです!↓
終わりに
皆様も良きModライフを!!!!