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);
        }


    }
}

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

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 = @"Imagesbutterfly.png";
                    break;
                case AnimalType.fish:
                    imagePath = @"Imagesfish.png";
                    break;
                default:
                    imagePath = @"Imagesbutterfly.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
    }
}

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

 

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)

fishDemo[2]

As always here is a link to a small demo project:

http://dl.dropbox.com/u/2600965/Blogposts/2010/03/FlockingAvoidance.zip

Advertisements

14 thoughts on “WPF : A Fun Little Boids Type Thing

  1. Gert-Jan says:

    You’re right, they’re terrified! I did not know that. Entertained and educated at the same time as always, I thank you.

  2. martin says:

    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!

  3. sacha says:

    Martin glad it helped you. My machine at work runs slightly higher (40%) but its XP running WPF, and a not so cool machine, so one has to make allowances

  4. John says:

    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!

  5. Josh says:

    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.

  6. Andy Preston says:

    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 says:

      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.

  7. This is a little gem Sacha, very interesting. The bit about the mouse cracked me up too. Nice one.

    Cheers,
    Daniel

  8. sacha says:

    Thanks Daniel

  9. Kirby Harris says:

    If only I had a penny for every time I came to sachabarber.net… Great writing.

    • sacha says:

      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.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: