1.0.4.0 업데이트 이후 오랜 시간이 걸렸습니다. 제가 게을렀거든요. 다운로드는 아래에 있고 정식 게시글은 http://www.minecraftforum.net/forums/mapping-and-modding/minecraft-mods/2228763-hungry-animals-more-realistic-animals 입니다.

1.0.4.1 http://adf.ly/z8GXr (1.7.10-10.13.2.1291)

 

1.0.4.0에서 동물 사냥을 위한 도구를 추가하고, 1.0.4.1에서는 동물 사냥을 어렵게 하는 동시에 조련 시스템이 추가되었습니다. 동물이 이제 플레이어로부터 도망가고 체력이 늘어납니다. 또한 야생 동물은 음식에 잘 유혹되지 않기 때문에 함정이나 지형적 이점, 몰이 사냥 등을 통해 사냥하세요. 이후 얻은 힘줄로 사냥추와 밧줄을 만들어 포획하시면 되겠습니다. 이렇게 포획된 동물은 특수한 방법으로 조련시킬 수 있습니다. 조련되면 도망가지도 않고 음식에 잘 따르게 됩니다.

 

변경 사항

  1. 새총
    피해량이 1에서 2로 상향되었습니다.
    버그 수정 (플레이어가 새총에 맞아도 화살이 박히는 효과)
  2. 큰 풀
    성장 속도가 설정 가능합니다.
  3. 배설물
    발효 메커니즘이 바뀌어서 쌓였을 때 보다 더 발효가 빨라집니다.
    버그 수정 (비 정상적인 비옥화(풀 위에서 배설물이 사라지는 것) 속도, 크리에이티브 모드에서 배설물 아이템 소모)
  4. 스폰
    버그 수정 (스폰 에그 사용시)
  5. 교배
    임신기간(?)이 8배 길어집니다.
    성장기간이 4배 길어집니다.
    임신기간과 성장기간이 음식을 먹을 때마다 줄어들어, 야생상태보다 최대 2배 짧아집니다.

    ( 아래 표에서 일은 마인크래프트 하루 = 24000 ticks )
     

    야생

    임신기간

    2 일

    1 일

    성장기간

    4 일

    2 일

  6. 동물
    깃털과 가죽 획득량이 늘어납니다. (늘어난 임신/성장기간에 대한 대가입니다)
    고기 획득량이 더 이상 무작위가 아닌, 동물의 허기에 비례합니다. (일반적인 경우 상향된 결과를 보여줍니다)
    체력이 거의 2배가 됩니다. 이미 생성되어 있는 동물에게는 적용되지 않을 수도 있습니다. (닭 8, 소 30, 양/돼지 20)
    허기가 충분하면 천천히 회복합니다. 물론 이 과정은 허기를 소모합니다.
    조련 시스템이 추가됩니다.

 

아래는 조련과 관련된 표 입니다.

 

배척 (~ -1)

중립 (-1 ~ +1)

우호 (+1 ~)

조련법

중립화

플레이어 근처에서

플레이어가 없으면

배척화

공격 당했을 때

우호화

음식을 먹을 때 ( 떨어진 음식, 손에 들린 음식 모두 )

행동

도망

플레이어 근처에서

도망가지 않음.

공격 당했을 때

도망가지 않음.

떨어진 음식

가끔 먹음.

자주 먹음.

항상 먹음.

손에 들린 음식

절대 안 먹음.

항상 먹음.

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

2.1 개발 현황  (0) 2015.10.26
2.0 업데이트  (3) 2015.08.10
1.0.4.0 업데이트  (2) 2015.02.05
1.0.3.1 업데이트  (0) 2015.02.05
1.0.3 업데이트  (1) 2015.02.05

이 문서는 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));

}

}

 

 

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

 

 

 

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

 

이번 버전에서는 크게 3가지 변화가 있습니다. 일단은 많은 버그를 해결해서 야생 동물들이 스스로의 삶을 영위할 수 있게 되었다는 것입니다. 두 번째는 인공 지능에 새로운 개념을 도입해서 더욱 안정적인 행동 양식을 창출했다는 점이고 마지막은 동물들이 을 싼다는 겁니다! 이번 버전에서는 1.0.3.1와 같이 더 세부적인 버전이 나올 수도 있습니다. 인공 지능 함수를 계속 개선한다던가 생태계 안정을 위해 변수들의 기본 값을 적절히 연구해 나갈 것이기 때문입니다. 1.0.4 버전 이전에는 배설물과 관련된 시스템, 기본적인 인공지능이 마무리 될 것입니다. 앞으로는 아마 배설물을 이용하는 콘텐츠가 많이 추가될 것으로 예상됩니다. 질병과 초석 제작을 계획하고 있습니다. 물론 퇴비로 만들어 뿌리는 것도 하고 있죠.

 

200 TPS로 디버깅을 할 수 있어 많은 오류들을 찾을 수 있었습니다. 대표적인 예로 동물들이 X,Z축 음수인 방향으로 계속 움직이던 경향이 있던 문제는 고속으로 돌리면 바로 확인이 됩니다.

 

다운로드는 다음의 링크를 이용해주세요. : http://adf.ly/siKoV

 

 

0. 개발환경

마인크래프트 - 1.7.10

포지 - 10.13.0.1180

 

1. 변경 사항

  1. 언어
    1. 한국어 언어 파일 추가
      동물 이름부터 컨피그 툴팁(Configuration Tooltip)까지 한국어를 지원합니다.

 

  1. 동물
    1. 월드 생성(스포닝)
      *다른 모드에서 추가된 바이옴에서는 배고픈 동물이 아닌 바닐라 동물이 나오는 잠재적 오류를 해결 (보고 및 확인된 적 없음)
    2. 배설물 수치
      *동물은 이제 자신이 소모한 허기량에 비례하여 배설물 수치가 증가합니다. 배설물 수치는 0~1까지의 값을 가집니다.
    3. 인공지능(AI)
      *배설물 수치가 1이되면 그 자리에서 배설물블록을 생성합니다. 만약 같은 장소에 배설물 블록이 있다면 메타데이터 값이 증가 합니다(이것은 사실 엄밀하게는 AI를 통해 이뤄지는 것은 아닙니다).
      *동물들이 풀을 자주 먹을 것이고 이는 개체 수 증가를 야기할 것입니다. 기초 대사량이나 풀의 열량을 조절해보세요. (풀의 기본 열량 제공이 2.0에서 1.0으로 하향되었습니다)
      *동물의 이동 위치 선정 알고리즘에 큰 변화가 있습니다. 무리를 지으며 동시에 배설물을 피하고 풀을 찾아 다닐 것입니다. 또한 이들은 서로 너무 과하게 밀집하는 것을 피합니다.
      *동물이 플레이어로부터 일정 거리이상 떨어지면 인공지능이 작동하지 않던 버그를 해결했습니다.
      *바닐라 상의 코딩 문제로 동물들이 점진적으로 x-, z- 방향으로 이동하던 경향이 있는 것을 해결했습니다. 긴 시간을 두고 관찰할 경우 굉장히 심각한 문제였습니다.

 

  1. 블록
    1. 배설물블록(BlockExcreta)
      1. 성질
        소재(Material)은 회로(circuit)입니다. 이에 따라 물에 의해 파괴될 수 있습니다.
        배설물과 퇴비는 한 블록 안에 총 1~4개까지 존재 할 수 있습니다. 그 정보는 블록의 메타데이터(Metadata)에 저장됩니다.
        이러한 메타데이터 값은 블록의 단단함(Hardness)에도 영향을 미쳐서 큰 배설물일수록 파괴하는데 많은 시간이 걸립니다.

  1. 특성
    중력에 의해 영향을 받아 받쳐주는 블록이 없을 경우 아래로 떨어집니다. 이때 아래의 블록이 같은 배설물이면 아래의 블록과 합쳐집니다(메타데이터 값으로 저장된 총 배설물량은 보존됩니다).
    배설물 블록을 같은 장소에 설치하면 블록의 메타데이터에 저장된 배설물량이 증가합니다.
  2. 외형
    포함된 배설물의 양(메타데이터)에 따라 높이가 달라집니다. 충돌 판정이 없어 통과 가능하지만 머리가 잠길 경우에는 피해를 입습니다. 퇴비는 무조건 아래에 깔리며 회색 빛을 띕니다.
  3. 아이템
    인벤토리에 저장되는 배설물은 낱개로 저장됩니다. 즉 꽉 찬 4개 들이 배설물을 파괴하면 4개의 작은 배설물 블록이 나옵니다.


  1. 새로운 시스템: 발효(Fermentation)와 퇴비(Manure)
    1. 발효
      발효는 배설물블록에 의해서 이뤄집니다. 발효를 통해서 퇴비를 얻을 수 있습니다.
      1. 발효 조건
        배설물블록이 있다면 시간이 지남에 따라서 배설물블록이 점차 퇴비로 변해갑니다.
      2. 발효 속도
        배설물블록 하나가 가지고 있는 배설물의 양에 따라 발효 속도가 빨라지며, 근처의 블록들이 포함하는 배설물의 양도 영향을 미칩니다.
    2. 퇴비
      1. 퇴비의 용도
        퇴비는 토양을 비옥하게 할 수 있습니다. 일단 배설물로부터 수확하게 되면 다시 블록의 형태로 놓을 수 없으며, 뼛가루를 만들 수 있습니다.
      2. 퇴비 손실
        퇴비는 흙이나 풀 위에 놓여있으면 자연스럽게 흙 속으로 흡수됩니다. 모래에도 흡수될 수 있는데 이 경우는 모래를 흙으로 바꿉니다.

 

2. 저작권

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

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

1.0.4.0 업데이트  (2) 2015.02.05
1.0.3.1 업데이트  (0) 2015.02.05
1.0.2 업데이트  (0) 2015.02.05
1.0.1 업데이트  (0) 2015.02.05
모드 소개  (0) 2015.02.05

1.1 버전을 내놓기 이전에 조금 더 동물의 행동을 개선 하기로 결정했습니다. 동물의 힘을 이용한 기계는 거의 완성단계에 이르렀으며 약간의 버그만 해결하면 개발이 완료됩니다.

 

이번 버전에서는 동물을 개선하면서 기틀을 닦았습니다. 눈에 띄는 큰 변화로는 컨피그로 동물과 관련된 여러 상수를 조작할 수 있다는 점, 동물들이 이제는 무리를 지으려 한다는 점, 마지막으로 움직이는 속도에 비례해서 추가적으로 허기를 소모한 다는 점이 있겠습니다.

 

다운로드는 다음의 AdFly를 이용해주시길 바랍니다.

링크: http://adf.ly/sSISJ

 

 

0. 제작 환경

마인크래프트 – 1.7.10

Forge – 10.13.0.1180

 

 

1. 변경 사항

1) 동물과 상호작용

이제는 음식을 들고 있으면 우클릭을 통해 동물에게 음식을 먹일 수 있습니다. 그렇다고 바닐라처럼 바로 동물이 사랑에 빠지는 것은 아닙니다. 충분히 배가 불러진 후에 확률적으로 사랑에 빠지게 될 겁니다.

 

2) 동물의 허기

동물은 더 빨리 걸을수록 배고파집니다. 밧줄로 동물을 끌고 갈 때 굶어 죽지 않도록 주의하세요.

이제 밀을 먹던 동물들은 사탕수수도 먹게 되었습니다. 그러나 이것은 별로 중요한 변경 사항이 아닙니다. 동물이 무엇을 먹을지는 이제 플레이어 여러분이 컨피그를 통해 결정할 수 있기 때문입니다.

 

3) 동물의 인공지능

가장 큰 변화는 동물들이 이제 최대한 무리 지으려고 한다는 겁니다. 이러한 변화는 야생 동물들에게 있어서는 번식의 기회를 키울 것입니다. 야생에서 동물들이 너무 많이 번식하는 것이 걱정되기는 하지만, 걸을 때 허기 소모를 키웠기 때문에 굶어 죽는 녀석도 간혹 발생할 겁니다.

이제 동물은 아무 이유 없이 들판을 거닐진 않습니다. 다른 풀을 찾거나, 혹은 무리를 찾을 때만 이동합니다. 너무 풀을 많이 찾아 다니는 녀석은 무리를 이탈할 수도 있습니다.

 

4) 컨피그

컨피그를 통해 동물의 허기와 번식에 관련된 부분을 많이 변경할 수 있게 되었습니다. 어떤 음식을 먹고 얼마나 허기를 회복할지, 배가 얼마나 불러야 번식하며 그 상태에서 얼마나 자주 사랑에 빠질지, 얼마나 허기를 많이 소모할지 모두 플레이어가 컨피그를 통해 수정할 수 있습니다.

1.7 포지부터는 게임 내에서 컨피그를 수정하는 기능이 추가되어 해당 사항을 구현했습니다. 타이틀 메뉴에서 Mods 버튼을 누르고, Hungry Animals를 찾아 Config를 누르면 각 동물에 대해 값들을 수정할 수 있으니 많이 이용해주시길 바랍니다.

 

각 변수 이름에 마우스를 올리면 설명을 알 수 있습니다.

 

 

 

 

5)기타

동물의 소리가 조금 작아졌습니다.

 

 

2. 저작권

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

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

1.0.4.0 업데이트  (2) 2015.02.05
1.0.3.1 업데이트  (0) 2015.02.05
1.0.3 업데이트  (1) 2015.02.05
1.0.1 업데이트  (0) 2015.02.05
모드 소개  (0) 2015.02.05

최근 배고픈 동물들 모드를 만들면서 공부한 것들을 복습하면서 기록하는 차원에서 강의를 작성했습니다. 1~2주 동안에는 게시글이 거의 없었는데 다른 말로 하자면 모드 제작 중에 더 배운게 거의 없다는 걸 의미합니다. 1주일이 넘는 시간동안 새로 추가하는 멀티 블록 구조의 렌더링과 관련해서 엄청난 에러와 싸웠기 때문에 별다른 진전이 없습니다.

 

1.0.1 버전은 그러한 과정 중에 중간 점검 차원에서 나온 버전입니다. 1.0.0에서 시범적으로 보여진 동물 개선 시스템을 예고된 바와 같이 다른 동물에도 적용시켰습니다. 물론 이까지의 작업은 1.0.0을 만든 후 하루도 채 되기 전에 완성될 수 있었지만 개인적인 욕심에 더 많은 컨텐츠를 추가한 이후 발표하고 싶었습니다.

 

이번 버전에서는 '조각된 블록Chiseled Stone'이 추가로 등장합니다. '정Chisel'을 이용하여 정해진 몇몇 블록을 깎을 수 있는데, 기존의 Chisel 모드가 다양한 텍스쳐를 추가했다면 저는 3D 모델을 바꾸는데 중점을 두었습니다. 물론 현재는 장식기능밖에 없지만 사실 이렇게 조각된 블록을 통해 멀티 블록 구조를 만드는 것을 제작 중입니다.

 

다운로드는 다음의 AdFly를 이용해주시길 바랍니다.

링크: http://adf.ly/rrSYs

 

0. 제작 환경

마인크래프트 – 1.7.10

Forge – 10.13.0.1180

 

1. 변경 사항

1) 동물에 대한 변경사항

1.0.0에서 소에게 적용되었던 변경사항이 대부분의 동물들(양, 돼지, 닭)에게 적용됩니다[http://a3626a.blog.me/220093562470]. 돼지는 썩은 고기도 먹을 수 있으며, 닭은 호박/수박 씨 모두 먹을 수 있습니다.

 

2) 조각된 블록과 정

조각된 블록과 정이 추가됩니다. 조각된 블록은 정으로 해당 블록을 캐서 얻을 수 있으며 현재 3단계까지 조각이 가능합니다.

현재 그림자 렌더링이 구현되어 있지 않습니다. 구체적인 형태는 인벤토리 화면을 참고해주세요.

 

정은 돌, 철, 다이아의 세 종류가 있습니다. 상위 소재일수록 조각 속도, 내구도, 조각 성공률*, 소재 손실률**이 좋아집니다.

* 조각은 항상 성공하지 않습니다. 일정 확률로 다음 단계로 조각됩니다.

** 가끔 조각을 진행하면 블록이 손실될 수 있습니다.

 

3. 저작권

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

 

4. 향후 개발

앞서 이야기한대로 멀티 블록 구조물을 개발하고 있습니다. 1.0에서 1.1로 넘어가는 만큼 본 모드의 정체성을 결정하는데 첫 발을 내딛을 업데이트일 것입니다. 저는 본 모드에서 '동물'을 '음식을 통해 역학적 에너지를 생산하는 매체'로 여기고 있습니다. 따라서 다음의 업데이트에서는 그 '역학적 에너지'를 사용하는 구조물이 등장할 가능성이 큽니다.

 

 

 

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

1.0.4.0 업데이트  (2) 2015.02.05
1.0.3.1 업데이트  (0) 2015.02.05
1.0.3 업데이트  (1) 2015.02.05
1.0.2 업데이트  (0) 2015.02.05
모드 소개  (0) 2015.02.05

+ Recent posts