본 강의는 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