Skip to main content

Multiplayer Programming Quick Start

Developing game processes for multiplayer games require implementing replication in the game's GameObject. It is also necessary to design features specific to the Server (acting as the host of the game session) and the Client (representing the player connected to the session).

This demo includes the following contents.

  • How to add replication to a base GameObject.
  • How to add replication and a callback function to a variable after the variable has been modified.
  • How to use Remote Procedure Calls (RPCs) in C++.
  • How to distinguish if the GameObject is on the Server or on the Client.

There will be a third-person multiplayer game in the end, where players can shoot projectiles at each other and cause damage.

Basic Settings

Click File -> Create Project in the Editor, and select Third Person Project C++ in the pop-up Create Project window, then fill in the Path and Name respectively, check Include Beginner Content, and click the Create button to create a new project.

image-20230217141911822

After the new project is created, click Build -> Windows Solution -> Generate to generate the new project.

generate

Starting the Game

The effect is as follows: standalong

A simple multiplayer game can be implemented by changing only some of the settings, and the network configuration is described as follows:

Startup Options Instructions

launchOp

PropertyDescription
Start StandaloneThe Editor starts the game in standalone mode.
Start As Listen ServerThe Editor starts as a listen Server, and if the configured Number of Players is more than 1, then additional Clients of one less than the players are connected to the Server.
Start As Client With Entry ServerThe Editor starts as a Client, and both the Entry Server and Member Server are created. The Editor is connected to the Entry Server as a Client. And if the configured Number of Players is more than 1, then an additional instance of the Client process is started.
Start As Client With Member ServerThe Editor starts as a Client, and the Member Server is created. The Editor is connected to the Member Server as a Client. And if the configured Number of Players is more than 1, then an additional instance of the Client process is started.
Number Of PlayersThe number of players in networked mode.

Network Connection Setup Instructions

Network Settings must be set before the Editor is started. Click Config -> Project Settings -> Network Settings to open the settings interface.

netmod

PropertyDescription
Connect SettingsReconnect Duration: The timeout duration in seconds for the Client to reconnect to the Server.
Entry Server SettingsAuto Login: Whether to enable auto login.
Server Port: The port number that the Entry Server listens on.
Member Server List: The list of Member Server to run.
Startup Scene: The path to the Member Server's startup scene.
Member Server SettingsStartup Scene: The path to the Member Server's startup scene, and also the path to the scene when the Editor is started in Listen Server mode.
Server Port: The port number that the Member Server listens on, and also the port number when the Editor starts in Listen Server mode.

In this project, the project configuration is as follows.

image-20230320152123015

After starting the game, the effect is as follows. begin

It's already a functioning multiplayer game by far, but players can only move and jump. Next, let's give it some gameplay.

Adding and Displaying the HP Progressbar

Opening C++ Projects

Click Build -> Windows Solution -> Open in the Menu Bar.

133234334240719452

  1. Add the replication property to tp_pawn.h:
protected:
// Max HP
FX_PROPERTY(Type = float, GetFunc = GetMaxHp, Name = MaxHp, NetProperty)
float m_fMaxHp;

// Current HP
FX_PROPERTY(Type = float, GetFunc = GetHp, SetFunc = SetHp, Name = CurrentHp, NetProperty)
float m_fCurrentHp;

The CurrentHp will be synced to all Clients, so add the NetProperty property Meta.

Another property Meta NetPropertyNotify=OnFunc represents the callback function that will be triggered when the property is synced.

Open tp_pawn.h, and add the following code to the class definition under public:

float GetMaxHp() const { return m_fMaxHp; }
void SetHp(float fHp) { m_fCurrentHp = fHp; }
float GetHp() const { return m_fCurrentHp; }

Add the following code to the constructor in tp_pawn.cpp.

m_fMaxHp = 100.0f;
m_fCurrentHp = m_fMaxHp;

2) Add the progressbar HP display to the script:

First, create the UI asset form_state_bar.ui (Please refer to ProgressBar for details):

ui_asset

Save the asset in the following directory:

asset

Set the corresponding script name and add the event callback: asset_script

Please refer to Appendix I for the resource code of the final generated asset form_state_bar.ui.

Create the script file form_state_bar.lua:

script

Add the following code to form_state_bar.lua:

--character properties UI
--State Bar UI

--------------
--local data--
--------------

local THIS_SCRIPT = "share\\ui\\form_state_bar"
local THIS_SKIN = "skin\\ui\\form\\form_state_bar.ui"

------------------
--local function--
------------------
local function get_form()
return nx_value(THIS_SCRIPT)
end

function get_scene_box()
local lscene = nx_value("lscene")

if not nx_is_valid(lscene) then
return nx_null()
end

return lscene.SceneBox
end

function load_form(form_name, form_res, scene_box)
form_name = nx_string(form_name)

form_res = nx_string(form_res)

local form = nx_null()

local gui = nx_value("gui")

local path = nx_resource_path()

if nil == string.find(form_res, ".ui") then
form_res = form_res.. ".ui"
end

form = gui.Loader:LoadForm(path, form_res)

if not nx_is_valid(form) then
nx_msgbox("msg_CreateFormFailed - ".. form_res)
return nx_null()
end

form.Name = form_name
form.Left = 0
form.Top = 0

scene_box:Add(form)
return form
end

local function setup_form(form)
local scene_box = get_scene_box()

form.Left = 40
form.Top = scene_box.Height - form.Height - 30
form.HAnchor = "Auto"
form.VAnchor = "Auto"

form.ProgressBar_HP.Value = 0
form:AddTopLayer(form.ProgressBar_HP)
end

---------------------
--control callbacks--
---------------------

function main_form_open(self)
nx_set_value(THIS_SCRIPT, self)
setup_form(self)

return 1
end

function main_form_close(self)
nx_destroy(self)
return 1
end

--------------------
--public functions--
--------------------
function open_state_bar(character)

local form = get_form()

if not nx_is_valid(form) then
form = load_form("form_state_bar", THIS_SKIN, get_scene_box())
form:Show()
form.owner = character
end
end

function close_state_bar()
local form = get_form()

if nx_is_valid(form) then
form:Close()
end
end

Go to the script file directory to open the tp_pawn.lua script, and in the on_begin_play method, add the execution of the open_state_bar method after the script and the entity are bound.

if nx_is_valid(owner) then
nx_bind_script(owner, nx_current())
--THe added code
nx_execute("share\\ui\\form_state_bar", "open_state_bar", owner)

The effect is as follows: show_hp

At this point, when the Client window opens, a progressbar HP will appear in the lower left corner of the screen.

Adding Projectile Classes and Shooting Projectiles

Creating the Projectile Class LFPSProjectile

Create files fps_projectile.h and fps_projectile.cpp.

newFile

Open the project and add the files to it:

addFile

addFile

Open fps_projectile.h and add the following code to the class definition under protected:

#ifndef _FPS_PROJECTLE_H_
#define _FPS_PROJECTLE_H_

#include "fx_component/game_object.h"

class LProjectileMovementComponent;
class LSphereComponent;
class LModelComponent;

class LFPSProjectile : public LGameObject
{
DECLARE_LCLASS(LFPSProjectile, LGameObject, COMPILED_IN_FLAGS(0))

public:
LFPSProjectile();
virtual ~LFPSProjectile();

virtual bool Init(const IVarList& args) override;
virtual void BeginPlay() override;
virtual void Update(float seconds) override;

protected:
// The Sphere component, used for collision detection.
LSphereComponent* m_pCollisionComponent;
// The projectile movement component, used for moving projectiles.
LProjectileMovementComponent* m_pProjectileMovement;
// The static mesh component, used for visual presentation of projectiles.
LModelComponent* m_pModelComponent;

} FX_ENTITY(Parent = LGameObject, EditorVisible, Category = Game);
#endif // _FPS_PROJECTLE_H_

Add the following code to fps_projectile.cpp:

#include "fps_projectile.h"

#include "tp_pawn.h"
#include "fx_component/scene.h"
#include "flexi/utils/fx_cast.h"
#include "fx_component/sphere_component.h"
#include "fx_component/projectile_movement_component.h"
#include "fx_component/model_component.h"

// Code gen
#include "code_gen/fps_projectile.gen.h"

IMPLEMENT_LCLASS(LFPSProjectile)

LFPSProjectile::LFPSProjectile()
{
// Replicable object
SetAddNetScene(true);
// Open in frame loop
m_GameObjectUpdate.bAllowUpdate = true;

m_pCollisionComponent = nullptr;
m_pProjectileMovement = nullptr;
m_pModelComponent = nullptr;

SetStringTag("FPSProjectile");
}

LFPSProjectile::~LFPSProjectile()
{
}

Add the following code to the Init function in fps_projectile.cpp:

bool LFPSProjectile::Init(const IVarList& args)
{
if (!Super::Init(args))
{
return false;
}

// Set the collider
if (!m_pCollisionComponent)
{
m_pCollisionComponent = NewObject<LSphereComponent>(this);
m_pCollisionComponent->SetSphereRadius(0.101f);
m_pCollisionComponent->SetCollisionEnabledAttr(COLLISION_ENABLED::NoCollision);
m_pCollisionComponent->SetCollisionProfileName("BlockAllDynamic");
m_pRootComponent = m_pCollisionComponent;
}

// Add the mesh
if (!m_pModelComponent)
{
m_pModelComponent = NewObject<LModelComponent>(this);
m_pModelComponent->SetLinkTarget(m_pCollisionComponent);
m_pModelComponent->SetScaleVector(XMVectorSet(1.1f, 1.1f, 1.1f, 0.f));
m_pModelComponent->SetRelativePositionY(-0.05f);
m_pModelComponent->SetPositionY(-0.1f);
IVisBase* pVisbase = m_pModelComponent->GetVisBase();
m_pModelComponent->SetCollisionEnabledAttr(COLLISION_ENABLED::NoCollision);
IModel* pModel = FX_KIND_CAST(IModel, pVisbase, "fx_world.Model");
if (pModel)
{
pModel->SetModelFile(FXTEXT("BeginnerContent/BaseShape/Box.xmod"));
g_pCore->SetProperty(pVisbase, "AutoUpdateLOD", CVar(VTYPE_BOOL, false));
}

if (GetScene())
{
m_pModelComponent->Load();
}
}


// Add projectile movement and collision
if (!m_pProjectileMovement)
{
m_pProjectileMovement = NewObject<LProjectileMovementComponent>(this);
m_pProjectileMovement->SetUpdatedComponent(m_pRootComponent);
m_pProjectileMovement->SetInitialSpeed(10.0f);
m_pProjectileMovement->SetMaxSpeed(10.0f);
m_pProjectileMovement->SetShouldBounce(false);
m_pProjectileMovement->SetRotationFollowsVelocity(false);
m_pProjectileMovement->SetProjectileGravityScale(0.0f);
}
return true;
}

Open fps_projectile.h and add the following code to the class definition under public:

public:
void OnProjectileHit(LPrimitiveComponent* pComponent, LGameObject* pOtherActor,LPrimitiveComponent* pOtherComponent, CXMVECTOR vImpluse,const hit_result_t& hit);

Add the projectile collision callback function, and the method will be registered in the collision component of the projectile.

Add the following code to fps_projectile.cpp:

void LFPSProjectile::BeginPlay()
{
Super::BeginPlay();
m_pCollisionComponent->GetComponentHit().add(this, &LFPSProjectile::OnProjectileHit);
}

void LFPSProjectile::OnProjectileHit(LPrimitiveComponent* pComponent, LGameObject* pOtherActor, LPrimitiveComponent* pOtherComponent, CXMVECTOR vImpluse, const hit_result_t& hit)
{
}

Open fps_projectile.h and add the following code to the class definition under protected:

float m_fLifeTime;
float m_fBornTime;

Define LifeTime and BornTime. If the projectile does not collide with an object in the scene, the projectile class will be destroyed by the two parameters.

Add the following code to the constructor in fps_projectile.cpp.

m_fLifeTime = 3.0f;
m_fBornTime = 0.f;

Add the following code to the very end of the Init function in fps_projectile.cpp above return true; :

if (GetScene())
{
m_fBornTime = GetScene()->GetTimeSeconds();
}

Add a header file to fps_projectile.cpp:

#include "fx_component/network/network_compatible.h"

Add the following code to the Update function in fps_projectile.cpp:

void LFPSProjectile::Update(float seconds)
{
Super::Update(seconds);
// Server code
if (Networkcompatible::GetSceneGameMode(GetScene()))
{
float nowTime = GetScene()->GetTimeSeconds();
if (m_fBornTime > 0.f && nowTime - m_fBornTime > m_fLifeTime)
{
DestroyGameObject();
}
}
}

The projectile class can be replicated from the Server to the Client. With collision callbacks added to handle the collision logic, the projectiles will be destroyed on time if there is no collision happened.

Shooting Projectiles

Open the file tp_pawn.lua and add the following code:

--Left-click pressed callback
function on_left_click_pressed(owner)
owner:Fire()
return 1
end

Open tp_pawn.h and add the code to public:

// RPC Function
FX_METHOD()
void Fire();
FX_METHOD()
void Fire_Server();

The Engine's RPC feature is applied here. The Client presses the key to trigger the event Fire(), and Fire() will remotely process a call to the Server's Fire_Server(), at which point the Server creates the projectile. Since the projectile is a replicable GameObject, each connected Client will generate the corresponding projectile at this time.

Open tp_pawn.cpp and add the following code:

#include "fx_component/network/network_compatible.h"

void LThirdPersonPawn::Fire()
{
auto* pScene = GetScene();
if (!pScene)
{
return;
}

// Server
if (Networkcompatible::GetSceneGameMode(GetScene()))
{
// Get the location and orientation of pawn.
XMVECTOR projectileRotation = GetTransform().GetAngle();
XMVECTOR projectileLocation = GetTransform().GetLocation();

// Move the shoot position forward a little to prevent collision with yourself.
projectileLocation = XMVectorAdd(projectileLocation,
GetTransform().TransformVectorNoScale(XMVectorSet(0.f, 0.f, 2.f, 0.f)));

spawn_parameters_t spawnParams;
spawnParams.pActorOwner = this;

TKindOfEntInfo<LGameObject> actorType
= TypeExtentManager::GetEntInfoByName("LFPSProjectile");

// Create the projectile in the scene.
pScene->SpawnGameObject(actorType, &projectileLocation, &projectileRotation, spawnParams);

return;
}

pScene->ServerRemoteFunction(this, "Fire_Server");
}

// rpc server calls
void LThirdPersonPawn::Fire_Server()
{
auto* pScene = GetScene();
if (!pScene)
{
return;
}

// Server
// Get the location and orientation of pawn.
XMVECTOR projectileRotation = GetTransform().GetAngle();
XMVECTOR projectileLocation = GetTransform().GetLocation();

// Move the shoot position forward a little to prevent collision with yourself.
projectileLocation = XMVectorAdd(projectileLocation,
GetTransform().TransformVectorNoScale(XMVectorSet(0.f, 0.f, 1.f, 0.f)));

spawn_parameters_t spawnParams;
spawnParams.pActorOwner = this;

TKindOfEntInfo<LGameObject> actorType
= TypeExtentManager::GetEntInfoByName("LFPSProjectile");

// Create the projectile in the scene.
pScene->SpawnGameObject(actorType, &projectileLocation, &projectileRotation, spawnParams);
}

Synchronizing Player Damaged States to the HP Progressbar

Open tp_pawn.h and add the following code under public:.

public:
void SetIsHit(bool bHit) { m_bHit = bHit; }
bool GetIsHit() const { return m_bHit; }

void SetIsDead(bool bDead) { m_bDead = bDead; }
bool GetIsDead() const { return m_bDead; }
protected:
// Whether the attack is hit
bool m_bHit;
bool m_bDead;

Add hit and dead states to pawn to change HP, etc.

Open tp_pawn.h and add the following code:

// When attacked
void OnBeAttacked();

Initialize m_bHit and m_bDead in the constructor of tp_pawn.cpp

m_bHit = false;
m_bDead = false;

Open tp_pawn.cpp and add the following code:

// Server code
void LThirdPersonPawn::OnBeAttacked()
{
if (m_bHit)
{
SetIsHit(false);
m_fCurrentHp = m_fCurrentHp - 50.0f;
if (m_fCurrentHp <= 0.0f)
{
SetIsDead(true);
}

if (GetIsDead())
{
DestroyGameObject();
}
}
}

void LThirdPersonPawn::Update(float seconds)
{
Super::Update(seconds);

// Handling of character getting hit
if (Networkcompatible::GetSceneGameMode(GetScene()))
{
OnBeAttacked();
}
}

The Server will get the GameMode and use this method to distinguish whether the current execution side is a Server or a Client.

Open fps_projectile.cpp and add the following code to the OnProjectileHit method:

    // Server code for handling of hitting different Actors 
if (Networkcompatible::GetSceneGameMode(GetScene()))
{
LThirdPersonPawn *pThridPerson = Cast<LThirdPersonPawn>(pOtherActor);
if (pThridPerson != NULL)
{
pThridPerson->SetIsHit(true);
}
DestroyGameObject();
}

When a projectile collides with other pawn, the collided pawn will set to be the attacked state and destroy the projectile. This logic occurs on the Server.

CurrentHp is a network replication property, and connected Clients will receive real-time HP of the target character. Then in the form_state_bar.lua script, the HP is monitored for modification in real-time:

function get_global_list(list_name)
local global_list = nx_value(list_name)

if not nx_is_valid(global_list) then
global_list = nx_create("ArrayList", list_name)
nx_set_value(list_name, global_list)
end

return global_list
end

function listen_command(cmd, ident, script, func, ...)
local command_hooks = get_global_list("command_hooks")

if not command_hooks:FindChild(cmd) then
command_hooks:CreateChild(cmd)
end

if ident == "" then
ident = "all"
end

local cmd_node = command_hooks:GetChild(cmd)

if not cmd_node:FindChild(ident) then
cmd_node:CreateChild(ident)
end

local ident_node = cmd_node:GetChild(ident)

if not ident_node:FindChild(script) then
ident_node:CreateChild(script)
end

local script_node = ident_node:GetChild(script)

if not script_node:FindChild(func) then
script_node:CreateChild(func)
end

local func_node = script_node:GetChild(func)

local arg_table = { unpack(arg) }

for i, t_arg in ipairs(arg_table) do
local arg_child = func_node:CreateChild(nx_string(i))
arg_child.value = t_arg
end
end


function on_tick(seconds)
local form = get_form()

if not nx_is_valid(form) then
return
end

local main_player = form.owner

if nx_is_valid(main_player) then
local max_hp = main_player.MaxHp
local hp = main_player.CurrentHp
local value_hp = hp / max_hp * 100
form.ProgressBar_HP.Value = value_hp

local label_hp = form.Label_HP
label_hp.Text = nx_widestr(hp)
nx_pause(0.0)
end
end

Register the on_tick function in the form_state_bar.lua script, and add the following code to the front of the return value of the main_form_open function:

listen_command("tick", "tp_pawn", THIS_SCRIPT, "on_tick")

Throw the player tick externally in tp_pawn.lua, and add the following code at the end of the tick function:

command("tick", "tp_pawn", delta_time)

Add the following function to tp_pawn.lua:

function get_global_list(list_name)
local global_list = nx_value(list_name)

if not nx_is_valid(global_list) then
global_list = nx_create("ArrayList", list_name)

nx_set_value(list_name, global_list)
end

return global_list
end

function command(cmd, ident, ...)
local command_hooks = get_global_list("command_hooks")

local cmd_node = command_hooks:GetChild(cmd)

if not nx_is_valid(cmd_node) then
return
end

local ident_table = { "all" }

if ident ~= "" and ident ~= "all" then
ident_table[#ident_table + 1] = ident
end

for _, t_ident in pairs(ident_table) do
local ident_node = cmd_node:GetChild(t_ident)

if nx_is_valid(ident_node) then
local script_node_table = ident_node:GetChildList()

for _, script_node in pairs(script_node_table) do
local script = script_node.Name

local func_node_table = script_node:GetChildList()

for _, func_node in pairs(func_node_table) do
local func = func_node.Name

--callback parameters: { commander's..., listener's... }
local full_arg_table = {}

local arg_table = { ... }

for _, t_arg in ipairs(arg_table) do
full_arg_table[#full_arg_table + 1] = t_arg
end

local arg_node_talbe = func_node:GetChildList()

for i, arg_node in ipairs(arg_node_talbe) do
full_arg_table[#full_arg_table + 1] = arg_node.value
end

nx_execute(script, func, unpack(full_arg_table))
end
end
end
end
end

Now the simple multiplayer networked shooter game is completed.

Displaying the Result

Each of the two Clients has its own HP display.show_fire_before1 After the left Client shoots, the right Client's HP is reduced by 50. show_fire_after2 After the right Client shoots, the left Client character is destroyed and neither the left nor the right character is displayed. show_die

Code Samples

tp_pawn.h

#ifndef _THIRDPERSON_PAWN_H_
#define _THIRDPERSON_PAWN_H_

#include "fx_component/gameplay/role.h"

// LThirdPersonPawn
class LThirdPersonPawn : public LRole
{
DECLARE_LCLASS(LThirdPersonPawn, LRole, COMPILED_IN_FLAGS(0))

public:
LThirdPersonPawn();
virtual ~LThirdPersonPawn();

virtual bool Init(const IVarList& args) override;
virtual void Update(float seconds) override;
virtual void PostInitializeComponents() override;

FX_METHOD()
void OnMouseWheelInput(float fValue);

float GetMaxHp() const { return m_fMaxHp; }
void SetHp(float fHp) { m_fCurrentHp = fHp; }
float GetHp() const { return m_fCurrentHp; }

//RPC Function
FX_METHOD()
void Fire();
FX_METHOD()
void Fire_Server();

void SetIsHit(bool bHit) { m_bHit = bHit; }
bool GetIsHit() const { return m_bHit; }
void SetIsDead(bool bDead) { m_bDead = bDead; }
bool GetIsDead() const { return m_bDead; }
void OnBeAttacked();

protected:
// Max HP
FX_PROPERTY(Type = float, GetFunc = GetMaxHp, Name = MaxHp, NetProperty)
float m_fMaxHp;

// Current HP
FX_PROPERTY(Type = float, GetFunc = GetHp, SetFunc = SetHp, Name = CurrentHp, NetProperty)
float m_fCurrentHp;

bool m_bHit;
bool m_bDead;
}FX_ENTITY(Parent = LRole, EditorVisible, Category = Game);
#endif //_THIRDPERSON_PAWN_H_

tp_pawn.cpp

#include "tp_pawn.h"

// Engine
#include "fx_component/scene.h"
#include "fx_component/camera_component.h"
#include "fx_component/capsule_component.h"
#include "fx_component/components/arrow_component.h"
#include "fx_component/gameplay/net_slide_movement_component.h"
#include "fx_component/script_component.h"
#include "fx_component/skeletal_mesh_component.h"
#include "fx_component/spring_arm_component.h"
#include "fx_component/network/network_compatible.h"

// Code gen
#include "code_gen/tp_pawn.gen.h"

IMPLEMENT_LCLASS(LThirdPersonPawn)

// LThirdPersonPawn
LThirdPersonPawn::LThirdPersonPawn()
{
SetAddNetScene(true);
m_fMaxHp = 100.0f;
m_fCurrentHp = m_fMaxHp;
m_bHit = false;
m_bDead = false;
}

LThirdPersonPawn::~LThirdPersonPawn()
{
}

bool LThirdPersonPawn::Init(const IVarList& args)
{
if (!Super::Init(args))
{
return false;
}

m_bUseControllerAngleYaw = true;
m_bUseControllerAnglePitch = false;

m_pCapsuleComponent = NewObject<LCapsuleComponent>(this, "TPPawnCollision");
if (m_pCapsuleComponent)
{
m_pCapsuleComponent->InitCapsuleSize(0.5F, 1.0F);
m_pCapsuleComponent->SetCollisionProfileName(
LCollisionProfile::Get()->GetPawnProfileName());
}
m_pRootComponent = m_pCapsuleComponent;

m_pArrowComponent = NewObject<LArrowComponent>(this, FXTEXT("TPPawnArrow"),
OBJECT_FLAGS::Transient);
if (m_pArrowComponent)
{
m_pArrowComponent->SetIsEditorOnly(true);
m_pArrowComponent->SetArrowColor(COLOR_ARGB(255, 150, 200, 255));
m_pArrowComponent->SetLinkTarget(m_pRootComponent);
}

m_pMeshComponent = NewObject<LSkeletalMeshComponent>(this, "TPPawnMesh");
if (m_pMeshComponent)
{
m_pMeshComponent->GetComponentUpdate().nUpdateGroup = UPDATE_GROUP_ENUM::PrePhysics;
m_pMeshComponent->SetLinkTarget(m_pRootComponent);
m_pMeshComponent->SetCollisionProfileName(
LCollisionProfile::Get()->GetNoCollisionProfileName());
m_pMeshComponent->SetRelativePosition(0.0f, -1.0f, 0.0f);
m_pMeshComponent->SetSkeletalMeshConfigFile(
FXTEXT("obj/tp_pawn/Meshes/SK_Human.actor"));

if (GetScene() != NULL)
{
m_pMeshComponent->Load();
}
}

m_pScriptComponent = NewObject<LScriptComponent>(this, FXTEXT("TPPawnScript"));
if (m_pScriptComponent)
{
m_pScriptComponent->SetScriptFile("tp_pawn.lua");
}

m_pSpringArm = NewObject<LSpringArmComponent>(this, FXTEXT("TPPawnSpringArm"));
if (m_pSpringArm)
{
m_pSpringArm->SetLinkTarget(GetCollisionRoot());
m_pSpringArm->SetTargetArmLength(5.0F);
m_pSpringArm->SetRelativePositionY(1.25f);
m_pSpringArm->SetRelativePositionX(0.25f);
m_pSpringArm->SetUsePawnControlRotation(true);
}

if (m_pSpringArm)
{
m_pCamera = NewObject<LCameraComponent>(this, FXTEXT("TPPawnCamera"));
if (m_pCamera)
{
m_pCamera->SetLinkTarget(m_pSpringArm);
}
}

m_pMovementComponent = NewObject<LNetSlideMovementComponent>(this,
"SlideMovementComp");

// Set net movement data synchronization
SetNetMovement(true);

return true;
}

void LThirdPersonPawn::OnBeAttacked()
{
if (m_bHit)
{
SetIsHit(false);
m_fCurrentHp = m_fCurrentHp - 50.0f;
if (m_fCurrentHp <= 0.0f)
{
SetIsDead(true);
}

if (GetIsDead())
{
DestroyGameObject();
}
}
}


void LThirdPersonPawn::Update(float seconds)
{
Super::Update(seconds);

// Deal with characters being hit
if (Networkcompatible::GetSceneGameMode(GetScene()))
{
OnBeAttacked();
}
}

void LThirdPersonPawn::PostInitializeComponents()
{
Super::PostInitializeComponents();

if (m_pMeshComponent)
{
// force animation update after movement component updates
if (m_pMeshComponent->GetComponentUpdate().bAllowUpdate &&
m_pMovementComponent)
{
m_pMeshComponent->GetComponentUpdate().AddDependency(
m_pMovementComponent, m_pMovementComponent->GetComponentUpdate());
}

XMStoreFloat3(&m_vMeshPositionOffset, m_pMeshComponent->GetRelativePosition());
XMStoreFloat3(&m_vMeshAngleOffset, m_pMeshComponent->GetRelativeAngle());
}

// Move the set update component of the move component here, because the previous location scene has not been initialized yet
if (m_pMovementComponent)
{
m_pMovementComponent->SetUpdatedComponent(GetCollisionRoot());
}
}

void LThirdPersonPawn::OnMouseWheelInput(float fValue)
{
// Stretch spring arm
if (m_pSpringArm)
{
// Scroll wheel backward for plus, forward for minus
float fSpringArmSpeed = 0.2F;
float fMinArmLength = -2.0F;
float fCurrentArmLength = m_pSpringArm->GetTargetArmLength();
m_pSpringArm->SetTargetArmLength(fCurrentArmLength + fValue * fSpringArmSpeed);
}
}

void LThirdPersonPawn::Fire()
{
auto* pScene = GetScene();
if (!pScene)
{
return;
}

// The server
if (Networkcompatible::GetSceneGameMode(GetScene()))
{
// Get the location and orientation of the pawn
XMVECTOR projectileRotation = GetTransform().GetAngle();
XMVECTOR projectileLocation = GetTransform().GetLocation();

// Move the shoot position forward a little to prevent collision with yourself
projectileLocation = XMVectorAdd(projectileLocation,
GetTransform().TransformVectorNoScale(XMVectorSet(0.f, 0.f, 2.f, 0.f)));

spawn_parameters_t spawnParams;
spawnParams.pActorOwner = this;

TKindOfEntInfo<LGameObject> actorType
= TypeExtentManager::GetEntInfoByName("LFPSProjectile");

// Create the projectile in the scene
pScene->SpawnGameObject(actorType, &projectileLocation, &projectileRotation, spawnParams);

return;
}

pScene->ServerRemoteFunction(this, "Fire_Server");
}

void LThirdPersonPawn::Fire_Server()
{
auto* pScene = GetScene();
if (!pScene)
{
return;
}

// The server
// Get the location and orientation of pawn
XMVECTOR projectileRotation = GetTransform().GetAngle();
XMVECTOR projectileLocation = GetTransform().GetLocation();

// Move the shoot position forward a little to prevent collision with yourself
projectileLocation = XMVectorAdd(projectileLocation,
GetTransform().TransformVectorNoScale(XMVectorSet(0.f, 0.f, 1.f, 0.f)));

spawn_parameters_t spawnParams;
spawnParams.pActorOwner = this;

TKindOfEntInfo<LGameObject> actorType
= TypeExtentManager::GetEntInfoByName("LFPSProjectile");

// Create the projectile in the scene
pScene->SpawnGameObject(actorType, &projectileLocation, &projectileRotation, spawnParams);
}

fps_projectile.h

#ifndef _FPS_PROJECTLE_H_
#define _FPS_PROJECTLE_H_

#include "fx_component/game_object.h"


class LProjectileMovementComponent;
class LSphereComponent;
class LModelComponent;

class LFPSProjectile : public LGameObject
{
DECLARE_LCLASS(LFPSProjectile, LGameObject, COMPILED_IN_FLAGS(0))

public:
LFPSProjectile();
virtual ~LFPSProjectile();

virtual bool Init(const IVarList& args) override;

virtual void BeginPlay() override;
virtual void Update(float seconds) override;

void OnProjectileHit(LPrimitiveComponent* pComponent, LGameObject* pOtherActor,
LPrimitiveComponent* pOtherComponent, CXMVECTOR vImpluse,
const hit_result_t& hit);

protected:
LSphereComponent* m_pCollisionComponent;
LProjectileMovementComponent* m_pProjectileMovement;
LModelComponent* m_pModelComponent;

float m_fLifeTime;
float m_fBornTime;

} FX_ENTITY(Parent = LGameObject, EditorVisible, Category = Game);
#endif // _FPS_PROJECTLE_H_

fps_projectile.cpp

#include "fps_projectile.h"

#include "tp_pawn.h"
#include "fx_component/scene.h"
#include "flexi/utils/fx_cast.h"
#include "fx_component/network/network_compatible.h"
#include "fx_component/sphere_component.h"
#include "fx_component/projectile_movement_component.h"
#include "fx_component/model_component.h"

// Code gen
#include "code_gen/fps_projectile.gen.h"

IMPLEMENT_LCLASS(LFPSProjectile)

LFPSProjectile::LFPSProjectile()
{
// The replicable object
SetAddNetScene(true);
// Frame loop turned on
m_GameObjectUpdate.bAllowUpdate = true;

m_pCollisionComponent = nullptr;
m_pProjectileMovement = nullptr;
m_pModelComponent = nullptr;

m_fLifeTime = 3.0f;
m_fBornTime = 0.f;

SetStringTag("FPSProjectile");
}

LFPSProjectile::~LFPSProjectile()
{

}

bool LFPSProjectile::Init(const IVarList& args)
{
if (!Super::Init(args))
{
return false;
}

// Set the collider
if (!m_pCollisionComponent)
{
m_pCollisionComponent = NewObject<LSphereComponent>(this);
m_pCollisionComponent->SetSphereRadius(0.101f);
m_pCollisionComponent->SetCollisionEnabledAttr(COLLISION_ENABLED::NoCollision);
m_pCollisionComponent->SetCollisionProfileName("BlockAllDynamic");
m_pRootComponent = m_pCollisionComponent;
}

// Add the mesh
if (!m_pModelComponent)
{
m_pModelComponent = NewObject<LModelComponent>(this);
m_pModelComponent->SetLinkTarget(m_pCollisionComponent);
m_pModelComponent->SetScaleVector(XMVectorSet(1.1f, 1.1f, 1.1f, 0.f));
m_pModelComponent->SetRelativePositionY(-0.05f);
m_pModelComponent->SetPositionY(-0.1f);
IVisBase* pVisbase = m_pModelComponent->GetVisBase();
m_pModelComponent->SetCollisionEnabledAttr(COLLISION_ENABLED::NoCollision);
IModel* pModel = FX_KIND_CAST(IModel, pVisbase, "fx_world.Model");
if (pModel)
{
pModel->SetModelFile(FXTEXT("BeginnerContent/BaseShape/Box.xmod"));
g_pCore->SetProperty(pVisbase, "AutoUpdateLOD", CVar(VTYPE_BOOL, false));
}

if (GetScene())
{
m_pModelComponent->Load();
}
}

// Add projectile movement and collision
if (!m_pProjectileMovement)
{
m_pProjectileMovement = NewObject<LProjectileMovementComponent>(this);
m_pProjectileMovement->SetUpdatedComponent(m_pRootComponent);
m_pProjectileMovement->SetInitialSpeed(10.0f);
m_pProjectileMovement->SetMaxSpeed(10.0f);
m_pProjectileMovement->SetShouldBounce(false);
m_pProjectileMovement->SetRotationFollowsVelocity(false);
m_pProjectileMovement->SetProjectileGravityScale(0.0f);
}

if (GetScene())
{
m_fBornTime = GetScene()->GetTimeSeconds();
}

return true;
}

void LFPSProjectile::BeginPlay()
{
Super::BeginPlay();
// Add collision event callbacks
m_pCollisionComponent->GetComponentHit().add(this, &LFPSProjectile::OnProjectileHit);
}

void LFPSProjectile::Update(float seconds)
{
Super::Update(seconds);
// Server code
if (Networkcompatible::GetSceneGameMode(GetScene()))
{
float nowTime = GetScene()->GetTimeSeconds();
if (m_fBornTime > 0.f && nowTime - m_fBornTime > m_fLifeTime)
{
DestroyGameObject();
}
}
}

void LFPSProjectile::OnProjectileHit(LPrimitiveComponent* pComponent, LGameObject* pOtherActor, LPrimitiveComponent* pOtherComponent, CXMVECTOR vImpluse, const hit_result_t& hit)
{
// Server code for handling of hitting different Actors
if (Networkcompatible::GetSceneGameMode(GetScene()))
{
LThirdPersonPawn *pThridPerson = Cast<LThirdPersonPawn>(pOtherActor);
if (pThridPerson != NULL)
{
pThridPerson->SetIsHit(true);
}
DestroyGameObject();
}
}


form_state_bar.lua

--UI for character properties
--State Bar UI

--------------
--local data--
--------------

local THIS_SCRIPT = "share\\ui\\" .. "form_state_bar"
local THIS_SKIN = "skin\\ui\\" .. "form\\" .. "form_state_bar.ui"

-------------------
--local functions--
-------------------

local function get_form()
return nx_value(THIS_SCRIPT)
end

function get_scene_box()
local lscene = nx_value("lscene")

if not nx_is_valid(lscene) then
return nx_null()
end

return lscene.SceneBox
end

function load_form(form_name, form_res, scene_box)
form_name = nx_string(form_name)
form_res = nx_string(form_res)

local form = nx_null()

local gui = nx_value("gui")

local path = nx_resource_path()
if nil == string.find(form_res, ".ui") then
form_res = form_res.. ".ui"
end

form = gui.Loader:LoadForm(path, form_res)

if not nx_is_valid(form) then
nx_msgbox("msg_CreateFormFailed - ".. form_res)
return nx_null()
end

form.Name = form_name
form.Left = 0
form.Top = 0

scene_box:Add(form)

return form
end

function get_global_list(list_name)
local global_list = nx_value(list_name)

if not nx_is_valid(global_list) then
global_list = nx_create("ArrayList", list_name)

nx_set_value(list_name, global_list)
end

return global_list
end

function listen_command(cmd, ident, script, func, ...)
local command_hooks = get_global_list("command_hooks")

if not command_hooks:FindChild(cmd) then
command_hooks:CreateChild(cmd)
end

if ident == "" then
ident = "all"
end

local cmd_node = command_hooks:GetChild(cmd)

if not cmd_node:FindChild(ident) then
cmd_node:CreateChild(ident)
end

local ident_node = cmd_node:GetChild(ident)

if not ident_node:FindChild(script) then
ident_node:CreateChild(script)
end

local script_node = ident_node:GetChild(script)

if not script_node:FindChild(func) then
script_node:CreateChild(func)
end

local func_node = script_node:GetChild(func)

local arg_table = { unpack(arg) }

for i, t_arg in ipairs(arg_table) do
local arg_child = func_node:CreateChild(nx_string(i))
arg_child.value = t_arg
end
end

local function setup_form(form)

local scene_box = get_scene_box()

form.Left = 40
form.Top = scene_box.Height - form.Height - 30
form.HAnchor = "Auto"
form.VAnchor = "Auto"

form.ProgressBar_HP.Value = 0
form:AddTopLayer(form.ProgressBar_HP)
end

function on_tick(seconds)
local form = get_form()

if not nx_is_valid(form) then
return
end

local main_player = form.owner

if nx_is_valid(main_player) then
local max_hp = main_player.MaxHp
local HP = main_player.CurrentHp
local value_hp = HP / max_hp * 100
form.ProgressBar_HP.Value = value_hp

local label_hp = form.Label_HP
label_hp.Text = nx_widestr(HP)
nx_pause(0.0)
end
end

---------------------
--control callbacks--
---------------------
function main_form_open(self)
nx_set_value(THIS_SCRIPT, self)
setup_form(self)
listen_command("tick", "tp_pawn",
THIS_SCRIPT, "on_tick")

return 1
end

function main_form_close(self)
nx_destroy(self)

return 1
end

--------------------
--public functions--
--------------------
function open_state_bar(character)

local form = get_form()

if not nx_is_valid(form) then

form = load_form("form_state_bar", THIS_SKIN, get_scene_box())

form:Show()
form.owner = character
end
end

function close_state_bar()
local form = get_form()

if nx_is_valid(form) then
form:Close()
end
end

tp_pawn.lua


local IE_Pressed = 0 --Key pressed
local IE_Released = 1 --Key released
local IE_Repeat = 2
local IE_DoubleClick = 3
local IE_Axis = 4
local IE_MAX = 5

local AIT_FORWARD = 0
local AIT_BACKWARD = 1
local AIT_LEFT = 2
local AIT_RIGHT = 3
local AIT_JUMP = 4
local AIT_ACCELERATE = 5
local AIT_E = 6
local AIT_YAW = 7
local AIT_PITCH = 8

-------------------
--innerr function--
-------------------
function get_global_list(list_name)
local global_list = nx_value(list_name)

if not nx_is_valid(global_list) then
global_list = nx_create("ArrayList", list_name)

nx_set_value(list_name, global_list)
end

return global_list
end

function command(cmd, ident, ...)
local command_hooks = get_global_list("command_hooks")

local cmd_node = command_hooks:GetChild(cmd)

if not nx_is_valid(cmd_node) then
return
end

local ident_table = { "all" }

if ident ~= "" and ident ~= "all" then
ident_table[#ident_table + 1] = ident
end

for _, t_ident in pairs(ident_table) do
local ident_node = cmd_node:GetChild(t_ident)

if nx_is_valid(ident_node) then
local script_node_table = ident_node:GetChildList()

for _, script_node in pairs(script_node_table) do
local script = script_node.Name

local func_node_table = script_node:GetChildList()

for _, func_node in pairs(func_node_table) do
local func = func_node.Name

--callback parameters: { commander's..., listener's... }
local full_arg_table = {}

local arg_table = { ... }

for _, t_arg in ipairs(arg_table) do
full_arg_table[#full_arg_table + 1] = t_arg
end

local arg_node_talbe = func_node:GetChildList()

for i, arg_node in ipairs(arg_node_talbe) do
full_arg_table[#full_arg_table + 1] = arg_node.value
end

nx_execute(script, func, unpack(full_arg_table))
end
end
end
end
end
-------------------------
----inner function end---
-------------------------

--use this for initialization
function on_begin_play(component)
local owner = component.GameObjectOwner

if nx_is_valid(owner) then
nx_bind_script(owner, nx_current())

nx_execute("share\\ui\\form_state_bar", "open_state_bar", owner)
local input_comp = owner.InputComponent
if nx_is_valid(input_comp) then
--Mouse button binding
input_comp:AddCombinationBinding("LeftClick", IE_Pressed, true, false, true, "InputActionEvent_LeftClick_Pressed")
input_comp:AddCombinationBinding("LeftClick", IE_Released, true, false, true, "InputActionEvent_LeftClick_Released")
input_comp:AddCombinationBinding("RightClick", IE_Pressed, true, false, true, "InputActionEvent_RightClick_Pressed")
input_comp:AddCombinationBinding("RightClick", IE_Released, true, false, true, "InputActionEvent_RightClick_Released")

input_comp:AddAxisBinding("MoveForward", true, false, true, "InputAxisEvent_MoveForward")
input_comp:AddAxisBinding("MoveRight", true, false, true, "InputAxisEvent_MoveRight")
input_comp:AddAxisBinding("Turn", true, false, true, "InputAxisEvent_Turn")
input_comp:AddAxisBinding("LookUp", true, false, true, "InputAxisEvent_LookUp")
input_comp:AddAxisBinding("AdjustCameraDis", true, false, true, "InputAxisEvent_AdjustCameraDis")
input_comp:AddCombinationBinding("Jump", IE_Pressed, true, false, true, "InputActionEvent_Jump_Pressed")
input_comp:AddCombinationBinding("Jump", IE_Released, true, false, true, "InputActionEvent_Jump_Released")

nx_callback(owner, "InputAxisEvent_MoveForward", "on_moveforward")
nx_callback(owner, "InputAxisEvent_MoveRight", "on_moveright")
nx_callback(owner, "InputAxisEvent_Turn", "on_turn")
nx_callback(owner, "InputAxisEvent_LookUp", "on_lookup")
nx_callback(owner, "InputAxisEvent_AdjustCameraDis", "on_adjust_camera_dis")
nx_callback(owner, "InputActionEvent_Jump_Pressed", "on_jump_pressed")
nx_callback(owner, "InputActionEvent_Jump_Released", "on_jump_released")

nx_callback(owner, "InputActionEvent_LeftClick_Pressed", "on_left_click_pressed")
nx_callback(owner, "InputActionEvent_LeftClick_Released", "on_left_click_released")
nx_callback(owner, "InputActionEvent_RightClick_Pressed", "on_right_click_pressed")
nx_callback(owner, "InputActionEvent_RightClick_Released", "on_right_click_released")
end
end

nx_callback(component, "on_tick", "tick")
end

--use this for release
function on_end_play(component)

end

--Move forward callback
function on_moveforward(owner, axis_value)
if not nx_is_valid(owner) then
return 0
end

--Get controller Y-axis angle
local controller = owner.Controller

if nx_is_valid(controller) then
local yaw = controller.AngleY
--Get forward position
local x, y, z = nx_function("ext_angle_get_forward_vector", 0.0, yaw, 0.0)

--Increase movement
owner:AddMovementInput(x, y, z, axis_value)
end

return 1
end

--Move right callback
function on_moveright(owner, axis_value)
if not nx_is_valid(owner) then
return 0
end

--Get controller Y-axis angle
local controller = owner.Controller

if nx_is_valid(controller) then
local yaw = controller.AngleY
--Get right direction
local x, y, z = nx_function("ext_angle_get_right_vector", 0.0, yaw, 0.0)

--Increase movement
owner:AddMovementInput(x, y, z, axis_value)
end
return 1
end

--Left and right rotation (rotation around Y axis) callback
function on_turn(owner, axis_value)
owner:AddControllerYawInput(axis_value)
return 1
end

--Up and down rotation (rotation around X-axis) callback
function on_lookup(owner, axis_value)
owner:AddControllerPitchInput(-axis_value)
return 1
end

--Mouse wheel callback
function on_adjust_camera_dis(owner, axis_value)
owner:OnMouseWheelInput(axis_value)
return 1
end

function character_get_actor(character)
if nx_is_valid(character) then
local mesh_component = character.MeshComponent

if nx_is_valid(mesh_component) then
local vis_base = mesh_component.VisBase

if nx_is_valid(vis_base) and nx_is_kind(vis_base, "Actor") then
return vis_base
end
end
end

return nx_null()
end

function tick(self, delta_time)
--Set Animation Value
local character = self.GameObjectOwner
local actor = character_get_actor(character)

if nx_is_valid(actor) then
if actor:GetBlendActionCount() > 0 then
local action_name = actor:GetBlendActionName(0)
local action_index = actor:GetActionIndex(action_name)

local isInAir = character.InAir
actor:SetAnimTreeValue(action_index, "IsJumping", isInAir)

local walkSpeed = character.WalkSpeed
actor:SetAnimTreeValue(action_index, "Speed", walkSpeed)

local moveDirection = character.MovementDirection
actor:SetAnimTreeValue(action_index, "MovementDirection", moveDirection)

local moveForwardDirection = character.MovementForwardDirection
actor:SetAnimTreeValue(action_index, "MovementForwardDirection", moveForwardDirection)

local moveSideDirection = character.MovementSideDirection
actor:SetAnimTreeValue(action_index, "MovementSideDirection", moveSideDirection)
end
end

command("tick", "tp_pawn", delta_time)
end

--Jump key pressed callback
function on_jump_pressed(owner)
--Character jump
owner:StartJump()
return 1
end

--Jump key released callback
function on_jump_released(owner)
--Character stop jumping
owner:StopJump()
return 1
end

--Left-click pressed callback
function on_left_click_pressed(owner)
owner:Fire()
return 1
end

--Left-click released callback
function on_left_click_released(owner)
return 1
end

--Right-click pressed callback
function on_right_click_pressed(owner)
return 1
end

--Right-click released callback
function on_right_click_released(owner)
return 1
end

Appendix I

form_state_bar.ui

<?xml version="1.0" encoding="utf-8"?>
<form>
<control name="main_form" entity="Form" script="share\\ui\\form_state_bar" init="main_form_init">
<prop ShowGrid="true" Left="2" Top="2" Right="-360" Bottom="-40" Width="358" Height="38" ScaleX="1.000000" ScaleY="1.000000" BackColor="0,255,255,255" LineColor="0,255,0,0" ShadowColor="0,0,0,0" BlendMode="OpacityBlend"/>
<event on_open="main_form_open" on_close="main_form_close"/>
<control name="ProgressBar_HP" entity="ProgressBar" script="" init="">
<prop Maximum="100" Value="100" WidthGap="2" HeightGap="2" ProgressImage="skin_res\status\prog_hp.png" ProgressMode="LeftToRight" Left="35" Top="15" Right="-285" Bottom="-25" Width="250" Height="10" ScaleX="1.000000" ScaleY="1.000000" BackColor="0,255,255,255" ShadowColor="0,0,0,0" NoFrame="true" DrawMode="FitWindow" BlendMode="OpacityBlend" BackImage="skin_res\status\prog_hp_back.png"/>
</control>
<control name="Label1" entity="Label" script="" init="">
<prop Align="Right" RefCursor="WIN_HELP" Left="6" Top="5" Right="-36" Bottom="-35" Width="30" Height="30" ScaleX="1.000000" ScaleY="1.000000" ShadowColor="0,0,0,0" DrawMode="FitWindow" BlendMode="OpacityBlend" BackImage="skin_res\status\heart.png"/>
</control>
<control name="Label_HP" entity="Label" script="" init="">
<prop RefCursor="WIN_HELP" Left="296" Top="12" Right="-376" Bottom="-28" Width="80" Height="16" ScaleX="1.000000" ScaleY="1.000000" ShadowColor="0,0,0,0" Text="100" DrawMode="FitWindow" BlendMode="OpacityBlend"/>
</control>
</control>
</form>

Related image resources(need to be saved under skin_res\status\):

heart prog_hp prog_hp_back