The pun was there, it had to be made. As you might've guessed, we now have Steam integrated with Live/Wire (oh yeah, we renamed the game too). We've also gotten approval from the Steamworks program, meaning we have our very own Steam store page and app ID!
What does that mean for me? Well, it means I have a lot of testing to do. While we have all of our mechanics and systems networked, we were only able to test locally over LAN, or via dedicated server for online play. To use Steam, however, we cannot use dedicated servers, as they must be registered with Steam, and we can't afford server hosting. This means we have to use listen servers and Steam's P2P relays.
Steam's P2P system is basically a subnet with its very own addressing system, allowing players to directly connect to another player through a relay server. For us, we just have to worry about programming code paths that differentiate listen servers, dedicated servers, and clients in our game code. On the back-end, the OnlineSubsystemSteam plugin by Epic Games handles session creation, joining, and advertisement on Steam's servers, while we also use the Advanced Sessions plugin for blueprint access to the subsystem's API. With these plugins combined, we didn't actually have to make many changes to our existing codebase, other than account for a few extra settings related to sessions. But the bugs... those were the major issue.
Public Enemy #1 for all networked games that must support listen servers is making sure the listen server does not inadvertently control or use another client's information for its own UI and game functionality. Since a listen server functions as both a server and client, we need to pay very close attention to how we initialize controllers & characters, since the host will run initialization for every single client and may mix up which controller is associated with which character. Here's an example from the project:
This image shows the initialization of a player character, necessary for various HUD updates and assignments. This works entirely fine on clients and a dedicated server, since it is client initialization, and a dedicated server has no local client. A listen server, however, cannot use this initialization code because of the "get owner" node, then the cast to BreakerPlayerController. On a listen server, this code will always return the wrong player controller for the specified character, meaning it will use another client's character information for it's own local client. To fix it, I figured out this method:
You'll notice the "Is Server" macro is the major difference. This simple macro allows us to control execution flow based on whether this object is a listen server, dedicated server, or client (not a server). Then, I wrote a small function that will always return the local player controller, whether it is on a client or listen server. This allows me to then set a simple flag in the player controller class indicating if the listen server's local client has already been initialized. This prevents the listen server from attempting to initialize multiple characters using it's own controller, while clients still initialize using their controller.
We'll have to use this sort of pattern everywhere to ensure the host has the same experience as a client who is connected. Another small issue we've encountered is that grappling has a different physical feel between clients and a listen server host. We can replicate this pattern to fix that when initializing the grappling component.
댓글