I have had a couple of people that have asked me how to draw fast running simulations / games in WPF. The thing that most people try and do is use controls, and move them about using RotateTransforms and TranslateTransforms. This does sound good in theory, but I just don’t think it’s fast enough.
You know if you are writing a game or some sort of physics thing you need speed, and the best way to do that is to do it OnRender.
Luckily most controls in WPF do expose an overridable OnRender method that gives you access to the DrawingContext,
see MSDN
This is a very cool object that allows you to do all sorts of things. Most people I know that have done any sort of Windows development such as WinForms, would be aware of a OnPaint event, or know how to override Paint. In WPF this is the OnRender() method which has a signature of the following
protected override void OnRender(DrawingContext dc)
Using this override we are easily able to perform quick running graphics operations.
To demonstrate this I have create a small boids type flocking panel, where the user may choose fish or butterfly icons. The fish/butterfly will flock together and tend to hover around the centre of the containing panel, but will be scared shitless of the mouse and shall do everything they can to avoid it.
In order to do this we will need to know how to use the DrawingContext to do quick operations such as Rotates/Translates.
Lets start with a flocking item shall we. The code for that is as follows:
FlockItem
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Windows;
using System.Windows.Shapes;
using System.Windows.Media;
namespace FlockingAvoidance
{
/// <summary>
/// Flocking Item
/// </summary>
public class FlockItem
{
public Double X { get; set; }
public Double Y { get; set; }
public Double VX { get; set; }
public Double VY { get; set; }
public static readonly Int32 ITEM_WIDTH = 30;
public static readonly Int32 ITEM_HEIGHT = 30;
/// <summary>
/// Ctor
/// </summary>
public FlockItem()
{
this.X = FlockingAvoidanceCanvas.rand.NextDouble()
* FlockingAvoidanceCanvas.CANVAS_WIDTH;
this.Y = FlockingAvoidanceCanvas.rand.NextDouble()
* FlockingAvoidanceCanvas.CANVAS_HEIGHT;
this.VX = 0;
this.VY = 0;
this.Move();
}
/// <summary>
/// Centre of item
/// </summary>
public Point CentrePoint
{
get
{
return new Point(
this.X + (FlockItem.ITEM_WIDTH / 2),
this.Y + (FlockItem.ITEM_HEIGHT / 2));
}
}
/// <summary>
/// Move calculations
/// </summary>
public void Move()
{
//the speed limit
if (this.VX > 3) this.VX = 3;
if (this.VX < -3) this.VX = -3;
if (this.VY > 3) this.VY = 3;
if (this.VY < -3) this.VY = -3;
this.X += this.VX;
this.Y += this.VY;
this.VX *= 0.9;
this.VY *= 0.9;
this.VX += (FlockingAvoidanceCanvas.
rand.NextDouble()
- 0.5) * 0.4;
this.VY += (FlockingAvoidanceCanvas.
rand.NextDouble()
- 0.5) * 0.4;
//go towards center
this.X = (this.X * 500 +
FlockingAvoidanceCanvas.
CANVAS_WIDTH / 2) / 501;
this.Y = (this.Y * 500 +
FlockingAvoidanceCanvas.
CANVAS_HEIGHT / 2) / 501;
}
/// <summary>
/// Work out an angle
/// </summary>
public static Int32 AngleItem(Double VX, Double VY)
{
return (Int32)(FlockingAvoidanceCanvas.
rand.NextDouble() * 30);
}
}
}
Its a very simple data class that holds some positional information. Now we need something that is going to manipulate these FlockItem objects. I have written a small class called FlockingAvoidanceCanvas, which is a custom Canvas control. The code for that is as follows:
FlockingAvoidanceCanvas
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Windows.Threading;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Input;
namespace FlockingAvoidance
{
/// <summary>
/// Simple flocking container canvas
/// </summary>
public class FlockingAvoidanceCanvas : Canvas, IDisposable
{
public static readonly Int32 CANVAS_WIDTH = 500;
public static readonly Int32 CANVAS_HEIGHT = 500;
public static Random rand = new Random();
private List<FlockItem> flockItems
= new List<FlockItem>();
private DispatcherTimer timer
= new DispatcherTimer();
private Point mousePoint
= new Point();
private enum AnimalType
{
fish = 1,
butterfly = 2
};
private AnimalType currentAnimalType
= AnimalType.fish;
private BitmapImage imgSource;
private Double offsetX
= FlockItem.ITEM_WIDTH / 2.0;
private Double offsetY
= FlockItem.ITEM_HEIGHT / 2.0;
/// <summary>
/// Ctor
/// </summary>
public FlockingAvoidanceCanvas()
{
this.Width = CANVAS_WIDTH;
this.Height = CANVAS_HEIGHT;
for (int i = 0; i < 200; i++)
flockItems.Add(new FlockItem());
timer.Interval = TimeSpan.FromMilliseconds(10);
timer.IsEnabled = true;
timer.Tick += timer_Tick;
String imagePath= String.Empty;
switch (currentAnimalType)
{
case AnimalType.butterfly:
imagePath = @"Images\butterfly.png";
break;
case AnimalType.fish:
imagePath = @"Images\fish.png";
break;
default:
imagePath = @"Images\butterfly.png";
break;
}
imgSource = new BitmapImage();
imgSource.BeginInit();
imgSource.UriSource =
new Uri(BaseUriHelper.GetBaseUri(this),
imagePath);
imgSource.EndInit();
imgSource.Freeze();
}
/// <summary>
/// Update flocking items
/// </summary>
private void timer_Tick(object sender, EventArgs e)
{
foreach (FlockItem ItemX in flockItems)
{
foreach (FlockItem ItemY in flockItems)
{
if (!Object.ReferenceEquals(ItemX, ItemY))
{
Double dx = ItemY.X - ItemX.X;
Double dy = ItemY.Y - ItemX.Y;
var d = Math.Sqrt(dx * dx + dy * dy);
if (d < 40)
{
ItemX.VX += 20 * (-dx / (d * d));
ItemX.VY += 20 * (-dy / (d * d));
}
else if (d < 100)
{
ItemX.VX += 0.07 * (dx / d);
ItemX.VY += 0.07 * (dy / d);
}
}
}
Double dxMouse = mousePoint.X - ItemX.X;
Double dyMouse = mousePoint.Y - ItemX.Y;
Double dSqrt =
Math.Sqrt(dxMouse * dxMouse +
dyMouse * dyMouse);
if (dSqrt < 100)
{
ItemX.VX += 1 * (-dxMouse / (dSqrt));
ItemX.VY += 1 * (-dyMouse / (dSqrt));
}
ItemX.Move();
}
//redraw all
this.InvalidateVisual();
}
//Asked to ReDraw so draw all
protected override void OnRender(DrawingContext dc)
{
base.OnRender(dc);
//draw flocking items
foreach (FlockItem item in flockItems)
{
Double angle =
FlockItem.AngleItem(item.VX, item.VY);
dc.PushTransform(
new TranslateTransform(
item.CentrePoint.X,
item.CentrePoint.Y));
dc.PushTransform(
new RotateTransform(angle,
offsetX, offsetY));
dc.DrawImage(imgSource,
new Rect(0, 0,
FlockItem.ITEM_WIDTH,
FlockItem.ITEM_HEIGHT));
dc.Pop(); // pop RotateTransform
dc.Pop(); // pop TranslateTransform
}
}
/// <summary>
/// Store Mouse Point to allow flocking
/// items to avoid the Mouse
/// </summary>
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
mousePoint = e.GetPosition(this);
}
#region IDisposable Members
/// <summary>
/// Clean up
/// </summary>
public void Dispose()
{
timer.Tick -= timer_Tick;
}
#endregion
}
}
Note the OnRender() method, we are able to push/pop Transforms such as
RotateTransform
TranslateTransform
directly onto the DrawingContext, which makes it very very fast.
Here is a small screen shot of it running with some fish avoiding the Mouse(remember the fish are scared shitless of the Mouse)
This attachment is hidden for guests. Please log in or register to see it.