Introduction
I'm a huge fan of the Rubik's Cube and I solve it several times, almost daily. Ever since I got my first Rubik's Cube it has captivated me, and frustrated me in my attempts at getting faster at solving - I currently average 17 sec. It was only natural then that I took a shot at making a WPF version.
Background
The 3D APIs in WPF are not designed to provide full-featured game development capabilities but it is possible to develop something as 'simple' as a Rubik's Cube app. In this article I'll not delve into the finer details of WPF's 3D features so I will expect that you are conversant with that part of WPF. Another thing that you should be familiar with is basic cubing terms. Just in case, this is how you denote the faces of a cube,
Using the App
To turn a layer of the cube hold down the left mouse button and swipe in the direction you intend to rotate the layer. Once you let go of the left-mouse button the layer will rotate as intended.
Rotating the U layer anti-clockwise
The same principle also applies to whole cube rotations but instead of using the left-mouse button you use the right-mouse button.
Note that to make rotations you only interact with the F and R faces of the cube. To scramble the cube click the Scramble button.
Design & Layout
Designing the 3D model of the cube by hand-coding XAML would have been a tedious affair so I instead opted to model it in Blender.
Modelling of the cube in progress
The final result
After modeling the cube in Blender I exported the 3D model as a Wavefront (.obj) file which I then added to my WPF project in Expression Blend, together with the corresponding material (.mtl) file.
The material and object files
With the material and object file in the project, adding the 3D model to a WPF container is just a matter of right-clicking the object file and selecting Insert from the context-menu.
For this project I added more lights for adequate illumination of the cube so the Viewport3D
contains several PointLight
s, a DirectionalLight
and an AmbientLight
.
The Viewport3D
is masked by several Path
s that are used to deal with mouse events. I could have opted to use ModelUIElement3D
- that support input, focus, and events - but opted to stick with ModelVisual3D
objects.
Each ModelVisual3D
object that represents a cube piece is named according to the colors of its 'stickers' e.g. WGR_Cubie
is the White-Green-Red corner piece.
Code
The project contains three enumerations. The PieceLocations
enumerations represent the location in 3D space of a cube piece.
Public Enum PieceLocations
FUL
FU
FUR
RU
BUR
BU
BUL
LU
UC
FL
FC
FR
RC
BR
BC
BL
LC
FDL
FD
FDR
RD
BDR
BD
BDL
LD
DC
End Enum
The Notations
enumerations represent the notations of a Rubik's Cube.
Public Enum Notations
F
F_prime
F2
B
B_prime
B2
U
U_prime
U2
D
D_prime
D2
R
R_prime
R2
L
L_prime
L2
End Enum
The FaceNotations
enumerations represent the location of a Path
on the faces of the cube. Remember that only two faces of the cube are masked by Path
s.
Public Enum FaceLocations
FUL
FU
FUR
RUF
RU
RUB
FL
FC
FR
RF
RC
RB
FDL
FD
FDR
RDF
RD
RDB
End Enum
Class FacePath
defines an attached property that is set on the masking Path
s.
Public Class FacePath
Public Shared ReadOnly LocationProperty As DependencyProperty =
DependencyProperty.RegisterAttached("Location", GetType(FaceLocations), GetType(FacePath),
New FrameworkPropertyMetadata(FaceLocations.FC,
FrameworkPropertyMetadataOptions.None))
Public Shared Sub SetLocation(ByVal element As Path, ByVal value As FaceLocations)
element.SetValue(LocationProperty, value)
End Sub
Public Shared Function GetLocation(ByVal element As Path) As FaceLocations
Return CType(element.GetValue(LocationProperty), FaceLocations)
End Function
End Class
Each cube piece in the Viewport3D
is associated with an object of class CubePiece
which contains methods for rotating the associated ModelVisual3D
around a particular axis.
Public Class CubePiece
Public piece As ModelVisual3D
Private axisPoint As New Point3D(0, 0, 0)
Private axisAngleRtn3D As New AxisAngleRotation3D(New Vector3D(0, 0, 1), 0)
Private dblAnimation As DoubleAnimation
Private rotateTx3D As RotateTransform3D
Private tx3dGroup As New Transform3DGroup
Private Const ROTATION_TIME As Double = 200
Friend Property PieceLocation() As PieceLocations
Private Sub RotateAroundAxis(ByVal angle As Double)
rotateTx3D = New RotateTransform3D(axisAngleRtn3D, axisPoint)
dblAnimation = New DoubleAnimation(CDbl(angle), TimeSpan.FromMilliseconds(ROTATION_TIME),
FillBehavior.HoldEnd)
axisAngleRtn3D.BeginAnimation(AxisAngleRotation3D.AngleProperty, dblAnimation)
tx3dGroup.Children.Add(rotateTx3D)
piece.Transform = tx3dGroup
End Sub
Public Sub RotateAround_X_axis(ByVal angle As Double)
axisAngleRtn3D = New AxisAngleRotation3D(New Vector3D(1, 0, 0), 0)
RotateAroundAxis(angle)
ChangeLocationOnXaxisRotation(angle)
End Sub
Public Sub RotateAround_Y_axis(ByVal angle As Double)
axisAngleRtn3D = New AxisAngleRotation3D(New Vector3D(0, 1, 0), 0)
RotateAroundAxis(angle)
ChangeLocationOnYaxisRotation(angle)
End Sub
Public Sub RotateAround_Z_axis(ByVal angle As Double)
axisAngleRtn3D = New AxisAngleRotation3D(New Vector3D(0, 0, 1), 0)
RotateAroundAxis(angle)
ChangeLocationOnZaxisRotation(angle)
End Sub
...
End Class
Class Scrambler
contains methods for scramble generation and scrambling of the cube.
Public Class Scrambler
Private index As Integer
Private isScrambling As Boolean
Private Const CLOCKWISE As Double = -90
Private Const ANTI_CLOCKWISE As Double = 90
Private mainWin As MainWindow
Private rnd As Random
Private scrambleTimer As DispatcherTimer
Private scramble As List(Of Notations)
Public Sub New(ByRef win As MainWindow)
mainWin = win
rnd = New Random
scramble = New List(Of Notations)
scrambleTimer = New DispatcherTimer
scrambleTimer.Interval = New TimeSpan(0, 0, 0, 0, 400)
AddHandler scrambleTimer.Tick, AddressOf Timer_Tick
End Sub
Public Sub ScrambleCube()
If isScrambling = False Then
scramble.Clear()
For i As Integer = 0 To 19
AddToScramble()
Next
isScrambling = True
scrambleTimer.Start()
mainWin.CubeGroupGrid.IsEnabled = False
Else
MessageBox.Show("Scrambling of cube already in progress", _
"WPF Rubiks", MessageBoxButton.OK, MessageBoxImage.Exclamation)
End If
End Sub
Private Sub AddToScramble()
Dim randomNum As Integer = rnd.Next(0, 18)
Dim newNotation As String = [Enum].GetName(GetType(Notations), randomNum)
Dim notation As Notations
If (scramble.Count > 0) Then
Dim lastItem As String = scramble(scramble.Count - 1).ToString
If newNotation.Contains("F") AndAlso lastItem.Contains("F") Then
AddToScramble()
ElseIf newNotation.Contains("R") AndAlso lastItem.Contains("R") Then
AddToScramble()
ElseIf newNotation.Contains("B") AndAlso lastItem.Contains("B") Then
AddToScramble()
ElseIf newNotation.Contains("L") AndAlso lastItem.Contains("L") Then
AddToScramble()
ElseIf newNotation.Contains("U") AndAlso lastItem.Contains("U") Then
AddToScramble()
ElseIf newNotation.Contains("D") AndAlso lastItem.Contains("D") Then
AddToScramble()
Else
notation = [Enum].Parse(GetType(Notations), newNotation)
scramble.Add(notation)
End If
Else
notation = [Enum].Parse(GetType(Notations), newNotation)
scramble.Add(notation)
End If
End Sub
Private Sub Timer_Tick(ByVal sender As Object, ByVal e As EventArgs)
If (index < 19) Then
RotateCubePieces(index)
index += 1
Else
isScrambling = False
scrambleTimer.Stop()
index = 0
mainWin.CubeGroupGrid.IsEnabled = True
End If
End Sub
...
End Class
The project contains a few more classes that deal mainly with determining which direction a layer or the cube should be rotated. You can take a look at these classes by downloading the project source from the download link at the top of the article page.
Conclusion
I hope you'll have a fun time solving the WPF Rubik's Cube. I have scrambled and solved it several times and it is quite as thrilling as the real thing.
History
- 1st Feb, 2012: Initial post,
- 25th Sep, 2014: Updated article and source