Don't like ads? PRO users don't see any ads ;-)

C# XNA - Per Pixel Collision Detection on Rotated Objects

By: etftw on Dec 19th, 2011  |  syntax: C#  |  size: 14.85 KB  |  hits: 465  |  expires: Never
download  |  raw  |  embed  |  report abuse  |  print
Text below is selected. Please press Ctrl+C to copy to your clipboard. (⌘+C on Mac)
  1. /*
  2.  * See http://www.etftw.co.uk/blog/posts/578 for more information about this class
  3.  */
  4.  
  5. using System;
  6. using Microsoft.Xna;
  7. using Microsoft.Xna.Framework;
  8. using Microsoft.Xna.Framework.Graphics;
  9.  
  10. namespace etftw
  11. {
  12.     /// <summary>
  13.     /// An object that can be used to depict a sprite in world space which can detect pixel level collisions with other CollidableObjects
  14.     /// </summary>
  15.     public class CollidableObject
  16.     {
  17.         #region Fields
  18.  
  19.         private Texture2D texture;
  20.         private Vector2 position;
  21.         private float rotation;
  22.         private Vector2 origin;
  23.         private Color[] textureData;
  24.  
  25.         #endregion
  26.  
  27.         #region Properties
  28.  
  29.         /// <summary>
  30.         /// The current position of the object in world space
  31.         /// </summary>
  32.         public Vector2 Position
  33.         {
  34.             get { return this.position; }
  35.         }
  36.  
  37.         /// <summary>
  38.         /// The currently loaded texture
  39.         /// </summary>
  40.         public Texture2D Texture
  41.         {
  42.             get { return this.texture; }
  43.         }
  44.  
  45.         /// <summary>
  46.         /// The pixel data of the loaded texture
  47.         /// </summary>
  48.         public Color[] TextureData
  49.         {
  50.             get { return this.textureData; }
  51.         }
  52.  
  53.         /// <summary>
  54.         /// The rotation factor
  55.         /// </summary>
  56.         public float Rotation
  57.         {
  58.             get
  59.             {
  60.                 return this.rotation;
  61.             }
  62.             set
  63.             {
  64.                 this.rotation = value;
  65.             }
  66.         }
  67.  
  68.         /// <summary>
  69.         /// The origin of the object, by default this is the center point of the texture.
  70.         /// </summary>
  71.         public Vector2 Origin
  72.         {
  73.             get { return this.origin; }
  74.             set { this.origin = value; }
  75.         }
  76.  
  77.         /// <summary>
  78.         /// A Rectangle that holds the width and height of the texture and zero in the X and Y points.
  79.         /// </summary>
  80.         public Rectangle Rect
  81.         {
  82.             get { return new Rectangle(0, 0, this.Texture.Width, this.Texture.Height); }
  83.         }
  84.  
  85.         /// <summary>
  86.         /// A Matrix based on the current rotation and position.
  87.         /// </summary>
  88.         public Matrix Transform
  89.         {
  90.             get
  91.             {
  92.                 return Matrix.CreateTranslation(new Vector3(-this.Origin, 0.0f)) *
  93.                                         Matrix.CreateRotationZ(this.Rotation) *
  94.                                         Matrix.CreateTranslation(new Vector3(this.Position, 0.0f));
  95.             }
  96.         }
  97.  
  98.         /// <summary>
  99.         /// An axis aligned rectangle which fully contains an arbitrarily transformed axis aligned rectangle.
  100.         /// </summary>
  101.         public Rectangle BoundingRectangle
  102.         {
  103.             get { return CalculateBoundingRectangle(this.Rect, this.Transform); }
  104.         }
  105.  
  106.         #endregion
  107.  
  108.         #region Constructors
  109.  
  110.         /// <summary>
  111.         /// Construct a new CollidableObject with a default texture and position in world space.
  112.         /// </summary>
  113.         /// <param name="texture">The texture associated with the object</param>
  114.         /// <param name="position">The position of the object in world space</param>
  115.         public CollidableObject(Texture2D texture, Vector2 position) : this(texture, position, 0.0f)
  116.         {
  117.         }
  118.  
  119.         /// <summary>
  120.         /// Constructs a new CollidableObject with a default texture, position and rotation in world space.
  121.         /// </summary>
  122.         /// <param name="texture">The texture associated with the object</param>
  123.         /// <param name="position">The position of the object in world space</param>
  124.         /// <param name="rotation">The rotation factor</param>
  125.         public CollidableObject(Texture2D texture, Vector2 position, float rotation)
  126.         {
  127.             this.LoadTexture(texture);
  128.             this.position = position;
  129.             this.rotation = rotation;
  130.         }
  131.  
  132.         #endregion
  133.  
  134.         #region Instance Methods
  135.  
  136.         /// <summary>
  137.         /// Moves the object left by the value passed in moveBy.
  138.         /// </summary>
  139.         /// <param name="moveBy">The floating point factor to move the object by</param>
  140.         public void MoveLeft(float moveBy)
  141.         {
  142.             this.position.X -= moveBy;
  143.         }
  144.  
  145.         /// <summary>
  146.         /// Moves the object right by the value passed in moveBy.
  147.         /// </summary>
  148.         /// <param name="moveBy">The floating point factor to move the object by</param>
  149.         public void MoveRight(float moveBy)
  150.         {
  151.             this.position.X += moveBy;
  152.         }
  153.  
  154.         /// <summary>
  155.         /// Moves the object up by the value passed in moveBy.
  156.         /// </summary>
  157.         /// <param name="moveBy">The floating point factor to move the object by</param>
  158.         public void MoveUp(float moveBy)
  159.         {
  160.             this.position.Y -= moveBy;
  161.         }
  162.  
  163.         /// <summary>
  164.         /// Moves the object down by the value passed in moveBy.
  165.         /// </summary>
  166.         /// <param name="moveBy">The floating point factor to move the object by</param>
  167.         public void MoveDown(float moveBy)
  168.         {
  169.             this.position.Y += moveBy;
  170.         }
  171.  
  172.         /// <summary>
  173.         /// Rotates the object by the value passed in moveBy, which can be both positive or negative to rotate in different directions.
  174.         /// </summary>
  175.         /// <param name="rotateBy">The floating point factor to move the object by</param>
  176.         public void Rotate(float rotateBy)
  177.         {
  178.             if (rotateBy < 0)
  179.             {
  180.                 this.rotation -= rotateBy;
  181.             }
  182.             else
  183.             {
  184.                 this.rotation += rotateBy;
  185.             }
  186.         }
  187.  
  188.         /// <summary>
  189.         /// Detects a pixel level collision between two CollidableObjects.
  190.         /// </summary>
  191.         /// <param name="collidable">The CollidableObject to check a collision against</param>
  192.         /// <returns>True if colliding, false if not.</returns>
  193.         public bool IsColliding(CollidableObject collidable)
  194.         {
  195.             bool retval = false;
  196.  
  197.             if (this.BoundingRectangle.Intersects(collidable.BoundingRectangle))
  198.             {
  199.                 if (IntersectPixels(this.Transform, this.Texture.Width, this.Texture.Height, this.TextureData, collidable.Transform, collidable.Texture.Width, collidable.Texture.Height, collidable.TextureData))
  200.                 {
  201.                     retval = true;
  202.                 }
  203.             }
  204.  
  205.             return retval;
  206.         }
  207.  
  208.         /// <summary>
  209.         /// Loads a new texture and resets the origin to be the center point of the texture, the previous transformation values will be maintained.
  210.         /// </summary>
  211.         /// <param name="texture">The new texture to load</param>
  212.         public void LoadTexture(Texture2D texture)
  213.         {
  214.             this.texture = texture;
  215.             this.origin = new Vector2(texture.Width / 2, texture.Height / 2);
  216.             this.textureData = new Color[texture.Width * texture.Height];
  217.             this.texture.GetData(this.textureData);
  218.         }
  219.  
  220.         /// <summary>
  221.         /// Loads a new texture and origin, the previous transformation values will be maintained.
  222.         /// </summary>
  223.         /// <param name="texture">The new texture to load</param>
  224.         /// <param name="origin">The new origin point</param>
  225.         public void LoadTexture(Texture2D texture, Vector2 origin)
  226.         {
  227.             this.LoadTexture(texture);
  228.             this.origin = origin;
  229.         }
  230.  
  231.         #endregion
  232.  
  233.         #region Static Methods
  234.  
  235.         /// <summary>
  236.         /// Determines if there is overlap of the non-transparent pixels
  237.         /// between two sprites.
  238.         /// </summary>
  239.         /// <param name="rectangleA">Bounding rectangle of the first sprite</param>
  240.         /// <param name="dataA">Pixel data of the first sprite</param>
  241.         /// <param name="rectangleB">Bouding rectangle of the second sprite</param>
  242.         /// <param name="dataB">Pixel data of the second sprite</param>
  243.         /// <returns>True if non-transparent pixels overlap; false otherwise</returns>
  244.         public static bool IntersectPixels(Rectangle rectangleA, Color[] dataA, Rectangle rectangleB, Color[] dataB)
  245.         {
  246.             // Find the bounds of the rectangle intersection
  247.             int top = Math.Max(rectangleA.Top, rectangleB.Top);
  248.             int bottom = Math.Min(rectangleA.Bottom, rectangleB.Bottom);
  249.             int left = Math.Max(rectangleA.Left, rectangleB.Left);
  250.             int right = Math.Min(rectangleA.Right, rectangleB.Right);
  251.  
  252.             // Check every point within the intersection bounds
  253.             for (int y = top; y < bottom; y++)
  254.             {
  255.                 for (int x = left; x < right; x++)
  256.                 {
  257.                     // Get the color of both pixels at this point
  258.                     Color colorA = dataA[(x - rectangleA.Left) +
  259.                                          (y - rectangleA.Top) * rectangleA.Width];
  260.                     Color colorB = dataB[(x - rectangleB.Left) +
  261.                                          (y - rectangleB.Top) * rectangleB.Width];
  262.  
  263.                     // If both pixels are not completely transparent,
  264.                     if (colorA.A != 0 && colorB.A != 0)
  265.                     {
  266.                         // then an intersection has been found
  267.                         return true;
  268.                     }
  269.                 }
  270.             }
  271.  
  272.             // No intersection found
  273.             return false;
  274.         }
  275.  
  276.         /// <summary>
  277.         /// Determines if there is overlap of the non-transparent pixels between two
  278.         /// sprites.
  279.         /// </summary>
  280.         /// <param name="transformA">World transform of the first sprite.</param>
  281.         /// <param name="widthA">Width of the first sprite's texture.</param>
  282.         /// <param name="heightA">Height of the first sprite's texture.</param>
  283.         /// <param name="dataA">Pixel color data of the first sprite.</param>
  284.         /// <param name="transformB">World transform of the second sprite.</param>
  285.         /// <param name="widthB">Width of the second sprite's texture.</param>
  286.         /// <param name="heightB">Height of the second sprite's texture.</param>
  287.         /// <param name="dataB">Pixel color data of the second sprite.</param>
  288.         /// <returns>True if non-transparent pixels overlap; false otherwise</returns>
  289.         public static bool IntersectPixels(Matrix transformA, int widthA, int heightA, Color[] dataA, Matrix transformB, int widthB, int heightB, Color[] dataB)
  290.         {
  291.             // Calculate a matrix which transforms from A's local space into
  292.             // world space and then into B's local space
  293.             Matrix transformAToB = transformA * Matrix.Invert(transformB);
  294.  
  295.             // When a point moves in A's local space, it moves in B's local space with a
  296.             // fixed direction and distance proportional to the movement in A.
  297.             // This algorithm steps through A one pixel at a time along A's X and Y axes
  298.             // Calculate the analogous steps in B:
  299.             Vector2 stepX = Vector2.TransformNormal(Vector2.UnitX, transformAToB);
  300.             Vector2 stepY = Vector2.TransformNormal(Vector2.UnitY, transformAToB);
  301.  
  302.             // Calculate the top left corner of A in B's local space
  303.             // This variable will be reused to keep track of the start of each row
  304.             Vector2 yPosInB = Vector2.Transform(Vector2.Zero, transformAToB);
  305.  
  306.             // For each row of pixels in A
  307.             for (int yA = 0; yA < heightA; yA++)
  308.             {
  309.                 // Start at the beginning of the row
  310.                 Vector2 posInB = yPosInB;
  311.  
  312.                 // For each pixel in this row
  313.                 for (int xA = 0; xA < widthA; xA++)
  314.                 {
  315.                     // Round to the nearest pixel
  316.                     int xB = (int)Math.Round(posInB.X);
  317.                     int yB = (int)Math.Round(posInB.Y);
  318.  
  319.                     // If the pixel lies within the bounds of B
  320.                     if (0 <= xB && xB < widthB &&
  321.                         0 <= yB && yB < heightB)
  322.                     {
  323.                         // Get the colors of the overlapping pixels
  324.                         Color colorA = dataA[xA + yA * widthA];
  325.                         Color colorB = dataB[xB + yB * widthB];
  326.  
  327.                         // If both pixels are not completely transparent,
  328.                         if (colorA.A != 0 && colorB.A != 0)
  329.                         {
  330.                             // then an intersection has been found
  331.                             return true;
  332.                         }
  333.                     }
  334.  
  335.                     // Move to the next pixel in the row
  336.                     posInB += stepX;
  337.                 }
  338.  
  339.                 // Move to the next row
  340.                 yPosInB += stepY;
  341.             }
  342.  
  343.             // No intersection found
  344.             return false;
  345.         }
  346.  
  347.         /// <summary>
  348.         /// Calculates an axis aligned rectangle which fully contains an arbitrarily
  349.         /// transformed axis aligned rectangle.
  350.         /// </summary>
  351.         /// <param name="rectangle">Original bounding rectangle.</param>
  352.         /// <param name="transform">World transform of the rectangle.</param>
  353.         /// <returns>A new rectangle which contains the trasnformed rectangle.</returns>
  354.         public static Rectangle CalculateBoundingRectangle(Rectangle rectangle, Matrix transform)
  355.         {
  356.             // Get all four corners in local space
  357.             Vector2 leftTop = new Vector2(rectangle.Left, rectangle.Top);
  358.             Vector2 rightTop = new Vector2(rectangle.Right, rectangle.Top);
  359.             Vector2 leftBottom = new Vector2(rectangle.Left, rectangle.Bottom);
  360.             Vector2 rightBottom = new Vector2(rectangle.Right, rectangle.Bottom);
  361.  
  362.             // Transform all four corners into work space
  363.             Vector2.Transform(ref leftTop, ref transform, out leftTop);
  364.             Vector2.Transform(ref rightTop, ref transform, out rightTop);
  365.             Vector2.Transform(ref leftBottom, ref transform, out leftBottom);
  366.             Vector2.Transform(ref rightBottom, ref transform, out rightBottom);
  367.  
  368.             // Find the minimum and maximum extents of the rectangle in world space
  369.             Vector2 min = Vector2.Min(Vector2.Min(leftTop, rightTop),
  370.                                       Vector2.Min(leftBottom, rightBottom));
  371.             Vector2 max = Vector2.Max(Vector2.Max(leftTop, rightTop),
  372.                                       Vector2.Max(leftBottom, rightBottom));
  373.  
  374.             // Return that as a rectangle
  375.             return new Rectangle((int)min.X, (int)min.Y,
  376.                                  (int)(max.X - min.X), (int)(max.Y - min.Y));
  377.         }
  378.  
  379.         #endregion
  380.     }
  381. }