diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 10d48e7fd..48aaf12b7 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,113 +1,283 @@ -# Architecture +# Boss Room architecture overview + This document describes the high-level architecture of Boss Room. + If you want to familiarize yourself with the code base, you are just in the right place! -Boss Room is an 8-player co-op RPG game experience, where players collaborate to take down some minions, and then a boss. Players can select between classes that each have skills with didactically interesting networking characteristics. Control model is click-to-move, with skills triggered by mouse button or hotkey. +Boss Room is an 8-player co-op RPG game experience, where players collaborate to fight some imps, and then a boss. Players can select between classes that each have skills with didactically interesting networking characteristics. The control model is click-to-move, with skills triggered by a mouse button or hotkey. + +- [Assembly structure](#assembly-structure) +- [Application flow](#application-flow) +- [Game state and scene flow](#game-state-and-scene-flow) +- [Application Flow Diagram](#application-flow-diagram) +- [Transports](#transports) +- [Connection flow state machine](#connection-flow-state-machine) +- [Session management and reconnection](#session-management-and-reconnection) +- [UGS Services integration - Lobby and Relay](#ugs-services-integration---lobby-and-relay) +- [Core gameplay structure](#core-gameplay-structure) +- [Characters](#characters) +- [Game data setup](#game-data-setup) +- [Action System](#action-system) + - [Movement action flow](#movement-action-flow) +- [Navigation system](#navigation-system) + - [Building a navigation mesh](#building-a-navigation-mesh) +- [Important architectural patterns and decisions](#important-architectural-patterns-and-decisions) +- [Dependency Injection](#dependency-injection) +- [Client/Server code separation](#clientserver-code-separation) +- [Publisher-Subscriber Messaging](#publisher-subscriber-messaging) + - [NetworkedMessageChannel](#networkedmessagechannel) + +## Assembly structure + +In Boss Room, code is organized into a multitude of domain-based assemblies. Each assembly serves a relatively self-contained purpose. + +An exception to this guideline is Gameplay assembly, which houses most of our networked gameplay logic and other functionality that is tightly coupled to the gameplay logic. + +This assembly separation style forces us to better separate concerns and serves as one of the ways to keep the code-base organized. It also provides more granular recompilation during our iterations, which saves us some time we would've spent looking at the progress bar. + +![boss room assemblies](Documentation/Images/BossRoomAssemblies.png "Boss Room Assemblies") + +## Application flow + +Boss Room assumes that the `Startup` scene is loaded first. -Code is organized into three separate assemblies: `Client`, `Server` and `Shared` (which, as its name implies, contains shared functionality that both client and the server require). +> __An interesting trick__: +> +> We have an editor tool that enforces start from that scene even if we're working in some other scene. This tool can be disabled via an editor Menu: `Boss Room > Don't Load Bootsrap Scene On Play` and vice-versa via `Boss Room > Load Bootsrap Scene On Play`. -## Host model -Boss Room uses a Host model for its server. This means one client acts as a server and hosts the other clients. +The `ApplicationController` component lives on a GameObject in the Startup scene and serves as both the entry point and composition root of the application. Here, we bind dependencies that should exist throughout the lifetime of the application - the core DI-managed “singletons” of our game. See [Dependency Injection](#dependency-injection) section for more information. -A common pitfall of this pattern is writing the game in such a way that it is virtually impossible to adapt to a dedicated server model. +## Game state and scene flow -We attempted to combat this by using a compositional model for our client and server logic (rather than having it all combined in single modules): - - On the Host, each GameObject has `{Server, Shared, Client}` components. - - If you start up the game as a dedicated server, the client components will disable themselves, leaving you with `{Server, Shared}` components. - - If you start up as a client, you get the complementary set of `{Shared, Client}` components. +After the initial bootstrap logic is complete, the ApplicationController loads the `MainMenu` scene. -This approach works, but requires some care: - - If you have server and clients of a shared base class, you need to remember that the shared code will run twice on the host. - - You also need to take care about code executing in `Start` and `Awake`: if this code runs contemporaneously with the `NetworkManager`'s initialization, it may not know yet whether the player is a host or client. - - We judged this extra complexity worth it, as it provides a clear road-map to supporting true dedicated servers. - - Client-server separation also allows not having god-classes where both client and server code are intermingled. This way, when reading server code, you do not have to mentally skip client code and vice versa. This helps making bigger classes more readable and maintainable. Please note that this pattern can be applied on a case by case basis. If your class never grows too big, having a single `NetworkBehaviour` is perfectly fine. +Each scene has its own entry point component sitting on a root-level game object. It serves as a scene-specific composition root. -## Connection flow -The Boss Room network connection flow is owned by the `GameNetPortal`: - - The Host will invoke either `GameNetPortal.StartHost` or `StartUnityRelayHost` if Unity Relay is being used. - - The client will invoke either `ClientGameNetPortal.StartClient` or `StartClientUnityRelayModeAsync`. - - Boss Room's own connection validation logic is performed in `ServerGameNetPortal.ApprovalCheck`, which is plugged in to the `NetworkManager`'s connection approval callback. Here, some basic information about the connection is recorded (including a GUID, to facilitate future reconnect logic), and success or failure is returned. In the future, additional game-level failures will be detected and returned (such as a `ServerFull` scenario). +The MainMenu scene only has the `MainMenuClientState`, whereas scenes that contain networked logic also have the `server` counterparts to the client scenes. In this case, both exist on the same game object. -## Data model -Game data in Boss Room is defined in `ScriptableObjects`. The `ScriptableObjects` are organized by enum and made available in a singleton class: the `GameDataSource`, in particular `ActionDescription` and `CharacterData`. `Actions` represent discrete verbs (like swinging a weapon, or reviving someone), and are substantially data driven. Characters represent both the different player classes, and also monsters, and represent basic details like health, as well as what "Skill" Actions are available to each Character. +As soon as we get into the CharSelect scene - either by joining or hosting a game - our NetworkManager instance is running. The host drives game state transitions and also controls the set of scenes that are currently loaded in the game. This indirectly forces all of the clients to load the same set of scenes as the server they are connected to (via Netcode's networked scene management). + +### Application Flow Diagram + +![boss room scene flow](Documentation/Images/BossRoomSceneFlowDiagram.png "Boss Room Scene Flow") + +> __Note__: +> +> The main room is split into four scenes. The primary scene (BossRoom's root scene) contains the state components, game logic, level navmesh and trigger areas that let the server know to load a given subscene. Each subscene is then loaded additively using those triggers. +> +> Subscenes contain spawn points for the enemies and visual assets for their respective segment of the level. The server unloads subscenes that don't contain any active players and then loads the subscenes that are needed based on the position of the players. If at least one player overlaps with the subscene's trigger area, the subscene is loaded. ## Transports -Currently two network transport mechanisms are supported: -- IP based -- Unity Relay Based -In the first, the clients connect directly to a host via IP address. This will only work if both are in the same local area network or if the host forwards ports. +Currently two network transport mechanisms are supported: + +- IP + +- Unity Relay -For Unity Relay based multiplayer sessions, some setup is required. Please see our guide [here](Documentation/Unity-Relay/README.md). +When using IP, clients connect directly to a host via an IP address. This will only work if both client and host are in the same local area network or if the host forward ports. + +For Unity Relay-based multiplayer sessions, some setup is required. Please see our guide [here](Documentation/Unity-Relay/README.md). Please see [Multiplayer over internet](README.md) section of our Readme for more information on using either one. -The transport is set in the transport field in the `NetworkManager`. We are using the following transport: -- **Unity Transport Package (UTP):** Unity Transport Package is a network transport layer, packaged with network simulation tools which are useful for spotting networking issues early during development. This protocol is initialized to use direct IP to connect, but is configured at runtime to use Unity Relay if starting a game as a host using the Lobby Service, or joining a Lobby as a client. Unity Relay is a relay service provided by Unity services, supported by Unity Transport. See the documentation on [Unity Transport Package](https://docs-multiplayer.unity3d.com/docs/transport-utp/about-transport-utp/#unity-transport-package-utp) and on [Unity Relay](https://docs-multiplayer.unity3d.com/docs/relay/relay). +The transport is set in the transport field in the `NetworkManager`. We are using __Unity Transport Package (UTP)__. -To add new transports in the project, parts of `GameNetPortal` and `ClientGameNetPortal` (transport switches) need to be extended. +The Unity Transport Package is a network transport layer, packaged with network simulation tools which are useful for spotting networking issues early during development. This protocol is initialized to use direct IP to connect, but is configured at runtime to use Unity Relay if starting a game as a host using the Lobby Service, or joining a Lobby as a client. -## Game state / Scene flow -In Boss Room, scenes correspond to top-level Game States (see `GameStateBehaviour` class) in a 1:1 way. That is, there is a `MainMenu` scene, `Character Select` scene (and state), and so on. +Unity Relay is provided by Unity Gaming Services (UGS) and is supported by Unity Transport. For more information, see our documentation on [Unity Transport Package](https://docs-multiplayer.unity3d.com/docs/transport-utp/about-transport-utp/#unity-transport-package-utp) and [Unity Relay](https://docs-multiplayer.unity3d.com/docs/relay/relay). -Because it is currently challenging to have a client be in a different scene than the server it's connected to, the options for Netcode developers are either to not use scenes at all, or to use scenes, and let game state transitions on the host drive game state transitions on the client indirectly by forcing client scene transitions through Netcode's networked scene management. -We chose the latter approach. +## Connection flow state machine -Each scene has exactly one `GameStateBehaviour` (a specialization of `Netcode.NetworkBehaviour`), that is responsible for running the global state logic for that scene. States are transitioned by triggered scene transitions. +The Boss Room network connection flow is owned by the `ConnectionManager`, which is a simple state machine. It receives inputs from Netcode or from the user, and handles them according to its current state. Each state inherits from the ConnectionState abstract class. The following diagram shows how each state transitions to the others based on outside inputs. +If you were to add a new transport, the `StartingHostState` and `ClientConnectingState` states would need to be extended. Both of these classes assume that you are using UTP. -## Important classes +![boss room connection manager state machine](Documentation/Images/BossRoomConnectionManager.png "connection manager state machine") -**Shared** - - `NetworkCharacterState` contains NetworkVariables that store the state of any given character, and both server and client RPC endpoints. The RPC endpoints only read out the call parameters and then raise events from them; they don’t do any logic internally. +## Session management and reconnection -**Server** - - `ServerCharacterMovement` manages the movement Finite State Machine (FSM) on the server. Updates the NetworkVariables that synchronize position, rotation and movement speed of the entity on its FixedUpdate. - - `ServerCharacter` has the `AIBrain`, as well as the ActionQueue. Receives action requests (either from the AIBrain in case of NPCs, or user input in case of player characters), and executes them. - - `AIBrain` contains main AI FSM. - - `Action` is the abstract base class for all server actions - - `MeleeAction`, `AoeAction`, etc. contain logic for their respective action types. +In order to allow users to reconnect to the game and restore their game state, we store a map of the GUIDs for their respective data. This way we ensure that when a player disconnects, data is accurately assigned back to that player when they reconnect. -**Client** - - `ClientCharacterVisualization` primarily is a host for the running `ActionFX` class. - - `ClientInputSender `. On a shadow entity, will self-destruct. Listens to inputs, interprets them, and then calls appropriate RPCs on the RPCStateComponent. - - `ActionFX` is the abstract base class for all the client-side action visualizers - - `MeleeActionFX`, `AoeActionFX`, etc. Contain graphics information for their respective action types. - -## Movement action flow - - Client clicks mouse on target destination. - - Client->server RPC, containing target destination. - - Anticipatory animation plays immediately on client. - - Server performs pathfinding. - - Once pathfinding is finished, server representation of entity starts updating its NetworkVariables at the same cadence as FixedUpdate. - - Visuals GameObject never outpaces the simulation GameObject, and so is always slightly behind and interpolating towards the networked position and rotation. +For more information check out the page on [Session Management](https://docs-multiplayer.unity3d.com/netcode/current/advanced-topics/session-management/index.html) in our NGO documentation. -## Navigation System -Each scene which uses navigation or dynamic navigation objects should have a `NavigationSystem` component on a scene GameObject. That object also needs to have the `NavigationSystem` tag. +## UGS Services integration - Lobby and Relay + +Boss Room is a multiplayer experience designed to be playable over the internet. To effectively support this, we have integrated a number of [Unity Gaming Services](https://unity.com/solutions/gaming-services). Authentication, Lobby, and Relay allow players to easily host and join games, without the need for port forwarding or out-of-game coordination. + +You can learn more about the classes associated with our UGS wrappers and integration below: -### Building a navigation mesh -The project is using `NavMeshComponents`. This means direct building from the Navigation window will not give the desired results. Instead find a `NavMeshComponent` in the given scene e.g. a **NavMeshSurface** and use the **Bake** button of that script. Also make sure that there is always only one navmesh file per scene. Navmesh files are stored in a folder with the same name as the corresponding scene. You can recognize them based on their icon in the editor. They follow the naming pattern "NavMesh-\" +To maintain a single source of truth for service access - and avoid scattering of service access logic - we've wrapped UGS SDK access into Facades and used UI mediators to contain the service logic triggered by UIs. These are called in multiple places throughout our code base. -### Dynamic Navigation Objects -A dynamic navigation object is an object which affects the state of the navigation mesh such as a door which can be opened or closed. -To create a dynamic navigation object add a NavMeshObstacle to it and configure the shape (in most cases this should just match the corresponding collider). Then add a DynamicNavObstacle component to it. +- [AuthenticationServiceFacade.cs](Assets/Scripts/UnityServices/Auth/AuthenticationServiceFacade.cs) +- [LobbyServiceFacade.cs](Assets/Scripts/UnityServices/Lobby/LobbyServiceFacade.cs) +- Lobby and relay - client join - JoinLobbyRequest() in [Assets/Scripts/Gameplay/UI/Lobby/LobbyUIMediator.cs](Assets/Scripts/Gameplay/UI/Lobby/LobbyUIMediator.cs) +- Relay Join - StartClientLobby() in [Assets/Scripts/ConnectionManagement/ConnectionState/OfflineState.cs](Assets/Scripts/ConnectionManagement/ConnectionState/OfflineState.cs) +- Relay Create - StartHostLobby() in [Assets/Scripts/ConnectionManagement/ConnectionState/OfflineState.cs](Assets/Scripts/ConnectionManagement/ConnectionState/OfflineState.cs) +- Lobby and relay - host creation - CreateLobbyRequest() in [Assets/Scripts/Gameplay/UI/Lobby/LobbyUIMediator.cs](Assets/Scripts/Gameplay/UI/Lobby/LobbyUIMediator.cs) -## Player Hierarchy +## Core gameplay structure -The `Player Prefab` field inside of Boss Room's `NetworkManager` is populated with `PersistentPlayer` prefab. Netcode will spawn a PersistentPlayer per client connection, with the client designated as the owner of the prefab instance. All `Player Prefab` prefab instances will be migrated between scenes internally by Netcode's scene management, therefore it is not necessary to mark this object as a `DontDestroyOnLoad` object. This object is suitable for storing data, in some cases in the form of `NetworkVariable`s, that could be accessed across scenes (eg. name, avatar GUID, etc). PersistentPlayer's GameObject hierarchy is quite trivial as it is comprised of only one GameObject: +> __Note__: +> +> An `Avatar` is at the same level as an `Imp` and live in a scene. A `Persistent Player` lives across scenes. -* PersistentPlayer: a `NetworkObject` that will not be destroyed between scenes +A `Persistent Player` prefab will go into the `Player Prefab` slot in the `Network Manager` of the Boss Room. As such, there will be one spawned per client, with the clients owning their respective `Persistent Player` instances. -####CharSelect Scene -Inside `CharSelect` scene, clients select from 8 possible avatar classes, and that selection is stored inside PersistentPlayer's `NetworkAvatarGuidState`. +Note: there is no need to mark these `Persistent Player` instances as `DontDestroyOnLoad` - NGO automatically keeps these prefabs alive between scene loads while the connections are live. -####BossRoom Scene -Inside `BossRoom` scene, `ServerBossRoomState` spawns a `PlayerAvatar` per PersistentPlayer present. This `PlayerAvatar` prefab instance, that is owned by the corresponding connected client, is destroyed by Netcode when a scene load occurs (either to `PostGame` scene, or back to `MainMenu` scene), or through client disconnection. +The purpose of `Persistent Player` is to store synchronized data about player, such as their name, chosen avatar GUID etc. -`ClientAvatarGuidHandler`, a `NetworkBehaviour` component residing on the `PlayerAvatar` prefab instance will fetch the validated avatar GUID from `NetworkAvatarGuidState`, and spawn a local, non-networked graphics GameObject corresponding to the avatar GUID. This GameObject is childed to PlayerAvatar's `PlayerGraphics` child GameObject. +This `PlayerAvatar` prefab instance is owned by the corresponding connected client. It is destroyed by Netcode when a scene load occurs (either to the `PostGame` or `MainMenu` scenes), or through client disconnection. -Once initialized successfully, the in-game PlayerAvatar GameObject hierarchy inside `BossRoom` scene will look something like (in the case of a selected Archer Boy class): +Inside the `CharSelect` scene, clients select from 8 possible avatar classes. That selection is then stored inside the PersistentPlayer's `NetworkAvatarGuidState`. -* Player Avatar: a `NetworkObject` that *will* be destroyed when `BossRoom` scene is unloaded - * Player Graphics: a child GameObject containing `NetworkAnimator` component responsible for replicating animations invoked on the server +Inside the `BossRoom` scene, `ServerBossRoomState` spawns a `PlayerAvatar` per PersistentPlayer present. + +Once initialized successfully, the `PlayerAvatar` GameObject hierarchy inside `BossRoom` will look like this: +> In this example, we have selected the 'Archer Boy' class. + +* PlayerAvatar: a `NetworkObject` that will be destroyed when `BossRoom` scene is unloaded + * PlayerGraphics: a child GameObject containing `NetworkAnimator` component responsible for replicating animations invoked on the server * PlayerGraphics_Archer_Boy: a purely graphical representation of the selected avatar class + +`ClientAvatarGuidHandler`, a `NetworkBehaviour` component residing on the `PlayerAvatar` prefab instance will fetch the validated avatar GUID from `NetworkAvatarGuidState`, and spawn a local, non-networked graphics GameObject corresponding to the avatar GUID. + +### Characters + +`ServerCharacter` lives on a PlayerAvatar or other NPC character and contains server RPCs and NetworkVariables that store the state of any given character. It is responsible for executing or kicking off the server-side logic for the characters, which includes: + +- movement and pathfinding via `ServerCharacterMovement` uses NavMeshAgent that lives on the server to translate the character’s transform, which is synchronized using the NetworkTransform component: +- player action queueing and execution via `ServerActionPlayer`; +- AI logic via `AIBrain` (applies to NPCs); +- Character animations via `ServerAnimationHandler`, which themselves are synchronized using NetworkAnimator; + +`ClientCharacter` is primarily a host for the `ClientActionPlayer` class. It also contains the client RPCs for the character gameplay logic. + +### Game config setup + +Game config in Boss Room is defined in `ScriptableObjects`. + +A singleton class [GameDataSource.cs](Assets/Scripts/Gameplay/GameplayObjects/RuntimeDataContainers/GameDataSource.cs) is responsible for storing all of the actions and character classes in the game. + +[CharacterClass](Assets/Scripts/Gameplay/Configuration/CharacterClass.cs) is the data representation of a Character, containing elements such as starting stats and a list of Actions that it can perform. This covers both player characters and NPCs alike. + +[Action](Assets/Scripts/Gameplay/Configuration/Action.cs) subclasses represent discrete verbs (like swinging a weapon, or reviving someone), and are substantially data driven. + +### Action System + +> __Note__: +> +> Boss Room's action system was built for Boss Room's own purpose. To allow for better game design emergence from your game designers, you'll need to implement your own. + +Boss Room's Action System is a generalized mechanism for Characters to "do stuff" in a networked way. ScriptableObject-derived Actions are implementing both the client and server logic of any given thing that the characters can do in the game. + +We have a variety of actions that serve different purposes. Some actions are generic and reused by different classes of character, while others are specific to a class. + +There is only ever one active Action (also called the "blocking" action) at a time on a character, but multiple Actions may exist at once. In this case, subsequent Actions may be pending behind the currently active one. "Non-blocking" actions may also be running in the background. + +We synchronize actions by calling a `ServerCharacter.RecvDoActionServerRPC` and passing the `ActionRequestData`; a struct which implements the `INetworkSerializable` interface. + +> __Note__: +> +> `ActionRequestData` has a field of `ActionID`, which is a simple struct that wraps an integer, which stores the index of a given scriptable object Action in the registry of abilities available to characters, which is stored in `GameDataSource`. + +From this struct we are able to reconstruct the action that was requested and play it on the server. We do this by creating a pooled clone of the scriptable object Action that corresponds to the action we’re playing. Clients will then play out the visual part of the ability, along with the particle effects and projectiles. + +We can also play an anticipatory animation on the client that is requesting an ability. For instance, a small jump animation when the character receives movement input, but hasn’t yet been brought to motion by synchronized data coming from the server computing it. + +[Server]() and [Client]() ActionPlayers are companion classes to actions that are used to actually play out the actions on both client and server. + +#### Movement action flow + +- Client clicks mouse on target destination. +- Client->server RPC, containing target destination. +- Anticipatory animation plays immediately on client. +- Network latency. +- Server receives the RPC. +- Server performs pathfinding. +- Once pathfinding is finished, server representation of entity starts updating its NetworkVariables at the same cadence as FixedUpdate. +- Network latency before clients receive replication data. +- Visuals GameObject never outpaces the simulation GameObject, and so is always slightly behind and interpolating towards the networked position and rotation. + +### Navigation system + +Each scene which uses navigation or dynamic navigation objects should have a `NavigationSystem` component on a scene GameObject. That object also needs to have the `NavigationSystem` tag. + +#### Building a navigation mesh + +The project uses `NavMeshComponents`. This means direct building from the Navigation window will not give the desired results. Instead, find a `NavMeshComponent` in the given scene e.g. a __NavMeshSurface__ and use the __Bake__ button of that script. Also make sure that there is always only one navmesh file per scene. Navmesh files are stored in a folder with the same name as the corresponding scene. You can recognize them based on their icon in the editor. They follow the naming pattern "NavMesh-" + +## Noteworthy architectural patterns and decisions + +### Dependency Injection + +We use [Dependency Injection](https://en.wikipedia.org/wiki/Dependency_injection) pattern, with our library of choice being [VContainer](https://vcontainer.hadashikick.jp/). + +DI allows us to clearly define our dependencies in code, as opposed to using static access, pervasive singletons or scriptable object references (aka Scriptable Object Architecture). Code is easy to version-control and comparatively easy to understand for a programmer, as opposed to Unity YAML-based objects, such as scenes, scriptable object instances and prefabs. + +DI also allows us to circumvent the problem of cross-scene references to common dependencies, even though we still have to manage the lifecycle of MonoBehaviour-based dependencies by marking them with DontDestroyOnLoad and destroying them manually when appropriate. + +> __Note__: +> +> `ApplilcationController` inherits from the `VContainer`'s `LifetimeScope` - a class that serves as a dependency injection scope and bootstrapper, where we can bind dependencies. Scene-specific State classes inherit from `LifetimeScope` too. +> +> In the Inspector we can choose a parent scope for any `LifetimeScope`s. When doing so, it’s useful to set a cross-scene reference to some parent scopes; most commonly `ApplicationController`. This allows us to bind our scene-specific dependencies, while maintaining easy access to the global dependencies of the `ApplicationController` in our State-specific version of a `LifetimeScope` object. + +### Client/Server code separation + +A challenge we encountered when developing Boss Room was that code will often run in a single context, either client or server. Reading mixed client and server code adds a layer of complexity; making it easier to make mistakes. + +To solve for this, we explored different client-server code separation approaches. For readers that have been following us since the beginning, we eventually decided to revert our initial client/server/shared assemblies to a more classic domain-driven assembly architecture, while still keeping more complex classes separated by client/server. + +Our initial thinking was that separating assemblies by client and server would allow for easier porting to Dedicated Game Server (DGS) afterward; you’d only need to strip a single assembly to make sure that code only runs when necessary. + +Issues with this approach: + +- Callback hell: this makes code that should be trivial, too complex. You can look at our different action implementations in our 1.3.1 version to see this. +- Lots of components could be single simple classes instead of 3 class horrors. + +After investigation, we determined this was not needed for the following reasons: + +- You can ifdef out single classes, there’s no need for asmdef stripping. +- ifdeffing classes isn’t 100% required. It’s a compile time insurance that certain parts of client side code will never run, but isn’t purely required. + - We realized the little pros that’d help with stripping whole assemblies out in one go were outweighed by the complexity this added to the project. +- Most client/server class couples are tightly coupled and will call one another; they are two split implementations of the same logical object. Separating them into different assemblies forces you to create “bridge classes” in order to avoid circular dependencies between your client and server assemblies. By putting your client and server classes in the same assemblies, you allow those circular dependencies in those tightly coupled classes and make sure you remove unnecessary bridging and abstractions. +- Whole assembly stripping is not compatible with NGO, in that NGO doesn’t support NetworkBehaviour stripping. Components related to a NetworkObject need to match client and server side. If this is incorrect, it will create difficulties with NetworkBehaviour indexing. + +After those experimentations, we established new rules for the team: + +- Domain based assemblies +- Use single classes for small components (think the boss room door with a simple on/off state). + - If your class never grows too big, having a single `NetworkBehaviour` remains easy to maintain. +- Use client and server classes (with each pointing to the other) for client/server separation. + - Client/Server pair is in the same assembly. + - If you start up the game as a client, the server components will disable themselves, leaving you with `{Client}` components executing. Make sure you don’t completely destroy the server components, as NGO still requires them for network message sending. + - The Client would have an m_Server and Server would have an m_Client property. + - The Server class would own server driven netvars, same for Client with Owner driven netvars. + - This way, when reading server code, you do not have to mentally skip client code and vice versa. This helps make bigger classes more readable and maintainable. +- Use partial classes when the above isn’t possible + - Still use the Client/Server prefix for keeping each context in your mind. + - Note: you can’t use prefixes for ScriptableObjects that have file name requirements to work. +- Use Client/Server/Shared separation when you have a 1 to many relationship where your server class needs to send info to many client classes. + - This can also be achieved with our NetworkedMessageChannel + +You still need to take care of code executing in `Start` and `Awake`: if this code runs contemporaneously with the `NetworkManager`'s initialization, it may not know yet whether the player is a host or client. + +### Publisher-Subscriber Messaging + +We have implemented a DI-friendly Publisher-Subscriber pattern (see Infrastructure assembly, PubSub folder). + +It allows us to send and receive strongly-typed messages in a loosely-coupled manner, where communicating systems only know about the IPublisher/ISubscriber of a given message type. Since publishers and subscribers are classes, we can have more interesting behavior for message transfer, such as Buffered messages (that keep the last message that went through the pipe and gives it to any new subscriber) and networked messaging (see NetworkedMessageChannel section). + +This mechanism allows us to both avoid circular references and have a more limited dependency surface between our assemblies. Cross-communicating systems rely on common messages, but don't necessarily need to know about each-other, thus allowing us to more easily separate them into smaller assemblies. + +It allows us to avoid having circular references between assemblies, the code of which needs only to know about the messaging protocol, but doesn't actually need to reference anything else. + +The other benefit is strong separation of concerns and coupling reduction, which is achieved by using PubSub along with Dependency Injection. DI is used to pass the handles to either `IPublisher` or `ISubscriber` of any given event type, and thus our message publishers and consumers are truly not aware of each-other. + +`MessageChannel` classes implement these interfaces and provide the actual messaging logic. + +#### NetworkedMessageChannel + +Along with in-process messaging, we have implemented the `NetworkedMessageChannel`, which uses the same API, but allows us to send data between peers. The actual netcode synchronization for these is implemented using custom NGO messaging. It serves as a useful synchronization primitive in our arsenal. diff --git a/CHANGELOG.md b/CHANGELOG.md index a80879855..6cfffd6be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ Additional documentation and release notes are available at [Multiplayer Documen * Removed DynamicNavObstacle - an unused class (#732) * Merged networked data classes into their Server counterparts. An example of that change is the contents of NetworkCharacterState getting moved into ServerCharacter, contents of NetworkDoorState getting moved into SwitchedDoor etc. (#732) * Engine version bump to 2021.3.10f1 (#740) +* Updated the Architecture.md to match the current state of the project, with all of our recent refactorings. Architecture.md now also has a lot of links to the relevant classes, simplifying the navigation in our code-base (#763) ### Removed * ### Fixed diff --git a/Documentation/Images/AutoGenerateLighting.png b/Documentation/Images/AutoGenerateLighting.png index 7b0a97202..454ce13a6 100644 --- a/Documentation/Images/AutoGenerateLighting.png +++ b/Documentation/Images/AutoGenerateLighting.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:64d4286e10c33db7baf97d73104cc04f2d41c0ad44dc831f8c52273a4f7936a6 -size 3065 +oid sha256:c0fc69b2b962148293f131a4309d1c06970d533aa18e92e3f29e3563a1421b7f +size 1992 diff --git a/Documentation/Images/Banner.png b/Documentation/Images/Banner.png index 7063e75e5..9edeb4e55 100644 --- a/Documentation/Images/Banner.png +++ b/Documentation/Images/Banner.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b5e052b7f44b18bfb67f8d3a43767678361521ca01a136377ac5e8a04fc86e9b -size 488679 +oid sha256:d2ea844dbba94a270eb1739953721f541d7390dc95793a3f606f890ec1f3f52a +size 427995 diff --git a/Documentation/Images/Boss.png b/Documentation/Images/Boss.png index 42fc88987..1c0627edb 100644 --- a/Documentation/Images/Boss.png +++ b/Documentation/Images/Boss.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c7a48799c42fc19707c8ab398967bbccf04dc69ce4471f1766e09b53552c832 -size 784216 +oid sha256:8150d84ae9f5c032abd73d292d5adf3e1769b6349c465d8c754efa8e0e8b5562 +size 764721 diff --git a/Documentation/Images/BossRoomAssemblies.png b/Documentation/Images/BossRoomAssemblies.png new file mode 100644 index 000000000..dc9a1cde1 --- /dev/null +++ b/Documentation/Images/BossRoomAssemblies.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dd64cb1cbf26368697607040ed08e01a52582cce1e5ab482c724e378d0362349 +size 51787 diff --git a/Documentation/Images/BossRoomConnectionManager.png b/Documentation/Images/BossRoomConnectionManager.png new file mode 100644 index 000000000..31d384108 --- /dev/null +++ b/Documentation/Images/BossRoomConnectionManager.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a22ea4fd5b2355272c50ab7d8e3b0f2c7e908f7aa545d36ec22b8ed690f31db1 +size 88312 diff --git a/Documentation/Images/BossRoomMenu.png b/Documentation/Images/BossRoomMenu.png index 99cc78a66..201ecc3e9 100644 --- a/Documentation/Images/BossRoomMenu.png +++ b/Documentation/Images/BossRoomMenu.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f05f3af0015c1946ec6d7cd3c7f3eb8ddc95087a2e6248d1868f9048edfeb54e -size 21176 +oid sha256:2fe0193cb0fa4ec6ce9e2d391f68158f677793bbf13e299f75ff81052a70f580 +size 7605 diff --git a/Documentation/Images/BossRoomSceneFlowDiagram.png b/Documentation/Images/BossRoomSceneFlowDiagram.png new file mode 100644 index 000000000..764dec0ba --- /dev/null +++ b/Documentation/Images/BossRoomSceneFlowDiagram.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8329e94d46870b63fac9a36a77b8be17ba48ce01136c10d8d91ed505ee0a0752 +size 52675 diff --git a/Documentation/Images/BuildProject.png b/Documentation/Images/BuildProject.png index e69db0b77..c6481aed8 100644 --- a/Documentation/Images/BuildProject.png +++ b/Documentation/Images/BuildProject.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6dc277128380fd4d790b35f19048250053564be2ca79a0ce8496685f2e15fb1b -size 54444 +oid sha256:19c326ac3f649a7a179cf1ea744769965aa34d0834016be834cd61ad0183420d +size 18474 diff --git a/Documentation/Images/ClearBakingData.png b/Documentation/Images/ClearBakingData.png index 72aa55fc6..df0ef2f75 100644 --- a/Documentation/Images/ClearBakingData.png +++ b/Documentation/Images/ClearBakingData.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:236cd72bb3f369261f20b7cd7ac1abbe8a39cbd2ffac6d46ac3686233c7f779c -size 5317 +oid sha256:1e49fef38a7464393a071f7713256f1781730bbb8ddcc7b74a28e4d3253b5f1a +size 2695 diff --git a/Documentation/Images/ImportantLightingSettings.png b/Documentation/Images/ImportantLightingSettings.png index 1d4a583a6..a055bf028 100644 --- a/Documentation/Images/ImportantLightingSettings.png +++ b/Documentation/Images/ImportantLightingSettings.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:68e13580f9ae0afdbdb05c6c470c6149310952f07fbba053a06275c586e6642a -size 92668 +oid sha256:d1b21cba6b8b834c9adb4e3d91cbb32ff5655ca37306812261b19ead298760bf +size 10361 diff --git a/Documentation/Images/LightExplorer.png b/Documentation/Images/LightExplorer.png index c20ba83e4..317778818 100644 --- a/Documentation/Images/LightExplorer.png +++ b/Documentation/Images/LightExplorer.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:55dd5b57c4c725c62647f4007d058b79779912269d871d0f62be2e11ddd4b662 -size 30433 +oid sha256:d2838c3c6ece1dae080928bdc792e3e98d0edf2d5edf87abea15591a057e9864 +size 9899 diff --git a/Documentation/Images/LightMode.png b/Documentation/Images/LightMode.png index 6f151a258..3d189a4dc 100644 --- a/Documentation/Images/LightMode.png +++ b/Documentation/Images/LightMode.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:89377d5bb8968ec6bddc6dcd25125598ca43fac7285ef7be4814593c9aa50fe6 -size 16396 +oid sha256:1539274ca1b17c5949252f12a0acd912af241154db3478d96afc65560ad18c64 +size 6494 diff --git a/Documentation/Images/LightingPanel.png b/Documentation/Images/LightingPanel.png index dd8c77b70..374e81cb2 100644 --- a/Documentation/Images/LightingPanel.png +++ b/Documentation/Images/LightingPanel.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:208ed201b8608bdbec7b2994d523612a4fce95deff2712adc409f473d8995ec8 -size 91997 +oid sha256:99a2935710a3a626279f4758883c7f44fff84e38ccfcc584be0f03f7e5c88d18 +size 29447 diff --git a/Documentation/Images/MeshSettings.png b/Documentation/Images/MeshSettings.png index 987931fab..473bb61d8 100644 --- a/Documentation/Images/MeshSettings.png +++ b/Documentation/Images/MeshSettings.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9c2b1c25f5e3d56e134ff8736648faa74a01ce76e38bea290e2469ca542d2be1 -size 40572 +oid sha256:10ef82a7fd4d7127411281f6160a12d359620c180b469fd856637fbbf4f6f21d +size 15007 diff --git a/Documentation/Images/Players.png b/Documentation/Images/Players.png index 705bdae90..72c9f5195 100644 --- a/Documentation/Images/Players.png +++ b/Documentation/Images/Players.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ba894b63476191fddce6e6af32ee5506de578006a1ccb29691c2248fa7d3468 -size 1091283 +oid sha256:5e3cb28cdc17caa5c45675730a3733fd53643ed6d8f80296f6c69450ae79c92f +size 375588 diff --git a/Documentation/Images/StartupScene.png b/Documentation/Images/StartupScene.png index 542b3a339..394fb9b18 100644 --- a/Documentation/Images/StartupScene.png +++ b/Documentation/Images/StartupScene.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f14363219b8ccf6e30ef4aa901d38ae12ce28f0eef337bef2a74523bb8187b6 -size 35861 +oid sha256:664b3898c109e57e9067d0e9d53901074a9779e2e1f608f6022671312ef9b5ad +size 10931 diff --git a/Documentation/Images/TorchPrefab.png b/Documentation/Images/TorchPrefab.png index 50effb3e4..9a3e05a07 100644 --- a/Documentation/Images/TorchPrefab.png +++ b/Documentation/Images/TorchPrefab.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e413b2a7b84c92b0f4b7f4aaaa7bdd2d7f4e6d8377ec51c69075fc4a1fa44b01 -size 12039 +oid sha256:fa7e0658d28afed4ce34ad94153041f143437352de490ae8d5768478a3df0639 +size 4920