Pick-a-Pocket: Multiplayer Madness

Pick-a-Pocket marked a lot of firsts in my career as a game developer. First game on Steam, first game released by Amythica, and also my first time working on a proper multiplayer game. It was tough, but ultimately rewarding, and I’d like to talk about the culmination of all the effort and self-directed learning it took to make this game.


Part 1: Striving For A Smooth Multiplayer Experience

One of the main issues I had to address when developing PAP was only having the game start when everyone was ready, and with no problems. During the earlier stages of development, I was only testing by myself. Inside of Unreal Engine you can set up your own session in the editor and have up to 4 games running on your machine. This was fantastic, I only needed 1 machine to test everything! Turns out, only slightly awesome. At this point there was no fancy functionality or loading screens, so when I tested the game everything seemed fine. That was until it was time for a real world test over Steam…

First-person perspectives, camera under the water, countdown playing while everyone else is moving - for a few minutes my life flashed before my eyes! Okay, it wasn’t that bad, but I knew I had to fix this somehow. After some research and deleting every delay node in the player blueprint it was time to build an actual multiplayer foundation. In this section I will talk about the steps taken to ensure that players get the smoothest experience possible, and how to use Unreal’s gameplay framework to handle developing a multiplayer game.

When we move from one level to another, all information is unloaded to make room for information about the new level. This info is stored in the GameMode - and its related classes. We make use of the Game Instance class inside UE to store persistent information about the game, for example the number of players in the session. This is because the Game Instance is loaded when the game starts and unloaded when the game application stops, so its information is persistent throughout the lifetime of the game. Whenever a new player connects to a lobby, we save that information in the GameMode, and when the host clicks on play that info is saved to the Game Instance.

I make use of the function ‘OnPostLogin’ inside the GameMode, which is called when a new player connects to our session. It comes with a reference to the new player’s controller which we can use to manipulate the player to be frozen and have certain UI or events happen on the client’s machine. In the case of the Pick-a-Pocket game loop, the level’s GameMode asks the Game Instance how many players connected to the session. We then run a check every second until all players have migrated from the lobby to the level, making sure every player’s PlayerController and PlayerState are valid on the server. The moment all checks are past, we are ready to start the game countdown. By this point all players have seen the tutorial splash and loading screen, and have possessed the level camera for the top-down view.

From this point on, it’s all about making sure information about each player and the world they are running around in are synced. In the next part I’ll be talking about how player visibility works, and the considerations to be made when developing a multiplayer game.


Part 2: How Visibility Works

When a player steps into the light they become visible to everyone, but if they step out of the light they’re invisible again. If a player walks into your listening range, they become visible for only you, so you can chase them down and steal their riches. These are the two ways that players visibility is updated, so it’s crucial that these systems work together. As an example, if a player is under a street lamp and is visible to all other players, we don’t want them turning invisible once we walk out of their range, as they should still be visible in the light. Updating the visibility is quite simple, it’s just important to get it righ

As the players move around in the world, they keep track of important world objects in their range through a list of objects that we call ActorsInRange, as well as a boolean IsUnderLight. If a player walks under a lamp or walks in range of another player, we add a reference of that object to our ActorsInRange list. This array is crucial to making sure players always have the correct level of visibility in any situation. 

We do this by asking the server to tell all players to update our visibility on every player’s game. The function we call uses the ‘Run On Server’ RPC, and we pass in a reference to the player we want to update.

So a player has just walked under a lamp, what happens now? First, they want to ask the server to tell all players “Hey, make me visible!”. This happens by running a server function called UpdateVisForAllPlayers and we pass in a reference to our own player in the world, and a bool for if we should turn visible or invisible. The server then tells all connected players to run a function to update the players visibility. Making a player visible is a lot simpler as there is nothing that prevents a player from turning visible. If the player walks out of the lamp however, that’s a different story. We cannot let this just happen without any checks, so this is where ActorsInRange comes in handy. 


Let us imagine this situation; Player 1 is chasing Player 2 through the light and P2 steps out of range of the lamp, should P2 turn invisible? Well it depends, but let’s assume both players are in the listening range of each other. We need checks to determine what the right outcome is. The server has just told P1 to make P2 turn invisible but P1 has a reference to P2 in their ActorsInRange list, so we don’t want to turn P2 invisible. 


For a game like Pick-a-Pocket, it’s really important that core systems like player visibility work as intended, and that the way the system functions match the players expectation of what should happen.


Updating player visibility through overlapping the range of another player uses the same ActorsInRange list to make sure the state of the player's visibility is correct, however it is slightly different. Instead of using the server, players can directly communicate with each other. The function UpdateOtherPlayerVis takes a player ref and bool like the previous function, except that the other players visibility isn’t getting updated for anyone else as it only relates to our version of the game. This means logic is not being run on the server, as players will have different visibility states on every machine.


Conclusion:

Learning multiplayer in Unreal Engine can be a daunting experience sometimes. Going into pick-a-pocket I only had limited experience with multiplayer games. It took a lot of trial and error to get to where the game is today, and I’ve built the skills to make multiplayer games that give players a smooth experience.

Thank you very much for reading this article. Feel free to ask any questions on the official Amythica Discord: https://discord.gg/6jcyUxzyc4

Stay tuned to keep up to date with more art and programming insight behind Project: JEL as development progresses!



Previous
Previous

PROJECT: JEL DEV LOG 02

Next
Next

Project: JEL Dev log 01