Log In  
Follow
nlordell
[ :: Read More :: ]

First of all, this post is not a feature request, but a proof-of-concept PICO-8 controller adapter over the GPIO SERIAL interface, allowing for extended controller input support including analogue sticks, analogue shoulder pads, and more buttons.

How It Works

GPIO works very differently on various PICO-8 targets. Because of this, the way it works on Desktop (Running PICO-8 locally on your computer) and on Web (so HTML/JS exported PICO-8 games) are very different.

Desktop

For desktop targets, you first start a controller host process that creates controller.data and controller.clock named pipe "serial" lines that can be "connected" to a PICO-8 console:

pico8 -i controller.data > controller.clock

Additional controller data is sent over a named pipe attached to PICO-8 process's -i input file (serial channel 0x806). PICO-8 SERIAL command allows scheduling reading a certain amount from the named pipe with:

serial(0x806, target_address, data_length)

The basic concept is to periodically send controller data over the named pipe for it to be read with SERIAL commands. The tricky bit is synchronizing the reading and writing of controller data. For this, we use the PICO-8 process's standard output. Specifically the controller application will wait for some well-formed message indicating that the game is requesting controller data. Once this message is received, controller state is read, encoded and sent over controller.data, which is attached to PICO-8's 0x806 serial channel. We use a single digit numerical value alone on a line representing the controller index being polled to request controller data. For example, to get the controller data for controllers index 2 and 5, you could:

printh"2\n5" -- request new controller data for controllers 2 and 5 over stdout
serial(0x806, 0x9a00, 60) -- read 60 bytes from input file, each controller state is 30 bytes long

Why Not -o?

In theory, it should be possible to use serial channels for both data and clock lines:

pico8 -i controller.data -o controller.clock

This would have a minor advantage over piping PICO-8's standard output into the controller.clock serial line of not losing the PICO-8 cart's standard output. This allows, for example, for PRINTH to be used for debugging.

Unfortunately, this also has a small issue and introduces a frame of controller input delay. This additional frame delay is interesting as it appears, in part, to be a PICO-8 quirk (and possibly even a bug). Specifically, PICO-8 will always wait for SERIAL reads before writes, even if the write was queued first. For example:

serial(0x807, ...) -- write to file specified in `-o` parameter
serial(0x806, ...) -- read from file specified in `-i` parameter

In this case, unintuitively, PICO-8 will first read from the file specified by the -i parameter before writing to the file specified by the -o parameter. Therefore, we need to request controller data one frame early. This leads to a total of 2 frames of delay for receiving controller input:

  • Frame 0: request controller data
    • FLIP causes the request to get flushed over the SERIAL interface
  • Frame 1: read controller data
    • The controller data gets written to memory between frames
  • Frame 2: controller data is in memory and can be used in the _UPDATE function
-- Frame 0
serial(0x807, ...) -- signal that we want controller data
flip() -- flush the serial output

-- Frame 1
serial(0x807, ...) -- already request controller data for next frame
serial(0x806, ...) -- queue read of the controller data
flip() -- flush the serial output and read serial data to memory

-- Frame 2
peek(...) -- we can now read controller data from memory

By using the fact that PRINTH flushes immediately and piping PICO-8's standard output to the controller.clock serial line, we only have 1 frame of delay.

Controller Detection

To detect if no controller is attached:

poke(0x9a0c, 0x9a) -- write marker value to memory location of first button
printh"0" -- request controller 0 data
serial(0x806, 0x9a00, 30) -- queue read of the controller data
flip() -- read serial data to memory
if @0x9a0c == 0x9a then
  -- controller not connected
end

Web

For web targets, it is very straight forward.
On each animation frame, we first set the pico8_gpio[0] pin to a marker value, so that the PICO-8 cart can detect that it is in web mode.
Then we read controller state and write it directly to pico8_gpio.
Easy-peasy-lemon-squeezy.

More Info

I created a GitHub project with the code used for the demo included in the GIF. It includes a C program for the controller host (tested on macOS and Linux) as well as the demo cart featured in the GIF included above.

POOM

Just for fun, I also adapted the AWESOME POOM cart to use extended controller support. It was changed to have "modern" first person shooter controls:

  • The left analogue stick is used for moving back and forth as well as strafing from left to right
  • The right analogue stick is used for turning left and right
  • The right shoulder button is used for shooting

The changes required to adapt POOM were minimal. You can try out the modified version online here. Instructions for running POOM with extended controller support is also included in the aforementioned GitHub repo.

P#114038 2022-07-07 19:22 ( Edited 2022-07-07 19:33)

[ :: Read More :: ]

This post is mostly just to share an experiment I did when playing around with compressing graphics.

I noticed when looking around, that many PICO-8 carts that do compress graphics were using some form of LZ or RLE compression. Out of curiosity, I decided to do some research on how the Generation 1 Pokemon games compressed an impressive amount of graphics data into the small cart size. It turns out that they use a form of RLE compression. I won't go into detail myself, but instead refer to a great YouTube video that very precisely describes how it works.

Essentially, this compression algorithm stands out because:

  • It splits the graphics into bit planes (i.e. 1 bit-per-pixel images)
  • It operates on pixel pairs, only encoding runs of pairs of 0's
  • It uses several flags to control how the bit planes are mixed in order to increase compression ratios
  • The original algorithm compresses 2bpp images (i.e. 4 colours). That being said, there is no reason why it cannot be extended to higher colour depths

Anyway, here is an implementation of a decompression routine for the Pokemon Generation 1 RLE image compression format. Overall, I'm not sure how useful this will be. It costs 400 tokens and decoding the sample 15x15 sprite in the demo cart takes 12 frames@60fps. On the plus side, it fits neatly into 544 bytes (less than a single row in the sprite editor), compared to its decompressed size of 7200 bytes, albeit with only a quarter of the colours. Note that the compression ratio greatly depends on the data. Also, I did not put much effort into optimizing the implementation for neither tokens nor CPU cyles.

In my opinion some of the "cool stuff" that this approach to compression does (that can be applied to other algorithms) is:

  • Using a reduced colour depth greatly reduces graphics size
  • RLE compressing a bit plane has some neat properties (the linked video goes into some more detail about this)

Cart #pkz_01-0 | 2021-02-24 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
11

P#88121 2021-02-24 21:00