Python Vector
Foreword
If we want to make a game in Python, we will need vector math at some point. In most cases, a vector is a position. For example, in a 2D game it would be (x, y). In a 3D game it would be (x, y, z). A common example for the need of vectors is monster movement. A monster stands at position (x, y, z) and wants to move to a destination (x, y, z) with a certain speed. This can be done very easily with the vector functions provided in this article.
The cool thing about our vector functions is that they are dimension independent. The result is a hand full of functions that work as Vector2, Vector3, Vector4 (and more) all together.
Required Knowledge
We will use Python's list comprehensions and zip function. If you never heard of it, feel free to give our The Beauty of Python article a quick read.
Short example about what zip does:
u = [3, 4]
v = [1, 2]
print zip(u, v) # gives [(1, 3), (2, 4)]
Creating the Module
As usual we start by creating a new module, let's name it vec.py.
We will need Python's math module, hence we import it first:
import math
Now instead of creating a whole vector class, we will only create a few vector functions. The reason is simple: we will use Python's list feature for our vectors:
v = [1, 2] # vector2
v = [1, 2, 3] # vector3
Adding two Vectors
Let's jump right into the implementation:
def add(u, v):
return [a + b for a, b in zip(u, v)]
Usage:
u = [1, 2]
v = [3, 4]
print add(u, v) # gives [4, 6]
So how does it work?
In the example above, the first thing that happens is zip. It creates a list where the entries of u and v are zipped together:
[(1, 3),
(2, 4)]
Now our list comprehension starts: we said a + b for .... hence for each tuple (a, b) it results in a + b:
[(1, 3) => 1 + 3 => 4,
(2, 4) => 2 + 4 => 6]
In the end we get the result [4, 6].
Note: as mentioned before, this and all the following functions work for vector3 as well.
Subtracting two Vectors
The sub function is no big surprise, it's just like the add function but with a - instead of a +:
def sub(u, v):
return [a - b for a, b in zip(u, v)]
Usage:
u = [1, 2]
v = [3, 4]
print sub(u, v) # gives [-2, -2]
Comparing Vectors
Often we want to find out if two vectors are equal. Here is how we do it:
def eq(u, v):
return all([a == b for a, b in zip(u, v)])
Usage:
u = [1, 2]
v = [3, 4]
print eq(u, v) # gives False
How it works: at first we zip our values together, then we compare the tuples with each other (a == b) which results in a list of boolean values:
[(1, 3) => 1 == 3 => False,
(2, 4) => 2 == 4 => False]
Then the last step is the all function. It returns true if each element in the list is true.
Vector Length and Length_Squared
Many algorithms need to calculate the length of a vector. Sometimes when performance matters, we only want to know the squared length of a vector. As weird as it sounds, it's easier to calculate the squared length than the length:
def length_squared(u):
return sum([a ** 2 for a in u])
def length(u):
return math.sqrt(length_squared(u))
Usage:
u = [1, 2]
print length(u) # gives 2.23
print length_squared(u) # gives 5
How it works: length_squared first creates a new vector with the modification a ** 2 which means a², which means a * a:
[1 => 1 * 1 => 1,
2 => 2 * 2 => 4]
And then sum sums up all elements in a list, resulting in 1 + 4 = 5.
Now according to vector math, this is not the real length but the squared length. If we want the real length, then we just calculate the square root of our length_squared function as seen in length.
Scaling a Vector
Let's assume we have our typical RPG with a monster in it. Now due to some kind of event, the monster became a boss monster which means that it should be bigger than before. This is done by scaling every single vector in the monster mesh.
There are two kinds of scale functions:
Scale by Scalar:
v = [3, 4]
# let's scale it with the scalar 5:
v = [3 * 5, 4 * 5]
v = [15, 20]
(in other words: we multiply each component by some value)
Scale by Vector:
u = [1, 2]
# let's scale it with the vector (3, 4):
v = [1 * 3, 2 * 4]
v = [3, 8]
(in other words: we multiply each component of the first vector with each component of the second vector)
Here is how it's done in Python:
def scale(u, v):
return [a * b for a, b in zip(u, v)]
def scale_by_scalar(u, scalar):
return [a * scalar for a in u]
Usage:
u = [1, 2]
v = [3, 4]
print scale(u, v) # gives [3, 8]
print scale_by_scalar(v, 5) # gives [15, 20]
Vector Distance and Dist_Squared
We can calculate a distance between two vectors similar to our length and length_squared functions:
def dist(u, v):
return length(sub(v, u))
def dist_squared(u, v):
return length_squared(sub(v, u))
Usage:
print dist(u, v) # 2.82842712475
print dist_squared(u, v) # 8
How it works: at first we subtract v - u and then we simply calculate the length (or length_squared). There are other ways to do it, but this is as easy as it gets for us, since we already implemented a sub and length function before.
Norming a Vector
Many algorithms need normed vectors to work with directions. A normed vector is just a vector with the length of exactly 1. Example:
n1 = [100, 0] # has length 100
n2 = [1, 0] # has length 1 (is normed)
The interesting thing about this example is that the vectors have different lengths but they still both point into the same direction (towards the x direction). This is very useful when implementing something like a move_towards function later.
Note: every vector can be normed (or in other words: we can set every vector's length to exactly 1).
Since we already implemented a scale_by_scalar and a length function, the implementation will be very easy:
def norm(u):
return scale_by_scalar(u, 1.0 / length(u))
Usage:
print norm([100, 0]) # gives [1, 0]
How it works: the function simply takes u and scales it with some weird value. The weird value is 1 / length(u) which results in our wished behavior of setting u's length exactly to 1.
Setting the Length of a Vector
So there might be some cases where we want a vector with the length 2 instead of 1 (normed). Now that we understood the norm function, the setlength function will be really easy to do:
def setlength(u, l):
return scale_by_scalar(u, l / length(u))
But hey, let's be smart. Now that we have a setlength function, we might as well simplify our norm function even further:
def norm(u):
return setlength(u, 1)
That's code reuse. That's beautiful!
Bonus: Move Towards
In the very beginning of this article we talked about moving a monster towards a destination, so we might as well implement a function for it.
As explained in our The Game Loop article, we do things like movement calculations inside of the game's update function. The update function is called several times per second, so in every update call we have to move the monster towards its destination by just one step more. This step will be the stepsize parameter in our function.
A proper implementation will first check if the destination would be reached within the next step (no matter how big that step might be), and if it's reached then it will just return the destination point. If it's not reached within the next step, then it will simply do one step towards the destination and return the new position.
Confused already? Let's jump into the code to get a better idea of how it works:
def movetowards(pos, dest, stepsize):
if dist(pos, dest) <= stepsize:
return dest
else:
d = sub(dest, pos)
d = setlength(d, stepsize)
return add(pos, d)
Note: d is the direction which points from the position to the destination.
Now let's test it with a real example where a monster walks towards a player:
m = [1, 2] # monster position
p = [3, 4] # player position
# let monster move towards the player with
# a stepsize of 2
m = movetowards(m, p, 2)
# => m is now [1.70, 2.70]
The monster moved a bit towards the player with just one little function call. Awesome!