Tutorial - Your First Network Component

Introduction

This tutorial will link all the relevant documentation to provide you with a detailed guide on getting started with creating multiplayer components. By the end of this tutorial you will learn how to create a new network component that has a network property and a remote procedure call.

Our starting base will be the O3DE MultiplayerSample project. You can find the instructions on how to build and run it in the MultiplayerSample project readme .

Network Components

Add a New Component

We will start by creating a network component - MyFirstNetworkComponent. It will only do one thing, which is to replicate a monotonic counter.

Since we already have MultiplayerSample setup for automated code generation (codegen), we can start by duplicating one of the existing codegen xml files.

  1. Copy <o3de-multiplayersample>\Gem\Code\Source\AutoGen\SimplePlayerCameraComponent.AutoComponent.xml to <o3de-multiplayersample>\Gem\Code\Source\AutoGen\MyFirstNetworkComponent.AutoComponent.xml

  2. Modify <o3de-multiplayersample>\Gem\Code\Source\AutoGen\MyFirstNetworkComponent.AutoComponent.xml to have the following content:

    <Component Name="MyFirstNetworkComponent"
            Namespace="MultiplayerSample"
            OverrideComponent="true"
            OverrideController="false"
            OverrideInclude="Source/Components/MyFirstNetworkComponent.h"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    </Component>
    
  3. Go to <o3de-multiplayersample>\Gem\Code\multiplayersample_files.cmake and add your new xml file:

    set(FILES
    ...
        Source/AutoGen/MyFirstNetworkComponent.AutoComponent.xml
    
  4. Re-run CMake configure for your O3DE project, or build MultiplayerSample.Static. A failure is expected, since we have not written our code on top of the code-generated files.

    3>------ Build started: Project: MultiplayerSample.Static, Configuration: profile x64 ------
    3>Generating Azcg/Generated/Source/AutoGen/NetworkAnimationComponent.AutoComponent.h, Azcg/Generated/Source/AutoGen/NetworkCharacterComponent.AutoComponent.h, Azcg/Generated/Source/AutoGen/NetworkHitVolumesComponent.AutoComponent.h, Azcg/Generated/Source/AutoGen/NetworkPlayerSpawnerComponent.AutoComponent.h, Azcg/Generated/Source/AutoGen/NetworkRigidBodyComponent.AutoComponent.h, Azcg/Generated/Source/AutoGen/NetworkWeaponsComponent.AutoComponent.h, Azcg/Generated/Source/AutoGen/MyFirstNetworkComponent.AutoComponent.h, Azcg/Generated/Source/AutoGen/SimplePlayerCameraComponent.AutoComponent.h, Azcg/Generated/Source/AutoGen/WasdPlayerMovementComponent.AutoComponent.h, Azcg/Generated/Source/AutoGen/NetworkAnimationComponent.AutoComponent.cpp, Azcg/Generated/Source/AutoGen/NetworkCharacterComponent.AutoComponent.cpp, Azcg/Generated/Source/AutoGen/NetworkHitVolumesComponent.AutoComponent.cpp, Azcg/Generated/Source/AutoGen/NetworkPlayerSpawnerComponent.AutoComponent.cpp, Azcg/Generated/Source/AutoGen/NetworkRigidBodyComponent.AutoComponent.cpp, Azcg/Generated/Source/AutoGen/NetworkWeaponsComponent.AutoComponent.cpp, Azcg/Generated/Source/AutoGen/MyFirstNetworkComponent.AutoComponent.cpp, Azcg/Generated/Source/AutoGen/SimplePlayerCameraComponent.AutoComponent.cpp, Azcg/Generated/Source/AutoGen/WasdPlayerMovementComponent.AutoComponent.cpp, Azcg/Generated/Source/AutoGen/AutoComponentTypes.h, Azcg/Generated/Source/AutoGen/AutoComponentTypes.cpp
    3>Running AutoGen for MultiplayerSample.Static
    3>Generating D:\git\o3de\build\-86ba765d\Gem\Code\Azcg\Generated\Source\AutoGen\MyFirstNetworkComponent.AutoComponent.h with template D:/git/o3de/Gems/Multiplayer/Code/Source/AutoGen/AutoComponent_Header.jinja and inputs D:\git\o3de-multiplayersample\Gem\Code\Source\AutoGen\MyFirstNetworkComponent.AutoComponent.xml
    3>Generating D:\git\o3de\build\-86ba765d\Gem\Code\Azcg\Generated\Source\AutoGen\MyFirstNetworkComponent.AutoComponent.cpp with template D:/git/o3de/Gems/Multiplayer/Code/Source/AutoGen/AutoComponent_Source.jinja and inputs D:\git\o3de-multiplayersample\Gem\Code\Source\AutoGen\MyFirstNetworkComponent.AutoComponent.xml
    3>Generating D:\git\o3de\build\-86ba765d\Gem\Code\Azcg\Generated\Source\AutoGen\AutoComponentTypes.cpp with template D:/git/o3de/Gems/Multiplayer/Code/Source/AutoGen/AutoComponentTypes_Source.jinja and inputs D:\git\o3de-multiplayersample\Gem\Code\Source\AutoGen\NetworkAnimationComponent.AutoComponent.xml, D:\git\o3de-multiplayersample\Gem\Code\Source\AutoGen\NetworkCharacterComponent.AutoComponent.xml, D:\git\o3de-multiplayersample\Gem\Code\Source\AutoGen\NetworkHitVolumesComponent.AutoComponent.xml, D:\git\o3de-multiplayersample\Gem\Code\Source\AutoGen\NetworkPlayerSpawnerComponent.AutoComponent.xml, D:\git\o3de-multiplayersample\Gem\Code\Source\AutoGen\NetworkRigidBodyComponent.AutoComponent.xml, D:\git\o3de-multiplayersample\Gem\Code\Source\AutoGen\NetworkWeaponsComponent.AutoComponent.xml, D:\git\o3de-multiplayersample\Gem\Code\Source\AutoGen\MyFirstNetworkComponent.AutoComponent.xml, D:\git\o3de-multiplayersample\Gem\Code\Source\AutoGen\SimplePlayerCameraComponent.AutoComponent.xml, D:\git\o3de-multiplayersample\Gem\Code\Source\AutoGen\WasdPlayerMovementComponent.AutoComponent.xml
    3>Total Time 0:00:00.01
    3>MyFirstNetworkComponent.AutoComponent.cpp
    3>AutoComponentTypes.cpp
    3>D:\git\o3de\build\-86ba765d\Gem\Code\Azcg\Generated\Source\AutoGen\AutoComponentTypes.cpp(14,10): fatal error C1083: Cannot open include file: 'Source/Components/MyFirstNetworkComponent.h': No such file or directory
    3>#include <Source/Components/MyFirstNetworkComponent.h>
    3>         ^
    3>D:\git\o3de\build\-86ba765d\Gem\Code\Azcg\Generated\Source\AutoGen\MyFirstNetworkComponent.AutoComponent.cpp(20,10): fatal error C1083: Cannot open include file: 'Source/Components/MyFirstNetworkComponent.h': No such file or directory
    3>#include <Source/Components/MyFirstNetworkComponent.h>
    3>         ^
    3>Done building project "MultiplayerSample.Static.vcxproj" -- FAILED.
    
  5. Notice what we did get so far:

    3>Generating D:\git\o3de\build\-86ba765d\Gem\Code\Azcg\Generated\Source\AutoGen\MyFirstNetworkComponent.AutoComponent.h ...
    3>Generating D:\git\o3de\build\-86ba765d\Gem\Code\Azcg\Generated\Source\AutoGen\MyFirstNetworkComponent.AutoComponent.cpp ...
    
  6. Open the header file MyFirstNetworkComponent.AutoComponent.h and look for the following, large comment block:

    /*
    /// You may use the classes below as a basis for your new derived classes. Derived classes must be marked in MyFirstNetworkComponent.AutoComponent.xml
    /// Place in your .h
    #pragma once
    
    #include <Source/AutoGen/MyFirstNetworkComponent.AutoComponent.h>
    
    namespace MultiplayerSample
    {
        class MyFirstNetworkComponent
            : public MyFirstNetworkComponentBase
        {
        public:
            AZ_MULTIPLAYER_COMPONENT(MultiplayerSample::MyFirstNetworkComponent, s_myFirstNetworkComponentConcreteUuid, MultiplayerSample::MyFirstNetworkComponentBase);
    
            static void Reflect(AZ::ReflectContext* context);
    
            void OnInit() override;
            void OnActivate(Multiplayer::EntityIsMigrating entityIsMigrating) override;
            void OnDeactivate(Multiplayer::EntityIsMigrating entityIsMigrating) override;
    
    
    
    
        protected:
    
        };
    
    }
    /// Place in your .cpp
    #include <Source/Components/MyFirstNetworkComponent.h>
    
    namespace MultiplayerSample
    {
        void MyFirstNetworkComponent::Reflect(AZ::ReflectContext* context)
        {
            AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context);
            if (serializeContext)
            {
                serializeContext->Class<MyFirstNetworkComponent, MyFirstNetworkComponentBase>()
                    ->Version(1);
            }
            MyFirstNetworkComponentBase::Reflect(context);
        }
    
        void MyFirstNetworkComponent::OnInit()
        {
        }
    
        void MyFirstNetworkComponent::OnActivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating)
        {
        }
    
        void MyFirstNetworkComponent::OnDeactivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating)
        {
        }
    
    }
    */
    
  7. The code in the preceding block is our starting base. Copy paste that code into <o3de-multiplayersample>\Gem\Code\Source\Components\MyFirstNetworkComponent.h and <o3de-multiplayersample>\Gem\Code\Source\Components\MyFirstNetworkComponent.cpp.

  8. Add these files to <o3de-multiplayersample>\Gem\Code\multiplayersample_files.cmake:

    set(FILES
    ...
        Source/Components/MyFirstNetworkComponent.cpp
        Source/Components/MyFirstNetworkComponent.h
    
  9. Now, MultiplayerSample project should compile without any issues. Open the Editor to see the new component in the Editor.

    My First Network Component in the Editor

    Additionally, you can find the generated files and your XML file in Visual Studio:

    Generated Code in Visual Studio

  10. At this point, we have a new multiplayer component, MyFirstNetworkComponent.

Add a Network Property

Let’s add a network property that will replicate uptime from the server to clients.

  1. Start by modifying MyFirstNetworkComponent.AutoComponent.xml. Here is a codegen xml file with a network property:

    <?xml version="1.0"?>
    <Component Name="MyFirstNetworkComponent"
            Namespace="MultiplayerSample"
            OverrideComponent="true"
            OverrideController="false"
            OverrideInclude="Source/Components/MyFirstNetworkComponent.h"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <NetworkProperty Type="double"
                    Name="UpTime"
                    Init="0.0"
                    ReplicateFrom="Authority"
                    ReplicateTo="Client"
                    IsRewindable="true"
                    IsPredictable="true"
                    IsPublic="true"
                    Container="Object"
                    ExposeToEditor="false"
                    ExposeToScript="false"
                    GenerateEventBindings="false"
                    Description="Time since the start of the application" />
    </Component>
    
  2. Focus on the basic details of a NetworkProperty. These are:

    <NetworkProperty Type="double"
                    Name="UpTime"
                    Init="0.0"
                    ReplicateFrom="Authority"
                    ReplicateTo="Client"
    

    Expressed in C++, this would look like:

    double m_UpTime = 0.0; // replicate from Authority to Client
    
    Note:
    For now, we can think of Authority as the multiplayer server. Authority to Client is the most common direction of replication.
  3. With these changes, MyFirstNetworkComponent can access UpTime via GetUpTime() method on clients. On the authority server we can set the value of UpTime by accessing the controller of our component - MyFirstNetworkComponentController, using the following code:

        void MyFirstNetworkComponent::OnTick( [[maybe_unused]] float deltaTime, AZ::ScriptTimePoint time )
        {
            if ( HasController() )
            {
                auto* controller = static_cast<MyFirstNetworkComponentController*>( GetController() );
                controller->ModifyUpTime() = time.GetSeconds();
                AZ_Printf( "MyFirstNetworkComponent", "server = %f", GetUpTime() );
            }
            else
            {
                AZ_Printf( "MyFirstNetworkComponent", "client = %f", GetUpTime() );
            }
        }
    
    Note:
    A controller only exists on the authority server, thus this effectively splits the execution into server-only and client-only logic.
  4. Our full code for the component so far is as follows. The header is <o3de-multiplayersample>\Gem\Code\Source\Components\MyFirstNetworkComponent.h:

    #pragma once
    
    #include <Source/AutoGen/MyFirstNetworkComponent.AutoComponent.h>
    #include <AzCore/Component/TickBus.h>
    
    namespace MultiplayerSample
    {
        class MyFirstNetworkComponent
            : public MyFirstNetworkComponentBase
            , public AZ::TickBus::Handler
        {
        public:
            AZ_MULTIPLAYER_COMPONENT(MultiplayerSample::MyFirstNetworkComponent, s_myFirstNetworkComponentConcreteUuid, MultiplayerSample::MyFirstNetworkComponentBase);
    
            static void Reflect(AZ::ReflectContext* context);
    
            void OnInit() override;
            void OnActivate(Multiplayer::EntityIsMigrating entityIsMigrating) override;
            void OnDeactivate(Multiplayer::EntityIsMigrating entityIsMigrating) override;
    
            void OnTick(float deltaTime, AZ::ScriptTimePoint time) override;
        };
    }
    

    And the source, <o3de-multiplayersample>\Gem\Code\Source\Components\MyFirstNetworkComponent.cpp:

    #include <Source/Components/MyFirstNetworkComponent.h>
    
    #include <AzCore/Serialization/SerializeContext.h>
    
    namespace MultiplayerSample
    {
        void MyFirstNetworkComponent::Reflect( AZ::ReflectContext* context )
        {
            AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>( context );
            if ( serializeContext )
            {
                serializeContext->Class<MyFirstNetworkComponent, MyFirstNetworkComponentBase>()
                    ->Version( 1 );
            }
            MyFirstNetworkComponentBase::Reflect( context );
        }
    
        void MyFirstNetworkComponent::OnInit()
        {
        }
    
        void MyFirstNetworkComponent::OnActivate( [[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating )
        {
            AZ::TickBus::Handler::BusConnect();
        }
    
        void MyFirstNetworkComponent::OnDeactivate( [[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating )
        {
            AZ::TickBus::Handler::BusDisconnect();
        }
    
        void MyFirstNetworkComponent::OnTick( [[maybe_unused]] float deltaTime, AZ::ScriptTimePoint time )
        {
            if ( HasController() )
            {
                auto* controller = static_cast<MyFirstNetworkComponentController*>( GetController() );
                controller->ModifyUpTime() = time.GetSeconds();
                AZ_Printf( "MyFirstNetworkComponent", "server = %f", GetUpTime() );
            }
            else
            {
                AZ_Printf( "MyFirstNetworkComponent", "client = %f", GetUpTime() );
            }
        }
    }
    
  5. Now you have a multiplayer component with a network property.

Listen for Network Property Changes on Clients

However, let’s improve on this. A client has no need to tick, it can instead listen for changes. In order to do that, enable GenerateEventBindings. This is what we have at the moment.

  <NetworkProperty Type="double"
                   Name="UpTime"
...
                   GenerateEventBindings="false"
...
                    />

Change GenerateEventBindings to true:

  <NetworkProperty Type="double"
                   Name="UpTime"
...
                   GenerateEventBindings="true"
...
                    />

A project build will generate a new method: UpTimeAddEvent. Let’s put it to use in our code.

To listen for changes on a client in our component, do the following:

  1. Create an event handler.

    // create an event handler
    AZ::Event<double>::Handler m_uptimeChanged;
    
  2. Create a callback method.

    void OnUpTimeChanged( double uptime );
    
  3. Assign it to the event handler.

    // assign the callback to the event handler
    MyFirstNetworkComponent::MyFirstNetworkComponent()
        : m_uptimeChanged( [this]( double uptime ) {OnUpTimeChanged( uptime ); } )
    {
    }
    
  4. Connect the event handler to UpTimeAddEvent.

    // connect the event handler to the generated UpTimeAddEvent method
    void MyFirstNetworkComponent::OnActivate( [[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating )
    {
        UpTimeAddEvent( m_uptimeChanged );
    }
    
  5. And now instead of ticking on clients, print our log message from the callback:

    void MyFirstNetworkComponent::OnUpTimeChanged( double uptime )
    {
        AZ_Printf( "MyFirstNetworkComponent", "client = %f", uptime );
    }
    

After making these changes, MyFirstNetworkComponent.h should look like this:

#pragma once

#include <Source/AutoGen/MyFirstNetworkComponent.AutoComponent.h>
#include <AzCore/Component/TickBus.h>

namespace MultiplayerSample
{
    class MyFirstNetworkComponent
        : public MyFirstNetworkComponentBase
        , public AZ::TickBus::Handler
    {
    public:
        AZ_MULTIPLAYER_COMPONENT( MultiplayerSample::MyFirstNetworkComponent, s_myFirstNetworkComponentConcreteUuid, MultiplayerSample::MyFirstNetworkComponentBase );

        static void Reflect( AZ::ReflectContext* context );

        MyFirstNetworkComponent();

        void OnInit() override;
        void OnActivate( Multiplayer::EntityIsMigrating entityIsMigrating ) override;
        void OnDeactivate( Multiplayer::EntityIsMigrating entityIsMigrating ) override;

        void OnTick( float deltaTime, AZ::ScriptTimePoint time ) override;

    private:
        AZ::Event<double>::Handler m_uptimeChanged;
        void OnUpTimeChanged( double uptime );
    };
}

And MyFirstNetworkComponent.cpp should look like this:

#include <Source/Components/MyFirstNetworkComponent.h>

#include <AzCore/Serialization/SerializeContext.h>

namespace MultiplayerSample
{
    void MyFirstNetworkComponent::Reflect( AZ::ReflectContext* context )
    {
        AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>( context );
        if ( serializeContext )
        {
            serializeContext->Class<MyFirstNetworkComponent, MyFirstNetworkComponentBase>()
                ->Version( 1 );
        }
        MyFirstNetworkComponentBase::Reflect( context );
    }

    MyFirstNetworkComponent::MyFirstNetworkComponent()
        : m_uptimeChanged( [this]( double uptime ) {OnUpTimeChanged( uptime ); } )
    {
    }

    void MyFirstNetworkComponent::OnInit()
    {
    }

    void MyFirstNetworkComponent::OnActivate( [[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating )
    {
        UpTimeAddEvent( m_uptimeChanged );

        if ( HasController() )
        {
            AZ::TickBus::Handler::BusConnect();
        }
    }

    void MyFirstNetworkComponent::OnDeactivate( [[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating )
    {
        AZ::TickBus::Handler::BusDisconnect();
    }

    void MyFirstNetworkComponent::OnTick( [[maybe_unused]] float deltaTime, AZ::ScriptTimePoint time )
    {
        // only happens on Authority (server)
        auto* controller = static_cast<MyFirstNetworkComponentController*>( GetController() );
        controller->ModifyUpTime() = time.GetSeconds();
        AZ_Printf( "MyFirstNetworkComponent", "server = %f", GetUpTime() );
    }

    void MyFirstNetworkComponent::OnUpTimeChanged( double uptime )
    {
        AZ_Printf( "MyFirstNetworkComponent", "client = %f", uptime );
    }
}

Network Component Controllers

Let’s improve on the separation between server and client code. At the moment, MyFirstNetworkComponent performs both server and client duties. Let’s break it up. In O3DE Multiplayer, the way to do that is by using controllers. A controller does not exist on Client role but does exist on Authority role. We already saw a glimpse of that in our previous examples:

auto* controller = static_cast<MyFirstNetworkComponentController*>( GetController() );
  1. Go back to codegen XML, <o3de-multiplayersample>\Gem\Code\Source\AutoGen\MyFirstNetworkComponent.AutoComponent.xml, where the controller was mentioned in the attribute OverrideController:

    <Component Name="MyFirstNetworkComponent"
            Namespace="MultiplayerSample"
            OverrideComponent="true"
            OverrideController="false"
    
  2. Change OverrideController to true, so that we can write custom logic inside a controller.

    <Component Name="MyFirstNetworkComponent"
            Namespace="MultiplayerSample"
            OverrideComponent="true"
            OverrideController="true"
    
  3. Build the project.

  4. When attempting to build the code, you will get expected compile errors. That is because the codegen expects us to override the controller base class.

  5. Look at the code generated header, \path\to\o3de\build\-86ba765d\Gem\Code\Azcg\Generated\Source\AutoGen\MyFirstNetworkComponent.AutoComponent.h, you will find that the large comment has been modified by codegen and now contains code example of a controller:

    /// You may use the classes below as a basis for your new derived classes. Derived classes must be marked in MyFirstNetworkComponent.AutoComponent.xml
    /*
        class MyFirstNetworkComponentController
            : public MyFirstNetworkComponentControllerBase
        {
        public:
            MyFirstNetworkComponentController(MyFirstNetworkComponent& parent);
    
            void OnActivate(Multiplayer::EntityIsMigrating entityIsMigrating) override;
            void OnDeactivate(Multiplayer::EntityIsMigrating entityIsMigrating) override;
        };
    
        MyFirstNetworkComponentController::MyFirstNetworkComponentController(MyFirstNetworkComponent& parent)
            : MyFirstNetworkComponentControllerBase(parent)
        {
        }
    
        void MyFirstNetworkComponentController::OnActivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating)
        {
        }
    
        void MyFirstNetworkComponentController::OnDeactivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating)
        {
        }
    */
    
  6. Add this content to our existing <o3de-multiplayersample>\Gem\Code\Source\Components\MyFirstNetworkComponent.h and <o3de-multiplayersample>\Gem\Code\Source\Components\MyFirstNetworkComponent.cpp.

  7. Move all the server logic into MyFirstNetworkComponentController and keep the client logic in MyFirstNetworkComponent, as shown in the following updated files, <o3de-multiplayersample>\Gem\Code\Source\Components\MyFirstNetworkComponent.h:

    #pragma once
    
    #include <Source/AutoGen/MyFirstNetworkComponent.AutoComponent.h>
    #include <AzCore/Component/TickBus.h>
    
    namespace MultiplayerSample
    {
        class MyFirstNetworkComponent
            : public MyFirstNetworkComponentBase
        {
        public:
            AZ_MULTIPLAYER_COMPONENT( MultiplayerSample::MyFirstNetworkComponent, s_myFirstNetworkComponentConcreteUuid, MultiplayerSample::MyFirstNetworkComponentBase );
    
            static void Reflect( AZ::ReflectContext* context );
    
            MyFirstNetworkComponent();
    
            void OnInit() override {}
            void OnActivate( Multiplayer::EntityIsMigrating entityIsMigrating ) override;
            void OnDeactivate( Multiplayer::EntityIsMigrating entityIsMigrating ) override;
    
        private:
            AZ::Event<double>::Handler m_uptimeChanged;
            void OnUpTimeChanged( double uptime );
        };
    
        class MyFirstNetworkComponentController
            : public MyFirstNetworkComponentControllerBase
            , public AZ::TickBus::Handler
        {
        public:
            MyFirstNetworkComponentController(MyFirstNetworkComponent& parent);
    
            void OnActivate(Multiplayer::EntityIsMigrating entityIsMigrating) override;
            void OnDeactivate(Multiplayer::EntityIsMigrating entityIsMigrating) override;
    
            void OnTick( float deltaTime, AZ::ScriptTimePoint time ) override;
        };
    }
    
  8. Note the key difference in the header: AZ::TickBus::Handler was moved from MyFirstNetworkComponent to MyFirstNetworkComponentController in <o3de-multiplayersample>\Gem\Code\Source\Components\MyFirstNetworkComponent.cpp:

    #include <Source/Components/MyFirstNetworkComponent.h>
    
    #include <AzCore/Serialization/SerializeContext.h>
    
    namespace MultiplayerSample
    {
        void MyFirstNetworkComponent::Reflect( AZ::ReflectContext* context )
        {
            AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>( context );
            if ( serializeContext )
            {
                serializeContext->Class<MyFirstNetworkComponent, MyFirstNetworkComponentBase>()
                    ->Version( 1 );
            }
            MyFirstNetworkComponentBase::Reflect( context );
        }
    
        MyFirstNetworkComponent::MyFirstNetworkComponent()
            : m_uptimeChanged( [this]( double uptime ) { OnUpTimeChanged( uptime ); } )
        {
        }
    
        void MyFirstNetworkComponent::OnActivate( [[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating )
        {
            if (HasController() == false) // client only
            {
                UpTimeAddEvent( m_uptimeChanged );
            }
        }
    
        void MyFirstNetworkComponent::OnDeactivate( [[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating )
        {
        }
    
        void MyFirstNetworkComponent::OnUpTimeChanged( double uptime )
        {
            AZ_Printf( "MyFirstNetworkComponent", "client = %f", uptime );
        }
    
        /////////// Controller ////////////////
    
        MyFirstNetworkComponentController::MyFirstNetworkComponentController( MyFirstNetworkComponent& parent )
            : MyFirstNetworkComponentControllerBase( parent )
        {
        }
    
        void MyFirstNetworkComponentController::OnActivate( [[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating )
        {
            AZ::TickBus::Handler::BusConnect();
        }
    
        void MyFirstNetworkComponentController::OnDeactivate( [[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating )
        {
            AZ::TickBus::Handler::BusDisconnect();
        }
    
        void MyFirstNetworkComponentController::OnTick( [[maybe_unused]] float deltaTime, AZ::ScriptTimePoint time )
        {
            ModifyUpTime() = time.GetSeconds();
            AZ_Printf( "MyFirstNetworkComponent", "server = %f", GetUpTime() );
        }
    }
    
  9. Our code gen xml file <o3de-multiplayersample>\Gem\Code\Source\AutoGen\MyFirstNetworkComponent.AutoComponent.xml needs to be:

    <?xml version="1.0"?>
    <Component Name="MyFirstNetworkComponent"
            Namespace="MultiplayerSample"
            OverrideComponent="true"
            OverrideController="true"
            OverrideInclude="Source/Components/MyFirstNetworkComponent.h"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <NetworkProperty Type="double"
                    Name="UpTime"
                    Init="0.0"
                    ReplicateFrom="Authority"
                    ReplicateTo="Client"
                    IsRewindable="true"
                    IsPredictable="true"
                    IsPublic="true"
                    Container="Object"
                    ExposeToEditor="false"
                    ExposeToScript="false"
                    GenerateEventBindings="true"
                    Description="Time since the start of the application" />
    </Component>
    

And now we have a clean separation between server and client logic.

Add a Remote Procedure Call

Now that we have created a data flow from the server to clients using network properties, we will add a remote procedure call (RPC). Whenever a client will get a new UpTime it will call a new RPC - SendConfirmUptime and send the value it got back to the server. Just for fun.

  1. Add a codegen section that defines the RPC:

    <RemoteProcedure Name="SendConfirmUptime"
                    InvokeFrom="Autonomous"
                    HandleOn="Authority"
                    IsPublic="false"
                    IsReliable="false"
                    GenerateEventBindings="false"
                    Description="Uptime confirmed by the client">
        <Param Type="double"
            Name="UpTime" />
    </RemoteProcedure>
    

    Notice that InvokeFrom is Autonomous instead of Client. Autonomous is a special type of client behavior where a client initiates an action and may send data to the server.

    Autonomous is commonly used for player character controllers; in this case a player has to be able to act on its own without waiting for the server to tell it what to do. Meanwhile, ‘Client’ only mirrors what the server tells them to do.

  2. By default, only the player prefab is marked as Autonomous, so we will move MyFirstNetworkComponent to the player prefab for this tutorial.

    Important:
    Only entities that are autonomous will have controllers, otherwise GetController() will give a null on clients. If you are getting a null on your GetController calls then you have attached your component to an entity that is not autonomous. Attach them to player prefabs instead, as those are marked as autonomous by the server.

    The player prefab for MultiplayerSample project can be found at <o3de-multiplayersample>\Prefabs\Player.prefab. Do the following steps to modify Player.prefab:

    1. Instantiate a player prefab in the level temporarily. (You can find the player prefab for MultiplayerSample project at <o3de-multiplayersample>\Prefabs\Player.prefab.)

    2. Modify the player prefab instance by adding MyFirstNetworkComponent to player entity.

    3. Save the player prefab.

    4. Delete the player prefab from the level.

  3. Re-build MultiplayerSample.Static project. You will notice that the generated header for our component, \path\to\o3de\build\MultiplayerSample-6db9bd97\Gem\Code\Azcg\Generated\Source\AutoGen\MyFirstNetworkComponent.AutoComponent.h, has a new empty virtual method in the large comment block for MyFirstNetworkComponentController:

    Note:
    You might get thrown by this folder name MultiplayerSample-6db9bd97. This is a code-generated folder inside build folder. You might have a different name in your build folder.
    class MyFirstNetworkComponentController
    ...
    
        void HandleSendConfirmUptime(AzNetworking::IConnection* invokingConnection, const double& UpTime) override {}
    
  4. Take note of this and provide your own implementation of HandleSendConfirmUptime:

    void MyFirstNetworkComponentController::HandleSendConfirmUptime([[maybe_unused]] AzNetworking::IConnection* invokingConnection,
        const double& upTime)
    {
        AZLOG("MyFirstNetworkComponent", "on server - client told us about %f", upTime);
    }
    
  5. The client will invoke the RPC by referring to its controller:

    void MyFirstNetworkComponent::OnUpTimeChanged(double uptime)
    {
        AZLOG("MyFirstNetworkComponent", "client = %f", uptime);
        static_cast<MyFirstNetworkComponentController*>(GetController())->SendConfirmUptime(uptime);
    }
    

    Attaching Network Component to an Autonomous Entity

  6. Your component source code should be as follows for \path\to\MultiplayerSample\Gem\Code\Source\Components\MyFirstNetworkComponent.h:

    #pragma once
    
    #include <Source/AutoGen/MyFirstNetworkComponent.AutoComponent.h>
    #include <AzCore/Component/TickBus.h>
    
    namespace MultiplayerSample
    {
        class MyFirstNetworkComponent
            : public MyFirstNetworkComponentBase
        {
        public:
            AZ_MULTIPLAYER_COMPONENT( MultiplayerSample::MyFirstNetworkComponent, s_myFirstNetworkComponentConcreteUuid, MultiplayerSample::MyFirstNetworkComponentBase );
    
            static void Reflect( AZ::ReflectContext* context );
    
            MyFirstNetworkComponent();
    
            void OnInit() override {}
            void OnActivate( Multiplayer::EntityIsMigrating entityIsMigrating ) override;
            void OnDeactivate( Multiplayer::EntityIsMigrating entityIsMigrating ) override;
    
        private:
            AZ::Event<double>::Handler m_uptimeChanged;
            void OnUpTimeChanged( double uptime );
        };
    
        class MyFirstNetworkComponentController
            : public MyFirstNetworkComponentControllerBase
            , public AZ::TickBus::Handler
        {
        public:
            MyFirstNetworkComponentController(MyFirstNetworkComponent& parent);
    
            void OnActivate(Multiplayer::EntityIsMigrating entityIsMigrating) override;
            void OnDeactivate(Multiplayer::EntityIsMigrating entityIsMigrating) override;
    
            void OnTick( float deltaTime, AZ::ScriptTimePoint time ) override;
    
            void HandleSendConfirmUptime(AzNetworking::IConnection* invokingConnection, const double& UpTime) override;
        };
    }
    
  7. And the source \path\to\MultiplayerSample\Gem\Code\Source\Components\MyFirstNetworkComponent.cpp should be:

    #include <Source/Components/MyFirstNetworkComponent.h>
    
    #include <AzCore/Serialization/SerializeContext.h>
    #include <Multiplayer/Components/NetBindComponent.h>
    
    namespace MultiplayerSample
    {
        void MyFirstNetworkComponent::Reflect(AZ::ReflectContext* context)
        {
            AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context);
            if (serializeContext)
            {
                serializeContext->Class<MyFirstNetworkComponent, MyFirstNetworkComponentBase>()
                    ->Version(1);
            }
            MyFirstNetworkComponentBase::Reflect(context);
        }
    
        MyFirstNetworkComponent::MyFirstNetworkComponent()
            : m_uptimeChanged([this](double uptime) { OnUpTimeChanged(uptime); })
        {
        }
    
        void MyFirstNetworkComponent::OnActivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating)
        {
            if (IsNetEntityRoleAutonomous())
            {
                UpTimeAddEvent(m_uptimeChanged); // listen only on clients
            }
        }
    
        void MyFirstNetworkComponent::OnDeactivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating)
        {
        }
    
        void MyFirstNetworkComponent::OnUpTimeChanged(double uptime)
        {
            AZLOG("MyFirstNetworkComponent", "client = %f", uptime);
            static_cast<MyFirstNetworkComponentController*>(GetController())->SendConfirmUptime(uptime);
        }
    
        /////////// Controller ////////////////
    
        MyFirstNetworkComponentController::MyFirstNetworkComponentController(MyFirstNetworkComponent& parent)
            : MyFirstNetworkComponentControllerBase(parent)
        {
        }
    
        void MyFirstNetworkComponentController::OnActivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating)
        {
            AZ::TickBus::Handler::BusConnect();
        }
    
        void MyFirstNetworkComponentController::OnDeactivate([[maybe_unused]] Multiplayer::EntityIsMigrating entityIsMigrating)
        {
            AZ::TickBus::Handler::BusDisconnect();
        }
    
        void MyFirstNetworkComponentController::OnTick([[maybe_unused]] float deltaTime, AZ::ScriptTimePoint time)
        {
            ModifyUpTime() = time.GetSeconds();
            AZLOG("MyFirstNetworkComponent", "server = %f", GetUpTime());
        }
    
        void MyFirstNetworkComponentController::HandleSendConfirmUptime([[maybe_unused]] AzNetworking::IConnection* invokingConnection,
            const double& upTime)
        {
            AZLOG("MyFirstNetworkComponent", "on server - client told us about %f", upTime);
        }
    }
    
  8. Our xml for the code generator, <o3de-multiplayersample>\Gem\Code\Source\AutoGen\MyFirstNetworkComponent.AutoComponent.xml:

    <?xml version="1.0"?>
    <Component Name="MyFirstNetworkComponent"
            Namespace="MultiplayerSample"
            OverrideComponent="true"
            OverrideController="true"
            OverrideInclude="Source/Components/MyFirstNetworkComponent.h"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <NetworkProperty Type="double"
                    Name="UpTime"
                    Init="0.0"
                    ReplicateFrom="Authority"
                    ReplicateTo="Client"
                    IsRewindable="true"
                    IsPredictable="true"
                    IsPublic="true"
                    Container="Object"
                    ExposeToEditor="false"
                    ExposeToScript="false"
                    GenerateEventBindings="true"
                    Description="Time since the start of the application" />
    <RemoteProcedure Name="SendConfirmUptime"
                    InvokeFrom="Autonomous"
                    HandleOn="Authority"
                    IsPublic="false"
                    IsReliable="false"
                    GenerateEventBindings="false"
                    Description="Uptime confirmed by the client">
        <Param Type="double"
            Name="UpTime" />
    </RemoteProcedure>
    </Component>
    

Copyright © 2022 Open 3D Engine Contributors

Documentation Distributed under CC BY 4.0.
For our trademark, privacy and antitrust policies, code of conduct, and terms of use, please click the applicable link below or see https://www.lfprojects.org.


The Linux Foundation has registered trademarks and uses trademarks. For a list of trademarks of The Linux Foundation, please see our Trademark Policy page.