13. Networking
This week's individual assignment was to design, build, and connect wired or wireless node(s) with network or bus addresses and local input &/or output device(s). Basically this means we had to make 2 devices talk to each other with a wired connection or wirelessly. The other requirement was that at least one of our devices had to be one we designed. I decided I would use the board I created in electronics design week when I made a game console development board with a semi-functioning pong game.
View the group assignment here
Da Lobby
I knew I wanted to build on what I had and finally bring wireless multiplayer gaming to the world. I also knew I didn't want it to rely on a central server because I liked the idea of just playing people in your vicinity without any extra required resources. My first step was creating a simple game lobby where you could see other players around you and challenge them.
Based on what I read, I liked some of the features bluetooth low energy (BLE) had to offer.
BLE has 2 actors. Peripherals and centrals. Peripherals are generally things like sensors that offer useful data or updates about the temperature, moisture, etc... They can also get updates by centrals connected to them. This could be to turn on or off a piece of functionality or change what the peripheral measures for. The "centrals" are the devices that are requesting the data from these peripherals, like computers or things with more processing power to bring in the data and actually do somethign with it. In my initial thinking I decided each of the gaming devices would act as both a central and a peripheral. After a central connected to a peripheral, you see the name of the other player in your lobby. At this point you could offer them a challenge. Names were created at random with a combination of 2 animal names from a list of 10. The scanning id run periodically and if a peripheral hasn't been seen for a while, the player is removed from the lobby. For the bluetooth communication I used the ArduinoBLE library.
Then every 100 milliseconds the central on each controller does a check for peripherals advertising on a specific channel set by me, sort of like listening on a specific radio station. BLEService discoveryService creates a new bluetooth service to allow other peers (other gaming controllers in the lobby) to search for it. Then a characteristic, challengeCharacteristic is added to it. The point of this characteristic is so when someone sees you in the lobby and want to challenge you, they will write to this characteristic saying "hey I want to play". When the message is received the "event handler" is called which is set with setEventHandler. This is basically just a function that you create to handle the logic of what should happen in the event that the "challengeCharacteristic" was written to. In our case it will let you know who the challenger is and if you'd want to accept (in an ideal world). After that the discoveryService created is set as the main service to advertise. This means that when advertising is started (which is done on the next lines) other peers will see this broadcasted and will subsequently be able to find the characteristic on it and write to it as mentioned above. Next advertising begins. Without this, no peers would learn about our service.
This scans for other peers on the appropiate ID we've set at the beginning of the code. This is a constant ID so all peers are scanning for the same thing. We need to make sure this is unique from other BLE devices in the area.
The next part actually checks to see if any peripherals are found while scanning. We do this every 100 milliseconds to not check so often.
After that if we find a peripheral we add them to our peripheral "map". You can read more about maps here. Essentially they allow us to store values for whatever keys we want. In our case the keys are the local names (which we set with the animal list above). The values are custom structs (read here to learn more) that store the timestamp when we last "saw" the peripheral and the peripheral object itself that we get while scanning. The reason the timestamp is stored is so we can expire the peripheral if it has been too long since we last saw it.
I only did a prototype of this code and it seemed to work although sometimes names were removed from the lobby a bit prematurely. A longer timeout could fix this but I have considered using ESPNow, the protocol that comes builtin with ESP32 chips.
Wireless gameplay
The next task was actually allowing for wireless game play. I wanted to try something else so I decided to use ESP's ESPNow protocol which is used to connect ESP boards peer to peer with low latency but high throughput. The downside was that it does resend failed messages which is not generally how games are managed over a network. The UDP protocol is normally used and requires no response for each message. This allows for a lot more data and bandwidth to be used. I, however, decided ESPNows simplicity made it worth a try. With UDP there would be a bit more setup. I also explored UDP in week 14 and decided to give ESPNow a shot. ESPNow it was. Here you can find a good starter example with ESPNow that shows how 2 ESP controllers can talk to each other. My strategy was to have one controller be the parent and the other be the child. The child would allow the game simulation to happen as it normally does but could be updated by the parent when it received a new message. On my first version I made some mistakes
I investigated how many packets were being dropped by each controller...not enough to justify the jitters. I checked the cores the espnow code was running on vs the main code. They were indeed on different cores. The ESP32 has 2 available. This was definitely causing inconsistent results. I resolved this by using the atomic library in C++ to use thread safe variables. This means when one core makes a change to a variable, the other core can't modify it until the write is done. This did solve some things but the jitter bug continued. Eventually I realized that even though my parent/child model made sense, I actually was trying to do too much. I changed the code so that the child only controlled its own paddle. The rest of the game state would be dictated by the parent. This worked very very well. The parent/child is chosen by whoever as the greater mac address (which would be exchanged in the lobby). An easy comparison without any extra communication.
Now that I have a good baseline communication its time to make more games and clean up the code so different components can be reused, etc...