Saturday, 9 October 2010

WPF Collision Detection with Canvas and ScaleTransform

As part of my continuing exercise learning IronPython, I ported an old Windows Forms game I had up on CodePlex to WPF. The game is called Asterisk, and was a favourite of mine when I was about 8 years old on the BBC Micro. The objective is simply to avoid the stars and get through the gap on the right hand side. The only control is to hold down a key to make the line go up instead of down.

Here’s what the current WPF version looks like:

WPF Asterisk

One of the challenges was implementing collision detection. I needed to check if the current point intersected with a star or not. I initially hoped I could use Andy Beaulieu’s Silverlight collision detection code, but sadly that uses VisualTreeHelper.FindElementsInHostCoordinates, which isn’t available in WPF.

However, a solution was readily at hand with VisualTreeHelper.HitTest. I rather stupidly failed to notice that there was a much simpler overload than the first search result on MSDN, so my initial Python code was over-complicated:

def CheckCollisionPoint(point, control):
    hits = []
    def callbackFunc(hit):
        hits.append(hit.VisualHit)
        return HitTestResultBehavior.Stop
    callback = HitTestResultCallback(callbackFunc)
    VisualTreeHelper.HitTest(control, None, callback,
        PointHitTestParameters(point))    
    return len(hits) > 0

Using a more appropriate HitTest overload simplifies things greatly:

def CheckCollisionPoint(point, control):
    hit = VisualTreeHelper.HitTest(control, point)
    return hit != None

However, there were still two difficulties. First was that point was coordinates relative to the canvas, whilst the control was a star object placed onto that canvas. This was simple enough to fix – I just needed to offset the coordinates by the Canvas.Top and Canvas.Left attached properties of the star.

However, problems came when I attempted to change the size of the stars with a scale transform:

<Path xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  Stroke="White" 
  StrokeThickness="2" 
  StrokeStartLineCap="Round" 
  StrokeEndLineCap="Round" 
  StrokeLineJoin="Round" 
  Data="M 0,0 l 5,0 l 2.5,-5 l 2.5,5 l 5,0 l -3.5,5 l 1,5 l -5,-2.5 l -5,2.5 l 1,-5 Z">
  <Path.RenderTransform>
    <ScaleTransform ScaleX="0.8" ScaleY="0.8" />
  </Path.RenderTransform>
</Path>

This caused collision detection to break because HitTest does not take the RenderTransform into account. I initially fixed this by dividing the point coordinates by the scale factor from the XAML. However, I then came across this blog post which demonstrates a better approach that will cope with any kind of transforms, including rotations. You can get an inverse transform from the render transform, and apply that to your point. So the final version of my WPF hit-test function in IronPython is as follows:

def CheckCollisionPoint(point, control):
    transformPoint = control.RenderTransform.Inverse.Transform(point)
    hit = VisualTreeHelper.HitTest(control, transformPoint)
    return hit != None

If you have IronPython installed, download the code and try it yourself.

2 comments:

Anonymous said...

nice, but it might be easier with CompositionTarget.Render Event

Unknown said...

thanks @anonymous, but I don't understand - how does CompositionTarget.Rendering event help me to do collision detection?