初心者modderの備忘録

マイクラのmodを作りたくて、初めて見たのですが難しくて忘れそうなので自分用の備忘録も兼ねてブログにしようと思います

7日目 MOBへの機能の追加

前回作ったMOBに機能を追加していきたいと思います。

とりあえずこんな感じの動きのものをつくりましたっていう動画あげておきます。


video1.mp4


編集したのは主に前回つくったEntitySample.javaです。
全部乗せるととても長くなってしまうので、編集または追加した箇所だけ載せて、
残りは最後にまとめて載せておきます。

EntitySample.java

public class EntitySample extends EntityAnimal
{
      ~~~~

	@Override
	public EntityAgeable createChild(EntityAgeable ageable) {
		return new EntitySample(this.world);
	}
}

まず、継承するクラスをEntityLivingからEntityAnimalに変えました。
イクラのEntityAnimalは繁殖できる生物のことを表しています。なので、Animalを継承すると必ず EntityAgeable createChildをオーバーライドする必要があります。
今回は自分と同じ種類であるEntitySampleを返していますが、ここに違うEntityをいれることで別の生き物を産むこともできます。


こどものサイズを小さくする場合は、Modelクラスで変更します。
ModelSample.java

    public void render(Entity entityIn, float limbSwing, float limbSwingAmount, float ageInTicks, float netHeadYaw, float headPitch, float scale)
    {
        this.setRotationAngles(limbSwing, limbSwingAmount, ageInTicks, netHeadYaw, headPitch, scale, entityIn);

        if (this.isChild)
        {
             GlStateManager.pushMatrix();
             GlStateManager.scale(0.3F, 0.3F, 0.3F);
             GlStateManager.translate(0.0F, 54.0F * scale, 0.0F);
             this.shape1.render(scale);
             this.shape2.render(scale);
             this.shape3.render(scale);
             GlStateManager.popMatrix();
        }
        else
        {
            this.shape1.render(scale);
            this.shape2.render(scale);
            this.shape3.render(scale);
        }
    }


レンダリングのスケールを親と子で場合分けしています。ここはバニラのソースをほぼコピペしたので、あまり理解していないのですが
GlStateManager.scale(0.3F, 0.3F, 0.3F)でスケールを何倍するかを設定していると思います。
そのまま縮小すると、小さくなったMOBが宙に浮いてしまうので
GlStateManager.translate(0.0F, 54.0F * scale, 0.0F);
の第2引数で表示するY座標を調整しています。

スケールとY座標のオフセットとの値の関連がわからなかったのですが、バニラのウサギなどを参考にすると
0.4 → 36
0.5 → 24
0.6 → 16
となっていたのでこれくらいの値を目安に微調整しました。

次に簡単なところから編集していきます。

    public static void registerFixesSample(DataFixer fixer)
    {
        EntityLiving.registerFixesMob(fixer, EntitySample.class);
    }

    protected void applyEntityAttributes()
    {
        super.applyEntityAttributes();
        this.getEntityAttribute(SharedMonsterAttributes.MAX_HEALTH).setBaseValue(13.0D);
    }

    public float getEyeHeight()
    {
        return this.height;
    }


このへんは他のMOBのコードをコピペして書き換えました
registerFixesSampleはよくわかりません。
applyEntityAttributesは体力を設定しています。今回は設定していませんが、動く早さなども変えられます。
EyeHeighはたぶん目線の高さですかね。



つぎにこのMOBは空を飛ぶようにしたので、コウモリの動きをコピペしました。

    public void onUpdate()
    {
        super.onUpdate();
        
        if(!this.getEntityWorld().isAnyPlayerWithinRangeAt(posX, posY, posZ, 5D))
        this.motionY *= 0.6000000238418579D;
        

        //コウモリの動きとは関係ない
        if(rand.nextInt(10000)<1) {
        	if(!world.isRemote)
        	this.dropItem(Items.BEEF, 4);
        }
    }

    protected void updateAITasks()
    {
        super.updateAITasks();

            if (this.spawnPosition != null && (!this.world.isAirBlock(this.spawnPosition) || this.spawnPosition.getY() < 1))
            {
                this.spawnPosition = null;
            }

            if (this.spawnPosition == null || this.rand.nextInt(30) == 0 || this.spawnPosition.distanceSq((double)((int)this.posX), (double)((int)this.posY), (double)((int)this.posZ)) < 4.0D)
            {
                this.spawnPosition = new BlockPos((int)this.posX + this.rand.nextInt(7) - this.rand.nextInt(7), (int)this.posY + this.rand.nextInt(6) - 2, (int)this.posZ + this.rand.nextInt(7) - this.rand.nextInt(7));
            }

            if(!this.getEntityWorld().isAnyPlayerWithinRangeAt(posX, posY, posZ, 5D))
            {
            	double d0 = (double)this.spawnPosition.getX() + 0.5D - this.posX;
            	double d1 = (double)this.spawnPosition.getY() + 0.1D - this.posY;
            	double d2 = (double)this.spawnPosition.getZ() + 0.5D - this.posZ;
            	this.motionX += (Math.signum(d0) * 0.5D - this.motionX) * 0.10000000149011612D;
            	this.motionY += (Math.signum(d1) * 0.699999988079071D - this.motionY) * 0.10000000149011612D;
            	this.motionZ += (Math.signum(d2) * 0.5D - this.motionZ) * 0.10000000149011612D;
            	float f = (float)(MathHelper.atan2(this.motionZ, this.motionX) * (180D / Math.PI)) - 90.0F;
            	float f1 = MathHelper.wrapDegrees(f - this.rotationYaw);
            	this.moveForward = 0.5F;
            	this.rotationYaw += f1;
            }
    }

    @Override
    public void fall(float distance, float damageMultiplier)
    {
    }

これもほとんどコウモリのコードの丸ぱくりです。
ただコウモリはずっと飛んでいるんですが、このMOBはプレイヤーが近くに来たら降りてくるようにしたと思いました。
そこで5行目と30行目の if(!this.getEntityWorld().isAnyPlayerWithinRangeAt(posX, posY, posZ, 5D))を追加します。
第1~3引数がこのEntityの座標、第4引数が判定する範囲です。

余談ですけど、この処理を見つけるまでに2時間くらいかかりました。最初はプレイヤーの座標を獲得して、そことこのMOBの座標で処理しようとしたんですけど、プレイヤーの座標にアクセスできるメソッドが見つからなくて、worldクラスの中身を眺めてるときにこのメソッドを見つけました。

それぞれこのif文の中身が飛ぶ処理です。

最後にオーバーライドしているfallはEntityが落下ダメージをうける処理です。これをオーバーライドして処理を空欄にすることで落下ダメージをうけないように設定できます。


また、onUpdate()に入れている最後の処理はdropItemの動作確認です。dropItemは呼び出されるとItemEntityをドロップするようです。今回は毎tic、1/10000の確率で牛肉4個をドロップするようになっています。1/10000と聞くと低いような感じがしますが、1/1000だと辺り一面牛肉だらけになります笑

つぎにEntityのAIを設定します。AIというとうろうろするとか、攻撃するとかそういうやつですね。

@SuppressWarnings({ "unchecked", "rawtypes" })
	protected void initEntityAI()
    {
        this.tasks.addTask(0, new EntityAISwimming(this));
        this.tasks.addTask(1, new EntityAIPanic(this, 2.0D));
        this.tasks.addTask(1, new EntityAITempt(this, 1.25D, Items.DIAMOND, false));
        this.tasks.addTask(2, new EntityAIMate(this, 1.0D));
        this.tasks.addTask(3, new EntityAITempt(this, 1.25D, Items.WHEAT, false));
        this.tasks.addTask(3, new EntityAIAvoidEntity(this, EntityPlayer.class, 8F, 1.8D, 2.4D));
        this.tasks.addTask(4, new EntityAIFollowParent(this, 1.25D));
        this.tasks.addTask(6, new EntityAIWatchClosest2(this, EntityPlayer.class, 20.0F, 100F));
        this.tasks.addTask(7, new EntityAILookIdle(this));
    }

addTaskの第1引数が優先度になっています。上から、
泳ぐ
攻撃されたときパニック
ダイヤでついてくる
子供を産む
小麦でついてくる
プレイヤーをさける
親について行く
プレイヤーをみる
きょろきょろする
です。
Mateより優先度の高いTemptのアイテムではハートが出なくなってただ近寄ってくるだけになるのではないかと思っています。
またdouble型の引数はほとんどそれを行うときのスピードです。


次は、エンティティを右クリックしたときのアクションを設定します。

    public boolean processInteract(EntityPlayer player, EnumHand hand)
    {
        ItemStack itemstack = player.getHeldItem(hand);

        if (itemstack.getItem() == Items.DIAMOND && !this.isChild())
        {
            
        	if (!player.capabilities.isCreativeMode)
            {
                itemstack.shrink(1);
            }
        	
        	if(!world.isRemote)
        	this.dropItem(Items.GOLDEN_APPLE, 4);

            return true;
        }
        else
        {
            return super.processInteract(player, hand);
        }
    }


ダイヤモンドを持っている状態で右クリックすると、金のリンゴを落とすように設定しました。


次は、落ちている牛肉にふれると牛肉を回収して、ダイヤモンドブロックをドロップします。

    public void onLivingUpdate()
    {
        super.onLivingUpdate();
        this.world.profiler.startSection("looting");

        if (!this.world.isRemote &&  !this.dead && net.minecraftforge.event.ForgeEventFactory.getMobGriefingEvent(this.world, this))
        {
            for (EntityItem entityitem : this.world.getEntitiesWithinAABB(EntityItem.class, this.getEntityBoundingBox().grow(1.0D, 0.0D, 1.0D)))
            {
                if (!entityitem.isDead && !entityitem.getItem().isEmpty())
                {
                    this.updateEquipmentIfNeeded(entityitem);
                }
            }
        }

        this.world.profiler.endSection();
    }
    
    
    protected void updateEquipmentIfNeeded(EntityItem itemEntity)
    {
        ItemStack itemstack = itemEntity.getItem();
        Item item = itemstack.getItem();
        
        if(item == Items.BEEF && counter>400) {
        	itemEntity.setDead();
        	world.spawnEntity(new EntityItem(world, posX, posY, posZ, new ItemStack(Blocks.DIAMOND_BLOCK)));
        	counter=0;
        }
        counter++;
    }

updateEquipmentIfNeededで処理をつくって、それをonLivingUpdate()で呼んでいます。
EntityLivingのコードを参考にしました。


最後にサウンドを追加しました。

    protected SoundEvent getHurtSound(DamageSource damageSourceIn)
    {
        return testmod.SMAPLE_HURTSOUND;
    }

    protected SoundEvent getDeathSound()
    {
        return testmod.SAMPLE_DEATHSOUND;
    }

Entityクラスにはこれだけ書いておき、メインのクラスでサウンドの登録を行う必要があります。

testmod.java

package testmod;

import net.minecraft.client.renderer.entity.Render;
import net.minecraft.client.renderer.entity.RenderManager;
import net.minecraft.entity.EnumCreatureType;
import net.minecraft.init.Biomes;
import net.minecraft.util.ResourceLocation;
import net.minecraft.util.SoundEvent;
import net.minecraftforge.fml.client.registry.IRenderFactory;
import net.minecraftforge.fml.client.registry.RenderingRegistry;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.event.FMLInitializationEvent;
import net.minecraftforge.fml.common.event.FMLPreInitializationEvent;
import net.minecraftforge.fml.common.registry.EntityRegistry;
import net.minecraftforge.fml.common.registry.ForgeRegistries;

@Mod(modid = testmod.MOD_ID, name = testmod.MOD_NAME, version = testmod.MOD_VERSION)
public class testmod {
    public static final String MOD_ID = "testmod";
    public static final String MOD_NAME = "testmod";
    public static final String MOD_VERSION = "1.0.0";
    
    public static SoundEvent SAMPLE_DEATHSOUND,SMAPLE_HURTSOUND;
    
    @Mod.EventHandler
	public void preInit(FMLPreInitializationEvent event) {
    	
    	SAMPLE_DEATHSOUND  = new SoundEvent(new ResourceLocation("testmod", "sample"))
    	.setRegistryName("sample");
    	
    	SMAPLE_HURTSOUND  = new SoundEvent(new ResourceLocation("testmod", "sample2"))
    	.setRegistryName("sample2");
    	
    	ForgeRegistries.SOUND_EVENTS.register(SAMPLE_DEATHSOUND);
    
    	if(event.getSide().isClient()) {
    		RenderingRegistry.registerEntityRenderingHandler(EntitySample.class, new IRenderFactory<EntitySample> (){
				@Override
				public Render<? super EntitySample> createRenderFor(RenderManager manager){
					return new RenderSample(manager, new ModelSample(), 0.3f);
				}
			});
    	}
    }
    
    @Mod.EventHandler
	public void Init(FMLInitializationEvent event) { 
    	EntityRegistry.registerModEntity(new ResourceLocation("sample"), EntitySample.class, "Sample", 0, this, 50, 1, true, 1000, 22);
    	EntityRegistry.addSpawn(EntitySample.class, 50, 6, 12, EnumCreatureType.MONSTER, Biomes.HELL,Biomes.PLAINS,Biomes.DEFAULT);
    } 
}


サウンドファイルは、
assets.testmod.soundsの中に、「sample.ogg」のようにogg形式の音声ファイルを入れておきます。
今回は鳴き声とかがなかったので、適当な音楽ファイルを切り取って、oggに変換してテストしました。
最後に音声ファイルを紐付けるため、jsonファイルを追加します。
assetsの直下にsounds.jsonを作ります。

sounds.jon

{

	"sample": {
		"category": "record",
		"sounds": [{
			"name": "testmod:sample",
			"stream": true
		}]
	},

	"sample2": {
		"category": "record",
		"sounds": [{
			"name": "testmod:sample2",
			"stream": true
		}]
	}
}

パッケージエクスプローラーはこんな感じです。

f:id:json_fileman:20190102150348p:plain


これでMOBの完成です。

ソースコードをあげておきます。

                                                                                                                                                                      • -

2019年4月29日追記

ドロップアイテムについて記載していなかったので追記。

ドロップアイテムを変更するには、EntityLivingの
getLootTableメソッドをオーバーライドして、ルートテーブルを記載したjsonファイルのロケーションを紐付けます。

@Override
protected ResourceLocation getLootTable()
{
	return LootTableList.register(new ResourceLocation(Main.MOD_ID, "entity_sample"));;
}


ルートテーブルのjsonファイルは
assets/modid/loot_tables
に保存します。


中身は次のような感じで記載します。

entity_sample.json

{
    "pools": [
        {
            "name": "sample",
            "rolls": 1,
            "entries": [
                {
                    "type": "item",
                    "name": "minecraft:yellow_flower",
                    "weight": 1,
                    "functions": [
                        {
                            "function": "set_count",
                            "count": {
                                "min": 1,
                                "max": 3
                            }
                        },
                        {
                            "function": "looting_enchant",
                            "count": {
                                "min": 0,
                                "max": 1
                            }
                        }
                    ]
                }
            ]
        },
        {
            "name": "sample2",
            "rolls": 1,
            "entries": [
                {
                    "type": "item",
                    "name": "your_modid:your_mod_item",
                    "weight": 1,
                    "functions": [
                        {
                            "function": "set_count",
                            "count": {
                                "min": 1,
                                "max": 3
                            }
                        },
                        {
                            "function": "looting_enchant",
                            "count": {
                                "min": 0,
                                "max": 1
                            }
                        }
                    ]
                }
            ]
        }
    ]
}

4行目や31行目にnameというのがあると思うんですけど、これはバニラのjsonファイルには書いてない行です。でも自作したルートテーブルの場合は、nameがないと動かないので注意です。


ルートテーブルの書き方はバニラのファイルや、次のサイトを参考にしました。

[Minecraft] 「Loot Table (ドロップ表)」の使い方/JSONの書き方を徹底解説。各種設定項目を網羅! | ナポアンドットコム



testmod.java
EntitySample.java
ModelSample.java
RenderSample.java