I realized I forgot to explain what is the road block type last time, so before talking about the ship rendering, I’m talking about the road again.

# The road

## Road type

If you remember, I explained that the road data structure is a straight array of road block, with each road block having properties (coordinates, turn effect, a color and a type). I also explained how to model a half-pipe road. So what is the type of road block ? As you can see when playing, the road is not always in the same configuration, sometimes flat, sometime only right or left part of half-pipe and sometime both. This is the road type. The road type is a bit field that tell how the half-pipe is.

Value 256|...|16|8|4|2|1| -----------------------+---+--+-+-+-+-+ 2nd right elevation | | | | | |X| 1st right elevation | | | | |X| | Normal flat road | | | |X| | | 1st left elevation | | |X| | | | 2nd left elevation | | X| | | | | Start lane X| | | | | | |

So a road type with full left elevation and 1 right is valued type=30 and looks like this :

This value is set automatically when there is turns but it also can be force. That’s the case of the screenshot where the default for a straight road type is 4. I didn’t have enough pico8 token to put all the special road type I first wanted, but it is the game with pico8 !

# The ship

## Stacked Data

Ships are stored directly on the spritesheet. They are composed of 3 stacked layers, so editing a ship is really easy (and fun, I think !), you just have to draw it. They are loaded in a array at the race init. You have the same kind of storage in Zepton and maybe other pico 8 cartridge. We also keep a shadow in a array, which is the 3 layers merged into one.

You can take a look at the implementation in the loadss function below. There is some extra information like width, height and mass compute from the pixels. You can also see that the shadow is half precision, to save some cpu when rendering.

-- loading a ship function loadss(nbss,ss,sss) local m,xmin,xmax,zmin,zmax= 0,8,0,8,0 for k=7,0,-1 do-- y for j=0,7 do-- x local shad=false for i=2,0,-1 do c=sget(8*i+j,k+nbss*8) if c~=0 then v,shad= {x=j-4,y=i-1,z=4-k, oz=4-k,c=c,pat=0}, c~=10 m+=1 add(ss,v) end end --half z if shad and k%2==0 then v={x=j-4,y=0,z=4-k, oz=0,c=9,pat=1} add(sss,v) xmin,xmax,zmin,zmax= min(v.x,xmin),max(v.x,xmax), min(v.z,zmin),max(v.z,zmax) end end end -- /5 because half_width*scale local si=ship_info[nbss+1] si.mass,si.width,si.height= m,(xmax-xmin)/5,(zmax-zmin)/5 end

## Rendering

As we always see ships from the back when playing, we carefully load them from front to back, so they can be draw without sorting them and this saves a lot of cpu. The projection is the same that the one used for the road vertices (weak perspective projection). This projection is applied for each voxel. Another important feature we need to render ship is rotation, on multiple axis, because of half-pipe. The ship has the same rotation for all the voxels, so I compute one rotation matrix (I use a euler angles), one per ship and per frame. Then I apply the matrix to all the voxels. In splitscreen, this is done only once too. I could have save some cpu by not computing rotation when the ships are not visible, it only save cpu for some kind of situation, when we only see few ships. When we have to see all ship, it doesn’t save cpu and may have a little overhead. Furthermore, I didn’t have enough pico8 tokens left ! Here is the calcrotmat function, you can see that pitch, yaw and roll may not be standard, but I messed up to early with axis name to change them.

function calcrotmat(rot) --pitch x yaw y roll z local cosa,sina,cosb,sinb,cosc,sinc= cos(rot.z),sin(rot.z), cos(-rot.y),sin(-rot.y), cos(rot.x),sin(rot.x) local axx,axy,axz= cosa*cosb, cosa*sinb*sinc-sina*cosc, cosa*sinb*cosc+sina*sinc local ayx,ayy,ayz= sina*cosb, sina*sinb*sinc+cosa*cosc, sina*sinb*cosc-cosa*sinc local azx,azy,azz= -sinb,cosb*sinc,cosb*cosc return {xx=axx,xy=axy,xz=axz, yx=ayx,yy=ayy,yz=ayz, zx=azx,zy=azy,zz=azz} end

A bit later, I use the applyrot function to apply the rotation on a ship. This is done once in the _update function.

function applyrot(ss,m,sort) -- faster 0.02cpu but +30 tokens local xx,xy,xz, yx,yy,yz, zx,zy,zz,rr =m.xx,m.xy,m.xz, m.yx,m.yy,m.yz, m.zx,m.zy,m.zz, {} for v in all(ss) do local r={} r.x,r.y,r.z, r.c,r.pat= xx*v.x+xy*v.y+xz*v.z, yx*v.x+yy*v.y+yz*v.z, zx*v.x+zy*v.y+zz*v.z, v.c,v.pat r.key=r.z add(rr,r) end -- z sort if (sort) shellsort(rr) return rr end

Another little performance trick here is because rotation matrix is stored in a array, by accessing them once before the loop, we save 2% cpu because we no longer use array acces for each voxel.

Then, we have rotated voxels in world space, we can project them in the screen space, like we did for the road vertices. And as I’m lucky, I get this :

On this early screenshot, you can see an orange shadow. Actually, it is not a shadow but the light projected by the sustentation system. But it is difficult to “read”, so after some tries and twitter advices, I paint it black 😀

In this case, we also sort the voxel on Z axis because the rotation is complete. But I only keep this sorting for the menu rendering, not in the game.

## Ships on the Road

As I mentionned earlier, the road is a straight array. I can even tell that the road is flat ! I handle the ship integration on a unfolded and flat road. Before rendering the road and the ship, the unfolded world is converted to the “real” world, the one with a half-pipe. At this moment, a rotation is applied around the ship Z axis to make it look like it fly parallel to the half-pipe part. I use a linear interpolation when changing angle to make the movement smooth.

So, according to the place the ship is placed on the unfolded road, a rotation on Z axis is added, and also gravity to make the ship “fall” to the half-pipe flat part. And here is the magic.

These tricks are the main structure of the rendering, all the details are integrated in this unfolded straight road. This save a lot of cpu for the other feature of the game. For example, collision checking is made in 2 dimensions, on the flat road.

Here is an excerpt of the convert function

function convert_xflat_to_x(pos,decx,ytmp,rot,spdp,shipp) local xflat,cumx,cumy,newz= pos.xflat-decx,0,0,1 pos.x,pos.y=pos.xflat,0 -- for each road part for stp in all(rd_info) do if xflat > stp.fl then -- startx,y stx,sty=stp.sx,stp.sy cumx,cumy=stx+stp.dx,sty+stp.dy pos.x=decx+lerp(stx,cumx,(xflat-stp.fl)/stp.dst) pos.y=lerp(sty,cumy,(xflat-stp.fl)/stp.dst) newz=stp.rotz break end end -- keep rotation if rot~=nil then rot.z=lerp(rot.z,newz,0.25) newz=rot.z end if pos.yflat then pos.x=pos.x +sin(1-newz)*pos.yflat pos.y=ytmp+pos.y +cos(newz)*pos.yflat end ... -- gravity if spdp~=nil then local gx=sin(newz) spdp.x-=gx end end

The other ship, the ones that are away from the one you control, are rendering the same. There is just one difference linked to the fact that the turns are “old school”, based on an offsets accumulation. This offset accumulation must be considered to place the ship on X axis. If not the other ships will be placed near the middle of the screen even instead of on the road, if the road turns left or right. This is well explained in the resource link I provide, so I’ll not tell more here.

I Hope you like this insight, if you have questions you can leave a comment or contact me on twitter @yourykiki. Don’t forget you can acces the whole code on lexaloffle bbs, the “code” link just above the game.

Have fun !

Resource :

This is incredible. The part about 3 layer stacking the ships making voxels out of pixels is so clever!

Hey, v cool game! i dont understand how the code works though; so the vertical and half vertical floors are all actually flat ?

Cheers.

Yes, the track use an oldschool illusion to avoid the pain of real 3D rotation, and give the feeling of the turns