RPG游戏开发 您所在的位置:网站首页 unity开发流程图 RPG游戏开发

RPG游戏开发

2023-06-05 00:56| 来源: 网络整理| 查看: 265

系统说明

在一款复杂RPG游戏中,任务往往伴随着各种复杂的对话,这些对话会跟随着任务的推进给玩家展示不同的文本内容与分支选项。

对话系统需要给玩家展示的部分包括:

NPC文本内容

玩家选项以及选择对应选项会触发的事件

对话是和任务强相关的,对话可以做到发放以及接取任务,而任务可以做到修改对话内容以及分支结构。所以对话系统其实是任务系统的一个附属系统。在设计本系统的时候需要考虑到和任务系统的兼容性。

系统特性

本系统是一个后端架构,不需要过于关注前端的表现形式,只需要用高效的方式解决数据结构方面的内容即可。

本系统需要给框架使用者留下足够的拓展和定制化功能开发空间,在遇到需要用对话系统触发的需求,并且此需求在整个游戏流程中使用次数不多,不值得写入对话系统本体的时候,需要留有接口让框架使用者可以定制化开发。

本系统主要运行在客户端,不太需要过于关注反作弊的内容,如果有作弊者操作客户端上对话达到给玩家发放稀有任务。这些反作弊操作应该交给任务系统进行管理。

系统框架设计 数据结构设计 简单需求使用栈可以解决问题

我在开发过程中参考过Unity游戏开发:对话系统的实现_unity对话系统-CSDN博客中对话实现的方式,发现是用弹栈,压栈的方式操作对话。此方法可以一定程度上有较好的表现,维护起来也比较方便。

但是这个方法存在一些问题:

如果单纯用栈来存储对话,当NPC身上存在多个任务相关的对话的时候一个栈无法解决此问题,只能使用多个栈来进行存储。

此方法无法解决NPC的对话,玩家存在多个选择,玩家的选择导向了不同的结果的需求。

对话的改部分相当不好维护,假设我想在对话中间插入一句话,则需要一个个弹栈,然后压栈。

综上所述,用栈来存储对话只能解决一些不复杂的系统需求,如果是复杂的环境,栈处理起来还是比较乏力的。

数组链表

抛弃栈存储之后,想到《底特律:变人》是以剧情对话为主的游戏,其中的剧情流程图其实就是给玩家打明牌。他们表现出来的结构就是一个横着的多叉树。

但我最终并没有选择使用多叉树来设计数据结构,而是选择使用数组中模拟链表的形式来解决问题。 多叉树和数组链表都可以处理对话回退的需求,但是说实话,做这个系统的时候我其实是存了锻炼自己数据结构的设计能力的心思在里面的,所以就没有选择相对好维护的多叉树来解决。

此数据结构分两层,外层是外部数组,内层是链表。

数据结构详解 外层数组

外层的数组决定了这个对话最长的长度。

用lua的Table模拟出来是以下结构

local DialogueArray= { [1]={...}, [2]={...}, [3]={...}, }

我们暂且不去考虑内层的链表的数据结构,只考虑外层的数组的功能和初始化方式。

对话的长度决定了数组的长度(就是NPC说了几句话)

可能会出现一个对话有多个分支选项,其中一个分支选项NPC说了10句话,另一个分支选项NPC只说了5句话。此时数组的长度是10

这个外层数组的最大作用是在出现回退对话选项的时候可以快速找到上一个选项有哪几个链表节点可供玩家选择

内层链表

数组元素内存储的是一个个链表的节点。

一个数组元素内可以同时存在多个链表节点。

数组下标为1的元素内存储的链表节点中存储着与链表节点2的元素中的链表节点之间的关系

第三点说起来有点绕口,但实际上很简单,就是常规的链表数据结构,在当前节点描述下一个节点的信息

用lua的Table模拟出来就是这样的

["节点ID"]= { Content="当前节点NPC的文本内容" Next="下一个节点的ID" Pre="上一个节点的ID" }

但是以上的数据结构还比较简陋,并不具备解决多分支选项的能力。

所以此时需要给这个数据结构加点料!

其实想要解决多分支选项问题很简单,上文中的数据结构中的Next以及Pre字段是一个string类型的变量。我们将其变为一个Table类型的变量即可解决这个问题。

["10003"]= { Content="当前节点NPC的文本内容" Next= { [1]="10001", [2]="10002", } Pre= { [1]="10004", [2]="10005", } }

这样的链表节点就可以导向多个链表节点了,而将上面的数据结构抽象化则如下图所示。 在这里插入图片描述 当然,在正式的对话对象中不可以存在如上图的一个对话对象,因为很显然,对话是挂载在NPC身上的,而NPC身上的对话进入点只能有一个,这意味着不可能存在对话对象开头就是两个节点的情况。

对话系统与任务系统

上面解决了基本的对话数据结构,但是前文提到了对话系统是和任务系统强相关的,此时还缺少和任务强相关的逻辑。

我们可以在上面的数据结构中再单独嵌套一个表来解决这个问题。

创建一个顺序表,里面存储着这个对话节点对任务的操作

用顺序表的Index代表操作类型,Value表示要操作的任务

Index==1时 代表提交任务,Index==2时 代表发放任务,Index==3时代表发放任务奖励

此处为啥要1代表提交任务,2代表发放任务呢?因为在任务系统中存在一种任务,它的接取任务前提是完成某前置任务。假设在对话A中操作玩家提交任务1,并且发放任务2,但任务2的接取条件是完成任务1,如果先发放任务2,再提交任务1,则会被任务系统拦住。用顺序表存储,1一定比2先遍历到,所以1代表提交任务,2代表发放任务

只要在玩家遍历到此对话节点,激活一下任务字段里面的逻辑,即可做到对话系统与任务系统的强关联性。

以下是lua添加了此表之后链表节点的数据结构

["10003"]= { Content="当前节点NPC的文本内容", NexContent= { [1]="这是选项1的文本内容", [2]="这是选项2的文本内容" }, NextNode= { [1]="10001", [2]="10002", }, PreNode= { [1]="10004", [2]="10005", }, QuestOption= { [1]= --玩家选择选项1时候触发的任务操作 { [1]="任务1", --提交任务1 [2]="任务2" --发放任务2 }, [2]= --玩家选择选项2时候触发的任务操作 { [1]="任务1", --提交任务1 [2]="任务2" --发放任务2 } }, }

注意:任务操作和玩家选项是以Index一一对应的

对话系统的定制化需求

在实际开发中,对话往往成为游戏中某个事件的触发条件,比如在游戏中,NPC与玩家说“来看看我的商店吧!”玩家如果选择“OK”,则会打开NPC商店的UI界面。

此类需求无比繁复,而且不是经常使用,此时我们就需要在对话系统中公开一个区域,让框架使用者可以在里面填写一些信息,达成一些定制化需求的效果。

创建一个顺序表,用于存储特殊操作函数名

当玩家遍历到此链表节点的时候激活这些函数名代表的函数

["10003"]= { Content="当前节点NPC的文本内容", NexContent= { [1]="这是选项1的文本内容", [2]="这是选项2的文本内容" }, NextNode= { [1]="10001", [2]="10002", }, PreNode= { [1]="10004", [2]="10005", }, QuestOption= { [1]= --玩家选择选项1时候触发的任务操作 { [1]="任务1", --提交任务1 [2]="任务2" --发放任务2 }, [2]= --玩家选择选项2时候触发的任务操作 { [1]="任务1", --提交任务1 [2]="任务2" --发放任务2 }, }, S_SpecialOperations= --服务端特殊操作 { [1]= --玩家选择选项1时候触发的服务端操作 { [1]="服务端特殊函数名_1", [2]="服务端特殊函数名_2" }, [2]= --玩家选择选项2时候触发的服务端操作 { [1]="服务端特殊函数名_1", [2]="服务端特殊函数名_2" } }, C_SpecialOperations= { [1]= --玩家选择选项1时候触发的客户端操作 { [1]="客户端特殊函数名_1", [2]="客户端特殊函数名_2" }, [2]= --玩家选择选项2时候触发的客户端操作 { [1]="客户端特殊函数名_1", [2]="客户端特殊函数名_2" } }, }

本质上其实与任务系统结合方式差不多,只是偷了一点懒,可以不用自己写具体功能,公开给框架使用者即可。

还是上文打开NPC商店的需求,策划在填表的时候只需要从前端程序员处获取打开NPC商店的函数名,填入该对话节点的配置信息里面,玩家遍历到此处,则会激活此函数。

增删查改存取

业务的本质是增删查改存取

对话系统的增删查改是完全由任务系统驱动的,当玩家接到任务A的时候,任务系统会控制某个NPC身上的对话进行增加该任务相关对话节点的操作,当任务A完成的时候,任务系统会控制某个NPC身上的对话进行删除该任务相关对话节点的操作。

其实增就很好说,就是常规链表的增加节点方式,只是需要注意在外面套的那一层数组,在增加链表节点的时候也需要更新数组的长度以及元素。

这个就很简单,玩家进行对话的过程本质上就是在遍历这个链表,然后根据玩家的选择进入不同的分支而已。

查的本身并不复杂,但是跟后文说的删有一定的关联性。

上文说过,玩家进行对话,本质上是在遍历对话链表,但如果玩家遍历对话链表到一半,就直接删掉了当前玩家遍历到的位置,此时就会出问题。

对话系统本身就像是在悬崖上伸出去一根独木桥,玩家遍历就是从悬崖走在独木桥上的过程,如果玩家站在独木桥上10米处,系统直接从独木桥5米处砍断,玩家自然就掉下去了。 在这里插入图片描述

所以在删这个对话链表的时候,一定要注意这一点,并且需要在删这个函数内进行容错,如果玩家在10米处,系统想删5米处,直接打回

改就是修改对话链表中的文本内容,这个好说,也没啥需要注意的,本身并不涉及到整个链表的结构变化。

存与取

这个就需要好好说说了。

问题所在(数据量)

NPC是跟着玩家走的,因为玩家的剧情是每个玩家独有的,所以NPC的对话其实是存在玩家数据内的,又因为是一个网游,一个玩家的数据不可以过大,如果我们把每个NPC身上的对话对象直接上传到玩家数据中,则有多少NPC就有多少个对话对象,时间一长,这个玩家的数据就会爆掉。

解决思路

其实我们可以不用存玩家的对话对象。

上文提到了每个NPC都有一个对话对象。

又知道对话对象的增删查改其实都是由任务驱动的。

所以我们只需要根据任务的完成情况,记录这个任务对一些对话的操作即可。

解决方案

玩家每个任务都会对NPC的对话进行操作,将操作存入一个队列中,每次操作即入队

当玩家任务A完成的时候,直接清除这个任务造成的对话操作队列。

玩家下线之后再上线,读取这些操作队列,将每个NPC身上的对话对象从头到尾操作一遍即可复原。

代价

每个任务只能操作本任务的对话,不可以影响其他任务对话的增删查改

每个NPC身上必须携带一个默认的对话对象,复原操作就是对这个对象进行增删查改

但不是真的上传的一个对话对象,而是一个对话头节点ID,每次NPC初始化的时候根据这个ID生成一个对话对象即可

系列任务必须有持续性,不可以断掉,比如提交任务A之后,任务A结束会立马发放任务B,否则会出现玩家下次上线的时候,任务操作队列中没有数据的情况

完整数据结构图示

在这里插入图片描述



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有