새로운 엔티티(Entity)를 만들게 되면 Attribute를 다루게 됩니다. Attribute는 최대 체럭, 공격력, 이동 속도와 같은 엔티티(Entity)의 능력치를 말합니다. Attribute가 정확히 무엇인가하는 더욱 자세한 설명은 마인크래프트 위키에 잘 나와있습니다. http://minecraft.gamepedia.com/Attribute

 

이 강의에서는 기존의 Attribute를 다루고, 직접 새로운 Attribute를 정의하고 더 나아가서 코드로 그 값들을 수정하는 법을 배울 것입니다.

 

Attribute 만들기

1단계

새로운 Attribute를 만들기 위해 가장 먼저 해야 할 일은 IAttribute 객체를 만드는 것입니다. 인터페이스(Interface) 객체를 만들어야 한다고 해서 IAttribute를 상속하는 클래스(Class)를 만들 필요는 없습니다. 이미 마인크래프트(Minecraft)에서 기존의 모든 Attribute에 적용되고 있는 RangedAttribute라는 클래스(Class)를 제공하기 때문입니다. 생성자는 다음과 같은 인자를 가지고 있습니다.

 

RangedAttribute(IAttribute p_i45891_1_, String p_i45891_2_, double p_i45891_3_, double p_i45891_5_, double p_i45891_7_)

IAttribute
p_i45891_1 :
정확한
기능을 파악할 없습니다. 모든 Attribute에서 "(IAttribute)null" 지정합니다.
String p_i45891_2_ : Attribute
이름입니다. 이름은 같은 Attribute 같은 엔티티(Entity)에게 2 이상 지정하지 못합니다. 그렇기 때문에 모드 아이디(Mod ID) 이용해서 구분하는 것이 바람직합니다. SoundBody.MOD_ID + ".digspeed"」와 같은 형식입니다.
double p_i45891_3_ : Attribute의 기본값입니다. 엔티티(Entity)가 처음 생성될 때, 기본 값으로 이 Attribute에 대입됩니다. 최소값, 최대값의 범위를 벗어날 수 없습니다. 만약 벗어난다면 IllegalArgumentException을 발생시킵니다.
double p_i45891_5_ : Attribute의 최소값입니다. 대부분 0.0D의 값을 가지고 있습니다. 최소값이기 때문에 실제 Attribute 값은 이 이하로 내려가지 않습니다. 이 값 자체가 최대값보다 커진다면 IllegalArgumentException을 발생시킵니다.
double p_i45891_7_ : Attribute의 최대값입니다. 대부분 Double.MAX_VALUE의 값을 가지고 있습니다. 최대값이기 때문에 실제 Attribute 값은 이보다 커질 수 없습니다. 이 값 자체가 최소값보다 작아진다면 IllegalArgumentException을 발생시킵니다.

 

Attribute를 만드는 것은 크게 어렵지 않습니다.

public static IAttribute digspeedFactor = new RangedAttribute((IAttribute)null, Strings.attribute_digspeedfactor_name, 1.0, 0.0, Double.MAX_VALUE);

 

하지만 이게 끝은 아닙니다. 만약 Attribute가 클라이언트(Client)와 동기화가 되어야 한다면 setShouldWatch()를 true로 설정해야만 합니다. false로 한다면 당연히 동기화 기능을 끌 수 있습니다. 이 함수는 값이 변경된 객체를 다시 반환하기 때문에 다음과 같이 코드를 작성할 수 있습니다.

public static IAttribute digspeedFactor = new RangedAttribute((IAttribute)null, Strings.attribute_digspeedfactor_name, 1.0, 0.0, Double.MAX_VALUE).setShouldWatch(true);

 

 

2단계

열심히 Attribute를 만들었지만 사실 엔티티(Entity)는 그런 Attribute가 있다는 사실조차 알지 못합니다. 그렇기 때문에 Attribute를 반드시 등록해야 합니다. 등록하는 시기는 엔티티(Entity) 클래스(Class)에서 applyEntityAttributes() 함수가 호출될 때입니다. 간단히 override하시면 됩니다. 그리고 this.getAttributeMap().registerAttribute(MY_OWN_ATTRIBUTE)을 호출하세요. 물론 「MY_OWN_ATTRIBUTE」는 그저 예시일 뿐입니다. 1단계에서 만든 Attribute를 등록하시면 될 겁니다.

@Override

protected void applyEntityAttributes() {

super.applyEntityAttributes(); // 상위 함수를 호출하지 않으면 체력이나 이동속도와 같은 기본적인 Attribute가 등록되지 않을 것입니다.

this.getAttributeMap().registerAttribute(TurretAttributes.MAX_AMMO_CAPACITY);

}

 

 

3단계

Attribute를 만들고 등록까지 마쳤지만 아직 쓰임은 없습니다. 이 값을 가져오기는 상당히 간단합니다. this.getEntityAttribute(MY_OWN_ATTRIBUTE).getAttributeValue()를 호출하시면 값을 가져올 수 있습니다.

 

 

 

Modifier 만들기

이제 Attribute 값을 수정만 하면 필요한 기능을 구현할 수 있을 것입니다.

1. 기본값 변경하기

이 방법은 직접적으로 값을 변경하기 때문에 간단합니다. 최대 체력을 직접 지정해주거나 할 때 많이 사용되는 방법입니다. 다음의 코드를 참고하세요.

this.getEntityAttribute(SharedMonsterAttributes.maxHealth).setBaseValue(40.0D); // 엔티티(Entity)의 최대 체력을 40(하트 반 칸)으로 지정합니다.

이 방법은 클라이언트(Client)와 자동으로 동기화되지 않습니다. 동기화 하려면 수동적으로 패킷(Packet)을 보내서 하셔야 합니다.

 

 

2. Attribute에 Modifier 추가하기

이 방법은 Attribute를 바꾸고 나서 언제든지 원래 값으로 되돌릴 수 있는 '가역적'인 방법입니다. 우리가 추가하려는 것은 기본적으로 Attribute Modifier이고 역시 위키에 잘 설명되어 있습니다. http://minecraft.gamepedia.com/Attribute#Modifiers

Attribute랑 비슷하게 객체를 만들어야 합니다. 이 경우에도 이미 AttributeModifier 클래스(Class)가 정의되어 있어 편리합니다. 생성자의 인자는 다음과 같습니다.

 

public AttributeModifier(UUID p_i1606_1_, String p_i1606_2_, double p_i1606_3_, int p_i1606_5_)

 

UUID p_i1606_1_ : UUID입니다. 그렇기 때문에 Modifier마다 유일해야 합니다. Attribute에서 이름(name)에 해당하는 값인 것이죠. https://www.uuidgenerator.net/ 에서 UUID를 생성할 수 있습니다. 그냥 이렇게 생성해도 겹칠 일은 거의 없습니다. 이게 UUID 클래스(Class) 의 편리함입니다. UUID.fromString()를 이용하면 사전에 생성 해 놓은 문자열로부터 UUID를 얻을 수 있습니다. UUID.fromString("e6107045-134f-4c54-a645-75c3ae5c7a27")」이런 식 입니다.
String p_i1606_2_ : Modifier 이름에 해당하는 인자입니다. 빈 문자열만 아니면 문제가 없습니다. 심지어 겹쳐도 됩니다. 어차피 식별에 사용되는 인자는 UUID이기 때문입니다.
double p_i1606_3_ : 얼마나 값을 변경할 것인지에 해당하는 인자입니다. Modifier를 통해서도 Attribute의 기본적인 최소, 최대값은 넘을 수 없으니 유의하셔야 합니다.
int p_i1606_5_ : Modifier의 연산자(Operation)입니다. 위키 내용을 참고하세요. 간단하게 설명하면 연산자(Operation)는 어떻게 값이 변경되는지를 결정하는 요소입니다. Attribute에 값을 더할 건지, 곱할 건지를 지정하는 것입니다.

연산자(Operation) 0: X를 만큼 더합니다. 연산자(Operation) 1: Y가 "X*"만큼 증가합니다. 연산자(Operation) 2: Y=Y*(1+). 처음 "X=기본값"으로 대입됩니다. 그 후 연산자(Operation) 0이 실행이 되고 "Y=X"로 대입됩니다. 이후 연산자(Operation) 1이, 마지막으로 연산자(Operation) 2가 적용됩니다.

 

이제 모든 인자를 알아보았으니 객체를 생성해 보겠습니다.

MY_CUSTOM_MODIFIER = new AttributeModifier(UUID.fromString("e6107045-134f-4c54-a645-75c3ae5c7a27"), "myCustomModifier420BlazeIt", 0.360D, EnumAttrModifierOperation.ADD_PERC_VAL_TO_SUM.ordinal());

이제 객체도 만들었으니 어떻게 적용하고 해제하는지 확인 해보겠습니다.

entity.getEntityAttribute(MY_ATTRIBUTE_INSTANCE).applyModifier(MY_CUSTOM_MODIFIER);
entity.getEntityAttribute(MY_ATTRIBUTE_INSTANCE).removeModifier(MY_CUSTOM_MODIFIER);

 

 

 

Attribute는 자동으로 저장되고 불러와집니다. 따라서 수동적으로 해당 작업을 해주실 필요는 없습니다. 다만 클라이언트(Client)와 동기화만 조금 신경 써 주시면 됩니다.

 

 

 

 

참고자료:

http://www.minecraftforge.net/forum/index.php/topic,30137.msg156378.html

위의 자료에서 강의 구성, 샘플 코드 등을 참고 했습니다. 사실상 거의 번역본입니다.

'마인크래프트 모딩 > 강의' 카테고리의 다른 글

아이템 스택(Item Stack)  (2) 2015.10.05
[모딩] 포션 추가  (0) 2015.08.15
5. AI  (5) 2015.02.06
4. 포션 추가(번역)  (0) 2015.02.05
3.동물을 추가해보자 (EntityAnimal)  (0) 2015.02.05

새로운 엔티티(Entity)의 행동은 크게 2가지를 통해 구현할 수 있습니다. 가장 간단한 방법은 onUpdate() 함수 내에서 엔티티(Entity)의 상태를 분석하여 행동을 유도하는 것입니다. 다른 방법은 이 강의에서 알아보겠지만 AI를 사용하는 것입니다. 바닐라(Vanilla)로 예를 들자면, 닭이 알을 낳는 것은 첫 번째 방법으로 구현되어 있습니다. 하지만 플레이어(Player)를 따라오고, 걸어 다니는 것을 포함한 대부분의 행동은 AI를 이용합니다. 교배의 경우에는 첫 번째 방법과 두 번째 방법 모두 구현되어 있으나 AI를 이용한 코드만이 동작하고 있습니다.

이번 강의에서는 AI의 작동 원리와 구현법을 알아 볼 것입니다. 마인크래프트 모드 제작엔티티(Entity)에 대한 기본적인 내용을 숙지하고 계셔야 강의를 온전히 이해하실 수 있습니다.

 

AI의 작동 원리

 

작동 중인 AI와 대기 중인 AI

엔티티(Entity)는 "tasks"라는 리스트(List)에 AI를 저장합니다. 이렇게 저장된 AI는 크게 2가지로 나눌 수 있습니다. 작동 중인 AI와 작동 중이지 않은(대기 중인) AI. 작동 중인 AI는 continueExecuting()이라는 함수를 통해 계속 작동할 것인지를 결정하고, 작동 중이지 않은 AI는 shouldExecute()라는 함수를 통해 작동을 시작할 것인지를 결정합니다. 또한 작동 중인 AI는 updateTask()라는 함수에서 그 내용을 실행하여 원하는 기능대로 동작합니다.

위의 작동 원리는 AI의 개별적인 정보만을 고려한 것입니다. 그러나 실제 동작에는 하나의 엔티티(Entity)라고 할지라도 여러 AI가 동시에 존재하여 서로 충돌의 가능성이 있습니다. 그렇기 때문에 각 AI에는 우선권(Priority)과 함께 isInterruptible, MutexBit의 개념이 있습니다. 이 세 개념은 continueExecuting(), shoudExecute()가 실행될 때, 함께 고려됩니다.

 

사용 가능성

작동 중인 AI가 계속 동작하고, 대기 중인 AI가 새로 실행되기 위해서는 사용 가능성 검사를 통과 해야 합니다. 사용 가능성이란 해당 AI가 동작 중인 모든 AI에 대해 우선권이 같거나 높은 대상과는 호환가능(Compatible) 해야 하고 낮은 대상들은 모두 중지가능(Interruptible) 한 것을 말합니다. 여기서 호환 가능하다는 것은 두 AI가 동시에 동작 가능하다는 것을 의미하고 중지 가능이라는 것은 AI가 동작 중간에 취소되는 것을 허용한다는 의미입니다.

AI 1~3은 동작 중인 AI입니다. 대상 AI는 대기 중인 AI라고 가정합니다. 그렇다면 대상 AI가 AI 2와 호환 불가능이기 때문에 실행이 되지 않을 것입니다. 여담이지만 사용 가능성 검사는 동작 중인 AI를 대상으로 먼저 시행되는데, AI 1~3은 모두 해당 틱(Tick)에서 사용 가능성 검사를 통과하였으므로(= 동작 중이므로) 서로 호환 가능일 것입니다.

계속 위의 상황에서 만약 대상 AI가 동작 중인 AI라고 가정해봅시다. 결론적으로는 AI 2로 인해 대상 AI는 대기 상태로 되돌아 갈 것입니다. 여기서 조금만 더 분석해 봅시다. 대상 AI는 분명 동작 중이었기 때문에 분명 AI 2보다는 먼저 실행되었을 것입니다. 대상 AI가 AI 2와 호환 불가능 관계이고 대상 AI가 이미 작동 중이었는데도 불구하고 AI 2가 실행되었다는 것은, AI 2의 우선권이 대상 AI보다 높고, 대상 AI는 중지 가능한 AI라는 것을 유추할 수 있어야 합니다. 이처럼 우선권이 높은 AI는 중지 가능한 AI를 강제로 종료시킬 수 있습니다.

 

호환 가능성과 중지 가능성

이제 호환 가능성과 중지 가능성에 대해 조금 더 자세히 알아보겠습니다. 중지 가능성은 각 AI 클래스(Class) 내부의 boolean isInterruptible() 함수의 반환 값으로 결정됩니다. 기본적으로 EntityAIBase에는 이 함수가 true를 반환하도록 설정되어 있습니다. 즉, 특별히 오버라이드(Override)하지 않는다면 중지 가능한 AI로 설정됩니다.

호환 가능성은 보다 복잡한 개념인 MutexBit를 사용합니다. MutexBit는 각 2진수의 자리를 통해 해당 AI의 동작을 간단히 분류하는 것이라 할 수 있습니다. 대부분 이동을 요구하는 AI는 1의 MutexBit를 포함합니다. 시선 처리를 요구하는 AI는 2의 MutexBit를 가집니다. 예를 들어 교배를 담당하는 AI는 이동과 시선 모두 요구하므로 (1+2)의 MutexBit를 가집니다.

이동을 요구하는 두 개의 AI는 동시에 실행 될 수 없을 것입니다. 또한 시선 처리를 요구하는 AI도 두 방향을 동시에 바라 볼 수 없으니 마찬가지 입니다. 따라서 두 AI의 MutexBit의 각 2진수 자리를 비교하여 겹치는 자리가 없으면 호환 가능하다고 할 수 있습니다. 수식으로 분석하면bit1 & bit2 == 0일 때 호환 가능한 것 입니다.

 

 

AI 구현 방법

 

지금까지 AI의 작동원리에 대해서 알아봤습니다. 위의 설명에 간혹 실제 함수 이름이 노출되기도 했지만 그것으로는 구현에 부족할 태니 실제 코드를 보며 AI 구현법에 대해 설명하겠습니다. 위의 작동 원리를 명확하게 이해하지 못한다면 원하는 AI를 완벽하게 구현하는 것은 힘듭니다.

 

EntityAIBase를 상속하는 클래스(Class)

AI는 반드시 EntityAIBase를 상속하여 구현됩니다. EntityAIBase는 추상(Abstract) 클래스(Class)이기 때문에 상속 시 반드시 오버라이드(Override) 해야 하는 함수들이 있습니다. 생성자와 함께 다음과 같이 구현합니다.

 

public class EntityAITest extends EntityAIBase {

 

    public EntityAITest() {

        

    }

    

    @Override

    public boolean shouldExecute() {

        // TODO Auto-generated method stub

        return false;

    }

 

}

 

하지만 위의 함수만 입력한다고 원하는 기능을 구현할 수 있는 것이 아닙니다. 따라서 본문에서 잠시 언급되었던 함수들을 모두 오버라이드(Override) 합니다.

 

public class EntityAITest extends EntityAIBase {

 

    public EntityAITest() {

        this.setMutexBits(1);

        // 만약 엔티티(Entity) 이동과 관련된 함수라면 이처럼 MutexBit 1 포함시켜야 합니다.

    }

    

    @Override

    public boolean isInterruptible() {

        // 중지 가능성을 말합니다.

        return super.isInterruptible();

    }

    

    @Override

    public boolean shouldExecute() {

        // AI 대기 상태일 호출되어 새로 실행되어야 하는 조건을 확인합니다.

        return false;

    }

    

    @Override

    public void startExecuting() {

        // shouldExecute() == true 만족하여 AI 새로 동작할 , 처음 실행되는 함수입니다.

        super.startExecuting();

    }

    

    @Override

    public void updateTask() {

        // AI 동작 상태일 (Tick)마다 호출되는 함수입니다.

        super.updateTask();

    }

    

    @Override

    public boolean continueExecuting() {

        // AI 계속 동작할 것인지를 판정합니다.

        return super.continueExecuting();

    }

 

}

 

대부분의 함수가 언급되었고 주석으로 설명을 달아 놓았으니 확인하시길 바랍니다.

 

엔티티(Entity)에게 AI 추가하기

마지막 단계입니다. 성공적으로 AI가 구현되었다면 이제 대상 엔티티(Entity)에게 인공지능을 주입해야 합니다. 위에서 언급되었던 것처럼 'tasks' 리스트(List)에 추가하게 됩니다. 일반적인 경우 엔티티(Entity)의 생성자에서 진행하는 것이 좋습니다.

 

    public EntityHungryChicken(World world) {

        super(world);

        

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

        //addTack(우선권, AI)으로 인자(Parameter) 받습니다. 정수만 가능하며 숫자가 작을수록 높은 우선권을 지닙니다.

    }

    

    public boolean isAIEnabled() {

        // 이것이 true 아니면 새로 생성자에서 추가된 AI 작동하지 않습니다.

        return true;

    }

 

여기서 주목해야 할 곳이 두 군데 있습니다. 첫 번째는 addTask()에서 우선권을 지정하는 수는 낮을수록 우선순위가 높다는 점입니다. 두 번째는 isAIEnabled() 함수가 true를 반환하지 않으면 추가된 AI가 작동하지 않는다는 점입니다. 이 두 사항을 모두 확인하여 구현에 어려움이 없으시길 바랍니다.

 

 

 

이것으로 AI에 대한 강의를 마칩니다. 몇 가지 유의할 사항이 있습니다. 일단 위 강의는 tasks 리스트(List)에 추가되는 종류의 AI에 대한 내용입니다. 예를 들어 공격할 상대를 지정하는 AI는 targetTasks에 저장됩니다. 구체적인 구현 방법은 바닐라(Vanilla) 마인크래프트에서 사용된 예들을 살펴보시길 바랍니다. 관련된 개념이나 함수에 대한 질문은 원하시는 만큼 자세히 답변해 드리겠습니다. 감사합니다.

이 문서는 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를 넣어야합니다)

 

 

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

+ Recent posts