独立游戏《星尘异变》UE5 C++程序开发日志4——实现任务系统

         本游戏作为工厂游戏,任务系统的主要功能就是给玩家生产的目标和动力,也就是给玩家发布一个需要一定数量某星尘的订单,玩家提交需要的星尘后会获得奖励,游戏中实际的奖励机制略微有点复杂,这里直接简化为完成任务后就能获得随机的事件来给天体上buff,游戏内效果如下图:

       

目录

一、任务的数据结构

二、任务栏

1.任务栏数据结构

2.任务进度的更新

三、随机事件奖励

1.随机事件的结构

2.随机事件池的初始化

3.生成随机事件


一、任务的数据结构

        先来看一下任务的结构体是怎么定义的:

USTRUCT(BlueprintType)
struct FQuest
{
	GENERATED_BODY()
    //任务名称
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Task")
	FString QuestName{ "Empty" };
    //任务类型
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Task")
	ETaskTypes QuestType{ ETaskTypes::Empty };
    //任务奖励
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Task")
	int NovaValueReward{ 10 };
    //任务描述
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Task")
	FText QuestDescription;
    //需求星尘
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Task/Stardust")
	TArray<FStardustBasic>RequiredStardust;
    //缺少的星尘
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Task/Stardust")
	TArray<FStardustBasic>LackedStardust;
    //任务当前是否满足完成条件
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Task")
	bool QuestCanBeCompleted{ 0 };
    //子任务
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Task")
	TArray<FName>ChildTask;
	FQuest() = default;
};

上面的结构中存了任务的名称,同时也是用来查找任务的索引

任务类型枚举决定了该任务完成条件,这里我们只介绍上面提到过的星尘订单这一类任务。

任务奖励在游戏中被称为星爆值,可以理解为玩家累积一定量的星爆值后就会获得上面提到过的随机事件奖励。

任务描述为玩家打开任务后看到的详细描述。

需求的星尘中存储的结构体中包含需求星尘的ID和需求星尘的数量,同时记录了当前要完成这个任务所缺少的星尘。

当缺少的星尘为空时,任务能否完成的bool值被置为真。

每个任务都会有一个或多个子任务,也就是完成该任务后,玩家就会获得该任务的子任务,使任务系统呈现一个树形结构。

二、任务栏

1.任务栏数据结构

玩家可能同时面对多个订单,所以需要一个任务栏容器存储当前玩家的任务,同样有增删查改功能,比库存系统实现起来要简单,就不多赘述了,看一下这些功能怎么定义的:

//返回包含该星尘的任务的索引
UFUNCTION(BlueprintCallable, Category = "QuestBoard")
TArray<int> GetQuestIndexFromStardustId(const FName StardustId)
//添加任务
UFUNCTION(BlueprintCallable,Category="QuestBoard")
bool AddQuest(const FQuest ExpectedQuest);
//删除任务
UFUNCTION(BlueprintCallable, Category = "QuestBoard")
bool RemoveQuest(const int Index);
//通过任务名称查找任务
UFUNCTION(BlueprintCallable, Category = "QuestBoard")
FQuest QueryQuestByName(const FString Name)const;

2.任务进度的更新

        我们需要实现的是在玩家背包更新之后,更新所有相关任务的进度,在库存系统的蓝图中有这样一个事件:

该事件会在每个库存变更的函数后面调用,调用后首先会对我们在库存系统中定义的UI更新的动态多播委托进行广播,之后会计算任务进度,其中用到的主要函数就是下面这个计算并更新单个任务进度的函数:

bool UTaskComponent::CalculateLackStardust(UStarInventoryComponent* Inventory, const int Index)
{//计算任务进度,输入玩家背包库存对象,和要计算的任务的索引
	if (Index < 0 || Index >= QuestArray.Num())
	{//检查索引是否合法
		UE_LOG(LogTemp, Error, TEXT("CalculateLackStardust at %d failed,invalid index"), Index);
		return false;
	}
	if (!IsValid(Inventory)
	{//检查对象是否有效
		UE_LOG(LogTemp, Error, TEXT("CalculateLackStardust failed,invalid pointer:Inventory"));
		return false;
	}
	bool flag = true;//返回该任务是否完成
	for (int i = 0; i < QuestArray[Index]->TaskInformation.RequiredStardust.Num(); i++)
	{//计算每个需要的星尘缺少的数量
		int CurrentLack = std::max(0, QuestArray[Index]->TaskInformation.RequiredStardust[i].Quantity - Inventory->CheckStardust(QuestArray[Index]->TaskInformation.RequiredStardust[i].StardustId));//需要的减去库存中拥有的就是缺少的
		QuestArray[Index]->TaskInformation.LackedStardust[i].Quantity = CurrentLack;
		if (CurrentLack)
		{//只要有一种星尘少了就不能完成任务
			flag = false;
		}
	}
	QuestArray[Index]->TaskInformation.QuestCanBeCompleted = flag;//更新任务是否能完成的标记
	return flag;
}

三、随机事件奖励

1.随机事件的结构

USTRUCT(BlueprintType)
struct FNovaBurstEventInfo : public FTableRowBase
{
	GENERATED_BODY()
	// 事件名
	UPROPERTY(EditAnywhere,BlueprintReadWrite,Category="NovaBurstEventInfo")
	FString EventName{ "Empty" };
	// 事件类型
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "NovaBurstEventInfo")
	ENovaBurstEventType EventType{ ENovaBurstEventType::Empty };
	//事件效果
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "NovaBurstEventInfo")
	ENovaBurstEventEffect Effect{ ENovaBurstEventEffect::Other };
	// 事件等级
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "NovaBurstEventInfo")
	int EventLevel{ 0 };
	// 默认数值
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "NovaBurstEventInfo")
	double DefaultValue{ 1 };
	//是否添加到事件随机池里
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "NovaBurstEventInfo")
	bool Randomable{true};
	FNovaBurstEventInfo() = default;
};

该结构也继承自FTableRowBase,所以也可以在数据表格中编辑。

事件名作为查找该事件的索引。

事件有多种类型,这里我们只介绍给天体添加buff的事件

效果枚举是用来决定事件具体该调用哪一个执行函数的

事件的等级决定了它在哪一个事件池中,在随机生成事件时,每个生成事件的等级是固定的,比如我们在玩家第一次星爆时,只会生成3个1级事件

默认数值是该事件的默认数值,表示事件的修正数值,例如加快天体的生产速度的事件,就会使产时间乘以这个默认值

是否可随机表示该事件是否会被添加到随机事件池,因为有一些事件是玩家到达某个阶段固定触发的,这类事件不用随机,这篇日志也不会细讲这类事件

2.随机事件池的初始化

        和日志2中加载星尘数据类似,我们同样在游戏实例中加载并保存所有随机事件池中的事件信息:

void UAstromutateGameInstance::LoadPrimeEventDataTable()
{
	UDataTable* EventTablePointer = LoadObject<UDataTable>(nullptr, UTF8_TO_TCHAR("DataTable'/Game/Data/NovaBurstEventInformation.NovaBurstEventInformation'"));//加载数据表
	PrimeEventPool.Empty();
	if (EventTablePointer == nullptr)
	{//没找到表格
		UE_LOG(LogTemp, Error, TEXT("Can't find PrimeEventInfo"));
		return;
	}
	TArray<FName> RowNames {EventTablePointer->GetRowNames()};//将所有星尘的名字储存进数组里
	for (const auto& it : RowNames)
	{
		FString ContextString;
		FNovaBurstEventInfo* Row = EventTablePointer->FindRow<FNovaBurstEventInfo>(it, ContextString);//获取对应名字这一行的信息
		if(!Row->Randomable)
			continue;
		PrimeEventPool.Add(*Row);
	}
	UE_LOG(LogTemp, Warning, TEXT("PrimeEventMap Loaded %d event"), PrimeEventPool.Num());//输出加载了多少个修正数据
}

3.生成随机事件

这里只展示天体加成的随机事件 ,输入参数为期望获得的事件的等级,例如[1,1,2]表示随机生成两个1级事件和一个2级事件,同时每个随机事件还会搭配生成一个随机的天体

TArray<FEventWithRandomAster> APrime::GetRandomEvent(TArray<int> ExpectedLevels)
{
	srand(static_cast<unsigned int>(time(nullptr)));
	std::unordered_map<int,TArray<FNovaBurstEventInfo>> CurrentEventPool;//事件等级到事件的映射
	UAstromutateGameInstance::RandomShuffle(ExpectedLevels);//将期望数组洗牌
	for(const auto&It:Instance->PrimeEventPool)
	{//拷贝一份事件池
		CurrentEventPool.emplace(It.first,TArray<FNovaBurstEventInfo>());
		for(const auto&It2:*It.second)
		{
			CurrentEventPool[It.first].Add(It2);
		}
	}
	auto Result{TArray<FEventWithRandomAster>()};//要返回的获取的随机事件
	if (!Instance->IsValidLowLevel())
	{
		UE_LOG(LogTemp, Error, TEXT("GetRandomEvent failed,invalid pointer:Instance"));
		return Result;
	}
	if (ExpectedLevels.IsEmpty())
	{
		UE_LOG(LogTemp, Error, TEXT("get random event failed, no expected levels,or game ended"));
		return Result;
	}
	for(const auto&It:ExpectedLevels)
	{
		if(CurrentEventPool[It].IsEmpty())
		{
			UE_LOG(LogTemp,Error,TEXT("No valid event for level: %d"),It);
			continue;
		}
		int RandomIndex{rand()%CurrentEventPool[It].Num()};
		FName	RandomAster{"Empty"};
		if(CurrentEventPool[It][RandomIndex].EventType==ENovaBurstEventType::UpgradeAster)
			RandomAster=GetRandomUnlockedAster();//获得随机的目标天体
		Result.Add(FEventWithRandomAster(RandomAster,CurrentEventPool[It][RandomIndex]));
	}
	return Result;
}

上面用到的洗牌函数,可以确保数组中的每个数等概率的放到每个位置,因为是模板函数,所以对任意类型的数组都可使用:

template<typename T>
	static void RandomShuffle(TArray<T>& Array)
	{
		if(Array.IsEmpty())
			return;
		for(int i=0;i<Array.Num();i++)
		{
			int j{rand()%Array.Num()};
			std::swap(Array[i],Array[j]);
		}
	}

关于事件如何被执行,与游戏中的其他系统有关,没有太大的参考意义,这里就不过多赘述了

下一篇日志我将会介绍游戏中的物流系统是如何实现的

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/700937.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

洛谷 P4913 二叉树深度(递归)

题目描述 有一个 &#x1d45b;(&#x1d45b;≤10^6) 个结点的二叉树。给出每个结点的两个子结点编号&#xff08;均不超过 &#x1d45b;&#xff09;&#xff0c;建立一棵二叉树&#xff08;根节点的编号为 1&#xff09;&#xff0c;如果是叶子结点&#xff0c;则输入 0。…

VitePress做一个自己的知识博客

创建项目 // 1.创建项目,直接在空项目下安装vitepress(npm/yarn等都可以,这个可以看官网,官网给了好几种安装方式) yarn add -D vitepress // 2.初始化配置项目(npm/官网也给了多种包管理工具的安装方式) yarn vitepress init // 初始化命令执行完会遇到以下几个问题 ┌ Welc…

温泉镇旅游微信小程序的设计与实现(论文+源码)_kaic

摘要 旅游业随着经济的快速发展呈现出一派欣欣向荣的景象&#xff0c;尤其是近两年来&#xff0c;各个行业运用科技以及因特网来促进旅游迅速发展&#xff0c;逐渐都显示出了的问题&#xff0c;特别突出的是在线上推广&#xff0c;其缺点也是特别明显。尽管在新冠肺炎的冲击下&…

【C++】STL空间配置器

STL空间配置器 一、什么是空间配置器二、为什么需要空间配置器三、SGI-STL空间配置器实现原理1、 一级空间配置器2、二级空间配置器 四、优缺点分析 一、什么是空间配置器 STL 有六大组件分别是&#xff1a;容器&#xff0c;算法&#xff0c;迭代器&#xff0c; 空间配置器&am…

创建第一个Springboot项目HelloWorld

目录 一、准备工作 一、创建springboot项目 三、使用git上传到代码仓库gitee 四、git使用过程问题总结 一、准备工作 安装jdk&#xff1a;8u201&#xff08;可以使用高一点的版本&#xff09; jdk所有版本下载&#xff1a;Java Archive | Oracle 安装maven&#xff1a;不用…

“改进型”Howland 电流泵电路

“改进型”Howland 电流泵电路 “改进型”Howland 电流泵是一种使用差分放大器在分流电阻器 (Rs) 上施加电压的电路&#xff0c;从而产生能够驱动大范 围负载电阻的双极性&#xff08;拉电流或灌电流&#xff09;压控电流源。 设计注释 确保运算放大器的输入端&#xff08;V…

Vue19-key的原理

一、v-for中key的作用 给节点进行一个标识&#xff0c;类似于身份证号。 1-1、需求1&#xff1a; 点击按钮&#xff0c;在<li>的最前面添加一个老刘的信息 <body><div id"root"><h1>人员信息</h1><button click.once"add&qu…

深度学习-注意力机制和分数

深度学习-注意力机制 注意力机制定义与起源原理与特点分类应用领域实现方式优点注意力机制的变体总结注意力分数定义计算方式注意力分数的作用注意力分数的设计总结 注意力机制&#xff08;Attention Mechanism&#xff09;是一个源自对人类视觉研究的概念&#xff0c;现已广泛…

NEFU服务科学与SOA

一、现代服务业与SSME 现代服务业 传统服务业 新业务模式 新型IT技术 知识密集 IT服务&#xff1a;由专门的IT组织向企业用户所提供的业务过程与功能性服务&#xff0c;以支持企业用户业务的正常运转。 现代服务业的四大领域 &#xff1a; 基础服务 生产服务 生活服…

怎么使用手机远程访问电脑文件?(3种方法)

手机远程访问电脑文件 “有时&#xff0c;当我离开电脑时&#xff0c;仍然需要访问和使用桌面上的文件。是否有一种工具可以通过WiFi而不是USB连接&#xff0c;让我的手机远程访问电脑上的文件&#xff1f;如果有任何建议&#xff0c;我将非常感激&#xff01;” 除了希望手机…

高效换热管

绕管式高效换热器 绕管换热器是一种结构紧凑&#xff0c;传热效率高的新型高效换热器。换热管按螺旋线形状交替缠绕在芯筒与外筒之间&#xff0c;相邻两层螺旋状换热管旋向相反&#xff0c;并采用一定形状的定距元件使之保持一定间距。层与层间换热管反向缠绕&#xff0c;极大…

800W-2300W-4500W-7000W线绕电阻器的选型参考

EAK线绕电阻器将普通电阻器材料的高脉冲稳定性与优化的导热和高度保护相结合。安装在导热表面上可进一步改善散热并提高稳定性。 EAK提供各种外壳设计和材料&#xff08;如铝和钢&#xff09;的导线电阻器。它们符合 UL508 的要求&#xff0c;在用作制动、充电、放电或加热电阻…

笨蛋学算法之LeetCodeHot100_3_最长连续序列(Java)

package com.lsy.leetcodehot100;import java.util.Arrays; import java.util.HashSet; import java.util.Set;public class _Hot3_最长连续序列 {public int longestConsecutive(int[] nums) {//创建set去重//对重复的数字进行去重Set<Integer> set new HashSet<>…

什么是校园抄表系统?

1.校园抄表系统的简述 校园抄表系统是当代高校管理中的一个重要组成部分&#xff0c;主要运用于全自动搜集、管理方法与分析校园里的电力能源使用数据&#xff0c;如水电煤等。它通过先进的方式方法&#xff0c;完成了对能源消耗的实时监控系统&#xff0c;提升了电力能源管理…

redis设计与实现(四)服务器中的数据库

服务器中的数据库 Redis服务器将所有数据库都保存在服务器状态server.h结构的db数组中&#xff0c;db数组的每个项都是一个redis.h/redisDb结构&#xff0c;每个redisDb结构代表一个数据库。 在初始化服务器时&#xff0c;程序会根据服务器状态的dbnum属性来决定应该创建多少…

CSS从入门到精通——背景样式

目录 背景颜色 任务描述 相关知识 背景色 编程要求 背景图片 任务描述 相关知识 背景图片 设置背景图片 平铺背景图像 任务要求 背景定位与背景关联 任务描述 相关知识 背景定位 背景关联 简写背景 编程要求 背景颜色 任务描述 本关任务&#xff1a;在本关…

PHP框架详解- symfony框架

GPT-4 (OpenAI) Symfony 是一个用 PHP 语言编写的开放源代码的 web 应用框架。Symfony 提供了一组可重用的组件和一个标准化、可扩展的框架&#xff0c;用于构建 web 应用、API、微服务等。它跟其他流行 PHP 框架&#xff08;比如 Laravel&#xff09;一样&#xff0c;旨在加快…

MySQL查询ab字段相同取时间最大的一条数据

MySQL是一个开源的关系型数据库管理系统&#xff0c;被广泛用于各种Web应用程序和大型企业级数据库系统。在实际应用中&#xff0c;经常会遇到需要查询某个字段相同的多条数据中&#xff0c;取时间最大的一条数据的需求。本文将通过代码示例来详细介绍如何使用MySQL实现这一功能…

内网Docker镜像无法使用?Debian/Ubuntu离线安装Dokcer

离线安装Docker Centos7停止技术支持&#xff0c;Dockerhub国内镜像也用不了&#xff0c;该教程只解决debian/ubuntu如何离线安装docker 卸载冲突的包 for pkg in docker.io docker-doc docker-compose podman-docker containerd runc; do sudo apt-get remove $pkg; done先…

Kafka生产者消息发送流程原理及源码分析

Kafka是一个分布式流处理平台,它能够以极高的吞吐量处理数据。在Kafka中,生产者负责将消息发送到Kafka集群,而消费者则负责从Kafka集群中读取消息。本文将探讨Kafka生产者消息发送流程的细节,包括消息的序列化、分区分配、记录提交等关键步骤。 先看一个生产者发送消息的代…