Log In  
Follow
superfluid
Infinite Golf with Friends!
by
Picogolf Endless 0.1
by
[ :: Read More :: ]

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!

We'll cover:

  • Stuff you need (such as javascript libraries, and where to get them)
  • How pico-8 talks to the outside world (General Purpose Input Output - GPIO - memory)
  • How to have a conversation between pico-8, the web browser running a pico-8 cart, and a database server
  • 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)

We won't cover:

  • Server-side coding such as would be required for concurrent multiplayer gaming like, er, Counterstrike or something (do people still play Counterstrike?)
  • Setting up a specific online database to hold your replay data - I use FatFractal http://fatfractal.com, but I am sure there are loads of options these days. Anything that can be accessed from client-side Javascript will do just fine.
  • How to make games or code pico-8 - this is an advanced-level tutorial I'd say

So let's dive in ...

A quick example or two

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...

Things we need

GPIO: How pico-8 talks to the outside world

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.

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.

Passing game data in 128 byte buffer

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.

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.

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.

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.

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.

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 "conversation" (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).

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)

Managing the 'conversation' between pico-8 and the host environment

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.

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.

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.

If this is all rather abstract, hopefully the index.html excerpt below will help things make a bit more sense:

<script src="pico8-gpio-listener.js"></script>

<!-- REPLACE THIS WITH YOUR OWN DATABASE JS API!! -->
<script src="FatFractal.js"></script> 

<script type="text/javascript">

// This array is how we read/write GPIO on the browser side
var pico8_gpio = new Array(128);

// benwiley4000's GPIO library - "other GPIO libraries are also available"
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>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 < 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 < 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>0)
        {
            // Lock to avoid 'talking to ourself'
            gpio[0]=4;

            // pack replay into GPIO memory
            for(var i = 1; i < 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'!

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 must be called index.html.

Summary

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!

Complete source code for Massive Multiplayer Moon Lander

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!

Unpacking all of this is beyond the scope of this article, but there are loads of great tweetcart resources on lexaloffle.com and elsewhere.

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)
?"❎",32,105,8
if(@g==2)m(u+n*o,g,o)p(g,3)n+=1
b=0a=0j=btn
if(f>0)then
if(j(⬅️))a+=z b-=z d("ˇ",x-3,y+4,9)f-=1
if(j(➡️))a-=z b-=z d("ˇ",x+4,y+4,9)f-=1
end
q+=a
v+=b+z
if pget(x+3,y+5)==8and v<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>k)h=o
?"웃",h,l,2
end
?"웃",x,y,7
flip()t+=1if(t%3==0and r<k)p(u+r*2,x)p(u+r*2+1,y)r+=1
if(y<o*2and w<1)goto _
p(u,1)m(g,u,o)
if(w<1)goto ❎

Thanks for reading!

Superfluid
@superfluid
itch.io: https://superfluid.itch.io
twitter: @trappyquotes

(**)
And fwiw a couple of iOS games too:

P#84109 2020-11-11 16:00 ( Edited 2020-11-12 10:08)

[ :: Read More :: ]

Cart #diruwopusa-0 | 2020-10-29 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
5

My first tweetcart! All the 1D 3-neighbour cellular automata in 194 characters :)

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!

Wikipedia article of relevance

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)>1and 4or 0u=p(x,t)>1and 2or 0r=p(x+1,t)>1and 1or 0if(band(w,shl(1,l+u+r))>0)pset(x,t+1,c)end
flip()end
run()
P#83465 2020-10-29 19:58

[ :: Read More :: ]

Cart #rijenipeko1-10 | 2019-09-27 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
10

ONLINE MULTIPLAYER VERSION IS HERE!

https://gamejolt.com/games/infinitegolf/441497

Please let me know what you think :D

Updates

  • Avatars! :D
  • Music and sfx
  • Particle fx
  • Leaderboard for online play (coming very soon...)
  • Scorecard every 18 holes
  • Tweaks and bug fixes
  • Intro screen
  • Full GPIO support for multiplayer (but that needs a website to host, so watch this space...)
  • Thanks @remcode @dw817 @josh999 for the awesome feedback ;)

Previous

  • Autotiling
  • Tweaked power meter - more power close to the max
  • Randomise the golfer avatar per cart reset
  • Increase par for holes with lots of water
  • Wind same for all users now

  • Much improved PAN (up arrow)
  • Fixing some crazy bugs (like hole 71 causing an infinite loop <facepalm>)

  • Stepping stones in lakes - hopefully this should sort the unplayable holes issue
  • Menu options to reset hole and delete save game

  • Tutorial level!
  • The holes start easy and get harder - after 50 or so holes anything goes
  • Procedural generation basic version is ready!
  • Up/Down to pan a bit
  • Lots of UI tweaks

TODO

  • Online replays via GPIO and a BAAS (e.g., FatFractal)
  • Stats screen (every 18 holes?)
P#67117 2019-09-02 09:21 ( Edited 2019-09-28 06:55)

[ :: Read More :: ]

Cart #donikapobo-0 | 2019-08-22 | Code ▽ | Embed ▽ | No License
4

Hi! I'd love to hear any feedback on my work in progress top-down golf game Picogolf Endless, thanks for reading/playing!

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)

Controls

LEFT/RIGHT - aim
X - hold for more shot power
LEFT/RIGHT while power meter is moving - spin

Planned features

  • Procedural generation
  • Online multiplayer using GPIO/JS
  • Complete the tree tiles / move on from these placeholders
  • Particles and polish
  • Music and more complete sfx

Thanks again :)

P#66860 2019-08-22 19:08

[ :: Read More :: ]

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

Would love to hear your thoughts!

sf

P#66703 2019-08-15 19:09 ( Edited 2019-08-15 19:09)