강의 환경 : forge-1.8.9-11.15.1.1855

1.8.9에서 작성된 강의이지만, NBT를 다루는 개념은 다른 버전과 동일합니다. 단, 텍스처(texture)와 관련된 부분은 완전히 다르니 적절히 코드를 수정하시길 바랍니다.

 

배경 지식

  1. 기본 아이템
  2. 아이템 이벤트 함수
  3. NBT
  4. 아이템 스택

강의목표

  1. NBTTagCompound를 아이템 스택(Item Stack)에 대해 사용하는 방법을 익힌다.

 

1. NBT(Named Binary Tag)

NBT는 마인크래프트에서 데이터를 저장하기 위한 파일에 쓰이는 자료 형식입니다. 다양한 이름표(tags)로 이루어진 트리(tree) 구조를 띄도록 설계 되었습니다. 각 이름표는 다양한 자료형의 데이터를 담을 수 있습니다. 가능한 자료형은 int, float과 같은 숫자부터 String, List, Compound(hash)까지 다양합니다. 트리 구조라는 것은 하나의 이름표가, 또 다른 이름표들을 계층적으로 담을 수 있다는 의미입니다.

여러 기능을 모드에 추가하다 보면 아이템에, 정확히는 아이템 스택에, 정보를 저장해야 할 일이 생깁니다. 모드를 처음 만들기 시작하는 사람들은 종종 아이템 클래스 내부에 멤버 변수를 만들어 문제를 해결하려고 합니다. 하지만 이런 접근은 완전히 잘못된 접근입니다. 왜냐하면 아이템 객체는 모든 아이템 스택이 공유하기 때문입니다. 따라서 데이터를 저장하기 위해서는 아이템 스택에 NBT의 형태로 정보를 저장해야 합니다.

그림 2 객체 표현도

아이템 스택(Item Stack)의 NBT는 항상 Compound의 자료형을 가집니다. Compound는 해쉬(Hash) 구조를 가지고 있어서 문자열(String) 키(Key)를 이용해서 다양한 NBT를 값(Value)으로 저장할 수 있습니다. 때문에 Compound가 가지는 모든 키(Key)는 유일 해야 합니다. 만약 같은 키를 가진 값이 입력되면 덮어쓰게 됩니다.

 

2. NBT를 사용하는 아이템 만들기

이번 강의에서는 특정 좌표를 지정해서 귀환할 수 있는 아이템을 만들어 보도록 하겠습니다. Shift + 우클릭을 하면 현재 좌표가 아이템의 NBT에 저장이 되고, 이후 우클릭을 하면 저장된 위치로 이동합니다. 본 예제에서는 NBT를 쓰고, 읽는 것과 더불어서 엔티티(Entity)를 조작하는 것 또한 부가적으로 다뤄집니다.

먼저 귀환석(ItemTelepoter)의 아이템 클래스를 작성합니다. 지난 강의에서 다뤘던 족집게(ItemTweezer)와 뼈대가 상당히 비슷하니 참고하시길 바랍니다.

ItemTeleporter.java

package oortcloud.basictutorial.item;

 

import net.minecraft.creativetab.CreativeTabs;

import net.minecraft.item.Item;

import net.minecraftforge.fml.common.registry.GameRegistry;

 

public class ItemTeleporter extends Item {

 

    public ItemTeleporter() {

     this.setMaxStackSize(1);

this.setCreativeTab(CreativeTabs.tabTools);

this.setUnlocalizedName("teleporter");

GameRegistry.registerItem(this,"teleporter");

    }    

}

이제 언어(Language) 파일과 텍스처(Texture) 및 모델(Model)을 추가해서 기본적인 틀을 갖추어 보도록 하겠습니다. 1.8.9 버전을 기준으로 하기 때문에, 이 부분에서 지난 강의들과 차이가 있습니다. 이곳을 확인하세요. 언어 파일은 동일합니다. ModItems 또한 적당히 작성해 주시면 됩니다.

그림 3 아이템 뼈대 적용

 

 

3. NBT를 사용하여 기능을 구현하기

이제 이벤트 함수에 NBT를 이용해서 아이템에 좌표를 저장하고, 저장된 좌표로 순간이동을 하는 코드를 작성해보도록 하겠습니다. 아이템을 손에 들고 우 클릭 했을 때 호출되는 이벤트 함수는 onItemRightClick ()입니다.

public ItemStack onItemRightClick(ItemStack itemStackIn, World worldIn, EntityPlayer playerIn)

ItemStack itemStack

사용된 아이템 스택(Item Stack)입니다.

World worldIn

아이템 스택(Item Stack)이 사용된 세계(World) 객체입니다.

EntityPlayer playerIn

아이템 스택(Item Stack)을 사용한 플레이어(Player)입니다.

반환

아이템 스택(Item Stack)이 사용되고 난 후의 아이템 스택(Item Stack)이 반환됩니다.

 

ItemTeleporter.java

    @Override

    public ItemStack onItemRightClick(ItemStack itemStackIn, World worldIn, EntityPlayer playerIn) {

 

        if (worldIn.isRemote)

            return itemStackIn;

 

        if (playerIn.isSneaking()) {

            // Shift + RC

        } else {

            // RC

        }

        

        return itemStackIn;

    }

 

가장 먼저 코드가 서버(Server)에서만 실행될 수 있도록 클라이언트(Client)에서 실행되면 바로 반환하게 했습니다. 아이템의 NBT는 서버에서 수정이 이뤄지면 자동으로 클라이언트로 전달되기 때문에, 서버에서만 수정하는 것이 바람직하기 때문입니다. 엔티티(Entity)의 정보를 수정하는 것도 마찬가지 입니다.

isSneaking()이라는 함수를 이용해서 Shift를 누른 상태인지 확인하였습니다. 이제 각 경우에 맞추어 기능을 구현하겠습니다.

 

Shift+우 클릭을 하는 경우 플레이어의 좌표를 Compound에 "xpos", "ypos", "zpos"라는 이름으로 저장하고, 그것을 다시 아이템 스택에 저장하는 코드입니다. BlockPos는 1.8에서 추가된 개념이며 1.7.10에서는 각 좌표를 직접 플레이어 객체에서 가져오면 됩니다.

ItemTeleporter.java

    // Shift + RC

    NBTTagCompound tag = new NBTTagCompound();

    BlockPos pos = playerIn.getPosition();

    tag.setInteger("xpos", pos.getX());

    tag.setInteger("ypos", pos.getY());

    tag.setInteger("zpos", pos.getZ());

    itemStackIn.setTagCompound(tag);

 

우 클릭만 하는 경우 NBT를 아이템 스택에서 가져오고, 좌표가 저장되어 있으면 해당 좌표로 플레이어를 이동시킵니다.

ItemTeleporter.java

    // RC

    NBTTagCompound tag = itemStackIn.getTagCompound();

    if (tag != null && tag.hasKey("xpos") && tag.hasKey("ypos") && tag.hasKey("zpos")) {

        playerIn.setPositionAndUpdate(tag.getInteger("xpos"), tag.getInteger("ypos"), tag.getInteger("zpos"));

    }

 

추가적으로 여러 개의 좌표를 저장하거나, 메시지나 시각 효과를 연출하는 것도 좋은 연습이 될 것입니다.

 

원하는 대로 기능 구현이 잘 되었습니다.

 

참고자료

http://www.minecraftforge.net/wiki/Creating_NBT_for_items

http://minecraft.gamepedia.com/NBT_format

강의 환경 : forge-1.7.10-10.13.4.1517-1.7.10

 

배경 지식

  1. 아이템 이벤트 함수
  2. 아이템 내구도

강의목표

  1. 아이템의 손상 값(Damage)을 이용하여 내구도를 가지는 아이템을 구현할 수 있다.
  2. 이벤트 함수를 이용하여 원하는 기능을 구현할 수 있다.

 

1. 아이템의 내구도(Durability)

내구도라는 것은 도구, 무기, 갑옷과 같이 유용한 기능을 하는 아이템들이 가진 속성입니다. 이 값은 아이템이 파괴되기 전까지 몇 번이나 더 기능을 해낼 수 있는지를 나타냅니다. 도구나 무기의 경우는 앞의 설명과 같이 남은 작동 횟수를 나타내고 갑옷은 조금 달라서 얼마만큼의 피해를 더 경감시킬 수 있는 지를 나타냅니다.

모든 아이템들의 잔여 내구도는 아이콘 아래 부분에 초록색 내구도 막대를 통해 확인 할 수 있습니다. 단, 한 번도 사용되지 않은 아이템은 막대가 그려지지 않습니다. 아이템이 손상될수록 막대의 길이가 짧아지고 색상 또한 붉은 색으로 변해갑니다. 내구도가 거의 다 소진 된 경우에는 내구도가 모두 소진된 것처럼 빈칸만이 그려지기도 합니다. 이것은 내구도가 픽셀의 크기에 맞춰져 그려지기 때문에 일어나는 현상입니다.

수치로서 존재하는 내구도는 게임 내에서 F3+H를 누르면 볼 수 있습니다. 이 옵션은 내구도 뿐만 아니라 몇몇 추가적인 설명(Tooltip)을 가방(Inventory)에서 제공합니다. 여기서 보이는 내구도 값은 실제 내구도보다 1이 작습니다. 왜냐하면 이 수치가 0이 되었을 때, 마지막으로 도구를 한 번 더 사용할 수 있기 때문입니다.

 

 

2. 내구도를 사용하는 아이템 만들기 – 족집게

이번 강의에서는 '족집게'를 만들어보며 도구 구현법을 익혀보도록 하겠습니다. 이 아이템은 닭에게 우 클릭을 통해 사용하면 깃털을 하나 얻고, 닭에게 1의 피해를 줄 수 있습니다. 본 예제를 통해서 내구도를 어떻게 다루는지, 이벤트 함수를 통해 다른 엔티티(Entity)와 어떻게 상호 작용하는지, 마지막으로 어떻게 아이템을 월드(World)에 떨어뜨리는 지 확인해보세요.

먼저 족집게의 아이템 종류를 결정할 아이템 클래스를 작성합니다. 이 클래스는 가위(ItemShears)를 바탕으로 작성됩니다. 참고하시면 도움이 되실 겁니다.

이번 족집게(ItemTweezer)는 해당 클래스에서 단 하나의 객체만 존재하는 싱글톤(Singleton)으로 작성합니다. 즉, 이 클래스에서 단 하나의 아이템 객체만이 생성되는 것을 가정하는 겁니다. 따라서 생성자 부분의 코드가 이전과는 조금 다릅니다. 이렇게 코드를 짜면 ModItems에서 아이템 객체를 생성하고 등록하는 과정이 훨씬 간단해진다는 장점이 있습니다.

ItemTweezer.java

package oortcloud.basictutorial.item;

 

import cpw.mods.fml.common.registry.GameRegistry;

import net.minecraft.creativetab.CreativeTabs;

import net.minecraft.item.Item;

 

public class ItemTweezers extends Item {

 

    public ItemTweezers() {

this.setMaxStackSize(1);

this.setMaxDamage(238);

this.setCreativeTab(CreativeTabs.tabTools);

this.setUnlocalizedName("tweezers");

this.setTextureName("basictutorial:tweezers");

GameRegistry.registerItem(this,"tweezers");

    }

    

}

이 아이템에서는 손상 값(Damage)이 내구도로 사용이 됩니다. 그렇게 때문에 여러 아이템이 겹쳐지게 되면 손상 값 데이터가 소실 될 수 있으므로, 반드시 setMaxStackSize (1)를 통해 최대 개수를 1개로 설정해야 합니다. 이 후 setMaxDamage(63)는 해당 도구의 최대 손상 값을 결정합니다. 내구도로 환산되면 64번 아이템을 사용할 수 있습니다. 생성자에서 바로 등록이 이뤄지고 이름을 생성자 밖에서 지정해줄 수 없으므로 생성자가 2번 이상 불리면 오류가 나게 됩니다.

이제 언어(Language) 파일과 텍스처(Texture)를 추가해서 기본적인 틀을 갖추어 보도록 하겠습니다.

en_US.lang

item.tweezers.name=Tweezers

ModItems는 이렇게 작성 되겠네요. 지난 강의의 코드도 함께 수록되어 있습니다.

ModItems.java

public final class ModItems {

    

    public static Item itemNumber;

    public static Item itemFortuneCookie;

    public static Item itemTweezers;

    

    public static final void init() {

        itemFortuneCookie = new ItemFortuneCookie().setUnlocalizedName("fortuneCookie").setCreativeTab(CreativeTabs.tabMisc).setTextureName("basictutorial:fortuneCookie");

        GameRegistry.registerItem(itemFortuneCookie, "fortuneCookie");

        itemNumber = new ItemMetadata().setUnlocalizedName("itemNumber").setCreativeTab(CreativeTabs.tabMisc);

        GameRegistry.registerItem(itemNumber, "itemNumber");

        itemTweezers = new ItemTweezers();

    }

    

}

 

 

3. 내구도를 사용하는 아이템 만들기 – 족집게

이제 이벤트 함수를 이용해서 닭을 우 클릭하면 내구도가 소모되고, 깃털이 나오며, 동시에 닭에게 1의 피해를 주도록 해보겠습니다. 아이템으로 엔티티(Entity)를 우 클릭 했을 때 호출되는 이벤트 함수는 itemInteractionForEntity()입니다.

public boolean itemInteractionForEntity(ItemStack itemStack, EntityPlayer entityPlayer, EntityLivingBase entityLivingBase)

ItemStack itemStack

사용된 아이템 스택입니다.

EntityPlayer entityPlayer

아이템을 사용한 플레이어의 엔티티(Entity) 객체입니다.

EntityLivingBase entityLivingBase

플레이어가 우 클릭을 한 대상 엔티티(Entity)입니다.

반환

상호작용이 일어나면 true, 그렇지 않으면 false를 반환해야 합니다.

 

ItemTweezer.java

    @Override

    public boolean itemInteractionForEntity(ItemStack itemStack, EntityPlayer entityPlayer, EntityLivingBase entityLivingBase) {

        return false;

    }

 

이제 여기에 우리가 원하는 기능을 할 코드를 입력하면 됩니다. 기능을 구현하기 전에, 일반적으로 엔티티(Entity)를 생성하는 코드(아이템 드롭)는 서버에서만 실행 되야 합니다. 따라서 다음과 같이 코드를 작성하여 클라이언트에서는 코드가 실행되지 않도록 합니다.

엔티티는 서버에서만 관리되는 대상입니다. 서버에서만 엔티티를 생성해도 그것이 클라이언트로 바로 반영이 되고, 만약 양 쪽에서 모두 생성해버리면 클라이언트에서는 2개의 엔티티가 생성된 것처럼 보이게 됩니다.

ItemTweezer.java

    @Override

    public boolean itemInteractionForEntity(ItemStack itemStack, EntityPlayer entityPlayer, EntityLivingBase entityLivingBase) {

        if (entityPlayer.worldObj.isRemote) {

            return false;

        }

        // IMPLEMENT HERE, 구현은 이곳에 하세요

        return false;

    }

 

이제 기능 구현에 필요한 코드를 입력하겠습니다. 주석을 참조하세요. 아래 코드에서 핵심은 damageItem ()이라고 할 수 있습니다. 이 함수가 결국 내구도를 소모시키기 때문입니다.

ItemTweezer.java

    @Override

    public boolean itemInteractionForEntity(ItemStack itemStack, EntityPlayer entityPlayer, EntityLivingBase entityLivingBase) {

        if (entityPlayer.worldObj.isRemote) {

            return false;

        }

        if (entityLivingBase instanceof EntityChicken) {

            // entityLivingBase 닭인지 확인합니다.

            

            if (entityLivingBase.getHealth() > 1.0F) {

                // 대상의 체력이 1보다 높을 때만 동작합니다. , 도구를 이용한다 해도 대상이 죽지는 않습니다. 죽기 전까지 털이 뽑힐 .

                

                // 아이템을 드롭하는, 자주 쓰이는 코드입니다.

                Random rand = new Random();

                // 임의의 방향으로 아이템이 튕겨 지도록 랜덤 객체를 만듭니다.

                EntityItem ent = entityLivingBase.entityDropItem(new ItemStack(Items.feather), 1.0F);

                // entityLivingBase 깃털의 EntityItem ent 떨어뜨리도록 하고, y 기본 속도를 1.0으로 설정합니다.

                ent.motionY += rand.nextFloat() * 0.05F;

                ent.motionX += (rand.nextFloat() - rand.nextFloat()) * 0.1F;

                ent.motionZ += (rand.nextFloat() - rand.nextFloat()) * 0.1F;

                // 임의의 속도를 부여합니다.

                

                

                entityLivingBase.attackEntityFrom(DamageSource.causePlayerDamage(entityPlayer), 1.0F);

                // entityLivingBase에게 플레이어(entityPlayer)로부터 피해를 1 받도록 합니다.

                

                itemStack.damageItem(1, entityLivingBase);

                // 내구도를 1 소모시킵니다.

                

                return true;

            }

        }

        return false;

    }

 

원하는 대로 기능 구현이 잘 되었습니다.

 

참고자료

http://minecraft.gamepedia.com/Item_durability

강의 환경 : forge-1.7.10-10.13.4.1517-1.7.10

 

배경 지식

  1. 기본 모드 파일
  2. 기본 아이템
  3. 서버와 클라이언트

 

강의목표

  1. 사건 기반 프로그래밍(Event-driven Programming, EDP)의 원리를 이해한다.
  2. 아이템 클래스의 이벤트 함수 각각을 이해한다.
  3. 원하는 기능을 구현하는 아이템을 만들 수 있다.

 

1. 사건 기반 프로그래밍(Event-driven Programming, EDP)

이 단락은 아이템의 동작에 대한 보충 설명이므로 굳이 읽지 않으셔도 됩니다.

사건 기반 프로그래밍은 사용자의 행동(마우스, 키보드 입력)이나 다른 프로그램이 보낸 메시지와 같은 '사건(Event)'에 따라 프로그램의 동작을 결정하는 프로그래밍 패러다임(Paradigm)입니다. 이러한 EDP(Event-driven Programming)는 주로 GUI나 사용자의 입력에 반응하는 것이 주 기능인 프로그램들에 사용됩니다. 주요 반복문(Main Loop)이 이벤트를 인식하고 관련된 함수를 호출하는 방식으로 구현됩니다.

마인크래프트의 아이템도 사건 기반 프로그래밍에 기반하여 동작합니다. 사용자가 아이템으로 무엇을 하냐에 따라서 아이템의 기능을 구현하는 것입니다. 예를 들어 아이템을 버리고, 세계(World)와 상호작용할 때마다 특수한 함수가 불려집니다. 개발자는 Item 클래스 내부의 이벤트 함수를 오버라이드하여 이러한 기능들을 조작할 수 있습니다.

 

 

2. 예시를 통한 개념 학습 – 포춘 쿠키(Fortune Cookie)

이번 단락에서는 아이템을 사용할 때 오늘의 운세를 출력하는 간단한 아이템을 만들어 보겠습니다. 포춘 쿠키라는 이 아이템은 우 클릭으로 사용하면 하나가 소모되고 채팅 창에 임의의 운세를 출력합니다. 크리에이티브 모드인 경우에는 소모되지 않도록 하겠습니다. 본 예시를 통해서 이벤트 함수를 어떻게 조작하는지 유심히 살펴보시고 지금까지 다뤄왔던 내용들을 복습하는 시간을 가지시길 바랍니다. 또한 실제 구현에서 자주 사용되는 함수들을 따로 강조해 드리겠습니다

1) 아이템 틀 만들기

지난 강의에서처럼 기능을 구현해야하기 때문에 Item을 상속하는 클래스를 만듭니다. 그리고는 이 클래스에는 추가적인 코드없이 기본 과정을 따라 등록해보도록 합시다. 텍스처는 이것을 이용하겠습니다.

ItemFortuneCookie.java

public class ItemFortuneCookie extends Item {

 

}

 

ModItems.java

public final class ModItems {

    

    public static Item itemFortuneCookie;

    

    public static final void init() {

        itemFortuneCookie = new ItemFortuneCookie().setUnlocalizedName("fortuneCookie").setCreativeTab(CreativeTabs.tabMisc).setTextureName("basictutorial:fortuneCookie");

        GameRegistry.registerItem(itemFortuneCookie, "fortuneCookie");

    }

    

}

 

en_US.lang

item.fortuneCookie.name=Fortune Cookie

 

그림 1 등록을 마친 아이템

 

2) 이벤트 함수 오버라이드

이제 우 클릭을 할 때 호출되는 함수를 오버라이드하여 기능을 구현하도록 하겠습니다. 이벤트 함수는 일반적으로 'on'으로 시작하고 주석 처리가 잘 되어 있어서 어떤 기능인지 확인하기는 용이합니다. onItemRightClick이라는 메소드(Method)는 아이템을 들고 우 클릭을 하는 모든 경우에서 호출됩니다. 플레이어의 손에 들린 아이템 스택, 플레이어가 속한 세계(World), 그리고 플레이어의 객체를 인자로 넘겨 받습니다. 그리고 다시 해당 아이템 스택을 반환합니다. 이 메소드는 클라이언트와 서버 모두에서 실행됩니다.

ItemFortuneCookie.java

@Override

public ItemStack onItemRightClick(ItemStack itemStack, World world, EntityPlayer player) {

 

    return itemStack;

 

}

 

먼저 플레이어에게 채팅 메시지를 출력하는 메소드를 살펴보겠습니다. 채팅 메시지는 player.addChatComponentMessage(IChatComponent msg)를 호출하여 출력합니다. IChatComponent 인터페이스를 반드시 구현할 필요는 없으며 단순한 문자 출력이 목적이라면 new ChatComponentText(String text)를 이용해서 간단히 객체를 생성할 수 있습니다. 그러면 간단한 메시지를 출력해보도록 하겠습니다

ItemFortuneCookie.java

@Override

public ItemStack onItemRightClick(ItemStack itemStack, World world, EntityPlayer player) {

 

    player.addChatComponentMessage(new ChatComponentText("Example Message"));

 

    return itemStack;

}

그림 2 2번 출력되는 메시지

아이템을 한 번만 사용해도 메시지가 2번 출력됩니다. 앞서 말했듯이 addChatComponentMessage 가 클라이언트와 서버 모두에서 실행되기 때문에 일어나는 문제입니다. 클라이언트에서 addChatComponentMessage 가 호출되면 바로 채팅창에 출력되고, 서버에서 호출되면 클라이언트로 해당 메시지를 전송합니다. 이 기능은 플레이어에게 메시지를 출력하는 것이 목적이므로 클라이언트에서만 메시지를 출력하도록 바꾸어 보도록 하겠습니다.

World 클래스에 존재하는 isRemote라는 멤버 변수는 클라이언트 세계면 true, 서버 세계면 false의 값을 가집니다. 따라서 다음과 같이 코드를 작성하면 클라이언트에서만 코드가 실행되며 문제가 해결됩니다.

ItemFortuneCookie.java

@Override

public ItemStack onItemRightClick(ItemStack itemStack, World world, EntityPlayer player) {

 

    if (world.isRemote) {

        player.addChatComponentMessage(new ChatComponentText("Example Message"));

    }

 

    return itemStack;

}

 

본래의 목적은 임의의 메시지를 출력하는 것이므로 코드를 조금 추가 하겠습니다. 내용은 아이작의 구속에 나오는 운세들입니다. EntityLivingBase 클래스에는 Random 객체를 반환하는 getRNG()라는 메소드가 있습니다. 이 객체를 이용해서 임의의 값을 획득하는게 가능합니다. EntityPlayer 클래스가 EntityLivingBase의 하위 클래스이기 때문에 해당 메소드를 호출할 수 있습니다.

ItemFortuneCookie.java

public static String[] fortune = {"BRING HIM THE PHOTO", "YOU WILL DIE ALONE", "YOU ARE THROWING YOUR LIFE AWAY", "GO OUTSIDE!", "ASK AGAIN LATER", "THINK FOR YOURSELF", "QUESTION AUTHORITY", "YOU ARE WORSHIPING A SUN GOD", "DON'T LEAVE THE HOUSE TODAY", "GIVE UP!", "MARRY AND REPRODUCE", "STAY ASLEEP", "WAKE UP", "WE WILL ALL DIE ONE DAY", "LOOK TO LA LUNA", "STEVEN LIVES ", "MEET STRANGERS WITHOUT PREJUDICE", "A HANGED MAN WILL BRING YOU NO LUCK TODAY", "WHAT DO YOU WANT TO DO TODAY?", "YOU ARE DARK INSIDE", "HAVE YOU SEEN THE EXIT?", "GET A BABY PET IT WILL CHEER YOU UP", "YOUR PRINCESS IS IN ANOTHER CASTLE", "YOU ARE PLAYING IT WRONG GIVE ME THE CONTROLLER", "TRUST GOOD PEOPLE", "LIVE TO DIE", "WHEN LIFE GIVES YOU LEMONS REROLL!"};

 

@Override

public ItemStack onItemRightClick(ItemStack itemStack, World world, EntityPlayer player) {

 

    if (world.isRemote) {

        player.addChatComponentMessage(new ChatComponentText(fortune[player.getRNG().nextInt(fortune.length)]));

    }

 

    return itemStack;

}

 

마지막으로 아이템의 개수를 하나 감소시키는 과정을 추가하도록 하겠습니다. 이때 일반적으로 크리에이티브 모드에서는 아이템이 소모되지 않도록 합니다. 아이템 스택의 stacksize 변수가 개수를 나타냅니다. player.capabilities.isCreativeMode는 플레이어가 크리에이티브 모드이면 true, 아니면 false의 값을 가집니다. 아이템 스택의 개수가 0이 되면 onItemRightClick을 호출한 코드가 알아서 인벤토리에서 해당 아이템을 삭제합니다. 지금 추가된 구문은 아이템을 소모시킬 때 널리 사용됩니다.

ItemFortuneCookie.java

@Override

public ItemStack onItemRightClick(ItemStack itemStack, World world, EntityPlayer player) {

 

    if (!player.capabilities.isCreativeMode) {

        --itemStack.stackSize;

    }

 

    if (world.isRemote) {

        player.addChatComponentMessage(new ChatComponentText(fortune[player.getRNG().nextInt(fortune.length)]));

    }

 

    return itemStack;

}

그림 3 완성본

이렇게 해서 포춘 쿠키가 완성 되었습니다. Item 클래스 내의 이벤트 함수들에 대한 구체적인 정보도 따로 정리하여 올리겠습니다.

 

 

참고자료

https://en.wikipedia.org/wiki/Event-driven_programming

http://bindingofisaac.wikia.com/wiki/Fortune_Telling_Machine

http://bedrockminer.jimdo.com/modding-tutorials/advanced-modding/modding-tips-and-tricks/#chatSP

https://en.wikipedia.org/wiki/Fortune_cookie

강의 환경 : forge-1.7.10-10.13.4.1517-1.7.10

 

배경 지식

  1. 기본 아이템
  2. 아이템 스택

 

강의목표

  1. 아이템의 메타데이터(metadata)를 이해한다.
  2. 아이템의 손상 값(Damage)을 이해한다.
  3. 손상 값(Damage)을 이용해서 하위 아이템을 만들 수 있다.

 

1. 아이템의 메타데이터

메타데이터(Metadata)는 데이터를 설명하기 위한 추가적인 데이터입니다. 예를 들면, 디지털 카메라에서는 사진을 찍을 때마다 촬영 시간, 노출, 플래시 사용 여부, 해상도, 사진 크기 등의 정보를 화상 데이터와 같이 저장하게 되어 있습니다. 이런 추가적인 데이터는 훗날 사진을 정렬하고 관리할 때 상당히 유용한 지표가 됩니다. 메타데이터는 일반적으로 이처럼 데이터의 검색과 관리를 편리하게 하기 위해서 부여됩니다.

마인크래프트에서 말하는 메타데이터는 약간 차이가 있습니다. 마인크래프트 세계에는 최소 수 백만 개의 블록이 존재하기 때문에 각 블록에 저장될 수 있는 데이터가 많지 않습니다. 일단 서로 다른 블록을 구분하도록 ID값이 저장되고, 거기에 추가적으로 4비트만큼의 저장 공간이 제공됩니다. 0~15까지 저장할 수 있는 이 공간을 메타데이터라고 부릅니다.

이러한 메타데이터는 단순한 데이터를 저장할 때 사용됩니다. 양털의 색, 화로의 방향, 도가니의 물과 같이 몇 개의 숫자만으로 충분히 표현 가능한 자료들을 저장합니다. 양털은 파괴되어 아이템이 된 후에도 그 색이 보존되기 때문에 아이템 스택에도 메타데이터를 저장하는 공간이 있다는 것을 유추할 수 있습니다. 이 값이 바로 아이템의 메타데이터입니다.

그림 1서로 다른 양털

 

 

2. 손상 값(Damage)

앞서 말한 메타데이터는 damage라는 변수에 저장됩니다. 이름 그대로 도구 아이템의 내구도를 나타낼 때 흔히 사용됩니다. 그렇기 때문에 내구도가 있는 도구는 메타데이터까지 따로 저장할 수는 없습니다. 한 변수에 두가지 정보를 저장하는 것은 굉장히 번거로운 일이니까요. 블록과 달리 아이템의 메타데이터는 -32768~32767의 훨씬 넓은 저장공간을 제공합니다.

이번 강의에서는 손상 값을 이용해서 하위 아이템을 만드는 법을 알아볼 것입니다. 이러한 방식이 가진 장점은 1) 32000까지 제한되어 있는 아이템 ID를 절약할 수 있고 2) 메타데이터를 이용하는 블록과 연계된 아이템을 만들 수 있으며, 3) 마지막으로 손상 값 자체를 응용해서 유연한 프로그래밍을 할 수 있습니다.

 

 

3. 하위 아이템을 사용하는 Item 클래스

지난 강의에서 추가한 기본 아이템과는 다르게 특수한 기능을 가지고 있기 때문에 새로 클래스를 작성해야 합니다. 객체지향의 원리에 따라 Item 클래스를 상속해야만 호환이 됩니다. 우선은 다음과 같이 클래스를 작성합니다. 패키지는 지난 강의와 같은 oortcloud.basictutorial.item으로 하겠습니다.

ItemMetadata.java

package oortcloud.basictutorial.item;

 

import net.minecraft.item.Item;

 

public class ItemMetadata extends Item {

 

}

여기서 하위 아이템을 가지는 아이템은 반드시 setHasSubtypes 메소드를 이용해서 해당 아이템이 하위 아이템을 가지는 것을 명시해야 합니다. 그렇지 않으면 서로 다른 손상 값이 모두 무시되어 인벤토리에서 그냥 겹쳐집니다. 이 사항은 하위 아이템을 가지는 모든 아이템 객체에 대해서 해당되는 사항이므로 생성자에 포함시키도록 하겠습니다.

ItemMetadata.java

public ItemMetadata() {

    setHasSubtypes(true);

}

그럼 이대로 적절한 이름(Unlocalized Name)을 부여해서 등록해보도록 하겠습니다.

ModItems.java

public final class ModItems {

    

    public static Item itemNumber;

    

    public static final void init() {

        itemNumber = new ItemMetadata().setUnlocalizedName("itemNumber").setCreativeTab(CreativeTabs.tabMisc);

        GameRegistry.registerItem(itemNumber, "itemNumber");

    }

    

}

/give 명령어를 이용해서 서로 다른 손상 값을 가지는 아이템을 받아보면 서로 겹쳐지지 않는 것을 확인할 수 있습니다. 반대로 조약돌은 아무리 손상 값을 달리해도 그냥 한 종류의 아이템으로 취급됩니다.

그림 2하위 아이템

여기서 우리는 총 세가지 문제를 확인할 수 있습니다. 1) 이름이 올바로 표시되지 않는다. 2) 텍스처가 올바로 표시되지 않는다. 3) 크리에이티브 탭에 한 종류(손상 값이 0)만 나타난다. 하나씩 해결해보도록 합시다.

 

1) 손상 값에 따른 이름(Unlocalized Name) 지정

아바스토로님의 강의를 참고하면 언어 설정에 맞게 올바른 이름이 표시되게 할 수 있습니다. 하지만 이 방법만으로는 setUnlocalizedName을 통해 부여한 이름이 단 하나의 번역에 대응되기 때문에 손상 값에 따라 다른 이름을 부여할 수는 없습니다. 따라서 손상 값에 따라 현지화 되지 않은 이름(Unlocalized Name)이 바뀌도록 해야합니다. lang파일이 다음처럼 서로 다른 손상 값에 대해 대응될 수 있도록 말이죠.

en_US.lang

item.itemNumber_0.name=Number Zero

item.itemNumber_1.name=Number One

item.itemNumber_2.name=Number Two

item.itemNumber_3.name=Number Three 

 

우리가 작성한 ItemMetadata 클래스 안에서 getUnlocalizedName(ItemStack itemStack)이라는 메소드를 오버라이드하면 아이템 스택의 손상 값이나 NBT 값에 따라서 서로 다른 이름을 대응시킬 수 있습니다. 우리는 손상 값에 따라 이름을 달리 지정할 것이므로 간단히 본래의 이름 뒤에 _(손상값)을 붙이도록 합시다.

ItemMetadata.java

@Override

public String getUnlocalizedName(ItemStack itemStack) {

    return getUnlocalizedName()+"_"+itemStack.getItemDamage();

}

getUnlocalizedName()은 아이템 스택의 상태(손상 값, NBT)에 영향 받지 않는 setUnlocalizedName()으로 등록한 이름 그 자체를 반환하고, itemStack.getItemDamage() itemStack 의 손상 값을 반환합니다.

 

다시 실행해보면 이름 문제는 해결된 것을 확인할 수 있습니다.

 

2) 손상 값에 따른 텍스처 지정

텍스처를 지정하는 과정은 2단계로 이루어집니다. 먼저 다수(혹은 하나)의 텍스처를 등록을 하고, 아이템 스택의 정보를 받아 상황에 맞는 텍스처를 반환하는 형식입니다. 우리는 사용할 텍스처를 모두 등록해서 배열에 저장한 후 주어진 아이템 스택의 손상 값에 맞는 텍스처를 반환하도록 하겠습니다.

텍스처 등록은 해당 아이템의 클래스 내에서 registerIcons(IIconRegister iconRegister) 메소드를 오버라이드하여 제어할 수 있습니다. 앞서 말했듯이 이렇게 등록한 텍스처(IIcon) 객체를 저장해 두어야하기 때문에 해당하는 배열 또한 클래스의 멤버 변수로서 생성해두도록 하겠습니다. 본 강의의 경우에는 ItemMetadata 클래스 내부에 다음의 코드를 추가로 작성하게 됩니다.

ItemMetadata.java

public IIcon[] icons = new IIcon[4];

 

( … )

 

@Override

public void registerIcons(IIconRegister iconRegister) {

for (int i = 0; i < icons.length; i++) {

this.icons[i] = iconRegister.registerIcon("basictutorial:itemNumber_" + i);

}

}

iconRegister.registerIcon 에 전달되는 인자는 setTectureName 메소드와 형식이 동일하여 (Mod ID):(파일 이름)의 형태로 작성해주시면 됩니다.

이제 getIconFromDamage(int damage) 메소드를 오버라이드해서 아이템의 손상 값에 따라 서로 다른 텍스처(IIcon)를 전달할 수 있습니다. /give 명령어를 사용하면 우리가 고려하지 않은 손상 값을 가진 아이템 스택도 생성될 수 있으므로, 사전에 꼭 손상 값을 확인하도록 합시다. 이 또한 당연히 ItemMedata 클래스 내부에서 작성됩니다.

ItemMetadata.java

@Override

public IIcon getIconFromDamage(int damage) {

    if (damage > icons.length) {

        damage = 0;

    }

    return icons[damage];

}

 

텍스처 등록 및 지정과 관련된 모든 과정을 구현했습니다. 이제 텍스처 파일이 있어야 하는데, 4개의 파일을 제공해 드리겠습니다. 각 파일의 경로는 이전 강의와 당연히 동일하고, 파일 이름은 텍스처를 등록하는 과정에서 지정한 것과 동일합니다.

그림 3텍스처 등록

 

3) 크리에이티브탭에 등록

이 과정은 구현에 필수는 아니지만 사용자의 편의를 위해 구현 해두면 좋습니다. 마인크래프트에 등록된 아이템은 기본적으로 손상 값이 0인 아이템 하나만 크리에이티브 탭에 표시됩니다. 크리에이티브 탭에 표시될 아이템 스택들을 제어하려면 getSubItems(Item item, CreativeTabs tab, List list) 구현해야 합니다.

ItemMetadata.java

@Override

public void getSubItems(Item item, CreativeTabs tab, List list) {

    for (int i = 0; i < icons.length; i ++) {

list.add(new ItemStack(item, 1, i));

}

}

Item은 현재 등록하려는 item의 객체, tab은 현재 추가하려는 크리에이티브 탭의 종류, list는 지금까지 추가된 아이템들을 모두 보관하고 있는 리스트입니다. add명령어를 이용해서 우리가 원하는 아이템 스택을 등록해주면 됩니다.

그림 4크리에이티브 탭

 

최종적으로 작성된 코드를 보여드리겠습니다.

ItemMetadata.java

package oortcloud.basictutorial.item;

 

import java.util.List;

 

import net.minecraft.client.renderer.texture.IIconRegister;

import net.minecraft.creativetab.CreativeTabs;

import net.minecraft.item.Item;

import net.minecraft.item.ItemStack;

import net.minecraft.util.IIcon;

 

public class ItemMetadata extends Item {

    

    public IIcon[] icons = new IIcon[4];

    

    public ItemMetadata() {

        setHasSubtypes(true);

    }

    

    @Override

    public void getSubItems(Item item, CreativeTabs tab, List list) {

        for (int i = 0; i < icons.length; i ++) {

     list.add(new ItemStack(item, 1, i));

     }

    }

    

    @Override

    public void registerIcons(IIconRegister iconRegister) {

     for (int i = 0; i < icons.length; i++) {

     this.icons[i] = iconRegister.registerIcon("basictutorial:itemNumber_" + i);

     }

    }

    

    @Override

    public IIcon getIconFromDamage(int damage) {

        if (damage > icons.length) {

            damage = 0;

        }

        return icons[damage];

    }

    

    @Override

    public String getUnlocalizedName(ItemStack itemStack) {

        return getUnlocalizedName()+"_"+itemStack.getItemDamage();

    }

      

    

}

 

ModItems.java

package oortcloud.basictutorial.item;

 

import cpw.mods.fml.common.registry.GameRegistry;

import net.minecraft.creativetab.CreativeTabs;

import net.minecraft.item.Item;

 

public final class ModItems {

    

    public static Item itemNumber;

    

    public static final void init() {

        itemNumber = new ItemMetadata().setUnlocalizedName("itemNumber").setCreativeTab(CreativeTabs.tabMisc);

        GameRegistry.registerItem(itemNumber, "itemNumber");

    }

    

}

 

 

 

참고자료

http://bedrockminer.jimdo.com/modding-tutorials/basic-modding-1-7/metadata-blocks-and-items/

https://ko.wikipedia.org/wiki/%EB%A9%94%ED%83%80%EB%8D%B0%EC%9D%B4%ED%84%B0

http://minecraft-ko.gamepedia.com/%EB%8D%B0%EC%9D%B4%ED%84%B0_%EA%B0%92

강의 환경 : forge-1.7.10-10.13.4.1517-1.7.10

 

배경 지식

  1. 기본 모드 파일

 

강의목표

  1. 아이템 객체를 생성한다.
  2. 생성된 객체를 등록한다.
  3. 등록된 아이템에 텍스처(Texture)를 부여한다.

 

1. ModItems 클래스 만들기

아이템을 만들기에 앞서, 먼저 우리가 추가할 아이템들을 관리할 수 있는 클래스를 준비해야 합니다. 클래스의 이름이나 패키지는 기능과는 크게 관련 없지만 훗날 능률을 높이기 위해서 정해진 약속대로 정의하는 것이 좋습니다. 본 강의에서는 이름은 ModItems, 패키지는 oortcloud.basictutorial.item으로 설정하겠습니다.

그림 1패키지 탐색기

이제 ModItems 클래스의 뼈대를 구성해보도록 하겠습니다. 먼저, ModItems 클래스가 다른 클래스로부터 상속되지 않도록 final로 선언을 합니다. 그리고 아이템 객체를 생성하고 등록할 init() 메소드를 만듭니다. 이때 init() 메소드는 1) 외부에서 메소드를 실행할 수 있도록 public으로, 2) ModItems는 객체가 생성되지 않으므로 static으로, 3) 클래스와 마찬가지로 오버라이드 될 수 없도록 final로 선언해줍니다.

ModItems.java

package oortcloud.basictutorial.item;

 

public final class ModItems {

    

    public static final void init() {

        

    }

    

}

 

 

2. 아이템 객체 생성 및 성질 부여

뼈대가 완성되었기 때문에 간단히 Item 객체를 만들어 보도록 하겠습니다. 위에서와 같이 public static으로 선언해줍니다. 처음 아이템 변수를 선언할 때는 임포트와 관련된 에러가 발생할 수 있습니다. Ctrl + Shift + O를 누르면 이클립스에서 필요한 클래스를 자동으로 임포트 해줍니다.

ModItems.java

package oortcloud.basictutorial.item;

 

import net.minecraft.item.Item;

 

public final class ModItems {

    

    public static Item tutItem;

    

    public static final void init() {

        

    }

    

}

 

이제 tutItem 에 적용될 아이템 객체를 만들겠습니다. Item 객체는 하나 또는 여럿의 아이템 종류를 대표하여 생성되는 아이템 스택(Item Stack)의 이름, 텍스쳐, 성질을 결정합니다. 오늘 우리가 다룰 아이템은 단순히 Item 클래스의 생성자를 사용하면 됩니다.

ModItems.java

package oortcloud.basictutorial.item;

 

import net.minecraft.item.Item;

 

public final class ModItems {

    

    public static Item tutItem;

    

    public static final void init() {

        tutItem = new Item();

    }

    

}

 

이렇게 만든 객체는 당연히 아무런 기능이나 성질을 가지고 있지 않습니다. 심지어 이름조차 없기 때문에 등록할 수도 없습니다. 성질을 부여할 때는 다음과 같은 메소드를 사용합니다. 이들은 모두 조작한 객체를 다시 반환합니다. (이 부분은 예시를 보면서 추가로 설명하겠습니다)

setUnlocalizedName(String name)

아이템의 이름을 name으로 설정합니다.

setCreativeTab(CreativeTabs tab)

크리에이티브 모드의 tab에서 이 아이템을 찾을 수 있습니다.

setTextureName(String name)

텍스쳐의 이름을 name으로 설정합니다.

setMaxStackSize(int size)

중첩될 수 있는 최대 개수를 size로 설정합니다.

 

각 메소드가 다시 아이템 객체를 반환하기 때문에 예시와 같이 한 줄에 나열해서 호출할 수 있습니다. 아래의 아이템은 tutItem이라는 이름을 가지게 되고 크리에이티브 모드에서 기타 항목에 들어가게 됩니다.

tutItem = new item().setUnlocalizedName("tutItem").setCreativeTab(CreativeTabs.tabMisc);

 

 

3. 아이템 등록

이렇게 객체를 생성하더라도 마인크래프트 내부에 이식된 것이 아니라서 실제로는 게임에 아무런 영향도 주지 않습니다. 따라서 등록(registration)이라는 과정을 통해 아이템을 내부에 이식해야 합니다.

public static void GameRegistry.registerItem(Item item, String name)

Item item

등록할 아이템 객체입니다.

String name

setUnlocalizedName()에서 사용한 이름과 같은 이름입니다.

 

ModItems.java

package oortcloud.basictutorial.item;

 

public final class ModItems {

    

    public static Item tutItem;

    

    public static final void init() {

        tutItem = new Item().setUnlocalizedName("tutItem").setCreativeTab(CreativeTabs.tabMisc);

        GameRegistry.registerItem(tutItem, "tutItem");

    }

    

}

 

마지막으로 init()을 호출하기만 하면 등록이 완료 됩니다. 아이템의 등록은 다른 모드와 호환을 위해 최대한 빨리 진행해야 하기 때문에 Pre-Initialization 단계가 적합합니다. 다음과 같이 기본 모드 파일의 해당 이벤트에서 호출하면 됩니다.

BasicTutorial.java

( … )

    @Mod.EventHandler

    public static void preInit(FMLPreInitializationEvent event) {

        ModItems.init();

    }

( … )

 

그림 2추가된 아이템(1)

아이템이 추가되어 크리에이티브 탭에서 찾아볼 수 있습니다. 하지만 이름은 item.tutItem.name이고 텍스쳐는 검정/분홍 체스판입니다. 등록은 완료되었으나 아직 (현지화 된)이름과 텍스쳐가 올바로 지정되지 않았기 때문입니다. 이 중 텍스쳐는 곧 바로 추가할 것이고 현지화는 아바스트로님의 강의를 확인하시면 됩니다.

 

 

4. 텍스처 등록

먼저 등록할 텍스처를 준비해 보겠습니다. 크기는 기본적으로 16*16에 png 형식입니다. 아래의 텍스처는 제 모드인 토지 중개사(Estate Agent)에 사용된 파일입니다.

일단 이 텍스쳐를 "(WorkspaceRoot)/src/main/resources/assets/(modid)/textures/items/(texture_name).png"에 저장해야 합니다. 괄호로 표시된 값은 변수라고 생각하시면 됩니다. 본 강의에서 다루는 예제라면 다음의 경로에 저장해야 합니다.

그림 3텍스처 경로

 

이제 아이템 객체와 텍스처 파일을 연결해야 합니다. ModItems 클래스에 선언된 객체에 .setTextureName("(modid):(file_name)") 메소드를 호출합니다. 이 역시 괄호로 표시된 부분은 변수입니다. ":" 기호로 구분되어 앞은 모드 ID, 뒤는 연결할 PNG 파일 이름입니다.

.setTextureName("basictutorial:tutItem")

본 강의에서는 위와 같이 호출해야 합니다.

 

ModItems.java

package oortcloud.basictutorial.item;

 

public final class ModItems {

    

    public static Item tutItem;

    

    public static final void init() {

        tutItem = new Item().setUnlocalizedName("tutItem").setCreativeTab(CreativeTabs.tabMisc).setTextureName("basictutorial:tutItem");

        GameRegistry.registerItem(tutItem, "tutItem");

    }

    

}

 

 

 

참고 자료

http://bedrockminer.jimdo.com/modding-tutorials/basic-modding-1-7/first-item/

강의 환경 : forge-1.7.10-10.13.4.1517-1.7.10

 

배경 지식

  1. 무형 조합법(Shapeless Recipe) 및 화로 조합법(Smelting Recipe) 추가하기
  2. 유형 조합법(Shaped Recipe) 추가하기
  3. 광물 사전

 

강의목표

  1. 광물 사전을 이용한 조합법을 추가한다.
  2. 아이템의 내구도(damage)에 영향을 받지 않는 조합법을 추가한다.

 

1. 광물 사전을 이용한 조합법

마인크래프트 포지에는 광물 사전이라는 개념이 있어서, 비슷하거나 완전히 동일한 아이템을 하나의 이름으로 묶습니다. 원목의 경우에는 참나무, 가문비나무 등 여러 종류가 있지만 모두 "logWood"라는 이름으로 대표되고, 구리 주괴도 IC2에서, 포레스트리(Forestry)에서 각각 아이템을 추가하지만 "ingotCopper"라는 이름으로 연결되어 있습니다. 자주 사용되는 목록여기에서 찾아보실 수 있습니다. 다만 최신 버전은 아닙니다.

조합법을 추가할 때, 광물 사전을 이용하지 않으면 원치 않은 결과가 발생할 수 있습니다. 나무를 이용한 조합법이 등록할 때 사용한 한 종류의 나무만 지원하고, 광물을 사용하는 조합법이 다른 모드의 광물은 인식하지 않는다면, 사용자 입자에서 굉장히 불편할 것입니다. 광물 사전은 이와 같이 조합법을 추가할 때, 사실상 반드시 이용해야하는 기능입니다.

광물 사전 조합법은 IRecipe를 구현하고 있으며 유형 조합법, 무형 조합법이 각각 따로 작성되어 있습니다. 다만, 유형 조합법/무형 조합법과 같이 GameRegistry에서 따로 추가하는 함수가 정의되어 있지 않아 addRecipe라는 일반적인 등록 함수에 IRecipe 객체를 전달하는 방식으로 조합법을 추가하게 됩니다.

void GameRegistry.addRecipe(IRecipe recipe)

IRecipe recipe

등록될 조합법

 

광물 사전 조합법은 IRecipe를 상속하고 있기 때문에 위 함수의 인자로서 적합합니다. 즉, 우리가 원하는 조합에 대한 광물 사전 조합법의 객체를 만든 뒤 인자로 전달하기만 하면 등록이 끝나는 것입니다.

new ShapedOreRecipe(ItemStack result, Object... recipe)

ItemStack result

조합법 결과

Object... recipe

조합법 배치 및 재료들

 

new ShapelessOreRecipe(ItemStack result, Object... recipe)

ItemStack result

조합법 결과

Object... recipe

조합법 재료들

 

위 두 생성자는 지난 강의에서 살펴본 GameRegistry.addShapelessRecipe/GameRegistry.addShapedRecipe와 거의 같습니다. 다만 아이템 스택(Item Stack)뿐만 아니라 광물 사전 이름도 주어 줄 수 있습니다. 서로 거의 적용 방법이 비슷하니 유형 조합법만 예시를 살펴보겠습니다.

GameRegistry.addRecipe(new ShapedOreRecipe(new ItemStack(Blocks.chest,8), "aaa","a a","aaa", 'a', "logWood"));

 



그림 1추가된 광물 사전 조합

위의 코드가 아래와 같은 조합법을 추가합니다. 상자 1개를 만드는 조합은 기본 조합이고, 포지(Forge)에서 자체적으로 광물 사전 조합으로 변경한 것을 알 수 있습니다. 아래가 추가된 조합법입니다. NEI를 사용해 보신 분들은 아시겠지만 광물 사전 조합법은 해당 광물 사전의 모든 아이템들이 무작위로 표시되며 바뀝니다.

 

 

2. 내구도에 영향을 받지 않는 조합법

아이템 스택(Item Stack)을 생성할 때, 인자로 내구도(damage) 값을 줄 수 있는 것은 이미 알고 계실 겁니다. 내구도 값에 따라 서로 다른 염료가 결정되고, 실제 도구의 손상 정도를 나타내기도 합니다. 조합법을 등록할 때도 적절한 내구도 값을 전달해서 명확한 조합법이 만들어 질 수 있습니다. 하지만 무기나 곡괭이를 이용한 조합의 경우에는 내구도 값이 조합법에서 문제가 되는 경우도 있습니다.

그림 2발사기 조합법(1)

한 번도 사용하지 않은 활이라면 위의 조합은 발사기(Dispenser)를 만들어야 합니다. 하지만 한 번 이상 사용한 활은 내구도 값이 다르기 때문에 조합법에서 다른 재료로 인식됩니다. 하지만 여기서 활의 가능한 모든 내구도(0, 1, 2, …)에 대해서 조합법을 추가하는 것은 성능을 저하시키는 요인이 됩니다. 결코 올바른 방법이 아니죠.

이런 상황에서는 아이템 스택(Item Stack)의 생성자에 일반적인 상수를 내구도 값으로 전달하는 것이 아닌 OreDictionary.WILDCARD_VALUE (=32767)라는 특수한 값을 전달하면 모든 내구도에 대해서 조합법이 재료를 인식합니다.

방법은 간단하기 때문에 바로 예시를 보며 강의를 마치겠습니다.

GameRegistry.addShapedRecipe(new ItemStack(Blocks.dispenser),

"aaa", "aba", "aca",

'a' , Blocks.cobblestone, 'b', new ItemStack(Items.bow, 1, OreDictionary.WILDCARD_VALUE), 'c', Items.redstone);

 

그림 3발사기 조합법(2)

 

 

참고 자료

http://greyminecraftcoder.blogspot.com.au/2015/02/recipes.html

강의 환경 : forge-1.7.10-10.13.4.1517-1.7.10

 

배경지식 및 선행과정 :

1. 무형 조합법(Shapeless Recipe) 및 화로 조합법(Smelting Recipe) 추가하기

 

강의 목표 :

1. 유형 조합법(Shaped Recipe)의 구성 요소를 이해한다.

2. 유형 조합법(Shaped Recipe)을 등록한다.

 

1. 유형 조합법

유형 조합법은 조합대의 아이템 배치에 따라 결과물이 바뀌는 조합법입니다. 3*3 이 최대 크기이고 그보다 작은 경우에는 모양은 유지한 채 자유롭게 옮겨도 같은 조합법으로 적용됩니다. 몇몇 조합법은 좌우로 대칭을 시킬 수도 있습니다. 조합법 자체는 항상 직사각형으로 정의되지만, '빈칸'을 설정할 수 있어서 다양한 모양을 가집니다. IC2처럼 해당 모드만의 유형 조합법을 따로 구현한 사례도 있고 광물 사전(Ore Dictionary)을 이용해서 목재와 같이 종류가 다양한 아이템을 모두 받아들이게 만든 조합법도 있습니다.

그림 1유형조합법

 

 

2. 유형 조합법 정의하기

1) 형태 정의하기

우선 유형 조합법을 어떻게 정의하는 지 알아보겠습니다. 유형 조합법은 형태와 구성요소로 이루어져 있습니다. 즉, 어떤 모양으로 어떤 아이템을 놓느냐에 따라 결과가 결정되는 것입니다. 형태를 어떻게 구성하는지 그 원리만 파악하면 각 위치에 아이템을 지정해서 프로그램 상에서 조합법을 추가하는 것은 크게 어렵지 않습니다.

앞서 모든 조합법은 직사각형이 기본 형태라고 말씀드렸습니다. 가로 N, 세로 M의 조합법은 M개의 길이 N짜리 문자열로 그 형태가 정의됩니다. 즉, 직사각형으로 표현한 후 위쪽부터 행 별로 문자열을 늘어놓은 꼴이 됩니다. 말로만 하면 굉장히 복잡하기 때문에 예를 통해 알아보겠습니다.

A

  

A

A

 

A

A

A

위와 같은 조합법이라면 "A ", "AA ", "AAA"으로 적을 수 있습니다. 직사각형의 조건을 맞추기 위해서 공백을 추가한 것을 주의해서 보세요.

 

A

 

B

 

B

이번에는 " A ", "B B"으로 적을 수 있습니다. 반드시 정사각형일 필요도 없습니다. 서로 다른 알파벳은 서로 다른 아이템을 의미합니다. 이번에도 A의 양쪽에 공백이 있으니 주의하세요.

 

A

A

B

B

이 조합법은 "AA", "BB"으로 표기 되고,

A

A

 

B

B

 
   

이 조합법은 "AA ", "BB ", " "으로 표기됩니다. 얼핏 보면 두 조합법이 같아 보이지만 첫 번째 조합법은 크기가 2*2이고, 두 번째 조합법은 3*3입니다. 그렇기 때문에 아래의 조합법은 다음과 같은 배치에서 조합이 되지 않습니다.

그림 2두 조합법의 차이

 

2) 구성 아이템 정의하기

다양한 예를 다루면서 주의점도 파악했기 때문에, 이제 각 문자에 대해서 아이템을 지정하는 방법을 가르쳐드리겠습니다. 형태를 정의하는 과정에서 여러 문자를 사용하여 다른 아이템이란 것을 나타냈기 때문에, 각 문자 별로 구성 아이템을 지정해주면 됩니다. 이때 주의하실 점은 문자열(String)이 아닌 문자(char)에 값을 대응 시켜야 한다는 점입니다.

"aa", "bb", 'a', new ItemStack(Items.potato), 'b', new ItemStack(Items.carrot)

위에서 언급된 조합법을 나타내면 위와 같습니다. 각 문자는 "가 아닌 '을 이용해서 문자(char)로 표현했다는 것을 다시 확인해주시길 바랍니다.

 

 

3. 유형 조합법 추가하기

유형 조합법을 추가하는 위치나 상태는 이전 강의와 동일합니다. 기본 모드 파일도 동일한 상태입니다.

 

GameRegistry.addShapedRecipe(ItemStack output, Object... params)

ItemStack output : 조합법의 결과인 ItemStack입니다.

Object... params : 조합법의 형태와 재료를 결정하는 다수의 문자열(String), 문자(char), ItemStack입니다.

조합법을 추가하는 함수도 무형 조합법과 거의 같습니다. 다만 params에 들어갈 인자가 지금까지 살펴본 문자열(String), 문자(char), 아이템스택(Item Stack)의 조합입니다.

구체적인 부분은 위에서 모두 살펴보았기 때문에 간단한 예를 통해 강의를 마무리하도록 하겠습니다. 다음의 코드가 그림에 해당하는 조합법을 추가합니다.

GameRegistry.addShapedRecipe(new ItemStack(Items.apple, 4), "ab", "ba", 'a', new ItemStack(Items.potato), 'b', new ItemStack(Items.carrot));

그림 3조합법 예

강의 환경 : forge-1.7.10-10.13.4.1517-1.7.10

 

배경지식 :

1. 아이템스택(Item Stack)

 

선행과정 :

1. 기본 모드 파일

 

강의 목표 :

1. 무형 조합법(Shapeless Recipe)를 추가한다

2. 화로 조합법(Smelting Recipe)를 추가한다

 

1. 조합법

조합법은 자신이 추가한 요소를 플레이어에게 제공하는 유용한 수단입니다. 모드 제작자는 자신이 만든 블록과 아이템을 어떠한 방식으로든 플레이어가 사용할 수 있도록 경로를 만들어야 하는데, 조합법을 추가하지 않는다면 엔티티(Entity)를 통한 드롭이나 명령어, 혹은 정말 특수한 방법을 이용해서라도 추가된 아이템 얻을 수 있게 해야 합니다. 조합법은 플레이어 입장에서도 직관적이고 제작자 입장에서도 추가가 용이합니다.

코드를 살펴보기 전에 유형 조합법(Shaped Recipe)과 무형 조합법(Shapeless Recipe)의 차이점을 먼저 알아보겠습니다. 유형 조합법은 조합의 결과가 재료들의 위치에 종속적인 조합법입니다. 즉, 재료들의 위치가 달라지면 결과가 달라지거나 혹은 사라지는 조합법이죠. 대부분의 조합법이 여기에 포함됩니다. 무형 조합법은 반대로 재료들의 위치가 결과에 영향을 끼치지 않는 조합법입니다. 염색 조합과 같은 "섞는" 조합법들이 여기에 속합니다.

그림 1 무형 조합법(Shapeless Recipe)의 예

그림 2 유형 조합법(Shaped Recipe)의 예

그림 3 화로 조합법(Smelting Recipe)의 예

 

2. Shapeless 조합법 추가하기

Shapeless 조합법을 추가해보도록 하겠습니다. 먼저, 기본 모드 파일이 다음과 같이 설정되어 있다고 가정합니다.

@Mod(modid = "basictutorial", name = "Basic Tutorial", version = "1.0.0")

public class BasicTutorial {

    @SidedProxy(clientSide = "oortcloud.basictutorial.ClientProxy", serverSide = "oortcloud.basictutorial.CommonProxy")

    public static CommonProxy proxy;

    

    @Mod.EventHandler

    public static void preInit(FMLPreInitializationEvent event) {

    }

 

    @Mod.EventHandler

    public static void Init(FMLInitializationEvent event) {

    }

 

    @Mod.EventHandler

    public static void postInit(FMLPostInitializationEvent event) {

    }

}

조합법은 일반적으로 Pre-Initialization 상태에서 추가합니다. 만약 다른 모드로부터 조합법이 영향을 받는다면 Initialization이나 Post-Initialization에서 추가할 수도 있으나, 가능하면 Pre-Initialization에서 추가하는 것이 바람직합니다. 다른 모드에서 여러분이 추가할 조합법을 참조할 수도 있기 때문입니다.

Shapeless 조합법은 다음의 함수를 통해서 추가합니다.

GameRegistry.addShapelessRecipe(ItemStack output, Object... params)

ItemStack output : 조합법의 결과인 ItemStack입니다.

Object... params : 조합법의 재료인 다수의 ItemStack입니다.

output의 아이템 스택은 개수가 반영됩니다. 하지만 params의 아이템 스택은 개수가 반영되지 않습니다. 결과는 코드를 통해 살펴보겠습니다.

    @Mod.EventHandler

    public static void preInit(FMLPreInitializationEvent event) {

        GameRegistry.addShapelessRecipe(new ItemStack(Items.bread, 2), new ItemStack(Items.wheat), new ItemStack(Items.wheat), new ItemStack(Items.sugar));

    }

위의 코드는 다음과 같은 조합법을 추가합니다.

그림 4 추가된 무형 조합법(Shapeless Recipe)

3. 화로 조합법(Smelting Recipe) 추가하기

화로 조합법의 구성요소는 재료 1개와 결과 1개입니다. 따라서 무형 조합법보다 추가가 간단합니다. 사실 무형 조합법에 없는 요소도 있는데, 바로 아이템을 구웠을 때 지급되는 경험치입니다. 즉, 화로 조합법에는 재료, 결과와 함께 경험치 양을 지정해야 합니다.

화로 조합법 또한 무형 조합법과 동일하게 Pre-Initialization에서 추가하는 것이 좋습니다.

화로 조합법은 다음 함수를 통해 추가합니다.

GameRegistry.addSmelting(ItemStack input, ItemStack output, float xp)

ItemStack input : 조합법의 재료가 되는 ItemStack입니다. Item과 Block이 직접 사용될 수도 있습니다.

ItemStack output : 조합법의 결과가 되는 ItemStack입니다.

float xp : 지급되는 경험치 양입니다.

예시를 통해 코드가 어떻게 적용되는지 확인해보도록 하겠습니다.

    @Mod.EventHandler

    public static void preInit(FMLPreInitializationEvent event) {

        GameRegistry.addSmelting(Items.wheat, new ItemStack(Items.bread), 1.0F);

    }

그림 5 추가된 화로 조합법

이로서 화로 조합법도 추가해보았습니다.

1. 게임 내에서 아이템 스택

아이템 스택은 모드를 개발함에 있어 아주 중요한 기본 개념입니다. 쉽게 말하면 가방의 한 칸을 차지하는 대상이 모두 아이템 스택입니다. 같은 아이템(Item)이라고 할 지라도 서로 개수가 다를 수 있고, 도구처럼 내구도나 부여된 마법이 다를 수도 있습니다. 이런 요소들이 모여 하나의 아이템 스택을 정의합니다.

그림 1 서로 다른 아이템 스택

위의 그림에서 첫 번째 줄은 내구도가 다른 도구와 부여된 마법(Enchantment)이 다른 도구를 보여줍니다. 내구도는 damage라는 변수에 값이 저장이 되고 마법 부여의 경우는 NBT라는 특수한 형태로 값이 저장됩니다. 두 번째 줄에서 블록도 아이템 스택이 될 수 있음을 확인할 수 있습니다. 세 번째 줄에서처럼 아이템 스택에는 stacksize라는 개수 정보를 가지고 있는 변수도 있습니다. 마지막 줄의 염료들은 서로 같은 아이템(Item) 종류입니다. 다만 이들 모두 damage 값이 달라서 그에 따라 완전히 다른 아이템으로 분류됩니다. 이런 아이템들은 하위 아이템(sub item)이라고 부릅니다.

2. 프로그램 내에서 아이템 스택

프로그램 내에서 이러한 아이템 스택을 생성하는 법을 간략하게 소개하겠습니다. 위에서 살펴본 아이템 스택의 요소 중에 가장 중요한 것은 "아이템(Item) 종류", "개수(stack size)", "내구도(damage)"입니다. ItemStack의 생성자도 위의 세 변수를 기본적으로 받아들입니다.

public ItemStack(Item item, int stacksize, int damage)

public ItemStack(Item item, int stacksize) : damage=0으로 설정

public ItemStack(Item item) : stacksize=1, damage=0으로 설정

생성자는 크게 3가지가 이용됩니다. 세 성분 모두 받는 생성자부터 아이템 종류만 받는 생성자까지 모두 사용할 수 있습니다. 인자를 1개나 2개만 받는 생성자는 개수(stacksize)나 내구도(damage)를 정해진 기본값으로 이용하기 때문에 정확히 어떤 값으로 설정되는지 상기하시기 바랍니다. 마인크래프트에서 미리 정의된 아이템 종류는 Items 클래스 내부에서 찾을 수 있습니다.

본 강의는 Forge-1.7.10-10.13.4.1492-1.7.10의 환경에서 작성되었습니다.

1. 강의 목표

- 새로운 포션을 만든다.

- 새로운 포션에 적절한 아이콘을 지정한다.

- 새로운 포션에 원하는 효과를 지정한다.

- 새로운 포션을 엔티티에게 적용시킨다.

 

2. 사전 요구 사항

- 포지(Forge)

- 기초 모드 제작 환경(Base Mod File)

 

3. 포션이란

마인크래프트에서 포션이라고 하면 물약을 떠올리는 사람이 많습니다. 이 강의에서 포션은 정확히는 물약을 마셨을 때 적용되는 효과를 의미합니다. 이러한 효과는 무기에 특별한 능력을 부여하거나 대상의 능력치를 변화시키기도 합니다.

 

4. 포션 등록하기(Registration)

가장 먼저 할 일은 포션의 틀을 만들고 등록하는 것입니다. 일단 모드 파일이 다음처럼 설정되어있다고 가정하겠습니다.

 

package tutorial;

 

import cpw.mods.fml.common.Mod;

import cpw.mods.fml.common.SidedProxy;

import cpw.mods.fml.common.event.FMLInitializationEvent;

import cpw.mods.fml.common.event.FMLPostInitializationEvent;

import cpw.mods.fml.common.event.FMLPreInitializationEvent;

 

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

public class Tutorial {

 

    @SidedProxy(clientSide = "tutorial.ClientProxy", serverSide = "tutorial.CommonProxy")

    public static CommonProxy proxy;

 

    @Mod.EventHandler

    public static void preInit(FMLPreInitializationEvent event) {

    }

 

    @Mod.EventHandler

    public static void Init(FMLInitializationEvent event) {

    }

 

    @Mod.EventHandler

    public static void postInit(FMLPostInitializationEvent event) {

    }

 

}

 

 

1) 바닐라 포션 등록 체계

첫 번째로 바닐라 마인크래프트에서 어떻게 포션을 등록하고 다루는 지 살펴보도록 합시다.

 

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.AttributeModifier;

import net.minecraft.entity.ai.attributes.BaseAttributeMap;

import net.minecraft.entity.ai.attributes.IAttribute;

import net.minecraft.entity.ai.attributes.IAttributeInstance;

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(1, false, 8171462)).setPotionName("potion.moveSpeed").setIconIndex(0, 0).func_111184_a(SharedMonsterAttributes.movementSpeed, "91AEAA56-376B-4498-935B-2F7F68070635", 0.20000000298023224D, 2);

public static final Potion moveSlowdown = (new Potion(2, true, 5926017)).setPotionName("potion.moveSlowdown").setIconIndex(1, 0).func_111184_a(SharedMonsterAttributes.movementSpeed, "7107DE5E-7CE8-4030-940E-514C1F160890", -0.15000000596046448D, 2);

public static final Potion digSpeed = (new Potion(3, false, 14270531)).setPotionName("potion.digSpeed").setIconIndex(2, 0).setEffectiveness(1.5D);

public static final Potion digSlowdown = (new Potion(4, true, 4866583)).setPotionName("potion.digSlowDown").setIconIndex(3, 0);

public static final Potion damageBoost = (new PotionAttackDamage(5, false, 9643043)).setPotionName("potion.damageBoost").setIconIndex(4, 0).func_111184_a(SharedMonsterAttributes.attackDamage, "648D7064-6A60-4F59-8ABE-C2C23A6DD7A9", 3.0D, 2);

public static final Potion heal = (new PotionHealth(6, false, 16262179)).setPotionName("potion.heal");

public static final Potion harm = (new PotionHealth(7, true, 4393481)).setPotionName("potion.harm");

public static final Potion jump = (new Potion(8, false, 7889559)).setPotionName("potion.jump").setIconIndex(2, 1);

public static final Potion confusion = (new Potion(9, true, 5578058)).setPotionName("potion.confusion").setIconIndex(3, 1).setEffectiveness(0.25D);

/** The regeneration Potion object. */

public static final Potion regeneration = (new Potion(10, false, 13458603)).setPotionName("potion.regeneration").setIconIndex(7, 0).setEffectiveness(0.25D);

public static final Potion resistance = (new Potion(11, false, 10044730)).setPotionName("potion.resistance").setIconIndex(6, 1);

/** The fire resistance Potion object. */

public static final Potion fireResistance = (new Potion(12, false, 14981690)).setPotionName("potion.fireResistance").setIconIndex(7, 1);

/** The water breathing Potion object. */

public static final Potion waterBreathing = (new Potion(13, false, 3035801)).setPotionName("potion.waterBreathing").setIconIndex(0, 2);

/** The invisibility Potion object. */

public static final Potion invisibility = (new Potion(14, false, 8356754)).setPotionName("potion.invisibility").setIconIndex(0, 1);

/** The blindness Potion object. */

public static final Potion blindness = (new Potion(15, true, 2039587)).setPotionName("potion.blindness").setIconIndex(5, 1).setEffectiveness(0.25D);

/** The night vision Potion object. */

public static final Potion nightVision = (new Potion(16, false, 2039713)).setPotionName("potion.nightVision").setIconIndex(4, 1);

/** The hunger Potion object. */

public static final Potion hunger = (new Potion(17, true, 5797459)).setPotionName("potion.hunger").setIconIndex(1, 1);

/** The weakness Potion object. */

public static final Potion weakness = (new PotionAttackDamage(18, true, 4738376)).setPotionName("potion.weakness").setIconIndex(5, 0).func_111184_a(SharedMonsterAttributes.attackDamage, "22653B89-116E-49DC-9B6B-9971489B5BE5", 2.0D, 0);

/** The poison Potion object. */

public static final Potion poison = (new Potion(19, true, 5149489)).setPotionName("potion.poison").setIconIndex(6, 0).setEffectiveness(0.25D);

/** The wither Potion object. */

public static final Potion wither = (new Potion(20, true, 3484199)).setPotionName("potion.wither").setIconIndex(1, 2).setEffectiveness(0.25D);

public static final Potion field_76434_w = (new PotionHealthBoost(21, false, 16284963)).setPotionName("potion.healthBoost").setIconIndex(2, 2).func_111184_a(SharedMonsterAttributes.maxHealth, "5D6F0BA2-1186-46AC-B896-C61C5CEE99CC", 4.0D, 0);

public static final Potion field_76444_x = (new PotionAbsoption(22, false, 2445989)).setPotionName("potion.absorption").setIconIndex(2, 2);

public static final Potion field_76443_y = (new PotionHealth(23, false, 16262179)).setPotionName("potion.saturation");

Potion 클래스의 일부

 

블록과 아이템과 비슷하게 Potion 클래스의 객체들이 미리 생성되어 있습니다. 각 포션에는 고유한 정수(int)값인 id가 있습니다. 이 id는 포션 객체가 저장되는 배열(potionTypes)에서 주소(index)를 의미합니다.

 

2) 포션 등록 체계 수정

 

여기서 주목 할 점은 마인크래프트에서 선언된 배열의 크기가 32이고 23까지 이미 기존 포션들이 사용 중이란 것입니다. 포지(Forge) 위에서 자신의 모드 이외에 다른 모드도 충분히 포션을 추가할 여지가 있으므로 부족한 배열 공간으로 인해 충돌이 일어날 수 있습니다. 따라서 우리는 1) 배열상에 비어있는 공간을 찾고, 2) 없다면 배열을 늘려야 합니다.

 

(1) 배열 상에서 사용 가능한 주소 찾기

다음의 함수(Method)는 Potion.potionTypes를 탐색하여 비어있는 공간을 찾아서 해당 주소를 반환해줍니다. 만약 빈 공간이 없다면 -1을 반환합니다. 탐색을 1부터 시작하는 이유는 바닐라 마인크래프트에서도 0은 비워놓기 때문입니다. 실제 /effect 명령어도 0은 받아들이지 않습니다.

 

    public static int getEmptyID() {

        for (int i = 1 ; i < Potion.potionTypes.length; i++) {

            if (Potion.potionTypes[i] == null) return i;

        }

        return -1;

    }

 

(2) 배열 확장하기

이제 하나 남은 과정은 배열 상에 남은 공간이 없는 경우 Potion.potionTypes를 더 큰 배열로 바꾸는 것 입니다. Potion.potionTypes가 final로 정의되어 있기 때문에 일반적인 방법으로는 불가능합니다. 따라서 Java Reflection를 사용하여 코드를 작성하겠습니다.

 

    public static void expand() {

        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[Potion.potionTypes.length*2];

                    System.arraycopy(potionTypes, 0, newPotionTypes, 0,

                            potionTypes.length);

                    f.set(null, newPotionTypes);

                }

            } catch (Exception e) {

            }

        }

    }

Ref: http://www.minecraftforum.net/forums/archive/tutorials/931752-forge-creating-custom-potion-effects

 

위의 함수는 Potion.potionTypes의 크기를 2배로 늘리는 함수 입니다. Reflection을 알면 이해에 큰 차질이 없을 것이고 그렇지 않다면 설명하기 어려우므로 생략하도록 하겠습니다.

 

3) 포션 클래스와 객체 만들기

이제 새로운 포션을 등록하기 위한 기초가 다져졌습니다. 이제는 등록할 포션 객체를 만들 차례입니다.

 

package tutorial;

 

import net.minecraft.potion.Potion;

 

public class PotionTutorial extends Potion {

    public PotionTutorial(int id, boolean isBadEffect, int color) {

        super(id, isBadEffect, color);

        setPotionName("potion.timeslow");

    }

}

 

위와 같이 간단하게 새로운 포션 클래스를 만드세요. setPotionName에는 적당한 이름을 지정해주시면 됩니다. .lang파일을 이용하면 언어마다 다른 이름을 지정해줄 수 있습니다. 현재 단계는 포션을 등록만 하는 과정이므로 구체적인 내용은 작성하지 않고 필요한 내용만 작성합니다.

 

 

그 다음 포션 객체를 선언합시다. 주 Mod 클래스에 만들겠습니다. 다음과 같이 해야 필요할 때, 배열이 확장됩니다.

 

    public static Potion potionTutorial;

 

@Mod.EventHandler

    public static void preInit(FMLPreInitializationEvent event) {

        int id = getEmptyID();

        if (id != -1) {

            potionTutorial = new PotionTutorial(getEmptyID(), false, 0xFFFFFF);

        } else {

            expand();

            potionTutorial = new PotionTutorial(getEmptyID(), false, 0xFFFFFF);

        }

    }

 

 

위의 코드를 이해하기 위해 포션(Potion) 클래스의 생성자를 설명하겠습니다.

 

protected Potion(int p_i1573_1_, boolean p_i1573_2_, int p_i1573_3_)

int p_i1573_1_,

포션의 id입니다

boolean p_i1573_2_

포션이 해로운 효과인지를 나타냅니다. 해로운 효과면 포션이 약화됩니다.

int p_i1573_3_

포션의 색 입니다. 입자 색을 결정합니다. 0xFFFFFF는 16진법으로, 흰색을 의미합니다.

 

 

5. 아이콘 지정하기

모든 포션은 아이콘을 가지고 있습니다. 포션 효과가 아이콘을 가지는 이유는 인벤토리에서 표시되기 때문입니다. 포션 아이콘 하나는 18*18의 크기를 가지고 있습니다.

 

1) 바닐라 포션 아이콘

 

마인크래프트 내부에 기본적으로 존재하는 포션 아이콘들입니다. 위의 배열을 보시면 Potion 클래스에서 선언된 포션 객체들의 ".setIconIndex(6, 0)" 함수가 이해되실 겁니다.

 

2) 포션 아이콘 만들기

포션 아이콘의 규격을 알았으니 이제 아이콘 하나를 만들어 보겠습니다. 크기는 18*18입니다.

 

3) 포션 아이콘 렌더링

제작한 포션 아이콘을 렌더링하려면 조금의 작업이 더 필요합니다. PotionTutorial 클래스로 가봅시다.

 

    ResourceLocation texture = new ResourceLocation("tutorial","textures/potions/potiontutorial.png");

    

    public PotionTutorial(int id, boolean isBadEffect, int color) {

        super(id, isBadEffect, color);

        setPotionName("potion.timeslow");    }

    

    @Override

    public void renderInventoryEffect(int x, int y, PotionEffect effect, Minecraft mc) {

        mc.getTextureManager().bindTexture(texture);

        drawTexturedRect(x+6, y+7, 16, 16);

    }

    

    public static void drawTexturedRect(int left, int up, int width, int height) {

Tessellator tessellator = Tessellator.instance;

tessellator.startDrawingQuads();

tessellator.addVertexWithUV((double)left, (double)(up + height), 0.0D, 0.0D, 1.0D);

tessellator.addVertexWithUV((double)(left + width), (double)(up + height), 0.0D, 1.0D, 1.0D);

tessellator.addVertexWithUV((double)(left + width), (double)up, 0.0D, 1.0D, 0.0D);

tessellator.addVertexWithUV((double)left, (double)up, 0.0D, 0.0D, 0.0D);

tessellator.draw();

    }

 

위 처럼 ResourceLocation을 지정하고, 인벤토리에서 아이콘을 그리는 함수인 renderInventoryEffect 함수를 오버라이드하면 포션이 그려져야 할 때, 저 함수가 호출됩니다.

 

지정한 ResourceLocation에 방금 만든 파일을 복사하여 옮겨 놓습니다. ResourceLocation의 생성자에 대해 간략히 설명하자면 첫 번째 인자는 Mod ID이고, 두 번째 인자가 상대 경로입니다.

 

 

6. 포션에 효과 설정하기

포션 효과는 정말 다양할 수 있습니다. 그렇기 때문에 능력치를 수정하는 것뿐만 아니라 더 다양한 것을 할 수 있도록 performEffect(EntityLivingBase entity, int level) 함수가 Potion 클래스에 구현되어 있습니다. 우리가 할 것은 이 함수를 오버라이드하여 원하는 효과를 작성하는 것입니다.

 

    @Override

    public void performEffect(EntityLivingBase entity, int level) {

 

    }

 

    @Override

    public boolean isReady(int duration, int level) {

        return true;

    }

 

하나 더 눈여겨 보셔야 할 부분은 isReady 함수입니다. 이 함수가 true를 반환해야만 performEffect 함수가 실행됩니다.

 

 

대상 엔티티(Entity) 근처의 투사체를 느리게 하는 효과를 작성해보겠습니다.

 

    @Override

    public void performEffect(EntityLivingBase entity, int level) {

        double radius = 8;

        double x = entity.posX;

        double y = entity.posY;

        double z = entity.posZ;

        List projectiles = entity.worldObj.getEntitiesWithinAABB(IProjectile.class, entity.boundingBox.expand(radius, radius, radius));

        for (Object i : projectiles) {

            Entity proj = (Entity)i;

            proj.motionX*=0.5;

            proj.motionY*=0.5;

            proj.motionZ*=0.5;

        }

    }

 

저 코드에 대한 설명은 생략하겠습니다. 지금은 포션을 추가하는 예시를 보여주는 것이지, 어떻게 엔티티(Entity)를 검출하고 다루는 지 보는 강의는 아니니까요.

 

 

7. 포션 효과를 엔티티에게 적용시키기

접근 가능한 한 엔티티(Entity)에게 포션 효과를 적용시키는 명령어는 다음과 같습니다.

 

        entity.addPotionEffect(new PotionEffect(Tutorial.potionTutorial.id, 6000, 1, false));

 

PotionEffect의 생성자에 대해 설명하자면 첫 인자는 대상 포션의 id, 두 번째 인자는 포션 효과가 적용될 기간(tick), 그리고 마지막은 신호기(Beacon)에서 비롯된 효과인지를 나타내는 인자입니다.

 

 

위의 코드를 실행시킬 상황을 만들자면 또 한참 복잡하니까, 그것은 여러분의 몫으로 남겨 두겠습니다.

 

 

8. 시험

이제 포션 코드를 실험해보겠습니다. 엔티티(Entity)에게 포션 효과 적용시키는 코드가 없으므로 명령어를 이용하겠습니다. 이 경우 포션 ID를 직접 알아야 하는데, 포션 등록 과정에서 콘솔로 출력하셔도 좋고 아니면 24를 넣어보세요. 바닐라 포션 바로 다음 ID이기 때문에 24번으로 할당 되었을 가능성이 큽니다.

 

 

 

이제 근처에 스켈레톤을 소환해서 투사체가 느려지는 지 확인해보겠습니다.

 

 

동영상이었다면 더욱 효과를 알아보기 쉬웠겠지만 사진도 충분할 것 같습니다. 화살이 오다가 땅으로 향하는 걸 확인할 수 있으실 겁니다.

 

 

 

인벤토리에서도 아이콘이 정상적으로 나타납니다. 시간은 너무 길게 설정해서 저렇게 표시되네요. 이름은 .lang 파일을 만드셔야 정상적으로 표시됩니다.

 

이제 포션 효과를 추가하는데 필요한 모든 것들을 다루어보았습니다.

 

 

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

+ Recent posts