-
-
Notifications
You must be signed in to change notification settings - Fork 488
Camera tutorial
You can try out the completed project for this tutorial here:
Using a camera system in TIC-80 may not be as difficult as you may think. Other programs like pico-8 and love2d provide you easier ways to translate the draw coordinates on the screen, however by simply drawing everything with their base coordinates plus the camera coordinates you could simply roll your own camera.
This camera will follow our player, with no lerping. It is simple and great for both top-down and side-scrolling games. For this tutorial I'll just use basic top down movement to keep the code clean.
First off we're going to make two variables in a table for our camera coordinates. Also we'll define our player and the frame count.
function init()
t=0 --frame count or time
p={x=32,y=32,spr=256} --player
cam={x=120,y=68}
end
This will start our camera off in the center of the screen, since TIC's screen resolution is 240 by 136.
While doing any draw calls, we want to draw everything by their original coordinates minus the camera coordinates. We also want to update the camera coordinates every frame if we want the camera to follow our player.
init()
function TIC()
cam.x=120-p.x
cam.y=68-p.y
if btn(0) then p.y=p.y-1 end
if btn(1) then p.y=p.y+1 end
if btn(2) then p.x=p.x-1 end
if btn(3) then p.x=p.x+1 end
spr(p.spr,p.x-cam.x,p.y-cam.y)
end
The map is the only draw call that gets treated differently. We simply draw the map by negative camera coordinates.
map(0,0,240,136,-cam.x,-cam.y)
Drawing the entire map though wouldn't be very efficient. For turn based games it should be fine, but action games you will immediately notice slow down. Since only a 30 by 17 slice of the map is visible at any given moment, we will only draw the minimum number of map cells required to have everything look fine. This takes a bit of planning and math though. Our end result for this tutorial will be drawing a 31 by 18 chuck of the map every frame. This is the minimum amount we can do without having any edge clipping. Below is an example of the edge clipping I'm talking about.
cam.x=math.min(120,120-p.x)
cam.y=math.min(64,64-p.y)
local ccx,ccy=cam.x/8,cam.y/8 -- camera cell x and y
map(ccx-15,ccy-8,32,17,(cam.x%8)-8,(cam.y%8)-8)
Because the map cells we draw will be constantly changing, drawing our map with the camera offsets will not work anymore. We can also use math.min
to disable scrolling when the player is less than half way from the center of the screen relative to the map coordinates. This way there isn't and black space on the screen. Also note that I draw the map with (cam.x%8)-8
because we "overdraw" an extra map cell to avoid more clipping.
We can use the modulo %
operator to get the remainder of dividing the coordinates by 8, then drawing our map cells with this remainder offset. The modulo method works great, except when our camera's target's coordinates divide evenly by 8. The effect looks like the entire map gets shifted by a cell and needs a resolution. Below is an example of the jerky effect.
local ccx=cam.x/8+(cam.x%8==0 and 1 or 0)
local ccy=cam.y/8+(cam.y%8==0 and 1 or 0)
This works because each cell is a 8 by 8 pixel square. If our camera's x coordinate is 12 for example, cam.x % 8
would return 4 because 12 can only be divided by 8 once, leaving 4 left over. These 4 pixels would then be used to offset our camera position. So each time our actual map cells change, this number gets reset back to 0. Pretty neat, huh? To deal with the jitter when our coordinates divide evenly by 8 we can shift ccx
and ccy
if this case is true.
This is the most efficient way I have found to draw your maps and will be a huge performance increase over drawing large chunks of maps.
With the example above, drawing everything besides our map will no longer work by x+cam.x
for example. We need to account for the screen's center coordinates as well.
function Enemy:draw()
spr(self.sprite,cam.x-120+self.x,cam.y-64+self.y)
end
Instead of typing out the camera coordinates minus the screen center, I would suggest storing it as a draw_x
variable to avoid typing it out too much.
There isn't too much to add when we want our camera movement to be gradual. We want the camera to follow the player, but not be exactly locked on the center of the player every frame. Using a lerped camera we can achieve this, and the results look very nice. This adds a new depth of polish to your game and most modern 2d games use something similar. All we have to do is implement a lerp function while updating our cam.x
and cam.y
variables.
function lerp(a,b,t) return (1-t)*a + t*b end
function TIC()
cam.x=math.min(120,lerp(cam.x,120-p.x,0.05))
cam.y=math.min(64,lerp(cam.y,64-p.y,0.05))
end
Adjusting the last argument will make the camera slower or faster. I used 0.05
for this example but feel free to use whatever you prefer.
TIC-80 tiny computer https://tic80.com | Twitter | Telegram | Terms
Built-in Editors
Console
Platform
RAM & VRAM | Display | Palette | Bits per Pixel (BPP) |
.tic
Format | Supported Languages
Other
Tutorials | Code Snippets | Libraries | External Tools | FFT
API
- BDR (0.90)
- BOOT (1.0)
- MENU
- OVR (deprecated)
- SCN (deprecated)
- TIC
- btn & btnp
- circ & circb
- clip
- cls
- elli & ellib (0.90)
- exit
- fget & fset (0.80)
- font
- key & keyp
- line
- map
- memcpy & memset
- mget & mset
- mouse
- music
- peek, peek4
- peek1, peek2 (1.0)
- pix
- pmem
- poke, poke4
- poke1, poke2 (1.0)
- rect & rectb
- reset
- sfx
- spr
- sync
- ttri (1.0)
- time
- trace
- tri & trib (0.90)
- tstamp (0.80)
- vbank (1.0)