Unreal 拋體組件(ProjectileMovementComponent)解析


一.引言

  因為工作需要,領導指定我使用拋體組件來實現某功能。故而翻閱拋體組件,剛開始看第一眼,感覺特別復雜。眾所周知,UE對於玩家角色移動做的同步非常精妙,沒想到隨便一個拋物線組件也如此復雜。

因為是運動,所以首先看的是他如何運動,直接看Tick中邏輯。如下(拉的源碼,隨便大致瀏覽一下即可)

void UProjectileMovementComponent::TickComponent(float 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_COUNTER(STAT_ProjectileMovementComponent_TickInterpolation);
        TickInterpolation(DeltaTime);
    }

    // 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() || ShouldSkipUpdate(DeltaTime))
    {
        return;
    }

    Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

    if (!IsValid(UpdatedComponent) || !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 Hit(1.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() ? GetSimulationTimeStep(RemainingTime, Iterations) : RemainingTime;
        RemainingTime -= TimeTick;
        
        // Logging
        UE_LOG(LogProjectileMovement, Verbose, TEXT("Projectile %s: (Role: %d, Iteration %d, step %.3f, [%.3f / %.3f] cur/total) sim (Pos %s, Vel %s)"),
            *GetNameSafe(ActorOwner), (int32)ActorOwner->GetLocalRole(), LoopCount, TimeTick, FMath::Max(0.f, DeltaTime - InitialTimeRemaining), DeltaTime,
            *UpdatedComponent->GetComponentLocation().ToString(), *Velocity.ToString());

        // Initial move state
        Hit.Time = 1.f;
        const FVector OldVelocity = Velocity;
        const FVector MoveDelta = ComputeMoveDelta(OldVelocity, TimeTick);
        FQuat NewRotation = (bRotationFollowsVelocity && !OldVelocity.IsNearlyZero(0.01f)) ? OldVelocity.ToOrientationQuat() : UpdatedComponent->GetComponentQuat();

        if (bRotationFollowsVelocity && bRotationRemainsVertical)
        {
            FRotator DesiredRotation = NewRotation.Rotator();
            DesiredRotation.Pitch = 0.0f;
            DesiredRotation.Yaw = FRotator::NormalizeAxis(DesiredRotation.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> ScopedFlagRestore(MoveComponentFlags, MoveComponentFlags | MOVECOMP_NeverIgnoreBlockingOverlaps);
            MoveUpdatedComponent(MoveDelta, 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 = ComputeVelocity(Velocity, TimeTick);                
            }

            // Logging
            UE_LOG(LogProjectileMovement, VeryVerbose, TEXT("Projectile %s: (Role: %d, Iteration %d, step %.3f) no hit (Pos %s, Vel %s)"),
                *GetNameSafe(ActorOwner), (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) ? ComputeVelocity(OldVelocity, TimeTick * Hit.Time) : OldVelocity;
            }

            // Logging
            UE_CLOG(UpdatedComponent != nullptr, LogProjectileMovement, VeryVerbose, TEXT("Projectile %s: (Role: %d, Iteration %d, step %.3f) new hit at t=%.3f: (Pos %s, Vel %s)"),
                *GetNameSafe(ActorOwner), (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 = HandleBlockingHit(Hit, TimeTick, MoveDelta, SubTickTimeRemaining);
            if (HandleBlockingResult == EHandleBlockingHitResult::Abort || HasStoppedSimulation())
            {
                break;
            }
            else if (HandleBlockingResult == EHandleBlockingHitResult::Deflect)
            {
                NumBounces++;
                HandleDeflection(Hit, OldVelocity, NumBounces, SubTickTimeRemaining);
                PreviousHitTime = Hit.Time;
                PreviousHitNormal = ConstrainNormalToPlane(Hit.Normal);
            }
            else if (HandleBlockingResult == EHandleBlockingHitResult::AdvanceNextSubstep)
            {
                // Reset deflection logic to ignore this hit
                PreviousHitTime = 1.f;
            }
            else
            {
                // Unhandled EHandleBlockingHitResult
                checkNoEntry();
            }
            
            // Logging
            UE_CLOG(UpdatedComponent != nullptr, LogProjectileMovement, VeryVerbose, TEXT("Projectile %s: (Role: %d, Iteration %d, step %.3f) deflect at t=%.3f: (Pos %s, Vel %s)"),
                *GetNameSafe(ActorOwner), (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_LOG(LogProjectileMovement, Verbose, TEXT("Projectile %s: (Role: %d, Iteration %d, step %.3f) allowing extra iteration after bounce %u (t=%.3f, adding %.3f secs)"),
                        *GetNameSafe(ActorOwner), (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::ComputeMoveDelta(const FVector& InVelocity, float DeltaTime) const
{
    const FVector NewVelocity = ComputeVelocity(InVelocity, 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)
        {
            SetVelocityInLocalSpace(Velocity);
        }

受初始化 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 即可。

  ④拋體組件究竟干嘛的?

            既然是拋體,自然是落地后出現反彈等一系列效果,使用拋體最佳。(具體使用,待補充)。

    


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM