Collision Detection

转自 http://algorithm.126.com

Collision Detection

Getting the most out of your collision tests

Dave Roberts

Dave is the author of PC Game Programming Explorer (Coriolis Group Books, 1994). He can be reached via CompuServe at 75572,1151.


Collision detection is fundamental to most fast-action arcade games. After all, the program has to know when a missile slams into an alien spaceship or when the giant frog leaps onto a player. If coded incorrectly, however, collision detection can take an inordinate amount of time, decrease the animation frame rate, and ruin an otherwise enjoyable game. In this article, I'll examine the basic collision-detection problem, describe situations that can cause collision-detection code to run slowly, and suggest ways to keep your code skipping along.

Explosive Growth

There are two main reasons why collision-detection code runs slowly: Either the code performs more collision tests than necessary or the individual collision tests themselves are slow. For the first case, imagine a game with two objects on the field of play. You want to determine if the objects have collided. With only two objects, collision detection is easy--you simply test whether object1 is touching object2 or object2 is touching object1. One of these tests is obviously redundant--if object1 is touching object2, then object2 is also touching object1.

A game with three objects requires three collision tests: object1 with object2, object1 with object3, and object2 with object3. With four objects the number of tests increases to six. Table 1 shows how many tests must be performed to check from 2 to 20 objects. The numbers were derived from the formula (n2--n)/2, where n is the number of objects; given n objects, you can determine which objects collide by exhaustively performing n2 tests. Since you don't compare an object with itself, you eliminate n tests, giving n2--n. And since testing object x with object y is the same as testing object y with object x, you eliminate half of the remaining tests, yielding the final formula.

O-notation describes how the run-time or memory requirements of a problem grow as the number of inputs or outputs, n, changes. In this case, n is the number of game objects for which you must calculate collision status, so the algorithm is O(n2). As n increases, the number of required tests increases in roughly the same way as n2, and the run time of the collision-testing code increases at a rate of roughly n2.

This means that having several objects active at once can slow your collision-testing code if you aren't careful. Doubling the number of objects on the screen roughly quadruples the number of tests you must perform. But it could be worse: Algorithms that are O(n3) or O(2n) increase run time much more quickly. Of course, it could also be better: An O(n) algorithm increases at a constant rate depending on how many inputs are fed to it. Doubling the number of inputs doubles the run time. An O(1) algorithm is pure joy because it runs at the same rate no matter how many inputs are fed to it.

Eliminating Collision Tests

Techniques for reducing the number of collision tests include game-rule elimination and eliminations based on spatial position such as axis sorting and the sector method.

Game rules typically dictate which objects are allowed to collide. Consequently, you can reduce the number of collision tests. For instance, imagine writing a game where the player controls a spaceship that shoots aliens. Since you're the programmer, you make the rules. You may decide that aliens can't collide with each other and that the player's ship can't collide with its own missiles. Alien missiles probably won't hit aliens. Player missiles won't hit other player missiles and alien missiles won't hit other alien missiles, although perhaps you should allow player missiles to hit other alien missiles.

Now assume that you have 1 player spaceship, 5 player missiles, 20 aliens, and 10 alien missiles all active in the game at a given point in time. That's 36 total objects, which would require 1296 tests (n2) if every object had to be compared with every other object. Our formula reduces that to 630 tests ((n2--n)/2), but taking the game rules into account reduces this number even further.

The game rules dictate that the player spaceship must be tested against each alien (20 tests) and each alien missile (10 tests); the player missiles must be tested against the aliens (100 tests) and the alien missiles (50 tests). That's it--180 tests total. Since aliens, player missiles, and alien missiles can't collide with objects of the same type, 450 tests (71 percent) of the 630 tests are eliminated.

The real advantage to this approach can be seen when you add one more alien to the screen. Using the original formula, this would require an increase of 36 tests; but because of the game rules, only six must be added (the alien tested against the player spaceship and each player missile).

Spatial-Test Elimination

Some game rules require that every object be tested with every other one. In such situations, it's best to use spatial-test elimination techniques.

Spatial-test elimination works by sorting game objects according to their position on the screen or play field (if the play field is larger than the screen itself). Objects are then tested for collisions with other objects near them. Objects farther away are not tested because they could not possibly be colliding.

Spatial elimination requires extra computation to sort the various objects and perform additional bookkeeping. The key is to make the bookkeeping run quickly and eliminate as many tests as possible. When bookkeeping saves more time than it adds, spatial elimination is worth the effort. It is better suited for games in which a large number of objects are active at once. If a game has only a few objects, there aren't many potential collision tests to eliminate, and the added bookkeeping can exceed the gain.

Axis Sorting

When many sprites are on the screen at one time, they are rarely at the same location. An easy way to eliminate collision tests is to sort the sprites in ascending order using each sprite's x- or y-axis coordinate as a key. To check for collisions, simply test a given sprite with the next few on the list. Stop testing sprites when you reach a sprite further down the list whose location has a key greater than the location of the first sprite, plus its width or height. For example, suppose you have five sprites (as in Table 2) sorted according to their x-coordinates. First, you'd test sprite #1 with sprite #2, then with sprite #3. At sprite #4, you'd stop because sprite #4 is located at x-coordinate 40 and sprite #1's right side is located at x-coordinate 19 (x-coordinate 10+width of 10--1). Since every sprite after sprite #4 has an x-coordinate greater than sprite #4, there is no need to check any further. You'd then continue with sprite #2, and so on, for a total of four tests. Listing One is an implementation of this method.

This method relies on the assumption that sprites are usually distributed across the screen. If the sprites are closely grouped together along the sorting axis, this method will degenerate to the (n2--n)/2 situation.

You could just as easily sort the sprites by y-coordinate. Since the y-resolution of the screen is usually less, however, using the x-coordinate tends to spread out the sprites. Before using either coordinate axis for this method, you should examine your situation to determine which axis will give you better results in general.

I haven't covered actual techniques for sorting the sprites because this is an implementation issue that varies depending on how the sprites in the game move. A common technique is to keep a doubly linked list of sprites in sorted order at all times. After the movement routine calculates the sprite's new position for the next animation frame, it moves the sprite forward or backward in the linked list to keep it sorted. Typically, a sprite's position will only move a few pixels per frame, leading to only one or two position changes relative to the other sprites in the linked list. Frequently, no position changes will be needed. Note that general-purpose sorting routines are not usually necessary or even beneficial. Most of the time, the linked list is nearly sorted anyway. General-purpose sorts like quicksort are overkill and can even show their worst-case running times when the objects are nearly sorted.

The Sector Method

The sector method divides the screen or play field into a grid of sectors. In the simplest case, the screen is divided into quadrants. You determine which sector each sprite is located in according to its position on the screen. You then perform standard collision testing among all the sprites within a given sector. Assuming that sprites are usually well distributed around the screen, most sectors will only contain a few sprites; some may not contain any. For instance, Figure 1 shows 6 sprites on a screen divided into 16 sectors. Sprites #1 and #2 are located in the same sector and would be tested against each other; the same goes for sprites #4 and #5. Sprite #3 is all alone in its sector and would not be tested against anything.

9513a1f1.gif

Figure 1

The primary difficulty of the sector method is in handling sprites that fall into more than one sector--sprite #6, for instance. Typically, the sprite is included in all sectors it covers. As long as sprites are smaller than sectors, a single sprite can only be located in one to four sectors. The locations of the sprite's corners determine in which sectors the sprite is located. If sprites can be larger than individual sectors, determining which sectors a sprite covers can involve more calculations.

Try to make sector widths and heights a power of 2 larger than the maximum size of the sprites. This approach makes determining a sprite's sector location much easier because you can simply divide its coordinate values by a power of 2. Dividing by a power of 2 is the same as left shifting the coordinate value, so no slow division instructions or routines need to be invoked. Sectors don't have to be square: The vertical size of a sector can be different than its horizontal size. This lets you optimize the number of sectors versus the sector size to more closely fit the needs of your particular game.

Hybrid Techniques

Sometimes no single technique can acceptably reduce the number of collision tests. This happens when many sprites cluster around each other. Using both game rules and spatial techniques can help out in these cases. For instance, if you're using the sector technique and find that many sprites end up in the same sectors, use game rules to reduce the number of collision tests among sprites within each sector.

Quick Tests

Generally, collision-testing methods are either bounding or pixel based. Bounding methods are fast, imperfect tests. Pixel-based methods are more precise, but much slower. Novice game programmers often make the mistake of using pixel-based methods for all collision tests. Even with aggressive test-reduction techniques, a pixel-based collision algorithm can slow a game down if it's used for every test.

Bounding methods don't compare actual sprite pixels with each other. Instead, a geometric object, typically a rectangle, is used to represent the sprite object in the test. The rectangle is sized to enclose just the pixels that make up the sprite. The advantage is that a few simple tests can determine whether two rectangles touch. Listing Two shows how to test two rectangles for overlap. The test determination takes no more than four comparisons. In fact, since logical expressions are evaluated in a short-circuit fashion in C, most tests will stop before the whole logical expression has been evaluated. The order of the tests in Listing Two is arbitrary. If objects in a specific game are typically above and below one another and collide horizontally, you can put the vertical tests first. Finally, the function call may take up much of the execution time in Listing Two. You can easily make CollisionTestRect into a C preprocessor macro using the ?: ternary operator. This allows the test to be expanded inline, saving the time required for a function call.

The only problem with bounding methods is that they are imperfect. Unless your sprite is shaped very much like a rectangle, for instance, some pixels enclosed by the bounding rectangle won't be a part of your sprite. Those pixels may be a part of your sprite bitmap, but they are transparent. Technically, these pixels are not part of the object but will be considered so for the purpose of testing collisions. Thus, a collision might be indicated between two objects when only a few transparent pixels have actually intersected.

Perfect collision detection requires pixel-based collision detection, in which the pixels of the bitmap are tested to see if they overlap in the current animation frame. Some methods use an auxiliary buffer, where sprites are drawn into the buffer as they are drawn to video memory and tests are performed as each pixel is drawn. This approach requires a lot of memory, however, and can slow down sprite-drawing code.

Another method uses much less memory by first calculating a collision map for each sprite. A collision map is a bitmap with one bit representing each pixel in the original bitmap, which could be larger, a 256-color byte-per-pixel bitmap, for instance. For each solid pixel in the original bitmap, the corresponding bit of the collision map is set to 1; for each transparent pixel, the corresponding bit is set to 0.

The program takes bytes from the two collision maps and aligns them according to the original sprites' positions using shifts and rolls. A logical AND is then performed between the two collision-map bytes. If any bits of the result are set to 1, then nontransparent pixels are colliding. Listing Three shows code that tests two collision maps.

Unfortunately, pixel-based collision testing runs slowly. It requires much more code than the bounding-rectangle test, and consequently, more run time. The ideal test would run as fast as the bounding rectangle test but retain the accuracy of the collision map test. Fortunately, this is possible.

Fast and Accurate

Quick elimination is the secret to fast collision detection. Most collision tests indicate a negative result. If a game runs at a frame rate of 20 frames per second (fps) and two objects collide about once a second, that's only one collision every 20 animation frames. Even if there are many objects on screen that collide fairly frequently, a given object doesn't collide with most of the others. Therefore, it makes no sense to use an accurate but slow collision-test method for every test. A bounding-rectangle collision test can determine that two objects aren't anywhere near each other as well as a pixel-based method, and is much faster. The only problem is that it will sometimes falsely indicate a collision when transparent pixels of the sprite within the bounding rectangle overlap.

Using both a bounding-rectangle test and a pixel-based test in tandem yields both speed and accuracy. First, the combined algorithm performs a bounding-rectangle test. If the sprites don't collide (the usual case), then the bounding-rectangle test indicates a negative result very quickly and the program proceeds to the next pair of sprites. If the bounding-rectangle test indicates a collision, however, the program performs a pixel-based test to determine whether the bounding-rectangle test was correct. The pixel-based test runs much more slowly than the bounding-rectangle test, but it is invoked infrequently.

Acknowledgments

The author gratefully acknowledges Coriolis Group Books for use of some material from the book PC Game Programming Explorer.

Table 1: Number of collision-detection tests that must be performed for a given number of objects.


Objects      Collision Tests   
2            1
3            3
4            6
5            10
6            15
7            21
8            28
9            36
10           45
15           105
20           190

Table 2: Sprites sorted in ascending order by x-coordinate.


Sprite                    Sprite
Number    X-coordinate    Width     
1         10                10
2         15                 5
3         18                 8
4         40                10
5         45                10

Figure 1
Six sprites and their sector locations.

Listing One
 
 
ExpandedBlockStart.gif ContractedBlock.gif typedef  struct  _SPRITE_DATA  dot.gif {
InBlock.gif    
struct _SPRITE_DATA * Next;
ExpandedSubBlockStart.gifContractedSubBlock.gif    
int Top;    /**//* sprite location */
InBlock.gif    
int Left;
ExpandedSubBlockStart.gifContractedSubBlock.gif    
int Width;  /**//* sprite dimensions */
InBlock.gif    
int Height;
ExpandedBlockEnd.gif}
 SPRITE_DATA;
None.gif
ExpandedBlockStart.gifContractedBlock.gif
/**/ /*
InBlock.gif    Function: CollisionTestSorted
InBlock.gif    Description:
InBlock.gif        Tests a linked list of sorted sprites to see if they
InBlock.gif        potentially overlap.  If so, they are collision tested.
ExpandedBlockEnd.gif
*/

None.gif
void  CollisionTestSorted(SPRITE_DATA  *  SpriteList)
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
InBlock.gif    SPRITE_DATA 
*s1, *s2;
InBlock.gif    
int s1Right;
InBlock.gif
InBlock.gif    s1 
= SpriteList;
ExpandedSubBlockStart.gifContractedSubBlock.gif    
while (s1 != NULL) dot.gif{
InBlock.gif        s1Right 
= s1->Left + s1->Width - 1;
ExpandedSubBlockStart.gifContractedSubBlock.gif        
/**//* Compare s1 with all following sprites until left edge */
ExpandedSubBlockStart.gifContractedSubBlock.gif        
/**//* of a following sprite is located beyond the right */
ExpandedSubBlockStart.gifContractedSubBlock.gif        
/**//* edge of s2. */
InBlock.gif        s2 
= s1->Next;
ExpandedSubBlockStart.gifContractedSubBlock.gif        
while (s2 != NULL && (s1Right > s2->Left)) dot.gif{
InBlock.gif            CollisionTest(s1, s2);
InBlock.gif            s2 
= s2->Next;
ExpandedSubBlockEnd.gif        }

InBlock.gif        s1 
= s1->Next;
ExpandedSubBlockEnd.gif    }

ExpandedBlockEnd.gif}

None.gif
None.gif
None.gif
None.gifListing Two
ExpandedBlockStart.gifContractedBlock.giftypedef 
struct   dot.gif {
InBlock.gif    
int Left;
InBlock.gif    
int Top;
InBlock.gif    
int Right;
InBlock.gif    
int Bottom;
ExpandedBlockEnd.gif}
 RECT;
None.gif
ExpandedBlockStart.gifContractedBlock.gif
/**/ /*
InBlock.gif    Function: CollisionTestRect
InBlock.gif    Description:
InBlock.gif        Tests two bounding rectangles to see if they overlap.
InBlock.gif        Returns TRUE if so, FALSE otherwise.
ExpandedBlockEnd.gif
*/

None.gifBOOL CollisionTestRect(RECT 
*  r1, RECT  *  r2)
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
InBlock.gif    
if (r1->Left > r2->Right || r2->Left > r1->Right ||
ExpandedSubBlockStart.gifContractedSubBlock.gif        r1
->Top > r2->Bottom || r2->Top > r1->Bottom) dot.gif{
InBlock.gif        
return FALSE;
ExpandedSubBlockEnd.gif    }

ExpandedSubBlockStart.gifContractedSubBlock.gif    
else dot.gif{
InBlock.gif        
return TRUE;
ExpandedSubBlockEnd.gif    }

ExpandedBlockEnd.gif}

None.gif
None.gif
None.gif
None.gifListing Three
None.giftypedef unsigned 
int  UINT16;
None.giftypedef unsigned 
char  UINT8;
None.gif
ExpandedBlockStart.gifContractedBlock.giftypedef 
struct   dot.gif {
ExpandedSubBlockStart.gifContractedSubBlock.gif    UINT16  Width;  
/**//* sprite pixel width / 8 bits per pixel */
InBlock.gif    UINT16  Height;
ExpandedSubBlockStart.gifContractedSubBlock.gif    UINT8   Data;       
/**//* first byte of variable length data */
ExpandedBlockEnd.gif}
 COLLISION_MAP;
None.gif
ExpandedBlockStart.gifContractedBlock.gif
/**/ /*
InBlock.gif    Function: CollisionTestBitmap
InBlock.gif    Description:
InBlock.gif        Tests two objects using COLLISION_MAPs.  The upper left corner
InBlock.gif        of each object is specified with (x1, y1) and (x2, y2).
ExpandedBlockEnd.gif
*/

None.gifBOOL CollisionTestBitmap
None.gif    (
None.gif    COLLISION_MAP far 
*  Object1,
None.gif    COLLISION_MAP far 
*  Object2,
None.gif    
int  x1,
None.gif    
int  y1,
None.gif    
int  x2,
None.gif    
int  y2
None.gif    )
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
InBlock.gif    UINT8 far 
* Data1;
InBlock.gif    UINT8 far 
* Data2;
InBlock.gif    COLLISION_MAP far 
* SwapTemp;
InBlock.gif    
int DeltaX;
InBlock.gif    
int DeltaY;
InBlock.gif    
int Shift;
InBlock.gif    
int Skip;
InBlock.gif    UINT16 WidthCounter1;
InBlock.gif    UINT16 WidthCounter2;
InBlock.gif    UINT16 HeightCounter1;
InBlock.gif    UINT16 HeightCounter2;
InBlock.gif    UINT8 Object1Data;
InBlock.gif    UINT8 ShiftRegister;
InBlock.gif    UINT8 OldObject2Data;
InBlock.gif    UINT8 NewObject2Data;
InBlock.gif    UINT8 FinalObject2Data;
InBlock.gif
InBlock.gif    assert(Object1 
!= NULL);
InBlock.gif    assert(Object2 
!= NULL);
InBlock.gif
InBlock.gif    DeltaX 
= x2 - x1;
InBlock.gif    DeltaY 
= y2 - y1;
InBlock.gif
ExpandedSubBlockStart.gifContractedSubBlock.gif    
/**//* swap objects to make the algorithm work */
ExpandedSubBlockStart.gifContractedSubBlock.gif    
if (DeltaX < 0dot.gif{
InBlock.gif        SwapTemp    
= Object1;
InBlock.gif        Object1     
= Object2;
InBlock.gif        Object2     
= SwapTemp;
InBlock.gif        DeltaX      
= -DeltaX;
InBlock.gif        DeltaY      
= -DeltaY;
ExpandedSubBlockEnd.gif    }

InBlock.gif
InBlock.gif    Data1           
= (UINT8 far *&(Object1->Data);
InBlock.gif    Data2           
= (UINT8 far *&(Object2->Data);
InBlock.gif
InBlock.gif    HeightCounter1  
= 0;
InBlock.gif    HeightCounter2  
= 0;
InBlock.gif
ExpandedSubBlockStart.gifContractedSubBlock.gif    
/**//* skip rows off the object with the least Y-value */
ExpandedSubBlockStart.gifContractedSubBlock.gif    
if (DeltaY > 0dot.gif{
InBlock.gif        Data1 
+= Object1->Width * DeltaY;
InBlock.gif        HeightCounter1 
+= DeltaY;
ExpandedSubBlockEnd.gif    }

ExpandedSubBlockStart.gifContractedSubBlock.gif    
else if (DeltaY < 0dot.gif{
InBlock.gif        Data2 
+= Object2->Width * -DeltaY;
InBlock.gif        HeightCounter2 
-= DeltaY;
ExpandedSubBlockEnd.gif    }

InBlock.gif
ExpandedSubBlockStart.gifContractedSubBlock.gif    Shift   
= DeltaX % 8;   /**//* amount to shift object 2 data to right */
ExpandedSubBlockStart.gifContractedSubBlock.gif    Skip    
= DeltaX / 8;   /**//* number of bytes to skip at beginning of */
ExpandedSubBlockStart.gifContractedSubBlock.gif                            
/**//*   object 1 data line */
InBlock.gif
InBlock.gif    
while (HeightCounter1 < Object1->Height &&
ExpandedSubBlockStart.gifContractedSubBlock.gif        HeightCounter2 
< Object2->Height) dot.gif{
InBlock.gif
ExpandedSubBlockStart.gifContractedSubBlock.gif        
/**//* potentially skip a few bytes 'cause obj 1 is to left of obj 2 */
InBlock.gif        WidthCounter1   
= Skip;
InBlock.gif        Data1           
+= Skip;
InBlock.gif
InBlock.gif        WidthCounter2   
= 0;
InBlock.gif        OldObject2Data  
= 0;
InBlock.gif
InBlock.gif        
while (WidthCounter1 < Object1->Width &&
ExpandedSubBlockStart.gifContractedSubBlock.gif            WidthCounter2 
< Object2->Width) dot.gif{
InBlock.gif
ExpandedSubBlockStart.gifContractedSubBlock.gif            
/**//* get data */
InBlock.gif            Object1Data     
= *Data1++;
InBlock.gif            NewObject2Data  
= *Data2++;
ExpandedSubBlockStart.gifContractedSubBlock.gif            
/**//* shift object 2 data to correct delta X differential */
InBlock.gif            ShiftRegister   
= ((UINT16) OldObject2Data << 8|
InBlock.gif                (UINT16) NewObject2Data;
InBlock.gif            ShiftRegister   
>>= Shift;
InBlock.gif            FinalObject2Data 
= ShiftRegister & 0xFF;
InBlock.gif
ExpandedSubBlockStart.gifContractedSubBlock.gif            
/**//* return if we have a collision */
ExpandedSubBlockStart.gifContractedSubBlock.gif            
if (Object1Data & FinalObject2Data) dot.gif{
InBlock.gif                
return TRUE;
ExpandedSubBlockEnd.gif            }

InBlock.gif
InBlock.gif            OldObject2Data 
= NewObject2Data;
InBlock.gif            WidthCounter1
++;
InBlock.gif            WidthCounter2
++;
ExpandedSubBlockEnd.gif        }

InBlock.gif
ExpandedSubBlockStart.gifContractedSubBlock.gif        
/**//* correct pointers at end of line */
InBlock.gif        Data1 
+= Object1->Width - WidthCounter1;
InBlock.gif

转载于:https://www.cnblogs.com/jerryhong/articles/698674.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值