[{"data":1,"prerenderedAt":2130},["ShallowReactive",2],{"blog-page-11":3,"blog-count":2129},[4,437,813,950,1779],{"id":5,"title":6,"body":7,"date":414,"description":13,"extension":415,"meta":416,"navigation":419,"path":431,"seo":432,"stem":433,"tags":434,"__hash__":436},"blogs\u002F_legacy\u002F2019\u002F2019-09-03-note-screen-writing-101.md","《Screen writing 101》笔记",{"type":8,"value":9,"toc":400},"minimark",[10,14,17,21,24,27,30,33,52,55,58,69,72,75,78,81,87,90,93,98,101,106,109,114,117,122,125,130,133,138,141,144,149,152,157,160,165,168,171,174,177,188,191,217,220,223,226,229,232,235,252,255,258,261,264,267,281,284,287,292,295,298,303,306,309,314,317,320,325,328,333,336,341,346,349,352,355,358,362,365,368,371,374,377,380],[11,12,13],"p",{},"如其名称，这是另一本关于银幕剧本创作的书。",[11,15,16],{},"这本书介绍的方向是从冲突开始的，创作的方法论虽然难免会有很多相似的地方。但是故事毕竟更加接近艺术的领域，自然无法像是软件工程一样精确。所以会有很多不同的观点呢。",[18,19,20],"h2",{"id":20},"戏剧就是冲突",[11,22,23],{},"戏剧是会给主角生活带来明显改变的冲突。",[11,25,26],{},"Drama is Ordered Conflict.",[11,28,29],{},"所以，需要从“明显改变”开始，对素材进行压缩。对事件发生的时间范围进行压缩，同时对事件进行拆解。",[11,31,32],{},"在对事件进行组织后，首先需要发展出的是前提：",[34,35,36,40,43,46,49],"ul",{},[37,38,39],"li",{},"谁是主要角色？",[37,41,42],{},"谁是对手？",[37,44,45],{},"他们为什么对抗？",[37,47,48],{},"这冲突的结果，会带来什么改变？",[37,50,51],{},"为什么主角必须采取行动来达成这个改变？",[18,53,54],{"id":54},"让观众满意",[11,56,57],{},"这里也是采用了三幕的形式：",[34,59,60,63,66],{},[37,61,62],{},"第一幕- 吸引力",[37,64,65],{},"第二幕- 预期心理",[37,67,68],{},"第三幕- 满意",[11,70,71],{},"这里第一幕的吸引力是很重要的：“我真想知道主角怎么解决这个难题。”",[11,73,74],{},"这里给出的《雨人》的大纲很有参考意义，用三句话讲明白了一个故事。",[18,76,77],{"id":77},"银幕故事的要素",[11,79,80],{},"这一章虽然叫这个名字，其实是从作者的观点对这个故事的前进结构进行了描述。",[11,82,83],{},[84,85,86],"strong",{},"背景故事",[11,88,89],{},"是在本篇之前发生的事件，根据不同的故事类型需要不同程度的背景故事。例如侦探故事和动作冒险类故事对背景故事的需求完全不同。",[11,91,92],{},"背景故事在处理上，要尽量避免背景故事对主要情节的时间占用以及对叙事节奏的破坏。",[11,94,95],{},[84,96,97],{},"内心需求",[11,99,100],{},"有了背景故事，然后就可以设置角色隐藏的未知的内心需求。",[11,102,103],{},[84,104,105],{},"刺激诱因",[11,107,108],{},"然后就会发生激励事件，改变主角的生活。",[11,110,111],{},[84,112,113],{},"外部目标",[11,115,116],{},"主角的生活平衡被打破，便需要确立一个外部目标来恢复生活的平衡。",[11,118,119],{},[84,120,121],{},"准备",[11,123,124],{},"外部目标不可能轻易达到，所以需要准备。",[11,126,127],{},[84,128,129],{},"敌对力量",[11,131,132],{},"敌对力量则会阻止主角达到目标。",[11,134,135],{},[84,136,137],{},"自我启示",[11,139,140],{},"主角在理解自我内心需求过程中的最低潮。",[11,142,143],{},"一个角色是不会凭空被真正了解的，观众对主角的理解，基于主角的所作所为。",[11,145,146],{},[84,147,148],{},"沉迷困扰",[11,150,151],{},"主角改变之后，开始专注行动，改变外部世界",[11,153,154],{},[84,155,156],{},"争斗",[11,158,159],{},"最终会与对手进行对决。",[11,161,162],{},[84,163,164],{},"问题解决",[11,166,167],{},"然后主角的生活重新回到平衡状态。",[18,169,170],{"id":170},"故事角色",[11,172,173],{},"将前面提到的故事要素结合起来 的，就是角色人物。",[11,175,176],{},"无法将情节（故事）和角色区分开来。",[34,178,179,182,185],{},[37,180,181],{},"要学习主动去倾听，让你的角色和你说话",[37,183,184],{},"使用自传体式的放大镜，同时搜寻比你更多的特质",[37,186,187],{},"引导角色，并喜爱他们",[11,189,190],{},"关于角色的问题：",[34,192,193,196,199,202,205,208,211,214],{},[37,194,195],{},"你的主角什么时候最快乐？",[37,197,198],{},"你的主角可能有什么才能？",[37,200,201],{},"如果你的角色改变自己一件事，会是什么？",[37,203,204],{},"你的主角认为自己最伟大的成就是什么？",[37,206,207],{},"你的主角最珍视的财产是什么？",[37,209,210],{},"你的主角最浪费的行为是什么？",[37,212,213],{},"什么时候你的主角会撒谎？",[37,215,216],{},"你的主角最后悔的事是什么？",[11,218,219],{},"去发现而不是去管理你的角色。",[11,221,222],{},"谁是你的主角？角色就代表他们的行动。角色就是动作。",[11,224,225],{},"角色的最少量行动，指的是合情合理的情况下，角色只会采取最少量的行动。要调整角色的行动，就需要调整场景中的价值筹码。",[11,227,228],{},"认知失调的理论上，人类会主动回避可能会引起失调的情况或信息。",[11,230,231],{},"而正是这种内心的冲突，会触发角色的自我怀疑。内心需求和自我观念的对抗最终会让角色作出戏剧性的选择。",[11,233,234],{},"冲突的焦点按照层面的不同，可以分为：",[34,236,237,240,243,246,249],{},[37,238,239],{},"个人自身的冲突焦点",[37,241,242],{},"人与人之间的冲突焦点",[37,244,245],{},"意外情况的冲突焦点（自然灾害）",[37,247,248],{},"社会级别的冲突焦点",[37,250,251],{},"利益相关的冲突焦点",[11,253,254],{},"在设置上，外部目标必须得到观众的认同。在观众认同角色的情况下，外部目标必须和观众有所关联，最好能够让观众感同身受。",[11,256,257],{},"在确立了角色的外部目标后，就可以去探寻：是什么力量阻止主角达成目标？",[18,259,260],{"id":260},"银幕故事脉络",[11,262,263],{},"观众开始观看电影时，其实是自愿的终止了怀疑，要严格防止干扰观众信任的行为。所以，为了保证可信度，需要做好研究与调查。可以多研究同类型的小说和电影，但同时要小心不要被资料所束缚。",[11,265,266],{},"然后作者介绍了他自己在这个阶段的创作方法：沙发上的写作。总体而言就是自由联想。让角色自己浮现出来，同时构成故事脉络的要素：",[34,268,269,272,275,278],{},[37,270,271],{},"戏剧化的强调：主角的外部目标，观众和主角的主轴关联性",[37,273,274],{},"物质世界：不能干扰观众的信任",[37,276,277],{},"时间：年代叙述要迅速带入",[37,279,280],{},"角色的思考方式：时代背景的态度、行为和价值观",[18,282,283],{"id":283},"银幕故事类型",[11,285,286],{},"这章主要是对类型进行了描述，类型只是一个工具，并没有明确区分的必要性。这里也没有都记录下来，只挑了几个和传统意义上感觉不是很一样的：",[11,288,289],{},[84,290,291],{},"美好的故事（Fairy Tale）",[11,293,294],{},"着重于由束缚中解放出来。",[11,296,297],{},"某种物体和行为，被当作释放的象征：钢琴课",[11,299,300],{},[84,301,302],{},"个人的探索",[11,304,305],{},"在相对封闭的地点或团体中。",[11,307,308],{},"对自我的真实的探究。",[11,310,311],{},[84,312,313],{},"侦探类型",[11,315,316],{},"强调区辨正邪。",[11,318,319],{},"在扭曲的环境中，一个寻求真实的人。",[11,321,322],{},[84,323,324],{},"恐怖",[11,326,327],{},"非人的或者环境的。",[11,329,330],{},[84,331,332],{},"惊悚",[11,334,335],{},"无辜的卷入极大的阴谋。",[11,337,338],{},[84,339,340],{},"动作冒险",[11,342,343],{},[84,344,345],{},"抽象的痛苦烦恼",[11,347,348],{},"例子：《莫扎特传》",[18,350,351],{"id":351},"剧本写作方式",[11,353,354],{},"一些写作剧本的规范。对我没啥意义。",[11,356,357],{},"通过台词和潜台词来传达戏剧冲突的节拍。",[18,359,361],{"id":360},"静下心来写你的剧本","静下心来，写你的剧本",[11,363,364],{},"每个编剧都是以自己的方式来创造剧本大纲的，你也要不断尝试，直到找到了最合适你个人习惯的技巧。",[11,366,367],{},"整本书其实介绍的是作者自己的创作方法，对于作者而言，沙发上写作阶段之后才会形成大纲。",[11,369,370],{},"草稿时期不需要在意很多剧本原则规范，修订草稿阶段才需要按照规范。",[18,372,373],{"id":373},"编剧这一行",[11,375,376],{},"有很多关于电影行业本身的咨询，没有什么太多的意义。",[11,378,379],{},"最后列出的是作者给出的需要的剧本检查项：",[34,381,382,385,388,391,394,397],{},[37,383,384],{},"有没有一位毋庸置疑的主角？",[37,386,387],{},"主角有没有明确定义的问题需要解决？",[37,389,390],{},"有没有集中对抗主角的鲜明对立的对手？",[37,392,393],{},"为了解决问题，需要主角采取行动来对付对手吗？",[37,395,396],{},"为了解决问题，是否要把主角的价值观带进问题里面？",[37,398,399],{},"你有没有故事情绪上的主要原动力，而不是使剧本的一切都显得太过聪明机巧了？",{"title":401,"searchDepth":402,"depth":403,"links":404},"",2,3,[405,406,407,408,409,410,411,412,413],{"id":20,"depth":402,"text":20},{"id":54,"depth":402,"text":54},{"id":77,"depth":402,"text":77},{"id":170,"depth":402,"text":170},{"id":260,"depth":402,"text":260},{"id":283,"depth":402,"text":283},{"id":351,"depth":402,"text":351},{"id":360,"depth":402,"text":361},{"id":373,"depth":402,"text":373},"2019-09-03","md",{"layout":417,"status":418,"published":419,"author":420,"author_login":422,"author_email":423,"wordpress_id":424,"wordpress_url":425,"date_gmt":426,"excerpt":427},"post","publish",true,{"display_name":421,"login":422,"email":423,"url":401},"风铃","flinkor","flinkor@foxmail.com",2680,"\u002F?p=2680","2019-09-03 01:01:11 +0000",{"type":8,"value":428},[429],[11,430,13],{},"\u002F2019-09-03-note-screen-writing-101",{"title":6,"description":13},"_legacy\u002F2019\u002F2019-09-03-note-screen-writing-101",[435],"narrative","DUlTRZYtwvTVBhC0BsIAEDYJkMgCDXrpvlo3ktaIGv8",{"id":438,"title":439,"body":440,"date":798,"description":444,"extension":415,"meta":799,"navigation":419,"path":808,"seo":809,"stem":810,"tags":811,"__hash__":812},"blogs\u002F_legacy\u002F2019\u002F2019-09-01-note-screenwriting-step-by-step.md","《Screenwriting: Step by Step》笔记",{"type":8,"value":441,"toc":784},[442,445,448,451,454,457,460,463,466,486,489,496,499,502,513,516,533,536,539,542,554,557,560,563,566,577,580,583,586,597,604,607,610,613,616,624,627,630,633,636,639,654,657,660,663,666,669,672,675,683,686,689,692,695,698,712,715,718,721,724,735,738,741,744,747,750,761,764,767,778,781],[11,443,444],{},"这是一本关于电影剧本的书，还算是比较基础。",[11,446,447],{},"之前有看过麦基的《故事》，但从某种角度上来说，《故事》并不是面向初学者的。所以虽然做了很多尝试，却始终不得其门而入。不得不感叹自己在基础知识上的不足，因此虽然不甘愿，还是要重新将时间投入到吸收知识上去。",[11,449,450],{},"只是，电影剧本的表现形式与游戏叙事大概还是有着本质上的不同，所以并没有过于深入的打算。",[18,452,453],{"id":453},"格式",[11,455,456],{},"这部分主要介绍的是好莱坞剧本的格式，有很多关于行距这些的规则。",[11,458,459],{},"对于目标不是电影剧本的人而言，没什么太大的意义。这里面能看到作者的立意，剧本只是电影工业的一部分，所以无论你怎么想，故事都是一种商品。既然是商品就必须遵循对应的行业规范，否则没有人愿意花时间与你合作。",[11,461,462],{},"实质上，剧本是一个买方市场。而且光靠剧本无法形成电影，如果你是独立游戏制作的话，你可以自己做很多无人问津的作品，但是至少那是成品。而没有拍成电影的剧本，就什么都不是。",[11,464,465],{},"其中也介绍了一些需要避免的写法：",[34,467,468,471,474,477,480,483],{},[37,469,470],{},"关注动作和对话，避免描述性文字",[37,472,473],{},"注意对话的长度，冗长的独白是戏剧才需要的",[37,475,476],{},"简略的描述特效即可",[37,478,479],{},"避免使用被动句式",[37,481,482],{},"删除观众无法看到和听到的细节",[37,484,485],{},"避免形容词和副词",[18,487,488],{"id":488},"人物",[11,490,491,492,495],{},"人物的诞生可以依赖于干扰事件，以干扰事件作为开始，才能顺利找到主角。这里说的干扰事件，与",[84,493,494],{},"激励事件","具有相似的含义。",[11,497,498],{},"人物只有通过行为才能让人感兴趣，特别是他们处理冲突的特殊方式。",[11,500,501],{},"最重要的人物自然是主角与对手。这里面作者先解释了误解，感觉其实以亚洲文化而言，先看了她的解释才会发生误解。所谓的对手，其实并不是敌人，而是指的在激励事件之后，主角试图恢复秩序时的对抗力量的核心。",[11,503,504,505,508,509,512],{},"对手必须是具有人格的，大自然和大白鲨不是对手。所以其实这里",[84,506,507],{},"对手","的意义优先级并没有",[84,510,511],{},"戏剧问题","的优先级高。也就是说，主导故事的核心冲突与对手其实是可以分离的，对手在更多的时候是接近创作者的主题工具，而不是展现给观众的表象。",[11,514,515],{},"一切从干扰事件开始，从中找到主角、对抗力量和戏剧问题，这一章最后需要列出：",[34,517,518,521,524,527,530],{},[37,519,520],{},"干扰事件（某种扰乱人物生活的困境或者危机）",[37,522,523],{},"戏剧性问题（主角必须采取行动并解决它）",[37,525,526],{},"主角（他或她如何尽力去解决问题？）",[37,528,529],{},"对手（他或者她如何扰乱并阻碍主角的努力）",[37,531,532],{},"主角最后如何解决问题",[11,534,535],{},"最后作者用作练习的《Citizen Kane》，是1941年的老电影，而且是黑白的。感觉叙事节奏上和习惯了二次元快速跳转的我有点不合，而且或许是时代背景有很大的偏差，开篇长达10分钟的旁白式叙述实在是看得有些发困……虽然是被奉为经典的作品，但是这边的目的是游戏叙事，能够共同的其实只到脚本为止，所以就放弃了。",[18,537,538],{"id":538},"主旨",[11,540,541],{},"主旨陈述，就是你的剧本的地图。在开始你的旅程之前，一定要好好利用它。",[543,544,545,548,551],"blockquote",{},[11,546,547],{},"主题→ 基本动作",[11,549,550],{},"目的句",[11,552,553],{},"————（你的主角）想要去—————————————（简述主角的愿望或欲望），尽管——————（对手的名字）（简述对手的反抗）",[18,555,556],{"id":556},"动作",[11,558,559],{},"这里所说的动作，感觉上接近于麦基所说的场景中的节拍。",[11,561,562],{},"动作是由情感产生，然后获得反应。",[11,564,565],{},"在情感的层次上，所有的人类都享有相同的感受。爱、欢乐、仇恨、悲伤、怜悯、害怕等，这些情感对于这个星球上的每一个人而言都是在正常不过的感受。然而其中区别就在于：",[34,567,568,571,574],{},[37,569,570],{},"具体的环境（即各种感情产生时的外在环境）各不相同",[37,572,573],{},"人们感受到的感情的强烈程度的不同",[37,575,576],{},"各种感情所可能引发的行为也各不相同",[11,578,579],{},"人物的情感必须反应在动作上，否则无法表现出来。即便是设定详实的背景故事，也必须通过动作展示出来，而不是由人物对话来叙述。",[18,581,582],{"id":582},"结构",[11,584,585],{},"故事在结构上通常包括：",[34,587,588,591,594],{},[37,589,590],{},"一个开端，一个中间阶段和一个结局。",[37,592,593],{},"一个能展开整个故事的干扰事件（一个引起观众兴趣的钩子或是戏剧性的事件）。",[37,595,596],{},"一个处于主人公及其对手之间的核心冲突。",[11,598,599,600,603],{},"为了保证能够很好的保持结构，需要使用",[84,601,602],{},"戏剧前提","来引路：",[11,605,606],{},"第一行总结你主要的干扰事件、推动情节发展的危机是什么。你的主旨陈述中的目的句可以作为这一步中合理而又强有力的基础。",[11,608,609],{},"第二行总结出第一幕的转折点，即它与第二幕衔接的“钩子”是什么。它使得冲突更加激烈，形成戏剧张力，并促使角色和故事进入第二幕。",[11,611,612],{},"第三行总结出第二幕的转折点，即它与第三幕衔接的“钩子”是什么。这里展示出主角将会面对的最为复杂和困难的情境。",[11,614,615],{},"后面作者举例的时候，在戏剧问题之外又列出了核心问题。以及整个故事的冲突的核心，以及问题的本质。在这里，戏剧问题更加接近于激励事件所抛出的直接问题：",[543,617,618,621],{},[11,619,620],{},"戏剧问题：乔是一个工作狂，虽然他很爱妻子苏，但很少有时间来陪她。",[11,622,623],{},"核心问题：乔能否真正地理解苏，并且挽回他们的关系？",[18,625,626],{"id":626},"回顾前文和深入修改",[11,628,629],{},"删减到只剩要点",[11,631,632],{},"只留下动作",[18,634,635],{"id":635},"第二幕",[11,637,638],{},"从结局往回对结构进行整理：",[640,641,642,645,648,651],"ol",{},[37,643,644],{},"固守你的主旨陈述、核心问题和戏剧性前提。",[37,646,647],{},"展示你的主角所做的决定，并使得对手能够对此作出反应。",[37,649,650],{},"通过冲突来驱动故事前进。",[37,652,653],{},"展示你的主角所冒的风险。",[11,655,656],{},"后面有关于主导场景的描述，感觉上没有麦基对如何用节拍构建场景的介绍详细。",[11,658,659],{},"中间点是主角下决断的地方，第二幕转折点则是最后的对抗揭开的地方，往往会有真相揭露。",[18,661,662],{"id":662},"人物弧",[11,664,665],{},"对于人物，可以使用人物小传的形式。或者制作一些角色的描述卡片。从示例上看，其实很接近一般在动画或者漫画中会看到的人物设定。",[11,667,668],{},"可以通过卡片上记载的内容，保证对人物的描写、人物的行动符合情理。",[11,670,671],{},"人物弧是由决定-结果-变化的序列不断形成的。",[11,673,674],{},"要想超越观众的预期，可以采用下面的形式：",[34,676,677,680],{},[37,678,679],{},"人物采取符合逻辑却不合拍的决定",[37,681,682],{},"人物的小决定导致天崩地裂的后果",[11,684,685],{},"如此，才能设置出强有力的干扰事件。同样的，也可以使用这种内含的矛盾来创造角色。",[18,687,688],{"id":688},"次要情节",[11,690,691],{},"次要情节一般在3~5个左右，目的是给剧本增加深度和内容。",[11,693,694],{},"次要情节属于进阶技巧，没有掌握好情节控制的话，就更难做好次要情节了。主要的要点如下：",[11,696,697],{},"每个和主要情节有关的主要人物，都可以激发一个次要情节",[34,699,700,703,706,709],{},[37,701,702],{},"次要情节的基础是主要情节",[37,704,705],{},"主要情节必须比次要情节更为完整",[37,707,708],{},"你可以在主要情节和次要情节之间来回切换",[37,710,711],{},"有些因素会在次要情节内激发不同的主角和对手",[11,713,714],{},"次情节可以通过添加陈述来确定自己在大纲中的位置：",[11,716,717],{},"这部剧本不仅是关于—————————————————————，而且还是关于————————————。",[18,719,720],{"id":720},"潜文本",[11,722,723],{},"通常有三种方式：",[34,725,726,729,732],{},[37,727,728],{},"视觉象征",[37,730,731],{},"动作潜文本",[37,733,734],{},"对话潜文",[18,736,737],{"id":737},"第三幕",[11,739,740],{},"第三幕是走向整个故事的高潮和大结局的一幕。",[11,742,743],{},"必须展示主角解决问题的过程，同时完善主题和人物关系。",[11,745,746],{},"你是否希望你的观众进入剧院以后，了解到那些他们以前无从知晓的东西？你希望他们从中感受到什么？这是你所有工作的终极目标。",[11,748,749],{},"切忌使用以下的手法：",[34,751,752,755,758],{},[37,753,754],{},"解释性结局",[37,756,757],{},"病急乱投医（奇迹结局）",[37,759,760],{},"强求的大团圆",[11,762,763],{},"对于结局明确的类型，例如英雄史诗等。影片的看点在于优秀老到的演员的表演和强烈的戏剧性动作。",[11,765,766],{},"最后，在拟定结局的时候需要思考，结局是如何结局以下问题的：",[34,768,769,772,775],{},[37,770,771],{},"解决故事的核心问题？",[37,773,774],{},"完成首要主题并且形成人物关系？",[37,776,777],{},"提供一个逻辑性强的结局满足观众？",[18,779,780],{"id":780},"生手上路",[11,782,783],{},"最后一章，更多的是介绍电影行业本身。",{"title":401,"searchDepth":402,"depth":403,"links":785},[786,787,788,789,790,791,792,793,794,795,796,797],{"id":453,"depth":402,"text":453},{"id":488,"depth":402,"text":488},{"id":538,"depth":402,"text":538},{"id":556,"depth":402,"text":556},{"id":582,"depth":402,"text":582},{"id":626,"depth":402,"text":626},{"id":635,"depth":402,"text":635},{"id":662,"depth":402,"text":662},{"id":688,"depth":402,"text":688},{"id":720,"depth":402,"text":720},{"id":737,"depth":402,"text":737},{"id":780,"depth":402,"text":780},"2019-09-01",{"layout":417,"status":418,"published":419,"author":800,"author_login":422,"author_email":423,"wordpress_id":801,"wordpress_url":802,"date_gmt":803,"excerpt":804},{"display_name":421,"login":422,"email":423,"url":401},2676,"\u002F?p=2676","2019-09-01 00:55:12 +0000",{"type":8,"value":805},[806],[11,807,444],{},"\u002F2019-09-01-note-screenwriting-step-by-step",{"title":439,"description":444},"_legacy\u002F2019\u002F2019-09-01-note-screenwriting-step-by-step",[435],"yDQci_66O0umiPmdJOnE_hUdC-5Rh5QtJGzueqAs8wA",{"id":814,"title":815,"body":816,"date":934,"description":820,"extension":415,"meta":935,"navigation":419,"path":944,"seo":945,"stem":946,"tags":947,"__hash__":949},"blogs\u002F_legacy\u002F2019\u002F2019-08-08-https-upgrade-log.md","Https改造记录",{"type":8,"value":817,"toc":929},[818,821,824,827,830,841,844,847,850,854,863,872,875,878,888,891,894,898,901,904,907,910,914,917,923,926],[11,819,820],{},"经过了几个月的观察，现在网站总算是完全平稳的运作在Https上了。",[11,822,823],{},"之所以要自己折腾Https证书这些事情，是因为最终网站迁移到Linode上了。",[11,825,826],{},"虽然现在的共享主机商都会提供Https签名服务，但是无奈一直都找不到靠谱的主机商。试来试去，发现还是Linode适合我这种规模小但对稳定性有要求的。虽然速度上偶尔会有非常卡的情况，但是现状来看实在无可奈何。因为本质上这边不产生收益，所以没有办法花费太多成本在上面。",[11,828,829],{},"记得以前Https证书很难搞定，只有Comodo的免费授权，要更新起来特别麻烦。而个人站点基本不可能去搞商业证书，完全意义不明~",[11,831,832,833,840],{},"好在现在有了",[834,835,839],"a",{"href":836,"rel":837},"https:\u002F\u002Fletsencrypt.org\u002F",[838],"nofollow","lets encrypt","，可以免费的进行Https证书的申请。",[11,842,843],{},"第一次弄的时候，由于没有什么时间，参照官方教程随便配置了下。",[11,845,846],{},"lets encrypte的签名有效时间比较短，需要自己隔一段时间重新签名一次。但是途中不小心忙忘了 ，导致证书失效。几个常见的浏览器，Firefox和Chrome，遇到这种情况是直接不让访问网站的，相当的尴尬。",[11,848,849],{},"于是就又开始折腾。折腾完了就有了下面的记录，方便下次需要配置的时候直接拷贝就可以了~",[18,851,853],{"id":852},"acmesh","Acme.sh",[11,855,856,857,862],{},"几经搜索，终于找到了这个自动签名工具。不但有中文的",[834,858,861],{"href":859,"rel":860},"https:\u002F\u002Fgithub.com\u002FNeilpang\u002Facme.sh\u002Fwiki\u002F%E8%AF%B4%E6%98%8E",[838],"说明文档","，功能上，也比官方的工具更直观一些。",[11,864,865,866,871],{},"操作上，为了方便下次自动签名，首先第一步是添加DNS API。具体可以参考",[834,867,870],{"href":868,"rel":869},"https:\u002F\u002Fgithub.com\u002FNeilpang\u002Facme.sh\u002Fwiki\u002Fdnsapi",[838],"官方说明","。",[11,873,874],{},"之后按照文档进行证书申请，完成申请后就可以安装了。",[11,876,877],{},"对于Nginx，安装的主要的命令是这样：",[879,880,885],"pre",{"className":881,"code":883,"language":884},[882],"language-text","acme.sh --install-cert -d *.ch-wind.com \\\n--cert-file      \u002Fetc\u002Fnginx\u002Fssl\u002Fch-wind.cer  \\\n--key-file       \u002Fetc\u002Fnginx\u002Fssl\u002Fch-wind.key  \\\n--ca-file       \u002Fetc\u002Fnginx\u002Fssl\u002Fca.cer  \\\n--fullchain-file \u002Fetc\u002Fnginx\u002Fssl\u002Ffullchain.cer \\\n--reloadcmd     \"service nginx force-reload\"\n\n","text",[886,887,883],"code",{"__ignoreMap":401},[11,889,890],{},"一旦完成一次操作，之后就会每隔60天自动更新证书，观察了几个月，基本算是没什么问题。",[11,892,893],{},"需要注意的是，对于wild card的域名证书，是不可以使用给根域名的。虽然不知道为什么，但是如果有做根域名跳转的话，需要对根域名另外签一次名。",[18,895,897],{"id":896},"ipv6","IPV6",[11,899,900],{},"既然都加HTTPS了，自然不能忘了IPV6。",[11,902,903],{},"网站的IPV6迁移是最简单的了，不过现在纯IPV6的用户应当还是比较少。大多数都只是根据线路来优先选择更快的一个。",[11,905,906],{},"配置上，只要服务器提供商有给IPV6地址，那么在DNS提供商那边添加一个AAAA记录就OK了。",[11,908,909],{},"当然，Nginx这边还需要配置下端口监听方式，如果之前没有配过的话。",[18,911,913],{"id":912},"nginx配置","Nginx配置",[11,915,916],{},"接下里就是Nginx配置，这个基本参考官方文档就可以了。但是每次去找也很麻烦，这里贴出现在在使用的版本。",[879,918,921],{"className":919,"code":920,"language":884},[882],"server{\n    server_name ch-wind.com;\n    return 301 https:\u002F\u002Fblog.$host$request_uri;\n\n    listen [::]:443 ssl; # managed by Certbot\n    listen 443 ssl; # managed by Certbot\n    ssl_certificate \u002Fetc\u002Fnginx\u002Fssl\u002Fraw_ch-wind.cer; # managed by Certbot \n    ssl_certificate_key \u002Fetc\u002Fnginx\u002Fssl\u002Fraw_ch-wind.key; # managed by Certbot\n}\n\nserver{\n    if ($host = blog.ch-wind.com) {\n        return 301 https:\u002F\u002F$host$request_uri;\n    } # managed by Certbot\n\n\n    listen [::]:80;\n    listen 80;\n\n    server_name blog.ch-wind.com;\n    return 301 https:\u002F\u002F$server_name$request_uri;\n\n\n}\n\nserver {\n        ## Your website name goes here.\n        server_name blog.ch-wind.com;\n        ## Your only path reference.\n        root \u002Fvar\u002Fwww\u002Fchwindblog;\n        ## This should be in your http block and if it is, it's not needed here.\n        index index.php;\n\n        location = \u002Ffavicon.ico {\n                log_not_found off;\n                access_log off;\n        }\n\n        location = \u002Frobots.txt {\n                allow all;\n                log_not_found off;\n                access_log off;\n        }\n\n        location \u002F {\n                # This is cool because no php is touched for static content.\n                # include the \"?$args\" part so non-default permalinks doesn't break when using query string\n                try_files $uri $uri\u002F \u002Findex.php?$args;\n        }\n\n        location ~ \\.php$ {\n                #NOTE: You should have \"cgi.fix_pathinfo = 0;\" in php.ini\n                include snippets\u002Ffastcgi-php.conf;\n                fastcgi_intercept_errors on;\n                fastcgi_pass unix:\u002Fvar\u002Frun\u002Fphp\u002Fphp7.2-fpm.sock;\n                fastcgi_buffers 16 16k;\n                fastcgi_buffer_size 32k;\n        }\n\n        location ~* \\.(js|css|png|jpg|jpeg|gif|ico)$ {\n                expires max;\n                log_not_found off;\n        }\n\n    ## listen 80;\n\n    listen [::]:443 ssl;\n    listen 443 ssl;\n    ssl_certificate \u002Fetc\u002Fnginx\u002Fssl\u002Fch-wind.cer; # managed by Certbot \n    ssl_certificate_key \u002Fetc\u002Fnginx\u002Fssl\u002Fch-wind.key; # managed by Certbot\n}\n\nserver{\n    if ($host = ch-wind.com) {\n        return 301 https:\u002F\u002F$host$request_uri;\n    } # managed by Certbot\n\n\n    server_name ch-wind.com;\n    listen [::]:80;\n    listen 80;\n    return 404; # managed by Certbot\n\n\n}\n\n",[886,922,920],{"__ignoreMap":401},[11,924,925],{},"里面有被Certbot配置过的痕迹，不过CertBot的自动更新功能配置起来有些迷。",[11,927,928],{},"整个配置有点乱，也没时间整，真的是凑活着用了……",{"title":401,"searchDepth":402,"depth":403,"links":930},[931,932,933],{"id":852,"depth":402,"text":853},{"id":896,"depth":402,"text":897},{"id":912,"depth":402,"text":913},"2019-08-08",{"layout":417,"status":418,"published":419,"author":936,"author_login":422,"author_email":423,"wordpress_id":937,"wordpress_url":938,"date_gmt":939,"excerpt":940},{"display_name":421,"login":422,"email":423,"url":401},2647,"\u002F?p=2647","2019-08-08 15:46:42 +0000",{"type":8,"value":941},[942],[11,943,820],{},"\u002F2019-08-08-https-upgrade-log",{"title":815,"description":820},"_legacy\u002F2019\u002F2019-08-08-https-upgrade-log",[948],"HTTPS","XZPIUsZ9_T28NxEpxUxcHMiAq-F1o65MGKNbVC6bhuc",{"id":951,"title":952,"body":953,"date":1762,"description":957,"extension":415,"meta":1763,"navigation":419,"path":1772,"seo":1773,"stem":1774,"tags":1775,"__hash__":1778},"blogs\u002F_legacy\u002F2018\u002F2018-10-14-ue4-network-overview.md","UE4网络底层概览",{"type":8,"value":954,"toc":1722},[955,958,961,964,967,970,974,977,984,987,990,994,997,1000,1003,1008,1011,1014,1017,1023,1026,1030,1033,1036,1041,1044,1058,1067,1071,1074,1077,1083,1086,1092,1095,1098,1101,1107,1110,1113,1116,1119,1122,1125,1128,1131,1137,1140,1143,1146,1158,1162,1165,1168,1171,1174,1180,1183,1186,1189,1192,1195,1198,1201,1204,1208,1211,1214,1217,1220,1223,1226,1229,1235,1238,1242,1245,1249,1252,1255,1261,1264,1268,1271,1276,1279,1283,1286,1289,1292,1295,1301,1304,1310,1313,1316,1319,1322,1326,1329,1332,1335,1338,1341,1344,1347,1350,1353,1357,1360,1365,1368,1372,1375,1379,1382,1385,1388,1392,1395,1398,1401,1404,1407,1410,1414,1417,1420,1423,1426,1430,1433,1439,1442,1448,1451,1454,1458,1461,1467,1470,1473,1479,1482,1485,1489,1492,1498,1501,1504,1510,1513,1516,1519,1522,1525,1528,1534,1537,1540,1543,1546,1549,1552,1555,1561,1564,1567,1571,1574,1577,1580,1584,1587,1590,1593,1596,1600,1603,1607,1610,1613,1616,1619,1622,1628,1631,1640,1644,1647,1653,1656,1662,1665,1668,1674,1677,1696,1699,1705,1708,1711,1718],[11,956,957],{},"日常使用的话不会接触到这块，最近刚好接触到，所以在此做下整理。",[11,959,960],{},"当前UE4版本为4.20。",[11,962,963],{},"UE4的网络部分，主要有RPC和值同步两种逻辑。其中在代码上有更多的维护的是值同步，因为RPC基本上是调用的时候就发出去，可优化的成分不高。而值同步就涉及同步频率、状态差分这些的维护成本，所以会有更多的逻辑在背后。",[18,965,966],{"id":966},"网络结构",[11,968,969],{},"虚幻本身的网络结构各个层次的封装还是比较好的，基本不会出现层次之间混杂导致难以定制的情况。",[971,972,973],"h3",{"id":973},"总体结构",[11,975,976],{},"结构上，每一个Actor通过ActorChannel注册到Connection来进行网络相关的操作。",[11,978,979],{},[980,981],"img",{"alt":982,"src":983},"image","\u002Fwp-content\u002Fuploads\u002F2018\u002F10\u002Fimage_thumb.png",[11,985,986],{},"在逻辑上每一个Connection对应的是一个客户端和服务端之间的远程连接，而NetDriver向下的话就只是对网络底层操作的封装。",[11,988,989],{},"在这里，首先从最底层开始。",[971,991,993],{"id":992},"fsocket和socketsubsystem","FSocket和SocketSubSystem",[11,995,996],{},"FSocket是对网络底层的封装，对于上层需要的网络状态查询、发包、收包这些操作，全部都封装在了这里。",[11,998,999],{},"与平台相关的底层全部隐藏掉，同时可以方便实现如steam网络接入这样的接入模式。",[11,1001,1002],{},"一般情况下的连接实际使用的是FSocketBSD，是对底层Socket API的直接封装。",[1004,1005,1007],"h4",{"id":1006},"fsocket工厂类","FSocket工厂类",[11,1009,1010],{},"SocketSubSystem是对UE4网络上层可见的FSocket工厂类，上层调用时不关心FSocket的具体实现是什么。而是通过预先配置好的SocketSubsytem来直接创建FSocket的基类指针。",[11,1012,1013],{},"在多平台实现上，和其他的系统基本是类似的。例如在Android上，实际使用的是FSocketSubsystemAndroid，而在IOS上则是FSocketSubsystemIOS。",[11,1015,1016],{},"具体而言，在SocketSubsystem的Module初始化时，会使用一个被宏区分的系统对应的SubSystem",[879,1018,1021],{"className":1019,"code":1020,"language":884},[882],"\u002F\u002F Initialize the platform defined socket subsystem first\nDefaultSocketSubsystem = CreateSocketSubsystem( *this );\n",[886,1022,1020],{"__ignoreMap":401},[11,1024,1025],{},"这样一来，通过ISocketSubsystem::Get获得默认的SocketSubSystem的时候就能取得对应平台的实现了。",[1004,1027,1029],{"id":1028},"ipv4和ipv6","IPv4和IPv6",[11,1031,1032],{},"在4.21之前向下FSocketSubsystemBSD会有Ipv6和Ipv4两个不同的实现，在IOS上继承的是FSocketSubsystemBSDIPv6，而Android的是FSocketSubsystemBSD。但是4.21将这两个协议栈合在了一起，避免了在进行具体实现和使用时的一些困扰。",[11,1034,1035],{},"关于IPv6和IPv4的问题，其实在Socket这一层，切换起来的难度并不大。",[543,1037,1038],{},[11,1039,1040],{},"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,1042,1043],{},"IPv4到IPv6有很多过渡的协议、地址格式，但是实际如果在Socket这里用v6 API的话，只需要关注IPv4-mapped-on-IPv6就可以了：",[879,1045,1049],{"className":1046,"code":1047,"language":1048,"meta":401,"style":401},"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",[886,1050,1051],{"__ignoreMap":401},[1052,1053,1056],"span",{"class":1054,"line":1055},"line",1,[1052,1057,1047],{},[11,1059,1060,1061,1066],{},"实际测试，将API直接替换掉基本没有什么问题。主要会有麻烦的，还是一些平台相关的问题。上面的两个引用来自[",[834,1062,1065],{"href":1063,"rel":1064},"http:\u002F\u002Fman7.org\u002Flinux\u002Fman-pages\u002Fman7\u002Fipv6.7.html",[838],"Man7","]，关于IPv6 Socket API的一些其他描述也可以参看该文档。",[971,1068,1070],{"id":1069},"netdriver和connection","NetDriver和Connection",[11,1072,1073],{},"UNetDriver是进行网络处理的核心，有一个比较特殊的UDemoNetDriver，是用于回放录制系统的，不会发送数据。",[11,1075,1076],{},"NetDriver是和UWorld绑定的，无论是客户端还是服务器只会有一个。而Connection是对应客户端和服务器的连接的。对于客户端，只会有一个连接到Server的Connection，而服务器则对应每一个客户端维护一个Connection。",[879,1078,1081],{"className":1079,"code":1080,"language":884},[882],"\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",[886,1082,1080],{"__ignoreMap":401},[11,1084,1085],{},"NetDriver和Connection不是完全的上下层的关系，实际上这两个类在进行数据发送时使用的都是自己定义的LowLevelSend。Connection更多的意义上是标识，在这之上的连接是有既定的发送者和接收者的。",[879,1087,1090],{"className":1088,"code":1089,"language":884},[882],"void UIpConnection::LowLevelSend(void* Data, int32 CountBytes, int32 CountBits)\n\nvoid UIpNetDriver::LowLevelSend(FString Address, void* Data, int32 CountBits)\n",[886,1091,1089],{"__ignoreMap":401},[11,1093,1094],{},"在实际的网络连接中，一般情况下使用的对应类就是UIpNetDriver和UIpConnection。",[1004,1096,1097],{"id":1097},"逻辑关系",[11,1099,1100],{},"在更新上，Connection由NetDriver负责更新。NetDriver本身的更新由UWorld驱动",[879,1102,1105],{"className":1103,"code":1104,"language":884},[882],"\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",[886,1106,1104],{"__ignoreMap":401},[11,1108,1109],{},"NetDriver的TickDispatch负责从注册的Socket上取出数据包，并进一步分发到对应的Connection中去。在服务器上，对于不同的数据包，默认是使用客户端的IP地址来区分要分发的Connection的。如果没有找到对应的Connection，且符合开新链接的条件的话，就会建立新的连接并开始连接初始化的流程。",[11,1111,1112],{},"TickFlush是负责进行值复制逻辑的，会调用到ServerReplicateActors开始执行值复制。",[11,1114,1115],{},"PostTickFlush是在TickFlush之后执行的逻辑，主要进行一些网络发送相关的清理逻辑。",[11,1117,1118],{},"网络相关的Tick逻辑和TickGroup所组织的Tick逻辑是相对独立的，这三个Tick函数都是由UWorld直接进行分发的，而没有具体的TickGroup。",[11,1120,1121],{},"在逻辑上，TickDispatch在所有的Tick执行之前进行，而TickFlush和PostTickFlush则在所有的Tick执行之后进行执行。",[11,1123,1124],{},"RPC和ControlChannel上的控制信息由于有更高的实时性需求，不会经过这边的逻辑，会直接调用FlushNet进行数据发送。",[1004,1126,1127],{"id":1127},"状态维护",[11,1129,1130],{},"UNetConnection自己维护一组时间戳用于监视连接的状态，这些时间在InitBase中进行统一的初始化：",[879,1132,1135],{"className":1133,"code":1134,"language":884},[882],"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",[886,1136,1134],{"__ignoreMap":401},[11,1138,1139],{},"Driver->Time是由NetDriver维护的帧时间，在TickDispatch中进行更新。",[11,1141,1142],{},"LastReceiveTime的更新发生在UNetConnection::ReceivedRawPacket调用到UNetConnection::ReceivedPacket之后，也就是每一次有效的数据包从Driver分发到Connection的时候就会进行更新。这个时间戳在UNetConnection::HandleClientPlayer中也会进行更新，这个函数是在本地对玩家登陆进行处理的。",[11,1144,1145],{},"相对的，LastSendTime就比较单纯，只是在UNetConnection::FlushNet中更新，但是这个时间只是发送数据包的时间，无法确认发送是否成功。LastRecvAckTime在每次收到ACK的时候进行更新，这个时间戳并用于Standby Cheat的检测。",[11,1147,1148,1153,1154,1157],{},[834,1149,1152],{"href":1150,"rel":1151},"https:\u002F\u002Fwww.urbandictionary.com\u002Fdefine.php?term=standby",[838],"Standby Cheat","是Listen Server的时候的一种作弊方式，指的是联机的主机端恶意的影响网络数据收发来获得优势的行为。主要的检测逻辑在",[886,1155,1156],{},"UNetDriver::UpdateStandbyCheatStatus","中，检测分为三种Bad Ping、Rx(收包问题)、Tx(发包问题)。详细的逻辑没有深究，因为现在没有Listen Server的需求。",[1004,1159,1161],{"id":1160},"channel","Channel",[11,1163,1164],{},"在Connection之上，针对每个需要进行发送的Actor会有ActorChannel的概念。",[11,1166,1167],{},"Actor的值复制逻辑最终由UActorChannel来负责维护和执行，实际进行数据对比的是FObjectReplicator。",[11,1169,1170],{},"Connection上还会有一个特别的控制Channel，为UControlChannel，这个Channel负责对连接本身进行控制。UControlChannel的作用是对整个Connection进行控制，最主要的逻辑作用是在连接建立过程中的控制处理。所有的控制信息都被定义成NMT_Hello这样的宏。",[11,1172,1173],{},"如果要自定义控制信息，可以参考源码中的注释：",[879,1175,1178],{"className":1176,"code":1177,"language":884},[882],"\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",[886,1179,1177],{"__ignoreMap":401},[11,1181,1182],{},"控制信息的DEFINE_CONTROL_CHANNEL_MESSAGE_* 定义都在DataChannel.h中。进一步的使用可以查看NMT_Hello这种比较简单的控制信息的处理方式。",[11,1184,1185],{},"不过一般情况下应该很少会用到自定义控制信息，有需要特殊控制信息的话，可以先考虑使用NMT_GameSpecific。",[11,1187,1188],{},"还有一种独特的UVoiceChannel，不过感觉一般不会用到。",[18,1190,1191],{"id":1191},"连接建立",[11,1193,1194],{},"连接建立过程在客户端上主要由UPendingNetGame这个类进行负责，在连接确立之后，这个类就功成身退了。",[11,1196,1197],{},"本地试图连接远程关卡时，UEngine就会建立UPendingNetGame开始进行远程关卡的载入。UPendingNetGame负责建立NetDriver，并进行一系列的初始连接操作，在完成连接之后，UPendingNetGame会从UEngine脱离控制。",[11,1199,1200],{},"UPendingNetGame自身的Tick来源于UEngine::TickWorldTravel，在WorldContext上有PendingNetGame时就会更新其状态，直到操作失败或者完成操作。",[11,1202,1203],{},"客户端要连接到服务器，在建立起值复制和RPC发送的UConnection之前，在逻辑上主要有以下两个步骤：",[971,1205,1207],{"id":1206},"packethandler握手","PacketHandler握手",[11,1209,1210],{},"PacketHandler是注册在NetDriver和NetConnection上的数据包底层处理类，一般用于与上层逻辑无关的数据包加密、握手以及其他的一些特殊处理。这个处理逻辑在4.20的时候结构还不完整，能在一些函数上看到Work in progress, don't use yet的注释。",[11,1212,1213],{},"在连接建立阶段，首先的操作就是对所有需要握手的PacketHandler进行握手操作。这个逻辑由PacketHandler::BeginHandshaking负责。在客户端由UPendingNetGame调用，在服务端由UIpNetDriver::TickDispatch在新建到客户端的链接时进行调用。",[11,1215,1216],{},"在默认的情况下，这个过程中主要就是StatelessConnectHandlerComponent。",[11,1218,1219],{},"StatelessConnectHandlerComponent是一个使用Cookie来防止Dos和数据重放的PacketHandler。因此，这个Component是所有PacketHandler的前置，在服务端新建连接前会首先确保这个Component完成握手才会构建UConnection并调用PacketHandler::BeginHandshaking。",[11,1221,1222],{},"这样就可以确保到达服务器的数据包如果没有经过StatelessConnectHandlerComponent的握手的话就不会得到进一步的处理。",[11,1224,1225],{},"如果操作是按照正常流程的话，服务器收到的第一个包就是由客户端发起的StatelessConnectHandlerComponent的握手包。",[11,1227,1228],{},"PacketHandler的操作不是完全对称的，有",[879,1230,1233],{"className":1231,"code":1232,"language":884},[882],"Handler::Mode Mode = Driver->ServerConnection != nullptr ? Handler::Mode::Client : Handler::Mode::Server;\n\n",[886,1234,1232],{"__ignoreMap":401},[11,1236,1237],{},"对其进行区分。",[971,1239,1241],{"id":1240},"controlchannel","ControlChannel",[11,1243,1244],{},"在完成PacketHandler的握手之后，UConnection已经建立，由Control Channel中的控制信息来接管接下来的初始化过程。此时在服务端是由UWorld来主要负责处理控制信息的。",[1004,1246,1248],{"id":1247},"nmt_hello","NMT_Hello",[11,1250,1251],{},"连接过程同样由客户端发起，UPendingNetGame在PacketHandler握手完成后会发送NMT_Hello，开始连接初始化过程。",[11,1253,1254],{},"服务端在Connection建立时，InitRemoteConnection时会设置为等待状态：",[879,1256,1259],{"className":1257,"code":1258,"language":884},[882],"SetClientLoginState( EClientLoginState::LoggingIn );\nSetExpectedClientLoginMsgType( NMT_Hello );\n",[886,1260,1258],{"__ignoreMap":401},[11,1262,1263],{},"NMT_Hello会携带客户端的字节序、Crc之后的客户端版本以及Url中的EncryptionToken。",[1004,1265,1267],{"id":1266},"服务器响应hello","服务器响应Hello",[11,1269,1270],{},"服务端收到NMT_Hello之后，总共有三个步骤：",[1272,1273,1275],"h5",{"id":1274},"version-check","Version Check",[11,1277,1278],{},"首先会对其携带的版本数据进行校验，如果不一致的话，就会返回NMT_Upgrade，要求客户端升级之后再进行连接。",[1272,1280,1282],{"id":1281},"encryption","Encryption",[11,1284,1285],{},"接下来是可选的加密配置过程，如果携带空的EncryptionToken就会跳过这个步骤。",[11,1287,1288],{},"如果Hello携带了EncryptionToken，服务端却没有绑定OnReceivedNetworkEncryptionToken的话，就会返回NMT_Failure。",[11,1290,1291],{},"这个函数默认是绑在UGameInstance::ReceivedNetworkEncryptionToken上的，不过这个默认实现是直接返回失败的。",[11,1293,1294],{},"在客户端由一个相对的处理流程：",[879,1296,1299],{"className":1297,"code":1298,"language":884},[882],"FNetDelegates::OnReceivedNetworkEncryptionToken.BindUObject(this, &ThisClass::ReceivedNetworkEncryptionToken);\nFNetDelegates::OnReceivedNetworkEncryptionAck.BindUObject(this, &ThisClass::ReceivedNetworkEncryptionAck);\n",[886,1300,1298],{"__ignoreMap":401},[11,1302,1303],{},"最终会使用Handler上的FEncryptionComponent来对数据进行处理，是否有这个包处理组件由net.AllowEncryption这个Console来决定。加密组件在Engine.ini中指定：",[879,1305,1308],{"className":1306,"code":1307,"language":884},[882],"[PacketHandlerComponents]\nEncryptionComponent=AESHandlerComponent\nbEnableReliability=false\n",[886,1309,1307],{"__ignoreMap":401},[11,1311,1312],{},"在引擎中搜索AESHandlerComponent能看到对应的加密组件的实现，以及还有RSAKeyAESEncryptionHandlerComponent、RSAEncryptionHandlerComponent、BlockEncryptionHandlerComponent、StreamEncryptionHandlerComponent这些组件，这里由于没有使用到，不再深究。",[11,1314,1315],{},"在UWorld::SendChallengeControlMessage的处理中可以看到，如果服务端的加密校验失败，会返回NMT_Failure，如果成功则会发送NMT_EncryptionAck到客户端并设置加密秘钥。",[11,1317,1318],{},"这里的FEncryptionKeyResponse是来源于UGameInstance::ReceivedNetworkEncryptionToken的，默认实现是直接Failure。",[11,1320,1321],{},"NMT_Failue一般都会携带具体的错误理由，可以按图索骥。",[1272,1323,1325],{"id":1324},"challenge-client","Challenge Client",[11,1327,1328],{},"如果没有提供加密的Key或者通过了加密的构造流程的话，会给客户端发送NMT_Challenge。",[1004,1330,1331],{"id":1331},"登录操作",[11,1333,1334],{},"接下来的连接过程就比较简单了：",[11,1336,1337],{},"客户端收到Challenge之后会收集登录信息，然后发送NMT_Login。",[11,1339,1340],{},"服务器处理Login之后回复NMT_Welcome。",[11,1342,1343],{},"客户端收到NMT_Welcome后，会记录下其中携带的地图加载对应的关卡，并向服务器上报客户端的网速NMT_Netspeed。然后到下一个Tick，会对地图进行加载，加载成功时，会发送NMT_Join并断开UPendingNetGame与Context的关联。",[11,1345,1346],{},"服务器处理NMT_Join后，会建立Connection与PlayerController的关联，客户端和服务端的连接就完全建立起来了。之后的操作就是使用RPC进行了。",[1004,1348,1349],{"id":1349},"其他控制操作",[11,1351,1352],{},"控制信息中还有一些其他的与连接建立过程关系不大的类型，在这里也稍微记录一下：",[1272,1354,1356],{"id":1355},"nmt_netguidassign","NMT_NetGUIDAssign",[11,1358,1359],{},"这个看注释似乎很有用",[543,1361,1362],{},[11,1363,1364],{},"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,1366,1367],{},"不过似乎没有实际作用，所有的调用会到达ResolvePathAndAssignNetGUID，这个函数的两个实现都没有作处理，在UPackageMapClient::ResolvePathAndAssignNetGUID中甚至能看到check(0)。",[1272,1369,1371],{"id":1370},"nmt_debugtext","NMT_DebugText",[11,1373,1374],{},"可以让连接的另一端输出调试信息。这个是给NETDEBUGTEXT指令用的。",[1272,1376,1378],{"id":1377},"nmt_gamespecific","NMT_GameSpecific",[11,1380,1381],{},"会将发送的内容推送到HandleGameNetControlMessage，发送的内容为uint8和FString。用uint8来指定数据类型，FString来作为数据内容。基本能够满足需求。",[18,1383,1384],{"id":1384},"数据收发",[11,1386,1387],{},"从最下往上看的话，数据在从Socket被取出后，首先会经过PacketHandler，然后到达NetDriver，在寻找到对应的Connection之后会通过ReceivedRawPacket将数据指针推送给对应的Connection。",[1004,1389,1391],{"id":1390},"packethandler","PacketHandler",[11,1393,1394],{},"UNetConnection::ReceivedRawPacket的处理主要集中在PacketHandler上，默认的情况下，工作的只有StatelessConnectHandlerComponent。",[11,1396,1397],{},"加密组件需要自己开启，也可以自己定义一些组件来在数据包一层进行处理。由于这里的是RawData，所以比较适合做加密、压缩或者校验之类的。因为在这里处理的数据还和UE4上层的网络结构没有什么关系。",[11,1399,1400],{},"在数据从Connection进一步发送到UE4逻辑之前，PacketHandler有两个可以处理的接入点。",[11,1402,1403],{},"一个是Incoming，一个是IncomingHigh。这两个处理的逻辑位置有些不同。",[11,1405,1406],{},"InComing是直接处理的原始数据，而InComingHigh接入的是经过了序列化之后的FBitReader。",[11,1408,1409],{},"经过这两个步骤之后，FBitReader会被发送到UNetConnection::ReceivedPacket。",[1004,1411,1413],{"id":1412},"bunch和channel","Bunch和Channel",[11,1415,1416],{},"ReceivedPacket这里会对Ack包和数据包进行区分处理。",[11,1418,1419],{},"Ack包会进入Ack处理逻辑。",[11,1421,1422],{},"数据包在作Ack处理后，被进一步的打包成FInBunch，在逻辑上这里的Bunch应当被称为RawBunch。之所以称为RawBunch，是因为UE4在数据包发送的时候会对上层过来的Bunch做分包处理。",[11,1424,1425],{},"RawBunch打包完成时，就已经获取了数据包对应的Channel，之后就会发送到UChannel::ReceivedRawBunch。",[1004,1427,1429],{"id":1428},"receivedrawbunch","ReceivedRawBunch",[11,1431,1432],{},"由于网络收发的不确定性，这里会有Reliable的RawBunch的第一道关卡，对于到达的数据包的序列号不符合预期的情况，数据包会被缓存到",[879,1434,1437],{"className":1435,"code":1436,"language":884},[882],"class FInBunch*        InRec;\n",[886,1438,1436],{"__ignoreMap":401},[11,1440,1441],{},"这个缓存是有大小限制的",[879,1443,1446],{"className":1444,"code":1445,"language":884},[882],"enum { RELIABLE_BUFFER = 256 }; \u002F\u002F Power of 2 >= 1.\n",[886,1447,1445],{"__ignoreMap":401},[11,1449,1450],{},"如果出现奇怪的丢包问题可以关注下Log的输出~",[11,1452,1453],{},"对于到达的有序RawBunch，则进入下一步处理：UChannel::ReceivedNextBunch",[1004,1455,1457],{"id":1456},"receivednextbunch","ReceivedNextBunch",[11,1459,1460],{},"到达了这里主要做的操作是对RawBunc进行合并，合并依赖的是以下标志位：",[879,1462,1465],{"className":1463,"code":1464,"language":884},[882],"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",[886,1466,1464],{"__ignoreMap":401},[11,1468,1469],{},"bPartialFinal为合并终止包。OutBunch和InBunch分开定义，VA会搜索不到。",[11,1471,1472],{},"合包时会有一个缓存，防止剩余包未到达的情况：",[879,1474,1477],{"className":1475,"code":1476,"language":884},[882],"class FInBunch*        InPartialBunch;        \u002F\u002F Partial bunch we are receiving (incoming partial bunches are appended to this)\n",[886,1478,1476],{"__ignoreMap":401},[11,1480,1481],{},"完成包合并后的Bunch会被交由UChannel::ReceivedSequencedBunch处理。",[11,1483,1484],{},"UChannel::ReceivedSequencedBunch则进一步调用到Channel的虚函数ReceivedBunch。",[1004,1486,1488],{"id":1487},"receivedbunch","ReceivedBunch",[11,1490,1491],{},"ActorChannel在得到Bunch之后进一步通过ProcessBunch将处理交到FObjectReplicator::ReceivedBunch中，在ReadFieldHeaderAndPayload解析之后，根据Filed信息来做更多的操作。",[879,1493,1496],{"className":1494,"code":1495,"language":884},[882],"\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",[886,1497,1495],{"__ignoreMap":401},[11,1499,1500],{},"这样，数据包就到达了UE4中的RPC和属性同步处理逻辑了。",[11,1502,1503],{},"在ProcessBunch中还有一个FNetworkGUID相关的逻辑，这个是一开始Actor初始化网络连接时会用到的处理流程。需要进一步了解的话，可以参考这个函数：",[879,1505,1508],{"className":1506,"code":1507,"language":884},[882],"\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",[886,1509,1507],{"__ignoreMap":401},[1004,1511,1512],{"id":1512},"数据发送",[11,1514,1515],{},"发送数据的过程基本上和接收数据的过程相反。不过在分包之后到发送RawBunch的过程更为简单一些。因为这里不会有序列的问题，只有在RawBunch发送之后会有一个Ack维护和重发的逻辑。",[11,1517,1518],{},"与InRec对应的缓存为OutRec，同样会应用缓存数量上限。",[11,1520,1521],{},"RPC会在发送时不会等待TickFlush，而是会直接调用Channel的SendBunch并立即调用FlushNet。只有两种情况例外，一种是函数本身被标记为ForceQueue的，另一种是非Reliable的多播函数。",[11,1523,1524],{},"多播函数官方并不建议使用Reliable，因为会导致相距很远、已经休眠的Channel重新被打开并在发送完毕之后又被关闭。",[11,1526,1527],{},"对于不立即调用的RPC，会被缓存并在之后走值复制的逻辑进行发送。",[879,1529,1532],{"className":1530,"code":1531,"language":884},[882],"FObjectReplicator::QueueRemoteFunctionBunch\n\nPackageMapClient->AppendExportBunches( OwningChannel->QueuedExportBunches );\n",[886,1533,1531],{"__ignoreMap":401},[11,1535,1536],{},"RPC的发送还涉及将参数进行NetSerializer发送的过程，这里没有深究，如有需要可以从UNetDriver::ProcessRemoteFunctionForChannel入手。",[18,1538,1539],{"id":1539},"值复制处理",[11,1541,1542],{},"值复制的数值对比部分的实际逻辑执行由FObjectReplicator::ReplicateProperties负责。",[11,1544,1545],{},"在ActorChannel确立之后，对于像是int、float这样的属性，只要进行简单的内存对比就可以知道有没有修改并进行发送了。",[11,1547,1548],{},"但是对于TArray这样的“动态”内容，就需要更多的处理。另外，当前版本的引擎是不提供TMap与TSet的值复制逻辑的。",[11,1550,1551],{},"在初始构造连接时的复制时或者RPC进行参数传递时会进行NetSerializer，而在之后的值复制过程中，会对值与之前的值进行对比，使用NetDeltaSerializer来判别变动了量并只对这些值进行发送。",[11,1553,1554],{},"当然，实际在进行的时候会有很多类和机制上的处理，但是大体上就是这个样子。想要进一步了解其执行方式的话，可以看SerializeProperties_r和CompareProperties_r。对于RPC可以参考以下函数：",[879,1556,1559],{"className":1557,"code":1558,"language":884},[882],"\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",[886,1560,1558],{"__ignoreMap":401},[11,1562,1563],{},"对于TArray，有一个专门的逻辑进行处理，主要在CompareProperties_Array_r中。会对数组进行判定，只对“改变”了的数值进行复制。如果只是简单的值修改的话，只会复制修改的部分。如果是在尾部添加和删除的话也不会有太大的数据量。但是如果是将中间的某个值删除的话，会被视为那个值之后的所有量都变动了，所以也并不是万能的。",[11,1565,1566],{},"因此官方还提供了一个FastArray来应对一些特殊的情况。",[971,1568,1570],{"id":1569},"自定义netserialization","自定义NetSerialization",[11,1572,1573],{},"为了提供一些自定义的结构的值复制逻辑变更，NetSerialization和NetDeltaSerialization是可以自定义的。",[11,1575,1576],{},"NetSerialization用于将整个结构序列化，这个其实在引擎内部的使用非常多，可以通过FVector_NetQuantize来参考如何进行实现。",[11,1578,1579],{},"NetDeltaSerialization用于生成状态差分，这个貌似只有FastArray在用。",[971,1581,1583],{"id":1582},"fastarray","FastArray",[11,1585,1586],{},"快速值复制的数组，其快速并不反应在初始的NetSerialization上，而是通过NetDeltaSerialization在进行差分对比的时候比TArray更加快速。",[11,1588,1589],{},"但是相对的，也会有缺点，那就是通过FastArray进行的值复制是不会保证数组在服务端和客户端之间的次序一致性的。",[11,1591,1592],{},"想要进一步使用FastArray同步逻辑的，可以参考NetSerialization.h中的注释，里面有一步一步的指导如何使用这个复制方法。",[11,1594,1595],{},"总体上，我们要做的其实只是定义一个支持这个复制方式的Struct而已，然后在发生了变动的时候，调用对应的接口就好了。引擎中有几处有使用到FastArray，参考FLobbyPlayerStateInfoArray也可以获得一些具体用法相关的知识。",[18,1597,1599],{"id":1598},"_420的优化项","4.20的优化项",[11,1601,1602],{},"这两个优化项记得是当时GDC上有说的从Fortnite合入的。",[971,1604,1606],{"id":1605},"significancemanager","SignificanceManager",[11,1608,1609],{},"一个单独的用来更新物体之间的相关性的类。",[11,1611,1612],{},"需要自己在合适的地方调用Update，而这个USignificanceManager::Update就是整个处理逻辑的核心。它会调用我们定义的优先级计算函数，之后对计算结果进行排序处理。",[11,1614,1615],{},"使用时需要注意的是，这个类本身并不提供性能提升，而是会对优先级进行管理，方便其他系统对性能进行调整。",[11,1617,1618],{},"另外，在注册到Manager的时候要留意EPostSignificanceType的传入值，可能会导致FPostSignificanceFunction的调用异步到达。而FSignificanceFunction原本就是异步的，所以不能在里面进行一些可能导致死锁的操作，或者默认假定它是单线程的。",[11,1620,1621],{},"从代码看",[879,1623,1626],{"className":1624,"code":1625,"language":884},[882],"SignificanceManagerClass = LoadClass\u003CUSignificanceManager>(nullptr, *GetDefault\u003CUSignificanceManager>()->SignificanceManagerClassName.ToString());\n",[886,1627,1625],{"__ignoreMap":401},[11,1629,1630],{},"这个类和UNetDriver那些一样，可以通过配置指向自己重载的基类。",[11,1632,1633,1634,1639],{},"具体的使用方法可以参考[",[834,1635,1638],{"href":1636,"rel":1637},"https:\u002F\u002Fdocs.unrealengine.com\u002Fen-us\u002FEngine\u002FPerformance\u002FSignificanceManager",[838],"官方文档","]。",[971,1641,1643],{"id":1642},"replicationgraph","ReplicationGraph",[11,1645,1646],{},"这个是直接介入到UNetDriver中的优化类，要使用这个类可以通过配置来重新导向：",[879,1648,1651],{"className":1649,"code":1650,"language":884},[882],"[\u002FScript\u002FOnlineSubsystemUtils.IpNetDriver]\nReplicationDriverClassName=\"\u002FScript\u002FMyGame.MyReplicationGraph\"\n",[886,1652,1650],{"__ignoreMap":401},[11,1654,1655],{},"或者将自己的构造函数绑定到",[879,1657,1660],{"className":1658,"code":1659,"language":884},[882],"UReplicationDriver::CreateReplicationDriverDelegate()\n",[886,1661,1659],{"__ignoreMap":401},[11,1663,1664],{},"这个Delegate上。",[11,1666,1667],{},"这个优化的作用，官方是这样描述的",[879,1669,1672],{"className":1670,"code":1671,"language":884},[882],"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",[886,1673,1671],{"__ignoreMap":401},[11,1675,1676],{},"总体上而言，就是可以实现自定义的值复制逻辑。官方给出的使用示例是：",[543,1678,1679],{},[640,1680,1681,1684,1687,1690,1693],{},[37,1682,1683],{},"将Actor通过位置聚合在一起提供更新判定，例如MMO中的独立的房间或者区域",[37,1685,1686],{},"对于独特的休眠物体进行分组管理，例如场景中的树，虽然只在被破坏的时候需要状态更新，却又不能降低更新频率",[37,1688,1689],{},"如果玩家可以推动或拾起物体，可以将它们的更新聚集到持有者上",[37,1691,1692],{},"可以将需要常态更新的物体聚合在一个组内，避免不必要的相关性检查",[37,1694,1695],{},"将对于指定Actor永远不需要和一直需要的相关性聚集在一起",[11,1697,1698],{},"逻辑上主要替换的是",[879,1700,1703],{"className":1701,"code":1702,"language":884},[882],"\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",[886,1704,1702],{"__ignoreMap":401},[11,1706,1707],{},"也就是说替换的是值复制从NetDriver到Channel为止的判定逻辑，对于Fortnite这种据说同步物体达到50000个左右的大规模Actor的值同步很有作用。",[11,1709,1710],{},"默认情况下的同步数据收集是会直接对所有的Actor进行遍历的，这个逻辑相对单纯，无法进行复杂的关系设定。而ReplicationGraph引入了Node来对Actor进行关系管理，这个Node也可以自己进行定义。这样就可以通过更加复杂的逻辑来控制Actor的复制关系。尽可能的减少浪费。",[11,1712,1713,1714,1639],{},"可以通过UBasicReplicationGraph看基本的功能是如何接入到UNetDriver系统中去的，据说ShooterGame中有更加详细的实现。需要更多信息也可以参考[",[834,1715,1638],{"href":1716,"rel":1717},"https:\u002F\u002Fdocs.unrealengine.com\u002Fen-US\u002FEngine\u002FNetworking\u002FReplicationGraph",[838],[1719,1720,1721],"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":401,"searchDepth":402,"depth":403,"links":1723},[1724,1737,1746,1754,1758],{"id":966,"depth":402,"text":966,"children":1725},[1726,1727,1732],{"id":973,"depth":403,"text":973},{"id":992,"depth":403,"text":993,"children":1728},[1729,1731],{"id":1006,"depth":1730,"text":1007},4,{"id":1028,"depth":1730,"text":1029},{"id":1069,"depth":403,"text":1070,"children":1733},[1734,1735,1736],{"id":1097,"depth":1730,"text":1097},{"id":1127,"depth":1730,"text":1127},{"id":1160,"depth":1730,"text":1161},{"id":1191,"depth":402,"text":1191,"children":1738},[1739,1740],{"id":1206,"depth":403,"text":1207},{"id":1240,"depth":403,"text":1241,"children":1741},[1742,1743,1744,1745],{"id":1247,"depth":1730,"text":1248},{"id":1266,"depth":1730,"text":1267},{"id":1331,"depth":1730,"text":1331},{"id":1349,"depth":1730,"text":1349},{"id":1384,"depth":402,"text":1384,"children":1747},[1748,1749,1750,1751,1752,1753],{"id":1390,"depth":1730,"text":1391},{"id":1412,"depth":1730,"text":1413},{"id":1428,"depth":1730,"text":1429},{"id":1456,"depth":1730,"text":1457},{"id":1487,"depth":1730,"text":1488},{"id":1512,"depth":1730,"text":1512},{"id":1539,"depth":402,"text":1539,"children":1755},[1756,1757],{"id":1569,"depth":403,"text":1570},{"id":1582,"depth":403,"text":1583},{"id":1598,"depth":402,"text":1599,"children":1759},[1760,1761],{"id":1605,"depth":403,"text":1606},{"id":1642,"depth":403,"text":1643},"2018-10-14",{"layout":417,"status":418,"published":419,"author":1764,"author_login":422,"author_email":423,"wordpress_id":1765,"wordpress_url":1766,"date_gmt":1767,"excerpt":1768},{"display_name":421,"login":422,"email":423,"url":401},2600,"\u002F?p=2600","2018-10-14 12:47:27 +0000",{"type":8,"value":1769},[1770],[11,1771,957],{},"\u002F2018-10-14-ue4-network-overview",{"title":952,"description":957},"_legacy\u002F2018\u002F2018-10-14-ue4-network-overview",[1776,1777],"UE4","NetWork","_ff1bmp4Ic9JuvoAnCa3tHweXg2UkFvjKZx9rib05WM",{"id":1780,"title":1781,"body":1782,"date":2113,"description":1786,"extension":415,"meta":2114,"navigation":419,"path":2123,"seo":2124,"stem":2125,"tags":2126,"__hash__":2128},"blogs\u002F_legacy\u002F2018\u002F2018-09-15-precise-timestamp.md","精确时间戳整理",{"type":8,"value":1783,"toc":2097},[1784,1787,1790,1793,1801,1804,1807,1812,1815,1818,1821,1824,1828,1831,1839,1843,1846,1855,1861,1864,1878,1886,1889,1893,1896,1905,1908,1913,1918,1921,1926,1929,1932,1936,1939,1942,1945,1951,1954,1957,1966,1969,1972,1975,1979,1982,1985,1988,1991,1996,1999,2004,2007,2010,2013,2016,2019,2022,2037,2041,2044,2048,2051,2054,2060,2063,2067,2070,2076,2079,2085,2088,2091,2094],[11,1785,1786],{},"当需要进行类似定时抢购或者用户操作验证时，就需要获得用户的精确时间戳。",[11,1788,1789],{},"当前使用的UE4版本为4.20。",[11,1791,1792],{},"首先，对于时间戳，这里需要的是满足这两个条件：",[640,1794,1795,1798],{},[37,1796,1797],{},"它是自然增加的，不会因为正常操作回转",[37,1799,1800],{},"它与现实时间具有同等的流速，不会突然跳变。",[11,1802,1803],{},"基本上就是通常意义上的MONOTONIC的时钟了，不过这个词的具体意义我也不是很明确就是了~",[11,1805,1806],{},"从wiki上的解释来看：",[543,1808,1809],{},[11,1810,1811],{},"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,1813,1814],{},"Monotonic似乎只有保持单调上升的定义，不过这里还是借用下吧。",[18,1816,1817],{"id":1817},"时间戳的获取",[11,1819,1820],{},"如果要使用上面的两个条件的话，关卡时间记录的TimeSeconds是不行的，因为它在游戏暂停的时候是不增加的。而RealTimeSeconds，虽然其间接来源是FPlatformTime::Seconds，但是中间会有演算的精度丢失，要使用的话不如直接使用FPlatformTime::Seconds。",[11,1822,1823],{},"FPlatformTime::Seconds在大部分情况下都是可用的，但是它有不同的平台实现，在实际使用时还是会遇到平台相关问题。",[971,1825,1827],{"id":1826},"windows","Windows",[11,1829,1830],{},"在Windows平台上，FPlatformTime::Seconds使用的是QueryPerformanceCounter。这是一个硬件时钟，不会受到用户时间调整、系统挂起等各种操作的影响，Windows上还可以使用TSC来获取CPU时钟，这个时钟微软并不推荐使用，因为它会受到CPU频率动态调整等硬件调节的影响。",[11,1832,1833,1834,871],{},"详细的说明可以参照微软的官方说明：",[834,1835,1838],{"href":1836,"rel":1837},"https:\u002F\u002Fdocs.microsoft.com\u002Fen-us\u002Fwindows\u002Fdesktop\u002Fsysinfo\u002Facquiring-high-resolution-time-stamps",[838],"Acquiring high-resolution time stamps",[971,1840,1842],{"id":1841},"linux","Linux",[11,1844,1845],{},"在Linux平台上有一个通用的时间获取函数clock_gettime，可以获得各种类型的时钟值。",[11,1847,1848,1849,1854],{},"这个函数的具体用法可以参考",[834,1850,1853],{"href":1851,"rel":1852},"http:\u002F\u002Fman7.org\u002Flinux\u002Fman-pages\u002Fman2\u002Fclock_gettime.2.html",[838],"clock_gettime","的文档，UE4在使用时会首先对各个类型的时钟进行一次效率对比，因为在不同的Linux内核版本上可能会有性能的差异，或者是没有实现的时钟出现。",[879,1856,1859],{"className":1857,"code":1858,"language":884},[882],"{ 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",[886,1860,1858],{"__ignoreMap":401},[11,1862,1863],{},"这四种时钟的不同在文档中有描述，这里作一个简要的整理：",[543,1865,1866,1869,1872,1875],{},[11,1867,1868],{},"CLOCK_REALTIME: 这个时钟获取的是时钟时间，会受到系统时间调整和NTP的影响。",[11,1870,1871],{},"CLOCK_MONOTONIC: 不会受到系统时间调整影响的时钟，记录的是从一个未指定的起始时间到现在的时间，但会受adjtime和NTP的影响。",[11,1873,1874],{},"CLOCK_MONOTONIC_RAW: 比MONOTONIC更加严格，甚至不会受到adjtime和NTP的影响。2.6.28内核以上才有。",[11,1876,1877],{},"CLOCK_MONOTONIC_COARSE: 一个更快速和低精确度的MONOTONIC时钟，需要系统架构支持。2.6.32内核以上才有。",[11,1879,1880,1881,1639],{},"这里有个问题，就是NTP。由于对Linux不是很熟悉，所以对NTP很为困惑。NTP简单的来看就是联网对时机制，从结果上看最终与adjtime一样，并不会导致CLOCK_MONOTONIC回转，只会让这个时钟变得更加接近世界时间，会造成时间计算的频率发生变化计让算结果出现一些小的不连续。而CLOCK_MONOTONIC_RAW则更接近于硬件时间，更加适合于硬件相关的监测或者线程同步。详细的说明可以参考[",[834,1882,1885],{"href":1883,"rel":1884},"https:\u002F\u002Fstackoverflow.com\u002Fa\u002F14270415",[838],"这里",[11,1887,1888],{},"结论上看，就是CLOCK_MONOTONIC基本就够用了。",[971,1890,1892],{"id":1891},"android","Android",[11,1894,1895],{},"由于Android的内核就是Linux的，所以使用的也是clock_gettime，不过这里并没有作时钟性能的比较，而是直接取用的CLOCK_MONOTONIC。大概是因为Android所基于的Linux内核版本是确定的吧。",[11,1897,1898,1899,1904],{},"Android在JavaApi一层是有提供elapsedRealtime函数来获取系统启动至今的时间的。从Android的源码[",[834,1900,1903],{"href":1901,"rel":1902},"https:\u002F\u002Fandroid.googlesource.com\u002Fplatform\u002Fsystem\u002Fcore\u002F+log\u002Fmaster\u002Flibutils\u002FSystemClock.cpp",[838],"变更记录","]上来看，这个API的实现经历过不少的变更。",[11,1906,1907],{},"最后采用的是使用clock_gettime的CLOCK_BOOTTIME来实现的，在过去的时代，Linux内核似乎有过bug，导致通过获得的CLOCK_BOOTTIME有问题。而在这个提交之后：",[11,1909,1910],{},[980,1911],{"alt":982,"src":1912},"\u002Fwp-content\u002Fuploads\u002F2018\u002F09\u002Fimage_thumb-4.png",[11,1914,1915],{},[980,1916],{"alt":982,"src":1917},"\u002Fwp-content\u002Fuploads\u002F2018\u002F09\u002Fimage_thumb-5.png",[11,1919,1920],{},"又开始重新回归了CLOCK_BOOTTIME，由于这个变更过程，所以不是很确定clock_gettime在旧版本Android上究竟会受到怎样的影响。从Android本身的变更上看：",[11,1922,1923],{},[980,1924],{"alt":982,"src":1925},"\u002Fwp-content\u002Fuploads\u002F2018\u002F09\u002Fimage_thumb-6.png",[11,1927,1928],{},"由于UE4对Android的API Level是从18还是19开始的，Linux内核已经到了3.0以上，应当对用于时间戳的CLOCK_MONOTONIC不会产生影响才对。如果在实际中遇到问题的话，就只能依赖于AndroidJavaEnv::GetJavaEnv来获取JavaAPI了。",[11,1930,1931],{},"在Android上使用FPlatformTime::Seconds有一个需要注意的问题，Androd平台使用clock_gettime的实现是4.20之后的版本才有的！如果你工作在4.20之前的版本的话，FPlatformTime::Seconds的实现是使用的默认的FGenericPlatformTime，只是单纯的将系统当前时间转换为秒数。",[971,1933,1935],{"id":1934},"ios","IOS",[11,1937,1938],{},"在IOS10之后，IOS有将clock_gettime从内核暴露出来，所以如果是针对之后的版本的话，是没有问题的。但是在IOS10之前是没有这个函数的，所以UE4在TimeSeconds中使用的是mach_absolute_time。",[11,1940,1941],{},"这个时钟有一个问题，那就是它会随着IOS锁屏而停止。",[11,1943,1944],{},"因此在IOS上如果想要获取一个MONOTONIC的时钟，实际上需要使用一个Trick：",[879,1946,1949],{"className":1947,"code":1948,"language":884},[882],"#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",[886,1950,1948],{"__ignoreMap":401},[11,1952,1953],{},"因为IOS的系统启动时间和当前时间记录都是会受系统时间变更影响的，所以它们的差值就能代表固定的时间流逝值。",[11,1955,1956],{},"上面的while是为了防止在两次时间获取之间发生时间变动而设定的，否则有可能在极其偶然的情况下导致获得的时间出现问题。",[11,1958,1959,1960,1965],{},"这个Trick来源于[",[834,1961,1964],{"href":1962,"rel":1963},"https:\u002F\u002Fstackoverflow.com\u002Fa\u002F40497811",[838],"stackoverflow","]，从大家评论的验证结果上看，并没有什么问题出现。",[18,1967,1968],{"id":1968},"浮点数精度问题",[11,1970,1971],{},"在上面的实现中，我们获得的时间戳都是由uint64的秒数单位构成然后转化到double空间的。",[11,1973,1974],{},"这里就会有一个传统的问题，那就是浮点精度溢出。在结论上，如果使用double来作为秒数的时间戳，且时间戳是基于开机的UpTime的话，是不需要处理浮点数精度问题的。",[971,1976,1978],{"id":1977},"ieee754","IEEE754",[11,1980,1981],{},"现代计算机对浮点数的存储基本遵循一个统一的标准，这是算法之前的事情，这里面有着许多关于数学和硬件实现的相关问题，不过已经离我们这样的程序员很远了。",[11,1983,1984],{},"简单的讲，计算机对浮点数的存储采用的是科学计数法。",[11,1986,1987],{},"实际存储的格式为：",[11,1989,1990],{},"double",[11,1992,1993],{},[980,1994],{"alt":982,"src":1995},"\u002Fwp-content\u002Fuploads\u002F2018\u002F09\u002Fimage_thumb-7.png",[11,1997,1998],{},"float",[11,2000,2001],{},[980,2002],{"alt":982,"src":2003},"\u002Fwp-content\u002Fuploads\u002F2018\u002F09\u002Fimage_thumb-8.png",[11,2005,2006],{},"上面两张图来自维基百科~",[11,2008,2009],{},"抛开实际进行浮点数表示的复杂实现的细节进行粗略的估算的话，在小数点位数为0位时，我们能从dobule中获得的精确的自然数与uint52相当，从float中能获得的则为uint23，由于float自带符号位，所以范围可以再乘以2。",[11,2011,2012],{},"那么把同等位数的int转化到同等位数的浮点数的话，一定是会引起精度丢失的。",[11,2014,2015],{},"简化的来看float能够表示的连续的正整数应该在2的23次方左右，也就是大概到8388608，要保留至少两位小数的话，就是83886。将这个作为秒数来计算，大概为23.3个小时。当然，实际的浮点数表示肯定能够容纳更多的数值空间。但是至少说明，在以天为单位的情况下，float用来表示秒数是有丢失精度的危险的。",[11,2017,2018],{},"由于服务器可能在20fps左右进行tick，那么单帧间隔在0.05秒左右，如果不能保证这个精度被表示到的话，很有可能造成运算混乱。",[11,2020,2021],{},"而如果是double的时间戳的话，使用这样的简化推算的话，两位数的精度可以维持521249956.8715天，也就是说能够在大概100万年内都不需要担心精度问题。由于时间戳使用的是system up time，所以如果真的会出问题的话，我们应该担心的是其他的方面~",[11,2023,2024,2025,2030,2031,2036],{},"不过这个推算只是一个大致的直观估计，想要更详细的了解这个主题的话，可以参考[",[834,2026,2029],{"href":2027,"rel":2028},"http:\u002F\u002Fmath.ecnu.edu.cn\u002F~jypan\u002FTeaching\u002Fbooks\u002Fsun_nc.pdf",[838],"Sun Studio 11: 數值計算指南","]中附录D对《",[834,2032,2035],{"href":2033,"rel":2034},"https:\u002F\u002Fdl.acm.org\u002Fcitation.cfm?id=103163",[838],"What every computer scientist should know about floating-point arithmetic","》的翻译。",[971,2038,2040],{"id":2039},"ue4中的一些处理","UE4中的一些处理",[11,2042,2043],{},"UE4的TickTime的Delta的直接来源是FPlatformTime::Seconds() ，这个是double的，所以不会有什么问题。",[1004,2045,2047],{"id":2046},"timeseconds","TimeSeconds",[11,2049,2050],{},"但如果是使用UE4本身的UWorld的时间登记GetWorld()->TimeSeconds 的话，是有可能遇到时间戳精度丢失的。不过要绕过UE4的保护机制，达到精度丢失的程度的话，还是需要一些特定的条件的。",[11,2052,2053],{},"从代码中看的话，UE4对TimeSeconds的保护只有这里有：",[879,2055,2058],{"className":2056,"code":2057,"language":884},[882],"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",[886,2059,2057],{"__ignoreMap":401},[11,2061,2062],{},"这个函数是在地图加载的时候调用的，也就是如果UWorld的生命周期超过了48个小时的话，就不允许SeamlessTravel，而是要求UWorld强制重新构建了。",[1004,2064,2066],{"id":2065},"movetimestamp","MoveTimeStamp",[11,2068,2069],{},"在角色移动组件中，有服务端和客户端的时间戳处理。这里官方在处理的时候引入了一个自动的回转机制",[879,2071,2074],{"className":2072,"code":2073,"language":884},[882],"\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",[886,2075,2073],{"__ignoreMap":401},[11,2077,2078],{},"官方的说明如下：",[879,2080,2083],{"className":2081,"code":2082,"language":884},[882],"\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",[886,2084,2082],{"__ignoreMap":401},[11,2086,2087],{},"也就是说，为了防止float引起的浮点精度丢失，会在累计到这个时间后将时钟往回拨。因为物理运算对时间的精度非常的敏感，这个参数的默认值为240s。",[11,2089,2090],{},"理论上，使用这个时间戳也是可以实现部分功能的。但是由于它会回转，所以可能不适用于所有的情况，会有通用性的问题。另外，这个时间戳严重依赖于移动组件，很难理清其中的头绪，会在不必要的地方花费分离逻辑的成本以及造成逻辑在奇怪的地方耦合。",[18,2092,2093],{"id":2093},"总结",[11,2095,2096],{},"时间戳的获取需要留心各个平台不同的实现，尤其是要小心FPlatformTime::Seconds()，由于平台实现不同它所能提供的保证是不同的。另外，如果对应的平台UE4本身没有对其进行实现的话，使用的会是默认的FGenericPlatformTime，是直接使用系统时间转化过来的秒数，极有可能不符合使用预期，一定要留心。",{"title":401,"searchDepth":402,"depth":403,"links":2098},[2099,2105,2112],{"id":1817,"depth":402,"text":1817,"children":2100},[2101,2102,2103,2104],{"id":1826,"depth":403,"text":1827},{"id":1841,"depth":403,"text":1842},{"id":1891,"depth":403,"text":1892},{"id":1934,"depth":403,"text":1935},{"id":1968,"depth":402,"text":1968,"children":2106},[2107,2108],{"id":1977,"depth":403,"text":1978},{"id":2039,"depth":403,"text":2040,"children":2109},[2110,2111],{"id":2046,"depth":1730,"text":2047},{"id":2065,"depth":1730,"text":2066},{"id":2093,"depth":402,"text":2093},"2018-09-15",{"layout":417,"status":418,"published":419,"author":2115,"author_login":422,"author_email":423,"wordpress_id":2116,"wordpress_url":2117,"date_gmt":2118,"excerpt":2119},{"display_name":421,"login":422,"email":423,"url":401},2583,"\u002F?p=2583","2018-09-15 14:11:20 +0000",{"type":8,"value":2120},[2121],[11,2122,1786],{},"\u002F2018-09-15-precise-timestamp",{"title":1781,"description":1786},"_legacy\u002F2018\u002F2018-09-15-precise-timestamp",[1776,2127],"TimeStamp","PSxMrIGXAhchXkLgLKRW4v7HxLAWiix4RmZkk5vGMRs",222,1788763182529]