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.
After the new project is created, click Build -> Windows Solution -> Generate to generate the new project.
Starting the Game
The effect is as follows:
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
Property | Description |
---|---|
Start Standalone | The Editor starts the game in standalone mode. |
Start As Listen Server | The 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 Server | The 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 Server | The 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 Players | The 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.
Property | Description |
---|---|
Connect Settings | Reconnect Duration: The timeout duration in seconds for the Client to reconnect to the Server. |
Entry Server Settings | Auto 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 Settings | Startup 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.
After starting the game, the effect is as follows.
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.
- 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):
Save the asset in the following directory:
Set the corresponding script name and add the event callback:
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:
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:
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.
Open the project and add the files to it:
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.
After the left Client shoots, the right Client's HP is reduced by 50.
After the right Client shoots, the left Client character is destroyed and neither the left nor the right character is displayed.
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\):