인터넷에 조금만 검색해도 블록이나 아이템을 추가하는 법을 설명하는 글은 많습니다. 하지만 엔티티(Entity)를 추가하는 건 한국어로 된 자료는 많이 보지 못했습니다. 최근에 배고픈 동물들(Hungry Animals) 모드를 개발하면서 엔티티에 대해서 공부한 내용을 여기에 조금 적어보고자 합니다.

 

 

1. 엔티티(Entity)란 무엇인가?

 

마인크래프트 월드(World)의 주된 구성요소는 블록(Block)과 엔티티입니다. (그 둘의 중간 성격을 띄었다고 할 수 있는 타일 엔티티(Tile Entity)도 있으나 논외로 하겠습니다) 즉 월드를 구성하는 요소 중 블록을 제외하면 대부분이 엔티티입니다. 플레이어, 동물 등은 물론이고 바닥에 떨어진 아이템, 날아가는 화살, 떨어지는 자갈이나 모래, TNT, 심지어는 번개까지도 엔티티입니다.

이들의 공통적인 특징은 위치, 속도, 회전 정보를 가져서 블록(Block)에 비해서 섬세한 좌표 표현이 가능하다는 겁니다. 또한 엔티티의 객체 그 자체가 월드에 저장됩니다. 이것은 일반적인 블록과 대비되는 특징이며 이는 다양한 정보를 엔티티에 저장할 수 있다는 것을 의미합니다. 그러나 그 데이터에 대한 서버(Server)와 클라이언트(Client)간의 동기화는 자동적으로 제공되지 않으니 그 정보가 클라이언트상으로 노출되는 경우라면 따로 동기화를 해주어야 합니다.

 

추가적인 정보는 http://minecraft.gamepedia.com/Entity을 참조하세요.

 

 

2. 엔티티의 구성 요소

 

하나의 엔티티가 마인크래프트에 등록이 되어, 실질적인 기능을 하기 위해서는 최소 세 가지의 클래스가 필요합니다. (1)엔티티의 실질적인 모든 정보를 가지는 엔티티 클래스, (2)렌더링(Rendering) 시에 엔티티의 3D 모델(Model)을 구성하는 모델 클래스, (3)모델 위에 어떠한 그림이 그려질지 표현하는 렌더(Render) 클래스가 그 세가지 클래스입니다. 이중 (2), (3)은 100% 클라이언트 사이드(Client Side) 클래스이기 때문에 데디케이티드 서버(DS, Dedicated Server)에서는 로드조차 되지 않습니다.

 

 

3. 엔티티 등록(Entity Registration)하기

 

엔티티 등록은 블록이나 아이템 등록과 마찬가지로 추가할 엔티티마다 해주어야 하는 작업입니다. 이 과정을 통해서 엔티티에게 id가 부여되고 마인크래프트 속에 실질적으로 주입되게 됩니다. 엔티티 등록은 두 가지 과정을 요구합니다. (1)엔티티 클래스의 등록이 그 첫 번째이며, 이것은 서버와 클라이언트 모두에서 이루어집니다. (2)두 번째는 렌더링 핸들러(Rendering Handler)를 등록하는 과정입니다. 이것은 클라이언트에서만 이루어집니다.

 

이해를 돕기 위해서 한가지 예제를 바탕으로 등록을 해보겠습니다. 이번에 만들어볼 엔티티는 '좀비모습을 한 스켈레톤'입니다. 이 예제는 엔티티 클래스는 스켈레톤을, 모델과 렌더 클래스는 스켈레톤의 것을 사용하여서 만들어집니다.

 

가장 먼저 메인 모드 클래스에 인스턴스를 등록해야합니다.

@Mod.Instance

public static ExampleMod instance;

와 같이 코드를 추가해주세요. (위의 코드는 메인 모드 클래스가 'ExampleMod'임을 가정한 것 입니다 )

 

 

 

그 이후 엔티티를 등록해야하는데, 이 강좌는 간단히 등록 과정만을 공부하므로 바닐라 스켈레톤의 코드를 모두 복사한 클래스를 만듭니다. 여기서 클래스이름을 EntityZombieArhcer로 할 탠데, 복사한 이후 찾아 바꾸기로 EntitySkeleton을 모두 EntityZombieArcher로 바꿔주셔야 합니다. 아래는 그 과정을 거친 클래스입니다. (Ctrl + Shift + O를 하면 임포트를 자동으로 할 수 있습니다)

import java.util.Calendar;

 

import net.minecraft.block.Block;

import net.minecraft.enchantment.Enchantment;

import net.minecraft.enchantment.EnchantmentHelper;

import net.minecraft.entity.Entity;

import net.minecraft.entity.EntityCreature;

import net.minecraft.entity.EntityLivingBase;

import net.minecraft.entity.EnumCreatureAttribute;

import net.minecraft.entity.IEntityLivingData;

import net.minecraft.entity.IRangedAttackMob;

import net.minecraft.entity.SharedMonsterAttributes;

import net.minecraft.entity.ai.EntityAIArrowAttack;

import net.minecraft.entity.ai.EntityAIAttackOnCollide;

import net.minecraft.entity.ai.EntityAIFleeSun;

import net.minecraft.entity.ai.EntityAIHurtByTarget;

import net.minecraft.entity.ai.EntityAILookIdle;

import net.minecraft.entity.ai.EntityAINearestAttackableTarget;

import net.minecraft.entity.ai.EntityAIRestrictSun;

import net.minecraft.entity.ai.EntityAISwimming;

import net.minecraft.entity.ai.EntityAIWander;

import net.minecraft.entity.ai.EntityAIWatchClosest;

import net.minecraft.entity.monster.EntityMob;

import net.minecraft.entity.player.EntityPlayer;

import net.minecraft.entity.projectile.EntityArrow;

import net.minecraft.init.Blocks;

import net.minecraft.init.Items;

import net.minecraft.item.Item;

import net.minecraft.item.ItemStack;

import net.minecraft.nbt.NBTTagCompound;

import net.minecraft.potion.Potion;

import net.minecraft.potion.PotionEffect;

import net.minecraft.stats.AchievementList;

import net.minecraft.util.DamageSource;

import net.minecraft.util.MathHelper;

import net.minecraft.world.World;

import net.minecraft.world.WorldProviderHell;

 

public class EntityZombieArcher extends EntityMob implements IRangedAttackMob

{

private EntityAIArrowAttack aiArrowAttack = new EntityAIArrowAttack(this, 1.0D, 20, 60, 15.0F);

private EntityAIAttackOnCollide aiAttackOnCollide = new EntityAIAttackOnCollide(this, EntityPlayer.class, 1.2D, false);

private static final String __OBFID = "CL_00001697";

 

public EntityZombieArcher(World p_i1741_1_)

{

super(p_i1741_1_);

this.tasks.addTask(1, new EntityAISwimming(this));

this.tasks.addTask(2, new EntityAIRestrictSun(this));

this.tasks.addTask(3, new EntityAIFleeSun(this, 1.0D));

this.tasks.addTask(5, new EntityAIWander(this, 1.0D));

this.tasks.addTask(6, new EntityAIWatchClosest(this, EntityPlayer.class, 8.0F));

this.tasks.addTask(6, new EntityAILookIdle(this));

this.targetTasks.addTask(1, new EntityAIHurtByTarget(this, false));

this.targetTasks.addTask(2, new EntityAINearestAttackableTarget(this, EntityPlayer.class, 0, true));

 

if (p_i1741_1_ != null && !p_i1741_1_.isRemote)

{

this.setCombatTask();

}

}

 

protected void applyEntityAttributes()

{

super.applyEntityAttributes();

this.getEntityAttribute(SharedMonsterAttributes.movementSpeed).setBaseValue(0.25D);

}

 

protected void entityInit()

{

super.entityInit();

this.dataWatcher.addObject(13, new Byte((byte)0));

}

 

/**

* Returns true if the newer Entity AI code should be run

*/

public boolean isAIEnabled()

{

return true;

}

 

/**

* Returns the sound this mob makes while it's alive.

*/

protected String getLivingSound()

{

return "mob.skeleton.say";

}

 

/**

* Returns the sound this mob makes when it is hurt.

*/

protected String getHurtSound()

{

return "mob.skeleton.hurt";

}

 

/**

* Returns the sound this mob makes on death.

*/

protected String getDeathSound()

{

return "mob.skeleton.death";

}

 

protected void func_145780_a(int p_145780_1_, int p_145780_2_, int p_145780_3_, Block p_145780_4_)

{

this.playSound("mob.skeleton.step", 0.15F, 1.0F);

}

 

public boolean attackEntityAsMob(Entity p_70652_1_)

{

if (super.attackEntityAsMob(p_70652_1_))

{

if (this.getSkeletonType() == 1 && p_70652_1_ instanceof EntityLivingBase)

{

((EntityLivingBase)p_70652_1_).addPotionEffect(new PotionEffect(Potion.wither.id, 200));

}

 

return true;

}

else

{

return false;

}

}

 

/**

* Get this Entity's EnumCreatureAttribute

*/

public EnumCreatureAttribute getCreatureAttribute()

{

return EnumCreatureAttribute.UNDEAD;

}

 

/**

* Called frequently so the entity can update its state every tick as required. For example, zombies and skeletons

* use this to react to sunlight and start to burn.

*/

public void onLivingUpdate()

{

if (this.worldObj.isDaytime() && !this.worldObj.isRemote)

{

float f = this.getBrightness(1.0F);

 

if (f > 0.5F && this.rand.nextFloat() * 30.0F < (f - 0.4F) * 2.0F && this.worldObj.canBlockSeeTheSky(MathHelper.floor_double(this.posX), MathHelper.floor_double(this.posY), MathHelper.floor_double(this.posZ)))

{

boolean flag = true;

ItemStack itemstack = this.getEquipmentInSlot(4);

 

if (itemstack != null)

{

if (itemstack.isItemStackDamageable())

{

itemstack.setItemDamage(itemstack.getItemDamageForDisplay() + this.rand.nextInt(2));

 

if (itemstack.getItemDamageForDisplay() >= itemstack.getMaxDamage())

{

this.renderBrokenItemStack(itemstack);

this.setCurrentItemOrArmor(4, (ItemStack)null);

}

}

 

flag = false;

}

 

if (flag)

{

this.setFire(8);

}

}

}

 

if (this.worldObj.isRemote && this.getSkeletonType() == 1)

{

this.setSize(0.72F, 2.34F);

}

 

super.onLivingUpdate();

}

 

/**

* Handles updating while being ridden by an entity

*/

public void updateRidden()

{

super.updateRidden();

 

if (this.ridingEntity instanceof EntityCreature)

{

EntityCreature entitycreature = (EntityCreature)this.ridingEntity;

this.renderYawOffset = entitycreature.renderYawOffset;

}

}

 

/**

* Called when the mob's health reaches 0.

*/

public void onDeath(DamageSource p_70645_1_)

{

super.onDeath(p_70645_1_);

 

if (p_70645_1_.getSourceOfDamage() instanceof EntityArrow && p_70645_1_.getEntity() instanceof EntityPlayer)

{

EntityPlayer entityplayer = (EntityPlayer)p_70645_1_.getEntity();

double d0 = entityplayer.posX - this.posX;

double d1 = entityplayer.posZ - this.posZ;

 

if (d0 * d0 + d1 * d1 >= 2500.0D)

{

entityplayer.triggerAchievement(AchievementList.snipeSkeleton);

}

}

}

 

protected Item getDropItem()

{

return Items.arrow;

}

 

/**

* Drop 0-2 items of this living's type. @param par1 - Whether this entity has recently been hit by a player. @param

* par2 - Level of Looting used to kill this mob.

*/

protected void dropFewItems(boolean p_70628_1_, int p_70628_2_)

{

int j;

int k;

 

if (this.getSkeletonType() == 1)

{

j = this.rand.nextInt(3 + p_70628_2_) - 1;

 

for (k = 0; k < j; ++k)

{

this.dropItem(Items.coal, 1);

}

}

else

{

j = this.rand.nextInt(3 + p_70628_2_);

 

for (k = 0; k < j; ++k)

{

this.dropItem(Items.arrow, 1);

}

}

 

j = this.rand.nextInt(3 + p_70628_2_);

 

for (k = 0; k < j; ++k)

{

this.dropItem(Items.bone, 1);

}

}

 

protected void dropRareDrop(int p_70600_1_)

{

if (this.getSkeletonType() == 1)

{

this.entityDropItem(new ItemStack(Items.skull, 1, 1), 0.0F);

}

}

 

/**

* Makes entity wear random armor based on difficulty

*/

protected void addRandomArmor()

{

super.addRandomArmor();

this.setCurrentItemOrArmor(0, new ItemStack(Items.bow));

}

 

public IEntityLivingData onSpawnWithEgg(IEntityLivingData p_110161_1_)

{

p_110161_1_ = super.onSpawnWithEgg(p_110161_1_);

 

if (this.worldObj.provider instanceof WorldProviderHell && this.getRNG().nextInt(5) > 0)

{

this.tasks.addTask(4, this.aiAttackOnCollide);

this.setSkeletonType(1);

this.setCurrentItemOrArmor(0, new ItemStack(Items.stone_sword));

this.getEntityAttribute(SharedMonsterAttributes.attackDamage).setBaseValue(4.0D);

}

else

{

this.tasks.addTask(4, this.aiArrowAttack);

this.addRandomArmor();

this.enchantEquipment();

}

 

this.setCanPickUpLoot(this.rand.nextFloat() < 0.55F * this.worldObj.func_147462_b(this.posX, this.posY, this.posZ));

 

if (this.getEquipmentInSlot(4) == null)

{

Calendar calendar = this.worldObj.getCurrentDate();

 

if (calendar.get(2) + 1 == 10 && calendar.get(5) == 31 && this.rand.nextFloat() < 0.25F)

{

this.setCurrentItemOrArmor(4, new ItemStack(this.rand.nextFloat() < 0.1F ? Blocks.lit_pumpkin : Blocks.pumpkin));

this.equipmentDropChances[4] = 0.0F;

}

}

 

return p_110161_1_;

}

 

/**

* sets this entity's combat AI.

*/

public void setCombatTask()

{

this.tasks.removeTask(this.aiAttackOnCollide);

this.tasks.removeTask(this.aiArrowAttack);

ItemStack itemstack = this.getHeldItem();

 

if (itemstack != null && itemstack.getItem() == Items.bow)

{

this.tasks.addTask(4, this.aiArrowAttack);

}

else

{

this.tasks.addTask(4, this.aiAttackOnCollide);

}

}

 

/**

* Attack the specified entity using a ranged attack.

*/

public void attackEntityWithRangedAttack(EntityLivingBase p_82196_1_, float p_82196_2_)

{

EntityArrow entityarrow = new EntityArrow(this.worldObj, this, p_82196_1_, 1.6F, (float)(14 - this.worldObj.difficultySetting.getDifficultyId() * 4));

int i = EnchantmentHelper.getEnchantmentLevel(Enchantment.power.effectId, this.getHeldItem());

int j = EnchantmentHelper.getEnchantmentLevel(Enchantment.punch.effectId, this.getHeldItem());

entityarrow.setDamage((double)(p_82196_2_ * 2.0F) + this.rand.nextGaussian() * 0.25D + (double)((float)this.worldObj.difficultySetting.getDifficultyId() * 0.11F));

 

if (i > 0)

{

entityarrow.setDamage(entityarrow.getDamage() + (double)i * 0.5D + 0.5D);

}

 

if (j > 0)

{

entityarrow.setKnockbackStrength(j);

}

 

if (EnchantmentHelper.getEnchantmentLevel(Enchantment.flame.effectId, this.getHeldItem()) > 0 || this.getSkeletonType() == 1)

{

entityarrow.setFire(100);

}

 

this.playSound("random.bow", 1.0F, 1.0F / (this.getRNG().nextFloat() * 0.4F + 0.8F));

this.worldObj.spawnEntityInWorld(entityarrow);

}

 

/**

* Return this skeleton's type.

*/

public int getSkeletonType()

{

return this.dataWatcher.getWatchableObjectByte(13);

}

 

/**

* Set this skeleton's type.

*/

public void setSkeletonType(int p_82201_1_)

{

this.dataWatcher.updateObject(13, Byte.valueOf((byte)p_82201_1_));

this.isImmuneToFire = p_82201_1_ == 1;

 

if (p_82201_1_ == 1)

{

this.setSize(0.72F, 2.34F);

}

else

{

this.setSize(0.6F, 1.8F);

}

}

 

/**

* (abstract) Protected helper method to read subclass entity data from NBT.

*/

public void readEntityFromNBT(NBTTagCompound p_70037_1_)

{

super.readEntityFromNBT(p_70037_1_);

 

if (p_70037_1_.hasKey("SkeletonType", 99))

{

byte b0 = p_70037_1_.getByte("SkeletonType");

this.setSkeletonType(b0);

}

 

this.setCombatTask();

}

 

/**

* (abstract) Protected helper method to write subclass entity data to NBT.

*/

public void writeEntityToNBT(NBTTagCompound p_70014_1_)

{

super.writeEntityToNBT(p_70014_1_);

p_70014_1_.setByte("SkeletonType", (byte)this.getSkeletonType());

}

 

/**

* Sets the held item, or an armor slot. Slot 0 is held item. Slot 1-4 is armor. Params: Item, slot

*/

public void setCurrentItemOrArmor(int p_70062_1_, ItemStack p_70062_2_)

{

super.setCurrentItemOrArmor(p_70062_1_, p_70062_2_);

 

if (!this.worldObj.isRemote && p_70062_1_ == 0)

{

this.setCombatTask();

}

}

 

/**

* Returns the Y Offset of this entity.

*/

public double getYOffset()

{

return super.getYOffset() - 0.5D;

}

}

 

 

이제 등록할 엔티티가 만들어 졌으니, 등록을 해봅시다. 엔티티는 메인 모드 클래스에서 다음과 같은 메소드를 통해 등록합니다. preInit. 단계에서 엔티티를 등록하게 되므로, 해당하는 메소드 내부에 입력하면 됩니다.

@Mod.EventHandler

public static void preInit(FMLPreInitializationEvent event) {

    EntityRegistry.registerModEntity(EntitySkeleton.class, "zombieArcher", 0, ExampleMod.instance, 80, 3, false);

}

첫 번째 인자(parameter)는 등록할 엔티티의 클래스, 두 번째는 고유 식별을 위한 이름, 세 번째는 id, 네 번째는 모드의 인스턴스이고 뒤에 부분은 서버가 엔티티를 처리하는 방법과 관련된 인자들입니다.

 

마지막 순서는 EntityZombieArcher 클래스에 등록 시켜줄 모델, 렌더 클래스를 만드는 것입니다. 이것은 좀비의 코드를 바탕으로 복사를 해보겠습니다. RenderZombie 클래스를 복사해서, RenderZombieArcher를 만듭니다. 이때, 이전과 같이 찾아 바꾸기로 RenderZombie를 모두 RenderZombieArcher로 바꾸어줍니다. 또한 EntityZombie를 EntityZombieArcher로 바꿉니다. 그리고 오류가 나오는 부분이 있는데, 좀비와 스켈레톤은 서로 다른 기능이 좀 있어서 일어나는 문제입니다. 해당하는 문제를 해결하여 앞에 옮겼습니다.

import net.minecraft.client.model.ModelBiped;

import net.minecraft.client.model.ModelZombie;

import net.minecraft.client.model.ModelZombieVillager;

import net.minecraft.client.renderer.entity.RenderBiped;

import net.minecraft.entity.Entity;

import net.minecraft.entity.EntityLiving;

import net.minecraft.entity.EntityLivingBase;

import net.minecraft.entity.monster.EntityPigZombie;

import net.minecraft.util.ResourceLocation;

import oortcloud.hungryanimals.entities.EntityZombieArcher;

import cpw.mods.fml.relauncher.Side;

import cpw.mods.fml.relauncher.SideOnly;

 

@SideOnly(Side.CLIENT)

public class RenderZombieArcher extends RenderBiped

{

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

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

private static final ResourceLocation zombieVillagerTextures = new ResourceLocation("textures/entity/zombie/zombie_villager.png");

private ModelBiped field_82434_o;

private ModelZombieVillager zombieVillagerModel;

protected ModelBiped field_82437_k;

protected ModelBiped field_82435_l;

protected ModelBiped field_82436_m;

protected ModelBiped field_82433_n;

private int field_82431_q = 1;

private static final String __OBFID = "CL_00001037";

 

public RenderZombieArcher()

{

super(new ModelZombie(), 0.5F, 1.0F);

this.field_82434_o = this.modelBipedMain;

this.zombieVillagerModel = new ModelZombieVillager();

}

 

protected void func_82421_b()

{

this.field_82423_g = new ModelZombie(1.0F, true);

this.field_82425_h = new ModelZombie(0.5F, true);

this.field_82437_k = this.field_82423_g;

this.field_82435_l = this.field_82425_h;

this.field_82436_m = new ModelZombieVillager(1.0F, 0.0F, true);

this.field_82433_n = new ModelZombieVillager(0.5F, 0.0F, true);

}

 

/**

* Queries whether should render the specified pass or not.

*/

protected int shouldRenderPass(EntityZombieArcher p_77032_1_, int p_77032_2_, float p_77032_3_)

{

this.func_82427_a(p_77032_1_);

return super.shouldRenderPass((EntityLiving)p_77032_1_, p_77032_2_, p_77032_3_);

}

 

/**

* Actually renders the given argument. This is a synthetic bridge method, always casting down its argument and then

* handing it off to a worker function which does the actual work. In all probabilty, the class Render is generic

* (Render<T extends Entity) and this method has signature public void func_76986_a(T entity, double d, double d1,

* double d2, float f, float f1). But JAD is pre 1.5 so doesn't do that.

*/

public void doRender(EntityZombieArcher p_76986_1_, double p_76986_2_, double p_76986_4_, double p_76986_6_, float p_76986_8_, float p_76986_9_)

{

this.func_82427_a(p_76986_1_);

super.doRender((EntityLiving)p_76986_1_, p_76986_2_, p_76986_4_, p_76986_6_, p_76986_8_, p_76986_9_);

}

 

/**

* Returns the location of an entity's texture. Doesn't seem to be called unless you call Render.bindEntityTexture.

*/

protected ResourceLocation getEntityTexture(EntityZombieArcher p_110775_1_)

{

return zombieTextures;

}

 

protected void renderEquippedItems(EntityZombieArcher p_77029_1_, float p_77029_2_)

{

this.func_82427_a(p_77029_1_);

super.renderEquippedItems((EntityLiving)p_77029_1_, p_77029_2_);

}

 

private void func_82427_a(EntityZombieArcher p_82427_1_)

{

 

 

this.mainModel = this.field_82434_o;

this.field_82423_g = this.field_82437_k;

this.field_82425_h = this.field_82435_l;

 

 

this.modelBipedMain = (ModelBiped)this.mainModel;

}

 

protected void rotateCorpse(EntityZombieArcher p_77043_1_, float p_77043_2_, float p_77043_3_, float p_77043_4_)

{

 

super.rotateCorpse(p_77043_1_, p_77043_2_, p_77043_3_, p_77043_4_);

}

 

protected void renderEquippedItems(EntityLiving p_77029_1_, float p_77029_2_)

{

this.renderEquippedItems((EntityZombieArcher)p_77029_1_, p_77029_2_);

}

 

/**

* Returns the location of an entity's texture. Doesn't seem to be called unless you call Render.bindEntityTexture.

*/

protected ResourceLocation getEntityTexture(EntityLiving p_110775_1_)

{

return this.getEntityTexture((EntityZombieArcher)p_110775_1_);

}

 

/**

* Actually renders the given argument. This is a synthetic bridge method, always casting down its argument and then

* handing it off to a worker function which does the actual work. In all probabilty, the class Render is generic

* (Render<T extends Entity) and this method has signature public void func_76986_a(T entity, double d, double d1,

* double d2, float f, float f1). But JAD is pre 1.5 so doesn't do that.

*/

public void doRender(EntityLiving p_76986_1_, double p_76986_2_, double p_76986_4_, double p_76986_6_, float p_76986_8_, float p_76986_9_)

{

this.doRender((EntityZombieArcher)p_76986_1_, p_76986_2_, p_76986_4_, p_76986_6_, p_76986_8_, p_76986_9_);

}

 

/**

* Queries whether should render the specified pass or not.

*/

protected int shouldRenderPass(EntityLiving p_77032_1_, int p_77032_2_, float p_77032_3_)

{

return this.shouldRenderPass((EntityZombieArcher)p_77032_1_, p_77032_2_, p_77032_3_);

}

 

/**

* Queries whether should render the specified pass or not.

*/

protected int shouldRenderPass(EntityLivingBase p_77032_1_, int p_77032_2_, float p_77032_3_)

{

return this.shouldRenderPass((EntityZombieArcher)p_77032_1_, p_77032_2_, p_77032_3_);

}

 

protected void renderEquippedItems(EntityLivingBase p_77029_1_, float p_77029_2_)

{

this.renderEquippedItems((EntityZombieArcher)p_77029_1_, p_77029_2_);

}

 

protected void rotateCorpse(EntityLivingBase p_77043_1_, float p_77043_2_, float p_77043_3_, float p_77043_4_)

{

this.rotateCorpse((EntityZombieArcher)p_77043_1_, p_77043_2_, p_77043_3_, p_77043_4_);

}

 

/**

* Actually renders the given argument. This is a synthetic bridge method, always casting down its argument and then

* handing it off to a worker function which does the actual work. In all probabilty, the class Render is generic

* (Render<T extends Entity) and this method has signature public void func_76986_a(T entity, double d, double d1,

* double d2, float f, float f1). But JAD is pre 1.5 so doesn't do that.

*/

public void doRender(EntityLivingBase p_76986_1_, double p_76986_2_, double p_76986_4_, double p_76986_6_, float p_76986_8_, float p_76986_9_)

{

this.doRender((EntityZombieArcher)p_76986_1_, p_76986_2_, p_76986_4_, p_76986_6_, p_76986_8_, p_76986_9_);

}

 

/**

* Returns the location of an entity's texture. Doesn't seem to be called unless you call Render.bindEntityTexture.

*/

protected ResourceLocation getEntityTexture(Entity p_110775_1_)

{

return this.getEntityTexture((EntityZombieArcher)p_110775_1_);

}

 

/**

* Actually renders the given argument. This is a synthetic bridge method, always casting down its argument and then

* handing it off to a worker function which does the actual work. In all probabilty, the class Render is generic

* (Render<T extends Entity) and this method has signature public void func_76986_a(T entity, double d, double d1,

* double d2, float f, float f1). But JAD is pre 1.5 so doesn't do that.

*/

public void doRender(Entity p_76986_1_, double p_76986_2_, double p_76986_4_, double p_76986_6_, float p_76986_8_, float p_76986_9_)

{

this.doRender((EntityZombieArcher)p_76986_1_, p_76986_2_, p_76986_4_, p_76986_6_, p_76986_8_, p_76986_9_);

}

}

 

 

마지막으로 렌더링 핸들러를 등록해야합니다. 앞서 이야기 했듯이 렌더링 핸들러의 등록은 클라이언트에서만 이루어져야 하기 때문에 조금 다른 방법을 사용해야 합니다. 지금 실행되고 있는 코드가 서버인지, 혹은 클라이언트인지를 인식해야 클라이언트에서만 등록을 해줄 수 있습니다. 이걸 위해 일반적으로 프록시를 이용하게 됩니다. 상속관계를 이용할 수도 있으나 저는 조금 더 직관적으로 다음 코드를 사용합니다. (상속관계를 이용하는 것이 모드가 거대해 질수록 도움이 됩니다)

if (proxy instanceof ClientProxy) {

    RenderingRegistry.registerEntityRenderingHandler(EntitySkeleton.class, new RenderZombieArcher());

}

 

 

4. 등록한 엔티티 소환하기

 

엔티티를 소환하는 방법은 여러가지가 있지만, 자연적인 스폰이 설정되지 않은 지금은 /summon 명령어를 사용하는 것이 가장 바람직합니다. 모드에서 추가한 엔티티의 이름은 modid.name과 같은 형태를 띄고 있습니다. 우리가 다뤄본 예제에서는 /summon examplemod.zombieArcher 를 하면 소환이 됩니다. (물론 examplemod는 예로 든 것이고, 여러분의 모드 id를 넣어야합니다)

 

 

좀비가 활을 들고 쏘는 것을 확인할 수 있습니다! 이것으로 엔티티 추가 강의를 마칩니다.

+ Recent posts