views:

1155

answers:

4

I am writing a small 2d game engine in C# for my own purposes, and it works fine except for the sprite collision detection. I've decided to make it a per-pixel detection (easiest for me to implement), but it is not working the way it's supposed to. The code detects a collision long before it happens. I've examined every component of the detection, but I can't find the problem.

The collision detection method:

public static bool CheckForCollision(Sprite s1, Sprite s2, bool perpixel) {
 if(!perpixel) {
  return s1.CollisionBox.IntersectsWith(s2.CollisionBox);
 }
 else {
  Rectangle rect;
  Image img1 = GraphicsHandler.ResizeImage(GraphicsHandler.RotateImagePoint(s1.Image, s1.Position, s1.Origin, s1.Rotation, out rect), s1.Scale);
  int posx1 = rect.X;
  int posy1 = rect.Y;

  Image img2 = GraphicsHandler.ResizeImage(GraphicsHandler.RotateImagePoint(s2.Image, s2.Position, s2.Origin, s2.Rotation, out rect), s2.Scale);
  int posx2 = rect.X;
  int posy2 = rect.Y;

  Rectangle abounds = new Rectangle(posx1, posy1, (int)img1.Width, (int)img1.Height);
  Rectangle bbounds = new Rectangle(posx2, posy2, (int)img2.Width, (int)img2.Height);

  if(Utilities.RectangleIntersects(abounds, bbounds)) {

   uint[] bitsA = s1.GetPixelData(false);

   uint[] bitsB = s2.GetPixelData(false);

   int x1 = Math.Max(abounds.X, bbounds.X);
   int x2 = Math.Min(abounds.X + abounds.Width, bbounds.X + bbounds.Width);

   int y1 = Math.Max(abounds.Y, bbounds.Y);
   int y2 = Math.Min(abounds.Y + abounds.Height, bbounds.Y + bbounds.Height);

   for(int y = y1; y < y2; ++y) {
    for(int x = x1; x < x2; ++x) {
     if(((bitsA[(x - abounds.X) + (y - abounds.Y) * abounds.Width] & 0xFF000000) >> 24) > 20 &&
      ((bitsB[(x - bbounds.X) + (y - bbounds.Y) * bbounds.Width] & 0xFF000000) >> 24) > 20)
      return true;
    }
   }
  }
  return false;
 }
}

The image rotation method:

internal static Image RotateImagePoint(Image img, Vector pos, Vector orig, double rotation, out Rectangle sz) {
 if(!(new Rectangle(new Point(0), img.Size).Contains(new Point((int)orig.X, (int)orig.Y))))
  Console.WriteLine("Origin point is not present in image bound; unwanted cropping might occur");
 rotation = (double)ra_de((double)rotation);
 sz = GetRotateDimensions((int)pos.X, (int)pos.Y, img.Width, img.Height, rotation, false);
 Bitmap bmp = new Bitmap(sz.Width, sz.Height);
 Graphics g = Graphics.FromImage(bmp);
 g.SmoothingMode = SmoothingMode.AntiAlias;
 g.InterpolationMode = InterpolationMode.HighQualityBicubic;
 g.PixelOffsetMode = PixelOffsetMode.HighQuality;
 g.RotateTransform((float)rotation);
 g.TranslateTransform(sz.Width / 2, sz.Height / 2, MatrixOrder.Append);
 g.DrawImage(img, (float)-orig.X, (float)-orig.Y);
 g.Dispose();
 return bmp;
}     
internal static Rectangle GetRotateDimensions(int imgx, int imgy, int imgwidth, int imgheight, double rotation, bool Crop) {
 Rectangle sz = new Rectangle();
 if (Crop == true) {
  // absolute trig values goes for all angles
  double dera = de_ra(rotation);
  double sin = Math.Abs(Math.Sin(dera));
  double cos = Math.Abs(Math.Cos(dera));
  // general trig rules:
  // length(adjacent) = cos(theta) * length(hypotenuse)
  // length(opposite) = sin(theta) * length(hypotenuse)
  // applied width = lo(img height) + la(img width)
  sz.Width = (int)(sin * imgheight + cos * imgwidth);
  // applied height = lo(img width) + la(img height)
  sz.Height = (int)(sin * imgwidth + cos * imgheight);
 }
 else {
  // get image diagonal to fit any rotation (w & h =diagonal)
  sz.X = imgx - (int)Math.Sqrt(Math.Pow(imgwidth, 2.0) + Math.Pow(imgheight, 2.0));
  sz.Y = imgy - (int)Math.Sqrt(Math.Pow(imgwidth, 2.0) + Math.Pow(imgheight, 2.0));
  sz.Width = (int)Math.Sqrt(Math.Pow(imgwidth, 2.0) + Math.Pow(imgheight, 2.0)) * 2;
  sz.Height = sz.Width;

 }
 return sz;
}

Pixel getting method:

public uint[] GetPixelData(bool useBaseImage) {
 Rectangle rect;
 Image image;
 if (useBaseImage)
  image = Image;
 else
  image = GraphicsHandler.ResizeImage(GraphicsHandler.RotateImagePoint(Image, Position, Origin, Rotation, out rect), Scale);

 BitmapData data;
 try {
  data = ((Bitmap)image).LockBits(new Rectangle(0, 0, image.Width, image.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
 }
 catch (ArgumentException) {
  data = ((Bitmap)image).LockBits(new Rectangle(0, 0, image.Width, image.Height), ImageLockMode.ReadOnly, image.PixelFormat);
 }

 byte[] rawdata = new byte[data.Stride * image.Height];
 Marshal.Copy(data.Scan0, rawdata, 0, data.Stride * image.Height);
 ((Bitmap)image).UnlockBits(data);
 int pixelsize = 4;
 if (data.PixelFormat == PixelFormat.Format24bppRgb)
  pixelsize = 3;
 else if (data.PixelFormat == PixelFormat.Format32bppArgb || data.PixelFormat == PixelFormat.Format32bppRgb)
  pixelsize = 4;

 double intdatasize = Math.Ceiling((double)rawdata.Length / pixelsize);
 uint[] intdata = new uint[(int)intdatasize];

 Buffer.BlockCopy(rawdata, 0, intdata, 0, rawdata.Length);

 return intdata;
}

The pixel retrieval method works, and the rotation method works as well, so the only place that the code might be wrong is the collision detection code, but I really have no idea where the problem might be.

+1  A: 

I don't think many people here will bother to scrutinize your code to figure out what exactly is wrong. But I can come with some hints to how you can find the problem.

If collision happens long before it is supposed to I suggest your bounding box check isn't working properly.

I would change the code to dump out all the data about rectangles at collision. So you can create some code that will display the situation at collision. That might be easier than looking over the numbers.

Apart from that I doubt that per pixel collision detection easier for you to implement. When you allow for rotation and scaling that quickly becomes difficult to get right. I would do polygon based collision detection instead.

I have made my own 2D engine like you but I used polygon based collision detection and that worked fine.

Adam Smith
I have written a small routine that draws lines on the bounding boxes at draw time. It doesn't interfere with the collision, so it's an effective debug. However, the "collisions" always occur when the boxes are intersecting, so there is nothing wrong with that code.
Bevin
A: 

I doubt this is the actual problem, but LockBits doesn't guarantee that the bits data is aligned to the image's Width.

I.e., there may be some padding. You need to access the image using data[x + y * stride] and not data[x + y * width]. The Stride is also part of the BitmapData.

configurator
But I thought that if the bits per pixel are 32, there is no padding; the pixel data takes up the entire 32 bits.
Bevin
I'll try it though.
Bevin
I think you're right - like I said I doubt this is the actual problem. You should still use the stride though, I believe.
configurator
I tried using the stride instead of the width, it didn't work. Gave me an ArrayIndexOutOfBoundsException, actually. The stride is 4 times the size of the width, so it must be related to the fact that I'm using one 32-bit integer for each pixel instead of four 8-bit integers.
Bevin
The stride is in bytes - so if you're going through an array of 32-bit ints you must divide it by four. Nevertheless, it's "the right thing to do".
configurator
A: 

I believe that there might be a problem with GetPixelData for Format24bppRgb, because in that case size of each pixel is 3 bytes, and you are still using BlockCopy to copy it to 32-bit Integer.

Groo
I answered your comment at first: I assumed that if pixeldata actually is 3 (which would rarely happen, I use PNGs) the last 8 bits become the padding, just 0s.
Bevin
In that case, each pixel has 3 bytes and is not aligned to 32 bits, so values would get pretty mixed up when copying (from a 24-bit array to a 32-bit array). But if you always have 32bpp, then the problem is somewhere else.
Groo
+1  A: 

I think I've found your problem.

internal static Rectangle GetRotateDimensions(int imgx, int imgy, int imgwidth, int imgheight, double rotation, bool Crop) {
    Rectangle sz = new Rectangle(); // <-- Default constructed rect here.
    if (Crop == true) {
            // absolute trig values goes for all angles
            double dera = de_ra(rotation);
            double sin = Math.Abs(Math.Sin(dera));
            double cos = Math.Abs(Math.Cos(dera));
            // general trig rules:
            // length(adjacent) = cos(theta) * length(hypotenuse)
            // length(opposite) = sin(theta) * length(hypotenuse)
            // applied width = lo(img height) + la(img width)
            sz.Width = (int)(sin * imgheight + cos * imgwidth);
            // applied height = lo(img width) + la(img height)
            sz.Height = (int)(sin * imgwidth + cos * imgheight);

            // <-- Never gets the X & Y assigned !!!
    }

Since you never assigned imgx and imgy to the X and Y coordinates of the Rectangle, every call of GetRotateDimensions will produce a Rectangle with the same location. They may be of differing sizes, but they will always be in the default X,Y position. This would cause the really early collisions that you are seeing because any time you tried to detect collisions on two sprites, GetRotateDimensions would put their bounds in the same position regardless of where they actually are.

Once you have corrected that problem, you may run into another error:

Rectangle rect;
Image img1 = GraphicsHandler.ResizeImage(GraphicsHandler.RotateImagePoint(s1.Image, s1.Position, s1.Origin, s1.Rotation, out rect), s1.Scale);
// <-- Should resize rect here.
int posx1 = rect.X;
int posy1 = rect.Y;

You get your boundary rect from the RotateImagePoint function, but you then resize the image. The X and Y from the rect are probably not exactly the same as that of the resized boundaries of the image. I'm guessing that you mean for the center of the image to remain in place while all points contract toward or expand from the center in the resize. If this is the case, then you need to resize rect as well as the image in order to get the correct position.

A. Levy
Sorry, but this can't be the cause (as much as would want it to). Look in the RotateImagePoint method. The GetRotateDimensions method is only called once, and Crop is set to false. The code in Crop == true was never finished, so it is never used.
Bevin
Well, what about the second bug with the rect that isn't resized? By the way, how far away are these sprites when the collision is detected? What is the size of the sprites, and what is the scaling factor used in GraphicsHnadler.ResizeImage?
A. Levy
You know, I did note that Crop was false and that I should only look at the false branch of the code, but I still examined the true branch. I guess that is what you get trying to debug code at 1 in the morning.
A. Levy