【UE4C++-ActionRougelike-21】同步行动系统
一、Action执行情况
1、Action执行情况
1.1 输出执行情况
在ActionRoguelike.h中定义一个静态函数用来输出Action执行的具体信息到屏幕上
static void LogOnScreen(UObject* WorldContext, FString Msg, FColor Color = FColor::White, float Duration = 5.0f)
{
// 确保WorldContext非空,否则退出函数
if (!ensure(WorldContext))
{
return;
}
// 从WorldContext获取UWorld对象
UWorld* World = WorldContext->GetWorld();
// 确保UWorld对象非空,否则退出函数
if (!ensure(World))
{
return;
}
// 判断当前运行模式是否为客户端模式,根据模式设置消息前缀
FString NetPrefix = World->IsNetMode(NM_Client) ? "[CLIENT] " : "[SERVER] ";
// 如果GEngine对象存在
if (GEngine)
{
// 使用GEngine将带有前缀的消息添加到屏幕上显示
GEngine->AddOnScreenDebugMessage(-1, Duration, Color, NetPrefix + Msg);
}
}
在MyActionComponent.cpp中获取当前角色的Action执行情况,并通过上面定义的LogOnScreen方法输出到屏幕
void UMyActionComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
//FString DebugMsg = GetNameSafe(GetOwner()) + " : " + ActiveGameplayTags.ToStringSimple();
//GEngine->AddOnScreenDebugMessage(-1, 0.0f, FColor::White, DebugMsg);
// 遍历Actions数组中的每一个UMyAction对象
for (UMyAction* Action : Actions)
{
// 根据UMyAction对象是否正在运行,设置文本颜色为蓝色或白色
FColor TextColor = Action->IsRunning() ? FColor::Blue : FColor::White;
// 使用格式化字符串(Printf)创建一个包含UMyAction对象信息的消息
FString ActionMsg = FString::Printf(TEXT("[%s] Action: %s : IsRunning: %s : Outer: %s"),
*GetNameSafe(GetOwner()), // GetOwner()的安全名称
*Action->ActionName.ToString(), // Action的名称
Action->IsRunning() ? TEXT("true") : TEXT("false"), // Action是否正在运行(true或false)
*GetNameSafe(Action->GetOuter())); // Action->GetOuter()的安全名称
// 调用之前定义的LogOnScreen函数,将ActionMsg显示在屏幕上,颜色为TextColor,持续时间为0秒
LogOnScreen(this, ActionMsg, TextColor, 0.0f);
}
}
1.2 执行情况
当某角色的Action组件中的一个Action执行时,会首先执行服务器端RPC函数ServerStartAction告诉服务器端执行Action;之后会执行Action->StartAction(相当于本地客户端执行Action)。但其他的客户端并不能同步此角色执行该Action。
如下方以多人运行时,当客户端1或客户端2执行Action时只会同步执行Action到服务器端和本地客户端;服务器端执行Action时只会同步到本地服务器端。
二、同步到客户端
1、同步执行Action
将执行Action同步给所有客户端的思路:
- 设置bIsRunning可复制,作为每个MyAction是否在运行中的标志,当服务器执行某个Action时,会修改bIsRunning为true
- 服务器端会将bIsRunning的修改结果同步给所有客户端
- 此时所有客户端会触发修改该MyAction属性bIsRunning的回调函数,回调函数内则执行本地客户端该MyAction
class ACTIONROGUELIKE_API UMyAction : public UObject
{
//bIsRunning属性将进行网络复制,当该属性发生变化时(由服务器接收到更新),将调用名为OnRep_IsRunning的函数
UPROPERTY(ReplicatedUsing="OnRep_IsRunning")
bool bIsRunning;
UFUNCTION()
void OnRep_IsRunning();
}
//被服务器告知bIsRunning属性变化后的回调函数,根据bIsRunning来更新是否执行该MyAction动作
void UMyAction::OnRep_IsRunning()
{
//如果bIsRunning为真,则会调用该MyAction的StartAction从而更新本地客户端这个MyAction的动作
if (bIsRunning)
{
StartAction(nullptr);
}
else
{
StopAction(nullptr);
}
}
void UMyAction::GetLifetimeReplicatedProps(TArray<class FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(UMyAction, bIsRunning);
}
在MyActionComponent中重写函数bool ReplicateSubobjects(class UActorChannel Channel, class FOutBunch Bunch, FReplicationFlags* RepFlags) override;**
//MyActionComponent.cpp
/*ReplicateSubobjects是一个虚函数,用于在Unreal Engine中处理子对象的网络复制。
当有一个继承自AActor或UActorComponent的类,并且该类具有需要在网络中进行复制的子对象时,
需要重载ReplicateSubobjects函数。这个函数用于定义如何复制子对象以保证服务器和客户端之间的游戏状态同步。*/
bool UMyActionComponent::ReplicateSubobjects(class UActorChannel* Channel, class FOutBunch* Bunch, FReplicationFlags* RepFlags)
{
bool WroteSomething = Super::ReplicateSubobjects(Channel, Bunch, RepFlags);
// 遍历Actions数组中的每一个UMyAction对象
for (UMyAction* Action : Actions)
{
if (Action)
{
// 调用Channel->ReplicateSubobject方法为当前Action对象进行网络复制
WroteSomething |= Channel->ReplicateSubobject(Action, *Bunch, *RepFlags);
}
}
// 返回是否成功复制了任何子对象
return WroteSomething;
}
至此,完成了执行Action在服务器端和所有客户端的同步。大致执行流程如下:
- 按键输入指令后,触发MyCharacter中处理函数,调用ActionComp执行动作:ActionComp->StartActionByName
- StartActionByName中判断触发者是否为服务器,如果不是服务器端,则先调用服务器RPC函数ServerStartAction在服务器端执行Action。
- 本地执行该Action,调用Action->StartAction(Instigator);
- 在StartAction中修改该Action的属性bIsRunning = true,服务器执行时会将bIsRunning(宏定义:UPROPERTY(ReplicatedUsing=”OnRep_IsRunning”))同步复制给所有客户端
- 客户端会触发修改bIsRunning的回调函数OnRep_IsRunning,执行OnRep_IsRunning,执行自己的StartAction。
三、优化和Debug
1、优化
在MyActionComponent的BeginPlay()函数中我们为每个MyActionComponent的Actions数组进行添加,但是BeginPlay是会在服务器和客户端上都调用。所以我们可以优化这部分代码:将添加Action只在服务器端执行,令Actions数组可复制,由UE系统复制给每个客户端
//MyActionComponent.h
// 声明一个可复制的动作指针的数组
UPROPERTY(Replicated)
TArray<UMyAction*> Actions;
//MyActionComponent.cpp
/*这里使AddAction(GetOwner(), ActionClass);只在服务器端执行,因为BeginPlay会在服务器和客户端上都调用。
我们可以将这部分代码只让服务器端执行,让UE系统把Actions复制给每个客户端。*/
void UMyActionComponent::BeginPlay()
{
Super::BeginPlay();
// 检查当前对象的拥有者是否具有网络权限(服务器)
if (GetOwner()->HasAuthority())
{
// 如果具有网络权限(是服务器),遍历 DefaultActions 数组,将其中的每个能力添加到 Actions 数组中
for (TSubclassOf<UMyAction> ActionClass : DefaultActions)
{
// 将ActionClass实例添加到当前对象的Actions数组中
AddAction(GetOwner(), ActionClass);
}
}
}
//复制属性
void UMyActionComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(UMyAction, bIsRunning);
DOREPLIFETIME(UMyActionComponent, Actions);
}
//在MyAction.h中添加以下函数,表示MyAction支持网络复制
//判断对象是否支持网络复制,在实现自定义的UObject子类时,重写这个函数,以表明你的类是否支持网络复制
bool IsSupportedForNetworking() const override
{
return true;
}
2、解决bug
注:这里运行后,客户端执行Action(攻击)时会报错,错误原因是World为空指针。
这里是因为MyAction.cpp中的GetWorld中的GetOuter在服务器上返回的是ActionComponent对象,在客户端上返回的是PlayerCharacter对象。所以客户端执行时会返回空指针。
// 报错版本
UWorld* UMyAction::GetWorld() const
{
// 获取该Action所在的容器对象
UActorComponent* Comp = Cast<UActorComponent>(GetOuter()); //客户端上这里的GetOuter返回的其实是PlayerCharacter
if (Comp)
{
// 如果容器对象为 UActorComponent 类型,就返回其所在的世界的指针
return Comp->GetWorld();
}
// 如果无法获取到容器对象或者世界的指针,则返回 nullptr
return nullptr;
}
// 修改版本
UWorld* UMyAction::GetWorld() const
{
// 获取该Action所在的容器对象
AActor* Actor = Cast<AActor>(GetOuter());
if (Actor)
{
// 如果容器对象为 AActor 类型,返回其所在的世界的指针
return Actor->GetWorld();
}
// 如果无法获取到容器对象或者世界的指针,则返回 nullptr
return nullptr;
}
修改后通过服务器端的提示信息能够看到两者的区别
因此也需要对其他方法进行修改
//修改前
//在UMyAction实例中可以通过该函数获取其所属的UMyActionComponent实例
UMyActionComponent* UMyAction::GetOwningComponent() const
{
return Cast<UMyActionComponent>(GetOuter());
}
//修改后
//方法1
UMyActionComponent* UMyAction::GetOwningComponent() const
{
AActor* Actor = Cast<AActor>(GetOuter());
//缺点:GetComponentByClass会在所有Component类中遍历,影响效率
return Actor->GetComponentByClass(UMyActionComponent::StaticClass());
}
//方法2
//MyAction.h
//在MyAction中增加一个ActionComp表示其所属MyActionComponent组件,当一个MyAction被加入到角色的行为组件中的Actions数组时,利用Initialize方法对这个Action的ActionComp初始化
UPROPERTY(Replicated)
UMyActionComponent* ActionComp;
void Initialize(UMyActionComponent* NewActionComp);
//MyAction.cpp
//为Action初始化其所属ActionComponent组件
void UMyAction::Initialize(UMyActionComponent* NewActionComp)
{
ActionComp = NewActionComp;
}
//在AddAction时将NewAction的所属ActionComponent利用Initialize函数初始化
void UMyActionComponent::AddAction(AActor* Instigator, TSubclassOf<UMyAction> ActionClass)
{
// 创建一个新的动作对象添加到 Actions 数组中
UMyAction* NewAction = NewObject<UMyAction>(GetOwner(), ActionClass);
if (ensure(NewAction))
{
NewAction->Initialize(this); //每次AddAction时调用该函数,记录该Action的ActionComp
}
}
//直接返回MyAction的ActionComp属性
UMyActionComponent* UMyAction::GetOwningComponent() const
{
//AActor* Actor = Cast<AActor>(GetOuter());
//return Actor->GetComponentByClass(UMyActionComponent::StaticClass());
return ActionComp;
}
//将ActionComponent复制
void UMyAction::GetLifetimeReplicatedProps(TArray<class FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(UMyAction, ActionComp);
}