I made a Pico 8 cart which produces a SNES Mode 7 style floor. The trick is in the function tline, which draws a line between two points, sampling from the map data along the way. You give it the map position to start from, as well as dx and dy values it should use as an increment after each pixel. The other main thing is setting a depth value per scanline, the closer to the bottom of the screen the nearer to the player’s view. The rest is just trigonometry, applying angles to vectors using sin and cos.
Click here to try it out, and here to download it.
Here is the top level code. This is run when the cart is first booted. I put constants here, and code that should run once and never again. The poke statements are taken from the Pico 8 manual section on tline.
--horizontal map loop size
poke(0x5f38,8)
--vertical map loop size
poke(0x5f39,8)
--constants
--scanline where ground starts
hrzn=63
--forward and backward movement
spd=0.1
trn_spd=0.006
This is the _init function. Similar to the top level code it’s called once when the cart is first booted. The main difference here is that you can call this function to effectively reset the cart.
function _init()
cam={h=128,x=5,y=5}
look(0)
end
This is a helper function to point the camera in a certain direction. It also stores some calculations for use when drawing, the sine and cosine of the angle, as well as the vectors of the left and right side of the screen.
function look(a)
local c=cos(a)
local s=sin(a)
--pico 8 uses "turns" rather
--than radians
cam.a=a%1
cam.c=c
cam.s=s
--calculate the left and right
--edges of the screen, 90 fov.
--adding and subtracting the
--vector looking right gives 45
--degrees in either direction
--
--(x=s,y=-c) == right vector
--e.g:
--(x=0,y=-1) == forwards
--(x=1,y= 0) == right
cam.l={x=c-s,y=s+c}
cam.r={x=c+s,y=s-c}
end
This function is to move the player on the map using the sine and cosine values calculated in look.
function move(spd)
--cosine of an angle is the
--x offset of a unit circle
cam.x+=cam.c*spd
--sine of an angle is the
--y offset of a unit circle
cam.y+=cam.s*spd
end
The _update60 function is called once per frame, before drawing. This is your chance to react to input and update the game state. The 60 in the name represents frames per second. The default function is just called _update and is called 30 times a second.
function _update60()
if btn(⬆️) then
move(spd)
end
if btn(⬇️) then
move(-spd)
end
if btn(➡️) then
look(cam.a+trn_spd)
end
if btn(⬅️) then
look(cam.a-trn_spd)
end
if btn(❎) then
cam.h=min(500,cam.h+1)
end
if btn(🅾️) then
cam.h=max(1,cam.h-1)
end
end
The _draw function is called after update to render the screen. When your game slows down and cannot keep up with the requested fps this is called less frequently, but update is still called regularly.
function _draw()
cls(12)
for y=hrzn,128 do
--start sampling the map from
--the top, no matter where the
--horizon is. adding 1 to stop div/0
--division by zero on the next
--line
local ln=y-hrzn+1
--since line increases down
--the screen, depth decreases
--higher line = further away
local dpth=cam.h/ln
--map tile coords of the left
--and right side of the screen
local lx=cam.x+dpth*cam.l.x
local ly=cam.y+dpth*cam.l.y
local rx=cam.x+dpth*cam.r.x
local ry=cam.y+dpth*cam.r.y
local ln_wth=rx-lx
local ln_hgt=ry-ly
tline(
--line coords, full scanline
0,y,128,y,
--map start coords
lx,ly,
--width/pixels=h step
ln_wth/128,
--height/pixels=v step
ln_hgt/128)
end
print("h :"..cam.h)
print("a :"..cam.a)
print("x,y:"..cam.x..","..cam.y)
end