Thursday, May 24, 2007

2D controls in 3D world - how to create "A wheel of fortune with WPF"

Before reading this post you should know what is geometry and trigonometry. As well as you should know, that in WPF you can "brush" 3D meshes with images or, even other XAML vector controls, inherit and create custom controls, use animations, dependency and attached properties and handle special events. Here the result. Now - how to do it

image

First of all, we have to create custom control to handle each wheel of slot. It must consists of cylinder or "tire", that will handle our images (or vectors). Additional "feature" is flexibility of the roll size, so we'll inherit Viewbox - the best choice for control, that do not really knows, how big it will be, but has something of certain size inside it.

Next we'll create our mesh - don't even enter with hand to this stuff - use any of well-knows editors or ready made meshes. Blender (this is not Expression Blend) - is good freeware choice with option to export rather good XAML geometry.

After this step we'll have to draw slots itself. Use Expression Design for it. You'll draw XAML - then you can port it into images if you'd like to.

Fine. We have 3d mesh and vector XAML slots to draw over the mesh. How to do it? Simple. Here nice example of usage my resource loader.

We'll need such control (3d mesh with images over) a lot, so it makes sense to create method for its creation. Also we'll have it static and it not going to change across the application life cycle, so preload, save and use - do it.

internal static FrameworkElement getSlotXAML(int index)
{
switch (index)
{
case 3: return loadResource<Canvas>("Resources/bar1.xaml"); break;
case 4: return loadResource<Canvas>("Resources/bar2.xaml"); break;
case 5: return loadResource<Canvas>("Resources/bar3.xaml"); break;
case 2: return loadResource<Canvas>("Resources/bell.xaml"); break;
case 1: return loadResource<Canvas>("Resources/limon.xaml"); break;
case 6: return loadResource<Canvas>("Resources/seven.xaml"); break;
case 0: return loadResource<Canvas>("Resources/sherry.xaml"); break;
}
return null;
}

 


Then add it to panel, that wraps cylinder.


StackPanel p = new StackPanel();

p.Orientation =
Orientation.Horizontal;

for (int i = 0; i < _cnvs.Length; i++)
{
FrameworkElement fe = getSlotXAML(i);
fe.Width = 48;
fe.Height = 48;
RotateTransform rt = new RotateTransform(-90);
rt.CenterX = fe.Width / 2;
rt.CenterY = fe.Height / 2;
fe.RenderTransform = rt;

p.Children.Add(fe
/*_cnvs[i]*/);
}

 


Now, load the cylinder itself and brush it with this panel


GeometryModel3D model = loadResource<GeometryModel3D>("Resources/Cylinder.xaml");
((DiffuseMaterial)model.Material).Brush = new VisualBrush(p);

 


Now, put in into viewport and turn the lights and the camera on. The show begins. 


ModelVisual3D visual = new ModelVisual3D();
visual.Content = model;

Viewport3D port = new Viewport3D();
port.Children.Add(visual);
PerspectiveCamera camera = new PerspectiveCamera(new Point3D(0, 0, 1.7), new Vector3D(0, 0, -1), new Vector3D(0, 1, 0), 45);
port.Camera = camera;

ModelVisual3D lights = new ModelVisual3D();
lights.Content =
new AmbientLight(Colors.White);
port.Children.Add(lights);

 


Fine. We have static wheel with XAML vectors over it. Let's create a movie. Very important to accelerate and decelerate the spinning speed in order to make a feel of "fear play", so we'll recreate our animation with factor of it each couple of seconds. Turn timer and do following on tick.


if (i == 10)
i = 1;
else
i++;
_animation =
new DoubleAnimation(0, 360, new Duration(TimeSpan.FromSeconds(2/i)));
_animation.RepeatBehavior =
RepeatBehavior.Forever;
((
RotateTransform3D)((GeometryModel3D)((ModelVisual3D)((Viewport3D)_element).Children[0]).Content).Transform).Rotation.BeginAnimation(AxisAngleRotation3D.AngleProperty, _animation);

 


What's the hell of this casting? For real? My laziness - do it better. All we have to do it apply rotation transform on our "wheel" - that's all. Now, upon the animation stop increase or decrease its speed ratio. Remember - don't too fast - cops with radars are around there and not too slow - user will fall in sleep.


if (!_ftime && _speed > 2000)
{

_speed = 2000;
_goesDown = -1;
_coef = 200;
_ftime =
true;
_stopRequested =
true;


}
if (_speed > 1000)
{
_coef = 500;
_ftime =
false;
}
else if (_speed > 500 & _speed <= 1000)
_coef = 300;
else if (_speed > 300 & _speed <= 500)
_coef = 100;
else if (_speed > 100 & _speed <= 300)
_coef = 50;
else if (_speed < 100)
_coef = 5;

if (_speed >= 2000)
{
_goesDown = -1;
_speed = 2000;
}

if (_speed <= 2000)
{
_speed += _goesDown * _coef;
}

if (_speed <= 0)
{
_speed += _coef;
_goesDown = 1;
}



_animation.Duration =
new Duration(TimeSpan.FromMilliseconds(_speed));
_animation.From = 0;

((RotateTransform3D)((GeometryModel3D)((ModelVisual3D)((Viewport3D)_element).Children[0]).Content).Transform).Rotation.BeginAnimation(AxisAngleRotation3D.AngleProperty, _animation);

 


Now, we'll have to stop it over random position, BUT the position should show full tile, so calculate it.


if (_stopRequested)
{
_stopRequested =
false;
_animation.Completed -=
new EventHandler(_animation_Completed);
_animation.Completed +=
new EventHandler(_animation_Win);

_win = _rnd.Next(_cnvs.Length);


_animation.To = (360 / _cnvs.Length * _win) - _animation.From;
((RotateTransform3D)((GeometryModel3D)((ModelVisual3D)((Viewport3D)_element).Children[0]).Content).Transform).Rotation.BeginAnimation(AxisAngleRotation3D.AngleProperty, _animation);

return;
}

 


To notify our future owner, you should have two read only dependency objects - one for the item (kind of enum) and the second is for item (if someone want to show it in order place). Cloning is not a big strength of WPF, so we'll have to read our tile once again.


        public static readonly DependencyPropertyKey WinningNumberPropertyKey = DependencyProperty.RegisterReadOnly("WinningNumber", typeof(int), typeof(RollSlot), new UIPropertyMetadata(0));

static DependencyProperty WinningNumberProperty = WinningNumberPropertyKey.DependencyProperty;

public int WinningNumber
{
get { return (int)GetValue(WinningNumberProperty); }
}

public static readonly DependencyPropertyKey WinningItemPropertyKey = DependencyProperty.RegisterReadOnly("WinningItem", typeof(FrameworkElement), typeof(RollSlot), new UIPropertyMetadata(null));

static DependencyProperty WinningItemProperty = WinningItemPropertyKey.DependencyProperty;

public FrameworkElement WinningItem
{
get { return (FrameworkElement)GetValue(WinningItemProperty); }
}

void _animation_Win(object sender, EventArgs e)
{

SetValue(WinningNumberPropertyKey, (
int)_win);
SetValue(WinningItemPropertyKey, GetSlotXAML((
Slots)_win));

}

 


We done. The only think we should do is initialize slots and put them into our new parent.


<StackPanel Orientation="Horizontal">
<
l:RollSlot Width="300" x:Name="slot1" MouseDown="reinit"/>
<
l:RollSlot Width="300" x:Name="slot2" MouseDown="reinit"/>
<
l:RollSlot Width="300" x:Name="slot3" MouseDown="reinit"/>
</
StackPanel>

 


That's all, folks. Source code is attahced - it's dirty 'cos I'm really lazy to clean it up, so use it as is (top secret - it consist of some very useful "dead code", that wrote while testing solutions. Those can be real helpers for your job). Ah, one other thing don't forget to give me a credit and write something about me.


Source code for this article.

2 comments:

xsignal said...

Nice job, but your not the first one doing this kind of stuff with wpf. We have been doing private wpf development in gaming for a while now.

xsignal said...
This comment has been removed by the author.