owned this note
owned this note
Published
Linked with GitHub
# WebRTC architecture
This is for Snopek Games' WebRTC with Nakama demo.
NON EXHAUSTIVE, EDITS WELCOME
## Singletons
any script in the project can access all of these files
### Build
useless, only used by GitLab CI when building the game
### GameState
for now, just tells your if the game is in Local or Online mode
### Nakama
a singleton for communicating with a Nakama server - part of the Nakama client library from Heroic Labs. Don't change unless you need an uncommitted patch to the upstream library.
### OnlineMatch
sets up all of your webrtc stuff. Don't touch this either, unless you know what your doing.
(TODO: Go back and document how all of this works, theres a lot of important stuff in here)
(I haven't covered everything in here yet, but I want to.)
Exposes these variables:
- Config variables
- min_players, max_players
- client_version
- ice_servers
- This seems to contain data relating to STUN servers the project uses.
- Nakama variables
- nakama_socket -> an object representation of the persistent connection the client has to the server.
- More specifically, the socket is the "realtime client" which is a websocket connection to the Nakama server.
- The actual game is run over WebRTC, but we are using the websocket to set up the matchmaking and act as a signaling server for the WebRTC to run on.
- This is also where the in game chat is hosted
- my_session_id, match_id -> both string representation of id's
- matchmaker_ticker
- WebRTC variables (variables starting with underscores aren't meant to be accessed from outside this script, they are private)
- _webrtc_multiplayer
- _webrtc_peers -> Dictionary
- _webrtc_peers_connected -> Dictionary
- Player variables
- players -> Dictionary of players
Networking enums:
- MatchState{LOBBY, MATCHING, CONNECTING, WAITING_FOR_ENOUGH_PLAYERS, READY, PLAYING}
- MatchMode{NONE, CREATE, JOIN, MATCHMAKER}
- PlayerStatus{CONNECTING, CONNECTED}
- MatchOpCode{WEBRTC_PEER_METHOD, JOIN_SUCCESS, JOIN_ERROR}
Exposes these signals:
- Error Signals
- error (takes in a "message")
- disconnected()
- Match Signals
- match_created(takes in a "match_id")
- match_joined(takes in a "match_id")
- matchmaker_matched(takes in "players", a dictionary of Player objects)
- Player Signals
- player_joined(takes in a Player object)
- player_left(takes in a Player object)
- player_status_changed(takes in a Player object, and also a "status object")
- Ready Up Signals
- match_ready(takes in "players", a dictionary of Player objects)
- match_not_ready()
#### Player
SUPER IMPORTANT: Online Match has its own class called Player that is used for many of the signals seen above. It's purpose is to connect a Nakama user to a Godot peer, and includes within it a lot of important information.
Variables:
- session_id -> String: how Nakama identifies the current player session
- peer_id -> int: how Godot identifies the peer in its High-level Multiplayer API
- username -> String
Methods:
- _init(_session_id: String, _username: String, _peer_id: int)
- A constuctor for the class
- static from_presence(presence: NakamaRTAPI.UserPresence, _peer_id: int)
- Returns a Player object built from Nakama presence data
- static func from_dict(data: Dictionary)
- converts a Player from it's Dictionary representation.
- useful for deserialization.
- to_dict()
- converts a Player to it's Dictionary representation. You can turn it back with the function above. Useful for serialzation.
### Online
Boilerplate networking stuff.
Exposes these variables:
- nakama_client -> the "request/response" client thats connecting to the server.
- It makes HTTP requests to the server (any one time thing that doesn't require a constant connections)
- For example, authentication requests.
- nakama_session -> an object representation of a successful authentication attempt to the server.
- nakama_socket -> see the explanation in OnlineMatch
Implements these functions:
- get_nakama_client
- if it doesn't exist, it creates a client object to connect to the Nakama server, and sets nakama_client to it.
- set_nakama_session:
- sets nakama_session to the new session passed in. Closes out the old session for you
- Emits signal "session_changed" if changed to any session.
- However, emits "session_connected" only if the session is valid.
- connect_nakama_socket
- if the current socket isn't null or currently connecting, then create a new socket, and set the current nakama_socket to it.
- emits "socket_connected"
## In-game Networking Scripts
These are the scripts that, while not being tied to the actual gameplay, help set up with the networking stuff and keeps it running.
Important note: There are a bunch of functions that are "remotesync" functions. What this means is that it can be called as a normal function, but it can also be called by remote clients on the local client. (Ex. Client B can call player_ready in an rpc call and say they want it called on Client A). When you rpc a remotesync function, you call it on both the local AND remote clients.
### Game
Sets up the players and does match/player life cycle stuff
Exposes these variables:
- Player - just a reference to the (actual in game) Player
- map_scene - just a packed scene
- map - a reference to the actual map that the match will take place in
- camera - just the in game camera
- original_camera_position - camera will probably track the players, so its best to store the original location for easy setup between each rounds
- game_started, game_over, both booleans, pretty self-explanatory
- players_alive - a dictionary of all the players left alive in the round.
- players_setup - a dictionary marking which players have reported back that they have finished setting up the game
Exposes these signals for callbacks:
- game_started()
- player_dead(player_id)
- game_over(player_id)
Implements these functions:
- game_start(players: Dictionary)
- If its an online match, an rpc is set out to call _do_game_setup(players) on ALL CLIENTS, both local and remote
- else, just _do_game_setup on the local client
- _do_game_setup(players: Dictionary)
- This is a remotesync function
- We first pause the game to set everything up
- If game_started is true, stop the game since we need to set everything up again
- After that, we set game_started to true, game over is false, and players_alive = players since we're starting with a clean slate and all players should be alive
- We then reload the map.
- After that, we spawn in all of the (in game) player nodes, and connect them to their peer representations. We take care to make sure they each spawn in the correct location on the map. We also make sure that the function _on_player_dead(player_id) is connected to the signal player dead for each player object.
- By the way, if its not an online match, we set up multiple local inputs for each local plyaer
- If it is an online match, we spawn in the player node that is the local client last, as that is a special case, since we need to set up our inputs to be directed towards it as well as the local client's peer id.
- After all that, we either do a normal game start(_do_game_start()) if its an offline match, or we call _finished_game_setup on the lobby host.
- _finished_game_setup(player_id: int)
- This is a mastersync function, which means this is ONLY called when the client is the host of the lobby.
- Since this client is the host, it gets the confermation that each player that's alive is setup.
- If all the players that are alive are set up, then the host calls _do_game_start() on every client including itself.
- _do_game_start()
- This is a remotesync function
- First, it starts up the map.
- Then, it sends out the signal "game_started", which will go be called in Main.gd as _on_Game_game_started()
- It also unpauses the game
- game_stop()
- Stop the map being played, and set game_started to false.
- Clear both players_setup and players_alive
- It also removes all of the current player peer representations from the scene.
- reload_map()
- Remove the map
- create a new instance of the map, in the same place as the old map in the scene tree
- Reset the camera to its original position in the map.
- kill_player(player_id)
- get the player_node related to player_id
- If it exists, call the die method on it.
- If it doesn't have that, just queue free it, and call _on_player_dead(player_id)
- _on_player_dead(player_id)
- emit "player_dead" with player_id
- erase the dead player from players_alive
- if there's no players left alive, game_over = true and emit_signal "game_over"
### Main
Gets you connected to Nakama and also sets up the WebRTC connections
(We will ignore the UI stuff as it's not really relevant and its pretty self-explanatory.)
Before we dive into this code, there are a few functions that use "get_tree().is_network_server()", so lets quickly explain what that is.
- In each lobby, there should be one peer/player who is considered the "host" - this is the player with a peer id of 1. In private matches, this is the player who originally created the match - in public matches created through the matchmaker it's chosen pseudo-randomly. Besides the other players/peers they have the extra responsibility of keeping track of certain things, including players scores and starting and ending the game.
Exposes these variables:
- game -> an object representation of the Game script (as seen above)
- players -> the list of all players currently in the server
- players_ready -> the list of players ready to actually start the match. Represented as a bunch of session_id (succesful authentication) tokens.
- players_score -> dictionary of the players current scores. This is only populated on the host
We'll skip over most of the UI related functions, but there's one important one:
_on_ReadyScreen_ready_pressed() -> void:
- This sends a callback to all of the remote clients player_ready function (defined down below) and passes in the session id from OnlineMatch
### OnlineMatch callback functions
- _on_OnlineMatch_error(message: String)
- Connected to OnlineMatch "error" signal, just prints out an error on screen
- _on_OnlineMatch_disconnected()
- Connected to OnlineMatch "disconnected" signal, prints out a disconnected message
- _on_OnlineMatch_player_left(player)
- Connected to OnlineMatch "player_left" signal, takes in a (OnlineMatch) Player object
- print out a message showing the player left
- Removes the Player from the variable "game" (reference to Game.gd)
- Deletes it from both players and players_ready
- _on_OnlineMatch_player_status_changed(player, status)
- Takes in an OnlineMatch Player object and status
- if the status has changed so that the player is now connected
- First check to see if we are the "host" for this lobby, which gives us permission to start the game.
- If so, then we will loop through all of the current players in players_ready.
- We will call player_ready() on each of them, sending in the peer_id of the player that has sucessfully connected.
### Gameplay methods and callbacks
- player_ready(session_id: String)
- Takes in a session_id, representing the game client that has signaled that its ready.
- This is a "remotesync" function, meaning that it can be called as a normal function, but it can also be called by remote clients on the local client. (Ex. Client B can call player_ready in an rpc call and say they want it called on Client A)
- We first print a message showing that we're ready
- Then, we make sure that we are the "host" for this lobby and that the session_id being passed in isnt the local client.
- Then, in players_ready, we set that the player with the session_id is ready (true)
- We also do another check to see if players_ready is equal to the amount of players currently in the lobby.
- If it is, then we se the match_state to playing if it isn't already, and we start the game (start_game()).
- start_game()
- If its an online game, it first sets players to the list of players stored in OnlineMatch, else just set it to some predefined defaults
- Then, start the game (by calling game.game_start(players))
- stop_game()
- Leave the online match (OnlineMatch.leave())
- Then, clear players, players_ready, and players_score
- Finally, stop the game (game.game_stop())
- restart_game()
- Just calls stop_game() and then start_game()
The following are callbacks from Game to Main, but they are all declared in the scene (Main.tscn)
- _on_Game_game_started()
- callback from game_started()
- sets up the game for playing
- _on_Game_player_dead(player_id: int)
- callback from player_dead(player_id)
- Check to see if the game is online.
- If so, get the id of the local player client (get_tree().get_network_unique_id()) and see if it's the same of the player_id passed into the function.
- If it is, print a message saying that you're dead, you lost or something to that effect.
- _on_Game_game_over(player_id: int)
- callback from game_over(player_id)
- Called on game over, player_id is the id of the player who won.
- First, clear out everyone in players_ready
- First check to see if this is an online game. If it is, just call show_winner, and pass in the player associated with the player_id
- Else, first we check to see if we are the "host" of this lobby.
- If we are, first we either set the player score of that player to 1 if it didn't exist yet, else we add it by one.
- after that, then we just rpc show_winner, passing in is_match.
- is_match being a boolean that checks if the game has reached match point.
(TODO: Document this function more. It's late and I'm tired)
- show_winner(name: String, session_id: String = '', score: int = 0, is_match: bool = false)
- name is the name of the plauyer, and is_match is a boolean that checks if the game has reached match point.
- This is a remotesync function
-
## Gameplay scripts w/ Networking
### Map
Constants:
- TILE_SIZE - tile size
Exposes these functions:
- map_start()
- Things you want to happen when the map starts. Not used in this game, but in Retro Tank Party this starts the powerup spawn timers, or in Fish Game this spawns a weapon on all the weapon spawners.
- map_stop()
- Things you want to happen when the map stops. Mostly, just stopping the things started in `map_start()`.
- get_map_rect()
- just returns the size of the map.
### Player
Exposes thses variables:
- player_name_label - reference to the UI label
- hitbox - Hitbox reference
- animation_player - animation player reference
- player_controlled - boolean saying if this player node is the one being controlled by local inputs
- input_prefix - tells which port the inputs are coming from.
- speed - speed of the player
Has these signals:
- player_dead() - called when the player dies.
Implements these functions:
- set_player_name(player_name: String)
- set's the player label text to the player name
- attack()
- hurts anyone who overlaps the hitbox except for the player node itself
- hurt()
- This is a very simple game, so the player just dies.
- If its online, its a normal die, else die gets rpc to every single client.
- die()
- This is a remotesync function
- the player node gets queue_free() and a "player_dead" signal is emitted.
- _physics_process(delta: float)
- if the local client is controlling the player node
- Get the input vector, and move the player node with move and slide with the strength of speed
- check to see if the player pressed attack and that an attacking animation is not being played. If so, play an attack animation
- if its online, do an rpc call for update_remote_player with the new game state values
- update_remote_player(_position: Vector2, is_attacking: bool)
- updates the gamestate
- This is marked as `puppet` because you only want it run on peers that aren't the master of this player (as set in `player.set_network_master()`)
- Extremely naive position and animation sync'ing.
- This will work locally, and under ideal network conditions, but likely won't be acceptable over the live internet for a large percentage of users.
- You'll need to replace this with more efficient sync'ing mechanism, which could include input prediction, rollback, limiting how often sync'ing happens or any other number of techniques.
- In addition to that, you'll also need to expand the number of things that are sync'd, depending on the needs of your game.