In my CardConquest blog series, I created a multiplayer game in Unity that uses Mirror for the multiplayer networking stuff. Mirror works great, especially when testing locally or on my home network. Things get a little more complicated when you want to create a game for people to join on the internet. Using Mirror and its “Kcp Transport” meant that whoever hosted the game would have need to doing port forwarding on their router in order for people to join their game.
That was all fine and good when I was making and testing a game on my own, but if I wanted anyone else to ever play my game, asking them to know what port forwarding is and then expecting them to figure out how to do that on their specific home router is going to be a bit of a turn off. The chances a random person would ever want to play my game would drop from 0.01% to a nice round 0.00%.
One solution around that would be to host my own dedicated servers for people to join. This would most likely cost me money, or involve running my desktop as a dedicated server out of my house. I’d also have to do some fiddling with the code to be able to run a headless server game. I could also try Photon PUN, which would host games in the cloud for me, but that would require a complete re-write of the game’s code.
Thankfully, the good people at Valve came to the rescue with the Steamworks API. Steamworks, among other things, allows for players to host their games local on their own computer and play with other people on the Internet. No port forwarding required! I didn’t dig too much into how Steamworks does this, but I believe what it is doing is referred to as NAT punch-through/punching. How a NAT punch-through works would require explaining a lot of networking concepts I’ve never covered in this blog before, but if you want to learn, Keith Johnston wrote a great blog post on what it is, how it works, and how it can be used for multiplayer gaming here.
Steamworks is written in C++, but there is a C#/.NET wrapper for it called Steamworks.NET. Steam is listed as a supported Transport for Mirror here. In order to use Steamworks.NET in Mirror, you need to use FizzySteamworks, which I will discuss next.
I used a couple of resources to create my Steamworks lobby. First was DapperDinos youtube tutorial on it. Next was this post on Gemesutra (I don’t know why the post is a black background with black text. I only looked at it briefly because of that though). And then the thing that I really stole most of my code from, the Steamworks.NET-matchmaking-lobbies-example github repo, specifically the code in the lobbyserverTEST.cs file.
The unity project for this can be found on Github here. Download that and save it, then add the project to Unity.
When Unity starts the project, you will likely get a bunch of errors in the console. You will need to install both Mirror and FizzySteamworks as packages. Install mirror through Unity’s asset store/package manager as usual. I described how to install Mirror through the Asset store in a previous post. Make sure that you have an up-to-date version. The version I tested this all on was 42.2.12.
After Mirror has been added/imported, you will need to import the FizzSteamworks package. If you go to FizzySteamworks’ release page, you should see an option to download a “.unitypackage” file. Download the latest version. For me, that was v4.4.1.
After you have downloaded the .unitypackage file, you can import it into Unity. You can do that either by importing a custom package through the Unity editor UI
Or, as long as you have the Unity project open that you want to import FizzySteamworks to, you can just double click on the .unitypackage file and it will import.
Steam Network Manager
The main thing FizzySteamworks will be doing for you is providing Mirror with a new transport to communicate through. This will allow Mirrors normal Commands/Rpcs to send data through the Steamworks API. All you need to do (or at least all it seems you need to do to me) to get a mirror game working through Steamworks is to do some initial setup to get Steamworks initialized and connected to steam.
If you look at the NetworkManager in the scene, you will see that the transport is set to FizzySteamworks.
There are two import scripts attached to the NetworkManager to allow Steamworks to work, the Steam Manager and Fizzy Steamworks scripts.
Then there is a third and final script called Steam Lobby attached to the NetworkManager. This is a custom script that will do all of the initialization to connect to Steamworks and sets up the necessary callbacks to send/receive data from steam.
Steam is Required
This should go without saying, but Steamworks uses Steam to communicate. Because of that, you will need steam running in the background and logged for this to work in Unity. If you try to run the game without steam running, you will get the following error:
[Steamworks.NET] SteamAPI_Init() failed.
When you run the game with Steam, you will see that Steam says you are running a game called “Spacewar.”
This is because the “AppId” associated with the project is for a game called Spacewar. You can apply for your own Steam AppId if you want, but it will cost you $100. The Spacewar AppId is provided by Valve to use for testing Steamworks.
This also means that in order to test multiplayer, you will need at least two computers (or virtual machines) running steam AND each steam client will need its own account. I created a new dummy account on steam to do this. It’s kind of annoying, but oh well.
What the “Game” Does
The “Game,” if you can call it that, is quite simple. Really all this project does is create a steam lobby and then start a game. It has a very bare bones (read: ugly) UI that uh, “gets the job done” I guess. You are presented with two options, to Create a Lobby or Get List of Lobbies
When you create a lobby, you have the option of providing the lobby a name and whether to make it “Friends Only.”
If you don’t provide a lobby name, the game sets the name to “(your steam profile name)’s Lobby.” If you make the game Friends Only, your lobby will not show up in any lobby searches. Players can only join your game after you invite them and if they are one of your Steam friends.
“Get List of Lobbies” will first show you a list of all lobbies available.
You can see there is a whole bunch of games available. Those lobbies aren’t all playing my game (I wish…) they are just other people running a game that is using the Spacewar AppId. You can filter lobbies through the search bar as well:
Clicking the “Join This Lobby” button on any of the lobbies will join that lobby. Note: you won’t be able to join any of those random lobbies. You will get an error saying something like the game isn’t available. You can only join a lobby that is running this lobby project.
All of this has been occurring in the “Scene_Steamworks” scene of the project. Once a player creates or joins a lobby, the “Scene_SteamworksLobby” scene will be loaded.
Once you are in a lobby You can ready up:
Once all players have readied up, the host can start the game.
After “Start Game” has been pressed, the “Game” scene called Scene_SteamworksGame loads, which for this project just allows player’s to send a “message” by updating a syncvar string variable that is displayed to everyone
Explaining (some) of the Code
The most important things in relation to Steamworks is happening in the SteamLobby.cs script.
At the beginning of the SteamLobby.cs, you will see a bunch of Callback variables created:
Callbacks are basically how Steamworks communicates to steam to get information on Lobbies and players. Those Callback variables are then initialized in the Start function:
Whats happening above is that Unity is being told what to do when the Callbacks receive data back from Steam.
OnLobbyCreated and Creating a Lobby
The lobbyCreated Callback will receive data back as a “LobbyCreated_t” callback. When it receives data, the game will call OnLobbyCreated.
The game will receive a LobbyCreated_t callback after it has requested to create a lobby. The code for OnLobbyCreated is shown below:
After the lobby has been created by steam, assuming you received back an “Result Ok” message, OnLobbyCreated will use
SteamMatchmaking.SetLobbyData to update some of the lobby metadata. Mainly it just updates the name of the lobby that was set by the player.
How is a Lobby created, though? Well, that’s first done in MainMenuManager.cs with its CreateNewLobby function. The two pieces of data you need to first create a lobby in Steam is the lobby type (public lobby vs. private) and the max number of players for the lobby. MainMenuManager.cs gets the lobby type by checking if the “Friends Only” option was checked or not.
After that, MainMenuManager.cs calls SteamLobby.cs’s CreateNewLobby and provides it the lobby type to create.
The max connections comes from “networkManager.maxConnections,” which is set on the NetworkManager object in the Unity Editor.
I have the value hardcoded to 69, because I am a child. You can change this to whatever you like for your game, or you can modify the project to let the player’s enter the max number of players themselves.
Joining a Lobby
When a lobby is created, Steam gives it a unique LobbyId value. To join a lobby, all you need to do is tell steam the LobbyId of the lobby you want to join. If that lobby is running a matching game you will then join it (assuming it isn’t private or anything else like that).
The game gets LobbyIds when it pulls down the list of Lobbies. SteamLobby.cs’s getListOfLobbies retrieves a list of lobbies using the
This will then trigger the Callback_lobbyList callback and run OnGetLobbiesList:
OnGetLobbiesList goes through each returned lobby and uses Steam’s
SteamMatchmaking.RequestLobbyData(lobbyID) to request for data fro each of the lobbies. That, in turn, triggers the Callback_lobbyInfo callback and executes OnGetLobbyInfo.
OnGetLobbyInfo provides MainMenuManager with the info its DisplayLobbies function needs to display the lobby data to the player.
DisplayLobbies creates a new UI object for each lobby and puts that UI object in a “Scroll Rect” object. This is what allows you to scroll through the lobbies. Part of those UI objects is the “Join This Lobby” button. When pressed, it executes JoinLobby in LobbyListItem.cs
Each LobbyListItem stores the LobbyId of the lobby it is displaying. So, LobbyListItem.cs gets that LobbyId and uses it to call JoinLobby in SteamLobby.cs.
SteamLobby.cs’s JoinLobby then uses
SteamMatchmaking.JoinLobby(lobbyId) to join the lobby with the provided LobbyId.
After that, you’re in a Steam lobby and start your game. To adapt this to your game, you can have the game start whatever your main game’s scene is after all players ready up instead of Scene_SteamworksGame. Or, you can skip the whole “readying up” in a lobby-lobby thing and have it load directly into your game instead of Scene_SteamworksLobby. Oh, and you will probably want to make the UI look good or something.
Hopefully, I can now adapt this to my CardConquest game to make it work through steam. Crossing my fingers Steamworks doesn’t break everything…
Smell ya later nerds!