Версия(и) Minecraft
1.7.10, 1.12.2
Здравствуйте. В этом туториале речь пойдёт об атрибутах и менеджере данных, которые используются для реализации свойств сущностей, о том как они применяются в игре и продемонстрирую создание собственного свойства для игрока, аналогичного запасу здоровья или опыта. Рассмотренный здесь пример доступен в моём репозитории: 1.7.10, 1.12.2.

IAttribute & DataWatcher/DataManager

Обычно для добавления новых свойств сущностям используется IExtendedEntityProperties (IEEP)(до 1.9) и Capability (1.9+). Однако если нужно добавить только одно или несколько новых свойств, стоит обратить внимание на то как они реализованы в стандартной игре. На атрибутах (IAttribute) и менеджере данных (DataWatcher до 1.9, DataManager с 1.9) строится ванильная система свойств сущностей и у нас есть возможность использовать её для своих нужд. Преимущества - это проще настройки IEEP/Capability и нет необходимости использовать пакетную систему для синхронизации с клиентом.

Атрибуты или IAttribute

IAttribute представляет собой интерфейс, который использует система атрибутов для сущностей. В игре используется реализация RangedAttribute, представляющая значение типа double, ограниченное верхним и нижним значениями.

Атрибуты – это система свойств для мобов и игроков. Определённый атрибут может управлять заданным свойством объекта. Он имеет базовое значение и границы, в которых это значение заключено и может изменяться с помощью модификаторов.

Так как атрибут (реализация IAttribute) лишь представляет значение с минимальным описанием (имя, базовое значение, границы и т.д.), то для его ассоциации с мобом или игроком используется объект ModifiableAttributeInstance (реализующий IAttributeInstance), хранящийся в регистрах атрибутов AttributeMap.

Для добавления атрибута необходимо создать экземпляр RangedAttribute, и зарегистрировать его в AttributeMap в процессе инициализации с помощью метода EntityLivingBase#getAttributeMap()#registerAttribute(IAttribute).

Получить существующий атрибут можно вызвав EntityLivingBase#getAttributeMap()#getAttributeInstance(IAttribute) или EntityLivingBase#getAttributeInstance(IAttribute).

Для модификации аттрибута небходимо создать экземпляр AttributeModifier и применить его используя EntityLivingBase#getAttributeInstance(IAttribute)#applyModifier(AttributeModifier). Удалить модификатор можно с помощью EntityLivingBase#getAttributeInstance(IAttribute)#removeModifier() используя в качестве параметра либо ссылку на добавленный AttributeModifier, либо его UUID (для 1.7 и более ранних версий только ссылку).

Модификаторы сохраняются в NBT автоматически.

DataWatcher

Для версий до 1.9

DataWatcher – это система управления изменяемыми данными для всех сущностей в игре. Он контролирует некоторые параметры, автоматически синхронизируя значения с клиентом. Каждая сущность может иметь до 32 таких параметров.

В качестве отслеживаемой единицы используются примитивные типы (оболочки) byte, short, int, float, String и ItemStack.

Чтобы добавить для сущности новый параметр нужно использовать метод Entity#getDataWatcher()#addObject() и в качестве параметров задать идентификатор и базовое значение параметра. Так как количество параметров ограничено нужно стараться исключить конфликты с существующими параметрами и теми, которые могут добавлять другие модификации (используя ASM трансформеры можно увеличить максимальное количество параметров с 32 до желаемого).

Для получения текущего значения используются соответствующие методы для поддерживаемых типов данных Entity#getDataWatcher()#getWatchableObjectByte(), Entity#getDataWatcher()#getWatchableObjectShort(), Entity#getDataWatcher()#getWatchableObjectString() и т.д., для изменения Entity#getDataWatcher()#updateObject(). Нужно внимательно следить за используемыми типами данных для избежания ошибок.

Значения собственных параметров требуется вручную сохранять в NBT и загружать оттуда. Делать это удобно при срабатывании эвентов PlayerEvent.SaveToFile и PlayerEvent.LoadFromFile (для игрока).

DataManager

Для версий 1.9+.

DataManager – это система управления изменяемыми данными для всех сущностей в игре, пришедшая на смену DataWatcher. Он контролирует такие параметры как имя, здоровье, урон, активные эффекты и т.д, автоматически синхронизируя значения с клиентом. Каждая сущность может иметь до 255 параметров менеджера данных.

В качестве отслеживаемой единицы используются обощённые объекты DataParameter, способные принимать значения примитивов byte, int, float, boolean (их оболочки), значения String, ItemStack, BlockPos, Facing, NBTCompoundTag и многое другое. Кроме того есть возможность добавить собственный тип данных, однако потребуется написать для него свой DataSerializer.

Чтобы добавить для сущности новый параметр нужно создать экземпляр DataParameter, используя EntityDataManager#createKey(Entity.class, DataSerializer) и зарегистрировать во время инициализации через Entity#getDataManager()#register(DataParameter, baseValue).

Для получения текущего значения используется метод Entity#getDataManager()#get(), для изменения Entity#getDataManager()#set().

Значения собственных параметров требуется вручную сохранять в NBT и загружать оттуда. Делать это удобно при срабатывании эвентов PlayerLoggedOutEvent и PlayerLoggedInEvent (для игрока).

1.7.10

Создание нового свойства

Наглядным примером может стать реализация стандартных свойств для сущностей. Например запас здоровья имеет атрибут SharedMonsterAttributes.maxHealth, который определяет его текущий максимальный запас и границы и параметр для DataWatcher с идентификатором 6.

Я покажу как создать аналогичное свойство для игрока на примере «жажды». Жажда будет увеличиваться при беге и ходьбе в присяди, утолить её можно будет поеданием долек арбузов, яблок и питьём воды или зелий. Кроме того у неё будет собственная полоска заполнения над «сытостью».

Атрибут

Начнём с атрибута. Объявление и регистрацию новых атрибутов я разместил в новом классе PropertiesRegistry:
Java:
public class PropertiesRegistry {

    //Жажда
    public static final IAttribute THIRSTY_MAX = new RangedAttribute(AttributesMain.MODID + ".thirsty", 20.0D, 0.0D, 30.0D).setShouldWatch(true);

    @SubscribeEvent
    public void onPlayerConstructing(EntityEvent.EntityConstructing event) {
    
        if (event.entity instanceof EntityPlayer) {       
        
            EntityPlayer player = (EntityPlayer) event.entity;    
                
            //Жажда
            player.getAttributeMap().registerAttribute(this.THIRSTY_MAX);//Регистрация атрибута "Жажда".
        }    
    }
}


Класс будет содержать событие и поэтому требуется его регистрация. Регистрацию я произвожу в CommonProxy в процессе преинициализации:
Java:
public class CommonProxy {

    public void preInit(FMLPreInitializationEvent event) {
    
        //Регистрация классов с событиями.
    
        MinecraftForge.EVENT_BUS.register(new PropertiesRegistry());
    }

    public void init(FMLInitializationEvent event) {}
}


preInit() вызывается из главного класса в методе, содержащем FMLPreInitializationEvent.

Обратите внимание на параметры RangedAttribute, первый параметр - название, второй – базовое значение (можно изменить с помощью модификаторов), аналогично сытости оно равно 20, третий параметр – нижняя граница – 0, четвёртый – верхняя граница – 30. BaseAttribute#setShouldWatch() определяет должен ли атрибут синхронизировать свое значение с клиентом автоматически.

Регистрируется атрибут для игрока при срабатывании события EntityEvent.EntityConstructing.

Всё что касается управления свойством вынесено в отдельный класс ThirstyHandler. Он будет содержать события и его так же требуется зарегистрировать:
Java:
public class ThirstyHandler {
    
    //Возвращает макс. значение жажды.
    public static float getThirstyMax(EntityPlayer player) {    
    
        return (float) player.getEntityAttribute(PropertiesRegistry.THIRSTY_MAX).getAttributeValue();            
    }
}


Статический метод getThirstyMax() возвращает значение атрибута для указанного игрока, которое является текущим максимальным значением запаса жажды.

Параметр для DataWatcher

Атрибут определяющий капу жажды есть, теперь нужно что то, что будет отражать текущее значение. Для этого в PropertiesRegistry объявлен идентификатор для нового параметра DataWatcher, происходит его добавление игроку (тип float):
Java:
public class PropertiesRegistry {

    //Жажда
    public static final IAttribute THIRSTY_MAX = new RangedAttribute(AttributesMain.MODID + ".thirsty", 20.0D, 0.0D, 30.0D).setShouldWatch(true);

    //Идентификатор для нового параметра - значения "Жажда" в DataWatcher.
    public static final int THIRSTY = 20;

    @SubscribeEvent
    public void onPlayerConstructing(EntityEvent.EntityConstructing event) {
    
        if (event.entity instanceof EntityPlayer) {
        
            AttributesMain.logger().info("Player constructing...");
        
            EntityPlayer player = (EntityPlayer) event.entity;   
                
            //Жажда
            player.getAttributeMap().registerAttribute(this.THIRSTY_MAX);//Регистрация атрибута "Жажда".
                
            //Регистрация нового объекта DataWatcher для управления значением "Жажды".
            //Базовое значение равно значению атрибута.
            player.getDataWatcher().addObject(this.THIRSTY, (float) this.THIRSTY_MAX.getDefaultValue());       
        }   
    }
}


Регистрируется параметр при срабатывании эвента EntityEvent.EntityConstructing. Всё остальное размещено в ThirstyHandler:
Java:
public class ThirstyHandler {
    
    //Возвращает макс. значение жажды.
    public static float getThirstyMax(EntityPlayer player) {    
    
        return (float) player.getEntityAttribute(PropertiesRegistry.THIRSTY_MAX).getAttributeValue();            
    }

    //Возвращает текущее значение жажды.
    public static float getThirsty(EntityPlayer player) {
            
        return player.getDataWatcher().getWatchableObjectFloat(PropertiesRegistry.THIRSTY);
    }

    //Устанавливает текущее значение жажды.
    public static void setThirsty(EntityPlayer player, float value) {
    
        player.getDataWatcher().updateObject(PropertiesRegistry.THIRSTY, MathHelper.clamp_float(value, 0.0F, getThirstyMax(player)));
    }

    //Сбрасывает значение жажды.
    public static void refillThirsty(EntityPlayer player) {
    
        player.getDataWatcher().updateObject(PropertiesRegistry.THIRSTY, getThirstyMax(player));
    }

    //Уменьшает жажду на указанную величину.
    public static void decreaseThirsty(EntityPlayer player, float value) {
    
        player.getDataWatcher().updateObject(PropertiesRegistry.THIRSTY, MathHelper.clamp_float(getThirsty(player) + value, 0.0F, getThirstyMax(player)));
    }

    //Увеличивает жажду на указанную величину.
    public static void increaseThirsty(EntityPlayer player, float value) {

        player.getDataWatcher().updateObject(PropertiesRegistry.THIRSTY, MathHelper.clamp_float(getThirsty(player) - value, 0.0F, getThirstyMax(player)));
    }
}


Так как значения параметра требуется сохранять и загружать вручную, используем события PlayerEvent.SaveToFile и PlayerEvent.LoadFromFile соответственно:
Java:
    //Сохранение значения жажды в NBT игрока.
    public static void saveThirstyToNBT(EntityPlayer player) {
    
        player.getEntityData().setFloat(AttributesMain.MODID + ":thirsty", getThirsty(player));
    }

    //Загрузка жажды из NBT.
    public static float loadThirstyFromNBT(EntityPlayer player) {
    
        return player.getEntityData().hasKey(AttributesMain.MODID + ":thirsty") ? player.getEntityData().getFloat(AttributesMain.MODID + ":thirsty") : getThirstyMax(player);
    }

    @SubscribeEvent
    public void onPlayerLoad(PlayerEvent.LoadFromFile event) {
    
        AttributesMain.logger().info("Player loading from file...");
    
        //Загрузка значения жажды из NBT при входе на сервер (срабатывает для физического и логического серверов).
        setThirsty(event.entityPlayer, loadThirstyFromNBT(event.entityPlayer));
    }

    @SubscribeEvent
    public void onPlayerSave(PlayerEvent.SaveToFile event) {
    
        AttributesMain.logger().info("Player saving to file...");
    
        //Сохранение значения жажды в NBT игрока при выходе.
        saveThirstyToNBT(event.entityPlayer);
    }


Дополнительно для синхронизации значения жажды при переходе между мирами используется событие EntityJoinWorldEvent. Тут происходит псевдоизменение значения для принудительного обновления клиентского значения. Это связано с тем, что DataWatcher синхронизирует данные с клиентом, только если они были изменены:
Java:
    @SubscribeEvent
    public void onPlayerJoinWorld(EntityJoinWorldEvent event) {
    
        if (event.entity instanceof EntityPlayer) {
        
            AttributesMain.logger().info("Player join world...");
        
            EntityPlayer player = (EntityPlayer) event.entity;
        
            if (!player.worldObj.isRemote) {
                            
                //Симуляция изменения значения для принудительной
                //синхронизации текущего параметра жажды с клиентом
                //при переходе между мирами.
                if (getThirsty(player) < getThirstyMax(player)) {
                
                    setThirsty(player, getThirsty(player) + 1.0F);
                    setThirsty(player, getThirsty(player) - 1.0F);
                }
            }
        }
    }


Для уменьшения запаса жажды при беге и перемещении в скрытности используется событие LivingUpdateEvent:
Java:
    @SubscribeEvent
    public void onPlayerUpdate(LivingUpdateEvent event) {
    
        if (event.entityLiving instanceof EntityPlayer) {
        
            EntityPlayer player = (EntityPlayer) event.entityLiving;

            if (!player.worldObj.isRemote) {
        
                if (player.isSprinting()) {
                                                        
                    if (player.ticksExisted % 60 == 0) {
                    
                        increaseThirsty(player, 1.0F);//Увеличение жажды при беге каждые 3 сек. на 1 ед.
                    }
                }
            
                else if (player.isSneaking()) {
                
                    if (player.ticksExisted % 120 == 0) {
                    
                        increaseThirsty(player, 1.0F);//Увеличение жажды в присяди каждые 6 сек. на 1 ед.
                    }
                }
            }
        }
    }


Для восстановления при поедании яблок и т.д. используется PlayerUseItemEvent.Tick. Срабатывает он в последний тик употребления пищи или зелий:
Java:
    @SubscribeEvent
    public void onPlayerEat(PlayerUseItemEvent.Tick event) {
    
        if (event.entityLiving instanceof EntityPlayer) {
        
            EntityPlayer player = (EntityPlayer) event.entityLiving;
        
            if (player.getItemInUseCount() == 1) {
                    
                if (event.item.getItem() == Items.potionitem) {
                
                    decreaseThirsty(player, 7.0F);//Уменьшение жажды при выпивании зелья на 3.5 ед.
                }
            
                else if (event.item.getItem() == Items.melon) {
                
                    decreaseThirsty(player, 3.0F);//Уменьшение жажды при съедании дольки арбуза на 1.5 ед.
                }
            
                else if (event.item.getItem() == Items.apple) {
                
                    decreaseThirsty(player, 2.0F);//Уменьшение жажды при съедании яблока на 2 ед.
                }
            
                else if (event.item.getItem() == Items.golden_apple || event.item.getItemDamage() == 1) {
                                    
                    player.addPotionEffect(new PotionEffect(PotionsRegistry.WETTING.getId(), 6000));//Эффект "Насыщение" на 5 минут при съедании зачарованного яблока.
                
                    decreaseThirsty(player, 10.0F);//Уменьшение жажды при съедании зачарованного яблока на 10 ед.
                }
            }
        }
    }


Для отрисовки полоски запаса жажды над «сытостью» используется событие RenderGameOverlayEvent.Post, однако сначала смещается позиция полоски запаса воздуха (так как запас воздуха отображается на «сытостью»):
Java:
    @SubscribeEvent
    @SideOnly(Side.CLIENT)
    public void onRenderOverlayPre(RenderGameOverlayEvent.Pre event) {
    
        if (event.type == ElementType.AIR) {
        
            GL11.glPushMatrix();
            GL11.glTranslatef(0.0F, - 11.0F, 0.0F);//Смещение полоски запаса воздуха на 11 пикселей выше стандартной позиции.
        }
    }

    @SubscribeEvent
    @SideOnly(Side.CLIENT)
    public void onRenderOverlayPost(RenderGameOverlayEvent.Post event) {       
    
        if (event.type == ElementType.AIR) {
        
            GL11.glPopMatrix();
        }
    
        if (event.type == ElementType.TEXT) {
        
            ThirstyBarRenderer.getInstance().renderThirstyBar();//Отрисовка полоски жажды.
        }
    }


Java:
@SideOnly(Side.CLIENT)
public class ThirstyBarRenderer {

    private Minecraft mc = Minecraft.getMinecraft();

    private static final ThirstyBarRenderer INSTANCE = new ThirstyBarRenderer();

    private static final ResourceLocation THIRSTY_ICON = new ResourceLocation(AttributesMain.MODID, "textures/gui/thirsty.png");

    private ThirstyBarRenderer() {}

    public static ThirstyBarRenderer getInstance() {
    
        return INSTANCE;
    }

    public void renderThirstyBar() {
                
        EntityPlayer player = (EntityPlayer) this.mc.thePlayer;

        if (!player.capabilities.isCreativeMode && !(this.mc.currentScreen instanceof GuiGameOver)) {
                                
            ScaledResolution resolution = new ScaledResolution(this.mc, this.mc.displayWidth, this.mc.displayHeight);
        
            int
            i = resolution.getScaledWidth() / 2 + 10,
            j = resolution.getScaledHeight() - 50,
            k,
            iconWidth = 80 / (((int) ThirstyHandler.getThirstyMax(player) / 2));
                
            GL11.glEnable(GL11.GL_BLEND);
            GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F);
        
            this.mc.getTextureManager().bindTexture(this.THIRSTY_ICON);
        
            for (k = 0; k < ((int) (ThirstyHandler.getThirstyMax(player) - ThirstyHandler.getThirsty(player)) / 2); k++) {
            
                Gui.func_146110_a(i + k * iconWidth, j, 20, 0, 9, 10, 29, 10);               
            }
        
            if ((int) ThirstyHandler.getThirsty(player) % 2 != 0) {
            
                Gui.func_146110_a(i + k * iconWidth, j, 10, 0, 9, 10, 29, 10);
            }
        
            for (k = 0; k < (int) ThirstyHandler.getThirsty(player) / 2; k++) {
            
                Gui.func_146110_a(i + 72 - k * iconWidth, j, 0, 0, 9, 10, 29, 10);               
            }             
        
            GL11.glDisable(GL11.GL_BLEND);
        }
    }
}

Значёк жажды:
thirsty.png


Класс ThirstyHandler целиком:
Java:
public class ThirstyHandler {
    
    //Возвращает макс. значение жажды.
    public static float getThirstyMax(EntityPlayer player) {   
    
        return (float) player.getEntityAttribute(PropertiesRegistry.THIRSTY_MAX).getAttributeValue();           
    }

    //Возвращает текущее значение жажды.
    public static float getThirsty(EntityPlayer player) {
            
        return player.getDataWatcher().getWatchableObjectFloat(PropertiesRegistry.THIRSTY);
    }

    //Устанавливает текущее значение жажды.
    public static void setThirsty(EntityPlayer player, float value) {
    
        player.getDataWatcher().updateObject(PropertiesRegistry.THIRSTY, MathHelper.clamp_float(value, 0.0F, getThirstyMax(player)));
    }

    //Сбрасывает значение жажды.
    public static void refillThirsty(EntityPlayer player) {
    
        player.getDataWatcher().updateObject(PropertiesRegistry.THIRSTY, getThirstyMax(player));
    }

    //Уменьшает жажду на указанную величину.
    public static void decreaseThirsty(EntityPlayer player, float value) {
    
        player.getDataWatcher().updateObject(PropertiesRegistry.THIRSTY, MathHelper.clamp_float(getThirsty(player) + value, 0.0F, getThirstyMax(player)));
    }

    //Увеличивает жажду на указанную величину.
    public static void increaseThirsty(EntityPlayer player, float value) {

        player.getDataWatcher().updateObject(PropertiesRegistry.THIRSTY, MathHelper.clamp_float(getThirsty(player) - value, 0.0F, getThirstyMax(player)));
    }

    //Сохранение значения жажды в NBT игрока.
    public static void saveThirstyToNBT(EntityPlayer player) {
    
        player.getEntityData().setFloat(AttributesMain.MODID + ":thirsty", getThirsty(player));
    }

    //Загрузка жажды из NBT.
    public static float loadThirstyFromNBT(EntityPlayer player) {
    
        return player.getEntityData().hasKey(AttributesMain.MODID + ":thirsty") ? player.getEntityData().getFloat(AttributesMain.MODID + ":thirsty") : getThirstyMax(player);
    }

    @SubscribeEvent
    public void onPlayerJoinWorld(EntityJoinWorldEvent event) {
    
        if (event.entity instanceof EntityPlayer) {
        
            AttributesMain.logger().info("Player join world...");
        
            EntityPlayer player = (EntityPlayer) event.entity;
        
            if (!player.worldObj.isRemote) {
                            
                //Симуляция изменения значения для принудительной
                //синхронизации текущего параметра жажды с клиентом
                //при переходе между мирами.
                if (getThirsty(player) < getThirstyMax(player)) {
                
                    setThirsty(player, getThirsty(player) + 1.0F);
                    setThirsty(player, getThirsty(player) - 1.0F);
                }
            }
        }
    }

    @SubscribeEvent
    public void onPlayerUpdate(LivingUpdateEvent event) {
    
        if (event.entityLiving instanceof EntityPlayer) {
        
            EntityPlayer player = (EntityPlayer) event.entityLiving;

            if (!player.worldObj.isRemote) {
        
                if (player.isSprinting()) {
                                                        
                    if (player.ticksExisted % 60 == 0) {
                    
                        increaseThirsty(player, 1.0F);//Увеличение жажды при беге каждые 3 сек. на 1 ед.
                    }
                }
            
                else if (player.isSneaking()) {
                
                    if (player.ticksExisted % 120 == 0) {
                    
                        increaseThirsty(player, 1.0F);//Увеличение жажды в присяди каждые 6 сек. на 1 ед.
                    }
                }
            }
        }
    }

    @SubscribeEvent
    public void onPlayerLoad(PlayerEvent.LoadFromFile event) {
    
        AttributesMain.logger().info("Player loading from file...");
    
        //Загрузка значения жажды из NBT при входе на сервер (срабатывает для физического и логического серверов).
        setThirsty(event.entityPlayer, loadThirstyFromNBT(event.entityPlayer));
    }

    @SubscribeEvent
    public void onPlayerSave(PlayerEvent.SaveToFile event) {
    
        AttributesMain.logger().info("Player saving to file...");
    
        //Сохранение значения жажды в NBT игрока при выходе.
        saveThirstyToNBT(event.entityPlayer);
    }

    @SubscribeEvent
    public void onPlayerEat(PlayerUseItemEvent.Tick event) {
    
        if (event.entityLiving instanceof EntityPlayer) {
        
            EntityPlayer player = (EntityPlayer) event.entityLiving;
        
            if (player.getItemInUseCount() == 1) {
                    
                if (event.item.getItem() == Items.potionitem) {
                
                    decreaseThirsty(player, 7.0F);//Уменьшение жажды при выпивании зелья на 3.5 ед.
                }
            
                else if (event.item.getItem() == Items.melon) {
                
                    decreaseThirsty(player, 3.0F);//Уменьшение жажды при съедании дольки арбуза на 1.5 ед.
                }
            
                else if (event.item.getItem() == Items.apple) {
                
                    decreaseThirsty(player, 2.0F);//Уменьшение жажды при съедании яблока на 2 ед.
                }
            
                else if (event.item.getItem() == Items.golden_apple || event.item.getItemDamage() == 1) {
                                    
                    player.addPotionEffect(new PotionEffect(PotionsRegistry.WETTING.getId(), 6000));//Эффект "Насыщение" на 5 минут при съедании зачарованного яблока.
                
                    decreaseThirsty(player, 10.0F);//Уменьшение жажды при съедании зачарованного яблока на 10 ед.
                }
            }
        }
    }

    @SubscribeEvent
    @SideOnly(Side.CLIENT)
    public void onRenderOverlayPre(RenderGameOverlayEvent.Pre event) {
    
        if (event.type == ElementType.AIR) {
        
            GL11.glPushMatrix();
            GL11.glTranslatef(0.0F, - 11.0F, 0.0F);//Смещение полоски запаса воздуха на 11 пикселей выше стандартной позиции.
        }
    }

    @SubscribeEvent
    @SideOnly(Side.CLIENT)
    public void onRenderOverlayPost(RenderGameOverlayEvent.Post event) {       
    
        if (event.type == ElementType.AIR) {
        
            GL11.glPopMatrix();
        }
    
        if (event.type == ElementType.TEXT) {
        
            ThirstyBarRenderer.getInstance().renderThirstyBar();//Отрисовка полоски жажды.
        }
    }
}

Результат:
a1710.png

Значение атрибута можно изменить в его пределах (от 0 до 30) с помощью модификаторов. Принцип не отличается от добавления ванильным атрибутам. Пример модификатора для этого свойства доступен на GitHub в исходниках (модификатор добавляется эффектом зелья при съедании зачарованного яблока).
a_modified1710.png

1.12.2

Создание нового свойства

Наглядным примером может стать реализация стандартных свойств для сущностей. Например запас здоровья имеет атрибут SharedMonsterAttributes#MAX_HEALTH, который определяет его текущий максимальный запас и границы и параметр для менеджера данных EntityLivingBase#HEALTH, отвечающий за текущее значение.

Я покажу как создать аналогичное свойство для игрока на примере «жажды». Жажда будет увеличиваться при беге и ходьбе в присяди, утолить её можно будет поеданием долек арбузов, яблок и питьём воды или зелий. Кроме того у неё будет собственная полоска заполнения над «сытостью».

Атрибут

Начнём с атрибута. Объявление и регистрацию новых атрибутов я разместил в новом классе PropertiesRegistry:
Java:
@Mod.EventBusSubscriber(modid = AttributesMain.MODID)
public class PropertiesRegistry {

    public static final IAttribute THIRSTY_MAX = new RangedAttribute(null, AttributesMain.MODID + ".thirsty", 20.0D, 0.0D, 30.0D).setShouldWatch(true);

    @SubscribeEvent
    public static void onPlayerConstructing(EntityEvent.EntityConstructing event) {
   
        if (event.getEntity() instanceof EntityPlayer) {
       
            EntityPlayer player = (EntityPlayer) event.getEntity();  
               
            player.getAttributeMap().registerAttribute(THIRSTY_MAX);//Регистрация атрибута "Жажда".
        }  
    }
}


Класс будет содержать событие и поэтому помечен аннотацией @Mod.EventBusSubscriber(modid). Указание modid требуется для того что бы загрузчик форджа загружал такие классы в порядке загрузки модов, не смешивая их.

Обратите внимание на параметры RangedAttribute, первый обозначает родительский атрибут и так как его нет, он null, второй параметр название атрибута, третий – базовое значение (можно изменить с помощью модификаторов), аналогично сытости оно равно 20, четвёртый параметр – нижняя граница – 0, пятый – верхняя граница – 30. BaseAttribute#setShouldWatch() определяет должен ли атрибут синхронизировать свое значение с клиентом автоматически.

Регистрируется атрибут для игрока при срабатывании события EntityEvent.EntityConstructing.

Всё что касается управления свойством вынесено в отдельный класс ThirstyHandler:
Java:
@Mod.EventBusSubscriber(modid = AttributesMain.MODID)
public class ThirstyHandler {
   
    //Возвращает макс. значение жажды.
    public static float getThirstyMax(EntityPlayer player) {  
   
        return (float) player.getEntityAttribute(PropertiesRegistry.THIRSTY_MAX).getAttributeValue();          
    }
}


Статический метод getThirstyMax() возвращает значение атрибута для указанного игрока, которое является текущим максимальным значением запаса жажды.

Параметр для DataManager

Атрибут определяющий капу жажды есть, теперь нужно что то, что будет отражать текущее значение. Для этого в PropertiesRegistry создан новый объект DataParameter типа float для игрока:
Java:
@Mod.EventBusSubscriber(modid = AttributesMain.MODID)
public class PropertiesRegistry {

    public static final IAttribute THIRSTY_MAX = new RangedAttribute(null, AttributesMain.MODID + ".thirsty", 20.0D, 0.0D, 30.0D).setShouldWatch(true);

    public static final DataParameter<Float> THIRSTY = EntityDataManager.<Float>createKey(EntityPlayer.class, DataSerializers.FLOAT);

    @SubscribeEvent
    public static void onPlayerConstructing(EntityEvent.EntityConstructing event) {
   
        if (event.getEntity() instanceof EntityPlayer) {
       
            EntityPlayer player = (EntityPlayer) event.getEntity();  
               
            player.getAttributeMap().registerAttribute(THIRSTY_MAX);//Регистрация атрибута "Жажда".
               
            //Регистрация нового объекта DataManager для управления значением "Жажды".
            //Базовое значение равно значению атрибута.
            player.getDataManager().register(THIRSTY, (float) THIRSTY_MAX.getDefaultValue());      
        }  
    }
}


Регистрируется параметр при срабатывании события EntityEvent.EntityConstructing. Всё остальное размещено в ThirstyHandler:
Java:
@Mod.EventBusSubscriber(modid = AttributesMain.MODID)
public class ThirstyHandler {
   
    //Возвращает макс. значение жажды.
    public static float getThirstyMax(EntityPlayer player) {     
     
        return (float) player.getEntityAttribute(PropertiesRegistry.THIRSTY_MAX).getAttributeValue();             
    }
 
    //Возвращает текущее значение жажды.
    public static float getThirsty(EntityPlayer player) {
             
        return player.getDataManager().get(PropertiesRegistry.THIRSTY);
    }
 
    //Устанавливает текущее значение жажды.
    public static void setThirsty(EntityPlayer player, float value) { 
     
        player.getDataManager().set(PropertiesRegistry.THIRSTY, MathHelper.clamp(value, 0.0F, getThirstyMax(player)));
    }
 
    //Сбрасывает значение жажды.
    public static void refillThirsty(EntityPlayer player) { 
     
        player.getDataManager().set(PropertiesRegistry.THIRSTY, getThirstyMax(player));
    }
 
    //Уменьшает жажду на указанную величину.
    public static void decreaseThirsty(EntityPlayer player, float value) {
     
        player.getDataManager().set(PropertiesRegistry.THIRSTY, MathHelper.clamp(getThirsty(player) + value, 0.0F, getThirstyMax(player))); 
    }
 
    //Увеличивает жажду на указанную величину.
    public static void increaseThirsty(EntityPlayer player, float value) {

        player.getDataManager().set(PropertiesRegistry.THIRSTY, MathHelper.clamp(getThirsty(player) - value, 0.0F, getThirstyMax(player)));
    }
}


Так как значения параметра требуется сохранять и загружать вручную, используем события PlayerLoggedOutEvent и PlayerLoggedInEvent соответственно:
Java:
    //Сохранение значения жажды в NBT игрока.
    //Желательно сохранять значение при каждом изменении, иначе в случае аварийного завершения
    //игры (к примеру комбинацией Alt+F4) данные не будут сохранены (PlayerLoggedOutEvent не сработает).
    private static void saveThirstyToNBT(EntityPlayer player) {
   
        player.getEntityData().setFloat(AttributesMain.MODID + ":thirsty", getThirsty(player));
    }

    //Загрузка жажды из NBT.
    private static float loadThirstyFromNBT(EntityPlayer player) {
   
        return player.getEntityData().hasKey(AttributesMain.MODID + ":thirsty") ? player.getEntityData().getFloat(AttributesMain.MODID + ":thirsty") : getThirstyMax(player);
    }

    @SubscribeEvent
    public static void onPlayerLogIn(PlayerLoggedInEvent event) {
                       
        setThirsty(event.player, loadThirstyFromNBT(event.player));//Загрузка значения жажды из NBT при входе на сервер (срабатывает для физического и логического серверов).
    }

    @SubscribeEvent
    public static void onPlayerLogOut(PlayerLoggedOutEvent event) {
                           
        saveThirstyToNBT(event.player);//Сохранение значения жажды в NBT игрока при выходе.
    }


Для уменьшения запаса жажды при беге и перемещении в скрытности используется событие LivingUpdateEvent:
Java:
    @SubscribeEvent
    public static void onPlayerUpdate(LivingUpdateEvent event) {
   
        if (event.getEntityLiving() instanceof EntityPlayer) {
       
            EntityPlayer player = (EntityPlayer) event.getEntityLiving();

            if (!player.world.isRemote) {
       
                if (player.isSprinting()) {
                                                       
                    if (player.ticksExisted % 60 == 0) {
                   
                        increaseThirsty(player, 1.0F);//Увеличение жажды при беге каждые 3 сек. на 1 ед.
                    }
                }
           
                else if (player.isSneaking()) {
               
                    if (player.ticksExisted % 120 == 0) {
                   
                        increaseThirsty(player, 1.0F);//Увеличение жажды в присяди каждые 6 сек. на 1 ед.
                    }
                }
            }
        }
    }


Для восстановления при поедании яблок и т.д. используется LivingUseItemEvent.Tick. Срабатывает он в последний тик употребления пищи или зелий:
Java:
    @SubscribeEvent
    public static void onPlayerEat(LivingEntityUseItemEvent.Tick event) {
   
        if (event.getEntityLiving() instanceof EntityPlayer) {
       
            EntityPlayer player = (EntityPlayer) event.getEntityLiving();
       
            if (player.getItemInUseCount() == 1) {
                   
                if (event.getItem().getItem() == Items.POTIONITEM) {
               
                    decreaseThirsty(player, 7.0F);//Уменьшение жажды при выпивании зелья на 3.5 ед.
                }
           
                else if (event.getItem().getItem() == Items.MELON) {
               
                    decreaseThirsty(player, 3.0F);//Уменьшение жажды при съедании дольки арбуза на 1.5 ед.
                }
           
                else if (event.getItem().getItem() == Items.APPLE) {
               
                    decreaseThirsty(player, 2.0F);//Уменьшение жажды при съедании яблока на 2 ед.
                }
            }
        }
    }


Для отрисовки полоски запаса жажды над «сытостью» используется событие RenderGameOverlayEvent.Post, однако сначала смещается позиция полоски запаса воздуха (так как запас воздуха отображается на «сытостью»):
Java:
    @SubscribeEvent
    @SideOnly(Side.CLIENT)
    public static void onRenderOverlayPre(RenderGameOverlayEvent.Pre event) {
   
        if (event.getType() == ElementType.AIR) {
       
            GlStateManager.pushMatrix();
            GlStateManager.translate(0.0F, - 11.0F, 0.0F);//Смещение полоски запаса воздуха на 11 пикселей выше стандартной позиции.
        }
    }

    @SubscribeEvent
    @SideOnly(Side.CLIENT)
    public static void onRenderOverlayPost(RenderGameOverlayEvent.Post event) {      
   
        if (event.getType() == ElementType.AIR) {
       
            GlStateManager.popMatrix();
        }
   
        if (event.getType() == ElementType.TEXT) {
       
            ThirstyBarRenderer.getInstance().renderThirstyBar();//Отрисовка полоски жажды.
        }
    }


Java:
@SideOnly(Side.CLIENT)
public class ThirstyBarRenderer {

    private Minecraft mc = Minecraft.getMinecraft();

    private static final ThirstyBarRenderer INSTANCE = new ThirstyBarRenderer();

    private static final ResourceLocation THIRSTY_ICON = new ResourceLocation(AttributesMain.MODID, "textures/gui/thirsty.png");

    private ThirstyBarRenderer() {}

    public static ThirstyBarRenderer getInstance() {
   
        return INSTANCE;
    }

    public void renderThirstyBar() {
   
        if (this.mc.getRenderViewEntity() instanceof EntityPlayer) {
       
            EntityPlayer player = (EntityPlayer) this.mc.getRenderViewEntity();

            if (!player.capabilities.isCreativeMode && !(this.mc.currentScreen instanceof GuiGameOver)) {
                                   
                ScaledResolution resolution = new ScaledResolution(this.mc);
           
                int
                i = resolution.getScaledWidth() / 2 + 10,
                j = resolution.getScaledHeight() - 50,
                k,
                iconWidth = 80 / (((int) ThirstyHandler.getThirstyMax(player) / 2));
                                   
                GlStateManager.disableRescaleNormal();        
                GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F);
                GlStateManager.enableBlend();
               
                this.mc.getTextureManager().bindTexture(this.THIRSTY_ICON);
           
                for (k = 0; k < ((int) (ThirstyHandler.getThirstyMax(player) - ThirstyHandler.getThirsty(player)) / 2); k++) {
               
                    this.drawCustomSizedTexturedRect(i + k * iconWidth, j, 20, 0, 9, 10, 29, 10);              
                }
           
                if ((int) ThirstyHandler.getThirsty(player) % 2 != 0) {
               
                    this.drawCustomSizedTexturedRect(i + k * iconWidth, j, 10, 0, 9, 10, 29, 10);
                }
           
                for (k = 0; k < (int) ThirstyHandler.getThirsty(player) / 2; k++) {
               
                    this.drawCustomSizedTexturedRect(i + 72 - k * iconWidth, j, 0, 0, 9, 10, 29, 10);
                   
                }            
           
                GlStateManager.disableBlend();
            }
        }  
    }

    private static void drawCustomSizedTexturedRect(int x, int y, int u, int v, int width, int height, int textureWidth, int textureHeight) {
   
        float f = 1.0F / textureWidth;
        float f1 = 1.0F / textureHeight;
   
        Tessellator tessellator = Tessellator.getInstance();
        BufferBuilder bufferbuilder = tessellator.getBuffer();  
   
        bufferbuilder.begin(7, DefaultVertexFormats.POSITION_TEX);
   
        bufferbuilder.pos((double) x, (double) (y + height), 0.0D).tex((double) (u * f), (double) ((v + (float) height) * f1)).endVertex();
        bufferbuilder.pos((double) (x + width), (double) (y + height), 0.0D).tex((double) ((u + (float) width) * f), (double) ((v + (float) height) * f1)).endVertex();
        bufferbuilder.pos((double) (x + width), (double) y, 0.0D).tex((double) ((u + (float) width) * f), (double) (v * f1)).endVertex();
        bufferbuilder.pos((double) x, (double) y, 0.0D).tex((double) (u * f), (double) (v * f1)).endVertex();
   
        tessellator.draw();
    }
}

Значёк жажды:
thirsty.png


Класс ThirstyHandler целиком:
Java:
@Mod.EventBusSubscriber(modid = AttributesMain.MODID)
public class ThirstyHandler {
     
    //Возвращает макс. значение жажды.
    public static float getThirstyMax(EntityPlayer player) {    
     
        return (float) player.getEntityAttribute(PropertiesRegistry.THIRSTY_MAX).getAttributeValue();            
    }
 
    //Возвращает текущее значение жажды.
    public static float getThirsty(EntityPlayer player) {
             
        return player.getDataManager().get(PropertiesRegistry.THIRSTY);
    }
 
    //Устанавливает текущее значение жажды.
    public static void setThirsty(EntityPlayer player, float value) {
     
        player.getDataManager().set(PropertiesRegistry.THIRSTY, MathHelper.clamp(value, 0.0F, getThirstyMax(player)));
    }
 
    //Сбрасывает значение жажды.
    public static void refillThirsty(EntityPlayer player) {
     
        player.getDataManager().set(PropertiesRegistry.THIRSTY, getThirstyMax(player));
    }
 
    //Уменьшает жажду на указанную величину.
    public static void decreaseThirsty(EntityPlayer player, float value) {
     
        player.getDataManager().set(PropertiesRegistry.THIRSTY, MathHelper.clamp(getThirsty(player) + value, 0.0F, getThirstyMax(player)));
    }
 
    //Увеличивает жажду на указанную величину.
    public static void increaseThirsty(EntityPlayer player, float value) {

        player.getDataManager().set(PropertiesRegistry.THIRSTY, MathHelper.clamp(getThirsty(player) - value, 0.0F, getThirstyMax(player)));
    }
 
    //Сохранение значения жажды в NBT игрока.
    //Желательно сохранять значение при каждом изменении, иначе в случае аварийного завершения
    //игры (к примеру комбинацией Alt+F4) данные не будут сохранены (PlayerLoggedOutEvent не сработает).
    private static void saveThirstyToNBT(EntityPlayer player) {
     
        player.getEntityData().setFloat(AttributesMain.MODID + ":thirsty", getThirsty(player));
    }
 
    //Загрузка жажды из NBT.
    private static float loadThirstyFromNBT(EntityPlayer player) {
     
        return player.getEntityData().hasKey(AttributesMain.MODID + ":thirsty") ? player.getEntityData().getFloat(AttributesMain.MODID + ":thirsty") : getThirstyMax(player);
    }
 
    @SubscribeEvent
    public static void onPlayerLogIn(PlayerLoggedInEvent event) {
     
        AttributesMain.logger().info("Player logging in...");
                         
        //Загрузка значения жажды из NBT при входе на сервер (срабатывает для физического и логического серверов).
        setThirsty(event.player, loadThirstyFromNBT(event.player));
    }
 
    @SubscribeEvent
    public static void onPlayerLogOut(PlayerLoggedOutEvent event) {
     
        AttributesMain.logger().info("Player logging out...");
     
        //Сохранение значения жажды в NBT игрока при выходе.
        saveThirstyToNBT(event.player);
    }
 
    @SubscribeEvent
    public static void onPlayerUpdate(LivingUpdateEvent event) {
     
        if (event.getEntityLiving() instanceof EntityPlayer) {
         
            EntityPlayer player = (EntityPlayer) event.getEntityLiving();

            if (!player.world.isRemote) {
         
                if (player.isSprinting()) {
                                                         
                    if (player.ticksExisted % 60 == 0) {
                     
                        increaseThirsty(player, 1.0F);//Увеличение жажды при беге каждые 3 сек. на 1 ед.
                    }
                }
             
                else if (player.isSneaking()) {
                 
                    if (player.ticksExisted % 120 == 0) {
                     
                        increaseThirsty(player, 1.0F);//Увеличение жажды в присяди каждые 6 сек. на 1 ед.
                    }
                }
            }
        }
    }
 
    @SubscribeEvent
    public static void onPlayerEat(LivingEntityUseItemEvent.Tick event) {
     
        if (event.getEntityLiving() instanceof EntityPlayer) {
         
            EntityPlayer player = (EntityPlayer) event.getEntityLiving();
         
            if (player.getItemInUseCount() == 1) {
                     
                if (event.getItem().getItem() == Items.POTIONITEM) {
                 
                    decreaseThirsty(player, 7.0F);//Уменьшение жажды при выпивании зелья на 3.5 ед.
                }
             
                else if (event.getItem().getItem() == Items.MELON) {
                 
                    decreaseThirsty(player, 3.0F);//Уменьшение жажды при съедании дольки арбуза на 1.5 ед.
                }
             
                else if (event.getItem().getItem() == Items.APPLE) {
                 
                    decreaseThirsty(player, 2.0F);//Уменьшение жажды при съедании яблока на 2 ед.
                }
             
                else if (event.getItem().getItem() == Items.GOLDEN_APPLE || event.getItem().getMetadata() == 1) {
                                     
                    player.addPotionEffect(new PotionEffect(PotionsRegistry.WETTING, 6000));//Эффект "Насыщение" на 5 минут при съедании зачарованного яблока.
                 
                    decreaseThirsty(player, 10.0F);//Уменьшение жажды при съедании зачарованного яблока на 10 ед.
                }
            }
        }
    }
 
    @SubscribeEvent
    public static void onLivingDeath(LivingDeathEvent event) {
     
        if (event.getEntityLiving() instanceof EntityPlayer) {
         
            if (event.getEntityLiving().isPotionActive(PotionsRegistry.WETTING)) {
                                 
                //Требуется для синхронизации максимального значения с клиентом.
                event.getEntityLiving().getActivePotionEffect(PotionsRegistry.WETTING).getPotion().removeAttributesModifiersFromEntity(event.getEntityLiving(), event.getEntityLiving().getAttributeMap(), 0);;    
            }
        }
    }
 
    @SubscribeEvent
    @SideOnly(Side.CLIENT)
    public static void onRenderOverlayPre(RenderGameOverlayEvent.Pre event) {
     
        if (event.getType() == ElementType.AIR) {
         
            GlStateManager.pushMatrix();
            GlStateManager.translate(0.0F, - 11.0F, 0.0F);//Смещение полоски запаса воздуха на 11 пикселей выше стандартной позиции.
        }
    }
 
    @SubscribeEvent
    @SideOnly(Side.CLIENT)
    public static void onRenderOverlayPost(RenderGameOverlayEvent.Post event) {        
     
        if (event.getType() == ElementType.AIR) {
         
            GlStateManager.popMatrix();
        }
     
        if (event.getType() == ElementType.TEXT) {
         
            ThirstyBarRenderer.getInstance().renderThirstyBar();//Отрисовка полоски жажды.
        }
    }
}

Результат:
thirsty_bar.png

Значение атрибута можно изменить в его пределах (от 0 до 30) с помощью модификаторов. Принцип не отличается от добавления ванильным атрибутам. Пример модификатора для этого свойства доступен на GitHub в исходниках (модификатор добавляется эффектом зелья при съедании зачарованного яблока).
thirsty_modified.png

Заключение

На этом всё. Вот так можно добавлять новые свойства игроку (и мобам) стандартными средствами.
Можете так же ознакомится с примером создания свойства «запас монет» в моём репозитории.

Оцените статью, если вам понравилось, если есть что добавить - пишите в обсуждении. Спасибо за внимание.

Автор
AustereTony
Просмотры
4,615
Первый выпуск
Обновление
Оценка
5.00 звёзд 4 оценок

Другие ресурсы пользователя AustereTony

Последние обновления

  1. Обновление #1

    Исправлены некоторые недочёты, правки терминов, перезалив обновлённого кода в репозиторий...

Последние рецензии

Отлично - за отличное описание.
Превосходная инструкция по добавлению игроку новых характеристик. С её помощью я смог начать писать мод, в основе которого лежат целых 5 дополнительных характеристик игрока, и справился за пару часов. Было искушение поставить 4 а не 5, т.к. ничего не было сказано о прблемах, возникающих если классы разложены по папкам - но т.к. я не знаю, пробовал ли автор сделать так же, и не уверен что это не была моя ошибка, то не стану снижать оценку, ставлю все 5 из 5, именно благодаря этому туториалу разработка моего мода сдвинулась с мёртвой точки и я не забил на неё навсегда. ОГРОМНОЕ спасибо!!!
// Прочитал под 1.7.10.

Настолько понятно и хорошо написано, что захотелось самому написать (опять же) жажду или ещё чего.
Сверху