【UE4C++-ActionRougelike-24】作业7+UMG菜单
作业7
- 在PlayerState中复制Credits
- 多人游戏下实现Pickup
- 同步状态(可见性&碰撞)
- 仅在服务器端改变积分/健康值
- 复制愤怒属性
- 仅在服务器端增加/移除愤怒值
- 将发现玩家UI复制展示给客户端
一、复制积分
1、复制积分属性
标记积分属性可复制,并通过回调函数OnRep_Credits来触发积分改变事件,广播告知积分的改变。
// MyPlayerState.h
class ACTIONROGUELIKE_API AMyPlayerState : public APlayerState
{
protected:
UPROPERTY(EditDefaultsOnly, ReplicatedUsing="OnRep_Credits", Category = "PlayerState|Credits")
int32 Credits;
// OnRep_Credits函数接收一个名为OldCredits的int32类型参数,表示属性的旧值
UFUNCTION()
void OnRep_Credits(int32 OldCredits);
};
// MyPlayerState.cpp
void AMyPlayerState::OnRep_Credits(int32 OldCredits)
{
//广播积分改变事件,并传入新的Credits值和新旧积分差值
OnCreditsChanged.Broadcast(this, Credits, Credits - OldCredits);
}
2、修改积分UI控件创建时间
此时如果运行游戏,客户端并不会更新Credits,而且会报错。原因是在于客户端进入游戏时就为其创建Credits_Widget控件过早,此时客户端还未从服务器端收到PlayerState的副本,因此无法进行之后的事件绑定
为了解决以上问题,我们需要在合适时机调用积分UI控件。在MyPlayerController中重写BeginPlayingState方法,当玩家控制器开始进入“正在玩”的状态时,此函数会被调用,此时在调用自定义蓝图事件BlueprintBeginPlayingState来初始化UI。
// MyPlayerController.h
// 声明一个具有一个参数的动态多播委托,用于通知PlayerState改变事件
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnPlayerStateChanged, APlayerState*, NewPlayerState);
UCLASS()
class ACTIONROGUELIKE_API AMyPlayerController : public APlayerController
{
GENERATED_BODY()
protected:
//监听传入的玩家状态(对于客户端而言,在最初加入游戏时玩家状态可能为nullptr,
//之后玩家状态将不再改变,因为PlayerController在整个关卡中保持相同的玩家状态)。
UPROPERTY(BlueprintAssignable)
FOnPlayerStateChanged OnPlayerStateReceived;
//当玩家控制器准备开始游戏时调用,这是初始化诸如UI之类的东西的合适时机,
//如果在BeginPlay中初始化为时过早(尤其是在多人游戏客户端中,可能尚未接收到所有数据,例如PlayerState)
virtual void BeginPlayingState() override;
// 创建一个可以在蓝图中实现的BlueprintBeginPlayingState事件
UFUNCTION(BlueprintImplementableEvent)
void BlueprintBeginPlayingState();
// 重写OnRep_PlayerState方法,用于处理PlayerState的复制
void OnRep_PlayerState() override;
};
// MyPlayerController.cpp
void AMyPlayerController::BeginPlayingState()
{
// 调用蓝图中实现的BlueprintBeginPlayingState方法
BlueprintBeginPlayingState();
}
void AMyPlayerController::OnRep_PlayerState()
{
Super::OnRep_PlayerState();
// 广播OnPlayerStateReceived事件,将PlayerState作为参数传递
OnPlayerStateReceived.Broadcast(PlayerState);
}
//MyPlayerState.cpp
void AMyPlayerState::LoadPlayerState_Implementation(UMySaveGame* SaveObject)
{
if (SaveObject)
{
//Credits = SaveObject->Credits;
// 因为创建UI的时间推后了,通过AddCredits的方式给PlayerState的Credits赋值,确保积分修改事件触发
AddCredits(SaveObject->Credits);
}
}
将之前初始化UI时机从Event BeginPlay设置为EventBlueprintBeginPlayingState
二、复制愤怒
1、复制愤怒属性
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class ACTIONROGUELIKE_API UMyAttributeComponent : public UActorComponent
{
protected:
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Replicated, Category = "Attributes")
float Rage;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Replicated, Category = "Attributes")
float MaxRage;
// 声明一个多播的RPC函数,用于同步愤怒值的改变
UFUNCTION(NetMulticast, UnReliable) // @FIXME: mark as unreliable once we moved the 'state' our of scharacter
void MulticastRageChanged(AActor* InstigatorActor, float NewHealth, float Delta);
}
void UMyAttributeComponent::MulticastRageChanged_Implementation(AActor* InstigatorActor, float NewRage, float Delta)
{
OnRageChange.Broadcast(InstigatorActor, this, NewRage, Delta);
}
// 修改ApplyRageChange仅在服务器端进行计算,并同步到客户端
// 课程中并未对此进行修改,通过打断点调试能看到这样的话客户端是不会触发Rage修改委托
bool UMyAttributeComponent::ApplyRageChange(AActor* InstigatorActor, float Delta)
{
float OldRage = Rage;
float NewRage = FMath::Clamp(Rage + Delta, 0.0f, MaxRage);
float ActualDelta = NewRage - OldRage;
if (GetOwner()->HasAuthority())
{
Rage = NewRage;
if (ActualDelta != 0.0f)
{
//OnRageChange.Broadcast(InstigatorActor,this, Rage,ActualDelta);
MulticastRageChanged(InstigatorActor, Rage, ActualDelta);
}
}
return ActualDelta != 0;
}
2、玩家死亡更新愤怒值
3、效果演示
三、同步拾取物
1、复制拾取物类
在拾取物基类MyPickup中创建一个属性bIsActive用来表示拾取物是否被激活(能否被看到、碰撞),一个OnRep_IsActive方法作为bIsActive修改复制时的回调函数。
class ACTIONROGUELIKE_API AMyPickup : public AActor, public ISInteractionInterface
{
protected:
// 使用UPROPERTY声明一个可复制的布尔变量bIsActive,并指定在复制时调用OnRep_IsActive方法
UPROPERTY(ReplicatedUsing="OnRep_IsActive")
bool bIsActive;
// 声明一个UFUNCTION,用于在属性复制时调用
UFUNCTION()
void OnRep_IsActive();
}
// 设置物品拾取的激活状态
void AMyPickup::SetPickupState(bool bNewIsActive)
{
// 更新激活状态
bIsActive = bNewIsActive;
// 调用OnRep_IsActive方法,以应用新的激活状态
OnRep_IsActive();
}
// 当激活状态被复制时调用的方法
void AMyPickup::OnRep_IsActive()
{
// 根据激活状态设置碰撞检测
SetActorEnableCollision(bIsActive);
// 设置根组件和所有子组件的可见性
RootComponent->SetVisibility(bIsActive, true);
}
// 获取需要在网络中复制的属性
void AMyPickup::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// 添加bIsActive属性以在网络中进行复制
DOREPLIFETIME(AMyPickup, bIsActive);
}
2、效果演示
四、同步AI发现玩家UI
1、同步发现函数
// MyAICharacter.h
// RPC函数,在所有连接的客户端上执行
UFUNCTION(NetMulticast, Unreliable)
void MulticastPawnSeen();
// MyAICharacter.cpp
void AMyAICharacter::OnPawnSeen(APawn* Pawn)
{
// 如果当前目标Actor不是已发现的Pawn,则更新目标Actor并创建新的世界Widget
if (GetTargetActor() != Pawn)
{
// 更新目标Actor
SetTargetActor(Pawn);
// 同步到所有客户端
MulticastPawnSeen();
}
}
void AMyAICharacter::MulticastPawnSeen_Implementation()
{
// 创建一个UMyWorldUserWidget类型的新控件实例
UMyWorldUserWidget* NewWidget = CreateWidget<UMyWorldUserWidget>(GetWorld(), SpottedWidgetClass);
if (NewWidget)
{
// 将当前AI角色附加到新创建的控件上
NewWidget->AttachedActor = this;
// 将新控件添加到视口,并设置其层级为10(高于默认的0)
// 这样可以确保新控件位于其他控件的上方,例如,它不会被小兵的血条遮挡
NewWidget->AddToViewport(10);
}
}
2、效果演示
五、主菜单
1、主菜单按钮控件
创建自定义主页按钮控件来作为主菜单的按钮控件。控件布局如下:
在EventGraph中,添加Text变量作为按钮的文本设置,添加事件分派器OnCliked作为按钮被按下时调用的事件
2、主菜单控件
创建主菜单Widget,设置控件布局如下:
在EventGraph中绑定三个按键对应操作
3、创建主菜单关卡
3.1 创建新关卡
创建一个新的Level:MainMenu_Entry作为主菜单关卡,并设置该Level的WorldSetting中的GameMode为MainMenu_GameMode(下面要创建的主菜单游戏模式),并在Project Settings->Maps&Modes中设置GameDefaultMap为MainMenu_Entry
3.2 创建主菜单游戏模式
主菜单游戏模式首先创建主菜单Widget,然后显示鼠标,并将玩家的鼠标应用于当前游戏窗口。并且要将Classes中的Default Pawn Class设置为None。
并且要在PlayerController中,当游戏开始后(Main HUD创建后)使得玩家只与游戏世界交互
4、效果演示
六、暂停菜单
1、暂停功能
在MyPlayerController.h中创建一个UUserWidget指针用于保存暂停菜单的实例,显示/隐藏暂停菜单的方法
class ACTIONROGUELIKE_API AMyPlayerController : public APlayerController
{
protected:
// 用于指定暂停菜单的蓝图类
UPROPERTY(EditDefaultsOnly, Category = "UI")
TSubclassOf<UUserWidget> PauseMenuClass;
// 一个UUserWidget对象的指针,它将在运行时保存暂停菜单的实例
UPROPERTY()
UUserWidget* PauseMenuInstance;
// 一个函数,用于切换暂停菜单的显示/隐藏状态
UFUNCTION(BlueprintCallable)
void TogglePauseMenu();
// 重写基类APlayerController的函数SetupInputComponent,用于设置玩家的输入绑定
void SetupInputComponent() override;
}
在MyPlayerController.cpp中实现显示/隐藏暂停菜单方法,并绑定暂停菜单事件函数
void AMyPlayerController::TogglePauseMenu()
{
// 检查暂停菜单是否存在且在视口中
if (PauseMenuInstance && PauseMenuInstance->IsInViewport())
{
// 如果是,将其从父类中移除,并将其指针设为null
PauseMenuInstance->RemoveFromParent();
PauseMenuInstance = nullptr;
// 如果是,将其从父类中移除,并将其指针设为null
bShowMouseCursor = false;
SetInputMode(FInputModeGameOnly());
return;
}
// 否则,创建暂停菜单的一个实例
PauseMenuInstance = CreateWidget<UUserWidget>(this, PauseMenuClass);
if (PauseMenuInstance)
{
// 否则,创建暂停菜单的一个实例
PauseMenuInstance->AddToViewport(100);
// 显示鼠标光标并将输入模式设为只接受UI输入
bShowMouseCursor = true;
SetInputMode(FInputModeUIOnly());
}
}
//绑定暂停游戏事件函数
void AMyPlayerController::SetupInputComponent()
{
Super::SetupInputComponent();
// 将"PauseMenu"动作绑定到TogglePauseMenu函数。当按键被按下时,TogglePauseMenu函数将被调用
InputComponent->BindAction("PauseMenu", IE_Pressed, this, &AMyPlayerController::TogglePauseMenu);
}
2、暂停菜单控件
创建暂停菜单控件。控件布局如下:
在EventGraph中绑定两个按键对应操作
3、绑定按键
在Input中绑定暂停菜单按键,并在MyPlayerController_BP中设置PauseMenuClass为上面创建的暂停菜单控件蓝图