WPF:魔方






4.96/5 (112投票s)
一个 WPF 3D 魔方应用程序
引言
我是魔方的忠实粉丝,几乎每天都要玩好几次。自从我有了第一个魔方以来,它就深深地吸引了我,也让我不断尝试加快解魔方的速度,并让我感到沮丧 - 我目前的平均速度是 17 秒。因此,我尝试制作一个 WPF 版本的魔方是很自然的事。
背景
WPF 中的 3D API 并非旨在提供全功能的游戏开发能力,但开发像魔方应用程序这样“简单”的东西是可行的。在本文中,我不会深入探讨 WPF 3D 功能的细节,因此我希望您熟悉 WPF 的这一部分。您应该熟悉的另一件事是基本的魔方术语。以防万一,这是你如何表示魔方的各个面,
使用应用程序
要旋转魔方的一层,请按住鼠标左键并朝您想要旋转该层的方向滑动。一旦您松开鼠标左键,该层就会按预期旋转。
同样的原理也适用于整个魔方的旋转,但您使用鼠标右键而不是鼠标左键。
请注意,要进行旋转,您只需要与魔方的 F 面和 R 面交互。 要打乱魔方,请单击“Scramble(打乱)”按钮。
设计 & 布局
手动编码 XAML 设计魔方的 3D 模型将是一件繁琐的事情,因此我选择在 Blender 中对其进行建模。
在 Blender 中对魔方进行建模后,我将 3D 模型导出为 Wavefront (.obj) 文件,然后将其与相应的材质 (.mtl) 文件一起添加到 Expression Blend 的 WPF 项目中。
将材质和对象文件添加到项目后,只需右键单击对象文件并从上下文菜单中选择插入,即可将 3D 模型添加到 WPF 容器中。
对于此项目,我添加了更多灯光以充分照亮魔方,因此 Viewport3D
包含多个 PointLight
、一个 DirectionalLight
和一个 AmbientLight
。
Viewport3D
被几个用于处理鼠标事件的 Path
屏蔽。 我可以选择使用 ModelUIElement3D
- 它支持输入、焦点和事件 - 但我选择坚持使用 ModelVisual3D
对象。
每个代表魔方块的 ModelVisual3D
对象的命名都根据其“贴纸”的颜色命名,例如,WGR_Cubie
是白色-绿色-红色角块。
代码
该项目包含三个枚举。PieceLocations
枚举表示魔方块在 3D 空间中的位置。
Public Enum PieceLocations
' U layer locations
FUL
FU
FUR
RU
BUR
BU
BUL
LU
UC
' E layer locations
FL
FC
FR
RC
BR
BC
BL
LC
' D layer locations
FDL
FD
FDR
RD
BDR
BD
BDL
LD
DC
End Enum
Notations
枚举表示魔方的符号。
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
FaceNotations
枚举表示 Path
在魔方表面上的位置。 请记住,只有魔方的两个面被 Path
屏蔽。
Public Enum FaceLocations
FUL
FU
FUR
RUF
RU
RUB
FL
FC
FR
RF
RC
RB
FDL
FD
FDR
RDF
RD
RDB
End Enum
类 FacePath
定义了一个附加属性,该属性设置在屏蔽 Path
上。
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
Viewport3D
中的每个魔方块都与一个 CubePiece
类的对象相关联,该对象包含用于围绕特定轴旋转相关 ModelVisual3D
的方法。
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
''' <summary>
''' Rotate cube piece around the X-axis
''' </summary>
''' <param name="angle">The angle of rotation; -90° or 90°</param>
Public Sub RotateAround_X_axis(ByVal angle As Double)
axisAngleRtn3D = New AxisAngleRotation3D(New Vector3D(1, 0, 0), 0)
RotateAroundAxis(angle)
ChangeLocationOnXaxisRotation(angle)
End Sub
''' <summary>
''' Rotate cube piece around the Y-axis
''' </summary>
''' <param name="angle">The angle of rotation; -90° or 90°</param>
Public Sub RotateAround_Y_axis(ByVal angle As Double)
axisAngleRtn3D = New AxisAngleRotation3D(New Vector3D(0, 1, 0), 0)
RotateAroundAxis(angle)
ChangeLocationOnYaxisRotation(angle)
End Sub
''' <summary>
''' Rotate cube piece around the Z-axis
''' </summary>
''' <param name="angle">The angle of rotation; -90° or 90°</param>
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
类 Scrambler
包含用于生成打乱步骤和打乱魔方的方法。
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()
' Create a scramble with 20 values
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
''' <summary>
''' Create scramble with dissimilar values following each other, and the new value
''' in the List is not an inverse or double of the preceding value e.g.
''' F' does not come after F, or F2 after F.
''' </summary>
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
该项目包含一些其他类,这些类主要用于确定应旋转魔方的一层还是整个魔方的方向。 您可以通过从文章页面顶部的下载链接下载项目源文件来查看这些类。
结论
我希望您能玩得开心,解开这个 WPF 魔方。 我已经多次打乱和解开了它,它和真实的魔方一样令人兴奋。
历史
- 2012 年 2 月 1 日:首次发布
- 2014 年 9 月 25 日:更新文章和源代码