3.3. Elliptical Hit Tests
Although rectangular hit tests are appropriate in
some cases, in others it might be useful to test against a round sprite
shape. To facilitate this, we can perform an elliptical hit test.
The ellipse that will be tested will completely fill the rectangular region occupied by the sprite, as shown in Figure 4.
Figure 4. The elliptical test region contained within a rectangular sprite
Of course, ellipses, unlike circles, are affected by
rotation, so we need to take this into account when working out whether a
test point falls inside the ellipse. In Figure 5(a),
we can see a rotated ellipse whose scale is such that its width is
twice its height. Also marked in the figure are two test points, the
first of which is within the ellipse, whereas the second is not (though
it is within the bounds of the sprite rectangle).
The approach that we take to determine whether the
points are within the ellipse starts off the same as that used for the
rectangle: performing the calculation to rotate the points and the
ellipse back to an angle of zero. Once that has been done we can ignore
the rotation and concentrate just on the elliptical shape. Again, we
don't actually draw the sprite with the angle reset to zero or update
the sprite's properties; we just perform the calculations that would be
required for this rotation. If we were to draw the rotation, we would
end up with the arrangement shown in Figure 5(b).
Figure 5. Elliptical hit tests against a rotated scaled sprite
Having obtained the coordinates relative to an
unrotated ellipse, we can now determine whether the points are within
the ellipse or not. For a circle this would be easy: we would find the
radius of the circle and we would find the distance from the test point
to the center of the circle. If the point distance is less than the
radius, the point is inside the circle.
For an ellipse, this process is more complex,
however. An ellipse doesn't have a radius because the distance from its
center to its edge varies as the edge is traversed.
Fortunately, there is a very easy way to resolve
this. We know how the sprite has been scaled, so we can divide the width
of the ellipse by the scaled sprite width, and divide the height of the
ellipse by the scaled sprite height. This will result in a new ellipse
that is exactly one unit wide and one unit high. The size is less
important than the fact that this resulting size is now that of a circle
(with a radius of 0.5) rather than an ellipse, meaning that we can
perform calculations against it very easily. Instead of scaling the
sprite in this way, we can scale the test point and then see whether its
distance from the circle center is less than 0.5. If so, the point is a
hit; otherwise, it's a miss.
The steps required for the whole procedure are as follows; they are just like the steps for the rectangular hit test:
Move the touch point to be in object space rather than in screen space.
Rotate the point back by the sprite rotation angle.
Move the point to be relative to the center of the ellipse.
the point's x position by the ellipse width and its y position by the
ellipse height to scale down relative to a unit-width circle.
Test the point distance from the circle center to see whether it is within the circle's radius of 0.5.
Table 4-2 shows each of these calculations for each of the touch points shown in Figure 5.
The sprite in question is 64 × 64 pixels and has been scaled to be
double its normal width, resulting in an ellipse with a width of 128
pixels and a height of 64 pixels. Its center (and origin) is at the
coordinate (200, 100).
Table 2. Calculation steps to determine whether a test point is within a rotated scaled ellipse
|Test Point 1||Test Point 2|
|Screen coordinate||(224, 117)||(248, 134)|
|Object-space coordinate||(24, 17)||(48, 34)|
|Rotated coordinate||(27.6, 10.2)||(55.2, 20.4)|
|Ellipse width/height||128 pixels by 64 pixels|
|Rotated coordinate scaled by width and height||(0.216, 0.159)||(0.432, 0.318)|
|Distance from circle center at (0, 0)||0.268||0.536|
|Point contained within rectangle (distance <= 0.5)||Yes||No|
As this table shows, the test point 1 coordinate is
inside the ellipse (its calculated distance is less than 0.5), and the
test point 2 coordinate is not.
The complete function to perform this calculation is shown in Listing 9. This code is taken from the SpriteObject class, so it has direct access to the sprite's properties.
Example 9. Checking a test point to see if it is within a rotated and scaled sprite ellipse
protected bool IsPointInObject_EllipseTest(Microsoft.Xna.Framework.Vector2 point)
Vector2 rotatedPoint = Vector2.Zero;
// Retrieve the basic sprite bounding box
bbox = BoundingBox;
// Subtract the ellipse's top-left position from the test point so that the test
// point is relative to the origin position rather than relative to the screen
point -= Position;
// Rotate the point by the negative angle of the sprite to cancel out the sprite
rotatedPoint.X = (float)(Math.Cos(-Angle) * point.X - Math.Sin(-Angle) * point.Y);
rotatedPoint.Y = (float)(Math.Sin(-Angle) * point.X + Math.Cos(-Angle) * point.Y);
// Add back the origin point multiplied by the scale.
// This will put us in the top-left corner of the bounding box.
rotatedPoint += Origin * Scale;
// Subtract the bounding box midpoint from each axis.
// This will put us in the center of the ellipse.
rotatedPoint -= new Vector2(bbox.Width / 2, bbox.Height / 2);
// Divide the point by the width and height of the bounding box.
// This will result in values between −0.5 and +0.5 on each axis for
// positions within the bounding box. As both axes are then on the same
// scale we can check the distance from the center point as a circle,
// without having to worry about elliptical shapes.
rotatedPoint /= new Vector2(bbox.Width, bbox.Height);
// See if the distance from the origin to the point is <= 0.5
// (the radius of a unit-size circle). If so, we are within the ellipse.
return (rotatedPoint.Length() <= 0.5f);
3.4. Building the Hit Tests into the Game Framework
Checking touch points against game objects to see
whether they have been selected is an operation that will be common to
many games. To save each game from having to reimplement this logic, we
will build these checks into the game framework.
This procedure starts off as an abstract function in GameObjectBase called IsPointInObject, as shown in Listing 10. It expects a Vector2
parameter to identify the position on the screen to test and returns a
boolean value indicating whether that point is contained within the
Example 10. The abstract declaration for IsPointInObject contained inside GameObjectBase
/// Determine whether the specified position is contained within the object
public abstract bool IsPointInObject(Vector2 point);
To implement the IsPointInObject function for sprites, it is overridden within SpriteObject.
We will enable our sprites to support testing against both the
rectangular and elliptical tests that we have described, and to allow
the game to specify which type of test to use a new property is added to
the class, named AutoHitTestMode. The property is given the AutoHitTestModes enumeration as its type, allowing either Rectangle or Ellipse to be selected.
The SpriteObject implementation of IsPointInObject checks to see which of these hit modes is selected and then calls into either IsPointInObject_RectangleTest (as shown in Listing 8) or IsPointInObject_EllipseTest (as shown in Listing 9). Any game object can thus have its AutoHitTestMode property set at initialization and can then simply test points by calling the IsPointInObject function.
For sprites that need to perform some alternative or
more complex processing when checking for hit points (perhaps just as
simple as only allowing a hit to take place under certain conditions or
perhaps implementing entirely new region calculations), the IsPointInObject can be further overridden in derived game object classes.