原视频文字全记录,侵删
如有错处,参见 原视频链接
一. 虚幻社区经理-大钊介绍
- 知乎:虚幻引擎
- B站:虚幻引擎官方
- QQ群:1018846968,微信群:unrealengine
- QQ:2722937652,微信:jack1046
- Unreal Circle
- MegaGrants
- 技术布道
- 传声筒
- 10+ C++经验
- …
二. 学习引擎的一般路线
新手村-核心骨干-江湖高手-坐镇大神-行业大佬
1. 新手村-学习引擎
- 学习引擎的编辑器使用
- 学习蓝图,简单的交互
- 学习引擎的各种功能模板:动画,AI,材质
UMG,网络,粒子等等 - 学会下载配置使用插件
能力标志:可以做一个小独立游戏(有UI有逻辑有动画…)
2. 核心骨干-掌握引擎
- 游戏的一些模板可以用C++实现,例如函数库实现计算逻辑
但是存在一些问题
(1) 懂Actor,不懂Object
(2) 懂Object,不懂Asset - 特定模块可以用C++编写,战斗逻辑,AI…
- 开始思考C++和蓝图如何更好结合的程序框架
- 开始收集整理一些常用的C++函数库(引擎)
- 开始学会创造自己的插件,在不同的项目使用
- 开始明确自己专攻的方向(只讨论程序方向)引擎每个模块都有深度
能力标志:C++和蓝图那个方便使用那个,使用起来适得其所
3. 江湖高手-懂得引擎
- 开始研究并理解引擎里模块的机制原理
- 开始在C++层开发支撑类和功能,引擎扩展Slate,改造渲染…
- 对引擎不合心意的地方,可以着手修改Hack,修改引擎bug
能力标志:对引擎的模块,都有信息可以找到源码并分析和扩展遗漏的功能,缺的只是时间!
4. 坐镇大神-超越引擎
- 可以重写某个引擎模块,做的比引擎的作者还好
- 经常负责优化引擎来适配自己的项目
- 改造引擎的一部分,改造开发配套流程,有机高效融入进自己的工作流中
能力标志:引擎于他只是自己项目框架工作流中的一个工作
5. 行业大佬-创造引擎
- 对引擎的整体结果了如指掌
- 可以站在整个引擎行业的高度创造更高生产力工具
- 编写创造的引擎功能或插件,对行业造成了深远的影响
能力标志:做事情,先从行业的高度来思考
三. 虚幻C++的进阶之路
5W+1H
What?Why?Who?When?Which?How?
1. What?-什么是虚幻C++?
- 扎实的标准C++基础能力
- 采用C++编写游戏逻辑
- 适应虚幻引擎的底层C++框架
- Module配置和插件使用
- UBT、UHT
- 掌握Core模块的C++库:FString,TArray,UE_LOG…
- 熟悉了解掌握CoreUObject:宏,GC,序列化
- 熟悉C++和蓝图交互的方式
2. What-学会虚幻C++的标志是什么?
- 懂得解决各种编译链接错误,常是因为Module配置出错
- 懂得一些常见的编写套路,CreateDefaultSubObject,UPROPERTY…
- 理解UObject的内存管理机制,不会经常造成内存崩溃
- 可以在源码里找到自己想要的代码块
3. Why?-为什么虚幻要开放C++来编写GamePlay?(为什么用C++写UE)
- 性能!性能!性能!
- 更高端,更需要压榨机能(更高的平台)
- 更底层,更有能力优化性能(接触底层)
- 更大项目,强类型更有能力管理项目(大项目)
(无类型语言一时爽,重构火葬场) - 更开发,更有灵活的定制性
- 更易迭代,不需要时时更新接口层
- 更相信开发者,进入了C++的自由领域,就得有自我负责的觉悟,(自己写的Bug自己Fix,上下限取决于自己)
当然还有友好的蓝图
4. Why-为什么要学习虚幻C++,可以不学吗?
- 方便扩展功能,直达引擎的底层结构
- 更好的性能,性能热点的优化
- 方便调试引擎,理解引擎流程和修改Bug
- 学习游戏引擎知识的好途径(不然做到东西的上限取决于别人写的API的上限)
- 很多时候,只有理解了Why,才能更好的解决How的问题
- 大项目必须有某种Hold得住的语言(不止蓝图)
- 虚幻程序员:从初级到中级的标志,其他的领域其实可以不学
5. Why-只有蓝图可以吗?
- 平衡蓝图和C++(官方文档有介绍)
- 没法实现定制化的需求
- 游戏逻辑只能达到有限的优化
- 不懂C++,失去了理解别人插件最有力的工具
- 项目大到一定项目就很难管理(文本编程有固有优势)
- 从使用工具到创造工具
6. Who-团队里有什么人需要学虚幻C++?
- 核心系统GamePlay程序员
- 实现功能的时候有可能需要扩展引擎的人,TA(技术美术),渲染…
- 引擎工具编写人员
- 引擎部的支持人员
(源码开放很幸福)
7. When-什么时候推荐开始学虚幻C++?
- 先把蓝图学明白用明白了
(否则就像游戏开启了困难模式) - 熟悉引擎GamePlay框架概念之后
- 也学会了引擎的各模块功能,动画蓝图,AI,UMG…
- 总而言之,先在自己工作领域把引擎用明白了(不懂就是不懂,实事求是,不要带着模模糊糊的概念)
8. When-什么时候算学会了蓝图?
- 蓝图太易用,从而导致很多人轻视
- 蓝图五脏俱全,类,结构,枚举,接口,回调
- 蓝图也是讲究设计模式和结构的
- 蓝图程序也要易于重构(职责划分清楚)
- 蓝图也可以数据驱动,灵活掌握引擎提供的各种赋值工具,有机结合起来
- 可以做到,不代表一定要时时这么做,特别是做原型的时候
9. Which-虚幻C++有哪些学习要点?
- Core (源码库)
- GamePlay (游戏模式之类)
- Module-UBT-C#
- 反射-UHT
- CoreUObject-GC
- 套路
- 各模块的“地方习俗”
1. Core
- 首先C++基础真的要去巩固,很多人栽在这上面(真的重要哈)
- UE4使用C++11,所以需要学习到能看懂:
- TArray,TSet,TMap等各种容器
- FString,FName, FText的操作互转
- TSharedPtr,TSharedRef,TWeakPtr等各种智能指针
- 有能力理解Delegate,TAttribute,TSubClassOf的机制和用法
- 多看看设计模式,这也才能更好理解UE4代码的结构
- 多线程的知识,这样才能用好FRunnable等多线程同步
- 人脑展开宏???
这部分学习同步进行,但如果只是使用的话,C++98其实就可以跑,
哪里去学?在源码的Core模块
2. GamePlay
- GamePlay的C++编写
- Actor的创建,组装 Component,BeginPlay,Tick,碰撞输入事件绑定
- 引擎GamePlay对象的继承组织使用
- UObject自定义对象的组织管理(根据逻辑而定)
- 引擎数据对象的使用,Config,DataTable
- 功能模块的C++层级编写(UMG,AI,动画)注意有机结合(别偏向一边)
在代码库Engine/Class/GameFramework
3. Module-UBT-C#
1. 思考:为什么用C#来管理编译流程?合适的头文件包含和库链接
- 主要问题
- 一个.cpp文件就是一个编译单元
- include就是复制粘贴
- include哪些文件?
- 链接那些dll?
- 答
- UE4支持众多平台,包括Windows,IOS,Android等,因此UE4为了方便你配置各个平台的参数和编译选项,简化编译流程,UE4实现了自己的一套编译系统,否则我们就得接受各个平台再单独配置一套项目之苦了。
- UnrealBuildTool(UBT,C#):UE4的自定义工具,来编译UE4的逐个模块并处理依赖等。我们编写的Target.cs,Build.cs都是为这个工具服务的。
- UnrealHeaderTool(UHT,C++):UE4的C++代码解析生成工具,我们在代码里写的那些宏UCLASS等和#include “*.generated.h”都为UHT提供了信息来生成相应的C++反射代码
- 一般来说,UBT会先调用UHT,解析一遍C++代码,生成相应其他代码。然后看是调用平台特定的编译工具(VisualStudio,LLVM)来编译各个模块。最后启动Editor或者是Game。
- 优缺点
- 优点:C#足够容易读,C#足够灵活制定逻辑
,C#可以动态编译,方便收集信息,C#足够强大可以调用其他工具 - 缺点:C#和C++混合经常搞得有些人糊涂,
C++项目里混进C#没有智能提示不够友好 - 解决:常见的错误都是这两者报出的,记住常用用法就行了,有问题再查
- 优点:C#足够容易读,C#足够灵活制定逻辑
悄悄说,UBT用的是NMake Build System
2. 学习要点
- 学会链接模块,项目和插件可包含多个模块
- UBT调用UHT生成代码,然后调用MSBuild编译代码
- Build.cs是重点:(ModuleRules.cs)
- Build发生的三件事情
- UBT收集项目的所有c++文件和c#文件
- UBT调用UHT,UHT根据宏生成反射C++代码
- 调用MSBuild将收集的文件和反射C++代码一起编译
3. ModuleRules.cs-模块链接
-
PublicDependencyModuleNames:
- public链接的模块名称,最常用
- 在自己的public和private里包含对方的public
-
PrivateDependencyModuleName:
- private链接的模块名称,只引用不暴露
- 在private里包含对方的public,不扩充自己的public
-
DynamicallyLoadedModuleName:
- 动态链接的模块名称,在runtime被ModuleManager加载,保证先编译
4. ModuleRules.cs-头文件include
- PublicIncludePaths
- public包含的路径
- 定义自己向外暴露的public,默认“Public”和“Classes”
- PrivateIncludePaths
- private包含的路径
- 定义自己的private,给自己内部引用,默认“Private”,一般用来定义Private子目录。当然也可以路径包含Private/Sub,但这是一种方便形式。
5. ModuleRules.cs-头文件include模块
- PublicIncludePathModuleNames:
- public包含的模块名称,可以只写模块名称
- PrivateIncludePathModuleName:
- private包含的模块名称,可以只写模块名称
- 用途
- 只包含对方模块的.h文件,比如模块,虽然挺少见
- 更多是动态链接,先包含头文件信息,之后加载
5. ModuleRules.cs-第三方库链接
- PublicAdditionalLibraries
引用的第三方lib路径 - PublicSystemLibraryPaths
引用的系统lib路径,其实也只是lib,只不过对于一些更"系统"底层的库用这个名字更友好一些 - PublicDelayLoadDlls
延迟加载的dll
6. ModuleRules.cs-其他常用
- PublicDefinitions+PrivateDefinitions
额外的其他C++宏定义 - Target:
- 得到当前的编译目标信息
- Platform(平台):Win64,Android,IOS
- Configuration(配置):Debug,Development,Shipping
很多人分不清public和private包含(如果只在cpp用,就不必暴露出来,用private)
4. 反射-UHT和CoreObject…
- UHT
- 理解.generated.h和.generated.cpp
- 理解MODULENAME_API的含义,常见的犯错地方
- 掌握使用这些宏的含义和用法(反射的重要标记)
– UCLASS()
– USTRUCT()
– UENUM()
– UPROPERTY()
– UFUNCTION()
– UINTERFACE()
– GENERATED_BODY() - 清晰理解类型和对象之间的关系
(ClassReference和ObjectReference) - 理解UClass*,UScriptStruct*,UField*,UFunction*,UProperty*
- 掌握通过反射遍历对象的属性,读入写入(常见)
- 掌握通过反射遍历对象的函数,并调用的方式(少一点)
- 通过对象找类型,通过类型找对象
- 理解"对象用类型描述,类型也是对象"
- CoreObject
- GC:理解对象之间的关系,标记清扫,有一些对象是Root!
- 在用法感觉上可简单和C#类比
- 注意只有用UPROPERTY标记的才参加GC! 因为要根据UProperty*来分析引用链!
- 注意FMyStruct和UObject的混用,FGCObject::AddReferencedObjects
- 自从用了UE4,我再也不new/delete了! (因为UE4重载了运算符)
- CDO:理解类型和对象实例化,模板
- 理解ClassDefaults作为模板的作用
- 理解CDO在序列化中的意义作用
- 通过UClass::GetDefaultObject来获得CDO信息
- Package:理解对象的相互组织方式
- 对象可以包含子对象
- 序列化时,把一系列对象用一个对象包起来,这个对象叫做包
- Package也可以相互引用,根据对象相对路径…
- GC:理解对象之间的关系,标记清扫,有一些对象是Root!
5. 套路
1. UHT的套路
- 宏!
2. 模块链接套路
- 几个常见属性.AddRange!
3. Actor创建的套路
- ConstructorHelpers
- CreateDefaultSubobject
- SetupAttachment
4. GamePlay继承的套路
- 尽量别再关卡蓝图里写逻辑
- 想要结构良好,尽量遵循引擎结构,各司其职
- GI,GM,GS,PC,Pawn,PS,继承,全都得会
- 遵循推荐结构,会发现后期扩展和支持联机有天然优势
5. C++和蓝图交互的套路:
- UPROPERTY,UFUNCTION
- C++定义基类写逻辑,蓝图继承配置可视化是种推荐易扩展高性能的方式
- 函数库是个好东西
6. 事件绑定的套路:
- DELEGATE,MULTICAST_DELEGATE,EVENT,DYNAMIC
- Input:BindAxis,BindAction
- Collision:Hit,Overlap,AddDynamic
- Slate&UMG Event:SLATE_EVENT(FOnClicked,OnClicked)
- FTimerManager
7. 引擎常用方法的套路
- Engine/Class/Kismet有好多库
- GameplayStatics很常用,可以访问GamePlay的很多对象
- UKismetSystemLibrary,系统目录等功能,LineTree
- UKismetMathLibrary,数学
10. Which-虚幻C++有哪些学习难点?
- C++基础太弱!工具越便利,人们接触底层的机会越少
- 数据结构算法,操作系统多线程
- 数学太渣:线性代数,向量矩阵,牛顿力学…
- 游戏开发知识不够:渲染,动画,AI…
- 项目开发经验不足:不懂得如何设计一个"足够好"得项目代码框架
- 知识要点太多记不住:宏太多,API记不住
- 翻找"借鉴"代码的能力不足:软件工程软技能不够
但是记住:比你聪明的人往往还比你勤奋…比你勤奋的人比你聪明
11. How-那应该如何学习虚幻C++?
1. 学习从模仿开始!形成肌肉记忆,也知道了去哪里抄!
- 模仿模板项目! 比如C++标准第一人称项目
- 模仿引擎源码,最权威也最借鉴,缺点是你首先要找得到,然后看得懂
- 模仿社区项目,例如ARPG,或者搞到的其他人项目或者插件,优点是接地气,缺点是水平参差不齐,一般写的比较杂乱,不要被带到沟里,因此建议多讨论交流
- 多看一些教程文章和视频,知乎,B站,YouYube,能吸收很多知识营养,包括本期直播!
2. 勇敢尝试动手改改
- 着手微调一些地方观察变化
- 遵循套路增加一些功能看能否做到
3. 总结规律
- 总结你发现观察到的概念,规律,规范,注意事项
- 用工具记录下来,鼓励社区分享贡献 !
- 然后你发现有人开始叫你大佬 !
4. 善于积累
- 可以弄个测试项目,用一个最小的场景测试你项理解的功能,这是你的车间
- 有用的代码功能块,可以用插件函数库封装起来,在项目之间复用,形成你的代码资产
- 对于概念,可以用思维导图来分析理解,理清关系和区别
- 尝试自己画流程图,能用图表示出来才说明自己理解透彻
5. 玩具项目实践
- 大大有助于自己思考项目架构,因为人在做自己想做的项目时都超兴奋的!
- 用项目级的实践来考验自己的掌握能力,只有把手弄脏才能明白很多学习阶段没发现的问题
- 万一成了呢 ?
大钊: 这辈子我是没法精通UE4了…但是学一分有一分的喜悦 !
Moota: +1
6. 个人常用工具
- VS+VA: 打开VS我就心情喜悦~(???)
- 思维导图:整理概念和流程,记录分析进度
- Processon:画流程图
- 笔记应用:记录一些搜集到的笔记文章和注意事项
- RSS订阅:订阅一些国外blog文章
- 音乐:写代码就像在战斗!
7. 个人常逛资源
- UE4官方YouTube:
https://www.youtube.com/user/UnrealDevelopmentKit/videos - 知乎: https://www.zhihu/com/org/xu-huan-yin-qing-24,技术文章
- B站: https://space.bilibili.com/138827797,技术视频
四. 虚幻C++推荐项目实践
(要嘛看不起锤子VS拿起锤子啥都想砸,指蓝图)
1. 应该有怎么样的思想觉悟?
- 有个流派看不起蓝图,代码原教旨主义
- 有个群体畏惧厌恶C++,比前浪还早到沙滩上
- 真正厉害的人应该是:我全都要!它们都是我可以利用的工具之一,工具就自然有擅长和薄弱,扬长避短,用不好是我的问题,不是工具的锅.
- 空谈用好没有用,真正的用好是建立在真正理解的基础上,所以得学习!
- 归根到底是为了项目得高效,开发,迭代,维护
- 所以有些东西你其实不用学,反正你也学不完…
- 但未雨绸缪还是得多学,为了未来项目的高效
- 所以选择方向很重要,项目资源有限,个人精力也有限
- 如何在有限的精力高效的学习,需要有清晰的学习路径和方法
- 所以我做了这期直播…
2. 合适的比例配方是什么?
- 我见过100%蓝图的游戏项目和企业建筑项目
- 我也见过100%C++的游戏项目
- 他们有些是因为不会,有些是因为不屑,但他们都失去了一些东西…
- 根据80/20原则,差不多20%核心用C++,80%表层蓝图
- 但也根据项目类型和个人技能掌握情况而定
- 大概规则:
- 偏向引擎底层,偏向性能热点,偏向稳定的,采用C++
- 偏向表现层,偏向经常操作的,偏向多变的,采用蓝图
3. 好用的套路
- 用插件来在项目间共享代码,动手编码前多想想
- 变量类型尽量的偏向抽象基类,比如UStaticMeshComponent和UPrimitiveComponent
- Include尽量IWYU,include what you use,加快编译速度
- 善用C++前置声明,减少include依赖,加快编译
- 蓝图函数库是个很有用的宝库
- C++代码里不要写数据配置!尝试数据驱动!C++ ->BP ->DataTable
- 蓝图也是要做好设计模式功能划分的,才能更好的协作
- 再说一次,项目一开始就C++定义基类+蓝图继承,后面肯定会发现这很有先见之明
- 没有C++基类的蓝图类,也可以reparent基类,记得先备份!
- 重构了C++基类名字,导致蓝图找不到父类?用CoreRedirects来修复
- 如果可以不改引擎,尽量不改!就算编译版引擎也可以用过UObject来打洞Hack,和C++奇技淫巧来访问private成员
- 如果想好要改,在改动上下包宏或注释标记,以后merge方便
五. 虚幻C++核心概念
-
一切从GEngine开始:UEngine* GEgine;UGameEngine/UEditorEngine
- 从GEngine可以开始获得Worlds,游戏播放和编辑的场景也都是个world
- 获取游戏视口进行slate叠加或截图等
-
GEditor对做编辑器扩展很有用:UEditorEngine*GEditor;
- 可以获得当前选择Actor等有用信息
- 可以获得当前视口
- 可以调用编辑器功能
- 可以获得事件回调
-
编辑器也是个游戏,也都是各种UObject组成的,所以只要你能获取到正确的对象,就能为所欲为了!
-
NewObject的背后,基本上所有的对象都存放在对象池里,意味着你能获取到任何对象
-
Actor里是可以runtime动态创添加Component的,想想Spline,想想编辑器本质只是在跑的一个Game.
六. 虚幻引擎源码剖析经验分享
1. 源码剖析方法论
1. 时空观
- 时:注重时间上的先后,观察函数的调用流程,事件的触发时机
- 空:注重数据的吞吐转换,观察信息的采集,数据的加工消化利用
- 有时要专注考察某个方面,有时要同时拥有两种脑袋
- 时空的交互,组成了有机的功能架构支撑,调度起资源,启动内循环,响应外部刺激
2. 信息论
- 最小信息原则=依赖最少=越稳定
- 信息本质也是能量,只有掌握了足够的信息,才有足够能量实现某些功能
- 数据是信息,代码结构是信息,项目协作方式也是信息
3. 从开始到结束
- 一开始有哪些东西?
- 第一把点火的推力是什么?
- 启动后自循环是那些推力?
- 有哪些外部输入的刺激?
- 向外部吐出了那些数据?向外部施加了那些力?
- 最后剩下什么?
4. 保持谦逊但不迷信
- 首先怀疑是自己太傻,最后才觉得人家写的不够好,97.34%最终会发现是自己的问题
- 觉得源码写得不够好,自己必须能切实的在充分理解问题区间的基础上,指出错误并写出更好方案,否则没有资格批评!忌空谈!
- 认识到引擎开发人员也是普通人,是人就会犯错,但大概率人家水平比自己高,小概率会疏忽
- 认识到引擎是个历史悠久的大工程,里面充满了历史馈赠的遗产,包袱和迷雾,所以很多觉得不够好的原因是历史带来的,不应该过于苛责
5. 比起好奇心更应该保持耐心
- 常常会困惑这是干嘛的?
- 常常会理解不了为什么这么做?有什么特殊的用意?
- 数据用来用去,代码调来调去,感觉一团乱麻!
- 调用链条太长,自己的脑容量堆栈不足以存储…
- 所需的背景知识有欠缺…
- 无人可问,孤军奋战…
- 直面磅礴的源码库,深感自己的渺小!
- 对引擎更新又激动又害怕,对自己一天只有24小时这个事实深感无力!(Moota:实际是总的时间少了)
承认自己是个凡人,余生只够掌握一小块知识,和自己妥协,从此虚心向别人请教,对别人的分享心存感激,为自己也能尽一份力分享而欣喜~
2. 源码剖析工具
1. 工具越简单越好,专注核心内容,别浪费时间调整格式
- 放弃除了VS之外的源码阅读工具,额外的都是负担,间接一层就多了一层信息同步消耗
- 放弃UML图系列工具,只取其思想,放弃其形式,对于上百个类来说,人类无法理解UML,哪怕各角度的表达,更何况还得手调其格式排版,一般情况下每新加一个类都得调整一遍
- 对于已经确定的内容,才需要画图来加深理解,别一边理解一边画图,90%的时间在调整图排版
- 我个人最后选择了思维导图(Xmind),有不足,但足够简单可理解
- 最后写文章,我用Markdown码字,Processon画流程图
2. 调试过程
- 开Debug配置编译源码,debug everything!
- 一边调试一边记录
- 在引擎源码内加debug输出,根据打印数据信息确定数据内容和先后
- 抓住主脉络,旁支情况记一下以后再收拾
3. "借鉴"的艺术
1. 寻找
找!顺藤摸瓜的找!
- WidgetReflector定位代码块
- VA查找所有引用或调用
- VS字符串查找
- Everything工具定位文件
- 开Debug断点查事件调用
2. 功底
瞧的懂大概是咋回事
- 见多识广
- 连蒙带猜
- 相信它一定在,只是自己找不着
3. 搬运
- 能找到办法直接调用的就别拷贝代码
- 能否找到调用的代码路径的功底在于:能否事先理解各模块对象的组织关系,事先修行有必要
- 别在runtime下想法子调用Editor的代码,不合法也费劲
- 找到目标代码块:输入和输出,小心内部的依赖
- 自己嫁接过来的时候要想法子给予足够的数据
- 如果是流程上插一脚,想法找到关键的"旋钮"