STATUS: ABANDONED
In game programming we often want to work with x,y coordinates and transform them in various ways. This article covers:
- Cartesian coordinates: x, y, for placing things on the screen
- Translate operation: x + Δx, y + Δy, for 2D game cameras
- Scale operation: x * k, y * k, for zooming
- World coordinates vs. view coordinates
- Chaining operations together, for converting world coordinates to screen coordinates
- Reversing operations, for converting mouse clicks to world coordinates
- Rotate operation, for rotating objects and also isometric views
- Shear operation, another way to make isometric and other views
- Matrices, a way to represent chains of operations
Coordinates#
introduce: x,y (use pixel coordinates)In most systems, 0,0 is on the top left, but some systems, 0,0 is on the bottom leftCamera pan
introduce: translationA translation in geometry slides the coordinates left, right, up, down. For example, if we want to make 0,0 the center of the screen we can slide the map down and right:
world → translate( , ) → screen
The code for this is simple:
screen.x = world.x + screen.y = world.y +
World and view coordinates
There can be more than one coordinate system in a game. Here there are world x-coordinates in blue and screen x-coordinates in red. They’re the same until you scroll the map:
For any position world.x
in the world we can calculate the position on the screen:
world.x = screen.x = world.x +
Camera scrolling with player
Here’s an example with a player:
As before, for any world position world.x
we can calculate the position on the screen:
screen.x = world.x + screen.y = world.y
The interesting thing here is that when the player moves right, the world moves to the left. Why is this?
When the camera follows the player, the player’s screen position will remain the same. In this case I want the screen position of the player to be at 250. Let’s work through the math:
screen.x = 250 screen.x = world.x + # therefore world.x = 250 - =
Notice the minus sign? When you move the player to the right, that means world.x
is increasing. We know screen.x
is fixed at 250. { not sure how to explain this yet } When the world moves to the right, the player moves to the left!
This is tricky so let’s pause. {another example?}
{ "natural" scrolling on ipad vs the traditional scrolling on Windows reflects this difference -- are you moving the world or the view? will minimap help explain this? }{minimap would be a reason to use “view” instead of “screen” -- or maybe we introduce view vs screen somewhere else -- whereas regular view is all of the view coordinates and a part of the world coordinates, the minimap shows all of the world coordinates and part of the screen is the view coordinates}
The fundamental relationship between the two is: view + camera = world. You can also express this as view = world - camera. In this demo, the camera is centered on the player character.
{ should the narrative introduce scrolling first, and then camera pan / recentering? } { is this a good time to introduce reversing a transform? } { separate diagram to focus on camera object? maybe better in the minimap }Camera zoom
introduce: scaleworld → scale( ) → screen
screen.x = world.x * screen.y = world.y *{ addition becomes translate() for geometry; multiplication becomes scale() for geometry }
Object coordinates
May be useful to show translate, rotate, scale for an object on the map, or the player spriteWorld and view coordinates, again
introduce: chainingworld → translate( , ) → r → scale( ) → screen
r.x = world.x + r.y = world.y + screen.x = r.x * screen.y = r.y *order matters! {show the other order}
Mouse clicks
introduce: reversal; reinforce: chainingworld ← translate( , ) ← r ← scale( ) ← screen
The last thing we did (scale) is the first thing we undo; the first thing we did is the last thing we undo. The order is reversed.
Object zoom#
reinforce: scaling, chainingCamera rotation#
introduce: rotateworld → rotate( ) → screen
{ but we might want to rotate around the center of the screen → let’s translate first } → reinforce chaining
q.x = p.x * cos() + p.y * sin() q.y = p.y * cos() - p.x * sin(){ How about reversal? }
Object rotation
reinforce: rotation, chainingOblique projection#
introduce: skewworld → skew( ) → screen
screen.x = world.x + world.y * tan() screen.y = world.yhttps://en.wikipedia.org/wiki/Oblique_projection
(NOTE: it’s unclear to me exactly what the difference between skew and shear is but I’m guessing that skew is expressed as angles and shear is expressed with the tangent of the angle, and they become the same thing in the end.)
Isometric projection
reinforce: shearing, rotation, chaining, reversal;world → rotate( 45° ) → r → scaleY( ) → screen
r.x = world.x * cos(45°) + world.y * sin(45°) r.y = world.y * cos(45°) - world.x * sin(45°) screen.x = r.x screen.y = r.y *https://en.wikipedia.org/wiki/Axonometric_projection
Aspect ratio
With a wide variety of screen aspect ratios, how do you make your game world fit?
{Cover extending world, black bars, and inner/outer boundaries. Show them all visually}Aspect ratio#
Especially for mobile games, your aspect ratio may not be the same as the aspect ratio of the screen (or window). You can either preserve the aspect ratio by adding black bars to the top/bottom (“letterboxing[1]” in movies) or left/right (“pillarboxing[2]). Alternatively, you can scale your content.
OpenGL coordinates#
OpenGL sets up the coordinate system to be -1 to +1 along both axes. Your screen/window likely isn’t square. How should we think about this?
OpenGL is applying a transform to the coordinates you give to it. It applies both scale and translate.
{ show the transform }You need to take into account what OpenGL is doing to your coordinates. If you draw a square, it won’t look square on the screen because OpenGL is scaling your coordinates. You will want to scale your own coordinates first before passing them to OpenGL. { work through the math }
Hexagonal grids
All of these operations chain together. If you’re using hex grids, you can use a hex-to-cartesian operation (and its inverse, cartesian-to-hex) along with any of the others listed above. For example, if you want isometric hex grids, you would chain hex-to-cartesian with the isometric operations (rotate and scaleY). Mouse clicks would be processed in reverse: invert scaleY, then invert rotate, then invert hex-to-cartesian (by using cartesian-to-hex).
Matrices
motivation: pattern for all of the transformations; introduce: matrixMatrices aren’t transforms. Matrices are representations of transforms.
Matrices allow you to optimize a bit. A chain of transforms might be q = f(g(h(p))). In math we can “compose” functions. We can combine f, g, h ahead of time into q = (f o g o h)(p). We don’t have a way to compose functions at run time in most programming languages we use.
All of our transforms happen to be representable as matrix multiplies, q = F * G * H * p. Matrix multiply is associative so F * (G * (H * p)) = ((F * G) * H) * p. By representing our functions as matrix operations, we can compose them ahead of time. Instead of applying a long chain of 8 operations to every point p, we can first combine those 8 operations into 1, and then apply that to p. This is common in 3D programming, and there is both CPU and GPU acceleration for 4x4 matrix operations.
u,v vectors; show how they are transformed; show how they are in the matrix; show a unit circle tooMore reading on matrices:
- http://ncase.me/matrix/
- http://franklinta.com/2014/09/08/computing-css-matrix3d-transforms/
- http://maxgoldste.in/invitation-to-another-dimension/