UE4 C++联网RPC教程笔记(二)(第5~7集)

这篇具有很好参考价值的文章主要介绍了UE4 C++联网RPC教程笔记(二)(第5~7集)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

5. 联网变量

在前面的课程里,我们都是通过 Actor 的生成来看服务端与客户端是否同步。接下来我们研究下 Actor 的变量复制来实现变量同步。

通过 Replicated 说明符来复制变量

下面文本截取自梁迪老师的 RPC 联网文档。

变量复制只有在服务端修改才会更新到服务端和所有客户端,在客户端修改只会更新所在客户端,对服务端和其他客户端没有影响。

(1)首先用 UPROPERTY(Replicated, ......) 定义变量,如:

	UPROPERTY(Replicated)
	FString Inventory;

(2)必须在 .cpp 文件重写 GetLifetimeReplicatedProps() 方法,如:

void ARPCProjectCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty> & OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
	......
}

注意,重写这个方法不需要在 .h 文件进行声明。

(3)添加头文件 #include "Net/UnrealNetwork.h"

在上面的重写方法里用宏注册变量到联网系统:

	DOREPLIFETIME(ARPCProjectCharacter, Inventory);		// 所有端的对象都更新
	DOREPLIFETIME_CONDITION(ARPCProjectCharacter, Inventory, COND_OwnerOnly);	// 该属性仅发送至 Actor 的所有者
	DOREPLIFETIME_CONDITION(ARPCProjectCharacter, Inventory, COND_SkipOwner);	// 该属性将发送至除所有者之外的每个连接
	DOREPLIFETIME_CONDITION(ARPCProjectCharacter, Inventory, COND_Custom);		// 该属性没有特定条件,但需要通过 SetCustomIsActiveOverride 得到开启/关闭能力

更多条件参考官方文档:条件属性复制 >>【】

接下来我们通过实操来探究一下。在默认路径创建一个 C++ 的 Actor 类,命名为 FireEffectActor

我们打算让这个 Actor 在场景里显示一个复制的 int32 数字,然后通过 Timer 来让这个数字递减。同时添加一个火焰粒子方便观察。

FireEffectActor.h

// 提前声明
class UParticleSystemComponent;
class UTextRenderComponent;

UCLASS()
class RPCCOURSE_API AFireEffectActor : public AActor
{

protected:

	void UpdateTimer();

protected:

	UPROPERTY(EditAnywhere)
	UParticleSystemComponent* FireEffect;

	UPROPERTY(EditAnywhere)
	UTextRenderComponent* TextRender;

	// 应用 变量复制 的说明符
	UPROPERTY(Replicated)
	int32 CountDownTimer;

	// 用于执行逐秒递减操作的计时器句柄
	FTimerHandle UpdateTimerHandle;
};

FireEffectActor.cpp

// 引入头文件
#include "Particles/ParticleSystemComponent.h"
#include "Components/TextRenderComponent.h"
#include "TimerManager.h"
#include "Net/UnrealNetwork.h"	// 变量复制需要用到

AFireEffectActor::AFireEffectActor()
{
	PrimaryActorTick.bCanEverTick = true;

	SetReplicates(true);

	RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("RootScene"));

	FireEffect = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("FireEffect"));
	FireEffect->SetupAttachment(RootComponent);

	TextRender = CreateDefaultSubobject<UTextRenderComponent>(TEXT("TextRender"));
	TextRender->SetupAttachment(RootComponent);
}

void AFireEffectActor::BeginPlay()
{
	Super::BeginPlay();
	
	CountDownTimer = 20;

	// 在服务端执行
	if (GetWorld()->IsServer()) {
		// 循环运行
		FTimerDelegate UpdateTimerDele = FTimerDelegate::CreateUObject(this, &AFireEffectActor::UpdateTimer);
		GetWorld()->GetTimerManager().SetTimer(UpdateTimerHandle, UpdateTimerDele, 1.f, true);
	}
}

void AFireEffectActor::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	// 渲染数字
	//TextRender->SetText(FString::FromInt(CountDownTimer));
	// 4.26 版本推荐使用以 FText 为参数的 SetText() 方法
	TextRender->SetText(FText::AsNumber(CountDownTimer));
}

void AFireEffectActor::UpdateTimer()
{
	if (CountDownTimer > 0)
		CountDownTimer -= 1;
}

// 需要把用 Replicated 定义的变量在这个方法的重写下进行注册
void AFireEffectActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);

	// 有一个复制变量就要调用一次这样的宏
	DOREPLIFETIME(AFireEffectActor, CountDownTimer);
}

编译后,在 Blueprint 下创建一个 FireEffectActor 的蓝图,命名为 FireEffectActor_BP,调整蓝图如下:

UE4 C++联网RPC教程笔记(二)(第5~7集),UE4/5 的学习笔记,ue4,c++,rpc
将这个蓝图拖拽至场景内,运行游戏(记得要开2个玩家的聆听服务器),可以看到服务端和客户端的 FireEffectActor_BP 的 TextRender 上的数字都在同步递减。

UE4 C++联网RPC教程笔记(二)(第5~7集),UE4/5 的学习笔记,ue4,c++,rpc

修改一下,让递减的 Timer 只在客户端运作。

FireEffectActor.cpp

void AFireEffectActor::BeginPlay()
{


	// 在客户端执行该操作
	if (!GetWorld()->IsServer()) {
		FTimerDelegate UpdateTimerDele = FTimerDelegate::CreateUObject(this, &AFireEffectActor::UpdateTimer);
		GetWorld()->GetTimerManager().SetTimer(UpdateTimerHandle, UpdateTimerDele, 1.f, true);
	}
}

编译后运行游戏,可以看到客户端的 TextRender 上的数字在递减,而服务端的则没有变化。

UE4 C++联网RPC教程笔记(二)(第5~7集),UE4/5 的学习笔记,ue4,c++,rpc

通过 RepNotify 和 ReplicatedUsing 来同步变量

(4)RepNotifyReplicatedUsing

参考文档:

  1. [UE4] RepNotify,更新通知
  2. UE4 c++ 变量RepNotify

复制通知的功能是在可复制的 Actor 下定义某个值,这个值绑定某个方法,当在服务端修改这个值时,绑定的方法会在客户端执行,如果想要在服务端也执行绑定的方法,需要再调用一次;当在客户端修改这个值或者执行这个值绑定的方法时,只会对所在的客户端有效果,对服务端和其他客户端没有效果。

使用例子如下:

.h文件

	UPROPERTY(ReplicatedUsing = OnRep_变量名)
	bool 变量名;
	
	UFUNCTION()
	void OnRep_变量名();

.cpp文件

	// 修改变量值,修改后客户端会执行 OnRep_变量名() 方法,但不会执行服务端的方法
	变量名 = true;
	// 执行变量方法,会在服务端执行 OnRep_变量名() 方法,但是不会在客户端执行
	OnRep_变量名();
	// 所以如果要在服务端和客户端都执行,必须两个语句一起写

接下来试一下复制通知的功能。我们打算声明一个应用复制通知的 bool 变量,通知方法的作用是将火焰特效关闭。然后在 CountDownTimer 递减为 0 时改变一下它的值使客户端自动运行通知方法,再主动调用通知方法使服务端的火焰特效也关闭。

FireEffectActor.h

protected:

	UFUNCTION()
	void OnRep_Deactivate();

protected:

	UPROPERTY(ReplicatedUsing = OnRep_Deactivate)
	bool Deactivate;

FireEffectActor.cpp

void AFireEffectActor::BeginPlay()
{


	// 将递减 Timer 的逻辑改回在服务端运行
	if (GetWorld()->IsServer()) {
		FTimerDelegate UpdateTimerDele = FTimerDelegate::CreateUObject(this, &AFireEffectActor::UpdateTimer);
		GetWorld()->GetTimerManager().SetTimer(UpdateTimerHandle, UpdateTimerDele, 1.f, true);
	}
}

void AFireEffectActor::UpdateTimer()
{
	if (CountDownTimer > 0)
		CountDownTimer -= 1;
	else {
		// 改变一下以便能够触发客户端执行通知方法
		Deactivate = !Deactivate;
		// 主动调用通知方法以便让服务端也执行对应逻辑
		OnRep_Deactivate();
	}
}

void AFireEffectActor::OnRep_Deactivate()
{
	// 关闭火焰特效
	FireEffect->Deactivate();
}

void AFireEffectActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{


	// 注册复制变量
	DOREPLIFETIME(AFireEffectActor, Deactivate);
}

编译后运行,可以看到服务端和客户端的 TextRender 的数字都在递减,当数字为 0 后,两个端的火焰特效都一起被关闭了。

UE4 C++联网RPC教程笔记(二)(第5~7集),UE4/5 的学习笔记,ue4,c++,rpc

再试一下不主动调用通知方法,看看服务端是否不会执行。

FireEffectActor.cpp

void AFireEffectActor::UpdateTimer()
{
	if (CountDownTimer > 0)
		CountDownTimer -= 1;
	else {
		Deactivate = !Deactivate;
		// 这次测试完毕后记得取消注释
		//OnRep_Deactivate();
	}
}

编译后运行,这次数字为 0 后,客户端的火焰特效被关闭了,但服务端的火焰特效还在。

UE4 C++联网RPC教程笔记(二)(第5~7集),UE4/5 的学习笔记,ue4,c++,rpc
接下来笔者又试了另外两种情况:

恢复上面对复制通知方法的调用,同时让 FireEffectActor.cpp 的 BeginPlay() 里面的计时操作在客户端运行,则只有客户端的 TextRender 会递减;数字为 0 后客户端的火焰特效消失。服务端没有任何变化。

但是如果注释掉对复制通知方法的调用,让计时操作在客户端运行,则只有客户端的 TextRender 会递减;但是数字为 0 后客户端的火焰特效不会消失。服务端没有任何变化。很显然,变量在客户端上变化的时候不会自动调用通知方法,并且此时调用通知方法只对客户端的对象有效。

6. 联网方法

前面讲到了复制变量,这节课来讲复制方法。下面文字截取自梁迪老师的 RPC 联网文档。

参考文档:UE4 RPC在C++中的使用简例

  1. ReliableUnreliable:前者一定会执行;后者在网络不好的时候可能会丢弃。
  2. ClientServerNetMulticast
    Client:如果在服务器运行,会在拥有该 Actor 的客户端上调用;如果在客户端调用,只会在当前客户端上执行
    Server:客户端调用时,在服务器运行,通常用于客户端给服务器传递数据,服务端调用只在服务端运行
    NetMulticast:在服务端调用,会广播到所有客户端;如果在客户端调用,只会在当前客户端上执行
  3. WithValidation 进行安全检查,如果是使用 Server 函数,一定要在 UFUNCTION() 内添加这个声明,并且在 .h 文件声明 void 方法名_Implementation(var)bool 方法名_Validate(var),并且在 .cpp 文件实现,而方法名本身的方法不需要实现,其中 _Implementation 实现逻辑,_Validate内实现安全检验,防止作弊。但是有些类型不能作为参数,比如 FString,编译会报错

案例:UE4 的 NetWork 简单原理(这篇文章目前是 VIP 文章,读者如果没开会员也可以不用看)

示例如下:

.h文件

	// 使用了 RPC 说明符的函数
	UFUNCTION(Server, Reliable, WithValidation) 
	void ServerMove(FVector Velocity, bool bSweep); 
	// 安全检验方法
	virtual bool ServerMove_Validate(FVector Velocity, bool bSweep); 
	// 执行方法
	virtual void ServerMove_Implementation(FVector Velocity, bool bSweep);

.cpp文件

	bool ASWeapon::ServerHandleFiring_Validate() 
	{ 
		return true;	// 默认返回为true,返回 false 则服务器会把该客户端踢掉
	} 
	void ASWeapon::ServerHandleFiring_Implementation() 
	{ 
		// 具体的函数逻辑 
	} 

测试多播方法

接下来开始实操。我们打算创建一个不可复制的、带火焰粒子组件的蓝图 Actor;给项目的默认角色添加一个多播(NetMulticast,也可以叫广播)的方法,该方法的逻辑是生成一个火焰 Actor 对象在当前角色的位置,并且在角色跳跃的时候调用这个方法。

在 Blueprint 下创建一个 Actor 蓝图,命名为 UnReplicateFire。然后修改如下:

UE4 C++联网RPC教程笔记(二)(第5~7集),UE4/5 的学习笔记,ue4,c++,rpc

接下来在项目自带角色类里添加多播方法,并将多播方法绑定到跳跃按键上;多播方法的逻辑就是在角色当前位置生成火焰特效 Actor。

RPCCourseCharacter.h

protected:

	// 广播事件
	UFUNCTION(NetMulticast, Reliable)
	void SpaceBarNetMulticast();

RPCCourseCharacter.cpp

void ARPCCourseCharacter::SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent)
{
	check(PlayerInputComponent);
	PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &ACharacter::Jump);
	PlayerInputComponent->BindAction("Jump", IE_Released, this, &ACharacter::StopJumping);

	// 添加这个按键绑定方法
	PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &ARPCCourseCharacter::SpaceBarNetMulticast);
	
}

void ARPCCourseCharacter::SpaceBarNetMulticast_Implementation()
{
	UClass* FireEffectClass = LoadClass<AActor>(NULL, TEXT("Blueprint'/Game/Blueprint/UnReplicateFire.UnReplicateFire_C'"));
	GetWorld()->SpawnActor<AActor>(FireEffectClass, GetActorTransform());
}

编译后,将运行的玩家人数改为 3(确保有聆听服务器)。运行游戏,让服务端的角色跳一下,可以看到服务端和两个客户端都能看到生成了火焰;但是让其中一个客户端跳一下,只有该客户端能看到生成火焰,服务端和另一个客户端都看不到。

GIF 动图录制出来超过 5MB,这里就不放图片了,后面如果有不放图片的也是同样原因。

测试服务端发消息给客户端的 Client 方法

接下来测试 Client 方法。我们打算新建一个不可复制的、只带有数字的 Actor;在角色类声明一个 Client 方法,其作用是生成数字 Actor 并传入值赋给这个数字。最后让角色类绑定一个按键到这个方法。

在默认路径新建一个 C++ 的 Actor 类,命名为 NumPad

NumPad.h

class UTextRenderComponent;

UCLASS()
class RPCCOURSE_API ANumPad : public AActor
{
	GENERATED_BODY()
	
public:

	// 由于 URenderTextComponent::SetText() 在 4.26 提倡传入 FText 类型的参数,所以笔者更改了形参类型,原本是 FString
	void AssignRenderText(FText InText);
	
public:

	UPROPERTY(EditAnywhere)
	UTextRenderComponent* TextRender;
};

NumPad.cpp

// 引入头文件
#include "Components/TextRenderComponent.h"

ANumPad::ANumPad()
{
	PrimaryActorTick.bCanEverTick = false;	// 关闭 Tick

	// 设置不可复制
	SetReplicates(false);

	RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("RootScene"));

	TextRender = CreateDefaultSubobject<UTextRenderComponent>(TEXT("TextRender"));
	TextRender->SetupAttachment(RootComponent);

	TextRender->SetWorldSize(80.f);
	TextRender->SetTextRenderColor(FColor::Red);
	TextRender->SetRelativeLocation(FVector(0.f, 0.f, 50.f));
}

void ANumPad::AssignRenderText(FText InText)
{
	TextRender->SetText(InText);
}

RPCCourseCharacter.h

protected:

	// J 键绑定
	void KeyJEvent();

	// Client 联网方法
	UFUNCTION(Client, Reliable)
	void KeyJClient(int32 InInt);

RPCCourseCharacter.cpp

// 引入头文件
#include "NumPad.h"

void ARPCCourseCharacter::SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent)
{


	// 添加按键绑定方法
	PlayerInputComponent->BindKey(EKeys::J, IE_Pressed, this, &ARPCCourseCharacter::KeyJEvent);
}


void ARPCCourseCharacter::KeyJEvent()
{
	// 只在服务端运行
	if (GetWorld()->IsServer()) {
		TArray<AActor*> ActArray;	// 老师多输入了一个 a
		UGameplayStatics::GetAllActorsOfClass(GetWorld(), ARPCCourseCharacter::StaticClass(), ActArray);
		for (int i = 0; i < ActArray.Num(); ++i) {
			if (ActArray[i] != this) {
				Cast<ARPCCourseCharacter>(ActArray[i])->KeyJClient(i);
			}
		}
	}
}

void ARPCCourseCharacter::KeyJClient_Implementation(int32 InInt)
{
	ANumPad* NumPad = GetWorld()->SpawnActor<ANumPad>(ANumPad::StaticClass(), GetActorTransform());
	NumPad->AssignRenderText(FText::AsNumber(InInt));
}

最后添加对 Slate 的依赖(因为设置了 TextRender 的参数),否则编译无法通过。

RPCCourse.Build.cs

public RPCCourse(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

		PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay", "Slate" });	// 添加 Slate 模块依赖
	}

编译后运行,在服务端按下 J 键,可以看到两个客户端角色所在位置分别生成了一个红色的数字,只有对应的客户端才能看到自己的数字。

UE4 C++联网RPC教程笔记(二)(第5~7集),UE4/5 的学习笔记,ue4,c++,rpc

测试客户端给服务端发消息的 Server 方法(带检验)

因为客户端的状况千奇百怪,所以给服务端发消息时必须要带安全检验,如果不符合检验条件则将其踢出会话。

这次我们打算让客户端按下按键后,在服务器端能看到客户端角色的位置生成一个数字 Actor。

RPCCourseCharacter.h

protected:

	// H 键绑定
	void KeyHEvent();

	// Server 方法
	UFUNCTION(Server, Reliable, WithValidation)
	void KeyHServer(int32 InInt);

	// Server 方法逻辑
	void KeyHServer_Implementation(int32 InInt);

	// Server 方法数据验证
	bool KeyHServer_Validate(int32 InInt);

RPCCourseCharacter.cpp

void ARPCCourseCharacter::SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent)
{


	PlayerInputComponent->BindKey(EKeys::H, IE_Pressed, this, &ARPCCourseCharacter::KeyHEvent);
}

void ARPCCourseCharacter::KeyHEvent()
{
	// 在客户端运行
	if (!GetWorld()->IsServer()) {
		KeyHServer(3);
	}
}

void ARPCCourseCharacter::KeyHServer_Implementation(int32 InInt)
{
	ANumPad* NumPad = GetWorld()->SpawnActor<ANumPad>(ANumPad::StaticClass(), GetActorTransform());
	NumPad->AssignRenderText(FText::AsNumber(InInt));
}

bool ARPCCourseCharacter::KeyHServer_Validate(int32 InInt)
{
	if (InInt > 0)
		return true;
	return false;
}

编译后运行,在任意一个客户端按下 H 键,只有服务端会看到该客户端的角色处生成一个红色的 3。

UE4 C++联网RPC教程笔记(二)(第5~7集),UE4/5 的学习笔记,ue4,c++,rpc

7. 蓝图实现监听服务器

这节课我们会利用蓝图节点来实现局域网联机功能,其中包括创建会话和加入会话这两个功能。之前我们在聆听服务器模式下运行游戏则省略了 “服务器创建会话” 和 “客户端加入会话” 这两个过程。

我们打算在另一个空白地图里放上一个主界面 UI,包括创建会话和加入会话的按钮,进入会话的端则会来到有可操控小白人的地图。

在默认路径下新建以下 C++ 类:

一个 GameModeBase,命名为 MenuGameMode。作为游戏主界面的游戏模式。
一个 PlayerController,命名为 MenuController。作为游戏主界面的玩家控制器。
一个 UserWidget,命名为 MenuWidget。作为游戏主界面的 UI。

新建一个 Map 文件夹,在里面创建一个新的空白 Level,命名为 MenuMap。然后打开该地图。

在编辑器界面 Edit -> Project Settings -> Maps & Modes 里,将 MenuMap 设置为编辑器初始地图和默认地图。

UE4 C++联网RPC教程笔记(二)(第5~7集),UE4/5 的学习笔记,ue4,c++,rpc

将 ThirdPersonCPP/Maps 路径下的 ThirdPersonExampleMap 移到 Map 文件夹。随后将其改名为 GameMap

右侧 WorldSettings,将 MenuMap 默认的 GameMode 设置为 MenuGameMode。

UE4 C++联网RPC教程笔记(二)(第5~7集),UE4/5 的学习笔记,ue4,c++,rpc
来到 MenuGameMode,给主界面游戏模式设定 PlayerController,并设置默认 Pawn 为 NULL。

MenuGameMode.h

public:

	AMenuGameMode();

MenuGameMode.cpp

// 引入头文件
#include "MenuController.h"

AMenuGameMode::AMenuGameMode()
{
	PlayerControllerClass = AMenuController::StaticClass();

	DefaultPawnClass = NULL;
}

在 Blueprint 下创建一个 Widget Blueprint,命名为 MenuWidget_BP。打开蓝图,将其父类设置为 MenuWidget。随后修改其界面如下:(两个按钮的锚点都改成居中)

UE4 C++联网RPC教程笔记(二)(第5~7集),UE4/5 的学习笔记,ue4,c++,rpc
在蓝图图表重写这两个按钮的点击事件,即创建会话和加入会话的逻辑:

UE4 C++联网RPC教程笔记(二)(第5~7集),UE4/5 的学习笔记,ue4,c++,rpc
UE4 C++联网RPC教程笔记(二)(第5~7集),UE4/5 的学习笔记,ue4,c++,rpc

其实还有一个 Destroy Session 的蓝图节点,可以用于关闭会话。

因为使用了 UMG 所以要添加 UMG 的依赖。

RPCCourse.Build.cs

public RPCCourse(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

		PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay", "Slate", "UMG" });	// 添加 UMG 模块依赖
	}

给主界面玩家控制器设置 UI 输入模式,并且显示主菜单 UI 到界面。

MenuController.h

protected:

	virtual void BeginPlay() override;

MenuController.cpp

// 引入头文件
#include "MenuWidget.h"

void AMenuController::BeginPlay()
{
	Super::BeginPlay();

	// 设定输入模式
	bShowMouseCursor = true;
	FInputModeUIOnly InputMode;
	InputMode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
	SetInputMode(InputMode);

	// 创建 UI
	UClass* MenuWidgetClass = LoadClass<UMenuWidget>(NULL, TEXT("WidgetBlueprint'/Game/Blueprint/MenuWidget_BP.MenuWidget_BP_C'"));
	UMenuWidget* MenuWidget = CreateWidget<UMenuWidget>(GetWorld(), MenuWidgetClass);
	MenuWidget->AddToViewport();
}

编译后,将运行选项的玩家数量改成 2。运行游戏,在服务端点击创建服务端按钮,创建成功并进入了 GameMap;在客户端点击加入服务器,稍等片刻后加入成功,进入了 GameMap。(让客户端创建服务器,服务端加入也可以)

(动图裁剪了 6 秒,避免图片体积过大)

UE4 C++联网RPC教程笔记(二)(第5~7集),UE4/5 的学习笔记,ue4,c++,rpc

不过有个问题是我们因为前面设置了 UI 输入模式所以现在无法控制角色,所以我们需要创建一个额外的 PlayerController。

在默认路径新建一个 C++ 的 PlayerController,命名为 RPCController。作为游玩时的玩家控制器。

RPCController.h

protected:

	virtual void BeginPlay() override;

RPCController.cpp

void ARPCController::BeginPlay()
{
	Super::BeginPlay();

	// 设定输入模式
	bShowMouseCursor = false;
	FInputModeGameOnly InputMode;
	SetInputMode(InputMode);
}

会话中的 GameMode 与会话状态监听

下面内容截取自梁迪老师准备的 RPC 联网文档:

  1. GameMode 只在服务端存在,可以用来管理服务端对象和客户端对象
  2. 创建项目时,会自动生成项目名 GameMode 类,如果没有在编辑器的 WorldSetting 中指定 GameMode,运行游戏后会自动使用这一个 GameMode 来作为游戏的 GameMode
  3. GameMode 的 DefaultPawnClass 需要自己设置,如果不设置的话会默认设置为 DefaultPawn 类,如果要使用自己定义的角色类,需要把 DefaultPawnClass 设置为 NULL,然后在 PostLogin() 以及 Logout() 等函数进行角色的生成
  4. 重写 GameMode 下的 PostLogin() 函数,在每次有新的端连接上服务端时(服务端创建时自己也会调用),会调用这个函数并且传入对应端的控制器 PlayerController,可以在这个方法里生成自定义的角色并且添加到对应控制器的 Process 让对应端进行控制,这样子的话角色就都是在服务端进行生成的,在服务端调用角色的 HasAuthority() 会返回 true,在客户端调用角色的 HasAuthority() 会返回 false。如果不重写这个 PostLogin() 方法,而是在 WorldSetting 下设定 DefaultPawnClass 为自定义的角色类,那么客户端的角色依然是由服务端生成的,在服务端调用角色的 HasAuthority() 会返回 true,客户端调用角色的 HasAuthority() 会返回 false。(推荐自己重写 PostLogin() 进行处理)
  5. 重写 GameMode 下的 Logout() 函数,可以处理客户端登出时的逻辑
  6. GameMode 提供 RestartPlayer() 函数允许寻找可用的 PlayerStart 点并且重新生成角色,其最主要的目的就是寻找生成点。可以在 PostLogin() 中通过 NewPlayer->GetPawn() 判断角色是否存在,如果存在的话,可以销毁。重写 RestartPlayer() 函数,销毁角色后调用 RestartPlayer() 函数,把 NewPlayer(类型为 PlayerController*) 参数传入进行自定义的重新生成对象,类似函数还有 RestartPlayerAtPlayerStart() 以及 RestartPlayerAtTransform(),如果自己书写生成点逻辑的话可以不用调用这个函数

接下来我们编写一下 GameMode 在 “端进入会话” 与 “端退出会话” 时所运行的逻辑。

RPCCourseGameMode.h

UCLASS(minimalapi)
class ARPCCourseGameMode : public AGameModeBase
{
	GENERATED_BODY()

public:
	ARPCCourseGameMode();

	// 用户进入
	virtual void PostLogin(APlayerController* NewPlayer) override;

	// 用户退出
	virtual void Logout(AController* Exiting) override;

protected:

	// 统计当前加入会话的玩家数量
	int32 PlayerCount;
};

RPCCourseGameMode.h

// 引入头文件
#include "RPCController.h"
#include "GameFramework/PlayerStart.h"
#include "Kismet/GameplayStatics.h"
#include "RPCHelper.h"

ARPCCourseGameMode::ARPCCourseGameMode()
{
	PlayerControllerClass = ARPCController::StaticClass();

	// 如果不给 WorldSetting 指定 GameMode,游戏运行时会自动把创建项目时生成的 {项目名}GameMode 这个类给设置上去
	// 如果创建的 GameMode 不指定 PawnClass 的话,会自动设定为 ADefaultPawn 类,所以这里必须给其设置 NULL
	DefaultPawnClass = NULL;

	PlayerCount = 0;
}

void ARPCCourseGameMode::PostLogin(APlayerController* NewPlayer)
{
	Super::PostLogin(NewPlayer);

	// 销毁已经存在的角色对象
	if (NewPlayer->GetPawn()) {
		GetWorld()->DestroyActor(NewPlayer->GetPawn());
	}

	// 获取场上所有的玩家生成点
	TArray<AActor*> ActArray;
	UGameplayStatics::GetAllActorsOfClass(GetWorld(), APlayerStart::StaticClass(), ActArray);
	if (ActArray.Num() > 0) {

		PlayerCount++;

		// 创建角色后让玩家控制器接管角色
		UClass* CharacterClass = LoadClass<ARPCCourseCharacter>(NULL, TEXT("Blueprint'/Game/ThirdPersonCPP/Blueprints/ThirdPersonCharacter.ThirdPersonCharacter_C'"));
		ARPCCourseCharacter* NewCharacter = GetWorld()->SpawnActor<ARPCCourseCharacter>(CharacterClass, ActArray[0]->GetActorLocation() + FVector(0.f, PlayerCount * 200.f, 0.f), ActArray[0]->GetActorRotation());
		NewPlayer->Possess(NewCharacter);

		DDH::Debug() << NewPlayer->GetName() << " Login" << DDH::Endl();
	}
}

void ARPCCourseGameMode::Logout(AController* Exiting)
{
	Super::Logout(Exiting);

	PlayerCount--;

	DDH::Debug() << Exiting->GetName() << " Logout" << DDH::Endl();
}

编译后,将 GameMap 的默认 GameMode 设置成 RPCCourseGameMode。

UE4 C++联网RPC教程笔记(二)(第5~7集),UE4/5 的学习笔记,ue4,c++,rpc
随后在 MenuMap 运行游戏,在服务端点击创建服务端按钮,创建成功并进入了 GameMap;在客户端点击加入服务器,稍等片刻后加入成功,进入了 GameMap,并且此时可以自由控制角色移动了。(客户端创建服务器,服务端加入也可以)

如果将玩家数量设置为 3,重复上述操作,然后在创建服务端的那个窗口操控角色按 J 键,可以看到只有另外两个端能看到各自角色的位置生成了一个红色的数字,这说明即便角色的生成逻辑在 GameMode,角色的所属权还是归客户端所有的。文章来源地址https://www.toymoban.com/news/detail-836615.html

到了这里,关于UE4 C++联网RPC教程笔记(二)(第5~7集)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • UE4运用C++和框架开发坦克大战教程笔记(十三)(第40~42集)

    上节课实现了按键绑定系统的 4 种基础绑定,这节课来实现多按键事件的绑定。 我们为多按键绑定额外编写一个类 InputBinder 。 DDMessage.h DDMessage.cpp 依旧是部署好 DDMessage – DDModule – DDOO – 对象 这条调用链。 DDModule.h DDModule.cpp DDOO.h 为了避免出现忘记注销事件指针导致访问错

    2024年02月03日
    浏览(43)
  • UE4运用C++和框架开发坦克大战教程笔记(十一)(第34~36集)

    我们前面已经在一个类里面实现了一套可行的协程系统,接下来我们需要通过宏来将它们变得更加方便可用,不必每次都写这么多代码。 将 CoroActor 头文件里的委托声明语句以及两个结构体全复制到 DDTypes 下,改成通用的结构。下面只列出需要更改的代码。 DDTypes.h 来到 Cor

    2024年02月03日
    浏览(34)
  • UE4运用C++和框架开发坦克大战教程笔记(十五)(第46~48集)

    逻辑和批量加载同类 UObject 资源的逻辑差不多。区别在 DealClassKindLoadStack() 内,如果已经有资源率先加载完成了,那后续资源加载的途中我们想让已经加载好的资源执行额外的处理逻辑(比如让它每帧生成),我们就需要补充额外的判断条件,即判断其是否第一次生成完毕。

    2024年01月25日
    浏览(46)
  • UE4运用C++和框架开发坦克大战教程笔记(十二)(第37~39集)

    由于梁迪老师是写 Unity 游戏出身的,所以即便 UE4 有自带的 TimeManager 这样的延时系统,老师还是重新写了一个符合 Unity 开发习惯的延时系统。 在 DDTypes 里定义延时任务结构体,以及它要用到的一个委托。 DDTypes.h 我们依旧将延时系统放在 DDMessage 这里。 协程系统和延时系统

    2024年02月02日
    浏览(44)
  • 【UE4】多人联机教程(重点笔记)

    1. 创建房间、搜索房间功能 2. 根据指定IP和端口加入游戏 1. 新建一个第三人称角色模板工程 2. 创建一个空白关卡,这里命名为“InitMap” 3. 新建一个控件蓝图,这里命名为“UMG_ConnectMenu” 在关卡蓝图中显示该控件蓝图 打开“UMG_ConnectMenu”,添加如下控件 首先添加创建房间按

    2024年02月14日
    浏览(47)
  • UE4 材质学习笔记

    CheapContrast与CheapContrast_RGB都是提升对比度的,一个是一维输入,一个是三维输入,让亮的地方更亮,暗的地方更暗,不像power虽然也是提升对比度,但是使用过后的结果都是变暗或者最多不变(值为1的情况) 乘法乘以超过1不会为纯白色,而加法超过一,材质就呈现白色,乘

    2024年02月11日
    浏览(45)
  • 墨尘 - UE4 入门教程笔记 —— 二

    旋转视图:Alt + 鼠标左键 缩放视图:Alt + 鼠标右键 平移视图:Alt + 鼠标中间 移动:E、缩放:T、R:旋转 撤回:Ctrl + Z 复原视图:Ctrl + shift + Z 转为可编辑对象:C 局部坐标与世界坐标切换:W 循环线选中:双击(确保坐标轴关闭) 打组:Alt + G、取消打组:shift + G 循环切刀工

    2024年02月16日
    浏览(46)
  • UE4 顶点着色 学习笔记

    首先区别一下StaticMesh和StaticMeshComponent StaticMesh是模型本身 而StaticMeshComponent是模型出来的实例 直接修改StaticMesh的内容,所有StaticMeshComponent实例都会产生变化 而修改StaticMeshComponent直会对实例产生影响不会对StaticMesh有任何修改 函数参数 1、要修改顶点着色的StaticMeshCommponent

    2024年02月04日
    浏览(52)
  • UE4 unlua学习笔记

    将这三个插件放入Plugins内并重新编译   创建一个BlueprintLibrary,声明一个全局函数  在这里声明路径 点击Create Lua Template   在Content的Script即可生成对应的lua文件打开它!  显示以上lua代码 打印Hello Unlua 创建该UI,就会在创建UI的Construct里面进行打印Hello Unlua效果如下:   静态

    2024年02月15日
    浏览(44)
  • UE4 HLSL学习笔记

    在Custom配置对应ush文件路径 在HLSL中写入对应代码 Custom里面增加两个Input,名字必须和ush文件内的未知变量名字一样 然后就对应输出对应效果的颜色 这就是简单的加法运算 减法同理: 乘法除法同理 HLSL取最小值 HLSL取最大值 绝对值: 取余: 四舍五入 Pow运算: 材质里面是

    2024年02月06日
    浏览(51)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包