The trick is to think in terms of coordinate spaces. The screen space is different from the world space. The screen is measured in pixels (
px), where +X is to the right, +Y is toward the bottom of the screen, and (0
px,0
px) is the top left corner of the screen. The world is measured in units or meters (
m) as defined by a camera. The camera then sets up the world space such that (cam.x
m, cam.y
m) is the center of the screen, +X is toward the right, and +Y is toward the top of the screen, and 1
m is some number of pixels. Drawing an 1 m sized object at (0, 0) should draw an object of N pixels centered to the screen.
So, now comes the actual transformation. I like to use matrices:
# assuming: cam = {x: 0, y: 0, scale: gfx_w/40}
# these transforms are executed from last to first, because math
mvp = mat3.identity!
mvp *= mat3.scale 1, -1 # invert the Y axis
mvp *= mat3.translate gfx_w/2, gfx_h/2 # translate to center of screen
mvp *= mat3.rotate cam.rotation # rotate camera
mvp *= mat3.scale cam.scale # convert from units to pixels, 1 unit = gfx_w/40 pixels
mvp *= mat3.translate -cam.x, -cam.y # make (cam.x, cam.y) the center of the screen
# the result of (mvp * player.pos) transforms the player’s position in world space to screen space
# with respect to the camera— this is typically done in a shader
shader.set "transform", mvp
graphics.draw player.shape, player.pos.x, player.pos.y, player.size
Matrices can make these transformations easier to read, though you can carry these transformations out by hand:
float2 center = float2 gfx_w/2, gfx_h/2
float2 inv_y = float2 1, -1
float2 cam_u = float2 (cos cam.rotation), (sin cam.rotation)
float2 cam_v = float2 -(sin cam.rotation), (cos cam.rotation)
float2 pos_cam = player.pos - cam.pos
float2 pos_px = (cam_u*pos_cam.x + cam_v*pos_cam.y) * cam.scale
float2 pos_screen = pos_px * inv_y + center
Then, controlling the zoom is just a matter of changing `cam.scale` :^)