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:
parent
8dc67875c9
commit
08813fbc0b
3 changed files with 319 additions and 22 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue