unreal 让actor移动_unreal 隐藏actor - CSDN
精华内容
参与话题
  • UE4移动组件详解(一)——移动框架与实现原理

    万次阅读 多人点赞 2018-03-03 11:27:33
    分别从移动框架与实现原理,移动的网络同步,移动组件的优化与改造三个方面来写。这三篇文档中难免有问题和漏洞,所以我也会在发现问题时及时更新和修改,也希望大家能给出一些建议。 一.深刻理解移动组件的意义...

    前言
    关于UE4的移动组件,我写了一篇非常详细的分析文档。由于篇幅比较大,我将其拆分成三个部分。分别从移动框架与实现原理,移动的网络同步,移动组件的优化与改造三个方面来写。这三篇文档中难免有问题和漏洞,所以我也会在发现问题时及时更新和修改,也希望大家能给出一些建议。

    一.深刻理解移动组件的意义

    在大部分游戏中,玩家的移动是最最核心的一个基本操作。UE提供的GamePlay框架就给开发者提供了一个比较完美的移动解决方案。由于UE采用了组件化的设计思路,所以这个移动解决方案的核心功能就都交给了移动组件来完成。移动可能根据游戏的复杂程度有不同的处理,如果是一个简单的俯视视角RTS类型的游戏,可能只提供基本的坐标移动就可以了;而对于第一人称的RPG游戏,玩家可能上天入地,潜水飞行,那需要的移动就要更复杂一些。但是不管是哪一种,UE都基本上帮我们实现了,这也得益于其早期的FPS游戏的开发经验。

    然而,引擎提供的基本移动并不一定能完成我们的目标,我们也不应该因此局限我们的设计。比如轻功的飞檐走壁,魔法飞船的超重力,弹簧鞋,喷气背包飞行控制,这些效果都需要我们自己去进一步的处理移动逻辑,我们可以在其基础上修改,也可以自定义自己的移动模式。不管怎么样,这些操作都需要对移动组件进行细致入微的调整,所以我们就必须要深刻理解移动组件的实现原理。

    再者,在一个网络游戏中,我们对移动的处理会更加的复杂。如何让不同客户端的玩家都体验到流畅的移动表现?如何保证角色不会由于一点点的延迟而产生“瞬移”?UE对这方面的处理都值得我们去学习与思考。

    移动组件看起来只是一个和移动相关的组件,但其本身涉及到状态机,同步解决方案,物理模块,不同移动状态的细节处理,动画以及与其他组件(Actor)之间的调用关系等相关内容,足够花上一段时间去好好研究。这篇文章会从移动的基本原理,移动状态的细节处理,移动同步的解决方案几个角度尽可能详细的分析其实现原理,然后帮助大家快速理解并更好的使用移动组件。最后,给出几个特殊移动模式的实现思路供大家参考。

    二.移动实现的基本原理

    2.1 移动组件与玩家角色

    角色的移动本质上就是合理的改变坐标位置,在UE里面角色移动的本质就是修改某个特定组件的坐标位置。图2-1是我们常见的一个Character的组件构成情况,可以看到我们通常将CapsuleComponent(胶囊体)作为自己的根组件,而Character的坐标本质上就是其RootComponent的坐标,Mesh网格等其他组件都会跟随胶囊体而移动。移动组件在初始化的时候会把胶囊体设置为移动基础组件UpdateComponent,随后的操作都是在计算UpdateComponent的位置。

    这里写图片描述
    图2-1 一个默认Character的组件构成

    当然,我们也并不是一定要设置胶囊体为UpdateComponent,对于DefaultPawn(观察者)会把他的SphereComponent作为UpdateComponent,对于交通工具对象AWheeledVehicle会默认把他的Mesh网格组件作为UpdateComponent。你可以自己定义你的UpdateComponent,但是你的自定义组件必须要继承USceneComponent(换句话说就是组件得有世界坐标信息),这样他才能正常的实现其移动的逻辑。

    2.2 移动组件继承树

    移动组件类并不是只有一个,他通过一个继承树,逐渐扩展了移动组件的能力。从最简单的提供移动功能,到可以正确模拟不同移动状态的移动效果。如图2-2所示
    这里写图片描述
    图2-2 移动组件继承关系类图

    移动组件类一共四个。首先是UMovementComponent,作为移动组件的基类实现了基本的移动接口SafeMovementUpdatedComponent(),可以调用UpdateComponent组件的接口函数来更新其位置。

    bool UMovementComponent::MoveUpdatedComponentImpl( const FVector& Delta, const FQuat& NewRotation, bool bSweep, FHitResult* OutHit, ETeleportType Teleport)
    {
        if (UpdatedComponent)
        {
            const FVector NewDelta = ConstrainDirectionToPlane(Delta);
            return UpdatedComponent->MoveComponent(NewDelta, NewRotation, bSweep, OutHit, MoveComponentFlags, Teleport);
        }
    
        return false;
    }

    通过上图可以看到UpdateComponent的类型是UScenceComponent,UScenceComponent类型的组件提供了基本的位置信息——ComponentToWorld,同时也提供了改变自身以及其子组件的位置的接口InternalSetWorldLocationAndRotation()。而UPrimitiveComponent又继承于UScenceComponent,增加了渲染以及物理方面的信息。我们常见的Mesh组件以及胶囊体都是继承自UPrimitiveComponent,因为想要实现一个真实的移动效果,我们时刻都可能与物理世界的某一个Actor接触着,而且移动的同时还需要渲染出我们移动的动画来表现给玩家看。

    下一个组件是UNavMovementComponent,该组件更多的是提供给AI寻路的能力,同时包括基本的移动状态,比如是否能游泳,是否能飞行等。

    UPawnMovementComponent组件开始变得可以和玩家交互了,前面都是基本的移动接口,不手动调用根本无法实现玩家操作。UPawnMovementComponent提供了AddInputVector(),可以实现接收玩家的输入并根据输入值修改所控制Pawn的位置。要注意的是,在UE中,Pawn是一个可控制的游戏角色(也可以是被AI控制),他的移动必须与UPawnMovementComponent配合才行,所以这也是名字的由来吧。一般的操作流程是,玩家通过InputComponent组件绑定一个按键操作,然后在按键响应时调用Pawn的AddMovementInput接口,进而调用移动组件的AddInputVector(),调用结束后会通过ConsumeMovementInputVector()接口消耗掉该次操作的输入数值,完成一次移动操作。

    最后到了移动组件的重头了UCharacterMovementComponent,该组件可以说是Epic做了多年游戏的经验集成了,里面非常精确的处理了各种常见的移动状态细节,实现了比较流畅的同步解决方案。各种位置校正,平滑处理才达到了目前的移动效果,而且我们不需要自己写代码就会使用这个完成度的相当高的移动组件,可以说确实很适合做第一,第三人称的RPG游戏了。

    其实还有一个比较常用的移动组件,UProjectileMovementComponent ,一般用来模拟弓箭,子弹等抛射物的运动状态。不过,这篇文档不将重点放在这里。

    2.3 移动组件相关类关系简析

    前面主要针对移动组件本身进行了分析,这里更全面的概括一下移动的整个框架。(参考图2-3)
    这里写图片描述
    图2-3 移动框架相关类图

    在一个普通的三维空间里,最简单的移动就是直接修改角色的坐标。所以,我们的角色只要有一个包含坐标信息的组件,就可以通过基本的移动组件完成移动。但是随着游戏世界的复杂程度加深,我们在游戏里面添加了可行走的地面,可以探索的海洋。我们发现移动就变得复杂起来,玩家的脚下有地面才能行走,那就需要不停的检测地面碰撞信息(FFindFloorResult,FBasedMovementInfo);玩家想进入水中游泳,那就需要检测到水的体积(GetPhysicsVolume(),Overlap事件,同样需要物理);水中的速度与效果与陆地上差别很大,那就把两个状态分开写(PhysSwimming,PhysWalking);移动的时候动画动作得匹配上啊,那就在更新位置的时候,更新动画(TickCharacterPose);移动的时候碰到障碍物怎么办,被其他玩家推怎么处理(MoveAlongFloor里有相关处理);游戏内容太少,想增加一些可以自己寻路的NPC,又需要设置导航网格(涉及到FNavAgentProperties);一个玩家太无聊,那就让大家一起联机玩(模拟移动同步FRepMovement,客户端移动修正ClientUpdatePositionAfterServerUpdate)。

    这么一看,做一个优秀移动组件还真不简单。但是不管怎么样,UE基本上都帮你实现了。通过上面的描述,你现在也大体上了解了移动组件在各个方面的处理,不过遇到具体的问题也许还是无从下手,所以咱们继续往下分析。

    三.各个移动状态的细节处理

    这一节我们把焦点集中在UCharacterMovementComponent组件上,来详细的分析一下他是如何处理各种移动状态下的玩家角色的。首先肯定是从Tick开始,每帧都要进行状态的检测与处理,状态通过一个移动模式MovementMode来区分,在合适的时候修改为正确的移动模式。移动模式默认有6种,基本常用的模式有行走、游泳、下落、飞行四种,有一种给AI代理提供的行走模式,最后还有一个自定义移动模式。
    这里写图片描述
    图3-1 单机模式下的移动处理流程

    3.1 Walking

    行走模式可以说是所有移动模式的基础,也是各个移动模式里面最为复杂的一个。为了模拟出出真实世界的移动效果,玩家的脚下必须要有一个可以支撑不会掉落的物理对象,就好像地面一样。在移动组件里面,这个地面通过成员变量FFindFloorResult CurrentFloor来记录。在游戏一开始的时候,移动组件就会根据配置设置默认的MovementMode,如果是Walking,就会通过FindFloor操作来找到当前的地面,CurrentFloor的初始化堆栈如下图3-2(Character Restart()的会覆盖Pawn的Restart()):
    这里写图片描述
    图3-2

    下面先分析一下FindFloor的流程,FindFloor本质上就是通过胶囊体的Sweep检测来找到脚下的地面,所以地面必须要有物理数据,而且通道类型要设置与玩家的Pawn有Block响应。这里还有一些小的细节,比如我们在寻找地面的时候,只考虑脚下位置附近的,而忽略掉腰部附近的物体;Sweep用的是胶囊体而不是射线检测,方便处理斜面移动,计算可站立半径等(参考图3-3,HitResult里面的Normal与ImpactNormal在胶囊体Sweep检测时不一定相同)。另外,目前Character的移动是基于胶囊体实现的,所以一个不带胶囊体组件的Actor是无法正常使用UCharacterMovementComponent的。
    这里写图片描述
    图3-3

    (关于物理方面的内容可以参考我的博客 基础:UE4碰撞规则详解 UE4蓝图碰撞检测解析 进阶:UE4物理模块分析)。

    找到了地面玩家就可以站立住么?不一定。这里又涉及到一个新的概念PerchRadiusThreshold,我称他为可栖息范围半径,也就是可站立半径。默认这个值为0,移动组件会忽略这个可站立半径的相关计算,一旦这个值大于0.15,就会做进一步的判断看看当前的地面空间是否足够让玩家站立在上面。

    前面的准备工作完成了,现在正式进入Walking的位移计算,这一段代码都是在PhysWalking里面计算的。为了表现的更为平滑流畅,UE4把一个Tick的移动分成了N段处理(每段的时间不能超过MaxSimulationTimeStep)。在处理每段时,首先把当前的位置信息,地面信息记录下来。在TickComponent的时候根据玩家的按键时长,计算出当前的加速度。随后在CalcVelocity()根据加速度计算速度,同时还会考虑地面摩擦,是否在水中等情况。

    // apply input to acceleration
    Acceleration = ScaleInputAcceleration(ConstrainInputAcceleration(InputVector));

    算出速度之后,调用函数MoveAlongFloor()改变当前对象的坐标位置。在真正调用移动接口SafeMoveUpdatedComponent()前还会简单处理一种特殊的情况——玩家沿着斜面行走。正常在walking状态下,玩家只会前后左右移动,不会有Z方向的移动速度。如果遇到斜坡怎么办?如果这个斜坡可以行走,就会调用ComputeGroundMovementDelta()函数去根据当前的水平速度计算出一个新的平行与斜面的速度,这样可以简单模拟一个沿着斜面行走的效果,而且一般来说上坡的时候玩家的水平速度应该减小,通过设置bMaintainHorizontalGroundVelocity为false可以自动处理这种情况。

    现在看起来我们已经可以比较完美的模拟一个移动的流程了,不过仔细想一下还有一种情况没考虑到。那就是遇到障碍的情况怎么处理?根据我们平时游戏经验,遇到障碍肯定是移动失败,还可能沿着墙面滑动一点。UE里面确实也就是这么处理的,在角色移动的过程中(SafeMoveUpdatedComponent),会有一个碰撞检测流程。由于UPrimitiveComponent组件才拥有物理数据,所以这个操作是在函数UPrimitiveComponent::MoveComponentImpl里面处理的。下面的代码会检测移动过程中是否遇到了障碍,如果遇到了障碍会把HitResult返回。

    FComponentQueryParams Params(PrimitiveComponentStatics::MoveComponentName, Actor);
    FCollisionResponseParams ResponseParam;
    InitSweepCollisionParams(Params, ResponseParam);
    bool const bHadBlockingHit = MyWorld->ComponentSweepMulti(Hits, this, TraceStart, TraceEnd, InitialRotationQuat, Params);

    在接收到SafeMoveUpdatedComponent()返回的HitResult后,会在下面的代码里面处理碰撞障碍的情况。
    1. 如果Hit.Normal在Z方向上有值而且还可以行走,那说明这是一个可以移动上去的斜面,随后让玩家沿着斜面移动
    2. 判断当前的碰撞体是否可以踩上去,如果可以的话就试着踩上去,如果过程中发现没有踩上去,也会调用SlideAlongSurface()沿着碰撞滑动。

    // UCharacterMovementComponent::PhysWalking
    else if (Hit.IsValidBlockingHit())
    {
        // We impacted something (most likely another ramp, but possibly a barrier).
        float PercentTimeApplied = Hit.Time;
        if ((Hit.Time > 0.f) && (Hit.Normal.Z > KINDA_SMALL_NUMBER) && IsWalkable(Hit))
        {
            // Another walkable ramp.
            const float InitialPercentRemaining = 1.f - PercentTimeApplied;
            RampVector = ComputeGroundMovementDelta(Delta * InitialPercentRemaining, Hit, false);
            LastMoveTimeSlice = InitialPercentRemaining * LastMoveTimeSlice;
            SafeMoveUpdatedComponent(RampVector, UpdatedComponent->GetComponentQuat(), true, Hit);
            const float SecondHitPercent = Hit.Time * InitialPercentRemaining;
            PercentTimeApplied = FMath::Clamp(PercentTimeApplied + SecondHitPercent, 0.f, 1.f);
        }
    
        if (Hit.IsValidBlockingHit())
        {
            if (CanStepUp(Hit) || (CharacterOwner->GetMovementBase() != NULL && CharacterOwner->GetMovementBase()->GetOwner() == Hit.GetActor()))
            {
                // hit a barrier, try to step up
                const FVector GravDir(0.f, 0.f, -1.f);
                if (!StepUp(GravDir, Delta * (1.f - PercentTimeApplied), Hit, OutStepDownResult))
                {
                    UE_LOG(LogCharacterMovement, Verbose, TEXT("- StepUp (ImpactNormal %s, Normal %s"), *Hit.ImpactNormal.ToString(), *Hit.Normal.ToString());
                    HandleImpact(Hit, LastMoveTimeSlice, RampVector);
                    SlideAlongSurface(Delta, 1.f - PercentTimeApplied, Hit.Normal, Hit, true);
                }
                else
                {
                    // Don't recalculate velocity based on this height adjustment, if considering vertical adjustments.
                    UE_LOG(LogCharacterMovement, Verbose, TEXT("+ StepUp (ImpactNormal %s, Normal %s"), *Hit.ImpactNormal.ToString(), *Hit.Normal.ToString());
                    bJustTeleported |= !bMaintainHorizontalGroundVelocity;
                }
            }
            else if ( Hit.Component.IsValid() && !Hit.Component.Get()->CanCharacterStepUp(CharacterOwner) )
            {
                HandleImpact(Hit, LastMoveTimeSlice, RampVector);
                SlideAlongSurface(Delta, 1.f - PercentTimeApplied, Hit.Normal, Hit, true);
            }
        }
    }

    基本上的移动处理就完成了,移动后还会立刻判断玩家是否进入水中,或者进入Falling状态,如果是的话立刻切换到新的状态。
    由于玩家在一帧里面可能会从Walking,Swiming,Falling的等状态不断的切换,所以在每次执行移动前都会有一个iteration记录当前帧的移动次数,如果超过限制就会取消本次的移动模拟行为。

    3.2 Falling

    Falling状态也算是处理Walking以外最常见的状态,只要玩家在空中(无论是跳起还是下落),玩家都会处于Falling状态。与Walking相似,为了表现的更为平滑流畅,Falling的计算也把一个Tick的移动分成了N段处理(每段的时间不能超过MaxSimulationTimeStep)。在处理每段时,首先计算玩家通过输入控制的水平速度,因为玩家在空中也可以受到玩家控制的影响。随后,获取重力计算速度。重力的获取有点意思,你会发现他是通过Volume体积获取的,

    float UMovementComponent::GetGravityZ() const
    {
        return GetPhysicsVolume()->GetGravityZ();
    }
    APhysicsVolume* UMovementComponent::GetPhysicsVolume() const
    {
        if (UpdatedComponent)
        {
            return UpdatedComponent->GetPhysicsVolume();
        }
        return GetWorld()->GetDefaultPhysicsVolume();
    }

    Volume里面会取WorldSetting里面的GlobalGravityZ,这里给我们一个提示,我们可以通过修改代码实现不同Volume的重力不同,实现自定义的玩法。注意,即使我们没有处在任何一个体积里面,他也会给我们的UpdateComponent绑定一个默认的DefaultVolume。那为什么要有一个DefaultVolume?因为在很多逻辑处理上都需要获取DefaultVolume以及里面的相关的数据。比如,DefaultVolume有一个TerminalLimit,在通过重力计算下降速度的时候不可以超过这个设置的速度,我们可以通过修改该值来改变速度的限制。默认情况下,DefaultVolume里面的很多属性都是通过ProjectSetting里面的Physics相关配置来初始化的。参考图3-4
    这里写图片描述
    图3-4

    通过获取到的Gravity计算出当前新的FallSpeed(NewFallVelocity里面计算,计算规则很简单,就是单纯的用当前速度-Gravity*deltaTime)。随后再根据当前以及上一帧的速度计算出位移并进行移动,公式如下

    FVector Adjusted = 0.5f*(OldVelocity + Velocity) * timeTick;
    SafeMoveUpdatedComponent( Adjusted, PawnRotation, true, Hit);

    前面我们计算完速度并移动玩家后,也一样要考虑到移动碰撞问题。
    第一种情况就是正常落地,如果玩家计算后发现碰撞到一个可以站立的地形,那直接调用ProcessLanded进行落地操作(这个判断主要是根据碰撞点的高度来的,可以筛选掉墙面)。

    第二种情况就是跳的过程中遇到一个平台,然后检测玩家的坐标与当前碰撞点是否在一个可接受的范围(IsWithinEdgeTolerance),是的话就执行FindFloor重新检测一遍地面,检测到的话就执行落地流程。

    第三种情况是就是墙面等一些不可踩上去的,下落过程如果碰到障碍,首先会执行HandleImpact给碰到的对象一个力。随后调用ComputeSlideVector计算一下滑动的位移,由于碰撞到障碍后,玩家的速度会有变化,这时候重新计算一下速度,再次调整玩家的位置与方向。如果玩家这时候有水平方向上的位移,还会通过LimitAirControl来限制玩家的速度,毕竟玩家在空中是无法自由控制角色的。对第三种情况做进一步的延伸,可能会出现碰撞调整后又碰到了另一个墙面,这里Falling的处理可以让玩家在两个墙面找到一个合适的位置。但是仍然不能解决玩家被夹在两个斜面但是却无法落地的情况(或者在Waling和Falling中不断切换)。如果有时间,我们后面可以尝试解决这个问题,解决思路可以从FindFloor下的ComputeFloorDist函数入手,目的就是让这个情况下玩家可以找到一个可行走的地面。
    这里写图片描述
    图3-5 夹在缝隙导致不停的切换状态

    3.2.1 Jump

    提到Falling,不得不提跳跃这一基本操作。下面大致描述了跳跃响应的基本流程,
    1. 绑定触发响应事件

    void APrimalCharacter::SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent)
    {
        // Set up gameplay key bindings
        check(PlayerInputComponent);
        PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &ACharacter::Jump);
        PlayerInputComponent->BindAction("Jump", IE_Released, this, &ACharacter::StopJumping);
    }
    void ACharacter::Jump()
    {
        bPressedJump = true;
        JumpKeyHoldTime = 0.0f;
    }
    
    void ACharacter::StopJumping()
    {
        bPressedJump = false;
        ResetJumpState();
    }

    2.一旦按键响应立刻设置bPressedJump为true。TickComponent的帧循环调用ACharacter::CheckJumpInput来立刻检测到是否执行跳跃操作

    • ①执行CanJump()函数,处理蓝图里面的相关限制逻辑。如果蓝图里面不重写该函数,就会默认执行ACharacter::CanJumpInternal_Implementation()。这里面是控制玩家能否跳跃的依据,比如蹲伏状态不能跳跃,游泳状态不能跳跃。另外,有一个JumpMaxHoldTime表示玩家按键超过这个值后不会触发跳跃。JumpMaxCount表示玩家可以执行跳跃的段数。(比如二段跳)
    • ②执行CharacterMovement->DoJump(bClientUpdating)函数,执行跳跃操作,进入Falling,设置跳跃速度为JumpZVelocity,这个值不能小于0。
    • ③ 判断const bool bDidJump = canJump && CharacterMovement &&
      DoJump;是否为真。做一些其他的相关操作。
    const bool bDidJump = CanJump() && CharacterMovement->DoJump(bClientUpdating);
    if (!bWasJumping && bDidJump)
    {
        JumpCurrentCount++;
        OnJumped();
    }

    3.在一次PerformMovement结束后,就会执行ClearJumpInput,设置设置bPressedJump为false。但是不会清除JumpCurrentCount这样可以继续处理多段跳。

    4.玩家松开按键p也会设置bPressedJump为false,清空相关状态。如果玩家仍在空中,那也不会清除JumpCurrentCount。一旦bPressedJump为false,就不会处理任何跳跃操作了。

    5.如果玩家在空中按下跳跃键,他也会进入ACharacter::CheckJumpInput,如果JumpCurrentCount小于JumpMaxCount,玩家就可以继续执行跳跃操作了。
    这里写图片描述
    图3-6

    3.3 Swiming

    各个状态的差异本质有三个点:

    1.速度的不同
    2.受重力影响的程度
    3.惯性大小
    

    游泳状态表现上来看是一个有移动惯性(松手后不会立刻停止),受重力影响小(在水中会慢慢下落或者不动),移动速度比平时慢(表现水有阻力)的状态。而玩家是否在水中的默认检测逻辑也比较简单,就是判断当前的updateComponent所在的Volume是否是WaterVolume。(在编辑器里面拉一个PhysicsVolume,修改属性WaterVolume即可)

    CharacterMovement组件里面有浮力大小配置Buoyancy,根据玩家潜入水中的程度(ImmersionDepth返回0-1)可计算最终的浮力。随后,开始要计算速度了,这时候我们需要获取Volume里面的摩擦力Friction,然后传入CalcVelocity里面,这体现出玩家在水中移动变慢的效果。随后在Z方向通过计算浮力大小该计算该方向的速度,随着玩家潜水的程度,你会发现玩家在Z方向的速度越来越小,一旦全身都浸入了水中,在Z轴方向的重力速度就会被完全忽略。

    // UCharacterMovementComponent::PhysSwimming
    const float Friction = 0.5f * GetPhysicsVolume()->FluidFriction * Depth;
    CalcVelocity(deltaTime, Friction, true, BrakingDecelerationSwimming);
    Velocity.Z += GetGravityZ() * deltaTime * (1.f - NetBuoyancy);
    
    // UCharacterMovementComponent::CalcVelocity Apply fluid friction
    if (bFluid)
    {
        Velocity = Velocity * (1.f - FMath::Min(Friction * DeltaTime, 1.f));
    }

    这里写图片描述
    图3-7 角色在水体积中飘浮

    速度计算后,玩家就可以移动了。这里UE单独写了一个接口Swim来执行移动操作,同时他考虑到如果移动后玩家离开了水体积而且超出水面过大,他机会强制把玩家调整到水面位置,表现会更好一些。

    接下来还要什么,那大家可能也猜出来了,就是处理移动中检测到碰撞障碍的情况。基本上和之前的逻辑差不多,如果可以踩上去(StepUp())就调整玩家位置踩上去,如果踩不上去就给障碍一个力,然后顺着障碍表面滑动一段距离(HandleImpact,SlideAlongSurface)。

    那水中移动的惯性表现是怎么处理的呢?其实并不是水中做了什么特殊处理,而是计算速度时有两个传入的参数与Walking不同。一个是Friction表示摩擦力,另一个是BrakingDeceleration表示刹车的反向速度。
    在加速度为0的时候(表示玩家的输入已经被清空),水中的传入的摩擦力要远比地面摩擦里小(0.15:8),而刹车速度为0(Walking为2048),所以ApplyVelocityBraking在处理的时候在Walking表现的好像立刻刹车一样,而在Swim和fly等情况下就好像有移动惯性一样。

    // 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;
        }
    }

    3.4 Flying

    终于讲到了最后一个移动状态了,如果你想调试该状态的话,在角色的移动组件里面修改DefaultLandMovementMode为Flying即可。
    Flying和其他状态套路差不多,而且相对更简单一些,首先根据前面输入计算Acceleration,然后根据摩擦力开始计算当前的速度。速度计算后调用SafeMoveUpdatedComponent进行移动。如果碰到障碍,就先看能不能踩上去,不能的话处理碰撞,沿着障碍表面滑动。

    //UCharacterMovementComponent::PhysFlying
    //RootMotion Relative
    RestorePreAdditiveRootMotionVelocity();
    
    if( !HasAnimRootMotion() && !CurrentRootMotion.HasOverrideVelocity() )
    {
        if( bCheatFlying && Acceleration.IsZero() )
        {
            Velocity = FVector::ZeroVector;
        }
        const float Friction = 0.5f * GetPhysicsVolume()->FluidFriction;
        CalcVelocity(deltaTime, Friction, true, BrakingDecelerationFlying);
    }
    //RootMotion Relative
    ApplyRootMotionToVelocity(deltaTime);

    有一个关于Flying状态的现象大家可能会产生疑问,当我设置默认移动方式为Flying的时候,玩家可以在松开键盘后进行滑行一段距离(有惯性)。但是使用GM命令的时候,为什么就像Walking状态一样,松开按键后立刻停止?
    其实时代码对cheat Flying做了特殊处理,玩家松开按键后,加速度变为0,这时候强制设置玩家速度为0。所以使用GM的表现与实际上的不太一样。

    3.5 FScopedMovementUpdate延迟更新

    FScopedMovementUpdate并不是一种状态,而是一种优化移动方案。因为大家在查看引擎代码时,可能会看到在执行移动前会有下面这样的代码:

    // Scoped updates can improve performance of multiple MoveComponent calls.
    {
        FScopedMovementUpdate ScopedMovementUpdate(UpdatedComponent, bEnableScopedMovementUpdates ? EScopedUpdate::DeferredUpdates : EScopedUpdate::ImmediateUpdates);
    
        MaybeUpdateBasedMovement(DeltaSeconds);
    
        //......其他逻辑处理,这里不给出具体代码
    
        // Clear jump input now, to allow movement events to trigger it for next update.
        CharacterOwner->ClearJumpInput();
        // change position
        StartNewPhysics(DeltaSeconds, 0);
    
        //......其他逻辑处理,这里不给出具体代码
    
        OnMovementUpdated(DeltaSeconds, OldLocation, OldVelocity);
    } // End scoped movement update

    为什么要把移动的代码放到这个大括号里面,FScopedMovementUpdate又是什么东西?仔细回想一下我们前面具体的移动处理逻辑,在一个帧里面,我们由于移动的不合法,碰到障碍等可能会多次重置或者修改我们的移动。如果只是简单修改胶囊体的位置,其实没什么,不过实际上我们还要同时修改子组件的位置,更新物理体积,更新物理位置等等,而计算过程中的那些移动数据其实是没有用的,我们只需要最后的那个移动数据。

    因此使用FScopedMovementUpdate可以在其作用域范围内,先锁定不更新物理等对象的移动,等这次移动真正的完成后再去更新。(等到FScopedMovementUpdate析构的时候再处理)

    想进一步了解移动组件同步的相关机制,请参考 UE4移动组件详解(二)——移动同步机制
    UE4移动组件详解(三)——RootMotion与特殊移动模式的实现思路

    原文链接(转载请标明):http://blog.csdn.net/u012999985/article/details/78669508

    展开全文
  • UE4 Actor移动性以及光源移动性类型

    千次阅读 2019-07-03 10:05:18
    Actor移动性 Mobility(移动性)设置控制是否允许在游戏过程中以某种方式移动或改变Actor。 该设置主要应用于静态网格物体Actor及光源Actor。 当Mobility(移动性)属性可用时,它有三种状态 移动性状态 ...

    Actor移动性

    Mobility(移动性)设置控制是否允许在游戏过程中以某种方式移动或改变Actor。 该设置主要应用于静态网格物体Actor及光源Actor。

    当Mobility(移动性)属性可用时,它有三种状态

     

    移动性状态

    描述

    Static(静态)

     

    Static(静态) 设置是为在游戏过程中不能以任何方式移动或更新的Actor预留的。

     

    静态网格物体Actors ,如果其Mobility属性是 Static ,那么将会在预计算光照贴图(Lightmass烘焙的光照)上产生阴影。 这使得它们非常适合游戏中不需要变换位置的建筑物网格物体或装饰性网格物体。 但是,注意,它们的材质仍然可以产生动画。

    光源Actors ,如果其Mobility属性是 Static ,那么将会影响预计算光照贴图(Lightmass烘焙的光照)。 由于间接光照缓存 的存在,所以它们仍然会照亮动态物体。 对于移动设备的处理流程来说,这是理想的光照应用方法,因为从本质上讲,它不影响游戏性能。

    Stationary(固定)

     

    Stationary(固定) 设置,用于不移动但可以在游戏中以某种方式更新的 光源Actors ,比如 打开/关闭 光源,改变光源的颜色等。以这种方式设置的光源仍会影响Lightmass预计算光照贴图,同时也可以投射移动对象的动态阴影。 但是,注意,不要使用太多这样的光源影响一个给定的Actor。 请参照固定光源文档 获得更多细节。 

    Movable(可移动

    Movable(可移动) 设置仅当Actor在游戏过程中需要移动时应用。

     

    静态网格物体Actor ,如果其移动性为 Movable ,那么将不会投射预计算阴影到光照贴图中。 由于间接光照缓存 的存在,所以它们仍然会被静态光源Actor照亮。 如果固定光源或可移动光源照亮了它们,它们将投射动态阴影。 这是所有不发生变形且需要在场景中移动的网格物体元素的典型设置,比如地面、电梯等。

    光源Actor ,如果可移动性是 Movable ,那么它仅能投射动态阴影。 因此,当应用很多这样的光源投射阴影时,一定要小心,因为它们投射阴影的方法是性能消耗最大的。 但需要注意的是,由于虚幻引擎有延迟渲染系统,所以 不投射阴影 的可移动光源的性能消耗是非常低的。

     

    光源移动性类型

    静态 - 完全静态的光源在游戏运行过程中几乎没有任何性能消耗。

    固定 - 这种类型的光源,可以在运行时改变颜色和亮度,但是不能移动、旋转或修改影响范围。

    移动 - 完全动态的光源,可以在运行时改变其自身的所有属性。

    性能消耗 移动光源>固定光源>静态光源

     

    静态光源

    1.静态光源运行完全无法改变的。静态光源对可移动对象无影响,也不会产生阴影

    2.静态光源仅在光照贴图中计算一旦处理完,对性能没有进一步影响主要是生生光照贴图时的耗时,在运行时不产生消耗)。

    3.当照亮的对象也是静态的时,静态光源可以产生区域(接触)阴影。这是通过调整 光源半径 属性实现的。然而,应该注意的是,为了获得较好的阴影效果,接收柔和阴影的表面需要合理设置它们的光照贴图分辨率

    4.静态光源 光源半径 有一个额外的作用是使得它投射的阴影变得柔和光源具有较小的 光源半径投射了生硬的阴影,源具有较大的 光源半径因此投射了较为柔和的阴影

    5.光照贴图分辨率 能够用来控制静态光源(以及固定光源)产生的预烘培光着信息的细节程度(如照射在当前物品上的阴影的精细度)。在静态网格体组件上,光照贴图分辨率要么设置在静态网格体的资源上,或者通过勾选 Override Lightmap Res 选项框并设置数值较大的数值意味着较高的分辨率,但也意味着更长的构建时间和内存消耗。然而,在 BSP 表面属性上,光照贴图分辨率是由 Lightmap Resolution 属性控制,这其实是一个密度属性。因此在这里,较低的数值会产生较高分辨率

    6.静态光源仅对移动性是静态对象产生阴影

     

    固定光源

    1.固定光源是运行时保持固定位置不变的光源,可以改变光源的亮度和颜色

    2.固定光源运行时改变亮度,仅影响直接光照

    3.固定光源运行时的间接(反射)光照不会改变,是在光照系统(lightmess)中预先计算的

    4.固定光源所有间接光照和间接阴影都存储在光照贴图中。直接阴影存储在阴影贴图中。

    5.固定光源直接光照具有和可移动光源一样的高质量解析高光

    6.固定光源直接阴影

    ㈠静态阴影

    不透明表面

    lightmass在构建光照过程中为静态对象上的固定光源生成距离场阴影贴图,最多只能有 4 个重叠的固定光源具有静态阴影,因为这些光源必须被分配到阴影贴图的不同通道。这是个图形色彩问题,由于这种拓扑结构,所以通常仅允许少于4个的光源重叠。阴影不能影响这个重叠测试,所以 太阳光一般需要从它所在关卡获得一个通道,即使地下区域也如此。一旦达到通道的极限,其他固定光源将会使用全景动态阴影,这会带来很大的性能消耗。您可以使用 StationaryLightOverlap 视图模式来可视化地查看重叠效果,它会随着您修改光源而动态地更新。当某个光源无法分配到一个通道时,该光源的图标会变为红色的 X。

    半透明表面

    半透明表面能够在开销较小的情况下接受固定光源的阴影投射—— Lightmass 会根据场景静态物体预计算阴影深度贴图,这将在运行时被应用到半透明表面。这种形式的阴影是比较粗糙的,仅仅在米的度量单位上计算阴影。

    ㈡动态阴影

    动态物体(比如 StaticMeshComponent 或 SkeletalMeshComponent)必须要从距离场阴影贴图中集成环境世界的静态阴影。这是通过使用 每个对象 的阴影完成的。每个可移动的对象从固定光源创建两个动态阴影:一个用于处理静态环境世界投射到该对象上的阴影,一个处理该对象投射到环境世界中的阴影。通过使用这种设置,固定光源唯一的阴影消耗就来源于它所影响的动态对象。这意味着,根据所具有的动态对象的数量不同,该性能消耗可能很小,也可能很大。如果足够多的动态对象,那么使用可移动光源会更加高效

     

    (由可移动组件形成的 一个物体 的阴影被用于该物体的边界框上,因此边界框必须是精确的。对骨骼网格体而言,这意味着需要有对应的物理资源(Physic Asset),对粒子系统而言,固定的边界框需要足够大来包含所有的粒子才行。)

    7.定向固定光源是特殊的, 支持采用 联级阴影贴图(Cascaded Shadow Maps) 的全景阴影同时作为静态阴影。

    这在具有很多带动画的植被的关卡是非常有用的,您想在玩家周围产生可以动的阴影,但是不想付出以让很多阴影重叠来覆盖较大的视图范围这样的代价。动态阴影会随着距离而渐变为静态阴影,但这种变换通常是很难察觉到。要想设置这样的处理,仅需把 DirectionalLightStationary 的 Dynamic Shadow Distance StationaryLight 修改为您想让渐变发生的范围即可即便在定向光源上使用联级阴影贴图的时候,可移动组件仍然将会创建 PerObject 的阴影。这么做在较小的 Dynamic Shadow Distances 时比较有用,但如果设置较大的时候这么做就会产生不必要的性能开销。要禁用 PerObject 阴影来优化性能的话,可以在光源属性上禁用 Use Inset Shadows For Movable Objects。

    8.固定光源间接光照,存储在光照贴图中。在运行时,通过修改亮度和颜色来改变直接光照的做法并不适用于改变间接光照这意味着即使当一个固定光源未选中 Visible 项时,在构建光照时,它的间接光照仍会存放到光照贴图中光源属性中的 IndirectLightingIntensity 可以用于控制或禁用该光源的间接光照强度,以便当在构建光照时减小甚至彻底关闭它的间接光照。

    9.固定光源使用区域阴影。固定的定向光源提供了一个新的阴影选项,在 Lightmass 区块内,叫 Use Area Shadows for Stationary Lights

    可移动光源

    可移动光源(Movable Lights) 产生完全动态的光照和阴影,可以改变光源位置、旋转度、颜色、亮度、衰减、半径等属性,几乎光源的任何属性都可以修改。它们产生的光照不会烘焙到光照贴图中,也不会产生间接光照效果

    可移动光源使用全场景动态阴影的方式来投射阴影,具有很大的性能开销。性能消耗的程度主要取决于受到该光源影响的模型的数量,以及这些模型的三角面的数量。也就是说一个半径较大的可移动光源造成阴影的性能开销可能会几倍于一个半径较小的可移动光源。

    实时阴影光源的实时阴影具有较大的性能消耗。渲染一个有阴影的完全动态的光源所带来的性能消耗,通常渲染一个没有阴影的动态光源的性能消耗的 20 倍

    展开全文
  • Unreal Engine 4 蓝图脚本学习,根据《Blueprints Visual Scripting for Unreal Engine》学习整理

    Unreal Engine 4 蓝图脚本学习,根据《Blueprints Visual Scripting for Unreal Engine》学习整理


    默认情况下,actor的Transform组件中 移动性属性为“static”,  就是在游戏运行时,actor是不可进行移动和操作的。所以为了实现对象
    在游戏中可移动的效果需要把这一属性修改为 "可移动"




    为了能够改变脚本以后所附加到的对象同样为可移动的,需要打开蓝图编辑器进行一些修改






    为了使物体在游戏世界中能够移动,我们需要知道以下信息

    •  Where the cylinder currently is 圆柱体在什么位置
    •  What direction it is supposed to move in 想要移动的方向是什么
    •  How fast it is supposed to move in that direction 在那个方向上移动的速度是多少

    创建速度变量和方向变量

    Speed 为float类型变量

    Direction为Vector类型变量


    速度设置 为200.0f;



    移动方向设置为 y = -10.0f;





    Normalizing is a common procedure in vector math that ensures that
    the vector is converted to a length of one unit, which will make it compatible with
    the rest of our calculations

    标准化向量(单位化向量)就是将向量值转化到一个单位长度(向量的模为一)。以便在后序程序运算中相兼容



    速度值乘以一个时间值,所以选择 float*float    时间值类型也为float


    将StaticMeshComponent组件拖拽到事件图表中  以获取圆柱体当前的位置



    GetWorldDeltaSeconds: 获取相邻两帧之间的间隔时间。 这个数值受游戏运行所在的机器性能影响的,性能高的机器时间间隔就较小,反之则较大。为了能够
    在不同性能的计算机上表现出相同的移动速度,所以会把速度值乘以DeltaSeconds。



    Transform属性中包含对象的 位置、  旋转 和 缩放的信息。 能过GetWorldTransform可以获得当前对象的Transform信息

    BreakTransform 可以提取Transform中的不同部分
    MakeTransform根据Location、Rotation和Scale "合成" 一个新的Transform


    事件Tick每一帧都会触发

    SetActorTransform 根据传入的Transform值设置Actor的Transform


    这时运行游戏,圆柱体只会朝着一个方向移动。
    为了实现圆柱体在游戏中来回移动,向事件图表中添加两个新的Direction 变量 ,选择设置并分别设置y值为10.0  和 -10.0


    Delay会根据Duration设置的秒数循环执行
    FlipFlop会执行 A 和 B所连接的Node 中的一个,默认先执行A。

    播放游戏观察事件图表中数据流的走向


    圆柱体在游戏中来回移动

    现在通过之前创建的蓝图脚本可以创建更多且有相同属性的游戏对象











    展开全文
  • UE4笔记C++篇---Actor物体上下移动

    千次阅读 2018-01-14 14:54:10
    UE4笔记C++篇---Actor球物体上下移动 C++文件: // Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "GameFramework/Actor.h" #include "Ball....


    UE4笔记C++篇---Actor球物体上下移动

    C++文件:

    // Fill out your copyright notice in the Description page of Project Settings.
    
    #pragma once
    
    #include "GameFramework/Actor.h"
    #include "Ball.generated.h"
    
    UCLASS()
    class TASK_API ABall : public AActor
    {
    	GENERATED_BODY()
    	
    public:	
    	// Sets default values for this actor's properties
    	ABall();
    
    	// Called when the game starts or when spawned
    	virtual void BeginPlay() override;
    	
    	// Called every frame
    	virtual void Tick( float DeltaSeconds ) override;
    
    	float RunningTime;
    
    	void Print_T(FString InStr);
    	
    };
    

    // Fill out your copyright notice in the Description page of Project Settings.
    
    #include "Task.h"
    #include "Ball.h"
    #include "UnrealMathUtility.h"
    
    
    // Sets default values
    ABall::ABall()
    {
     	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
    	PrimaryActorTick.bCanEverTick = true;
    
    }
    
    // Called when the game starts or when spawned
    void ABall::BeginPlay()
    {
    	Super::BeginPlay();
    }
    
    // Called every frame
    void ABall::Tick( float DeltaTime )
    {
    	Super::Tick( DeltaTime );
    
    	// 获取位置
    	FVector NewLoacation = GetActorLocation();
    	// 防止浮点无限大导致不精确,每2*PI一个循环
    	if (RunningTime >= 2 * PI)
    	{
    		RunningTime = 0;
    	}
    	// 获取高度变化曲线
    	float DeltaHeight = FMath::Sin(RunningTime);
    	// DeltaTime是上一帧和下一帧之间使用的时间
    	RunningTime += DeltaTime;	
    	
    	// 20是沿着Z轴的伸缩变化值
    	NewLoacation.Z += DeltaHeight /** 20.f*/;
    	SetActorLocation(NewLoacation);
    }
    
    void ABall::Print_T(FString InStr)
    {
    	if (GEngine)
    	{
    		GEngine->AddOnScreenDebugMessage(-1, 2.f, FColor::Red, InStr);
    	}
    }
    



    展开全文
  • Unreal Engine 4 初学者教程:开始

    万次阅读 多人点赞 2018-06-14 16:11:14
    Unreal Engine 4 初学者教程:开始 原文:Unreal Engine 4 Tutorial for Beginners: Getting Started 作者:Tommy Tran 译者:kmyhy Unreal Engine 4 是一个游戏开发工具集,能够开发从 2D 手机游戏到 3A ...
  • 第一部分从移动相关架构以及单机情况下移动的处理细节讲起 UE4移动组件详解(一)——移动框架与实现原理 而第二部分是关于移动组件同步解决方案的描述,里面有诸多细节来让移动的同步表现的更为流畅。关于移动网络...
  • unreal中为pawn添加移动报错,蓝图,报错信息如下 原因是少了一个组件 添加上该组件可正常移动
  • Unreal Engine 3到Unreal Engine 4

    千次阅读 2015-07-02 19:38:32
    介绍Unreal Engine 4与Unreal Engine 3的一些不同点。
  • 一个Actor可以是放在场景中的任何物体。Actor是泛型的类,它支持3D变换,比如平移,旋转和缩放。Actor可以通过游戏代码或者蓝图进行创建或者销毁。在C++中,AActor是所有Actor的基类。 这里有几种不同类型的Actors,...
  • 一、场景准备, 1.更改游戏模式,创建游戏模式更改并选择,更改default pawn class 为none,用于输入鼠标事件,将floor地板调整到视角可见位置, 2.创建球体,通过 模式栏中的Geometry下的Sphere进行创建 ...
  • 每个Actor,为其定义一个代理(ActorProxy),真实的Actor放在服务端,代理ActorProxy放在客户端,移动Actor时,实际是移动服务端上的Actor,然后对客户端ActorProxy的位置进行同步。 摄像机绑定的是ActorProxy,...
  • UE4 鼠标控制actor旋转

    千次阅读 2018-05-04 16:56:37
    物体Tick事件转动,初始不动则Rotate 都为0,关卡蓝图里边鼠标左键事件,鼠标点击和释放的位置做减法 并设置目标的Rotate属性,Clamp做速度控制.不知道其他有没有好的办法,项目需要没找到相关案例 临时写的...
  • UE4中,一个PlayerController同时只能控制一个Actor,如果要实现RTS游戏中的那种多个Actor同时移动,需要Spawn出来多个Controller(PlayerController或者AIController等,具体看你需求),每个Controller去Possess一...
  • Unreal3 碰撞参考指南

    千次阅读 2013-10-22 20:48:33
    碰撞参考指南 ...概述创建骨架网格物体碰撞外壳创建静态网格物体碰撞外壳 ... K-DOP 工具创建碰撞外壳使用球体简化碰撞创建碰撞外壳使用自动凸面碰撞...玩家移动和武器开火 在游戏中检查碰撞 概述
  • 最后效果如下,Floor细节面板中对生成的Wall数量和位置进行设置 本章我们将实现在构造脚本程序化生成若干个StaticMesh并对其Transform...融合Actor实验 Component总结 场景制作 Wall升级——WallType Wall升...
  • UE4-控制相机移动

    2020-05-20 12:06:02
    项目中有时候会使用actor类型或者Pawn类型蓝图用来单纯控制相机移动旋转操作。(测试为Pawn类型蓝图,添加camera组件) 一、首先要在项目设置中编辑好输入轴映射(或者在蓝图中使用WSAD键分别控制设置向前,向右...
  • 本节书摘来异步社区《Unreal Engine 4蓝图可视化编程》一书中的第1章,第1.5节,作者: Brenden Sewell 译者: 陈东林 责编: 胡俊英,更多章节内容可以访问云栖社区“异步社区”公众号查看。 1.5 制作移动标靶 ...
  • 移动物体,一般是调用UE4内建的寻路API:UNavigationSystem::SimpleMoveToLocation()。 但是如果项目的逻辑比较特殊,想绕开UE4自带的寻路系统的话,一般可以调用:YourActor-&gt;GetOwner()-&gt;...
  • Actor

    2015-12-30 11:01:19
    Actor On this page: 概述Actor 创建组件Ticking(更新)复制 概述 Actor是一种基本类型的对象,您可以将它放到世界中或者可以在世界中生成 它。关于Actor的一个很好的简介是“可以...
1 2 3 4 5 ... 20
收藏数 595
精华内容 238
关键字:

unreal 让actor移动