iT邦幫忙

2025 iThome 鐵人賽

DAY 15
0

有GIF需載入

現在使用的攻擊判定是跟著 Frame Rate 生成的,所以如果幀數突然變低,就會直接穿過去打不到敵人。

所以今天要製作的進階判定是,提取 前一幀目前幀 的 Socket,用 中間幀 來做判斷。

可以看到,即使只有 20 FPS 也還是可以打到敵人。
command(~) t.MaxFPS 20 設置最高 FPS 為 20

1. Code

iThomeAnimNotifyState_DoAttackTrace.h
(這是 Day 11|為攻擊增加判定 的東西)

protected:
    // If Trace Amount = -1, Use Sphere Trace.
    UPROPERTY(EditAnywhere, Category="Attack")
    int TraceAmount=-1;

    UPROPERTY(EditAnywhere, Category="Attack")
    float TraceThickness=5.f;
public:
    virtual void NotifyBegin(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, float TotalDuration, const FAnimNotifyEventReference& EventReference) override;

iThomeAnimNotifyState_DoAttackTrace.cpp

void UiThomeAnimNotifyState_DoAttackTrace::NotifyTick(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, float FrameDeltaTime, const FAnimNotifyEventReference& EventReference)
{
    if (AiThome30daysCharacter* Character = Cast<AiThome30daysCharacter>(MeshComp->GetOwner()))
    {
        // 加了 TraceThickness, TraceAmount
        Character->DoAttackTrace(TraceSocketStart, TraceSocketEnd, Radius, TraceThickness, TraceAmount, CollideObjectType);
    }
}

void UiThomeAnimNotifyState_DoAttackTrace::NotifyBegin(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, float TotalDuration, const FAnimNotifyEventReference& EventReference)
{
    if (AiThome30daysCharacter* Character = Cast<AiThome30daysCharacter>(MeshComp->GetOwner()))
    {
        Character->DoAttackStart(TraceSocketStart, TraceSocketEnd);
    }
}

iThome30daysCharacter.h

public:
    //! Debug
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Debug")
    bool Debug=true;
    FVector LastStartSocketPos={0,0,0};
    FVector LastEndSocketPos={0,0,0};

iThome30daysCharacter.cpp

  • 設定初始 Socket Location
void AiThome30daysCharacter::DoAttackStart(FName TraceSocketStart, FName TraceSocketEnd)
{
    if (CurrentWeapon)
    {
        LastStartSocketPos = CurrentWeapon->WeaponMesh->GetSocketLocation(TraceSocketStart);
        LastEndSocketPos = CurrentWeapon->WeaponMesh->GetSocketLocation(TraceSocketEnd);
    }else
    {
        LastStartSocketPos = GetMesh()->GetSocketLocation(TraceSocketStart);
        LastEndSocketPos = (TraceSocketEnd!=NAME_None) ? GetMesh()->GetSocketLocation(TraceSocketEnd) : LastStartSocketPos;
    }
}
  • 根據 TraceAmount 進行 For 迴圈沿著 Start Socket 到 End Socket 做 Collision 判定。
    • TraceAmount = 2,就會是頭尾 Socket 中間再加2個判斷端點。
  • 前一幀目前幀 Socket 之間的寬度作為 Collision 的寬度
  • 頭尾 Socket 距離 ÷ TraceAmount 作為 長度
  • 厚度為自己設的 Thickness
void AiThome30daysCharacter::DoAttackTrace(FName TraceSocketStart, FName TraceSocketEnd, float Radius, float Thickness, int TraceAmount, TArray<TEnumAsByte<EObjectTypeQuery>> CollideObjectType)
{
    TArray<AActor*> IgnoreActors;
    IgnoreActors.Add(this);
    FCollisionQueryParams QueryParams;
    QueryParams.AddIgnoredActor(this);
    
    FHitResult HitResult;
    bool bHit=false;
    if (TraceAmount>=0) for (int i=0; i<TraceAmount+2; i++)
    {
        float Alpha = (float)i / (TraceAmount+2 - 1);
        FVector hStart = FMath::Lerp(LastStartSocketPos, LastEndSocketPos, Alpha);
        FVector hEnd   = FMath::Lerp(Start, End, Alpha);
        if (hStart==hEnd) continue;
        
        float Length = (hStart-hEnd).Size()*0.5f;
        if (Length < KINDA_SMALL_NUMBER) Length = 0.1f;
        float Size = (Start-End).Size()*0.5f/(TraceAmount+2);
        if (Size < KINDA_SMALL_NUMBER) Size = 0.1f;
        
        bHit = GetWorld()->SweepSingleByObjectType(
            HitResult,
            hStart,
            hEnd,
            FQuat::Identity,
            CollideObjectType,
            FCollisionShape::MakeBox(FVector3f(Length,Size,Thickness)),
            QueryParams
        );

        if (Debug)
        {
            FVector Center = (hStart + hEnd) * 0.5f;

            // 計算旋轉
            FVector SweepDir = (hEnd - hStart);
            SweepDir.Normalize();

            // Up 向量可以沿劍面方向或使用 World Up
            FVector Up = FVector::UpVector;
            Up = (Up - SweepDir * FVector::DotProduct(Up, SweepDir)).GetSafeNormal();
            FQuat Rotation = FRotationMatrix::MakeFromXZ(SweepDir, Up).ToQuat();
        
            DrawDebugBox(GetWorld(), Center, FVector(Length, Size, Thickness), Rotation, FColor::Red, false, 2.f, 0, 1.f);
        }
        if (bHit)
        {
            if (Debug) DrawDebugSphere(
                GetWorld(),
                HitResult.ImpactPoint,
                10.0f,
                12,
                FColor::Green,
                false,
                2.0f // 顯示秒數
            );
            break;
        }
    }
    
    LastStartSocketPos = Start;
    LastEndSocketPos = End;
  • 如果 TraceAmount 為 -1,則使用原本的 Shpere Trace,用在拳頭這類不須精準判斷也能擊中的情況。
if (TraceAmount==-1)
    {
        bHit = UKismetSystemLibrary::SphereTraceSingleForObjects(
            GetWorld(),
            Start,
            End,
            Radius,
            CollideObjectType,
            false,
            IgnoreActors,
            Debug ? EDrawDebugTrace::ForDuration : EDrawDebugTrace::None,
            HitResult,
            true,
            FColor::Red,
            FColor::Green,
            2.f
        );
    }

2. Editor 使用

  • 在 Animation Montage 裡的 Notify State 設定你要的數值。
    https://ithelp.ithome.com.tw/upload/images/20250929/20171036Xk29NGCYBo.png

就完成了 :D


上一篇
# Day 14|傷害(血量)系統
系列文
30 天用 Unreal Engine 5 C++ 開發遊戲15
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言