【UE4C++-ActionRougelike-24】作业7+UMG菜单


【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的副本,因此无法进行之后的事件绑定

绑定Credits_Widget

为了解决以上问题,我们需要在合适时机调用积分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

初始化UI

二、复制愤怒

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、玩家死亡更新愤怒值

PlayerRage_Widget蓝图设置

3、效果演示

Rage效果演示

三、同步拾取物

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、效果演示

AI发现玩家UI

五、主菜单

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。

主菜单GameMode蓝图设置

并且要在PlayerController中,当游戏开始后(Main HUD创建后)使得玩家只与游戏世界交互

PlayerController蓝图设置

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为上面创建的暂停菜单控件蓝图

4、效果演示

暂停菜单效果演示

文章作者: Woilin
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Woilin !
评论
  目录