Aegidius
 Plüss   Aplulogo
     
 www.aplu.ch      Print Text 
© 2021, V10.4
 
  JGameGrid
 
 

The source code of all examples is included in the JGameGrid distribution.

Ex08, Ex08a: Actor Collision Detection

In many game scenarios the interaction between actors is of great importance. In the classical Space Invaders game for instance the bombs will destroy the aliens, so a bomb must be "aware" to hit an alien. If you don't like shooters you still have actor interaction in innocent games or simulations whenever an actor encounters an obstacle. In general collision happens when two two-dimensional areas overlap. These "collision areas" may be simple geometric shapes like rectangles, circles, lines or points or the set of the non-transparent pixels of a sprite image. Despite for a human it is simple to detect if two areas overlap it is a non-trivial problem for a computer program. If we assume that the areas are sets of pixel points, a brute-force algorithm would check if any of the points of the first area is part of the second area. Even if the areas are small, say 50 x 50 pixels wide, the check would involve 2500 x 2500 comparisons, too much to be done in real-time, where we should react within milliseconds.

To overcome this problem we attach a simple shape as collision area to the sprite image and handle the collision between these shapes. In JGameGrid the default collision area of a sprite the bounding rectangle. If the actor is made rotatable, the bounding rectangle will rotate too. If this default is not accurate enough, you may define a collision rectangle of arbitrary size, orientation and location relative to the sprite image. Instead of a rectangle you can use a circle, line or point collision area at any location relative to the image. If one collision partner has a point or circle collision area, collision detection with non-transparent pixels of the second actor image is provided.

Even for simple geometric shape like (rotated) rectangles the collision detection is not an easy task and requires sophisticated algorithms to be executed fast enough in real-time. JGameGrid implements a high-performance algorithm based on the Separating Axis Theorem (SAT). You can find information about the SAT by searching the internet.

To prepare a nice collision demonstration we first construct an actor class with a sprite image that can be positioned by dragging it with the mouse. We select a dart because in our demonstration we will bring a balloon to explosion by picking it with the dart needle. We make the Dart class a GGMouseListener and implement mouseEvent(). Here we first retrieve the current cursor position and set the actor to this location (We need a conversion method to transform the mouse coordinates to location indices.) We want to give the dart the direction of the moving mouse cursor. The idea to do this is simple: the mouse cursor follows a curve and we determine the direction of a tangent by using two adjacent positions, the last and the current one. If the two points are too close together, the accuracy of the direction is not good enough and we discard it.

Because we will only register the lDrag mouse event, there is no need to distinguish it from other mouse events at the beginning of the callback method.

import ch.aplu.jgamegrid.*;

public
 class Dart extends Actor implements GGMouseListener
{
  
private Location oldLocation = new Location();

  
public Dart()
  
{
    
super(true"sprites/dart.gif");  // Rotatable
  
}

  
public boolean mouseEvent(GGMouse mouse)
  
{
    Location location 
=
      
gameGrid.toLocationInGrid(mouse.getX(), mouse.getY());
    
setLocation(location);
    
double dx = location.x - oldLocation.x;
    
double dy = location.y - oldLocation.y;
    
if (dx * dx + dy * dy < 25)
      
return true;
    
double phi = Math.atan2(dy, dx);
    
setDirection(Math.toDegrees(phi));
    oldLocation.x 
= location.x;
    oldLocation.y 
= location.y;
    
return true;
  
}
}

The purpose of our application is only to create a test environment for this class. This is typical for many software projects where you want to test parts of the system before assembling them into the final application. Look how the GGMouseListener is registered using the GGMouse.lDrag mask. We have to start the simulation cycling in order to obtain an automatic refresh.

import ch.aplu.jgamegrid.*;
import
 java.awt.Color;

public
 class Ex08 extends GameGrid
{
  
public Ex08()
  
{
    
super(400, 300, 1, false);
    
setTitle("Move dart using mouse left button drag");
    
setSimulationPeriod(50);
    
setBgColor(new Color(182, 220, 234));
    Dart dart 
= new Dart();
    
addActor(dart, new Location(50, 50));
    
addMouseListener(dart, GGMouse.lDrag);
    
show();
    
doRun();
  
}

  
public static void main(String[] args)
  
{
    
new Ex08();
  
}
}

Execute the program locally using WebStart.

It is difficult to imagine a more typical event than a collision. Therefore it is absolutely natural that JGameGrid handles collisions according to the Java event model using a GGActorCollisionListener that declares the single method collide(Actor a1, Actor a2) . The callback reports the two actors involved in the collision, so it is easy to react accordingly. Treating collisions by callbacks decouples the code for collision detection and reaction from the rest of the program and improves the overall cleanness of the program design. As a bonus you don't have to worry about the current positions of all actors yourself to check if they collide, all is done in the background by the library code. (As we see later on, collision detection between actors and tiles of a tile map is also supported.)

Since collisions are detected by the game thread, the simulation cycling must be turned on by the Run button or by calling doRun() in order to get collision notifications. collide() is called after all act() methods are processed.

Our intension is to bring a balloon to explosion by picking it with the dart's needle. We want to get the collision notification in the application class Ex08a, so we implement a GGActionCollisionListener by declaring collide() and registering it with dart.addActionListener(this). We select the dart's needle endpoint as action area. Using an image editor we find out that the size of the dart image is 60 x 17 pixels. To position the collision area a coordinate system with the origin at the image center is used. As you can see in the picture below the endpoint is at coordinate (30, 0) relative to the image center.

GG8

Now we select the collision area by calling dart.setCollisionSpot(new Point(30, 0). To enable high-precision collision detection, IMAGE type collision is used for the balloon what means that collision is reported when the dart endpoint collides with non-transparent balloon image pixels. Because the balloon actor has two sprite images, one for the inflated and one for the exploding balloon, we select image type collision with sprite ID 0 by calling balloon.setCollisionImage(0).

In this example we use the main application thread to handle the collision. In all examples above the constructor ran to an end and the application thread was no longer used. All work was done by the game thread (and eventually by the EDT). In this example we want the application thread to participate a bit longer and report the collision by showing a explosion image and emitting a beep. It is generally a good idea to use the application thread to do some supervisor work and report game states and results. In an endless loop we first put the application thread in a waiting state using Monitor.putSleep(), what actually is an Object.wait(). The collision callback wakes up the application thread using Monitor.wakeUp(), what is actually an Object.notify().

The same could be achieved by checking a flag mustWork in the endless loop and setting it in the collision callback:

  while (true)
  
{
    
if (mustWork)
    
{
      mustWork 
= false;
      ...
    
}
  
}

This is generally a very bad idea because most of the time the application thread is just consuming CPU power without doing anything than checking a flag. Somewhat better is to let the thread sleep at least one millisecond in every loop cycle by calling Thread.sleep(1). But the best solution is definitely the wait/notify mechanism.

In the collision callback we disable the mouse events to avoid any further collision notifications until the original situation is restored.

import ch.aplu.jgamegrid.*;
import
 ch.aplu.util.*;
import
 java.awt.*;

public
 class Ex08a extends GameGrid implements GGActorCollisionListener
{
  
public Ex08a()
  
{
    
super(400, 300, 1, false);
    
setTitle("Move dart with mouse to pick the balloon");
    
setSimulationPeriod(50);
    
playSound(GGSound.DUMMY);
    
setBgColor(new Color(182, 220, 234));

    Dart dart 
= new Dart();
    
addActor(dart, new Location(50, 50));
    dart.
setCollisionSpot(new Point(30, 0)); // Endpoint of dart needle
    
addMouseListener(dart, GGMouse.lDrag);
    dart.
addActorCollisionListener(this);

    Actor balloon 
= new Actor("sprites/balloon.gif", 2);
    balloon.
setCollisionImage(0);  // Select IMAGE type detection
    
addActor(balloon, new Location(300, 200));
    dart.
addCollisionActor(balloon);
    
show();
    
doRun();
    
while (true)
    
{
      Monitor.
putSleep();             // Stop processing
      dart.hide(
);                    // Hide dart
      balloon.
show(1);                // Show exlode image
      
playSound(GGSound.PING);        // Play ping
      dart.
setLocation(dart.getLocationStart());   // Init dart
      dart.
setDirection(dart.getDirectionStart()); // ditto
      
delay(1000);                    // Wait a moment
      balloon.
show(0);                // Show heart image
      dart.
show();                    // Show dart
      
setMouseEnabled(true);          // Enable mouse events
    
}
  
}

  
public int collide(Actor actor1, Actor actor2)
  
{
    
setMouseEnabled(false);  // Inhibit further mouse events
    Monitor.
wakeUp();        // Resume processing
    
return 0;
  
}

  
public static void main(String[] args)
  
{
    
new Ex08a();
  
}
}

Execute the program locally using WebStart.

If your sound system is turned on, you hear a short beep when the balloon explodes. We do this by playing a system sound clip with playSound(GGSound.PING). playSound(GGSound.DUMMY) prevents a short delay when the sound system is used the first time.

GG9 GG10