Greetings, I’m Chase Hutchens the Network Game Engineer developing in Blunderbuss on our game Chrono-Drive.
I’m going to go over a high level of the network infrastructure that has been in the works. I hope someone out there will find it interesting.
When I was first beginning this adventure I pondered back on my experiences using dedicated server applications for various games. Specifically how I used dedicated server applications for hosting on my local machine for both Network and LAN play. Such as within the early Medal of Honor series, Call of Duty, various Steam games, Minecraft and others.
The essential first piece of our own networked system revolves around our dedicated server application and game server creation process. Upon the dedicated server launching, this game server communicates with our master server about it’s existence to allow for other players to be able to connect to this particular host.
From the other player’s perspective they will choose to join our game server that is in progress. For them to join, their client also communicates with our master server which tells this client which game servers are alive. Allowing the player to choose the game server they are wanting to play on.
After our host has been established, any connected client will be able to create a game if they want to. When we request to the game server that we are creating a game we send a message that looks like this:
1 |
:<gameMsg>!<createMultiplayerGameMsg> <gameID>:<gameName>:<gameMap>:<maxPlayers>:<ownerUserName> |
Followed by a response from the game server that the game has been created, with our player object’s associated netID that he will utilize:
1 |
:<gameMsg>!<multiplayerGameCreatedMsg> <netObjId> |
Once our main host has loaded into their game space, now they need to tell the game server about the network game objects that exist within this particular level. For each networked object we send a corresponding message for being able to store this information on the game server:
1 |
:<gameMsg>!<netGameObjectMsg> <netId>:<archeTypeName>:<transformInfo>:<specificNetObjInfo> |
If a player chooses to join a game, a message is first sent to the server on a refresh that requests the games that are in session:
1 |
:<gameMsg>!<requestMultiplayerGamesMsg> |
The response received from the game server about the game information & games that exist looks similar to the create game message we saw above:
1 2 |
:<gameMsg>!<multiplayerGamesInfoMsg> <totalGames>:<totalPlayers>: :<gameMsg>!<requestedMultiplayerGamesMsg> <gameId0>:<gameName0>:<mapName0>:<maxPlayers0>:<currentPlayers0>|...|<gameIdN>:<gameNameN>:<mapNameN>:<maxPlayersN>:<currentPlayerN>| |
After the player knows what games exist and chooses to join a game we send a message to the game server requesting to join that game:
1 |
:<gameMsg>!<joinMultiplayerGameMsg> <username>:<netGameId> |
Followed by either a failed to join or successfully joined message with the player object’s associated netID that it’ll utilize, from the game server:
1 2 |
:<gameMsg>!<joiningMultiplayerGameMsg> <netObjId> :<gameMsg>!<failedToJoinMultiplayerGameMsg> |
At the time of this occurring the players that are within the game that is being joined are presented with a message telling them about the new player that is joining:
1 |
:<gameMsg>!<playerJoinedGameMsg> <username>:<netObjId> |
After the player has loaded into the game they are joining game’s space, they also need to be told about the existence of the various network objects that exist within the present state of this game world. This new player lets the game server know they are ready to receive the information about the network game objects that exist:
1 |
:<gameMsg>!<requestInitGameObjectsMsg> |
Whether other players, enemies, switches, level transitions or anything else, the game server then sends this player the information about the network game objects that currently exist in this particular game:
1 2 3 4 5 |
:<gameMsg>!<activePlayerInGameMsg> <activeUser>:<userNetId>:<x|y|z|rx|ry|rz|s|>: :<gameMsg>!<activeNpcInGameMsg> <netId>|<objectNameInLevel>|<x0|y0|z0|rx0|ry0|rz0|s0>| :<gameMsg>!<activeSwitchInGameMsg> <netObjId0>|<objectNameInLevel0>|<activated0>| :<gameMsg>!<activeLevelTransitionInGameMsg> <netObjId0>|<objectNameInLevel0>|<x0|y0|z0|rx0|ry0|rz0|s0>| :<gameMsg>!<activeItemInGameMsg> <netObjId0>|<archeTypeName0>|<x0|y0|z0|rx0|ry0|rz0|s0>|<specificNetObjInfo0>| |
From here, we are greeted by our tried & true Boxman, the manifestation of the other player.
One of the most influential aspects of this system has been the utilization of sending & receiving relay messages between the networked objects across multiple clients. Doing so has allowed for being able to toggle network events within our game scripts to relay over the network to the other corresponding client’s perspective. This has also allowed for a significant optimization in particular situations for executing client side code to execute behavior instead of multiple constant messages.
Such as how we’re able to inject this SmartObjectData network event into our player’s normal shoot functionality to allow for it’s corresponding Boxman – NetPlayer object, to appear to be shooting for the other client:
1 2 3 4 5 6 7 8 9 10 11 |
function PlayerShoot:OnMouseDown(inputevent) -- ... if CompareMouseButton(inputevent.mouseEvent.button, "LEFT") then if (NetworkEventHelpers.GetActiveNetworkGameManager():IsActiveGame() and not self.inNetFireState) then self.comp:AttachSmartObjectEvent("SmartObjectDataEvent").smartMsgId = "NetPlayerBeginFire" self.comp:AttachSmartObjectEvent("SmartObjectDataEvent").objSrc = self.comp:GetParent() self.comp:DispatchEvent("SmartObjectDataEvent", NetworkEventHelpers.GetNetworkManager()) self.inNetFireState = true end -- ... end |
Similarly we are able to just send a net stop firing relay into the player’s stop shooting functionality:
1 2 3 4 5 6 7 8 9 10 11 |
function PlayerShoot:OnMouseUp(inputevent) -- ... if CompareMouseButton(inputevent.mouseEvent.button, "LEFT") then if (NetworkEventHelpers.GetActiveNetworkGameManager():IsActiveGame() and self.inNetFireState) then self.comp:AttachSmartObjectEvent("SmartObjectDataEvent").smartMsgId = "NetPlayerEndFire" self.comp:AttachSmartObjectEvent("SmartObjectDataEvent").objSrc = self.comp:GetParent() self.comp:DispatchEvent("SmartObjectDataEvent", NetworkEventHelpers.GetNetworkManager()) self.inNetFireState = false end -- ... end |
Within our NetPlayer object there is a NetPlayerShoot script that contains a SetInitData network event that listens for the various relays that were sent from it’s true client representation, in this case we have it setup so “UpdateFireState” corresponds to either the “NetPlayerBeginFire” & “NetPlayerEndFire”:
1 2 3 4 5 6 7 8 9 10 11 12 |
function NetPlayerShoot:OnSetInitDataEvent(initdata) if (initdata.identifier == "UpdateFireState") then -- intVal0 represents whether we are stopping or starting firing if (initdata.intVal0 == 0) then self.ContinuousFire = false self:EndFire() else self.ContinuousFire = true self:StartFire() end -- ... end |
However, there are still prerequisites needed to setup our relays on our C++ side for the particular network game object. This is how we are connecting together the associated message relays:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// Relay Establishment void NetPlayerObject::InitializeRelays() { NetObjRelayMap& relays = NetGameObjectManager::GetInstance().GetNetObjectRelays(); NetObjectRelayHandler playerFireRelay = NetObjectRelayHandler( NetworkMessages::PLAYERFIRESTATE, "UpdateFireState", "Gun"); playerFireRelay.relayActivators.push_back("NetPlayerBeginFire"); playerFireRelay.relayActivators.push_back("NetPlayerEndFire"); relays[NetObjModule::PLAYER].push_back(playerFireRelay); NetObjectRelayHandler playerGunModeRelay = NetObjectRelayHandler( NetworkMessages::PLAYERGUNMODE, "UpdateGunMode", "Gun"); playerGunModeRelay.relayActivators.push_back("NetPlayerChangeMode"); relays[NetObjModule::PLAYER].push_back(playerGunModeRelay); NetObjectRelayHandler playerBlinkStateRelay = NetObjectRelayHandler( NetworkMessages::PLAYERBLINKSTATE, "UpdateBlinkState", "Arm"); playerBlinkStateRelay.relayActivators.push_back("NetPlayerBeginBlink"); playerBlinkStateRelay.relayActivators.push_back("NetPlayerEndBlink"); relays[NetObjModule::PLAYER].push_back(playerBlinkStateRelay); } |
Upon the relay being triggered to execute, an associated message will be sent for relaying to all other clients except the client that sent the original message, that is in the form:
1 |
:<gameMsg>!<specificObjMsg> !<relayMsgId> <netObjId>:<particularRelayData>: |
Once we receive our relay network message from our game server from the network object that triggered the relay, we also have to make sure to create the data and link the pieces together:
1 2 3 4 5 6 7 8 9 |
// Creates the corresponding relay update data that is used for dispatching the relay to the particular object void NetPlayerObject::CreateReceivedUpdateRelay(const NetObjectRelayHandler& relayHandler, const std::vector<std::string>& msgData) { // msgData[0] => NetID // msgData[1] => Relay update state value int updateState = std::atoi(msgData[1].c_str()); NetGameObjectManager::GetInstance().AddNetGameObjectToUpdate(std::make_unique<NetPlayerUpdateInfo>(this, msgData[0], relayHandler.updateMsg, updateState)); return; } |
Which eventually the particular relay is determined in need of update, dispatching it’s SetInitData event:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// Grabs the corresponding data from the relayHander and dispatches through the relayData NetBaseObject void NetPlayerObject::AssociateDispatchData(const NetObjectUpdateInfo& relayData, const NetObjectRelayHandler& relayHandler) { const NetPlayerUpdateInfo& playerData = static_cast<const NetPlayerUpdateInfo&>(relayData); if (relayData.identifier == "UpdateFireState" || relayData.identifier == "UpdateGunMode" || relayData.identifier == "UpdateBlinkState") { GameObject* updatePlayer = NetGameObjectManager::GetInstance().GetGameObjectFromNetID(netCompRef.GetNetObjID()); relayData.netBaseObject->AttachData<SetInitDataEvent>(EventList::SetInitData)->identifier = relayHandler.updateMsg; relayData.netBaseObject->AttachData<SetInitDataEvent>(EventList::SetInitData)->intVal0 = playerData.valueState; // right now we're assuming we only send to a child of the updatePlayer relayData.netBaseObject->DispatchEvent(EventList::SetInitData, GOM::GetGameObject(updatePlayer->GetChildByName(relayHandler.objDispatchName))); } } |
The NetPlayerObject definitely being one of the simpler relay setups due to only utilizing a single integer for it’s relay initialization.
Overall, I’m still in the process of generalizing this further, ideally to make it so the relays can be linked within our game scripts and then dispatched accordingly without having to recompile our code-base. Even then, I’m pleased with the current state of the relay system in unison with the network game objects. Being able to execute an array of client side code in-favor of not having to send multiple network messages to do the same thing is fantastic.