攀爬
本篇基于 GitHub - aizawaayame/OnaGameSample: Rewrite the ALS project using C++. 项目。
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);
MantleUpdate
到 TimelineUpdate
事件,绑定 MantleEnd
到 TimelineFinished
事件。 绑定 MantleTimeline
的 MantleTimelineCurve
曲线事件为 TimelineUpdated
。绑定 MantleTimeline
的结束事件为 TimelineFinished
。
绑定 OwnerCharacter
的 JumpPressedDelegate
事件到 OnOwnerJumpInput
方法。
攀爬检测(Mantle Check)¶
检测是否符合攀爬条件。
核心思路是进行射线检测和碰撞检测,判断是否符合攀爬条件。
如果是,则关闭 Component
的 Tick
事件,并调用 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。
如果落脚点 !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
(LowMantle
,HighMantle
,FallingCatch
)。
传递 HitComponnet
。
传递 TargetTransform
,这个参数是根据,ImpactPoint
来动态构造的,注意这个 Transform 的 Rotator 的 \((X,Y)\) 和 ImpactNormal
的方向是相反的。这样就能指向 ImpactPoint
方向。
攀爬开始(Mantle Start)¶
该函数核心作用有两个
禁用组件Tick¶
组件每帧Tick会去进行MantleCheck,在开始攀爬后,暂时禁用Tick。
播放蒙太奇¶
完成了和动画匹配的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
。