Basic Collision Detection

Source code: http://silverlightrocks.com/cs/files/folders/slg101_tutorials/entry250.aspx

It's all well and good to move objects around, and create particles, but most games require some kind of collision handling. A collision is when two objects intersect. There are many different ways to determine if two objects collide, each with varying levels of complexity and accuracy. In 2D games, the most accurate and complex is probably "pixel perfect" collision which checks whether any pixel of the two objects overlap. You can also use bounding boxes, or polygons that approximate the outline, and calculate if the two polygons intersect. One of the simplest collision detection algorithms is to use bounding circles, or in 3D bounding spheres. The math for this type of collision is very straightforward, and can also be used as an intermediate collision detection step before more complex and CPU intensive algorithms.

Collisions can get very involved if you're dealing with multiple objects potentially colliding simultaneously, or dealing with gravity and other forces along with the collisions. It also adds a major level of complexity if the objects that collide need to bounce off of each other. One of the core features of most commercial game engines is a physics engine, which specifically handles these types of interactions.

Fortunately in our case, we don't need to handle objects bouncing off each other. When objects collide in our game, they explode. We also can approximate our objects with circles, and therefore use the simplest of collision detection routines.

The first collision we will want to handle is between the ship and the asteroids. Since there is such as small number of potential objects, we can simply loop through the list of asteroids and check if they collide with the ship. To check if two circles intersect, you can find the distance between the centers and if that value is smaller than the sum of the radii of the two circles, then the circles intersect. The distance between two points can be found using the pythagorean theorem, and the distance is the square root of the sum of the X distance squared and the Y distance squared. To make things easier, the Vector type has a Length method which can help with the calculation. So since the ship and asteroids inherit from the sprite class, it makes sense to put the logic in the Sprite class to calculate whether a collision occurs. Here is the Collides method for the Sprite class:

public static bool Collides(Sprite s1, Sprite s2)
{
    Vector v = new Vector(s1.Position.X - s2.Position.X, s1.Position.Y - s2.Position.Y);
    if (s1.CollisionRadius + s2.CollisionRadius > v.Length)
    {
        return true;
    }
    else
    {
        return false;
    }
}

The CollisionRadius is a field that has been added to the Sprite class to store the collision radius, I know, very creative naming... Now for a useful game programming tip. To account for irregularities in the object (for example, the ship isn't really round, and the asteroids are bumpy) make your collision radius smaller than the radius that would encompass the entire sprite. This also makes the gameplay a little more forgiving and actually feels a little more natural.

So now in the gameLoop_Update method, we need to call this collision method. If the objects collide, the ship will explode and the asteroid will explode. If the asteroid has a radius of more than 10, we'll split it into two new asteroids and remove the existing one. To make it easier to remove items from a collection, it is helpful to loop backwards. This is because if you're looping forwards and remove an item, the indexes will be off. Also any new objects that are added will be processed immediately, which isn't what we want. 

for (int i = asteroids.Count - 1; i >= 0; i--)
{
    if (Sprite.Collides(ship, asteroids[i]))
    {
        if (asteroids[i].Radius > 10)
        {
            Asteroid a1 = new Asteroid(asteroids[i].Radius / 2);
            asteroids.Add(a1);
            this.Children.Add(a1);
            a1.Position = asteroids[i].Position;
            Asteroid a2 = new Asteroid(asteroids[i].Radius / 2);
            asteroids.Add(a2);
            this.Children.Add(a2);
            a2.Position = asteroids[i].Position;
        }
        ship.Explode();
        asteroids[i].Explode();
        this.Children.Remove(asteroids[i]);
        asteroids.RemoveAt(i);
    }
}

Then for the Explode method, this is also added to the Sprite class, since both a ship and an asteroid can explode. The Explode method generates a set of particles that fly out in all directions from the current position. It's pretty similar to the AddExhaust method and looks like this:

public void Explode()
{
    ParticleFactory p = new ParticleFactory();
    p.FromOpacity = 1;
    p.ToOpacity = .3f;
    p.LifeSpanSeconds = 1f;
    p.StartPosition = this.Position;
    for (int i = 0; i < 20; i++)
    {
        Ellipse ellipse = XamlReader.Load(explodeXaml) as Ellipse;
        Vector velocity = MathHelper.CreateVectorFromAngle(rand.Next(360), rand.Next(60, 100));
        p.AddParticle(ellipse, velocity);
    }
}

Now I haven't gone through all of the code changes, some things were moved around, and other fields added, but I have covered the main code related to collision handling for the game. The rest of it is similar to things we have already covered, or is just basic programming and not related to game programming or Silverlight specifically. Please look through the latest code to get an idea for the other changes.

collision

Published Tuesday, August 28, 2007 10:24 AM by Bill Reiss

Comments

# re: Basic Collision Detection

I noticed an artifact (detached storyboard/particle pairs) problem when there were MANY particles (probably more than 4,000 or so, I didn't measure).  I thought it may have been an issue with the particle name extraction from the storyboard name string, and subsequent FindName() in sb_Completed().  So, I changed it a bit and now, there are no more artifacts (detached FrameworkElement-particles). Here is the modified code:

using System;

using System.Windows;

using System.Windows.Controls;

using System.Windows.Documents;

using System.Windows.Ink;

using System.Windows.Input;

using System.Windows.Media;

using System.Windows.Media.Animation;

using System.Windows.Shapes;

using System.IO;

using System.Collections.Generic;

namespace SilverlightGames101.Utilities

{

   public class ParticleFactory

   {

       static public Canvas HostCanvas;

       static int currentCount=0;

       public Point StartPosition;

       public double StartPositionDeviation=0;

       public double LifeSpanSeconds;

       public double FromOpacity=1;

       public double ToOpacity=1;

       Random rand = new Random();

       // Create mapping

       Dictionary<Storyboard, FrameworkElement> spDictionary = new Dictionary<Storyboard, FrameworkElement>(1000);

       public void AddParticle(FrameworkElement particle, Vector velocity)

       {

           string particleName = "slg101particle" + currentCount.ToString();

           currentCount++;

           particle.SetValue<string>(FrameworkElement.NameProperty, particleName);

           StringWriter sw = new StringWriter();

           sw.Write("<Storyboard xmlns:x=\"schemas.microsoft.com/.../xaml\" x:Name=\"{1}_animation\" Duration=\"{0}\">");

           if (velocity.Length > 0)

           {

               sw.Write("<DoubleAnimation Storyboard.TargetName=\"{1}\" Storyboard.TargetProperty=\"(Canvas.Left)\" From=\"{2}\" To=\"{3}\" Duration=\"{0}\"/><DoubleAnimation Storyboard.TargetName=\"{1}\" Storyboard.TargetProperty=\"(Canvas.Top)\" From=\"{4}\" To=\"{5}\" Duration=\"{0}\"/>");

           }

           if (FromOpacity != 1 || ToOpacity != 1)

           {

               sw.Write("<DoubleAnimation Storyboard.TargetName=\"{1}\" Storyboard.TargetProperty=\"(Opacity)\" From=\"{6}\" To=\"{7}\" Duration=\"{0}\"/>");

           }

           sw.Write("</Storyboard>");

           Point start = StartPosition;

           Vector offset = MathHelper.CreateVectorFromAngle(rand.Next(360), rand.Next((int)(StartPositionDeviation * 100)) / 100.0);

           start += offset;

           TimeSpan tsDuration = TimeSpan.FromSeconds(LifeSpanSeconds);

           string duration = string.Format("{0}:{1}:{2}.{3:000}", tsDuration.Hours, tsDuration.Minutes, tsDuration.Seconds, tsDuration.Milliseconds);

           string animationXaml = string.Format(sw.ToString(), duration, particleName, start.X, start.X + velocity.X * LifeSpanSeconds, start.Y, start.Y + velocity.Y * LifeSpanSeconds, FromOpacity, ToOpacity);

           Storyboard sb = XamlReader.Load(animationXaml) as Storyboard;

           sb.Completed += new EventHandler(sb_Completed);

           HostCanvas.Resources.Add(sb);

           HostCanvas.Children.Add(particle);

           // Add to mapping

           this.spDictionary.Add(sb, particle);

           sb.Begin();

       }

       void sb_Completed(object sender, EventArgs e)

       {

           Storyboard sb = sender as Storyboard;

           HostCanvas.Resources.Remove(sb as DependencyObject);

           //string particleName = sb.Name.Substring(0, sb.Name.Length - 10);

           //FrameworkElement fe = HostCanvas.FindName(particleName) as FrameworkElement;

           // Use mapping to get FrameworkElement

           HostCanvas.Children.Remove((FrameworkElement)this.spDictionary[sb]);

           this.spDictionary.Remove(sb);

       }

   }

}

Saturday, September 01, 2007 1:25 AM by rand

# re: Basic Collision Detection

Wow rand, thanks for this. Do you mind if I update the package using your improvements?

Saturday, September 01, 2007 8:01 AM by Bill Reiss