사전 요구사항 :

  • 기본 자바 지식
  • 1, 2번 강의
  • Forge 1.7.10-10.13.0.1180.
  • Eclipse

 

이번에 다뤄볼 내용은 마인크래프트에 동물을 추가하는 겁니다. 여기서 동물이란 건 소, 돼지처럼 음식을 이용해서 번식이 되는 가축을 의미합니다. 마인크래프트 상에서 EntityAnimal 클래스를 상속하는 동물은 닭(EntityChicken), 소(EntityCow), 말(EntityHorse), 돼지(EntityPig), 양(EntitySheep)입니다. 말을 제외하고는 먹는 음식과 AI를 빼곤 근본적인 차이가 없기 때문에, 비슷한 기능을 가지는 동물은 쉽게 추가할 수 있습니다. 이번 강의에서는 여기에 닭의 코드를 응용해서 호박씨를 먹는 칠면조를 추가해보겠습니다.

 

 

1. EntityTurkey 클래스 만들기

 

굳이 칠면조를 택한 이유는 닭의 모델을 재탕이 가능한 덕에 간단히 강의를 작성할 수 있었기 때문입니다. 정말 그게 답니다.

 

1) EntityAnimal 클래스

EntityAnimal 클래스의 주요 메소드와 변수를 알아보겠습니다. 모두 교배와 번식에 관련된 내용입니다.

 

private int inLove;

0보다 크면 사랑에 빠진 상태(짝을 찾아 돌진함).

음식을 먹으면 600이됨. 이후 1씩 감소.

private int breeding;

두 개체가 만난 이후 세어지는 시간.

60이 되면 새끼를 낳음.

private EntityPlayer field_146084_br;

도전 과제를 받을 플레이어

private void procreate(EntityAnimal p_70876_1_)

새끼와 하트를 생성함.

부모의 나이(age)를 6000으로 설정하여 그 동안 사랑에 빠지지 않음.

public boolean isBreedingItem(ItemStack p_70877_1_)

주어진 아이템으로 사랑에 빠지는지 확인하는 함수.

public void func_146082_f(EntityPlayer p_146082_1_)

사랑에 빠지게 하는 함수.

public boolean isInLove()

사랑에 빠졌는지 확인하는 함수.

public boolean canMateWith(EntityAnimal p_70878_1_)

주어진 동물과 교배가 가능한지 확인하는 함수.

(이름이 이상한 메소드나 변수는 마인크래프트가 패치되어 난독화가 바뀌게 되면 그 이름이 달라질 수 있으니 주의하세요)

 

2) EntityTurkey 클래스 만들기

자 그러면 위의 EntityAnimal을 상속하는 EntityTurkey 클래스를 만들어 보겠습니다. 일단 상속하고 생성자와 추상 메소드를 추가하면 다음과 같이 될 겁니다.

public class EntityTurkey extends EntityAnimal

{

 

    public EntityTurkey(World world) {

        super(world);

    }

 

    @Override

    public EntityAgeable createChild(EntityAgeable entity) {

        return null;

    }

 

}

여기까지만 만들면 시체처럼 가만히 서있을 수 밖에 없습니다. 이전에 다루어본 쌍둥이 슬라임처럼요. EntityAnimal의 기본 설정에 의해 밀을 먹이면 사랑에 빠지게 됩니다.

 

 

2. EntityTurkey 구체적으로 작성하기

 

1) AI

이제 동물의 행동을 책임질 AI(인공지능)를 추가할건데요, 해당 사항은 EntityChicken 클래스의 내용을 가져와서 적용하겠습니다. 항상 AI를 추가적으로 적용시킬때는is AIEnabled() 함수가 true를 반환하도록 해주셔야 합니다.

public EntityTurkey (World world)

{

super(world);

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

this.tasks.addTask(1, new EntityAIPanic(this, 1.4D));

this.tasks.addTask(2, new EntityAIMate(this, 1.0D));

this.tasks.addTask(3, new EntityAITempt(this, 1.0D, Items.wheat_seeds, false));

this.tasks.addTask(4, new EntityAIFollowParent(this, 1.1D));

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

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

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

}

 

public boolean isAIEnabled()

{

return true;

}

우리의 칠면조는 호박씨를 먹으니까 호박씨를 보면 따라가야겠죠? EntityAITempt() 동물이 해당 아이템을 플레이어를 따라가게 하는 인공지능인데, 호박 씨를 따라가기로 했으므로

public EntityTurkey (World world)

{

super(world);

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

this.tasks.addTask(1, new EntityAIPanic(this, 1.4D));

this.tasks.addTask(2, new EntityAIMate(this, 1.0D));

this.tasks.addTask(3, new EntityAITempt(this, 1.0D, Items.pumpkin_seeds, false));

this.tasks.addTask(4, new EntityAIFollowParent(this, 1.1D));

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

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

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

}

"Items.pumpkin_seeds" 해당 파라미터를 호박씨로 바꿔주면 이제 호박씨를 따라갑니다.

마지막으로 새롭게 인공지능을 추가하는 경우, isAIEnabled를 다음과 같이 오버라이드해야 합니다.

public boolean isAIEnabled()

{

return true;

}

 

2) 능력치(Entity Attribute)

그 다음 칠면조의 능력치를 설정해줍니다. 여기서 Entity Attribute라는 것을 사용하게 됩니다. 모든 능력치는 applyEntityAttribute함수를 오버라이드하여 적용하게 됩니다.

protected void applyEntityAttributes() {

    super.applyEntityAttributes();

        this.getEntityAttribute(SharedMonsterAttributes.maxHealth).setBaseValue(6.0D);

    //체력        this.getEntityAttribute(SharedMonsterAttributes.movementSpeed).setBaseValue(0.25D);

    //이동속도

}

getEntityAttribute() 함수에 원하는 Attribute를 입력해서 대상을 얻고, setBaseValue()로 값을 수정하는 형식입니다. SharedMonsterAttributes에는 다음과 같은 능력치가 더 있습니다.

 

SharedMonsterAttributes.followRange

몬스터가 대상을 추적하는 거리.

SharedMonsterAttributes.knockbackResistance

대상이 폭발이나 공격에 의한 튕겨나감에 저항할 확률

SharedMonsterAttributes.attackDamage

공격력(몸을 부딛히는 일반 공격)

추가적인 내용은 http://minecraft.gamepedia.com/Attribute를 참고하세요.

 

3) 교배

이제 마지막으로 칠면조에게 교배와 관련된 사항을 넣어보도록 하겠습니다.

일단 첫째로 가장 먼저 오버라이드 한 함수인 createChild()를 수정해봅시다. 간단하게 새로운 EntityTurkey 객체를 반환하면 됩니다.

@Override

public EntityAgeable createChild(EntityAgeable entity) {

    return new EntityTurkey(this.worldObj);

}

 

그 다음 isBreedingItem()을 오버라이드합니다. 호박씨로 교배하기로 했으므로, 그대로 구현해줍니다.

@Override

public boolean isBreedingItem(ItemStack stack)

{

return stack != null && stack.getItem().equals(Items.pumpkin_seeds);

}

원래 EntityChicken의 코드는 다음과 같습니다. 이것으로 보아 닭은 밀 씨앗말고도 다른 것도 먹을 수 있었나 봅니다.

@Override

public boolean isBreedingItem(ItemStack stack)

{

return stack != null && stack.getItem instnaceof ItemSeed;

}

 

4) 애니메이션

이미 EntityChicken에는 애니메이션과 관련된 코드가 있습니다. 날개를 퍼덕이는 게 가장 대표적이죠. 그래서 우리가 차후에 렌더러(Renderer)를 추가할 때 문제가 생기지 않도록 관련 코드를 추가해 주어야합니다. EntityChicken에서 애니메이션과 관련된 부분만 선택 취사 해보도록 하겠습니다. 이 부분은 간단히 넘어가시고 복사만 하셔도 좋습니다.

일단 다음의 다섯 멤버 변수가 모두 애니메이션과 관련된 것들입니다. (그래서 그런지 대부분 난독화 되어있네요)

public float field_70886_e;

public float destPos;

public float field_70884_g;

public float field_70888_h;

public float field_70889_i = 1.0F;

이제 이것들이 사용되는 함수를 찾으면 onLivingUpdate()라는 것을 알 수 있습니다. onLivingUpdate()는 엔티티가 살아있다면 매 틱 실행되는 함수이기 때문에 여러 가지 응용에 사용될 수 있습니다. EntityChicken의 경우에는 여기에 애니메이션과 산란에 해당하는 코드가 있습니다.

public void onLivingUpdate()

{

super.onLivingUpdate();

this.field_70888_h = this.field_70886_e;

this.field_70884_g = this.destPos;

this.destPos = (float)((double)this.destPos + (double)(this.onGround ? -1 : 4) * 0.3D);

 

if (this.destPos < 0.0F)

{

this.destPos = 0.0F;

}

 

if (this.destPos > 1.0F)

{

this.destPos = 1.0F;

}

 

if (!this.onGround && this.field_70889_i < 1.0F)

{

this.field_70889_i = 1.0F;

}

 

this.field_70889_i = (float)((double)this.field_70889_i * 0.9D);

 

if (!this.onGround && this.motionY < 0.0D)

{

this.motionY *= 0.6D;

}

 

this.field_70886_e += this.field_70889_i * 2.0F;

}

산란과 관련된 코드를 삭제하면 위와 같이 됩니다. 이것까지 포함하면 EntityTurkey는 완성입니다.

 

 

3. 렌더러(Renderer) 만들기

 

이전 강의에서 여러 번 해 봤듯이 렌더러는 모델과 렌더 클래스로 이루어집니다. 칠면조를 만들 때는 모델은 그대로 사용할 것이고 렌더 클래스에 쓰일 텍스쳐만 칠면조라는 컨셉에 맞게 색을 조금 고쳐보도록 하겠습니다.

 

1) 모델 클래스

ModelChicken 클래스를 가져와서 클래스, 생성자 이름만 적당히 바꿔줍니다. 역시 처음에 배울 때는 잘 짜여진(?!) 바닐라 코드를 보는 것이 좋습니다. 애니메이션을 적용하기 쉽게 각 부위별로 모델을 만든 것을 확인할 수 있습니다.

public class ModelTurkey extends ModelBase

{

public ModelRenderer head;

public ModelRenderer body;

public ModelRenderer rightLeg;

public ModelRenderer leftLeg;

public ModelRenderer rightWing;

public ModelRenderer leftWing;

public ModelRenderer bill;

public ModelRenderer chin;

 

public ModelTurkey()

{

byte b0 = 16;

this.head = new ModelRenderer(this, 0, 0);

this.head.addBox(-2.0F, -6.0F, -2.0F, 4, 6, 3, 0.0F);

this.head.setRotationPoint(0.0F, (float)(-1 + b0), -4.0F);

this.bill = new ModelRenderer(this, 14, 0);

this.bill.addBox(-2.0F, -4.0F, -4.0F, 4, 2, 2, 0.0F);

this.bill.setRotationPoint(0.0F, (float)(-1 + b0), -4.0F);

this.chin = new ModelRenderer(this, 14, 4);

this.chin.addBox(-1.0F, -2.0F, -3.0F, 2, 2, 2, 0.0F);

this.chin.setRotationPoint(0.0F, (float)(-1 + b0), -4.0F);

this.body = new ModelRenderer(this, 0, 9);

this.body.addBox(-3.0F, -4.0F, -3.0F, 6, 8, 6, 0.0F);

this.body.setRotationPoint(0.0F, (float)b0, 0.0F);

this.rightLeg = new ModelRenderer(this, 26, 0);

this.rightLeg.addBox(-1.0F, 0.0F, -3.0F, 3, 5, 3);

this.rightLeg.setRotationPoint(-2.0F, (float)(3 + b0), 1.0F);

this.leftLeg = new ModelRenderer(this, 26, 0);

this.leftLeg.addBox(-1.0F, 0.0F, -3.0F, 3, 5, 3);

this.leftLeg.setRotationPoint(1.0F, (float)(3 + b0), 1.0F);

this.rightWing = new ModelRenderer(this, 24, 13);

this.rightWing.addBox(0.0F, 0.0F, -3.0F, 1, 4, 6);

this.rightWing.setRotationPoint(-4.0F, (float)(-3 + b0), 0.0F);

this.leftWing = new ModelRenderer(this, 24, 13);

this.leftWing.addBox(-1.0F, 0.0F, -3.0F, 1, 4, 6);

this.leftWing.setRotationPoint(4.0F, (float)(-3 + b0), 0.0F);

}

 

/**

* Sets the models various rotation angles then renders the model.

*/

public void render(Entity p_78088_1_, float p_78088_2_, float p_78088_3_, float p_78088_4_, float p_78088_5_, float p_78088_6_, float p_78088_7_)

{

this.setRotationAngles(p_78088_2_, p_78088_3_, p_78088_4_, p_78088_5_, p_78088_6_, p_78088_7_, p_78088_1_);

 

if (this.isChild)

{

float f6 = 2.0F;

GL11.glPushMatrix();

GL11.glTranslatef(0.0F, 5.0F * p_78088_7_, 2.0F * p_78088_7_);

this.head.render(p_78088_7_);

this.bill.render(p_78088_7_);

this.chin.render(p_78088_7_);

GL11.glPopMatrix();

GL11.glPushMatrix();

GL11.glScalef(1.0F / f6, 1.0F / f6, 1.0F / f6);

GL11.glTranslatef(0.0F, 24.0F * p_78088_7_, 0.0F);

this.body.render(p_78088_7_);

this.rightLeg.render(p_78088_7_);

this.leftLeg.render(p_78088_7_);

this.rightWing.render(p_78088_7_);

this.leftWing.render(p_78088_7_);

GL11.glPopMatrix();

}

else

{

this.head.render(p_78088_7_);

this.bill.render(p_78088_7_);

this.chin.render(p_78088_7_);

this.body.render(p_78088_7_);

this.rightLeg.render(p_78088_7_);

this.leftLeg.render(p_78088_7_);

this.rightWing.render(p_78088_7_);

this.leftWing.render(p_78088_7_);

}

}

 

/**

* Sets the model's various rotation angles. For bipeds, par1 and par2 are used for animating the movement of arms

* and legs, where par1 represents the time(so that arms and legs swing back and forth) and par2 represents how

* "far" arms and legs can swing at most.

*/

public void setRotationAngles(float p_78087_1_, float p_78087_2_, float p_78087_3_, float p_78087_4_, float p_78087_5_, float p_78087_6_, Entity p_78087_7_)

{

this.head.rotateAngleX = p_78087_5_ / (180F / (float)Math.PI);

this.head.rotateAngleY = p_78087_4_ / (180F / (float)Math.PI);

this.bill.rotateAngleX = this.head.rotateAngleX;

this.bill.rotateAngleY = this.head.rotateAngleY;

this.chin.rotateAngleX = this.head.rotateAngleX;

this.chin.rotateAngleY = this.head.rotateAngleY;

this.body.rotateAngleX = ((float)Math.PI / 2F);

this.rightLeg.rotateAngleX = MathHelper.cos(p_78087_1_ * 0.6662F) * 1.4F * p_78087_2_;

this.leftLeg.rotateAngleX = MathHelper.cos(p_78087_1_ * 0.6662F + (float)Math.PI) * 1.4F * p_78087_2_;

this.rightWing.rotateAngleZ = p_78087_3_;

this.leftWing.rotateAngleZ = -p_78087_3_;

}

}

 

2) 렌더 클래스

렌더 클래스도 마찬가지로 RenderChicken을 가져옵니다. 대신 여기서 텍스쳐의 경로를 새로운 칠면조 텍스쳐로 바꿀 것입니다. 여기서 주의할 점은 코드 내의 Chicken을 모두 Turkey로 바꿔주셔야 한다는 겁니다. 찾아바꾸기를 이용하시면 빠르게 할 수 있습니다.

@SideOnly(Side.CLIENT)

public class RenderTurkey extends RenderLiving

{

private static final ResourceLocation chickenTextures = new ResourceLocation("hungryanimals:textures/entities/turkey.png");

 

public RenderTurkey(ModelBase p_i1252_1_, float p_i1252_2_)

{

super(p_i1252_1_, p_i1252_2_);

}

 

/**

* 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(EntityTurkey p_76986_1_, double p_76986_2_, double p_76986_4_, double p_76986_6_, float p_76986_8_, float p_76986_9_)

{

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(EntityTurkey p_110775_1_)

{

return chickenTextures;

}

 

/**

* Defines what float the third param in setRotationAngles of ModelBase is

*/

protected float handleRotationFloat(EntityTurkey p_77044_1_, float p_77044_2_)

{

float f1 = p_77044_1_.field_70888_h + (p_77044_1_.field_70886_e - p_77044_1_.field_70888_h) * p_77044_2_;

float f2 = p_77044_1_.field_70884_g + (p_77044_1_.destPos - p_77044_1_.field_70884_g) * p_77044_2_;

return (MathHelper.sin(f1) + 1.0F) * f2;

}

 

 

/**

* 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((EntityTurkey)p_76986_1_, p_76986_2_, p_76986_4_, p_76986_6_, p_76986_8_, p_76986_9_);

}

 

/**

* Defines what float the third param in setRotationAngles of ModelBase is

*/

protected float handleRotationFloat(EntityLivingBase p_77044_1_, float p_77044_2_)

{

return this.handleRotationFloat((EntityTurkey)p_77044_1_, p_77044_2_);

}

 

/**

* 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((EntityTurkey)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((EntityTurkey)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((EntityTurkey)p_76986_1_, p_76986_2_, p_76986_4_, p_76986_6_, p_76986_8_, p_76986_9_);

}

}

 

3) 텍스쳐

본래의 닭 텍스쳐는 크기가 64*32입니다. 원본을 참조해서 잘 수정해봤습니다. 텍스쳐 파일의 크기가 달라지면 안된다는 사실을 항상 명심하시길 바랍니다. 크기가 달라질 경우에는 쌍둥이 슬라임의 경우처럼 따로 모델 클래스에 지정을 해주셔야합니다.

아래는 실제 png 파일입니다.

 

4) 바운딩 박스

이전처럼 바운딩 박스의 크기를 EntityTurkey에서 설정해줘야 합니다. "this.setSize(0.3F, 0.7F);" 주목하세요. 저게 원래 닭에 설정되어있는 값입니다.

public EntityTurkey(World world) {

    super(world);

    this.setSize(0.3F, 0.7F);

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

    this.tasks.addTask(1, new EntityAIPanic(this, 1.4D));

    this.tasks.addTask(2, new EntityAIMate(this, 1.0D));

    this.tasks.addTask(3, new EntityAITempt(this, 1.0D, Items.pumpkin_seeds, false));

    this.tasks.addTask(4, new EntityAIFollowParent(this, 1.1D));

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

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

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

}

 

 

4. 등록(Registration) 및 시험(Test)

매일 하는 과정이네요. 이제는 이 과정을 적지도 않겠습니다. 참고로 "new RenderTurkey(new ModelTurkey(),0.3f)"에서 0.3f 그림자의 크기를 뜻한다고 합니다.

@Mod.EventHandler

public static void preInit(FMLPreInitializationEvent event) {

    EntityRegistry.registerModEntity(EntityTurkey.class, "turkey", 6, HungryAnimals.instance, 80, 3, false);

    if (proxy instanceof ClientProxy) {

        RenderingRegistry.registerEntityRenderingHandler(EntityTurkey.class, new RenderTurkey(new ModelTurkey(),0.3f));

    }

}

 

전 다음 사진처럼 됐습니다. 여러분은 어떤가요?

 

언제나 질문 받습니다. 댓글로 달아주세요.

+ Recent posts