一.引言
因为工作需要,领导指定我使用抛体组件来实现某功能。故而翻阅抛体组件,刚开始看第一眼,感觉特别复杂。众所周知,UE对于玩家角色移动做的同步非常精妙,没想到随便一个抛物线组件也如此复杂。
因为是运动,所以首先看的是他如何运动,直接看Tick中逻辑。如下拉的源码,随便大致浏览一下即可)
void UProjectileMovementComponent::TickComponentfloat DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) { QUICK_SCOPE_CYCLE_COUNTER STAT_ProjectileMovementComponent_TickComponent ); // Still need to finish interpolating after we've stopped simulating, so do that first. if bInterpMovement && !bInterpolationComplete) { QUICK_SCOPE_CYCLE_COUNTERSTAT_ProjectileMovementComponent_TickInterpolation); TickInterpolationDeltaTime); } // Consume PendingForce and reset to zero. // At this point, any calls to AddForce) will apply to the next frame. PendingForceThisUpdate = PendingForce; ClearPendingForce); // skip if don't want component updated when not rendered or updated component can't move if HasStoppedSimulation) || ShouldSkipUpdateDeltaTime)) { return; } Super::TickComponentDeltaTime, TickType, ThisTickFunction); if !IsValidUpdatedComponent) || !bSimulationEnabled) { return; } AActor* ActorOwner = UpdatedComponent->GetOwner); if !ActorOwner || !CheckStillInWorld) ) { return; } if UpdatedComponent->IsSimulatingPhysics)) { return; } float RemainingTime = DeltaTime; int32 NumImpacts = 0; int32 NumBounces = 0; int32 LoopCount = 0; int32 Iterations = 0; FHitResult Hit1.f); while bSimulationEnabled && RemainingTime >= MIN_TICK_TIME && Iterations < MaxSimulationIterations) && !ActorOwner->IsPendingKill) && !HasStoppedSimulation)) { LoopCount++; Iterations++; // subdivide long ticks to more closely follow parabolic trajectory const float InitialTimeRemaining = RemainingTime; const float TimeTick = ShouldUseSubStepping) ? GetSimulationTimeStepRemainingTime, Iterations) : RemainingTime; RemainingTime -= TimeTick; // Logging UE_LOGLogProjectileMovement, Verbose, TEXT"Projectile %s: Role: %d, Iteration %d, step %.3f, [%.3f / %.3f] cur/total) sim Pos %s, Vel %s)"), *GetNameSafeActorOwner), int32)ActorOwner->GetLocalRole), LoopCount, TimeTick, FMath::Max0.f, DeltaTime - InitialTimeRemaining), DeltaTime, *UpdatedComponent->GetComponentLocation).ToString), *Velocity.ToString)); // Initial move state Hit.Time = 1.f; const FVector OldVelocity = Velocity; const FVector MoveDelta = ComputeMoveDeltaOldVelocity, TimeTick); FQuat NewRotation = bRotationFollowsVelocity && !OldVelocity.IsNearlyZero0.01f)) ? OldVelocity.ToOrientationQuat) : UpdatedComponent->GetComponentQuat); if bRotationFollowsVelocity && bRotationRemainsVertical) { FRotator DesiredRotation = NewRotation.Rotator); DesiredRotation.Pitch = 0.0f; DesiredRotation.Yaw = FRotator::NormalizeAxisDesiredRotation.Yaw); DesiredRotation.Roll = 0.0f; NewRotation = DesiredRotation.Quaternion); } // Move the component if bShouldBounce) { // If we can bounce, we are allowed to move out of penetrations, so use SafeMoveUpdatedComponent which does that automatically. SafeMoveUpdatedComponent MoveDelta, NewRotation, bSweepCollision, Hit ); } else { // If we can't bounce, then we shouldn't adjust if initially penetrating, because that should be a blocking hit that causes a hit event and stop simulation. TGuardValue<EMoveComponentFlags> ScopedFlagRestoreMoveComponentFlags, MoveComponentFlags | MOVECOMP_NeverIgnoreBlockingOverlaps); MoveUpdatedComponentMoveDelta, NewRotation, bSweepCollision, &Hit ); } // If we hit a trigger that destroyed us, abort. if ActorOwner->IsPendingKill) || HasStoppedSimulation) ) { return; } // Handle hit result after movement if !Hit.bBlockingHit ) { PreviousHitTime = 1.f; bIsSliding = false; // Only calculate new velocity if events didn't change it during the movement update. if Velocity == OldVelocity) { Velocity = ComputeVelocityVelocity, TimeTick); } // Logging UE_LOGLogProjectileMovement, VeryVerbose, TEXT"Projectile %s: Role: %d, Iteration %d, step %.3f) no hit Pos %s, Vel %s)"), *GetNameSafeActorOwner), int32)ActorOwner->GetLocalRole), LoopCount, TimeTick, *UpdatedComponent->GetComponentLocation).ToString), *Velocity.ToString)); } else { // Only calculate new velocity if events didn't change it during the movement update. if Velocity == OldVelocity) { // re-calculate end velocity for partial time Velocity = Hit.Time > KINDA_SMALL_NUMBER) ? ComputeVelocityOldVelocity, TimeTick * Hit.Time) : OldVelocity; } // Logging UE_CLOGUpdatedComponent != nullptr, LogProjectileMovement, VeryVerbose, TEXT"Projectile %s: Role: %d, Iteration %d, step %.3f) new hit at t=%.3f: Pos %s, Vel %s)"), *GetNameSafeActorOwner), int32)ActorOwner->GetLocalRole), LoopCount, TimeTick, Hit.Time, *UpdatedComponent->GetComponentLocation).ToString), *Velocity.ToString)); // Handle blocking hit NumImpacts++; float SubTickTimeRemaining = TimeTick * 1.f - Hit.Time); const EHandleBlockingHitResult HandleBlockingResult = HandleBlockingHitHit, TimeTick, MoveDelta, SubTickTimeRemaining); if HandleBlockingResult == EHandleBlockingHitResult::Abort || HasStoppedSimulation)) { break; } else if HandleBlockingResult == EHandleBlockingHitResult::Deflect) { NumBounces++; HandleDeflectionHit, OldVelocity, NumBounces, SubTickTimeRemaining); PreviousHitTime = Hit.Time; PreviousHitNormal = ConstrainNormalToPlaneHit.Normal); } else if HandleBlockingResult == EHandleBlockingHitResult::AdvanceNextSubstep) { // Reset deflection logic to ignore this hit PreviousHitTime = 1.f; } else { // Unhandled EHandleBlockingHitResult checkNoEntry); } // Logging UE_CLOGUpdatedComponent != nullptr, LogProjectileMovement, VeryVerbose, TEXT"Projectile %s: Role: %d, Iteration %d, step %.3f) deflect at t=%.3f: Pos %s, Vel %s)"), *GetNameSafeActorOwner), int32)ActorOwner->GetLocalRole), Iterations, TimeTick, Hit.Time, *UpdatedComponent->GetComponentLocation).ToString), *Velocity.ToString)); // Add unprocessed time after impact if SubTickTimeRemaining >= MIN_TICK_TIME) { RemainingTime += SubTickTimeRemaining; // A few initial impacts should possibly allow more iterations to complete more of the simulation. if NumImpacts <= BounceAdditionalIterations) { Iterations--; // Logging UE_LOGLogProjectileMovement, Verbose, TEXT"Projectile %s: Role: %d, Iteration %d, step %.3f) allowing extra iteration after bounce %u t=%.3f, adding %.3f secs)"), *GetNameSafeActorOwner), int32)ActorOwner->GetLocalRole), LoopCount, TimeTick, NumBounces, Hit.Time, SubTickTimeRemaining); } } } } UpdateComponentVelocity); }
View Code
二.分析
抛物线运动的逻辑就如上,可真是多啊。
直接说重点吧。
①既然抛体运动,就是受重力加速度影响,其实就是匀变速运动,那么肯定是需要知道 公式:V = Vo+ a*t
②因为是移动组件,所以需要计算出每帧需要做多少位移。
③需要做多少位移。因为是抛体组件,运动公式是知道的,所以根据推算,需要了解下述公式
匀变速运动的位移公式:S = V*t +0.5*a * t^2,即可以得出做多少位移就是
I. 匀变速直线运动的速度与时间关系的公式:V=V0+a*t → a = V – v0)/t
II. 匀变速直线运动的位移与时间关系的公式:x=v0*t+1/2*a*t^2 → x=v0*t+1/2*V – v0)/t * t^2 → x = v0*t+1/2*V – v0) * t
FVector UProjectileMovementComponent::ComputeMoveDeltaconst FVector& InVelocity, float DeltaTime) const { const FVector NewVelocity = ComputeVelocityInVelocity, DeltaTime); const FVector Delta = InVelocity * DeltaTime) + NewVelocity - InVelocity) * 0.5f * DeltaTime); return Delta; }
④最后根据算出的MoveDelta,直接赋给SceneComponent,即可。
三.注意
①关于如何使用这个组件纳,那就看看初始化函数 UProjectileMovementComponent::InitializeComponent)
if InitialSpeed > 0.f) { Velocity = Velocity.GetSafeNormal) * InitialSpeed; } if bInitialVelocityInLocalSpace) { SetVelocityInLocalSpaceVelocity); }
受初始化 InitialSpeed 影响,显而易见,当然也受 MaxSpeed 影响,所以你把Velocity设置的再大,也没用,这个只是方向而已。
②这是一个纯工具类组件,搜索UProjectileMovementComponent.h 并没有发现replicated的变量。所以,如果你的需求是在DS 也跑,Client也跑,那么肯定会有异常。最为简单的做法是你DS设置好,Client也设置好,可以使用,但是会存在一定误差,DS和Client的位移始终相差一段误差,这段误差是DS 同步到Client的误差时间 t, 平均速度 v,误差就大概等于 s = v *t,因此如果你的速度非常大,那么这个误差也就越大,如果需求是纯表现那还好,但是如果有DS上交互,出现的问题,就显而易见。那怎么办纳,还有ReplicateMovement可以帮忙同步模拟,这里扯得有点远了,就不说了,可以参考:https://www.cnblogs.com/haisong1991/p/11305783.html
完全不同的运动轨迹,当然如果你非要使用抛体,完成抛物线,那么就需要动态修改Velocity,需要加Tick之类逻辑。那么还不如自己撸,直接指定好抛物线轨迹,只需要同步float的 time 即可。
④抛体组件究竟干嘛的?
既然是抛体,自然是落地后出现反弹等一系列效果,使用抛体最佳。具体使用,待补充)。
学以致用,不致用,何学?