如果有个需求是:一个character 时而 接受input ,charactermovement 网络同步 正常工作,时而就当一个普通的actor沿着固定的路线自动运动
分析一下,正常模式下是charactermovement来影响RootComponent的位移和旋转。
正常模式下的character的位移和rotator变化的流程是
第一 rootcomponent是关闭网络同步的,要不和charactermovement打架
第二charactermovement的网络同步,在不同端分别影响updatecomponent。
所以在沿着固定路线自动移动的时候,要解决最大的问题是,让charactermovement不能影响RootComponent。让charactermovement不工作。同时启用rootcomponent的网络同步,通过在服务器上直接设置setactortransform,通过RootComponent->MoveComponent自动网络同步。
那么如何动态的设置charactermovement不工作呢
第一把组件的tick关掉
第二把另外事件驱动的ServerMove_XXXX之类的关掉,经过查找都有IsActive()过滤,于是可以通过setactive来滤掉ServerMove等操作。
例子如下
void XXCharacter::SetMovementTickEnable(bool TickEnable)
{
if (GetRootComponent() != nullptr)
{
GetRootComponent()->SetIsReplicated(!TickEnable);
}
if (UCharacterMovementComponent* Movement = GetCharacterMovement())
{
Movement->SetComponentTickEnabledAsync(TickEnable);
Movement->SetActive(TickEnable);
}
}
之后就可以在server端的tick里面每帧都设置character的位置,就可以自动同步了
------------------------------------------------------------------------------------------------------------
关于胶囊体的旋转
先 本地移动
然后 把移动保存为 save character 传给服务器
服务器 再移动
然后自动同步给模拟端
关于 compressedflag只同步到服务器 就不会往模拟端同步了
UpdateFromCompressedFlags 这里在服务器解压获得jump变量
可以在这里设置服务器上的 character的变量 然后把这个变量属性同步到模拟端
-----------------------------------------------------------------------------------------------------------------
角色 运动 停止 物理滑动 braking
只有加速度为0 或者 速度超过最大速度了,才会起作用,看如下代码
// Only apply braking if there is no acceleration, or we are over our max speed and need to slow down to it.
if ((bZeroAcceleration && bZeroRequestedAcceleration) || bVelocityOverMax)
{
const FVector OldVelocity = Velocity;
const float ActualBrakingFriction = (bUseSeparateBrakingFriction ? BrakingFriction : Friction);
ApplyVelocityBraking(DeltaTime, ActualBrakingFriction, BrakingDeceleration);
// Don't allow braking to lower us below max speed if we started above it.
if (bVelocityOverMax && Velocity.SizeSquared() < FMath::Square(MaxSpeed) && FVector::DotProduct(Acceleration, OldVelocity) > 0.0f)
{
Velocity = OldVelocity.GetSafeNormal() * MaxSpeed;
}
}
如下代码是根据速度和摩擦系数等预计算出要刹车多长距离
FVector UAnimCharacterMovementLibrary::PredictGroundMovementStopLocation(const FVector& Velocity,
bool bUseSeparateBrakingFriction, float BrakingFriction, float GroundFriction, float BrakingFrictionFactor, float BrakingDecelerationWalking)
{
FVector PredictedStopLocation = FVector::ZeroVector;
float ActualBrakingFriction = (bUseSeparateBrakingFriction ? BrakingFriction : GroundFriction);
const float FrictionFactor = FMath::Max(0.f, BrakingFrictionFactor);
ActualBrakingFriction = FMath::Max(0.f, ActualBrakingFriction * FrictionFactor);
float BrakingDeceleration = FMath::Max(0.f, BrakingDecelerationWalking);
const FVector Velocity2D = Velocity * FVector(1.f, 1.f, 0.f);
FVector VelocityDir2D;
float Speed2D;
Velocity2D.ToDirectionAndLength(VelocityDir2D, Speed2D);
const float Divisor = ActualBrakingFriction * Speed2D + BrakingDeceleration;
if (Divisor > 0.f)
{
const float TimeToStop = Speed2D / Divisor;
PredictedStopLocation = Velocity2D * TimeToStop + 0.5f * ((-ActualBrakingFriction) * Velocity2D - BrakingDeceleration * VelocityDir2D) * TimeToStop * TimeToStop;
}
return PredictedStopLocation;
}
上面代码的蓝图
然后在动画蓝图中根据预计算出的距离,计算出刹车动画的那一帧开始播放
动画必须是把距离烘培到曲线的动画并且不能开启rootmotion
------------------------------------------------------------------------
-----------------------------------------------------------------------
AActor中
/**
* Return true if the given Pawn can be "based" on this actor (ie walk on it).
* @param Pawn - The pawn that wants to be based on this actor
*/
virtual bool CanBeBaseForCharacter(class APawn* Pawn) const;
决定movement行走的时候 ,能不能跨过或者压过这个actor过去
另外
/**
* Return true if the given Pawn can step up onto this component.
* This controls whether they can try to step up on it when they bump in to it, not whether they can walk on it after landing on it.
* @param Pawn the Pawn that wants to step onto this component.
* @see CanCharacterStepUpOn
*/
UFUNCTION(BlueprintCallable, Category=Collision)
virtual bool CanCharacterStepUp(class APawn* Pawn) const;
bool UPrimitiveComponent::CanCharacterStepUp(APawn* Pawn) const
{
if ( CanCharacterStepUpOn != ECB_Owner )
{
return CanCharacterStepUpOn == ECB_Yes;
}
else
{
const AActor* Owner = GetOwner();
return Owner && Owner->CanBeBaseForCharacter(Pawn);
}
}
----------------------------------------------------------
true 能走下悬崖
false 不能向前走
--------------------------------------------------------------------------------
跳起来 在空中 响应玩家input
跟 这三个参数有关,尤其是AirControl有关
//在计算速度的时候用的加速度是FallAcceleration
void UCharacterMovementComponent::PhysFalling(float deltaTime, int32 Iterations)
{
........
FVector FallAcceleration = GetFallingLateralAcceleration(deltaTime);
FallAcceleration.Z = 0.f;
const bool bHasLimitedAirControl = ShouldLimitAirControl(deltaTime, FallAcceleration);
while( (remainingTime >= MIN_TICK_TIME) && (Iterations < MaxSimulationIterations) )
{
........
const FVector OldVelocity = Velocity;
// Apply input
const float MaxDecel = GetMaxBrakingDeceleration();
if (!HasAnimRootMotion() && !CurrentRootMotion.HasOverrideVelocity())
{
// Compute Velocity
{
// Acceleration = FallAcceleration for CalcVelocity(), but we restore it after using it.
TGuardValue<FVector> RestoreAcceleration(Acceleration, FallAcceleration);
Velocity.Z = 0.f;
CalcVelocity(timeTick, FallingLateralFriction, false, MaxDecel);
Velocity.Z = OldVelocity.Z;
}
}
}
}
//FallAcceleration 用的是加速度的xy,本身包含input
//但是如果FallAcceleration.SizeSquared2D() > 0.f 就会对FallAcceleration 改变
//
FVector UCharacterMovementComponent::GetFallingLateralAcceleration(float DeltaTime)
{
// No acceleration in Z
FVector FallAcceleration = FVector(Acceleration.X, Acceleration.Y, 0.f);
// bound acceleration, falling object has minimal ability to impact acceleration
if (!HasAnimRootMotion() && FallAcceleration.SizeSquared2D() > 0.f)
{
FallAcceleration = GetAirControl(DeltaTime, AirControl, FallAcceleration);
FallAcceleration = FallAcceleration.GetClampedToMaxSize(GetMaxAcceleration());
}
return FallAcceleration;
}
//TickAirControl 大于0 就改变TickAirControl
FVector UCharacterMovementComponent::GetAirControl(float DeltaTime, float TickAirControl, const FVector& FallAcceleration)
{
// Boost
if (TickAirControl != 0.f)
{
TickAirControl = BoostAirControl(DeltaTime, TickAirControl, FallAcceleration);
}
return TickAirControl * FallAcceleration;
}
//如果速度小于AirControlBoostVelocityThreshold 就改变TickAirControl
//AirControlBoostMultiplier * TickAirControl
//所以把TickAirControl=1就会完全用原始加速度的xy
float UCharacterMovementComponent::BoostAirControl(float DeltaTime, float TickAirControl, const FVector& FallAcceleration)
{
// Allow a burst of initial acceleration
if (AirControlBoostMultiplier > 0.f && Velocity.SizeSquared2D() < FMath::Square(AirControlBoostVelocityThreshold))
{
TickAirControl = FMath::Min(1.f, AirControlBoostMultiplier * TickAirControl);
}
return TickAirControl;
}
所以把AirControl=1 并且 AirControlBoostMultiplier>1 就会用原始的加速度的xy,就会响应input
-----------------------------------------------------------------------------------------------
input
用户的input 在这里传输到movement里面
void UCharacterMovementComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction)
{
SCOPED_NAMED_EVENT(UCharacterMovementComponent_TickComponent, FColor::Yellow);
SCOPE_CYCLE_COUNTER(STAT_CharacterMovement);
SCOPE_CYCLE_COUNTER(STAT_CharacterMovementTick);
CSV_SCOPED_TIMING_STAT_EXCLUSIVE(CharacterMovement);
FVector InputVector = FVector::ZeroVector;
bool bUsingAsyncTick = (CharacterMovementCVars::AsyncCharacterMovement == 1) && IsAsyncCallbackRegistered();
if (!bUsingAsyncTick)
{
// Do not consume input if simulating asynchronously, we will consume input when filling out async inputs.
InputVector = ConsumeInputVector();
}
..........................................
// Allow root motion to move characters that have no controller.
if (CharacterOwner->IsLocallyControlled() || (!CharacterOwner->Controller && bRunPhysicsWithNoController) || (!CharacterOwner->Controller && CharacterOwner->IsPlayingRootMotion()))
{
ControlledCharacterMove(InputVector, DeltaTime);
}
}
void UCharacterMovementComponent::ControlledCharacterMove(const FVector& InputVector, float DeltaSeconds)
{
{
SCOPE_CYCLE_COUNTER(STAT_CharUpdateAcceleration);
// We need to check the jump state before adjusting input acceleration, to minimize latency
// and to make sure acceleration respects our potentially new falling state.
CharacterOwner->CheckJumpInput(DeltaSeconds);
// apply input to acceleration
Acceleration = ScaleInputAcceleration(ConstrainInputAcceleration(InputVector));
AnalogInputModifier = ComputeAnalogInputModifier();
}
if (CharacterOwner->GetLocalRole() == ROLE_Authority)
{
PerformMovement(DeltaSeconds);
}
else if (CharacterOwner->GetLocalRole() == ROLE_AutonomousProxy && IsNetMode(NM_Client))
{
ReplicateMoveToServer(DeltaSeconds, Acceleration);
}
}
FVector UCharacterMovementComponent::ConstrainInputAcceleration(const FVector& InputAcceleration) const
{
// walking or falling pawns ignore up/down sliding
if (InputAcceleration.Z != 0.f && (IsMovingOnGround() || IsFalling()))
{
return FVector(InputAcceleration.X, InputAcceleration.Y, 0.f);
}
return InputAcceleration;
}
//
FVector UCharacterMovementComponent::ScaleInputAcceleration(const FVector& InputAcceleration) const
{
return GetMaxAcceleration() * InputAcceleration.GetClampedToMaxSize(1.0f);
}
//input最大就是1 acc最大也就是maxacc
//加速度直接就影响速度了
void UCharacterMovementComponent::CalcVelocity(float DeltaTime, float Friction, bool bFluid, float BrakingDeceleration)
{
// Do not update velocity when using root motion or when SimulatedProxy and not simulating root motion - SimulatedProxy are repped their Velocity
if (!HasValidData() || HasAnimRootMotion() || DeltaTime < MIN_TICK_TIME || (CharacterOwner && CharacterOwner->GetLocalRole() == ROLE_SimulatedProxy && !bWasSimulatingRootMotion))
{
return;
}
Friction = FMath::Max(0.f, Friction);
const float MaxAccel = GetMaxAcceleration();
float MaxSpeed = GetMaxSpeed();
// Check if path following requested movement
bool bZeroRequestedAcceleration = true;
FVector RequestedAcceleration = FVector::ZeroVector;
float RequestedSpeed = 0.0f;
if (ApplyRequestedMove(DeltaTime, MaxAccel, MaxSpeed, Friction, BrakingDeceleration, RequestedAcceleration, RequestedSpeed))
{
bZeroRequestedAcceleration = false;
}
if (bForceMaxAccel)
{
// Force acceleration at full speed.
// In consideration order for direction: Acceleration, then Velocity, then Pawn's rotation.
if (Acceleration.SizeSquared() > UE_SMALL_NUMBER)
{
Acceleration = Acceleration.GetSafeNormal() * MaxAccel;
}
else
{
Acceleration = MaxAccel * (Velocity.SizeSquared() < UE_SMALL_NUMBER ? UpdatedComponent->GetForwardVector() : Velocity.GetSafeNormal());
}
AnalogInputModifier = 1.f;
}
// Path following above didn't care about the analog modifier, but we do for everything else below, so get the fully modified value.
// Use max of requested speed and max speed if we modified the speed in ApplyRequestedMove above.
const float MaxInputSpeed = FMath::Max(MaxSpeed * AnalogInputModifier, GetMinAnalogSpeed());
MaxSpeed = FMath::Max(RequestedSpeed, MaxInputSpeed);
// Apply braking or deceleration
const bool bZeroAcceleration = Acceleration.IsZero();
const bool bVelocityOverMax = IsExceedingMaxSpeed(MaxSpeed);
// Only apply braking if there is no acceleration, or we are over our max speed and need to slow down to it.
if ((bZeroAcceleration && bZeroRequestedAcceleration) || bVelocityOverMax)
{
const FVector OldVelocity = Velocity;
const float ActualBrakingFriction = (bUseSeparateBrakingFriction ? BrakingFriction : Friction);
ApplyVelocityBraking(DeltaTime, ActualBrakingFriction, BrakingDeceleration);
// Don't allow braking to lower us below max speed if we started above it.
if (bVelocityOverMax && Velocity.SizeSquared() < FMath::Square(MaxSpeed) && FVector::DotProduct(Acceleration, OldVelocity) > 0.0f)
{
Velocity = OldVelocity.GetSafeNormal() * MaxSpeed;
}
}
else if (!bZeroAcceleration)
{
// Friction affects our ability to change direction. This is only done for input acceleration, not path following.
const FVector AccelDir = Acceleration.GetSafeNormal();
const float VelSize = Velocity.Size();
Velocity = Velocity - (Velocity - AccelDir * VelSize) * FMath::Min(DeltaTime * Friction, 1.f);
}
// Apply fluid friction
if (bFluid)
{
Velocity = Velocity * (1.f - FMath::Min(Friction * DeltaTime, 1.f));
}
// Apply input acceleration
if (!bZeroAcceleration)
{
const float NewMaxInputSpeed = IsExceedingMaxSpeed(MaxInputSpeed) ? Velocity.Size() : MaxInputSpeed;
Velocity += Acceleration * DeltaTime;
Velocity = Velocity.GetClampedToMaxSize(NewMaxInputSpeed);
}
// Apply additional requested acceleration
if (!bZeroRequestedAcceleration)
{
const float NewMaxRequestedSpeed = IsExceedingMaxSpeed(RequestedSpeed) ? Velocity.Size() : RequestedSpeed;
Velocity += RequestedAcceleration * DeltaTime;
Velocity = Velocity.GetClampedToMaxSize(NewMaxRequestedSpeed);
}
if (bUseRVOAvoidance)
{
CalcAvoidanceVelocity(DeltaTime);
}
}
值得注意的是 input 跟角色质量没有关系。如果input要考虑质量就得重写ScaleInputAcceleration方法
---------------------------------------------------------------------
charactermovement里面有几个方法是跟质量有关的
void UCharacterMovementComponent::AddImpulse( FVector Impulse, bool bVelocityChange )
{
if (!Impulse.IsZero() && (MovementMode != MOVE_None) && IsActive() && HasValidData())
{
// handle scaling by mass
FVector FinalImpulse = Impulse;
if ( !bVelocityChange )
{
if (Mass > UE_SMALL_NUMBER)
{
FinalImpulse = FinalImpulse / Mass;
}
else
{
UE_LOG(LogCharacterMovement, Warning, TEXT("Attempt to apply impulse to zero or negative Mass in CharacterMovement"));
}
}
PendingImpulseToApply += FinalImpulse;
}
}
void UCharacterMovementComponent::AddForce( FVector Force )
{
if (!Force.IsZero() && (MovementMode != MOVE_None) && IsActive() && HasValidData())
{
if (Mass > UE_SMALL_NUMBER)
{
PendingForceToApply += Force / Mass;
}
else
{
UE_LOG(LogCharacterMovement, Warning, TEXT("Attempt to apply force to zero or negative Mass in CharacterMovement"));
}
}
}
//PendingImpulseToApply 和 PendingForceToApply 直接影响速度Velocity ,注意force是要乘上帧时间的
void UCharacterMovementComponent::ApplyAccumulatedForces(float DeltaSeconds)
{
if (PendingImpulseToApply.Z != 0.f || PendingForceToApply.Z != 0.f)
{
// check to see if applied momentum is enough to overcome gravity
if ( IsMovingOnGround() && (PendingImpulseToApply.Z + (PendingForceToApply.Z * DeltaSeconds) + (GetGravityZ() * DeltaSeconds) > UE_SMALL_NUMBER))
{
SetMovementMode(MOVE_Falling);
}
}
Velocity += PendingImpulseToApply + (PendingForceToApply * DeltaSeconds);
// Don't call ClearAccumulatedForces() because it could affect launch velocity
PendingImpulseToApply = FVector::ZeroVector;
PendingForceToApply = FVector::ZeroVector;
}
ApplyAccumulatedForces(DeltaSeconds);和质量有关
ApplyAccumulatedForces函数是在Physxxx函数之前调用的。
input和质量无关
UPrimitiveComponent下有这个,但是movement的物理并不走UpdatedComponent的物理
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Collision, meta=(ShowOnlyInnerProperties, SkipUCSModifiedProperties))
FBodyInstance BodyInstance;
从UpdatedComponent的类型是 USceneComponent类型 就可以看到,movement只是生硬的操纵USceneComponent的位置和朝向而已。
UPROPERTY(BlueprintReadOnly, Transient, DuplicateTransient, Category=MovementComponent)
TObjectPtr<USceneComponent> UpdatedComponent;
-----------------------------------------------------------------------------------------------
UCharacterMovementComponent管理角色的移动和空间状态(比如飞翔,游泳,攀爬)
UCharacterMovementComponent是Acharacter的一个组件。它最终操纵的是USceneComponent,如图中的CapsuleComponent。UCharacterMovementComponent::UpdateComponent。
------------------------------------------------------------------------------------------------------------------------------------------------------------
同类的移动组件不止一个如图(盗图)
详解上图四类移动组件
1、
UMovementComponent,实现了最基本的移动功能。调用顺序如下
UMovementComponent::SafeMoveUpdatedComponent()
UMovementComponent::MoveUpdatedComponentImpl()
UpdatedComponent->MoveComponent()
这里的UpdatedComponent就是UScenceComponent。
2、UNavMovementComponent,用于ai,能自动寻路。
3、UPawnMovementComponent,增加了被玩家控制的功能。
void UPawnMovementComponent::AddInputVector(FVector WorldAccel, bool bForce /*=false*/)
{
if (PawnOwner)
{
PawnOwner->Internal_AddMovementInput(WorldAccel, bForce);
}
}
流程是,玩家通过InputComponent组件绑定一个按键操作,当按下按键的时候,调用pawn的AddMovementInput方法。
void APawn::AddMovementInput(FVector WorldDirection, float ScaleValue, bool bForce /*=false*/)
{
UPawnMovementComponent* MovementComponent = GetMovementComponent();
if (MovementComponent)
{
MovementComponent->AddInputVector(WorldDirection * ScaleValue, bForce);
}
else
{
Internal_AddMovementInput(WorldDirection * ScaleValue, bForce);
}
}
调用结束后会通过ConsumeMovementInputVector()接口消耗掉该次操作的输入数值,完成一次移动操作。
4、UCharacterMovementComponent,此组件就是能操纵人型角色的移动组件了,里面非常精准的处理了各种常见的移动状态细节,实现了比较流畅的同步解决方案,各种位置矫正,平滑处理。我们不需要写很多自己的代码就能做出动作复杂的真实的FPS和第三人称的RPG游戏。
5、UProjectileMovementComponent ,一般用来模拟弓箭,子弹等抛射物的运动状态。
----------------------------------------------------------------------------------------------------------------------------------------------------------------
一个总的框架图(盗图)
来看看UCharacterMovementComponent进化历史:
在一个普通的三维空间里,最简单的移动就是直接修改角色的坐标。所以,我们的角色只要有一个包含坐标信息的组件,就可以通过基本的移动组件完成移动。但是随着游戏世界的复杂程度加深,我们在游戏里面添加了可行走的地面,可以探索的海洋。我们发现移动就变得复杂起来,玩家的脚下有地面才能行走,那就需要不停的检测地面碰撞信息(FFindFloorResult,FBasedMovementInfo);玩家想进入水中游泳,那就需要检测到水的体积(GetPhysicsVolume(),Overlap事件,同样需要物理);水中的速度与效果与陆地上差别很大,那就把两个状态分开写(PhysSwimming,PhysWalking);移动的时候动画动作得匹配上啊,那就在更新位置的时候,更新动画(TickCharacterPose);移动的时候碰到障碍物怎么办,被其他玩家推怎么处理(MoveAlongFloor里有相关处理);游戏内容太少,想增加一些可以自己寻路的NPC,又需要设置导航网格(涉及到FNavAgentProperties);一个玩家太无聊,那就让大家一起联机玩(模拟移动同步FRepMovement,客户端移动修正ClientUpdatePositionAfterServerUpdate)。
-------------------------------------------------------------------------------------------------------------------------------------------------------------------
UCharacterMovementComponent是如何处理各种移动状态下的玩家呢?首先从tick开始,因为每帧都要检测、判断、处理各种状态,状态通过MovementMode来区分,在合适的时候修改为正确的移动模式。移动模式默认几种:
/** Movement modes for Characters. */
UENUM(BlueprintType)
enum EMovementMode
{
/** None (movement is disabled). */
MOVE_None UMETA(DisplayName="None"),
/** Walking on a surface. */
MOVE_Walking UMETA(DisplayName="Walking"),
/**
* Simplified walking on navigation data (e.g. navmesh).
* If GetGenerateOverlapEvents() is true, then we will perform sweeps with each navmesh move.
* If GetGenerateOverlapEvents() is false then movement is cheaper but characters can overlap other objects without some extra process to repel/resolve their collisions.
*/
MOVE_NavWalking UMETA(DisplayName="Navmesh Walking"),
/** Falling under the effects of gravity, such as after jumping or walking off the edge of a surface. */
MOVE_Falling UMETA(DisplayName="Falling"),
/** Swimming through a fluid volume, under the effects of gravity and buoyancy. */
MOVE_Swimming UMETA(DisplayName="Swimming"),
/** Flying, ignoring the effects of gravity. Affected by the current physics volume's fluid friction. */
MOVE_Flying UMETA(DisplayName="Flying"),
/** User-defined custom movement mode, including many possible sub-modes. */
MOVE_Custom UMETA(DisplayName="Custom"),
MOVE_MAX UMETA(Hidden),
};
单机模式下移动处理流程如下:
未完待续