// Fill out your copyright notice in the Description page of Project Settings. #include "LagCompensationComponent.h" #include "Blaster/Blaster.h" #include "Blaster/Weapon/Weapon.h" #include "Components/BoxComponent.h" #include "Kismet/GameplayStatics.h" ULagCompensationComponent::ULagCompensationComponent() { PrimaryComponentTick.bCanEverTick = true; } FFramePackage ULagCompensationComponent::InterpBetweenFrames(const FFramePackage& OlderFrame, const FFramePackage& YoungerFrame, float HitTime) { const float Distance = YoungerFrame.Time - OlderFrame.Time; const float InterpFraction = FMath::Clamp((HitTime - OlderFrame.Time) / Distance, 0.f, 1.f); FFramePackage InterpFramePackage; InterpFramePackage.Time = HitTime; for (auto& YoungerPair : YoungerFrame.HitBoxInfo) { const FName& BoxInfoName = YoungerPair.Key; const FBoxInformation& OlderBox = OlderFrame.HitBoxInfo[BoxInfoName]; const FBoxInformation& YoungerBox = YoungerFrame.HitBoxInfo[BoxInfoName]; FBoxInformation InterpBoxInfo; InterpBoxInfo.Location = FMath::VInterpTo(OlderBox.Location, YoungerBox.Location, 1.f, InterpFraction); InterpBoxInfo.Rotation = FMath::RInterpTo(OlderBox.Rotation, YoungerBox.Rotation, 1.f, InterpFraction); InterpBoxInfo.BoxExtent = YoungerBox.BoxExtent; InterpFramePackage.HitBoxInfo.Add(BoxInfoName, InterpBoxInfo); } return InterpFramePackage; } FServerSideRewindResult ULagCompensationComponent::ConfirmHit(const FFramePackage& Package, ABlasterCharacter* HitCharacter, const FVector_NetQuantize& TraceStart, const FVector_NetQuantize HitLocation) { if (HitCharacter == nullptr) return FServerSideRewindResult(); FFramePackage CurrentFrame; CacheBoxPositions(HitCharacter, CurrentFrame); MoveBoxes(HitCharacter, Package); EnableCharacterMeshCollision(HitCharacter, ECollisionEnabled::NoCollision); // Enable collision for the head first UBoxComponent* HeadBox = HitCharacter->HitCollisionBoxes[FName("head")]; HeadBox->SetCollisionEnabled(ECollisionEnabled::QueryOnly); HeadBox->SetCollisionResponseToChannel(ECC_HitBox, ECR_Block); const FVector TraceEnd = TraceStart + (HitLocation - TraceStart) * 1.25f; if (UWorld* World = GetWorld()) { FHitResult ConfirmHitResult; World->LineTraceSingleByChannel( ConfirmHitResult, TraceStart, TraceEnd, ECC_HitBox ); if (ConfirmHitResult.bBlockingHit) // we hit the head, return early { ResetHitBoxes(HitCharacter, CurrentFrame); EnableCharacterMeshCollision(HitCharacter, ECollisionEnabled::QueryAndPhysics); return FServerSideRewindResult{ true, true }; } // Didn't hit the head, check the rest of the boxes for (auto& HitBoxPair : HitCharacter->HitCollisionBoxes) { if (HitBoxPair.Value != nullptr) { HitBoxPair.Value->SetCollisionEnabled(ECollisionEnabled::QueryOnly); HitBoxPair.Value->SetCollisionResponseToChannel(ECC_HitBox, ECR_Block); } } World->LineTraceSingleByChannel( ConfirmHitResult, TraceStart, TraceEnd, ECC_HitBox ); if (ConfirmHitResult.bBlockingHit) { ResetHitBoxes(HitCharacter, CurrentFrame); EnableCharacterMeshCollision(HitCharacter, ECollisionEnabled::QueryAndPhysics); return FServerSideRewindResult{ true, false }; } } ResetHitBoxes(HitCharacter, CurrentFrame); EnableCharacterMeshCollision(HitCharacter, ECollisionEnabled::QueryAndPhysics); return FServerSideRewindResult{ false, false }; } FServerSideRewindResult ULagCompensationComponent::ProjectileConfirmHit(const FFramePackage& Package, ABlasterCharacter* HitCharacter, const FVector_NetQuantize& TraceStart, const FVector_NetQuantize100& InitialVelocity, float HitTime) { FFramePackage CurrentFrame; CacheBoxPositions(HitCharacter, CurrentFrame); MoveBoxes(HitCharacter, Package); EnableCharacterMeshCollision(HitCharacter, ECollisionEnabled::NoCollision); // Enable collision for the head first UBoxComponent* HeadBox = HitCharacter->HitCollisionBoxes[FName("head")]; HeadBox->SetCollisionEnabled(ECollisionEnabled::QueryOnly); HeadBox->SetCollisionResponseToChannel(ECC_HitBox, ECR_Block); FPredictProjectilePathParams PathParams; PathParams.bTraceWithCollision = true; PathParams.MaxSimTime = MaxRecordTime; PathParams.LaunchVelocity = InitialVelocity; PathParams.StartLocation = TraceStart; PathParams.SimFrequency = 15.f; PathParams.ProjectileRadius = 5.f; PathParams.TraceChannel = ECC_HitBox; PathParams.ActorsToIgnore.Add(GetOwner()); FPredictProjectilePathResult PathResult; UGameplayStatics::PredictProjectilePath(this, PathParams, PathResult); if (PathResult.HitResult.bBlockingHit) // we hit the head, return early { ResetHitBoxes(HitCharacter, CurrentFrame); EnableCharacterMeshCollision(HitCharacter, ECollisionEnabled::QueryAndPhysics); return FServerSideRewindResult{ true, true }; } // we didn't hit the head; check the rest of the boxes for (auto& HitBoxPair : HitCharacter->HitCollisionBoxes) { if (HitBoxPair.Value != nullptr) { HitBoxPair.Value->SetCollisionEnabled(ECollisionEnabled::QueryOnly); HitBoxPair.Value->SetCollisionResponseToChannel(ECC_HitBox, ECR_Block); } } UGameplayStatics::PredictProjectilePath(this, PathParams, PathResult); if (PathResult.HitResult.bBlockingHit) { ResetHitBoxes(HitCharacter, CurrentFrame); EnableCharacterMeshCollision(HitCharacter, ECollisionEnabled::QueryAndPhysics); return FServerSideRewindResult{ true, false }; } ResetHitBoxes(HitCharacter, CurrentFrame); EnableCharacterMeshCollision(HitCharacter, ECollisionEnabled::QueryAndPhysics); return FServerSideRewindResult{ false, false }; } FShotgunServerSideRewindResult ULagCompensationComponent::ShotgunConfirmHit(const TArray& FramePackages, const FVector_NetQuantize& TraceStart, const TArray& HitLocations) { for (auto& Frame : FramePackages) { if (Frame.Character == nullptr) return FShotgunServerSideRewindResult(); } FShotgunServerSideRewindResult ShotgunResult; TArray CurrentFrames; for (auto& Frame : FramePackages) { FFramePackage CurrentFrame; CurrentFrame.Character = Frame.Character; CacheBoxPositions(Frame.Character, CurrentFrame); MoveBoxes(Frame.Character, Frame); EnableCharacterMeshCollision(Frame.Character, ECollisionEnabled::NoCollision); CurrentFrames.Add(CurrentFrame); } for (auto& Frame : FramePackages) { // Enable collision for the head first UBoxComponent* HeadBox = Frame.Character->HitCollisionBoxes[FName("head")]; HeadBox->SetCollisionEnabled(ECollisionEnabled::QueryOnly); HeadBox->SetCollisionResponseToChannel(ECC_HitBox, ECR_Block); } UWorld* World = GetWorld(); // Check for headshots for (auto& HitLocation : HitLocations) { const FVector TraceEnd = TraceStart + (HitLocation - TraceStart) * 1.25f; if (World) { FHitResult ConfirmHitResult; World->LineTraceSingleByChannel( ConfirmHitResult, TraceStart, TraceEnd, ECC_HitBox ); ABlasterCharacter* BlasterCharacter = Cast(ConfirmHitResult.GetActor()); if (BlasterCharacter) { if (ShotgunResult.Headshots.Contains(BlasterCharacter)) { ShotgunResult.Headshots[BlasterCharacter]++; } else { ShotgunResult.Headshots.Emplace(BlasterCharacter, 1); } } } } // Enable collision for all boxes, then disable for head box for (auto& Frame : FramePackages) { for (auto& HitBoxPair : Frame.Character->HitCollisionBoxes) { if (HitBoxPair.Value != nullptr) { HitBoxPair.Value->SetCollisionEnabled(ECollisionEnabled::QueryOnly); HitBoxPair.Value->SetCollisionResponseToChannel(ECC_HitBox, ECR_Block); } } UBoxComponent* HeadBox = Frame.Character->HitCollisionBoxes[FName("head")]; HeadBox->SetCollisionEnabled(ECollisionEnabled::NoCollision); } // Check for body shots for (auto& HitLocation : HitLocations) { const FVector TraceEnd = TraceStart + (HitLocation - TraceStart) * 1.25f; if (World) { FHitResult ConfirmHitResult; World->LineTraceSingleByChannel( ConfirmHitResult, TraceStart, TraceEnd, ECC_HitBox ); ABlasterCharacter* BlasterCharacter = Cast(ConfirmHitResult.GetActor()); if (BlasterCharacter) { if (ShotgunResult.BodyShots.Contains(BlasterCharacter)) { ShotgunResult.BodyShots[BlasterCharacter]++; } else { ShotgunResult.BodyShots.Emplace(BlasterCharacter, 1); } } } } for (auto& CurrentFrame : CurrentFrames) { ResetHitBoxes(CurrentFrame.Character, CurrentFrame); EnableCharacterMeshCollision(CurrentFrame.Character, ECollisionEnabled::QueryAndPhysics); } return ShotgunResult; } void ULagCompensationComponent::CacheBoxPositions(ABlasterCharacter* HitCharacter, FFramePackage& OutFramePackage) { if (HitCharacter == nullptr) return; for (auto& HitBoxPair : HitCharacter->HitCollisionBoxes) { if (HitBoxPair.Value != nullptr) { FBoxInformation BoxInfo; BoxInfo.Location = HitBoxPair.Value->GetComponentLocation(); BoxInfo.Rotation = HitBoxPair.Value->GetComponentRotation(); BoxInfo.BoxExtent = HitBoxPair.Value->GetScaledBoxExtent(); OutFramePackage.HitBoxInfo.Add(HitBoxPair.Key, BoxInfo); } } } void ULagCompensationComponent::MoveBoxes(ABlasterCharacter* HitCharacter, const FFramePackage& Package) { if (HitCharacter == nullptr) return; for (auto& HitBoxPair : HitCharacter->HitCollisionBoxes) { if (HitBoxPair.Value != nullptr) { HitBoxPair.Value->SetWorldLocation(Package.HitBoxInfo[HitBoxPair.Key].Location); HitBoxPair.Value->SetWorldRotation(Package.HitBoxInfo[HitBoxPair.Key].Rotation); HitBoxPair.Value->SetBoxExtent(Package.HitBoxInfo[HitBoxPair.Key].BoxExtent); } } } void ULagCompensationComponent::ResetHitBoxes(ABlasterCharacter* HitCharacter, const FFramePackage& Package) { if (HitCharacter == nullptr) return; for (auto& HitBoxPair : HitCharacter->HitCollisionBoxes) { if (HitBoxPair.Value != nullptr) { HitBoxPair.Value->SetWorldLocation(Package.HitBoxInfo[HitBoxPair.Key].Location); HitBoxPair.Value->SetWorldRotation(Package.HitBoxInfo[HitBoxPair.Key].Rotation); HitBoxPair.Value->SetBoxExtent(Package.HitBoxInfo[HitBoxPair.Key].BoxExtent); HitBoxPair.Value->SetCollisionEnabled(ECollisionEnabled::NoCollision); } } } void ULagCompensationComponent::ShowFramePackage(const FFramePackage& Package, const FColor Color) { for (auto& BoxInfo : Package.HitBoxInfo) { DrawDebugBox( GetWorld(), BoxInfo.Value.Location, BoxInfo.Value.BoxExtent, FQuat(BoxInfo.Value.Rotation), Color, false, 4.f ); } } FServerSideRewindResult ULagCompensationComponent::ServerSideRewind(ABlasterCharacter* HitCharacter, const FVector_NetQuantize& TraceStart, const FVector_NetQuantize& HitLocation, float HitTime) { const FFramePackage FrameToCheck = GetFrameToCheck(HitCharacter, HitTime); return ConfirmHit(FrameToCheck, HitCharacter, TraceStart, HitLocation); } FServerSideRewindResult ULagCompensationComponent::ProjectileServerSideRewind(ABlasterCharacter* HitCharacter, const FVector_NetQuantize& TraceStart, const FVector_NetQuantize100& InitialVelocity, float HitTime) { const FFramePackage FrameToCheck = GetFrameToCheck(HitCharacter, HitTime); return ProjectileConfirmHit(FrameToCheck, HitCharacter, TraceStart, InitialVelocity, HitTime); } FShotgunServerSideRewindResult ULagCompensationComponent::ShotgunServerSideRewind(const TArray& HitCharacters, const FVector_NetQuantize& TraceStart, const TArray& HitLocations, float HitTime) { TArray FramesToCheck; for (ABlasterCharacter* HitCharacter : HitCharacters) { FramesToCheck.Add(GetFrameToCheck(HitCharacter, HitTime)); } return ShotgunConfirmHit(FramesToCheck, TraceStart, HitLocations); } FFramePackage ULagCompensationComponent::GetFrameToCheck(ABlasterCharacter* HitCharacter, float HitTime) { bool bReturn = HitCharacter == nullptr || HitCharacter->GetLagCompensation() == nullptr || HitCharacter->GetLagCompensation()->FrameHistory.GetHead() == nullptr || HitCharacter->GetLagCompensation()->FrameHistory.GetTail() == nullptr; if (bReturn) FFramePackage(); // Frame package that we check to verify a hit FFramePackage FrameToCheck; bool bShouldInterpolate = true; const TDoubleLinkedList& History = HitCharacter->GetLagCompensation()->FrameHistory; const float OldestHistoryTime = History.GetTail()->GetValue().Time; const float NewestHistoryTime = History.GetHead()->GetValue().Time; if (OldestHistoryTime > HitTime) { // Too far back, too laggy to do SSR FFramePackage(); } if (OldestHistoryTime == HitTime) { FrameToCheck = History.GetTail()->GetValue(); bShouldInterpolate = false; } if (NewestHistoryTime <= HitTime) { FrameToCheck = History.GetHead()->GetValue(); bShouldInterpolate = false; } auto Younger = History.GetHead(); auto Older = History.GetHead(); while (Older->GetValue().Time > HitTime) { if (Older->GetNextNode() == nullptr) break; Older = Older->GetNextNode(); if (Older->GetValue().Time > HitTime) { Younger = Older; } } if (Older->GetValue().Time == HitTime) { FrameToCheck = Older->GetValue(); bShouldInterpolate = false; } if (bShouldInterpolate) // Why not just check if FrameToCheck == nullptr? { // Interpolate between Younger and Older FrameToCheck = InterpBetweenFrames(Older->GetValue(), Younger->GetValue(), HitTime); } FrameToCheck.Character = HitCharacter; return FrameToCheck; } void ULagCompensationComponent::ServerScoreRequest_Implementation(ABlasterCharacter* HitCharacter, const FVector_NetQuantize& TraceStart, const FVector_NetQuantize& HitLocation, float HitTime) { const FServerSideRewindResult Confirm = ServerSideRewind(HitCharacter, TraceStart, HitLocation, HitTime); if (Character && HitCharacter && Character->GetEquippedWeapon() && Confirm.bHitConfirmed) { const float Damage = Confirm.bHeadShot ? Character->GetEquippedWeapon()->GetHeadShotDamage() : Character->GetEquippedWeapon()->GetDamage(); UGameplayStatics::ApplyDamage( HitCharacter, Damage, Character->Controller, Character->GetEquippedWeapon(), UDamageType::StaticClass() ); } } void ULagCompensationComponent::ProjectileServerScoreRequest_Implementation(ABlasterCharacter* HitCharacter, const FVector_NetQuantize& TraceStart, const FVector_NetQuantize100& InitialVelocity, float HitTime) { const FServerSideRewindResult Confirm = ProjectileServerSideRewind(HitCharacter, TraceStart, InitialVelocity, HitTime); if (Character && HitCharacter && Character->GetEquippedWeapon() && Confirm.bHitConfirmed) { const float Damage = Confirm.bHeadShot ? Character->GetEquippedWeapon()->GetHeadShotDamage() : Character->GetEquippedWeapon()->GetDamage(); UGameplayStatics::ApplyDamage( HitCharacter, Damage, Character->Controller, Character->GetEquippedWeapon(), UDamageType::StaticClass() ); } } void ULagCompensationComponent::ShotgunServerScoreRequest_Implementation(const TArray& HitCharacters, const FVector_NetQuantize& TraceStart, const TArray& HitLocations, float HitTime) { FShotgunServerSideRewindResult Confirm = ShotgunServerSideRewind(HitCharacters, TraceStart, HitLocations, HitTime); for (auto& HitCharacter : HitCharacters) { if (HitCharacter == nullptr || Character == nullptr || HitCharacter->GetEquippedWeapon() == nullptr) continue; float TotalDamage = 0.f; if (Confirm.Headshots.Contains(HitCharacter)) { TotalDamage += Confirm.Headshots[HitCharacter] * Character->GetEquippedWeapon()->GetHeadShotDamage(); } if (Confirm.BodyShots.Contains(HitCharacter)) { TotalDamage += Confirm.BodyShots[HitCharacter] * Character->GetEquippedWeapon()->GetDamage(); } UGameplayStatics::ApplyDamage( HitCharacter, TotalDamage, Character->Controller, Character->GetEquippedWeapon(), UDamageType::StaticClass() ); } } void ULagCompensationComponent::EnableCharacterMeshCollision(ABlasterCharacter* HitCharacter, ECollisionEnabled::Type CollisionEnabled) { if (HitCharacter && HitCharacter->GetMesh()) { HitCharacter->GetMesh()->SetCollisionEnabled(CollisionEnabled); } } void ULagCompensationComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) { Super::TickComponent(DeltaTime, TickType, ThisTickFunction); SaveFramePackage(); } void ULagCompensationComponent::SaveFramePackage() { if (Character == nullptr || !Character->HasAuthority()) return; if (FrameHistory.Num() <= 1) { FFramePackage ThisFrame; SaveFramePackage(ThisFrame); FrameHistory.AddHead(ThisFrame); } else { float HistoryLength = FrameHistory.GetHead()->GetValue().Time - FrameHistory.GetTail()->GetValue().Time; while (HistoryLength > MaxRecordTime) { FrameHistory.RemoveNode(FrameHistory.GetTail()); HistoryLength = FrameHistory.GetHead()->GetValue().Time - FrameHistory.GetTail()->GetValue().Time; } FFramePackage ThisFrame; SaveFramePackage(ThisFrame); FrameHistory.AddHead(ThisFrame); } } void ULagCompensationComponent::SaveFramePackage(FFramePackage& Package) { Character = Character == nullptr ? Cast(GetOwner()) : Character; if (Character) { Package.Time = GetWorld()->GetTimeSeconds(); Package.Character = Character; for (auto& BoxPair : Character->HitCollisionBoxes) { FBoxInformation BoxInformation; BoxInformation.Location = BoxPair.Value->GetComponentLocation(); BoxInformation.Rotation = BoxPair.Value->GetComponentRotation(); BoxInformation.BoxExtent = BoxPair.Value->GetScaledBoxExtent(); Package.HitBoxInfo.Add(BoxPair.Key, BoxInformation); } } }