今天我們來操作一些有互動的物體 - 實體(Entity)
在Minecraft裡的實體有一些是主動的,通常都具有攻擊性,像是骷髏弓箭手、苦力怕、殭屍。而另外被動的實體,通常都是動物,像是豬、羊、牛、兔子等。
一隻豬在世界裡,會受到玩家手裡拿著胡蘿蔔而跟著,或是你可以騎著一個有裝上鞍的豬,而豬死亡時會掉下生豬肉 (或烤豬肉,端看豬有沒有著火)。如果今天豬掉下來的東西不只是自己的肉呢?
聽起來好像有些搞頭,不是嗎?
在進入今天主題之前,我們先來了解實體死亡後掉落物品的事件 - LivingDeathEvent
開啟專案,在com.ithome.mymod下加入新的Class名為PigDroppingItems
在該Class內加入新的方法名為dropDiamonds,一樣加上@SubscribeEvent
的注釋
接下來就是選擇要處理的事件了,這裡我們換一個方式,來讓各位一起有一個思考流程,假如你對這個沒興趣,請直接跳到下一步。
想一想:
這裡要做的是"豬死亡要掉東西"這類型的事件,我們這邊產生可能的關鍵字有pig
、dead
、death
、drop
、item
有了關鍵字,在public void dropDiamonds()
的括弧內,嘗試輸入上面幾個關鍵字,你會發現Intellij自動幫你找尋有可能的類別。不過很可惜的是,沒有一個關鍵字找到東西是與我們要處理的事件有關 (事件一定會有Event做結尾 - 這也是一個在設計API時可以考慮的,讓之後的開發者可以有一個規則去依循)
因此我們剩下最後一種方法,從Event
這個類別下手,透過Ctrl+H
來尋找
如下圖,在()內鍵入Event關鍵字後,按下Ctrl+H
,在右手邊就會跳出所有可供使用的Event類別
當我們一個一個往下...看到一個EntityEvent
。這時候,就要有一種感覺,我們要的事件就會在這個"實體事件"類別下
果不其然,EntityEvent(實體事件) -> LivingEvent(有生命的實體事件) -> LivingDeathEvent(生物死亡事件)
找到事件類別後,開始建立我們的鑽石豬模組吧
在dropDiamonds內,使用LivingDeathEvent
作為我們要處理的事件,完整的邏輯如下(記得import
):
@SubscribeEvent
public void dropDiamonds(LivingDeathEvent event) {
// 判斷這個死亡的實體是不是豬 (EntityPig)
if(event.entity instanceof EntityPig) {
// 建立隨機種子
Random random = new Random();
// 如果這個是server端呼叫才執行
if(!event.entity.worldObj.isRemote) {
// 讓豬掉下的物品變成鑽石,並且掉下的數量從0 ~ 4顆
event.entity.dropItem(Items.diamond, random.nextInt(5));
}
}
}
最後,在主程式內註冊我們的事件
MinecraftForge.EVENT_BUS.register(new PigDroppingItems());
打開遊戲,找到豬隊友,看看這些豬身上都藏些什麼!
這裡的淺綠色圓形物品就是鑽石,而另外一個則是豬肉 - 這邊可以知道,dropItem的功能並不是"取代",而是"增加"要掉落的物品。
不知道各位有沒有聽過俄羅斯娃娃?俄羅斯娃娃是由多個一樣圖案的空心木娃娃一個套一個組成,因為是空心的木娃娃,在套的時候是從最大的依序套到最小的,而數量一般在6個以上。
在我們前面的鑽石豬模組內,我們做的事情是將豬死亡時綁定牠掉落的物品,我們想像一下:如果生成豬後會產生另一個小一點版本的豬,然後依序產生到一定數量,聽起來好像可以作為一個藝術品?
我們來著手進行吧!
EntityJoinWorldEvent
,並且命名為joinPigs
@SubscribeEvent
public void joinPigs(EntityJoinWorldEvent event) {
}
@EventHandler
public void init(FMLInitializationEvent event) {
MinecraftForge.EVENT_BUS.register(new PigDoll());
}
上面的每一個問題都不是很簡單,我們這邊就一步一步慢慢說明。
動物在Minecraft世界裡有它自己隨機產生的機制,如果每一隻豬產生都會有一定數量的另外一群豬產生,我們會很難處理"一定數量"這件事 - 畢竟所有的豬生成都會經過EntityJoinWorldEvent
這個事件!到時候你會看到世界滿滿的都是豬仔...很噁心,不要再想下去了。
因此我們只處理透過生怪蛋產生的豬 (這個物品只會在創造模式才能使用)。
以我們現在有處理的事件下,當生怪蛋產生豬後,會觸發EntityJoinWorldEvent
事件。我們可以利用一個"全域變數"來控制豬的數量,以及利用玩家與豬的距離來判斷是否為生怪蛋"產生"的豬。
完整程式碼如下,說明直接放在註解內。
public class PigDoll {
// AtomicInteger是多執行續安全的類別,在用全域變數時盡可能使用thread-safe的宣告
// 我們用的是整數類別AtomicInteger去計算當前有多少豬被產生了
// 初始值給0
private AtomicInteger integer = new AtomicInteger(0);
@SubscribeEvent
public void joinPigs(EntityJoinWorldEvent event) {
// 如果這個世界是客戶端,我們不做任何處理;只處理伺服器端
if (event.entity.worldObj.isRemote) return;
// 若實體是豬才處理
if(event.entity instanceof EntityPig) {
// 假如進入這個事件時玩家還未建立好,不做任何事
if (event.world.playerEntities.isEmpty()) return;
// 因為我們只有一個人,這裡直接透過.get(0)取得玩家就好
EntityPlayer player = (EntityPlayer) event.world.playerEntities.get(0);
float distance = event.entity.getDistanceToEntity(player);
// 只處理5個方塊內的實體產生事件
if(distance < 5.0f) {
// 開始計數,如果當前數量已經超過5隻就不處理
if (integer.getAndIncrement() > 5) {
return;
}
}
}
}
}
每一個在Minecraft的實體都有自己的行為(AI),我們需要將豬的AI拿掉 (讓牠變笨),就可以達成這個目的。
我們在前一步distance的IF判斷式內放入這部分的邏輯。
// 只處理5個方塊內的實體產生事件
if(distance < 5.0f) {
// 開始計數,如果當前數量已經超過5隻就不處理
if (integer.getAndIncrement() > 5) {
return;
}
// 建立新的豬實體
EntityPig pig = new EntityPig(event.entity.worldObj);
// 將豬的AI行為去除
EntityAIBase ai = new EntityAIBase() {
@Override
public boolean shouldExecute() {
return true;
}
};
ai.setMutexBits(0xFFFFFF);
pig.tasks.addTask(0, ai);
}
這一部分牽涉到要去處理Minecraft將畫面渲染(Render)的行為,而不能從實體加入到世界的事件裡面處理。因為實體的大小已經被類別 (比如我們這邊是使用EntityPig
)內部定義好,並且我們真正看到的"畫面"也不是從類別內去決定牠應該要看起來多大。因此這裡我們需要額外去處理另外兩個事件:RenderLivingEvent.Pre
與RenderLivingEvent.Post
。
另外要注意的是,我們會需要處理豬產生的位置,不要重疊在一起,最後完整的程式就會是:
package com.ithome.mymod;
import net.minecraft.entity.ai.EntityAIBase;
import net.minecraft.entity.passive.EntityPig;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraftforge.client.event.RenderLivingEvent;
import net.minecraftforge.event.entity.EntityJoinWorldEvent;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import org.lwjgl.opengl.GL11;
import java.util.concurrent.atomic.AtomicInteger;
public class PigDoll {
// AtomicInteger是多執行續安全的類別,在用全域變數時盡可能使用thread-safe的宣告
// 我們用的是整數類別AtomicInteger去計算當前有多少豬被產生了
// 初始值給0
private AtomicInteger integer = new AtomicInteger(0);
//初始比例變數scale
private float scale = 1.0F;
@SubscribeEvent
public void preRender(RenderLivingEvent.Pre event) {
// 是豬而且名字是myPig開頭的的才做縮小動作
if(event.entity instanceof EntityPig && event.entity.getCustomNameTag().startsWith("myPig")) {
// 等比例縮小並準備Render
GL11.glPushMatrix();
String tag = event.entity.getCustomNameTag();
float scaleFactor = Float.parseFloat(tag.split("-")[1]);
GL11.glTranslatef(0F, 1.5F-1.5F*scaleFactor, 0F);
GL11.glScalef(scaleFactor, scaleFactor, scaleFactor);
}
}
@SubscribeEvent
public void postRender(RenderLivingEvent.Post event) {
if(event.entity instanceof EntityPig && event.entity.getCustomNameTag().startsWith("myPig")) {
// 應用Render動作
GL11.glPopMatrix();
}
}
@SubscribeEvent
public void joinPigs(EntityJoinWorldEvent event) {
// 如果這個世界是客戶端,我們不做任何處理;只處理伺服器端
if (event.entity.worldObj.isRemote) return;
// 若實體是豬才處理
if(event.entity instanceof EntityPig) {
// 假如進入這個事件時玩家還未建立好,不做任何事
if (event.world.playerEntities.isEmpty()) return;
// 因為我們只有一個人,這裡直接透過.get(0)取得玩家就好
EntityPlayer player = (EntityPlayer) event.world.playerEntities.get(0);
float distance = event.entity.getDistanceToEntity(player);
// 只處理5個方塊內的實體產生事件
if(distance < 5.0f) {
// 開始計數,如果當前數量已經超過5隻就不處理
if (integer.getAndIncrement() > 5) {
return;
}
// 建立新的豬實體
EntityPig pig = new EntityPig(event.entity.worldObj);
// 設定比例,並且放到自定義名字內
scale = scale * 0.6f;
// 定義我們豬的名字,在Render時可以用做篩選
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);
// 每一隻豬的位置都會在前一隻豬的X軸往西方向1個方塊處
pig.setPositionAndRotation(
event.entity.posX - 1.0f,
event.entity.posY,
event.entity.posZ,
0.0f,
0.0f
);
// 加入豬到這個世界內
event.entity.worldObj.spawnEntityInWorld(pig);
}
}
}
}
存檔後,透過創造模式進入遊戲。在一個四周圍都沒有豬的地方,裝備下圖所示的生怪蛋
使用產生豬後,你應該會看到類似下圖的結果
第一階段達成!五隻不會動的豬隊友會在你前方從大到小依序排列。
明天我們會繼續這個功能,請各位先看看有沒有發現什麼問題?
很有趣XD
不過生豬的方式為什麼用recursive的方式,而不直接就給他for迴圈5次?
這樣是不是只能丟一次蛋XD
不錯的問題~
你可以試看看,將IF的判斷改成For迴圈後,在迴圈內加上訊息印出System.out.println("hello")
看看會發生什麼事XD
其實到目前為止這種將邏輯綁定在"事件"內的作法已經開始會有些問題了,包含你提到只能丟一次蛋這件事 (當然,這也有方法可以解)。
最主要的原因是 - 我們無法控制事件處理不會與另一個事件處理發生關係!
這個會留待下一篇文章來做說明
如果有很多位玩家該怎麼處理呢?
Minecraft之中的指令已經有一個內建的NBT標籤叫做:NoAI
有沒有更簡單的方法拔掉AI呢?
本系列的實作是透過Forge API來操作Minecraft世界的內容;透過NBT指令標籤來操作不在這個系列的範圍內喔~
另外,拔除AI行為的程式碼可以參考Day9