1. Preface
The Unity FpsSample Demo was released around 2018 for an official demo of an FPS multiplayer matchmaking demo for MLAPI (NetCode predecessor) + DOTS.
Demo download (requires Git LFS):/Unity-Technologies/FPSSample
The download is about 3-40GB after completion, if the size is not correct the download may not be complete.
The writing is not complete for time reasons, but it roughly depicts the outline of the framework of the project.
1.1. Accompanying Documentation and Main Configuration Interface
The accompanying documentation can be found in the project root directory:
The main configuration screen can be opened at Fps Sample/Windows/Project Tools in the project:
The way in which the AssetBundle is packaged is worth mentioning, as marking the AssetBundle at the bottom of the resource is very inconvenient.
FpsSample stores AssetBunlde in ScriptableObject by hash value and distinguishes Server/Client.
The server side packages the AssetBundle with alternative files that are less resource- and performance-intensive, and the client side packages the AssetBundle with the
Then the full version.
Referring to the documentation, different GameLoops determine the main loop logic under the current game:
Each of the several GameLoops within the game corresponds to the following:
- ClientGameLoop Client Game Loop
- ServerGameLoop server-side game loop
- PreviewGameLoop editor to execute the game loop corresponding to the level test (standalone run mode)
- ThinClientGameLoop A lightweight version of the client-side game loop for debugging, with little to no internal System.
2.1 GameLoop Trigger Logic
The entrance to the game is:
The GameLoop interface is defined in:
public interface IGameLoop { bool Init(string[] args); void Shutdown(); void Update(); void FixedUpdate(); void LateUpdate(); }
The required GameLoop is then initialized by command and will be created internally via reflection (in):
void CmdServe(string[] args) { RequestGameLoop(typeof(ServerGameLoop), args); Console.s_PendingCommandsWaitForFrames = 1; }
IGameLoop gameLoop = (IGameLoop)(m_RequestedGameLoopTypes[i]);
initSucceeded = (m_RequestedGameLoopArguments[i]);
3. Network operation logic
3.1 ClientGameLoop
First, let's look at ClientGameLoop. Initialization will call the Init function, NetworkTransport is the network layer encapsulated by Unity.
NetworkClient is the upper level package that comes with some game logic.
public bool Init(string[] args) { ... m_NetworkTransport = new SocketTransport(); m_NetworkClient = new NetworkClient(m_NetworkTransport);
3.1.1 NetworkClient Internal Logic
Followed by looking at the structure of NetworkClient, deleted some content, part of the interface is as follows:
public class NetworkClient { ... public bool isConnected { get; } public ConnectionState connectionState { get; } public int clientId { get; } public NetworkClient(INetworkTransport transport) public void Shutdown() public void QueueCommand(int time, DataGenerator generator) public void QueueEvent(ushort typeId, bool reliable, NetworkEventGenerator generator) ClientConnection m_Connection; }
Where QueueCommand is used to handle information such as movement and jumping of the character and is contained in the Command structure.
QueueEvent is used to handle the status of a role's connection, startup, etc.
3.1.2 NetworkClient External Calls
Continuing back to ClientGameLoop, you can see the NetworkClient update logic in Update
public void Update() { (""); ("-NetworkClientUpdate"); m_NetworkClient.Update(this, m_clientWorld?.GetSnapshotConsumer()); //client receives data (); ("-StateMachine update"); m_StateMachine.Update(); (); // TODO (petera) change if we have a lobby like setup one day if (m_StateMachine.CurrentState() == && != null) (m_ChatSystem); m_NetworkClient.SendData(); //client sends data
One of the ClientGameLoop Update function signatures is as follows:
public void Update(INetworkClientCallbacks clientNetworkConsumer, ISnapshotConsumer snapshotConsumer)
Parameter 1 is used to handle messages such as OnConnect, OnDisconnect, etc., and parameter 2 is used to handle various types of snapshot information in the scene.
3.1.3 m_NetworkClient.Update
Go to the Update function and look at the receive logic:public void Update(INetworkClientCallbacks clientNetworkConsumer, ISnapshotConsumer snapshotConsumer) { ... TransportEvent e = new TransportEvent(); while (m_Transport.NextEvent(ref e)) { switch () { case : OnConnect(); break; case : OnDisconnect(); break; case : OnData(, , , clientNetworkConsumer, snapshotConsumer); break; } } }
You can see that the specific logic is handled in OnData
3.1.4 m_NetworkClient.SendData
Go to the SendData function and see how sending data is handled.
public void SendPackage<TOutputStream>() where TOutputStream : struct, { ...if (commandSequence > 0) { lastSentCommandSeq = commandSequence; WriteCommands(info, ref output); } WriteEvents(info, ref output); int compressedSize = (); (compressedSize); CompleteSendPackage(info, ref rawOutputStream); }
As you can see, the Command and Event previously added to the queue are removed and written to the buffer ready to be sent.
3.
As with ClientGameLoop, the Transport network layer and NetworkServer are initialized in Init.
public bool Init(string[] args) { // Set up statemachine for ServerGame m_StateMachine = new StateMachine<ServerState>(); m_StateMachine.Add(, null, UpdateIdleState, null); m_StateMachine.Add(, null, UpdateLoadingState, null); m_StateMachine.Add(, EnterActiveState, UpdateActiveState, LeaveActiveState); m_StateMachine.SwitchTo(); m_NetworkTransport = new SocketTransport(, ); m_NetworkServer = new NetworkServer(m_NetworkTransport);
Note that one of the operations to generate the snapshot is in Active of the state machine.
Update and SendData in Update:
public void Update() { UpdateNetwork();/Update SQP query servers and invocations m_StateMachine.Update(); m_NetworkServer.SendData(); m_NetworkStatistics.Update(); if ( > 0) OnDebugDrawGameloopInfo(); }
3.2.1 Server - HandleClientCommands
To see how the client command is handled after it is received, within the ServerTick function, call the
HandleClientCommands handles commands from the client.
public class ServerGameWorld : ISnapshotGenerator, IClientCommandProcessor { ... public void ServerTickUpdate() { ... m_NetworkServer.HandleClientCommands(m_GameWorld., this); }
public void HandleClientCommands(int tick, IClientCommandProcessor processor) { foreach (var c in m_Connections) (tick, processor); }
It is then deserialized, plus the ComponentData is given to the corresponding System for processing:
public class ServerGameWorld : ISnapshotGenerator, IClientCommandProcessor {
... public void ProcessCommand(int connectionId, int tick, ref NetworkReader data) {
... if (tick == m_GameWorld.) (ref serializeContext, ref data); if ( != ) { var userCommand = m_GameWorld.GetEntityManager().GetComponentData<UserCommandComponentData>( ); = ; m_GameWorld.GetEntityManager().SetComponentData<UserCommandComponentData>( ,userCommand); } }
4.1 Snapshot Process
All client-side logic in the project is sent to the server for execution, the server creates Snapshot snapshots, and the client receives Snapshot snapshots to synchronize content.
The Server section looks at calls to ReplicatedEntityModuleServer and ISnapshotGenerator:
public class ServerGameWorld : ISnapshotGenerator, IClientCommandProcessor { public ServerGameWorld(GameWorld world, NetworkServer networkServer, Dictionary<int, > clients, ChatSystemServer m_ChatSystem, BundledResourceManager resourceSystem) { ... m_ReplicatedEntityModule = new ReplicatedEntityModuleServer(m_GameWorld, resourceSystem, m_NetworkServer); m_ReplicatedEntityModule.ReserveSceneEntities(networkServer); } public void ServerTickUpdate() { ... m_ReplicatedEntityModule.HandleSpawning(); m_ReplicatedEntityModule.HandleDespawning(); } public void GenerateEntitySnapshot(int entityId, ref NetworkWriter writer) { ... m_ReplicatedEntityModule.GenerateEntitySnapshot(entityId, ref writer); } public string GenerateEntityName(int entityId) { ... return m_ReplicatedEntityModule.GenerateName(entityId); } }
The Client section focuses on ReplicatedEntityModuleClient and ISnapshotConsumer calls:
foreach (var id in updates) { var info = entities[id]; ( != null, "Processing update of id {0} but type is null", id); fixed (uint* data = ) { var reader = new NetworkReader(data, ); (serverTime, id, ref reader); } }
4.2 SnapshotGenerator process
Call the snapshot creation logic in ServerGameLoop:
public class ServerGameWorld : ISnapshotGenerator, IClientCommandProcessor { void UpdateActiveState() { int tickCount = 0; while ( > m_nextTickTime) { tickCount++; m_serverGameWorld.ServerTickUpdate(); ... m_NetworkServer.GenerateSnapshot(m_serverGameWorld, m_LastSimTime); }
All the entities are stored in the Server, and each entity has the EntityInfo structure, which holds the snapshots field.
Traverses the entity and calls the GenerateEntitySnapshot interface to generate the entity content:
unsafe public class NetworkServer { unsafe public void GenerateSnapshot(ISnapshotGenerator snapshotGenerator, float simTime) { ... // Run through all the registered network entities and serialize the snapshot for (var id = 0; id < m_Entities.Count; id++) { var entity = m_Entities[id]; EntityTypeInfo typeInfo; bool generateSchema = false; if (!m_EntityTypes.TryGetValue(, out typeInfo)) { typeInfo = new EntityTypeInfo() { name = (id), typeId = , createdSequence = m_ServerSequence, schema = new NetworkSchema( + ) }; m_EntityTypes.Add(, typeInfo); generateSchema = true; } // Generate entity snapshot var snapshotInfo = (m_ServerSequence); = + ; var writer = new NetworkWriter(, / 4 - , , generateSchema); (id, ref writer); (); = ();
4.3 SnapshotConsumer process
Processing snapshot information in NetworkClient's OnData
case : OnData(, , , clientNetworkConsumer, snapshotConsumer); break;
The corresponding handler function:
public void ProcessEntityUpdate(int serverTick, int id, ref NetworkReader reader) { var data = m_replicatedData[id]; ( < serverTick, "Failed to apply snapshot. Wrong tick order. entityId:{0} snapshot tick:{1} last server tick:{2}", id, serverTick, ); = serverTick; ( != null, "Failed to apply snapshot. Serializablearray is null"); foreach (var entry in ) (ref reader, serverTick); foreach (var entry in ) (ref reader, serverTick); foreach (var entry in ) (ref reader, serverTick); m_replicatedData[id] = data; }
5. Game module logic
5.1 ECS System Extension
class contains various System base class extensions:
- BaseComponentSystem<T1 - T3> Filter out generic MonoBehaviour to ComponentGroup but ignore destroyed objects (DespawningEntity), you can add IComponentData filtering conditions in subclasses
- BaseComponentDataSystem<T1 - T5> Filter out generic ComponentData, rest consistent with BaseComponentSystem
- InitializeComponentSystem<T> Filter MonoBehaviour of type T and then execute the Initialize function, making sure that the initialization is performed only once
- InitializeComponentDataSystem<T,K> add ComponentData K to each object containing ComponentData T, ensuring that the initialization is performed only once
- DeinitializeComponentSystem<T> Filter objects containing MonoBehaviour T and destroyed markers
- DeinitializeComponentDataSystem<T> Filter objects containing ComponentData T and destroyed markers
- InitializeComponentGroupSystem<T,S> sameInitializeComponentSystem,But it's markedAlwaysUpdateSystem
- DeinitializeComponentGroupSystem<T> sameDeinitializeComponentSystem,But it's markedAlwaysUpdateSystem
5.2 Role Creation
Take the example of opening Level_01_Main.unity run under the editor.
After running it will go to the scene run callback that triggered the corresponding binding:
[InitializeOnLoad] public class EditorLevelManager { static EditorLevelManager() { += OnPlayModeStateChanged; } ... static void OnPlayModeStateChanged(PlayModeStateChange mode) { if (mode == ) { ... case : ( typeof(PreviewGameLoop), new string[0]); break; } }
The logic of PreviewGameMode is written in PreviewGameLoop, where it triggers the creation of controlledEntity if it is empty:
public class PreviewGameMode : BaseComponentSystem { ... protected override void OnUpdate() { if (m_Player.controlledEntity == ) { Spawn(false); return; } }
Finally tune in here to create:
(PostUpdateCommands, , m_SpawnPos, m_SpawnRot, playerEntity);
When HandleCharacterSpawn is executed to after creation, role-related logic is initiated:
public static void CreateHandleSpawnSystems(GameWorld world,SystemCollection systems, BundledResourceManager resourceManager, bool server) { (().CreateManager<HandleCharacterSpawn>(world, resourceManager, server)); // TODO (mogensh) needs to be done first as it creates presentation (().CreateManager<HandleAnimStateCtrlSpawn>(world)); }
If you comment out this line of code and run it, you'll see that the role won't start.
5.3 Role system
The Roles module is divided into Client and Server side with the following differences:
Client | Server | clarification |
UpdateCharacter1PSpawn | Handling first-person characters | |
PlayerCharacterControlSystem | PlayerCharacterControlSystem | Synchronize parameters such as role Id |
CreateHandleSpawnSystems | CreateHandleSpawnSystems | Handling Role Generation |
CreateHandleDespawnSystems | CreateHandleDespawnSystems | Handling role destruction |
CreateAbilityRequestSystems | CreateAbilityRequestSystems | Skill-related logic |
CreateAbilityStartSystems | CreateAbilityStartSystems | Skill-related logic |
CreateAbilityResolveSystems | CreateAbilityResolveSystems | Skill-related logic |
CreateMovementStartSystems | CreateMovementStartSystems | Mobile Related Logic |
CreateMovementResolveSystems | CreateMovementResolveSystems | Applying Mobile Data Logic |
UpdatePresentationRootTransform | UpdatePresentationRootTransform | Handles information about the rotation of the root position of the display character |
UpdatePresentationAttachmentTransform | UpdatePresentationAttachmentTransform | Processing root position rotation information for additional objects |
UpdateCharPresentationState | UpdateCharPresentationState | Updating Role Presentation Status for Network Transmission |
ApplyPresentationState | ApplyPresentationState | Apply character display state to AnimGraph |
HandleDamage | Treating injuries | |
UpdateTeleportation | Handling character location transfer | |
CharacterLateUpdate | Synchronizing some parameters in LateUpdate timing | |
UpdateCharacterUI | Updated character UI | |
UpdateCharacterCamera | Updated character camera | |
HandleCharacterEvents | Handling role events |
5.4 CharacterMoveQuery
It is still the role controller that is used inside the role:
Character generation is split across multiple Systems, so the character controllers are also separate GameObjects, the
The creation code is as follows:
public class CharacterMoveQuery : MonoBehaviour { public void Initialize(Settings settings, Entity hitCollOwner) { //(""); this.settings = settings; var go = new GameObject("MoveColl_" + name,typeof(CharacterController), typeof(HitCollision)); charController = <CharacterController>();
Pass deltaPos to moveQuery in the System of Movement_Update:
class Movement_Update : BaseComponentDataSystem<CharBehaviour, AbilityControl, Ability_Movement.Settings> { protected override void Update(Entity abilityEntity, CharBehaviour charAbility, AbilityControl abilityCtrl, Ability_Movement.Settings settings ) { // Calculate movement and move character var deltaPos = ; CalculateMovement(ref time, ref predictedState, ref command, ref deltaPos); // Setup movement query = == 0 ? m_charCollisionALayer : m_charCollisionBLayer; = ; = + (float3)deltaPos; (,predictedState); } }
Finally apply deltaPos to the role controller in moveQuery:
class HandleMovementQueries : BaseComponentSystem { protected override void OnUpdate() { ... var deltaPos = - currentControllerPos; (deltaPos); = ; = ; (); } }
6. Miscellaneous
6.1 MaterialPropertyOverride
This widget supports modifying material sphere parameters without creating additional material spheres.
And there is no project dependency, you can directly get another project to use:
6.2 RopeLine
Rapidly Build Dynamic Interactive Rope Sections Tool
Reference:
/p/347ded2a8e7a
/p/c4ea9073f443