iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 9
1

https://ithelp.ithome.com.tw/upload/images/20190924/20115823RZi3PIHuCb.png

延續昨天的工作,我們最後的工作就是要將這些拼圖湊在一起,完成我們的模組。

先看看我們現在有了什麼:

  • PigDoll_v2 : 一個有"實體進入世界的事件"方法的類別
  • EntityScalePig:一個可以自定義大小的實體類別
  • RenderScalePig:一個可以自定義大小的渲染類別

其實關於實體還有很多的功能可以自定義,我們這裡只需要關心自定義實體的大小就好。

  1. Minecraft一定是先載入模組與所有資源,因此註冊事件與註冊實體類別這些事情一定要先完成。我們昨天已經將實體註冊好,還缺將事件類別註冊:

    @EventHandler
    public void preInit(FMLPreInitializationEvent event) {
    ...略
    }
    
    @EventHandler
    public void init(FMLInitializationEvent event) {
        MinecraftForge.EVENT_BUS.register(new PigDoll_v2());
    }
    

    同樣的,不需要的事件就取消註冊,這邊只保留PigDoll_V2

  2. 再來,當豬實體加入世界後(EntityJoinWorldEvent),我們需要產生新的豬實體,並且是使用我們的EntityScalePig類別,這樣之後我們才可以更改豬的大小

    @SubscribeEvent
    public void pigDoll(EntityJoinWorldEvent event) {
        if (EntityPig.class.isAssignableFrom(event.entity.getClass())) {
            EntityScalePig pig = new EntityScalePig(event.entity.worldObj);
            event.entity.worldObj.spawnEntityInWorld(pig);
        }
    }
    

    這裡使用另一種方法來判斷今天加入到世界的實體是不是"豬" - isAssignableFrom()
    從左至右的意思是"EntityPig是不是與event.entity的類別相同,或是EntityPig是被event.entity繼承的類別"。

    因為第一次呼叫這個方法的時候,event.entity一定是一般的豬實體(EntityPig);只有在我們透過spawnEntityInWorld方法加入自定義EntityScalePig實體後,會再次觸發實體加入世界的事件 - 這時候event.entity取得的就是自定義大小的豬實體EntityPig。我們的行為是"1個EntityPig要產生多個EntityScalePig",所以這裡要用isAssignableFrom來接受兩種類別都可以產生新的豬。

    你可以發現到這裡只能用IF,不能用For迴圈來完成我們的工作。因為EntityJoinWorldEvent觸發的其中一個條件就是spawnEntityInWorld方法,而用For迴圈會造成無窮呼叫!
    https://ithelp.ithome.com.tw/upload/images/20190923/20115823BfIu0q416U.png
    這個觀念很重要,事件的發生會被所有的訂閱者 - 即事件類別 - 所看到並處理

    恩...覺得我有點像老人家廢話有點多,我們繼續下面一個步驟好了。

  3. 實體產生後,我們要變更兩個大小:碰撞箱渲染大小。這兩個動作只能在"客戶端"進行 (因為它是畫面產生的動作,不是固定的物件 - 你總不能仰賴伺服器端對每一個連線的客戶端都幫忙將畫面產生好吧?),所以我們需要昨天提到的@SideOnly(Side.CLIENT)註釋。另外,有一個事件是專門處理實體(Event),或更精確地說 - 活體(Living)的預先渲染動作,RenderLivingEvent.Pre。我們在PigDoll_v2類別內加入一個新的方法preRender

    @SubscribeEvent
    @SideOnly(Side.CLIENT)
    public void preRender(RenderLivingEvent.Pre event) {
    }
    
  4. 在上面的新方法內,我們要加上兩種大小的變更:

    @SubscribeEvent
    @SideOnly(Side.CLIENT)
    public void preRender(RenderLivingEvent.Pre event) {
        if(event.renderer instanceof RenderScalePig) {
            RenderScalePig render = (RenderScalePig) event.renderer;
            render.setScale(parserScale);
        }
    
        if (event.entity instanceof EntityScalePig)) {
            EntityScalePig pig = (EntityScalePig) event.entity;
            pig.scale(parserScale);
        }
    }
    

    這裡的會產生紅字parserScale不存在,我們先不管它。先看第一個IF判斷式,在預渲染事件中,當渲染的物件event.renderer是我們自定義的渲染類別RenderScalePig時,做一個強制轉換的動作 (就是這邊的RenderScalePig) event.render),因為有先判斷過了,所以這裡的強制轉換是安全的;接下來透過我們定義好的render.setScale()方法來改變渲染的大小比例是parserScale。
    後半段是將event.entity強制轉換成自定義的實體類別EntityScalePig,然後透過定義的pig.scale()改變碰撞箱大小比例是parserScale。

  5. parseScale是我們要動態改變的比例,這個參數我們可以透過event.entity.getCustomNameTag()來取得自定義的實體名稱,然後在實體名稱裡面加上我們想要的比例,因此名稱為:myPig-{比例}

    另外,程式碼到目前乍看之下好像沒問題,但我們忽略了RenderLivingEvent.Pre事件的觸發規則:只要當客戶端的活體需要做渲染時,這個事件就可能會被觸發一次,因此,每一次世界內有動作發生 (你按了方向鍵、旋轉鏡頭、豬移動、有新的動物產生等等),都會呼叫一次改變大小一次!
    render.setScale()方法沒問題,它本來就是每次渲染觸發都要重新改變;但pig.scale()是"改變目前的碰撞箱的大小乘上parserScale值"!我們需要修改這段讓它只會被呼叫一次。
    修改後的程式碼為:

    private List<String> firstRenderPig = new ArrayList<String>();
    
    @SubscribeEvent
    @SideOnly(Side.CLIENT)
    public void preRender(RenderLivingEvent.Pre event) {
        if (event.entity.getCustomNameTag().startsWith("myPig")) {
    
            String tag = event.entity.getCustomNameTag();
            float parserScale = Float.parseFloat(tag.split("-")[1]);
    
            if(event.renderer instanceof RenderScalePig) {
                RenderScalePig render = (RenderScalePig) event.renderer;
                render.setScale(parserScale);
            }
    
        if(!firstRenderPig.contains(tag)) {
            EntityScalePig pig = (EntityScalePig) event.entity;
            pig.scale(parserScale);
            firstRenderPig.add(tag);
        }
    
  6. 接下來,我們需要將自定義的渲染類別註冊到Minecraft內,不然我們的EntityScalePig是沒辦法被Minecraft識別如何進行渲染:

    private AtomicBoolean flag = new AtomicBoolean(false);
    
    @SubscribeEvent
    @SideOnly(Side.CLIENT)
    public void preRender(RenderLivingEvent.Pre event) {
        if (event.entity.getCustomNameTag().startsWith("myPig")) {
    
            String tag = event.entity.getCustomNameTag();
            float parserScale = Float.parseFloat(tag.split("-")[1]);
    
            if (flag.compareAndSet(false, true)) { 
                Minecraft.getMinecraft()
                .getRenderManager()
                .entityRenderMap
                .put(EntityScalePig.class,
                     new RenderScalePig(Minecraft.getMinecraft().getRenderManager(), new ModelPig(), 0.5f * parserScale));
            }
    ...後面省略
    
  7. 剩下的就是要處理我們的實體加入世界的事件了,這裡我們會完成下面的功能:

    • 第一個透過生怪蛋加入的豬(EntityPig),讓牠一進入到世界就死亡
    • 只處理距離我們玩家7格方塊的豬 (依此來判斷是我們生怪蛋與手動建立的豬,而不是世界產生的)
    • 我們一共產生5隻豬(EntityScalePig),並且在產生完後可以再次透過生怪蛋產生
    • 後一隻豬的大小是前一隻豬的大小 x 0.8
    • 每一隻豬的間隔1.2個方塊,往X軸方向排列
    • 自定義我們的豬名稱為myPig-{比例}
    • 將豬的AI拔掉,不讓牠動
      功能有點多,依序完成的程式碼如下
    private float scale = 1.0f;
    private AtomicInteger count = new AtomicInteger(0);
    
    @SubscribeEvent
    public void pigDoll(EntityJoinWorldEvent event) {
        if (event.entity.worldObj.isRemote) return;
        if (event.world.playerEntities.isEmpty()) return;
    
        EntityPlayer player = (EntityPlayer) event.entity.worldObj.playerEntities.get(0);
        float distance = event.entity.getDistanceToEntity(player);
    
        if (distance < 7.0f) {
            if (EntityPig.class.isAssignableFrom(event.entity.getClass())) {
    
                if(!event.entity.getCustomNameTag().startsWith("myPig")) {
                    event.entity.setDead();
                }
    
                if(count.compareAndSet(5, 0)) {
                    scale = 1.0f;
                    firstRenderPig.clear();
                    return;
                }
    
                scale = scale * 0.8f;
                EntityScalePig pig = new EntityScalePig(event.entity.worldObj);
                pig.setPosition(event.entity.posX - 1.2f, event.entity.posY, event.entity.posZ);
                pig.setCustomNameTag(String.format("myPig-%f", scale));
    
                // 將豬的AI行為去除
                EntityAIBase ai = new EntityAIBase() {
                    @Override
                    public boolean shouldExecute() {
                        return true;
                    }
                };
                ai.setMutexBits(0xFFFFFF);
                pig.tasks.addTask(0, ai);
    
                count.getAndIncrement();
                event.entity.worldObj.spawnEntityInWorld(pig);
            }
        }
    }
    

方便對照,最後完整程式碼如下:

PigDoll_v2.java

public class PigDoll_v2 {
    private float scale = 1.0f;
    private AtomicInteger count = new AtomicInteger(0);
    private AtomicBoolean flag = new AtomicBoolean(false);
    private List<String> firstRenderPig = new ArrayList<String>();
    
    @SubscribeEvent
    @SideOnly(Side.CLIENT)
    public void preRender(RenderLivingEvent.Pre event) {
        if (event.entity.getCustomNameTag().startsWith("myPig")) {

            String tag = event.entity.getCustomNameTag();
            float parserScale = Float.parseFloat(tag.split("-")[1]);

            if (flag.compareAndSet(false, true)) { 
                Minecraft.getMinecraft()
                .getRenderManager()
                .entityRenderMap
                .put(EntityScalePig.class,
                     new RenderScalePig(Minecraft.getMinecraft().getRenderManager(), new ModelPig(), 0.5f * parserScale));
            }
            
            if(event.renderer instanceof RenderScalePig) {
                RenderScalePig render = (RenderScalePig) event.renderer;
                render.setScale(parserScale);
            }

            if(!firstRenderPig.contains(tag)) {
                EntityScalePig pig = (EntityScalePig) event.entity;
                pig.scale(parserScale);
                firstRenderPig.add(tag);
            }
        }
    }
    
    @SubscribeEvent
    public void pigDoll(EntityJoinWorldEvent event) {
        if (event.entity.worldObj.isRemote) return;
        if (event.world.playerEntities.isEmpty()) return;

        EntityPlayer player = (EntityPlayer) event.entity.worldObj.playerEntities.get(0);
        float distance = event.entity.getDistanceToEntity(player);

        if (distance < 7.0f) {
            if (EntityPig.class.isAssignableFrom(event.entity.getClass())) {

                if(!event.entity.getCustomNameTag().startsWith("myPig")) {
                    event.entity.setDead();
                }

                if(count.compareAndSet(5, 0)) {
                    scale = 1.0f;
                    firstRenderPig.clear();
                    return;
                }

                scale = scale * 0.8f;
                EntityScalePig pig = new EntityScalePig(event.entity.worldObj);
                pig.setPosition(event.entity.posX - 1.2f, event.entity.posY, event.entity.posZ);
                pig.setCustomNameTag(String.format("myPig-%f", scale));

                // 將豬的AI行為去除
                EntityAIBase ai = new EntityAIBase() {
                    @Override
                    public boolean shouldExecute() {
                        return true;
                    }
                };
                ai.setMutexBits(0xFFFFFF);
                pig.tasks.addTask(0, ai);

                count.getAndIncrement();
                event.entity.worldObj.spawnEntityInWorld(pig);
            }
        }
    }

Main.java

@Mod(modid = Main.MODID, version = Main.VERSION)
public class Main {

    // 作為辨識模組的唯一識別碼 (此值不可與其他模組名稱相同)
    public static final String MODID = "myFancyMods";
    // 模組版本號,後續可以更新
    public static final String VERSION = "1.0";

    @EventHandler
    public void preInit(FMLPreInitializationEvent event) {
        Set ids = EntityList.idToClassMapping.keySet();
        int availableId = 1;
        while (EntityList.getClassFromID(availableId) != null) {
            availableId++;
        }
        EntityRegistry.registerGlobalEntityID(EntityScalePig.class, EntityScalePig.name, availableId);
    }

    // 告訴Forge這個是FML(Forge Mod Loader)事件處理的方法
    // FMLInitializationEvent : 這裡我們處理的是模組的初始化的事件
    @EventHandler
    public void init(FMLInitializationEvent event) {
        MinecraftForge.EVENT_BUS.register(new PigDoll_v2());
    }
}

EntityScalePig.java

public class EntityScalePig extends EntityPig {

    public static final String name = "MyPig";

    public EntityScalePig(World worldIn) {
        super(worldIn);
    }

    public void scale(float scale) {
        this.setSize(width * scale, height * scale);
    }
}

RenderScalePig.java

@SideOnly(Side.CLIENT)
public class RenderScalePig extends RenderLiving {

    private static final ResourceLocation textures = new ResourceLocation("textures/entity/pig/pig.png");

    private float scale = 1.0f;

    public RenderScalePig(RenderManager renderManager, ModelBase modelBase, float shadowSize)
    {
        super(renderManager, modelBase, shadowSize);
    }

    public void setScale(float scale) {
        this.scale = scale;
    }

    protected void preRenderCallback(EntityLivingBase entityLivingBase, float tickTime)
    {
        GlStateManager.scale(this.scale, this.scale, this.scale);
    }

    @Override
    protected ResourceLocation getEntityTexture(Entity entity)
    {
        return textures;
    }
}

存檔,直接透過runClient進入遊戲。拿出你的生怪蛋,找一個平坦的地方欣賞豬豬版的俄羅斯娃娃吧!

今天的內容有點多,主要是想把內容一步一步說清楚。未來我們會慢慢的把這幾天提到的功能 (自定義類別、Client與Server端的差別等)在其他主題內說明清楚與補齊,希望今天這個功能不會因此打消你想要自己打造模組的興趣/images/emoticon/emoticon25.gif


上一篇
[Day8] Minecraft版的俄羅斯娃娃(中)
下一篇
[Day10] 讓我們可以跳得更高
系列文
[Minecraft - 當個創世神] 從玩遊戲到設計遊戲30

尚未有邦友留言

立即登入留言