Occasionally it can be helpful to pretend an object has it’s own personal coordinate system: (000) glued to the object, xyz spinning with it. We can imagine that grid overlaid on the real coordinates.
Since we have two xyz’s, we’ll say the real one is global, and the one on us is our
local coordinate system. Obviously there’s only one global, but everyone has their
own personal local coords.
It turns out that if we know one, we can figure out the other. Global to Local and
back. This is great. It means we can place an object using our personal xyz, if that’s
easier. Then convert into the real xyz. With any luck, we can make the system do
that for us.
What often happens is you have a rotated object, often rotating. It could be facing any direction at any time. And the math for “go to my right” and ”behind me” and so on is hard – piles of trig hard.
“Try it in local coordinates” can be a magic spell. It says to work out the problem as if you were always facing forward, and also pretend you’re always at 000. Our trig problem often turns into grade-school math. Solve that and you’re done. Really. The last step is to tell the computer you were using local space. It will account for it, like magic.
Before math and coding, let’s just play with local axis in the Unity3D editor. Even
though they aren’t real, the tools in Unity let you use them.
Get in Scene view, select any rotated cow and pick the Translate tool (on top: it’s the Crossed arrows, between the Hand the the Circle arrows). Red, green and blue arrows should come out of it, aligned with the real x, y and z axes
A few buttons to the right, you should see a button that says Global. It’s a toggle. Tapping it flips between Local&Global. If the object is rotated, you should see the xyz arrows snap to a new spot, then back.
On the Local setting, spinning the object spins your local arrows. As you
might guess, +z is always out your front, +x is always your right side. If you
turn upside-down, your local +y goes down (which is what up-side-down
means).
If you drag a rotated box using the xyz local axes, you’ll see it move in a nice
grid. It feels like a regular xyz system. Technically, it’s all fake. When you move on
just 1 local axis, the Inspector shows x, y, and z all changing at once. It’s
re-calculating the global coords for you. The local axes aren’t “real-real”. But we can
use it and it works great. It’s real enough.
Pretty much any 3D program has options for Local/Global. Many users who aren’t so hot on math have an excellent understanding of Local and Global axes. It just takes practice.
A note: the colors are standard. Everyone lines up rgb and xyz – the red arrow is always x, green is y and blue is z.
Since they’re so useful, Unity provides your local-axis arrows in code: transform.right, transform.forward and transform.up.
The way I remember is that transform means me, and holds my position and
spin. So transform.right is my right. Vector3.right is global right. It never
changes, but transform.right has to be calculated based on the spin.
We can use them like regular offsets. This puts the red cube 3 units to our right:
Since this uses our right, spinning us will move the red cube, keeping it always 3
units right of us.
As normal, we can combine these offsets. This puts the red cube 2 in front and 4 left of us. Again, spinning shows that it’s our front and left, based on our facing:
Unity didn’t give us transform.left, since by now everyone knows left is negative
right.
We can improve our old movement code by using these new arrows. This line in Update moves us the way we’re facing:
You can check that it always uses the our current forwards by running it and
spinning a little. Notice how I was too lazy to figure out speed-per-second
and then divide by 60. Instead I just guessed 0.01 for slow, but not too
slow.
The rest of these are examples of how everyone has their own personal local coordinates. Our script can look at other peoples’, and use them like regular offsets.
This code moves the red and green cubes along their forwards (face them in various directions to check):
As you might guess transform.forward is my forward, redCube.forward is her
forward, and so on. You can ask any Transform, such as redCube, for any of their
local axes.
This next example is basically a mistake, but legal. We’re trying to move ourself forward, but accidentally used the red cube’s forward, not ours:
It’s pretty weird. We’ll move in a funny direction. Spinning the red cube, which
isn’t moving, will change the direction we move. it’s a little like a remote-control
steering wheel.
Here’s another garbage example, where we position the green cube based on the red and blue forward arrows:
If both are pointing in the same direction, green will be about 3+4 units away. If
facing in opposite directions, they mostly cancel out. It’s like the green cube is on an
invisible 2-part robot arm, controlled remotely by the red and blue cubes’
spin.
Here’s one where it makes sense to use other people’s arrows. Each cube is 2 units in front of the previous one, forming a chain:
The math in all three lines uses the same cube’s position and forward. “2 in front
of me” makes sense. The chain is fun – if we rotate a cube, everything after it tracks
the spin. That looks pretty cool if we do it while running.
Now back to movement code. We can adjust the old code where we shot out a green cube over and over. The movement line can now be our personal forward. The rest of the code is the same. As with all the others, it looks best if the object is rotated, to show how it always goes forward:
It starts from our origin. For a cow, that’s down by the feet, which isn’t so nice. We can bring it higher up, maybe out the mouth, by adding an extra transform.up offset:
It’s looking a little complicated, but all we’re doing is adding 2 offsets. We always go 2 in our up, then go a variable amount in our forward. The zooming camera did the same thing:
It can feel strange not having transform.forward come straight out of us. But it’s just an arrow representing our personal +z. Moving our up, then our forward feels natural enough.
This uses the built-in Translate command to move slowly in our forward direction. The inputs are x, y, and z. But see if you notice the strange part:
The new/strange/nice part is that we didn’t have to do anything funny to get our local z. Translate was written for when you want to move along your arrows. You give it the amounts on them, and it does the rest. Technically, Translate expects the inputs to be in your Local Space.
Suppose we want to move forward and a little up, at a speed of z=0.01 and y=0.002. Written both ways, the Translate one looks nicer:
People like to think in local coordinates, and expect to have built-in functions using them. Translate is a useful, simple example of that.
But now we have one more thing to worry about: xyz’s can be in global or local.
Local is so useful that it’s worth being a little confusing. And in most commands it’s
obvious which they want.
On to the next command. Anything with a rigidbody and a collider will normally fall and bounce around. Unity also has two commands to shove, using global or local coordinates. Relative isn’t a technical term, but it clearly means local:
The interesting thing is how (0,0,4) can mean Local or Global, depending on how you use it.
Similar side-by-side commands are common when either way might be useful.
First decide whether the motion should be described using your personal arrows,
or the real xyz. That tells you which command to use, then work out the
numbers.
Back to Translate. The short version which we saw is automatically in Local. It has 2 longer versions where you can select Local/Global using the 4th input:
Self and World words are more informal terms. Some game designers prefer them
to Local and Global.
There’s no special reason why AddForce is 2 different commands, while Translate is one with 2 options. But it doesn’t really matter. The main thing is knowing there can be a local and global version. If we want to give the directions using our personal arrows, we can. We just have to find the command that wants Local Space.
Suppose we have two cubes side-by-side, off somewhere. One might be at (3,0,6) and the one next to it at (4,0,6). If we go to the list of names and drag the second cube into the first, it becomes a child. It’s locked to the first cube, tracking it. Nothing special so far.
But another interesting thing happens. The numbers for the second cube’s position snap to (1,0,0). It doesn’t move, and it’s not at (1,0,0), But the Inspector shows those numbers. If we move and spin the first parent cube, the child cube moves to track it, but always displays position (1,0,0).
Moving the second cube does another strange thing. Suppose we pull it further right so it says (2,0,0). Now it tracks the parent based on that new position. The position numbers are still locked, now to (2,0,0):
The rule is: objects with parents use the parent’s local space. “Locked 2 spaces
right of our parent” is just another way to say “(2,0,0) in our parent’s local space”.
The Inspector is showing our local position in our parent’s space, and allowing us to
adjust it. The only very minor problem is how the label misleadingly still says
Position.
A fun local-space parent trick is snapping one object to another. First make
yourself a child of the thing you want to go to. Then enter 000 for your
position. That puts it 000 away from the parent. Neat, right? Finish up by
unparenting.
Another fun child trick is sliding yourself based on another object. Say you want
to slide yourself along a very tilted wall. Just make yourself a child of it. Now sliding
your Inspector xyz’s uses the wall’s arrows (to slide a number, click just to
the left when the cursor turns to a slider-symbol, then slide). x and y slide
you perfectly along the surface of the wall, no matter how the wall’s been
rotated.
You can use the child-trick to play a fun game: get a cow and a ball, neither a child of the other, and spin the cow. The game is to place the ball exactly in front of the cow, 3 units away. Once you think you have it, check your work by making the ball a child of the cow.
The numbers you see will instantly snap to local. The closer you are to (0,0,3), the better you’re lined up. If x is almost 0, you got the left/right almost perfect.
Of course, you could get it perfect by making yourself a child, typing in (0,0,3)
and un-childing. But playing it like a game is educational.
In code things are different, a little nicer. transform.position is always your real-world position. Even children can use this as normal. If you’re a child, transform.localPosition is where you are in your parent’s local space.
Here’s a crazy-looking script to run on a child. Just drag yourself into anything and tilt it. This code moves you along your parent’s forward:
The key is that localPosition is in the parent’s local coords. Adding to z is the same as adding to Inspector z, which is local in the parent. Vector3.forward “feels” global, but it’s just (0,0,1). What it does depends on where we put it.
It’s another case where we can choose Local or Global based on the command we choose:
This next one is a little silly, showing more localPosition use. Assume the green cube and us share a parent. This puts the green cube where we are, except mirrored on our parent’s x-axis:
To test, we can slide ourself around and see green mirror us – the same spot on
the cow, but on the other side.
We can redo the “fire a cube out of the cow’s mouth” example using localPosition (the cube has to be a child of the cow). Instead of a percent, this will use meters going from 1 to 8. But otherwise it’s the same:
localPosition = new Vector3(0, 2, zDist); is the fun part. It’s a clear, short way to say “2 up and z forward on my parent”.
This section is very optional. It’s for if you aren’t happy unless you see the numbers
and can check them out.
This simple program shows values for your 3 local arrows:
If we have no spin, right will be (1,0,0), and so on – our local axes start out lined up perfectly with the real ones. I didn’t say this before, but they’re also length 1, just like the real ones.
If we spin 90 degrees clockwise on y, right will be (0,0,-1). That’s still normal – our right is the real backwards. But tilting 45 degrees gives some strange numbers. fwd will be (0.707, 0, 0.707).
That’s correct. It’s length 1 going diagonal. It turns out 0.707 is the cosine of 45 degrees, and is also 1/2 of the square root of 2, and the pythagorean theorem says the whole thing is length 1.
Spinning 30 degrees makes fwd be (0.5, 0, 0.86). That’s also length 1, and also the
sin and cosine of 30 degrees.
If you tilt on y and x, you’ll get even stranger values for x and y and z. But the
math says they check out.
Finally, this is one more thing that might be bugging you: the manual says transform.forward is in world space. What?? We use it for local space positioning, so how can it be world space?
It’s about which arrows the numbers are meant for. (0.707, 0, 0.707) is how to make a diagonal arrow, using the real x, y, and z. transform.forward*3 is like a 2-step process: we think “3 forward local”, which is (0,0,3); then convert using the arrow.
Mixing local and world axes in the same math usually gives junk, for example:
transform.right*3 + Vector3.right*2. That’s 3 to out personal right, then 2 on
the real right. Depending on our spin, they could mostly cancel out, add, or anything
in-between. It’s not illegal, but it’s almost never useful. It’s usually a sign you made a
mistake.
Vector3.up vs. transform.up can be an exception. Suppose you want a label above someone and a little behind. transform.up*3 - transform.forward. seems correct. It works fine when they spin. But if they tilt or lean, then transform.up tilts and pulls the label too much. The label looks better if we go straight up from the feet: Vector3.up*3 - transform.forward.
The opposite problem can happen. You place a hat using Vector3.up*1.4f. That
works perfectly at first, since they rarely bend over. But when they do, the hat is
floating above their feet.
The most confusing errors are double-conversions. If something expects local coordinates, you can’t use transform.forward and its friends. This is legal but gives wrong movement:
transform.forward converts into the values we want. But Translate doesn’t know that. It assumes they’re unconverted locals and does it again. It’s a little bit like converting feet to inches twice in a row.
Hopefully this also feels redundant. We use commands taking Local coords so we
don’t have to use any other tricks. We like how Translate(0,0,1) means our
forwards.
Double-conversions can be hard to spot during testing, since the wrongness of the
result can be a lot or a little, or none if you aren’t rotated.
Another double-convert error which is pretty much the same: we’re setting our child’s localPosition, which wants Local coords:
To go 2 forward we should have used just (0,0,2). localPosition will handle the
rest.
The same error with AddForce:
We chose to use RelativeForce so we could use (0,0,6). Adding the transform.foward messed that up. Either of these would work correctly. Obviously the second one is nicer:
There’s nothing new here, but it’s fun to see a few things used at once.
As we all know, X-wing fighters fire a blast from each wing tip, angled inward a little so that they meet after 50 meters:
Pretend we have some tiny rigidbody balls as prefabs, ready to be used as laser bolts. As a quick review, anything with a rigidbody moves by itself. We set the velocity once, at the start, in units/second.
This code handles placing the balls at our wingtips:
The only tricky part, for me anyway, was the wingtip math. On my first try I
flipped the entire left equation, so the ball was 5 left and 2 backwards.
Next we set the ball’s speeds using the velocity built-in. They should fly forward and a little inward:
To test this, it looks nicer if you uncheck the balls’ UseGravity box. With gravity turned on, they’ll probably fall below the camera before meeting in the middle.
You may have noticed how transform.forward*2 + transform.right are basically hacks. They work great, but they aren’t they way we like to think about local space.
The best way to do this stuff is to think completely in local, at first. If we
want to be 2 ahead and 1 right, we write (1,0,2) and remember it stands for
local. Then we either find something that wants local, or convert at the
end.
For example, if we want a child at (1,0,2) from us, c1.localPosition = new Vector3(1,0,2); is great. It says exactly what we were thinking, without being very long. c1.position = transform.position + transform.forward*2 + transform.right will work, but it’s basically hiding “local (1,0,2)” from us.
Likewise rb.AddRelativeForce(new Vector3(0,1,10)); is the nicest way to say
that we want a local push, of this xyz local amount.
So far, there’s no good shortcut for placing a non-child using our local coords. We’ll get one later, but just for fun we can make it now.
We’ve seen the equation over-and-over: to be (1,0,2) from us, start where we are, and multiply out direction arrows. Here’s a function doing that:
Using it to place the green cube (1,0,2) from us:
It almost seems like cheating, but that’s what functions are for. We can see it’s taking transform.right and transform.forward*2, and adding it to our position.
It’s also a complete sum-up of the entire chapter. “(1,0,2) from us” is a fine way to think, the computer can do it, and it’s even easy to read (once you know about the local trick).