Skip to content

Animation Controlling

Rotciv Ocnarb edited this page Nov 4, 2018 · 2 revisions

Animation Controllers are a thing that helps a lot when organizing how your character will move upon certain sittuations. Unity and Unreal has a very cool way of dealing with animation controllers, Finite State Machines, and i did manage to make use of it!

The software we are using the model the State Machines is Finite State Machine Editor, you can download it, make your state machines, and export them as a XML file.

To create an Animation Controller, you will need to create a class that is your Animation Controller! It is usualy set to work in pair with an GameObject wich is the object that has the the Spriter Animation.

Create a new class make it an extension of DefaultAnimationController

public class TestAnimationController extends DefaultAnimationController{

	public TestAnimationController(GameObject personagem) {
		super(personagem);
	}

}

The next thing you want to do, is set up your States and Transitions, both of them will be Enums.

The State enum needs to extend State<T extends DefaultAnimationController>. Most of the time, you will simply type it as your own class, for example:

	public TestAnimationController(GameObject personagem) {
		super(personagem);
	}
	
	public enum MyState implements State<TestAnimationController>{
		
		TEST_STATE {
			public Player anim(TestAnimationController controller) {
				return null;
			}
			
		}
		
	}

}

All the enums in the State must implement the anim(TestAnimationController controller) method, this will be the one telling wich animation should be playing at that state.

Next is the Trigger Enum:

	public enum MyTrigger implements Trigger<Personagem>{
		TEST_TRANSITION{
			public boolean accept(Personagem gameObject, Animation stateAnimation) {
				return false;
			}
			
		}
	}

The typing of this enum is not the Controller class, but the actual GameObject class that is your player, and it implements the accept(GameObject gameObject, Animation stateAnimation) method, that returns true when it should fire.

You can create all your animations in the constructor class. For the example, i am creating 4 animations:

	PlayerTweener idle_run;
	Player jumpBegin;
	Player jumpEnd;
	Player jumpLoop;
	Personagem personagem;
	
	public TestAnimationController(Personagem personagem) {
		super(personagem);
		this.personagem = personagem;
		
		idle_run = personagem.getSpriterAnimation().createInterpolatedAnimation("Idle", "Run", 0);

		jumpBegin = new Player(personagem.getPlayer().getEntity());
		jumpBegin.setAnimation("JumpBegin");
		jumpBegin.setTime(0);
		
		jumpLoop = new Player(personagem.getPlayer().getEntity());
		jumpLoop.setAnimation("JumpLoop");
		
		jumpEnd = new Player(personagem.getPlayer().getEntity());
		jumpEnd.setAnimation("JumpEnd");
	}

Next i will assign the animations to the states:

	public enum MyState implements State<TestAnimationController>{
		IDLE_RUN{
			public Player anim(TestAnimationController controller) {
				controller.idle_run.setWeight(controller.personagem.endVelocityX / controller.personagem.getSpeed());
				return controller.idle_run;
			}
		},
		JUMP_BEGIN{
			public Player anim(TestAnimationController controller) {
				return controller.jumpBegin;
			}
		},
		JUMP_LOOP{
			public Player anim(TestAnimationController controller) {
				return controller.jumpLoop;
			}
		},
		JUMP_END{
			public Player anim(TestAnimationController controller) {
				return controller.jumpEnd;
			}
		};
	}

The next thing you may want to do, before assigning the transitions, is actually creating the StateMachine, so you can see what you will need as Transitions, here i have an example:

Here you can see that you will need to put the state names EXACTLY equal as the Enum names (same for transitions). So after doing this, you will see that we will need the Transitions:

  • REQUEST_JUMP
  • BEGIN_FINISH
  • STEP_ON_FLOOR
  • END_FINISH
  • ON_AIR

Each of those transitions need to be created as Enums, and set their respective activation requirements:

	public enum MyTrigger implements Trigger<Personagem>{
		REQUEST_JUMP {
			public boolean accept(Personagem personagem, Animation stateAnimation) {
				return personagem.requestJump;
			}
		},
		BEGIN_FINISH{
			public boolean accept(Personagem personagem, Animation stateAnimation) {
				return personagem.getPlayer().getTime() + personagem.getPlayer().speed*2 >= stateAnimation.length;
			}
		},
		CLOSE_FLOOR{
			public boolean accept(Personagem personagem, Animation stateAnimation) {
				return personagem.getDistanceToFloor() < 1;
			}
		},
		END_FINISH{
			public boolean accept(Personagem personagem, Animation stateAnimation) {
				return personagem.getPlayer().getTime() + personagem.getPlayer().speed*2 >= stateAnimation.length;
			}
		},
		ON_AIR{
			public boolean accept(Personagem personagem, Animation stateAnimation) {
				return personagem.getDistanceToFloor() > 1.5;
			}
		},
		STEP_ON_FLOOR{
			public boolean accept(Personagem personagem, Animation stateAnimation) {
				return personagem.isOnFloor();
			}
		};
	}

At this point you can see that you can create variables in your Player class to help you with transitions.

But now we actually need to tell awesome-libgdx that we are reading a StateMachine from a XML file with the method:

readXML(String fileName, EnumReader<State> stateReader, EnumReader<Trigger> transitionReader)

The fileName is the path to your controller (the .fsm file), the stateReader and transitionReader are classes that need to be implemented to read correctly the State Names found on the State Machine. A default solution would be:

		readXML("spriter/controller.fsm",
		new DefaultAnimationController.EnumReader<State>(){
			public State<?> read(String text) {
				return MyState.valueOf(text);
			}
		},
		new EnumReader<DefaultAnimationController.Trigger>() {
			public Trigger<?> read(String text) {
				return MyTrigger.valueOf(text);
			}
		});

And after all the animations are created and referenced on the Enums, don't forget to call the method:

finishConstructor();

so at the end the class should be something like this:

public class TestAnimationController extends DefaultAnimationController{

	PlayerTweener idle_run;
	Player jumpBegin;
	Player jumpEnd;
	Player jumpLoop;
	Personagem personagem;
	
	public TestAnimationController(Personagem personagem) {
		super(personagem);
		this.personagem = personagem;
		
		readXML("spriter/controller.fsm",
		new DefaultAnimationController.EnumReader<State>(){
			public State<?> read(String text) {
				return MyState.valueOf(text);
			}
		},
		new EnumReader<DefaultAnimationController.Trigger>() {
			public Trigger<?> read(String text) {
				return MyTrigger.valueOf(text);
			}
		});
		
		idle_run = personagem.getSpriterAnimation().createInterpolatedAnimation("Idle", "Run", 0);

		jumpBegin = new Player(personagem.getPlayer().getEntity());
		jumpBegin.setAnimation("JumpBegin");
		jumpBegin.setTime(0);
		
		jumpLoop = new Player(personagem.getPlayer().getEntity());
		jumpLoop.setAnimation("JumpLoop");
		
		jumpEnd = new Player(personagem.getPlayer().getEntity());
		jumpEnd.setAnimation("JumpEnd");
		
		finishConstructor();
	}
	
	public enum MyState implements State<TestAnimationController>{
		IDLE_RUN{
			public Player anim(TestAnimationController controller) {
				controller.idle_run.setWeight(controller.personagem.endVelocityX / controller.personagem.getSpeed());
				return controller.idle_run;
			}
		},
		JUMP_BEGIN{
			public Player anim(TestAnimationController controller) {
				return controller.jumpBegin;
			}
		},
		JUMP_LOOP{
			public Player anim(TestAnimationController controller) {
				return controller.jumpLoop;
			}
		},
		JUMP_END{
			public Player anim(TestAnimationController controller) {
				return controller.jumpEnd;
			}
		};
	}
	
	public enum MyTrigger implements Trigger<Personagem>{
		REQUEST_JUMP {
			public boolean accept(Personagem personagem, Animation stateAnimation) {
				return personagem.requestJump;
			}
		},
		BEGIN_FINISH{
			public boolean accept(Personagem personagem, Animation stateAnimation) {
				return personagem.getPlayer().getTime() + personagem.getPlayer().speed*2 >= stateAnimation.length;
			}
		},
		CLOSE_FLOOR{
			public boolean accept(Personagem personagem, Animation stateAnimation) {
				return personagem.getDistanceToFloor() < 1;
			}
		},
		END_FINISH{
			public boolean accept(Personagem personagem, Animation stateAnimation) {
				return personagem.getPlayer().getTime() + personagem.getPlayer().speed*2 >= stateAnimation.length;
			}
		},
		ON_AIR{
			public boolean accept(Personagem personagem, Animation stateAnimation) {
				return personagem.getDistanceToFloor() > 1.5;
			}
		},
		STEP_ON_FLOOR{
			public boolean accept(Personagem personagem, Animation stateAnimation) {
				return personagem.isOnFloor();
			}
		};
	}

}

After creating the class, simply create an instance of it on your player class, like this:

TestAnimationController controller = new TestAnimationController(this);

and at the update() method:

controller.update(delta);
animation.setPlayer(controller.getCurrentPlayer());

And that should make your player animation move accordingly to the state machine created!

There are also a few things you can add to your controller, like State Listeners, to listen to an entry or exiting of a state, and performing some action (put this on your controller constructor):

		state.configure(MyState.JUMP_BEGIN).onEntry(new Action() {
			public void doIt() {
				personagem.jump();
			}
		});

(Same for the onExit() method)

Clone this wiki locally