diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistClassicCubeActor.cpp b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistClassicCubeActor.cpp index 21bbe3b..a5444c3 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistClassicCubeActor.cpp +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Private/HyperTwistSimulation/HyperTwistClassicCubeActor.cpp @@ -2,7 +2,7 @@ AHyperTwistClassicCubeActor::AHyperTwistClassicCubeActor() { - PrimaryActorTick.bCanEverTick = false; + PrimaryActorTick.bCanEverTick = true; RootComponent = CreateDefaultSubobject(TEXT("Root")); } @@ -11,8 +11,7 @@ void AHyperTwistClassicCubeActor::OnConstruction(const FTransform& Transform) Super::OnConstruction(Transform); if (bGenerateOnConstruction) { - ClearPieces(); - GenerateCube(); + ResetCube(); } } @@ -21,29 +20,71 @@ void AHyperTwistClassicCubeActor::BeginPlay() Super::BeginPlay(); if (!bGenerateOnConstruction) { - ClearPieces(); - GenerateCube(); + ResetCube(); } } +void AHyperTwistClassicCubeActor::Tick(float DeltaTime) +{ + Super::Tick(DeltaTime); + + if (!bIsAnimating || !ActiveRotation.Pivot) + { + return; + } + + const float Now = GetWorld()->GetTimeSeconds(); + const float Elapsed = Now - ActiveRotation.StartTime; + float Alpha = FMath::Clamp(Elapsed / ActiveRotation.Duration, 0.0f, 1.0f); + // Smooth step for nicer animation feel + Alpha = Alpha * Alpha * (3.0f - 2.0f * Alpha); + + const FQuat CurrentRot = FQuat::Slerp(ActiveRotation.PivotStartRot, ActiveRotation.PivotTargetRot, Alpha); + ActiveRotation.Pivot->SetWorldRotation(CurrentRot); + + if (Alpha >= 1.0f) + { + FinalizeRotation(); + } +} + +void AHyperTwistClassicCubeActor::ResetCube() +{ + ClearPieces(); + TrackedPieces.Empty(); + RotationQueue.Empty(); + bIsAnimating = false; + ActiveRotation = FActiveRotation(); + GenerateCube(); +} + void AHyperTwistClassicCubeActor::ClearPieces() { - for (UProceduralMeshComponent* Mesh : PieceMeshes) + for (const FTrackedPiece& Piece : TrackedPieces) { - if (Mesh && Mesh->IsValidLowLevel()) + if (Piece.Mesh && Piece.Mesh->IsValidLowLevel()) { - Mesh->DestroyComponent(); + Piece.Mesh->DestroyComponent(); } } - PieceMeshes.Empty(); + TrackedPieces.Empty(); + + if (ActiveRotation.Pivot && ActiveRotation.Pivot->IsValidLowLevel()) + { + ActiveRotation.Pivot->DestroyComponent(); + } + ActiveRotation = FActiveRotation(); + bIsAnimating = false; } void AHyperTwistClassicCubeActor::GenerateCube() { + TrackedPieces.SetNum(27); + struct FPieceDef { FVector GridPos; - TArray Faces; + TArray IdentityFaces; }; const TArray Pieces = { @@ -78,18 +119,17 @@ void AHyperTwistClassicCubeActor::GenerateCube() {FVector(-1, -1, -1), {EHyperTwistClassicCubeFace::Down, EHyperTwistClassicCubeFace::Back, EHyperTwistClassicCubeFace::Left}}, }; - for (const FPieceDef& Piece : Pieces) + for (const FPieceDef& Def : Pieces) { - CreatePiece(Piece.GridPos, Piece.Faces); + CreatePiece(Def.GridPos, Def.IdentityFaces); } } -void AHyperTwistClassicCubeActor::CreatePiece(const FVector& GridPos, const TArray& ColoredFaces) +void AHyperTwistClassicCubeActor::CreatePiece(const FVector& GridPos, const TArray& IdentityFaces) { UProceduralMeshComponent* Mesh = NewObject(this, NAME_None, RF_Transactional); Mesh->RegisterComponent(); Mesh->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepRelativeTransform); - PieceMeshes.Add(Mesh); FVector Center = GridPos * (CubeletSize + Gap); float HalfSize = CubeletSize * 0.5f; @@ -97,22 +137,24 @@ void AHyperTwistClassicCubeActor::CreatePiece(const FVector& GridPos, const TArr for (int32 i = 0; i < 6; ++i) { EHyperTwistClassicCubeFace Face = static_cast(i); - - // Look up material: use FaceMaterials if assigned, otherwise default (null) UMaterialInterface* Mat = nullptr; int32 FaceIndex = static_cast(Face); - if (FaceMaterials.IsValidIndex(FaceIndex) && FaceMaterials[FaceIndex] != nullptr - && ColoredFaces.Contains(Face)) + if (IdentityFaces.Contains(Face) && FaceMaterials.IsValidIndex(FaceIndex) && FaceMaterials[FaceIndex] != nullptr) { Mat = FaceMaterials[FaceIndex]; } - else if (InternalMaterial != nullptr && !ColoredFaces.Contains(Face)) + else if (InternalMaterial != nullptr && !IdentityFaces.Contains(Face)) { Mat = InternalMaterial; } - CreateCubeletFace(Mesh, i, Center, HalfSize, Face, Mat); } + + int32 Index = GridToIndex(GridPos); + if (TrackedPieces.IsValidIndex(Index)) + { + TrackedPieces[Index] = { Mesh, GridPos, FQuat::Identity, IdentityFaces }; + } } void AHyperTwistClassicCubeActor::CreateCubeletFace(UProceduralMeshComponent* Mesh, int32 SectionIndex, @@ -215,3 +257,199 @@ void AHyperTwistClassicCubeActor::CreateCubeletFace(UProceduralMeshComponent* Me Mesh->SetMaterial(SectionIndex, Material); } } + +bool AHyperTwistClassicCubeActor::IsAnimating() const +{ + return bIsAnimating; +} + +void AHyperTwistClassicCubeActor::RotateFace(EHyperTwistClassicCubeFace Face, EHyperTwistRotationDirection Direction) +{ + RotationQueue.Add(TPair(Face, Direction)); + ProcessRotationQueue(); +} + +void AHyperTwistClassicCubeActor::ProcessRotationQueue() +{ + if (bIsAnimating || RotationQueue.IsEmpty()) + { + return; + } + + auto Next = RotationQueue[0]; + RotationQueue.RemoveAt(0); + StartFaceRotation(Next.Key, Next.Value); +} + +void AHyperTwistClassicCubeActor::StartFaceRotation(EHyperTwistClassicCubeFace Face, EHyperTwistRotationDirection Direction) +{ + TArray Indices = GetFacePieceIndices(TrackedPieces, Face); + if (Indices.IsEmpty()) + { + ProcessRotationQueue(); + return; + } + + // Create pivot at face center + USceneComponent* Pivot = NewObject(this, NAME_None, RF_Transactional); + Pivot->RegisterComponent(); + Pivot->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepRelativeTransform); + Pivot->SetWorldLocation(GetFaceCenter(Face) * (CubeletSize + Gap)); + + // Attach face pieces to pivot (keep world transform) + for (int32 Idx : Indices) + { + if (TrackedPieces[Idx].Mesh) + { + TrackedPieces[Idx].Mesh->AttachToComponent(Pivot, FAttachmentTransformRules::KeepWorldTransform); + } + } + + FQuat FaceRot = GetFaceRotationQuat(Face, Direction); + ActiveRotation.Pivot = Pivot; + ActiveRotation.PieceIndices = Indices; + ActiveRotation.PivotStartRot = FQuat::Identity; + ActiveRotation.PivotTargetRot = FaceRot; + ActiveRotation.StartTime = GetWorld()->GetTimeSeconds(); + ActiveRotation.Duration = TurnDuration; + ActiveRotation.Face = Face; + ActiveRotation.Direction = Direction; + bIsAnimating = true; +} + +void AHyperTwistClassicCubeActor::FinalizeRotation() +{ + if (!ActiveRotation.Pivot) + { + bIsAnimating = false; + ProcessRotationQueue(); + return; + } + + // Detach pieces from pivot, keeping their world transforms + for (int32 Idx : ActiveRotation.PieceIndices) + { + if (TrackedPieces[Idx].Mesh) + { + TrackedPieces[Idx].Mesh->DetachFromComponent(FDetachmentTransformRules::KeepWorldTransform); + } + } + + // Compute and apply new grid positions and orientations + FQuat FaceRot = ActiveRotation.PivotTargetRot; + for (int32 Idx : ActiveRotation.PieceIndices) + { + FTrackedPiece& Piece = TrackedPieces[Idx]; + Piece.GridPos = RotateGridPosition(Piece.GridPos, ActiveRotation.Face, ActiveRotation.Direction); + Piece.Orientation = FaceRot * Piece.Orientation; + } + + // Destroy pivot + ActiveRotation.Pivot->DestroyComponent(); + ActiveRotation = FActiveRotation(); + bIsAnimating = false; + ProcessRotationQueue(); +} + +int32 AHyperTwistClassicCubeActor::GridToIndex(const FVector& GridPos) const +{ + return (FMath::RoundToInt(GridPos.X) + 1) * 9 + (FMath::RoundToInt(GridPos.Y) + 1) * 3 + (FMath::RoundToInt(GridPos.Z) + 1); +} + +FVector AHyperTwistClassicCubeActor::IndexToGrid(int32 Index) const +{ + int32 X = Index / 9 - 1; + int32 Y = (Index % 9) / 3 - 1; + int32 Z = Index % 3 - 1; + return FVector((float)X, (float)Y, (float)Z); +} + +FVector AHyperTwistClassicCubeActor::RotateGridPosition(const FVector& Pos, EHyperTwistClassicCubeFace Face, EHyperTwistRotationDirection Direction) +{ + const float X = Pos.X; + const float Y = Pos.Y; + const float Z = Pos.Z; + + if (Direction == EHyperTwistRotationDirection::Clockwise) + { + switch (Face) + { + case EHyperTwistClassicCubeFace::Up: return FVector( Y, -X, Z); + case EHyperTwistClassicCubeFace::Down: return FVector(-Y, X, Z); + case EHyperTwistClassicCubeFace::Front: return FVector(-Z, Y, X); + case EHyperTwistClassicCubeFace::Back: return FVector(-Z, Y, X); + case EHyperTwistClassicCubeFace::Right: return FVector( X, -Z, Y); + case EHyperTwistClassicCubeFace::Left: return FVector( X, -Z, Y); + } + } + else + { + switch (Face) + { + case EHyperTwistClassicCubeFace::Up: return FVector(-Y, X, Z); + case EHyperTwistClassicCubeFace::Down: return FVector( Y, -X, Z); + case EHyperTwistClassicCubeFace::Front: return FVector( Z, Y, -X); + case EHyperTwistClassicCubeFace::Back: return FVector( Z, Y, -X); + case EHyperTwistClassicCubeFace::Right: return FVector( X, Z, -Y); + case EHyperTwistClassicCubeFace::Left: return FVector( X, Z, -Y); + } + } + return Pos; +} + +FQuat AHyperTwistClassicCubeActor::GetFaceRotationQuat(EHyperTwistClassicCubeFace Face, EHyperTwistRotationDirection Direction) +{ + const bool bCW = (Direction == EHyperTwistRotationDirection::Clockwise); + switch (Face) + { + case EHyperTwistClassicCubeFace::Up: return FQuat(FRotator(0.0f, bCW ? -90.0f : 90.0f, 0.0f)); + case EHyperTwistClassicCubeFace::Down: return FQuat(FRotator(0.0f, bCW ? 90.0f : -90.0f, 0.0f)); + case EHyperTwistClassicCubeFace::Front: return FQuat(FRotator(bCW ? -90.0f : 90.0f, 0.0f, 0.0f)); + case EHyperTwistClassicCubeFace::Back: return FQuat(FRotator(bCW ? -90.0f : 90.0f, 0.0f, 0.0f)); + case EHyperTwistClassicCubeFace::Right: return FQuat(FRotator(0.0f, 0.0f, bCW ? 90.0f : -90.0f)); + case EHyperTwistClassicCubeFace::Left: return FQuat(FRotator(0.0f, 0.0f, bCW ? 90.0f : -90.0f)); + } + return FQuat::Identity; +} + +TArray AHyperTwistClassicCubeActor::GetFacePieceIndices(const TArray& Pieces, EHyperTwistClassicCubeFace Face) +{ + TArray Result; + for (int32 i = 0; i < Pieces.Num(); ++i) + { + if (!Pieces[i].Mesh) + { + continue; + } + const FVector& Pos = Pieces[i].GridPos; + bool bOnFace = false; + switch (Face) + { + case EHyperTwistClassicCubeFace::Up: bOnFace = FMath::IsNearlyEqual(Pos.Z, 1.0f); break; + case EHyperTwistClassicCubeFace::Down: bOnFace = FMath::IsNearlyEqual(Pos.Z, -1.0f); break; + case EHyperTwistClassicCubeFace::Front: bOnFace = FMath::IsNearlyEqual(Pos.Y, 1.0f); break; + case EHyperTwistClassicCubeFace::Back: bOnFace = FMath::IsNearlyEqual(Pos.Y, -1.0f); break; + case EHyperTwistClassicCubeFace::Right: bOnFace = FMath::IsNearlyEqual(Pos.X, 1.0f); break; + case EHyperTwistClassicCubeFace::Left: bOnFace = FMath::IsNearlyEqual(Pos.X, -1.0f); break; + } + if (bOnFace) + { + Result.Add(i); + } + } + return Result; +} + +FVector AHyperTwistClassicCubeActor::GetFaceCenter(EHyperTwistClassicCubeFace Face) +{ + switch (Face) + { + case EHyperTwistClassicCubeFace::Up: return FVector(0, 0, 1); + case EHyperTwistClassicCubeFace::Down: return FVector(0, 0, -1); + case EHyperTwistClassicCubeFace::Front: return FVector(0, 1, 0); + case EHyperTwistClassicCubeFace::Back: return FVector(0, -1, 0); + case EHyperTwistClassicCubeFace::Right: return FVector(1, 0, 0); + case EHyperTwistClassicCubeFace::Left: return FVector(-1, 0, 0); + } + return FVector::ZeroVector; +} diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistClassicCubeActor.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistClassicCubeActor.h index 2f835cc..12c8832 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistClassicCubeActor.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistClassicCubeActor.h @@ -38,18 +38,70 @@ public: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Cube") UMaterialInterface* InternalMaterial; + /** Duration of a quarter-turn animation in seconds. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "HyperTwist|Cube") + float TurnDuration = 0.15f; + + /** Rotate one face of the cube. Queued if already animating. */ + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Cube") + void RotateFace(EHyperTwistClassicCubeFace Face, EHyperTwistRotationDirection Direction); + + /** True if a face rotation animation is in progress. */ + UFUNCTION(BlueprintPure, Category = "HyperTwist|Cube") + bool IsAnimating() const; + + /** Reset the cube to solved state. */ + UFUNCTION(BlueprintCallable, Category = "HyperTwist|Cube") + void ResetCube(); + virtual void OnConstruction(const FTransform& Transform) override; protected: virtual void BeginPlay() override; + virtual void Tick(float DeltaTime) override; private: + struct FTrackedPiece + { + UProceduralMeshComponent* Mesh = nullptr; + FVector GridPos = FVector::ZeroVector; + FQuat Orientation = FQuat::Identity; + TArray IdentityFaces; + }; + + struct FActiveRotation + { + USceneComponent* Pivot = nullptr; + TArray PieceIndices; + FQuat PivotStartRot; + FQuat PivotTargetRot; + float StartTime = 0.0f; + float Duration = 0.15f; + EHyperTwistClassicCubeFace Face = EHyperTwistClassicCubeFace::Up; + EHyperTwistRotationDirection Direction = EHyperTwistRotationDirection::Clockwise; + }; + void GenerateCube(); void ClearPieces(); - void CreatePiece(const FVector& GridPos, const TArray& ColoredFaces); + void CreatePiece(const FVector& GridPos, const TArray& IdentityFaces); void CreateCubeletFace(UProceduralMeshComponent* Mesh, int32 SectionIndex, const FVector& Center, float HalfSize, EHyperTwistClassicCubeFace Face, UMaterialInterface* Material); - TArray PieceMeshes; + void StartFaceRotation(EHyperTwistClassicCubeFace Face, EHyperTwistRotationDirection Direction); + void FinalizeRotation(); + void ProcessRotationQueue(); + + static FVector RotateGridPosition(const FVector& Pos, EHyperTwistClassicCubeFace Face, EHyperTwistRotationDirection Direction); + static FQuat GetFaceRotationQuat(EHyperTwistClassicCubeFace Face, EHyperTwistRotationDirection Direction); + static TArray GetFacePieceIndices(const TArray& Pieces, EHyperTwistClassicCubeFace Face); + static FVector GetFaceCenter(EHyperTwistClassicCubeFace Face); + + int32 GridToIndex(const FVector& GridPos) const; + FVector IndexToGrid(int32 Index) const; + + TArray TrackedPieces; + TArray> RotationQueue; + FActiveRotation ActiveRotation; + bool bIsAnimating = false; }; diff --git a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistClassicCubeTypes.h b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistClassicCubeTypes.h index 3ea3a7c..cb0f1bf 100644 --- a/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistClassicCubeTypes.h +++ b/UnrealHyperTwist/Source/UnrealHyperTwist/Public/HyperTwistSimulation/HyperTwistClassicCubeTypes.h @@ -14,6 +14,13 @@ enum class EHyperTwistClassicCubeFace : uint8 Right UMETA(DisplayName = "Right") }; +UENUM(BlueprintType) +enum class EHyperTwistRotationDirection : uint8 +{ + Clockwise UMETA(DisplayName = "Clockwise"), + CounterClockwise UMETA(DisplayName = "Counter-Clockwise") +}; + USTRUCT(BlueprintType) struct FHyperTwistClassicCubePieceData {