Gameplay重要类及重要功能使用方法(三)
虚幻的委托机制
- 虚幻委托之间的区别
- 序列化就是是否可以在蓝图中执行
多播与单播的创建
- 制作功能:使用多播与单播将血条与血量进行实时更新
- 首先新建一个单播与一个多播委托
- 实例化这两个委托的标签
- 然后将角色属性类中的血量与最大血量封装一下
- 封装之后就得更改之前角色类里面引用到血量的逻辑
- 给MaxHP也添加一个属性通知
- 进行单播与多播的创建
单播与多播的绑定
补全之前的封装性
- 首先为了体现封装性,将之前的UI封装为保护域,提供共有接口
- 之前引用到UI的变量需要变化
开始进行单播与多播绑定
-
之前写的默认参数给注释掉
-
新建两个存储变量来存储血量与最大血量
-
重写一下角色信息UI类里面的SetPlayerHPBar函数,我们不需要传递参数Percent进行设置百分比,让函数自身去实现逻辑,不采用传参方式
-
在设置血量的函数中更新存储的血量变量
-
在角色信息UI类里面添加一个初始化角色数据的函数来绑定单播与多播,获取角色属性中的属性数据,然后更新血条
-
重写角色属性类中的BeginPlay
-
进行调用初始化角色数据函数
-
运行结果
PlayerState在各端的执行顺序
-
在开两个客户端的情况下
- BeginPlay 1 生成了6次
- 第一次是在Server中客户端1中,客户端1的控制器中生成
- 第二次就是在客户端1中,客户端1中的控制器中生成
- 第三次是在Server中客户端2中,客户端2的控制器中生成
- 第四次在客户端1中,客户端2控制器中生成
- 第五次在客户端2中,客户端2控制器中生成
- 第六次在客户端2中,客户端1控制器中生成
-
验证
-
验证结果,这也验证了虚幻引擎中服务器代码与客户端代码是一起的
-
也验证了这个PlayerState既在服务器上又在所有客户端上
数据表格DataTable在CPP中的使用方法
在蓝图中如何使用DataTable
- DataTable需要创建结构体数据使用
C++中使用DataTable
- 需要创建一个能被引擎识别的Struct
- 创建一个空类然后在里面创建Struct
- 创建结构体与枚举
- 使用
FTableRowBase
要加头文件,#include "Engine/DataTable.h"
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "Engine/DataTable.h"
#include "CharacterTemplate.generated.h"
/**
*
*/
UCLASS()
class GAMEPLAYCODEPARSING_API UCharacterTemplate : public UObject
{
GENERATED_BODY()
};
//第一种创建枚举的方式
UENUM(BlueprintType)
enum class ECharacterColor
{
WHITE,
YELLOW,
BLACK
};
//第二种创建枚举的方式
namespace ECharacter
{
enum ColorType
{
RED,
GREEN,
BLUE
};
}
USTRUCT(BlueprintType)
struct FGPPlayerInitData : public FTableRowBase
{
//宏反射到蓝图
GENERATED_USTRUCT_BODY()
UPROPERTY(EditAnywhere,BlueprintReadWrite)
int32 ID;
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (ClampMin = 10 , ClampMax = 500))
float MaxHP;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
ECharacterColor CharacterColor;
//第二种创建枚举的方法声明变量就有点特别
TEnumAsByte<ECharacter::ColorType> CharacterColorType;
//CharacterColor必须选择YELLOW才可以编辑CharacterName
UPROPERTY(EditAnywhere,BlueprintReadWrite,meta = (EditCondition = "CharacterColor == ECharacterColor::YELLOW"))
FName CharacterName;
};
- 运行结果
- 选择刚才类中创建的
FGPPlayerInitData
结构体,因为添加了宏反射,所以虚幻引擎识别得到
在CPP中读取数据表格中的数据
- 首先得给行数据起个名字到时候在C++里面好获取
- 对于角色数据这种属性一般在
GameInstance
里面进行读取 - 使用
DataTable
需要加头文件,#include “Engine/DataTable.h”
- 新建一个
FName
变量为FindRow
函数占位
- 去角色类里面进行数据读取修改,在
Character
类中PlayerState
不能直接获取PlayerState
进行数据修改,因为直接获取的PlayerState
是Pawn
类里面的,我们得等PlayerState
有值的时候才能再进行修改,否则就为空
- 我们追溯到
PlayerState
被赋值的位置
- 查看堆栈,发现这个
PlayerState
是从PossessedBy
过来的
- 所以,我们进行数据修改时得在
PossessedBy
进行修改,在PlayerState
赋值之后才能进行修改数据
- 将修改行数据逻辑放到
PossessedBy
函数中进行
- 行数据的名字添加到我们C++写的那个
FName
变量为FindRow
函数占位的变量里面
- 运行结果,血量就写入了
程序运行崩溃调试
- 我们运行服务器批处理脚本会发现有报错,说我们没有初始化角色的行数据
- 我们将其初始化,就没问题了
- 第二个错误报错,开启服务器与客户端批处理脚本,服务器会挂掉,客户端也出现错误
- 查看项目中的日志,查看是什么问题
- 查看错误是一个未知的指针,查看堆栈发现逻辑出现在我们的
PossessedBy
函数
- 我们可以附加批处理脚本到
vs
里面进行调试,因为批处理脚本就是在编译器里进行独立游戏模式运行,先开启服务器批处理脚本,然后添加到vs
调试里面
- 附加之后,开启客户端批处理脚本,此时就会报错,查看堆栈是从
PossessedBy
进入的,去查看一下里面里面是否可能的错误
- 在这个函数里面,只有
PlayerInitData
最可疑,GI如果是空指针都不会进入在这里面,PlayerDataRow
数据也没问题 - 把
PlayerInitData
放到监视窗口里面发现已经为空了,已经被清空了
- 这是为什么,这是个
U
类在虚幻引擎机制里面,是会被垃圾回收机制给回收掉的,我们客户端连服务器的时候会连这个表,但是此时已经被回收了,所以空指针问题就出现在这,只要客户端连接服务器的时候就会崩溃的原因出现在这 - 解决方法:添加一个
UPROPERTY()
宏,它就不会被回收掉了
- 运行结果
各类的执行顺序
- 结论图:
- 验证:
- 我们制作一个功能,来验证这个结论。之前起客户端人名是通过读取命令行文本进行读取存到
GameInstance
上然后在UI
类的构造函数上进行设置的这个是在本地设置的。只有本地客户端知道,但是服务器是不知道的。 - 功能:让服务器去设置客户端的名字
- 思路:当服务器连接客户端的时候为会客户端新建一个
PlayerController
,然后客户端的PlayerState
与HUD
就会跟着改变
- 我们制作一个功能,来验证这个结论。之前起客户端人名是通过读取命令行文本进行读取存到
- 先注释掉之前
UI
类中设置客户端名字的逻辑
- 之前看
PlayerState
源码的时候,里面提供了获取PlayerName
接口,进行 了挖断,直接在初始化里面使用这个函数进行设置名字
- 然后我们批处理脚本选择了名字后,我们得把这个名字传给服务器,直接在
PlayerController
里面链接服务器的时候进行传递名字
- 在
GameMode
类里面的登录函数中将服务器获取的名字复制到客户端
-
ParseOption:从包含多个选项的字符串中提取特定的值
-
输入参数:
Options
(选项字符串):一个字符串,其中包含了以某种格式(通常为键值对形式)组织的多个选项。
Key(键):需要在 Options 字符串中查找的特定键。 -
返回值:
如果在 Options 字符串中找到了与 Key 相对应的值,则返回该值。 -
例如,如果 Options 字符串是 “
Graphics=High,Sound=Low,Difficulty=Medium
”,并且Key
是 “Sound
”,那么该函数将返回 “Low
”。
-
-
此函数标记有
BlueprintPure
,意味着它可以在蓝图中作为纯函数使用,不会改变任何状态。同时,meta=(BlueprintThreadSafe)
表示该函数是可以在线程安全的环境下从蓝图调用的,增强了使用的灵活性和安全性。
-
- 运行结果,名字已经复制成功
SaveGame_CPP中如何存储数据
保存数据
- 虚幻提供了一个存储游戏数据的类SaveGame类,创建这个类
- 在SaveGame类里面写上需要存储的数据,注意加宏反射
- 然后在角色属性类里面重写EndPlay函数,在EndPlay里面存储游戏数据
- 运行结果,使用批处理脚本打开服务器和一个客户端看看数据是否在本地保存了,服务器与客户端都保存了一份,这是不合理的,应该客户端不保存数据,就服务器保存,然后到时候服务器复制数据到客户端
- 改变一下逻辑,添加一个判断限制
- 删除项目中的SaveGames文件夹,重新启动服务器与客户端生成一下,这个就是服务器给我们生成的保存的数据
获取数据
- 读取数据,因为要用到角色属性类,读表也是在
PossessedBy
函数里面读的,所以读取存档也在这里读取
void AGamePlayCodeParsingCharacter::PossessedBy(AController* NewController)
{
Super::PossessedBy(NewController);
if (GetNetMode() == NM_DedicatedServer)
{
UGPProjectGameInstance* GI = GetGameInstance<UGPProjectGameInstance>();
AGPProjectPlayerState* PS = GetPlayerState<AGPProjectPlayerState>();
bool IsNeedDataTable = true;//设置一个bool值设计逻辑读了存档后就不需要读表数据了
if (GI)
{
//读存档
UMyDataSaveGame* SaveGame =
Cast<UMyDataSaveGame>(UGameplayStatics::LoadGameFromSlot(FString("MySaveGame_" + PS->GetPlayerName()),0));
if (SaveGame)
{
//设置存档里面的数据
PS->SetPlayerCurHP(SaveGame->SG_CurHP);
PS->SetPlayerMaxHP(SaveGame->SG_MaxHP);
}
IsNeedDataTable = false;
}
//读表数据
if (IsNeedDataTable)
{
if (GI->PlayerInitData)
{
//获取到行数据
FGPPlayerInitData* PlayerData = GI->PlayerInitData->FindRow<FGPPlayerInitData>(PlayerDataRow, TEXT(""));
if (ensure(PS))
{
PS->SetPlayerCurHP(PlayerData->MaxHP);
PS->SetPlayerMaxHP(PlayerData->MaxHP);
}
}
}
}
}
- 删除项目中的SaveGames文件夹,重新启动服务器与客户端生成一下,然后关闭客户端,重新打开刚才选择名字的客户端
在ini配置文件中为CPP中变量设置初始值
- 查看项目目录的
Config
文件夹,里面会有一些ini文件,这些是虚幻引擎提供的给代码里的一些属性进行初始值
- 实现一个功能,将表的引用路径存储到配置文件里,然后进行读取数据
- 如果你想写入到
Editor
配置文件里面就config=Editor
,Engine
里面的话就config=engine
- 存储路径要加上
Config
宏,表明这个属性源自ini
文件的
- 然后打开
DefaultGame.ini
文件进行格式填写
- 然后我们重新写一下获取数据表格逻辑,进行测试
- 运行结果
- 修改一下DataTable表数据看看是否改变