Monday, January 5, 2009

Tutorial 4: writing a vector class (excitement)

We want to make the ship fly around, but how does this work? Well it's the same control scheme that's been confusing people since Asteroids - the ship maintains a certain velocity, that can only be changed by applying a force (thrust) in the direction the ship is facing. Sounds like physics. Sounds like vectors.

Vectors? If you don't know what I'm talking about, you have some serious reading to do, because I'm not doing a maths tutorial here. This is programming. Yes it is.

I want to program a useful little Vector2D class. Where to put it? Yes, another package! Now we use the Math class within flash for all our basic Mathematic needs (like squareRoot and power etc.) but not Vector2D. So I'm going to create a package that is quintessentially British: Maths. If you're worried that you might get confused between Maths and Math, call it myMath or squirrelEggs, though I do want to point out that logical names are by far the best. You might think sometimes that my function names are stupidly long, but I prefer a long name than a name I can't remember. Generally abbreviations should be avoided for this reason.

Brazil's a brilliant film

I'm going to be using this Vector2D class a lot, so I want to make it as simple to use as possible. That's why I'm going to use setters and getters for things like angle and length. So what are these damn things?

Well getters and setters are just function calls that do not need arguments. Also, you don't actually need a variable within the class for the getters and setters to get and set. For example, my Vector2D class will not have an angle variable, but you'll be able to get it and set it all the same (that's all done by playing with the x and y values).

I'm going to be extending the Point class, because it shares a lot of functionality with vectors (as in, it has an x and y value, as well as length). We just want to add some stuff.

Point happily provides us with a get length function, but not a set length function. What it does is provide us with a normalize function which allows you to set the length to anything you want. This is not what normalise means (besides, they spell it normalize the Yankee bastards). Infuriatingly my text editor refuses to accept that normalise with an s is the correct spelling.

So what I've done is given length a setter, and I've created my own noramalise function which, as it should, reduces the vector to the length of 1 unit. Note that normalised and normal vectors are not the same (we'll be using normal vectors, of sorts, further down the line). Confused yet? Hope not.

I've made a couple of static functions. Remember that? In fact, go read that link about methods you ignored earlier when I was talking about getters and setters, it tells you all about them (calls them class methods). I'm using them mostly to provide me with alternative constructors (to create a vector based on an angle, or copying another vector).

Quick note to somebody better than me - I would love to be able to override the Point.clone() function, but of course I would need it to return a Vector2D as opposed to a Point, and overriding functions have to return the same type. Is there some clever solution to this? I've vaguely read about overloading, but only to the point that it's not supported in ActionScript 3.

For those of you who don't know what override means, don't worry, we'll get to it soon.

Right, most of the rest of this tutorial will take place in comment form - here is the entire Vector2D class in all its wonderfulness, and notice my funky commenting scheme for clarity. As the classes get bigger and more complex, this kind of clarity is really useful. I've tried to get things right based on the intimidating programming conventions page...

Hopefully the comments in this code should explain everything (if you're confused about anything, feel free to ask me questions in the blog comments).

////////////////////////////////////////////////////////////////////////////////
//
// AWOOGAMUFFIN
// Copyright er... I don't understand copyright. I think uploading this code
// possibly robs me of all rights. Just be nice - if you use my code, mention
// where you got it from, yeah?
//
// NOTICE: Awoogamuffin permits you to use, modify, and distribute this file
// in accordance with the terms of the license agreement accompanying it.
//
// Hahahaha, I just copied that from the conventions page (like everything I
// do). There's no license agreement. Stop looking for it.
//
// VECTOR2D - wonderful stuff with maths
//
////////////////////////////////////////////////////////////////////////////////

package Maths //call it myMath if you don't like the subtle British s
{
import flash.display.*;
import flash.geom.Point;

/* I've decided to make Vector2D extend Point so that I can use the x and y
values it inherits */

public class Vector2D extends Point
{
//--------------------------------------------------------------------------
//
// STATIC METHODS (CLASS METHODS)
//
//--------------------------------------------------------------------------

/* that funky title you saw up there? Got that idea from the AS3 coding
conventions page... I think it looks cool. I've also tried to respect
the order they suggest but I get confused... the url for the convention
page is http://opensource.adobe.com/wiki/display/flexsdk/Coding+Conventions */

/* these methods are basically alternative constructors, hence the term "new"
before each function name */

//so we'd have: var myVector:Vector2D = Vector2D.newVectorFromAngle(90)

//returns a normal vector pointing in the directions specified by angle
public static function newVectorFromAngle(angle:Number):Vector2D
{
var radians:Number = (angle - 90) * Math.PI / 180;
return new Vector2D(Math.cos(radians), Math.sin(radians));
}

public static function newVectorClone(vector:Vector2D):Vector2D
{
return new Vector2D(vector.x, vector.y);
}

//--------------------------------------------------------------------------
//
// CONSTRUCTOR
//
//--------------------------------------------------------------------------

public function Vector2D(x = 0.0, y = 0.0)
{
this.x = x;
this.y = y;
}

//--------------------------------------------------------------------------
//
// GETTERS AND SETTERS
//
//--------------------------------------------------------------------------

/* I'm using getters and setters for dealing with vector length to make it
easier to code with vectors in the future */

//returns the angle in which a vector is facing...
public function get angle():Number
{
/* first you have to normalise. If you know the vector is normal it's more
efficient to directly call getAngleFromNormal */
var normalisedVector:Vector2D = newVectorClone(this);
if(normalisedVector.normalise()) return normalisedVector.getAngleFromNormalised();
else return 0;
}

public function set angle(angle:Number):void
{
var temp:Vector2D = newVectorFromAngle(angle);
temp.multiply(length);
this.copyVector(temp);
}

/* The Point class already has a length property, but it's read only, and I
want to be able to set it. The Point class has function called normalize()
which will set it to any length, but that's just wrong! A normalised vector
specifically has lengh 1, so I've written my own normalise function, and it's
spelt correctly too! see http://mathworld.wolfram.com/NormalizedVector.html */
public function set length(value:Number)
{
//see below for the normalise and multiply functions
if (normalise()) multiply(value);
}

//getting the length squared is less espensive because it avoids Math.sqrt
public function get lengthSquared():Number
{
if (x == 0 && y == 0) return 0;
else return x * x + y * y;
}

//--------------------------------------------------------------------------
//
// METHODS
//
//--------------------------------------------------------------------------

// does anyone know if it's possible to override + * and = operands?

/* add is already a function for Point - it produces a new Point, but here I
just want to modify the current vector */
public function addVector(vector:Vector2D)
{
x += vector.x;
y += vector.y;
}
//Point has a function offset(x:Number, y:Number). Plus is just for two vectors

public function multiply(factor:Number)
{
x *= factor;
y *= factor;
}

/* Again, Point already has an equals function, but it's used for comparison as
opposed to assigning values... */
public function assignValues(x:Number, y:Number)
{
this.x = x;
this.y = y;
}

public function copyVector(vector:Vector2D)
{
x = vector.x;
y = vector.y;
}

//dot multiplication used for getting projection and whatnot
public function dot(vector:Vector2D):Number
{
return x * vector.x + y * vector.y;
}

//I'm only writing this because I can't override clone()
public function cloneVector2D():Vector2D
{
return new Vector2D(x, y);
}

//sets vector length to one, or returns false if the vector is 0 length
public function normalise():Boolean
{
if (!(x == 0 && y == 0))
{
var vectorLength:Number = Math.sqrt(x * x + y * y);
x /= vectorLength;
y /= vectorLength;
return true;
}
else
{
return false;
}
}

//returns the angle in which a normalised vector is facing...
//stupidly long name, but it's logical and I can remember it
public function getAngleFromNormalised():Number
{
/* look up acos and asin to learn how this works... it's all fun with
trigonometry! */
var temp:int;
Math.asin(-x) > 0 ? temp = -1 : temp = 1;
var radians:Number = Math.acos(-y);
return (radians * 180 / Math.PI) * temp;
}
}
}


Right, so now we want to check that all these functions work. So in the SpaceGame.as file I make some Vector2Ds and call their methods, then trace the results to see if they are as they should be. They are! Wahooo! I've commented the trace results but feel free to check it out yourself...

//THE DOCUMENT CLASS
package
{

import flash.display.*;
import flash.events.*;

import ships.Ship;
import Maths.Vector2D;

public class SpaceGame extends MovieClip
{
/*this will allow other classes to access the instance
of the document class created at the beginning of the game */
private static var _instance:SpaceGame;

public static function get instance():SpaceGame
{
return _instance;
}

public function SpaceGame()
{
_instance = this;

var myShip:Ship = new Ship();
addChild(myShip);

//testing Vector class:

//testing constructors
var vector1:Vector2D = new Vector2D();
trace("Basic constructor: "+vector1); //calls the Point toString() function which returns (x=0, y=0)

var vector2 = new Vector2D(3,-4);
trace("Vector 2: "+vector2); //(x=3, y=-4)
trace("Vector2 length: "+vector2.length); //5. basic Pythagorus at work here
trace("Vector2 length squared: "+vector2.lengthSquared); //25. Yup. that's 5 * 5

vector1.assignValues(0,3);
trace("Vector1 assigned values: "+vector1); //(x=0, y=3)

vector1.addVector(vector2);
trace("vector1 += vector2: "+vector1);

vector1 = Vector2D.newVectorFromAngle(45);
trace("normalised vector at 45 degrees: "+vector1); //(x=0.7071067811865476, y=-0.7071067811865475) - remember this

vector1 = Vector2D.newVectorClone(vector2);
trace("vector1 constructor copying vector2: "+vector1); //(x=3, y=-5)

vector1.assignValues(0,2.3456);
vector1.length = 6.0;
trace("vector1 assigning length to 6: "+vector1); //(x=0, y=6)

vector1.multiply(2);
trace("vector1 * 2: "+vector1);

vector1.angle = 45;
trace("vector1 angle assigned to 45: "+vector1); //(x=8.485281374238571, y=-8.48528137423857)
trace("vector1 length: "+vector1.length); //12
trace("vector1 angle: "+vector1.angle); //45.00000000000001

vector1.normalise();
trace("vector1 normalised: "+vector1);//(x=0.7071067811865476, y=-0.7071067811865475) - remember from earlier?
trace("vector1 lenght: "+vector1.length); //1. The distance a normalised vector should be :-D

trace("vector1 dot vector2: "+vector1.dot(vector2)); //4.949747468305833
}
}
}


Great, we have a lovely new Vector2D class, so its time to use it, right? We want to make this damn ship move, no? That'll be for the next tutorial!

6 comments:

  1. You should really be using the "this" keyword when referencing class methods or class variables. It's a convention (at least, where I'm from), and it helps separate local variables from class variables. Also, when following code, it helps the reader better understand from where things are coming. =)

    ReplyDelete
  2. Interesting - I only use this when I the function takes arguments that will change the variables of the same name within the class (for example in the constructor). Are you saying I should always use this when referencing a class variable, or even a function?

    I like the idea - I see what you mean about making the code more readable.

    By the way, you do realise this blog hasn't been update for ages, right? It's all happening on the Dregs of War development blog, but I'm not talking about the actual code on that. Once it's finished I might write some more tutorials.

    A final note: are you Nidht from the flashkit forums?

    ReplyDelete
  3. You've guessed correctly, sir! I am one in the same; Nidht from Flash Kit.

    I realize your progress is moving more onto Dregs of War, but it was the code here that intrigued me when I was seeking an answer to better understanding some of the nuances of Adobe Flash. I realized the blog ended short, and long ago, but I thought I might as well go through the entire tutorial, learn something new, and then progress on my own. =)

    I am also following the Dregs of War blog, and I would love to see sample code on some of the fun things you've been doing in that game.

    Back to the topic at hand:

    You should use "this" in any context from within the class where you are referencing class vars or class methods. You do it sometimes, but it's not consistent - which is fine! However, it just helps with overall readability and prevent confusion.

    For instance:

    temp.multiply(length)

    should be:

    temp.multiply(this.length)

    For a moment, I was confused, thinking "length" was a variable being passed to the function and that you missed declaring it.

    Not a HUGE deal, but it's a nice thing to see, especially when a reader is following a tutorial (a great tutorial, at that). =)

    ReplyDelete
  4. I have another quick question.

    Why use this static function:

    public static function newVectorClone(vector:Vector2D):Vector2D
    {
    return new Vector2D(vector.x, vector.y);
    }

    which takes an existing Vector2D object, when you can just call this function:

    public function cloneVector2D():Vector2D
    {
    return new Vector2D(x, y);
    }

    ON the object, itself, since you already have one? =)

    ReplyDelete
  5. No reason - just two ways of accomplishing the same thing I guess... One is treated as a constructor, the other as a cloner. Admittedly it's a little silly.

    ReplyDelete