Developing hobby games I usually just stick to libraries that already handle things like physics. Basically I follow examples, copy/paste code, and plugin numbers. This is good for getting a hobby game built but not so good to learn the math and gain an intuition of what’s actually going on, which is what I want to do now.
Vectors are the basic building block of 2D game physics. They describe the distance and direction of game objects (from player characters to projectiles, even lighting). As such, I think they may be the best place to start this exploration and self-learning exercise.
This article was written as I attempt to learn the subject matter at hand, so there is a strong possibility of incorrect information based on my own ignorance.
Please, if you see an error of any kind, feel free to send me an email!
Check out the source code to this page, I used processing.js to generate all the visual examples, and I tried to pepper the source files with a bunch of comments.
Vectors represent two measurements – a magnitude and a direction. For example, “traveling at 2 kph (kilometers per hour) due east” is a vector! This data is descriptive, but not necessarily useful for computation in that format. To make the data more useful, it is typically encoded into two numerical values,
y, representing a distance from the origin on the x and y axis.
NOTE: I am assuming the use of a 2-dimensiononal, Cartesian coordinate system, and kind of ignoring 3-dimensional stuff for now
Our “traveling at 2 kph due east” example then turns into something like
[2, 0], which would look something like:
The important thing to remember is in order to think about a (2D) vector in concrete numbers, we can express it as a pair of these x and y values. IE (0, 1), (30.3, 2.2), etc. These 2 numbers represent a distance on the x and y axis, and not a point – this is important because it means vectors are encoded as the legs of a right triangle. Why is this important? I’ve found that it helps me to visualize vectors in a 2D space.
In terms of (Nim) code, this could be expressed as a 2 dimensional array, a tuple, or more explicitly, like:
type Vector* = object x*: float y*: float
I like the more explicit approach in this case because I think it’ll make writing dependent code a little more clear, and I need as much clarity I can get.
The first really useful value to calculate based off of this vector encoding is the magnitude. This turns out to be pretty simple. Remember how the this vector encoding is basically showing us the lengths of the legs of a right triangle? Taking advantage of that, we can use Pythagoras Theorem, which boils down to this formula: x2 + y2 = h2.
The magnitude, then, is the h value, and since the formula is for h2, rearranging it to solve for h turns it into: sqrt(x2 + y^2) = h.
I think the results of both formulas will be useful later, so I’m going to make two procedures – one to calculate the magnitude squared (_h2_) and the magnitude itself (just h), here’s the code:
# magSq == "magnitude squared" == h*h proc magnitudeSq*(self: Vector): float = result = self.x * self.x + self.y * self.y # the actual magnitude value (h) proc magnitude*(self: Vector): float = result = sqrt(self.magSq)
sqrt comes from the Nim standard library
The definition of a vector is a magnitude AND direction, so how do we encode the direction in terms of
y? Simple put, the sign of the
y values indicates their direction. If the
x is positive then the vector is going an easterly direction (if we’re thinking of direction in terms of the cardinal directions) and a positive
y would be a northernly direction. A negative
x value will be in a westerly direction, and a negative
y value would be in a southernly direction.
In terms of code, there’s nothing extra needed since
Vector.y are signed values.
First, why add and subtract vectors? The best answer is an example: presuming a characters movement is described by velocity (direction and speed, encoded as a vector) and acceleration (change in direction and speed, encoded as a vector), then it is only natural that when it comes time for a character to move within the simulation, their acceleration should be added to their velocity, and the result should be added to their current position. Being able to add acceleration and velocity (both vectors) becomes super important here.
Adding and subtracting vectors, conceptually, is like traveling along the the hypotenuse of one triangle, then traveling along the hypotenuse of a second triangle. From where the first vector began to where the second vector ends is the resulting vector. Confusing? Khan Academy has a pretty good video on adding vectors.
Here’s my attempt to visualize it:
The first vector:
The second vector:
Traveling along the first vector, then traveling along the second vector to get a third vector:
The resulting vector:
The math for this is straight forward: add the two
x values for the result
x value, add the two
y values for the result
y value. The same formula can be used for subtracting vectors as well, just subract the
y values instead of adding them.
The resulting code might look like:
# just overload the `+` operator proc `+`*(a, b: Vector): Vector = result.x = a.x + b.x result.y = a.y + b.y # just overload the `-` operator proc `-`*(a, b: Vector): Vector = result.x = a.x - b.x result.y = a.y - b.y
A scalar is just a single number, and scaling a vector by a scalar is the act of multiplying the vector by the scalar. Why is this useful? Let’s say there’s a scenario where a ball rolls along a floor… how fast does it slow down? This could be modeled by using a scalar value for friction, and shrinking the velocity (a vector) of the ball by that much every tick of the simulation.
The math for scaling a vector is as simple as multiplying the scalar by the
y components of the vector. In terms of code, it might look something like:
# just overloaded the `*` operator proc `*`*(a: Vector, b: float): Vector = result.x = a.x * b; result.y = a.y * b;
A unit vector is a vector who’s length/magnitude is exactly 1. A normalized vector is a vector who’s components have been divided by it’s magnitude in order to be in proportion with a unit vector. For example, the vector
[1, 2] has a magnitude of about
2.236. If this vector is normalized, it’s value would become
Why is this useful? The key is that a normalized vector is proportional to the original vector. This means the normalized vector carries direction with a proportional encoding for magnitude, thus enabling the normalized vector to be scaled to any size proportional to the original magnitude.
Again, why is this useful? I think it’s best illustrated with an example! Think of light reflecting off of several objects. Each object has a vector describing an amount of light that get’s reflected, as well as a direction of reflection. The intensity of light can be represented by a single scalar value, turning the problem into one of scaling each objects light vector.
If each object’s light vector is normalized, then they will have a consistent representation of reflected light based on the same scale. If each light vector were not normalized, then there would be a very inconsistent representation of light, each object behaving like it existed in it’s own little world.
Whew. I hope that helped. Here’s some code to implement normalization:
proc normalize*(self: Vector): Vector = result.x = self.x/self.mag; result.y = self.y/self.mag;
Seriously, Wolfire’s Linear Algebra for Game Developers, Part 2 has a great example of the dot product and why it’s useful. Not even going to try to make a better example and explanation, because I doubt I could at this point.
I’ll paraphrase though, the dot product is kind of a proportion of how much the vectors are pointing in the same direction. It’s also kind of useful in projection.
Suffice it to say, here’s some code for implementing the dot product:
proc dot*(A, B: Vector): float = return A.x*B.x + A.y*B.y;
Notice that this code looks an aweful lot like the magnitudeSq method! Intuitively, then, the dot product of vector A against intself would be the h2 value when solving for Pythagora’s Theorem… which means our magnitude code could be adjusted to look more like:
# just get the square-root of the dot product of the vector against itself proc magnitude*(self: Vector): float = result = sqrt(self.dot(self))
The act of projecting a vector onto another is, essentially, finding the vector between the points
b in the following image:
Intuitively, if the vector being projected onto is a normalized vector, then really the problem becomes straight forward! First, find the magnitude of the vector resulting from a vector projected onto a normalized vector (turns out, this is the dot product). Then, scale the normalized vector (who’s legs are proportional to each other) by the magnitude to get the resulting vector encoded as
Why is this useful? This is kinda hard for me to answer in what I feel is total understanding, as I don’t think I’ve actually used it enough to really grasp it use. The text book answer might be a few things:
That’s still a pretty meaty explaination. I guess it boils down to being able to see a vector in the context of another, and making assumptions based off the result.
I’m pretty sure that I will be using projection pretty heavily once I start getting into implementing the Separating Axis Theorem
Enough talk! Let’s see some code:
# this procedure handles a more generalized form of projection (IE when unit # vectors aren't explicitly used # A projected onto B proc project*(A, B: Vector): Vector = var magSq = B.dot(B); var dot = A.dot(B) / magSq; result.x = A.x * dot; result.y = A.y * dot; # B is an already normalized vector # A projected onto B proc projectN*(A, B: Vector): Vector = var dot = A.dot(B); result.x = A.x * dot; result.y = A.x * dot;
I think I’ve gone through all of the absolute basics of vectors as they immediately relate to 2D game physics. The next step for me to take will be to (re)learn about linear transformations (for things like vector rotation, reflection, etc). I’m fairly certain there are shortcuts for these things, especially for 2D, but the goal is to develop an intuition about this stuff, so it’s important that I don’t just copy/paste code or take every formula at face value.
Next time will probably be a much shorter post, specifically covering my re-learning useful linear transformations for 2D vectors and hopefully dealing with a lot more code!