NOTE: In order to play this game online, go to https://pico-pong-online.herokuapp.com/
This game is one example of how to make online multiplayer games with pico-socket, a new library that I've been working on, that came out of a collaborative project with Ethan Jurman and his Tiny Tanks game
You can read some of the finer details about the pong game here: https://github.com/JRJurman/pico-pong-online
For a smaller example, and details around the library, and how to use it, check out the project on github. Below is an abridged version of the README and how it works:
pico-socket
Server and Client library for adding Online Multiplayer to Pico-8
What is it?
pico-socket is a library that allows multiple Pico-8 web clients (HTML export)
to talk to each other via websockets. Once the data has reached each client, it
is loaded in the Pico-8 environment using GPIO addresses.
Simple Example
In your Pico-8 game, use GPIO addresses as a substitute for game state, using
PEEK
and POKE
to access data for the different players.
room_id_addr = 0x5f80 -- index 0 player_id_addr = 0x5f81 -- index 1 player_0_y_addr = 0x5f82 -- index 2 player_1_y_addr = 0x5f83 -- index 3 function _init() poke(room_id_addr, 0) -- hard code to 0 poke(player_id_addr, 0) -- start as player 0 poke(player_0_y_addr, 64) poke(player_1_y_addr, 64) end function _update() player_addr = 0 if (peek(player_id_addr) == 0) player_addr = player_0_y_addr if (peek(player_id_addr) == 1) player_addr = player_1_y_addr -- move up and down cur_y = peek(player_addr) if (btn(⬆️)) poke(player_addr, cur_y-1) if (btn(⬇️)) poke(player_addr, cur_y+1) -- swap player id if (btnp(❎)) poke(player_id_addr, 0) if (btnp(🅾️)) poke(player_id_addr, 1) end function _draw() cls() rect(40, peek(player_0_y_addr), 44, peek(player_0_y_addr)+4, 12) rect(88, peek(player_1_y_addr), 92, peek(player_1_y_addr)+4, 8) end |
Outside of Pico-8, all you need to do is make a javascript file that
pulls in the pico-socket library and calls createPicoSocketServer
.
You will need to pass in specific configuration based on your game, but besides the config, nothing else is required. The below snippet is the entire file you need for getting a service running locally or on a cloud
service like Heroku.
const { createPicoSocketServer } = require("pico-socket"); createPicoSocketServer({ assetFilesPath: ".", htmlGameFilePath: "./sample.html", clientConfig: { roomIdIndex: 0, // ROOM_ID // index to determine the player id playerIdIndex: 1, // PLAYER_ID // indicies that contain player specific data playerDataIndicies: [ [2], // PLAYER_0_Y [3], // PLAYER_1_Y ], }, }); |
And that's it! Running this javascript file with NodeJS will start a server, that you can connect to. You can run this on your own hardware if you are familiar with setting up servers or LAN setups that other people can connect to. You can also use a cloud service like Heroku to fairly trivially deploy and host your games.
one point of confusion: this post and the github repo show pico-8 code, server-side code, but not the client-side javascript code that connects them!
@merwok - there is no actual client-side javascript required (that developers need to add)! When the server hosts the HTML file, it pre-injects all the client-side javascript required!
If your curious, it happens here - https://github.com/JRJurman/pico-socket/blob/main/src/createPicoSocketServer.js#L28-L36
Slight problem, @JRJurman. I only played the online version.
I played against no opponent yet when the ball was going at a high velocity and went past me, score was not registered for my opponent.
If the ball was traveling slower, score was registered though. If need be I can record a video to demonstrate this.
@dw817 hmmmm, interesting... I'm not able to recreate that happening locally (when I modify the code), but to be honest, I don't plan on putting too much more effort into the Pong game - it's more a proof of concept of the online multiplayer, rather than a polished game - I do appreciate you bringing it up though!
My expertise is in software development, not game development, so I imagine there's a lot of these gotchas and checks that I'm missing!
EDIT I figured out what the issue was! One nuance of working with just GPIO addresses is that they can only be 0-255 - if a value would be negative, it instead rolls-over to 255. So, in this case, what was happening was the ball just skipped over the range I'm looking for on the left side X < 4
, and went straight to the right side, triggering the X > 120
. I have made the range a little wider on the left side which should account for it (a more complex solution would also bound the right check, but I don't want to overcomplicate the code).
@Snow_ I think you could definitely use pico-socket for this - I've actually made some improvements to the library to make it even more easy and straight-forward since building my simple pong game - https://github.com/JRJurman/pico-socket
To handle lots of elements and negative values might be a little tricky. Depending on how big your numbers get, you could use a combination of GPIO values: Let's say you expect some value x
to be -255 to 255, you could have a separate value to say what sign the variable is - if (peek(x_sign) === 0): peek(x_value) else -peek(x_value)
. If you're clever with binary, you could probably store the sign of lots of variables in a single GPIO pin:
binary | value | meaning 00 | 0 | x is positive, y is positive 01 | 1 | x is negative, y is positive 10 | 2 | x is positive, y is negative 11 | 3 | x is negative, y is negative |
255 is 11111111
- so you can store up to 8 flags in this way! I think to get the most out of GPIO, you'll have to do stuff like this. If you have a specific example of something you're worried about encoding, let me know and I'd be happy to help decipher what that might look like here.
As for a lobby, if you want to do all of the lobby stuff in pico-8, you should totally be able to do that! In this game the ball starts moving right away as soon as one player joins, but you could have a dedicated pin for which players have joined (in the sample folder here, player 1 joined
and player 2 joined
are the first pins that each player is responsible for: https://github.com/JRJurman/pico-socket/blob/main/sample/pico-socket.yml#L10
some of us are waiting for scoresub
to implement lobby and turn-by-turn gameplay, without javascript!
@Snow_ I think if you want to implement a separate lobby pico-8 cartridge, you could totally do that. My gut feeling is that regardles of what you do in the cart, you would need to handle the actual redirects in javascript - if you look at https://github.com/JRJurman/pico-socket/blob/main/lib/createPicoSocketClient.js you can see some of the logic used to interact with GPIO values in javascript. You could do something similar where you wait for some lobbyId
to be set, and then do a URL redirect to whatever that value is.
function onFrameUpdate() { // get the lobbyId from the specified index const lobbyId = window.pico8_gpio[lobbyIdIndex]; // check if the lobbyId is set, if it is, redirect there if (lobbyId !== undefined) { window.location.pathname = `/${lobbyId}` } } |
You could additionally add other attributes in the URL for encoding the team, player, map, etc...
window.location.pathname = `/${lobbyId}?playerId=${playerId}&teamId=${teamId}` |
You'd have to parse that information on the other side, which would also require some javascript. Probably something like...
// get lobbyId from url const lobbyId = window.location.pathname.substring(1) // get other attributes from query parameters const urlSearchParams = new URLSearchParams(window.location.search); const { playerId, teamId } = Object.fromEntries(urlSearchParams.entries()); // set the pico8 pin for what lobby this is window.pico8_gpio[lobbyIdIndex] = lobbyId // set the pico8 pin for what player this is window.pico8_gpio[playerIdIndex] = playerId // set the pico8 pins for all this player's information const playerIndicies = playerDataIndicies[playerId] const [teamIdIndex /*, and others defined in the yml*/] = playerIndicies window.pico8_gpio[teamIdIndex] = teamId |
Technically you wouldn't need to set the lobbyId, that would really just get read back in pico-socket to be emitted to the server (and you could just do that yourself), but it doesn't really hurt to do this here, at the risk of being slightly redundant.
All that being said, I think you would be better off squeezing the lobby code (even if it was super minimal) in the original cart. The headache here of swapping between carts, and hijacking this javascript behavior is probably not worth the hassle, and doesn't create for a good experience (since you'll have to wait for everyone to start pico-8 again in the new page).
Then again, if you're looking for an excuse to really split the logic in half here (one for the menus and selections, and one for the actual game), then I could understand wanting to do this.
@merwok I had not known about scoresub
being used for that sort of thing, but looking through the discussions on the discord, it seems very promising, and very cool! I've taken a bit of a break from pico-8 stuff to go back to web-dev, but I might have to play around with that the next time I get an itch to go back to this!
[Please log in to post a comment]