Skip to content

攀爬

本篇基于 GitHub - aizawaayame/OnaGameSample: Rewrite the ALS project using C++. 项目。

具体为 OnaGameSample/Source/Ona/Private/CharacterLogic/CharacterMovement/OnaMantleComponent.cpp at master · aizawaayame/OnaGameSample · GitHub 部分

flowchart TD
    A["非攀爬状态"] --> |"玩家按下起跳键或角色处于下落状态时"| B["攀爬检测(Mantle Check)"]
    B --> C{"是否符合攀爬条件"}
    C -->|否| D["执行起跳或继续下落"]
    C -->|是| E["开始攀爬(Mantle Start)"]
    E --> F{"攀爬是否结束"}
    F -->|否| G["攀爬更新(Mantle Update)"]
    G --> F
    F -->|是| H["结束攀爬(Mantle End)"]
    H --> A

    style B fill:#00BFFF,stroke:#000,stroke-width:2px
    style E fill:#00BFFF,stroke:#000,stroke-width:2px
    style G fill:#00BFFF,stroke:#000,stroke-width:2px
    style H fill:#00BFFF,stroke:#000,stroke-width:2px

绑定和驱动

PrimaryComponentTick.bCanEverTick = true;  
PrimaryComponentTick.bStartWithTickEnabled = true;

MantleTimeline = CreateDefaultSubobject<UTimelineComponent>(NAME_MantleTimeline);
创建 UTimelineComponent 对象以供后续驱动使用。

FOnTimelineFloat TimelineUpdated;  
FOnTimelineEvent TimelineFinished;

TimelineUpdated.BindUFunction(this, NAME_MantleUpdate);
TimelineFinished.BindUFunction(this, NAME_MantleEnd);

MantleTimeline->SetTimelineFinishedFunc(TimelineFinished);
MantleTimeline->SetLooping(false);
MantleTimeline->SetTimelineLengthMode(TL_TimelineLength);
MantleTimeline->AddInterpFloat(MantleTimelineCurve, TimelineUpdated);

OwnerCharacter->JumpPressedDelegate.AddUniqueDynamic(this,
    &UOnaMantleComponent::OnOwnerJumpInput);
绑定 MantleUpdateTimelineUpdate 事件,绑定 MantleEndTimelineFinished 事件。

绑定 MantleTimelineMantleTimelineCurve 曲线事件为 TimelineUpdated。绑定 MantleTimeline 的结束事件为 TimelineFinished

绑定 OwnerCharacterJumpPressedDelegate 事件到 OnOwnerJumpInput 方法。

if (OwnerCharacter && OwnerCharacter->GetMovementState() == EOnaMovementState::InAir)  
{  
    if (OwnerCharacter->HasMovementInput())  
    {  
        MantleCheck(FallingTraceSettings, EDrawDebugTrace::Type::ForOneFrame);  
    }  
}  

Tick MantleCheck

攀爬检测(Mantle Check)

检测是否符合攀爬条件。

核心思路是进行射线检测和碰撞检测,判断是否符合攀爬条件。
如果是,则关闭 ComponentTick 事件,并调用 MantleStart 方法。

从Pawn位置向前碰撞检测

向前打一个胶囊体来判断前面是否有可攀爬的障碍物。

const FVector& TraceDirection = OwnerCharacter->GetActorForwardVector();  
// GetCapsuleBaseLocation 获取 Capsule 底部位置
const FVector& CapsuleBaseLocation = 
    UOnaMathLibrary::GetCapsuleBaseLocation(
        2.f, 
        OwnerCharacter->GetCapsuleComponent());  

FVector TraceStart = CapsuleBaseLocation + TraceDirection * -30.f;  
TraceStart.Z += (TraceSettings.MaxLedgeHeight + TraceSettings.MinLedgeHeight) / 2.f;  

FVector TraceEnd = TraceStart + TraceDirection * TraceSettings.ReachDistance;  
float HalfHeight = 1.f + (TraceSettings.MaxLedgeHeight - TraceSettings.MinLedgeHeight) / 2.f;

FHitResult HitResult;  
{  
    const FCollisionShape CapsuleCollisionShape = 
        FCollisionShape::MakeCapsule(
            TraceSettings.ForwardTraceRadius, 
            HalfHeight);  

    const bool bHit = World->SweepSingleByProfile(
        HitResult, 
        TraceStart, 
        TraceEnd, 
        FQuat::Identity,
        MantleObjectDetectionProfile, 
        CapsuleCollisionShape, 
        Parameters);
}
Tracesetting

TraceSetting在外部定义,是根据动画来定义的。本项目只有高攀爬和低攀爬两组动画。

MinLedgeHeight,最低可攀爬障碍物的高度,低于这个高度就直接走过去,而不触发攀爬。

MaxLedegeHeight,最高可攀爬动画,高于这个高度,没有合适的动画进行匹配。

ForwardTraceRadius,根据当前角色的胶囊体设计的前向碰撞检测的胶囊体的半径。

Capsule: 碰撞检测
Raduis: 由外部配置 TraceSetting.FrowarTraceRadius 决定。
Height: 高度就是整个可攀爬范围高度。
结合后面的TraceStart.Center的位置这个 Capsule 刚好覆盖整个可攀爬的范围。

TraceStart: Center放在可检测范围的中点,起始点有个-30的常量,相比当前角色胶囊体略微靠后。这样能防止角色靠近障碍物碰撞检测打出时 Penetrate 导致不正常触发攀爬。

TraceEnd: TraceStart+ TraceDirection * TraceSettings.ReachDistance。ReachDistance由动画的前倾动作来决定。

if (!HitResult.IsValidBlockingHit() || OwnerCharacter->GetCharacterMovement()->IsWalkable(HitResult))  
    return false;  

if (HitResult.GetComponent() != nullptr)  
{  
    UPrimitiveComponent* PrimitiveComponent = HitResult.GetComponent();  
    if (PrimitiveComponent && PrimitiveComponent->GetComponentVelocity().Size() > AcceptableVelocityWhileMantling)  
    {      
         return false;  
    }
}

未检测到障碍物退出。

障碍物 Iswalkable 退出

障碍物移动速度过快退出

从落脚点上方向下碰撞检测

检测落脚点是否能继续行走。

const FVector InitialTraceImpactPoint = HitResult.ImpactPoint;  
const FVector InitialTraceNormal = HitResult.ImpactNormal;  

FVector DownwardTraceEnd = InitialTraceImpactPoint;  
DownwardTraceEnd.Z = CapsuleBaseLocation.Z;  
DownwardTraceEnd += InitialTraceNormal * -15.0f;  

FVector DownwardTraceStart = DownwardTraceEnd;  
DownwardTraceStart.Z += TraceSettings.MaxLedgeHeight + TraceSettings.DownwardTraceRadius + 1.0f;  

{  
    const FCollisionShape SphereCollisionShape = 
        FCollisionShape::MakeSphere(TraceSettings.DownwardTraceRadius);  
    const bool bHit = 
        World->SweepSingleByChannel(
            HitResult, 
            DownwardTraceStart, 
            DownwardTraceEnd, 
            FQuat::Identity, 
            WalkableSurfaceDetectionChannel, 
            SphereCollisionShape);
}
TraceSetting.DownwardTraceRadius

和其他TraceSetting参数一样,在外部配置,该参数由角色的实际大小来决定。

Sphere: 碰撞检测
Raduis: 由外部配置 TraceSetting.DownwardTraceRadius 决定。

TraceStart\((x,y)\) 的位置根据第一次碰撞结果决定。\(Z\) 先算出 TraceEnd,再加上 TraceSettings.MaxLedgeHeight + TraceSettings.DownwardTraceRadius + 1

TraceEnd:取当前的角色胶囊体底部位置,并添加一个 InitialTraceNormal * -15 的偏移。15这个常量来源是源攀爬动画的根位移,执行一个攀爬动作,该动画在 \(Y\) 方向上会+15。

if (!OwnerCharacter->GetCharacterMovement()->IsWalkable(HitResult))  
{  
    return false;  
}

如果落脚点 !IsWalkable 则退出。

检测落脚点是否有足够空间

逻辑比较简单,就是在落脚点处构造一个 Capsule 进行碰撞检测。

const FVector& CapsuleLocationFromBase = 
    UOnaMathLibrary::GetCapsuleLocationFromBase(
        DownTraceLocation, 
        2.f, 
        OwnerCharacter->GetCapsuleComponent());  

const bool bCapsuleHasRoom = 
    UOnaMathLibrary::CapsuleHasRoomCheck(
        OwnerCharacter->GetCapsuleComponent(), 
        CapsuleLocationFromBase, 
        0.f, 
        0.f, 
        DebugType, 
        OnaCharacterDebugComponent && OnaCharacterDebugComponent->GetShowTraces());  

if (!bCapsuleHasRoom)  
{  
    return false;  
}

触发MantleStart方法

触发 MantleStart,开始进行攀爬。

const FTransform TargetTransform(
    (InitialTraceNormal * FVector(-1, -1, 0)).ToOrientationRotator(),  
    CapsuleLocationFromBase,    
    FVector::OneVector);

const float MantleHeight = (CapsuleLocationFromBase - OwnerCharacter->GetActorLocation()).Z;  
EOnaMantleType MantleType;  
if (OwnerCharacter->GetMovementState() == EOnaMovementState::InAir)  
{  
    MantleType = EOnaMantleType::FallingCatch;  
}  
else  
{  
    MantleType = MantleHeight > 125.0f ? EOnaMantleType::HighMantle : EOnaMantleType::LowMantle;  
}

FOnaComponentAndTransform MantleWS;  
MantleWS.Component = HitComponent;  
MantleWS.Transform = TargetTransform;  
MantleStart(MantleHeight, MantleWS, MantleType);

根据当前状态和攀爬高度决定 MantleType (LowMantleHighMantleFallingCatch)。

传递 HitComponnet

传递 TargetTransform,这个参数是根据,ImpactPoint 来动态构造的,注意这个 Transform 的 Rotator 的 \((X,Y)\)ImpactNormal 的方向是相反的。这样就能指向 ImpactPoint 方向。

攀爬开始(Mantle Start)

该函数核心作用有两个

禁用组件Tick

组件每帧Tick会去进行MantleCheck,在开始攀爬后,暂时禁用Tick。

SetComponentTickEnabledAsync(false);

播放蒙太奇

完成了和动画匹配的Timeline逻辑驱动,执行蒙太奇动画播放。

if (MantleParams.AnimMontage && OwnerCharacter->GetMesh()->GetAnimInstance())  
{  
    OwnerCharacter->GetMesh()->GetAnimInstance()->Montage_Play(
                MantleParams.AnimMontage, 
                MantleParams.PlayRate,  
                EMontagePlayReturnType::MontageLength,  
                MantleParams.StartingPosition, 
                false);  
}

攀爬更新(Mantle Update)

MantleUpdate 实际上是由 MantleTimeline 进行驱动的。

核心逻辑是 执行和动画匹配的位移和旋转

攀爬结束(Mantle End)

MantleUpdate 实际上是由 MantleTimeline 进行驱动的。

在 Timeline 播放结束后,会触发该事件。

核心逻辑是重新打开组件的Tick,以使能进行 MantleCheck

SetComponentTickEnabledAsync(true);