Skip to content

JSON Based Entity Animations (1.15.2)

Andrey edited this page Oct 20, 2020 · 3 revisions

For animation to be used you don't need to write it manually, it will be loaded from the mod's resources.

Full examples can be seen in this package: ru.timeconqueror.timecore.animation_example

Prologue. How to create animations and models for entities?

One of the most convenient way to do them in my opinion is Blockbench. The JSON models that are needed for animations are nothing more than the Bedrock Entity Model, which can also be done in Blockbench (Link).

1. Animation Loading:

In order to load animations, they must be located along this path: resources/data/<your_mod_id>/path, so they should be in a mod's datapack.

You should load animations during FMLCommonSetupEvent.

To load animations, you need to use one of the provided methods in the AnimationAPI, depending on how many animations are contained in one file:

  1. Animation loadAndRegisterAnimation(ResourceLocation animationFileLocation) - Creates and registers animation, which is a single one in the file with provided path.
  2. Map<String, Animation> loadAndRegisterAnimations(ResourceLocation animationFileLocation) - Creates a map of all animations in the file with provided path and registers all retrieved animations. Key in this map is the name of the animation, that is indicated in the file.

animationFileLocation is a path to the file, it should contain path to the file under the data/ folder. Example: new ResourceLocation(TimeCore.MODID, "animations/zombie_hit.json")) will result in data/timecore/animations/zombie_hit.json.

The system can also reverse the animation, i.e. make it play in the opposite direction. This can be done using AnimationAPI.register (Animation animation) during the FMLCommonSetupEvent event. It is important to register this animation (during the same event) with the AnimationAPI.register (Animation animation) method.

Example:

@Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD)
public class Animations {
    public static Animation floroWalk;
    public static Animation floroShoot;
    public static Animation floroReveal;
    public static Animation floroHidden;
    public static Animation floroHide;

    @SubscribeEvent
    public static void registerAnimations(FMLCommonSetupEvent event) {
        floroWalk = AnimationAPI.loadAndRegisterAnimation(new ResourceLocation(TimeCore.MODID, "animations/floro.walk.json"));
        floroShoot = AnimationAPI.loadAndRegisterAnimation(new ResourceLocation(TimeCore.MODID, "animations/floro.shoot.json"));
        floroReveal = AnimationAPI.loadAndRegisterAnimation(new ResourceLocation(TimeCore.MODID, ("animations/floro.showing.json"));
        floroHidden = AnimationAPI.loadAndRegisterAnimation(new ResourceLocation(TimeCore.MODID, "animations/floro.hidden.json"));
        floroHide = AnimationAPI.register(AnimationAPI.reverse(floroReveal));
    }
}

2. Json Models Loading:

For the animations to work, the entity must have a JSON model.

To do this, we also need to load them from a file. It's better to do this on the FMLClientSetupEvent event. Use the TimeModelLoader.loadJsonEntityModel method to load them. More details about the method can be found in its javadoc.

Example:

@Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD)
public class EntityRegistry {
    public static TimeEntityModel<FloroEntity> floroModel;

    @SubscribeEvent
    public static void registerRenders(FMLClientSetupEvent event) {
        floroModel = TimeModelLoader.loadJsonEntityModel(new ResourceLocation(TimeCore.MODID, "models/entity/floro.json"));
    }
}

The JSON model has a setScaleMultiplier method that can be used to change the initial scale of the model.

3. AnimationSystem

The AnimationSystem is the entry point for everything related to animations. From this object we can refer to ActionManager and AnimationManager via its getters.

4. Action Manager, Animation Manager, AnimationStarter.

The Action Manager manages animated actions. Using its enableAction and disableAction methods, you can enable and disable delayed actions (for example, on a tick or if something happened), which immediately start the animation, and then at a specified time (while this animation is being performed) perform some tasks. After the animation ends, the action will turn itself off.

The AnimationManager allows you to directly control the animations themselves. When the animation is played, it's divided into so-called layers. They are needed in order to be able to simultaneously play several animations at once for one entity. Layer contains:

  1. blendType. It determines how animation on this layer will be applied to the entity depending on the previous ones. BLENDING: Animation on this layer will overwrite all positions, rotations, scales that were applied by the animations on the previous layers; ADDING: Animation on this layer will add all positions, rotations, scales to the corresponding ones from the animations on the previous layers.
  2. weight. The number from 0 to 1, which determines, how much the animation will overlap with the ones from the previous layers. It's like a percentage. For example, if animation rotates model piece by 90 degrees and weight is 0.5, then model piece will be rotated by 45 degrees (50% from 90 degrees).

To start the animation, we need to create a Starter. That can be achieved by creating a new instance of AnimationStarter: new AnimationStarter (YOUR_ANIMATION). The Starter itself is an animation wrapper that allows you to customize one animation for many situations.

Its optional settings ( which you can change with setters):

  • ignorability. When you start this animation on the layer, which is playing the same animation, it won't be rerun. Useful for walking animations, so you don't need to worry how to control animation endings. Default: true.
  • transitionTime defines the time (in milliseconds) of the transition animation between the previous animation and the one we want to run. ** Default: AnimationConstants.BASIC_TRANSITION_TIME **.
  • speed - a factor that will speed up or slow down the animation. Default: 1F.
  • nextAnimation - setting with which you can make a chain of played animations. As soon as one ends, the next one will start immediately. This setting will avoid unpleasant flickering when moving from one animation to another. Default: null.

5. Applying animations to LivingEntity, Entity or Tile Entity.

All setup you need to bind animation system to Living Entity will be almostly the same for Entity and Tile Entity, specific details will be covered.

So for the animations to work, we need to make the entity class implement the interface AnimatedObject. Now you need a field containing the Animation System of this creature: private final AnimationSystem<FloroEntity> animationSystem; and this is the field that should be returned in the getSystem method of the AnimatedObject interface.

Result:

public class FloroEntity extends MonsterEntity implements IRangedAttackMob, AnimatedObject<FloroEntity> {
    private final AnimationSystem<FloroEntity> animationSystem;
    
    ...

    @Override
    public @NotNull AnimationSystem<FloroEntity> getSystem() {
        return animationSystem;
    }
}

Next, you need to create Animation System object. This must be done in the Entity constructor via ru.timeconqueror.timecore.api.animation.builders.AnimationSystemBuilder. For entity you should choose one of the static methods with name forEntity, for tile entities - forTileEntity. Params:

  • entity - the entity, for which we create an animation system. (for tile entities it will be tileEntity)
  • world - the world of that entity.
  • animationManagerTuner - the settings, which you can apply onto the animation manager:
    • addLayer - adds layer to the animation manager with provided layerName, blendType and weight, which are described earlier.
    • addMainLayer - adds the layer with the AnimationConstants.MAIN_LAYER_NAME name, OVERRIDE blend type and weight, which is equal 1. Usually you don't need it. It will be added automatically as a single layer, if you don't add any layer by yourselves.
  • predefinedAnimationsTuner - the settings, where you can setup predefined animations with built-in support, such as idle animation, walking, etc. By the way, idle and walking animations should be infinite, so don't forget to make them looped!
public class FloroEntity extends MonsterEntity implements IRangedAttackMob, AnimatedObject<FloroEntity> {
    private static final String LAYER_SHOWING = "showing";
    private static final String LAYER_WALKING = "walking";
    private static final String LAYER_ATTACK = "attack";

    private final AnimationSystem<FloroEntity> animationSystem;

    public FloroEntity(EntityType<? extends FloroEntity> type, World world) {
        super(type, world);

        animationSystem = AnimationSystemBuilder.forEntity(this, world, animationManagerBuilder -> {
            animationManagerBuilder.addLayer(LAYER_SHOWING, BlendType.OVERRIDE, 1F);
            animationManagerBuilder.addLayer(LAYER_WALKING, BlendType.ADDING, 1F);
            animationManagerBuilder.addLayer(LAYER_ATTACK, BlendType.ADDING, 0.9F);
        }, predefinedAnimations -> {
            predefinedAnimations.setWalkingAnimation(new AnimationStarter(EntityAnimations.floroWalk).setSpeed(3F), LAYER_WALKING);
        });
    }

    public @NotNull AnimationSystem<FloroEntity> getSystem() {
        return animationSystem;
    }
}

6. How to use Animations:

The following methods can be used to start the animation (they do the same thing):

  • AnimationAPI.startAnimation
  • AnimationStarter.startAt
  • AnimationManager.setAnimation

To find out which animation is currently playing on a particular layer, you can use AnimationManager.getLayer(YOUR_LAYER_NAME).getCurrentAnimation().

To remove the animation that is currently playing on the layer, you can use the following methods (they do the same thing):

  • AnimationAPI.removeAnimation
  • AnimationManager.removeAnimation

Animations can be manipulated on either side, be it client or server, they will sync. You can run them at any time in the existence of a entity, for example, on tick.

7. How to use Delayed Actions

Delayed Actions are needed in order to play some tasks at some point while the animation is running. They are very useful for animated versions of Entity's AI.

General Information: They are Entity-independent, so you don't need to create them every time the entity is created, just make them static.

private static final DelayedAction<FloroEntity, Void> HIDING_ACTION;

The first generic type determines the entity, for which Action can be started. The second generic type represents the extra information, that can be accessed later in setOnCall.

Creation:

private static final DelayedAction<FloroEntity, Void> HIDING_ACTION = 
new DelayedAction<FloroEntity, Void>(new ResourceLocation("timecore:floro/hiding"), new AnimationStarter(Animations.floroHide), LAYER_SHOWING)
                .setDelayPredicate(StandardDelayPredicates.onEnd())
                .setOnCall((floroEntity, nothing) -> floroEntity.setHidden(true));

DelayedAction constructor: DelayedAction(ResourceLocation id, AnimationStarter animationStarter, String animationLayer)

  • id - ID of action. By this ID they will be compared for deletion and addition.
  • animationStarter - animation, which will be played when action is started.
  • animationLayer - layer, where animation will be played.

Extra settings:

  • setDelayPredicate() determines in which moment of time (relatively to the playing animation, in milliseconds) task, which is set by setOnCall() will be run. See StandardDelayPredicates for common predicates. Default: StandardDelayPredicates.onStart.
  • setOnCall() - task, that will be run, when the predicate from .setDelayPredicate() will return true. Default: () -> {}.

So far, task from setOnCall can only be played once per animation.

TimeCore also has some built-in animated Entity AI Goals/Tasks, which you can use. They are located in ru.timeconqueror.timecore.animation.entityai package.

Executing:

To run the action, use ActionManager.enableAction().

To remove the action use ActionManager.disableAction().

To check if the action with the same id is already enabled use ActionManager.isActionEnabled.