《梦幻西游三维版》程序篇:新式技能制作解决方案

点滴

2022-06-232539次浏览

0评论

6收藏

1点赞

分享

本技术提供了一套技能制作(或行为制作)的完整解决方案。该方案使得制作者能以类似蓝图的方式,可视化的在本地完成技能制作,并直接运用在真实的网络游戏中。

该方案涉及技能概念剖析,分支流程,网络同步,自分裂等各种技能制作中需要用到的技术。目前已在梦幻的多个项目中使用。

技能制作一直都是一个难点, 其难点在于:

1.技能复杂,多变: 策划每想到一个新的技能,都可能需要程序hardcode许多内容,还涉及到各种增删表列的情况。

2.调适性差: 策划设计技能,然后由程序实现,效果不好,返工,再查看, 再返工,沟通成本大,而且也不一定能达到策划想要的效果。

3.调优较难: 美术效果等的调优,比如特效时间调整,打击点修改等,填表里不直观,程序实现的话,又产生了沟通成本。

抛开具体实现,现有的技能编辑器很多时候还有这么一个问题,每当策划新开一个脑洞,就需要hardcode一堆东西来完成,导致编辑器并没有那么实用了。

本技术提供的解决方案,从17年初完成,一直服务于梦幻西游三维版。到目前为止,其基本框架和基础服务几乎没有任何修改,接住了策划的各种脑洞攻击, 是一套非常成熟的解决方案。

1.技能流程剖析

在如何制作技能之前, 首先是对技能流程剖析, 在本解决方案中技能流程拆分如下:

逻辑与表现分离

表现: 动画, 特效, 音乐等

逻辑: 某个时间选目标, 某个时间对已选目标施加技能效果

从该方案中可看出:

①所有的表现都是挂件, 不应该影响逻辑本身。

②逻辑中并没有所谓的前摇后摇等固有概念。

③ 选目标和施加技能效果是两件独立的事情, 即不用成对存在, 也不用同时发生。

2.参考系

不管是特效播放还是目标选取, 都是在二维或者三维世界中找到一个点或者一个区域。在三维世界中描述一个区块位置, 需要三个参数, 参考系, 偏移, 区块形状。

①参考系: 参考系由坐标点和方向构成的, 最简单的是一个带方向的坐标点, 更进一步可以是一个目标, 即取该目标的当前位置和朝向作为参考系, 还可以是一个鸽子。 鸽子这个概念是该方案提出的, 表示该技能流程中已记录的点和方向。

②偏移: 即坐标偏移, 表示了区块形状在该参考系中的位置。

③区块形状:圆形(带半径参数), 矩形(带长宽参数), 球形(带半径参数), 点等。

3.运行框架

为解决技能现有的和可能出现的技术问题, 设计了行为流Actionline这一运行框架, 其主要功能如下:

①组成结构: 由多个时间轴和多个节点图构成, 编辑方式简单有效。

②同步功能: 该框架提供了Actionline的同步和内部节点的同步, 在制作技能的时候, 当成是制作单机游戏的技能即可。

③并行: Actionline分为主从行为流, 主行为流只能有一个, 但从行为流可以有无限个。 这意味着, 你可以做很多有趣的事。 比如, 一个BUFF放到怪物身上, 这个BUFF会在该怪物身上运行一个从行为流, 该行为流的作用是每秒攻击周围所有的怪物, 并且带有各式各样的特效播放。 而且该行为流不影响怪物自身的技能释放。 该功能也在处理客服延迟和延迟抖动上起到了十分重要的作用。

④多副本: 同一个Actionline在不同环境下运行的结果可能是不同的, 有些复杂的技能, 可能还会存在技能分裂的情况。 比如释放一个火球, 击中目标后散射成3朵中火球, 各自击中目标后, 再各自散射3多小火球。 根据目标的情况, 最终小火球可能有0个到9个, 各自击中的时间也不同。 这种技能就涉及到了Actionline的流程分裂了。 而这种分裂是Actionline的基础服务, 技能制作者只需要关注火球击中第一个目标, 散射后击中第二个目标, 再散射后击中第三个目标这一单一流程即可。

⑤分支: 技能过程中可能会根据不同情况选择不同的流程。 比如一个蓄力技能, 如果蓄力被打断就释放一个小伤害, 否则就是一个大伤害, 这是一个技能的不同流程。 再比如一个技能, 会在地上产生一些圈圈, 如果每个圈都有玩家站进去, 则释放一个小伤害, 否则是全场巨额伤害, 这也是一个技能的不同流程。

1.编辑器全貌

该解决方案提供了一个编辑器Polaris,Polaris由节点图和timeline组成, Actionline的运行也是基于节点图和Timeline的, 接下来会分别讲解。

2.节点图

节点图由多个节点及连节点间的连线组成, 这些节点和连线组成了一张有向无环图。每个节点中有对应的逻辑, 这些逻辑会在节点激活的时候运行。关于节点图, 有以下几个具体实现步骤。

(1)节点格式: 节点包含多个入口和多个出口, 从入口连入的节点是当前节点的父节点,从出口连出的节点是当前节点的子节点。

(2)节点状态: 节点包含三个状态, 分别为 等待, 激活和释放。

a) 等待:该状态表示节点还未激活, 即节点逻辑还未运行的时候。

b) 激活:该状态表示节点处于激活状态, 即节点逻辑正在运行的时候。

c) 释放:该状态表示节点已经运行完毕。

(3)运行方式: 节点图的运行顺序是拓扑序。 具体实现方式为:有一个运行队列, 队列中的节点会按队列顺序依次运行。 一个节点会在他的所有父节点成为释放状态的那一刻放入队列末端, 在初始化的时候, 所有没有父节点的节点会被放入队列。 同一个节点释放时引发的其子节点放入队列末端的顺序是不可控的。 例如, 初始化时没有父亲的那些节点, 放入队列的顺序是不可控的。

(4)阻塞与非阻塞: 阻塞节点指节点从激活到释放不是线性的, 即节点的激活后不会立即释放, 节点的释放可能是等待一个时间或者等待一个事件, 如吟唱节点, 节点会在吟唱时间结束后释放。 非阻塞节点指在节点逻辑运行后立即释放, 如上诉的播放动画节点, 该节点不会等待动画播放完毕释放, 而是播放逻辑发给动画系统后, 立即释放。

3.时间轴

时间轴就是以时间序运行的帧逻辑,一共有两种帧:

①图帧:这种帧运行的时候,会运行该帧所包含的节点图。

②循环图帧:该帧会循环运行,每次运行都会运行该帧所包含的节点图。

4.数据格式

行为流是多节点图和多时间轴的逻辑框架,行为流是由节点图和时间轴作为树节点的一棵树。节点图树节点和时间轴树节点以下简称为图节点和轴节点。

行为流树的根节点是图节点,任意一个图节点若包含子节点则这些子节点一定是轴节点,同理,任意一个轴节点若包含子节点则这些子节点一定是图节点。如图所示:

具体的跳转关系是通过节点和帧来实现的,节点图中的某些节点是含有时间轴的,在运行的时候就会激活该时间轴,帧中也是包含节点图的,在帧激活的时候就会激活对应的节点图来运行。

5.同步方式

同步方式分为Actionline的同步和Actionline内部节点的同步。

6.Actionline的同步

Actionline的同步就是各端都跑同一个Actionline, 比如主角放一个技能, 那么只需要要主客户端,服务器,从客户端都跑同一个Actionline即可。同步就是通知各端,你该跑这个Actionline了,并给于一定的参数(比如随机数种子,位置,目标等)。

如图所示,Actionline的同步有两种情况,主客户端发起和服务端发起,分别对应主角放技能,和怪物放技能。

7.Actionline内部节点同步

如图所示,是内部节点同步的一个用法。在某些情况下,技能本身可能存在分支,为了让各端走同样的分支,需要用到节点同步。

节点的同步有两种情况,需求方先运行到达该节点,结果方先运行到该节点,具体处理方式如下:

提供了两个堆,需求堆和结果堆,如果需求先到达,则会把需求放入需求堆,在结果到达的时候,会先去需求堆查,一旦匹配上,就将需求1回调了。同理, 结果先到达,放入结果堆,需求到达的时候,发现结果堆里面有直接需要的东西,就直接回调了。

需求和结果匹配的方式运行时的节点编号,任意一个运行中的Actionline中的任意一个节点都有唯一的编号,即使两个Actionline是相同的。

每个节点的编号为:

其中,alID是一个随机数,每个Actionline运行的时候都会有的,graph.salt是一些加盐操作,毕竟同一个图是可能被复制多份运行的(多副本)。

回到最开始的同步节点,该节点的功能是,需求方运行到该节点会阻塞住,当结果方也运行到该节点即放行。上述图结果方根据随机结果只会运行一个同步节点, 所以其他端的Actionline也只会运行一个分支了。

该同步节点的具体实现方式如下:

Actionline框架提供了非常简单的同步接口,等待方只需要注册即可,结果方通过RPC的方式激活对应函数,esID即为节点编号。

这些接口是可以传参数的, 在有些情况下, 比如服务端把计算结果传给客户端, 客户端再使用等。 在G68中, 我们优化了选敌节点, 其操作流程是主客户端先选敌, 发给服务端校验, 服务端校验后, 自己使用并发给从客户端。 如此复杂的同步在一个节点内即可简单实现了。 并且这些同步对于技能编辑者是透明的。

8.并行

Actionline分为主Actionline和从Actionline, 会根据情况相互切换。 同一时间, 只有一条主行为流, 而从行为流可以有无数条。

其设计目的在于, 一般情况下, 一个人在同一时刻只能干一件事(边煮鸡蛋边切菜也是一件事, 煮鸡蛋不是你在煮)。 

比如玩家丢出了一个法球, 击中目标后一定时间后爆炸。 在丢出的时候该行为流是主行为流, 某个时间解除限制后, 就变成从行为流了, 玩家可以放其他技能, 可能又丢了个法球, 这两个法球所在的行为流会不干扰, 并且会按各自的逻辑继续运行下去。

有些时候一些复杂的BUFF, 也可能直接在任意目标上播放Actionline, 这类Actionline一来就是从行为流, 不会影响到这些Entity本身的诸如技能释放之类的逻辑。

在客户端服务器通信方面, 这个并行也有很重要的角色。 由于网络波动, 主客户端释放下一个技能的时候, 传达到服务器的时候, 服务器可能还没放完上一个技能。 这时并不能拒绝这次技能请求, 也不能打断上一次的技能释放, 有可能结算什么的还没跑。 所以, 这里要做的就是把当前释放Actionline改为从Actionline, 然后接受新的技能释放请求, 跑新的Actionline即可。

即使同时运行这么多Actionline, 也不用担心内存管理的问题, Actionline自己是一套十分自洽的系统, 其结构如图:

外部几乎没有从Actionline的任何引用(除非节点内部自己绑定了, 那么节点自己负责), 当一个Actionline运行结束的时候, 所有对象的引用计数自动就变为0了, python自行就回收了。 换句话说, Actionline运行的时候, 所有东西都是局部变量。

这种结构很美, 就像分出一个影分身, 他会帮你完成一系列的事情, 完成后就自己消失的, 你完全不用去管他。

9.多副本

Actionline数据本身只是一份蓝图, 他只是知道Actionline实例的构建, 但是具体构建几份, 他是不知道的。

前面已经讲了, Actionline是一个树结构, 包含图节点和轴节点。 Actionline中的任意一颗子树的数据都可能创建出任意多个子树的实例。 例如:

一个大火球, 击中目标后分裂成最多三个中火球, 各自再次击中目标后, 各自分裂成最多三个小火球, 再攻击周围的目标。

在Actionline制作的时候, 只需要考虑第一个火球飞出, 击中后发射第二个火球, 再次击中后释放第三个火球。

多副本功能可以自动根据实际情况分裂Actionline, 这种复杂的逻辑对制作者本身也是透明的。

评论 0

0/1000
网易游学APP
为热爱赋能
扫描二维码下载APP