// Copyright 2025 https://yuewu.dev/en All Rights Reserved. #include "Mover/Zipline/GMS_ZipliningMode.h" #include "MoverComponent.h" #include "DefaultMovementSet/Settings/CommonLegacyMovementSettings.h" #include "Kismet/KismetSystemLibrary.h" #include "MoveLibrary/MovementUtils.h" #include "Mover/Zipline/GMS_ZiplineInterface.h" #include "Mover/Zipline/GMS_ZiplineModeTransition.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(GMS_ZipliningMode) // FGMS_ZipliningState ////////////////////////////// FMoverDataStructBase* FGMS_ZipliningState::Clone() const { FGMS_ZipliningState* CopyPtr = new FGMS_ZipliningState(*this); return CopyPtr; } bool FGMS_ZipliningState::NetSerialize(FArchive& Ar, UPackageMap* Map, bool& bOutSuccess) { Super::NetSerialize(Ar, Map, bOutSuccess); Ar << ZiplineActor; Ar.SerializeBits(&bIsMovingAtoB, 1); bOutSuccess = true; return true; } void FGMS_ZipliningState::ToString(FAnsiStringBuilderBase& Out) const { Super::ToString(Out); Out.Appendf("ZiplineActor: %s\n", *GetNameSafe(ZiplineActor)); Out.Appendf("IsMovingAtoB: %d\n", bIsMovingAtoB); } bool FGMS_ZipliningState::ShouldReconcile(const FMoverDataStructBase& AuthorityState) const { const FGMS_ZipliningState* AuthorityZiplineState = static_cast(&AuthorityState); return (ZiplineActor != AuthorityZiplineState->ZiplineActor) || (bIsMovingAtoB != AuthorityZiplineState->bIsMovingAtoB); } void FGMS_ZipliningState::Interpolate(const FMoverDataStructBase& From, const FMoverDataStructBase& To, float Pct) { const FGMS_ZipliningState* FromState = static_cast(&From); const FGMS_ZipliningState* ToState = static_cast(&To); ZiplineActor = ToState->ZiplineActor; bIsMovingAtoB = ToState->bIsMovingAtoB; } // UGMS_ZipliningMode ////////////////////////////// UGMS_ZipliningMode::UGMS_ZipliningMode(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { Transitions.Add(CreateDefaultSubobject(TEXT("ZiplineEndTransition"))); } #if ENGINE_MINOR_VERSION >=6 void UGMS_ZipliningMode::GenerateMove_Implementation(const FMoverTickStartData& StartState, const FMoverTimeStep& TimeStep, FProposedMove& OutProposedMove) const #else void UGMS_ZipliningMode::OnGenerateMove(const FMoverTickStartData& StartState, const FMoverTimeStep& TimeStep, FProposedMove& OutProposedMove) const #endif { UMoverComponent* MoverComp = GetMoverComponent(); // Ziplining is just following a path from A to B, so all movement is handled in OnSimulationTick OutProposedMove = FProposedMove(); } #if ENGINE_MINOR_VERSION >=6 void UGMS_ZipliningMode::SimulationTick_Implementation(const FSimulationTickParams& Params, FMoverTickEndData& OutputState) #else void UGMS_ZipliningMode::OnSimulationTick(const FSimulationTickParams& Params, FMoverTickEndData& OutputState) #endif { // Are we continuing a move or starting fresh? const FGMS_ZipliningState* StartingZipState = Params.StartState.SyncState.SyncStateCollection.FindDataByType(); FMoverDefaultSyncState& OutputSyncState = OutputState.SyncState.SyncStateCollection.FindOrAddMutableDataByType(); FGMS_ZipliningState& OutZipState = OutputState.SyncState.SyncStateCollection.FindOrAddMutableDataByType(); USceneComponent* UpdatedComponent = Params.MovingComps.UpdatedComponent.Get(); UMoverComponent* MoverComp = Params.MovingComps.MoverComponent.Get(); AActor* MoverActor = MoverComp->GetOwner(); USceneComponent* StartPoint = nullptr; USceneComponent* EndPoint = nullptr; FVector ZipDirection; FVector FlatFacingDir; const float DeltaSeconds = Params.TimeStep.StepMs * 0.001f; FVector ActorOrigin; FVector BoxExtent; MoverActor->GetActorBounds(true, OUT ActorOrigin, OUT BoxExtent); const FVector ActorToZiplineOffset = MoverComp->GetUpDirection() * BoxExtent.Z; if (!StartingZipState) { // There is no existing zipline state... so let's find the target // A) teleport to the closest starting point, set the zip direction // B) choose the appropriate facing direction // C) choose the appropriate initial velocity TArray OverlappingActors; MoverComp->GetOwner()->GetOverlappingActors(OUT OverlappingActors); for (AActor* CandidateActor : OverlappingActors) { bool bIsZipline = UKismetSystemLibrary::DoesImplementInterface(CandidateActor, UGMS_ZiplineInterface::StaticClass()); if (bIsZipline) { const FVector MoverLoc = UpdatedComponent->GetComponentLocation(); USceneComponent* ZipPointA = IGMS_ZiplineInterface::Execute_GetStartComponent(CandidateActor); USceneComponent* ZipPointB = IGMS_ZiplineInterface::Execute_GetEndComponent(CandidateActor); if (FVector::DistSquared(ZipPointA->GetComponentLocation(), MoverLoc) < FVector::DistSquared(ZipPointB->GetComponentLocation(), MoverLoc)) { OutZipState.bIsMovingAtoB = true; StartPoint = ZipPointA; EndPoint = ZipPointB; } else { OutZipState.bIsMovingAtoB = false; StartPoint = ZipPointB; EndPoint = ZipPointA; } ZipDirection = (EndPoint->GetComponentLocation() - StartPoint->GetComponentLocation()).GetSafeNormal(); const FVector WarpLocation = StartPoint->GetComponentLocation() - ActorToZiplineOffset; FlatFacingDir = FVector::VectorPlaneProject(ZipDirection, MoverComp->GetUpDirection()).GetSafeNormal(); OutZipState.ZiplineActor = CandidateActor; UpdatedComponent->GetOwner()->TeleportTo(WarpLocation, FlatFacingDir.ToOrientationRotator()); break; } } // If we were unable to find a valid target zipline, refund all the time and let the actor fall if (!StartPoint || !EndPoint) { FName DefaultAirMode = DefaultModeNames::Falling; if (UCommonLegacyMovementSettings* LegacySettings = MoverComp->FindSharedSettings_Mutable()) { DefaultAirMode = LegacySettings->AirMovementModeName; } OutputState.MovementEndState.NextModeName = DefaultModeNames::Falling; OutputState.MovementEndState.RemainingMs = Params.TimeStep.StepMs; return; } } else { check(StartingZipState->ZiplineActor); OutZipState = *StartingZipState; USceneComponent* ZipPointA = IGMS_ZiplineInterface::Execute_GetStartComponent(StartingZipState->ZiplineActor); USceneComponent* ZipPointB = IGMS_ZiplineInterface::Execute_GetEndComponent(StartingZipState->ZiplineActor); if (StartingZipState->bIsMovingAtoB) { StartPoint = ZipPointA; EndPoint = ZipPointB; } else { StartPoint = ZipPointB; EndPoint = ZipPointA; } ZipDirection = (EndPoint->GetComponentLocation() - StartPoint->GetComponentLocation()).GetSafeNormal(); FlatFacingDir = FVector::VectorPlaneProject(ZipDirection, MoverComp->GetUpDirection()).GetSafeNormal(); } // Now let's slide along the zipline const FVector StepStartPos = UpdatedComponent->GetComponentLocation() + ActorToZiplineOffset; const FVector DesiredEndPos = StepStartPos + (ZipDirection * MaxSpeed * DeltaSeconds); // TODO: Make speed more dynamic FVector ActualEndPos = FMath::ClosestPointOnSegment(DesiredEndPos, StartPoint->GetComponentLocation(), EndPoint->GetComponentLocation()); bool bWillReachEndPosition = (ActualEndPos - EndPoint->GetComponentLocation()).IsNearlyZero(); FVector MoveDelta = ActualEndPos - StepStartPos; FMovementRecord MoveRecord; MoveRecord.SetDeltaSeconds(DeltaSeconds); if (!MoveDelta.IsNearlyZero()) { FHitResult Hit(1.f); UMovementUtils::TrySafeMoveUpdatedComponent(Params.MovingComps, MoveDelta, FlatFacingDir.ToOrientationQuat(), true, Hit, ETeleportType::None, MoveRecord); } const FVector FinalLocation = UpdatedComponent->GetComponentLocation(); const FVector FinalVelocity = MoveRecord.GetRelevantVelocity(); OutputSyncState.SetTransforms_WorldSpace(FinalLocation, UpdatedComponent->GetComponentRotation(), FinalVelocity, nullptr); // no movement base UpdatedComponent->ComponentVelocity = FinalVelocity; if (bWillReachEndPosition) { FName DefaultAirMode = DefaultModeNames::Falling; if (UCommonLegacyMovementSettings* LegacySettings = MoverComp->FindSharedSettings_Mutable()) { DefaultAirMode = LegacySettings->AirMovementModeName; } OutputState.MovementEndState.NextModeName = DefaultAirMode; // TODO: If we reach the end position early, we should refund the remaining time } }