Skip to main content

多人游戏编程快速入门指南

开发多人游戏的游戏进程需要在游戏的GameObject中实现复制。 还必须设计特定于服务器(充当游戏会话的主机)和客户端(代表连接到会话的玩家)的功能。

本示例包括以下内容:

  • 如何向基本GameObject添加复制。
  • 如何向变量添加复制及变量被修改后的回调函数。
  • 如何在C++环境下使用远程过程调用(RPC)
  • 如何区分GameObject在服务器或者在客户端。

最终形成第三人称多人游戏, 玩家可以向对方发射投射物, 并造成伤害。

基本设置

在编辑器中依次点击文件(File) -> 创建项目(Create Project),在弹出的创建项目(Create Project)窗口选择Third Person Project C++,然后在路径(Path)名称(Name)下分别填写项目路径及名称,勾选包含初学者内容包(Include Beginner Content),最后点击创建(Create)按钮来创建一个新项目。

image-20230217141911822

新项目创建后, 依次点击构建(Build)-> Windows解决方案(Windows Solution)-> 生成(Generate)来生成新项目。

generate

启动游戏

效果如下: standalong

只需要改变部分设置即可实现简易多人游戏, 网络配置说明如下:

启动选项说明

launchOp

属性说明
以单机模式运行(Start Standalone)编辑器以单机模式启动游戏。
以Listen服务器模式运行(Start As Listen Server)编辑器做为监听服务器启动,如果配置的玩家数量(Number of Players)大于1,则额外启动玩家数量减1个客户端连接服务器。
以客户端模式运行并启动Entry服务器(Start As Client With Entry Server)编辑器做为客户端启动,同时创建Entry服务器和Member服务器,编辑器做为客户端连接Entry服务器,如果配置的玩家数量大于1,则额外启动客户端进程实例。
以客户端模式运行并启动Member服务器(Start As Client With Member Server)编辑器做为客户端启动,同时创建Member服务器,编辑器做为客户端连接到Member服务器上,如果配置的玩家数量大于1,则额外启动客户端进程实例。
玩家数量(Number Of Players)联网模式下的玩家数量。

网络连接设置说明

编辑器启动前必须设置网络设置(Network Settings)。依次点击配置(Config) -> 项目设置(Project Settings)-> 网络设置(Network Settings) 打开设置界面。

netmod

属性说明
连接设置(Connect Settings)自动连接超时时长(Reconnect Duration):客户端重连服务器的超时时长,单位是秒。
Entry服务器设置(Entry Server Settings)自动登录(Auto Login):是否开启自动登录。
服务器端口(Server Port):Entry服务器监听的端口号。
Member服务器列表(Member Server List):要运行的Member服务器列表。
启动场景(Startup Scene):Member服务器的启动场景路径。
Member服务器设置(Member Server Settings)启动场景(Startup Scene):Member服务器的启动场景路径,同时也做为编辑器以监听服务器模式启动时的场景路径。
服务器端口(Server Port):Member服务器监听的端口号,同时也做为编辑器以监听服务器模式启动时的端口号。

在此项目中, 项目配置如下:

image-20230320152051198

启动游戏之后,效果如下: begin

此时已经是一个功能正常的多人游戏了,但玩家仅可以移动和跳跃。接下来让我们添加一些玩法。

添加血条及显示

C++项目打开步骤

依次点击菜单栏构建(Build) -> Windows解决方案(Windows Solution) -> 打开(Open)

image-20230316174408568

  1. tp_pawn.h中添加复制属性:
protected:
// 最大血量
FX_PROPERTY(Type = float, GetFunc = GetMaxHp, Name = MaxHp, NetProperty)
float m_fMaxHp;

// 当前血量
FX_PROPERTY(Type = float, GetFunc = GetHp, SetFunc = SetHp, Name = CurrentHp, NetProperty)
float m_fCurrentHp;

当前血量会同步给所有客户端, 所以添加NetProperty属性Meta

另一个属性Meta NetPropertyNotify=OnFunc,表示属性同步后会触发的回调函数。

打开tp_pawn.h, 并将以下代码添加到类定义的public下:

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

将以下代码添加到tp_pawn.cpp中的构造函数中:

m_fMaxHp = 100.0f;
m_fCurrentHp = m_fMaxHp;

2) 在脚本中添加血条显示:

首先创建UI资产 form_state_bar.ui: (具体创建详情请参考进度条

ui_asset

将资产保存到以下目录:

asset

设置对应的脚本名及添加事件回调: asset_script

最终生成的资产form_state_bar.ui的资源代码请参考附录一

创建脚本文件form_state_bar.lua

script

form_state_bar.lua中,添加以下代码:

--人物属性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

在脚本文件目录中找到tp_pawn.lua脚本文件, 打开文件,在on_begin_play方法中,在脚本和实体绑定之后添加执行open_state_bar方法。

if nx_is_valid(owner) then
nx_bind_script(owner, nx_current())
--添加的代码
nx_execute("share\\ui\\form_state_bar", "open_state_bar", owner)

效果如下: show_hp

至此, 打开客户端窗口, 就有血条状态栏出现在屏幕左下角。

添加抛射物类并发射抛射物

创建抛射物类 LFPSProjectile

创建文件fps_projectile.hfps_projectile.cpp.

newFile

打开项目, 把文件添加到项目中:

addFile

addFile

打开 fps_projectile.h,并将以下代码添加到类定义中的 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:
// 球体组件, 用于碰撞检测
LSphereComponent* m_pCollisionComponent;
// 投射物移动组件, 用于移动投射物。
LProjectileMovementComponent* m_pProjectileMovement;
// 静态网格体组件, 作为投射物的视觉呈现。
LModelComponent* m_pModelComponent;

} 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/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()
{
// 可复制对象
SetAddNetScene(true);
// 帧循环打开
m_GameObjectUpdate.bAllowUpdate = true;

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

SetStringTag("FPSProjectile");
}

LFPSProjectile::~LFPSProjectile()
{
}

将以下代码添加到fps_projectile.cpp中的Init函数中:

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

// 设置碰撞体
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;
}

// 添加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();
}
}

// 添加发射物的移动及碰撞
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;
}

打开 fps_projectile.h,并将以下代码添加到类定义中的 public 下:

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

添加抛射物碰撞回调函数,该方法会注册在抛射物的碰撞组件中。

将以下代码添加到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)
{
}

打开 fps_projectile.h,并将以下代码添加到类定义中的 protected 下:

float m_fLifeTime;
float m_fBornTime;

定义有效时间和生成时间, 通过这两个时间来销毁抛射物类,如果抛射物没有和场景中的物体碰撞。

将以下代码添加到fps_projectile.cpp中的构造函数中:

m_fLifeTime = 3.0f;
m_fBornTime = 0.f;

将以下代码添加到fps_projectile.cpp中的Init函数中的末尾,return true; 的上方:

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

fps_projectile.cpp中添加头文件:

#include "fx_component/network/network_compatible.h"

将以下代码添加到fps_projectile.cpp中的Update函数中:

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

抛射物类可以由服务器复制到客户端, 添加了碰撞回调处理碰撞逻辑, 如果没碰撞, 按时销毁。

发射抛射物

打开文件tp_pawn.lua, 添加以下代码:

--鼠标左键按下回调
function on_left_click_pressed(owner)
owner:Fire()
return 1
end

打开tp_pawn.h, 添加代码到public中:

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

这里使用了引擎的RPC功能,客户端按键触发事件Fire()Fire()会远程过程调用服务端的Fire_Server(), 此时服务端创建抛射物。 由于抛射物是可复制GameObject, 此时每个连接的客户端都会生成对应的抛射物。

打开tp_pawn.cpp, 添加以下代码:

#include "fx_component/network/network_compatible.h"

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

// 服务端
if (Networkcompatible::GetSceneGameMode(GetScene()))
{
// 获取pawn所在位置及朝向
XMVECTOR projectileRotation = GetTransform().GetAngle();
XMVECTOR projectileLocation = GetTransform().GetLocation();

// 将发射位置靠前一点, 防止与自己碰撞
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");

// 场景中创建发射物
pScene->SpawnGameObject(actorType, &projectileLocation, &projectileRotation, spawnParams);

return;
}

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

// rpc服务端调用
void LThirdPersonPawn::Fire_Server()
{
auto* pScene = GetScene();
if (!pScene)
{
return;
}

// 服务端
// 获取pawn所在位置及朝向
XMVECTOR projectileRotation = GetTransform().GetAngle();
XMVECTOR projectileLocation = GetTransform().GetLocation();

// 将发射位置靠前一点, 防止与自己碰撞
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");

// 场景中创建发射物
pScene->SpawnGameObject(actorType, &projectileLocation, &projectileRotation, spawnParams);
}

玩家造成伤害并同步血条状态

打开tp_pawn.h, 添加以下代码:

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:
// 攻击是否命中
bool m_bHit;
bool m_bDead;

pawn添加被攻击和死亡状态,以此来更改血量等。

打开tp_pawn.h, 添加以下代码:

// 被攻击时
void OnBeAttacked();

tp_pawn.cpp的构造函数中初始化m_bHit, m_bDead

m_bHit = false;
m_bDead = false;

打开tp_pawn.cpp, 添加以下代码:

// 服务端代码
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);

// 角色被命中的处理
if (Networkcompatible::GetSceneGameMode(GetScene()))
{
OnBeAttacked();
}
}

服务端会获取到GameMode, 以此方法来判断当前执行端为服务器或客户端。

打开fps_projectile.cpp, 添加以下代码至OnProjectileHit方法中:

    // 命中不同Actor的处理 服务端代码
if (Networkcompatible::GetSceneGameMode(GetScene()))
{
LThirdPersonPawn *pThridPerson = Cast<LThirdPersonPawn>(pOtherActor);
if (pThridPerson != NULL)
{
pThridPerson->SetIsHit(true);
}
DestroyGameObject();
}

当抛射物与其他pawn发生碰撞时, 被碰撞pawn设置状态被攻击, 并销毁抛射物。 此逻辑发生在服务端。

CurrentHp为网络复制属性, 已连接的客户端会收到目标角色实时血量。 然后在form_state_bar.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 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

form_state_bar.lua脚本中注册on_tick函数, 将以下代码添加至函数main_form_open的返回值前:

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

tp_pawn.lua中将玩家tick对外抛出, 在tick函数结尾处添加以下代码:

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

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

至此,简单的多人联网射击游戏已完成。

结果展示

两个客户端,各自有各自的血量展示。

show_fire_before1左边的客户端进行射击之后, 右边的客户端人物血量减少50show_fire_after2 右边的客户端进行射击之后, 左边的客户端角色被销毁, 左右角色都不显示。 show_die

代码示例

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:
// 最大血量
FX_PROPERTY(Type = float, GetFunc = GetMaxHp, Name = MaxHp, NetProperty)
float m_fMaxHp;

// 当前血量
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");

// 设置移动数据同步
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);

// 角色被命中的处理
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());
}

// 移动组件的设置更新组件移到这,因为之前的位置场景还没有初始化好
if (m_pMovementComponent)
{
m_pMovementComponent->SetUpdatedComponent(GetCollisionRoot());
}
}

void LThirdPersonPawn::OnMouseWheelInput(float fValue)
{
// 伸缩弹簧臂
if (m_pSpringArm)
{
// 滚轮向后为加,向前为减
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;
}

// 服务端
if (Networkcompatible::GetSceneGameMode(GetScene()))
{
// 获取pawn所在位置及朝向
XMVECTOR projectileRotation = GetTransform().GetAngle();
XMVECTOR projectileLocation = GetTransform().GetLocation();

// 将发射位置靠前一点, 防止与自己碰撞
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");

// 场景中创建发射物
pScene->SpawnGameObject(actorType, &projectileLocation, &projectileRotation, spawnParams);

return;
}

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

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

// 服务端
// 获取pawn所在位置及朝向
XMVECTOR projectileRotation = GetTransform().GetAngle();
XMVECTOR projectileLocation = GetTransform().GetLocation();

// 将发射位置靠前一点, 防止与自己碰撞
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");

// 场景中创建发射物
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()
{
// 可复制对象
SetAddNetScene(true);
// 帧循环打开
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;
}

// 设置碰撞体
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;
}

// 添加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();
}
}

// 添加发射物的移动及碰撞
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();
// 添加碰撞事件回调
m_pCollisionComponent->GetComponentHit().add(this, &LFPSProjectile::OnProjectileHit);
}

void LFPSProjectile::Update(float seconds)
{
Super::Update(seconds);
// 服务端代码
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)
{
// 命中不同Actor的处理 服务端代码
if (Networkcompatible::GetSceneGameMode(GetScene()))
{
LThirdPersonPawn *pThridPerson = Cast<LThirdPersonPawn>(pOtherActor);
if (pThridPerson != NULL)
{
pThridPerson->SetIsHit(true);
}
DestroyGameObject();
}
}


form_state_bar.lua

--人物属性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 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

require("public_attr")

local IE_Pressed = 0 --按键按下
local IE_Released = 1 --按键抬起
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
--鼠标按键绑定
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

--向前移动回调
function on_moveforward(owner, axis_value)
if not nx_is_valid(owner) then
return 0
end

--获得控制器Y轴角度
local controller = owner.Controller

if nx_is_valid(controller) then
local yaw = controller.AngleY
--获得向前方向
local x, y, z = nx_function("ext_angle_get_forward_vector", 0.0, yaw, 0.0)

--增加移动量
owner:AddMovementInput(x, y, z, axis_value)
end

return 1
end

--向右移动回调
function on_moveright(owner, axis_value)
if not nx_is_valid(owner) then
return 0
end

--获得控制器Y轴角度
local controller = owner.Controller

if nx_is_valid(controller) then
local yaw = controller.AngleY
--获得向右方向
local x, y, z = nx_function("ext_angle_get_right_vector", 0.0, yaw, 0.0)

--增加移动量
owner:AddMovementInput(x, y, z, axis_value)
end
return 1
end

--左右旋转(绕Y轴旋转)回调
function on_turn(owner, axis_value)
owner:AddControllerYawInput(axis_value)
return 1
end

--上下旋转(绕X轴旋转)回调
function on_lookup(owner, axis_value)
owner:AddControllerPitchInput(-axis_value)
return 1
end

--鼠标滚轮回调
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

--跳跃键按下回调
function on_jump_pressed(owner)
--角色跳跃
owner:StartJump()
return 1
end

--跳跃键抬起回调
function on_jump_released(owner)
--角色停止跳跃
owner:StopJump()
return 1
end

--鼠标左键按下回调
function on_left_click_pressed(owner)
owner:Fire()
return 1
end

--鼠标左键抬起回调
function on_left_click_released(owner)
return 1
end

--鼠标右键按下回调
function on_right_click_pressed(owner)
return 1
end

--鼠标右键抬起回调
function on_right_click_released(owner)
return 1
end

附录一

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>

相关图片资源(需放在skin_res\status\下):

heart prog_hp prog_hp_back