[{"data":1,"prerenderedAt":1167},["ShallowReactive",2],{"page-UE4-3":3,"page-count-UE4":1166},[4,116,245,891,995],{"id":5,"title":6,"body":7,"date":92,"description":13,"extension":93,"meta":94,"navigation":97,"path":109,"seo":110,"stem":111,"tags":112,"__hash__":115},"blogs\u002F_legacy\u002F2020\u002F2020-03-31-note-tweakobjectptr-compile.md","记录TWeakObjectPtr的一个比较坑的编译报错",{"type":8,"value":9,"toc":86},"minimark",[10,14,17,20,23,26,30,33,44,47,50,53,56,62,65,71,74,80,83],[11,12,13],"p",{},"最近在使用TWeakObjectPtr时遇到了一个比较奇怪的编译报错，记得之前解决过，后面又忘记了，所以这次记录下来。",[11,15,16],{},"为了防止UObject的生命周期混乱问题，官方的建议是加上UProperty。但是有的时候考虑到引用关系的维护，我们不希望使用UProperty来维护指针。",[11,18,19],{},"而且，如果一个指针并不会给蓝图使用却加上了UProperty的话，会无端的感觉很“重”。",[11,21,22],{},"如果不希望控制指针的生命周期，而又希望维护引用的话，可以使用TWeakObjectPtr。如果希望自己保护生命周期的话，可以使用TSharedPtr。TSharedPtr记得如果使用不当的话会有二次释放的问题，不过现在记不清楚触发方式了。",[11,24,25],{},"这边还是回答奇怪的编译错误上吧。",[27,28,29],"h2",{"id":29},"编译错误",[11,31,32],{},"在对裸指针进行保护时，使用TWeakObjectPtr后出现了这样的报错：",[34,35,40],"pre",{"className":36,"code":38,"language":39},[37],"language-text","2>xxx\\engine\\source\\runtime\\core\\public\\UObject\u002FWeakObjectPtrTemplates.h(55): error C2338: TWeakObjectPtr can only be constructed with UObject types\n2>xxx\\engine\\source\\runtime\\core\\public\\UObject\u002FWeakObjectPtrTemplates.h(50): note: while compiling class template member function 'TWeakObjectPtr\u003CAAwesomeActor,FWeakObjectPtr>::TWeakObjectPtr(const T *)'\n2>        with\n2>        [\n2>            T=AAwesomeActor\n2>        ]\n2>Project\\(62): note: see reference to function template instantiation 'TWeakObjectPtr\u003CAAwesomeActor,FWeakObjectPtr>::TWeakObjectPtr(const T *)' being compiled\n2>        with\n2>        [\n2>            T=AAwesomeActor\n2>        ]\n2>Project\\(62): note: see reference to class template instantiation 'TWeakObjectPtr\u003CAAwesomeActor,FWeakObjectPtr>' being compiled\n","text",[41,42,38],"code",{"__ignoreMap":43},"",[11,45,46],{},"编译器似乎不认识我们的AAwesomeActor，使用各种forward declaration反而让问题越来越复杂。",[11,48,49],{},"但是我们又不想破坏include隔离，还是少许有些尴尬。",[27,51,52],{"id":52},"原因",[11,54,55],{},"其实这个错误是因为一个“坏”习惯造成的，通常为了避免忘记写初始化的情况，我们会给指针赋个初始值，改造之后就变成了这样：",[34,57,60],{"className":58,"code":59,"language":39},[37],"TWeakObjectPtr\u003CAAwesomeActor> MyAwesomeActor = nullptr;\n",[41,61,59],{"__ignoreMap":43},[11,63,64],{},"这样的话就导致模板编译的时候进入了“错误”的分支，在WeakObjectPtrTemplates.h中报错的行上面其实可以看到解释：",[34,66,69],{"className":67,"code":68,"language":39},[37],"\u002F\u002F This static assert is in here rather than in the body of the class because we want\n\u002F\u002F to be able to define TWeakObjectPtr\u003CUUndefinedClass>.\nstatic_assert(TPointerIsConvertibleFromTo\u003CT, const volatile UObject>::Value, \"TWeakObjectPtr can only be constructed with UObject types\");\n",[41,70,68],{"__ignoreMap":43},[11,72,73],{},"结论上来说，只要不加初始化就可以了，让模板编译走默认的构造函数就不会进到这里。",[34,75,78],{"className":76,"code":77,"language":39},[37],"TWeakObjectPtr\u003CAAwesomeActor> MyAwesomeActor;\n",[41,79,77],{"__ignoreMap":43},[11,81,82],{},"也就是说，这里初始化就会造成画蛇添足的效果。",[11,84,85],{},"有时候比较着急的时候一下子想不起来就真的比较郁闷。",{"title":43,"searchDepth":87,"depth":88,"links":89},2,3,[90,91],{"id":29,"depth":87,"text":29},{"id":52,"depth":87,"text":52},"2020-03-31","md",{"layout":95,"status":96,"published":97,"author":98,"author_login":100,"author_email":101,"wordpress_id":102,"wordpress_url":103,"date_gmt":104,"excerpt":105},"post","publish",true,{"display_name":99,"login":100,"email":101,"url":43},"风铃","flinkor","flinkor@foxmail.com",2781,"\u002F?p=2781","2020-03-31 14:17:22 +0000",{"type":8,"value":106},[107],[11,108,13],{},"\u002F2020-03-31-note-tweakobjectptr-compile",{"title":6,"description":13},"_legacy\u002F2020\u002F2020-03-31-note-tweakobjectptr-compile",[113,114],"UE4","Smart Pointer","66eOSSicIZqmQ-fcthAADAoN-Z7TkL2xVYC867SVlGg",{"id":117,"title":118,"body":119,"date":229,"description":123,"extension":93,"meta":230,"navigation":97,"path":239,"seo":240,"stem":241,"tags":242,"__hash__":244},"blogs\u002F_legacy\u002F2019\u002F2019-12-30-ue4-live2d-plugin-rewrite.md","重构Live2D插件",{"type":8,"value":120,"toc":221},[121,124,127,130,133,136,139,142,145,148,151,162,165,168,171,175,178,181,184,187,190,193,196,199,202,206,209,212],[11,122,123],{},"这个插件一直没有更新过，有些重要的功能也没有完成，最近好容易找到些时间。",[11,125,126],{},"由于和上一次的更新已经有了很长的时间，Live2D的SDK已经变化很大。",[11,128,129],{},"再加上打算换成新的渲染方式，所以干脆对整个插件进行了重写。",[27,131,132],{"id":132},"放弃的部分",[11,134,135],{},"首先，从目标上，放弃了编辑器支持。因为实质上Live2D的大部分编辑功能都是在官方的编辑器里面的，外部几乎没有什么可以控制的。而如果要做一些UE4这边的编辑器内控制的话，由于没有具体的需求，实在是很难把握分寸，反而会无端的投入时间。所以这一块就放弃了。",[11,137,138],{},"另外，也放弃了将所有的控制功能都接入到蓝图层面。这部分的工作其实就只是函数转发，但是也会无端的浪费劳动力，因为如何对Live2D的模型进行控制其实每个人在用的时候也会不一样吧。",[11,140,141],{},"因此，这边的主要工作就关注在Live2D模型的读取和渲染上了。",[27,143,144],{"id":144},"模型渲染",[11,146,147],{},"由于前次定的目标比较大，所以并没有在渲染上花太多的精力，直接使用了RenderTarget的Canvas绘制功能。",[11,149,150],{},"这样的缺点是，渲染的效率非常的低下。很难想象能够在移动平台上使用。",[11,152,153,154,161],{},"因此，这次使用了UE4后面提供的[",[155,156,160],"a",{"href":157,"rel":158},"https:\u002F\u002Fdocs.unrealengine.com\u002Fen-US\u002FProgramming\u002FRendering\u002FShaderInPlugin\u002FQuickStart\u002Findex.html",[159],"nofollow","Global Shader Plugin","]系统。",[11,163,164],{},"这个系统在官方的说明里面非常的简略，但是其实实际的作用还是非常强大的。",[11,166,167],{},"本次就是通过自定义的VertexShader和PixelShader来进行Live2D的模型绘制。由于之前对渲染管线的知识有很多一知半解的部分，这次算是遇到了不少坑，也学到了很多东西。",[11,169,170],{},"其中遇到的最大的两个坑先记录在这里，其他的部分感觉就只是在探索API的用法了……",[172,173,174],"h3",{"id":174},"参数传递",[11,176,177],{},"参数传递上有个不知道能不能算作是坑的地方，UE4这边VertexShader和PixelShader在例子里面是有作分别实现的。",[11,179,180],{},"但是实际如果要传递参数的话，参数必须在公共的基类里面声明，否则传递过去的值就会出现错乱。",[11,182,183],{},"尤其是在PixelShader这边的表现特别明显，会出现奇怪的结果。",[172,185,186],{"id":186},"绘制混乱",[11,188,189],{},"另一个问题就是RenderThread和GameThread的关系。",[11,191,192],{},"由于之前没有怎么接触过需要双边控制的逻辑，所以遇到了奇怪的问题。",[11,194,195],{},"最主要的问题是，GameThread在将任务发到RenderThread之后，并不会马上就执行。",[11,197,198],{},"在偶然的情况下，会出现GameThread进入第二次Update的时候，RenderThread里面的任务还没有执行完成。",[11,200,201],{},"这次遇到的就是，由于渲染还在进行中，GameThread却又进去更新了Live2D模型，导致渲染那边取到了错误的VertexBuffer，最后出现了奇怪的渲染结果。",[27,203,205],{"id":204},"todo","TODO",[11,207,208],{},"其实有些渲染的细节选项还没有接入，但是由于手头上没有用到对应功能的模型，就算做了也不知道效果是不是正确的。",[11,210,211],{},"这块也只能等到用到的时候再搞了，不过既然主体已经完成了的话，也不会有什么太大的工作量。",[11,213,214,215,220],{},"最后，插件的地址更新了，现在在[",[155,216,219],{"href":217,"rel":218},"https:\u002F\u002Fgithub.com\u002FArisego\u002FUnrealLive2D",[159],"这里","]。",{"title":43,"searchDepth":87,"depth":88,"links":222},[223,224,228],{"id":132,"depth":87,"text":132},{"id":144,"depth":87,"text":144,"children":225},[226,227],{"id":174,"depth":88,"text":174},{"id":186,"depth":88,"text":186},{"id":204,"depth":87,"text":205},"2019-12-30",{"layout":95,"status":96,"published":97,"author":231,"author_login":100,"author_email":101,"wordpress_id":232,"wordpress_url":233,"date_gmt":234,"excerpt":235},{"display_name":99,"login":100,"email":101,"url":43},2769,"\u002F?p=2769","2019-12-30 15:08:00 +0000",{"type":8,"value":236},[237],[11,238,123],{},"\u002F2019-12-30-ue4-live2d-plugin-rewrite",{"title":118,"description":123},"_legacy\u002F2019\u002F2019-12-30-ue4-live2d-plugin-rewrite",[113,243],"Live2D","kYMvcs0bNv3W-P72SsfYWsfTvFrQ6K1R_p_8aZx_feY",{"id":246,"title":247,"body":248,"date":875,"description":252,"extension":93,"meta":876,"navigation":97,"path":885,"seo":886,"stem":887,"tags":888,"__hash__":890},"blogs\u002F_legacy\u002F2019\u002F2019-11-24-ue4-characer-move-note.md","UE4人物同步机制小结",{"type":8,"value":249,"toc":829},[250,253,256,259,262,265,268,279,282,289,297,300,303,306,310,313,319,322,325,331,334,344,352,355,358,364,367,370,373,376,379,382,385,388,391,394,398,401,407,410,413,416,419,422,425,428,431,434,437,440,443,446,452,455,458,473,476,479,482,485,488,491,494,497,500,504,507,511,518,521,524,527,530,533,536,539,542,548,551,554,557,560,563,570,573,576,579,582,585,591,594,597,600,603,607,614,620,623,626,629,632,635,639,642,645,651,654,657,661,664,671,682,686,689,692,695,698,701,704,708,711,714,717,720,723,726,729,732,735,741,744,749,752,757,760,765,768,772,775,778,781,785,788,791,794,798,801,804,807,811,814,817,820,823,826],[11,251,252],{},"虚幻自带的人物网络同步移动机制在结构上有些复杂，这边刚好最近有接触到，稍微整理下逻辑。",[11,254,255],{},"当前的UE4版本为4.23。",[11,257,258],{},"由于部分描述可能是来自项目魔改引擎的经验，所以可能会与实际引擎的工作方式稍有出入。",[11,260,261],{},"按照Role的不同，人物在主控端、服务器端和模拟端分别使用不同的逻辑进行移动模拟。这里只记录了DS的情况，如果是Listen Server的话，在一些细节的部分会稍微有些不同，由于目前没有具体的接触过，便没有详细的考究过。",[11,263,264],{},"在机制上，操作者(ROLE_AutonomousProxy)首先进行移动，然后将移动数据发送到服务器(ROLE_Authority)，服务器进行模拟后判定是否接受操作者上报的位置，并在一帧的末尾将新的角色位置信息同步到模拟端(ROLE_SimulatedProxy)。",[27,266,267],{"id":267},"服务器",[11,269,270,271,274,275,278],{},"服务器在处理上的逻辑相对比较单纯一些，虽然主控端会根据不同的情况使用不同的RPC来对数据进行发送。到最后都会归结到两个函数上：",[41,272,273],{},"ServerMoveOld","和",[41,276,277],{},"ServerMove","。",[11,280,281],{},"这两个函数的内部逻辑很相似，其中，ServerMove负责主要的移动数据处理，而ServerMoveOld则处理旧的关键包，详细的逻辑整理在主控端逻辑中。",[11,283,284,285,288],{},"服务器使用",[41,286,287],{},"FNetworkPredictionData_Server_Character","来对移动状态进行维护。很多计算的中间和缓存量都会保存在这个数据结构里面。",[11,290,291,292,296],{},"另外，逻辑上，服务器在没有接受到主控端上报的情况下，是不会进行移动模拟的。为了防止出现一些问题，有一个",[155,293,295],{"href":294},"\u002F2019-11-24-ue4-characer-move-note\u002F#ForcePositionUpdate","ForcePositionUpdate","的机制来对服务端行为进行保护。",[172,298,273],{"id":299},"servermoveold",[11,301,302],{},"目前所有的移动都是走的非可靠包发送的，而ServerMoveOld就是为了防止关键性的移动包被丢包而存在的一个保险机制。",[11,304,305],{},"这个函数的处理相对简单，只作了两个服务器同步的核心处理。",[307,308,309],"h4",{"id":309},"时间戳转验证和转换",[11,311,312],{},"为了保证移动计算的精度，虚幻在移动上报中使用的时间戳并不是光卡时间。而是一个经过回转处理的时间戳。这个设定值是:",[34,314,317],{"className":315,"code":316,"language":39},[37],"\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.\n We do this as we use floats for TimeStamps, and server derives DeltaTime from two TimeStamps. \n As time goes on, accuracy decreases from those floating point numbers.\n So we trigger a TimeStamp reset at regular intervals to maintain a high level of accuracy. *\u002F\nUPROPERTY()\nfloat MinTimeBetweenTimeStampResets;\n",[41,318,316],{"__ignoreMap":43},[11,320,321],{},"整个时间戳检查的逻辑都在VerifyClientTimeStamp这个函数中，同时在检查到回转发生的情况下也会对NetworkPredictionData中还会用到的时间戳进行回转处理。",[307,323,324],{"id":324},"服务端模拟",[11,326,327,328,278],{},"在经过时间戳验证后，会根据上报的时间戳来计算一个",[41,329,330],{},"DeltaTime",[11,332,333],{},"虚幻有自带一个时间补偿的反加速机制，但是由于粒度有点粗，所以并没有用过。在没有触发这个机制的情况下，会根据本次的时间戳于上一次的时间戳差来计算一个差值。",[11,335,336,337,339,340,343],{},"当然，服务器不会无限度的接受客户端的",[41,338,330],{},"，考虑到丢包和作弊的情况，有一个 ",[41,341,342],{},"MaxDeltaTime","会对单次处理的最大时间差作限制，这个值的设置是0.125s。",[11,345,346,347,351],{},"取出Delta后，就会调用",[155,348,350],{"href":349},"\u002F2019-11-24-ue4-characer-move-note\u002F#i-19","MoveAutonomous","进行移动的模拟。",[172,353,277],{"id":354},"servermove",[11,356,357],{},"这个函数承担了服务器移动模拟的更多的检查工作，在主控端上报上，由于会出现合包的情况，会有传入位置是特殊的(1,2,3)的情况，例如在ServerMoveDual_Implementation中就有：",[34,359,362],{"className":360,"code":361,"language":39},[37],"ServerMove_Implementation(TimeStamp0, InAccel0, FVector(1.f,2.f,3.f), PendingFlags, ClientRoll, View0, ClientMovementBase, ClientBaseBone, ClientMovementMode);\nServerMove_Implementation(TimeStamp, InAccel, ClientLoc, NewFlags, ClientRoll, View, ClientMovementBase, ClientBaseBone, ClientMovementMode);\n",[41,363,361],{"__ignoreMap":43},[11,365,366],{},"如果在ServerMove作了特殊处理的话需要留意。",[11,368,369],{},"与ServerMoveOld相比，这个函数中多作了几个处理：",[307,371,372],{"id":372},"状态检查和旋转处理",[11,374,375],{},"通过调用NotifyServerReceivedClientData来检查服务器是否处在可以接受移动包的状态。",[11,377,378],{},"同时在执行移动模拟前会将旋转状态传到Controller那边去作处理。",[307,380,381],{"id":381},"位置受理检查",[11,383,384],{},"主要在ServerMoveHandleClientError中进行的一个检查，这里的检查结果将会决定服务器是否接受上报的移动，如果不接受的话会对主控端进行修正。如果接受位置且有设置ClientAuthorativePosition的话，会将服务器的位置设置到上报的位置上去。",[11,386,387],{},"对位置的受理或者修正并不是在这里进行的，而是会将标志位以及必要的信息存在ServerData中，之后再进行处理。",[11,389,390],{},"这边的结果其实有时候比较难以预知，因为前面的ServerMoveOld并没有作这个检查，导致服务器的模拟基础难以预期。",[11,392,393],{},"这些检查的结果也是缓存在ServerData中的。",[395,396,397],"h5",{"id":397},"调用频率控制",[11,399,400],{},"这个频率控制是基于ServerData中的LastUpdateTime的，这个时间会在服务器决定对客户端进行修正时更新。",[34,402,405],{"className":403,"code":404,"language":39},[37],"APlayerController* PC = Cast\u003CAPlayerController>(CharacterOwner->GetController());\nif( (ServerData->LastUpdateTime != GetWorld()->TimeSeconds))\n{\n  const AGameNetworkManager* GameNetworkManager = (const AGameNetworkManager*)(AGameNetworkManager::StaticClass()->GetDefaultObject());\n  if (GameNetworkManager->WithinUpdateDelayBounds(PC, ServerData->LastUpdateTime))\n  {\n    return;\n  }\n}\n",[41,406,404],{"__ignoreMap":43},[11,408,409],{},"也就是说，在当前帧没有修正过主控端位置的情况下，检查是否最近修正过主控端位置。如果有配置对应的频率限制的话，会不进行受理检查。在这种情况下，服务器的移动状态与ServerMoveOld的处理有些类似。",[11,411,412],{},"当然，在ServerData中的强制更新标签等状态也会一直保留，等到下一次进入这个函数会继续起作用。",[395,414,415],{"id":415},"差异检查",[11,417,418],{},"在检查之前会对地板状态进行处理，主要是对动态地板的位置转换，以及对客户端明明在Walking却没有地板的情况进行修正。",[11,420,421],{},"如果此时ServerData中有设置bForceClientUpdate为True的话，就强制进行位置修正。其他情况则会调用ServerCheckClientError对主控端和服务端的移动状态进行比对。",[11,423,424],{},"比对的主要逻辑在ServerCheckClientError中，主要就是对两端的位置差异进行比较，如果超过了阈值则会触发修正。以及，如果两端的MovementMode不一致，则也会强制的触发修正。",[11,426,427],{},"是否受理会决定ServerData中bAckGoodMove的值，影响之后下发数据到主控端的分支选择。",[11,429,430],{},"以及，如果开启了对应选项的话，在受理的同时也会更新服务器的位置到上报位置。",[172,432,295],{"id":433},"forcepositionupdate",[11,435,436],{},"在逻辑上，服务器在收到客户端的数据包之前是不会进行移动的。",[11,438,439],{},"但是如果实际上这样运行的话，会有很多问题。例如，如果玩家在跳到空中之后掉线或者屏蔽ServerMove的话，就可能会一直留在空中了。",[11,441,442],{},"因此，在PlayerController中，有一个额外的ForcePositionUpdate的逻辑。",[11,444,445],{},"这个逻辑会根据配置的MAXCLIENTUPDATEINTERVAL来对移动状态进行检查，如果有一段时间没有收到客户端的ServerMove的话，会强制调用ForcePositionUpdate来对服务器的运动进行模拟。",[34,447,450],{"className":448,"code":449,"language":39},[37],"const float TimeSinceUpdate = GetWorld()->GetTimeSeconds() - ServerData->ServerTimeStamp;\nconst float PawnTimeSinceUpdate = TimeSinceUpdate * GetPawn()->CustomTimeDilation;\nif (PawnTimeSinceUpdate > FMath::Max\u003Cfloat>(DeltaSeconds+0.06f, AGameNetworkManager::StaticClass()->GetDefaultObject\u003CAGameNetworkManager>()->MAXCLIENTUPDATEINTERVAL * GetPawn()->GetActorTimeDilation()))\n{\n  \u002F\u002FUE_LOG(LogPlayerController, Warning, TEXT(\"ForcedMovementTick. PawnTimeSinceUpdate: %f, DeltaSeconds: %f, DeltaSeconds+: %f\"), PawnTimeSinceUpdate, DeltaSeconds, DeltaSeconds+0.06f);\n  const USkeletalMeshComponent* PawnMesh = GetPawn()->FindComponentByClass\u003CUSkeletalMeshComponent>();\n  if (!PawnMesh || !PawnMesh->IsSimulatingPhysics())\n  {\n    \u002F\u002F We are setting the ServerData timestamp BEFORE updating position below since that may cause ServerData to become deleted (like if the pawn was unpossessed as a result of the move)\n    \u002F\u002F Also null the pointer to make sure no one accidentally starts using it below the call to ForcePositionUpdate\n    ServerData->ServerTimeStamp = GetWorld()->GetTimeSeconds();\n    ServerData = nullptr;\n\n    NetworkPredictionInterface->ForcePositionUpdate(PawnTimeSinceUpdate);\n  }\n}\n",[41,451,449],{"__ignoreMap":43},[11,453,454],{},"最终会调用到PerformMovemen来进行移动的模拟。",[172,456,457],{"id":457},"数据下发",[11,459,460,461,464,465,468,469,472],{},"这里指的主要是针对主控端的数据下发，模拟端的数据是通过正常的值复制逻辑进行的。也就是说是在",[41,462,463],{},"PreReplicate","阶段通过",[41,466,467],{},"GatherCurrentMovement","收集到",[41,470,471],{},"ReplicatedMovement","然后值复制下行的。",[11,474,475],{},"服务器上单帧内，可能会收到很多ServerMove的移动包，其处理结果会被缓存到ServerData之中。",[11,477,478],{},"这个逻辑的核心处理逻辑在SendClientAdjustment中，会在引擎的数据下发阶段，在ServerReplicateActors中直接通过PlayerController调用到。",[11,480,481],{},"ServerData中影响最大的数据是bAckGoodMove，这个标记为True时，就只会调用ClientAckGoodMove来通知主控端服务器已经接受了本次移动，这里下发的是客户端上行的时间戳。如果标记为False，则会对主控端进行修正，根据情况的不同会走不同的RPC进行下发以保证只会发送必要的数据。",[11,483,484],{},"两种下发方式都有各自的频率控制，详细的逻辑可以在UCharacterMovementComponent::SendClientAdjustment进行查看。",[27,486,487],{"id":487},"主控端",[11,489,490],{},"主控端是接受玩家直接控制的，所以在逻辑上是最为复杂的。",[11,492,493],{},"所以，与服务器不同，并不是单纯的一个ServerData的缓存，而是一个SavedMoves的数组在对移动状态缓存。",[172,495,496],{"id":496},"服务器状态接收",[11,498,499],{},"从单帧的时序上，最早执行的是服务器状态回复包的检查。",[307,501,503],{"id":502},"ackgood","AckGood",[11,505,506],{},"如果服务器回报的是AckGoodMove的话，在整体的逻辑上会比较简单。就只是根据回复的时间戳，到历史队列中找到对应的移动包，并将在其之前的缓存包全部删除掉。因为既然服务器已经接受了这个包，那么之前的移动缓存就没有用了，这个逻辑在UCharacterMovementComponent::ClientAckGoodMove_Implementation中。",[307,508,510],{"id":509},"adjust","Adjust",[11,512,513,514,517],{},"如果是修正包的话，情况会稍微有些复杂。不过与服务器类似的是，虽然有很多RPC的通道，到最后会归结到到",[41,515,516],{},"ClientAdjustPosition","中。当然不同的RPC会有一些独自的处理，不过由于没有使用过RootMotion之类的，这边只记录与移动逻辑有光的部分。",[11,519,520],{},"数据修正时首先还是对本地缓存进行更新，这里有个特殊的丢包处理。",[11,522,523],{},"如果下发的标志中有表明地板信息而下发的地板指针却是空的话，则会认定地板还没有值复制成功，暂时不处理这个修正。",[11,525,526],{},"之后同样的会根据时间戳对缓存数据进行删除，并通过OnClientCorrectionReceived发送事件出去。",[11,528,529],{},"然后会将本地位置和状态都设置到服务器下发的位置上去，同时设置ClientData标签bUpdatePosition为True。这个标签是为了之后重放移动准备的，由于移动重放操作还是比较重的，而且如果每个修正操作都会设置位置的话，在单次的RPC中设置就没有什么意义，因为如果又收到一个修正包的话就只是凭空的浪费了运算过程。",[172,531,532],{"id":532},"缓存重放",[11,534,535],{},"在进行了位置修正的情况下，需要对缓存的移动进行重放。",[11,537,538],{},"这个操作是在TickComponent中执行的。",[11,540,541],{},"移动的重放最后也是调用MoveAutonomous，在重放操作中会对必要的数据进行缓存并在之后重新设置回去。",[34,543,546],{"className":544,"code":545,"language":39},[37],"\u002F\u002F Save important values that might get affected by the replay.\nconst float SavedAnalogInputModifier = AnalogInputModifier;\nconst FRootMotionMovementParams BackupRootMotionParams = RootMotionParams; \u002F\u002F For animation root motion\nconst FRootMotionSourceGroup BackupRootMotion = CurrentRootMotion;\nconst bool bRealJump = CharacterOwner->bPressedJump;\nconst bool bRealCrouch = bWantsToCrouch;\nconst bool bRealForceMaxAccel = bForceMaxAccel;\nCharacterOwner->bClientWasFalling = (MovementMode == MOVE_Falling);\nCharacterOwner->bClientUpdating = true;\nbForceNextFloorCheck = true;\n\n\u002F\u002F Replay moves that have not yet been acked.\nUE_LOG(LogNetPlayerMovement, Verbose, TEXT(\"ClientUpdatePositionAfterServerUpdate Replaying %d Moves, starting at Timestamp %f\"), ClientData->SavedMoves.Num(), ClientData->SavedMoves[0]->TimeStamp);\nfor (int32 i=0; i\u003CClientData->SavedMoves.Num(); i++)\n{\n  FSavedMove_Character* const CurrentMove = ClientData->SavedMoves[i].Get();\n  checkSlow(CurrentMove != nullptr);\n  CurrentMove->PrepMoveFor(CharacterOwner);\n  MoveAutonomous(CurrentMove->TimeStamp, CurrentMove->DeltaTime, CurrentMove->GetCompressedFlags(), CurrentMove->Acceleration);\n  CurrentMove->PostUpdate(CharacterOwner, FSavedMove_Character::PostUpdate_Replay);\n}\n\nif (FSavedMove_Character* const PendingMove = ClientData->PendingMove.Get())\n{\n  PendingMove->bForceNoCombine = true;\n}\n\n\u002F\u002F Restore saved values.\nAnalogInputModifier = SavedAnalogInputModifier;\nRootMotionParams = BackupRootMotionParams;\nCurrentRootMotion = BackupRootMotion;\n",[41,547,545],{"__ignoreMap":43},[11,549,550],{},"逻辑上还是比较单纯的，虽然看上去会处理很多数据，但是由于主控端每帧只会对自己做一次这个操作，运算消耗还是在可控制范围内的。",[172,552,553],{"id":553},"移动并上报",[11,555,556],{},"在移动重放之后，会在ReplicateMoveToServer中对当帧的移动进行模拟并上报。",[307,558,559],{"id":559},"状态更新",[11,561,562],{},"由于真正的移动操作就只是调用PerformMovement，所以实际上这个函数主要处理的移动上报和缓存更新。",[11,564,565,566,569],{},"在",[41,567,568],{},"UpdateTimeStampAndDeltaTime","中进行时间戳的更新，主要处理的是CurrentTimeStamp的累计、时间戳回转以及必要的Clamp操作。",[11,571,572],{},"接下来，会从缓存的移动中，以IsImportantMove为标准尝试寻找出一个最旧的关键移动包作为OldMove。",[307,574,575],{"id":575},"移动模拟",[11,577,578],{},"这里会有一个PendingMove和NewMove的逻辑，主要的目的是尝试对移动进行合并，减少不必要的数据传输。",[11,580,581],{},"在移动之前，会对PendingMove进行检查，如果通过了CanCombineWith的话，就会对NewMove进行CombineWith操作来将移动操作合并。",[11,583,584],{},"PendingMove本身是在移动后生成的，如果单次移动满足条件，就会尝试进行缓存并不再继续进行发包操作。",[34,586,589],{"className":587,"code":588,"language":39},[37],"const bool bCanDelayMove = (CharacterMovementCVars::NetEnableMoveCombining != 0) && CanDelaySendingMove(NewMovePtr);\n\nif (bCanDelayMove && ClientData->PendingMove.IsValid() == false)\n{\n  \u002F\u002F Decide whether to hold off on move\n  const float NetMoveDelta = FMath::Clamp(GetClientNetSendDeltaTime(PC, ClientData, NewMovePtr), 1.f\u002F120.f, 1.f\u002F5.f);\n\n  if ((MyWorld->TimeSeconds - ClientData->ClientUpdateTime) * MyWorld->GetWorldSettings()->GetEffectiveTimeDilation() \u003C NetMoveDelta)\n  {\n    \u002F\u002F Delay sending this move.\n    ClientData->PendingMove = NewMovePtr;\n    return;\n  }\n}\n",[41,590,588],{"__ignoreMap":43},[172,592,593],{"id":593},"数据上行",[11,595,596],{},"数据上行在之后通过CallServerMove进行，这里能看到一些用于移动丢包测试的Console。",[11,598,599],{},"在CallServerMove中的逻辑其实就只是根据不同的情况调用不同的RPC进行数据上报，MoveOld是单独通过ServerMoveOld进行的，NewMove虽然操作会有不同，最后都会抵达ServerMove。",[11,601,602],{},"这里其实主要的区分是在PendingMove上，如果合并包失败的话，这边就会使用Dual的RPC，将两次移动一并带上去。",[172,604,606],{"id":605},"misc","Misc",[11,608,609,610,613],{},"有一个特殊的标签",[41,611,612],{},"bIgnoreClientMovementErrorChecksAndCorrection","可以暂停掉服务器和主控端的修正处理流程。",[11,615,616,619],{},[41,617,618],{},"FlushServerMoves","会将所有缓存的移动全部作为NewMove通过CallServerMove发送出去，不过当前只有UE4自带的技能系统有使用到。",[27,621,622],{"id":622},"模拟端",[11,624,625],{},"模拟端的角色移动是被动的，在逻辑上特殊的地方，其实只有一个Mesh与Actor分离插值的过程。",[172,627,628],{"id":628},"值复制",[11,630,631],{},"模拟端的处理是从值复制的OnRep操作开始的，通常这个操作在OnRep_ReplicatedMovement中传递到ACharacter::PostNetReceiveLocationAndRotation；对于有动态地板的情况，会在OnRep_ReplicatedBasedMovement中。作为插值参考的ReplicatedServerLastTransformUpdateTimeStamp是单独值复制的。",[11,633,634],{},"对于值复制的处理分为两个主要的部分：SmoothCorrection和OnUpdateSimulatedPosition。",[307,636,638],{"id":637},"smoothcorrection","SmoothCorrection",[11,640,641],{},"在插值模式为需要插值的情况，这里会对Mesh与Actor的偏差MeshTranslationOffset以及MeshRotationOffset进行更新。",[11,643,644],{},"有两个限制值，保证Actor本身的新的位置与当前的位置的偏差NewToOldVector不会过大的拉大插值距离：",[34,646,649],{"className":647,"code":648,"language":39},[37],"const float DistSq = NewToOldVector.SizeSquared();\nif (DistSq > FMath::Square(ClientData->MaxSmoothNetUpdateDist))\n{\n  ClientData->MeshTranslationOffset = (DistSq > FMath::Square(ClientData->NoSmoothNetUpdateDist))\n    ? FVector::ZeroVector\n    : ClientData->MeshTranslationOffset + ClientData->MaxSmoothNetUpdateDist * NewToOldVector.GetSafeNormal();\n}\nelse\n{\n  ClientData->MeshTranslationOffset = ClientData->MeshTranslationOffset + NewToOldVector;\n}\n",[41,650,648],{"__ignoreMap":43},[11,652,653],{},"之后就是将UpdatedComponent的位置移动到新的位置，这里有一个帮助类FScopedPreventAttachedComponentMove，保证移动操作不会变更Mesh的位置。",[11,655,656],{},"接下来就是一些时间戳的维护操作，主要是确保SmoothingClientTimeStamp与SmoothingServerTimeStamp能够在一个合理的容错范围内。",[307,658,660],{"id":659},"onupdatesimulatedposition","OnUpdateSimulatedPosition",[11,662,663],{},"这边的逻辑比较单纯，只是作一些状态设置。",[11,665,666,667,670],{},"如果位置发生了变动的话，会将",[41,668,669],{},"bJustTeleported","设置为true。",[11,672,673,674,677,678,681],{},"如果下发的运动状态没有速度却发生了位置改变的话，会对目标位置作一次",[41,675,676],{},"EncroachingBlockingGeometry","检查，如果位置被修正了的话，会启用",[41,679,680],{},"bSimGravityDisabled","标志，防止之后的模拟导致不可预知的下坠。",[172,683,685],{"id":684},"simulatedtick","SimulatedTick",[11,687,688],{},"模拟端的插值和模拟逻辑由TickComponnet在SimulatedTick中进行。",[307,690,691],{"id":691},"执行移动",[11,693,694],{},"模拟移动的逻辑在SimulateMovement中，这个函数与PerformMovement相比，进行了大幅的简化。",[11,696,697],{},"对于SkipProxyPredictionOnNetUpdate开启了的情况，如果当前帧进行了网络更新，则不会进行移动模拟。",[11,699,700],{},"如果有相应的标签的话，会尝试对网络下发的移动状态进行应用以及对地板进行更新。",[11,702,703],{},"在状态设置过之后，就会调用MoveSmooth来进行模拟端的移动模拟。",[307,705,707],{"id":706},"mesh位置插值","Mesh位置插值",[11,709,710],{},"这个过程分为两个部分，SmoothClientPosition_Interpolate负责逻辑更新，SmoothClientPosition_UpdateVisuals负责实际的Mesh位置移动。",[11,712,713],{},"由于插值过程中的Mesh移动不会Sweep，所以会有穿墙的可能性，但是由于位置是由服务器下发的，所以实际上穿墙的情况还是比较短暂和罕见的。",[11,715,716],{},"插值的位置可以使用p.NetVisualizeSimulatedCorrections来进行一定程度的观察。",[11,718,719],{},"插值模式主要在Linear和Exponential上，这两者除了计算方式不同之外，Linear会在插值过程中有一定的“预测”作用，所以在取舍上需要留意。",[27,721,575],{"id":722},"移动模拟-1",[11,724,725],{},"移动模拟的逻辑，主要在MoveAutonomous中，而在模拟端有一个弱化的MoveSmooth。",[11,727,728],{},"在实际的使用中，有的逻辑会在处理状态之后直接调用到PerformMovement中，基本上主控端和服务器都会最后使用到PerfromMovement，而模拟端只会使用MoveSmooth。",[11,730,731],{},"除了状态的准备之外，PerformMovement最后的移动逻辑以StartNewPhysics的计算为中心，而模拟端用的MoveSmooth则使用了一个弱化的计算方式。",[11,733,734],{},"在MoveAutonomous中会使用bClientUpdating来决定是否对Mesh姿态进行更新。有几个比较特别的处理需要留意",[11,736,737],{},[738,739,740],"strong",{},"CompressedFlags",[11,742,743],{},"这里面会对一些特殊的移动状态进行压缩，当前默认的有bPressedJump和bWantsToCrouch。服务器在解开这些标志位之后会尝试让移动进入对应的状态。",[11,745,746],{},[738,747,748],{},"ApplyAccumulatedForces",[11,750,751],{},"移动模拟并不会实际执行物理模拟，所以对于AddForce等一些列物理操作，会缓存起来，最后在这里实际的叠加到模拟中去。",[11,753,754],{},[738,755,756],{},"HandlePendingLaunch",[11,758,759],{},"这边是和上面的冲量系列一样，对物理操作的“模拟”。",[11,761,762],{},[738,763,764],{},"加速度",[11,766,767],{},"会有一个AnalogInputModifier的处理以及MaxAcceleration和竖直加速度清理的操作，加速度最后会反应到速度计算上。",[172,769,771],{"id":770},"startnewphysics","StartNewPhysics",[11,773,774],{},"这个才是真正意义上进行移动模拟的地方，会根据不同的MovementMode来对移动进行按照DeltaTime的分段模拟，同时也会处理移动模式的切换和继续模拟。",[11,776,777],{},"具体的过程可以实际在函数中进行观测。",[11,779,780],{},"操作上，首先是通过CalcVelocity来计算速度，包括了对AI Path Follow的处理以及正常玩家操作的处理。同时会对Friction等进行计算，保证阻力等被正常应用到速度计算中。",[307,782,784],{"id":783},"findfloor","FindFloor",[11,786,787],{},"如果在移动之后没有地板信息的话，会通过这个函数来寻找地板。",[11,789,790],{},"其中一个关键性的函数时ComputeFloorDist，由于其中有两种模式的计算。一种是通过向下的LineTrace，另一种是通过胶囊体的Sweep，对于一些不符合预期的站在物体的边缘或卡进动态物体的情况，可以在这里检查和作特殊处理。",[11,792,793],{},"对于Walking的情况，如果地板变更的话，会有一个HandleWalkingOffLedge的处理。对于新找到的地板不能站立的情况，会尝试回退单次模拟、改变方向或者尝试跳跃。这里的单次模拟是指StartNewPhysics内部分割后的模拟。",[307,795,797],{"id":796},"movealongfloor","MoveAlongFloor",[11,799,800],{},"这个是Walking时用来进行角色移动的函数，到这里其实代码上的逻辑还是比较清晰的，这里就不多作赘述了。其中有一个SlideAlongSurface的处理，是一个在移动模拟中比较常用到的贴边移动函数。里面除了ComputeSlideVector之外，还有一个TwoWallAdjust的回避处理，如果感兴趣的话可以深入的看一下，不过实际上如果没有出问题的话，这边的细节处理一下就会忘记了……",[11,802,803],{},"这里面还有一个非常精细的StepUp然后StepDown的操作，详细的逻辑可以查看UCharacterMovementComponent::StepUp。",[11,805,806],{},"通常如果移动进入奇怪的状态的话，从MoveAlongFloor来检查比较好。",[172,808,810],{"id":809},"movesmooth","MoveSmooth",[11,812,813],{},"相比StartNewPhyscis，这个给模拟端用的函数逻辑非常简单。",[11,815,816],{},"如果是在地面上移动的话，会使用MoveAlongFloor来更新，否则直接用SafeMoveUpdatedComponent来移动并作StepUp和SlideAlongSurface的操作。",[11,818,819],{},"此外UCharacterMovementComponent::SimulateMovement也会作一些基本的状态更新，在移动模拟之后会对地板和Faling状态进行更新。",[27,821,822],{"id":822},"总结",[11,824,825],{},"这边没有办法列出很多细节，而且实现方式也会随着版本出现变化，所以记录下来主要是方便进行快速的逻辑查找。",[11,827,828],{},"移动模拟在三个端各自有不同的逻辑，所以在出现问题时有时候查起来会比较头痛，希望这里的内容至少能够帮助缩短到达问题原因的路径~",{"title":43,"searchDepth":87,"depth":88,"links":830},[831,844,857,867,874],{"id":267,"depth":87,"text":267,"children":832},[833,838,842,843],{"id":299,"depth":88,"text":273,"children":834},[835,837],{"id":309,"depth":836,"text":309},4,{"id":324,"depth":836,"text":324},{"id":354,"depth":88,"text":277,"children":839},[840,841],{"id":372,"depth":836,"text":372},{"id":381,"depth":836,"text":381},{"id":433,"depth":88,"text":295},{"id":457,"depth":88,"text":457},{"id":487,"depth":87,"text":487,"children":845},[846,850,851,855,856],{"id":496,"depth":88,"text":496,"children":847},[848,849],{"id":502,"depth":836,"text":503},{"id":509,"depth":836,"text":510},{"id":532,"depth":88,"text":532},{"id":553,"depth":88,"text":553,"children":852},[853,854],{"id":559,"depth":836,"text":559},{"id":575,"depth":836,"text":575},{"id":593,"depth":88,"text":593},{"id":605,"depth":88,"text":606},{"id":622,"depth":87,"text":622,"children":858},[859,863],{"id":628,"depth":88,"text":628,"children":860},[861,862],{"id":637,"depth":836,"text":638},{"id":659,"depth":836,"text":660},{"id":684,"depth":88,"text":685,"children":864},[865,866],{"id":691,"depth":836,"text":691},{"id":706,"depth":836,"text":707},{"id":722,"depth":87,"text":575,"children":868},[869,873],{"id":770,"depth":88,"text":771,"children":870},[871,872],{"id":783,"depth":836,"text":784},{"id":796,"depth":836,"text":797},{"id":809,"depth":88,"text":810},{"id":822,"depth":87,"text":822},"2019-11-24",{"layout":95,"status":96,"published":97,"author":877,"author_login":100,"author_email":101,"wordpress_id":878,"wordpress_url":879,"date_gmt":880,"excerpt":881},{"display_name":99,"login":100,"email":101,"url":43},2737,"\u002F?p=2737","2019-11-24 04:25:50 +0000",{"type":8,"value":882},[883],[11,884,252],{},"\u002F2019-11-24-ue4-characer-move-note",{"title":247,"description":252},"_legacy\u002F2019\u002F2019-11-24-ue4-characer-move-note",[113,889],"移动","TgqzY2DceBGglhDonmrhmQgQCM1ohi5zbysHPDMwH1o",{"id":892,"title":893,"body":894,"date":980,"description":898,"extension":93,"meta":981,"navigation":97,"path":990,"seo":991,"stem":992,"tags":993,"__hash__":994},"blogs\u002F_legacy\u002F2019\u002F2019-10-31-ue4-singleton-note.md","UE4单例相关",{"type":8,"value":895,"toc":976},[896,899,902,905,908,915,921,927,930,933,942,948,951,973],[11,897,898],{},"有很多关于设计模式的书都不建议使用单例，但是实际在很多地方还是会用到。",[11,900,901],{},"因为单例实在是太方便了，而且很“干净”。",[27,903,904],{"id":904},"编辑器中使用",[11,906,907],{},"在UE4的编辑器中使用单例最大的问题是，默认的调试模式下，所有的PIE共用一个。",[11,909,910,911,914],{},"如果要进行DS的调试的话，很容易出现一些没有预料的问题。这个时候可以选择使用",[41,912,913],{},"GEditorID","来分离。",[11,916,917,918,920],{},"由于",[41,919,913],{},"的特殊性质，可以实现一个奇怪的Trick，能够在不使用RPC的情况下直接在编辑器对服务器上的位置信息进行调试绘制：",[34,922,925],{"className":923,"code":924,"language":39},[37],"#if !UE_BUILD_SHIPPING \nif(IS_CLIENT) \n{ \n  UWorld* tp_World = GetWorld(); \n  if (tp_World) \n  { \n    DrawDebugSphere(tp_World, AActor::GetActorLocation(), 30.0f, 7, FColor::Green, false, 4.0f);\n    DrawDebugSphere(tp_World, ReplicatedMovement.Location, 35.0f, 7, FColor::Blue, false, 4.0f); \n  } \n} \n#if WITH_EDITOR \nif (IS_SERVER) \n{ \n    UWorld* tp_World = GEditor->GetWorldContextFromPIEInstance(2)->World(); \n    DrawDebugSphere(tp_World, AActor::GetActorLocation(), 40.0f, 7, FColor::Red, false, 4.0f); \n} \n#endif\n\n#endif\n",[41,926,924],{"__ignoreMap":43},[11,928,929],{},"如果编译没过的话，注意检查是否引用了UnrealEd这个Module。",[27,931,932],{"id":932},"新版本提供的单例类",[11,934,935,936,941],{},"之前的引擎版本中要使用与引擎相关的单例实现总会有各种各样的麻烦，从UE4.22开始，引擎提供了一组新的类型，包括",[155,937,940],{"href":938,"rel":939},"https:\u002F\u002Fdocs.unrealengine.com\u002Fen-US\u002FAPI\u002FRuntime\u002FEngine\u002FSubsystems\u002FUGameInstanceSubsystem\u002Findex.html",[159],"UGameInstanceSubsystem","等。可以方便的在引擎内部实现单例的功能，实际进行测试的话：",[34,943,946],{"className":944,"code":945,"language":39},[37],"void UMyGameInstanceSubsystem::RefreshAndPrint() \n{ \n  int32 td_LastId = MyGameId; \n  MyGameId = GPlayInEditorID; \n  UE_LOG(LogTemp, Log, TEXT(\" UMyGameInstanceSubsystem::RefreshAndPrint: %d->%d || %p\"), td_LastId, MyGameId, this); \n}\n",[41,947,945],{"__ignoreMap":43},[11,949,950],{},"输出的结果如下：",[952,953,954,957,959,962,964,967,969,971],"blockquote",{},[11,955,956],{},"LogTemp: UMyGameInstanceSubsystem::RefreshAndPrint: 2->2 || 00000184F8152AC0",[11,958,956],{},[11,960,961],{},"LogTemp: UMyGameInstanceSubsystem::RefreshAndPrint: 3->3 || 00000184F8152570",[11,963,961],{},[11,965,966],{},"LogTemp: UMyGameInstanceSubsystem::RefreshAndPrint: 1->1 || 00000184F8153FB0",[11,968,966],{},[11,970,956],{},[11,972,956],{},[11,974,975],{},"这样的话就可以不用顾及一些其他的细节问题了。",{"title":43,"searchDepth":87,"depth":88,"links":977},[978,979],{"id":904,"depth":87,"text":904},{"id":932,"depth":87,"text":932},"2019-10-31",{"layout":95,"status":96,"published":97,"author":982,"author_login":100,"author_email":101,"wordpress_id":983,"wordpress_url":984,"date_gmt":985,"excerpt":986},{"display_name":99,"login":100,"email":101,"url":43},2712,"\u002F?p=2712","2019-10-31 07:40:14 +0000",{"type":8,"value":987},[988],[11,989,898],{},"\u002F2019-10-31-ue4-singleton-note",{"title":893,"description":898},"_legacy\u002F2019\u002F2019-10-31-ue4-singleton-note",[113],"fxXaOz18H1iEySDeB5DX_qBNCeagnDfDL6AfYsCuJ9Q",{"id":996,"title":997,"body":998,"date":1151,"description":1002,"extension":93,"meta":1152,"navigation":97,"path":1161,"seo":1162,"stem":1163,"tags":1164,"__hash__":1165},"blogs\u002F_legacy\u002F2019\u002F2019-09-08-ue4-reflection-note.md","UE4属性反射小结",{"type":8,"value":999,"toc":1140},[1000,1003,1009,1012,1015,1018,1022,1025,1031,1042,1045,1051,1054,1057,1063,1067,1070,1074,1077,1083,1086,1092,1095,1098,1104,1108,1111,1114,1120,1123,1129,1131,1137],[11,1001,1002],{},"UProperty是UE4自己建立的一个反射系统的一部分，这里稍微整理下接触到的东西。",[11,1004,1005,1006],{},"平时使用时，如果声明使用了属性系统，就必须在头文件上包含",[41,1007,1008],{},"*.generated.h",[11,1010,1011],{},"这样的话UE4就会“自动”的帮你完成反射部分的工作。这部分依赖于UBT和UHT，一般情况下不需要在意。",[11,1013,1014],{},"有的时候加完新的.h会提示这个文件不存在，一般情况下重新Generate一下项目就好了。",[11,1016,1017],{},"之所以动到这里是因为之前有个导出的需求，发现有个属性是Private的而有定义成UProperty。由于是制作插件所以不能修改引擎代码，所以只能从旁边绕路了。",[27,1019,1021],{"id":1020},"uproperty相关读取","UProperty相关读取",[11,1023,1024],{},"对于指定Class里面的所有属性，可以直接使用帮助类来遍历：",[34,1026,1029],{"className":1027,"code":1028,"language":39},[37],"for (TFieldIterator\u003CUProperty> PropIt(GetClass()); PropIt; ++PropIt) \n{ \n  UProperty* Property = *PropIt; \u002F\u002F Do something with the property \n}\n",[41,1030,1028],{"__ignoreMap":43},[11,1032,1033,1034,1037,1038,1041],{},"在其之上就可以使用",[738,1035,1036],{},"ContainerPtrToValuePtr","或者",[738,1039,1040],{},"GetPropertyValue","来对属性进行读取了，在BlueprintNodeHelpers::CollectPropertyDescription中也能看到使用的实例。",[11,1043,1044],{},"或者也可以封装成函数：",[34,1046,1049],{"className":1047,"code":1048,"language":39},[37],"bool GetFloatByName(UObject * Target, FName VarName, float &outFloat) \n{ \n  if (Target) \u002F\u002Fmake sure Target was set in blueprints. \n  { \n    float FoundFloat; UFloatProperty* FloatProp = FindField\u003CUFloatProperty>(Target->GetClass(), VarName);  \u002F\u002F try to find float property in Target named VarName \n    if (FloatProp) \u002F\u002Fif we found variable \n    { \n      FoundFloat = FloatProp->GetPropertyValue_InContainer(Target);  \u002F\u002F get the value from \n      FloatProp outFloat = FoundFloat;  \u002F\u002F return float \n      return true; \u002F\u002F we can return \n    } \n  } \n  return false; \u002F\u002F we haven't found variable return false \n}\n",[41,1050,1048],{"__ignoreMap":43},[172,1052,1053],{"id":1053},"蓝图属性",[11,1055,1056],{},"关于蓝图的属性载入过程，可以参考AActor::Serialize：",[34,1058,1061],{"className":1059,"code":1060,"language":39},[37],"if (const UBlueprintGeneratedClass* BPGC = Cast\u003CUBlueprintGeneratedClass>(GetClass()))\n{\n  NativeConstructedComponentToPropertyMap.Reset();\n  NativeConstructedComponentToPropertyMap.Reserve(OwnedComponents.Num());\n  for(TFieldIterator\u003CUObjectProperty> PropertyIt(BPGC, EFieldIteratorFlags::IncludeSuper); PropertyIt; ++PropertyIt)\n  {\n    UObjectProperty* ObjProp = *PropertyIt;\n    \u002F\u002F Ignore transient properties since they won't be serialized\n    if(!ObjProp->HasAnyPropertyFlags(CPF_Transient))\n    {\n      UActorComponent* ActorComponent = Cast\u003CUActorComponent>(ObjProp->GetObjectPropertyValue_InContainer(this));\n      if(ActorComponent != nullptr && ActorComponent->CreationMethod == EComponentCreationMethod::Native)\n      {\n        NativeConstructedComponentToPropertyMap.Add(ActorComponent->GetFName(), ObjProp);\n      }\n    }\n  }\n}\n",[41,1062,1060],{"__ignoreMap":43},[27,1064,1066],{"id":1065},"fguid","FGuid",[11,1068,1069],{},"这里要读取的是FGuid，当时做了两种尝试",[172,1071,1073],{"id":1072},"通过exporttextitem","通过ExportTextItem",[11,1075,1076],{},"这样做是因为FGuid有提供LexFromString方法，ExportTextItem本身封装在DescribeProperty中：",[34,1078,1081],{"className":1079,"code":1080,"language":39},[37],"FString DescribeProperty(const UProperty* Prop, const uint8* PropertyAddr)\n{\n  FString ExportedStringValue;\n  const UStructProperty* StructProp = Cast\u003Cconst UStructProperty>(Prop);\n  const UFloatProperty* FloatProp = Cast\u003Cconst UFloatProperty>(Prop);\n  if (StructProp && StructProp->GetCPPType(NULL, CPPF_None).Contains(GET_STRUCT_NAME_CHECKED(FBlackboardKeySelector)))\n  {\n    \u002F\u002F special case for blackboard key selectors\n    const FBlackboardKeySelector* PropertyValue = (const FBlackboardKeySelector*)PropertyAddr;\n    ExportedStringValue = PropertyValue->SelectedKeyName.ToString();\n  }\n  else if (FloatProp)\n  {\n    \u002F\u002F special case for floats to remove unnecessary zeros\n    const float FloatValue = FloatProp->GetPropertyValue(PropertyAddr);\n    ExportedStringValue = FString::SanitizeFloat(FloatValue);\n  }\n  else\n  {\n    Prop->ExportTextItem(ExportedStringValue, PropertyAddr, NULL, NULL, PPF_PropertyWindow, NULL);\n  }\n  const bool bIsBool = Prop->IsA(UBoolProperty::StaticClass());\n  return FString::Printf(TEXT(\"%s: %s\"), *FName::NameToDisplayString(Prop->GetName(), bIsBool), *ExportedStringValue);\n}\n",[41,1082,1080],{"__ignoreMap":43},[11,1084,1085],{},"实际使用的话是这样的：",[34,1087,1090],{"className":1088,"code":1089,"language":39},[37],"UProperty* tp_FindedProp = FindField\u003CUProperty>(tp_LandscapeHCC->GetClass(), TEXT(\"HeightfieldGuid\"));\nconst uint8* PropData = tp_FindedProp->ContainerPtrToValuePtr\u003Cuint8>(tp_LandscapeHCC);\nFString tstr_DProperty = DescribeProperty(tp_FindedProp, PropData);\nFGuid ts_Guid;\nLexFromString(ts_Guid, DProperty);\n",[41,1091,1089],{"__ignoreMap":43},[172,1093,1094],{"id":1094},"直接转换",[11,1096,1097],{},"其实也可以直接强制转换类型，不过会有一定的危险性",[34,1099,1102],{"className":1100,"code":1101,"language":39},[37],"const FGuid* tp_GuidConvert = (FGuid*)PropData;\nUE_LOG(LogTemp, Log, TEXT(\"Compare to see: %s || %s\"), *tstr_DProperty, *LexToString(*tp_GuidConvert));\n",[41,1103,1101],{"__ignoreMap":43},[27,1105,1107],{"id":1106},"ufunction","UFUNCTION",[11,1109,1110],{},"这里的另一个需求是要求目标的Object实现一个事先约定好的接口，而插件这边则负责调用改接口来实现特定的额外导出。",[11,1112,1113],{},"其实FindFunctionByName是引擎内本来就有的 ，蓝图节点UK2Node_CallFunction中也能看到引擎是如何将功能暴露到蓝图的。在参考上UObject::CallFunctionByNameWithArguments和UObject::FindFunction也可以得到一些帮助，最后在实现上采用了这样的形式：",[34,1115,1118],{"className":1116,"code":1117,"language":39},[37],"UE_LOG(LogWorldExport, Log, TEXT(\"Get target class\"));\nstatic FName ts_FuncName(TEXT(\"ExportOut\"));\nUFunction* tp_ExportFunc = tp_IterActor->FindFunction(ts_FuncName);\n\u002F\u002Ftp_IterActor->ProcessEvent(tp_ExportFunc, nullptr);\nif (tp_ExportFunc)\n{\n  FFrame Stack(tp_IterActor, tp_ExportFunc, nullptr);\n  tp_ExportFunc->Invoke(tp_IterActor, Stack, nullptr);\n}\nelse\n{\n  UE_LOG(LogWorldExport, Error, TEXT(\"Target class %s found but func %s is not fund\"), *tstr_ActorName, *ts_FuncName.ToString());\n}\n",[41,1119,1117],{"__ignoreMap":43},[11,1121,1122],{},"如果需要返回值的话，可以这样：",[34,1124,1127],{"className":1125,"code":1126,"language":39},[37],"const bool bHasReturnParam = Function->ReturnValueOffset != MAX_uint16;\nuint8* ReturnValueAddress = bHasReturnParam ? ((uint8*)Parms + Function->ReturnValueOffset) : nullptr;\n",[41,1128,1126],{"__ignoreMap":43},[27,1130,822],{"id":822},[11,1132,1133,1134,1136],{},"其实在读取UStruct的时候，应当是可以使用",[738,1135,1036],{},"作Struct的转换的，不过不知道当时为什么没有这么做，这点只能留到下次有相关需求的时候再考证了。",[11,1138,1139],{},"另外，如果对UProperty的读写有兴趣的话，其实可以参考引擎的LoadConfig和SaveConfig，里面包含了所有类型的属性的读写。",{"title":43,"searchDepth":87,"depth":88,"links":1141},[1142,1145,1149,1150],{"id":1020,"depth":87,"text":1021,"children":1143},[1144],{"id":1053,"depth":88,"text":1053},{"id":1065,"depth":87,"text":1066,"children":1146},[1147,1148],{"id":1072,"depth":88,"text":1073},{"id":1094,"depth":88,"text":1094},{"id":1106,"depth":87,"text":1107},{"id":822,"depth":87,"text":822},"2019-09-08",{"layout":95,"status":96,"published":97,"author":1153,"author_login":100,"author_email":101,"wordpress_id":1154,"wordpress_url":1155,"date_gmt":1156,"excerpt":1157},{"display_name":99,"login":100,"email":101,"url":43},2695,"\u002F?p=2695","2019-09-08 12:04:30 +0000",{"type":8,"value":1158},[1159],[11,1160,1002],{},"\u002F2019-09-08-ue4-reflection-note",{"title":997,"description":1002},"_legacy\u002F2019\u002F2019-09-08-ue4-reflection-note",[113],"lzKUFzuZlc7mwr6a2SnRdYsc8_H_hkI4Ro6IpGLAPUI",85,1788763178177]