PDA

View Full Version : CGRect Collision Detection




Blakeasd
Aug 4, 2013, 06:48 PM
Hello,

I am experimenting with CGRects and am trying to work on some simple collision detection. Here is my problematic mouseDragged: code


-(void)mouseDragged:(NSEvent *)theEvent{

NSPoint eventLocation = [theEvent locationInWindow];
NSPoint location = [self convertPoint:eventLocation fromView:self];

int locationX = location.x;
int locationY = location.y;


[self setNeedsDisplay:TRUE];

if (CGRectIntersectsRect(myRect, anotherRect)==true) {
NSLog(@"Two rects collide");
rectCollision = true;
}else{

rectCollision = false;

}



if(rectCollision == false){

if(isInMyRect == true){

myRect.origin.x = (locationX-50);
myRect.origin.y = (locationY-50);


}
}




if (isInAnotherRect == true) {
anotherRect.origin.x = (locationX-50);
anotherRect.origin.y = (locationY-50);
}



}



(Note that I haven't implemented dragging collision for anotherRect yet. I'm just waiting to find a good way to do it.)
Everything is great until an actual collision occurs. Once a collision happens, I can no longer drag myRect anymore. This is because rectCollision is ticked to true. Is there a way to see if myRect is moving away from the other rect, so that I can untick rectCollision to false?

Hopefully my question isn't too incoherent.

Thanks!



Sander
Aug 5, 2013, 03:31 AM
The trick in cases like these is that you keep two rectangles around: One is the visible one as it is drawn, the other is a "virtual" one keeping track of where the rectangle would have been if there were no contraints. You test the virtual one for constraint violations. During the mouse drag, check the virtual one for violations and leave the visible one to the last valid position if violations occur. Else, set the visible one to be the same as the virtual one. On mouseup, set the virtual back to the visible.

Blakeasd
Aug 5, 2013, 02:19 PM
Thanks, I think I understand what you're saying. So should the invisible one be the same size and be at the same exact position as the visible?

Thanks for your response!

Sander
Aug 6, 2013, 02:55 AM
Thanks, I think I understand what you're saying. So should the invisible one be the same size and be at the same exact position as the visible?

No, if they're identical, there's no point in having two :-)

The algorithm is:

On mousedown, create a copy of your rect. This copy is never displayed.
During a drag, modify the position of the invisible copy. Use this copy to calculate your collision detection/constraints. If there are no collisions/constraint violations, copy the invisible rect to the visible one. If there are, don't.
On mouseup, throw away the invisible copy again.

Blakeasd
Aug 6, 2013, 11:16 AM
No, if they're identical, there's no point in having two :-)


That makes sense, haha.

I tried out your method and it works wonderfully! I enjoyed the crafty-"ness" of it.

The only problem is that if the mouse moves too quickly, the invisible collision occurs too quickly and the actual rectangle never makes a collision.

The collision between the two is almost there -- but not quite.

Here is my if-statement that does the moving (mouseDragged):


if (isInMyRect == true) {
myRectInvisible.origin.x = (locationX - 50);
myRectInvisible.origin.y = (locationY- 50);

if (CGRectIntersectsRect(myRectInvisible, anotherRect) != true) {
myRect.origin.x = myRectInvisible.origin.x;
myRect.origin.y = myRectInvisible.origin.y;
}


}


Is using if-statments here inefficient?

Thanks for all of your help!

Sander
Aug 6, 2013, 02:32 PM
The only problem is that if the mouse moves to quickly, the invisible collision occurs to quickly and the actual rectangle never makes a collision.

I was already afraid that would happen. The reason is that when you move the mouse quickly, you're not getting all intermediate mouse positions. So where in the previous mousemove everything was fine, the current one may land you past the collision point.

This takes a bit more effort to get right. The best option would be

if (collision detected)
{
set visible rect to nearest non-colliding position
}

There are various ways to do this; in your case it would probably make sense to interpolate a straight line between the previously known "good point" (which you still have, since you haven't updated the visible rectangle yet at this point) and the current, "very bad" point, intersect this with the colliding rectangle, and position the visible rectangle at this position.

Be sure to do this only the first time you detect a collision, or your interpolating routine will allow the visible rectangle to slowly "tunnel through" the obstacle if you continue wiggling the mouse during a collision.

Welcome to engineering, where "the remaining 10% of the work takes the remaining 90% of the effort"...

Blakeasd
Aug 6, 2013, 07:20 PM
I've just written code that resets the rect to the last OK position, but I'm running into the same problem. Here is my new mouseDragged code:


-(void)mouseDragged:(NSEvent *)theEvent{

NSPoint eventLocation = [theEvent locationInWindow];
NSPoint location = [self convertPoint:eventLocation fromView:self];

int locationX = location.x;
int locationY = location.y;

NSMutableArray *previousMyRectOrigin;
previousMyRectOrigin = [[NSMutableArray alloc] initWithCapacity:2];

if (isInMyRect == true) {
myRectInvisible.origin.x = (locationX - 50);
myRectInvisible.origin.y = (locationY- 50);


if (CGRectIntersectsRect(myRectInvisible, anotherRect) != true) {

myRect.origin.x = myRectInvisible.origin.x;
myRect.origin.y = myRectInvisible.origin.y;

NSNumber *myRectXValue = [NSNumber numberWithFloat:myRect.origin.x];
NSNumber *myRectYValue = [NSNumber numberWithFloat:myRect.origin.y];
[previousMyRectOrigin insertObject:myRectXValue atIndex:0];
[previousMyRectOrigin insertObject:myRectYValue atIndex:1];

previousMyRectXValue = [[previousMyRectOrigin objectAtIndex:0] floatValue];
previousMyRectYValue = [[previousMyRectOrigin objectAtIndex:1]floatValue];

}else{

NSLog(@"X: %f",previousMyRectXValue);
NSLog(@"Y: %f",previousMyRectYValue);

myRect.origin.x = previousMyRectXValue;
myRect.origin.y = previousMyRectYValue;
}

}

[self setNeedsDisplay:TRUE];

}


How should I go about interpolation now that I have the last good value stored at all times? Setting the square to the last position is the same as what was happening before when I tested for collision...I think.

Sander
Aug 7, 2013, 02:49 AM
No, this isn't solving your problem. First, get rid of that mutable array, that's overkill. Second, you set the previous position in the drag, and you should deal with the previous position from the previous drag. So this is no good.

You already have the relevant previous position, since that's simply myRect. This contains the "last known good position" (and also the currently visible one).

Your current implementation is:

- I have a rect somewhere (myRect)
- I want to move it somewhere else (myRectInvisible)
- If that's allowed, set myRect = myRectInvisible
- If not, leave myRect alone.

(Note that you don't need to have myRectInvisible as an instance variable, since it's only ever used inside your mousedrag.)

Now, the problem you're trying to solve is that this algorithm prevents collisions, but doesn't "look" nice, and you want to change the last part of the algorithm:

- If not, place myRect so that it moves to the "best possible" position without causing a collision.

For this, I suggested you need to interpolate between the current position (myRect) and the desired but disallowed position (myRectInvisible). Somewhere between those positions, there is a position which almost collides.

To figure this out, draw it out on a sheet of paper. Draw the obstacle (the rectangle you're colliding with) in red, draw the original position in green, and the desired position in blue (or something like that) - which will intersect with the red one. Then, work out how to find the position at which a rectangle moving from the green position to the blue position would "bump" into the red one. You may need some if-statements, since it may bump with either of its four sides.

If you lack the math skills to work this out, then there is an (extremely inelegant) alternative: Simply "pretend" that you are receiving all the intermediate mouse positions (in a straight line from the previous mousedrag to the current one) and iterate over them until you detect the collision.