本文主要围绕《黑神话悟空》的开发框架与战斗系统解析展开
-
主要内容
-
《黑神话悟空》采用的技术栈
-
《黑神话悟空》战斗系统的实现方式
-
四种攻击模式
-
连招系统的创建
-
如何实现高扩展性的战斗系统
-
包括角色属性系统、技能配置文件和逻辑节点的抽象等关键技术点
版权声明
-
本文为"优梦创客"原创文章,您可以自由转载,但必须加入完整的版权声明
-
文章内容不得删减、修改、演绎
-
本文视频版本:见文末
议题
-
上图展示了我们本节将主要讨论的三个议题
-
第一个议题是"黑猴解包分析",那么为什么要解包呢?
- 为了分析黑猴的技术栈
-
仅仅看游戏是无法分析出黑猴的技术栈的
-
我们试图通过分析《黑神话悟空》的技术栈,来进一步窥探开发3A游戏所需要的技术栈
-
第二个议题是"整体技术栈",此议题中,我们主要尝试分析解包后获得的整体技术栈的信息
-
第三个议题则详细解析了黑猴战斗系统的部分实
黑猴解包
声明
- 解包游戏是非法行为,本专题只会告诉大家一些结论,中间的解剖过程需要大家自行参悟
我要防什么?不能脱离平台运行
- 作为单机游戏,《黑神话悟空》反盗版的对抗核心思想就是不能脱离平台运行。对于黑猴来说,这个平台就是WeGame和Steam
静态反破解 和 动态反破解
- 游戏的反破解分为静态和动态两个层面
静态反破解
-
首先是静态反破解
-
从解包出的代码来看,悟空的主程序高达800多兆,文件膨胀了至少上百倍;程序中有高达151万个函数
-
这些都是代码膨胀的特征
代码膨胀,提高逆向难度
- 代码膨胀,就是给原有代码添加无关的代码
- 代码膨胀的目标,是提高逆向破解的难度
-
黑猴是C++编写的,为了大家能理解我,此处用一个简单的Python程序来讲解
-
上图中,该示例函数实现了两个数相加的简单逻辑
-
下面,给大家展示一下将这个简单逻辑膨胀之后的代码:
-
该代码在原本简单的两数相加逻辑上,增加了以下三类冗余代码
-
一个无用的随机数
-
一个无用的Range检查
-
一个无用的if条件判断
-
这个if条件判断的逻辑永远不会触发
-
相对于原先的代码,新的代码量足足膨胀了4倍
-
冗余代码的增加,使得阅读代码时,需要更多的时间去理解
-
并且,当逆向反编译后,是无法看到具体代码的,只能看到一堆汇编代码,这使得破解难度增加了更多
- 上图展示了逆向出来的代码的样式
- 除了代码膨胀之外, 代码混淆,代码等价交换等手段,也常被用来提高逆向工程的难度,它们都是游戏防外挂的一些常见的手段
插入CPUID
-
除此之外,《黑神话·悟空》还有非常多的地方插入了CPUID。这也是一个反破解的关键手段
-
经过统计,整个程序中,至少有3000多次CPUID插入
-
CPUID指令可以获取CPU的详细信息
-
通过获取计算机硬件唯一标识符,可以有效防止游戏程序被非法拷贝
-
这在反盗版的领域,特别是软件本地授权非常重要
-
通过CPUID,还可以判断当前环境是否是虚拟机
-
毕竟很多逆向工程都是要在虚拟机下进行的
-
基于以上两点,要破解《黑神话·悟空》,难度可能真要堪比登天了
动态反破解
-
动态反破解是游戏在运行过程中实施的反破解手段
-
主要手段是使用哈希检查来对文件修改进行监测
哈希(Hash)
-
哈希可以理解为数字指纹
-
数字指纹可以代表内容的唯一性,哪怕内容只改变一个字节,其数字指纹也将发生改变
-
通过"哈希",动态反破解可以针对以下三个地方进行检查
-
一、系统核心的dll检查
-
系统核心dll主要包括ntdll,kernel32,kernelbase
-
它们是是WINDOWS的内核函数
-
要在Steam上面去反破解,主要就是针对这些内核dll进行操作
-
系统核心dll的检查,主要目的是防止注入破解代码
-
二、进程环境块/进程数据块检查
-
进程环境块:Process Environment Block
-
每一个游戏进程在我们的操作系统里面都有一个进程环境块
-
PEB的哈希是WINDOWS操作系统中的一个进程级的数据结构,包含了与进程相关的多个信息,比如加载的模块列表,命令行参数等等
-
常规的情况下,对进程环境块进行反破解,主要通过以下两种方式
-
修改进程环境块
-
将进程环境块从内存中抹掉
-
PEB检查,主要用于判断是否存在反调试,或者进程环境被修改的情况
-
例如:
-
通过检查PEB的结构中的标志位PEB.BeingDebugged,就可以判断进程是否正在被调试,从而判断判断是否存在反破解行为
-
当然,这个方案并不一定有效,部分外挂工具具备跳过PEB.BeingDebugged的功能,这将导致PEB检查失效
-
三、共享的数据结构检查
-
共享的数据结构(KUSER_SHARED_DATA)是WINDOWS内核中的一个共享数据结构,所有用户模式进程都可以从其中读取信息
-
该结构包含了一些全局变量,如系统时间,时区信息,版本信息等
-
通过检查这部分的数据变化,可以判断游戏是否被调试
-
以上三种检查的目的都是反调试
-
反调试技术通过增加软件运行时逆向工程的难度,以阻止调试器的正常工作,从而阻止反破解
黑猴的反破解技术是否影响游戏性能
-
有的朋友会好奇,反破解技术,又是增加冗余代码,又是插入CPUID,又是检查哈希的,这么多操作,难道不会影响游戏性能吗?
-
答案是肯定的,肯定会影响
-
它主要影响CPU运行,因此,电脑CPU性能越差,受到的影响越大,直观感受越明显
-
如果主要的性能瓶颈是显卡,那么说明反破解并没有影响到你
黑猴的技术栈
游戏引擎的选择
-
使用引擎:UE
-
选择UE的原因
-
渲染画质是UE的一个比较重要的优势
-
Unity与UE对比
-
跨平台能力的对比
-
大部分PC端,包括主机端的游戏,如果要开发追求3A品质,UE通常都是一个比较优先的选择
-
主机端如果不追求3A画质,更多时候也会选择使用Unity进行开发,比如一些VR,AR,MR应用的开发
-
相比来说,手机游戏则更多选择Unity进行开发
-
应用商店Top10项目数量对比
-
移动端的各大应用商店中(苹果商店、谷歌商店、国内各手机厂商应用市场等),手游排行榜靠前的游戏大部分都是Unity开发的
-
如米哈游的《原神》《崩坏》《绝区零》,其它游戏厂商产品如《王者荣耀》《炉石传说》《皇室战争》等等
-
相对来说,使用UE开发的手游项目要相对少一些
-
小游戏领域,目前使用UE进行开发的团队也占少数,大多数小游戏的开发还是使用Unity或者其它游戏引擎,如Cocos,来进行
-
学习资源对比
-
Unity很显然比UE丰富得多,可以说占绝对优势
-
市场占有率情况对比
-
目前(2024年8月),Unity的市场占有率在60%以上
-
UE的市场占有率则是20%多
-
其它市场份额被godot , cocos , laya等引擎瓜分
-
上手难度对比
-
Unity是更加友好
-
Unity的API接口设计,使用的编程语言(C#)都使Unity更容易更容易上手
-
UE的上手难度会稍微的大一些
-
编程语言为C++
-
性能好
-
容易出现程序Bug
-
Bug难修
-
学习曲线对比
-
Unity相对平坦
-
Unity一开始难度比较高,但后续难度是逐步上升的,随着工作和不断学习,自然而然就成为专家了
-
UE比较陡峭一些
-
UE则是一个相反的曲线
-
它一开始的学习难度相对比较高,然后逐步曲线变得越来越平缓
-
总体来说,起步要稍微难一些,在后期则可能会轻松一些
-
Unity拥有这么多优势,黑猴仍然选择了UE引擎来进行开发,显然,对于黑猴团队来说,许多在编程新手看来是问题的存在,对于他们可能并不存在问题
-
比如UE原版只支持C++进行编程,但黑猴最终选择的开发语言却不是C++,接下来我们来一起分析一下黑猴开发语言的选择
开发语言的选择
-
直接揭晓答案:黑猴最终选择的语言是C#
-
为什么使用C#,不使用C++
-
使用C++编写程序脚本存在开发时间长、容易产生异常和崩溃的问题;C#则相对安全、快捷许多
-
使用C++编程需要自己手动管理内存,极容易出错;C#自动管理,开发简洁省事,更为安全
-
由于历史原因,C++功能库比较杂,且标准不统一,导致其程序出现异常时,错误排查会比较困难;C#具有统一的标准,有专门的公司(微软)进行维护,异常调试机制不断更新进步,正变得更便捷直观
-
综上,使用C#能极大压缩开发成本
-
为什么能使用C#
-
为何有此疑问?
-
UE原生只支持使用C++编写程序脚本,并不直接支持C#
-
之所以能使用C#,是因为"黑猴"在脚本层上面使用了一个插件
-
这个插件能够允许在UE中编写C#代码,并且把C#代码转换成C++代码(或者说机器指令),最终编译成可执行程序,让UE引擎来进行执行
脚本层的其它可能
-
除了C#之外,当前市场上比较流行的还有其它选择,那么为什么最终选择了C#而不是其它方案呢?
-
为什么不选择Lua?
-
Lua是弱类型脚本语言
-
弱类型的语言的问题:
-
弱类型语言天生就有一个问题:没有类型
-
"没有类型"导致写代码时可以获得的代码提示很少,编译错误的检查也不方便
-
执行效率不高
-
为什么不选择PureTS?
-
PureTS是基于TypeScript的一种脚本语言方案
-
TypeScript带有类型检查,偏强类型,但是它最终会被转换成弱类型的JavaScript,执行效率并不高
-
相较而言,C#具备以下优点
-
C#脚本可以被转换成类似于机器指令来执行,所以说从执行效率上来说C#语言效率是最高的
-
C#是一门强类型的语言,我们可以从这些类型信息获得丰富的代码提示,并获得准确的编译错误提示
-
事实证明,通过Pak解包,我们确实没有在最终解出来的符号表中,发现与Lua或JS相关的字符串信息
-
接下来,我们一起来对Pak数据包进行解析
Pak解包
Pak数据包中包含哪些东西?
- .data(配置文件),里面包含资源路径信息,以及角色属性、道具、武器、防具、技能等配置信息
-
.data的结构类似于Protobuf
-
Protobuf:一种网络协议。既可以用来存储游戏当中的单机的数据(能对数据进行压缩);也可以用来进行网络数据传输
- .Data(二进制文件)
-
.Data是与程序代码相关的数据
-
正是通过对这部分文件的分析,我们没有找到Lua和TS的痕迹,因此确定黑猴在脚本层并未真正使用Lua和TS进行游戏开发
-
在解包文件中,我们还观察到了"Process.Machine"的字样,我们推测,这应该与"状态机"或者"行为树"有关
-
状态机:"小白的游戏梦":Unity全栈开发大师(公开课) 大厂商业架构/优化/热更/渲染/王者荣耀/MMO
-
行为树:"行为树训练营:行为树实战训练营:以空洞骑士BOSS AI为例
-
当然,也可以通过其它途径印证我们的这一推测
那么如何印证黑猴使用了C#的猜测呢?
- 从解包的文件中,我们找到了SharpClass类
-
如上图,可知SharpClass继承自BlueprintGeneratedClass
-
同时,熟悉UE的朋友应该知道,SharpClass是USharp当中的一个自定义类
-
PS:蓝图生成的类都从BlueprintGeneratedClass继承,SharpClass是从BlueprintGeneratedClass类继承的,是所有C#脚本的基类
-
关于USharp
-
是UE的一个插件
-
其作用是在运行时将C#中定义的UClass动态地转换成虚幻引擎(UE)能够识别的类型
-
由于该插件本身并不支持虚幻引擎5,也不支持主机和移动设备平台,我们基本可以判定,黑猴使用的USharp插件,一定是经过魔改的
-
到这里,细心的同学肯定会提出这样的质疑:
-
项目当中只看到了USharp这个字符串,并没有直接找到USharp的dll文件,这个现象和Lua、TS一模一样,如何能说黑猴一定使用的是C#呢?
-
PS:如果EXE可执行程序里面要想引用一个插件,那么在最终打包的项目里,是肯定必须包括这个插件相关的dll文件的
-
事实上,通过进一步分析,我们可以看到,此处黑猴使用了il2cpp对所有dll进行了打包
-
这样做的直接结果就是dll文件不再直接暴露在外
-
关于il2cpp,可以参考视频"Unity热更新那些事":Unity全栈开发大师(公开课) 大厂商业架构/优化/热更/渲染/王者荣耀/MMO
-
如果想要系统的学习如何在项目当中掌握il2cpp,可以参考我们的主程进阶之路的S3、S6系列技术专题
-
以上就是黑猴解包的全部内容,接下来,我们开始分析黑猴的战斗系统
战斗系统分析
黑猴的四种攻击模式
-
我们先来对黑猴的攻击技能做一个初步的了解
-
黑猴的攻击方式只有一种:棍
-
并且棍子的攻击模式也只有四种:轻棍,棍势(重棍),棍花,跃击
-
棍花主要用来进行格挡,也具备一定攻击能力;
-
跃击则比较特殊,它可以跟轻棍和棍势混合使用;同时也具备一定的防御特性
-
看起来,黑猴的攻击方式似乎十分单一,但随着技能点的提升,会有很多种的变化,组合起来还是非常有玩头的
-
接下来,我们详细分析一下轻棍和棍势的实现
-
首先是轻棍
-
黑猴的轻棍有五段攻击:横扫,挑击,圆月扫,舞花,劈提
-
这五段攻击就是常规意义上的一套连招
-
怎样实现连招?
-
连招技术的简单实现版,详细操作可参见文章末尾视频的"十分钟掌握游戏连招系统"(1:11:06开始)
-
此处贴出"十分钟掌握游戏连招系统"的核心代码,有兴趣的可以参阅一下
-
相关变量
- 鼠标点击检测
- 鼠标点击检测的主要工作是检测和记录鼠标点击事件,并在满足连击条件的情况下,是动画状态机进入连击的下一个动作
- Update函数中主要完成以下任务
-
检测当前正在播放动画是否即将结束,如果是,则关闭对应动画,避免同意动画重复播放;这意味着如果没能在动画播放完成之前完成连击,将无法进入下一个动作
-
调用"鼠标点击检测"
-
更复杂的版本,如更复杂的连招系统、联网的连招系统等,可以关注一下我们的主程进阶和二次元游戏开发相关内容:
-
然后是棍势
-
棍势,换个描述或许更便于理解:重棍
-
黑猴中拥有三种棍势,即劈棍势,立棍式,戳棍势
-
棍势的实现
-
与轻棍相同,棍势的主要表现也是在动画上
-
此外,棍势的使用需受到体力限制(这些东西都可以且理应在配置表中进行配置)
-
棍势的实现方式和前面轻棍的连招相仿,从基本的角度来说,都需要动画状态机,也都需要逻辑上对战斗状态、技能状态的控制,同时,也要考虑角色的属性系统、装备系统对角色战斗能力的加成
-
由于篇幅限制,并且其实现方式与轻棍、棍势相仿,其它两种攻击模式就不再展开讨论
-
接下来,我们将进入更硬核的内容,开始学习如何逐步构建我们的战斗系统框架
战斗系统构成
-
首先,让我们先了解一下战斗系统的构成
-
上图中展示了通用战斗系统所常见一些子系统
-
基本上来说,所有带战斗的游戏,都少不了这些子系统
-
比如二次元游戏《原神》《绝区零》,回合制游戏《星铁》等
-
但是同样是这个架子,不同团体、不同程序做出来的最终成品质量却可能天差地别,其中究竟是哪些部分造成了如此差别?
战斗系统开发难点
-
普遍来说,战斗系统开发的难点,主要可以从以下三个反面进行分析
-
灵活性:战斗系统的"灵活性"体现在战斗算法的高通用能力上
-
比如,我们开发一个单独的攻击技能其实并不难,但是当我们要考虑,我们需要开发的是"一种"而不是"一个"技能时,难度将直线上升
-
"一种技能"和"一个技能"的区别是,当我们开发的是"一种技能"时,那么你写一遍算法,这个算法就能用在所有类似的技能上
-
比如所有近战技能,或所有远程技能上
-
可扩展性:战斗系统的"可扩展性"体现在对新增内容的高度兼容上
-
如新增角色,新增技能时,战斗系统能便捷地跟随策划的需求不断进行拓展
-
可维护性:战斗系统的"可维护性"体现在对现有代码维护的便捷性上
-
比如,当技能表现效果不正常时,应很方便地定位到相应位置
-
基于以上所述的三个难点,我们想要解决它们,首先,我们就需要为战斗系统构建一个强大的数值属性系统
属性数值构建
-
一个优秀的数值系统,在构建时不仅要考虑实现策划案的所有需求,还要考虑如何能方便程序开发
-
对于程序来说,数值系统的构建是最有技术含量的,最考验程序在项目设计、数据结构设计、框架搭建的设计能力
-
设计得有问题数据结构,其算法会比设计得好的数据结构的算法要复杂十倍
-
而只要涉及属性,基本就无法规避属性加成的存在
-
接下来,我们来具体聊一聊装备的属性加成
装备属性加成
-
要实现属性的加成,那么,就需要一个包含支持属性加成的装备的配置表
-
如上图,该装备配置表中,包括了攻击、速度、暴击伤害等属性的加成
-
实际项目中,每一种技能的加成都是不一样的
-
要想使属性加成表产生效果,还需要一个能很方便实现属性加成功能的配套的代码
-
什么叫很方便实现?
-
就是说在战斗时候,当我把某一件装备装配到角色身上后,程序可以很方便的在逻辑算法中去使用它
-
当角色卸下这个装备后,之前的所有属性加成都要消失;并且网络上所有的玩家都能看到我的角色属性消失以后所产生的变化
-
了解完属性配置相关的基本知识,接下来,我们开始战斗核心系统的拆解
战斗核心系统:战斗的流程
-
战斗系统的实现,也有一套标准的流程
-
比如技能编辑器的实现,首先,需要策划进行剧情编排
-
可以使用各式各样的智能编辑器
-
如上图,在设计技能时,策划使用了Timeline对技能编辑器进行编排
-
Timeline如何使用,可以翻阅我以往发布的视频或者相关训练营(如《鱿鱼游戏训练营:鱿鱼游戏实战训练营》)
-
然后,当策划流程配置完成,就会形成一个技能配置文件
-
再之后,就需要使用程序去调用这些动能,来实现这些编排好的剧情的表现
-
比如一个战斗的前摇、施法、后摇,技能配套的动画、音效、特效,敌人受击后的反馈产生的数值变化等等
-
在实现上述基础功能之外,程序代码还需要保证:
-
代码的高扩展性,低耦合性
-
框架要能满足让策划能随意添加技能,且尽量少的避免修改代码
- 那么,如何实现战斗系统的高扩展性呢
如何实现战斗系统的高扩展性
-
在此之前,我们不妨先进行如下思考:
-
米哈游再出一个英雄,你是不是要重新全部实现一次英雄的技能呢?
-
答案是:不需要
-
一个扩展性高的战斗系统,在面对这样的需求时,是不需要去重新写的,甚至代码都不需要改动
-
只需直接修改配置表,增加一两条数据项,就可以实现新角色、新技能的添加
战斗技能实现高扩展性的两大法宝
法宝一:技能配置文件
-
首先,我们需要根据策划需求,来确定技能流程,并最终形成技能配置文件
-
那么在此之后,最核心的东西,就是要把这些配置文件用算法合理实现出来
-
再次强调:配置文件是否合理,决定了算法是否高效
-
以下给出一个配置案例,仅供参考
法宝二:逻辑节点抽象
上图中,提供了两个技能的策划案需求
- 思考这两个需求示例,你是否能分辨出这两个需求中,哪些部分是相异的,需要单独处理的需求;哪些点是类似的,可以合并处理、进行抽象的逻辑?
-
上图中,红色字体是需要特殊处理的部分;黑色字体是可以合并的、抽象成一个统一技能流程的部分
-
讲到技能,就不能忽略Buff,接下来,我们再来聊一聊Buff系统
Buff的运行逻辑
-
技能释放的时候,有些技能会产生Buff效果
-
对于每一个项目,Buff的实施方式可能并不一样,不过标准的流程一般包含以下步骤
-
通过Buff配置表对Buff进行配置
-
配置完成,导出为程序可读的配置文件
-
程序读取,执行
- 具体的制作步骤,由于时间原因,这里不再展开,这里,我再提出一些Buff制作过程中,值得我们关注以及需要我们提前考虑的点,抛砖引玉,以供大家进一步思考
-
一个角色可能不止一个Buff,如何让这些Buff同时产生作用?
-
网络状态下,Buff如何进行同步?
小结
-
那么最后给大家做一个总结
-
本节内容,我们首先对《黑神话悟空》做了一个技术栈的拆解;
-
然后给大家讲解了黑神话悟空的战斗系统;
-
战斗系统必须建立在拥有一个角色的属性系统上,因此我们延伸到了属性系统的构建
-
基于属性系统,我们聊到了如何建立各种各样的装备、技能加成
-
然后有些技能是远程技能,那么还需要考虑到子弹的释放,因此也必须把它加入到我们的战斗流程里面
-
此外,有些技能是会产生Buff的,那如何去执行一个Buff?
-
对于一个简单的游戏,Buff系统的制作可能很简单;
-
但是对于一个复杂的游戏,如果组织管理不好Buff的产生、销毁所带来的改变,会导致技能系统非常混乱,最后发现,代码写得越多,Bug越是按下这个又起来那个
-
这些都我们需要在实际项目当中去完善的内容
-
最后,我们还讲了战斗系统的执行逻辑,大家可以参考这个执行逻辑,自己尝试能不能实现一套完善的战斗系统
- 总的来说,我们需要一套稳定的技能系统、战斗系统框架,这个框架,要支持网络同步、高性能高效的客户端表现、以及服务器计算
完整视频请点击本链接观看:
【全栈游戏开发】黑猴开发框架与战斗系统解析_哔哩哔哩_bilibili
技术交流请加本人v:alice17173