superfluid [Lexaloffle Blog Feed]https://www.lexaloffle.com/bbs/?uid=39298 Online multiplayer GPIO tutorial <img style="margin-bottom:16px" border=0 src="/media/39298/ttj_gpio_0.gif" alt="" /> <p>People have been asking how I managed to fit a multiplayer game into the space of two tweets (560 chars) for TweetTweetJam5, so I thought I'd write a mini tutorial to explain the process!</p> <h3>We'll cover:</h3> <ul> <li>Stuff you need (such as javascript libraries, and where to get them)</li> <li>How pico-8 talks to the outside world (General Purpose Input Output - GPIO - memory)</li> <li>How to have a conversation between pico-8, the web browser running a pico-8 cart, and a database server</li> <li>How to implement multiplayer that allows users to play against 'replays' or 'ghosts' of previous players (which is rather cool and under-explored territory imo)</li> </ul> <h3>We won't cover:</h3> <ul> <li>Server-side coding such as would be required for concurrent multiplayer gaming like, er, Counterstrike or something (do people still play Counterstrike?)</li> <li>Setting up a specific online database to hold your replay data - I use FatFractal <a href="http://fatfractal.com">http://fatfractal.com</a>, but I am sure there are loads of options these days. Anything that can be accessed from client-side Javascript will do just fine.</li> <li>How to make games or code pico-8 - this is an advanced-level tutorial I'd say</li> </ul> <p>So let's dive in ...</p> <h2>A quick example or two</h2> <p>I've used the method for a few pico-8(**) games now - if you want to get a feel for what this tutorial will teach you, feel free to play these (and please let me know what you think!) - I'll wait...</p> <ul> <li>Massive Multiplayer Moon Lander: (560 chars for #TweetTweetJam 5) <a href="https://superfluid.itch.io/massive-multiplayer-moon">https://superfluid.itch.io/massive-multiplayer-moon</a></li> <li>Infinite Zombies with Friends: (for #LowRezJam 2020) <a href="https://superfluid.itch.io/infinite-zombies-with-friends">https://superfluid.itch.io/infinite-zombies-with-friends</a></li> <li>Infinite Golf with Friends: <a href="https://gamejolt.com/games/infinitegolf/441497">https://gamejolt.com/games/infinitegolf/441497</a></li> </ul> <h2>Things we need</h2> <ul> <li>Pico-8, obviously, and a recent version thereof (so that the HTML exporter produces carts that play perfectly on mobile - thanks <a href="https://www.lexaloffle.com/bbs/?uid=1"> @zep</a>!)</li> <li>To know how to export your cart as an HTML page: &quot;EXPORT FOO.HTML&quot;</li> <li><a href="https://www.lexaloffle.com/bbs/?uid=6654"> @BenWiley4000</a>'s excellent pico8-gpio-listener library <a href="https://github.com/benwiley4000/pico8-gpio-listener">https://github.com/benwiley4000/pico8-gpio-listener</a></li> <li>Access to a database with Javascript interface, such as FatFractal (which is free, and easy to use <a href="http://www.fatfractal.com">http://www.fatfractal.com</a>)</li> <li>For testing, it is handy to be able to run a local webserver (or you can upload to itch.io and have the page as a draft, but that makes for slower iteration). Some options here: <a href="https://developer.mozilla.org/en-US/docs/Learn/Common_questions/set_up_a_local_testing_server">https://developer.mozilla.org/en-US/docs/Learn/Common_questions/set_up_a_local_testing_server</a></li> </ul> <h2>GPIO: How pico-8 talks to the outside world</h2> <p>GPIO (general purpose input output) is a 128 byte segment of pico-8 memory, starting at 0x5f80. We can access it like any other chunk of pico-8 RAM, using peek, poke, memcpy and memset. Each of the 128 GPIO bytes can store a number in the range 0--0xff (0--255 in decimal). If we want to store larger numbers we can do so using poke2 (0--0xffff) or, for full precision, poke4 (0--0xffff.ffff). For TweetCarts we can make use of handy function aliases @=peek, %=peek2, $=peek4.</p> <p>GPIO memory is shared between the pico-8 cart, and the host environment (in our case, this is the HTML page running the exported cart Javascript). This means that both sides can read from, and write to, the memory. This allows data to be passed back and forth 128 bytes at a time between pico-8 and the outside world. We need two tricks. The first trick is to set up the 'conversation' so that both sides don't try to 'speak' at the same time (which would result in lost data). The second trick is to encode the game data so it can pass through the small 128 byte buffer. Let's take the second one first.</p> <h3>Passing game data in 128 byte buffer</h3> <p>How to approach this really depends on your specific game. For my games, I want to save the player position as a ghost replay. It is convenient to fit a replay into a single 128-byte chunk, so that the whole thing can be sent to the server in one transaction. Splitting across multiple chunks requires more sophisticated synchronisation and is beyond the scope of this article, but there are other resources on lexaloffle.com that might give some ideas here. </p> <p>Now, 128 bytes is not much space. If we are storing x,y coordinates at full pico-8 numerical precision, each 'frame' would require 8 bytes meaning we could only store 16 frames, which would cover 1/4 of a second's worth of action at 60fps - not much of a replay! We therefore need to intelligently compress information. </p> <p>The first compression is to use lower precision numbers, just in the range 0-255, so that each frame can be stored in two bytes. This gives per-pixel accuracy for games where the world doesn't scroll. For scrolling games we can divide by 2, or 4 etc to get per 2-pixel or per-4-pixel accuracy but with the ability to represent numbers out to 512 or 1024, to support bigger game worlds. </p> <p>The second compression is to only record replay frames every N pico-8 frames, using something like if(t%10==0)add(replay,{x,y}). This snippet appends a replay frame every 10 pico-8 frames. </p> <p>But won't these compression methods lead to replay sprites jumping around the screen in an unappealing way? Yes! But to get around that we can interpolate between two frames when rendering replays, to smoothly blend replay spite positions from one replay frame to the next. </p> <p>Oh, and one more thing, we can't use all 128 bytes of GPIO for replay frames - we need at least one byte to manage the &quot;conversation&quot; (more on that next), and perhaps more bytes to hold things like the current level number, the player's name, the score, and the number of replay frames in the packet (if this isn't constant between plays).</p> <p>Here is a gif from Infinite Zombies with Friends. The player moves over extended ranges, and can take up to a minute. By using interpolation the replayed motion is nice and smooth, even though the replays use hardly any data. (apologies for the poor colour reproduction here - blame GiphyCapture)</p> <img style="margin-bottom:16px" border=0 src="/media/39298/zombie_1.gif" alt="" /> <h3>Managing the 'conversation' between pico-8 and the host environment</h3> <p>The 'conversation' between the cart, the browser, and the cloud database (the 'communications protocol' if you want the proper term) uses the first byte of the GPIO array to control which party is 'speaking' at any given time. If the cart is sending data to the browser, it memcpy's the data into the GPIO memory, and sets the first byte (let's call it the comms byte) to 1 with poke(0x5f80,1). The browser listens for changes to the GPIO buffer, and if it sees a comms byte of 1, sends the replay data off to the cloud database. </p> <p>On the other hand, if the browser wants to send data to the cart, it loads the data for a single replay into GPIO and set the comms byte to 2. The cart checks the state of the comms byte once per frame, and if it sees a 2, knows it can load 128 bytes from GPIO to user data, with memcpy(0x4300 + 128*N, 0x5f80, 128) where N is the number of replays it has already loaded. The cart then sets the comms byte to 3, which is the cart's way of signalling the browser that it is ready for more data. </p> <p>The final matter to resolve is 'who speaks first'. I always have the cart speak first, by setting special comms state to 9. The simple reason being that the browser side is ready before the cart, and may have data ready to send before the cart is ready to receive. By starting with the cart, the conversation can be driven more directly by the needs of the player. For instance, maybe the cart needs data for a specific map - in this case the browser needs the cart to tell it which map that is, before data can be fetched from the cloud.</p> <p>If this is all rather abstract, hopefully the index.html excerpt below will help things make a bit more sense:</p> <div> <div class=scrollable_with_touch style="width:100%; 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>&lt;script src=&quot;pico8-gpio-listener.js&quot;&gt;&lt;/script&gt; &lt;!-- REPLACE THIS WITH YOUR OWN DATABASE JS API!! --&gt; &lt;script src=&quot;FatFractal.js&quot;&gt;&lt;/script&gt; &lt;script type=&quot;text/javascript&quot;&gt; // This array is how we read/write GPIO on the browser side var pico8_gpio = new Array(128); // benwiley4000's GPIO library - &quot;other GPIO libraries are also available&quot; var gpio = getP8Gpio(); // register a callback handler which is triggered whenever the GPIO is // changed by EITHER pico-8 or the browser gpio.subscribe(function(indices) { // read current communication state - who is 'speaking'? Cart, or browser? var comms_state = gpio[0]; // these comms states are set by the browser side, and so we don't want // to respond to them (no 'talking to ourself') if (comms_state==4) return; if (comms_state==0) return; if (comms_state==2) return; if (comms_state==9) { // this represents the cart saying 'hello', and triggers us to fetch data // from our cloud database loaded_replays = // YOUR DATABASE QUERY HERE nrep = loaded_replays.length; if (nrep&gt;0) { // lock GPIO - this means that as we insert data into the GPIO // array, we won't keep triggering the browser // ('no talking to ourself') gpio[0]=4; // pack replay data into GPIO array for(var i = 1; i &lt; 128; i++) { gpio[i]=loaded_replays[nrep-1].raw_gpio_n[i]; } // decrement counter so we can keep track of what's already sent nrep-=1; // Setting the GPIO comms byte to 2 indicates to the cart that // there is data ready for them to consume gpio[0]=2; } else { // If there is no data to send, we indicate this to the cart by // setting the GPIO comms byte to 0 gpio[0]=0; } } else if (comms_state==1) { // This comms state represents the cart SENDING data to the browser. All we // have to do here is push it up to the cloud server. // We pack the whole GPIO array into a field called raw_gpio, and send it to // our cloud database service (not shown) var raw_gpio = new Array(128); for(var i = 0; i &lt; 128; i++) { raw_gpio[i]=gpio[i]; } // SEND raw_gpio TO YOUR CLOUD DATABASE // No need to do anything else with GPIO } else if (comms_state==3) { // In this comms state, the cart is signalling that they have received a data // item, and are ready for more if there is more to send. // We either send more (and set comms state back to 2) or if there is nothing // to send, set comms state to 0 if(nrep&gt;0) { // Lock to avoid 'talking to ourself' gpio[0]=4; // pack replay into GPIO memory for(var i = 1; i &lt; 128; i++) { gpio[i]=loaded_replays[nrep-1].raw_gpio_n[i]; } // decrement counter nrep-=1; // signal cart that the data is ready for them to read from GPIO gpio[0]=2; } else { // no more data - indicate by setting gpio comms state to 0 gpio[0]=0; } } }, true); // don't forget that '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>Here's a tip: you won't need to export the .html file every time. You can do EXPORT FOO.JS to just export the javascript innards. I append a version number here, and then update the FOO_v?.js reference in index.html. Bonus tip, for your game to work on itch.io, the html file <strong>must</strong> be called index.html.</p> <h2>Summary</h2> <p>If you've read this far, you already know that Pico-8 is a wonderfully expressive tool. I hope this article has given you some ideas for how you could extend your exploration of Pico-8's creative possibilities. I would be very grateful for any comments, feedback, or questions - I always reply promptly :) Looking forward to seeing what you create with Pico-8 GPIO online functionality!</p> <h2>Complete source code for Massive Multiplayer Moon Lander</h2> <p>The code is fully 'golfed' to fit in minimal size- 560 characters in this case - in no way would I recommend writing code in this style unless it is absolutely necessary! </p> <p>Unpacking all of this is beyond the scope of this article, but there are loads of great tweetcart resources on lexaloffle.com and elsewhere.</p> <div> <div class=scrollable_with_touch style="width:100%; 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>g=0x5f80m=memcpy u=0x4300s=memset k=63s(g,9,1)n=1p=poke::❎::d=print o=128t=0r=1x=o*rnd()y=0q=0v=0z=0.05f=99w=0::_::cls()circfill(k,230,o,6)rect(0,0,1,f,9) ?&quot;❎&quot;,32,105,8 if(@g==2)m(u+n*o,g,o)p(g,3)n+=1 b=0a=0j=btn if(f&gt;0)then if(j(⬅️))a+=z b-=z d(&quot;ˇ&quot;,x-3,y+4,9)f-=1 if(j(➡️))a-=z b-=z d(&quot;ˇ&quot;,x+4,y+4,9)f-=1 end q+=a v+=b+z if pget(x+3,y+5)==8and v&lt;1then w=1else x+=q y+=v end for e=1,n-1do h=@(u+e*o+r*2)l=@(u+e*o+r*2+1) if(r&gt;k)h=o ?&quot;웃&quot;,h,l,2 end ?&quot;웃&quot;,x,y,7 flip()t+=1if(t%3==0and r&lt;k)p(u+r*2,x)p(u+r*2+1,y)r+=1 if(y&lt;o*2and w&lt;1)goto _ p(u,1)m(g,u,o) if(w&lt;1)goto ❎</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> <h3>Thanks for reading!</h3> <p>Superfluid<br /> @superfluid<br /> itch.io: <a href="https://superfluid.itch.io">https://superfluid.itch.io</a><br /> twitter: @trappyquotes</p> <p>(**)<br /> And fwiw a couple of iOS games too:</p> <ul> <li>Trappy Tomb: <a href="https://toucharcade.com/2015/07/08/trappy-tomb-review/">https://toucharcade.com/2015/07/08/trappy-tomb-review/</a></li> <li>MiniGolf Endless MMO: <a href="https://toucharcade.com/2015/09/11/minigolf-endless-mmo-review/">https://toucharcade.com/2015/09/11/minigolf-endless-mmo-review/</a></li> </ul> https://www.lexaloffle.com/bbs/?tid=40334 https://www.lexaloffle.com/bbs/?tid=40334 Wed, 11 Nov 2020 16:00:53 UTC Cellular automata tweetcart <p> <table><tr><td> <a href="/bbs/?pid=83465#p"> <img src="/bbs/thumbs/pico8_diruwopusa-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=83465#p"> cellular automata tweetcart</a><br><br> by <a href="/bbs/?uid=39298"> superfluid</a> <br><br><br> <a href="/bbs/?pid=83465#p"> [Click to Play]</a> </td></tr></table> </p> <p>My first tweetcart! All the 1D 3-neighbour cellular automata in 194 characters :)</p> <p>The 1D cellular automata are a simply family of rules that are applied a row at a time in the tweetcart - there are 2^2^3=256 possible rules, and some make very cool fractal non-repeating patterns ... I love that such complexity can emerge from such simple rules - hope you enjoy!</p> <p><a href="https://en.wikipedia.org/wiki/Elementary_cellular_automaton">Wikipedia article of relevance</a></p> <div> <div class=scrollable_with_touch style="width:100%; 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>cls()w=flr(rnd()*256)c=7+w%8p=pget ?w,60,1,c-1 for t=5,128do for x=0,127do l=p(x-1,t)&gt;1and 4or 0u=p(x,t)&gt;1and 2or 0r=p(x+1,t)&gt;1and 1or 0if(band(w,shl(1,l+u+r))&gt;0)pset(x,t+1,c)end flip()end run()</pre></div></td> <td background=/gfx/code_bg1.png width=16><div style="width:16px;display:block"></div></td> </tr></table></div></div> https://www.lexaloffle.com/bbs/?tid=40070 https://www.lexaloffle.com/bbs/?tid=40070 Thu, 29 Oct 2020 19:58:06 UTC Infinite Golf with Friends! <p> <table><tr><td> <a href="/bbs/?pid=67117#p"> <img src="/bbs/thumbs/pico8_rijenipeko1-10.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=67117#p"> Infinite Golf with Friends! 1.0</a><br><br> by <a href="/bbs/?uid=39298"> superfluid</a> <br><br><br> <a href="/bbs/?pid=67117#p"> [Click to Play]</a> </td></tr></table> </p> <h3>ONLINE MULTIPLAYER VERSION IS HERE!</h3> <p><a href="https://gamejolt.com/games/infinitegolf/441497">https://gamejolt.com/games/infinitegolf/441497</a></p> <p>Please let me know what you think :D</p> <h3>Updates</h3> <ul> <li>Avatars! :D</li> <li>Music and sfx</li> <li>Particle fx</li> <li>Leaderboard for online play (coming very soon...)</li> <li>Scorecard every 18 holes</li> <li>Tweaks and bug fixes</li> <li>Intro screen</li> <li>Full GPIO support for multiplayer (but that needs a website to host, so watch this space...)</li> <li>Thanks <a href="https://www.lexaloffle.com/bbs/?uid=39267"> @remcode</a> <a href="https://www.lexaloffle.com/bbs/?uid=15232"> @dw817</a> @josh999 for the awesome feedback ;)</li> </ul> <h3>Previous</h3> <ul> <li>Autotiling</li> <li>Tweaked power meter - more power close to the max</li> <li>Randomise the golfer avatar per cart reset</li> <li>Increase par for holes with lots of water</li> <li> <p>Wind same for all users now</p> </li> <li>Much improved PAN (up arrow)</li> <li> <p>Fixing some crazy bugs (like hole 71 causing an infinite loop &lt;facepalm&gt;)</p> </li> <li>Stepping stones in lakes - hopefully this should sort the unplayable holes issue</li> <li> <p>Menu options to reset hole and delete save game</p> </li> <li>Tutorial level!</li> <li>The holes start easy and get harder - after 50 or so holes anything goes</li> <li>Procedural generation basic version is ready!</li> <li>Up/Down to pan a bit</li> <li>Lots of UI tweaks</li> </ul> <h3>TODO</h3> <ul> <li>Online replays via GPIO and a BAAS (e.g., FatFractal)</li> <li>Stats screen (every 18 holes?)</li> </ul> https://www.lexaloffle.com/bbs/?tid=35236 https://www.lexaloffle.com/bbs/?tid=35236 Mon, 02 Sep 2019 09:21:48 UTC Picogolf Endless 0.1 <p> <table><tr><td> <a href="/bbs/?pid=66860#p"> <img src="/bbs/thumbs/pico8_donikapobo-0.png" style="height:256px"></a> </td><td width=10></td><td valign=top> <a href="/bbs/?pid=66860#p"> Picogolf Endless 0.1</a><br><br> by <a href="/bbs/?uid=39298"> superfluid</a> <br><br><br> <a href="/bbs/?pid=66860#p"> [Click to Play]</a> </td></tr></table> </p> <p>Hi! I'd love to hear any feedback on my work in progress top-down golf game Picogolf Endless, thanks for reading/playing!</p> <p>Since it is work in progress it is just one course at the moment (procedural generation to come) but most of the physics are in place (wind is shown by the arrow top centre)</p> <h2>Controls</h2> <p>LEFT/RIGHT - aim<br /> X - hold for more shot power<br /> LEFT/RIGHT while power meter is moving - spin</p> <h2>Planned features</h2> <ul> <li>Procedural generation</li> <li>Online multiplayer using GPIO/JS</li> <li>Complete the tree tiles / move on from these placeholders</li> <li>Particles and polish</li> <li>Music and more complete sfx</li> </ul> <p>Thanks again :)</p> https://www.lexaloffle.com/bbs/?tid=35149 https://www.lexaloffle.com/bbs/?tid=35149 Thu, 22 Aug 2019 19:08:22 UTC Golf buggy <p>Hi everyone! I'm new to the pico-8 community and getting back into the best hobby on the planet (gamedev, of course) after a long break. I just wanted to share my very early work in progress - a top down golf game. I had a funny bug where I set the spin force a bit too high o_0</p> <p>Would love to hear your thoughts!</p> <p>sf</p> <img style="margin-bottom:16px" border=0 src="https://www.lexaloffle.com/bbs/files/39298/golf_0.gif" alt="" /> https://www.lexaloffle.com/bbs/?tid=35085 https://www.lexaloffle.com/bbs/?tid=35085 Thu, 15 Aug 2019 19:09:02 UTC