Jump to content
Search In
  • More options...
Find results that contain...
Find results in...

TileEntity Animation System Explanation


Recommended Posts

There is a WIP PR for documentation on this that has everything that is on this tutorial, and will soon have more such as Items and Entities. Here is a link to the PR.

 

In this tutorial I will be explaining how to use the Animation State Machine or Forge's Animation System on a TileEntity, though this can be applied to Items or Entities as well.

 

Let's start off easy, with the code for the Block, TileEntity and the binding of the TESR.

 

Block

Spoiler

 


public class AnimatedBlock extends Block {

	public AnimatedBlock(Material materialIn) {
		super(materialIn);
		// Set registryName, unlocalizedName, etc.
	}
	
	/**
	 * Creates the ExtendedBlockStateContainer that is needed for the AnimationProperty.
	 * 
	 * Properties.StaticProperty allows for the block to render a normal model and then animate a second model within AnimationTESR. 
	 * This property is not required and anything specifically used just for this property is not required if it is not in use.
	 * 
	 * Properties.AnimationProperty stores the IModelState to be rendered/applied by AnimationTESR.
	 */
	@Override
	public BlockStateContainer createBlockState() {
		return new ExtendedBlockState(this, new IProperty<?>[] { Properties.StaticProperty }, new IUnlistedProperty<?>[] { Properties.AnimationProperty });
	}
	
	/**
	 * Required to override as the game will throw an exception since there are properties within the BlockState. Note it does not have to return 0.
	 */
	@Override
	public int getMetaFromState(IBlockState state) {
		return 0;
	}
	
	/**
	 * Forces the state that is rendered to use the model AnimationTESR will not animate.
	 */
	@Override
	public IBlockState getActualState(IBlockState state, IBlockAccess worldIn, BlockPos pos) {
		return state.withProperty(Properties.StaticProperty, true);
	}

	/**
	 * Let's the world know that this block has a TileEntity associated with it.
	 */
	@Override
	public boolean hasTileEntity(IBlockState state) {
		return true;
	}
	
	/**
	 * Returns a new instance of our TileEntity to be placed in the world.
	 */
	@Override
	public TileEntity createTileEntity(World world, IBlockState state) {
		return new TileEntityAnimated();
	}
	
	/**
	 * Override these two methods if your block is not a solid opaque cube used in rendering.
	 */
	@Override
	public boolean isOpaqueCube(IBlockState state) {
		return false;
	}
	
    @Override
    public boolean isFullCube(IBlockState state) {
    	return false;
    }
    
    /**
     * Only overrode to explain that if your block doesn't have a model that is not animated, it should return EnumBlockRenderType.ENTITYBLOCK_ANIMATED.
     */
    @Override
    public EnumBlockRenderType getRenderType(IBlockState state) {
    	return EnumBlockRenderType.MODEL;
    }
	
}
 

TileEntity

Spoiler

public class TileEntityAnimated extends TileEntity {
	
	/**
	 * This is the thing that makes everything work. It is loaded from a JSON we will make later. 
	 * This will be exposed as a capability instance.
	 */
	private final IAnimationStateMachine asm;
	
	/**
	 * You can also wrap this method in your ClientProxy class and call ClientProxy#loadASM instead of checking the side of FMLCommonHandler.
	 */
	public TileEntityAnimated() {
		if (FMLCommonHandler.instance().getSide() == Side.CLIENT) {
			asm = ModelLoaderRegistry.loadASM(new ResourceLocation(YOUR_MODID, "asms/block/animatedblock.json"), ImmutableMap.of());
		} else asm = null;
	}
	
	// If you don't know about getCapability and hasCapability please read the forge documentation about them.
	
	/**
	 * Exposes the instance of the IAnimationStateMachine to be used in AnimationTESR.
	 */
	@Override
	public <T> T getCapability(Capability<T> capability, EnumFacing facing) {
		if (capability == CapabilityAnimation.ANIMATION_CAPABILITY) return CapabilityAnimation.ANIMATION_CAPABILITY.cast(asm);
		return super.getCapability(capability, facing);
	}
	
	/**
	 * Informs that this TileEntity has the CapabilityAnimation.
	 */
	@Override
	public boolean hasCapability(Capability<?> capability, EnumFacing facing) {
		return getCapability(capability, facing) != null;
	}
	
}

 

 

TESR Binding

Spoiler

/**
* This needs to be done on the client only. Either put this in your ModelRegistryEvent or in your ClientProxy.
*/
ClientRegistry.bindTileEntitySpecialRenderer(TileEntityAnimated.class, new AnimationTESR<TileEntityAnimated>());

 

 

 

This is where the confusion comes from for the animation system. The armature and ASM JSON files. The ASM file has it's own "grammar" which is outlined by fry, here. This is a little vague so I will try to explain this a bit more, so that it is easier to understand with what I have come to know through my own attempts at using this system.

 

Armature File

Spoiler

NOTE: The armature file has to have the same name as the model that it will be applied to, IE if the model is animatedblock.json then the armatures file needs to be animatedblock.json. Also, it needs to be in assets/modid/armatures/block

 

The armature file is made up of two sections joints and clips. Joints are like groups. They allow you to group elements of models together, elements are essentially the cubes that make up a model. They are referenced by the order they come in(indexed at 0). So if I had a model with three elements this could be what my joints section looks like.


"joints": {
	// I am unsures as to why the element is bound to an array. But only the first value matters as it is used as a multiplier.
	"group1": { "0": [1.0] }, // Element 1
  	"group2": { "1": [1.0], "2": [1.0] } // Element 2 and 3
}

 

The clips section is a bit more hectic, but this is where you put your desired animations. This holds more JSON objects that specify what happens. Each object here contains three different parts, the names are loop, joint_clips, and events. Loop just specifies if the clip repeats over and over to the model until the clip is changed. Joint_clips is where the specifications on the animation are, and they are specified to the joint. Events are ways to let code run when the animation has reached a certain point in its life-cycle. This is what an example clips section might look like.


"clips": {
  	"default": {
    	"loop": false, // DO not loop
      	"joint_clips": {}, // No change to the model
      	"events": {} // No events
    },
	"moving": {
    	"loop": true, // Loop animation
      	"joint_clips": {
        	"group1": [
              {
              	"variable": "offset_y", // Specifies what transformation that needs to happen.
                "type": "uniform", // The only type that exists is uniform, not sure why this is included.
                "interpolation": "linear", // Linear is smooth between switches, and nearest is choppy and instant.
                "samples": [0, 0.5, 0] // The values applied, based on the life-cycle of the animation. 
              }
            ],
          	"group2": [
              {
              	"variable": "scale",
                "type": "uniform",
                "interpolation": "nearest", // Scale happens instantly instead of a growing animation.
                "samples": [1, 2, 1] // 1 is normal scale and 2 is times two scale.
              },
              {
              	"variable": "axis_z", // Provides a multiplier for the angle variable
                "type": "uniform",
                "interpolation": "nearest", // Applies the changes instantly
                "samples": [1, 2, 1] // Since there is no other axis declared their samples are 0, any value is treated as a 1.
              },
              	{
                "variable": "angle",
                "type": "uniform",
                "interpolation": "linear", // Smoothly rotates
                "samples": [0, 180, 0]
                }
            ]
          		
        },
      	"events": {
        	"0.5": "halfway" // Sends an event at halfway through the life-cycle due to the 0.5.
        } 
    }
}

 

The whole armature file.

Spoiler


{
	"joints": {
		// I am unsures as to why the element is bound to an array. But only the first value matters as it is used as a multiplier.
		"group1": { "0": [1.0] }, // Element 1
  		"group2": { "1": [1.0], "2": [1.0] } // Element 2 and 3
	},
  	"clips": {
  		"default": {
    		"loop": false, // DO not loop
      		"joint_clips": {}, // No change to the model
      		"events": {} // No events
    	},
		"moving": {
    		"loop": true, // Loop animation
      		"joint_clips": {
        		"group1": [
              		{
              			"variable": "offset_y", // Specifies what transformation that needs to happen.
                		"type": "uniform", // The only type that exists is uniform, not sure why this is included.
                		"interpolation": "linear", // Linear is smooth between switches, and nearest is choppy and instant.
                		"samples": [0, 0.5, 0] // The values applied, based on the life-cycle of the animation. 
              		}
            	],
          		"group2": [
              		{
              			"variable": "scale",
                		"type": "uniform",
                		"interpolation": "nearest", // Scale happens instantly instead of a growing animation.
                		"samples": [1, 2, 1] // 1 is normal scale and 2 is times two scale.
              		},
              		{
              			"variable": "axis_z", // Provides a multiplier for the angle variable
                		"type": "uniform",
                		"interpolation": "nearest", // Applies the changes instantly
                		"samples": [1, 2, 1] // Since there is no other axis declared their samples are 0, any value is treated as a 1.
              		},
              		{
                		"variable": "angle",
                		"type": "uniform",
                		"interpolation": "linear", // Smoothly rotates
                		"samples": [0, 180, 0]
                	}
            	]
          		
       		},
      		"events": {
        		"0.5": "halfway" // Sends an event at halfway through the life-cycle due to the 0.5.
        	} 
    	}
	}
}

 

 

1

 

ASM File

Spoiler

Note: The ASM file can be anywhere within the assets/modid package, but convention would dictate it goes in assets/modid/asms/block/. It;s position is denoted when loadASM is called, its path is within the ResourceLocation.

 

The ASM file is contains five main things: the parameters, clips, states,transitions, and the starting state(start_state). Parameters(JSON object) are essentially variables that you can modify using the arithmetic operations stated in the grammer file that was linked earlier. They are used in the clips section, speaking of which; the clips section(JSON object) stores references to the clips in the armature file along with operators for the clips. States is just an array that contains all of the clip names. Transitions is a JSON object that just maps each state to which state it can transition to. The start_state is a string that gives the name of the starting state when the ASM is loaded.

 

Parameters Section


"parameters": {
	"raw_anim_time": [ "+", 0 ], // Percentage of animation time using the add arithmatic operator.
  	"slowed_anim_time": [ "/", 4 ], // Percentage of animation time divided by 4 using the division arithmatic operator.
	"four": [ "/", "#slowed_anim_time" ] // Just using the division operator with another parameter.
  	// You can also reference parameters passed in from code when the loadASM method is called. ImmutableMap.of(String, ITimeValue);
}

 

Clips Section


"clips": {
	"default": "modid:block/animation_block@default", // Just a reference to the clip in the armature file.
  	"moving": [ "apply", "modid:block/animation_block@moving", "#slowed_anim_time" ] // Applies the slowed_anim_time to the moving clip in the armature file.
}

 

States Section


"states": [
	"default", "moving" // As seen above these are our two states, these must match clip names.
]

 

Transitions Section


"transitions": {
	"default": "moving", // default state will be able to transition to moving
  	"moving": "default" // mocing state will be able to transition to default state
}

 

Starting State


"start_state": "moving" // The ASM will start in the moving state

 

Whole ASM File

Spoiler


{
  	"parameters": {
		"raw_anim_time": [ "+", 0 ], // Percentage of animation time using the add arithmatic operator.
  		"slowed_anim_time": [ "/", 4 ], // Percentage of animation time divided by 4 using the division arithmatic operator.
		"four": [ "/", "#slowed_anim_time" ] // Just using the division operator with another parameter.
  		// You can also reference parameters passed in from code when the loadASM method is called. ImmutableMap.of(String, ITimeValue);
	},
	"clips": {
		"default": "modid:block/animation_block@default", // Just a reference to the clip in the armature file.
  		"moving": [ "apply", "modid:block/animation_block@moving", "#slowed_anim_time" ] // Applies the slowed_anim_time to the moving clip in the armature file.
	},
	"states": [
		"default", "moving" // As seen above these are our two states, these must match clip names.
	],
  	"transitions": {
		"default": "moving", // default state will be able to transition to moving
  		"moving": "default" // mocing state will be able to transition to default state
	},
  	"startstate": "moving" // The ASM will start in the moving state
}

 

 

 

Sample BlockState File

Spoiler

{
	"forge_marker": 1,
  	"defaults": {
    	// Declare your defaults
    },
  	"variants": {
    	"normal": [{}],
      	"inventory": [{}],
      	"static": { // Remember this is optional.
        	"true": { }, // Non animated model.
        	"false": { } // Animated model.
        }
    }
}

 

 

I'm sure there are typos or possibly something I have missed or not explained well enough, so I would love some feedback even on formatting issues. I will also post my raw notes on this stuff that might contain extra information.

Spoiler

Armatures File

Note: File name needs to be the same as the model files name or else it will not work!

joints: obj
	joint: obj
		model element(referenced by declaration in model file): name, value array(what's inside doesn't seem to matter.)
		
clips: obj
	clip name: obj
		loop: boolean(whether the clip should continuously run)
		joint_clips: obj
			joint name: array
				obj
					variable: string(which variable; offets_x/y/z, scale/_x/y/z, etc)
					type: string(uniform is the only option; not sure what the point of this is)
					interpolation: string(linear or nearest; linear seems smooth, while nearest is choppy/instant)
					samples: array(values that will be applied, multiple allows for animation)
	events: obj
		time event is fired(decimal; percentage of animation complete): string(name of event used in AnimationTESR#handleEvents)
			
			
ASM File
	Best reference
	https://gist.github.com/RainWarrior/964ed4692f4da1fd4964
	
	parameters: obj
		parameter name: array(uses arithmetic operations outlined in the link above and code parameters are referenced with a # and the key in the parameters map)
	clips: obj
		clip name: array or string
			string(simply just references the clip from the armatures file, doesn't apply a parameter value)
				"modid:block/filename@armaturesclipname" or "modid:item/filename@armaturesclipname"
			array of strings
				"apply"(animation call; the clip is modified by the parameters value), string from above, "#parametername"
				"slerp"("spherical linear blend between to clips"-Fry), string from above(initial clip), string from above(final clip), "#parametername"(input), "#parametername"(progress)
				"trigger_positive", "#clipname"(not from the armatures file), "#parametername", armatures event name or "!transition:clipname"(not from armatures file)
	states: array(contains clip names)
	transitions: obj
		state name: string(state name to transition to)
	start_state: string (state name for the animation to start on)

Code
	Properties.AnimationProperty in an ExtendedBlockState
	TileEntity needs to expose the CapabilityAnimation.ANIMATION_CAPABILITY in hasCapability and getCapability
	ClientRegistry.bindTileEntitySpecialRenderer(TileEntityClass, new AnimationTESR<TileEntityClass>());

 

 

Edited by Animefan8888
  • Like 1
  • Thanks 1

VANILLA MINECRAFT CLASSES ARE THE BEST RESOURCES WHEN MODDING

I will be posting 1.15.2 modding tutorials on this channel. If you want to be notified of it do the normal YouTube stuff like subscribing, ect.

Forge and vanilla BlockState generator.

Link to comment
Share on other sites

  • 4 weeks later...

Thank you for this tutorial.

However, you made a small but significant typo in your ASM json file which crashes the game.

The key "start-state" should actually be "start_state" as annotated in AnimationStateMachine.java:74.

@SerializedName("start_state")
private final String startState;

It took me quite a while to figure out what's wrong, because this kind of typo is particularly hard to notice.

Link to comment
Share on other sites

Just now, yurifag said:

However, you made a small but significant typo in your ASM json file which crashes the game.

The key "start-state" should actually be "start_state" as annotated in AnimationStateMachine.java:74.

Oh, thank you I wrote this late at night after finally getting my animation to work. I will change that.

VANILLA MINECRAFT CLASSES ARE THE BEST RESOURCES WHEN MODDING

I will be posting 1.15.2 modding tutorials on this channel. If you want to be notified of it do the normal YouTube stuff like subscribing, ect.

Forge and vanilla BlockState generator.

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

 Share



  • Recently Browsing

    No registered users viewing this page.

  • Posts

    • Thanks for the help everybody! I tried this and although the block did register, the fluid just appears as a flat plane you can only see if you break the block under it. I tried debugging it as much as I could but alas I couldn't solve it Here is the code for my "fixed" liquidblock as suggested by Luis_ST: package com.hotmail.majdroaydi.minitech.blocks; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.sounds.SoundEvent; import net.minecraft.tags.FluidTags; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.BlockGetter; import net.minecraft.world.level.Level; import net.minecraft.world.level.LevelAccessor; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.LiquidBlock; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.material.FlowingFluid; import net.minecraft.world.level.pathfinder.PathComputationType; import net.minecraft.world.phys.shapes.CollisionContext; import net.minecraft.world.phys.shapes.Shapes; import net.minecraft.world.phys.shapes.VoxelShape; import java.util.Optional; import java.util.function.Supplier; public class ForgeLiquidBlock extends LiquidBlock { public ForgeLiquidBlock(Supplier<? extends FlowingFluid> supplier, Properties properties) { super(supplier, properties); } @Override public VoxelShape getCollisionShape(BlockState p_54760_, BlockGetter p_54761_, BlockPos p_54762_, CollisionContext p_54763_) { return p_54763_.isAbove(STABLE_SHAPE, p_54762_, true) && p_54760_.getValue(LEVEL) == 0 && p_54763_.canStandOnFluid(p_54761_.getFluidState(p_54762_.above()), getFluid()) ? STABLE_SHAPE : Shapes.empty(); } @Override public boolean isPathfindable(BlockState p_54704_, BlockGetter p_54705_, BlockPos p_54706_, PathComputationType p_54707_) { return !getFluid().is(FluidTags.LAVA); } @Override public boolean skipRendering(BlockState p_54716_, BlockState p_54717_, Direction p_54718_) { return p_54717_.getFluidState().getType().isSame(getFluid()); } @Override public void onPlace(BlockState p_54754_, Level p_54755_, BlockPos p_54756_, BlockState p_54757_, boolean p_54758_) { if (this.shouldSpreadLiquid(p_54755_, p_54756_, p_54754_)) { p_54755_.getLiquidTicks().scheduleTick(p_54756_, p_54754_.getFluidState().getType(), getFluid().getTickDelay(p_54755_)); } } @Override public BlockState updateShape(BlockState p_54723_, Direction p_54724_, BlockState p_54725_, LevelAccessor p_54726_, BlockPos p_54727_, BlockPos p_54728_) { if (p_54723_.getFluidState().isSource() || p_54725_.getFluidState().isSource()) { p_54726_.getLiquidTicks().scheduleTick(p_54727_, p_54723_.getFluidState().getType(), getFluid().getTickDelay(p_54726_)); } //return super.updateShape(p_54723_, p_54724_, p_54725_, p_54726_, p_54727_, p_54728_); return p_54723_; // Calling super.updateShape will just call LiquidBlock's updateShape, not what we are looking for! Thankfully, Block.updateShape, simply enough, just returns the first parameter. } @Override public void neighborChanged(BlockState p_54709_, Level p_54710_, BlockPos p_54711_, Block p_54712_, BlockPos p_54713_, boolean p_54714_) { if (this.shouldSpreadLiquid(p_54710_, p_54711_, p_54709_)) { p_54710_.getLiquidTicks().scheduleTick(p_54711_, p_54709_.getFluidState().getType(), getFluid().getTickDelay(p_54710_)); } } private boolean shouldSpreadLiquid(Level p_54697_, BlockPos p_54698_, BlockState p_54699_) { if (getFluid().is(FluidTags.LAVA)) { boolean flag = p_54697_.getBlockState(p_54698_.below()).is(Blocks.SOUL_SOIL); for(Direction direction : POSSIBLE_FLOW_DIRECTIONS) { BlockPos blockpos = p_54698_.relative(direction.getOpposite()); if (p_54697_.getFluidState(blockpos).is(FluidTags.WATER)) { Block block = p_54697_.getFluidState(p_54698_).isSource() ? Blocks.OBSIDIAN : Blocks.COBBLESTONE; p_54697_.setBlockAndUpdate(p_54698_, net.minecraftforge.event.ForgeEventFactory.fireFluidPlaceBlockEvent(p_54697_, p_54698_, p_54698_, block.defaultBlockState())); this.fizz(p_54697_, p_54698_); return false; } if (flag && p_54697_.getBlockState(blockpos).is(Blocks.BLUE_ICE)) { p_54697_.setBlockAndUpdate(p_54698_, net.minecraftforge.event.ForgeEventFactory.fireFluidPlaceBlockEvent(p_54697_, p_54698_, p_54698_, Blocks.BASALT.defaultBlockState())); this.fizz(p_54697_, p_54698_); return false; } } } return true; } private void fizz(LevelAccessor p_54701_, BlockPos p_54702_) { p_54701_.levelEvent(1501, p_54702_, 0); } @Override public ItemStack pickupBlock(LevelAccessor p_153772_, BlockPos p_153773_, BlockState p_153774_) { if (p_153774_.getValue(LEVEL) == 0) { p_153772_.setBlock(p_153773_, Blocks.AIR.defaultBlockState(), 11); return new ItemStack(getFluid().getBucket()); } else { return ItemStack.EMPTY; } } @Override public Optional<SoundEvent> getPickupSound() { return getFluid().getPickupSound(); } } The code for my OilFluid can be found above.
    • Whoops! Sorry, my fault. But yes, a block entity (tile entity in 1.16-) *is* needed for storing data (i.e. furnace). But I think what OP is doing is making an item combiner, which doesn't need one
    • Looking to work with reliable Fabric & Forge developers to produce original mods for use on my YouTube channel! We'll be regularly working together and you'll be given a detailed brief for each commission that outlines exactly what I want. From there, you can give me a price that fits the scope of the project and we can get to work :thumbsup: Because these commissions are only for YouTube videos, you're essentially making vertical-slices / proof-of-concepts. They can have bugs, you can take shortcuts & we can use video editing to hide bugs & achieve some effects. With that in mind, turnaround speed depends on project complexity but for an average commission I'd expect delivery two days - five days Send me a email at either lmaololtbhhonest@gmail.com OR Business@tbhhonest.com if you're interested! preview of channel: https://imgur.com/a/bOVkiUo LINK:  https://www.youtube.com/channel/UCnwo3X3eCrUp7N1DZPc-MGQ  
    • If you want a tiny fraction of offset, use float.epsilon. 0.01 will actually be noticeable. It's about 1/6th of a texture pixel.
  • Topics

  • Who's Online (See full list)

×
×
  • Create New...

Important Information

By using this site, you agree to our Privacy Policy.