环境配置
实在受不了visual studio code了,提示没有,报错离谱。unity用了很久很久还是没有适应,直接转Rider了。
内容参考这一篇Unreal 开发环境(Rider for unreal)配置 – 知乎,同时,对于项目的启动,也可以直接右键.uproject然后用Rider打开了,省去了自己构建.sin的步骤。
框架
MVC架构
启动流程
基本的游戏流程:Player的构造函数,随后Controller的Onposses,最后Player->Beginplay();
其中Onposses就是控制器获取对Pawn的控制权的时候触发的函数。
- Player的构造函数:通常用于初始化默认值和组件,但不会涉及与其他游戏对象的交互(因为世界还没准备好)。例如初始化默认变量(如速度、生命值)、创建组件(如相机、碰撞盒、武器等)。
- BeginPlay:当 Pawn(玩家或AI角色) 正式进入游戏世界 时调用。此时,世界(World)已准备好,Controller也已经 Possess 了 Pawn,可以访问World和 Controller。一般可以设置动画状态、初始化 HUD 或 UI、访问其他 Actor,比如武器、队友、敌人等。
- Onposses:存储 Pawn的引用,用来后续控制角色,然后做一些控制层面的初始化。
基本概念
面向对象
UE5高度面向对象,使用Cpp和其反射系统共同管理脚本,对Cpp面向对象有一定更改。
- 类和对象:一个是抽象一个是具体的实例
- 继承和接口:虚幻采用单继承,一个类只有一个父类;但支持接口(通过UInterface),允许实现多个接口,从而实现一个多重继承的效果。
- 封装:
- 访问控制:public,protected,private
- UPROPERTY:可以用这个宏来控制访问权限和反射信息,一般自己写Cpp类就会用到
- 虚函数和反射:virtual和override;反射系统允许运行时动态的访问和调用对象的属性和函数
继承和组合
继承是一个类从另一个类获取属性和行为的方式。相当于is-a
组合是把不同组件放在一起构建复杂系统的设计方式,比如说Actor和Component。相当于has-a,因此灵活性比较高,可以复用组件之类的。
AActor、APawn、ACharacter
- UObject:万物皆生于UObject,一个公共的基类便于实现UE的反射系统。
- AActor:UE世界中的一个个“演员”,有了一些额外的功能,最重要的就是位置坐标。因为一些演员没有碰撞体积(观战者),因此AActor没有赋予碰撞检测功能。
- APawn:可以操作的棋子,也就是角色,主要提供三个功能:
- 可被Controller控制
- PhysicsCollision表示
- MovementInput的基本响应接口
- ACharacter:简单理解就是人形的角色,这是因为游戏中代入的角色大部分是人。提供了像人一样行走的CharacterMovementComponent, 尽量贴合的CapsuleComponent,再加上骨骼上蒙皮的网格。
GameModeBase 和 GameMode
- AGameModeBase
- 游戏模式的基类,定义了游戏的规则,行为,逻辑
- 可以通过创建自定义的游戏模式类扩展它
- AGameMode
- 是AGameModeBase的一个子类,提供了更多控制权和功能,比如玩家控制,团队管理,记分系统等(一般也不用,所以一般GameModoBase就够了)
- 可以使用AGameMode类来实现更复杂的游戏逻辑和规则
GameSession
用来控制联机状态下,会话管理,游戏状态同步,跨服务器通信等情况,也可以管理控制权限
GameState 和 PlayerState
一个是游戏全局的状态,全局唯一包含了整个游戏状态;而后面的代表了单个玩家的状态,针对特定玩家
DefaultPawnClass
用来指定没有特定玩家控制情况下的默认角色,一般可以设置为游戏中任何Pawn类或其他子类
APlayerController和AUHD
前者控制玩家的输入,视角,动作;后者是用于显示游戏界面,可以显示各种元素,如生命值,得分,道具信息之类的
MVC思想
按照MVC思想,一个Gameplay可以分为三个部分,表现(View)、逻辑(Controller)、数据(Model)
GamePlay框架
对于游戏本身有GameMode和GameState两个大类,GameMode由游戏的基础规则构成,基于GameMode发生事件的信息则由GameState保存并同步给客户端。
玩家通过 PlayerController 加入游戏并交互,所以这个类也会提供一些与玩家的交互接口/功能,比如Input接收玩家的输入(键盘/鼠标/手柄),同时PlayerController 作为中间层,对于接收到玩家的交互,就会去管理控制 Pawn,产生对应行为如攻击,移动这些
HUD 提供UI显示,比如玩家的血条之类的
PlayerCameraManager 管理玩家的相机
对于在联机中,有很多数据/属性都是玩家固有的(比如玩家health/coins),保存在PlayerController中本身不会同步给其他客户端,在UE中还有一个PlayerState类,用来保存玩家强相关的一些数据/属性
对应过来UE的设置,在世界场景设置之中可以看到这些东西。
图上为世界场景设置下的一些选项,可以看到还有一个 SpectatorPawn 没有介绍,介绍功能来说,其相当于一个能让玩家在游戏中自由观察但不可以互动的东西,个人理解可能类似于王者或pubg里面的观战
- 职责核心:驱动一个无实体碰撞,无游戏逻辑的相机,提供观察者视角
- 主要用途:玩家死亡后等待观察,观众管理员视角,赛前赛后视角
- 状态关联:代表“非活跃参与者”,即观察者状态
- 一般来说会提供组件关联,即 SpectatorPawnMovement 组件来控制移动
Controller与Pawn
这个疑惑来自于我配置时候,即下面这段代码
void APlayCharacter::Look(const FInputActionValue& Value)
{
FVector2D LookAxis = Value.Get<FVector2D>();
// 对于继承自Pawn类的,一定会有Controller
// 对于配置,应当在GameMode中配置过,默认绑定了,外部可见
if (Controller)
{
// 控制摄像头旋转
AddControllerYawInput(LookAxis.X); // 控制摄像头水平旋转
AddControllerPitchInput(LookAxis.Y); // 控制摄像头垂直旋转
}
}
这是一个Pawn类中的对象,我知道controller是可以控制Pawn的,但是为什么这里也可以反向检查一下Controller是否存在?同时,我并没有进行绑定,只有在Gamemode里面,配置了Pawn和Controller,二者是怎么就关联起来了呢?
检查后吧,发现二者创建了一个关系,是一个相互引用的关系,即双向绑定:
- Pawn持有Controller指针:通过
Controller
成员变量,Pawn可以知道谁在控制它 - Controller持有Pawn指针:通过
PossessedPawn
或Pawn
成员,Controller知道它控制哪个Pawn
那绑定的是什么时候发生的呢?反正总之UE自动搞定了,GameMode配置好就行……
具体来说,玩家加入时候引擎自动创建PlayerController实例,然后生成Pawn时自动调用Possess()
或SpawnDefaultPawnFor()
完成一个双向绑定
// 引擎内部大致执行以下流程:
APlayerController* PC = SpawnPlayerController();
APawn* Pawn = SpawnDefaultPawn();
PC->Possess(Pawn); // 这里建立双向引用
开发类方式
目前阶段,我总觉得纯依靠Cpp不太可行。同时,蓝图和Cpp都还是有不可替代的优势的。因此还是采用二者结合的方式,比如说我创建C++类继承ACharacter,然后创建蓝图类继承该C++类。官方也是比较推荐这一种开发方式。
虽然使用了蓝图,但是属于“蓝图与C++协同开发模式”,C++去完成基础架构功能,核心逻辑函数网络之类的,然后蓝图只负责一些简单的可视化编辑和参数快速调整。
不过,随之而来的,是一堆问题。典型的来说,你在Cpp中比如说修改设置了一个变量,结果回到游戏发现没有任何更改,最后检查了很久,才发现蓝图中没有更改,而由于你蓝图是继承的关系,那肯定还是以蓝图中的值为准…….
这种对于自己写的变量还好说,不要用EditAnywhere解决,但是有些你使用的默认参数,就没办法解决。
解决方案是创建配置数据资产类,比如说我们控制相机杆子,就创建一个UCameraSettings 类,里面写我们需要控制的相机参数;接着我们在角色类中,使用我们这个配置资产
// 配置资产引用
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Camera")
UCameraSettings* CameraSettings;
然后,创建并配置数据资产:内容浏览器 → 右键 → 蓝图/资源 → CameraSettings,命名为DA_DefaultCameraSettings
,然后配置参数,在Player蓝图里面,面板的CameraSettings设置为DA_DefaultCameraSettings
宏
UE里面的C++包含了一堆堆宏
UPROPERTY
举例来说,对于下面这个常见的
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Camera", meta = (AllowPrivateAccess = "true"))
EditAnywhere
:表示什么时候编辑器可以编辑这个属性- EditAnywhere,对应的所有
- EditDefaultsOnly(只有默认面板,即对于一个类的默认值可以修改,那么实例出来的所有对象都是一样的),EditInstanceOnly(只有实例面板,意思是只能够修改实力出来的这个单个对象,不会影响到全局),VisibleAnywhere(仅可查看)
BlueprintReadOnly
:表示蓝图控制访问层级- BlueprintReadOnly:可读不可写,用于计算值or派生属性
- BlueprintReadWrite:可读可写,用于配置参数
- (无蓝图标志):均不可
- Category:给变量分组,提供高可读性
Category = "Camera|Zoom"
,表示在Camera目录下的Zoom目录下- 便于快速过滤查找,继承分组之类
meta = (AllowPrivateAccess = "true")
:允许蓝图访问标记为private成员变量- 保持C++封装性同时开放特定权限
基础操作
日志输出
需要用到 UE_LOG 宏,按照特定的方式去传递,下面是一个简单的例子
UE_LOG(LogTemp, Warning, TEXT("Hello World"));
- 第一个参数代表的是 LogCategory(日志分类),用来对日志进行分组筛选
- LogTemp一般默认都是这个,用作临时日志。当然,还有LogInit,LogInit,LogGame,LogNet等
- 第二个参数代表 LogLevel (日志级别),点那个过滤器就可以看到区别
- 一般自己输出Warning够了,当然还有Log,Display,Error这些
- 第三个参数代表 FormatString (格式化字符串),必须用TEXT()宏包裹,但是也支持printf格式说明符解释,后面的就是解释占位符的内容了。
- %s 字符串,%d 整数 ,%f 浮点数,%v向量,%c字符
UE_LOG(LogTemp, Warning, TEXT("状态: %s"), bIsActive ? TEXT("true") : TEXT("false"));
新建类
工具->新建C++类
删除特定类
UE里面删除一个类非常非常麻烦,原因大概可以认为C++编译后UE优化后,形成了一个复杂的依赖网关系,UHT,编译系统,资源系统,配置文件。这些方面的连接都需要删除。
那UE你为什么不做自动删除呢?一个是基于安全考量,UE保守策略宁不删除也不误删(所以就把问题推给程序员?)二是UE设计哲学就这么考量的…..
操作步骤:
- 关闭UE编辑器和VS;
- 这俩东西会在内存中锁定源文件(特别是.pdb和dll),那么就会导致删除失败or残留
- 在项目文件夹中找到要删除的C++类的.h头文件和.cpp文件,直接删除它们
- 删除项目文件夹
Binaries/
和Intermediate/
Binaries/
存储编译后的机器码,包含已删除类成员函数地址表,肯定要删Intermediate/
包含Unreal Build工具生成的文件,有一些缓存配置所以可删
- 然后退到外面,找到开始那个.uproject后缀文件,右键选择
Generate Visual Stduio project files
重新生成一批VS文件
特定功能实现
自拍杆式摄像机
需要用到两个组件(component)
- USpringArmComponent,用来实现摄像机的平滑跟随和碰撞处理。
- 原理是通过虚拟的弹簧臂连接摄像机到一个父物体,自动处理摄像机与环境的碰撞和调整。
- UCameraComponent,用来控制UE中相机视角的组件
在角色的构造函数中去绑定
.h文件定义,类去最前面定义
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
TObjectPtr<USpringArmComponent> CameraBoom;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
TObjectPtr<UCameraComponent> PlayerCamera;
.cpp文件,实现类的构造函数
#include "Player/PlayCharacter.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/SpringArmComponent.h"
// Sets default values
APlayCharacter::APlayCharacter()
{
// 确认是否开启每一帧调用的
PrimaryActorTick.bCanEverTick = true;
CameraBoom = CreateDefaultSubobject<USpringArmComponent>("CameraBoom");
// 相当于设置父组件
CameraBoom->SetupAttachment(RootComponent);
CameraBoom->TargetArmLength = 300.0f;
CameraBoom->SetRelativeRotation(FRotator(-40.0f, 0.0f, 0.0f));
// 相机附着到杆子上
PlayerCamera = CreateDefaultSubobject<UCameraComponent>("PlayerCamera");
PlayerCamera->SetupAttachment(CameraBoom);
// 使用LOG,会输出在日志中
UE_LOG(LogTemp, Warning, TEXT("YES"));
}
控制输入
传统的通过 Input Mapping Context 和 Input Action实现,前者定义了触发规则,后者定义了对应的角色行为
不过现在已经优化了,用了更好的 EnhancedInputComponent,优点是灵活绑定好,输入流程有更好的改进之类的(比如控制触发优先级)
下面是实现一个第三人称跑动的效果
.h文件中,相机和相机杆自然是必要的。当然还不止于此,还需要创建下面这三个,创建了三个指针对象
// UPROPERTY 属性说明符
// Camera
// 相机杆
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
TObjectPtr<USpringArmComponent> CameraBoom;
// 相机
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
TObjectPtr<UCameraComponent> PlayerCamera;
// Input
// 用于存储映射上下文
UPROPERTY(EditAnywhere,Category="Input")
TObjectPtr<UInputMappingContext> DefaultMapping;
// 用于存储移动输入动作
UPROPERTY(EditAnywhere,Category="Input")
TObjectPtr<UInputAction> MoveAction;
// 存储移动输出动作
UPROPERTY(EditAnywhere,Category="Input")
TObjectPtr<UInputAction> LookAction;
这个InputMap,是用作输入和输出的映射使用,剩下两个Action,是在InputMap触发后,产生对应的动作
具体实现见下文,代码注释已详细给出
详细实现
// PlayerCharacter.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "PlayerCharacter.generated.h"
class USpringArmComponent;
class UCameraComponent;
class UInputMappingContext;
class UInputAction;
struct FInputActionValue;
UCLASS()
class MYCPP_API APlayerCharacter : public ACharacter
{
GENERATED_BODY()
public:
// Sets default values for this character's properties
APlayerCharacter();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
void Move(const FInputActionValue& Value);
void Look(const FInputActionValue& Value);
private:
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
TObjectPtr<USpringArmComponent> CameraBoom;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
TObjectPtr<UCameraComponent> PlayerCamera;
// Input
// 声明一个 UInputMappingContext 类型的指针,用于存储输入映射上下文。
UPROPERTY(EditAnywhere, Category = "Input")
TObjectPtr<UInputMappingContext> DefaultMapping;
// 声明一个 UInputAction 类型的指针,用于存储移动输入动作。
UPROPERTY(EditAnywhere, Category = "Input")
TObjectPtr<UInputAction> MoveAction;
// 声明一个 UInputAction 类型的指针,用于存储视角输入动作。
UPROPERTY(EditAnywhere, Category = "Input")
TObjectPtr<UInputAction> LookAction;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
// Called to bind functionality to input
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
};
//.cpp
#include "Player/PlayCharacter.h"
#include "EnhancedInputSubsystems.h"
#include "Camera/CameraComponent.h"
#include <EnhancedInputComponent.h>
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/SpringArmComponent.h"
// Sets default values
APlayCharacter::APlayCharacter()
{
PrimaryActorTick.bCanEverTick = true;
// 摄像机弹簧臂设置
CameraBoom = CreateDefaultSubobject<USpringArmComponent>("CameraBoom");
CameraBoom->SetupAttachment(RootComponent);
CameraBoom->TargetArmLength = 300.0f;
CameraBoom->SetRelativeRotation(FRotator(-40.0f, 0.0f, 0.0f));
CameraBoom->bUsePawnControlRotation = true; // 相机杆可以旋转
// 摄像机设置
PlayerCamera = CreateDefaultSubobject<UCameraComponent>("PlayerCamera");
PlayerCamera->SetupAttachment(CameraBoom);
PlayerCamera->bUsePawnControlRotation = false;// 相机不可以旋转
// 旋转控制设置
// 不让角色跟着控制器旋转
bUseControllerRotationPitch = false;
bUseControllerRotationYaw = false;
bUseControllerRotationRoll = false;
// 移动设置
// 让角色根据真实的运动方向去旋转
GetCharacterMovement()->bOrientRotationToMovement = true;
GetCharacterMovement()->RotationRate = FRotator(0.f, 400.f, 0.f);
}
// Called when the game starts or when spawned
void APlayCharacter::BeginPlay()
{
Super::BeginPlay(); //父类的 BeginPlay 函数
// 获取本地玩家
// 检查 GEngine(全局游戏引擎实例指针) 和 GetWorld(获取当前游戏世界上下文) 是否有效
// 如果有效,第当前世界第一个玩家为本地玩家
// 如果无效,把 Player 设置为 nullper
if (const ULocalPlayer * Player = (GEngine && GetWorld()) ? GEngine->GetFirstGamePlayer(GetWorld()): nullptr)
{
// 获取 Enhanced Input 子系统
// 用来管理输入映射上下文
UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(Player);
// 如果 DefaultMapping 有效,将其添加到子系统中
if (DefaultMapping)
{
//将输入映射上下文添加到子系统中
Subsystem->AddMappingContext(DefaultMapping, 0);
}
}
}
// Called every frame
void APlayCharacter::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
// Called to bind functionality to input
void APlayCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
// 调用父类逻辑
Super::SetupPlayerInputComponent(PlayerInputComponent);
// 强制转换完增强组件
if (UEnhancedInputComponent* EnhancedInputComponent = CastChecked<UEnhancedInputComponent>(PlayerInputComponent))
{
// 绑定 LookAction 到 Look 函数
// 四个参数分别代表:输入动作资源,触发条件,接收对象,处理函数
// ETriggerEvent::Triggered表示持续时间内每一帧都触发
// 当然还有 Started,Completed,Canceld,对应开始,结束,中断时候触发
EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &APlayCharacter::Look);
// 绑定 MoveAction 到 Move 函数
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &APlayCharacter::Move);
}
}
// 自己写的两个有关函数
void APlayCharacter::Move(const FInputActionValue& Value)
{
FVector2D MoveVector = Value.Get<FVector2D>();
if (Controller)
{
// 获取控制器的 Yaw 旋转(摄像头方向)
// 控制器的朝向就是摄像头的方向,因为之前设置了CameraBoom->bUsePawnControlRotation = true;
// 因此此时弹簧臂跟随控制器旋转
const FRotator YawRotation(0, Controller->GetControlRotation().Yaw, 0);
// 获取角色自身的旋转(忽略俯仰和滚动)
// const FRotator YawRotation(0, GetActorRotation().Yaw, 0);
// 根据 Yaw 旋转计算前后和左右方向
// FRotationMatrix:将旋转转换为方向矩阵
// GetUnitAxis:获取标准化方向向量
const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
// 添加移动输入
AddMovementInput(ForwardDirection, MoveVector.Y);
AddMovementInput(RightDirection, MoveVector.X);
}
}
void APlayCharacter::Look(const FInputActionValue& Value)
{
FVector2D LookAxis = Value.Get<FVector2D>();
// 对于继承自Pawn类的,一定会有Controller
// 对于配置,应当在GameMode中配置过,默认绑定了,外部可见
if (Controller)
{
// 控制摄像头旋转
AddControllerYawInput(LookAxis.X); // 控制摄像头水平旋转
AddControllerPitchInput(LookAxis.Y); // 控制摄像头垂直旋转
}
}
选择出生点
先放置一个出生点,命名为PlayerStart。在场景内没有PlayerStart时候,会默认在摄像头位置;当场景中有,就会从这几个出生点中随机选择一个出来,我们需要选择一个
首先在PlayerStart,可以给它打一个tag
接着在GameMode里面设置,打开蓝图,重写选择玩家出生点函数,对于其中的Player改为寻找特定Player
动画示例
任务:
- 创建C++ Anim Instance
- 获取Character Movement、Player、Speed暴露给蓝图
开始还是先创建一个Cpp类,继承自UAnimInstance;然后,我们需要重写几个函数
#include "CoreMinimal.h"
#include "Animation/AnimInstance.h"
#include "Player/PlayCharacter.h"
#include "PlayerAnimInstance.generated.h"
UCLASS()
class CPP_STUDY_API UPlayerAnimInstance : public UAnimInstance
{
GENERATED_BODY()
public:
// 相当于 Start
virtual void NativeInitializeAnimation() override;
// 相当于 Update
virtual void NativeUpdateAnimation(float DeltaSeconds) override;
UPROPERTY(BlueprintReadOnly)
TObjectPtr<APlayCharacter> PlayerCharacter;
UPROPERTY(BlueprintReadOnly)
TObjectPtr<UCharacterMovementComponent> PlayerCharacterMovement;
UPROPERTY(BlueprintReadOnly)
float Speed;
};
我们需要两个指针,一个PlayerCharacter,一个PlayerCharacterMovement,同时,我们动作判断条件很有可能用到Speed,因此都添加进来
对于 NativeInitializeAnimation 函数和 NativeUpdateAnimation 函数,有点类似Start和Update函数感觉
void UPlayerAnimInstance::NativeInitializeAnimation()
{
// 父类的先用上
Super::NativeInitializeAnimation();
// 获取当前动画实例所属的角色对象,Cast把APown强制转化为APlayCharacter
PlayerCharacter = Cast<APlayCharacter>(TryGetPawnOwner());
if (PlayerCharacter)
{
// 如果角色存在,获取角色的移动组件
PlayerCharacterMovement = PlayerCharacter->GetCharacterMovement();
}
}
void UPlayerAnimInstance::NativeUpdateAnimation(float DeltaSeconds)
{
Super::NativeUpdateAnimation(DeltaSeconds);
if (PlayerCharacterMovement) { // 检查移动组件是否有效
// 获取角色的运动分量,但只保留水平分量,求的水平方向的速度
Speed = UKismetMathLibrary::VSizeXY(PlayerCharacterMovement->Velocity);
}
}
在蓝图部分,我们创建一个动画蓝图,选择骨骼开始使用,记得继承我们刚才创建的类
接着,新建一个状态机连线,然后状态机创建两个状态,Idle和Run,速度为0就Idle,不为0就Run
记得绑定一下转换条件,当前状态的动作,转化的条件,完成后编译即可
倒计时部分
主要用到两个控件蓝图,在用户界面里面,一个代表屏幕一个代表倒计时的组件
倒计时部分我们完成核心逻辑,这里的数据都是存在蓝图里面的,虽然我感觉很不合适,但是只是对于这个任务而言还是比较合适的。
这是一个Widght下来的类,因此我们可以先做一个画板设计我们的组件,默认就会打开设计器
然后对于内部,我们可以创建一个ResTime的变量,自己创建一个DeTimer函数,这个函数的作用是每调用一个这个ResTime就会-1,下面这是一个计时器委托,我们把、这个函数和出发时间放上去,然后回去设计组件的那个文本块为我们的ResTime
小地图
最终效果展示:
实现先懒得写了,参考 【UE5】【蓝图】UMG-小地图制作分享_哔哩哔哩_bilibili
广播调用机制
【虚幻5基础课堂】3. 一个案例学会使用事件分发器_UE5_UnrealEngine_哔哩哔哩_bilibili
不过这是纯蓝图,如果是Cpp可以参考一下内容,但是注意需要相关网络内容开启后才可以使用
.h
// 声明动态多播委托(BlueprintAssignable 必须用动态委托)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnStateChangedSignature, int, NewState);
UCLASS()
class UE_PROJ01_API AMyGameStateBase : public AGameStateBase
{
GENERATED_BODY()
AMyGameStateBase();
public:
// 属性同步时的回调函数
UFUNCTION()
void OnRep_State();
// 事件分发器(供蓝图绑定)
UPROPERTY(BlueprintAssignable, Category = "Event")
FOnStateChangedSignature OnStateChanged;
UPROPERTY(ReplicatedUsing = OnRep_State, EditAnywhere, BlueprintReadWrite, Category = UserState, meta = (AllowPrivateAccess = "true"))
int State; // 0.表示在出生岛 1.表示在飞机上 2.表示跳伞 3.表示开伞后状态 4.表示又落地
}
.cpp
void AMyGameStateBase::OnRep_State()
{
OnStateChanged.Broadcast(State);
}
问题汇总
编译时出错
UE5 Rider C++环境使用,编译无法通过的解决方法 – 哔哩哔哩
我们每次对于C++内的文件需要编译一下,build即可。因此需要在IDE内build,或者引擎内右下角点一下