이 문서는 http://www.minecraftforge.net/wiki/Potion_Tutorial의 문서를 번역한 것입니다. 중간에 번역이 잘못된 곳이 있을 수 있으나 강의를 전달함에는 문제가 없게 번역하고 있습니다.

 

 

제목: 포션 강의

이 문서는 Spartan322가 Lolcroc의 강의를 각색한 것입니다. 난이도는 중급입니다.

 

1. 목표

새로운 포션을 만든다.

포션을 조작한다.

새로운 포션을 사용한다.

// 코멘트: 여기서 포션이란 마시는 아이템이 아닌 플레이어나 동물에게 걸리는 효과를 의미합니다.

 

2. 사전 요구사항

포지 설치

기초 모드 제작 환경 설정

 

3. 포션의 기초

포션은 무기나 갑옷에 추가적인 기능을 부여하거나 새로운 효과, 외관을 만들어내며 심지어는 특수한 방식으로 몬스터를 소환할 때도 사용됩니다. 이 강의에서는 사용시 몬스터를 사용자 근처에 소환하는 포션을 만들어 볼 것입니다. (그런 의미에서 이 강의는 엔티티를 소환하고 다루는 유용한 방법 또한 소개해준다고 할 수 있습니다) 저는 이번 강의에서 보고 배우기 기법으로 가르쳐드릴 것이므로 하나하나 해야 할 것을 가르쳐주리라 기대하지 마세요. 전 어디가 어떻게 작동하는 지만 설명할겁니다.

자 시작해봅시다.

 

4. 포션 추가 도입부

포션 설정

자, 가장 기본적이면서 아무 효과도 없는 포션을 만들어봅시다. 조금 어려운 부분이므로 먼저 보여주고 설명하겠습니다.

일단은 당신의 모드 파일이 다음과 같이 설정되어 있다고 생각하겠습니다.

 

package mods.tutorial.common;

import java.util.Objects;

import mods.zmass.client.ClientProxyMass;
import net.minecraft.block.Block;
import net.minecraft.block.StepSound;
import net.minecraft.block.material.Material;
import net.minecraft.creativetab.CreativeTabs;
import net.minecraft.item.EnumToolMaterial;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.item.EnumAction
import net.minecraft.world.biome.BiomeGenBase;
import net.minecraftforge.common.EnumHelper;
import net.minecraftforge.common.MinecraftForge;
import cpw.mods.fml.common.Mod;
import cpw.mods.fml.common.Mod.EventHandle
import cpw.mods.fml.common.Mod.Init;
import cpw.mods.fml.common.Mod.PreInit;
import cpw.mods.fml.common.SidedProxy;
import cpw.mods.fml.common.event.FMLInitializationEvent;
import cpw.mods.fml.common.event.FMLPreInitializationEvent;
import cpw.mods.fml.common.network.NetworkMod;
import cpw.mods.fml.common.registry.GameRegistry;
import cpw.mods.fml.common.registry.LanguageRegistry;
import net.minecraftforge.common.Configuration;
import cpw.mods.fml.common.network.NetworkMod;
import cpw.mods.fml.common.registry.EntityRegistry;



@Mod(modid = "TutorialMod", name = "Tutorial Mod", version = "Version Tutorial Version")

@NetworkMod(clientSideRequired = true, serverSideRequired = false)
public class TutorialMod
{
    @SidedProxy(clientSide = "mods.Tutorial.client.ClientProxy",
                serverSide = "mods.tutorial.common.CommonProxy")

    @EventHandler
    public void preInit(FMLPreInitializationEvent event)
    {
        //PreInit tasks
    }
        
    @EventHandler
    public void load(FMLInitializationEvent event)
    {
        //Load tasks
    }
}

 

위가 이해가 되지 않는다면 더 기초적인 강의를 보고 오셔야 합니다.

 

이제는 특별한 포션 객체를 만들어야 할 시간입니다. 다음의 내용을 새로운 파일에 작성해도 좋고, 기본 모드 파일에 작성해도 좋으니 알아서 하세요.

 

public static Potion PotionModName;


@EventHandler
public void preInit(FMLPreInitializationEvent event) {
    Potion[] potionTypes = null;

    for (Field f : Potion.class.getDeclaredFields()) {
        f.setAccessible(true);
        try {
            if (f.getName().equals("potionTypes") || f.getName().equals("field_76425_a")) {
                Field modfield = Field.class.getDeclaredField("modifiers");
                modfield.setAccessible(true);
                modfield.setInt(f, f.getModifiers() & ~Modifier.FINAL);

                potionTypes = (Potion[])f.get(null);
                final Potion[] newPotionTypes = new Potion[256];
                System.arraycopy(potionTypes, 0, newPotionTypes, 0, potionTypes.length);
                f.set(null, newPotionTypes);
            }
        } catch (Exception e) {
            System.err.println("Severe error, please report this to the mod author:");
            System.err.println(e);
        }
    }

    MinecraftForge.EVENT_BUS.register(new ModNameEventHooks());
}

 

위의 내용을 쉽게 설명하자면 포지가 새로운 포션을 추가하는 기능을 제공하기 이전까진 위의 코드가 여러분이 새 포션을 추가할 수 있게 해 줄 것이란 거고 어렵게 말하자면 새로운 포션 배열을 만들어서 기존의 마인크래프트 메소드를 통해서 전달할 필요 없이 포션을 만들 수 있게 해 준다는 겁니다. 그 이외에도 이름을 바꾸고 효과를 부여하고 하는 등 많은 것들을 할 수 있습니다. http://www.minecraftforum.net/forums/archive/tutorials/931752-forge-creating-custom-potion-effects에서 코드를 참조했습니다.

 

자 이제 포션을 추가해봅시다.

 

포션을 추가함에 있어 먼저 해야 할 것은 우리가 아는 거랑 비슷한지 확인하기 위해 Potion.java를 한번 보는 겁니다.

.

package net.minecraft.potion;

import com.google.common.collect.Maps;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import java.util.Iterator;
import java.util.Map;
import java.util.UUID;
import java.util.Map.Entry;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.SharedMonsterAttributes;
import net.minecraft.entity.ai.attributes.Attribute;
import net.minecraft.entity.ai.attributes.AttributeInstance;
import net.minecraft.entity.ai.attributes.AttributeModifier;
import net.minecraft.entity.ai.attributes.BaseAttributeMap;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.util.DamageSource;
import net.minecraft.util.StringUtils;

public class Potion
{
    /** The array of potion types. */
    public static final Potion[] potionTypes = new Potion[32];
    public static final Potion field_76423_b = null;
    public static final Potion moveSpeed = (new Potion(1false8171462)).setPotionName("potion.moveSpeed").setIconIndex(00).func_111184_a(SharedMonsterAttributes.field_111263_d"91AEAA56-376B-4498-935B-2F7F68070635", 0.20000000298023224D, 2);
    public static final Potion moveSlowdown = (new Potion(2true5926017)).setPotionName("potion.moveSlowdown").setIconIndex(10).func_111184_a(SharedMonsterAttributes.field_111263_d"7107DE5E-7CE8-4030-940E-514C1F160890"-0.15000000596046448D, 2);
    public static final Potion digSpeed = (new Potion(3false14270531)).setPotionName("potion.digSpeed").setIconIndex(20).setEffectiveness(1.5D);
    public static final Potion digSlowdown = (new Potion(4true4866583)).setPotionName("potion.digSlowDown").setIconIndex(30);
    public static final Potion damageBoost = (new PotionAttackDamage(5false9643043)).setPotionName("potion.damageBoost").setIconIndex(40).func_111184_a(SharedMonsterAttributes.field_111264_e"648D7064-6A60-4F59-8ABE-C2C23A6DD7A9", 3.0D, 2);
    public static final Potion heal = (new PotionHealth(6false16262179)).setPotionName("potion.heal");
    public static final Potion harm = (new PotionHealth(7true4393481)).setPotionName("potion.harm");
    public static final Potion jump = (new Potion(8false7889559)).setPotionName("potion.jump").setIconIndex(21);
    public static final Potion confusion = (new Potion(9true5578058)).setPotionName("potion.confusion").setIconIndex(31).setEffectiveness(0.25D);

    /** The regeneration Potion object. */
    public static final Potion regeneration = (new Potion(10false13458603)).setPotionName("potion.regeneration").setIconIndex(70).setEffectiveness(0.25D);
    public static final Potion resistance = (new Potion(11false10044730)).setPotionName("potion.resistance").setIconIndex(61);

    /** The fire resistance Potion object. */
    public static final Potion fireResistance = (new Potion(12false14981690)).setPotionName("potion.fireResistance").setIconIndex(71);

    /** The water breathing Potion object. */
    public static final Potion waterBreathing = (new Potion(13false3035801)).setPotionName("potion.waterBreathing").setIconIndex(02);

    /** The invisibility Potion object. */
    public static final Potion invisibility = (new Potion(14false8356754)).setPotionName("potion.invisibility").setIconIndex(01);

    /** The blindness Potion object. */
    public static final Potion blindness = (new Potion(15true2039587)).setPotionName("potion.blindness").setIconIndex(51).setEffectiveness(0.25D);

    /** The night vision Potion object. */
    public static final Potion nightVision = (new Potion(16false2039713)).setPotionName("potion.nightVision").setIconIndex(41);

    /** The hunger Potion object. */
    public static final Potion hunger = (new Potion(17true5797459)).setPotionName("potion.hunger").setIconIndex(11);
    
    /** Rest of class omitted */
}

 

여기가 코드의 윗부분인데, 블록이나 아이템 등록 과정과 상당히 유사하단 걸 알 수 있습니다. 저 위에서 하나를 복사해서 우리에게 맞게 바꿀 수 있겠네요.

 

 /** The hunger Potion object. */
    tutorialPotion = (new PotionModName(40false0)).setPotionName("potion.potionName").setIconIndex(00);

 

일단 이걸 복사하면 몇 가지 오류가 생길 겁니다. 당장은 오류를 무시하도록 하고.

포션 클래스 생성자의 첫 번째 인자는 포션 ID입니다. 포션 ID는 프로그램이 포션을 구분하는 이름표와 같은 겁니다. 그렇기 때문에 절대 겹쳐서는 안됩니다. 포지가 알아서 겹친 ID를 재 설정 해 주지는 않기 때문이죠. 두 번째 인자는 포션이 완전한 효과를 가지는지 아닌지에 대한 설명입니다ㅣ. 'True'라면 효과가 절반만 적용 됩니다. 마지막 인자는 포션 병의 색을 나타냅니다. 지금 당장은 신경 쓰지 않겠습니다.(사전식, html 색 지정을 잘 아는 사람이라면 쉬운 내용입니다)

포션의 이름(Potion Name)은 코드가 포션을 알아내는 이름표입니다. 사실 여러분의 포션은 모드 파일로부터 만들어지기 때문에 딱히 이름이 쓸모는 없으나 그냥 붙입니다. 아이콘 주소(Icon Index)는 포션이 적용되었을 때 인벤토리에 보여지는 아이콘을 설정하는 기능입니다. 마인크래프트의 포션 아이콘은 하나의 거대한 텍스쳐 파일로 저장되는데, 거기서 어떤 좌표에 원하는 아이콘이 있는 지를 설정하는 겁니다. (x, y)인 형식인데, 각각 0~7의 값이 들어갑니다.(원본 설명이 부족하여 역자인 제가 덧붙입니다)

 

이제는 굉장히 간단한 부분입니다. Potion 클래스를 상속하는 클래스를 만들 건데, 사실 내용물은 하나도 없습니다.

 

package mod.tutorial.common;

import net.minecraft.potion.Potion;

public class PotionModName extends Potion {

  public PotionModName(int par1, boolean par2, int par3) 
  {
     super(par1, par2, par3);
  }

   public Potion setIconIndex(int par1, int par2) 
   {
     super.setIconIndex(par1, par2);
     return this;
   }
}

 

이게 여러분이 처음 만들어야 할 기본 Potion 클래스입니다. 이 클래스가 다른 포션 효과를 만들 때도 계속 기본 틀로 사용될 것이므로 이름을 PotionMod.java 처럼 범용적으로 짓는 것이 좋습니다. 이건 정말 기본에 불과하지만 여러분이 훨씬 멋있는 효과를 넣고자 한다면 결국엔 여러 내용이 추가 되야 만 할 것입니다.

 

5. 효과 부여하기

자 이제 제일 재미있는 부분입니다. 이제 사람, 엔티티, 심지어는 월드에 적용될 엄청난 효과들을 만들어낼 차례입니다. 그러기 위해서 이벤트 후킹을 할 파일을 하나 만들어야 합니다. 일단 코드를 보고 이야기 합시다.

 

package mods.tutorial.common;

public class ModNameEventHooks {



@ForgeSubscribe
public void onEntityUpdate(LivingUpdateEvent event) 
{
     //entityLiving in fact refers to EntityLivingBase so to understand everything about this part go to EntityLivingBase instead
     if (event.entityLiving.isPotionActive(ModName.potionName)) 
     {
         if (event.entityLiving.worldObj.rand.nextInt(20) == 0) 
         {
                  
         }
     }
}

 

onEntityUpdate라는 메소드는 이벤트 후킹에 의해서 살아 움직이는 모든 EntityLiving에 대해서 호출됩니다. 이제 우리는 그 중 우리가 만든 포션 효과가 적용된 엔티티에 대해서만 살펴본다는 의미입니다. 이 메소드는 매 틱 실행되기 때문에 중간에 nextInt를 이용해서 1/20의 확률로 발동되게 했습니다.

자 다음의 코드는 Zombie.java에 있는 NPC를 좀비로 바꾸는 코드입니다.

 

EntityZombie entityzombie = new EntityZombie(event.entityLiving.worldObj);
entityzombie.copyLocationAndAnglesFrom(event.entityLiving);
entityzombie.func_110161_a((EntityLivingData)null);
entityzombie.func_82187_q();

event.entityLiving.worldObj.removeEntity(event.entityLiving);
event.entityLiving.worldObj.spawnEntityInWorld(entityzombie);

 

자 이제 우리가 할 것은 이 포션에 중독된 엔티티를 서서히 죽이고 좀비를 소한하는 것입니다.

일단 좀비가 소환되는 내용을 적어보면 다음과 같이 될 겁니다.

 

package mods.tutorial.common;

public class ModNameEventHooks {



@ForgeSubscribe
public void onEntityUpdate(LivingUpdateEvent event) 
{
     //entityLiving in fact refers to EntityLivingBase so to understand everything about this part go to EntityLivingBase instead
     if (event.entityLiving.isPotionActive(ModName.potionName)) 
     {
         if (event.entityLiving.worldObj.rand.nextInt(20) == 0) 
         {
                  
                    EntityZombie entityzombie = new EntityZombie(event.entityLiving.worldObj);
                    entityzombie.copyLocationAndAnglesFrom(event.entityLiving);
                    entityzombie.func_110161_a((EntityLivingData)null);
                    entityzombie.func_82187_q();

            

                    event.entityLiving.worldObj.removeEntity(event.entityLiving);
                    event.entityLiving.worldObj.spawnEntityInWorld(entityzombie);

         }
     }
}

 

자 다음에 추가된 것은 포션이 데미지를 가하고, 죽음을 인식하는 내용입니다. 이 정도면 좀비 감염 바이러스가 충분히 구현된 것 같습니다.

 

package mods.tutorial.common;

public class ModNameEventHooks {



@ForgeSubscribe
public void onEntityUpdate(LivingUpdateEvent event) 
{
     //entityLiving in fact refers to EntityLivingBase so to understand everything about this part go to EntityLivingBase instead
     if (event.entityLiving.isPotionActive(ModName.potionName)) 
     {
         if (event.entityLiving.worldObj.rand.nextInt(20) == 0) 
         {

            event.entityLiving.attackEntityFrom(DamageSource.generic2);

            if( event.entityLiving.isDead() == true )
                  
                    EntityZombie entityzombie = new EntityZombie(event.entityLiving.worldObj);
                    entityzombie.copyLocationAndAnglesFrom(event.entityLiving);
                    entityzombie.func_110161_a((EntityLivingData)null);
                    entityzombie.func_82187_q();

            

                    event.entityLiving.worldObj.removeEntity(event.entityLiving);
                    event.entityLiving.worldObj.spawnEntityInWorld(entityzombie);

         }
     }
}

 

자 마지막으로 알아볼 내용은 특정한 엔티티에게 포션 효과를 적용시키는 것입니다.

 

entityliving.addPotionEffect(new PotionEffect(ModName.potionName.id2001false));

 

첫 번째 인자는 포션의 ID, 두 번째 인자는 지속 시간, 세 번째 인자는 효과 레벨입니다. 1이 기본이고 두 개 이상의 레벨을 가질 수는 없습니다. 마지막은 신호기에서 발생된 효과인지를 나타내는 인자인데 false로 하고 싶으면 그냥 안 적어도 됩니다.

지속시간을 적용시킬 거라면 다음의 코드가 이벤트 후크에 적으세요.

 

if (event.entityLiving.getActivePotionEffect(Yourmod.customPotion).getDuration()==0) 
{
            event.entityLiving.removePotionEffect(Yourmod.customPotion.id);
            return;
}

 

사전 요구사항 :

  • 기본 자바 지식
  • 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));

    }

}

 

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

 

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

해당 강의의 개발환경은 Forge : 1.7.10-10.13.0.1180, IDE : Eclipse입니다.

블록, 아이템 추가와 기본적인 모드 제작 능력이 있다고 가정하고 쓰는 글 입니다. 난이도는 중급 이상이므로 이 점 고려하시고 읽어주시길 바랍니다.

 

이 강좌에서 소개되는 방법은 코드를 이용하여 모델링을 하는 것을 주로 하고 있으며, 외부 프로그램에서 직접적으로 들여와서 게임에 적용시키는 것이 아니라는 것을 명심해주시길 바랍니다.

 

저번 강의에서 엔티티를 추가하기 위해서는 3가지의 클래스가 필요하다고 했습니다. 그 각각은 :

1) 엔티티 클래스 : 실질적인 엔티티의 행동양식을 담고 있다. -[Server]

2) 모델 클래스: 엔티티의 3D 정보를 담고 있다. -[Client]

3) 렌더 클래스: 엔티티의 겉 표면 그래픽을 담고있다. -[Client]

 

오늘은 이 중 2)과 3)을 직접 만들어 보겠습니다.

 

 

1.모델 클래스 만들기

 

1) 클래스 기본 형태 갖추기

모델을 만들기 위해서는 ModelBase를 상속하는 클래스를 먼저 만들어야 됩니다. 그리고 또한 모델은 클라이언트 사이드 클래스라서 어노테이션(Annotation)을 입력해줘야 합니다. 예제로 간단히 쌍둥이 슬라임을 만들어봅시다.

@SideOnly(Side.CLIENT)

public class ModelTwinSlime extends ModelBase {

 

    public ModelTwinSlime()

{

        

}

 

}

슬라임과 똑같이 만들면 재미가 없으니 이름을 TwinSlime으로 하고 모델링을 시작해 보겠습니다.

 

 

2) 구상하기

가장 먼저 해야 할 것은 구상입니다. 어떤 모습으로 모델링을 할 건지를 먼저 확실하게 떠올려야 작업을 진행할 수 있습니다. 아니면 사이즈, 위치, 텍스쳐 등을 여러 번 수정해야 하기 때문에 굉장히 작업이 더디고 느려지게 됩니다. 이때는 다른 3D 렌더링 프로그램을 이용해서 스케치를 해보는 것도 도움이 됩니다. 추천하는 프로그램은 블렌더나 구글 스케치 업과 같은 무료 프로그램들입니다.

이 정도로 배치할 육면체의 개수와 각각의 위치, 크기, 각도를 명확하게 스케치하면 작업이 차질 없이 진행될 수 있습니다. (위는 블렌더를 이용해서 스케치했습니다.)

 

 

3) 모델링(Modeling)

ModelTwinSlime 클래스 안에 각 육면체에 해당하는 ModelRenderer를 선언하겠습니다. ModelRenderer는 렌더링을 위한 클래스이기도 하면서, 모델링 중에는 각 객체가 하나의 그룹 역할을 하게 됩니다. 이렇게 그룹을 나누는 이유는 애니메이션을 만들 때 ModelRenderer 단위로 회전 시키고, 또한 복사나 반사 대칭과 같은 기능이 제공되어 굉장히 유용하기 때문입니다. 따라서 그룹을 잘 만들면 대상이 복잡해 질수록 작업량을 크게 줄일 수 있습니다.

@SideOnly(Side.CLIENT)

public class ModelTwinSlime extends ModelBase {

 

    private ModelRenderer top;

    private ModelRenderer bot;

    

    public ModelTwinSlime()

{

          

}

 

}

Top이 위에 있는 작은 슬라임을, bot이 아래에 있는 큰 슬라임을 표현할 것입니다.

 

ModelTwinSlime의 생성자에서 이제 top과 bot의 모델링을 진행하여야 합니다. 각각에 대해 새로운 객체를 만듭니다.

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

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

여기서 인자(parameter)에 대해서 설명하자면, 첫 번째는 렌더러의 대상 모델(this), 두 번째와 세 번째는 텍스쳐의 위치(offset)을 의미하는데 이것은 다음의 그림을 참조하여 결정해봅시다.

 

모델 렌더러에 육면체를 추가하여 그리는 경우, 그 전개도는 다음과 같이 됩니다. 하늘색이 앞부분을 의미하며 T는 윗면, B는 아랫면에 해당합니다. 여기서 두 번째, 세 번째 인자는 전개도의 왼쪽 윗 점의 좌표(x, y)를 의미합니다.

 

ModelTwinSlime에서는 bot의 경우 x,z가 1일 때 y가 0.8입니다.(그렇게 구상되었습니다) 이것을 적당히 20배하여 텍스쳐를 제작한다고 가정하면 x, z는 20픽셀, y는 16 픽셀이 됩니다. 따라서 저 전개도의 높이는 36이 되어서 두 번째 육면체에 대한 오프셋은 (0,36)이 되면 알맞게 설정이 됩니다.(텍스쳐 파일 상에서 bot을 위에, top을 아래에 그리겠습니다.) 그리고 또한 전체적으로 필요한 텍스쳐의 크기가 가로는 80, 세로는 50이되므로 그것 또한 적용해주면.

public ModelTwinSlime()

{

    textureWidth = 80;

    textureHeight = 50;

    this.top = new ModelRenderer(this,0,36);

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

}

이렇게 됩니다. 텍스쳐 크기를 명확하게 정하지 않으면 렌더링이 이상하게 될 수 있으므로 주의하세요.

 

이제 각각에 적절한 크기의 육면체를 추가합니다. 이때 모두 육면체의 중심을 원점으로 설정해주세요. Top의 경우에는 스케치에서는 그 중심이 원점이 아니지만 회전을 한 후에 추가적으로 옮겨줄 겁니다.

    This.top = new ModelRenderer(this,0,36);

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

    bot.addBox(-10, -8, -10, 20, 16, 20);

    top.addBox(-4, -3, -4, 8, 6, 8);

addBox라는 메소드를 이용하는데 각 인자에 대해서 설명을 하면, 123번 인자는 직육면체의 한 모서리의 좌표(좌표축 상에서 x,y,z좌표 값이 가장 작은)이고 456번 인자는 x,y,z축 길이입니다. 여기서 주의할 점은 y좌표 값이 작아질수록 실제 게임에서는 위쪽에 그려진다는 것입니다. 박스를 추가하는데 필요한 좌표와 길이는 처음에 스케치를 했던 블렌더에서 몇 가지 정보를 가져와 addBox의 인자가 요구하는 값으로 변환시킨 것입니다.

 

이제 top을 Y축에 대해서 45도 회전시킨 다음에, 우리가 블렌더에서 계획했던 위치로 평행이동 시키겠습니다. Top과 bot의 높이의 합이 11이기 때문에 높이 방향(Y)으로는 11만큼 평행이동 시켰습니다.

    this.top = new ModelRenderer(this,0,36);

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

    bot.addBox(-10, -8, -10, 20, 16, 20);

    top.addBox(-4, -3, -4, 8, 6, 8);

    

    top.rotateAngleY = 0.25f*(float)Math.PI;

    top.setRotationPoint(3, -11, 3);

rotateAngleY는 Y축을 중심으로 대상을 얼만큼 회전시키는 가에 대한 변수인데, 라디안(Radian)단위이므로 180도 = 1파이 라는 것을 유의하세요. setRotationPoint는 회전 후에 대상을 평행이동 하게 합니다.

이렇게 하면 우리가 최초에 스케치했던 모양대로 모델링이 완성됩니다.

 

그러나 여기서 한 가지 고려해야 할 점은 실제 엔티티의 바운딩 박스(bounding box)의 가장 아래 부분의 좌표가 y=24라는 것 입니다. 즉, 지금 위처럼 박스들의 y좌표를 설정하면 실제로 엔티티가 붕 뜬 것처럼 나오게 됩니다.

이렇게 말이죠. F3 + B를 눌러보면 실제 엔티티의 바운딩 박스를 확인해 볼 수 있는데,

보이는 바와 같이 훨씬 아랫쪽에 바운딩 박스가 있습니다. 이 문제를 해결하기 위해서 각 박스(top, bot)의 위치를 y=24에 맞추어서 평행이동 해줘야 합니다.

public ModelTwinSlime()

{

    textureWidth = 80;

    textureHeight = 50;

    this.top = new ModelRenderer(this,0,36);

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

    bot.addBox(-10, 8, -10, 20, 16, 20);

    top.addBox(-4, 13, -4, 8, 6, 8);

 

    top.rotateAngleY = 0.25f*(float)Math.PI;

    top.setRotationPoint(3, -11, 3);

}

이렇게 맞추어 주시면 정확하게 렌더링 됩니다. (지금 슬라임에게 텍스쳐가 있는 이유는 제가 붙여놨기 때문이고, 아직까지 강의에는 그 방법이 나오지 않았습니다. 조금 더 읽어보세요)

 

마지막으로 render 메소드를 오버라이드해서 top과 bot을 렌더링하도록 해야 합니다.

@SideOnly(Side.CLIENT)

public class ModelTwinSlime extends ModelBase {

 

    private ModelRenderer top;

    private ModelRenderer bot;

    

    public ModelTwinSlime()

{

        textureWidth = 80;

        textureHeight = 50;

        this.top = new ModelRenderer(this,0,36);

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

        bot.addBox(-10, 8, -10, 20, 16, 20);

        top.addBox(-4, 13, -4, 8, 6, 8);

 

        top.rotateAngleY = 0.25f*(float)Math.PI;

        top.setRotationPoint(3, -11, 3);

}

 

    public void render(Entity par1Entity, float par2, float par3, float par4,

            float par5, float par6, float par7) {

        

        this.top.render(par7);

        this.bot.render(par7);

        

    }

}

 

 

2. 렌더 클래스 만들기

 

1) 렌더 클래스 만들기

이제 텍스쳐(Texture)를 설정해주는 렌더 클래스를 만들어보겠습니다. 렌더 클래스는 RenderLiving을 상속하게 하여 만들게 됩니다. 텍스쳐가 변하지 않는 경우는 상당히 간단하게 끝납니다.

public class RenderTwinSlime extends RenderLiving {

 

    protected ResourceLocation texture;

    

    public RenderTwinSlime(ModelBase p_i1262_1_, float p_i1262_2_) {

        super(p_i1262_1_, p_i1262_2_);

        texture = new ResourceLocation("hungryanimals:textures/entities/twinslime.png");

        // TODO Auto-generated constructor stub

    }

    

    @Override

    protected ResourceLocation getEntityTexture(Entity p_110775_1_) {

        // TODO Auto-generated method stub

        return texture;

    }

 

}

여기서 중요한 것은 png파일의 경로입니다. "…\Main\resources\assets\hungryanimals\textures\entities\twinslime.png"가 실제 경로고, assets까지의 코드를 인식하기 때문에 우리가 입력해야 하는 경로의 형식은 위에 코드와 같습니다.

 

2) 텍스쳐(Texture) 만들기

텍스쳐는 항상 아래의 그림과, 우리가 코드상에서 설정한 텍스쳐 오프셋을 잘 고려하여 찍어주시면 됩니다. 파일명은 렌더 클래스에서 설정한 것과 같이 twinslime.png여야 하고, 각 전개도의 위치도 위와 같아야 합니다.

이런 식으로 말이죠 아래의 사진은 실제 png파일입니다.

 

 

3. 엔티티 클래스 만들기

 

엔티티 클래스는 딱히 열심히 만들지는 않을 것입니다. 우리가 렌더링이 잘 되는 지 확인하기 위한 용도이므로, 다음의 코드를 복사만 하셔도 좋습니다. 다만, 중간의 setSize()는 주목하셔야 합니다. setSize는 바운딩 박스의 크기를 변환시켜주는 메소드로써 첫 번째 인자가 바운딩 박스의 xz축 길이, 두 번째 인자가 y축 길이입니다.(단위는 픽셀이 아닌, 마인크래프트 월드상의 좌표와 같습니다. 즉 1이 한 블록 크기입니다.)

public class EntityTwinSlime extends EntityLiving {

    public EntityTwinSlime(World p_i1582_1_) {

        super(p_i1582_1_);

        setSize(1.25f, 1f);

    }

}

바운딩 박스의 크기가 알맞게 설정된 것을 확인할 수 있습니다.

 

 

4. 엔티티 등록하기 & 소환하여 확인하기

 

이 부분은 이전 강좌의 내용과 100% 일치합니다. 코드와 사진만 붙이겠습니다.

@Mod.EventHandler

public static void preInit(FMLPreInitializationEvent event) {

    

    EntityRegistry.registerModEntity(EntityTwinSlime.class, "twinSlime", 5, ExampleMod.instance, 80, 3, false);

    

    if (proxy instanceof ClientProxy) {

     RenderingRegistry.registerEntityRenderingHandler(EntityTwinSlime.class, new RenderTwinSlime(new ModelTwinSlime(),1f));

}

}

 

 

끝. 귀엽네요. 엔티티 클래스에 내용이 없어 숨만 쉴 뿐 아무것도 하지 않습니다. 여러분이 어떤 내용을 짜 넣는가에 따라 행동이 달라지겠지만 그와 관련된 내용은 다음 강의부터 다뤄보도록 하겠습니다. 질문 있으시면 댓글로 달아주세요.

 

 

 

+ Recent posts