建德若偷,质真若渝。大方无隅,大器晚成。
大音希声,大象无形。夫唯道善贷且成。
——老子,《道德经》
----------------------------------------------------------------------------------------------------------------------------------
注解:
“无声”、“无形”本来是虚空的东西,谈不上“大音”、“大象”。所谓“大音希声,大象无形”,应是大音若无声,大象若无形。至美的乐音,至美的形象已经到了和自然融为一体的境界,反倒给人以无音、无形的感觉。“大音”、“大象”至少有一个负载它们的实体,才能显示其“大”。
“大音希声,大象无形”出自《老子》(又名《道德经》),是老子对“道”的阐释,应解释为“最美的声音就是听起来无声响,最美的形象就是看不见行迹”。大音若无声,大象若无形,至美的乐音、至美的形象已经到了和自然融为一体的境界,反倒给人以无音、无形的感觉。
在现代,“大音希声,大象无形”则更代表一种将美融入生活的智慧;情感热烈深沉而不矫饰喧嚣,智慧隽永明快而不邀宠于形。拥有这种智慧的人不用刻意地去想什么、做什么,便自然无形地把情感使用到最值得、最有意义的地方去,从而使自己更好地享受生活!
-----------------------------------------------------------------------------------------------------------------------------摘自百度知道
学习虚幻引擎引擎,抓住最核心的五个类:UObject、Actor、Pawn、Controller、Character
一、UObject类提供以下功能:
1、垃圾回收
c++内存由程序员完成,不需要对象a的时候,该不该释放该内存?如果释放,万一其他对象引用了,释放后就产生野指针,当另一个对象访问时,会看到空空如也,如果释放,如果我已经是最后一个使用的人,这片内存就永远无法被回收。
对此虚幻这样解决:
(1)继承UObject类,同时指向UObject类实例对象的指针成员变量,使用UPROPERTY宏标记,虚幻引擎的UObject架构会自动被UProperty标记的变量考虑到垃圾回收系统中,自动进行对象生命周期管理。
(2)采用智能指针:只有非UObject类型才能使用智能指针进行内存释放。
2、反射
反射的主要作用在于:运行时如何获取一个类,如何获取成员变量、成员函数。
c++没有提供这样一套机制,虚幻实现了这样一套机制。
3、序列化
希望把一个类的对象保存到磁盘,下次无损加载,需要继承UObject类。
4、和引擎编辑器的交互
如果希望类的变量能被虚幻引擎编辑器Editor简单编辑,需要继承这个类。
5、运行时类型识别
虚幻引擎中无法使用c++标准的dynamic_cast可以继承UObject类,然后Cast<>函数完成
6、网络复制
网络游戏中服务器、客户端能够自动处理变量的同步,被宏标记的变量能自动完成网络复制功能,从服务器端复制变量到客户端。
二、Actor类
它能够被挂载组件,在Actor中,坐标和旋转量只是一个Scene Component组件,想让Actor被渲染,挂载一个静态网格组件,想让Actor有骨骼动画,挂载一个骨架网格物体组件,希望Actor能移动,挂在一个Movement组件,也即需要挂载组件的时候,才应该继承Actor类。
三、Pawn类
提供了被操控的特性能被一个Controller操纵,这个Controller可以是玩家Player Controller类,也可以是AI Controller类,Pawn类一旦脱离Controller就是无法行动的肉体。
四、Character
Character类继承Pawn类,它提供了一个特殊的组件,CharacterMovement,该组件提供了角色移动、跳跃等功能,如果不需要移动逻辑,可以不继承Character类只继承Pawn类。
五、Controller
既然它是灵魂,通过Possess/UnPossess来控制/卸载一个肉体(Pawn)
六、创建C++类
工程目录下的Source文件夹下,找到和你游戏名称一致的文件夹,会发现Public文件夹、Private文件夹、.build.cs文件
我们将.h文件放在public文件夹中,.cpp文件放到private文件夹中
在.h中声明你的类,如果类继承UObject,类名上方加入UCLASS()宏,同时在类的第一行加GENERATED_UCLASS_BODY()宏,或者GENERATED_BODY()宏,前者需要手动实现一个带有const FObjectInitializer&参数的构造函数,后者手动实现一个无参构造函数,
七、虚幻引擎类命名规则
F:纯c++类
U:继承UObject但不继承Actor
A:继承Actor
S:Slate控件相关类
H:HitResult相关类
八、类对象的产生
如果类是一个纯c++(F开头),通过new产生对象
如果继承UObject但不继承Actor,需要通过NewObject函数产生出对象,NewObject<T>()会返回一个指向该类的对象,通过UWorld(可以通过GetWorld()获得)的SpawnActor产生出对象。这样调用:GetWorld()->SpawnActor<AYourActorClass>()
如果继承AActor,通过SpawnActor函数产生出对象
九、类对象的销毁
1、如果纯c++类在函数体中创建,而且不是通过new来分配内存,如下:
FYourClass YourObject=FYourClass(); //通过函数拿到类
那么类对象在函数调用结束后随函数的释放而释放,不需手动
2、如果纯c++类使用new来分配内存,而且直接传递类的指针,除非手动删除,否则内存永远不会释放。如果忘记了,造成内存泄露。
3、如果纯c++类使用new来分配内存,而且使用智能指针TSharedPtr/TShared-Ref来管理,不应该再手动释放,智能指针会使用引用计数来自动释放内存,使用MakeShareable函数将普通指针转化为智能指针。
十、UObject类
借助NewObject<T>函数产生,无法通过标准New操作符产生
无法使用智能指针管理对象,它采取自动垃圾回收机制,当类成员变量包含UObject对象,带有UPROPERTY宏,这个成员变量会触发引用计数机制。
垃圾回收期会定时从根节点Root开始检查,当一个UObject没有被别的任何UObject引用就会被垃圾回收。可以通过AddToRoot函数让一个UObject一直不被回收。
十一、Actor类
通过Destroy函数销毁
十二、UPROPERTY宏
将一个UObject类的子类成员变量注册到蓝图中,需要该宏,比如注册一个变量到蓝图中:
UPROPERTY(BlueprintReadWrite,VisibleAnywhere,Category="Object")
十三、UFUNCTION宏
注册函数到蓝图中
UFUNCTION(BlueprintCallable,Category="Test")
BlueprintCallable表示函数可以被蓝图调用,可选的还有BlueprintImplementEvent表示这个成员函数由蓝图子类实现,不该在C++中给出函数的实现,BlueprintNativeEvent表示成员函数提供一个C++的默认实现,同时可以被蓝图重载,需要提供一个"函数名_Implementation"为名字的函数实现,放在.cpp中。
十四、行为树
虚幻4中状态机被行为树代替,状态机在动画蓝图中保留,因为行为树更简化,更接近人的思维。
十五、同步
客户端是对服务端的拙劣模仿:客户端自己也运行着一个世界,不断预测服务端的行为,从而不断更新当前世界,最大程度接近服务端的世界,在延迟情况下,客户端不在试图同步服务器,而是模仿服务器,客户端根据同步数据发送时的当前对象位置和速度,猜测出当前对象在服务端的可能位置,并且通过修正当前世界,去模仿服务端的位置,如果服务端客户端差距太大,强行闪现修正。
十六、预编译
c++中只编译.cpp文件,编译成*.cpp文件(\debug目录下),c++中宏展开:代码中出现的宏用宏实体代替
#define 定义常量、函数宏
#undef 结束常量、函数宏定义
c++的预处理器是在编译器之前运行,将宏定义、头文件加载到.cpp也即#include文件,将该文件的代码全部拷贝替换了#include语句,理解为将头文件的内容粘贴到#include处
c++中头文件是不被编译的。cpp引用头文件是当预编译时将头文件插入到cpp中,因此变量的定义、函数定义不写在头文件中,因为头文件可能被多个cpp引用,连接的时候可能出现重复定义的情况
为了防止头文件被重复引用,应当使用#if ndef #define ... #endif结构
十七、预编译头
一个工程中总有一大堆头文件,几乎所有cpp都必须包含,可不可以把这些头文件提取出来,只编译一遍然后其他cpp都能使用呢?这就是预编译头的由来。
在Debug或Release目录中有一个很大的.pch文件,这就是编译之后的预编译头
十八、模块机制
1、快速完成模块创建文件结构:
在c++工程的Source文件夹下,创建一个新的模块文件夹,结构如下:
1、模块名.Build.cs
2、模块名.h
3、模块名.cpp
模块名.Build.cs,该文件来告知UBT如何配置编译和构建环境,如下可见,这里添加了Core,CoreUobject,Engine,InputCore这四个模块,.Build.cs模块告知UBT如何配置编译和构建环境
using UnrealBuildTool;
public class pluginDev : ModuleRules
{
public pluginDev(TargetInfo Target)
{
publicDependencyModuleNames.AddRange(new string[]{"Core","CoreUObject","Engine","InputCore"});
PrivateDependencyModuleNames.AddRange(new string[]{});
}
}
模块名.h
#pragma once
#include "Engine.h"
模块名.cpp
#include "pluginDev.h" //由于.h文件为预编译头文件,预编译头文件可以加速代码的编译,只要是.cpp文件都要包含预编译头文件
IMPLEMENT_PRIMARY_GAME_MODULE(FDefaultGameModuleImpl, pluginDev, "pluginDev")
2、模块卸载、加载函数
在.h和.cpp中覆写StartupModule和ShutdownModule函数,来书写模块启动和卸载时需要执行的内容。
3、预编译头文件
预编译头文件能加速代码的编译,头文件的标准命名方式为:“模块名PrivatePCH.h”放置于Private文件夹中,当前模块所有.cpp文件都要包含预编译头文件
4、引入模块
引入当前模块的方式是在目录下的Source文件夹中,找到工程名.Target.cs
public override void SetupBinaries(TargetInfo Target,ref List<UEBuildBinaryConfiguration>OutBuildBinaryConfigurations,ref List<string> OutExtraModuleNames)
{
OutExtraModuleNames.AddRange(new string[]{"pluginDev"}); //在这里添加引入的模块名称
}
5、虚幻引擎模块加载顺序
十九、内存分配
虚幻引擎是通过宏来控制,在几个内存分配器中选择的
二十、垃圾回收
1、引用计数法:
优点:不需要程序暂停,垃圾回收的过程分配到运行中。
缺点:
(1)如果成千上万个对象生成,那么频繁修改数值开销很大
(2)环形引用:有相互引用的对象会出错
2、标记-清扫算法(追踪式GC)
和引用计数法只关注单个对象的思路相反,遍历每一个对象,看有没有被引用,没有就回收
优点:没有环形引用问题
缺点:
(1)执行该算法需要程序暂停,这会导致系统有明显延迟
(2)只是丢掉垃圾而不整理,导致可用空间越来越细碎,导致大型对象无法被分配
虚幻引用的智能指针采用引用计数算法,使用弱指针解决环形引用。
二十一、UBT和UHT
UBT(Unreal Build Tool):
UHT(Unreal Header Tool):
一个引擎独立应用程序,UBT会通过命令行参数告诉UHT,游戏模块对应的定义文件在哪,包含了编译的相关信息,然后UHT开始了自己的三道编译:Public Classes Headers、Public Headers、Private Headers
经过三道编译,最终生成.generated.cpp和generated.h两种文件,
同时对含有UProperty和UFunction字样的数据类型进行登记。
二十二、虚幻引擎的垃圾回收机制
二十三、Actor对象