4摄像头总是乱动 unreal
unreal 4
2016-01-11 12:05:54 ttt301 阅读数 194
1  资料推荐
2015-10-27 19:40:30 pizi0475 阅读数 987

http://blog.csdn.net/neil3d/article/details/46723589


Unreal Engine 4发布好长好长时间了,直到最近才有时间仔细去看一下。

TimSweeney老大一句话“IF YOU LOVE SOMETHING, SET IT FREE”,原来需要几十万授权金才能拿到的东西,就从$19,变成免费了,而且开源。作为一个国际顶尖的引擎,能走出这一步,对我等普通开发者真是福音啊。如此的牛X,再加上开源,相信Unreal Engine 4会火起来的,你看看最近E3上的产品展示也能感觉到。不过,Unreal的定位毕竟是“国际顶尖”,而非Unity3D那样的“开发大众化”,所以上手是有一定难度的。

下面我把对Unreal Engine 4的初步印象总结一下,特别针对像我一样有Unreal Engine 3的同学,希望更多的同学来已经学习Unreal。


UnrealScript去掉了

开发语言上最大的变化,莫过于把UnrealScript去掉了。
UnrealScript吧,当年来看还是一个非常NB的东西,要知道Unreal一代发布是1998年的事儿,而JAVA语言也就是95年才发布的。据说Tim Sweeney在开始设计Unreal的时候曾经考虑过使用Java作为上层语言,不过那时候Java还不成熟,所以他参考Java,实现了这个一个面向对象的,单根继承的脚本语言。不过,随着时间流转,Epic似乎并没有花太大的力气去演进UnrealScript。在Unreal Engine 3的时代,它确实显得有点老旧了,书写起来比一些现代语言确实有很多不方便的地方。所以,去掉就去掉吧。不过,值得注意的是,官方也留出了整合其他脚本语言的接口,例如LUA。


C++11

底层很多基础代码看上去有很强的亲切感,底层架构设计思路沿用了许多。底层依然是使用C++,不过用了很多C++11的特性,代码看上去简洁了不少。
项目的编译、构建使用UnrealBuildTool,这应该是从3代延续过来;另外,就是增加了一个UnrealHeaderTool工具,猜想是根据UCLASS,UPROPERTY等宏,生成自定义反射信息用的,具体的还待进一步研究。


Blueprint Visual Scripting

据说这是UE4最牛X的改进了。看了看,原来是Kismet的延伸,连源代码很多都是UKismentXXX那一套。UE3里面的Kistmet只限于在一个关卡中使用,而Blueprint扩展了。关卡可以有唯一的一个Level Blueprint,相当于原来的Kismet;另外增加了Blueprint Class,大概就是用Blueprint创建自定义的Actor、Pown之类的,由于有了这个功能所以原来的Archetype顺带也就被替代了。其实,作为老一代屌丝Coder,我一直对Kismet那种表达式层级可视化编程,一直不太感冒(像Unity的PlayMaker那样,提供更高层级抽象的可视化工具更好)。不过,既然是UE4主推的个东东,还是得看看。
不过,总体上给Designer一套可视化编程的东西,让他们自己实现一些关卡逻辑、游戏规则之类的,还真是一个特别好的方法。当然,我们这些Coder的工作还是不会丢掉的,例如游戏框架,游戏一些底层功能、常用模块还是要C++写好(或者使用LUA脚本?),封装给Blueprint来使用的。


AnimTree哪去了

UE3的AnimTree给我震撼太大了,所以特别关心UE4的动画系统。看了一下,貌似被分解成了BlendSpace和AnimGraph。
  • BlendSpace
    好比说“站立、走、跑”这三个动作,在UE3的AnimTree里面是有一个特定的node来混合的,根据移动速度不同。在UE4里,则需要创建一个BlendSpace1D资源,然后暴露出Speed参数。
  • AnimBlueprint
    使用Blueprint,AnimGraph,状态机等等控制角色的动画,怎么看上去和Unity的Mecanim有点像呢,唉~
看来AnimTree是真的不见了,很遗憾,因为我觉得那个使用树形结构来抽象的动画系统,实在是非常清晰而且强大。

渲染系统

基于物理的渲染(PBR:Physically-Based Rendering)效果真的是太NB了,Unity5虽然也是PBR,好像比UE4还是略逊一筹啊!这个无需多言了,各种DEMO视频大家都看了不少了。渲染流程也完全走延迟渲染了。但多线程渲染,SceneProxy、Material之类的基础架构没怎么变。

Behavior Tree

这个东西好像在国外的游戏AI领域这几年挺流行了,是个很高大上的东西,UE4直接做了,太好了。

另外,还有很多重大改进,例如Package,资源导入,增加插件支持等,这里就不一一细说了。推荐看一下官网的文章吧,作为本文的补充偷笑

2015-07-02 19:38:32 Neil3D 阅读数 3908


Unreal Engine 4发布好长好长时间了,直到最近才有时间仔细去看一下。

TimSweeney老大一句话“IF YOU LOVE SOMETHING, SET IT FREE”,原来需要几十万授权金才能拿到的东西,就从$19,变成免费了,而且开源。作为一个国际顶尖的引擎,能走出这一步,对我等普通开发者真是福音啊。如此的牛X,再加上开源,相信Unreal Engine 4会火起来的,你看看最近E3上的产品展示也能感觉到。不过,Unreal的定位毕竟是“国际顶尖”,而非Unity3D那样的“开发大众化”,所以上手是有一定难度的。

下面我把对Unreal Engine 4的初步印象总结一下,特别针对像我一样有Unreal Engine 3的同学,希望更多的同学来已经学习Unreal。


UnrealScript去掉了

开发语言上最大的变化,莫过于把UnrealScript去掉了。
UnrealScript吧,当年来看还是一个非常NB的东西,要知道Unreal一代发布是1998年的事儿,而JAVA语言也就是95年才发布的。据说Tim Sweeney在开始设计Unreal的时候曾经考虑过使用Java作为上层语言,不过那时候Java还不成熟,所以他参考Java,实现了这个一个面向对象的,单根继承的脚本语言。不过,随着时间流转,Epic似乎并没有花太大的力气去演进UnrealScript。在Unreal Engine 3的时代,它确实显得有点老旧了,书写起来比一些现代语言确实有很多不方便的地方。所以,去掉就去掉吧。不过,值得注意的是,官方也留出了整合其他脚本语言的接口,例如LUA。


C++11

底层很多基础代码看上去有很强的亲切感,底层架构设计思路沿用了许多。底层依然是使用C++,不过用了很多C++11的特性,代码看上去简洁了不少。
项目的编译、构建使用UnrealBuildTool,这应该是从3代延续过来;另外,就是增加了一个UnrealHeaderTool工具,猜想是根据UCLASS,UPROPERTY等宏,生成自定义反射信息用的,具体的还待进一步研究。


Blueprint Visual Scripting

据说这是UE4最牛X的改进了。看了看,原来是Kismet的延伸,连源代码很多都是UKismentXXX那一套。UE3里面的Kistmet只限于在一个关卡中使用,而Blueprint扩展了。关卡可以有唯一的一个Level Blueprint,相当于原来的Kismet;另外增加了Blueprint Class,大概就是用Blueprint创建自定义的Actor、Pown之类的,由于有了这个功能所以原来的Archetype顺带也就被替代了。其实,作为老一代屌丝Coder,我一直对Kismet那种表达式层级可视化编程,一直不太感冒(像Unity的PlayMaker那样,提供更高层级抽象的可视化工具更好)。不过,既然是UE4主推的个东东,还是得看看。
不过,总体上给Designer一套可视化编程的东西,让他们自己实现一些关卡逻辑、游戏规则之类的,还真是一个特别好的方法。当然,我们这些Coder的工作还是不会丢掉的,例如游戏框架,游戏一些底层功能、常用模块还是要C++写好(或者使用LUA脚本?),封装给Blueprint来使用的。


AnimTree哪去了

UE3的AnimTree给我震撼太大了,所以特别关心UE4的动画系统。看了一下,貌似被分解成了BlendSpace和AnimGraph。
  • BlendSpace
    好比说“站立、走、跑”这三个动作,在UE3的AnimTree里面是有一个特定的node来混合的,根据移动速度不同。在UE4里,则需要创建一个BlendSpace1D资源,然后暴露出Speed参数。
  • AnimBlueprint
    使用Blueprint,AnimGraph,状态机等等控制角色的动画,怎么看上去和Unity的Mecanim有点像呢,唉~
看来AnimTree是真的不见了,很遗憾,因为我觉得那个使用树形结构来抽象的动画系统,实在是非常清晰而且强大。

渲染系统

基于物理的渲染(PBR:Physically-Based Rendering)效果真的是太NB了,Unity5虽然也是PBR,好像比UE4还是略逊一筹啊!这个无需多言了,各种DEMO视频大家都看了不少了。渲染流程也完全走延迟渲染了。但多线程渲染,SceneProxy、Material之类的基础架构没怎么变。

Behavior Tree

这个东西好像在国外的游戏AI领域这几年挺流行了,是个很高大上的东西,UE4直接做了,太好了。

另外,还有很多重大改进,例如Package,资源导入,增加插件支持等,这里就不一一细说了。推荐看一下官网的文章吧,作为本文的补充偷笑

2014-07-09 14:56:27 x_studying 阅读数 787

Unreal Engine 4前几天发布了,不得不佩服EpicGames和Tim Sweeny的霸气和远见,下面是几个关键点:

  • 授权方式默认为订阅,每月19美元+5%的毛收入,如果你还没有开始赚钱,那么就等于每月只用掏19美元就可以完整地使用UE4引擎,并能获得所有的更新
  • 如果你觉得每月19美元还贵了,那么你可以随时取消订阅,唯一的后果就是你不能再收到任何后续更新了,但你已经下载的当前版本仍然可以继续使用!
  • 当你订阅后,除了能获取所有发布的工具、文档外,你还能通过GitHub得到所有的C++源代码(包括引擎和所有的外围工具),并可通过GitHub提交自己的贡献!
  • Epic还公布了一个MarketPlace(市场),类似于Unity3D的AssetStore,未来将允许所有订阅者在其中发布、出售自己的作品(代码、艺术资源、插件等等)

要知道Unreal Engine和Epic Games在业界可不是什么二流货,迫于生存和市场压力才被迫采用上面这种激进的方式。它们说自己业界第二,就没人敢说自己是业界第一,然而这样的一个行业巨头却拿出了这样一套几乎没有门槛的授权方式,还难以置信地公开了全部的C++源代码,以一种前所未有的开放方式将自己最先进的核心产品放到了所有感兴趣的人面前,这种魄力和胆识令人震撼、钦佩。

 

可以预见的是游戏业界将由此发生不小的改变,至少Unity3D现在晚上睡觉应该不太踏实了。

 

附上其它两个主流商业引擎的默认授权方式(这里默认的前提是你要用这些引擎开发赚钱的软件,如果是开发完全免费的软件,这里提到的所有引擎都可免费使用):

Unity3D:分买断制和订阅制

  • 买断制基础费用1500美元,开发的产品只能发布到桌面平台;如果还想发布到移动平台,那么每个平台再掏1500美元。举例来说你想在Andriod和iOS上发布游戏,那么在买断方式下你要掏1500*3 = 4500美元
  • 订阅制基础费用为每月75美元,开发的产品只能发布到桌面平台;如果还想发布到移动平台,那么每个平台再掏75美元/月。举例来说你想在Andriod和iOS上发布游戏,那么在订阅方式下你要掏75*3 = 225美元/月
  • 以上两种授权方式均不包含引擎和外围工具的源代码

CryEngine: 这个引擎主要用于桌面平台和主机平台的游戏开发,不适用与移动平台

  • 在3月19日(也就是GDC2014举办的时间,UE4也是在这个会议上发布的)之前,开发商业游戏需要缴纳毛利润的25%,且不包含引擎和工具源代码
  • 在GDC2014上,Crytek宣布从今年5月份开始授权方式变为订阅制:每月9.9美元即可获取最新的SDK进行游戏开发,并可收到持续的更新
  • 如果开发者想发布商业游戏,那么授权费需要和Crytek签订NDA(保密协议)后私下“面议”

 

看了这些之后,是不是觉得UE4无论从那个方面都甩了竞争对手几条街?

UE4 已经扫清了前进 道路的所有障碍,未来游戏开发必定大势所趋。。

我们再来看看三者的对比:

1.开发难度:Unity3D使用C#或者JS语言开发游戏逻辑,对于需要快速开发的游戏来说大大减少的开发复杂度,UE和CryEngine使用C++语言,优势为速度较快,但是对于现今的游戏开发来说,简单容易使用更加有利。否则代码一旦混乱,容易陷进代码的噩梦,大大耗费开发时间。

2.画质:基本上游戏画质关键因素是美工,而不是引擎。因为主要渲染方式大同小异。个人觉得在这个方面没有可比性。就宣传来看,目前UE和CE的画质略胜U3D一筹.

总的来说,C++语言对于UE4游戏开发设置了一道门槛,如果学习Unity3D,在游戏逻辑层使用C#脚本将大大减少开发复杂度。在任何方面都无懈可击了。CE引擎个人不推荐使用,内部代码混乱,注释和文档稀少,画面颤抖问题到现在依然存在。谁用谁知道。

2015-08-06 21:37:21 pizi0475 阅读数 2144

简介

在Epic内部,我们遵循一些简单的编码标准和规则。书写本文的目的不是为了进行相关讨论或者将其作为一项正在进行中的工作,而是为了反映Epic 的目前使用的编码规范的状态。

编码规范对于程序员来说非常重要,原因如下:

  • 一套软件 80% 的生命周期都是维护。

  • 在软件的整个生命周期中,几乎不可能一直是软件的原始作者来对其进行维护。

  • 编码规范可以改进软件的可读性,从而使得工程师可以快速并透彻地理解新的代码。在项目的整个生命周期中我们肯定会需要雇佣新的工程师及实习生,并且我们可能要将我们制作的一些新的引擎改进应用到接下来的项目中。

  • 如果我们决定将源代码公布到 mod 开发者社区,那么我们想让它通俗易懂。

  • 其中大部分编码规范实际上是交叉编译器兼容性所要求的。

类的组织结构

类的组织结构应该以阅读代码的人而不是以编写代码的人为中心。因为大部分阅读代码的人都要使用类的公共接口,所以在类中应该先声明公共接口,然后是类的私有实现。

版权声明

Epic发布时提供的任何源码文件 (.h.cpp.xaml等等) 都必须包含版权声明,将其放置在文件的第一行。版权声明的格式必须完全符合以下格式:

// 版权1998-2014 Epic Games, Inc. 版权所有。

如果没有这行声明或者格式不正确,那么CIS(持续集成系统)将会产生错误并导致集成失败。

命名规范

  • 名字中每个单词的首字母(比如类型或变量)要大写,单词之间通常不要使用下划线。比如, Health和UPrimitiveComponent是符合规范的,而lastMouseCoordinates或delta_coordinates是不符合规范的。

    • 类型名称有一个额外的大写字母前缀,以便将类型名称和变量名称区分开来。比如,FSkin是类型名称,而Skin是FSkin的一个实例。

    • 模板类以T为前缀。

    • 继承UObject的类以U为前缀。

    • 继承AAtor的类以A为前缀。

    • 继承SWidget 的类以S为前缀。

    • 抽象接口类以 I为前缀。

    • 大部分其他类都以F为前缀, 但某些子系统使用其他字母。

    • 类型和变量名称必须是名词。

    • 方法名称是动词,该动词描述了该方法的作用或者描述了无效的方法返回值。

变量、方法及类的名称应该清晰、明确且具有描述性。名称的作用域越大,取一个符合标准的具有描述性的名称的重要性便越强。避免过度缩写。

所有变量都应该一次仅声明一个,以便可以提供有关这个变量的含义的注释。同时,这也符合JavaDocs 风格的要求。您可以在变量前面使用多行或单行注释,可以留一个空行来给变量分类。

所有返回布尔值的函数都应该询问返回值是真还是假这个问题,比如 "IsVisible()" 或 "ShouldClearBuffer()" 。所有布尔变量都必须以"b"字母为前缀(比如 "bPendingDestruction" 或 "bHasFadedIn")。

过程(没有返回值的函数)命名时应该使用一个强动词后面加一个对象。如果方法的对象正是该方法所属的对象,那么方法将根据上下情境来获得对象,这是种例外情况。命名时要避免使用"Handle"和"Process"开头,这些动词表达的意思模糊不清。

如果一个参数是通过引用传入函数且该函数要向此参数写入值,那么我们鼓励您使用 "Out" 作为函数参数名称的前缀,但这不是强制要求。这样便显而易见地表示出传入到该参数中的值会被该函数所代替。

对于能够返回值的函数的名称,应该描述出该返回值的意思;名称应该可以清楚地表达出要返回的是什么值。这对于布尔函数尤为重要。考虑以下两个示例方法:

    bool CheckTea(FTea Tea) {...} // true 代表什么意思?
    bool IsTeaFresh(FTea Tea) {...} // 名称可以明确表示true代表茶是新鲜的

示例

    /** 茶重量(单位:千克)*/
    float TeaWeight;

    /** 茶叶数量 */
    int32 TeaCount;

    /** true 表示茶叶很香 */
    bool bDoesTeaStink;

    /** 茶叶的非人类可读 FName */
    FName TeaName;

    /** 茶叶的人类可读名称 */
    FString TeaFriendlyName;

    /** 要使用的是茶叶的哪一个类 */
    UClass* TeaClass;

    /** 倒茶的声音 */
    USoundCue* TeaSound;

    /** 茶叶的图片 */
    UTexture* TeaTexture;

基本C++数据类型的可移植别名

  • bool代表布尔值 (永远不要假设布尔值的大小) 。BOOL 将不会进行编译。

  • TCHAR 代表字符型(永远不要假设TCHAR的大小)。

  • uint8代表无符号字节(占1个字节)。

  • int8代表有符号的字节(占1个字节)。

  • uint16 代表无符号"短整型" (占2 个字节)。

  • int16代表有符号"短整型" (占2 个字节)。

  • uint32 代表无符号整型(占4字节)。

  • int32代表带符号整型(占4字节)。

  • uint64代表无符号"四字" (8个字节)。

  • int64 代表有符号"四字"(8个字节)。

  • float代表单精度浮点型 (占4 个字节)。

  • double代表双精度浮点型 (占8 个字节)。

  • PTRINT代表可以存放一个指针的整型 (永远不要假设PTRINT的大小)。

请不要在可移植代码中使用C++整型,因为需要根据编译器决定这种数据类型的大小。

注释

注释是一种信息交流;而这种信息交流 至关重要 。关于注释,需要注意以下几点(节选自 Kernighan & Pike 的 编程实践 ):

指南

  • 编写具备良好可读性的代码:

    // 不好:
    t = s + l + b;
    
    // 好:
    TotalLeaves = SmallLeaves + LargeLeaves - SmallAndLargeLeaves;
  • 编写有用的注释:

    // 不好:
    // iLeaves进行自加运算
    ++Leaves;
    
    // 好:
    // 我们知道有另一片茶叶
    ++Leaves;
  • 不要给可读性不好的代码添加注释 - 重新编写这段代码:

    // 不好:
    // 茶叶的总量是
    // 大茶叶和小茶叶的总量之和减去
    // 既包含大茶叶又包含小茶叶的茶叶量
    t = s + l + b;
    
    // 好:
    TotalLeaves = SmallLeaves + LargeLeaves - SmallAndLargeLeaves;
  • 编写的注释不要和代码相冲突:

    // 不好:
    // 永远不要让iLeaves进行自加运算!
    ++Leaves;
    
    // 好:
    // 我们知道有另一片茶叶
    ++Leaves;

示例格式

我们使用基于JavaDoc 的系统来自动地从代码中提取注释并构建成文档,所以这就要求遵循一些特定的注释格式规则。

以下示例展示了类、状态、方法和变量的注释格式。请记住注释应该是辅助加强代码的。代码是功能实现,注释表明了代码的目的。请确保在您更改一段代码意图时更新注释。

注意,正如下面Steep和Sweeten方法所呈现的,我们支持两种不同的参数注释风格。Steep所应用的@param风格是传统风格,但对于简单的函数来说,把参数介绍集成到函数的描述性注释中会使其看上去更加清晰,正如Sweeten 中的注释所示。

和UE3编码规范不同,UE4中应该仅包含一次方法注释,即在公开声明方法的地方提供该注释。方法注释应该仅包含和方法的函数调用相关的信息,包括可能和该函数调用相关的方法重载的任何信息。关于那些和函数调用无关的方法及其重载方法的实现细节,应该在方法实现内部进行注释。

    /** 可饮用对象的接口。*/
    class IDrinkable
    {
    public:

        /**
         * 当玩家饮用该对象时调用。
         * @param OutFocusMultiplier - 返回时,将包含乘数以应用于饮用者。
         * @param OutThirstQuenchingFraction - 返回时,将包含饮用者的口渴值的分数值归0 (0-1)。
         */
        virtual void Drink(float& OutFocusMultiplier,float& OutThirstQuenchingFraction) = 0;
    };

    /** 一杯茶 (茶) */
    class FTea : public IDrinkable
    {
    public:

        /**
         * 根据浸泡茶的水的体积和水温来计算茶叶的变换口味值。
         * @param VolumeOfWater - 以毫升计算的用于酿造的水量
         * @param TemperatureOfWater - 以开氏度计算的水温
         * @param OutNewPotency - 在浸泡开始后的茶叶效能,从0.97到1.04
         * @return    会返回每分钟茶叶口味单位值 (TTU) 中茶叶浓度的改变
         */
        float Steep(
            float VolumeOfWater,
            float TemperatureOfWater,
            float& OutNewPotency
            );

        /** 对茶叶添加甜味剂,根据产生相同甜度的蔗糖的量来作为数量,以克计算。*/
        void Sweeten(float EquivalentGramsOfSucrose);

        /**在日本出售的茶叶的价格,以日元为单位。*/
        float GetPrice() const
        {
            return Price;
        }

        // IDrinkable接口
        virtual void Drink(float& OutFocusMultiplier,float& OutThirstQuenchingFraction);

    private:

        /** 以日元计算的茶叶的价值。*/
        float Price;

        /** 茶叶的甜味,根据产生相同甜度的蔗糖的量来作为数量,以克计算。*/
        float Sweetness;
    };

    float FTea::Steep(float VolumeOfWater,float TemperatureOfWater,float& OutNewPotency)
    {
        ...
    }

    void FTea::Sweeten(float EquivalentGramsOfSucrose)
    {
        ...
    }

    void FTea::Drink(float& OutFocusMultiplier,float& OutThirstQuenchingFraction)
    {
        ...
    }

类注释中都包括哪些内容?

  • 这个类解决的问题的说明。为什么创建这个类?

方法注释的所有组成部分的意思?

  • 首先是函数的作用;这记录了 函数所解决的问题 。正如上面所说的,注释表明 意图 ,而代码记录 实现 。

  • 然后是参数注释;每个参数注释应该包含了度量单位、期望值的范围、不符合要求的值、及 状态/错误 代码的意思。

  • 然后是返回值注释,它说明了期望的返回值的信息,注释方法和给输出变量添加注释一样。

C++ 11和现代语法

虚幻引擎可大量移植到许多C++编译器中,所以我们对于与支持的编译器相兼容功能的使用非常谨慎。有时一些功能相当好用,我们会把它们放在宏中并大量使用它们,但一般我们会等到对所有可能的编译器的支持达到最新标准才开始使用。

我们正在使用特定的对现代编译器进行良好支持的C++ 11语言功能,诸如"auto"关键字,range-based-for以及 lambdas。在一些情况下,我们能够以预处理器条件句的形式对这些功能进行打包(诸如容器中的rvalue引用)。但是,对一些特定的语法功能,我们可能需要尽力避免,除非我们确信这个语法在新平台上可以被识别。

除非是下方特别指出的受支持的现代C++编译器功能,否则您应尽量不使用编译器的特定语法功能,除非它们被封装在预处理器宏或条件句中,并且被保守使用。

原有宏的新关键字

原有的宏'checkAtCompileTime', 'OVERRIDE', 'FINAL'和'NULL'现在可以替换为'static_assert', 'override', 'final' 和 'nullptr'。一些宏可能仍保留定义,因为它们的使用仍较为广泛。

其中的一个例外情况是C++/CX版本中的nullptr(例如Xbox One)实际上是管理的无效引用类型。除了类型和一些模板实例关联外,它与原生C++的nullptr最为兼容,因此出于兼容性的考量,您应该使用TYPE_OF_NULLPTR宏而不是更为常见的decltype(nullptr)。

'auto'关键字

所有的编译器虚幻引擎4对象都支持'auto'关键字,我们建议您在您代码中的合适位置使用。

正如您使用类型名称那样,您可以且应该使用带auto的常量或* 。使用auto关键字将会强制将推测类型变为您想要使用的类型。

我们建议将auto关键字用于迭代器循环(消除样板) - 或更佳: ranged-based for loops - 以及在您初始化变量到新建实例时(消除多余的类型名称)。一些使用更为持续,但您可以随意使用,并且我们也会不断学习并改进其应用。

技巧: 如果您将鼠标悬停于Visual Studio中的变量上,一般它会告知您推测类型。

Range Based For

可以在所有的引擎和编辑器代码中使用,并且我们建议将其用于更容易了解和维护代码的位置。在迁移使用原有TMap迭代器的代码时,请注意原有的作为迭代器类型方式的Key()和Value()函数现在成为底层键值TPair的简单键值域:

    TMap<FString, int32> MyMap;

    // 原有样式
    for (auto It = MyMap.CreateIterator(); It; ++It)
    {
        UE_LOG(LogCategory, Log, TEXT("Key: %s, Value: %d"), It.Key(), *It.Value());
    }

    // 新建样式
    for (auto& Kvp : MyMap)
    {
        UE_LOG(LogCategory, Log, TEXT("Key: %s, Value: %d"), Kvp.Key, *Kvp.Value);
    }

对于单独的迭代器类型,我们也有范围替换:

    // 原有样式
    for (TFieldIterator<UProperty> PropertyIt(InStruct, EFieldIteratorFlags::IncludeSuper); PropertyIt; ++PropertyIt)
    {
        UProperty* Property = *PropertyIt;
        UE_LOG(LogCategory, Log, TEXT("Property name: %s"), *Property->GetName());
    }

    // 新建样式
    for (UProperty* Property : TFieldRange<UProperty>(InStruct, EFieldIteratorFlags::IncludeSuper))
    {
        UE_LOG(LogCategory, Log, TEXT("Property name: %s"), *Property->GetName());
    }

Lambdas以及匿名函数

现在允许使用Lambdas,但是对于捕获堆栈变量的状态lambdas的使用我们要谨慎 --目前我们仍在学习哪些使用是恰当的。另外,状态lambdas无法指派给我们使用很多的函数指针。

Lambdas的最佳用法是与通用算法中的断言一起使用,举例来说:

    // 搜寻首个名称包含文字"Hello"的对象
    Thing* HelloThing = ArrayOfThings.FindByPredicate([](const Thing& Th){ return Th.GetName().Contains(TEXT("Hello")); });

    // 以名称的反向来对数组排序
    AnotherArray.Sort([](const Thing& Lhs, const Thing& Rhs){ return Lhs.GetName() > Rhs.GetName(); });

我们期望在未来建立最佳实践之后来更新本文。

强类型枚举

枚举类受所有编译器的支持,并且我们推荐将其替换原有类型的命名空间枚举,比如常规枚举和UENUMs。比如:

    // 原有枚举
    UENUM()
    namespace EThing
    {
        enum Type
        {
            Thing1,
            Thing2
        };
    }

    // 新建枚举
    UENUM()
    enum class EThing : uint8
    {
        Thing1,
        Thing2
    };

只要它们是基于uint8,它们也被作为UPROPERTYs而受到支持 - 它替换了原有的TEnumAsByte<>解决方案:

    // 原有属性
    UPROPERTY()
    TEnumAsByte<EThing::Type> MyProperty;

    // 新建属性
    UPROPERTY()
    EThing MyProperty;

作为标识使用的枚举类可以通过新建ENUM_CLASS_FLAGS(EnumType)宏来自动定义所有的按位操作符:

    enum class EFlags
    {
        Flag1 = 0x01,
        Flag2 = 0x02,
        Flag3 = 0x04
    };

    ENUM_CLASS_FLAGS(EFlags)

其中的例外情况是在'truth'关联中的标识使用 - 这是对语法的限制,并且可以使用'double bang' 操作符来变通处理:

    // 原有
    if (Flags & EFlags::Flag1)

    // 新增
    if (!!(Flags & EFlags::Flag1))

Move语句

所有的主容器类型- TArray, TMap, TSet, FString -都具有move构造函数以及move赋值运算符。在根据值来传递/返回这些类型时,它们经常会被自动使用,但可以通过使用MoveTemp来明确调用,MoveTemp等同于虚幻引擎4的std::move。

通过值来返回容器或字符串有助于表达而不会使用通常的临时拷贝。值传递以及对MoveTemp的使用的规则仍在建立中,但已经可以在代码库的一些最优化区域中找到。

第三方代码

任何时候当您修改引擎中我们使用的库的代码时,请一定要使用带//@UE4的注释标记该变更,并解释您进行该修改的原因。这样可以更加容易地将变更合并到这个库的新版本中,并且使授权用户可以轻松地找到我们已经进行的任何修改。

引擎中包含的任何第三方代码都应该使用格式化的注释进行标记,以便可以轻松地进行查找。比如:

    // @third party code - BEGIN PhysX
    #include <PhysX.h>
    // @third party code - END PhysX

    // @third party code - BEGIN MSDN SetThreadName
    // [http://msdn.microsoft.com/en-us/library/xcb2z8hs.aspx]
    // Used to set the thread name in the debugger
    ...
    //@third party code - END MSDN SetThreadName

代码格式

大括号 { }

大括号冲突是不符合规则的。Epic长期使用的一种模式是将大括号单独放在新的一行上。请继续遵守这个规则。

If - Else

if-else 语句的每个代码执行块都应该位于大括号内。这是为了防止编辑错误 - 如果没有使用大括号,某人可能会不经意地向if代码块中添加另一行代码。而这行代码并不会受到if表达式的控制,这是非常糟糕的。更糟糕的是,当条件化地编译各项时会导致if/else语句中断。所以一定要使用大括号。

    if(HaveUnrealLicense)
    {
        InsertYourGameHere();
    }
    else
    {
        CallMarkRein();
    }

多重if语句在缩进时应该让每个else if语句的缩进和首个if语句对齐,这样对读者来说结构更加清晰。

    if(TannicAcid < 10)
    {
        Log("Low Acid");
    }
    else if(TannicAcid < 100)
    {
        Log("Medium Acid");
    }
    else
    {
        Log("High Acid");
    }

Tabs(制表符)

  • 通过可执行代码块缩进代码。

  • 使用制表符而不是空格来设置每行首部的空白。设置您的制表符大小为4个字符。但是,有时候也是需要使用空格,以便无论一个制表符中包含多少个空格都可以保持代码对齐;比如,当对齐没有应用制表符字符后面的代码时。

  • 如果您正编写C#代码,那么您也要使用制表符字符而不是空格。这样做的原因是程序员经常要在C#和C++之间进行切换,所以大部分人都希望使用统一的制表符设置。Visual Studio默认在C#文件使用空格,所以当您在处理虚幻引擎代码时,您需要记住改变这个设置。

Switch 语句

除了空的case(具有相同代码的多个case)外,swittch case 语句应该明确地标注出某个case运行到下一个case。在每个case中或者包含一个break语句或者包含一个向下执行的注释。也可以使用其他的代码控制变换命令(return、continue等)。

通常都有一个default case,它包含一个break语句 - 这样可以防止某人在default语句后再添加新的case。

    switch (condition)
    {
        case 1:
            ...
            // falls through
        case 2:
            ...
            break;
        case 3:
            ...
            return;
        case 4:
        case 5:
            ...
            break;
        default:
            break;
    }

Namespaces(命名空间)

您可以在适当的地方使用命名空间来组织您的类、函数、变量,只要遵循以下规则即可。

  • 虚幻代码目前没有封装在全局命名空间中。所以您需要小心以免在全局作用域内发生冲突,尤其是当引入第三方代码的时候。

  • 不要把 "using" 声明放到全局作用域内,即使是放在.cpp文件中也不行(这会导致我们“联合的”编译系统出现问题)。

  • 可以把"using"声明放在另一个命名空间或函数体内。

  • 注意如果您把"using"声明放到一个命名空间内,那么它将继续存在于同一编译单元中其它地方出现的该命名空间中 。只要您使其保持一致即可。

  • 如果您遵守上面的规则,您可以仅在头文件中安全地使用 "using" 。

  • 注意,进行前置声明的数据类型需要在其各自的命名空间中进行声明,否则将会出现连接错误。

  • 如果您在一个命名空间中声明了很多类/数据类型,那么在其他具有全局作用域的类中使用这些数据类型可能会很难。(比如,函数签名出现在类声明中时需要使用显式命名空间。)

  • 您可以使用 "using"来把命名空间中的特定变量在您的作用域中进行别名设置(比如 using Foo::FBar), 但我们一般不会在虚幻引擎代码中进行这样的操作。

  • 我们要求C++ 枚举变量声明要[封装在一个命名空间中](#C++枚举值和命名空间作用域) ,以便枚举值名称不会位于全局作用域中。

C++ 枚举值(命名空间作用域)

  • 在虚幻引擎的代码中我们一般会在枚举类型前面加上 "E"字符作为前缀。

  • 我们要求所有枚举类型都使用命名空间来 (或空的结构体) 确定作用域。这样做的原因是在C++中枚举值的作用域和枚举类型本身的作用域一样。这样可能导致命名冲突,使得程序员必须创建奇怪的名称或者给枚举值加上前缀使它们的值保持唯一性。相反,我们通常会使用命名空间来规定新的枚举类型的作用范围。命名空间内的实际枚举类型的名称应该总是声明为 "Type" 。

  • 通过命名空间确定枚举类型作用于的示例:

    /** 定义命名空间内的枚举来完成C#-样式的枚举范围 */
    namespace EColorChannel
    {
        /** 按照此枚举的实际类型来声明EColorChannel::Type */
        enum Type
        {
            Red,
            Green,
            Blue
        };
    }
    
    /** 给定颜色通道,返回该通道的名称。*/
    FString GetNameForColorChannel(const EColorChannel::Type ColorChannel)
    {
        switch(ColorChannel)
        {
            case EColorChannel::Red:   return TEXT("Red");
            case EColorChannel::Green: return TEXT("Green");
            case EColorChannel::Blue:  return TEXT("Blue");
            default:                   return TEXT("Unknown");
        }
    }
  • 注意对于局部声明的枚举类型来说,您不能使用命名空间来规定作用范围。在这些情况中,我们选择声明一个没有成员变量的局部结构体,该结构体内仅有一个局部的枚举类型,使用该结构体来规定作用范围。

    /** 使用结构体定义本地作用的枚举*/
    class FObjectMover
    {
    public:
    
        /** 待移动的方向 */
        struct EMoveDirection
        {
            enum Type
            {
                Forward,
                Reverse,
            };
        };
    
        /** 构建具有特定移动方向的FObjectMover */
        FObjectMover( const EMoveDirection::Type Direction );
    }

物理依赖

  • 不要在任何地方都给文件名添加前缀;比如,要命名为 Scene.cpp 而不是UnScene.cpp。这样做通过减少用于消除您想要的文件的歧义性所需的字符数,使得在解决方案中使用像Workspace Whiz或Visual Assist的Open File这样的工具时更加方便。

  • 使用 #pragma once 指令来避免所有头文件中包含多个包含文件。注意现在我们所使用的所有编译器都支持 #pragma once。

    #pragma once
    
    <file contents>
  • 一般,请尝试最小化物理耦合。 如果您可以使用前置声明来代替包含一个头文件,那么请这样做。 包含头文件时要尽可能地包含具体文件;不要包含Core.h文件,而是包含Core中具有您所需定义的特定头文件。

  • 请尝试包含您直接需要的每个头文件,以便使得包含详细具体的头文件变得更加容易。 不要依赖于您包含的另一个头文件中间接地包含的头文件。 不要通过另一个头文件来包含需要的内容;而是自己包含您所需要的任何内容。

  • Modules(模块)有私有和公有源码目录。其他模块中需要的任何定义必须位于公开目录中的头文件内,但是其他内容应该位于私有目录内。注意,在旧版的虚幻模块中,这些目录可能仍然称为Src和Inc,但是这些目录同样是为了分离私有和公有代码,而不是为了从源码文件中分离头文件。

  • 不要担心把您的头文件设置为预编译头文件生成。UnrealBuildTool可以比您更好地完成这个工作。

一般风格问题

  • 最小化依赖距离 。当代码依赖于具有特定值的变量时,尝试恰好在使用该变量之前设置该变量的值。在可执行代码块的顶部初始化变量且在接下来的上百行代码中都不使用它,会导致其他人很可能会不小心地修改这个值,而没有意识到依赖关系。将变量初始化放在变量应用的上一行使得大家可以清晰地看到变量初始化的方式、及其应用的地方。

  • 在可能的情况下都尽量把方法分割成子方法。人们更喜欢先看到大局然后再继续研究感兴趣的细节,而不是从大量的细节开始然后根据细节重构出大局。同样,理解一个调用了几个具有良好可读性命名的子方法的方法比理解一个简单地包含了子方法中所有代码的等价方法更加容易。

  • 在函数声明或函数调用的地方,不要在函数名称和包含参数列表的小括号之间添加空格。

  • 解决编译器警告。 编译器警告信息表示某些内容和其期望内容不符。您可以修复编译器所抱怨的问题。如果您确实无法解决这个问题,那么使用 #pragma 禁止该警告;这是最后一种补救方法。

  • 在文件结尾留下一行空白行。 所有 .cpp 和 .h 文件应该包括一行空白行,以便可以很好地同gcc编译器协同工作。

  • 绝对不允许将 FLOAT 隐性转换成int32。 这个操作很慢,且不会在所有编译器上进行编译。相反,通常使用 appTrunc() 函数来转换为int32。这样可以确保交叉编译器兼容性并生成更快速的代码。

  • 使用保护关键字进行封装 类成员应该声明为私有的,除非它们是该类的公有接口的一部分。

  • 接口类(以 "I" 为前缀)应该是抽象类,不能包含任何成员变量。接口可以包含非纯虚函数、甚至可以包含非虚函数或静态函数,只要将它们作为内联函数实现即可。

  • 在任何可能的地方使用常量。 尤其是在引用参数和类方法中。关于常量的文档很多,它就像个编译器指令一样。

  • 调试代码一般是有用的且经过改进的, 不要将其迁入源码控制工具。 如果讲调试代码和其他代码相混合,那么会导致其他代码非常难以理解。

  • 使用中间变量来简化复杂的表达式。 如果您有一个复杂的表达式,若您将它分为几个子表达式,并在父项表达式中创建一个具有描述了子表达式意思的名称的中间变量,然后将该子表达式赋予该中间变量,这将会使得表达式更加容易理解。比如:

    if ((Blah->BlahP->WindowExists->Etc && Stuff) &&
        !(bPlayerExists && bGameStarted && bPlayerStillHasPawn &&
        IsTuesday())))
    {
        DoSomething();
    }

    应该替换为

    const bool bIsLegalWindow = Blah->BlahP->WindowExists->Etc && Stuff;
    const bool bIsPlayerDead = bPlayerExists && bGameStarted && bPlayerStillHasPawn && IsTuesday();
    if(bIsLegalWindow && !bIsPlayerDead)
    {
        DoSomething();
    }
  • 当声明重写方法时请使用virtual和OVERRIDE关键字。 当在子类中声明一个重写父类虚函数的虚函数时,您 必须 使用virtual和OVERRIDE关键字。比如:

    class A
    {
    public:
        virtual void F() {}
    };
    class B : public A
    {
    public:
        virtual void F() override;
    };

注意,由于最近才添加OVERRIDE 关键字,所以有很多现有代码还不符合这条规则,在任何方便的时候应该向该代码中添加OVERRIDE关键字。

  • 指针和引用应该仅有一个空格,该空格位于指针/引用的右侧。 这样使得可以快速地在文件中查找某种特定类型的所有指针或引用。

    请使用这种格式:
    FShaderType* Type

    而不是这些格式:

    FShaderType *Type
    FShaderType * Type

Unreal 4 术语简介

阅读数 874

Unreal Engine 4 笔记

阅读数 28

没有更多推荐了,返回首页