zoukankan      html  css  js  c++  java
  • 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 即可。

      ④抛体组件究竟干嘛的?

                既然是抛体,自然是落地后出现反弹等一系列效果,使用抛体最佳。(具体使用,待补充)。

        

    学以致用,不致用,何学?
  • 相关阅读:
    java设计模式-----3、抽象工厂模式
    java设计模式-----2、工厂方法模式
    java设计模式-----1、简单工厂模式
    解决windows10和ubuntu16.04双系统下时间不对的问题
    个人第二次作业-熟悉使用工具成绩汇总
    第二周助教小结——工作步入正轨
    第一周助教小结——助教工作初体验
    助教培训总结——熟练掌握GitHub及Git的使用方法
    助教培训总结——原型设计工具及练习
    助教培训总结——对学生任务博客的评分练习
  • 原文地址:https://www.cnblogs.com/u3ddjw/p/14699817.html
Copyright © 2011-2022 走看看