[{"data":1,"prerenderedAt":1953},["ShallowReactive",2],{"page-UE4-4":3,"page-count-UE4":1952},[4,855,1206,1364,1805],{"id":5,"title":6,"body":7,"date":831,"description":13,"extension":832,"meta":833,"navigation":836,"path":848,"seo":849,"stem":850,"tags":851,"__hash__":854},"blogs\u002F_legacy\u002F2018\u002F2018-10-14-ue4-network-overview.md","UE4网络底层概览",{"type":8,"value":9,"toc":789},"minimark",[10,14,17,20,24,27,31,34,41,44,47,51,54,57,60,65,68,71,74,85,88,92,95,98,104,107,121,132,136,139,142,148,151,157,160,163,166,172,175,178,181,184,187,190,193,196,202,205,208,211,223,227,230,233,236,239,245,248,251,254,257,260,263,266,269,273,276,279,282,285,288,291,294,300,303,307,310,314,317,320,326,329,333,336,341,344,348,351,354,357,360,366,369,375,378,381,384,387,391,394,397,400,403,406,409,412,415,418,422,425,430,433,437,440,444,447,450,453,457,460,463,466,469,472,475,479,482,485,488,491,495,498,504,507,513,516,519,523,526,532,535,538,544,547,550,554,557,563,566,569,575,578,581,584,587,590,593,599,602,605,608,611,614,617,620,626,629,632,636,639,642,645,649,652,655,658,661,665,668,672,675,678,681,684,687,693,696,705,709,712,718,721,727,730,733,739,742,763,766,772,775,778,785],[11,12,13],"p",{},"日常使用的话不会接触到这块，最近刚好接触到，所以在此做下整理。",[11,15,16],{},"当前UE4版本为4.20。",[11,18,19],{},"UE4的网络部分，主要有RPC和值同步两种逻辑。其中在代码上有更多的维护的是值同步，因为RPC基本上是调用的时候就发出去，可优化的成分不高。而值同步就涉及同步频率、状态差分这些的维护成本，所以会有更多的逻辑在背后。",[21,22,23],"h2",{"id":23},"网络结构",[11,25,26],{},"虚幻本身的网络结构各个层次的封装还是比较好的，基本不会出现层次之间混杂导致难以定制的情况。",[28,29,30],"h3",{"id":30},"总体结构",[11,32,33],{},"结构上，每一个Actor通过ActorChannel注册到Connection来进行网络相关的操作。",[11,35,36],{},[37,38],"img",{"alt":39,"src":40},"image","\u002Fwp-content\u002Fuploads\u002F2018\u002F10\u002Fimage_thumb.png",[11,42,43],{},"在逻辑上每一个Connection对应的是一个客户端和服务端之间的远程连接，而NetDriver向下的话就只是对网络底层操作的封装。",[11,45,46],{},"在这里，首先从最底层开始。",[28,48,50],{"id":49},"fsocket和socketsubsystem","FSocket和SocketSubSystem",[11,52,53],{},"FSocket是对网络底层的封装，对于上层需要的网络状态查询、发包、收包这些操作，全部都封装在了这里。",[11,55,56],{},"与平台相关的底层全部隐藏掉，同时可以方便实现如steam网络接入这样的接入模式。",[11,58,59],{},"一般情况下的连接实际使用的是FSocketBSD，是对底层Socket API的直接封装。",[61,62,64],"h4",{"id":63},"fsocket工厂类","FSocket工厂类",[11,66,67],{},"SocketSubSystem是对UE4网络上层可见的FSocket工厂类，上层调用时不关心FSocket的具体实现是什么。而是通过预先配置好的SocketSubsytem来直接创建FSocket的基类指针。",[11,69,70],{},"在多平台实现上，和其他的系统基本是类似的。例如在Android上，实际使用的是FSocketSubsystemAndroid，而在IOS上则是FSocketSubsystemIOS。",[11,72,73],{},"具体而言，在SocketSubsystem的Module初始化时，会使用一个被宏区分的系统对应的SubSystem",[75,76,81],"pre",{"className":77,"code":79,"language":80},[78],"language-text","\u002F\u002F Initialize the platform defined socket subsystem first\nDefaultSocketSubsystem = CreateSocketSubsystem( *this );\n","text",[82,83,79],"code",{"__ignoreMap":84},"",[11,86,87],{},"这样一来，通过ISocketSubsystem::Get获得默认的SocketSubSystem的时候就能取得对应平台的实现了。",[61,89,91],{"id":90},"ipv4和ipv6","IPv4和IPv6",[11,93,94],{},"在4.21之前向下FSocketSubsystemBSD会有Ipv6和Ipv4两个不同的实现，在IOS上继承的是FSocketSubsystemBSDIPv6，而Android的是FSocketSubsystemBSD。但是4.21将这两个协议栈合在了一起，避免了在进行具体实现和使用时的一些困扰。",[11,96,97],{},"关于IPv6和IPv4的问题，其实在Socket这一层，切换起来的难度并不大。",[99,100,101],"blockquote",{},[11,102,103],{},"IPv4 connections can be handled with the v6 API by using the v4-mapped-on-v6 address type; thus a program needs to support only this API type to support both protocols. This is handled transparently by the address handling functions in the C library.",[11,105,106],{},"IPv4到IPv6有很多过渡的协议、地址格式，但是实际如果在Socket这里用v6 API的话，只需要关注IPv4-mapped-on-IPv6就可以了：",[75,108,112],{"className":109,"code":110,"language":111,"meta":84,"style":84},"language-log shiki shiki-themes github-light-high-contrast github-dark monokai","The address notation for IPv6 is a group of 8 4-digit hexadecimalnumbers, separated with a ':'. \"::\" stands for a string of 0 bits.Special addresses are ::1 for loopback and ::FFFF:\u003CIPv4 address> for IPv4-mapped-on-IPv6.\n","log",[82,113,114],{"__ignoreMap":84},[115,116,119],"span",{"class":117,"line":118},"line",1,[115,120,110],{},[11,122,123,124,131],{},"实际测试，将API直接替换掉基本没有什么问题。主要会有麻烦的，还是一些平台相关的问题。上面的两个引用来自[",[125,126,130],"a",{"href":127,"rel":128},"http:\u002F\u002Fman7.org\u002Flinux\u002Fman-pages\u002Fman7\u002Fipv6.7.html",[129],"nofollow","Man7","]，关于IPv6 Socket API的一些其他描述也可以参看该文档。",[28,133,135],{"id":134},"netdriver和connection","NetDriver和Connection",[11,137,138],{},"UNetDriver是进行网络处理的核心，有一个比较特殊的UDemoNetDriver，是用于回放录制系统的，不会发送数据。",[11,140,141],{},"NetDriver是和UWorld绑定的，无论是客户端还是服务器只会有一个。而Connection是对应客户端和服务器的连接的。对于客户端，只会有一个连接到Server的Connection，而服务器则对应每一个客户端维护一个Connection。",[75,143,146],{"className":144,"code":145,"language":80},[78],"\u002F** Connection to the server (this net driver is a client) *\u002F\nUPROPERTY()\nclass UNetConnection* ServerConnection;\n\n\u002F** Array of connections to clients (this net driver is a host) *\u002F\nUPROPERTY()\nTArray\u003Cclass UNetConnection*> ClientConnections;\n",[82,147,145],{"__ignoreMap":84},[11,149,150],{},"NetDriver和Connection不是完全的上下层的关系，实际上这两个类在进行数据发送时使用的都是自己定义的LowLevelSend。Connection更多的意义上是标识，在这之上的连接是有既定的发送者和接收者的。",[75,152,155],{"className":153,"code":154,"language":80},[78],"void UIpConnection::LowLevelSend(void* Data, int32 CountBytes, int32 CountBits)\n\nvoid UIpNetDriver::LowLevelSend(FString Address, void* Data, int32 CountBits)\n",[82,156,154],{"__ignoreMap":84},[11,158,159],{},"在实际的网络连接中，一般情况下使用的对应类就是UIpNetDriver和UIpConnection。",[61,161,162],{"id":162},"逻辑关系",[11,164,165],{},"在更新上，Connection由NetDriver负责更新。NetDriver本身的更新由UWorld驱动",[75,167,170],{"className":168,"code":169,"language":80},[78],"\u002F** Event to gather up all net drivers and call TickDispatch at once *\u002F\nFOnNetTickEvent TickDispatchEvent;\n\n\u002F** Event to gather up all net drivers and call TickFlush at once *\u002F\nFOnNetTickEvent TickFlushEvent;\n\n\u002F** Event to gather up all net drivers and call PostTickFlush at once *\u002F\nFOnTickFlushEvent PostTickFlushEvent;\n",[82,171,169],{"__ignoreMap":84},[11,173,174],{},"NetDriver的TickDispatch负责从注册的Socket上取出数据包，并进一步分发到对应的Connection中去。在服务器上，对于不同的数据包，默认是使用客户端的IP地址来区分要分发的Connection的。如果没有找到对应的Connection，且符合开新链接的条件的话，就会建立新的连接并开始连接初始化的流程。",[11,176,177],{},"TickFlush是负责进行值复制逻辑的，会调用到ServerReplicateActors开始执行值复制。",[11,179,180],{},"PostTickFlush是在TickFlush之后执行的逻辑，主要进行一些网络发送相关的清理逻辑。",[11,182,183],{},"网络相关的Tick逻辑和TickGroup所组织的Tick逻辑是相对独立的，这三个Tick函数都是由UWorld直接进行分发的，而没有具体的TickGroup。",[11,185,186],{},"在逻辑上，TickDispatch在所有的Tick执行之前进行，而TickFlush和PostTickFlush则在所有的Tick执行之后进行执行。",[11,188,189],{},"RPC和ControlChannel上的控制信息由于有更高的实时性需求，不会经过这边的逻辑，会直接调用FlushNet进行数据发送。",[61,191,192],{"id":192},"状态维护",[11,194,195],{},"UNetConnection自己维护一组时间戳用于监视连接的状态，这些时间在InitBase中进行统一的初始化：",[75,197,200],{"className":198,"code":199,"language":80},[78],"StatUpdateTime= Driver->Time;\nLastReceiveTime= Driver->Time;\nLastReceiveRealtime= FPlatformTime::Seconds();\nLastGoodPacketRealtime= FPlatformTime::Seconds();\nLastTime= FPlatformTime::Seconds();\nLastSendTime= Driver->Time;\nLastTickTime= Driver->Time;\nLastRecvAckTime= Driver->Time;\nConnectTime= Driver->Time;\n",[82,201,199],{"__ignoreMap":84},[11,203,204],{},"Driver->Time是由NetDriver维护的帧时间，在TickDispatch中进行更新。",[11,206,207],{},"LastReceiveTime的更新发生在UNetConnection::ReceivedRawPacket调用到UNetConnection::ReceivedPacket之后，也就是每一次有效的数据包从Driver分发到Connection的时候就会进行更新。这个时间戳在UNetConnection::HandleClientPlayer中也会进行更新，这个函数是在本地对玩家登陆进行处理的。",[11,209,210],{},"相对的，LastSendTime就比较单纯，只是在UNetConnection::FlushNet中更新，但是这个时间只是发送数据包的时间，无法确认发送是否成功。LastRecvAckTime在每次收到ACK的时候进行更新，这个时间戳并用于Standby Cheat的检测。",[11,212,213,218,219,222],{},[125,214,217],{"href":215,"rel":216},"https:\u002F\u002Fwww.urbandictionary.com\u002Fdefine.php?term=standby",[129],"Standby Cheat","是Listen Server的时候的一种作弊方式，指的是联机的主机端恶意的影响网络数据收发来获得优势的行为。主要的检测逻辑在",[82,220,221],{},"UNetDriver::UpdateStandbyCheatStatus","中，检测分为三种Bad Ping、Rx(收包问题)、Tx(发包问题)。详细的逻辑没有深究，因为现在没有Listen Server的需求。",[61,224,226],{"id":225},"channel","Channel",[11,228,229],{},"在Connection之上，针对每个需要进行发送的Actor会有ActorChannel的概念。",[11,231,232],{},"Actor的值复制逻辑最终由UActorChannel来负责维护和执行，实际进行数据对比的是FObjectReplicator。",[11,234,235],{},"Connection上还会有一个特别的控制Channel，为UControlChannel，这个Channel负责对连接本身进行控制。UControlChannel的作用是对整个Connection进行控制，最主要的逻辑作用是在连接建立过程中的控制处理。所有的控制信息都被定义成NMT_Hello这样的宏。",[11,237,238],{},"如果要自定义控制信息，可以参考源码中的注释：",[75,240,243],{"className":241,"code":242,"language":80},[78],"\u002F** network control channel message types\n*\n* to add a new message type, you need to:\n* - add a DEFINE_CONTROL_CHANNEL_MESSAGE_* for the message type with the appropriate parameters to this file\n* - add IMPLEMENT_CONTROL_CHANNEL_MESSAGE for the message type to DataChannel.cpp\n* - implement the fallback behavior (eat an unparsed message) to UControlChannel::ReceivedBunch()\n*\n* @warning: modifying control channel messages breaks network compatibility (update GEngineMinNetVersion)\n*\u002F\n",[82,244,242],{"__ignoreMap":84},[11,246,247],{},"控制信息的DEFINE_CONTROL_CHANNEL_MESSAGE_* 定义都在DataChannel.h中。进一步的使用可以查看NMT_Hello这种比较简单的控制信息的处理方式。",[11,249,250],{},"不过一般情况下应该很少会用到自定义控制信息，有需要特殊控制信息的话，可以先考虑使用NMT_GameSpecific。",[11,252,253],{},"还有一种独特的UVoiceChannel，不过感觉一般不会用到。",[21,255,256],{"id":256},"连接建立",[11,258,259],{},"连接建立过程在客户端上主要由UPendingNetGame这个类进行负责，在连接确立之后，这个类就功成身退了。",[11,261,262],{},"本地试图连接远程关卡时，UEngine就会建立UPendingNetGame开始进行远程关卡的载入。UPendingNetGame负责建立NetDriver，并进行一系列的初始连接操作，在完成连接之后，UPendingNetGame会从UEngine脱离控制。",[11,264,265],{},"UPendingNetGame自身的Tick来源于UEngine::TickWorldTravel，在WorldContext上有PendingNetGame时就会更新其状态，直到操作失败或者完成操作。",[11,267,268],{},"客户端要连接到服务器，在建立起值复制和RPC发送的UConnection之前，在逻辑上主要有以下两个步骤：",[28,270,272],{"id":271},"packethandler握手","PacketHandler握手",[11,274,275],{},"PacketHandler是注册在NetDriver和NetConnection上的数据包底层处理类，一般用于与上层逻辑无关的数据包加密、握手以及其他的一些特殊处理。这个处理逻辑在4.20的时候结构还不完整，能在一些函数上看到Work in progress, don't use yet的注释。",[11,277,278],{},"在连接建立阶段，首先的操作就是对所有需要握手的PacketHandler进行握手操作。这个逻辑由PacketHandler::BeginHandshaking负责。在客户端由UPendingNetGame调用，在服务端由UIpNetDriver::TickDispatch在新建到客户端的链接时进行调用。",[11,280,281],{},"在默认的情况下，这个过程中主要就是StatelessConnectHandlerComponent。",[11,283,284],{},"StatelessConnectHandlerComponent是一个使用Cookie来防止Dos和数据重放的PacketHandler。因此，这个Component是所有PacketHandler的前置，在服务端新建连接前会首先确保这个Component完成握手才会构建UConnection并调用PacketHandler::BeginHandshaking。",[11,286,287],{},"这样就可以确保到达服务器的数据包如果没有经过StatelessConnectHandlerComponent的握手的话就不会得到进一步的处理。",[11,289,290],{},"如果操作是按照正常流程的话，服务器收到的第一个包就是由客户端发起的StatelessConnectHandlerComponent的握手包。",[11,292,293],{},"PacketHandler的操作不是完全对称的，有",[75,295,298],{"className":296,"code":297,"language":80},[78],"Handler::Mode Mode = Driver->ServerConnection != nullptr ? Handler::Mode::Client : Handler::Mode::Server;\n\n",[82,299,297],{"__ignoreMap":84},[11,301,302],{},"对其进行区分。",[28,304,306],{"id":305},"controlchannel","ControlChannel",[11,308,309],{},"在完成PacketHandler的握手之后，UConnection已经建立，由Control Channel中的控制信息来接管接下来的初始化过程。此时在服务端是由UWorld来主要负责处理控制信息的。",[61,311,313],{"id":312},"nmt_hello","NMT_Hello",[11,315,316],{},"连接过程同样由客户端发起，UPendingNetGame在PacketHandler握手完成后会发送NMT_Hello，开始连接初始化过程。",[11,318,319],{},"服务端在Connection建立时，InitRemoteConnection时会设置为等待状态：",[75,321,324],{"className":322,"code":323,"language":80},[78],"SetClientLoginState( EClientLoginState::LoggingIn );\nSetExpectedClientLoginMsgType( NMT_Hello );\n",[82,325,323],{"__ignoreMap":84},[11,327,328],{},"NMT_Hello会携带客户端的字节序、Crc之后的客户端版本以及Url中的EncryptionToken。",[61,330,332],{"id":331},"服务器响应hello","服务器响应Hello",[11,334,335],{},"服务端收到NMT_Hello之后，总共有三个步骤：",[337,338,340],"h5",{"id":339},"version-check","Version Check",[11,342,343],{},"首先会对其携带的版本数据进行校验，如果不一致的话，就会返回NMT_Upgrade，要求客户端升级之后再进行连接。",[337,345,347],{"id":346},"encryption","Encryption",[11,349,350],{},"接下来是可选的加密配置过程，如果携带空的EncryptionToken就会跳过这个步骤。",[11,352,353],{},"如果Hello携带了EncryptionToken，服务端却没有绑定OnReceivedNetworkEncryptionToken的话，就会返回NMT_Failure。",[11,355,356],{},"这个函数默认是绑在UGameInstance::ReceivedNetworkEncryptionToken上的，不过这个默认实现是直接返回失败的。",[11,358,359],{},"在客户端由一个相对的处理流程：",[75,361,364],{"className":362,"code":363,"language":80},[78],"FNetDelegates::OnReceivedNetworkEncryptionToken.BindUObject(this, &ThisClass::ReceivedNetworkEncryptionToken);\nFNetDelegates::OnReceivedNetworkEncryptionAck.BindUObject(this, &ThisClass::ReceivedNetworkEncryptionAck);\n",[82,365,363],{"__ignoreMap":84},[11,367,368],{},"最终会使用Handler上的FEncryptionComponent来对数据进行处理，是否有这个包处理组件由net.AllowEncryption这个Console来决定。加密组件在Engine.ini中指定：",[75,370,373],{"className":371,"code":372,"language":80},[78],"[PacketHandlerComponents]\nEncryptionComponent=AESHandlerComponent\nbEnableReliability=false\n",[82,374,372],{"__ignoreMap":84},[11,376,377],{},"在引擎中搜索AESHandlerComponent能看到对应的加密组件的实现，以及还有RSAKeyAESEncryptionHandlerComponent、RSAEncryptionHandlerComponent、BlockEncryptionHandlerComponent、StreamEncryptionHandlerComponent这些组件，这里由于没有使用到，不再深究。",[11,379,380],{},"在UWorld::SendChallengeControlMessage的处理中可以看到，如果服务端的加密校验失败，会返回NMT_Failure，如果成功则会发送NMT_EncryptionAck到客户端并设置加密秘钥。",[11,382,383],{},"这里的FEncryptionKeyResponse是来源于UGameInstance::ReceivedNetworkEncryptionToken的，默认实现是直接Failure。",[11,385,386],{},"NMT_Failue一般都会携带具体的错误理由，可以按图索骥。",[337,388,390],{"id":389},"challenge-client","Challenge Client",[11,392,393],{},"如果没有提供加密的Key或者通过了加密的构造流程的话，会给客户端发送NMT_Challenge。",[61,395,396],{"id":396},"登录操作",[11,398,399],{},"接下来的连接过程就比较简单了：",[11,401,402],{},"客户端收到Challenge之后会收集登录信息，然后发送NMT_Login。",[11,404,405],{},"服务器处理Login之后回复NMT_Welcome。",[11,407,408],{},"客户端收到NMT_Welcome后，会记录下其中携带的地图加载对应的关卡，并向服务器上报客户端的网速NMT_Netspeed。然后到下一个Tick，会对地图进行加载，加载成功时，会发送NMT_Join并断开UPendingNetGame与Context的关联。",[11,410,411],{},"服务器处理NMT_Join后，会建立Connection与PlayerController的关联，客户端和服务端的连接就完全建立起来了。之后的操作就是使用RPC进行了。",[61,413,414],{"id":414},"其他控制操作",[11,416,417],{},"控制信息中还有一些其他的与连接建立过程关系不大的类型，在这里也稍微记录一下：",[337,419,421],{"id":420},"nmt_netguidassign","NMT_NetGUIDAssign",[11,423,424],{},"这个看注释似乎很有用",[99,426,427],{},[11,428,429],{},"Explicit NetworkGUID assignment. This is rare and only happens if a netguid is only serialized client->server (this msg goes server->client to tell client what ID to use in that case)",[11,431,432],{},"不过似乎没有实际作用，所有的调用会到达ResolvePathAndAssignNetGUID，这个函数的两个实现都没有作处理，在UPackageMapClient::ResolvePathAndAssignNetGUID中甚至能看到check(0)。",[337,434,436],{"id":435},"nmt_debugtext","NMT_DebugText",[11,438,439],{},"可以让连接的另一端输出调试信息。这个是给NETDEBUGTEXT指令用的。",[337,441,443],{"id":442},"nmt_gamespecific","NMT_GameSpecific",[11,445,446],{},"会将发送的内容推送到HandleGameNetControlMessage，发送的内容为uint8和FString。用uint8来指定数据类型，FString来作为数据内容。基本能够满足需求。",[21,448,449],{"id":449},"数据收发",[11,451,452],{},"从最下往上看的话，数据在从Socket被取出后，首先会经过PacketHandler，然后到达NetDriver，在寻找到对应的Connection之后会通过ReceivedRawPacket将数据指针推送给对应的Connection。",[61,454,456],{"id":455},"packethandler","PacketHandler",[11,458,459],{},"UNetConnection::ReceivedRawPacket的处理主要集中在PacketHandler上，默认的情况下，工作的只有StatelessConnectHandlerComponent。",[11,461,462],{},"加密组件需要自己开启，也可以自己定义一些组件来在数据包一层进行处理。由于这里的是RawData，所以比较适合做加密、压缩或者校验之类的。因为在这里处理的数据还和UE4上层的网络结构没有什么关系。",[11,464,465],{},"在数据从Connection进一步发送到UE4逻辑之前，PacketHandler有两个可以处理的接入点。",[11,467,468],{},"一个是Incoming，一个是IncomingHigh。这两个处理的逻辑位置有些不同。",[11,470,471],{},"InComing是直接处理的原始数据，而InComingHigh接入的是经过了序列化之后的FBitReader。",[11,473,474],{},"经过这两个步骤之后，FBitReader会被发送到UNetConnection::ReceivedPacket。",[61,476,478],{"id":477},"bunch和channel","Bunch和Channel",[11,480,481],{},"ReceivedPacket这里会对Ack包和数据包进行区分处理。",[11,483,484],{},"Ack包会进入Ack处理逻辑。",[11,486,487],{},"数据包在作Ack处理后，被进一步的打包成FInBunch，在逻辑上这里的Bunch应当被称为RawBunch。之所以称为RawBunch，是因为UE4在数据包发送的时候会对上层过来的Bunch做分包处理。",[11,489,490],{},"RawBunch打包完成时，就已经获取了数据包对应的Channel，之后就会发送到UChannel::ReceivedRawBunch。",[61,492,494],{"id":493},"receivedrawbunch","ReceivedRawBunch",[11,496,497],{},"由于网络收发的不确定性，这里会有Reliable的RawBunch的第一道关卡，对于到达的数据包的序列号不符合预期的情况，数据包会被缓存到",[75,499,502],{"className":500,"code":501,"language":80},[78],"class FInBunch*        InRec;\n",[82,503,501],{"__ignoreMap":84},[11,505,506],{},"这个缓存是有大小限制的",[75,508,511],{"className":509,"code":510,"language":80},[78],"enum { RELIABLE_BUFFER = 256 }; \u002F\u002F Power of 2 >= 1.\n",[82,512,510],{"__ignoreMap":84},[11,514,515],{},"如果出现奇怪的丢包问题可以关注下Log的输出~",[11,517,518],{},"对于到达的有序RawBunch，则进入下一步处理：UChannel::ReceivedNextBunch",[61,520,522],{"id":521},"receivednextbunch","ReceivedNextBunch",[11,524,525],{},"到达了这里主要做的操作是对RawBunc进行合并，合并依赖的是以下标志位：",[75,527,530],{"className":528,"code":529,"language":80},[78],"uint8   bPartial;           \u002F\u002F Not a complete bunch\nuint8   bPartialInitial;    \u002F\u002F The first bunch of a partial bunch\nuint8   bPartialFinal;      \u002F\u002F The final bunch of a partial bunch\n",[82,531,529],{"__ignoreMap":84},[11,533,534],{},"bPartialFinal为合并终止包。OutBunch和InBunch分开定义，VA会搜索不到。",[11,536,537],{},"合包时会有一个缓存，防止剩余包未到达的情况：",[75,539,542],{"className":540,"code":541,"language":80},[78],"class FInBunch*        InPartialBunch;        \u002F\u002F Partial bunch we are receiving (incoming partial bunches are appended to this)\n",[82,543,541],{"__ignoreMap":84},[11,545,546],{},"完成包合并后的Bunch会被交由UChannel::ReceivedSequencedBunch处理。",[11,548,549],{},"UChannel::ReceivedSequencedBunch则进一步调用到Channel的虚函数ReceivedBunch。",[61,551,553],{"id":552},"receivedbunch","ReceivedBunch",[11,555,556],{},"ActorChannel在得到Bunch之后进一步通过ProcessBunch将处理交到FObjectReplicator::ReceivedBunch中，在ReadFieldHeaderAndPayload解析之后，根据Filed信息来做更多的操作。",[75,558,561],{"className":559,"code":560,"language":80},[78],"\u002F\u002F Handle property\nif ( UProperty* ReplicatedProp = Cast\u003C UProperty >( FieldCache->Field ) ){...}\n\u002F\u002F Handle function call\nelse if ( Cast\u003C UFunction >( FieldCache->Field ) ){...}\nelse\n{\nUE_LOG( LogRep, Error, TEXT( \"ReceivedBunch: Invalid replicated field %i in %s\" ), FieldCache->FieldNetIndex, *Object->GetFullName() );\nreturn false;\n}\n",[82,562,560],{"__ignoreMap":84},[11,564,565],{},"这样，数据包就到达了UE4中的RPC和属性同步处理逻辑了。",[11,567,568],{},"在ProcessBunch中还有一个FNetworkGUID相关的逻辑，这个是一开始Actor初始化网络连接时会用到的处理流程。需要进一步了解的话，可以参考这个函数：",[75,570,573],{"className":571,"code":572,"language":80},[78],"\u002F**\n*Standard method of serializing a new actor.\n*For static actors, this will just be a single call to SerializeObject, since they can be referenced by their path name.\n*For dynamic actors, first the actor's reference is serialized but will not resolve on clients since they haven't spawned the actor yet.\n*The actor archetype is then serialized along with the starting location, rotation, and velocity.\n*After reading this information, the client spawns this actor in the NetDriver's World and assigns it the NetGUID it read at the top of the function.\n*\n*returns true if a new actor was spawned. false means an existing actor was found for the netguid.\n*\u002F\nbool UPackageMapClient::SerializeNewActor(FArchive& Ar, class UActorChannel *Channel, class AActor*& Actor)\n",[82,574,572],{"__ignoreMap":84},[61,576,577],{"id":577},"数据发送",[11,579,580],{},"发送数据的过程基本上和接收数据的过程相反。不过在分包之后到发送RawBunch的过程更为简单一些。因为这里不会有序列的问题，只有在RawBunch发送之后会有一个Ack维护和重发的逻辑。",[11,582,583],{},"与InRec对应的缓存为OutRec，同样会应用缓存数量上限。",[11,585,586],{},"RPC会在发送时不会等待TickFlush，而是会直接调用Channel的SendBunch并立即调用FlushNet。只有两种情况例外，一种是函数本身被标记为ForceQueue的，另一种是非Reliable的多播函数。",[11,588,589],{},"多播函数官方并不建议使用Reliable，因为会导致相距很远、已经休眠的Channel重新被打开并在发送完毕之后又被关闭。",[11,591,592],{},"对于不立即调用的RPC，会被缓存并在之后走值复制的逻辑进行发送。",[75,594,597],{"className":595,"code":596,"language":80},[78],"FObjectReplicator::QueueRemoteFunctionBunch\n\nPackageMapClient->AppendExportBunches( OwningChannel->QueuedExportBunches );\n",[82,598,596],{"__ignoreMap":84},[11,600,601],{},"RPC的发送还涉及将参数进行NetSerializer发送的过程，这里没有深究，如有需要可以从UNetDriver::ProcessRemoteFunctionForChannel入手。",[21,603,604],{"id":604},"值复制处理",[11,606,607],{},"值复制的数值对比部分的实际逻辑执行由FObjectReplicator::ReplicateProperties负责。",[11,609,610],{},"在ActorChannel确立之后，对于像是int、float这样的属性，只要进行简单的内存对比就可以知道有没有修改并进行发送了。",[11,612,613],{},"但是对于TArray这样的“动态”内容，就需要更多的处理。另外，当前版本的引擎是不提供TMap与TSet的值复制逻辑的。",[11,615,616],{},"在初始构造连接时的复制时或者RPC进行参数传递时会进行NetSerializer，而在之后的值复制过程中，会对值与之前的值进行对比，使用NetDeltaSerializer来判别变动了量并只对这些值进行发送。",[11,618,619],{},"当然，实际在进行的时候会有很多类和机制上的处理，但是大体上就是这个样子。想要进一步了解其执行方式的话，可以看SerializeProperties_r和CompareProperties_r。对于RPC可以参考以下函数：",[75,621,624],{"className":622,"code":623,"language":80},[78],"\u002F\u002F RPC support\nvoid InitFromFunction( UFunction * InFunction );\nvoid SendPropertiesForRPC( UObject* Object, UFunction * Function, UActorChannel * Channel, FNetBitWriter & Writer, void* Data ) const;\nvoid ReceivePropertiesForRPC( UObject* Object, UFunction * Function, UActorChannel * Channel, FNetBitReader & Reader, void* Data, TSet\u003CFNetworkGUID>& UnmappedGuids) const;\n\n\u002F\u002F Struct support\nvoid SerializePropertiesForStruct( UStruct * Struct, FArchive & Ar, UPackageMap    * Map, void* Data, bool & bHasUnmapped ) const;\nvoid InitFromStruct( UStruct * InStruct );\n\n\u002F\u002F Serializes all replicated properties of a UObject in or out of an archive (depending on what type of archive it is)\nENGINE_API void SerializeObjectReplicatedProperties(UObject* Object, FArchive & Ar) const;\n",[82,625,623],{"__ignoreMap":84},[11,627,628],{},"对于TArray，有一个专门的逻辑进行处理，主要在CompareProperties_Array_r中。会对数组进行判定，只对“改变”了的数值进行复制。如果只是简单的值修改的话，只会复制修改的部分。如果是在尾部添加和删除的话也不会有太大的数据量。但是如果是将中间的某个值删除的话，会被视为那个值之后的所有量都变动了，所以也并不是万能的。",[11,630,631],{},"因此官方还提供了一个FastArray来应对一些特殊的情况。",[28,633,635],{"id":634},"自定义netserialization","自定义NetSerialization",[11,637,638],{},"为了提供一些自定义的结构的值复制逻辑变更，NetSerialization和NetDeltaSerialization是可以自定义的。",[11,640,641],{},"NetSerialization用于将整个结构序列化，这个其实在引擎内部的使用非常多，可以通过FVector_NetQuantize来参考如何进行实现。",[11,643,644],{},"NetDeltaSerialization用于生成状态差分，这个貌似只有FastArray在用。",[28,646,648],{"id":647},"fastarray","FastArray",[11,650,651],{},"快速值复制的数组，其快速并不反应在初始的NetSerialization上，而是通过NetDeltaSerialization在进行差分对比的时候比TArray更加快速。",[11,653,654],{},"但是相对的，也会有缺点，那就是通过FastArray进行的值复制是不会保证数组在服务端和客户端之间的次序一致性的。",[11,656,657],{},"想要进一步使用FastArray同步逻辑的，可以参考NetSerialization.h中的注释，里面有一步一步的指导如何使用这个复制方法。",[11,659,660],{},"总体上，我们要做的其实只是定义一个支持这个复制方式的Struct而已，然后在发生了变动的时候，调用对应的接口就好了。引擎中有几处有使用到FastArray，参考FLobbyPlayerStateInfoArray也可以获得一些具体用法相关的知识。",[21,662,664],{"id":663},"_420的优化项","4.20的优化项",[11,666,667],{},"这两个优化项记得是当时GDC上有说的从Fortnite合入的。",[28,669,671],{"id":670},"significancemanager","SignificanceManager",[11,673,674],{},"一个单独的用来更新物体之间的相关性的类。",[11,676,677],{},"需要自己在合适的地方调用Update，而这个USignificanceManager::Update就是整个处理逻辑的核心。它会调用我们定义的优先级计算函数，之后对计算结果进行排序处理。",[11,679,680],{},"使用时需要注意的是，这个类本身并不提供性能提升，而是会对优先级进行管理，方便其他系统对性能进行调整。",[11,682,683],{},"另外，在注册到Manager的时候要留意EPostSignificanceType的传入值，可能会导致FPostSignificanceFunction的调用异步到达。而FSignificanceFunction原本就是异步的，所以不能在里面进行一些可能导致死锁的操作，或者默认假定它是单线程的。",[11,685,686],{},"从代码看",[75,688,691],{"className":689,"code":690,"language":80},[78],"SignificanceManagerClass = LoadClass\u003CUSignificanceManager>(nullptr, *GetDefault\u003CUSignificanceManager>()->SignificanceManagerClassName.ToString());\n",[82,692,690],{"__ignoreMap":84},[11,694,695],{},"这个类和UNetDriver那些一样，可以通过配置指向自己重载的基类。",[11,697,698,699,704],{},"具体的使用方法可以参考[",[125,700,703],{"href":701,"rel":702},"https:\u002F\u002Fdocs.unrealengine.com\u002Fen-us\u002FEngine\u002FPerformance\u002FSignificanceManager",[129],"官方文档","]。",[28,706,708],{"id":707},"replicationgraph","ReplicationGraph",[11,710,711],{},"这个是直接介入到UNetDriver中的优化类，要使用这个类可以通过配置来重新导向：",[75,713,716],{"className":714,"code":715,"language":80},[78],"[\u002FScript\u002FOnlineSubsystemUtils.IpNetDriver]\nReplicationDriverClassName=\"\u002FScript\u002FMyGame.MyReplicationGraph\"\n",[82,717,715],{"__ignoreMap":84},[11,719,720],{},"或者将自己的构造函数绑定到",[75,722,725],{"className":723,"code":724,"language":80},[78],"UReplicationDriver::CreateReplicationDriverDelegate()\n",[82,726,724],{"__ignoreMap":84},[11,728,729],{},"这个Delegate上。",[11,731,732],{},"这个优化的作用，官方是这样描述的",[75,734,737],{"className":735,"code":736,"language":80},[78],"UReplicationDriver is a base class that can be used for implementing custom server replication logic.\nUReplicationGraph is an implementation of UReplicationDriver that provides a replication system optimized for games with large actor and player counts.\n",[82,738,736],{"__ignoreMap":84},[11,740,741],{},"总体上而言，就是可以实现自定义的值复制逻辑。官方给出的使用示例是：",[99,743,744],{},[745,746,747,751,754,757,760],"ol",{},[748,749,750],"li",{},"将Actor通过位置聚合在一起提供更新判定，例如MMO中的独立的房间或者区域",[748,752,753],{},"对于独特的休眠物体进行分组管理，例如场景中的树，虽然只在被破坏的时候需要状态更新，却又不能降低更新频率",[748,755,756],{},"如果玩家可以推动或拾起物体，可以将它们的更新聚集到持有者上",[748,758,759],{},"可以将需要常态更新的物体聚合在一个组内，避免不必要的相关性检查",[748,761,762],{},"将对于指定Actor永远不需要和一直需要的相关性聚集在一起",[11,764,765],{},"逻辑上主要替换的是",[75,767,770],{"className":768,"code":769,"language":80},[78],"\u002F**\n* Called to replicate any relevant actors to the connections contained within this net driver\n*\n* Process as many clients as allowed given Engine.NetClientTicksPerSecond, first building a list of actors to consider for relevancy checking,\n* and then attempting to replicate each actor for each connection that it is relevant to until the connection becomes saturated.\n*\n* NetClientTicksPerSecond is used to throttle how many clients are updated each frame, hoping to avoid saturating the server's upstream bandwidth, although\n* the current solution is far from optimal.  Ideally the throttling could be based upon the server connection becoming saturated, at which point each\n* connection is reduced to priority only updates, and spread out amongst several ticks.  Also might want to investigate eliminating the redundant consider\u002Frelevancy\n* checks for Actors that were successfully replicated for some channels but not all, since that would make a decent CPU optimization.\n*\n* @param DeltaSeconds elapsed time since last call\n*\n* @return the number of actors that were replicated\n*\u002F\nENGINE_API virtual int32 ServerReplicateActors(float DeltaSeconds);\n",[82,771,769],{"__ignoreMap":84},[11,773,774],{},"也就是说替换的是值复制从NetDriver到Channel为止的判定逻辑，对于Fortnite这种据说同步物体达到50000个左右的大规模Actor的值同步很有作用。",[11,776,777],{},"默认情况下的同步数据收集是会直接对所有的Actor进行遍历的，这个逻辑相对单纯，无法进行复杂的关系设定。而ReplicationGraph引入了Node来对Actor进行关系管理，这个Node也可以自己进行定义。这样就可以通过更加复杂的逻辑来控制Actor的复制关系。尽可能的减少浪费。",[11,779,780,781,704],{},"可以通过UBasicReplicationGraph看基本的功能是如何接入到UNetDriver系统中去的，据说ShooterGame中有更加详细的实现。需要更多信息也可以参考[",[125,782,703],{"href":783,"rel":784},"https:\u002F\u002Fdocs.unrealengine.com\u002Fen-US\u002FEngine\u002FNetworking\u002FReplicationGraph",[129],[786,787,788],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html .sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html.sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}",{"title":84,"searchDepth":790,"depth":791,"links":792},2,3,[793,806,815,823,827],{"id":23,"depth":790,"text":23,"children":794},[795,796,801],{"id":30,"depth":791,"text":30},{"id":49,"depth":791,"text":50,"children":797},[798,800],{"id":63,"depth":799,"text":64},4,{"id":90,"depth":799,"text":91},{"id":134,"depth":791,"text":135,"children":802},[803,804,805],{"id":162,"depth":799,"text":162},{"id":192,"depth":799,"text":192},{"id":225,"depth":799,"text":226},{"id":256,"depth":790,"text":256,"children":807},[808,809],{"id":271,"depth":791,"text":272},{"id":305,"depth":791,"text":306,"children":810},[811,812,813,814],{"id":312,"depth":799,"text":313},{"id":331,"depth":799,"text":332},{"id":396,"depth":799,"text":396},{"id":414,"depth":799,"text":414},{"id":449,"depth":790,"text":449,"children":816},[817,818,819,820,821,822],{"id":455,"depth":799,"text":456},{"id":477,"depth":799,"text":478},{"id":493,"depth":799,"text":494},{"id":521,"depth":799,"text":522},{"id":552,"depth":799,"text":553},{"id":577,"depth":799,"text":577},{"id":604,"depth":790,"text":604,"children":824},[825,826],{"id":634,"depth":791,"text":635},{"id":647,"depth":791,"text":648},{"id":663,"depth":790,"text":664,"children":828},[829,830],{"id":670,"depth":791,"text":671},{"id":707,"depth":791,"text":708},"2018-10-14","md",{"layout":834,"status":835,"published":836,"author":837,"author_login":839,"author_email":840,"wordpress_id":841,"wordpress_url":842,"date_gmt":843,"excerpt":844},"post","publish",true,{"display_name":838,"login":839,"email":840,"url":84},"风铃","flinkor","flinkor@foxmail.com",2600,"\u002F?p=2600","2018-10-14 12:47:27 +0000",{"type":8,"value":845},[846],[11,847,13],{},"\u002F2018-10-14-ue4-network-overview",{"title":6,"description":13},"_legacy\u002F2018\u002F2018-10-14-ue4-network-overview",[852,853],"UE4","NetWork","_ff1bmp4Ic9JuvoAnCa3tHweXg2UkFvjKZx9rib05WM",{"id":856,"title":857,"body":858,"date":1190,"description":862,"extension":832,"meta":1191,"navigation":836,"path":1200,"seo":1201,"stem":1202,"tags":1203,"__hash__":1205},"blogs\u002F_legacy\u002F2018\u002F2018-09-15-precise-timestamp.md","精确时间戳整理",{"type":8,"value":859,"toc":1174},[860,863,866,869,877,880,883,888,891,894,897,900,904,907,916,920,923,932,938,941,955,963,966,970,973,982,985,990,995,998,1003,1006,1009,1013,1016,1019,1022,1028,1031,1034,1043,1046,1049,1052,1056,1059,1062,1065,1068,1073,1076,1081,1084,1087,1090,1093,1096,1099,1114,1118,1121,1125,1128,1131,1137,1140,1144,1147,1153,1156,1162,1165,1168,1171],[11,861,862],{},"当需要进行类似定时抢购或者用户操作验证时，就需要获得用户的精确时间戳。",[11,864,865],{},"当前使用的UE4版本为4.20。",[11,867,868],{},"首先，对于时间戳，这里需要的是满足这两个条件：",[745,870,871,874],{},[748,872,873],{},"它是自然增加的，不会因为正常操作回转",[748,875,876],{},"它与现实时间具有同等的流速，不会突然跳变。",[11,878,879],{},"基本上就是通常意义上的MONOTONIC的时钟了，不过这个词的具体意义我也不是很明确就是了~",[11,881,882],{},"从wiki上的解释来看：",[99,884,885],{},[11,886,887],{},"In mathematics, a monotonic function(or monotone function) is a function between ordered sets that preserves or reverses the given order. This concept first arose in calculus, and was later generalized to the more abstract setting of order theory.",[11,889,890],{},"Monotonic似乎只有保持单调上升的定义，不过这里还是借用下吧。",[21,892,893],{"id":893},"时间戳的获取",[11,895,896],{},"如果要使用上面的两个条件的话，关卡时间记录的TimeSeconds是不行的，因为它在游戏暂停的时候是不增加的。而RealTimeSeconds，虽然其间接来源是FPlatformTime::Seconds，但是中间会有演算的精度丢失，要使用的话不如直接使用FPlatformTime::Seconds。",[11,898,899],{},"FPlatformTime::Seconds在大部分情况下都是可用的，但是它有不同的平台实现，在实际使用时还是会遇到平台相关问题。",[28,901,903],{"id":902},"windows","Windows",[11,905,906],{},"在Windows平台上，FPlatformTime::Seconds使用的是QueryPerformanceCounter。这是一个硬件时钟，不会受到用户时间调整、系统挂起等各种操作的影响，Windows上还可以使用TSC来获取CPU时钟，这个时钟微软并不推荐使用，因为它会受到CPU频率动态调整等硬件调节的影响。",[11,908,909,910,915],{},"详细的说明可以参照微软的官方说明：",[125,911,914],{"href":912,"rel":913},"https:\u002F\u002Fdocs.microsoft.com\u002Fen-us\u002Fwindows\u002Fdesktop\u002Fsysinfo\u002Facquiring-high-resolution-time-stamps",[129],"Acquiring high-resolution time stamps","。",[28,917,919],{"id":918},"linux","Linux",[11,921,922],{},"在Linux平台上有一个通用的时间获取函数clock_gettime，可以获得各种类型的时钟值。",[11,924,925,926,931],{},"这个函数的具体用法可以参考",[125,927,930],{"href":928,"rel":929},"http:\u002F\u002Fman7.org\u002Flinux\u002Fman-pages\u002Fman2\u002Fclock_gettime.2.html",[129],"clock_gettime","的文档，UE4在使用时会首先对各个类型的时钟进行一次效率对比，因为在不同的Linux内核版本上可能会有性能的差异，或者是没有实现的时钟出现。",[75,933,936],{"className":934,"code":935,"language":80},[78],"{ CLOCK_REALTIME, \"CLOCK_REALTIME\", 0 },\n{ CLOCK_MONOTONIC, \"CLOCK_MONOTONIC\", 0 },\n{ CLOCK_MONOTONIC_RAW, \"CLOCK_MONOTONIC_RAW\", 0 },\n{ CLOCK_MONOTONIC_COARSE, \"CLOCK_MONOTONIC_COARSE\", 0 }\n\n",[82,937,935],{"__ignoreMap":84},[11,939,940],{},"这四种时钟的不同在文档中有描述，这里作一个简要的整理：",[99,942,943,946,949,952],{},[11,944,945],{},"CLOCK_REALTIME: 这个时钟获取的是时钟时间，会受到系统时间调整和NTP的影响。",[11,947,948],{},"CLOCK_MONOTONIC: 不会受到系统时间调整影响的时钟，记录的是从一个未指定的起始时间到现在的时间，但会受adjtime和NTP的影响。",[11,950,951],{},"CLOCK_MONOTONIC_RAW: 比MONOTONIC更加严格，甚至不会受到adjtime和NTP的影响。2.6.28内核以上才有。",[11,953,954],{},"CLOCK_MONOTONIC_COARSE: 一个更快速和低精确度的MONOTONIC时钟，需要系统架构支持。2.6.32内核以上才有。",[11,956,957,958,704],{},"这里有个问题，就是NTP。由于对Linux不是很熟悉，所以对NTP很为困惑。NTP简单的来看就是联网对时机制，从结果上看最终与adjtime一样，并不会导致CLOCK_MONOTONIC回转，只会让这个时钟变得更加接近世界时间，会造成时间计算的频率发生变化计让算结果出现一些小的不连续。而CLOCK_MONOTONIC_RAW则更接近于硬件时间，更加适合于硬件相关的监测或者线程同步。详细的说明可以参考[",[125,959,962],{"href":960,"rel":961},"https:\u002F\u002Fstackoverflow.com\u002Fa\u002F14270415",[129],"这里",[11,964,965],{},"结论上看，就是CLOCK_MONOTONIC基本就够用了。",[28,967,969],{"id":968},"android","Android",[11,971,972],{},"由于Android的内核就是Linux的，所以使用的也是clock_gettime，不过这里并没有作时钟性能的比较，而是直接取用的CLOCK_MONOTONIC。大概是因为Android所基于的Linux内核版本是确定的吧。",[11,974,975,976,981],{},"Android在JavaApi一层是有提供elapsedRealtime函数来获取系统启动至今的时间的。从Android的源码[",[125,977,980],{"href":978,"rel":979},"https:\u002F\u002Fandroid.googlesource.com\u002Fplatform\u002Fsystem\u002Fcore\u002F+log\u002Fmaster\u002Flibutils\u002FSystemClock.cpp",[129],"变更记录","]上来看，这个API的实现经历过不少的变更。",[11,983,984],{},"最后采用的是使用clock_gettime的CLOCK_BOOTTIME来实现的，在过去的时代，Linux内核似乎有过bug，导致通过获得的CLOCK_BOOTTIME有问题。而在这个提交之后：",[11,986,987],{},[37,988],{"alt":39,"src":989},"\u002Fwp-content\u002Fuploads\u002F2018\u002F09\u002Fimage_thumb-4.png",[11,991,992],{},[37,993],{"alt":39,"src":994},"\u002Fwp-content\u002Fuploads\u002F2018\u002F09\u002Fimage_thumb-5.png",[11,996,997],{},"又开始重新回归了CLOCK_BOOTTIME，由于这个变更过程，所以不是很确定clock_gettime在旧版本Android上究竟会受到怎样的影响。从Android本身的变更上看：",[11,999,1000],{},[37,1001],{"alt":39,"src":1002},"\u002Fwp-content\u002Fuploads\u002F2018\u002F09\u002Fimage_thumb-6.png",[11,1004,1005],{},"由于UE4对Android的API Level是从18还是19开始的，Linux内核已经到了3.0以上，应当对用于时间戳的CLOCK_MONOTONIC不会产生影响才对。如果在实际中遇到问题的话，就只能依赖于AndroidJavaEnv::GetJavaEnv来获取JavaAPI了。",[11,1007,1008],{},"在Android上使用FPlatformTime::Seconds有一个需要注意的问题，Androd平台使用clock_gettime的实现是4.20之后的版本才有的！如果你工作在4.20之前的版本的话，FPlatformTime::Seconds的实现是使用的默认的FGenericPlatformTime，只是单纯的将系统当前时间转换为秒数。",[28,1010,1012],{"id":1011},"ios","IOS",[11,1014,1015],{},"在IOS10之后，IOS有将clock_gettime从内核暴露出来，所以如果是针对之后的版本的话，是没有问题的。但是在IOS10之前是没有这个函数的，所以UE4在TimeSeconds中使用的是mach_absolute_time。",[11,1017,1018],{},"这个时钟有一个问题，那就是它会随着IOS锁屏而停止。",[11,1020,1021],{},"因此在IOS上如果想要获取一个MONOTONIC的时钟，实际上需要使用一个Trick：",[75,1023,1026],{"className":1024,"code":1025,"language":80},[78],"#include \u003Csys\u002Fsysctl.h>\nstatic int64_t us_since_boot() {\n  struct timeval boottime;\n  int mib[2] = {CTL_KERN, KERN_BOOTTIME};\n  size_t size = sizeof(boottime);\n  int rc = sysctl(mib, 2, &boottime, &size, NULL, 0);\n  if (rc != 0) {\n    return 0;\n  }\n  return (int64_t)boottime.tv_sec * 1000000 + (int64_t)boottime.tv_usec;\n}\n\n- (int64_t)us_uptime\n{\n  int64_t before_now;\n  int64_t after_now;\n  struct timeval now;\n  after_now = us_since_boot();\n  do {\n    before_now = after_now;\n    gettimeofday(&now, NULL);\n    after_now = us_since_boot();\n  } while (after_now != before_now);\n  return (int64_t)now.tv_sec * 1000000 + (int64_t)now.tv_usec - before_now;\n}\n",[82,1027,1025],{"__ignoreMap":84},[11,1029,1030],{},"因为IOS的系统启动时间和当前时间记录都是会受系统时间变更影响的，所以它们的差值就能代表固定的时间流逝值。",[11,1032,1033],{},"上面的while是为了防止在两次时间获取之间发生时间变动而设定的，否则有可能在极其偶然的情况下导致获得的时间出现问题。",[11,1035,1036,1037,1042],{},"这个Trick来源于[",[125,1038,1041],{"href":1039,"rel":1040},"https:\u002F\u002Fstackoverflow.com\u002Fa\u002F40497811",[129],"stackoverflow","]，从大家评论的验证结果上看，并没有什么问题出现。",[21,1044,1045],{"id":1045},"浮点数精度问题",[11,1047,1048],{},"在上面的实现中，我们获得的时间戳都是由uint64的秒数单位构成然后转化到double空间的。",[11,1050,1051],{},"这里就会有一个传统的问题，那就是浮点精度溢出。在结论上，如果使用double来作为秒数的时间戳，且时间戳是基于开机的UpTime的话，是不需要处理浮点数精度问题的。",[28,1053,1055],{"id":1054},"ieee754","IEEE754",[11,1057,1058],{},"现代计算机对浮点数的存储基本遵循一个统一的标准，这是算法之前的事情，这里面有着许多关于数学和硬件实现的相关问题，不过已经离我们这样的程序员很远了。",[11,1060,1061],{},"简单的讲，计算机对浮点数的存储采用的是科学计数法。",[11,1063,1064],{},"实际存储的格式为：",[11,1066,1067],{},"double",[11,1069,1070],{},[37,1071],{"alt":39,"src":1072},"\u002Fwp-content\u002Fuploads\u002F2018\u002F09\u002Fimage_thumb-7.png",[11,1074,1075],{},"float",[11,1077,1078],{},[37,1079],{"alt":39,"src":1080},"\u002Fwp-content\u002Fuploads\u002F2018\u002F09\u002Fimage_thumb-8.png",[11,1082,1083],{},"上面两张图来自维基百科~",[11,1085,1086],{},"抛开实际进行浮点数表示的复杂实现的细节进行粗略的估算的话，在小数点位数为0位时，我们能从dobule中获得的精确的自然数与uint52相当，从float中能获得的则为uint23，由于float自带符号位，所以范围可以再乘以2。",[11,1088,1089],{},"那么把同等位数的int转化到同等位数的浮点数的话，一定是会引起精度丢失的。",[11,1091,1092],{},"简化的来看float能够表示的连续的正整数应该在2的23次方左右，也就是大概到8388608，要保留至少两位小数的话，就是83886。将这个作为秒数来计算，大概为23.3个小时。当然，实际的浮点数表示肯定能够容纳更多的数值空间。但是至少说明，在以天为单位的情况下，float用来表示秒数是有丢失精度的危险的。",[11,1094,1095],{},"由于服务器可能在20fps左右进行tick，那么单帧间隔在0.05秒左右，如果不能保证这个精度被表示到的话，很有可能造成运算混乱。",[11,1097,1098],{},"而如果是double的时间戳的话，使用这样的简化推算的话，两位数的精度可以维持521249956.8715天，也就是说能够在大概100万年内都不需要担心精度问题。由于时间戳使用的是system up time，所以如果真的会出问题的话，我们应该担心的是其他的方面~",[11,1100,1101,1102,1107,1108,1113],{},"不过这个推算只是一个大致的直观估计，想要更详细的了解这个主题的话，可以参考[",[125,1103,1106],{"href":1104,"rel":1105},"http:\u002F\u002Fmath.ecnu.edu.cn\u002F~jypan\u002FTeaching\u002Fbooks\u002Fsun_nc.pdf",[129],"Sun Studio 11: 數值計算指南","]中附录D对《",[125,1109,1112],{"href":1110,"rel":1111},"https:\u002F\u002Fdl.acm.org\u002Fcitation.cfm?id=103163",[129],"What every computer scientist should know about floating-point arithmetic","》的翻译。",[28,1115,1117],{"id":1116},"ue4中的一些处理","UE4中的一些处理",[11,1119,1120],{},"UE4的TickTime的Delta的直接来源是FPlatformTime::Seconds() ，这个是double的，所以不会有什么问题。",[61,1122,1124],{"id":1123},"timeseconds","TimeSeconds",[11,1126,1127],{},"但如果是使用UE4本身的UWorld的时间登记GetWorld()->TimeSeconds 的话，是有可能遇到时间戳精度丢失的。不过要绕过UE4的保护机制，达到精度丢失的程度的话，还是需要一些特定的条件的。",[11,1129,1130],{},"从代码中看的话，UE4对TimeSeconds的保护只有这里有：",[75,1132,1135],{"className":1133,"code":1134,"language":80},[78],"AGameModeBase::ProcessServerTravel\n---------------------------------------------------------------\n\u002F\u002F Force an old style load screen if the server has been up for a long time so that TimeSeconds doesn't overflow and break everything\nbool bSeamless = (bUseSeamlessTravel && GetWorld()->TimeSeconds \u003C 172800.0f); \u002F\u002F 172800 seconds == 48 hours\n",[82,1136,1134],{"__ignoreMap":84},[11,1138,1139],{},"这个函数是在地图加载的时候调用的，也就是如果UWorld的生命周期超过了48个小时的话，就不允许SeamlessTravel，而是要求UWorld强制重新构建了。",[61,1141,1143],{"id":1142},"movetimestamp","MoveTimeStamp",[11,1145,1146],{},"在角色移动组件中，有服务端和客户端的时间戳处理。这里官方在处理的时候引入了一个自动的回转机制",[75,1148,1151],{"className":1149,"code":1150,"language":80},[78],"\u002F\u002F Reset TimeStamp regularly to combat float accuracy decreasing over time.\nif( CurrentTimeStamp > CharacterMovementComponent.MinTimeBetweenTimeStampResets )\n{\n  UE_LOG(LogNetPlayerMovement, Log, TEXT(\"Resetting Client's TimeStamp %f\"), CurrentTimeStamp);\n  CurrentTimeStamp -= CharacterMovementComponent.MinTimeBetweenTimeStampResets;\n",[82,1152,1150],{"__ignoreMap":84},[11,1154,1155],{},"官方的说明如下：",[75,1157,1160],{"className":1158,"code":1159,"language":80},[78],"\u002F** Minimum time between client TimeStamp resets.\n!! This has to be large enough so that we don't confuse the server if the client can stall or timeout.\nWe do this as we use floats for TimeStamps, and server derives DeltaTime from two TimeStamps.\nAs time goes on, accuracy decreases from those floating point numbers.\nSo we trigger a TimeStamp reset at regular intervals to maintain a high level of accuracy. *\u002F\nUPROPERTY()\nfloat MinTimeBetweenTimeStampResets;\n",[82,1161,1159],{"__ignoreMap":84},[11,1163,1164],{},"也就是说，为了防止float引起的浮点精度丢失，会在累计到这个时间后将时钟往回拨。因为物理运算对时间的精度非常的敏感，这个参数的默认值为240s。",[11,1166,1167],{},"理论上，使用这个时间戳也是可以实现部分功能的。但是由于它会回转，所以可能不适用于所有的情况，会有通用性的问题。另外，这个时间戳严重依赖于移动组件，很难理清其中的头绪，会在不必要的地方花费分离逻辑的成本以及造成逻辑在奇怪的地方耦合。",[21,1169,1170],{"id":1170},"总结",[11,1172,1173],{},"时间戳的获取需要留心各个平台不同的实现，尤其是要小心FPlatformTime::Seconds()，由于平台实现不同它所能提供的保证是不同的。另外，如果对应的平台UE4本身没有对其进行实现的话，使用的会是默认的FGenericPlatformTime，是直接使用系统时间转化过来的秒数，极有可能不符合使用预期，一定要留心。",{"title":84,"searchDepth":790,"depth":791,"links":1175},[1176,1182,1189],{"id":893,"depth":790,"text":893,"children":1177},[1178,1179,1180,1181],{"id":902,"depth":791,"text":903},{"id":918,"depth":791,"text":919},{"id":968,"depth":791,"text":969},{"id":1011,"depth":791,"text":1012},{"id":1045,"depth":790,"text":1045,"children":1183},[1184,1185],{"id":1054,"depth":791,"text":1055},{"id":1116,"depth":791,"text":1117,"children":1186},[1187,1188],{"id":1123,"depth":799,"text":1124},{"id":1142,"depth":799,"text":1143},{"id":1170,"depth":790,"text":1170},"2018-09-15",{"layout":834,"status":835,"published":836,"author":1192,"author_login":839,"author_email":840,"wordpress_id":1193,"wordpress_url":1194,"date_gmt":1195,"excerpt":1196},{"display_name":838,"login":839,"email":840,"url":84},2583,"\u002F?p=2583","2018-09-15 14:11:20 +0000",{"type":8,"value":1197},[1198],[11,1199,862],{},"\u002F2018-09-15-precise-timestamp",{"title":857,"description":862},"_legacy\u002F2018\u002F2018-09-15-precise-timestamp",[852,1204],"TimeStamp","PSxMrIGXAhchXkLgLKRW4v7HxLAWiix4RmZkk5vGMRs",{"id":1207,"title":1208,"body":1209,"date":1348,"description":1213,"extension":832,"meta":1349,"navigation":836,"path":1358,"seo":1359,"stem":1360,"tags":1361,"__hash__":1363},"blogs\u002F_legacy\u002F2018\u002F2018-07-28-ue4-tick-note.md","UE4中Tick使用小结",{"type":8,"value":1210,"toc":1337},[1211,1214,1217,1220,1223,1226,1229,1232,1236,1239,1242,1245,1248,1252,1255,1258,1262,1265,1268,1271,1275,1278,1281,1287,1290,1294,1297,1301,1304,1307,1310,1316,1319,1325,1328,1334],[11,1212,1213],{},"Tick在UE4的逻辑中是最基本的单元之一，不过也有些地方需要注意的。",[11,1215,1216],{},"当前使用的UE4版本为4.18。",[21,1218,1219],{"id":1219},"并行",[11,1221,1222],{},"Tick是可以打开并行的，对于一些和主要逻辑没有耦合的逻辑，可以打开该Object的并行Tick：bRunOnAnyThread来进行运行优化。",[11,1224,1225],{},"实际并行的执行方式可以到FTickTaskSequencer::ReleaseTickGroup中去查看。",[11,1227,1228],{},"并行只在TickGroup内部执行，但是要小心多线程同步问题，考虑到加锁的成本，还是只对独立的逻辑进行异步比较好。",[11,1230,1231],{},"在优化上，还有一个bAllowTickOnDedicatedServer的选项，不过具体与使用_Server宏来屏蔽代码相比哪个更好些并没有测试过~",[21,1233,1235],{"id":1234},"actualtickgroup","ActualTickGroup",[11,1237,1238],{},"每个TickFunc都可以指定TickGroup，但是这个TickGroup并不代表最终实际执行的TickGroup。",[11,1240,1241],{},"根据每个Tick注册的Prerequisite不同，Tick的执行Group会被延迟。",[11,1243,1244],{},"例如，如果TickFunc要求的Prerequisite是在PostPhiscs中的，那么即便它自己是注册为PrePhyiscs，也会被推迟到Post才能执行。",[11,1246,1247],{},"这个时候可以看到TickFunc的ActualStartTickGroup会变成实际的Tick执行组。",[21,1249,1251],{"id":1250},"自定义tick","自定义Tick",[11,1253,1254],{},"可以通过自定义TickFunc来构建自己的Tick逻辑，相比TimerManager最大的优势就是这样可以在原有的Tick之外获得自己需要的逻辑执行时间。",[11,1256,1257],{},"引擎内部有一些额外定义的TickFunc，以前似乎是有SecondaryTick的，不过后来弃用了。",[28,1259,1261],{"id":1260},"uworld","UWorld",[11,1263,1264],{},"作为PhsxScene的维护者，拥有额外的三个TickFunc：FStartPhysicsTickFunction、FEndPhysicsTickFunction、FStartAsyncSimulationFunction。",[11,1266,1267],{},"都是用于对物理世界进行操作的Tick，其中StartAsyncSimulation是用于cloth的模拟的，注册在EndPhysicsTickFunction之后。而StartPysics是用于启动物理模拟，EndPhysics则是物理模拟结束的。",[11,1269,1270],{},"所以如果要保证物理处理已经完成，最好是注册到PostPhysics。",[28,1272,1274],{"id":1273},"uskeletalmeshcomponent","USkeletalMeshComponent",[11,1276,1277],{},"FSkeletalMeshComponentEndPhysicsTickFunction是注册在EndPhysics的，用于骨骼动画的状态同步等操作，没有仔细的看过，详情参考USkeletalMeshComponent::EndPhysicsTickComponent。",[11,1279,1280],{},"这个Tick是要求在关卡完成物理结算之后的：",[75,1282,1285],{"className":1283,"code":1284,"language":80},[78],"EndPhysicsTickFunction.AddPrerequisite(World, World->EndPhysicsTickFunction);\n",[82,1286,1284],{"__ignoreMap":84},[11,1288,1289],{},"FSkeletalMeshComponentClothTickFunction则是负责对Cloth进行更新的，它被添加到了SkeletalMesh的EndPhysicsTickFunction之后，保证物理更新完成之后再进行衣服的演算更新。",[28,1291,1293],{"id":1292},"ucharactermovmentcomponent","UCharacterMovmentComponent",[11,1295,1296],{},"FCharacterMovementComponentPostPhysicsTickFunction这个是在角色移动中，执行PostPhysicsTickComponent，当bDeferUpdateBasedMovement打开了的话，会在这里进行一次移动更新。",[28,1298,1300],{"id":1299},"uprimitivecomponent","UPrimitiveComponent",[11,1302,1303],{},"FPrimitiveComponentPostPhysicsTickFunction，这个是用来执行每个Component的PostPhysicsTick的，按照官方注释已经弃用。",[28,1305,1306],{"id":1306},"自定义",[11,1308,1309],{},"参照上面的TickFunc的用法，自定义比较简单",[75,1311,1314],{"className":1312,"code":1313,"language":80},[78],"USTRUCT()\nstruct FMyCustomTick : public FTickFunction\n{\n  GENERATED_USTRUCT_BODY()\n\n  class UVehicleSyncComponent*    Target;\n\n  FMyCustomTick();\n\n  virtual void ExecuteTick(float DeltaTime, ELevelTick TickType, ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent) override;\n  \u002F** Abstract function to describe this tick. Used to print messages about illegal cycles in the dependency graph **\u002F\n  virtual FString DiagnosticMessage() override;\n};\n\ntemplate\u003C>\nstruct TStructOpsTypeTraits\u003CFMyCustomTick> : public TStructOpsTypeTraitsBase2\u003CFMyCustomTick>\n{\n  enum\n  {\n    WithCopy = false\n  };\n};\n",[82,1315,1313],{"__ignoreMap":84},[11,1317,1318],{},"其中，下面的宏是必须的，否则会导致编译出错。因为TickFunc是不允许复制的。之后再对函数进行实现：",[75,1320,1323],{"className":1321,"code":1322,"language":80},[78],"FMyCustomTick::FMyCustomTick()\n{\n  TickGroup = ETickingGroup::TG_PostPhysics;\n  bCanEverTick = true;\n}\n\nvoid FMyCustomTick::ExecuteTick(float DeltaTime, ELevelTick TickType, ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)\n{\n  FActorComponentTickFunction::ExecuteTickHelper(Target, \u002F*bTickInEditor=*\u002F false, DeltaTime, TickType, [this](float DilatedTime) { Target->PostPhysicsTick(*this); });\n}\n\nFString FMyCustomTick::DiagnosticMessage()\n{\n  return Target->GetFullName() + TEXT(\"[UMyComp::PostPhysxTick]\");\n}\n",[82,1324,1322],{"__ignoreMap":84},[11,1326,1327],{},"对于Component，定义了FMyCustomTick的变量PostPhysxComponentTick之后，在其Tick注册函数RegisterComponentTickFunctions中继续注册就可以了，可以使用的帮助函数SetupActorComponentTickFunction来进行注册，以及注意解注册：",[75,1329,1332],{"className":1330,"code":1331,"language":80},[78],"if (bRegister)\n{\n  if (SetupActorComponentTickFunction(&PostPhysxComponentTick))\n  {\n    PostPhysxComponentTick.Target = this;\n\n    UWorld* World = GetWorld();\n    if (World != nullptr)\n    {\n      PostPhysxComponentTick.AddPrerequisite(World, World->EndPhysicsTickFunction);\n    }\n  }\n}\nelse\n{\n  if (PostPhysxComponentTick.IsTickFunctionRegistered())\n  {\n    PostPhysxComponentTick.UnRegisterTickFunction();\n  }\n}\n",[82,1333,1331],{"__ignoreMap":84},[11,1335,1336],{},"TickFunc的自定义还是比较简单的 ，不明白的地方可以参考上面那些引擎内部用法。",{"title":84,"searchDepth":790,"depth":791,"links":1338},[1339,1340,1341],{"id":1219,"depth":790,"text":1219},{"id":1234,"depth":790,"text":1235},{"id":1250,"depth":790,"text":1251,"children":1342},[1343,1344,1345,1346,1347],{"id":1260,"depth":791,"text":1261},{"id":1273,"depth":791,"text":1274},{"id":1292,"depth":791,"text":1293},{"id":1299,"depth":791,"text":1300},{"id":1306,"depth":791,"text":1306},"2018-07-28",{"layout":834,"status":835,"published":836,"author":1350,"author_login":839,"author_email":840,"wordpress_id":1351,"wordpress_url":1352,"date_gmt":1353,"excerpt":1354},{"display_name":838,"login":839,"email":840,"url":84},2409,"\u002F?p=2409","2018-07-28 02:03:37 +0000",{"type":8,"value":1355},[1356],[11,1357,1213],{},"\u002F2018-07-28-ue4-tick-note",{"title":1208,"description":1213},"_legacy\u002F2018\u002F2018-07-28-ue4-tick-note",[852,1362],"Tick","WcoVUqA7rnnKC0CuEd2pU1q4cND6cJ72eY60f8a31Hc",{"id":1365,"title":1366,"body":1367,"date":1789,"description":1371,"extension":832,"meta":1790,"navigation":836,"path":1799,"seo":1800,"stem":1801,"tags":1802,"__hash__":1804},"blogs\u002F_legacy\u002F2018\u002F2018-07-26-ue4-loading-gc-optimize.md","UE4中Loading和GC的优化",{"type":8,"value":1368,"toc":1767},[1369,1372,1379,1383,1386,1390,1393,1398,1401,1404,1422,1425,1429,1432,1435,1438,1441,1444,1450,1453,1456,1464,1467,1471,1474,1477,1480,1485,1488,1491,1494,1498,1501,1504,1507,1521,1524,1527,1530,1536,1539,1542,1545,1548,1557,1560,1563,1568,1571,1574,1578,1581,1584,1587,1590,1593,1596,1599,1603,1606,1611,1614,1619,1622,1627,1630,1633,1639,1642,1647,1650,1653,1656,1660,1663,1668,1671,1674,1680,1683,1689,1692,1695,1699,1702,1705,1710,1713,1716,1719,1722,1727,1730,1733,1736,1740,1743,1756,1759,1761,1764],[11,1370,1371],{},"本文翻译整理自Epic Japan分享的Slide，是官方的一些优化建议。",[11,1373,1374,1375,704],{},"原始的PPT请参看[",[125,1376,962],{"href":1377,"rel":1378},"https:\u002F\u002Fwww.slideshare.net\u002FEpicGamesJapan\u002Fue4loadinggcprofiling",[129],[21,1380,1382],{"id":1381},"loading-time","Loading Time",[11,1384,1385],{},"载入时间在逻辑上分为两个部分：从硬盘中将需要的资源载入内存的时间；以及在AddToWorld时对系统造成的瞬间负载时间。",[28,1387,1389],{"id":1388},"stat-level","Stat Level",[11,1391,1392],{},"通常要对载入状态进行追踪，可以使用Stat Level指令。",[11,1394,1395],{},[37,1396],{"alt":39,"src":1397},"\u002Fwp-content\u002Fuploads\u002F2018\u002F07\u002Fimage_thumb.png",[11,1399,1400],{},"这个指令会将关卡的载入情况显示出来，不过对于没有使用流关卡的情况，似乎作用不大。",[11,1402,1403],{},"这里显示出来的就是各个关卡载入的情况，总共有五种情况对应不同的颜色：",[1405,1406,1407,1410,1413,1416,1419],"ul",{},[748,1408,1409],{},"灰色-Persistent level",[748,1411,1412],{},"红色-UnLoad level",[748,1414,1415],{},"紫色-Loading",[748,1417,1418],{},"橙色-AddToWorld",[748,1420,1421],{},"绿色-Loaded Level",[11,1423,1424],{},"通常使用MemReport指令输出的关卡状态，由于是命令瞬间的状态，通常只会看到UnLoadLevel和Loaded Level两种常驻状态。",[28,1426,1428],{"id":1427},"loadtimes","Loadtimes",[11,1430,1431],{},"要对载入情况进行更加精细的分析，可以借助官方提供的Loadtimes指令。",[11,1433,1434],{},"使用LoadTimes.DumpReport可以将关卡的详细载入时间消耗情况输出到log，如果使用LoadTimes.DumpReport FILE的话就可以将报告输出到\\Saved\\Profiling\\LoadReports\\目录。",[11,1436,1437],{},"此外LoadTimes.DumpReport还可以使用LOWTIME=0.05等来过滤小于这个时间的载入报告，方便排查大的时间消耗。",[11,1439,1440],{},"而使用Loadtimes.reset可以对累计的数据进行清理，避免整个测试跑得太久累计下来的不需要的信息。",[11,1442,1443],{},"整个LoadTimes的统计核心是在UObjectGlobals.cpp中的，例如在Endload中这样的Scope：",[75,1445,1448],{"className":1446,"code":1447,"language":80},[78],"\u002F\u002F The time tracker keeps track of time spent in EndLoad.\nFExclusiveLoadPackageTimeTracker::FScopedEndLoadTracker Tracker;\n",[82,1449,1447],{"__ignoreMap":84},[11,1451,1452],{},"所有的追踪由单例FExclusiveLoadPackageTimeTracker负责，有兴趣的可以详细的阅读其代码。",[11,1454,1455],{},"报告大体上可以分为下面的两个部分",[1405,1457,1458,1461],{},[748,1459,1460],{},"asset type load times sorted by exclusive time: 按类型排列的载入时间统计(s)",[748,1462,1463],{},"Dumping all loaded assets by exclusive\u002Finclusive load time: 按具体的资源排列(ms)",[11,1465,1466],{},"Exclusive的统计不包括具体的Object的依赖资源，而Inclusive是包含所有依赖资源的。",[28,1468,1470],{"id":1469},"addtoworld","AddToWorld",[11,1472,1473],{},"在加载过程中AddToWorld是最后一个过程，物体在载入之后必须注册到渲染世界和物理世界中才能正常的进行游戏。",[11,1475,1476],{},"要对这个过程进行记录必须打开PERF_TRACK_DETAILED_ASYNC_STATS宏才行，打开之后可以对FAsyncPackage的CreateExport和PostLoad以及World中的AddToWorld和RemoveFromWorld获得更多的信息。",[11,1478,1479],{},"输出的内容会有像是这样的：",[11,1481,1482],{},[37,1483],{"alt":39,"src":1484},"\u002Fwp-content\u002Fuploads\u002F2018\u002F07\u002Fimage_thumb-1.png",[11,1486,1487],{},"不过实际上感觉用处不是很大~",[28,1489,1490],{"id":1490},"优化",[11,1492,1493],{},"优化载入时间首先的建议是使用官方的PAK机制，社区也有看到使用7zip之类的压缩算法来进行内容压缩的。不过压缩率是一个需要权衡的地方，因为虽然能够缩短载入到内存的时间以及减小包尺寸，却会导致额外的解压缩成本。",[61,1495,1497],{"id":1496},"fileopenorder","FileOpenOrder",[11,1499,1500],{},"在载入时间优化方面，官方在PAK机制内还提供了一个FileOpenOrder的优化工具。",[11,1502,1503],{},"要使用这个工具，在运行游戏实例时加上-flieopenorder指令。然后在游戏中将常规操作都执行一遍并退出游戏。这时候会生成一个GameOpenOrder.log，将这个文件拷贝到\u002FBuild\u002FWindowsNoEditor\u002FFileOpenOrder\u002F ，这样重新打包的时候就可以得到优化的文件顺序了。之后根据项目的迭代不断的更新这个文件就可以了。",[11,1505,1506],{},"这个优化主要是为了减少在对资源进行载入的时候的寻道成本了，另外这个优化对于SD卡这样的Mobile其实也是有优化作用的。因为SD卡的文件碎片虽然不会导致机械硬盘那样的延迟，也会导致读取效率的下降。",[99,1508,1509,1512],{},[11,1510,1511],{},"File fragmentation: where there is not sufficient space for a file to be recorded in a contiguous region, it is split into non-contiguous fragments. This does not cause rotational or head-movement delays as with electromechanical hard drives, but may decrease speed; for instance, by requiring additional reads and computation to determine where on the card the file's next fragment is stored.",[11,1513,1514,1515,1520],{},"[",[125,1516,1519],{"href":1517,"rel":1518},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FSecure_Digital",[129],"Secure Digital Card","]",[61,1522,1523],{"id":1523},"减少尺寸",[11,1525,1526],{},"除此之外的就都是一些常见的优化建议了，例如Shader Permutation Reduction 、材质的Instance策略等。",[11,1528,1529],{},"以及一些在不使用的情况下可以去掉的Index Buffer:",[75,1531,1534],{"className":1532,"code":1533,"language":80},[78],"\u002F** Reversed depth only index buffer, used to prevent changing culling state between drawcalls. *\u002F\nFRawStaticIndexBuffer ReversedDepthOnlyIndexBuffer;\n\n\u002F** Index buffer containing adjacency information required by tessellation. *\u002F\nFRawStaticIndexBuffer AdjacencyIndexBuffer;\n\n\u002F** Reversed index buffer, used to prevent changing culling state between drawcalls. *\u002F\nFRawStaticIndexBuffer ReversedIndexBuffer;\n",[82,1535,1533],{"__ignoreMap":84},[11,1537,1538],{},"其中AdjacencyIndexBuffer是为tessellation而提供的，如果不用的话可以关掉，而ReversedIndexBuffer似乎是为了在DrawCall之间减少culling state计算的成本而存在的，实际在渲染中的作用没有仔细的考据。",[11,1540,1541],{},"代码中能看到的注释是provide a minor rendering speedup at the expense of using twice the index buffer memory，这方面的权衡就需要实际测试了。",[61,1543,1544],{"id":1544},"加载控制",[11,1546,1547],{},"通过异步，将资源加载推迟或者提前。在4.19后Asset Manager应当已经可以使用了，可以通过Asset Manager对资源的加载进行更好的控制。",[11,1549,1550,1551,1556],{},"详情可以参考官方的[",[125,1552,1555],{"href":1553,"rel":1554},"http:\u002F\u002Fapi.unrealengine.com\u002FCHN\u002FEngine\u002FBasics\u002FAssetsAndPackages\u002FAssetManagement\u002Findex.html",[129],"资源管理","]文档。",[11,1558,1559],{},"传统上的做法，还包括使用软指针来防止蓝图自动加载关联资源等手段。",[11,1561,1562],{},"同时，针对地图的Stream In\u002FOut官方还提供了一些异步化的优化选项：",[11,1564,1565],{},[37,1566],{"alt":39,"src":1567},"\u002Fwp-content\u002Fuploads\u002F2018\u002F07\u002Fimage_thumb-2.png",[11,1569,1570],{},"将Object的加载分散到各个帧去，而不会因为AddToWorld操作导致单帧瞬间卡顿。",[11,1572,1573],{},"另外，蓝图中尽量将逻辑从Begin Play移动到Construction Script中去，可以享受CDO带来的速度加成。",[21,1575,1577],{"id":1576},"gc","GC",[11,1579,1580],{},"UE4内部的垃圾回收系统理论上除了提供的优化接口之外应当最好不要手动修改，由于GC本身要照顾很多方面，所以其中必然会有权衡存在。",[11,1582,1583],{},"通常GC会引起问题都是在LevelStreaming的时候，不过如果你在代码中申请了大规模的UObject，在不再使用的时候也不删除。或者频繁而无意义的调用ForceGC，自然也可能会造成性能问题。",[11,1585,1586],{},"UE4通过一张链表进行GC维护基础，数据类型为FUObjectArray。",[11,1588,1589],{},"GC的部分逻辑可以到GarbageCollection.cpp中进行查看。",[11,1591,1592],{},"通常情况下GC采用多线程的形式，但是当可用的处理器核心为1或者关闭了并行GC的时候就会变成单线程的形式。另外，Per Class的GC被打开的时候，由于无法保证线程安全，也会导致GC变为单线程。目前在编辑器中，由于HandleObjectReference中的Modify()的存在，是强制单线程进行的。",[11,1594,1595],{},"上面的内容来自官方的注释，由于不打算深入GC逻辑的修改，所以没有考据为何编辑器中Modify无法被移除。",[11,1597,1598],{},"GC的主要成本来来自遍历成本和实际的Object删除，4.16更新的GC优化就是将Object的实际删除分散到了各个帧中，这样在GC执行的时候就只需要执行依赖关系的切断了。",[28,1600,1602],{"id":1601},"profiling","Profiling",[11,1604,1605],{},"要对GC的实际消耗进行查看的话，可以打开log LogGarbage log",[11,1607,1608],{},[37,1609],{"alt":39,"src":1610},"\u002Fwp-content\u002Fuploads\u002F2018\u002F07\u002Fimage_thumb-3.png",[11,1612,1613],{},"也可以使用Stat dumphitches来定位GC造成的瞬间延迟",[11,1615,1616],{},[37,1617],{"alt":39,"src":1618},"\u002Fwp-content\u002Fuploads\u002F2018\u002F07\u002Fimage_thumb-4.png",[11,1620,1621],{},"通常使用FrontEnd的时候会看到的警告",[11,1623,1624],{},[37,1625],{"alt":39,"src":1626},"\u002Fwp-content\u002Fuploads\u002F2018\u002F07\u002Fimage_thumb-5.png",[11,1628,1629],{},"可以使用-NOVERIFYGC指令运行来去掉。",[11,1631,1632],{},"对于需要进一步的GC的信息的情况，可以选择打开",[75,1634,1637],{"className":1635,"code":1636,"language":80},[78],"# define PROFILE_GCConditionalBeginDestroy 1\n# define PROFILE_GCConditionalBeginDestroy_byClass 1\n",[82,1638,1636],{"__ignoreMap":84},[11,1640,1641],{},"来输出更加详细的信息：",[11,1643,1644],{},[37,1645],{"alt":39,"src":1646},"\u002Fwp-content\u002Fuploads\u002F2018\u002F07\u002Fimage_thumb-6.png",[28,1648,1490],{"id":1649},"优化-1",[11,1651,1652],{},"为了避免LevelStreaming造成的GC瞬间成本，可以通过缩小细分关卡的规模来进行。",[11,1654,1655],{},"这样的话在进行角色的移动时，就可以形成小规模的载入和移除，而不是一次性的大规模变更。",[61,1657,1659],{"id":1658},"disregardgcobject","DisregardGCObject",[11,1661,1662],{},"另外，针对其实不需要进行GC的一些常驻Object，可以通过",[11,1664,1665],{},[37,1666],{"alt":39,"src":1667},"\u002Fwp-content\u002Fuploads\u002F2018\u002F07\u002Fimage_thumb-7.png",[11,1669,1670],{},"选项来进行限制，这样的话就可以有效的减少遍历成本。",[11,1672,1673],{},"针对这个选项，官方有提供辅助设定的工具，不需要自己进行试错",[75,1675,1678],{"className":1676,"code":1677,"language":80},[78],"[\u002FScript\u002FEngine.GarbageCollectionSettings]\ngc.MaxObjectsNotConsideredByGC=1\ngc.SizeOfPermanentObjectPool=0\n",[82,1679,1677],{"__ignoreMap":84},[11,1681,1682],{},"在设定中这样设置后，游戏开启后会在log中输出",[75,1684,1687],{"className":1685,"code":1686,"language":80},[78],"LogUObjectArray: 52083 objects as part of root set at end of initial load.\nLogUObjectAllocator: 9937152 out of 0 bytes used by permanent object pool.\n",[82,1688,1686],{"__ignoreMap":84},[11,1690,1691],{},"这样就可以得到一个有效的设定值了。",[11,1693,1694],{},"在GarbageCollectionSettings中还有一些其他的设定值，以及用于gc的console选项，不过一般情况应当不会动到。",[61,1696,1698],{"id":1697},"cluster","Cluster",[11,1700,1701],{},"对于复合性的逻辑物体，其内部的Object随着父物体的状态进行管理的，可以使用Cluster来进行GC管理。",[11,1703,1704],{},"防止不必要的对很多子物体进行GC遍历：",[11,1706,1707],{},[37,1708],{"alt":39,"src":1709},"\u002Fwp-content\u002Fuploads\u002F2018\u002F07\u002Fimage_thumb-8.png",[11,1711,1712],{},"其中，Merge GC Clusters可以运行聚合之间相互构成更大的聚合。",[11,1714,1715],{},"Actor的话需要打开Can be in Cluster，StaticMeshActor是默认打开的。因为其中引用的很多资源基本是跟随其自身的生命周期的。",[11,1717,1718],{},"Cluster由于是在AddToWorld中进行的，所以在Sequence中可能会有问题。",[11,1720,1721],{},"下面的是官方给出的优化结果：",[11,1723,1724],{},[37,1725],{"alt":39,"src":1726},"\u002Fwp-content\u002Fuploads\u002F2018\u002F07\u002Fimage_thumb-9.png",[61,1728,1729],{"id":1729},"减少规模",[11,1731,1732],{},"这个算是通用的建议，例如蓝图的Macros在使用上需要谨慎，因为可能会造成在打包展开时变成很大的结构，应当尽可能的函数化。",[11,1734,1735],{},"还有就是尽可能的使用Blueprint Nativization，但是这个似乎一直没有离开实验阶段……",[28,1737,1739],{"id":1738},"finishdestroy的帧分散","FinishDestroy的帧分散",[11,1741,1742],{},"帧分散的思路可以用在LevelStreaming的StreamOut中FinishDestroy的工作分散掉：",[99,1744,1745],{},[745,1746,1747,1750,1753],{},[748,1748,1749],{},"void UWorld::UpdateLevelStreaming()中ForceGarbageCollection(true); 改为false。",[748,1751,1752],{},"UWorld* UWorld::FindWorldInPackage()中GetObjectsWithOuter改为GetObjectsWithOuter(Package, PotentialWorlds, false, EObjectFlags::RF_NoFlags,EInternalObjectFlags::PendingKill);。",[748,1754,1755],{},"UWorld* UWorld::FollowWorldRedirectorInPackage()中GetObjectsWithOuter改为GetObjectsWithOuter(Package, PotentialRedirectors, false,EObjectFlags::RF_NoFlags,EInternalObjectFlags::PendingKill);",[11,1757,1758],{},"这个是EpicJapan向Epic官方确认会有效果的一种优化方式。",[21,1760,1170],{"id":1170},[11,1762,1763],{},"由于是官方的PPT，所以内容上都是一些通用的建议，以及官方本身的优化工具的介绍。",[11,1765,1766],{},"实际项目优化中大概都会有不少的引擎魔改，不过参考一下官方的思路，把基础的优化功能打开也是很重要的~",{"title":84,"searchDepth":790,"depth":791,"links":1768},[1769,1779,1788],{"id":1381,"depth":790,"text":1382,"children":1770},[1771,1772,1773,1774],{"id":1388,"depth":791,"text":1389},{"id":1427,"depth":791,"text":1428},{"id":1469,"depth":791,"text":1470},{"id":1490,"depth":791,"text":1490,"children":1775},[1776,1777,1778],{"id":1496,"depth":799,"text":1497},{"id":1523,"depth":799,"text":1523},{"id":1544,"depth":799,"text":1544},{"id":1576,"depth":790,"text":1577,"children":1780},[1781,1782,1787],{"id":1601,"depth":791,"text":1602},{"id":1649,"depth":791,"text":1490,"children":1783},[1784,1785,1786],{"id":1658,"depth":799,"text":1659},{"id":1697,"depth":799,"text":1698},{"id":1729,"depth":799,"text":1729},{"id":1738,"depth":791,"text":1739},{"id":1170,"depth":790,"text":1170},"2018-07-26",{"layout":834,"status":835,"published":836,"author":1791,"author_login":839,"author_email":840,"wordpress_id":1792,"wordpress_url":1793,"date_gmt":1794,"excerpt":1795},{"display_name":838,"login":839,"email":840,"url":84},2390,"\u002F?p=2390","2018-07-25 19:57:21 +0000",{"type":8,"value":1796},[1797],[11,1798,1371],{},"\u002F2018-07-26-ue4-loading-gc-optimize",{"title":1366,"description":1371},"_legacy\u002F2018\u002F2018-07-26-ue4-loading-gc-optimize",[852,1577,1803],"Loading","GevD6rMhLCgBoNhVGXgJ0gK6aRt2AwgsXRLezCEdlNw",{"id":1806,"title":1807,"body":1808,"date":1936,"description":1812,"extension":832,"meta":1937,"navigation":836,"path":1946,"seo":1947,"stem":1948,"tags":1949,"__hash__":1951},"blogs\u002F_legacy\u002F2018\u002F2018-05-09-ue4-and-sol2.md","UE4中Sol2的接入",{"type":8,"value":1809,"toc":1930},[1810,1813,1816,1819,1828,1832,1835,1838,1841,1844,1847,1850,1853,1857,1860,1863,1866,1869,1872,1878,1881,1885,1888,1891,1897,1900,1903,1906,1909,1912,1918,1921],[11,1811,1812],{},"虽然说Lua的C++接入本身在网上能找到很多示例，但是在尝试对Sol2进行接入的时候还是遇到了一些问题，所以在此进行记录。",[11,1814,1815],{},"当前使用的UE4版本为4.19.1。",[11,1817,1818],{},"Sol2是进行Lua绑定的C++类库，过程中最主要的问题是，对Lua本身一知半解，所以没有很好的理解Sol2官方文档中所描述的一些术语。",[11,1820,1821,1822,1827],{},"之所以会选择Sol2，是因为它号称自己是[",[125,1823,1826],{"href":1824,"rel":1825},"http:\u002F\u002Fsol2.readthedocs.io\u002Fen\u002Flatest\u002Fbenchmarks.html",[129],"地上最快","]的~",[21,1829,1831],{"id":1830},"luajit","LuaJit",[11,1833,1834],{},"似乎lua本身的脚本解释器有两个公开的版本，一个是LuaJit，一个是vanilla Lua。",[11,1836,1837],{},"由于没有仔细的对这两者之间的差别进行区分，所以并不是特别清楚其中的具体区别。",[11,1839,1840],{},"直接Google的话，Lua是指向vanilla lua的，而且LuaJit的Lua版本也比较靠前。",[11,1842,1843],{},"但是由于Sol2的官方文档中说LuaJit的速度会比较快，所以就采用了LuaJit。",[11,1845,1846],{},"那么首先第一步就是下载LuaJit，到了这一步就比较明了了。",[11,1848,1849],{},"LuaJit有提供CMake的配置文件，用CMake配置一下之后就可以进行生成了。",[11,1851,1852],{},"对于下一步的接入需要的是生成的Lua51.lib和Lua51.dll。",[21,1854,1856],{"id":1855},"sol2","Sol2",[11,1858,1859],{},"这个库本身也有提供CMake，直接使用CMake配置就好了。",[11,1861,1862],{},"但是遇到的主要问题是，在和LuaJit的对接上，不过实际上在对整个库进行理清之后就不会有什么问题。",[11,1864,1865],{},"Sol2有提供很多的Test所以在使用方面会有很多的便利。",[11,1867,1868],{},"不过这里用CMake进行配置其实是不必要的，Sol2本身有提供单文件版本，其实只要include就可以了。",[11,1870,1871],{},"但是LuaJit的话还是需要进行链接的，并且必须根据使用的Lua解释器的不同来提供不同的宏。基本这样包含就可以了：",[75,1873,1876],{"className":1874,"code":1875,"language":80},[78],"extern \"C\"\n{\n#include \u003Clua.h>\n#include \u003Clualib.h>\n#include \u003Clauxlib.h>\n}\n\n#define SOL_USING_CXX_LUA        1\n#define SOL_USING_CXX_LUAJIT    1\n#include \"sol.hpp\"\n",[82,1877,1875],{"__ignoreMap":84},[11,1879,1880],{},"最主要的是前面的Extern “C”不能忘记，不然会报很多的链接错误~",[21,1882,1884],{"id":1883},"ue4插件接入","UE4插件接入",[11,1886,1887],{},"据说以前Sol2接入的话会有一些问题，因为sol2本身对check有定义，所以会造成定冲突。",[11,1889,1890],{},"于是sol2的作者后面给出了解决方案：",[75,1892,1895],{"className":1893,"code":1894,"language":80},[78],"#if defined(UE_BUILD_DEBUG) || defined(UE_BUILD_DEVELOPMENT) || defined(UE_BUILD_TEST) || defined(UE_BUILD_SHIPPING) || defined(UE_SERVER)\n#define SOL_INSIDE_UNREAL\n#endif \u002F\u002F Unreal Engine 4 bullshit\n\n#ifdef SOL_INSIDE_UNREAL\n#ifdef check\n#define SOL_INSIDE_UNREAL_REMOVED_CHECK\n#undef check\n#endif\n#endif \u002F\u002F Unreal Engine 4 Bullshit\n",[82,1896,1894],{"__ignoreMap":84},[11,1898,1899],{},"总之就是回避了一下：D",[11,1901,1902],{},"不过说到UE4兼容，最近VA也新增了对UE4的特别支持。记得一开始的时候，VA经常在对UE4的函数进行跳转的时候崩掉VS，而且还有一定几率的崩掉VA建立的代码解析缓存，然后需要重新进行一次代码解析，非常的欢乐。",[11,1904,1905],{},"不过新增的UE4支持还没有完全的测试过，不知实际上有什么改进还不是特别的清楚呢~",[21,1907,1908],{"id":1908},"测试",[11,1910,1911],{},"最后在UE4中使用蓝图函数进行了简单的测试：",[75,1913,1916],{"className":1914,"code":1915,"language":80},[78],"UE_LOG(LogTemp,Log, TEXT(\"=== basic ===\"));\n\n\u002F\u002F create an empty lua state\nsol::state lua;\n\n\u002F\u002F by default, libraries are not opened\n\u002F\u002F you can open libraries by using open_libraries\n\u002F\u002F the libraries reside in the sol::lib enum class\nlua.open_libraries(sol::lib::base);\n\u002F\u002F you can open all libraries by passing no arguments\n\u002F\u002Flua.open_libraries();\n\n\u002F\u002F call lua code directly\nlua.script(\"print('hello world')\");\n\n\u002F\u002F call lua code, and check to make sure it has loaded and run properly:\nauto handler = &sol::script_default_on_error;\nlua.script(\"print('hello again, world')\", handler);\n\n\u002F\u002F Use a custom error handler if you need it\n\u002F\u002F This gets called when the result is bad\nauto simple_handler = [](lua_State*, sol::protected_function_result result) {\n\u002F\u002F You can just pass it through to let the call-site handle it\n  return result;\n};\n\u002F\u002F the above lambda is identical to sol::simple_on_error, but it's\n\u002F\u002F shown here to show you can write whatever you like\n\n\u002F\u002F\n{\n  auto result = lua.script(\"print('hello hello again, world') \\n return 24\", simple_handler);\n  if (result.valid()) {\n    UE_LOG(LogTemp, Log, TEXT(\"the third script worked, and a double-hello statement should appear above this one!\"));\n    int value = result;\n    ensure(value == 24);\n  }\n  else {\n    UE_LOG(LogTemp, Log, TEXT(\"the third script failed, check the result type for more information!\"));\n  }\n}\n\n\u002F**    \u003CThis test will log out 0xE24C4A03 *\u002F\n{\n  auto result = lua.script(\"does.not.exist\", simple_handler);\n  if (result.valid()) {\n    UE_LOG(LogTemp, Log, TEXT(\"the fourth script worked, which it wasn't supposed to! Panic!\"));\n    int value = result;\n    ensure(value == 24);\n  }\n  else {\n    sol::error err = result;\n    UE_LOG(LogTemp, Log, TEXT(\"the fourth script failed, which was intentional! nError: %s\"), ANSI_TO_TCHAR(err.what()));\n  }\n}\n",[82,1917,1915],{"__ignoreMap":84},[11,1919,1920],{},"由于接下来还没有具体的使用计划，所以就在这里了。",[11,1922,1923,1924,1929],{},"绑定好的Sol2插件可以到Github上下载：[",[125,1925,1928],{"href":1926,"rel":1927},"https:\u002F\u002Fgithub.com\u002FArisego\u002FSol2UE4",[129],"Sol2UE4","]，这样有需要的童鞋就没有必要重新再绑一遍了~",{"title":84,"searchDepth":790,"depth":791,"links":1931},[1932,1933,1934,1935],{"id":1830,"depth":790,"text":1831},{"id":1855,"depth":790,"text":1856},{"id":1883,"depth":790,"text":1884},{"id":1908,"depth":790,"text":1908},"2018-05-09",{"layout":834,"status":835,"published":836,"author":1938,"author_login":839,"author_email":840,"wordpress_id":1939,"wordpress_url":1940,"date_gmt":1941,"excerpt":1942},{"display_name":838,"login":839,"email":840,"url":84},2352,"\u002F?p=2352","2018-05-09 15:25:11 +0000",{"type":8,"value":1943},[1944],[11,1945,1812],{},"\u002F2018-05-09-ue4-and-sol2",{"title":1807,"description":1812},"_legacy\u002F2018\u002F2018-05-09-ue4-and-sol2",[852,1856,1950],"lua","AN5DDWVMtt3GIYCq4gzzCGWkoUTmUT45hP9SOx1QSL0",85,1788763178183]