Keen:Subpart Animation System

From Medieval Engineers Wiki
Jump to navigation Jump to search

Version: 0.4


As of version 0.4.0 we introduced the new subpart animation system. The new subpart animation system allows modders to create animations on any cube block. It is no longer required to make any animated block a MyMedievalDoor block. The new system is a lot more powerful than the old system and will allow a lot more creative freedom and expansion.

It is recommended to no longer use MyMedievalDoor for block animations. It is now deprecated and will be going away eventually.

In this document the new system is explained. First, there will be an overview of the new components we have added. After that, the document will explain more in-depth how to create a new block. Finally, the interaction of players with the block will be explained, both through scripting and through ModAPI.

For any questions regarding this guide it is recommended to ask the community on the forum or Discord. Asking a Medieval Engineers programmer is also an option, if this option is available to the reader.

Block components overview

In this section the components involved in the subpart animations are defined. For each component a short description will be given, explaining its purpose and reason for existence. The modder is able to custom-tailer the behaviour of their block through specifying which components they want. Some components require other components, listed under the component name, to be there. The modder is otherwise free to do what they believe they need.

This is the root component for the entire system. The subpart component defines what subparts exist on the block’s model. It also loads and stores subpart states between game saves, as well as synchronizes the subpart states over the network. This component allows modders to specify the subparts for the cubeblocks.
All the other subpart affecting components require this component to be present on the cubeblock, they will throw an error if added without this component present.


This component defines animations for the subparts. It is able to create animation sequences, perform subpart rotation and translation, and trigger animation events. The animations can use various interpolation curves, see “Appendix A: Interpolation equations” for more information. This component is also responsible for loading and storing the currently active animation for game saves, as well as synchronizing the active animation over the network.
This component allows modders to specify the animations for the cubeblocks.


This component defines entity states. While the component itself is very simple, its functionality is actually really powerful. It is a basic state machine. It is possible to set up states and the possible transitions to other states. It triggers an event whenever the state is changed, and it is possible for other components to listen to this. This component allows modders to set up entity states for their blocks, which other components and scripts can respond to.


This component defines use objects available on the entity. Use objects are the elements players can aim at and interact with by pressing their Use key. For this system, all use objects should use the Generic use object.
The MyUseObjectGeneric use object has a public event modders can register to, to receive events whenever the user interacts with the object.


This component listens to animation events and will be able to play audio upon certain events being triggered. This component allows modders to add sound effects to their animations.


This component triggers entity state transitions when animation events occur. This will allow modders to create more complicated entities and have them reflect their state correctly.


This component listens to state transition events by the MyEntityStateComponent and tells the MySubpartAnimationComponent to start playing animations. It allows the modder to create a block that automatically responds to entity state changes.


This component defines the actions that can be performed on the object. It will show the player a notification displaying what they can do with this object. It is possible to specify which state transitions are performed when the object is interacted with. This component allows modders to create dynamic use object reactions to their cubeblocks.
Whenever the object is interacted with it will select all possible actions that match the detector name, the current active state, and then it will select the first allowed action. Any other actions that matched the requirements will be ignored.
This component acts as a data interface for the Generic use object. If it is not present, the Generic Use Object will not do anything.


This component listens to state change triggers and will schedule a new transition after a set amount of time. This component allows designers to create objects that perform automatic behaviour, such as a door that closes itself after a few seconds.

Creating a new block

This section will explain the steps required to create a new block. For this purpose, one of the blocks in the game will be used as an example, to showcase all the features that are currently available in the game.

For the purpose of this document, the block that will be explained is the palisade gate. It is a newly introduced gate block that fits well with the palisade walls. The exact details can be found in ./Content/Data/CubeBlocks/Doors/PalisadeGate.sbc in case more information is required.

Setting up cube block definition

The first required element for creating a new block is the CubeBlock definition. The CubeBlock definition is the same as it has always been, so setting this one up is pretty simple. The most important thing to take note of here is the SubtypeId value.

  <Definition xsi:type="MyObjectBuilder_CubeBlockDefinition">
    <Id Type="MyObjectBuilder_CubeBlock" Subtype="PalisadeGate" />

Setting up the subparts

After setting up the cube block definition, the first new component must be added. This is the CubeBlock Subpart Component, and its definition looks like thus:

<Definition xsi:type="MyObjectBuilder_CubeBlockSubpartComponentDefinition">
  <Id Type="MyObjectBuilder_CubeBlockSubpartComponent" Subtype="PalisadeGate" />

The SubtypeId value needs to be identical to the CubeBlock definition, and it is case sensitive.

Setting up subparts

The Palisade Gate has two subparts, the left and right gate. And these need to be defined in the subpart component. This definition goes right after <Subparts> and looks like thus:

  	<Subpart Name="PalisadeGateLargeLeft" HingeBone="PalisadeGateLargeLeft_Pin" RequiresAxialCorrection="false" HasDummy="false" />
  	<Subpart Name="PalisadeGateLargeRight" HingeBone="PalisadeGateLargeRight_Pin" RequiresAxialCorrection="false" HasDummy="false" />
a flag necessary if the doors are not where they were expected to be. Usually, this means that either oriented the block is oriented incorrectly in the 3D editor, or it was exported incorrectly. Axial Correction will invert the Z axis, and swap the X and Y axes. This was necessary for some of the older blocks, as changing them in the editor now would mean all the blocks would rotate in the existing blueprints on the steam workshop.
a new feature, introduced in 0.4.4, where it allows the interaction dummy to move with the moving parts. This provides a more intuitive interaction model, however it has some implementation issues currently, where it does not obey access rights correctly. It is not recommended for blocks that utilize the permissions system.

Setting up bones

One of the new features in the subpart animation system is the ability to position subparts to a bone instead of specifying the position in x,y,z coordinates. This will make it a lot simpler to place the subparts. In order to use this, the base model must be exported with the bones, and each subpart model must have its anchor point at the 0,0,0 coordinate.

For those who would rather present the data in x,y,z format, this is still possible, and an example of this can be found in the WoodenGate.sbc file.

Setting up the animations

After setting up the subparts it is now logical to set up the animations for each subpart. The animations are reasoned from the anchor point, and support some basic interpolation options. The animation format is kind of complex and supports a lot of options, but it has some limitations as well. See Limitations for more information.

Here is the layout of the SubpartAnimationComponentDefinition:

<Definition xsi:type="MyObjectBuilder_SubpartAnimationComponentDefinition">
  <Id Type="MyObjectBuilder_SubpartAnimationComponent" Subtype="PalisadeGate" />

Notice how the SubtypeId is once again the same as in the cubeblock definition.

Setting up the Subpart Animations

After defining the component, it is time to define the animations that are part of the component. The animations are structured into sequences of animation steps, and each sequence consists of events and animations. The two sequences used in the PalisadeGate look like this:

  <Sequence Name="OpenSequence" WrapMode="Once">
    	<Event Start="0.2" Name="DoorOpening" />
    	<Event Start="2.2" Name="DoorOpened" />

    	<Animation Start="0" End="2.2">
      		<Subpart Name="PalisadeGateLargeRight" Type="Rotation" Axis="Y" From="-90" To="0" Method="QuadraticEaseInOut" />
      		<Subpart Name="PalisadeGateLargeLeft" Type="Rotation" Axis="Y" From="90" To="0" Method="QuadraticEaseInOut" />
  <Sequence Name="CloseSequence" WrapMode="Once">
    	<Event Start="0.2" Name="DoorClosing" />
    	<Event Start="2.2" Name="DoorClosed" />

    	<Animation Start="0" End="2.2">
      		<Subpart Name="PalisadeGateLargeRight" Type="Rotation" Axis="Y" From="0" To="-90" Method="QuadraticEaseInOut" />
      		<Subpart Name="PalisadeGateLargeLeft" Type="Rotation" Axis="Y" From="0" To="90" Method="QuadraticEaseInOut" />

Each animation sequence can contain 0 or more events and 0 or more animations. The total duration of a sequence is calculated from either the last finishing animation or the last event. Each animation is executed parallel to the other animation, and if an animation finishes, its effect is applied for the whole remaining duration of the animation.

Each animation contains 0 or more subpart instructions.

the subpart it will be affecting
the type of animation (Rotation or Translation)
the axis on which it operates
the angle or position the animation starts at
the angle or position the animation ends at
the animation method

For more information on the animation methods, look at Appendix A: Interpolation Equations.

Setting up animation events

As mentioned before, it is possible to set up animation events. Certain entity components will be listening to them and acting upon them being fired, and it is even possible for ModAPI to connect to these events through the code. Each event is simply a point in time during the animation and a name.

Setting up Animation Sound Events

One of the entity components that act on animation events is the Animation Event Sound Component. It plays a sound when it receives an event it knows of. Its definition looks like this:

<Definition xsi:type="MyObjectBuilder_AnimationEventSoundComponentDefinition">
  <Id Type="MyObjectBuilder_AnimationEventSoundComponent" Subtype="PalisadeGate" />
    <Event Name="DoorClosed" Sound="DoorOpen" />

Again the SubtypeId matches that of the CubeBlock definition.

Controlling a new block

Having the subparts and animations defined is not enough. The game needs to know how to control them, trigger the right animation at the right time, respond to player interaction, etc.

Entity state

The next entity component that is needed is the EntityStateComponent. This component is responsible for one thing: Tracking the entity’s state. For a door, there are four states, open, closing, closed, and opening. When a door is open or closed, it is not moving, and it can respond to player interaction. When a door is opening or closing, it is actually moving and it should ignore player input.

Setting up entity state

The entity state component definition is quite simple. The initial state is defined, and for each possible state, transitions to other states are defined. The whole definition looks like this:

<Definition xsi:type="MyObjectBuilder_EntityStateComponentDefinition">
  <Id Type="MyObjectBuilder_EntityStateComponent" Subtype="PalisadeGate" />
    <State Name="Open">
    <State Name="Closing">
    <State Name="Closed">
    <State Name="Opening">

As usual, SubtypeId is matching the CubeBlock definition.

the state in which the entity starts
the name of the state
the state that can be transitioned to from this state

Setting up State Animations

One of the powerful features of this state system is that it is possible to trigger animations on state transitions. For example, when the gate that starts as open wants to transition to a closed state, it has to transition to the Closing state. When the game detects that the closing state is being transitioned to, it will trigger the appropriate animation. The definition looks like this:

<Definition xsi:type="MyObjectBuilder_StateAnimationComponentDefinition">
  <Id Type="MyObjectBuilder_StateAnimationComponent" Subtype="PalisadeGate" />
    <Animation State="Opening" Animation="OpenSequence" />
    <Animation State="Closing" Animation="CloseSequence" />

And as usual, SubtypeId is matching the CubeBlock definition.

the state that will trigger the animation
the animation sequence to be triggered

Setting up interaction

Finally, it is time to set up the interaction components. At this point, the most complex definitions have been put in place, and all that remains is connecting it to input and finalizing the last state transitions.

Use Objects Component

First, the use objects component has to be set up. This component enables the game to interact with the use object dummies on the cube block, and it looks like this:

<Definition xsi:type="MyObjectBuilder_UseObjectsComponentDefinition">
  <Id Type="MyObjectBuilder_UseObjectsComponent" Subtype="PalisadeGate" />
    <UseObject Dummy="detector_gate_01" Name="Generic" />
    <UseObject Dummy="detector_gate_02" Name="Generic" />
    <UseObject Dummy="detector_gate_03" Name="Generic" />
    <UseObject Dummy="detector_gate_04" Name="Generic" />

Like all the previous definitions, SubtypeId must match the CubeBlock definition.

must be set to false or the dummies will not behave appropriately.
the name of the dummy to use for interactions.
Use objects are created in the model, and must have a name that follows a strict format. The name must start with detector_, followed by a unique name.
defines the name of the use object type to use, which in this case is Generic for each of the door’s detector dummies.
Normally, the dummy name by itself determines the type of use object that is selected for the object. Inventory for example would be called detector_inventory, and the game would automatically figure out that the desired effect is inventory. The Name attribute overrides the chosen use object, and was added for legacy block support.

Please keep in mind that there is a possibility that in the future this system will be altered. Right now it is working in a somewhat obtuse manner and there are future plans for changing this, however, for now it functions like this.

State Use Object Component

The state use object component is the meat of the interaction. It is quite complex to set up and contains many settings. Once it is understood however, it is not so difficult. It looks like this:

<Definition xsi:type="MyObjectBuilder_StateUseObjectComponentDefinition">
  <Id Type="MyObjectBuilder_StateUseObjectComponent" Subtype="PalisadeGate" />
    <Trigger Dummy="detector_gate_01" From="Open" To="Closing" ActionName="Action_Close" SecondaryActionName="Action_Configure" />
    <Trigger Dummy="detector_gate_01" From="Closed" To="Opening" ActionName="Action_Open" SecondaryActionName="Action_Configure" />
    <Trigger Dummy="detector_gate_02" From="Open" To="Closing" ActionName="Action_Close" SecondaryActionName="Action_Configure" />
    <Trigger Dummy="detector_gate_02" From="Closed" To="Opening" ActionName="Action_Open" SecondaryActionName="Action_Configure" />
    <Trigger Dummy="detector_gate_03" From="Open" To="Closing" ActionName="Action_Close" SecondaryActionName="Action_Configure" />
    <Trigger Dummy="detector_gate_03" From="Closed" To="Opening" ActionName="Action_Open" SecondaryActionName="Action_Configure" />
    <Trigger Dummy="detector_gate_04" From="Open" To="Closing" ActionName="Action_Close" SecondaryActionName="Action_Configure" />
    <Trigger Dummy="detector_gate_04" From="Closed" To="Opening" ActionName="Action_Open" SecondaryActionName="Action_Configure" />

Naturally, SubtypeId is the same as the CubeBlock definition.

displayed to the player when they aim at a dummy. The one used here is a formatted one that creates a message informing the player that they can tap to open the door, and hold to edit the permissions.
determines the tap interaction. In this case, it will perform the manipulation action which triggers one of the use object transition triggers.
determines the alternative interaction, in this case it will open the configuration contextual menu.
determines whether or not this block will respect access rights.

Finally, there is a list of UseObjectTransitionTriggers. Each trigger has a couple of fields.

the name of the dummy as specified in the UseObjectsComponent.
indicates what the current EntityState must be for this one to be considered.
indicates which state the EntityStateComponent must transition to when it is triggered by the primary action.
ActionName and SecondaryActionName
the texts put into the tooltip when the player aims at the block and observes the possible actions.

Setting up Animation Event State Transition Component

The last component in the chain is the AnimationEventStateTransitionComponent. This component enables the entity to trigger state transitions based on animation events. It is formatted quite similar to the AnimationEventSoundComponent and looks like this:

<Definition xsi:type="MyObjectBuilder_AnimationEventStateTransitionComponentDefinition">
  <Id Type="MyObjectBuilder_AnimationEventStateTransitionComponent" Subtype="PalisadeGate" />
    <Event Name="DoorClosed" TransitionToState="Closed" />
    <Event Name="DoorOpened" TransitionToState="Open" />

As always, SubtypeId is the same as the CubeBlock definition.

Each event lists

the current state.
the state to transition to.

Setting up State Timer Component

A bonus component that exists in the system is the StateTimerComponent. It is not actually used in any of the doors, but it could be used for automated state transitions. It is easily possible, for example, to make a door that closes itself after 10 seconds. Its definition looks like this:

<Definition xsi:type="MyObjectBuilder_StateTimerComponentDefinition">
  <Id Type="MyObjectBuilder_StateTimerComponent" Subtype="PalisadeGate" />
    <Timer From="Open" To="Closing" Delay="10" />

It goes without saying that once again the SubtypeId must match the CubeBlock definition.

The StateTimerComponent can list as many timers as is desired, in this block’s case though, just making it close after 10 seconds is a good enough example.

the entity state that triggers it
indicates which state to transition to
the delay before it is triggered, in seconds.

Tying it all together

Finally, after all the components are defined, it is time to set up the entity container. The entity container defines which components make up an entity.

Setting up the entity container

In the entity container, we must specify which entity components are on the entity.

      <Component BuilderType="MyObjectBuilder_MedievalGridOwnershipComponent"/>
      <Component BuilderType="MyObjectBuilder_AccessPermissionComponent"/>
      <Component BuilderType="MyObjectBuilder_UseObjectsComponent" SubtypeId="PalisadeGate" ForceCreate="true" />
      <Component BuilderType="MyObjectBuilder_SubpartAnimationComponent" SubtypeId="PalisadeGate" ForceCreate="true" />
      <Component BuilderType="MyObjectBuilder_CubeBlockSubpartComponent" SubtypeId="PalisadeGate" ForceCreate="true" />
      <Component BuilderType="MyObjectBuilder_EntityStateComponent" SubtypeId="PalisadeGate" ForceCreate="true" />
      <Component BuilderType="MyObjectBuilder_StateAnimationComponent" SubtypeId="PalisadeGate" ForceCreate="true" />
      <Component BuilderType="MyObjectBuilder_StateUseObjectComponent" SubtypeId="PalisadeGate" ForceCreate="true" />
      <Component BuilderType="MyObjectBuilder_AnimationEventStateTransitionComponent" SubtypeId="PalisadeGate" ForceCreate="true" />
      <Component BuilderType="MyObjectBuilder_AnimationEventSoundComponent" SubtypeId="PalisadeGate" ForceCreate="true" />

Last but not least, even here, the SubtypeId is identical to the CubeBlock definition.

Each Component in the <DefaultComponents> list has

matches the TypeId of the entity component
the same SubtypeId as, you guessed it, the CubeBlock definition
must be true

Testing it out in the game

Now that all the elements are properly defined, the block should function in the game. Launch it, add the mod to the world, make sure it is set to offline if it is not uploaded to steam, then load the save and open the g-screen. It should be possible to search for the block and place it.


Animation System

One of the limitations of the animation system is that each animation is played from the anchor point. Anchor points are a fixed coordinate and cannot move. They also do not support child bones. This means that it is not possible to make animations that have a subpart be a child part of another subpart.

Appendix A: Interpolation equations

The supported interpolation equations have been obtained from this website: Tweener Documentation

It also provides a visual display of the interpolation style, though there are two sets that we do not fully support from the definitions at this time.

The full list:

Animation style Notes
Linear Fully supported. Usually boring visually.
QuadraticEaseIn, QuadraticEaseOut, QuadraticEaseInOut, QuadraticEaseOutIn Fully supported. Usually visually most pleasing effect.
CubicEaseIn, CubicEaseOut, CubicEaseInOut, CubicEaseOutIn Fully supported.
QuarticEaseIn, QuarticEaseOut, QuarticEaseInOut, QuarticEaseOutIn Fully supported.
QuinticEaseIn, QuinticEaseOut, QuinticEaseInOut, QuinticEaseOutIn Fully supported.
SinusoidalEaseIn, SinusoidalEaseOut, SinusoidalEaseInOut, SinusoidalEaseOutIn Fully supported.
ExponentialEaseIn, ExponentialEaseOut, ExponentialEaseInOut, ExponentialEaseOutIn Fully supported.
CircularEaseIn, CircularEaseOut, CircularEaseInOut, CircularEaseOutIn Fully supported.
ElasticEaseIn, ElasticEaseOut, ElasticEaseInOut, ElasticEaseOutIn Partially supported, it is currently not possible to specify the period and amplitude parameters.
BackEaseIn, BackEaseOut, BackEaseInOut, BackEaseOutIn Partially supported, it is currently not possible to specify the overshoot parameter.
BounceEaseIn, BounceEaseOut, BounceEaseInOut, BounceEaseOutIn Fully supported.

Appendix B: ModAPI interfaces

There are three ModAPI interfaces currently available for interacting with the entity components.

Please be aware these interfaces are liable to change in the future!


public interface IMyCubeBlockSubpartComponent
	/// <summary>
	/// Tries to get a subpart by name.
	/// </summary>
	/// <param name="name">Name of the subpart</param>
	/// <param name="subpart">Output of the subpart</param>
	/// <returns>True if subpart exists, false otherwise</returns>
	bool TryGetSubpart(string name, out VRage.ModAPI.IMyEntity subpart);

	/// <summary>
	/// Sets the transformation of a subpart, relative to its hinge position or bone.
	/// </summary>
	/// <param name="subpartName">Name of the subpart.</param>
	/// <param name="translation">Translation of the subpart.</param>
	/// <param name="orientation">Orientation of the subpart.</param>
	/// <returns>False if the specified subpart is not found, true otherwise.</returns>
	bool SetSubpartTransformation(string subpartName, Vector3 translation, Quaternion orientation);

	/// <summary>
	/// Get the subpart visibility state.
	/// </summary>
	/// <param name="name">Name of the subpart.</param>
	/// <param name="isVisible">Visible or not.</param>
	/// <returns>True if the subpart was found, false otherwise.</returns>
	bool TryGetSubpartVisibility(string name, out bool isVisible);

	/// <summary>
	/// Sets the subpart visibility state.
	/// </summary>
	/// <param name="name">Name of the subpart.</param>
	/// <param name="isVisible">Visible or not.</param>
	/// <returns>True if the subpart visibility was changed, false otherwise.</returns>
	bool SetSubpartVisibility(string name, bool isVisible);

	/// <summary>
	/// Get the subpart physics enabled state.
	/// </summary>
	/// <param name="name">Name of the subpart.</param>
	/// <param name="isPhysicsEnabled">Physics enabled or not.</param>
	/// <returns>True if the subpart was found, false otherwise.</returns>
	bool TryGetSubpartPhysicsEnabled(string name, out bool isPhysicsEnabled);

	/// <summary>
	/// Sets the subpart physics enabled state.
	/// </summary>
	/// <param name="name">Name of the subpart.</param>
	/// <param name="isPhysicsEnabled">Physics enabled or not.</param>
	/// <returns>True if the subpart Physics was changed, false otherwise.</returns>
	bool SetSubpartPhysicsEnabled(string name, bool isPhysicsEnabled);


public delegate void OnStateChangedDelegate(string oldState, string newState);

public interface IMyEntityStateComponent
    	/// <summary>
    	/// The state the entity is currently in.
    	/// </summary>
    	string CurrentState { get; }

    	/// <summary>
    	/// Transition to another state.
    	/// </summary>
    	/// <param name="newState">The new state to transition to.</param>
    	/// <returns>False if transition failed, true otherwise.</returns>
    	bool TransitionTo(string newState);

    	/// <summary>
    	/// Registers the passed function as a callback to be called when the state is changed
    	/// </summary>
    	void RegisterForStateChange(Medieval.ModAPI.Components.Entity.OnStateChangedDelegate eventDelegate);

    	/// <summary>
    	/// Unregisters the passed function as a callback to be called when the state is changed
    	/// </summary>
    	void UnregisterForStateChange(Medieval.ModAPI.Components.Entity.OnStateChangedDelegate eventDelegate);


public interface IMySubpartAnimationComponent
    	/// <summary>
    	/// Playback speed of the animation.
    	/// </summary>
    	float PlaybackSpeed { get; set; }

    	/// <summary>
    	/// Name of the active animation.
    	/// </summary>
    	string ActiveAnimation { get; }

    	/// <summary>
    	/// Progress, in seconds, through the active animation.
    	/// </summary>
    	float ActiveAnimationTime { get; set; }

    	/// <summary>
    	/// Duration, in seconds, of the active animation.
    	/// </summary>
    	float ActiveAnimationDuration { get; }

    	/// <summary>
    	/// Plays the specified animation.
    	/// </summary>
    	/// <param name="name">Name of the animation to play.</param>
    	/// <param name="time">The time of the animation to start at.</param>
    	/// <returns>True if the animation was found, false otherwise.</returns>
    	bool PlayAnimation(string name, float time = 0);

    	/// <summary>
    	/// Checks if an animation is playing. If the name parameter is specified, checks if that animation is playing.
    	/// If the name parameter is empty, checks if any animation is playing.
    	/// </summary>
    	bool IsPlayingAnimation(string name = null);

    	/// <summary>
    	/// Stops the animation. If the name parameter is specified, it checks if that animation is playing and stops it.
    	/// If the name parameter is empty it stops the current animation.
    	/// </summary>
    	void StopAnimation(string name = null);

    	/// <summary>
    	/// Registers the passed function as a callback to be called when an event is triggered.
    	/// </summary>
    	void RegisterForEvents(Medieval.ModAPI.Components.Entity.OnEventTriggeredDelegate eventDelegate);

    	/// <summary>
    	/// Unregisters the passed function as a callback to be called when an event is triggered.
    	/// </summary>
    	void UnregisterForEvents(Medieval.ModAPI.Components.Entity.OnEventTriggeredDelegate eventDelegate);