인터넷에 조금만 검색해도 블록이나 아이템을 추가하는 법을 설명하는 글은 많습니다. 하지만 엔티티(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를 넣어야합니다)

 

 

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

정식 게시글은 http://www.minecraftforum.net/forums/mapping-and-modding/minecraft-mods/2228763-hungry-animals-more-realistic-animals에서 확인하실 수 있습니다.

 

이번 버전은 거대한 변화의 시작이라고 할 수 있습니다. 1.0.4는 동물의 사냥과 길들이기가 완전히 바뀔 것이기 때문입니다. 1.0.4.0은 그러한 변화에 앞서 사냥을 위한 도구들이 들어왔습니다. 새총, 사냥추, 함정 덮개가 추가되었고 제초제가 추가되어 원치 않는 풀의 성장을 막을 수 있습니다. 그 이외에 버그 수정, 난이도 조정이 있습니다.

 

다음은 구체적인 변경사항입니다.

1.0.4.0

  1. 사냥추
    사냥추는 재빠른 동물들을 포획하기 위한 원거리 1회성 무기입니다. 사냥추에 맞은 적은 크게 둔화되며, 활과 같이 오래 충전하면 사거리가 늘어납니다.
  2. 새총
    새총은 가장 단순한 원거리 무기입니다. 조약돌을 탄환으로 사용해서 쓰기가 간편한 대신 공격력이 맨손과 같습니다.
  3. 제초제
    제초제는 풀 블록에서 잔디가 자라는 것을 막아줍니다. 손에 들었을 때는 제초제가 뿌려진 블록이 다른 색으로 보입니다. 우클릭을 통해 넓은 범위에 사용할 수 있고, 시프트를 이용하면 원하는 부분에만 적용시킬 수 있습니다. 사용시 잔디가 바로 사라지며 더 이상 자라지도 않습니다.
  4. 함정 덮개
    함정 덮개는 구덩이 함정을 가리는 용도로 사용됩니다. 동물이나 사람이 그 위에서 걸으면 연결된 함정 덮개가 모두 차례로 부서집니다. 부서지는 과정에서 재료로 사용되었던 나무 막대를 일부 돌려줍니다.
  5. 동물
    기아 상태에서 데미지를 입습니다. (따라서 때려서 죽이는 경우와 같은 아이템을 드랍하게 됩니다)
    힘줄을 드랍합니다.
    인공지능 버그가 수정되었습니다.
    음식을 통해 얻는 만복도가 증가되었습니다
    면역력이 증대되었습니다.
  6. 배설물
    인벤토리 렌더링이 개선되었습니다.
  7. 힘줄
    새총, 밧줄, 사냥추를 만들 때 사용됩니다.

 

 

추가된 도구들의 사용법과 조합법입니다.

 

1) 구덩이 함정 만들기


구덩이 함정은 기다란 수직 통로만 있으면 끝인 간단한 함정입니다. 높이가 2이상 이라면 어느 동물일지라도 충분히 잡아둘 수 있습니다. 가장 중요한 부분은 윗부분을 함정 덮개로 덮개로 덮는 것입니다.



구덩이를 함정 덮개로 덮고, 가능하다면 동물들을 유인할 먹이를 놓아보세요.





사람이나 동물이 덮개 위를 걷게 되면 연결된 덮개들이 순차적으로 무너지고, 그 위의 대상은 자연스레 구덩이로 빠지게 됩니다.






2) 새총

새총은 조약돌을 쏘는 아주 약하고 간단한 무기입니다. 새총을 만들 때는 힘줄이 필요한데요, 힘줄은 동물을 사냥해서 얻을 수 있습니다. 단, 닭은 힘줄을 주지 않습니다. 처음에는 질병에 걸린 동물을 잡거나 구덩이 함정을 이용해보세요. 힘줄로는 새총뿐만 아니라 사냥추와 밧줄도 만들 수 있습니다.




조약돌을 충분히 소지하신 후 마음껏 쏘세요!



3) 사냥추

사냥추는 재빠른 동물을 포획할 때 사용되는 일회용 투척 무기입니다. 사냥추는 타격한 대상을 느리게 합니다. 활처럼 오래 충전할수록 멀리 날아가기 때문에 정확한 사용에는 연습이 필요합니다. 일회용이니 신중하게 사용하세요.



목표물을 정하고, 신중하게 조준해서 포획하세요.





 

4) 제초제

잔디가 너무 많이 자라서 불만이신 분들이 있었을 겁니다. 제초제는 그런 분들을 위해서 잔디의 성장을 막기 위해 나온 아이템입니다. 우클릭만으로 잔디를 모두 날려버리세요.

제초제는 만들 때 독성이 있는 물질이 사용됩니다.




(유리판 색은 중요하지 않습니다)

풀 블록 위에 사용하면 근처의 잔디가 즉시 사라지며, 더 이상 그 위에서 자라지 않습니다. 제초제를 손에 들고 있으면 제초제가 사용된 땅은 색이 변한게 보이는데, 이것은 제초제를 들고 있지 않을 때는 보이지 않습니다. 시프트 누르면 범위 사용이 아닌 바라보는 블록에만 적용됩니다.





집을 깔끔하게 꾸며보세요~

'마인크래프트 자작 모드 > 배고픈동물들' 카테고리의 다른 글

2.0 업데이트  (3) 2015.08.10
1.0.4.1 업데이트  (6) 2015.02.06
1.0.3.1 업데이트  (0) 2015.02.05
1.0.3 업데이트  (1) 2015.02.05
1.0.2 업데이트  (0) 2015.02.05

사전에 예고된 바와 같이 1.0.4가 아닌 1.0.3.1 업데이트를 하게 되었습니다. 1.0.3.* 버전의 테마는 동물들의 배변 활동입니다. 그에 따라 1.0.3(.0)에서 배설물과 퇴비가 처음으로 추가되었습니다. 이번 1.0.3.1에서는 '질병'이 가장 주목할 만한 변화입니다. 또한 포럼에서의 피드백과 버그 리포트를 반영했습니다.

 

다운로드 : http://adf.ly/spFaw

 

1. 개발 환경

마인크래프트: 1.7.10

포지: 10.13.0.1180

 

2. 변경 사항

  1. 질병(Disease)
    1. 효과
      질병은 포션(버프)의 일종입니다. 동물에게만 적용되며 기초 대사량이 5배가 되며 이동속도가 느려지기 때문에 생존 활동에 큰 위협이 됩니다. 질병은 하루 동안 지속됩니다.
    2. 발생
      질병은 2개 이상 쌓인 배설물로부터 발생합니다. 근처(3블록)의 동물들은 일정 확률로 질병이 걸릴 수 있습니다. 야생에서는 질병이 거의 발생하지 않지만 좁은 우리 안에서 배설물이 많이 쌓인 경우에는 큰 위협이 될 수 있으니 주의하세요.
    3. 치료
      하루가 지나면 질병은 자동적으로 치료됩니다. 그러나 그 기간 동안 동물이 생존할 확률은 굉장히 희박합니다. 추가적으로 영양을 공급하거나 버섯을 통해 치료하세요.
    4. 발견
      질병에 걸린 동물에게는 특별한 시각 효과가 부여되며 이동속도가 크게 느려져서 눈에 띄게 됩니다.

 

 

  1. 블록
    1. 배설물 블록
      1. 비옥화
        배설물 블록이 잔디, 흙, 모래 위에 있는 경우 일정 확률로 땅에 흡수됩니다. 흡수될 때는 퇴비가 배설물보다 우선적으로 흡수됩니다. 흡수의 결과로 모래는 흙으로, 흙은 잔디로 변합니다.
      2. 침식
        위에 명시된 잔디, 흙, 모래 이외의 블록 위에 놓인 퇴비는 낮은 확률로 침식되어 사라집니다.
  2. 동물
    1. AI
      배가 고파야만 음식을 먹습니다.
    2. 생성
      강제적으로 바닐라 동물이 생성되어도 배고픈 동물로 대체됩니다. (달걀, 스폰 알 등)
      바이옴을 다루는 모드와 충돌이 일어나서 크래쉬가 일어나는 버그를 해결(?)했습니다.
    3. 호환성
      아마도! 다른 모드와 호환성이 좋아졌다고 판단됩니다. 직접적으로 지원하지는 않지만 다른 모드에서 바닐라 동물에 접근하려 하는 코드가 배고픈 동물에도 적용될 수 있습니다.
    4. 능력치
      능력치는 컨피그를 통해 플레이어가 수정할 수 있지만 적절한 기본 설정값을 제공하는 것도 제작자의 중요한 역할이라 판단되어 몇몇 수정이 있습니다.
      최대 허기량이나 기초 대사량을 정할 때 사용한 동물들간의 불변량은 음식의 열량입니다. 즉 특정한 음식이 제공하는 열량은 어떤 동물이 먹든 간에 기본 설정값은 모두 동일합니다. 잔디가 기준이며 1.0의 열량을 가집니다.
      아래에 명시된 최대 생존 시간은 추가적인 열량 공급이 없을 때 동물이 얼마나 오래 살수 있는 기간이지만, 동물이 아무것도 하지 않을 때를 기준으로 계산한 값이므로 실제로는 조금 더 일찍 죽습니다.
      추가적으로 최대 열량에 따라 구애 비용과 출산 비용이 재 조정(높아짐) 되었으며 음식이 제공하는 열량도 바뀌었습니다. (높아짐)

      1. 최대 열량 : 150
        기초 대사량: 0.002/tick
        최대 생존 시간: 3.125 Minecraft Day

      2. 최대 열량 : 400
        기초 대사량 : 0.005/tick
        최대 생존 시간: 3.33 Minecraft Day
      3. 돼지
        최대 열량 : 300
        기초 대사량 : 0.004/tick
        최대 생존 시간: 3.125 Minecraft Day

      4. 최대 열량 : 300
        기초 대사량 : 0.004/tick
        최대 생존 시간: 3.125 Minecraft Day
    5. 보상

      일부 보상(고기)이 조정되었습니다. (낮아짐)

  3. 컨피그
    1. 추가된 항목
      출산 비용. 최대 허기량.

 

3. 알려진 버그 (치명적이지는 않은 버그이나 아직 고치지 못한 버그의 목록)

- 원래 동물이 있던 장소에 스폰 알을 이용해서 추가적으로 동물을 소환하면 배설물(!)과 함께 소환됨.

4. 저작권

본 모드는 무단 수정, 배포가 불가능합니다. 또한 허가되지 않은 영리적 목적의 사용을 모두 금지합니다. 영리적 목적의 사용에는 후원이 가능한 서버에 추가, 아프리카 방송 등을 포함합니다. 비 영리적 목적의 경우 어떤 모드 팩이라고 할 지라도 본 모드가 포함될 수 있습니다. 다만 원 제작자(oortcloud1996) 명시는 필수입니다. 모드 수정/재배포 및 영리적 사용에 관한 문의는 메일, 쪽지 등을 이용해주시길 바랍니다. 위의 제반 사항을 위반할 시 크리에이티브 커먼즈에 의거 법적인 처벌을 받으실 수 있습니다.

'마인크래프트 자작 모드 > 배고픈동물들' 카테고리의 다른 글

1.0.4.1 업데이트  (6) 2015.02.06
1.0.4.0 업데이트  (2) 2015.02.05
1.0.3 업데이트  (1) 2015.02.05
1.0.2 업데이트  (0) 2015.02.05
1.0.1 업데이트  (0) 2015.02.05

+ Recent posts