Monday, November 3, 2008

AS3 06 - Time based animation, My clownShip

Previously on X-Men, we see Professor X-Xavier being beaten to pulp… we managed to get a character on screen (clownShip) and control it with the keyboard (left and right arrow). We learned how to listen for events and call functions whenever the events occur. However we noticed that it’s not ‘behaving’ like a normal ship in any arcade game. It’s sluggish and there is a strange interval between the first and second keys before it starts to move continuously. Now lets make it smoother. Instead of moving the ship with each time the key press is detected, why not we create a switch which alternates between ‘move’ and ‘stop’… sounds confusing? Why not we have a switch which sets to ‘move’ when we first hit the arrow key (KEP_DOWN) and ‘stop’ when the key is release (KEY_UP). Let’s call the switch leftdown and rightdown and declare it as …

var leftdown:Boolean = false;
var rightdown:Boolean = false;

What’s a Boolean? It can only hold ‘true’ or ‘false’ which is suitable for what we want now. If we hold the left arrow down, then we set the leftdown to ‘true’, if we release the key (let go), we set the leftdown to ‘false’. In order to do that we need to change the event listeners for the keyboard.


stage.addEventListener(KeyboardEvent.KEY_DOWN, listenKeyDown);
stage.addEventListener(KeyboardEvent.KEY_UP, listenKeyUp);

function listenKeyDown(event:KeyboardEvent)
{
if (event.keyCode == 37) { //Left arrow
leftdown = true;
}
if (event.keyCode == 39) { //Right arrow
rightdown = true;
}
}


function listenKeyUp(event:KeyboardEvent)
{
if (event.keyCode == 37) { //Left arrow
leftdown = false;
}
if (event.keyCode == 39) { //Right arrow
rightdown = false;
}
}

That should get rid of the loop interval at the start.

Now that we have the alternating switch for the movement, we need to actually move the clownShip based on the Boolean value. Here we add another event listener to run at each frame with the Event.ENTER_FRAME and call the moveClownShip function.

addEventListener(Event.ENTER_FRAME, moveClownShip);
function moveClownShip(event:Event) {
if (leftdown == true){
clownShip.x = clownShip.x - 10
}
if (rightdown == true){
clownShip.x = clownShip.x + 10
}
}

Try to test the move now. It’s smoother now isn’t it? There is something still not right here. What we did so far is program based on frame rate. What this means is the clownShip’s movement is directly linked to the number of frames this Flash document is currently running in per second. Basically it’s 24 FPS (frames per second). Where did I get this? It’s a default framerate set by Flash if you start a new document which can be seen at the document properties section.



Basically the Event.ENTER_FRAME runs every frame so the higher the framerate the more times this is triggered. Try increasing the FPS and run the test movie. Move the clownShip around. It moves faster!!!... not smoother. . What we are going to do next is to alter the programming structure to base on time instead of frame. This is very important as this enables control of the animation based on the time passed no matter how slow it takes for the PC to render the image. Duh? Ok… try this… when you play single player Diablo (that’s so olddd…) on an old PC with below minimum requirement, the games slows down but you still get all the animation frames being viewable and all… (much like when you set the Flash FPS to 1) but when you play multiplayer Diablo it skips the frames and keep up with time yeah… that’s it… whatever…. Ok.. I think DoTA would be a better example. Once a while you will experience frame skipping within the game. This needs to happen because the game is trying to keep up (sync) with the rest of the clients (players) based on time. If it’s based on frame, the moment one PC slows down, the synchronization is gone. Am I making any sense???... aii… forget it

First we need a placeholder (variable) to hold time values. Let’s call it timeVal and define it…

var timeVal:int;

Now we need to alter the moveClownShip function as below…

function moveClownShip(event:Event) {

var speed:Number = 110.0;
var timeDiff:int = getTimer()-timeVal;
timeVal += timeDiff;

if (leftdown == true){
clownShip.x -= speed*timeDiff/1000;
}

if (rightdown == true){
clownShip.x += speed*timeDiff/1000;
}
}

Confused already? Instead of fixing the movement to 10 pixels per frame (previously), now we get the time difference between each frame

var timeDiff:int = getTimer()-timeVal;

and move the clownShip to their appropriate horizontal location based on the time passed from previous frame.

clownShip.x -= speed*timeDiff/1000;

Why divide it by 1000? Well, getTimer() returns the current time value in milliseconds so I am merely converting it back to seconds. Just like 1 meter = 100 centimeter = 1000 milimeters, hmm.. that should give us 1 second = 1000 miliseconds. With the speed value of 110, that means I am trying to make the clownShip move 110 pixel per second. Now try running the test movie at FPS of 1,30,60,120 moving the clownShip across the screen. Note that they all took approx 5 seconds to complete. This would be completely different if the movement was based on frame instead of time. Also note that at around 60 frames per seconds it smooth as silk  Higher framerates are have not so noticeable increase in smoothness unless you have superior/bionic eye/brain. For your info.. your cinema operates at 24fps, television 30 fps and you monitor / LCD probably gets 90 Hertz as the refresh rate. So your fps is actually capped by the display device you are using . Also films in cinema achieve such smoothness because of motion blur.. a topic which I will only go into when I talk about Lightwave 3D some err… . Anyway if all things go right, the final stuff should look something like this.


The whole code is as below :

var clownShip:myShip = new myShip;
clownShip.x = stage.stageWidth/2;
clownShip.y = stage.stageHeight - 40;
var leftdown:Boolean = false;
var rightdown:Boolean = false;
var timeVal:int; // animation time

addChild(clownShip);

stage.addEventListener(KeyboardEvent.KEY_DOWN, listenKey);
stage.addEventListener(KeyboardEvent.KEY_UP, listenKeyUp);

function listenKey(event:KeyboardEvent)
{
if (event.keyCode == 37) { //Left arrow
leftdown = true;
}
if (event.keyCode == 39) { //Right arrow
rightdown = true;
}
}


function listenKeyUp(event:KeyboardEvent)
{
if (event.keyCode == 37) { //Left arrow
leftdown = false;
}
if (event.keyCode == 39) { //Right arrow
rightdown = false;
}
}

addEventListener(Event.ENTER_FRAME, moveClownShip);

/*
function moveClownShip(event:Event) {
if (leftdown == true){
clownShip.x = clownShip.x - 10
}
if (rightdown == true){
clownShip.x = clownShip.x + 10
}
}
*/

function moveClownShip(event:Event) {

var speed:Number = 110.0;
var timeDiff:int = getTimer()-timeVal;
timeVal += timeDiff;

if (leftdown == true){
clownShip.x -= speed*timeDiff/1000;
}

if (rightdown == true){
clownShip.x += speed*timeDiff/1000;
}
}

One more thing the /* and */ serves as the opening and closing of comments in AS3.

Well, I hope you grab the difference between frame based and time based animation programming. This tutorial has been quite confusing to write for some. Next I am going to talk about how we are going to rearrange these stuffs into classes. Well, you can’t actually escape from classes, from school classes to C++ … classes are there to stay. We need them to keep things organize if we are going to do anything bigger than moving single object with our thoughts and balance ourselves with one hand around and also it’s the OOP (object oriented programming – I can see you all yawning now) way.

3 comments:

Anonymous said...

A very good tutorial. Hope you can continue this effort and share your experience/expertise in flash AS3 game programming. Cheers!

WinterGlass said...

Thanks :) I am putting in a syntax highlighter, hope it works so that it's easier for reader to view the codes instead of looking all the same with the post.

Thanks a lot. I guess you are the first to put in any comments :)

runkeeper said...

I have read your blog its very attractive and impressive. I like it your blog.