[{"data":1,"prerenderedAt":2043},["ShallowReactive",2],{"blog-page-10":3,"blog-count":2042},[4,670,775,946,1368],{"id":5,"title":6,"body":7,"date":646,"description":13,"extension":647,"meta":648,"navigation":651,"path":663,"seo":664,"stem":665,"tags":666,"__hash__":669},"blogs\u002F_legacy\u002F2019\u002F2019-11-24-ue4-characer-move-note.md","UE4人物同步机制小结",{"type":8,"value":9,"toc":598},"minimark",[10,14,17,20,23,26,30,42,45,52,61,65,68,71,75,78,88,91,94,100,103,113,121,124,127,133,136,139,142,145,148,151,154,157,160,163,167,170,176,179,182,185,188,191,194,197,200,203,206,209,212,215,221,224,227,242,245,248,251,254,257,260,263,266,269,273,276,280,287,290,293,296,299,302,305,308,311,317,320,323,326,329,332,339,342,345,348,351,354,360,363,366,369,372,376,383,389,392,395,398,401,404,408,411,414,420,423,426,430,433,440,451,455,458,461,464,467,470,473,477,480,483,486,489,492,495,498,501,504,510,513,518,521,526,529,534,537,541,544,547,550,554,557,560,563,567,570,573,576,580,583,586,589,592,595],[11,12,13],"p",{},"虚幻自带的人物网络同步移动机制在结构上有些复杂，这边刚好最近有接触到，稍微整理下逻辑。",[11,15,16],{},"当前的UE4版本为4.23。",[11,18,19],{},"由于部分描述可能是来自项目魔改引擎的经验，所以可能会与实际引擎的工作方式稍有出入。",[11,21,22],{},"按照Role的不同，人物在主控端、服务器端和模拟端分别使用不同的逻辑进行移动模拟。这里只记录了DS的情况，如果是Listen Server的话，在一些细节的部分会稍微有些不同，由于目前没有具体的接触过，便没有详细的考究过。",[11,24,25],{},"在机制上，操作者(ROLE_AutonomousProxy)首先进行移动，然后将移动数据发送到服务器(ROLE_Authority)，服务器进行模拟后判定是否接受操作者上报的位置，并在一帧的末尾将新的角色位置信息同步到模拟端(ROLE_SimulatedProxy)。",[27,28,29],"h2",{"id":29},"服务器",[11,31,32,33,37,38,41],{},"服务器在处理上的逻辑相对比较单纯一些，虽然主控端会根据不同的情况使用不同的RPC来对数据进行发送。到最后都会归结到两个函数上：",[34,35,36],"code",{},"ServerMoveOld","和",[34,39,40],{},"ServerMove","。",[11,43,44],{},"这两个函数的内部逻辑很相似，其中，ServerMove负责主要的移动数据处理，而ServerMoveOld则处理旧的关键包，详细的逻辑整理在主控端逻辑中。",[11,46,47,48,51],{},"服务器使用",[34,49,50],{},"FNetworkPredictionData_Server_Character","来对移动状态进行维护。很多计算的中间和缓存量都会保存在这个数据结构里面。",[11,53,54,55,60],{},"另外，逻辑上，服务器在没有接受到主控端上报的情况下，是不会进行移动模拟的。为了防止出现一些问题，有一个",[56,57,59],"a",{"href":58},"\u002F2019-11-24-ue4-characer-move-note\u002F#ForcePositionUpdate","ForcePositionUpdate","的机制来对服务端行为进行保护。",[62,63,36],"h3",{"id":64},"servermoveold",[11,66,67],{},"目前所有的移动都是走的非可靠包发送的，而ServerMoveOld就是为了防止关键性的移动包被丢包而存在的一个保险机制。",[11,69,70],{},"这个函数的处理相对简单，只作了两个服务器同步的核心处理。",[72,73,74],"h4",{"id":74},"时间戳转验证和转换",[11,76,77],{},"为了保证移动计算的精度，虚幻在移动上报中使用的时间戳并不是光卡时间。而是一个经过回转处理的时间戳。这个设定值是:",[79,80,85],"pre",{"className":81,"code":83,"language":84},[82],"language-text","\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","text",[34,86,83],{"__ignoreMap":87},"",[11,89,90],{},"整个时间戳检查的逻辑都在VerifyClientTimeStamp这个函数中，同时在检查到回转发生的情况下也会对NetworkPredictionData中还会用到的时间戳进行回转处理。",[72,92,93],{"id":93},"服务端模拟",[11,95,96,97,41],{},"在经过时间戳验证后，会根据上报的时间戳来计算一个",[34,98,99],{},"DeltaTime",[11,101,102],{},"虚幻有自带一个时间补偿的反加速机制，但是由于粒度有点粗，所以并没有用过。在没有触发这个机制的情况下，会根据本次的时间戳于上一次的时间戳差来计算一个差值。",[11,104,105,106,108,109,112],{},"当然，服务器不会无限度的接受客户端的",[34,107,99],{},"，考虑到丢包和作弊的情况，有一个 ",[34,110,111],{},"MaxDeltaTime","会对单次处理的最大时间差作限制，这个值的设置是0.125s。",[11,114,115,116,120],{},"取出Delta后，就会调用",[56,117,119],{"href":118},"\u002F2019-11-24-ue4-characer-move-note\u002F#i-19","MoveAutonomous","进行移动的模拟。",[62,122,40],{"id":123},"servermove",[11,125,126],{},"这个函数承担了服务器移动模拟的更多的检查工作，在主控端上报上，由于会出现合包的情况，会有传入位置是特殊的(1,2,3)的情况，例如在ServerMoveDual_Implementation中就有：",[79,128,131],{"className":129,"code":130,"language":84},[82],"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",[34,132,130],{"__ignoreMap":87},[11,134,135],{},"如果在ServerMove作了特殊处理的话需要留意。",[11,137,138],{},"与ServerMoveOld相比，这个函数中多作了几个处理：",[72,140,141],{"id":141},"状态检查和旋转处理",[11,143,144],{},"通过调用NotifyServerReceivedClientData来检查服务器是否处在可以接受移动包的状态。",[11,146,147],{},"同时在执行移动模拟前会将旋转状态传到Controller那边去作处理。",[72,149,150],{"id":150},"位置受理检查",[11,152,153],{},"主要在ServerMoveHandleClientError中进行的一个检查，这里的检查结果将会决定服务器是否接受上报的移动，如果不接受的话会对主控端进行修正。如果接受位置且有设置ClientAuthorativePosition的话，会将服务器的位置设置到上报的位置上去。",[11,155,156],{},"对位置的受理或者修正并不是在这里进行的，而是会将标志位以及必要的信息存在ServerData中，之后再进行处理。",[11,158,159],{},"这边的结果其实有时候比较难以预知，因为前面的ServerMoveOld并没有作这个检查，导致服务器的模拟基础难以预期。",[11,161,162],{},"这些检查的结果也是缓存在ServerData中的。",[164,165,166],"h5",{"id":166},"调用频率控制",[11,168,169],{},"这个频率控制是基于ServerData中的LastUpdateTime的，这个时间会在服务器决定对客户端进行修正时更新。",[79,171,174],{"className":172,"code":173,"language":84},[82],"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",[34,175,173],{"__ignoreMap":87},[11,177,178],{},"也就是说，在当前帧没有修正过主控端位置的情况下，检查是否最近修正过主控端位置。如果有配置对应的频率限制的话，会不进行受理检查。在这种情况下，服务器的移动状态与ServerMoveOld的处理有些类似。",[11,180,181],{},"当然，在ServerData中的强制更新标签等状态也会一直保留，等到下一次进入这个函数会继续起作用。",[164,183,184],{"id":184},"差异检查",[11,186,187],{},"在检查之前会对地板状态进行处理，主要是对动态地板的位置转换，以及对客户端明明在Walking却没有地板的情况进行修正。",[11,189,190],{},"如果此时ServerData中有设置bForceClientUpdate为True的话，就强制进行位置修正。其他情况则会调用ServerCheckClientError对主控端和服务端的移动状态进行比对。",[11,192,193],{},"比对的主要逻辑在ServerCheckClientError中，主要就是对两端的位置差异进行比较，如果超过了阈值则会触发修正。以及，如果两端的MovementMode不一致，则也会强制的触发修正。",[11,195,196],{},"是否受理会决定ServerData中bAckGoodMove的值，影响之后下发数据到主控端的分支选择。",[11,198,199],{},"以及，如果开启了对应选项的话，在受理的同时也会更新服务器的位置到上报位置。",[62,201,59],{"id":202},"forcepositionupdate",[11,204,205],{},"在逻辑上，服务器在收到客户端的数据包之前是不会进行移动的。",[11,207,208],{},"但是如果实际上这样运行的话，会有很多问题。例如，如果玩家在跳到空中之后掉线或者屏蔽ServerMove的话，就可能会一直留在空中了。",[11,210,211],{},"因此，在PlayerController中，有一个额外的ForcePositionUpdate的逻辑。",[11,213,214],{},"这个逻辑会根据配置的MAXCLIENTUPDATEINTERVAL来对移动状态进行检查，如果有一段时间没有收到客户端的ServerMove的话，会强制调用ForcePositionUpdate来对服务器的运动进行模拟。",[79,216,219],{"className":217,"code":218,"language":84},[82],"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",[34,220,218],{"__ignoreMap":87},[11,222,223],{},"最终会调用到PerformMovemen来进行移动的模拟。",[62,225,226],{"id":226},"数据下发",[11,228,229,230,233,234,237,238,241],{},"这里指的主要是针对主控端的数据下发，模拟端的数据是通过正常的值复制逻辑进行的。也就是说是在",[34,231,232],{},"PreReplicate","阶段通过",[34,235,236],{},"GatherCurrentMovement","收集到",[34,239,240],{},"ReplicatedMovement","然后值复制下行的。",[11,243,244],{},"服务器上单帧内，可能会收到很多ServerMove的移动包，其处理结果会被缓存到ServerData之中。",[11,246,247],{},"这个逻辑的核心处理逻辑在SendClientAdjustment中，会在引擎的数据下发阶段，在ServerReplicateActors中直接通过PlayerController调用到。",[11,249,250],{},"ServerData中影响最大的数据是bAckGoodMove，这个标记为True时，就只会调用ClientAckGoodMove来通知主控端服务器已经接受了本次移动，这里下发的是客户端上行的时间戳。如果标记为False，则会对主控端进行修正，根据情况的不同会走不同的RPC进行下发以保证只会发送必要的数据。",[11,252,253],{},"两种下发方式都有各自的频率控制，详细的逻辑可以在UCharacterMovementComponent::SendClientAdjustment进行查看。",[27,255,256],{"id":256},"主控端",[11,258,259],{},"主控端是接受玩家直接控制的，所以在逻辑上是最为复杂的。",[11,261,262],{},"所以，与服务器不同，并不是单纯的一个ServerData的缓存，而是一个SavedMoves的数组在对移动状态缓存。",[62,264,265],{"id":265},"服务器状态接收",[11,267,268],{},"从单帧的时序上，最早执行的是服务器状态回复包的检查。",[72,270,272],{"id":271},"ackgood","AckGood",[11,274,275],{},"如果服务器回报的是AckGoodMove的话，在整体的逻辑上会比较简单。就只是根据回复的时间戳，到历史队列中找到对应的移动包，并将在其之前的缓存包全部删除掉。因为既然服务器已经接受了这个包，那么之前的移动缓存就没有用了，这个逻辑在UCharacterMovementComponent::ClientAckGoodMove_Implementation中。",[72,277,279],{"id":278},"adjust","Adjust",[11,281,282,283,286],{},"如果是修正包的话，情况会稍微有些复杂。不过与服务器类似的是，虽然有很多RPC的通道，到最后会归结到到",[34,284,285],{},"ClientAdjustPosition","中。当然不同的RPC会有一些独自的处理，不过由于没有使用过RootMotion之类的，这边只记录与移动逻辑有光的部分。",[11,288,289],{},"数据修正时首先还是对本地缓存进行更新，这里有个特殊的丢包处理。",[11,291,292],{},"如果下发的标志中有表明地板信息而下发的地板指针却是空的话，则会认定地板还没有值复制成功，暂时不处理这个修正。",[11,294,295],{},"之后同样的会根据时间戳对缓存数据进行删除，并通过OnClientCorrectionReceived发送事件出去。",[11,297,298],{},"然后会将本地位置和状态都设置到服务器下发的位置上去，同时设置ClientData标签bUpdatePosition为True。这个标签是为了之后重放移动准备的，由于移动重放操作还是比较重的，而且如果每个修正操作都会设置位置的话，在单次的RPC中设置就没有什么意义，因为如果又收到一个修正包的话就只是凭空的浪费了运算过程。",[62,300,301],{"id":301},"缓存重放",[11,303,304],{},"在进行了位置修正的情况下，需要对缓存的移动进行重放。",[11,306,307],{},"这个操作是在TickComponent中执行的。",[11,309,310],{},"移动的重放最后也是调用MoveAutonomous，在重放操作中会对必要的数据进行缓存并在之后重新设置回去。",[79,312,315],{"className":313,"code":314,"language":84},[82],"\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",[34,316,314],{"__ignoreMap":87},[11,318,319],{},"逻辑上还是比较单纯的，虽然看上去会处理很多数据，但是由于主控端每帧只会对自己做一次这个操作，运算消耗还是在可控制范围内的。",[62,321,322],{"id":322},"移动并上报",[11,324,325],{},"在移动重放之后，会在ReplicateMoveToServer中对当帧的移动进行模拟并上报。",[72,327,328],{"id":328},"状态更新",[11,330,331],{},"由于真正的移动操作就只是调用PerformMovement，所以实际上这个函数主要处理的移动上报和缓存更新。",[11,333,334,335,338],{},"在",[34,336,337],{},"UpdateTimeStampAndDeltaTime","中进行时间戳的更新，主要处理的是CurrentTimeStamp的累计、时间戳回转以及必要的Clamp操作。",[11,340,341],{},"接下来，会从缓存的移动中，以IsImportantMove为标准尝试寻找出一个最旧的关键移动包作为OldMove。",[72,343,344],{"id":344},"移动模拟",[11,346,347],{},"这里会有一个PendingMove和NewMove的逻辑，主要的目的是尝试对移动进行合并，减少不必要的数据传输。",[11,349,350],{},"在移动之前，会对PendingMove进行检查，如果通过了CanCombineWith的话，就会对NewMove进行CombineWith操作来将移动操作合并。",[11,352,353],{},"PendingMove本身是在移动后生成的，如果单次移动满足条件，就会尝试进行缓存并不再继续进行发包操作。",[79,355,358],{"className":356,"code":357,"language":84},[82],"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",[34,359,357],{"__ignoreMap":87},[62,361,362],{"id":362},"数据上行",[11,364,365],{},"数据上行在之后通过CallServerMove进行，这里能看到一些用于移动丢包测试的Console。",[11,367,368],{},"在CallServerMove中的逻辑其实就只是根据不同的情况调用不同的RPC进行数据上报，MoveOld是单独通过ServerMoveOld进行的，NewMove虽然操作会有不同，最后都会抵达ServerMove。",[11,370,371],{},"这里其实主要的区分是在PendingMove上，如果合并包失败的话，这边就会使用Dual的RPC，将两次移动一并带上去。",[62,373,375],{"id":374},"misc","Misc",[11,377,378,379,382],{},"有一个特殊的标签",[34,380,381],{},"bIgnoreClientMovementErrorChecksAndCorrection","可以暂停掉服务器和主控端的修正处理流程。",[11,384,385,388],{},[34,386,387],{},"FlushServerMoves","会将所有缓存的移动全部作为NewMove通过CallServerMove发送出去，不过当前只有UE4自带的技能系统有使用到。",[27,390,391],{"id":391},"模拟端",[11,393,394],{},"模拟端的角色移动是被动的，在逻辑上特殊的地方，其实只有一个Mesh与Actor分离插值的过程。",[62,396,397],{"id":397},"值复制",[11,399,400],{},"模拟端的处理是从值复制的OnRep操作开始的，通常这个操作在OnRep_ReplicatedMovement中传递到ACharacter::PostNetReceiveLocationAndRotation；对于有动态地板的情况，会在OnRep_ReplicatedBasedMovement中。作为插值参考的ReplicatedServerLastTransformUpdateTimeStamp是单独值复制的。",[11,402,403],{},"对于值复制的处理分为两个主要的部分：SmoothCorrection和OnUpdateSimulatedPosition。",[72,405,407],{"id":406},"smoothcorrection","SmoothCorrection",[11,409,410],{},"在插值模式为需要插值的情况，这里会对Mesh与Actor的偏差MeshTranslationOffset以及MeshRotationOffset进行更新。",[11,412,413],{},"有两个限制值，保证Actor本身的新的位置与当前的位置的偏差NewToOldVector不会过大的拉大插值距离：",[79,415,418],{"className":416,"code":417,"language":84},[82],"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",[34,419,417],{"__ignoreMap":87},[11,421,422],{},"之后就是将UpdatedComponent的位置移动到新的位置，这里有一个帮助类FScopedPreventAttachedComponentMove，保证移动操作不会变更Mesh的位置。",[11,424,425],{},"接下来就是一些时间戳的维护操作，主要是确保SmoothingClientTimeStamp与SmoothingServerTimeStamp能够在一个合理的容错范围内。",[72,427,429],{"id":428},"onupdatesimulatedposition","OnUpdateSimulatedPosition",[11,431,432],{},"这边的逻辑比较单纯，只是作一些状态设置。",[11,434,435,436,439],{},"如果位置发生了变动的话，会将",[34,437,438],{},"bJustTeleported","设置为true。",[11,441,442,443,446,447,450],{},"如果下发的运动状态没有速度却发生了位置改变的话，会对目标位置作一次",[34,444,445],{},"EncroachingBlockingGeometry","检查，如果位置被修正了的话，会启用",[34,448,449],{},"bSimGravityDisabled","标志，防止之后的模拟导致不可预知的下坠。",[62,452,454],{"id":453},"simulatedtick","SimulatedTick",[11,456,457],{},"模拟端的插值和模拟逻辑由TickComponnet在SimulatedTick中进行。",[72,459,460],{"id":460},"执行移动",[11,462,463],{},"模拟移动的逻辑在SimulateMovement中，这个函数与PerformMovement相比，进行了大幅的简化。",[11,465,466],{},"对于SkipProxyPredictionOnNetUpdate开启了的情况，如果当前帧进行了网络更新，则不会进行移动模拟。",[11,468,469],{},"如果有相应的标签的话，会尝试对网络下发的移动状态进行应用以及对地板进行更新。",[11,471,472],{},"在状态设置过之后，就会调用MoveSmooth来进行模拟端的移动模拟。",[72,474,476],{"id":475},"mesh位置插值","Mesh位置插值",[11,478,479],{},"这个过程分为两个部分，SmoothClientPosition_Interpolate负责逻辑更新，SmoothClientPosition_UpdateVisuals负责实际的Mesh位置移动。",[11,481,482],{},"由于插值过程中的Mesh移动不会Sweep，所以会有穿墙的可能性，但是由于位置是由服务器下发的，所以实际上穿墙的情况还是比较短暂和罕见的。",[11,484,485],{},"插值的位置可以使用p.NetVisualizeSimulatedCorrections来进行一定程度的观察。",[11,487,488],{},"插值模式主要在Linear和Exponential上，这两者除了计算方式不同之外，Linear会在插值过程中有一定的“预测”作用，所以在取舍上需要留意。",[27,490,344],{"id":491},"移动模拟-1",[11,493,494],{},"移动模拟的逻辑，主要在MoveAutonomous中，而在模拟端有一个弱化的MoveSmooth。",[11,496,497],{},"在实际的使用中，有的逻辑会在处理状态之后直接调用到PerformMovement中，基本上主控端和服务器都会最后使用到PerfromMovement，而模拟端只会使用MoveSmooth。",[11,499,500],{},"除了状态的准备之外，PerformMovement最后的移动逻辑以StartNewPhysics的计算为中心，而模拟端用的MoveSmooth则使用了一个弱化的计算方式。",[11,502,503],{},"在MoveAutonomous中会使用bClientUpdating来决定是否对Mesh姿态进行更新。有几个比较特别的处理需要留意",[11,505,506],{},[507,508,509],"strong",{},"CompressedFlags",[11,511,512],{},"这里面会对一些特殊的移动状态进行压缩，当前默认的有bPressedJump和bWantsToCrouch。服务器在解开这些标志位之后会尝试让移动进入对应的状态。",[11,514,515],{},[507,516,517],{},"ApplyAccumulatedForces",[11,519,520],{},"移动模拟并不会实际执行物理模拟，所以对于AddForce等一些列物理操作，会缓存起来，最后在这里实际的叠加到模拟中去。",[11,522,523],{},[507,524,525],{},"HandlePendingLaunch",[11,527,528],{},"这边是和上面的冲量系列一样，对物理操作的“模拟”。",[11,530,531],{},[507,532,533],{},"加速度",[11,535,536],{},"会有一个AnalogInputModifier的处理以及MaxAcceleration和竖直加速度清理的操作，加速度最后会反应到速度计算上。",[62,538,540],{"id":539},"startnewphysics","StartNewPhysics",[11,542,543],{},"这个才是真正意义上进行移动模拟的地方，会根据不同的MovementMode来对移动进行按照DeltaTime的分段模拟，同时也会处理移动模式的切换和继续模拟。",[11,545,546],{},"具体的过程可以实际在函数中进行观测。",[11,548,549],{},"操作上，首先是通过CalcVelocity来计算速度，包括了对AI Path Follow的处理以及正常玩家操作的处理。同时会对Friction等进行计算，保证阻力等被正常应用到速度计算中。",[72,551,553],{"id":552},"findfloor","FindFloor",[11,555,556],{},"如果在移动之后没有地板信息的话，会通过这个函数来寻找地板。",[11,558,559],{},"其中一个关键性的函数时ComputeFloorDist，由于其中有两种模式的计算。一种是通过向下的LineTrace，另一种是通过胶囊体的Sweep，对于一些不符合预期的站在物体的边缘或卡进动态物体的情况，可以在这里检查和作特殊处理。",[11,561,562],{},"对于Walking的情况，如果地板变更的话，会有一个HandleWalkingOffLedge的处理。对于新找到的地板不能站立的情况，会尝试回退单次模拟、改变方向或者尝试跳跃。这里的单次模拟是指StartNewPhysics内部分割后的模拟。",[72,564,566],{"id":565},"movealongfloor","MoveAlongFloor",[11,568,569],{},"这个是Walking时用来进行角色移动的函数，到这里其实代码上的逻辑还是比较清晰的，这里就不多作赘述了。其中有一个SlideAlongSurface的处理，是一个在移动模拟中比较常用到的贴边移动函数。里面除了ComputeSlideVector之外，还有一个TwoWallAdjust的回避处理，如果感兴趣的话可以深入的看一下，不过实际上如果没有出问题的话，这边的细节处理一下就会忘记了……",[11,571,572],{},"这里面还有一个非常精细的StepUp然后StepDown的操作，详细的逻辑可以查看UCharacterMovementComponent::StepUp。",[11,574,575],{},"通常如果移动进入奇怪的状态的话，从MoveAlongFloor来检查比较好。",[62,577,579],{"id":578},"movesmooth","MoveSmooth",[11,581,582],{},"相比StartNewPhyscis，这个给模拟端用的函数逻辑非常简单。",[11,584,585],{},"如果是在地面上移动的话，会使用MoveAlongFloor来更新，否则直接用SafeMoveUpdatedComponent来移动并作StepUp和SlideAlongSurface的操作。",[11,587,588],{},"此外UCharacterMovementComponent::SimulateMovement也会作一些基本的状态更新，在移动模拟之后会对地板和Faling状态进行更新。",[27,590,591],{"id":591},"总结",[11,593,594],{},"这边没有办法列出很多细节，而且实现方式也会随着版本出现变化，所以记录下来主要是方便进行快速的逻辑查找。",[11,596,597],{},"移动模拟在三个端各自有不同的逻辑，所以在出现问题时有时候查起来会比较头痛，希望这里的内容至少能够帮助缩短到达问题原因的路径~",{"title":87,"searchDepth":599,"depth":600,"links":601},2,3,[602,615,628,638,645],{"id":29,"depth":599,"text":29,"children":603},[604,609,613,614],{"id":64,"depth":600,"text":36,"children":605},[606,608],{"id":74,"depth":607,"text":74},4,{"id":93,"depth":607,"text":93},{"id":123,"depth":600,"text":40,"children":610},[611,612],{"id":141,"depth":607,"text":141},{"id":150,"depth":607,"text":150},{"id":202,"depth":600,"text":59},{"id":226,"depth":600,"text":226},{"id":256,"depth":599,"text":256,"children":616},[617,621,622,626,627],{"id":265,"depth":600,"text":265,"children":618},[619,620],{"id":271,"depth":607,"text":272},{"id":278,"depth":607,"text":279},{"id":301,"depth":600,"text":301},{"id":322,"depth":600,"text":322,"children":623},[624,625],{"id":328,"depth":607,"text":328},{"id":344,"depth":607,"text":344},{"id":362,"depth":600,"text":362},{"id":374,"depth":600,"text":375},{"id":391,"depth":599,"text":391,"children":629},[630,634],{"id":397,"depth":600,"text":397,"children":631},[632,633],{"id":406,"depth":607,"text":407},{"id":428,"depth":607,"text":429},{"id":453,"depth":600,"text":454,"children":635},[636,637],{"id":460,"depth":607,"text":460},{"id":475,"depth":607,"text":476},{"id":491,"depth":599,"text":344,"children":639},[640,644],{"id":539,"depth":600,"text":540,"children":641},[642,643],{"id":552,"depth":607,"text":553},{"id":565,"depth":607,"text":566},{"id":578,"depth":600,"text":579},{"id":591,"depth":599,"text":591},"2019-11-24","md",{"layout":649,"status":650,"published":651,"author":652,"author_login":654,"author_email":655,"wordpress_id":656,"wordpress_url":657,"date_gmt":658,"excerpt":659},"post","publish",true,{"display_name":653,"login":654,"email":655,"url":87},"风铃","flinkor","flinkor@foxmail.com",2737,"\u002F?p=2737","2019-11-24 04:25:50 +0000",{"type":8,"value":660},[661],[11,662,13],{},"\u002F2019-11-24-ue4-characer-move-note",{"title":6,"description":13},"_legacy\u002F2019\u002F2019-11-24-ue4-characer-move-note",[667,668],"UE4","移动","TgqzY2DceBGglhDonmrhmQgQCM1ohi5zbysHPDMwH1o",{"id":671,"title":672,"body":673,"date":760,"description":677,"extension":647,"meta":761,"navigation":651,"path":770,"seo":771,"stem":772,"tags":773,"__hash__":774},"blogs\u002F_legacy\u002F2019\u002F2019-10-31-ue4-singleton-note.md","UE4单例相关",{"type":8,"value":674,"toc":756},[675,678,681,684,687,694,700,706,709,712,722,728,731,753],[11,676,677],{},"有很多关于设计模式的书都不建议使用单例，但是实际在很多地方还是会用到。",[11,679,680],{},"因为单例实在是太方便了，而且很“干净”。",[27,682,683],{"id":683},"编辑器中使用",[11,685,686],{},"在UE4的编辑器中使用单例最大的问题是，默认的调试模式下，所有的PIE共用一个。",[11,688,689,690,693],{},"如果要进行DS的调试的话，很容易出现一些没有预料的问题。这个时候可以选择使用",[34,691,692],{},"GEditorID","来分离。",[11,695,696,697,699],{},"由于",[34,698,692],{},"的特殊性质，可以实现一个奇怪的Trick，能够在不使用RPC的情况下直接在编辑器对服务器上的位置信息进行调试绘制：",[79,701,704],{"className":702,"code":703,"language":84},[82],"#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",[34,705,703],{"__ignoreMap":87},[11,707,708],{},"如果编译没过的话，注意检查是否引用了UnrealEd这个Module。",[27,710,711],{"id":711},"新版本提供的单例类",[11,713,714,715,721],{},"之前的引擎版本中要使用与引擎相关的单例实现总会有各种各样的麻烦，从UE4.22开始，引擎提供了一组新的类型，包括",[56,716,720],{"href":717,"rel":718},"https:\u002F\u002Fdocs.unrealengine.com\u002Fen-US\u002FAPI\u002FRuntime\u002FEngine\u002FSubsystems\u002FUGameInstanceSubsystem\u002Findex.html",[719],"nofollow","UGameInstanceSubsystem","等。可以方便的在引擎内部实现单例的功能，实际进行测试的话：",[79,723,726],{"className":724,"code":725,"language":84},[82],"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",[34,727,725],{"__ignoreMap":87},[11,729,730],{},"输出的结果如下：",[732,733,734,737,739,742,744,747,749,751],"blockquote",{},[11,735,736],{},"LogTemp: UMyGameInstanceSubsystem::RefreshAndPrint: 2->2 || 00000184F8152AC0",[11,738,736],{},[11,740,741],{},"LogTemp: UMyGameInstanceSubsystem::RefreshAndPrint: 3->3 || 00000184F8152570",[11,743,741],{},[11,745,746],{},"LogTemp: UMyGameInstanceSubsystem::RefreshAndPrint: 1->1 || 00000184F8153FB0",[11,748,746],{},[11,750,736],{},[11,752,736],{},[11,754,755],{},"这样的话就可以不用顾及一些其他的细节问题了。",{"title":87,"searchDepth":599,"depth":600,"links":757},[758,759],{"id":683,"depth":599,"text":683},{"id":711,"depth":599,"text":711},"2019-10-31",{"layout":649,"status":650,"published":651,"author":762,"author_login":654,"author_email":655,"wordpress_id":763,"wordpress_url":764,"date_gmt":765,"excerpt":766},{"display_name":653,"login":654,"email":655,"url":87},2712,"\u002F?p=2712","2019-10-31 07:40:14 +0000",{"type":8,"value":767},[768],[11,769,677],{},"\u002F2019-10-31-ue4-singleton-note",{"title":672,"description":677},"_legacy\u002F2019\u002F2019-10-31-ue4-singleton-note",[667],"fxXaOz18H1iEySDeB5DX_qBNCeagnDfDL6AfYsCuJ9Q",{"id":776,"title":777,"body":778,"date":931,"description":782,"extension":647,"meta":932,"navigation":651,"path":941,"seo":942,"stem":943,"tags":944,"__hash__":945},"blogs\u002F_legacy\u002F2019\u002F2019-09-08-ue4-reflection-note.md","UE4属性反射小结",{"type":8,"value":779,"toc":920},[780,783,789,792,795,798,802,805,811,822,825,831,834,837,843,847,850,854,857,863,866,872,875,878,884,888,891,894,900,903,909,911,917],[11,781,782],{},"UProperty是UE4自己建立的一个反射系统的一部分，这里稍微整理下接触到的东西。",[11,784,785,786],{},"平时使用时，如果声明使用了属性系统，就必须在头文件上包含",[34,787,788],{},"*.generated.h",[11,790,791],{},"这样的话UE4就会“自动”的帮你完成反射部分的工作。这部分依赖于UBT和UHT，一般情况下不需要在意。",[11,793,794],{},"有的时候加完新的.h会提示这个文件不存在，一般情况下重新Generate一下项目就好了。",[11,796,797],{},"之所以动到这里是因为之前有个导出的需求，发现有个属性是Private的而有定义成UProperty。由于是制作插件所以不能修改引擎代码，所以只能从旁边绕路了。",[27,799,801],{"id":800},"uproperty相关读取","UProperty相关读取",[11,803,804],{},"对于指定Class里面的所有属性，可以直接使用帮助类来遍历：",[79,806,809],{"className":807,"code":808,"language":84},[82],"for (TFieldIterator\u003CUProperty> PropIt(GetClass()); PropIt; ++PropIt) \n{ \n  UProperty* Property = *PropIt; \u002F\u002F Do something with the property \n}\n",[34,810,808],{"__ignoreMap":87},[11,812,813,814,817,818,821],{},"在其之上就可以使用",[507,815,816],{},"ContainerPtrToValuePtr","或者",[507,819,820],{},"GetPropertyValue","来对属性进行读取了，在BlueprintNodeHelpers::CollectPropertyDescription中也能看到使用的实例。",[11,823,824],{},"或者也可以封装成函数：",[79,826,829],{"className":827,"code":828,"language":84},[82],"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",[34,830,828],{"__ignoreMap":87},[62,832,833],{"id":833},"蓝图属性",[11,835,836],{},"关于蓝图的属性载入过程，可以参考AActor::Serialize：",[79,838,841],{"className":839,"code":840,"language":84},[82],"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",[34,842,840],{"__ignoreMap":87},[27,844,846],{"id":845},"fguid","FGuid",[11,848,849],{},"这里要读取的是FGuid，当时做了两种尝试",[62,851,853],{"id":852},"通过exporttextitem","通过ExportTextItem",[11,855,856],{},"这样做是因为FGuid有提供LexFromString方法，ExportTextItem本身封装在DescribeProperty中：",[79,858,861],{"className":859,"code":860,"language":84},[82],"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",[34,862,860],{"__ignoreMap":87},[11,864,865],{},"实际使用的话是这样的：",[79,867,870],{"className":868,"code":869,"language":84},[82],"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",[34,871,869],{"__ignoreMap":87},[62,873,874],{"id":874},"直接转换",[11,876,877],{},"其实也可以直接强制转换类型，不过会有一定的危险性",[79,879,882],{"className":880,"code":881,"language":84},[82],"const FGuid* tp_GuidConvert = (FGuid*)PropData;\nUE_LOG(LogTemp, Log, TEXT(\"Compare to see: %s || %s\"), *tstr_DProperty, *LexToString(*tp_GuidConvert));\n",[34,883,881],{"__ignoreMap":87},[27,885,887],{"id":886},"ufunction","UFUNCTION",[11,889,890],{},"这里的另一个需求是要求目标的Object实现一个事先约定好的接口，而插件这边则负责调用改接口来实现特定的额外导出。",[11,892,893],{},"其实FindFunctionByName是引擎内本来就有的 ，蓝图节点UK2Node_CallFunction中也能看到引擎是如何将功能暴露到蓝图的。在参考上UObject::CallFunctionByNameWithArguments和UObject::FindFunction也可以得到一些帮助，最后在实现上采用了这样的形式：",[79,895,898],{"className":896,"code":897,"language":84},[82],"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",[34,899,897],{"__ignoreMap":87},[11,901,902],{},"如果需要返回值的话，可以这样：",[79,904,907],{"className":905,"code":906,"language":84},[82],"const bool bHasReturnParam = Function->ReturnValueOffset != MAX_uint16;\nuint8* ReturnValueAddress = bHasReturnParam ? ((uint8*)Parms + Function->ReturnValueOffset) : nullptr;\n",[34,908,906],{"__ignoreMap":87},[27,910,591],{"id":591},[11,912,913,914,916],{},"其实在读取UStruct的时候，应当是可以使用",[507,915,816],{},"作Struct的转换的，不过不知道当时为什么没有这么做，这点只能留到下次有相关需求的时候再考证了。",[11,918,919],{},"另外，如果对UProperty的读写有兴趣的话，其实可以参考引擎的LoadConfig和SaveConfig，里面包含了所有类型的属性的读写。",{"title":87,"searchDepth":599,"depth":600,"links":921},[922,925,929,930],{"id":800,"depth":599,"text":801,"children":923},[924],{"id":833,"depth":600,"text":833},{"id":845,"depth":599,"text":846,"children":926},[927,928],{"id":852,"depth":600,"text":853},{"id":874,"depth":600,"text":874},{"id":886,"depth":599,"text":887},{"id":591,"depth":599,"text":591},"2019-09-08",{"layout":649,"status":650,"published":651,"author":933,"author_login":654,"author_email":655,"wordpress_id":934,"wordpress_url":935,"date_gmt":936,"excerpt":937},{"display_name":653,"login":654,"email":655,"url":87},2695,"\u002F?p=2695","2019-09-08 12:04:30 +0000",{"type":8,"value":938},[939],[11,940,782],{},"\u002F2019-09-08-ue4-reflection-note",{"title":777,"description":782},"_legacy\u002F2019\u002F2019-09-08-ue4-reflection-note",[667],"lzKUFzuZlc7mwr6a2SnRdYsc8_H_hkI4Ro6IpGLAPUI",{"id":947,"title":948,"body":949,"date":1352,"description":953,"extension":647,"meta":1353,"navigation":651,"path":1362,"seo":1363,"stem":1364,"tags":1365,"__hash__":1367},"blogs\u002F_legacy\u002F2019\u002F2019-09-06-screen-writing-book-note.md","银幕剧本读书笔记",{"type":8,"value":950,"toc":1335},[951,954,957,960,963,966,969,972,975,980,983,986,991,994,997,1000,1003,1006,1009,1012,1019,1022,1025,1028,1031,1034,1050,1053,1056,1059,1062,1094,1097,1100,1103,1114,1117,1128,1131,1134,1137,1154,1157,1160,1164,1167,1170,1173,1176,1179,1182,1185,1188,1191,1194,1197,1200,1203,1206,1209,1212,1215,1218,1225,1228,1231,1234,1237,1241,1244,1249,1252,1257,1260,1265,1268,1273,1276,1281,1284,1289,1292,1297,1300,1303,1306],[11,952,953],{},"最近一直在读银幕剧本相关的书籍，最后的一些笔记有些零散，整理到了一起。",[11,955,956],{},"读了几本关于电影剧本的书之后，发现其实这方面的书都是大同小异。无非是角色、冲突以及结构。有的作者会给你介绍剧本行业的现状，根据作者身份的不同甚至会有截然相反的观点。",[27,958,959],{"id":959},"你的剧本逊毙了",[11,961,962],{},"这本书初看颇觉惊艳，但是看下去的话，会发现其实还是老调重弹。不过是换了个方式把剧本要素重新解释了一遍。",[11,964,965],{},"感觉上不是很适合我这种连基本的结构知识都不是很全的人，主要原因是其内容的组织形式是以你有这些方面的知识为前提的，如果不是的话，看的时候觉得很有道理，看完之后还是一头雾水。",[11,967,968],{},"因为对于我这样的初学者而言，更想知道应该如何做，而不是什么不能做。",[62,970,971],{"id":971},"创作工具",[11,973,974],{},"作者介绍了两种工具比较有印象：",[11,976,977],{},[507,978,979],{},"一句话大纲",[11,981,982],{},"每个场景一句话，将整个故事描述出来。",[11,984,985],{},"方便调整场景顺序，以及确定惊喜消失的时长，人物消失的时长。",[11,987,988],{},[507,989,990],{},"随想大纲",[11,992,993],{},"用于扩充单个场景。",[11,995,996],{},"不断的对单个场景进行随想，直到想不出东西为止。",[62,998,999],{"id":999},"建议",[11,1001,1002],{},"不要做太多的调查，只做你写剧本所需的调查。先找到你的故事，然后再做调查。有了故事，就会有针对性和方向性。因为，调查往往比写作更好玩……",[11,1004,1005],{},"真正的写作时改写，需要对本子不断的进行改写。",[11,1007,1008],{},"人物的名字不要容易搞混。",[27,1010,1011],{"id":1011},"救猫咪",[11,1013,1014,1015,1018],{},"这本书还挺有名的，作者的创作思路是从",[507,1016,1017],{},"创意","开始的。",[11,1020,1021],{},"由于作者似乎比较常写喜剧类，所以有些观点和之前写正剧的作者由些微妙的差别。",[62,1023,1024],{"id":1024},"故事线",[11,1026,1027],{},"指的是对整个故事的一句话概述，每个故事线都是包含六七个人物的精彩创意。",[11,1029,1030],{},"故事线就是会出现在宣传画上的东西，以电影而言，就是最终要吸引观众买电影票的那个东西。",[11,1032,1033],{},"故事线有四个成功的元素：",[1035,1036,1037,1041,1044,1047],"ul",{},[1038,1039,1040],"li",{},"意外",[1038,1042,1043],{},"强烈的心理影像",[1038,1045,1046],{},"观众和成本",[1038,1048,1049],{},"有杀伤力的片名",[11,1051,1052],{},"这里的意外指的是与日常生活相比的意料之外，而观众和成本其实指的是故事本身面向的市场。关于片名，作者说他经常先确定片名，然后让故事跟片名相匹配。",[11,1054,1055],{},"高度概念化虽然不是唯一的方法，但是一开始确立的话就会有比较好的市场目标。就像是现在每个季度都会有的“后宫番”一样，而且与电影行业一样，名作的续集永远都是一张安全牌。",[62,1057,1058],{"id":1058},"似而不同",[11,1060,1061],{},"在有了故事线之后，对其按照类型进行分类。类型的话，其实也更加接近一个市场工具。作者划分的10个类型为：",[1035,1063,1064,1067,1070,1073,1076,1079,1082,1085,1088,1091],{},[1038,1065,1066],{},"“鬼怪屋”型",[1038,1068,1069],{},"“金羊毛”型",[1038,1071,1072],{},"“如愿以偿”型",[1038,1074,1075],{},"“麻烦家伙”型",[1038,1077,1078],{},"“变迁仪式”型",[1038,1080,1081],{},"“伙伴之情”型",[1038,1083,1084],{},"“推理侦探”型",[1038,1086,1087],{},"“愚者成功”型",[1038,1089,1090],{},"“被制度化”型",[1038,1092,1093],{},"“超级英雄”型",[11,1095,1096],{},"类型提供的是经过市场验证和观众所习惯的叙事节奏。",[62,1098,1099],{"id":1099},"寻找主角",[11,1101,1102],{},"接下来就需要从故事线中寻找主角，一个“完美”的故事线必须包含：",[1035,1104,1105,1108,1111],{},[1038,1106,1107],{},"一个描述主角的形容词;",[1038,1109,1110],{},"一个描述坏蛋的形容词;",[1038,1112,1113],{},"一个我们人类能够认同的、扣人心弦的目标。",[11,1115,1116],{},"主角是让故事线活起来的重要因素，必须从故事线中这样去寻找主角：",[1035,1118,1119,1122,1125],{},[1038,1120,1121],{},"在那种情景下能够带来最大的冲突;",[1038,1123,1124],{},"情感上能够走得最远;",[1038,1126,1127],{},"能够让最多的观众喜爱。",[11,1129,1130],{},"在进行角色构建的时候，不能以具体的“演员”来构建，而应当以角色经典原型来。每个原型都有我们愿意再三看到的故事曲线(story arc)。也就是说要把我们潜意识中的东西和银幕上看到的东西匹配起来。",[11,1132,1133],{},"这类主角的故事及使其故事吸引人的数学方程式已经深入到我们的DNA基因里面了。",[11,1135,1136],{},"所讲的故事是关于这样的一个家伙:",[1035,1138,1139,1142,1145,1148,1151],{},[1038,1140,1141],{},"我能够认同他;",[1038,1143,1144],{},"我能够学习他;",[1038,1146,1147],{},"我有充分的理由接受他;",[1038,1149,1150],{},"我认为他应该获胜;",[1038,1152,1153],{},"而且有原始的奖赏并能真正吸引我。",[11,1155,1156],{},"如果故事线具有冲突最大化、立场最明确的主角和坏蛋,还有最清晰、最原始的目标,那么这个故事线是成功的",[62,1158,1159],{"id":1159},"结构工具",[72,1161,1163],{"id":1162},"布莱克斯奈德节拍表","布莱克·斯奈德节拍表",[11,1165,1166],{},"故事中的重要的场景，可以帮助从故事线形成故事：",[11,1168,1169],{},"1.开场画面(第1页)",[11,1171,1172],{},"2.主题呈现(第5页)",[11,1174,1175],{},"3.铺垫(第1—10页)",[11,1177,1178],{},"4.推动(催化剂)(第12页)",[11,1180,1181],{},"5.争执(辩论)(第12—25页)",[11,1183,1184],{},"6.第二幕衔接点(第25页)",[11,1186,1187],{},"7.B故事(第30页)",[11,1189,1190],{},"8.游戏(第30—55页)",[11,1192,1193],{},"9.中点(第55页)",[11,1195,1196],{},"10.坏蛋逼近(第55—75页)",[11,1198,1199],{},"11.一无所有(第75页)",[11,1201,1202],{},"12.灵魂的黑夜(第75—85页)",[11,1204,1205],{},"13.第三幕衔接点(第85页)",[11,1207,1208],{},"14.结局(第85—110页)",[11,1210,1211],{},"15.终场画面(第110页)",[72,1213,1214],{"id":1214},"案板",[11,1216,1217],{},"卡片工具的辅助，从上到下分为四个区域：第一幕、第二幕（前半）、第二幕（后半）、第三幕。",[11,1219,1220],{},[1221,1222],"img",{"alt":1223,"src":1224},"image","\u002Fwp-content\u002Fuploads\u002F2019\u002F09\u002Fimage_thumb.png",[11,1226,1227],{},"完成时第一行应该有9张卡,第二、三、四行也应该有9张卡。以及额外的四张必不可少的场景。",[11,1229,1230],{},"在制作过程中，可以对不同的人物的故事卡片使用不同的颜色，方便观察叙事线路的穿插。",[11,1232,1233],{},"同时，在有粗略的故事线之后，对每一张卡片标明冲突(＞ ＜)和价值变化(+ \u002F –)。",[62,1235,1236],{"id":1236},"规则",[11,1238,1239],{},[507,1240,1011],{},[11,1242,1243],{},"当我们见到主角时他必须做点事情让我们喜欢他并希望他胜出。并不是要主角是一个怜悯的圣母，而是要让观众产生关心，对主角产生认同。",[11,1245,1246],{},[507,1247,1248],{},"泳池里的教皇",[11,1250,1251],{},"当实在需要一些解说性的场景时，在其中穿插些能吸引观众注意力的要素。",[11,1253,1254],{},[507,1255,1256],{},"不要有双重神秘力量",[11,1258,1259],{},"你不可能看到外太空异形的UFO降落地球然后被吸血鬼咬中,结果现在既是异形,又能永远不死。对于2小时的电影而言，观众很难将注意力分散到两种很难统合的世界观。",[11,1261,1262],{},[507,1263,1264],{},"铺垫",[11,1266,1267],{},"不要用得太多！不要吊着观众预期。",[11,1269,1270],{},[507,1271,1272],{},"黑人兽医,糖太多了",[11,1274,1275],{},"简单反而更好。一个时间,一个创意。你不能吸收太多信息或堆积创意使故事变得更好。",[11,1277,1278],{},[507,1279,1280],{},"小心那块冰川!",[11,1282,1283],{},"危机要有紧迫性，如果没有就需要安排别的冲突。",[11,1285,1286],{},[507,1287,1288],{},"曲线公约",[11,1290,1291],{},"影片中的每个人物在故事进行中都要有所转变。只有坏蛋才不会发生转变。",[11,1293,1294],{},[507,1295,1296],{},"别让媒体介入",[11,1298,1299],{},"除非影片讲的是媒体,或影片中有世界性的问题,使得故事中要跟踪世界各地的人物,并且不得不让他们相互了解。",[62,1301,1302],{"id":1302},"剧本检查",[11,1304,1305],{},"问问这些问题,看有没有打破规则。测试:",[732,1307,1308,1311,1314,1317,1320,1323,1326,1329,1332],{},[11,1309,1310],{},"1.主角引领动作了吗?在游戏的每个阶段他是不是积极的?是不是因某个愿望或目标激发起来的?",[11,1312,1313],{},"2.人物“说情节”了吗?我是在透过人物长篇大论呢,还是通过剧本中的动作来展示?",[11,1315,1316],{},"3.坏蛋足够坏了吗?他有没有为主角带来合适的挑战?他们都适合这部影片吗?",[11,1318,1319],{},"4.中点之后情节有没有提速?强度有没有增加?在第三幕结尾时主角和坏蛋是不是有更多的东西揭示出来?",[11,1321,1322],{},"5.剧本情绪是否单调?是纯正剧吗?还是纯喜剧?纯悲剧、全都是挫折?是否感觉缺乏所需的情绪中断?",[11,1324,1325],{},"6.台词乏味吗?在经过“烂台词检测”后每个人的说话方式还一样吗?我是否能够只通过人物的说话方式来辨别人物?",[11,1327,1328],{},"7.次要人物之间有没有区别,能否通过外表轻松地记住他们的区别?每个次要人物的言谈举止、外貌是否都独一无二?",[11,1330,1331],{},"8.主角旅程的起点是否尽可能往后退了?故事中是否看到主角情感成长的整个历程了?",[11,1333,1334],{},"9.原始吗?人物本质上是由原始愿望——被爱、生存、保护家庭、复仇等激发的吗?",{"title":87,"searchDepth":599,"depth":600,"links":1336},[1337,1341],{"id":959,"depth":599,"text":959,"children":1338},[1339,1340],{"id":971,"depth":600,"text":971},{"id":999,"depth":600,"text":999},{"id":1011,"depth":599,"text":1011,"children":1342},[1343,1344,1345,1346,1350,1351],{"id":1024,"depth":600,"text":1024},{"id":1058,"depth":600,"text":1058},{"id":1099,"depth":600,"text":1099},{"id":1159,"depth":600,"text":1159,"children":1347},[1348,1349],{"id":1162,"depth":607,"text":1163},{"id":1214,"depth":607,"text":1214},{"id":1236,"depth":600,"text":1236},{"id":1302,"depth":600,"text":1302},"2019-09-06",{"layout":649,"status":650,"published":651,"author":1354,"author_login":654,"author_email":655,"wordpress_id":1355,"wordpress_url":1356,"date_gmt":1357,"excerpt":1358},{"display_name":653,"login":654,"email":655,"url":87},2690,"\u002F?p=2690","2019-09-06 09:03:27 +0000",{"type":8,"value":1359},[1360],[11,1361,953],{},"\u002F2019-09-06-screen-writing-book-note",{"title":948,"description":953},"_legacy\u002F2019\u002F2019-09-06-screen-writing-book-note",[1366],"narrative","dMCVIBJDZywApuufRjsT3yAJxCBwG7HIS0NDl6mmKg0",{"id":1369,"title":1370,"body":1371,"date":2027,"description":1375,"extension":647,"meta":2028,"navigation":651,"path":2037,"seo":2038,"stem":2039,"tags":2040,"__hash__":2041},"blogs\u002F_legacy\u002F2019\u002F2019-09-04-%e3%80%8athe-story-solution%e3%80%8b%e7%ac%94%e8%ae%b0.md","《The story solution》笔记",{"type":8,"value":1372,"toc":2018},[1373,1376,1383,1386,1389,1392,1395,1424,1427,1430,1433,1436,1459,1462,1465,1468,1471,1474,1479,1482,1487,1490,1493,1496,1499,1502,1505,1508,1511,1514,1517,1520,1523,1528,1533,1536,1541,1546,1551,1554,1559,1562,1567,1570,1573,1576,1579,1582,1585,1588,1591,1617,1620,1623,1626,1629,1632,1635,1638,1641,1644,1647,1650,1653,1656,1659,1662,1665,1668,1671,2003,2006,2009,2012,2015],[11,1374,1375],{},"这本书的副标题很恐怖，感觉上有点像是《7天学会Java编程》。",[11,1377,1378,1379,1382],{},"内容的话其实还算正统，介绍了一些基础之后推广了",[507,1380,1381],{},"英雄目标序列","创作工具。",[11,1384,1385],{},"介绍的创作方式是，首先要为故事找到一个主题，而这个主题就是主角发现自我的旅途：",[11,1387,1388],{},"为了——你必须——",[27,1390,1391],{"id":1391},"人物",[11,1393,1394],{},"作为主角的人物必须引发观众的共鸣，作者给出了九个要素：",[1035,1396,1397,1400,1403,1406,1409,1412,1415,1418,1421],{},[1038,1398,1399],{},"勇气",[1038,1401,1402],{},"不公平的伤害",[1038,1404,1405],{},"本事",[1038,1407,1408],{},"幽默",[1038,1410,1411],{},"善良",[1038,1413,1414],{},"身处险境",[1038,1416,1417],{},"受到朋友和家人的深爱",[1038,1419,1420],{},"努力",[1038,1422,1423],{},"执着",[11,1425,1426],{},"人物改变需要吸引观众，就需要必要的赌注。而最好的表达方式，就是生死攸关。无论这是字面意义上的还是隐喻意义上的。",[11,1428,1429],{},"另外需要注意的是，改变必须由主人公发起，一个被动的主人公很难吸引观众。",[27,1431,1432],{"id":1432},"冲突",[11,1434,1435],{},"作者将冲突分为两个层次：来自外部世界的冲突和情感、内心等个人的冲突。对于冲突，需要：",[1035,1437,1438,1441,1444,1447,1450,1453,1456],{},[1038,1439,1440],{},"冲突必须激烈",[1038,1442,1443],{},"冲突必须可见",[1038,1445,1446],{},"冲突必须凶险",[1038,1448,1449],{},"冲突必须不断发展和升级、带给主人公更多的挑战",[1038,1451,1452],{},"冲突必须令观众意外",[1038,1454,1455],{},"冲突必须令人信服",[1038,1457,1458],{},"冲突必须以有意义的方式解决",[11,1460,1461],{},"为了让故事的冲突拥有足够的戏剧效果，一个看似不可战胜的对手是最好的。",[11,1463,1464],{},"冲突是所有剧本的心脏和灵魂。",[27,1466,1467],{"id":1467},"人物要素",[11,1469,1470],{},"电影中的每一个人物只能来自于这个类型清单。",[11,1472,1473],{},"这里有个问题，就是一个人物的类型是由最终故事中的行为来决定的，而有时候在故事发展过程中，却是可以伪装的。",[11,1475,1476],{},[507,1477,1478],{},"主人公",[11,1480,1481],{},"还有一些派生的种类：非传统英雄、悲剧英雄、搞笑式英雄、催化剂英雄",[11,1483,1484],{},[507,1485,1486],{},"对手\u002F反派",[11,1488,1489],{},"相比其他任何人物，对手是主人公最强劲的敌人",[11,1491,1492],{},"对手不应该是一队人马或一个概念，而应是个体",[11,1494,1495],{},"一个看似无法击败的强大对手",[11,1497,1498],{},"对手深信自己的述求，并认为对抗和阻止被高估的主人公是正确的事情",[11,1500,1501],{},"心理学上，对手可以正是主人公自己的另一面",[11,1503,1504],{},"对手可能会指使帮凶对抗主人公",[11,1506,1507],{},"主人公不应该成为自己的对手，否则无法建立可见的和引人注意的冲突。",[11,1509,1510],{},"自然灾难不是对手",[11,1512,1513],{},"对手可以是一个伪装的“变形者”",[11,1515,1516],{},"对手并不总是一个恶人",[11,1518,1519],{},"对手即便在故事中处于支配地位也不能替代主人公",[11,1521,1522],{},"当浪漫成为主要情节，则爱恋对象成为对手",[11,1524,1525],{},[507,1526,1527],{},"爱恋对象",[11,1529,1530],{},[507,1531,1532],{},"导师",[11,1534,1535],{},"可以有负面导师，可以有多个导师。",[11,1537,1538],{},[507,1539,1540],{},"伙伴",[11,1542,1543],{},[507,1544,1545],{},"边界护卫",[11,1547,1548],{},[507,1549,1550],{},"其他盟友人物",[11,1552,1553],{},"对照盟友、喜剧盟友、帮手-追随者盟友、有望成为救星的盟友（一般不会成功）、助威的盟友、深陷险境的盟友",[11,1555,1556],{},[507,1557,1558],{},"对立人物",[11,1560,1561],{},"对手代理人、独立的麻烦制造者、反派的小喽啰",[11,1563,1564],{},[507,1565,1566],{},"氛围人物",[27,1568,1569],{"id":1569},"结构",[11,1571,1572],{},"电影故事结构的十大基本要素：",[11,1574,1575],{},"第一幕——故事建置段落",[11,1577,1578],{},"第二幕——冲突递增的发展段落",[11,1580,1581],{},"第三幕——情节解决段落",[11,1583,1584],{},"引发事件——第一幕中开始这一特定故事的时刻",[11,1586,1587],{},"惊人意外#1——结束第一幕并开始第二幕的意外事件，把主人公推进一个全新的世界",[11,1589,1590],{},"中间点——在这重要的场景组合中可能包括以下特征：",[1035,1592,1593,1596,1599,1602,1605,1608,1611,1614],{},[1038,1594,1595],{},"在基调和风格上，中间点的感觉都不同于剧本其他部分，并常有配以音乐的转场蒙太奇",[1038,1597,1598],{},"中间点场景作为情感或行动层面的无路可退的临界点",[1038,1600,1601],{},"主人公于对手的冲突变成个人恩怨",[1038,1603,1604],{},"伙伴第一次作为一个真正的团队精诚联手",[1038,1606,1607],{},"常包括一个事实上或隐喻上的揭露",[1038,1609,1610],{},"中间点几乎总是包含人物成长的第二大步",[1038,1612,1613],{},"常伴有倒计时来增强焦虑",[1038,1615,1616],{},"常有事实上或隐喻上的死亡和重生时刻",[11,1618,1619],{},"第二幕高潮——戏剧性行动逐渐增强，主人公期盼的胜利近在咫尺",[11,1621,1622],{},"惊人意外#2——令人惊讶的戏剧反转，通过打碎主人公的求生计划来结束第二幕并开始第三幕（偶尔可以是好消息）",[11,1624,1625],{},"必须场景——主人公和对手之间的第三幕决战时刻，这将彻底解决最主要的情节问题",[11,1627,1628],{},"结局——解决所有未了结的情节问题和关系",[27,1630,1631],{"id":1631},"人物成长",[11,1633,1634],{},"人物成长场景的三个场景全部发生在第二幕",[11,1636,1637],{},"场景1：表达",[11,1639,1640],{},"主人公表达（有意或无意的）束缚他的情感包袱的本质，必须通过行为和态度来达到，而不是让主人公直接说出来。",[11,1642,1643],{},"场景2：斗争",[11,1645,1646],{},"在中间点，主人公终于对抗他的自我孤立和创伤心态——但是这次尝试没有成功。",[11,1648,1649],{},"场景3：克服",[11,1651,1652],{},"主人公终于克服和战胜了孤立的情感防御体系。",[11,1654,1655],{},"在第三幕之初，虽然情感防御可能重现，但他很快意识到人物成长是根深蒂固的——他已经不再是从前的自己，而是一个最终能实现故事情节目标的更加成熟的人。",[11,1657,1658],{},"这三个场景构成了第二幕中的情感故事线。",[27,1660,1661],{"id":1661},"英雄目标段落",[11,1663,1664],{},"一个英雄目标段落通常是2～4个场景，在段落内，主人公追求一个短期的有形目标，向着实现故事中最终胜利跨前一步。随后，主人公发现新情况的某种形式的新信息，这标志当前目标的结束并带出一个新的短期的行动目标——由此开启下一段英雄目标段落。",[11,1666,1667],{},"按照幕来划分的话，第一幕固定为6个目标段落，第二幕由中间点分割为前后6个目标段落。第一幕结束的目标段落HGS#6为惊人意外#1，而第二幕结束的HGS18为惊人意外#2。",[11,1669,1670],{},"这里的目标段落仅涉及主人公参与的剧情部分，不计算任何次要情节。也就是说，在HGS#1前面可以有一个开场的次要情节的情节钩子之类的。实际上加上这一点后，HGS本身就只是一条模糊的主线而已。",[732,1672,1673,1678,1681,1684,1687,1690,1695,1698,1701,1704,1709,1712,1715,1718,1721,1724,1729,1732,1735,1738,1741,1744,1749,1752,1755,1758,1761,1766,1769,1772,1775,1780,1783,1786,1789,1792,1795,1800,1803,1806,1809,1812,1815,1820,1823,1826,1829,1832,1837,1840,1843,1846,1849,1854,1857,1860,1863,1866,1869,1874,1877,1880,1883,1886,1889,1892,1895,1900,1903,1906,1909,1912,1917,1920,1923,1926,1929,1934,1937,1940,1943,1946,1951,1954,1957,1960,1963,1968,1971,1974,1977,1980,1985,1988,1991,1994,1997,2000],{},[11,1674,1675],{},[507,1676,1677],{},"HGS#1",[11,1679,1680],{},"给观众呈现一小段主人公当前的日常生活",[11,1682,1683],{},"给观众迅速喜欢主人公的理由，即便是探讨人性阴暗面的剧本中的非传统英雄",[11,1685,1686],{},"让观众第一次看见主人公的情感保护壳",[11,1688,1689],{},"建立主人公追求的短期目标的方向，即使是上班，这样我们可以认识在追寻中遭遇一些不公平的伤害和危险的主人公",[11,1691,1692],{},[507,1693,1694],{},"HGS#2",[11,1696,1697],{},"核心的冲突以总体的方式呈现，同时推动故事发展的情节开始",[11,1699,1700],{},"引发事件也最常出现于此，作为故事的催化剂事件",[11,1702,1703],{},"主人公特定的情感伤痛的第一个暗示常在次显露",[11,1705,1706],{},[507,1707,1708],{},"HGS#3",[11,1710,1711],{},"主人公面对历险召唤",[11,1713,1714],{},"新的情节提升人物登场",[11,1716,1717],{},"故事节奏加快",[11,1719,1720],{},"爱恋对象初次登场，或已出场的人物的爱情功能得以建立并挑战主人公的情感",[11,1722,1723],{},"针对主人公的陷阱已布好",[11,1725,1726],{},[507,1727,1728],{},"HGS#4",[11,1730,1731],{},"继续呈现新人物和关系发展",[11,1733,1734],{},"通常导师人物会登场，此前如果仍未登场的爱恋对象或伙伴则也该出场",[11,1736,1737],{},"随着主人公追求他的总体目标，节奏进一步加快",[11,1739,1740],{},"主人公被迫冒险",[11,1742,1743],{},"主人公掉入HGS#3中布下的陷阱",[11,1745,1746],{},[507,1747,1748],{},"HGS#5",[11,1750,1751],{},"主人公的主要渴望得以揭示",[11,1753,1754],{},"重新考虑历险召唤中存在的任何危险的可能，主人公随时有可能拒绝召唤",[11,1756,1757],{},"主人公陷入之前设好的陷阱——除非这已经在HGS#4中发生了",[11,1759,1760],{},"主人公必须在HGS#4或者HGS#5进入陷阱，因为在HGS#6中，陷阱机关必须生效",[11,1762,1763],{},[507,1764,1765],{},"HGS#6",[11,1767,1768],{},"陷阱机关启动，困住主人公",[11,1770,1771],{},"惊人意外#1发生",[11,1773,1774],{},"主人公追求的贯穿第二幕的特定的行动目标变得清晰",[11,1776,1777],{},[507,1778,1779],{},"HGS#7",[11,1781,1782],{},"主人公绞尽脑汁寻觅良策，独立应对或与他人合作",[11,1784,1785],{},"主人公担起电影特定的情节目标，这需要相当的挣扎和努力",[11,1787,1788],{},"主人公的内心冲突显现……",[11,1790,1791],{},"……和揭示出电影的主题",[11,1793,1794],{},"新人物走进主人公的挣扎，他必须开始辨别敌友",[11,1796,1797],{},[507,1798,1799],{},"HGS#8",[11,1801,1802],{},"对手的力量得到展现",[11,1804,1805],{},"风险增高",[11,1807,1808],{},"主人公经受训练或得到教导来掌握前路斗争所需的特定技能",[11,1810,1811],{},"盟友和敌人进一步确立",[11,1813,1814],{},"当主人公或他的亲近之人言语间表达出他内心的情感问题时，人物成长的第一步时常出现",[11,1816,1817],{},[507,1818,1819],{},"HGS#9",[11,1821,1822],{},"推高紧张度和提升节奏或故事力量的行动迸发或情感强烈的时刻出现",[11,1824,1825],{},"主人公展现他的身体能力",[11,1827,1828],{},"主人公冒险一搏，却事与愿违",[11,1830,1831],{},"主人公考虑放弃",[11,1833,1834],{},[507,1835,1836],{},"HGS#10",[11,1838,1839],{},"主人公受挫倍遭打击",[11,1841,1842],{},"主人公咨询新的导师",[11,1844,1845],{},"一个出人意料的行动障碍突然出现",[11,1847,1848],{},"主人公没有出现的次要情节的旁切场景展开",[11,1850,1851],{},[507,1852,1853],{},"HGS#11",[11,1855,1856],{},"主人公接近对手的虎穴或权力中心",[11,1858,1859],{},"冲突变得越发激烈",[11,1861,1862],{},"主人公和对手的代理人的战斗爆发",[11,1864,1865],{},"风险和紧迫性都猛烈飙升",[11,1867,1868],{},"由导师提供培训、指导或咨询的最后机会",[11,1870,1871],{},[507,1872,1873],{},"HGS#12",[11,1875,1876],{},"主人公到达无路可退的十字路口，一旦跨过便无回头的可能性",[11,1878,1879],{},"人物成长的重要的第二步出现，此时主人公直接对抗阻碍他实现自我目标的内心冲突",[11,1881,1882],{},"同伙伴的搭档关系正式确立，加深了彼此的承诺",[11,1884,1885],{},"基调和叙事风格不同于电影中的其他任何部分",[11,1887,1888],{},"倒计时开始",[11,1890,1891],{},"事实上或象征性地卸下伪装，内在真心得以揭示",[11,1893,1894],{},"以事实上或象征性的死亡和重生的出现来代表主人公幼稚的结束和成熟的开始",[11,1896,1897],{},[507,1898,1899],{},"HGS#13",[11,1901,1902],{},"主人公带着重拾的勇气向前挺进",[11,1904,1905],{},"一个新的难题突然出现",[11,1907,1908],{},"主人公再一次拿起他那破损的自我保护和孤立的内心情感保护壳",[11,1910,1911],{},"主人公和对手之间的冲突已经变得极为私人的额外证据出现",[11,1913,1914],{},[507,1915,1916],{},"HGS#14",[11,1918,1919],{},"节奏略微放缓和为主人公提供了一个沉思时刻",[11,1921,1922],{},"爱恋对象或伙伴，或者甚至是主人公自己带出他此刻尚不愿意面对的内心的冲突问题",[11,1924,1925],{},"一个挑战主人公的重要的新想法出现",[11,1927,1928],{},"重要的新想法为在HGS#15中即将出现的第二个行动迸发做好了铺垫",[11,1930,1931],{},[507,1932,1933],{},"HGS#15",[11,1935,1936],{},"行动迸发中的行动或能量，又或戏剧冲突让事件骤然激烈起来",[11,1938,1939],{},"主人公体验到胜利的假象，或至少相信他能和对手平分秋色，所以他的终极目标看起来也不是遥不可及",[11,1941,1942],{},"主人公短暂的安全感转瞬即逝",[11,1944,1945],{},"牢固的感情关系可以激励主人公闯过行动迸发",[11,1947,1948],{},[507,1949,1950],{},"HGS#16",[11,1952,1953],{},"戏剧能量继续抬升，不是作为更多的动作冲突，而是作为冲突的强度持续上升",[11,1955,1956],{},"主人公对自己的能力和判断感到信心满满，并重新投身到终极的现实目标",[11,1958,1959],{},"主人公对核心的故事冲突发现了一些新的视角",[11,1961,1962],{},"人物成长的第三步可以发生在此，主人公战胜了他的内心冲突",[11,1964,1965],{},[507,1966,1967],{},"HGS#17",[11,1969,1970],{},"为与对手或对手的代理人在第二幕的高潮中摊牌决战做好了最后的准备",[11,1972,1973],{},"对手的力量以主人公最为个人的方式得以再一次证明",[11,1975,1976],{},"倒计时接近零点，或致命危险步步逼近",[11,1978,1979],{},"当主人公怎么他为何与众不同时，他的勇气的力量得以展现——通常通过战胜他的内心冲突的人物成长的第三步",[11,1981,1982],{},[507,1983,1984],{},"HGS#18",[11,1986,1987],{},"一个比常规更长的段落——有时候会长达10分钟或以上",[11,1989,1990],{},"第二幕的高潮到达了冲突与行动的顶点——但这没有彻底解决故事的核心问题",[11,1992,1993],{},"主人公展现成长的新自我",[11,1995,1996],{},"主人公相信自己已经战胜了内心冲突",[11,1998,1999],{},"惊人意外#2带给主人公整个故事中最大的反转——通常是消极的",[11,2001,2002],{},"在浪漫喜剧中，感情中核心的谎言或欺骗常州惊人意外#2中得到揭露，背叛的痛苦让恋人看似彻底分手",[27,2004,2005],{"id":2005},"第三幕",[11,2007,2008],{},"如果惊人意外#2是积极的，基本上只会有两个序列，一个作为必备场景，一个是结局场景。",[11,2010,2011],{},"如果相反的，那么主人公则在受到打击后进入最后的决战的必备场景，这样就会需要将HGS#19作为缓冲，然后才有必备场景和结局场景。",[11,2013,2014],{},"如果需要四个场景的话，通常都是为了对次情节进行回收，或者需要一些额外的准备。",[11,2016,2017],{},"通常第三幕不会太长，否则会破坏叙事结构和观众的情感预期。",{"title":87,"searchDepth":599,"depth":600,"links":2019},[2020,2021,2022,2023,2024,2025,2026],{"id":1391,"depth":599,"text":1391},{"id":1432,"depth":599,"text":1432},{"id":1467,"depth":599,"text":1467},{"id":1569,"depth":599,"text":1569},{"id":1631,"depth":599,"text":1631},{"id":1661,"depth":599,"text":1661},{"id":2005,"depth":599,"text":2005},"2019-09-04",{"layout":649,"status":650,"published":651,"author":2029,"author_login":654,"author_email":655,"wordpress_id":2030,"wordpress_url":2031,"date_gmt":2032,"excerpt":2033},{"display_name":653,"login":654,"email":655,"url":87},2684,"\u002F?p=2684","2019-09-04 14:59:18 +0000",{"type":8,"value":2034},[2035],[11,2036,1375],{},"\u002F2019-09-04-the-story-solution-note",{"title":1370,"description":1375},"_legacy\u002F2019\u002F2019-09-04-%e3%80%8athe-story-solution%e3%80%8b%e7%ac%94%e8%ae%b0",[],"k8IRJYqwfJPVFiYOON19tr0QKmv4FIEtHJwdyirc9mI",222,1788763182419]