Unity3D handles math with xyz points in the standard way. This first part is those
rules, plus how they translate in Unity:
3D points are named Vector3, and have dot-x, y and z. They’re structs, so using new is optional:
They have a constructor. p=new Vector3(0,6,30); is the same as the lines
above.
You can add two xyz’s, and it happens pairwise: x to x, y to y and z to z. The result is another xyz. The code below shows this:
Subtraction is also pairwise. C=A-B subtracts each column:
The other useful shortcut is multiplying by a single float. Each part is multiplied by that number:
We call the single number a scalar since it scales the vector by that
amount.
As usual, operators can be combined and mixed, with times going before plus.
A+C*3 triples everything in C, then adds it pairwise to A. We can also use shortcuts
like A+=B; and A*=2;
There are shortcuts for common Vector3’s. Two handy ones are all 1’s and all 0’s:
The last is a nice way to blank something: A=Vector3.zero;. The first one is
often used with scalars. A=Vector3.one*7 is an easy way to make (7, 7,
7).
There are also shortcuts for 1 unit in all six directions. These use the Unity orientation, so forward is positive z. Ex’s:
These are nice with scalars for creating points. Vector3.up*5 is a nice-looking way to write (0,5,0). Or we can combine them to “walk” to a point:
That’s the same as new Vector3(0,9,-4);. But sometimes it’s nicer if we can
see it as “4 backwards and 9 up”.
Note that all of these are merely shortcuts. C=A+B is a short way to write C.x=A.x+B.x; and the same for y and z. You may have also noticed there’s no A*B. That’s on purpose. We could easily make it – just multiply pairs – but we’ll never want to use it for anything, so it’s better not to have it.
Vector3’s are often spots on the grid. It seems like that’s all they can be, but there’s a second common way we use them, as arrows or offsets.
Suppose we have a 2D map, and Cardiff is 2 west and 3 north of Glaston, written as (-2,3). Saying Cardiff is at (-2,3) is wrong – that’s in the ocean – but also something we’d never do by mistake. “(-2,3) from Glaston” is a totally understandable use. That’s what an offset is.
In our vector math, it’s going to be important to keep points vs. offsets straight.
We can do some useful, and easy math using offsets. Often we’ll think of them as
arrows. We’ll also need to be careful not to use an offset as if it were a a point – not
to look for Cardiff in the ocean.
A 3D offset warm-up: suppose cows wear a bluetooth in their left ears. To place it, we’ll want directions from the cow’s xyz position to the ear. We know the cow’s origin is center bottom. That means our left ear offset is up (y) from the ground, forward (z) by 1/2 a cow then left a little. Let’s say it’s (0.25, 1.5, 1).
To find the left ear of any cow, we take the cow’s position, plus the offset. The whole thing:
Let’s check the math. cow1Pos + toLeftEar says to start between cow 1’s hooves
and move 1.5 up, 1 forward then a little left. Sounds good – it’s the math we worked
out. The cool thing is how a “Pos+Offset” equation feels so natural: start at the
cow’s base, and follow the arrow to the ear. We don’t need to think about how it’s
doing pairwise xyz addition. It’s just as accurate, and easier, to imagine adding an
arrow to a point.
To keep going: cow2Pos + toLeftEar figures out where the 2nd cow’s bluetooth should be. A different cow, plus the same offset, gives the ear of the new cow.
Even more fun, suppose blueteeth have floating antenna, which are toAntenna from the actual bluetooth. cow1+toLeftEar+toAntenna finds it. A position plus two arrows, end-to-end. It’s like following a treasure map: start at the cow, walk to the ear, then walk a little to the antenna.
The actual rule is that you have to start at any position, and can add any number
of offsets. Of course, the offsets have to make sense. cow1Pos+toAntenna is junk:
toAntenna has to start from a bluetooth. cow1Pos+toLeftEar+ToLeftEar would only
work if one cow had another standing on its left ear.
Offsets can be scaled. It’s super cool. Suppose we have a 10% larger cow. All we need to do is make toLeftEar 10% longer. cow3Pos+toLeftEar*1.1f will go to the left ear of the larger cow3.
You can think about toLeftEar*1.1f the math way – x, y, and z are each 10%
larger. But we usually think of it as stretching an arrow to be the same direction, but
10% longer. It almost seems like cheating, but the math works, and arrows are the
preferred way mathematicians think of offsets.
Before the next example, let’s do some clean-up. So far, the cows can’t be rotated.
That seems limiting, but in a few chapters we’ll fix it by rotating the offset. The
other funny thing is “position plus offset = position”. That feels wrong, like “Apples
plus Oranges gives Apples”. But the math checks out, and you get used to it pretty
quickly.
For this longer example we want to find the squares on a tilted checkers board (8x8 squares). We only need someone to enter the 2 bottom corners. They don’t have to be on a straight line and can be any distance apart:
The first step is to find the arrow from the left to right corner, and divide it into 8th’s. That will give the corners of every bottom square. The math to get an arrow along the bottom is a new rule: subtract two points to get an offset from one to another:
It seems weird at first, but a point minus another point gives us an offset. It
literally gives the xyz’s between them, but thinking of it as a single arrow is accurate
and easier. Notice how it’s the right corner minus the left. It’s End - Start to get the
correct arrow.
Now that we have the arrow, we can use scalers to takes 8th’s, then add to the left side. Some sample corners of bottom squares, using math we’ve already seen:
The math works out so nicely since it’s on a board, with perfectly arranged same-sized squares. And remember the cool part, it works for tilted boards. A possible picture:
To walk up the squares, I’m going to cheat and use a 10th grade math trick. If you remember, you can make a 90 degree counter-clockwise rotation by flipping x and y and taking the new x times -1 (you don’t need to remember the trick for the rest to make sense). We can use that to spin leftToRight and get bottomToTop:
Notice how I’m respecting Unity’s coordinates. x&z counts as flat on the
ground, and that’s what I’m flipping, keeping y the same. Also notice how
we’re spinning an arrow. We can’t spin positions. Arrows get to have all the
fun.
We can walk to any corner of any of the 64 squares by starting at the lower-left corner and adding multiples of the across and up offsets:
The last line is considered good vector math, and should feel very natural: start at the corner, walk 3 squares across, then 4 squares up. You could think of it as 7 single-square arrows, or as 2 different length across and up arrows.
To wrap up this part, we can find the other two corners:
Notice how the math is still Position+Offsets. In the second one, I went up then
across. Across then up would be the same.
Finally, suppose we need to find the center of each square. Each center is 1/2-way across and up from the corner. We can compute this and save it in an offset:
It’s another semi-new rule: an offset plus an offset is another offset. Or, to put it another way: adding two arrows makes a single diagonal arrow.
This finds the center of the 8th square along the bottom. It moves there with the usual math, then adds the offset to the center, similar to the cow’s bluetooth’s antenna:
The end of this chapter has a math review. But a mini review: 1) Only offsets can be scaled or added together, 2) to use an offset, you have to start from a point and add it, 3) you can make an offset by subtracting two points.
This section is about seeing the math working in Unity/C#. We’ll have one script,
placed on a cube that will be “us”, then 3 sample cubes that our code will place at
various nearby spots, using vector math.
I’ll assume you know how to make the cubes, put a script on one, know what Update() does, know transform.position is where we are, and know how to drag objects into Inspector slots in a script. I’ll call the extra cubes red, green and blue. I’ll assume you know how to color them (if you want to. All we really need is to be able to tell them apart).
This will place the other cubes around us using 3 simple, slightly different types of offsets:
All three are simply “Position + Offset” math. They just look a little
different. transform.position is new, sort of, but it’s just a Vector3. Likewise
Vector3.right*3 is just another way of using an offset of (3,0,0). Blue used green’s
arrow, except 50% longer; or another way to say it – blue and green are at different
spots along the same arrow.
A fun way to test this is by adding a Rigidbody and collider to us, with a floor and bounciness. The other 3 cubes will track us as we spin and roll around.
Playing with the code should do the obvious things. Substitute Vector3.forward*4.
Change the greenOff numbers, tweak the multiplier on the last greenOff. Try using
the style Vector3.right*2+Vector3.up*4. Nothing exciting, but we can visually
test all of our math.
A note here: the rules say that in transform.position + greenOff, the second
thing must be an offset. It’s a Vector3, but how do we know it’s an offset? The rule is
that if you were thinking of “how far from something else” when you made it,
it’s an offset. If you were thinking “exactly where does it go” then it’s a
position.
For our second real example, lets pick one point a little away from us, and place the other cubes evenly spaced, using scalars:
This is similar to the checkerboard using 1/8th, 2/8ths and so on. Except here
we’re using thirds. Since it’s running in Update, we can change cubeLine as it runs,
and see the cubes snap to the new positions.
Instead of using fixed percents, what if we place one cube on the line, using a percent that gradually goes from 0 to 1? The math isn’t very exciting, but the result should be really cool. It will zip along the line, over and over:
Instead of moving along a line given to us, we might want to move to a target. In this case, we want to zoom up to the red cube. That’s easy enough: use the End minus Start trick to compute the offset to it, then re-use the code from before:
cubeLine = redCube.position - transform.position is the interesting part, and a chance to check our new math. transform.position and redCube.position are each positions. Our rules say we can subtract positions, giving an offset from one to the other. So that checks out.
To play with this one, we can run it and move around either cube. It will move
from us to red no matter where they are. You might notice two things: 1) it
moves faster as they get further apart. This is because we’re moving by 1%
each time. 1% of a longer line is a larger amount. And 2) the moving green
cube snaps when we move either end. That’s because our math locks it to
being exactly on the line – not on purpose – that’s just the easiest way to do
it.
This next one is sort of a new rule: multiplying an arrow by a negative number flips it around.
I’d like the green cube to hide behind us, from the red cube. That seems tricky, but think of it like this: we already know how to put it 1/10th of the way towards red. We can compute that offset, times -1:
As before, we can move either end cube and watch the red one move to stay hidden. Since we’re using 1/10th the distance it hides further behind us as the red cube moves further away. That’s not intended – it just works out that way.
In this section, instead of placing a cube we’ll place the game-camera, which looks much cooler. It’s also a neat example of how once you know vector math, you can figure out lots of things that seem very different.
To really see how this works, our object needs to looks different from different angles – like a cow, of a collection of different-sized boxes if you don’t have a cow.
A problem with these is that we don’t know how to aim the camera yet.
Since they’ll all put the camera above us, we’ll need to pre-rotate it to face
down.
This code puts the camera above us, and a little ahead. For fun I made the offset by adding the forward and up shortcuts, but it’s the same as regular (0,20,2):
By itself, this isn’t very exciting – the camera snaps to a spot. The neat part is
that it will track us. If we had scenery and were bouncing and spinning around, this
code keeps the camera in a steady position.
We can change that code a little to get a zoom. Instead of adding all of playerToCam, we’ll add a percent. That’s nothing new. We’ll use the arrow keys to change the percent (which is new):
The important line is no different than C=A+B*0.5. It’s pretty slick how 0.5 is really the zoomPct variable, which the user can change. But it’s still “position plus percent of an offset”.
An obvious bug is that nothing keeps the zoom in range. We could add IF’s to
limit zoomPct between 0.2 and 1. But right now nothing stops us from a negative
zoom, which takes the camera through and past us.
Another common tweak is making it so we don’t zoom straight in. We might zoom to a spot slightly ahead of the player. We can change our equation to look like: C=A+B+C*pct. B is the offset to the zoom point, and C is the “zoomable” arrow:
I worked out some different numbers, for looks, but otherwise the code is about the same (leaving out the parts that didn’t change):
If you see it in action, it looks much nicer than a straight-in zoom.
Instead of moving a cube along a line made between 2 points, we can give a cube a personal speed offset. In our minds that’s an arrow sticking out of that cube, showing how much it moves per second.
It seems funny having it be per-second. It would be easier if it was the
amount to move each update, but per-second makes lots of other things
easier.
This code lets us enter a velocity vector for the green cube (the script is still on our base cube, which just sits there):
I think += works really well here – it’s saying to change green’s position by a small
offset, which is a pretty good definition of moving. Dividing greenMvArrow by 60 is
because I’m assuming Update runs 60 times/second.
For fun, let it run, then change greenMv to 000. It will stop – it’s “moving” at a speed of 0. Enter -1 for x and it will slowly drift left. It won’t snap-change to a new position, like it did with the old “line” movement.
For more fun, add greenMove*=0.99f; anywhere in Update(). It will gradually
drift to a stop as the movement line shrinks.
A note on framerate: I’m assuming Update runs 60 times a second. It often won’t,
which means it won’t run at the speed we set, but that’s not a problem for us playing
around. If you know about the Time.deltaTime trick, that’s the real way to be
sure.
An average of two points looks and works just like a regular average. (A+B)*0.5f; gives you a point exactly between A and B. It works no matter where they are.
In the game board example we can average the two bottom corners to get the bottom middle: bottomMiddle = (cornerLL+cornerLR)/2;.
Or, sneakier, the center of the board is the average of two diagonal corners:
Vector3 center = (cornerUL+cornerLR)*0.5f;. That’s pretty cool, since it works
for tilted boards, too.
We can also find the average using our old point+arrow*percent method. This also computes the bottom middle:
As we know, writing it out lets us use any percent. Average is just a shortcut for Position+Offset*0.5.
Above, most examples had one new rule or trick. Here they are written all together:
I think of these rules when things break. Suppose sq.position=A+B+C; is
working wrong. Check whether A counts as a position, and B and C are offsets.
That’s the only way the math makes sense. If something with B*2 works funny,
check “does B count as an arrow?” and “am I wanting to double how long it
is?”
Here’s a picture of why adding together two points, p1 and p2, makes no sense:
Suppose A is the real (000). p1+p2 would be way over to the right. That’s not a spot we’d care about. If the real (000) were at B, p1+p2 would be in a totally different place, up above, which is also not a useful spot.
In contrast, offset math is always good. o12=p2-p1 is always the arrow between
them. p1+o12/2 is always halfway between them, and so on.
Another fun error is accidentally using an offset by itself, forgetting to add the starting point. That’s like starting from (000), which you never want to do:
A picture of the problem, where either A or B is the origin. We should be between p1 and p2, but aren’t:
offset is just a short line going up and left. Since we forgot to add p1, we’ll be up and left of the origin, which could be oa or ob, depending where the origin is.
If you get motion and placement going the correct way, but in the wrong spot,
check that you added the starting point.
It’s also easy to flip the arrow direction by mistake. B-A and A-B are the same arrows, but backwards (from A to B, or B to A). This code tries to put the green cube between p1 and p2, but accidentally starts at p1 and moves away from p2 with a backwards arrow: