Attributes describe the preset yet fluctuating properties of an entity. Every living entity shares a few attributes, such as movement speed, maximum health, and attack damage, and some entities have attributes specific to their type, like a player entity's attack speed.
In this guide, we will create a Jump Boost attribute that controls the jump height of an entity.
To create an attribute, we'll construct a ClampedEntityAttribute. This extends the base EntityAttribute and allows min and max values for its modifiers, which is useful if there's a certain range of values that make sense. All vanilla attributes are clamped.
Additionally, all numerical values in the constructor are doubles. This doesn't make sense for some attributes, but as long as you have a whole number, it will display as a regular integer.
The constructor takes:
From AttributesExample.java:
// ...
public static final EntityAttribute GENERIC_JUMP_BOOST = new ClampedEntityAttribute("attribute.name.generic_jump_boost", 1.0, 0.0, 2.0).setTracked(true);
// ...
The generic preceding the attribute name is a convention meaning 'applying to multiple living entities (and players).' Specific attributes get their own prefixes, such as horse in horse_jump_strength.
You might notice the setTracked call; this ensures that the value of this attribute will be synchronized between the server and client. Since we're dealing with player movement, it's important to have this set to true, which is what you'll want in most cases regardless.
At this point, we might want to add a language entry:
{
"attribute.name.generic_jump_boost": "Jump Boost"
}
With our EntityAttribute instance created, we can go ahead and register it.
From AttributesExample.java:
// ...
Registry.register(Registry.ATTRIBUTE, new Identifier("example", "generic.jump_boost"), GENERIC_JUMP_BOOST);
// ...
This identifier doesn't follow normal conventions, but it's what the vanilla attributes use.
The next step is letting certain entities know that they accept our new attribute. Let's walk through how entities do this in the first place.
DefaultAttributeContainer.Builder. Here's what the one in LivingEntity looks like:public static DefaultAttributeContainer.Builder createLivingAttributes() {
return DefaultAttributeContainer.builder()
.add(EntityAttributes.GENERIC_MAX_HEALTH)
.add(EntityAttributes.GENERIC_KNOCKBACK_RESISTANCE)
.add(EntityAttributes.GENERIC_MOVEMENT_SPEED)
.add(EntityAttributes.GENERIC_ARMOR)
.add(EntityAttributes.GENERIC_ARMOR_TOUGHNESS);
}
This builder is still mutable at this point, so other entities call LivingEntity.createLivingAttributes and build off of it when defining their own accepted attributes.
DefaultAttributeRegistry class when creating its namesake map (DEFAULT_ATTRIBUTE_REGISTRY). This maps entity types to their corresponding DefaultAttributeContainers, now built and immutable. This map is immutable and private, but the class has wrappers for get and containsKey.When creating your own entity classes, you can use
FabricDefaultAttributeRegistry.registerto take care of this without your own mixins.
During play, the LivingEntity constructor initializes the attributes field as a new AttributeContainer instance based on the result of the getter in DefaultAttributeRegistry mentioned prior. This is the value returned in the LivingEntity#getAttributes getter.
When using an attribute's value stored in an entity, you need to retrieve its corresponding EntityAttributeInstance; we'll go over this in more detail later. For now, all you should know is that if the entity doesn't have an attribute in its attributes field, it won't have that attribute instance, and you won't be able to apply a modifier.
Each entity that does something different with attributes has their own attribute creation method, which you can find in DefaultAttributeRegistry. Since we want our attribute on all entities, we'll mix into the LivingEntity.createLivingAttributes method shown in the previous section.
Remember, at this point, the builder is still mutable - and a reference - so our mixin is very straightforward.
From LivingEntityMixin.java:
// ...
@Inject(method = "createLivingAttributes", at = @At(value = "RETURN"))
private static void addCustomAttribute(CallbackInfoReturnable<DefaultAttributeContainer.Builder> cir) {
cir.getReturnValue().add(AttributesExample.GENERIC_JUMP_BOOST);
}
// ...
We've registered our attribute and let all living entities know that they have it. The next step is to actually use it somewhere: in our case, to modify jump height.
LivingEntity#getAttributes gets the possible attributes that the entity knows it has, but not the actual values. Instead, each attribute has its own 'instance' on an entity known as an EntityAttributeInstance, which has all the logic for controlling attribute values. You can use the LivingEntity#getAttributeInstance method for getting one of these instances.
We'll need another mixin into LivingEntity, this time for the getJumpVelocity method. Since our initial fallback value is 1.0, we can feel free to just multiply the end result by the attribute value.
From LivingEntityMixin.java:
// ...
@Shadow @Nullable public abstract EntityAttributeInstance getAttributeInstance(EntityAttribute attribute);
@Inject(method = "getJumpVelocity", at = @At(value = "RETURN"), cancellable = true)
private void applyJumpBoost(CallbackInfoReturnable<Float> cir) {
EntityAttributeInstance instance = getAttributeInstance(AttributesExample.GENERIC_JUMP_BOOST);
cir.setReturnValue(cir.getReturnValue() * (float) instance.getValue());
}
// ...
Now that we've given our attribute functionality, it's time to modify its value in some way. The next section will discuss three methods you might use to do so, but they all have one thing in common: EntityAttributeModifier.
Attribute modifiers are completely disconnected from any particular attribute and are only concerned with numbers, operations, and identities. The constructor takes:
Identifier, a UUID that you need to hard-code or generate randomly if you don't care about identifying the modifier afterwards
LivingEntity#getEquipment.StatusEffects link to attributes, notably showing up in potion tooltipsEntityAttributeModifier.Operation that specifies how the modifier should be applied and by what magnitudeFrom AttributesExample.java:
// ...
public static final UUID SOME_MODIFIER_ID = UUID.fromString("de17612a-adec-11ec-b909-0242ac120002");
public static final EntityAttributeModifier SOME_MODIFIER = new EntityAttributeModifier(
SOME_MODIFIER_ID,
"Jump stick modifier",
0.5,
EntityAttributeModifier.Operation.ADDITION
);
// ...
It's important to note that all attribute modifiers are calculated in order based on type, with the order being ADDITION, MULTIPLY_BASE, then MULTIPLY_TOTAL. You can see the exact behavior in EntityAttributeInstance#computeValue.
The final step is to apply this modifier. These examples use the SOME_MODIFIER field created in the previous section.
It's as simple as getting the attribute instance and calling addTemporaryModifier (or addPersistentModifier if you want it to be serialized in the instance's NBT).
From JumpBoostCommand.java:
// ...
EntityAttributeInstance instance = player.getAttributeInstance(AttributesExample.GENERIC_JUMP_BOOST);
instance.addTemporaryModifier(AttributesExample.SOME_MODIFIER);
// ...
This is also the most appropriate case where you would remove the modifier at some other point in time. You can either use the UUID associated with the modifier or the modifier itself.
From JumpBoostCommand.java:
// ...
instance.removeModifier(AttributesExample.SOME_MODIFIER_ID);
// ...
Upon equipping an item, the game checks if it has any attribute modifiers marked in the slot it was equipped in. If so, it applies those modifiers to the entity. There are two ways items can achieve this: through ItemStack NBT or custom Item instance behavior.
You can use the ItemStack#addAttributeModifier method to apply a modifier via NBT.
From JumpBoostCommand.java:
// ...
ItemStack stack = new ItemStack(Items.STICK);
stack.addAttributeModifier(
AttributesExample.GENERIC_JUMP_BOOST,
AttributesExample.SOME_MODIFIER,
EquipmentSlot.MAINHAND
);
// ...
The Item class contains a getAttributeModifiers method which returns a multimap of modifiers to apply when the item is equipped in the specified slot. We can override this in our own item class.
From JumpStick.java:
package org.quiltmc.wiki.entity_attributes;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.Multimap;
import net.minecraft.entity.EquipmentSlot;
import net.minecraft.entity.attribute.EntityAttribute;
import net.minecraft.entity.attribute.EntityAttributeModifier;
import net.minecraft.item.Item;
public class JumpStick extends Item {
public JumpStick(Settings settings) {
super(settings);
}
@Override
public Multimap<EntityAttribute, EntityAttributeModifier> getAttributeModifiers(EquipmentSlot slot) {
// If held in main hand, return our modifier, otherwise nothing
if (slot == EquipmentSlot.MAINHAND) {
ImmutableMultimap.Builder<EntityAttribute, EntityAttributeModifier> builder = ImmutableMultimap.builder();
builder.put(AttributesExample.GENERIC_JUMP_BOOST, AttributesExample.SOME_MODIFIER);
return builder.build();
}
return super.getAttributeModifiers(slot);
}
}
By default, NBT and hard-coded modifiers don't mix. If you've ever applied a modifier via NBT on a sword, for example, you might have noticed that your modifier does get applied, but the item suddenly doesn't have any attack damage or attack speed. You can see why in ItemStack#getAttributeModifiers: it completely ignores any hard-coded ones like in the previous example if the stack has any modifiers in its NBT.
Here's a mod that fixes this. It's basically a one-line mixin, but, hey, someone had to do it.
Check out the Minecraft Wiki page for a comprehensive list of vanilla attributes, modifiers, and features.