WPF : A Fun Little Boids Type Thing
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 :
http://msdn.microsoft.com/en-us/library/system.windows.media.drawingcontext.aspx
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)
As always here is a link to a small demo project:
http://dl.dropbox.com/u/2600965/Blogposts/2010/03/FlockingAvoidance.zip


























Gert-Jan said
am March 1 2010 @ 9:03 pm
You’re right, they’re terrified! I did not know that. Entertained and educated at the same time as always, I thank you.
sacha said
am March 2 2010 @ 6:57 am
Yeah it is a little known fact that fish and butterflies for that matter are scared of mice.
martin said
am March 2 2010 @ 11:18 am
WOW, I knew that method, but I didn’t know how insane FAST it is?!
My CPU is not even at 10%!
Thanks for a great article again!
John said
am March 2 2010 @ 1:36 pm
For Silverlight, Im sure you have also seen this:
http://silverlightc64.codeplex.com/
This uses the MediaStreamSource (managed codec) introduced in Silverlight 3 to create a video stream, which the app then writes into for a smooth display – good idea!
sacha said
am March 2 2010 @ 2:01 pm
Had not seen that, thanks for the link
Josh said
am March 3 2010 @ 10:26 am
This scenario to me screams for the WriteableBitmap class, which would be far more performant than invalidiating the render surface of the control every 10 milliseconds. Imagine if you had a grid packed full of instances of this control doing this – your performance would still be in the toilet.
The problem with using a WriteableBitmap is that you have to render your items yourself, without so much help from WPF, but if performance is a real requirement here then it’s really the only way to go.
sacha said
am March 3 2010 @ 10:38 am
True WriteableBitmap would be excellent choice too.
Andy Preston said
am March 6 2010 @ 2:19 am
I like this – be good to see a WriteableBitmap or the WriteableBitmapEx implementation to compare. This is quite a funny app to watch!! Look forward to the next..!
sacha said
am March 6 2010 @ 6:29 am
Like Josh states the WriteableBitmap / WriteableBitmapEx would be the fastest by far. But you have to do everything yourself. Obviously using this technique things are a little easier, but as Josh also points out you would not want more than 1 item like this in your app, and the timer is very quick.
It is a complete trade off, obviously drawing every thing manually is a pain, but its fast, and then there is this way which I feel is good providing you only have 1 such item like this in your app.
Daniel Vaughan said
am March 14 2010 @ 12:10 am
This is a little gem Sacha, very interesting. The bit about the mouse cracked me up too. Nice one.
Cheers,
Daniel
Kirby Harris said
am May 29 2010 @ 3:54 am
If only I had a penny for every time I came to sachabarber.net… Great writing.
sacha said
am May 29 2010 @ 5:51 am
Thanks Kirby. I am a little quiet right now as I am working on CinchV2, which is the 2nd release of my MVVM framework, and that is eating up my time, but in a good way, I really like how it is working out. Just writing up demo articles as we speak, and soon articles to explain all the new goodness it does.