Javaは分からないけどマイクラMODを作りたい #3 村人スキンを変える

前回、Entity の登録と村人を継承した機能を実装しました。実行してみましたがまさかのエラー。コンパイルは成功しているのでソースコード自体に問題はないようですが...。

1. エラーログを見る

こんな一文を見つけたのでエラーログを確認しに行きます。VSCodeには便利な機能で、ディレクトリパスを Ctrl + クリック で開くことができます。

[net.minecraft.init.Bootstrap:printToSYSOUT:629]: #@!@# Game crashed! Crash report saved to: #@!@# F:\GitHub\TakunologyMod\TakunologyMod\run\.\crash-reports\crash-2020-03-11_03.27.06-client.txt

さて、中身を見てみますか...。

---- Minecraft Crash Report ----
// Don't do that.

Time: 3/11/20 3:27 AM
Description: There was a severe problem during mod loading that has caused the game to fail

net.minecraftforge.fml.common.LoaderExceptionModCrash: Caught exception from TakunologyMod (takunologymod)
Caused by: java.lang.NullPointerException
    at jp.takunology.takunologymod.TakunologyMod.init(TakunologyMod.java:36)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at net.minecraftforge.fml.common.FMLModContainer.handleModStateEvent(FMLModContainer.java:626)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.google.common.eventbus.Subscriber.invokeSubscriberMethod(Subscriber.java:91)
    at com.google.common.eventbus.Subscriber$SynchronizedSubscriber.invokeSubscriberMethod(Subscriber.java:150)
    at com.google.common.eventbus.Subscriber$1.run(Subscriber.java:76)
    at com.google.common.util.concurrent.MoreExecutors$DirectExecutor.execute(MoreExecutors.java:399)
    at com.google.common.eventbus.Subscriber.dispatchEvent(Subscriber.java:71)
    at com.google.common.eventbus.Dispatcher$PerThreadQueuedDispatcher.dispatch(Dispatcher.java:116)
    at com.google.common.eventbus.EventBus.post(EventBus.java:217)
    at net.minecraftforge.fml.common.LoadController.sendEventToModContainer(LoadController.java:218)
    at net.minecraftforge.fml.common.LoadController.propogateStateMessage(LoadController.java:196)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.google.common.eventbus.Subscriber.invokeSubscriberMethod(Subscriber.java:91)
    at com.google.common.eventbus.Subscriber$SynchronizedSubscriber.invokeSubscriberMethod(Subscriber.java:150)
    at com.google.common.eventbus.Subscriber$1.run(Subscriber.java:76)
    at com.google.common.util.concurrent.MoreExecutors$DirectExecutor.execute(MoreExecutors.java:399)
    at com.google.common.eventbus.Subscriber.dispatchEvent(Subscriber.java:71)
    at com.google.common.eventbus.Dispatcher$PerThreadQueuedDispatcher.dispatch(Dispatcher.java:116)
    at com.google.common.eventbus.EventBus.post(EventBus.java:217)
    at net.minecraftforge.fml.common.LoadController.distributeStateMessage(LoadController.java:135)
    at net.minecraftforge.fml.common.Loader.initializeMods(Loader.java:744)
    at net.minecraftforge.fml.client.FMLClientHandler.finishMinecraftLoading(FMLClientHandler.java:336)
    at net.minecraft.client.Minecraft.init(Minecraft.java:582)
    at net.minecraft.client.Minecraft.run(Minecraft.java:422)
    at net.minecraft.client.main.Main.main(Main.java:118)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at net.minecraft.launchwrapper.Launch.launch(Launch.java:135)
    at net.minecraft.launchwrapper.Launch.main(Launch.java:28)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at net.minecraftforge.gradle.GradleStartCommon.launch(GradleStartCommon.java:97)
    at GradleStart.main(GradleStart.java:25)


A detailed walkthrough of the error, its code path and all known details is as follows:
---------------------------------------------------------------------------------------

こいつぁひでぇと思いましたが、上から順番にそれっぽいバグを修正していきます。まずはこの1文ですね。

at jp.takunology.takunologymod.TakunologyMod.init(TakunologyMod.java:36)

TakunologyMod.java:36 とあるので、恐らくメインクラスの36行目で引っかかっているのでしょう。ということで、(画像の水色の下線部分を)コメントアウトして再度実行してみました。ちなみに36行目はログ出力に関するところです。

f:id:takunology:20200311032525j:plain

直りました...ね。たったこれだけかよと思いましたが簡単に起動してくれました。 ちゃんとスポーンエッグが登録されているか確認します。

f:id:takunology:20200311034118p:plain

登録した Entity が表示されていますね。今回は村人の機能そのまま継承しているのでスポーンさせてもただの村人です。ちなみに、職業はいろいろでした。おそらくIDを0にしておくとランダムでスポーンするのだと思います。

f:id:takunology:20200311034456p:plain

2. スキンの取得

機能は時間が結構かかりそうなので、まずは見た目をどうにかしていきます。スキンは自作する余裕がないというか画伯になってしまうので、こちらのサイト様からお借りします。

bambooo-skin.blogspot.com

画像ファイル名は humanunit_001.png などにしておきます。ファイル名は小文字で認識されるので、大文字を入れても読み取ってくれませんでした。

スキン画像は /resources/assets/takunologymod/textures/entity の中に保存します。

3 Renderを実装する

まずは人間ユニットのレンダラーを実装していきます。どこのディレクトリにクラスを作っているかはパッケージの部分を見ればわかると思います。

package jp.takunology.takunologymod.entity.render;

import jp.takunology.takunologymod.TakunologyMod;
import jp.takunology.takunologymod.entity.HumanUnit;
import jp.takunology.takunologymod.entity.model.ModelHumanUnit;
import net.minecraft.client.renderer.entity.RenderLiving;
import net.minecraft.client.renderer.entity.RenderManager;
import net.minecraft.util.ResourceLocation;

public class RenderHumanUnit extends RenderLiving<HumanUnit> 
{
    public static final ResourceLocation TEXTURES = new ResourceLocation(TakunologyMod.MODID + ":textures/entity/humanunit_001.png");

    public RenderHumanUnit(RenderManager manager)
    {
        super(manager, new ModelHumanUnit(1, true), 0.5f);
    }
    
    @Override
    protected ResourceLocation getEntityTexture(HumanUnit entity)
    {
        return TEXTURES;
    }

    @Override
    protected void applyRotations(HumanUnit entityLiving, float p_77043_2_, float rotationYaw, float partialTicks) 
    {
        super.applyRotations(entityLiving, p_77043_2_, rotationYaw, partialTicks);
    }
}

ResourceLocation はテクスチャを指定するために使用するクラスです。インスタンス生成する際に、そのテクスチャを指定することで参照できます。

RenderLiving はよく分かりませんでした。ただ、クラス内を覗いてみるとレンダラー管理、テクスチャ読み込み、影の追加などがありました。おそらく描画関係を管理しているのだと思います。

applyRotations は回転の描画関係を処理するメソッドですね。覗いてみるとあらゆる回転角を設定されていますが、パラメータが多くて弄ろうとは思えませんでした。細かい動きをさせたいならオーバーライドしていくといいと思います。

4. レンダラーハンドラの登録

次に描画イベント登録用クラスを書いていきます。

package jp.takunology.takunologymod.util.handler;

import jp.takunology.takunologymod.entity.HumanUnit;
import jp.takunology.takunologymod.entity.render.RenderHumanUnit;
import net.minecraft.client.renderer.entity.Render;
import net.minecraft.client.renderer.entity.RenderManager;
import net.minecraftforge.fml.client.registry.IRenderFactory;
import net.minecraftforge.fml.client.registry.RenderingRegistry;

public class RenderHandler
{
    public static void registerEntityHandlers()
    {
        RenderingRegistry.registerEntityRenderingHandler(HumanUnit.class, new IRenderFactory<HumanUnit>() 
        {
            @Override
            public Render<? super HumanUnit> createRenderFor(RenderManager manager)
            {
                return new RenderHumanUnit(manager);
            }
        });
    }
}

registerEntityRenderingHandler は描画イベントを登録するためのメソッドですね。HumanUnit という Entity クラスに登録されている描画処理をわたすことで、そのスキンで初期化されることになるのだと思います。で、その描画をオーバーライドして変更するのでしょうか...?あまり情報が得られなかったので推測です。

登録クラスを書いたので、もちろんメインのクラスでも描画イベントの初期化を追記していきます。

@EventHandler
public void preInit(FMLPreInitializationEvent event)
{
    //自作したエンティティを起動時に読み込ませる
    EntityInit.registerEntitys();
    //エンティティのレンダーを読み込ませる
    RenderHandler.registerEntityHandlers();
}

5. モデルの登録

モデルは体のパーツを定義するところです。足、顔、頭、手、腕などすべての部位を定義しなければなりません。面倒なので形だけ作って実行してしまいました。

package jp.takunology.takunologymod.entity.model;

public class ModelHumanUnit extends ModelBiped
{
    public ModelHumanUnit()
    {
    
    }
}

f:id:takunology:20200311071114p:plain

まぁ、定義していないのでそうなりますよねーw

透明人間になりましたw

6. モデルをちゃんと書く

ちゃんと書くのは面倒ですね。人間ユニットなのでプレイヤーのモデルがないかと適当に打ち込んでみたら見つけました。ModelPlayer というクラスがあるのですが、さらに継承されていて複雑でした。とりあえずこのクラスをコピペして、良い感じに書いたのがこちらです。

package jp.takunology.takunologymod.entity.model;

import net.minecraft.client.model.ModelBiped;
import net.minecraft.client.model.ModelRenderer;
import net.minecraft.client.renderer.GlStateManager;
import net.minecraft.entity.Entity;
import net.minecraft.util.EnumHandSide;

public class ModelHumanUnit extends ModelBiped
{
    public ModelRenderer bipedLeftArmwear;
    public ModelRenderer bipedRightArmwear;
    public ModelRenderer bipedLeftLegwear;
    public ModelRenderer bipedRightLegwear;
    public ModelRenderer bipedBodyWear;
    private final ModelRenderer bipedCape;
    private final ModelRenderer bipedDeadmau5Head;
    private final boolean smallArms;

    public ModelHumanUnit(float modelSize, boolean smallArmsIn)
    {
        super(modelSize, 0.0F, 64, 32);
        this.smallArms = smallArmsIn;
        this.bipedDeadmau5Head = new ModelRenderer(this, 24, 0);
        this.bipedDeadmau5Head.addBox(-3.0F, -6.0F, -1.0F, 6, 6, 1, modelSize);
        this.bipedCape = new ModelRenderer(this, 0, 0);
        this.bipedCape.setTextureSize(64, 32);
        this.bipedCape.addBox(-5.0F, 0.0F, -1.0F, 10, 16, 1, modelSize);

//長いので省略 ココから下は ModelPlayer クラスを参照してみてください。

色々書いてあるのですが、super の部分でテクスチャの幅と高さを変更しています。配布されていたものが 64 × 32 だったのですが、ModelPlayer クラスは 64 × 64 でうまく反映されなかったためです。オーバーライドしようとしてもコンストラクタにはできないようだったので、仕方なく写経しました。

7. 動かしてみる

これで Entity の描画が実装できたので実行してみます。

f:id:takunology:20200311072058p:plain

無事に人間ユニットとして実行できています。ただ、機能は村人を継承したままなので交易ができます。