虚幻引擎5 Gameplay框架(四)

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"
cpp 复制代码
// 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进行数据修改,因为直接获取的PlayerStatePawn类里面的,我们得等PlayerState有值的时候才能再进行修改,否则就为空
  • 我们追溯到PlayerState被赋值的位置
  • 查看堆栈,发现这个PlayerState是从PossessedBy过来的
  • 所以,我们进行数据修改时得在PossessedBy进行修改,在PlayerState赋值之后才能进行修改数据
  • 将修改行数据逻辑放到PossessedBy函数中进行
  • 行数据的名字添加到我们C++写的那个FName变量为FindRow函数占位的变量里面
  • 运行结果,血量就写入了

程序运行崩溃调试

  • 我们运行服务器批处理脚本会发现有报错,说我们没有初始化角色的行数据
  • 我们将其初始化,就没问题了
  • 第二个错误报错,开启服务器与客户端批处理脚本,服务器会挂掉,客户端也出现错误
  • 查看项目中的日志,查看是什么问题
  • 查看错误是一个未知的指针,查看堆栈发现逻辑出现在我们的PossessedBy函数
  • 我们可以附加批处理脚本到vs里面进行调试,因为批处理脚本就是在编译器里进行独立游戏模式运行,先开启服务器批处理脚本,然后添加到vs调试里面

  • 附加之后,开启客户端批处理脚本,此时就会报错,查看堆栈是从PossessedBy进入的,去查看一下里面里面是否可能的错误
  • 在这个函数里面,只有PlayerInitData最可疑,GI如果是空指针都不会进入在这里面,PlayerDataRow数据也没问题
  • PlayerInitData放到监视窗口里面发现已经为空了,已经被清空了
  • 这是为什么,这是个U类在虚幻引擎机制里面,是会被垃圾回收机制给回收掉的,我们客户端连服务器的时候会连这个表,但是此时已经被回收了,所以空指针问题就出现在这,只要客户端连接服务器的时候就会崩溃的原因出现在这
  • 解决方法:添加一个UPROPERTY()宏,它就不会被回收掉了
  • 运行结果

各类的执行顺序

  • 结论图:

  • 验证:

    • 我们制作一个功能,来验证这个结论。之前起客户端人名是通过读取命令行文本进行读取存到GameInstance上然后在UI类的构造函数上进行设置的这个是在本地设置的。只有本地客户端知道,但是服务器是不知道的。
    • 功能:让服务器去设置客户端的名字
    • 思路:当服务器连接客户端的时候为会客户端新建一个PlayerController,然后客户端的PlayerStateHUD就会跟着改变
  • 先注释掉之前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函数里面读的,所以读取存档也在这里读取
cpp 复制代码
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=EditorEngine里面的话就config=engine
  • 存储路径要加上Config宏,表明这个属性源自ini文件的
  • 然后打开DefaultGame.ini文件进行格式填写
  • 然后我们重新写一下获取数据表格逻辑,进行测试

  • 运行结果
  • 修改一下DataTable表数据看看是否改变

相关推荐
咕噜企业签名分发-淼淼38 分钟前
新手入门必备:游戏引擎推荐指南
游戏引擎
javaer炒粉44 分钟前
鸿蒙笔记导航栏,路由,还有axios
笔记
子龙烜1 小时前
Pandas 学习笔记(四)--CSV文件
笔记·学习·数据分析·pandas·csv·python数据可视化
Java追光着2 小时前
谷粒商城学习笔记-逆向工程错误记录
笔记·学习·谷粒商城
to be a question2 小时前
【物联网工程导论期末复习完整知识点】第四章物联网智能硬件与嵌入式
笔记·物联网·智能硬件·期末复习
灵韵设计3 小时前
学习笔记——动态路由——IS-IS中间系统到中间系统(区域划分)
笔记·学习·is-is区域划分·is-is和ospf区域区别
小天呐4 小时前
eggjs笔记
笔记·node.js·eggjs
大舍传媒4 小时前
欧美海外媒体发稿,国外新闻发布,外媒发布
大数据·人工智能·游戏引擎·信息与通信·用户运营
飞翔的佩奇4 小时前
Java项目:基于SSM框架实现的游戏攻略网站系统分前后台【ssm+B/S架构+源码+数据库+毕业论文+任务书】
java·数据库·spring·游戏·架构·maven·ssm框架
guanpinkeji5 小时前
剧本杀小程序:助力商家发展,提高游戏体验
游戏·小程序·游戏开发·小程序开发·剧本杀·剧本杀小程序