Spring Cleaning

So with the last article you probably have enough information now to add 3D Animated objects to your current project but unfortunately the code is in bad shape. We have the main class dealing with multiple concerns. We also didn’t really use the animations in a way that most projects would. I.E. We aren’t actually moving the model when walking. So this article will allow me to scratch the itch of cleaning up the code a little, add more realistic animation sequences, and use the idle animation.

Legend

I’m using this following to identify different types of commands or actions.

Menus: Menu1->Menu2->Menu3 A series of menu clicks.

Keys: Key One or more keys to press.

Properties: Property Name A property of an object.

Value: “Value” The value to set a property too.

Moving Day

I usually start my refactoring in small pieces so the first thing I want to do is move the cube related code into a single class. Lets call this class “Cube” and create it in a folder called “entity”.

Then move all of the cube related code to the new class so it looks like this:

package entity;

import hxd.Key;
import hxd.res.Model;
import h3d.prim.ModelCache;
import h3d.anim.Animation;
import h3d.scene.Object;

class Cube extends Object {
	private var walkAnimation:Animation;
	private var jumpAnimation:Animation;

	public function new(cache:ModelCache, model:Model) {
		super();

		var baseModel = cache.loadModel(model);
		walkAnimation = cache.loadAnimation(model, "Walk");
		jumpAnimation = cache.loadAnimation(model, "Jump");
		addChild(baseModel);
	}

	public function update(dt:Float) {
		if (Key.isPressed(Key.SPACE)) {
			playAnimation(jumpAnimation);
		} else if (Key.isPressed(Key.UP)) {
			playAnimation(walkAnimation);
		}
	}
}

Now in the main class we can delete all of the old code and replace it with the following:

import entity.Cube;

class Main extends hxd.App {
	private var cube:Cube;

	override function init() {
		s3d.lightSystem.ambientLight.set(0.74, 0.74, 0.74);

		var cache = new h3d.prim.ModelCache();
		cache.loadLibrary(hxd.Res.cube);

		cube = new Cube(cache, hxd.Res.cube);

		s3d.addChild(cube);

		cache.dispose();

		new h3d.scene.CameraController(s3d).loadFromCamera();
	}

	override function update(dt:Float) {
		super.update(dt);
		
		cube.update(dt);
	}

	static function main() {
		hxd.Res.initEmbed();

		new Main();
	}
}

Make sure everything still works and we will keep going.

Animation

Next thing I noticed is duplicate code for the animations. Right now we have a variable for each animation and with the current code the number will grow as we add more animations. So let’s put the animations into a hash map based on their name and create a method to load animations by name. In the end it should look like this:

package entity;

import hxd.Key;
import hxd.res.Model;
import h3d.prim.ModelCache;
import h3d.anim.Animation;
import h3d.scene.Object;

class Cube extends Object {
	private var animations:Map<CubeState, Animation> = [];

	public function new(cache:ModelCache, model:Model) {
		super();

		var baseModel = cache.loadModel(model);

		loadAnimation(cache, model, Walk);
		loadAnimation(cache, model, Jump);
		loadAnimation(cache, model, Idle);

		addChild(baseModel);
	}

	function loadAnimation(cache:ModelCache, model:Model, animationName:CubeState) {
		var animation:Animation = cache.loadAnimation(model, animationName.getName());
		animations.set(animationName, animation);
	}

	public function update(dt:Float) {
		if (Key.isPressed(Key.SPACE)) {
			playAnimation(animations[Jump]);
		} else if (Key.isPressed(Key.UP)) {
			playAnimation(animations[Walk]);
		}
	}
}

enum CubeState {
	None;
	Walk;
	Jump;
	Idle;
}

Moving On Up

Now let’s update our cube to move forward while animating. First lets add a state variable to track the current animation state and two variables that will define our movement speeds. The values were picked though trial and error.

private var state:CubeState = None;

private var currentTranslationSpeed = 0.01;
private var currentRotationSpeed = 1.0;

In the update method we can add specific Key checks. We will move the cube forward when the Up arrow is pressed and rotate when the Right or Left arrows are pressed:

public function update(dt:Float) {
	if (Key.isPressed(Key.SPACE)) {
		stopAnimation(true);
		state = Jump;
	}

	if (Key.isDown(Key.LEFT)) {
		if (state == Walk) {
			stopAnimation(true);
		}
		rotate(0, 0, -currentRotationSpeed * dt);
	} else if (Key.isDown(Key.RIGHT)) {
		if (state == Walk) {
			stopAnimation(true);
		}
		rotate(0, 0, currentRotationSpeed * dt);
	}

	if (Key.isDown(Key.UP)) {
		if (state != Walk) {
			state = Walk;
			var animation = playAnimation(animations[Walk]);
			animation.onEvent = moveModel;
			for (i in 0...animation.frameCount) {
				animation.addEvent(i, "" + i);
			}
		}
	} else if (Key.isReleased(Key.UP)) {
		stopAnimation(true);
		state = None;
	}
}

We are also adding a new method moveModel. This method will move the model every frame.

function moveModel(data:String) {
	var currentPosition:Vector = getTransform().getPosition();
	var direction:Vector = getTransform().getDirection();
	direction.normalize();
	setPosition(
		currentPosition.x + (direction.x * currentTranslationSpeed), 
		currentPosition.y + (direction.y * currentTranslationSpeed), 
		currentPosition.z
	);
}

Keys

Hmmm. Seems to be a lot of duplicate key codes and it will probably only get worse. Lets move the key codes into variables that will make it easier to change the values.

private var forward:Int = Key.UP;
private var jump:Int = Key.SPACE;
private var left:Int = Key.LEFT;
private var right:Int = Key.RIGHT;

And update the update method to use the values.

public function update(dt:Float) {
	if (Key.isPressed(jump)) {
		stopAnimation(true);
		state = Jump;
		if (Key.isDown(forward)) {
			setupMovementAnimation(state);
		} else {
			playAnimation(animations[Jump]).onAnimEnd = resetIdleAnimation;
		}
	}

	if (Key.isDown(left)) {
		if (state == Walk) {
			stopAnimation(true);
		}
		rotate(0, 0, -currentRotationSpeed * dt);
	} else if (Key.isDown(right)) {
		if (state == Walk) {
			stopAnimation(true);
		}
		rotate(0, 0, currentRotationSpeed * dt);
	} else if ((Key.isReleased(right) || Key.isReleased(left)) && Key.isDown(forward) && state == Walk) {
		setupMovementAnimation(state);
	}

	if (Key.isDown(forward) && state != Jump) {
		if (state != Walk) {
			state = Walk;
			setupMovementAnimation(state);
		}
	} else if (Key.isReleased(forward)) {
		stopAnimation(true);
		state = None;
	}
}

Idle

Finally, let’s add some code to use the Idle animation of the cube doesn’t move for a while. First we will add some variables to track the idle time to wait before running a little animation.

private var rand:Rand = Rand.create();
private var currentIdleTimeSec:Float = 0;
private var idleTimeSec:Float = 0;

Then we need make changes to the update method to reset the idle timer when the player moves and play an animation if the idle time is exceeded.

public function update(dt:Float) {
	if (Key.isPressed(jump)) {
		stopAnimation(true);
		state = Jump;
		if (Key.isDown(forward)) {
			setupMovementAnimation(state);
		} else {
			playAnimation(animations[Jump]).onAnimEnd = resetIdleAnimation;
		}
		resetIdleTimer();
	}

	if (Key.isDown(left)) {
		if (state == Walk) {
			stopAnimation(true);
		}
		rotate(0, 0, -currentRotationSpeed * dt);
		resetIdleTimer();
	} else if (Key.isDown(right)) {
		if (state == Walk) {
			stopAnimation(true);
		}
		rotate(0, 0, currentRotationSpeed * dt);
		resetIdleTimer();
	} else if ((Key.isReleased(right) || Key.isReleased(left)) && Key.isDown(forward) && state == Walk) {
		setupMovementAnimation(state);
	}

	if (Key.isDown(forward) && state != Jump) {
		if (state != Walk) {
			state = Walk;
			setupMovementAnimation(state);
		}
		resetIdleTimer();
	} else if (Key.isReleased(forward)) {
		stopAnimation(true);
		state = None;
		resetIdleAnimation();
	}

	if (state == None) {
		currentIdleTimeSec += dt;
		if (currentIdleTimeSec >= idleTimeSec) {
			state = Idle;
			playAnimation(animations[Idle]).onAnimEnd = resetIdleAnimation;
		}
	}
}

Finally the update changes introduce two new methods, one to reset the idle time and other to reset the idle animation.

function resetIdleTimer() {
    currentIdleTimeSec = 0;
    idleTimeSec = 10 + (10 * rand.rand());
}

function resetIdleAnimation() {
    state = None;
    stopAnimation(true);
    resetIdleTimer();
}

Wrap Up

And with that I think we will wrap up this tutorial. We have a complete development pipeline now and you should be able to use the exact same workflow to develop more complex animations and more complex models.

The code can be found here: https://gitlab.com/gillman/heaps-model-animation-tutorial/-/tree/v4

I’ll leave it to the reader to refactor the Cube class again and pull out common functionality when they are ready to have more than one object in their game.

Happy coding!