Mot [Lexaloffle Blog Feed]https://www.lexaloffle.com/bbs/?uid=39676 Instant 3D plus! <h1>Instant 3D plus!</h1> <img style="" border=0 src="/media/39676/d3d2.p8_1.gif" alt="" /> <p><a href="https://www.lexaloffle.com/bbs/?tid=37733">Instant 3D!</a> was a random idea, quickly thrown together to see if it was possible. But after seeing the cool things people can do with it, I wanted to clean it up properly, and also present some of the internal functions more cleanly.</p> <p>Making the 3D functions more accessible means:</p> <ol> <li>You can often get your game working correctly in 3D even if the Instant 3D &quot;magic&quot; doesn't work correctly, by calling the 3D spr/map functions directly with the right parameters.</li> <li>You can do things that the original Instant 3D can't do, like having objects that hover into the air.</li> </ol> <p>I've added a little tutorial of converting a 2D game to 3D to illustrate how this works, at the bottom of this post.</p> <p>Obviously this &quot;snippet&quot; is still very limited, compared to a general purpose 3D library say. You can't use it to create an FPS or a flight simulator. But I think it's a lot easier to use - start with a 2D game, drop it in, and fix up the bits that don't come out right. And you can still do some cool looking 3D stuff with it.</p> <p>Here's the updated snippet:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>-- instant 3d+! do -- parameters p3d={ vanish={x=64,y=0}, -- vanishing pt d=128, -- screen dist in pixels near=1, -- near plane z camyoff=32, -- added to cam y pos camheight=32 -- camera height } -- save 2d versions map2d,spr2d,sspr2d,pset2d,camera2d=map,spr,sspr,pset,camera -- 3d camera position local cam={x=0,y=0,z=0} -- is 3d mode enabled? is3d=false -- helper functions -- screen to camera space local function s2c(x,y,z) return x-cam.x,y-cam.y,z-cam.z end -- perspective projection local function proj(x,y,z) if -y&gt;=p3d.near then local scale=p3d.d/-y return x*scale+p3d.vanish.x,-z*scale+p3d.vanish.y,scale end end -- screen to projected local function s2p(x,y,z) local x,y,z=s2c(x,y,z) return proj(x,y,z) end -- 3d drawing fns function sspr3d(sx,sy,sw,sh,x,y,z,w,h,fx,fy) w=w or sw h=h or sh local px,py,scale=s2p(x,y,z) if(not scale)return local pw,ph=w*scale,h*scale -- sub pixel stuff local x0,x1=flr(px),flr(px+pw) local y0,y1=flr(py),flr(py+ph) sspr2d(sx,sy,sw,sh,x0,y0,x1-x0,y1-y0,fx,fy) end spr3d=function(n,x,y,z,w,h,fx,fy) if(not z)return -- convert to equivalent sspr() call w=(w or 1)*8 h=(h or 1)*8 local sx,sy=flr(n%16)*8,flr(n/16)*8 sspr3d(sx,sy,w,h,x,y,z,w,h,fx,fy) end function map3d(cx,cy,x,y,z,w,h,lyr) if(not h)return -- near/far corners local fx,fy,fz=s2c(x,y,z) local nx,ny,nz=s2c(x,y+h*8,z) -- clip ny=min(ny,-p3d.near) if(fy&gt;=ny)return -- project local npx,npy,nscale=proj(nx,ny,nz) local fpx,fpy,fscale=proj(fx,fy,fz) if npy&lt;fpy then local tx,ty,ts=npx,npy,nscale npx,npy,nscale=fpx,fpy,fscale fpx,fpy,fscale=tx,ty,ts end -- clamp npy=min(npy,128) fpy=max(fpy,0) -- rasterise local py=flr(npy) while py&gt;=fpy do -- floor plane intercept local g=(py-p3d.vanish.y)/p3d.d local d=-nz/g -- map coords local mx,my=cx,(-fy-d)/8+cy -- project to get left/right local lpx,lpy,lscale=proj(nx,-d,nz) local rpx,rpy,rscale=proj(nx+w*8,-d,nz) -- delta x local dx=w/(rpx-lpx) -- sub-pixel correction local l,r=flr(lpx+0.5)+1,flr(rpx+0.5) mx+=(l-lpx)*dx -- render tline(l,py,r,py,mx,my,dx,0,lyr) py-=1 end end function map3dupright(cx,cy,x,y,z,w,h,lyr) if(not h)return local px,py,scale=s2p(x,y,z) if(not scale)return local pw,ph=w*8*scale,h*8*scale -- texture step local dx,dy=w/pw,h/ph local mx,my=cx+0.0625,cy+0.0625 -- sub pixel stuff local x0,x1=flr(px),flr(px+pw) local y0,y1=flr(py),flr(py+ph) mx+=(x0-px)*dx my+=(y0-py)*dy if(x0&gt;=x1 or y0&gt;=y1)return for y=y0,y1-1 do tline(x0,y,x1,y,mx,my,dx,0,lyr) my+=dy end end function camera3d(x,y,z) cam.x,cam.y,cam.z=x,y,z end -- &quot;instant 3d&quot; wrapper functions local function icamera(x,y) cam.x=(x or 0)+64 cam.y=(y or 0)+128+p3d.camyoff cam.z=p3d.camheight end local function isspr(sx,sy,sw,sh,x,y,w,h,fx,fy) z=h or sh y+=z sspr3d(sx,sy,sw,sh,x,y,z,w,h,fx,fy) end local function ispr(n,x,y,w,h,fx,fy) z=(h or 1)*8 y+=z spr3d(n,x,y,z,w,h,fx,fy) end local function imap(cx,cy,x,y,w,h,lyr) cx=cx or 0 cy=cy or 0 x=x or 0 y=y or 0 w=w or 128 h=h or 64 map3d(cx,cy,x,y,0,w,h,lyr) end function go3d() camera,sspr,spr,map=icamera,isspr,ispr,imap camera2d() is3d=true end function go2d() map,spr,sspr,pset,camera=map2d,spr2d,sspr2d,pset2d,camera2d is3d=false end -- defaults icamera() end -- enable 3d mode go3d() menuitem(3,&quot;3d&quot;,go3d) menuitem(2,&quot;2d&quot;,go2d)</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>As before, to use it, just copy it into your 2D program. It should behave exactly the same.<br /> There's one new feature, in that you can toggle between 2D and 3D in the Pico-8 menu.</p> <img style="" border=0 src="/media/39676/pakutto boy 1.00_3.gif" alt="" /> <p>You can also do it in code using</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>go2d()</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>and </p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>go3d()</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>which might be useful for 2D title screens etc.</p> <h3>Taking it further</h3> <p>The basics are the same as before, but you can now - with a little bit of work - take your games a bit further by making use of explicit 2D and 3D commands, rather than leaving it up to the snippet to guess your intent.</p> <p>To illustrate this, I made a little 2D cart to convert into 3D.</p> <p> <table><tr><td> <a href="/bbs/?pid=76690#p"> <img src="/bbs/thumbs/pico8_instant3dplus-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=76690#p"> instant3dplus</a><br><br> by <a href="/bbs/?uid=39676"> Mot</a> <br><br><br> <a href="/bbs/?pid=76690#p"> [Click to Play]</a> </td></tr></table> </p> <p>This is a simple little game where you're a bouncing ball that collects coins. It's not finished, but is enough to demonstrate the process. The game is already a little bit 3D in that the ball and coins also have a height, and objects can move in front and behind each other, so it has to sort their positions and draw them from back to front.</p> <img style="" border=0 src="/media/39676/demo3d_1.gif" alt="" /> <p>Dropping the snippet into this program has... mixed results.</p> <img style="" border=0 src="/media/39676/d3d1.p8_0.gif" alt="" /> <p>As you can see, it's kind of 3D, but it has some issues:</p> <ul> <li>The ball doesn't always bounce straight up. In fact if you look closely it's actually staying on the ground and just bounces away and back again.</li> <li>The coins aren't raised up properly either.</li> <li>The metallic struts are flat on the ground, rather than standing up.</li> <li>Likewise the gratings on top are also flat on the ground.</li> <li>The 3 lives are displayed in the wrong place.</li> </ul> <p>Obviously the snippet doesn't know exactly what we're trying to achieve, but fortunately we can help it out.</p> <p>With a little bit of work we can make it look like this:</p> <img style="" border=0 src="/media/39676/d3d2.p8_1.gif" alt="" /> <h3>3D functions</h3> <p>We can fix up the ball using an explicit 3D function. The snippet provides explicit 3D functions spr3d, sspr3d and map3d. They have the same parameters as the standard functions, except there's a Z parameter immediately after the X and Y screen parameters.</p> <p>They use a 3D coordinate system where:</p> <ul> <li>The X axis is to the right</li> <li>The Y axis is out of the screen (towards you)</li> <li>The Z axis is up</li> </ul> <p>This keeps X and Y consistent with the &quot;Instant 3D&quot; logic, where instead of moving up the screen as Y decreases, objects move away from you. The new Z parameter allows us to also specify the height.</p> <p>The ball drawing code looks like this:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre> elseif thing.typ==&quot;player&quot; then shadowcols() sspr(0,32, 8,8, thing.x-4,thing.y-1, 8,4) pal() spr(64+thing.frame%9, thing.x-4, thing.y-thing.height-8) end</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>The sspr() call draws the shadow, which looks correct already, so it doesn't need to change.<br /> The spr() call draws the ball, based on the thing.x,-.y and -.height variables. The game stores the position of bottom center of the ball, so it has to subtract 4 and 8 to get the top left corner for spr().<br /> We can change it to an explicit 3D call as follows:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre> spr3d(64+thing.frame%9, thing.x-4, thing.y, thing.height+8) </pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>We're still specifying the top left corner, but now it's in 3D.</p> <p>The coin code is similar:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre> if thing.typ==&quot;coin&quot; then shadowcols() sspr(0,40, 8,8, thing.x-4,thing.y-1, 8,4) pal() spr(80, thing.x-4,thing.y-thing.height-8) </pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Once again we change the spr call to an spr3d:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre> spr3d(80, thing.x-4, thing.y, thing.height+8) </pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>With 3D positions supplied, the ball and coins now bounce/float above the ground properly.</p> <img style="" border=0 src="/media/39676/demo3d_2.gif" alt="" /> <h3>Upright maps</h3> <p>Next we can address the red metal structs. Currently they are lying flat on the ground instead of standing up straight.<br /> The &quot;instant 3D&quot; snippet assumes everything drawn with &quot;spr&quot;/&quot;sspr&quot; is upright, and everything drawn with &quot;map&quot; is flat on the ground. However the struts are drawn using map(), so that they can be composed of multiple sprites.<br /> So we need to tell the game to draw them upright.</p> <p>map3d won't help in this case. We can use it to draw them higher up, but they will still be lying flat, not standing upright. So to help with this the snippet provides an alternative &quot;map3dupright&quot; function.</p> <p>The strut drawing code is:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre> elseif thing.typ==&quot;strut&quot; then map(127,0, thing.x,thing.y-40, 1,5)</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>We can change the map call to a map3dupright like this:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre> map3dupright(127,0, thing.x, thing.y, 40, 1,5)</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Once again it has the same parameters as &quot;map&quot;, except there's a Z parameter immediately after the X and Y.<br /> The struts are 40 pixels high, so we set the Z (i.e. the height) to 40, to specify the top left corner position.</p> <p>With this in place, the struts now stand upright.</p> <img style="" border=0 src="/media/39676/demo3d_3.gif" alt="" /> <p> <table><tr><td> <a href="/bbs/?pid=76690#p"> <img src="/bbs/thumbs/pico8_instant3dplus-1.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=76690#p"> instant3dplus</a><br><br> by <a href="/bbs/?uid=39676"> Mot</a> <br><br><br> <a href="/bbs/?pid=76690#p"> [Click to Play]</a> </td></tr></table> </p> <h3>3D parameters</h3> <p>Now that objects are above the ground they often disappear above the top of the screen.<br /> This is due to the camera height, and the &quot;vanishing point&quot; of the 3D projection, which is currently set to the top of the screen.</p> <p>We can easily fix this by changing the 3D parameters.<br /> The default parameters are in the snippet:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre> -- parameters p3d={ vanish={x=64,y=0}, -- vanishing pt d=128, -- screen dist in pixels near=1, -- near plane z camyoff=32, -- added to cam y pos camheight=32 -- camera height }</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>We can move the vanishing point into the center of the screen by adding a line to the _init function:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre> p3d.vanish.y=64</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Moving the vanishing point is like rotating the camera upwards slightly. Now we can easily see everything.<br /> In fact we can even move the camera down a little and nearer to the ball:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre> p3d.camheight=20 p3d.camyoff=20</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <img style="" border=0 src="/media/39676/demo3d2_0.gif" alt="" /> <h3>3D map coordinates</h3> <p>Now we'll fix the metal grates. These are supposed to sit on top of the struts.<br /> The grates are rendered as a single map, much like the floor. We need to tell the game to draw that map above the ground.</p> <p>This time map3d is the correct function to use.</p> <p>The existing code looks like this.</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre> -- roof map map(32,0,0,-40,16,64) </pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>The 3D code is quite similar:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre> -- roof map map3d(32,0,0,0,40,16,64) </pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Once again we have a new Z parameter after the X and Y, which we set to 40 to move it up into the air.</p> <p>In the 2D version the grates are drawn last, as because they are above everything. However in the 3D version they actually need to be drawn before the ball, coins and struts to get the correct ordering.<br /> So the line should be moved up immediately after the &quot;map&quot; call that draws the floor.</p> <h3>2D drawing</h3> <p>The last thing to fix is the lives display. Lives are displayed as 3 balls, but they are drawn in the wrong place, because the &quot;instant 3D&quot; logic is trying to position them in 3D.</p> <p>In this case we really just want to draw them in 2D.</p> <p>Fortunately the snippet saves the original 2D functions as &quot;spr2d&quot;, &quot;sspr2d&quot; and &quot;map2d&quot;, so we can call them directly if we need to.</p> <p>The life drawing code looks like this:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>-- overlay camera() fancyprint(&quot;lives&quot;,4,4,12) for i=1,player.lives do spr(64,25+(i-1)*9,2) end</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Simply change the &quot;spr&quot; to &quot;spr2d&quot; and we're done.</p> <img style="" border=0 src="/media/39676/d3d2.p8_1.gif" alt="" /> <h3>2D/3D code</h3> <p>Adding the various 3D calls makes the game look correct in 3D now. But if you switch it back to 2D (via the pause menu) it now looks broken.</p> <p>One solution is to simply remove the &quot;menuitem&quot; calls from the snippet and disable mode switching. This is perfectly valid if the game is only supposed to be 3D.</p> <p>But if you really do want the 2D option, it is still possible. The snippet includes a variable &quot;is3d&quot; which is set to true in 3D mode. Before calling any 3D function, check it to ensure you are actually in 3D mode. If not, perform the original 2D call instead. For example:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>if is3d then spr3d(64+thing.frame%9, thing.x-4, thing.y, thing.height+8) else spr(64+thing.frame%9, thing.x-4, thing.y-thing.height-8) end</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Here's the final cart with 2D and 3D mode support.</p> <p> <table><tr><td> <a href="/bbs/?pid=76690#p"> <img src="/bbs/thumbs/pico8_instant3dplus-2.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=76690#p"> instant3dplus</a><br><br> by <a href="/bbs/?uid=39676"> Mot</a> <br><br><br> <a href="/bbs/?pid=76690#p"> [Click to Play]</a> </td></tr></table> </p> <h3>Other bits</h3> <p>That's the gist of how to use it. There are a couple of functions I've missed, like camera3d (sets the 3D camera position explicitly, rather than inferring it from the 2D position and height/y-offset parameters), and a pset3d/pset2d which should do what you'd expect.</p> <p>Feel free to throw me any questions you have.</p> <p>-Mot</p> <p>** Update 1: Fix spr() when drawing larger than 1x1 sprites</p> https://www.lexaloffle.com/bbs/?tid=37982 https://www.lexaloffle.com/bbs/?tid=37982 Sun, 17 May 2020 00:58:30 UTC Object literals and token counting <p>I often find myself declaring a lot of arrays/object literals that are essentially just data. No computation involved.<br /> For example:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>-- background types bg_tree={ tex={ {sx=0,sy=64,sw=32,sh=32} }, w=2,h=2, spacing=2 } bg_lamp={ tex={ {sx=0,sy=32,sw=16,sh=8} }, w=.5,h=.25, spacing=2, trans=12 } </pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>It's a useful way to create flexible, extendable systems.</p> <p>Unfortunately it quickly chews through the token count.</p> <p>So I was wondering what people's views were of <strong>changing the token counting rules</strong> around such object literals? - say 1 token per object literal.</p> <p>They would have to be restricted in order to qualify for a reduced token count, i.e. data only, no computation, functions etc, similar to how JSON is a restricted subset of JavaScript.<br /> The compiler could either detect this automatically, or possibly a new &quot;data only literal&quot; syntax could be introduced.</p> <p>I feel like this probably aligns with the PICO-8 principals, as it's arguably data rather than code, and you still have the compressed size limit. It seems like a much cleaner solution than the string shredding/JSON parsing workarounds that are currently used.</p> https://www.lexaloffle.com/bbs/?tid=37941 https://www.lexaloffle.com/bbs/?tid=37941 Thu, 14 May 2020 02:15:29 UTC Variable inspector <h1>Variable inspector window</h1> <img style="" border=0 src="/media/39676/planegame_000.png" alt="" /> <p>Debugging carts with &quot;print&quot; and &quot;printh&quot; can be cumbersome, so I made a little snippet to help.<br /> It adds a little window where you can view variables and drill down into them.</p> <p>To use it, add the snippet somewhere into your program:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>dbg=(function() poke(0x5f2d, 1) -- watched variables local vars,emp={},true -- window state local exp=false -- text cursor local x,y -- scrollbar local sy=0 local sdrag -- mouse state local mx,my,mb,pb,click,mw function clicked(x,y,w,h) return click and mx&gt;=x and mx&lt;x+w and my&gt;=y and my&lt;y+h end function butn(txt,x,y,c) print(txt,x,y,c) return clicked(x,y,4,6) end -- convert value into something easier to traverse and inspect function inspect(v,d) d=d or 0 local t=type(v) if t==&quot;table&quot; then if(d&gt;5)return &quot;[table]&quot; local props={} for key,val in pairs(v) do props[key]=inspect(val,d+1) end return { expand=false, props=props } elseif t==&quot;string&quot; then return chr(34)..v..chr(34) elseif t==&quot;boolean&quot; then return v and &quot;true&quot; or &quot;false&quot; elseif t==&quot;nil&quot; or t==&quot;function&quot; or t==&quot;thread&quot; then return &quot;[&quot;..t..&quot;]&quot; else return &quot;&quot;..v end end function drawvar(var,name) if type(var)==&quot;string&quot; then print(name..&quot;:&quot;,x+4,y,6) print(var,x+#(&quot;&quot;..name)*4+8,y,7) y+=6 else -- expand button if(butn(var.expand and &quot;-&quot; or &quot;+&quot;,x,y,7))var.expand=not var.expand -- name print(name,x+4,y,12) y+=6 -- content if var.expand then x+=4 for key,val in pairs(var.props) do drawvar(val,key) end x-=4 end end end function copyuistate(src,dst) if type(src)==&quot;table&quot; and type(dst)==&quot;table&quot; then dst.expand=src.expand for key,val in pairs(src.props) do copyuistate(val,dst.props[key]) end end end function watch(var,name) name=name or &quot;[var]&quot; local p,i=vars[name],inspect(var) if(p)copyuistate(p,i) vars[name]=i emp=false end function clear() vars,emp={},true end function draw(dx,dy,w,h) dx=dx or 0 dy=dy or 48 w=w or 128-dx h=h or 128-dy -- collapsed mode if not exp then dx+=w-10 w,h=10,5 end -- window clip(dx,dy,w,h) rectfill(0,0,128,128,1) x=dx+2 y=dy+2-sy -- read mouse mx,my,mw=stat(32),stat(33),stat(36) mb=band(stat(34),1)~=0 click=mb and not pb and mx&gt;=dx and mx&lt;dx+w and my&gt;=dy and my&lt;dy+h pb=mb if exp then -- variables for k,v in pairs(vars) do drawvar(v,k) end -- scrollbar local sh=y+sy-dy if sh&gt;h then local sx=dx+w-4 local by=sy/sh*h+dy local bh=h/sh*h rectfill(sx,dy,dx+w,dy+h-1,5) rectfill(sx,by,dx+w,by+bh-1,sdrag and 12 or 6) rect(sx,by,dx+w-1,by+bh-1,13) if click and mx&gt;=sx then if my&lt;by then sy-=h elseif my&gt;by+bh then sy+=h else sdrag=by-my end end if sdrag then sy=(my+sdrag-dy)*sh/h else sy-=mw*8 end if(sy&lt;0)sy=0 if(sy+h&gt;sh)sy=sh-h if(not mb)sdrag=nil else sy=0 end -- clear btn if(butn(&quot;x&quot;,dx+w-15,dy,14))clear() end -- expand/collapse btn if(butn(exp and &quot;-&quot; or &quot;+&quot;,dx+w-10,dy,14))exp=not exp -- draw mouse ptr clip() line(mx,my,mx,my+2,8) color(7) end function show() exp=true while exp do draw() flip() end end function prnt(v,name) watch(v,name) show() end return{ watch=watch, clear=clear, empty=function() return emp end, expand=function(val) if(val~=nil)exp=val return exp end, draw=draw, show=show, print=prnt } end)()</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>There are a few different ways to use it.</p> <h3>Global variables</h3> <p>You can view global variables when your program is paused by typing:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>dbg.print(myvariable)</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <img style="" border=0 src="/media/39676/planegame_001.png" alt="" /> <p>The window will popup and run until you click &quot;-&quot; at the top right to exit it.<br /> <em>Note: If you press &quot;Esc&quot; to exit the inspection window, and try to resume your game with &quot;resume&quot;, it will resume the popup window instead! To avoid this, use the &quot;-&quot; button.</em></p> <h3>Local variables</h3> <p>To view a local variable inside a function you need to add code to that function to capture it.</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>dbg.watch(myvariable,&quot;my variable&quot;)</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>This will capture it's value and add it to your variable list. The second parameter is the name. You can add multiple variables so long as they all have different names. Calling dbg.watch again with the same name causes it to replace the previous value.</p> <p>To view your captured variables, pause your program by pressing Esc, and type:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>dbg.show()</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <img style="" border=0 src="/media/39676/planegame_002.png" alt="" /> <p>Be aware that the variable inspector window shows the variable's value <strong>at the time dbg.watch() was executed</strong>. If the variable has been changed since, the changes will <strong>not</strong> show in the inspector. (This is deliberate.)</p> <h3>Displaying variables while the program is running</h3> <p>If you want to run the inspector window directly in your program, add:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>dbg.draw()</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>to your _draw() function.</p> <p>You can optionally specify the window position (x,y,width,height), e.g.</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>dbg.draw(64,0,64,48)</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>By default the window displays collapsed, until you click the &quot;+&quot; button.</p> <img style="" border=0 src="/media/39676/planegame_003.png" alt="" /> <p>Remember you still need to capture variables with &quot;dbg.watch()&quot;, otherwise the window will be empty.</p> <h3>Pausing while the inspector is open</h3> <p>The game will continue running while the inspector window is open.<br /> If you'd prefer it to pause the game, you can add:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>if(dbg.expand())return</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>to the top of your &quot;_update&quot; function.</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>dbg.expand()</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>returns true when the window is expanded or false when collapsed.<br /> You can also force it open with:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>dbg.expand(true)</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>or collapse it with:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>dbg.expand(false)</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>For example, to invoke the editor from the Pico-8 built in menu:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>menuitem(1,&quot;debug&quot;,function()dbg.expand(true)end)</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <h3>Performance considerations</h3> <p>This snippet works by traversing your variables and building a &quot;snapshot&quot; tree structure. If you capture large variables every frame you might get some slow down, due to the traversal and possibly the Lua garbage collector cleaning up snapshots from previous frames. So make sure to remove your dbg.watch() calls when you want your program to run fast again.</p> https://www.lexaloffle.com/bbs/?tid=37822 https://www.lexaloffle.com/bbs/?tid=37822 Sat, 09 May 2020 02:37:20 UTC Instant 3D! <h1>Turn your 2D game into a 3D game!</h1> <img style="" border=0 src="/media/39676/pakutto boy 1.00_0.gif" alt="" /> <img style="" border=0 src="/media/39676/pic-oh mummy!_0.gif" alt="" /> <p>That's <strong>50% extra D!</strong><br /> 100% <em>guaranteed</em> to work, sometimes, if you're lucky.<br /> Simply paste this into the top of your 2D game, and be the envy of all your 2D coding friends!!!1!1</p> <p>(OK, I'll stop now.)</p> <p>This works best for top down games like &quot;Pic-oh mummy!&quot; by <a href="https://www.lexaloffle.com/bbs/?uid=13427"> @Hokutoy</a>, &quot;Pakutto boy&quot; by <a href="https://www.lexaloffle.com/bbs/?uid=15494"> @Konimiru</a> (both pictured)<br /> Admittedly it's more of a gimmick/experiment - the result isn't very playable due to not being able to see the whole play area.</p> <p>I mainly just wanted to see if it would work :)</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>_={ map=map, spr=spr, sspr=sspr, pset=pset, circ=circ, camera=camera, cx=0, cy=0, cyoff=32, ch=32, d=128, ymid=0, s2w=function(x,y) return (x-_.cx)-64,_.ch,128-(y-_.cy) end, proj=function(x,y,z) local scale=_.d/z return x*scale+64,y*scale+_.ymid,scale end } camera=function(x,y) _.cx=x or 0 _.cy=(y or 0)+_.cyoff end camera(0,0) pset=function(x,y,c) local wx,wy,wz=_.s2w(x,y) if(wz&lt;1)return local px,py=_.proj(wx,wy,wz) _.pset(px,py,c) end circ=function(x,y,r,c) local wx,wy,wz=_.s2w(x,y) if(wz&lt;1)return local px,py,scale=_.proj(wx,wy,wz) _.circ(px,py,r*scale,c) end sspr=function(sx,sy,sw,sh,x,y,w,h,fx,fy) w=w or sw h=h or sh local wx,wy,wz=_.s2w(x+w/2,y+h) if(wz&lt;1)return local px,py,scale=_.proj(wx,wy,wz) local pw,ph=w*scale,h*scale -- sub pixel stuff local x0,x1=flr(px-pw/2),flr(px+pw/2) local y0,y1=flr(py-ph),flr(py) _.sspr(sx,sy,w,h,x0,y0,x1-x0,y1-y0,fx,fy) end spr=function(n,x,y,w,h,fx,fy) if(not n or not x or not y)return -- convert to equivalent sspr() call w=(w or 1)*8 h=(h or 1)*8 local sx,sy=flr(n%16)*8,flr(n/16)*8 sspr(sx,sy,w,h,x,y,w,h,fx,fy) end map=function(cx,cy,x,y,w,h,lyr) cx=cx or 0 cy=cy or 0 x=x or 0 y=y or 0 w=w or 128 h=h or 64 -- near/far corners local fx,fy,fz=_.s2w(x,y) local nx,ny,nz=_.s2w(x,y+h*8) -- clip nz=max(nz,0.5) if(fz&lt;=nz)return -- project local npx,npy,nscale=_.proj(nx,ny,nz) local fpx,fpy,fscale=_.proj(fx,fy,fz) -- clamp npy=min(npy,128) -- rasterise local py=flr(npy) while py&gt;=fpy do -- floor plane intercept local g=(py-_.ymid)/_.d local z=_.ch/g -- map coords local mx,my=cx,(fz-z)/8+cy -- project to get left/right local lpx,lpy,lscale=_.proj(nx,ny,z) local rpx,rpy,rscale=_.proj(nx+w*8,ny,z) -- delta x local dx=w/(rpx-lpx) -- sub-pixel correction local l,r=flr(lpx+0.5)+1,flr(rpx+0.5) mx+=(l-lpx)*dx -- render tline(l,py,r,py,mx,my,dx,0,lyr) py-=1 end end</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p><strong>EDIT 1:</strong> adjusted near plane to avoid numeric overflow. Defaults for omitted map() parameters.<br /> <strong>EDIT 2:</strong> also 3Dise sspr, pset and circ<br /> <strong>EDIT 3:</strong> sub-pixel logic<br /> <strong>EDIT 4:</strong> add map() layer parameter. fix gaps between adjacent sprites.</p> <h1>New version!</h1> <p>I've made a new version called <a href="https://www.lexaloffle.com/bbs/?tid=37982">Instant 3D plus!</a> :-).<br /> It should behave exactly like the original, but it's cleaned up quite a bit, and you can call the 3D functions directly to fix the bits that it doesn't get right automatically - or place things in exact 3D positions (like up in the air).</p> <img style="" border=0 src="/media/39676/d3d2.p8_1.gif" alt="" /> <p>More details in <a href="https://www.lexaloffle.com/bbs/?tid=37982">the other post</a>.</p> https://www.lexaloffle.com/bbs/?tid=37733 https://www.lexaloffle.com/bbs/?tid=37733 Mon, 04 May 2020 11:57:12 UTC Ramps <p> <table><tr><td> <a href="/bbs/?pid=72871#p"> <img src="/bbs/thumbs/pico8_mot_ramps-14.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=72871#p"> Ramps (working title)</a><br><br> by <a href="/bbs/?uid=39676"> Mot</a> <br><br><br> <a href="/bbs/?pid=72871#p"> [Click to Play]</a> </td></tr></table> </p> <p>This is a little scaled-sprite 3D graphics engine inspired by the old classic game Power Drift.<br /> Still work in progress. Needs some basic game-play rules (laps, win/lose etc) and some difficulty balancing. But it's playable.</p> <p><img style="" border=0 src="/media/39676/rampsgame.p8_0.gif" alt="" /> <img style="" border=0 src="/media/39676/rampsgame.p8_1.gif" alt="" /> <img style="" border=0 src="/media/39676/rampsgame.p8_2.gif" alt="" /> <img style="" border=0 src="/media/39676/rampsgame.p8_3.gif" alt="" /> <img style="" border=0 src="/media/39676/rampsgame_4.gif" alt="" /></p> <p><strong>Update</strong></p> <ul> <li>Fixed bug in saving tracks to external cart.</li> <li>Added a little bit of steering assist to help line up the jumps.</li> <li>Some other tweaks I forget :)</li> </ul> <p><strong>Update 2</strong></p> <ul> <li>2 tracks</li> <li>Track selector</li> <li>Trees generate in the same place</li> </ul> <p><strong>Update 3</strong></p> <ul> <li>New track</li> <li>(Slightly) better physics/collisions</li> <li>Finish line and corner signs</li> </ul> <p><strong>Update 4</strong></p> <ul> <li>New larger test track</li> <li>Performance optimisations. LOD system. More aggressive view volume culling</li> <li>Tweaked the steering</li> </ul> <p><strong>Update 5</strong></p> <ul> <li>More performance tweaks</li> </ul> <p><strong>Update 6</strong></p> <ul> <li>More performance tweaks, thanks to <a href="https://www.lexaloffle.com/bbs/?uid=25532"> @freds72</a></li> <li>Updated physics to support driving upside down!</li> <li>New track with loop-the-loop (to test driving upside down :)</li> </ul> <p><strong>Update 7</strong></p> <ul> <li>AI cars. WIP. No collisions yet. Difficulty will probably be scaled down a bit eventually.</li> <li>Moved editor to separate cart.</li> <li>I tried to draw a palm tree.</li> </ul> <p><strong>Update 8</strong></p> <ul> <li>Different AI car colours.</li> <li>Fixed flip-Y logic when AI cars are upside down.</li> </ul> <p>*<em>Update 9</em></p> <ul> <li>Basic car collisions</li> <li>AI cars now steer around each other (and you)</li> </ul> <p><strong>Update 10</strong></p> <ul> <li>Cockpit graphics with animated front wheels.</li> <li>Main menu</li> <li>Race start sequence</li> </ul> <p><strong>Update 11</strong></p> <ul> <li>Fix wheel animation when frame rate &lt;30</li> <li>Fix NPC cars getting stuck on hills</li> </ul> <p><strong>Update 12</strong></p> <ul> <li>Lap and position displayed on screen</li> <li>Race finishes after 5 laps</li> <li>Note: Difficulty level logic is not hooked up yet!</li> </ul> <p><strong>Update 13</strong></p> <ul> <li>Hook up difficulty levels</li> <li>First attempt at engine sounds</li> <li>&quot;Ramps&quot; logo :)</li> </ul> https://www.lexaloffle.com/bbs/?tid=36777 https://www.lexaloffle.com/bbs/?tid=36777 Fri, 07 Feb 2020 12:34:37 UTC Dodge-balls <p> <table><tr><td> <a href="/bbs/?pid=69892#p"> <img src="/bbs/thumbs/pico8_mot_dodgeballs-1.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=69892#p"> mot_dodgeballs</a><br><br> by <a href="/bbs/?uid=39676"> Mot</a> <br><br><br> <a href="/bbs/?pid=69892#p"> [Click to Play]</a> </td></tr></table> </p> <p>A little &lt;560 chars game.<br /> Press left/right to dodge between the white balls.</p> <p>Vaguely inspired by &quot;Beam rider&quot;, so there's actually 5 lanes to jump between. I ran out of chars before I could draw the lanes though :)</p> https://www.lexaloffle.com/bbs/?tid=35909 https://www.lexaloffle.com/bbs/?tid=35909 Thu, 14 Nov 2019 10:06:19 UTC Immediate mode GUI <p> <table><tr><td> <a href="/bbs/?pid=69801#p"> <img src="/bbs/thumbs/pico8_mot_imgui-2.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=69801#p"> mot_imgui</a><br><br> by <a href="/bbs/?uid=39676"> Mot</a> <br><br><br> <a href="/bbs/?pid=69801#p"> [Click to Play]</a> </td></tr></table> </p> <p><a href="https://www.lexaloffle.com/bbs/files/39676/gui.p8_000.png" target=_view_image><img style="" border=0 src="https://www.lexaloffle.com/bbs/files/39676/gui.p8_000.png" width=160 height=160 alt="" /></a><a href="https://www.lexaloffle.com/bbs/files/39676/gui.p8_001.png" target=_view_image><img style="" border=0 src="https://www.lexaloffle.com/bbs/files/39676/gui.p8_001.png" width=160 height=160 alt="" /></a></p> <p>For some reason I felt like writing a little immediate-mode GUI library.</p> <p>It has:</p> <ul> <li>Labels</li> <li>Buttons</li> <li>Check boxes</li> <li>Radio buttons</li> <li>Sliders</li> <li>Text boxes</li> </ul> <p>You write into the text boxes on a little popup keyboard.</p> <p>It works best with the mouse, but it can run in controller-only mode, where you move the pointer around with the direction arrows (it's pretty cumbersome though).</p> <p>&quot;Immediate mode&quot; means you call the functions to draw the widgets in your drawing code, and it returns the user's input. It's generally easier to integrate into projects than a traditional widget UI would be, as you don't have to create and manage objects, you just draw the UI controls you need where and when you need them.</p> <p>The first tab in the source is the library. The second is a little demo, that doubles as the documentation :)</p> <p>UPDATE: Better looking buttons (thanks <a href="https://www.lexaloffle.com/bbs/?uid=25532"> @freds72</a>)</p> https://www.lexaloffle.com/bbs/?tid=35889 https://www.lexaloffle.com/bbs/?tid=35889 Mon, 11 Nov 2019 08:08:41 UTC Creating a pseudo 3D racer - part 3 <p> <table><tr><td> <a href="/bbs/?pid=69744#p"> <img src="/bbs/thumbs/pico8_tudanawati-10.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=69744#p"> Pseudo 3D racer tutorial 17</a><br><br> by <a href="/bbs/?uid=39676"> Mot</a> <br><br><br> <a href="/bbs/?pid=69744#p"> [Click to Play]</a> </td></tr></table> </p> <p>This tutorial is part 3 of a series. View <a href="https://www.lexaloffle.com/bbs/?tid=35767">part 1</a> here.</p> <p>And the end of part 2 we had roadside objects drawn using scaled sprites and sections of the map.</p> <img style="" border=0 src="https://www.lexaloffle.com/bbs/files/39676/tut12_003.png" alt="" /> <p>Everything so far has been flat, so in this tutorial we will add some hills and valleys.<br /> This will require solving some overlap issues which we will solve using a clip rectangle &quot;trick&quot;, which will in turn set us up nicely for implementing tunnels - so we'll do that too.</p> <h1>Defining the pitch</h1> <p>First we'll define the pitch of each corner with a field called &quot;pi&quot; (not to be mistaken for the Greek letter and mathematical constant).</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>road={ {ct=10,tu=0,bgl=bg_tree,bgr=bg_tree}, {ct=6,tu=-.25,bgl=bg_tree,bgr=bg_sign}, {ct=8,tu=0,pi=-.75,bgl=bg_tree,bgr=bg_tree}, {ct=4,tu=.375,bgl=bg_sign,bgr=bg_tree}, {ct=10,tu=0.05,pi=.75,bgl=bg_tree}, {ct=4,tu=0,bgl=bg_tree,bgr=bg_tree}, {ct=5,tu=-.25,bgl=bg_tree,bgr=bg_sign}, {ct=15,tu=0,pi=-.5,bgc=bg_beams}, {ct=12,tu=0,bgl=bg_house,bgr=bg_house}, {ct=8,tu=-.5,bgl=bg_house,bgr=bg_sign}, {ct=8,tu=.5,bgl=bg_sign,bgr=bg_house}, }</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>This will define the gradient at the start of each corner. So a &quot;pitch&quot; of 1 means the ground rises 1 unit for every unit it advances forward - a 45 degree incline. Likewise -1 would be a 45 degree decline. And 0 is of course level.</p> <p>For brevity we've made the pitch optional - it will default to 0 if not supplied.</p> <p>To make the hills and valleys smooth the pitch will smoothly interpolate between values across each corner. We will calculate the delta to add to the pitch for each segment in the _init() method:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre> -- calculate the change in pitch -- per segment for each corner. for i=1,#road do local corner=road[i] local pi=corner.pi or 0 local nextpi=road[i%#road+1].pi or 0 corner.dpi=(nextpi-pi)/corner.ct corner.pi=pi end</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>The pitch delta is stored in field &quot;dpi&quot; (not to be confused with &quot;dots per inch&quot; - naming stuff is hard).</p> <h1>Drawing</h1> <p>The pitch affects the vertical direction of the road as it is drawn. We actually already have a &quot;yd&quot; variable which is added to the y coordinate after each segment is drawn. It's just that until now the yd has always been 0, so the ground has always been flat. Now we can use it to create the hills and valleys.</p> <p>First we calculate the initial value at the start of _draw()</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre> -- direction local camang=camz*road[camcnr].tu local xd=-camang local yd=road[camcnr].pi+road[camcnr].dpi*(camseg-1) local zd=1</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>This is the start value &quot;pi&quot; plus the delta &quot;dpi&quot; added for every segment the camera has traversed along the corner.</p> <p>Then we add &quot;dpi&quot; to the &quot;yd&quot; after each segment is drawn, similar to how we've added the turn &quot;tu&quot; to &quot;xd&quot;:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre> -- turn and pitch xd+=road[cnr].tu yd+=road[cnr].dpi</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>This is all we need to give the road a smooth rising and falling effect.<br /> <table><tr><td> <a href="/bbs/?pid=69744#p"> <img src="/bbs/thumbs/pico8_tudanawati-6.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=69744#p"> Pseudo 3D racer tutorial 13</a><br><br> by <a href="/bbs/?uid=39676"> Mot</a> <br><br><br> <a href="/bbs/?pid=69744#p"> [Click to Play]</a> </td></tr></table> </p> <h3>A note on camera movement</h3> <p>It's worth pointing out that the camera automatically follows the shape of the hills and valleys smoothly, without us having to write any specific code.</p> <p>This works, because the camera position is skewed in the direction of the segment it is on. Back in tutorial one we implemented the skew function like this:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>function skew(x,y,z,xd,yd) return x+z*xd,y+z*yd,z end</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>In particular the &quot;y+z*yd&quot; ensures the camera moves up and down with the shape of the road.</p> <p>Storing positions &quot;unskewed&quot; and skewing them at draw time has some advantages. Regardless of how the road turns or pitches:</p> <ul> <li>The ground is always at y=0</li> <li>The center of the road is always at x=0</li> <li>The sides of the road are always at x=+/-3</li> </ul> <p>This approach will also make implementing AI cars easier later on.</p> <h1>Clipping</h1> <p>The hills and valleys introduce some obvious overlap issues - sprites visible through hills, far away ground being drawn in front instead of behind.</p> <p>One approach to fixing this would be to change the road drawing order so that it is back-to-front as well.<br /> This would be a perfectly valid approach (although it would require careful management to incorporate the sprites and road/ground at the same time).</p> <p>However we're going to use a different approach. We're going to use clipping rectangles to prevent far away road/ground being drawn over near objects.</p> <p>It will work like this:</p> <ul> <li>We start with a clipping rectangle covering the whole screen. I.e. nothing is clipped.</li> <li>As we draw forward, we move the bottom of the clipping rectangle up so that it does not include the road and ground we have just drawn.</li> </ul> <img style="" border=0 src="https://www.lexaloffle.com/bbs/files/39676/tmp_001.png" alt="" /> <ul> <li>The clipping rectangle prevents anything drawn further down the road from overlapping with what we've already drawn. Anything that would overlap will simply be clipped away.</li> </ul> <p>The advantage of this method is that we can continue drawing the road as we walk forward along it. We don't have to change the algorithm too much.<br /> It also reduces overdraw, which was important back-in-the-day on platforms that were fill-rate limited. Pico-8 drawing is fast enough that it doesn't really matter though.</p> <p>To start with we define the initial clip region before the main drawing loop:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre> -- current clip region local clp={0,0,128,128} clip()</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>The &quot;clp&quot; array defines the left, top, right and bottom values in that order.</p> <p>We reduce the clip region after drawing each segment, immediately after adding the background sprites (for reasons we'll see later):</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre> -- reduce clip region clp[4]=min(clp[4],ceil(py)) setclip(clp)</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>The &quot;min&quot; function ensures that the bottom of the rectangle only ever moves up, which is important for drawing crests of hills correctly.</p> <p>&quot;setclip&quot; is simply a helper function that sets the Pico-8 clip region for drawing:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>function setclip(clp) clip(clp[1],clp[2],clp[3]-clp[1],clp[4]-clp[2]) end</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Putting this together we get:<br /> <table><tr><td> <a href="/bbs/?pid=69744#p"> <img src="/bbs/thumbs/pico8_tudanawati-7.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=69744#p"> Pseudo 3D racer tutorial 14</a><br><br> by <a href="/bbs/?uid=39676"> Mot</a> <br><br><br> <a href="/bbs/?pid=69744#p"> [Click to Play]</a> </td></tr></table> </p> <h2>Clipping sprites</h2> <p>The hills are no longer see through, but now the background sprites are not being drawn correctly. The problem is the sprites are being drawn after the road, when the clipping rectangle has been reduced to cover just the sky. We could reset the clipping rectangle to cover the whole screen before drawing the sprites, which would improve things, but sprites would still be visible through the hills. </p> <p>What we really need to do is draw each sprite using the clipping rectangle that was active when its segment was drawn, which we can do by storing a copy of the clip rectangle in the sprites array.</p> <p>We'll add a &quot;clp&quot; parameter to addbgsprite:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>function addbgsprite(sp,sumct,bg,side,px,py,scale,clp) ... -- add to sprite array add(sp,{ x=px,y=py,w=w,h=h, img=bg.img, mp=bg.mp, flp=flp, clp={clp[1],clp[2],clp[3],clp[4]} }) end</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>It's important that we create a new array and copy each of the &quot;clp&quot; fields individually, so that we get a snapshot of the &quot;clp&quot; array at that point. Referencing &quot;clp&quot; directly would not work, because &quot;clp&quot; changes as the road is drawn.</p> <p>Now we update the calls in the main drawing loop to pass in the clipping rectangle:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre> -- add background sprites addbgsprite(sp,sumct,road[cnr].bgl,-1,px,py,scale,clp) addbgsprite(sp,sumct,road[cnr].bgr, 1,px,py,scale,clp) addbgsprite(sp,sumct,road[cnr].bgc, 0,px,py,scale,clp)</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Finally we apply the clipping rectangle in drawbgsprite:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>function drawbgsprite(s) setclip(s.clp) ...</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>With this in place we should have hills and valleys drawn correctly, including the background sprites.<br /> <table><tr><td> <a href="/bbs/?pid=69744#p"> <img src="/bbs/thumbs/pico8_tudanawati-8.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=69744#p"> Pseudo 3D racer tutorial 15</a><br><br> by <a href="/bbs/?uid=39676"> Mot</a> <br><br><br> <a href="/bbs/?pid=69744#p"> [Click to Play]</a> </td></tr></table> </p> <h1>Tunnels</h1> <p>For the last part of this tutorial we will implement basic tunnels. Tunnels give racing games a cool change of environment and they're just fun to drive through.</p> <p>And with the clipping rectangle logic is in place they're reasonably straightforward to implement.</p> <p>It's essentially an extension of the clipping logic used for the ground. But now we also reduce the top of the clipping rectangle as we draw the tunnel ceiling, and the left and right sides as we draw the tunnel walls.</p> <p>We'll start by defining the tunnel in our road:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>road={ {ct=10,tu=0,bgl=bg_tree,bgr=bg_tree}, {ct=6,tu=-.25,bgl=bg_tree,bgr=bg_sign}, {ct=8,tu=0,pi=-.75,bgl=bg_tree,bgr=bg_tree}, {ct=8,tu=0,tnl=true}, {ct=4,tu=0,tnl=true}, {ct=8,tu=0,pi=.75,tnl=true}, {ct=8,tu=-.5,pi=.75,tnl=true}, {ct=4,tu=0,tnl=true}, {ct=8,tu=.5,tnl=true}, {ct=4,tu=0,pi=-.5,tnl=true}, {ct=8,tu=0,pi=-.5,tnl=true}, {ct=4,tu=.375,bgl=bg_sign,bgr=bg_tree}, {ct=10,tu=0.05,pi=.75,bgl=bg_tree}, {ct=4,tu=0,bgl=bg_tree,bgr=bg_tree}, {ct=5,tu=-.25,bgl=bg_tree,bgr=bg_sign}, {ct=15,tu=0,pi=-.5,bgc=bg_beams}, {ct=12,tu=0,bgl=bg_house,bgr=bg_house}, {ct=8,tu=-.5,bgl=bg_house,bgr=bg_sign}, {ct=8,tu=.5,bgl=bg_sign,bgr=bg_house}, }</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Tunnel corners are denoted by &quot;tnl=true&quot;.</p> <p>I've placed the tunnel section towards the start of the road so that it's quicker to get to for testing and debugging. (It can be moved later once everything is working correctly).</p> <h2>Tunnel front face</h2> <p>Next we'll draw the tunnel face.<br /> We need to detect the first corner tunnel (&quot;tnl&quot; is set to true) where the <em>previous</em> corner was not a tunnel, which we can do by tracking the tunnel flag for the current corner, and the previous corner.</p> <p>We set the initial value of the previous tunnel flag before the main drawing loop:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre> -- previous tunnel flag local ptnl=road[cnr].tnl</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Strictly speaking this should be set to the &quot;tnl&quot; property of the previous segment, but it doesn't actually make any noticeable difference for our purposes.</p> <p>Inside the main loop we compare it with the current segment's tunnel flag, and draw the tunnel face at the start of the tunnel:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre> -- draw tunnel face local tnl=road[cnr].tnl if tnl and not ptnl then drawtunnelface(ppx,ppy,pscale) end</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Note that the tunnel face will be drawn at the <em>start</em> of the current segment. This means we must use the <em>previous</em> cursor position (ppx, ppy, pscale), as cursor positions are for the <em>end</em> of their segment.</p> <p>We also need to copy &quot;tnl&quot; to &quot;ptnl&quot; just before the loop ends, so that &quot;ptnl&quot; is set correctly for the next segment:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre> -- track previous projected position ppx,ppy,pscale=px,py,scale ptnl=tnl</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>We will draw the tunnel face by drawing rectangles around the mouth of the tunnel.<br /> We'll start by creating a helper function that takes the projected road position and calculates a rectangle describing the tunnel floor, ceiling and walls.</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>function gettunnelrect(px,py,scale) local w,h=6.4*scale,4*scale local x1=ceil(px-w/2) local y1=ceil(py-h) local x2=ceil(px+w/2) local y2=ceil(py) return x1,y1,x2,y2 end</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <img style="" border=0 src="https://www.lexaloffle.com/bbs/files/39676/tmp2_000.png" alt="" /> <p><em>The red rectangle is the &quot;tunnel rectangle&quot;. I.e. the mouth of the tunnel.</em></p> <p>We're allowing 6.4 units of width for the road, rather than 6, because the red and white shoulder things stick out another 0.2 units each way.<br /> The tunnel will be 4 units high, which is low enough to make the tunnel feel claustrophobic but high enough to be above the camera.</p> <p>Using this we can implement drawtunnelface:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>function drawtunnelface(px,py,scale) -- tunnel mouth local x1,y1,x2,y2=gettunnelrect(px,py,scale) -- tunnel wall top local wh=4.5*scale local wy=ceil(py-wh) -- draw faces if(y1&gt;0)rectfill(0,wy,128,y1-1,7) if(x1&gt;0)rectfill(0,y1,x1-1,y2-1,7) if(x2&lt;128)rectfill(x2,y1,127,y2-1,7) end</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Essentially we're drawing a rectangle that extends from the top of the facing wall down to the top of the tunnel mouth. Then we draw two more rectangles either side of the mouth down to the ground.</p> <img style="" border=0 src="https://www.lexaloffle.com/bbs/files/39676/tmp2_001.png" alt="" /> <p>The last thing we need to do is restrict the clipping rectangle to the tunnel mouth.<br /> We'll create this function to adjust the clipping rectangle:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>function cliptotunnel(px,py,scale,clp) local x1,y1,x2,y2=gettunnelrect(px,py,scale) clp[1]=max(clp[1],x1) clp[2]=max(clp[2],y1) clp[3]=min(clp[3],x2) clp[4]=min(clp[4],y2) end</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>And update the tunnel face drawing code in the main drawing loop:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre> -- draw tunnel face local tnl=road[cnr].tnl if tnl and not ptnl then drawtunnelface(ppx,ppy,pscale) cliptotunnel(ppx,ppy,pscale,clp) setclip(clp) end</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Now we should have a front facing tunnel wall:<br /> <table><tr><td> <a href="/bbs/?pid=69744#p"> <img src="/bbs/thumbs/pico8_tudanawati-9.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=69744#p"> Pseudo 3D racer tutorial 16</a><br><br> by <a href="/bbs/?uid=39676"> Mot</a> <br><br><br> <a href="/bbs/?pid=69744#p"> [Click to Play]</a> </td></tr></table> </p> <h2>Tunnel interior</h2> <p>Next we need to draw the tunnel interior. This will consist of the ceiling, walls and road.</p> <p>First we need to separate the road drawing code from the ground drawing code, so that we can just draw the road when inside the tunnel. We'll move the ground drawing code out into it's own function:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>function drawground(y1,y2,sumct) if(flr(y2)&lt;ceil(y1))return -- draw ground local gndcol=3 if((sumct%6)&gt;=3)gndcol=11 rectfill(0,ceil(y1),128,flr(y2),gndcol) end</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>And delete the &quot;draw ground&quot; lines from drawroad.</p> <p>We'll update the drawing code in the main loop to draw the ground or tunnel interior as appropriate, then draw the road:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre> -- draw ground/tunnel walls local sumct=getsumct(cnr,seg) if tnl then drawtunnelwalls(px,py,scale,ppx,ppy,pscale,sumct) else drawground(py,ppy,sumct) end -- draw road drawroad(px,py,scale,ppx,ppy,pscale,sumct)</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>We draw the tunnel walls by comparing the tunnel rectangles at the start and end of the current segment.</p> <img style="" border=0 src="https://www.lexaloffle.com/bbs/files/39676/tmp3_000.png" alt="" /> <p>We draw a ceiling rectangle to connect the top of each tunnel rectangle.<br /> Then we draw wall rectangles on each side to connect the left and right sides.</p> <img style="" border=0 src="https://www.lexaloffle.com/bbs/files/39676/tmp3_001.png" alt="" /> <p>We'll use an alternating colour for effect.</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>function drawtunnelwalls(px,py,scale,ppx,ppy,pscale,sumct) -- colour local wallcol=0 if(sumct%4&lt;2)wallcol=1 -- draw walls local x1,y1,x2,y2=gettunnelrect(px,py,scale) local px1,py1,px2,py2=gettunnelrect(ppx,ppy,pscale) if(y1&gt;py1)rectfill(px1,py1,px2-1,y1-1,wallcol) if(x1&gt;px1)rectfill(px1,y1,x1-1,py2-1,wallcol) if(x2&lt;px2)rectfill(x2,y1,px2-1,py2-1,wallcol) end</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>The final step is to update the reduce-clip-region logic in the main drawing loop to handle tunnels.</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre> -- reduce clip region if tnl then cliptotunnel(px,py,scale,clp) else clp[4]=min(clp[4],ceil(py)) end setclip(clp)</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>And this completes our tunnel:<br /> <table><tr><td> <a href="/bbs/?pid=69744#p"> <img src="/bbs/thumbs/pico8_tudanawati-10.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=69744#p"> Pseudo 3D racer tutorial 17</a><br><br> by <a href="/bbs/?uid=39676"> Mot</a> <br><br><br> <a href="/bbs/?pid=69744#p"> [Click to Play]</a> </td></tr></table> </p> <h1>Next steps</h1> <p>This is the end of part 3. In part 4 we'll add some cars to overtake.</p> https://www.lexaloffle.com/bbs/?tid=35868 https://www.lexaloffle.com/bbs/?tid=35868 Sat, 09 Nov 2019 23:30:38 UTC Creating a pseudo 3D racer - part 2 <p> <table><tr><td> <a href="/bbs/?pid=69550#p"> <img src="/bbs/thumbs/pico8_tudanawati-5.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=69550#p"> Pseudo 3D racer tutorial 12</a><br><br> by <a href="/bbs/?uid=39676"> Mot</a> <br><br><br> <a href="/bbs/?pid=69550#p"> [Click to Play]</a> </td></tr></table> <br /> <em>This tutorial is part 2 of a series. <a href="https://www.lexaloffle.com/bbs/?tid=35767">View part 1 here</a></em></p> <p>At the end of part one we had a basic road with corners.</p> <img style="" border=0 src="https://www.lexaloffle.com/bbs/files/39676/tut7_000.png" alt="" /> <p>Next we'll add some background objects along the sides of the road. Pseudo-3D racers commonly have objects like trees, houses and signs spaced along the side of the road to make things more interesting.</p> <h1>Adding trees</h1> <p>I've added a few sprites to play with. We'll start by adding this tree along each side, every 3 segments.<br /> <table><tr><td width=64> <img src="https://www.lexaloffle.com/bbs/gfxc/39676_0.png" width=64 height=128> </td> <td valign=bottom> <a style="cursor:pointer;font-size:8pt" onclick=' var el = document.getElementById("gfxcode_39676_0"); if (el.style.display == "none") el.style.display = ""; else el.style.display = "none"; microAjax("https://www.lexaloffle.com/bbs/gfxc/39676_0.txt", function (retdata){ var el = document.getElementById("gfxcode_39676_0"); el.innerHTML = retdata; el.focus(); el.select(); } ); '> [16x32]</a> </td></tr> <tr><td colspan=2> <textarea rows=3 class=lexinput id="gfxcode_39676_0" style="width:640px;background-color:#fed;display:none;overflow:hidden; font-size:6pt;"></textarea> </td> </tr> </table> </p> <p>Drawing is straightforward.<br /> We can use the projected position and scale of the road at each segment to calculate the position and size of the tree to draw. Then we can pass this to the scale sprite command (sspr), and draw our tree.</p> <p>For example:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>if (sumct%3)==0 then local tx,ty=px-4.5*scale,py local tw,th=1.5*scale,3*scale sspr(8,0,16,32,tx-tw/2,ty-th,tw,th) end</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Will draw a 3x1.5 unit tree 4.5 units to the left of the center of the road (which is far enough to move it off the road).</p> <p><strong>However!</strong></p> <p>Because we draw the road front-to-back, <em>we can't just draw the trees at the same time</em>. They must be drawn in back-to-front order so that the near trees appear in front, and it looks correct.</p> <p>So instead we must create an array of tree sprites:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>local sp={}</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>And add the position of each tree to the array inside the drawing loop:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>if (sumct%3)==0 then -- left tree local tx,ty=px-4.5*scale,py local tw,th=1.5*scale,3*scale add(sp,{x=tx,y=ty,w=tw,h=th}) -- right tree tx=px+4.5*scale add(sp,{x=tx,y=ty,w=tw,h=th}) end</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>After the road is drawn, we have an array of tree positions in front-to-back order.<br /> We can then loop through it <em>backwards</em> to draw them in back-to-front order.</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>for i=#sp,1,-1 do drawbgsprite(sp[i]) end </pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>function drawbgsprite(s) sspr(8,0,16,32,s.x-s.w/2,s.y-s.h,s.w,s.h) end</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Putting it all together we get:<br /> <table><tr><td> <a href="/bbs/?pid=69550#p"> <img src="/bbs/thumbs/pico8_tudanawati-1.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=69550#p"> Pseudo 3D racer tutorial 8</a><br><br> by <a href="/bbs/?uid=39676"> Mot</a> <br><br><br> <a href="/bbs/?pid=69550#p"> [Click to Play]</a> </td></tr></table> </p> <h1>Different background types</h1> <p>Now that the basic logic is working, we can extend it to support different background types.<br /> First we need to define the background types, with enough information to position them and draw them at the correct size:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>bg_tree={ img={8,0,16,32}, -- sprite image pos={1.5,0}, -- position rel 2 side of road siz={1.5,3}, -- size spc=3 -- spacing } bg_sign={ img={80,0,32,32}, pos={.5,0}, siz={1.5,1.5}, spc=1, flpr=true -- flip when on right hand side }</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Now we can assign a background type to each corner. (bgl is for the left hand side, bgr is for the right).</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>road={ {ct=10,tu=0,bgl=bg_tree,bgr=bg_tree}, {ct=6,tu=-.25,bgl=bg_tree,bgr=bg_sign}, {ct=8,tu=0,bgl=bg_tree,bgr=bg_tree}, {ct=4,tu=.375,bgl=bg_sign,bgr=bg_tree}, {ct=10,tu=0.05,bgl=bg_tree,bgr=bg_tree}, {ct=4,tu=0,bgl=bg_tree,bgr=bg_tree}, {ct=5,tu=-.25,bgl=bg_tree,bgr=bg_sign}, }</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>We'll use this information to populate the &quot;sp&quot; array.<br /> We need to store a little bit more than before, like the width and height and sprite image to draw.<br /> To keep the main loop clean we can put it in a function:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>function addbgsprite(sp,sumct,bg,side,px,py,scale) if(not bg)return if((sumct%bg.spc)~=0)return -- find position px+=3*scale*side if bg.pos then px+=bg.pos[1]*scale*side py+=bg.pos[2]*scale end -- calculate size local w,h=bg.siz[1]*scale,bg.siz[2]*scale -- flip horizontally? local flp=side&gt;0 and bg.flpr -- add to sprite array add(sp,{ x=px,y=py,w=w,h=h, img=bg.img, flp=flp }) end</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Note that the &quot;side&quot; parameter is -1 for objects on the left hand side and 1 for objects on the right.</p> <p>We must call it from the main drawing loop. Once for the left hand side and once for the right:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>addbgsprite(sp,sumct,road[cnr].bgl,-1,px,py,scale) addbgsprite(sp,sumct,road[cnr].bgr, 1,px,py,scale)</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>And finally we need to change the drawbgsprite to handle the new format:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>function drawbgsprite(s) sspr( s.img[1],s.img[2],s.img[3],s.img[4], s.x-s.w/2,s.y-s.h,s.w,s.h, s.flp) end</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>And now we have trees and signs:<br /> <table><tr><td> <a href="/bbs/?pid=69550#p"> <img src="/bbs/thumbs/pico8_tudanawati-2.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=69550#p"> Pseudo 3D racer tutorial 9</a><br><br> by <a href="/bbs/?uid=39676"> Mot</a> <br><br><br> <a href="/bbs/?pid=69550#p"> [Click to Play]</a> </td></tr></table> </p> <h1>Tweaking sprite scaling</h1> <p>Just a quick tweak to the sprite scaling before we continue. Feel free to skip this section if it doesn't interest you.</p> <p>You might have noticed the trees sometimes float 1 pixel above the ground. </p> <p>This is due to how sspr converts its parameters into integers before drawing. The projection and scaling calculations produce position and size values with a fractional component. The sspr command simply discards the fraction and rounds down to the nearest integer, for both the position and size parameters.</p> <p>I prefer for it to render the pixels between the top and bottom values. For example if a sprite has a top of 1.9 and a bottom of 5.7, I want it to render on scan lines 2,3,4 and 5. This is how the road rendering has been implemented, and it ensures that the rendered segments fit together cleanly with no gaps or overlap.</p> <p>sspr however will truncate 1.9 to 1 and the height (5.7-1.9=3.8) down to 3, then render it on scan lines 1,2,3,4. So it appears one pixel higher.</p> <p>But we don't have to let sspr do the rounding. We can perform our own explicit rounding and tell sspr exactly which rows and columns we want our sprite stretched over.<br /> In this case the ceil(1.9) gives me the scan line to start drawing (line 2) and ceil(5.7) gives the scan line to <em>stop</em> drawing (line 6). Subtracting the two gives the number of scan lines I want covered (4), which I can pass to sspr as the height.</p> <p>And the logic is similar for horizontal columns.</p> <p>Here's the updated drawbgsprite:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>function drawbgsprite(s) local x1=ceil(s.x-s.w/2) local x2=ceil(s.x+s.w/2) local y1=ceil(s.y-s.h) local y2=ceil(s.y) sspr( s.img[1],s.img[2],s.img[3],s.img[4], x1,y1,x2-x1,y2-y1, s.flp) end</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>And here's the result:<br /> <table><tr><td> <a href="/bbs/?pid=69550#p"> <img src="/bbs/thumbs/pico8_tudanawati-3.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=69550#p"> Pseudo 3D racer tutorial 10</a><br><br> by <a href="/bbs/?uid=39676"> Mot</a> <br><br><br> <a href="/bbs/?pid=69550#p"> [Click to Play]</a> </td></tr></table> </p> <h1>Bigger background objects</h1> <p>Scaled sprites work really well for background objects, but they can only get so big before we start to run out of Pico8 sprite space. A lot of pseudo 3D racers have nice big roadside objects like buildings and bridges to make things varied and interesting.</p> <p>Pico8 has the sprite &quot;map&quot; where you can layout sprites to make much larger objects. So if we can scale sections of the map to the screen this would solve our problem. But Pico8 does not have a built in &quot;scale map&quot; command like sspr does for sprites.</p> <p>Fortunately though, we can roll our own.</p> <p>The idea is to loop over the map tiles and use the &quot;mget&quot; function to get the sprite for each position in the map. Then we can calculate its position and size on screen and use sspr to draw it.</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>function smap(mx,my,mw,mh,dx,dy,dw,dh) -- tile size on screen local tw,th=dw/mw,dh/mh -- loop over map tiles for y=0,mh-1 do for x=0,mw-1 do -- lookup sprite local s=mget(mx+x,my+y) -- don't draw sprite 0 if s~=0 then -- sprite row and column index -- use to get sprite image coords local sc,sr=s%16,flr(s/16) -- 16 sprites per row local sx,sy=sc*8,sr*8 -- 8x8 pixels per sprite -- sprite position on screen local x1=ceil(dx+x*tw) local y1=ceil(dy+y*th) local x2=ceil(dx+x*tw+tw) local y2=ceil(dy+y*th+th) -- scale sprite sspr(sx,sy,8,8, x1,y1,x2-x1,y2-y1) end end end end</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>mx,my,mw,mh are the map coordinates in cells. dx,dy,dw,dh are the screen coordinates to draw.<br /> We can test this function by typing (in immediate mode):</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>smap(0,0,8,5,0,0,128,128)</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Which should draw a scaled house across the screen.</p> <p>Armed with our new function, we can add some houses to our map. This takes a little bit of plumbing.</p> <p>First we define a background type for it:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>bg_house={ mp={0,0,8,5}, -- map image (x,y,w,h in tiles) pos={3.5,0}, siz={6,3.5}, spc=4 }</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>And add some houses to the end of our road:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>road={ {ct=10,tu=0,bgl=bg_tree,bgr=bg_tree}, {ct=6,tu=-.25,bgl=bg_tree,bgr=bg_sign}, {ct=8,tu=0,bgl=bg_tree,bgr=bg_tree}, {ct=4,tu=.375,bgl=bg_sign,bgr=bg_tree}, {ct=10,tu=0.05,bgl=bg_tree}, {ct=4,tu=0,bgl=bg_tree,bgr=bg_tree}, {ct=5,tu=-.25,bgl=bg_tree,bgr=bg_sign}, {ct=12,tu=0,bgl=bg_house,bgr=bg_house}, {ct=8,tu=-.5,bgl=bg_house,bgr=bg_sign}, {ct=8,tu=.5,bgl=bg_sign,bgr=bg_house}, }</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>We need to copy the &quot;mp&quot; property when we write the entry into the &quot;sp&quot; sprite array, in the &quot;addbgsprite&quot; function:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre> -- add to sprite array add(sp,{ x=px,y=py,w=w,h=h, img=bg.img, mp=bg.mp, flp=flp })</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>The last step is to update &quot;drawbgsprite&quot; to call our smap function when it receives a sprite with an &quot;mp&quot; property:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>function drawbgsprite(s) if s.mp then smap(s.mp[1],s.mp[2],s.mp[3],s.mp[4], s.x-s.w/2,s.y-s.h,s.w,s.h) else local x1=ceil(s.x-s.w/2) local x2=ceil(s.x+s.w/2) local y1=ceil(s.y-s.h) local y2=ceil(s.y) sspr(s.img[1],s.img[2],s.img[3],s.img[4], x1,y1,x2-x1,y2-y1, s.flp) end end</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Putting it all together gives:<br /> <table><tr><td> <a href="/bbs/?pid=69550#p"> <img src="/bbs/thumbs/pico8_tudanawati-4.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=69550#p"> Pseudo 3D racer tutorial 11</a><br><br> by <a href="/bbs/?uid=39676"> Mot</a> <br><br><br> <a href="/bbs/?pid=69550#p"> [Click to Play]</a> </td></tr></table> </p> <p>It should be kept in mind that scaling a map results in a lot of sspr calls. Our 8x5 tile house has 40 sprites to draw (well really 38, as it skips the blank top left and right tiles). A larger building could have 100,200 or more.<br /> So it's worth keeping an eye on the CPU usage to make sure Pico-8 isn't going to max out, and maybe space larger objects out a bit more.<br /> Having said that, we're still only at 17% CPU tops, so Pico-8 definitely has the ability to handle a decent amount of scenery.</p> <h1>Bridges and beams</h1> <p>To finish off let's add some metal beams for the player to drive underneath. This will be a centered background object, which means it's technically in the middle of the road. However because the beam object is wider than the road and is essentially bridge shaped, the camera will drive through/underneath it, instead of crashing into it.</p> <p>We have most of what we need already. We'll define a background type:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>bg_beams={ mp={8,0,16,8}, siz={10,5}, spc=2 }</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>And add it to our road, using &quot;bgc&quot; to indicate it's a centered background object.</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>road={ {ct=10,tu=0,bgl=bg_tree,bgr=bg_tree}, {ct=6,tu=-.25,bgl=bg_tree,bgr=bg_sign}, {ct=8,tu=0,bgl=bg_tree,bgr=bg_tree}, {ct=4,tu=.375,bgl=bg_sign,bgr=bg_tree}, {ct=10,tu=0.05,bgl=bg_tree}, {ct=4,tu=0,bgl=bg_tree,bgr=bg_tree}, {ct=5,tu=-.25,bgl=bg_tree,bgr=bg_sign}, {ct=15,tu=0,bgc=bg_beams}, {ct=12,tu=0,bgl=bg_house,bgr=bg_house}, {ct=8,tu=-.5,bgl=bg_house,bgr=bg_sign}, {ct=8,tu=.5,bgl=bg_sign,bgr=bg_house}, }</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>In the main drawing loop, we'll add another call to &quot;addbgsprite&quot; for centered objects:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre> -- add background sprites addbgsprite(sp,sumct,road[cnr].bgl,-1,px,py,scale) addbgsprite(sp,sumct,road[cnr].bgr, 1,px,py,scale) addbgsprite(sp,sumct,road[cnr].bgc, 0,px,py,scale)</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>By passing 0 as the &quot;side&quot; parameter we cancel out any horizontal positioning, so that it ends up in the middle of the road.</p> <p>And that's all we need to do:<br /> <table><tr><td> <a href="/bbs/?pid=69550#p"> <img src="/bbs/thumbs/pico8_tudanawati-5.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=69550#p"> Pseudo 3D racer tutorial 12</a><br><br> by <a href="/bbs/?uid=39676"> Mot</a> <br><br><br> <a href="/bbs/?pid=69550#p"> [Click to Play]</a> </td></tr></table> </p> <h1>Next steps</h1> <p>And that's it for part 2. <a href="https://www.lexaloffle.com/bbs/?tid=35868">Part 3</a> is about creating hills and tunnels.</p> https://www.lexaloffle.com/bbs/?tid=35824 https://www.lexaloffle.com/bbs/?tid=35824 Tue, 05 Nov 2019 10:08:06 UTC SDF ray-marcher <p> <table><tr><td> <a href="/bbs/?pid=69517#p"> <img src="/bbs/thumbs/pico8_ywosahuru-3.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=69517#p"> Raymarch test 3</a><br><br> by <a href="/bbs/?uid=39676"> Mot</a> <br><br><br> <a href="/bbs/?pid=69517#p"> [Click to Play]</a> </td></tr></table> </p> <p>Reposting this in carts, because it's as finished as it's ever going to be :-). </p> <p>This is a little SDF ray marcher I've been playing with.<br /> It renders two randomly placed spheres and a torus above a checker-board plane in different colours. Often one of them is reflective.</p> <p>The code is ugly as heck, and the specular highlights are completely wrong, but it occasionally spits out something pretty imo.</p> <p><a href="https://www.lexaloffle.com/bbs/files/39676/marchtest2_000.png" target=_view_image><img style="" border=0 src="https://www.lexaloffle.com/bbs/files/39676/marchtest2_000.png" width=160 height=160 alt="" /></a><a href="https://www.lexaloffle.com/bbs/files/39676/marchtest_002.png" target=_view_image><img style="" border=0 src="https://www.lexaloffle.com/bbs/files/39676/marchtest_002.png" width=160 height=160 alt="" /></a><a href="https://www.lexaloffle.com/bbs/files/39676/marchtest_001.png" target=_view_image><img style="" border=0 src="https://www.lexaloffle.com/bbs/files/39676/marchtest_001.png" width=160 height=160 alt="" /></a></p> <p><strong>Update: Changed to use sqrt() again, as it's much more accurate in v0.2.0 </strong></p> https://www.lexaloffle.com/bbs/?tid=35815 https://www.lexaloffle.com/bbs/?tid=35815 Thu, 31 Oct 2019 07:24:15 UTC Raymarch test <p> <table><tr><td> <a href="/bbs/?pid=69443#p"> <img src="/bbs/thumbs/pico8_yijebofuse-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=69443#p"> Ray march test</a><br><br> by <a href="/bbs/?uid=39676"> Mot</a> <br><br><br> <a href="/bbs/?pid=69443#p"> [Click to Play]</a> </td></tr></table> </p> <p>I felt like ray marching a signed distance field in PICO-8.<br /> (Don't ask me why..).</p> https://www.lexaloffle.com/bbs/?tid=35796 https://www.lexaloffle.com/bbs/?tid=35796 Tue, 29 Oct 2019 10:12:01 UTC Creating a pseudo 3D racer <p> <table><tr><td> <a href="/bbs/?pid=69343#p"> <img src="/bbs/thumbs/pico8_tudanawati-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=69343#p"> Pseudo 3D racer tutorial 7</a><br><br> by <a href="/bbs/?uid=39676"> Mot</a> <br><br><br> <a href="/bbs/?pid=69343#p"> [Click to Play]</a> </td></tr></table> </p> <p>Pseudo 3D racing games are fun. They have a cool retro arcade feel, with a good sense of speed, and can run on low end hardware. Plus they're pretty straightforward to implement - and satisfying.</p> <p>There are lots of different ways to write a pseudo 3D racer, but I'm just going to show you how I do it.<br /> This is the method I used for <a href="https://www.lexaloffle.com/bbs/?tid=35686">Loose Gravel</a>, and can render corners, hills, tunnels and background sprites in an efficient manner. It doesn't need any 3D drawing hardware, just basic 2D rectangles, lines, scaled sprites and rectangular clip regions. Pico-8 can do all of these.</p> <h1>Defining the road</h1> <p>The road is made out of &quot;corners&quot; (for our purposes we will call straight bits &quot;corners&quot; as well). Corners need to curve in the direction the road turns, so we will simulate this by building them out of smaller straight &quot;segments&quot;.</p> <p>We can define the track as an array of these corners.</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>road={ {ct=10,tu=0}, {ct=6,tu=-1}, {ct=8,tu=0}, {ct=4,tu=1.5}, {ct=10,tu=0.2}, {ct=4,tu=0}, {ct=5,tu=-1}, }</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Each corner has a segment count &quot;ct&quot; and a value indicating how much the direction turns between each segment &quot;tu&quot;.<br /> So tu=0 creates a straight piece, tu=1 will turn to the right, -1 left etc.</p> <p>For simplicity we'll ignore hills and valleys for now.</p> <h1>Drawing the road</h1> <p>We will draw the road by walking a 3D &quot;cursor&quot; along it and drawing the road at each point.<br /> We'll define the cursor like this:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>local x,y,z=0,1,1</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>We will start drawing the road slightly in front (z=1) and below (y=1) the camera position.<br /> <em>Note: I use x=right, y=down, z=into the screen as my coordinate system. I find this easiest to work with, having x and y in the same direction as on the screen and keeping z coordinates positive.</em></p> <p>The direction of the road is another 3D vector:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>local xd,yd,zd=0,0,1</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>So initially the road will point straight forward (zd=1).<br /> The direction will be added to the cursor's 3D position to move from the current segment to the start of the next. To keep the maths simple we're using segments of length 1.</p> <p>We also need to track which corner and segment we are drawing:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>local cnr,seg=1,1</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>The main drawing loop will draw 30 segments of the road from the starting position, as follows:</p> <ul> <li>Draw the road at the current position</li> <li>Move to the next position in 3D space</li> <li>Adjust the direction based on how the road turns</li> <li>Advance to the next segment (and corner if applicable)</li> </ul> <p>Putting this together, gives us something like this:</p> <p> <table><tr><td> <a href="/bbs/?pid=69343#p"> <img src="/bbs/thumbs/pico8_tadirutora-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=69343#p"> Pseudo 3D racer tutorial 1</a><br><br> by <a href="/bbs/?uid=39676"> Mot</a> <br><br><br> <a href="/bbs/?pid=69343#p"> [Click to Play]</a> </td></tr></table> </p> <p>Click &quot;code&quot; up above to view the code.</p> <p>It's just a static boring line for now, but the basic logic is there. I was pretty vague about what &quot;draw the road&quot; means so we're just drawing a line to the x and z coordinates of the &quot;cursor&quot; for now.</p> <p>We can see that the road goes straight for a bit then turns to the left as it moves down the screen.<br /> We're &quot;adjusting the direction&quot; by adding the turn amount for the current corner to the X coordinate of our direction vector:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>xd+=road[cnr].tu</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>which means we're actually skewing the road rather than rotating it. This is part of the &quot;pseudo&quot; in our pseudo 3D - the difference is if we were using proper rotation the road would eventually turn around and start coming back towards us - if it turned far enough - whereas with skewing it just keeps stretching out more and more horizontally.<br /> Although less realistic, skewing is much simpler to implement, and means that the road will always face away from the camera, which makes drawing things in the right order a lot simpler. And as long as the corners aren't too sharp it's an acceptable approximation.</p> <h1>Making it 3D</h1> <p>The key making this <strong>3D</strong> is called perspective projection.<br /> This converts a 3D coordinate into a 2D screen coordinate. I won't bore you with the mathematics - there are plenty of <a href="https://en.wikipedia.org/wiki/3D_projection">other places</a> you can find this information if you really want to.<br /> The important thing is the formula, which is:</p> <ul> <li>px=x*64/z+64</li> <li>py=y*64/z+64</li> </ul> <p>This gives a 90 degree field of view (FOV). Replacing the 64 in 64/z with a smaller value would give a wider FOV, or a larger value would give a narrower FOV. We'll stick with 64 however. The +64 moves everything into the center of the screen.</p> <p>64/z is also a useful value to keep, because it is the scale factor for anything drawn at that position, such as scaled sprites, or the road width. So the project function will return this too:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>function project(x,y,z) local scale=64/z return x*scale+64,y*scale+64,scale end</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>With this we can replace the line drawing code in the main loop:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>-- project local px,py,scale=project(x,y,z) -- draw road local width=3*scale line(px-width,py,px+width,py)</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>The projection allows us to draw a horizontal line 6 units wide, scaled appropriately for the 3D position.<br /> This produces something a little more like looking down a road.</p> <p> <table><tr><td> <a href="/bbs/?pid=69343#p"> <img src="/bbs/thumbs/pico8_gukusoggu-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=69343#p"> Pseudo 3D racer tutorial 2</a><br><br> by <a href="/bbs/?uid=39676"> Mot</a> <br><br><br> <a href="/bbs/?pid=69343#p"> [Click to Play]</a> </td></tr></table> </p> <h1>Adding movement</h1> <p>Right now the road is static, because it is always drawn from the same position.<br /> To get the sensation of movement we need to simulate a camera moving down the road, and draw it from the camera's position.</p> <p>So we'll declare the camera:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>camcnr,camseg=1,1</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>And change &quot;cursor&quot; in the _draw() function to start from the camera position:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>local cnr,seg=camcnr,camseg</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>We can use the same logic to advance the camera to the next segment as we do for the &quot;cursor&quot; when drawing, by moving it out into a function:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>function advance(cnr,seg) seg+=1 if seg&gt;road[cnr].ct then seg=1 cnr+=1 if(cnr&gt;#road)cnr=1 end return cnr,seg end</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>This advances to the next segment in the corner, and if necessary the next corner in the road, looping around to the first corner if required.</p> <p>Then we call it from the _draw() function:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>-- advance along road cnr,seg=advance(cnr,seg)</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>And use it to advance the camera in _update():</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>function _update() camcnr,camseg=advance(camcnr,camseg) end</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Putting it together we get:<br /> <table><tr><td> <a href="/bbs/?pid=69343#p"> <img src="/bbs/thumbs/pico8_sodarizetu-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=69343#p"> Pseudo 3D racer tutorial 3</a><br><br> by <a href="/bbs/?uid=39676"> Mot</a> <br><br><br> <a href="/bbs/?pid=69343#p"> [Click to Play]</a> </td></tr></table> </p> <p>Which sort-of looks like movement, but isn't totally convincing.<br /> The camera is moving one full segment per rendered frame, which means the segment lines are rendered at the same distance each time.<br /> We need to move less than a full segment per rendered frame, which means we need to track the camera position relative to the current segment:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>camx,camy,camz=0,0,0</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Now we can advance the camera by less than a full segment length:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>function _update() camz+=0.1 if camz&gt;1 then camz-=1 camcnr,camseg=advance(camcnr,camseg) end end</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Inside _draw() we start drawing relative to the camera position, by subtracting the camera coordinates from the starting coordinates.</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>local x,y,z=-camx,-camy+1,-camz+1</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>With these changes the forward movement feels more convincing:<br /> <table><tr><td> <a href="/bbs/?pid=69343#p"> <img src="/bbs/thumbs/pico8_fumibitub-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=69343#p"> Pseudo 3D racer tutorial 4</a><br><br> by <a href="/bbs/?uid=39676"> Mot</a> <br><br><br> <a href="/bbs/?pid=69343#p"> [Click to Play]</a> </td></tr></table> </p> <p>But now cornering feels janky.<br /> This is because the camera is always aligned exactly with the segment it is on, so when it moves to the next segment it snaps sharply.</p> <p>To counter this we need to turn the camera smoothly towards the next segment's angle as it progresses down the current segment. We can compute this angle in _draw() as:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>local camang=camz*road[camcnr].tu</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Then subtract it from the initial cursor direction:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>local xd,yd,zd=-camang,0,1</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>This gives some improvement:<br /> <table><tr><td> <a href="/bbs/?pid=69343#p"> <img src="/bbs/thumbs/pico8_kojoduyono-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=69343#p"> Pseudo 3D racer tutorial 5</a><br><br> by <a href="/bbs/?uid=39676"> Mot</a> <br><br><br> <a href="/bbs/?pid=69343#p"> [Click to Play]</a> </td></tr></table> </p> <p>It's still not 100% though - there's still some horizontal juddering.</p> <p>This can be a little tricky to understand the cause of.</p> <p>The gist of it is that by turning the camera, we're skewing the first segment will be rendered. But when we calculate the cursor position relative to the camera, we're using the un-skewed camera offset. So when we draw the road forward again, the part that should pass through the camera point will actually be skewed left or right of it. This makes the camera appear to diverge from the path of the road as it moves towards the end of the segment. It then snaps back into the center when it progresses to the next segment.</p> <p>To fix this issue we need to first skew the camera position in the &quot;cursor&quot; direction, <strong>then</strong> calculate the cursor position relative to the skewed camera position.</p> <p>We'll start by creating a basic skew function:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>function skew(x,y,z,xd,yd) return x+z*xd,y+z*yd,z end</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Essentially we're skewing the Z axis from (0,0,1) to (xd,yd,1).</p> <p>We'll need to re-order the &quot;cursor&quot; setup code in _draw(), so that the direction is calculated first.<br /> I.e. move this bit in front of the initial x,y,z calculation:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>-- direction local camang=camz*road[camcnr].tu local xd,yd,zd=-camang,0,1</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Then we can calculate the skewed camera position:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>local cx,cy,cz=skew(camx,camy,camz,xd,yd)</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>And calculate the initial &quot;cursor&quot; position relative to the skewed camera:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>local x,y,z=-cx,-cy+1,-cz+1</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>This finally gives us nice smooth camera movement:<br /> <table><tr><td> <a href="/bbs/?pid=69343#p"> <img src="/bbs/thumbs/pico8_makiybusu-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=69343#p"> Pseudo 3D racer tutorial 6</a><br><br> by <a href="/bbs/?uid=39676"> Mot</a> <br><br><br> <a href="/bbs/?pid=69343#p"> [Click to Play]</a> </td></tr></table> </p> <p>This is the core road drawing algorithm for a pseudo 3D racer.</p> <p>The last step is to simply clean up the rendering so it's not just white lines on black.<br /> With a little bit of work we can turn it into something like this:<br /> <table><tr><td> <a href="/bbs/?pid=69343#p"> <img src="/bbs/thumbs/pico8_tudanawati-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=69343#p"> Pseudo 3D racer tutorial 7</a><br><br> by <a href="/bbs/?uid=39676"> Mot</a> <br><br><br> <a href="/bbs/?pid=69343#p"> [Click to Play]</a> </td></tr></table> </p> <p>I won't go into too much detail here - you can refer to the cartridge code for specifics - I'll just touch on a few basic points.</p> <p>To draw the road we need to render a trapezium (also known as a trapezoid). Essentially we're joining the horizontal lines together and filling them in. Pico-8 doesn't have a built in trapezium drawing function, but we can roll our own by stacking 1-pixel high rectangles.</p> <p>We use alternating colours to communicate speed, which is a common technique in pseudo 3D racers. The easiest way to do this is to track how many segments we are along the road as a whole. Then we can use the modulo operator (%) to determine whether we are on an odd or even segment, or every 6th segment etc, and use that to select a colour.<br /> We pre-calculate this (as &quot;sumct&quot;) for each corner of the road in an _init() function. This makes it easy to calculate for any corner and segment:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>function getsumct(cnr,seg) return road[cnr].sumct+seg-1 end</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>We use this to alternate the ground colour every 3 segments.</p> <p>We don't have to stop at just drawing a plain gray road. The projected positions, trapezium drawing routine and colour alternation give us the tools we need to draw other road features. So we've drawn road markings as a thin trapezium every 4 segments in the middle of the road, and shoulder barrier things on the sides of the road, alternating red and white every other segment.</p> <p>With the basic rendering in place this is a good time to tweak the parameters to get the right look and feel. The following adjustments were made:</p> <ul> <li>Moved the road start point to (0,2,2) in front of the camera</li> <li>Reduced the corner &quot;tu&quot; values, as the corners were too sharp</li> <li>Increased the movement speed from 0.1 to 0.3</li> </ul> <h1>Next steps</h1> <p>This feels like a good place to leave the first tutorial.</p> <p>In <a href="https://www.lexaloffle.com/bbs/?tid=35824">part 2</a> we'll cover drawing roadside objects (and overhead objects) using scaled sprites.</p> https://www.lexaloffle.com/bbs/?tid=35767 https://www.lexaloffle.com/bbs/?tid=35767 Sun, 27 Oct 2019 02:03:28 UTC += and multiple values <p>I just noticed the += shorthand doesn't work the way I expected with comma separated values.</p> <p>For example:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>x,y,z=1,2,3 x,y,z+=10,20,30</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>Sets x to 11 as expected, but y and z are set to <strong>20</strong> and <strong>30</strong> respectively, rather than 22 and 33.</p> <p>Should the last line be equivalent to:</p> <div> <div style="max-width:800px; overflow:auto; margin-bottom:12px"> <table style="width:100%" cellspacing=0 cellpadding=0> <tr><td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> <td background=/gfx/code_bg0.png> <div style="font-family : courier; color: #000000; display:absolute; padding-left:10px; padding-top:4px; padding-bottom:4px; "> <pre>x+=10 y+=20 z+=30</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <p>?</p> <p>Or is there some reason I'm missing?</p> https://www.lexaloffle.com/bbs/?tid=35760 https://www.lexaloffle.com/bbs/?tid=35760 Sat, 26 Oct 2019 06:02:48 UTC Mot3D <p> <table><tr><td> <a href="/bbs/?pid=69222#p"> <img src="/bbs/thumbs/pico8_mot3d-2.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=69222#p"> Mot3D</a><br><br> by <a href="/bbs/?uid=39676"> Mot</a> <br><br><br> <a href="/bbs/?pid=69222#p"> [Click to Play]</a> </td></tr></table> </p> <p>I'm sure this has been done dozens of times already, but I thought it would be fun to give it a go.<br /> So here's a simple Wolfenstein 3D style ray-caster.</p> <p>There's no game-play at all, you can just move around.<br /> You can edit the level in the pico8 map editor (make sure it is always enclosed, otherwise you'll get an infinite loop - rays only terminate when they hit a wall).</p> https://www.lexaloffle.com/bbs/?tid=35743 https://www.lexaloffle.com/bbs/?tid=35743 Thu, 24 Oct 2019 07:20:17 UTC Loose Gravel <p> <table><tr><td> <a href="/bbs/?pid=68979#p"> <img src="/bbs/thumbs/pico8_loose_gravel-4.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=68979#p"> Loose Gravel 1.1</a><br><br> by <a href="/bbs/?uid=39676"> Mot</a> <br><br><br> <a href="/bbs/?pid=68979#p"> [Click to Play]</a> </td></tr></table> </p> <p><img style="" border=0 src="https://www.lexaloffle.com/bbs/files/39676/cars_12.gif" alt="" /><img style="" border=0 src="https://www.lexaloffle.com/bbs/files/39676/cars_15.gif" alt="" /><img style="" border=0 src="https://www.lexaloffle.com/bbs/files/39676/cars_18.gif" alt="" /></p> <p>This is Loose Gravel! A pseudo-3d racer that started as a proof of concept, and gradually grew into something of a game.<br /> If you're curious, you can <a href="https://twitter.com/tommulgrew/status/1184756273180364800">view the progress here</a>.</p> <p>Pretty self explanatory. Choose a course and try to overtake the other cars in 3 laps to win.<br /> Courses are randomly generated but have their own unique parameters and feel.<br /> I was planning to add a tournament mode (and some more tracks), but I ran out of cart space (so that means it's finished! :) )</p> <p>Tip: If you tap the up arrow you will keep accelerating until you hit something or drive off the road. You don't need to hold it down.</p> <p>Enjoy.<br /> -Mot</p> <p><strong>Update: Replace scaled map routine with tline version</strong></p> https://www.lexaloffle.com/bbs/?tid=35686 https://www.lexaloffle.com/bbs/?tid=35686 Thu, 17 Oct 2019 20:38:07 UTC Dungeon Guy (WIP) <p> <table><tr><td> <a href="/bbs/?pid=67663#p"> <img src="/bbs/thumbs/pico8_dungeonguy-1.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=67663#p"> Dungeon Guy (WIP)</a><br><br> by <a href="/bbs/?uid=39676"> Mot</a> <br><br><br> <a href="/bbs/?pid=67663#p"> [Click to Play]</a> </td></tr></table> <br /> I wanted to share a little work-in-progress I've been tinkering on.</p> <p>Dungeon guy doesn't know what he's searching for. Maybe treasure? Maybe a way out? Maybe dungeon girl?<br /> All he knows is to keep searching.</p> <p>Levels are randomly generated and get progressively larger.<br /> The only objective so far is to find the key and the exit door.</p> https://www.lexaloffle.com/bbs/?tid=35349 https://www.lexaloffle.com/bbs/?tid=35349 Sat, 14 Sep 2019 11:25:11 UTC