Phase 2B: Face rotation logic with tick-based animation

- Added EHyperTwistRotationDirection enum
- Added FTrackedPiece with identity, grid position, and orientation
- Implemented RotateFace() with move queueing
- Tick-based pivot animation with smooth-step interpolation
- 0.15s quarter-turn duration
- Face rotation permutation tables for all 6 faces
- ResetCube() to return to solved state
- Build: TBD
This commit is contained in:
axiomlogicnexus 2026-06-10 06:25:25 +00:00
parent 8dc67875c9
commit 08813fbc0b
3 changed files with 319 additions and 22 deletions

View file

@ -2,7 +2,7 @@
AHyperTwistClassicCubeActor::AHyperTwistClassicCubeActor()
{
PrimaryActorTick.bCanEverTick = false;
PrimaryActorTick.bCanEverTick = true;
RootComponent = CreateDefaultSubobject<USceneComponent>(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<EHyperTwistClassicCubeFace> Faces;
TArray<EHyperTwistClassicCubeFace> IdentityFaces;
};
const TArray<FPieceDef> 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<EHyperTwistClassicCubeFace>& ColoredFaces)
void AHyperTwistClassicCubeActor::CreatePiece(const FVector& GridPos, const TArray<EHyperTwistClassicCubeFace>& IdentityFaces)
{
UProceduralMeshComponent* Mesh = NewObject<UProceduralMeshComponent>(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<EHyperTwistClassicCubeFace>(i);
// Look up material: use FaceMaterials if assigned, otherwise default (null)
UMaterialInterface* Mat = nullptr;
int32 FaceIndex = static_cast<int32>(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<EHyperTwistClassicCubeFace, EHyperTwistRotationDirection>(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<int32> Indices = GetFacePieceIndices(TrackedPieces, Face);
if (Indices.IsEmpty())
{
ProcessRotationQueue();
return;
}
// Create pivot at face center
USceneComponent* Pivot = NewObject<USceneComponent>(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<int32> AHyperTwistClassicCubeActor::GetFacePieceIndices(const TArray<FTrackedPiece>& Pieces, EHyperTwistClassicCubeFace Face)
{
TArray<int32> 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;
}

View file

@ -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<EHyperTwistClassicCubeFace> IdentityFaces;
};
struct FActiveRotation
{
USceneComponent* Pivot = nullptr;
TArray<int32> 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<EHyperTwistClassicCubeFace>& ColoredFaces);
void CreatePiece(const FVector& GridPos, const TArray<EHyperTwistClassicCubeFace>& IdentityFaces);
void CreateCubeletFace(UProceduralMeshComponent* Mesh, int32 SectionIndex,
const FVector& Center, float HalfSize, EHyperTwistClassicCubeFace Face,
UMaterialInterface* Material);
TArray<UProceduralMeshComponent*> 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<int32> GetFacePieceIndices(const TArray<FTrackedPiece>& Pieces, EHyperTwistClassicCubeFace Face);
static FVector GetFaceCenter(EHyperTwistClassicCubeFace Face);
int32 GridToIndex(const FVector& GridPos) const;
FVector IndexToGrid(int32 Index) const;
TArray<FTrackedPiece> TrackedPieces;
TArray<TPair<EHyperTwistClassicCubeFace, EHyperTwistRotationDirection>> RotationQueue;
FActiveRotation ActiveRotation;
bool bIsAnimating = false;
};

View file

@ -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
{