Chapter 7 – Collision Detection: Asteroids The Asteroids Game How Are Bullets Fired? We handle a “space” input and call the fire() method. How Are Asteroids Created? We call the addAsteroids() method in the Space constructor What does an Explosion do? Placing an Explosion object creates the Explosion Visual and makes an Explosion Sound. What does a Proton Wave do? Placing a ProtonWave object has no effect except a ProtonWave visual. What does the Counter do? Apparently nothing No effect except a ProtonWave visual. What is currently NOT coded? When experimenting with the current scenario, you will notice that some fundamental functionality is missing. The rocket does not move. It cannot be turned, nor can it be moved forward. Nothing happens when an asteroid collides with the rocket. It flies straight through it, instead of damaging the rocket. As a result of this, you cannot lose. The game never ends, and a final score is never displayed. The ScoreBoard, Explosion, and ProtonWave classes, which we can see in the class diagram, do not seem to feature in the scenario. What should be coded? Controls for the Rocket Collision: Asteroid vs. Rocket Explosion Logic ScoreBoard/Counter Logic ProtonWave Logic Turning the Rocket We want to make the rocket turn left or right using the left and right arrow keys. Handling Key Presses /* * Check whether there are any key pressed and react to them. */ private void checkKeys() The method { checkKeys handles if (Greenfoot.isKeyDown("space")) keyboard input { fire(); } } Actor Class Methods Turning the Rocket if (Greenfoot.isKeyDown("left")) setRotation(getRotation() - 5); if (Greenfoot.isKeyDown("right")) setRotation(getRotation() + 5); Left: Negative Degrees Right: Positive Degrees Turning the Rocket /* * Check whether there are any key pressed and react to them. If left arrow key is */ down rotate left 5 private void checkKeys() degrees { if (Greenfoot.isKeyDown ("space")) { fire(); } if (Greenfoot.isKeyDown ("left")) setRotation (getRotation() - 5); } Turning the Rocket Turning the Rocket /* * Check whether there are any key pressed and react to them. */ private void checkKeys() If right arrow key is down { rotate right 5 degrees if (Greenfoot.isKeyDown("space")) { fire(); } if (Greenfoot.isKeyDown ("left")) setRotation (getRotation() - 5); if (Greenfoot.isKeyDown ("right")) setRotation (getRotation() + 5); } Turning the Rocket Flying Forward Our Rocket class is a subclass of the SmoothMover class. This means that it holds a movement vector that determines its movement and that it has a move () method that makes it move according to this vector Flying Forward The rocket does not move because we did not specify a movement vector yet. Flying Forward /* Add an initial movement * Initialize this rocket. to the rocket constructor. */ public Rocket() { reloadDelayCount = 5; addForce ( new Vector (13, 0.3)); //initially slow drifting } Flying Forward /* * Do what a rocket's gotta do. (Which is: mostly * flying about, and turning, * accelerating and shooting when the right keys are pressed.) */ Add the move () method public void act() { move (); checkKeys(); reloadDelayCount++; } Flying Forward The rocket drifts slowly toward the right of the World Ignite the Engines Ignite Algorithm 1) If “up” arrow key is pressed then 1.1) change image to show engine fire 1.2) add movement 2) If “up” arrow key is released then 2.1) change back to normal rocket image Ignite the Engines Define a stub method for ignite /* * Go with thrust on */ private void ignite (boolean boosterOn) { } Ignite the Engines A boolean parameter /* * Go with thrust on */ private void ignite (boolean boosterOn) { if (boosterOn) { setImage (rocketWithThrust); addForce (new Vector (getRotation(), 0.3)); } else { setImage (rocket); } } Ignite the Engines /* * Check whether there are any key pressed and react to them. */ private void checkKeys() Add a call to ignite { if (Greenfoot.isKeyDown("space")) fire(); ignite (Greenfoot.isKeyDown ("up")); if (Greenfoot.isKeyDown("left")) setRotation(getRotation() - 5); if (Greenfoot.isKeyDown("right")) setRotation(getRotation() + 5 } Flying Forward Colliding with Asteroids Colliding Algorithm 1) If we have collided with an asteroid then 1.1) remove the rocket from the world 1.2) place an explosion into the world 1.3) show final score (game over) Colliding with Asteroids /* * Check for a collision with an Asteroid */ private void checkCollision() { } Define a stub method for checkCollision Colliding with Asteroids /* * Do what a rocket's gotta do. (Which is: mostly flying about, and turning, * accelerating and shooting when the right keys are pressed.) */ public void act() Make a call to { checkCollision from the move (); Rocket Act method checkKeys(); checkCollision(); reloadDelayCount++; } Intersecting Objects Bounding Box Intersection Visible Image Intersecting Objects Methods List getIntersectingObjects (Class cls) Actor getOneIntersectingObject (Class cls) Intersection Bounding Boxes Visible Image getOneIntersectingObject() /* * Check for a collision with an Asteroid */ private void checkCollision() { Actor a = getOneIntersectingObject (Asteroid.class); } Even though we can specify what class we are looking for, the method always returns an Actor object getOneIntersectingObject() /* * Check for a collision with an Asteroid */ private void checkCollision() { Asteroid a = (Asteroid) getOneIntersectingObject (Asteroid.class); } However, we can re-interpret the returned Actor to a more specific class via a process called “Casting”. More on that later. getOneIntersectingObject() /* * Check for a collision with an Asteroid */ private void checkCollision() { Actor a = getOneIntersectingObject (Asteroid.class); if (a != null) { } } We have a collision if: a is not “null”, meaning if the getOneIntersectingObject has returned something Colliding with Asteroids /* * Check for a collision with an Asteroid */ private void checkCollision() { Actor a = getOneIntersectingObject (Asteroid.class); if (a != null) { World space = getWorld(); space.removeObject (this); space.addObject (new Explosion(), getX(), getY()); } } Adding the Explosion and removing the Rocket has to be done via Worldmethods. Colliding with Asteroids /* * Check for a collision with an Asteroid */ private void checkCollision() { Actor a = getOneIntersectingObject (Asteroid.class); if (a != null) { World space = getWorld(); space.addObject (new Explosion(), getX(), getY()); space.removeObject (this); } } The sequence of commands is very important when removing objects! Colliding with Asteroids Game Over After a collision, we want to show a ScoreBoard object in the middle of the Level Game Over Method in the Space class /* * This method is called when the game is over to display the final score. */ public void gameOver() { // TODO: show the score board here. Currently missing. } ScoreBoard /* * Create a score board with dummy result for testing. */ public ScoreBoard() { The ScoreBoard this(100); class has two } constructors, a Default Constructor /* and a second * Create a score board for the final result. Constructor with */ parameters public ScoreBoard(int score) { makeImage("Game Over", "Score: ", score); } Game Over /* * This method is called when the game is over to display the final score. */ public void gameOver() { addObject(new ScoreBoard(999), getWidth()/2, getHeight()/2); } Making sure that the Game Over screen appears right in the middle of the screen. Game Over Colliding with Asteroids II /* * Check for a collision with an Asteroid */ private void checkCollision() { Actor a = getOneIntersectingObject (Asteroid.class); if (a != null) { World space = getWorld(); space.addObject (new Explosion(), getX(), getY()); space.removeObject (this); space.gameOver(); } } Why does this code produce an error? Colliding with Asteroids II /* * Check for a collision with an Asteroid */ private void checkCollision() { Actor a = getOneIntersectingObject (Asteroid.class); if (a != null) { World space = getWorld(); space.addObject (new Explosion(), getX(), getY()); space.removeObject (this); space.gameOver(); } } The gameOver() method is a method of the Space class. Our Compiler cannot find it in the World class. Thus, we cannot call it from objects of type World! Colliding with Asteroids II /* * Check for a collision with an Asteroid */ private void checkCollision() { Actor a = getOneIntersectingObject (Asteroid.class); if (a != null) { Space space = (Space) getWorld(); space.addObject (new Explosion(), getX(), getY()); space.removeObject (this); space.gameOver(); } } We can use Casting to specify that the object returned by the getWorld() method is not only a World object, but actually an object of the more specific Subclass Space. This does NOT change the actual type of the object, it only changes the information available to our program. Painting Stars The Asteroid Scenario does not use an image file for the background. Instead, the background image is generated directly in the Constructor. Painting Stars The Background is Created by These Three Statements. Painting Stars Code to Create the Background is Commented Out No Background The GreenfootImage Class Draw Line Draw Oval Draw Rectangle Fill Oval Point Coordinates in Greenfoot Each point of a World object can be identified using a Cartesian coordinate system The origin of the coordinate system (i.e. the point (0,0) ) is in the top-left corner Drawing A Line drawLine(10, 20, 150, 45); or drawLine(150, 45, 10, 20); Drawing A Rectangle drawRect (50, 20, 100, 40); Drawing An Oval drawOval (175, 20, 50, 80); Filling an Oval fillOval (175, 20, 50, 80); Method to create stars /* * Method to create stars. The integer number is how many. */ private void createStars(int number) { GreenfootImage background = getBackground(); for (int i = 0; i < number; i++) { } } We want to draw a set amount of stars. This means that our new methods requires a parameter. Method to create stars /* * Method to create stars. The integer number is how many. */ private void createStars(int number) { GreenfootImage background = getBackground(); for (int i = 0; i < number; i++) { int x = Greenfoot.getRandomNumber( getWidth() ); int y = Greenfoot.getRandomNumber( getHeight() ); background.setColor (new Color(255, 255, 255)); background.fillOval(x, y, 2, 2); } } Note the sequencing: We first specify the color, then what we want to draw in said color. Also note how we apply the operations to a specific image, using dot notation. Painting Stars Calling the createStars methods in the Space constructor Painting Stars Painting Stars With Random Brightness /* * Method to create stars. The integer number is how many. */ Generate a random private void createStars(int number) number for color in the { GreenfootImage background = getBackground(); range 0 to 255 for (int i = 0; i < number; i++) { int x = Greenfoot.getRandomNumber( getWidth() ); int y = Greenfoot.getRandomNumber( getHeight() ); int color = Greenfoot.getRandomNumber (256); background.setColor(new Color(color, color, color)); background.fillOval(x, y, 2, 2); } } Will be more or less bright, but always on the whiteblack spectrum Painting Stars With Random Brightness Adding Fire Power: The Proton Wave The idea is this: Our proton wave, once released, radiates outward from our rocket, damaging or destroying every asteroid in its path. Since it works in all directions simultaneously, it is a much more powerful weapon than our bullets. The Proton Wave Does not Move Does not Disappear Does not cause Damage The Proton Wave public ProtonWave() { initializeImages(); } Already implemented code: Constructor initializeImages () act () public static void initializeImages() { if(images == null) { GreenfootImage baseImage = new GreenfootImage("wave.png"); images = new GreenfootImage[NUMBER_IMAGES]; int i = 0; while (i < NUMBER_IMAGES) { int size = (i+1) * ( baseImage.getWidth() / NUMBER_IMAGES ); images[i] = new GreenfootImage(baseImage); images[i].scale(size, size); i++; } } } public void act() { } initializeImages() creates an Array of Growing Images GreenfootImage [ ] images 0 1 2 3 4 29 Setting up The Proton Wave /* * Index of the currently used image. */ private int imageCount = 0; /* * Create a new proton wave. */ public ProtonWave() { initializeImages(); setImage(images [0]); Greenfoot.playSound ("proton.wav"); } The Proton Wave The Proton Wave /* * Act for the proton wave is: grow and check whether we hit anything. */ public void act() { grow(); } /* * Grow the wave. If we get to full size remove it. */ private void grow () { } The Proton Wave Grow Algorithm 1) If our index has exceed the number of images then 1.1) Remove the wave from the world 2) Otherwise 2.1) set the next image in the array 2.2) increment the index The Proton Wave /* * Grow the wave. If we get to full size remove it. */ private void grow () { if (imageCount >= NUMBER_IMAGES) getWorld().removeObject (this); else setImage(images[imageCount++]); } The Proton Wave Create a new ProtonWave by hand Click “> Act” to watch the wave grow sequentially Create ProtonWave from Rocket /* * Release a proton wave (if it is loaded). */ private void startProtonWave() { ProtonWave wave = new ProtonWave(); getWorld().addObject (wave, getX(), getY()); } Create ProtonWave from Rocket /* * Check whether there are any key pressed and react to them. */ private void checkKeys() { if (Greenfoot.isKeyDown("space")) fire(); if (Greenfoot.isKeyDown("z")) startProtonWave(); ignite (Greenfoot.isKeyDown ("up")); if (Greenfoot.isKeyDown("left")) setRotation(getRotation() - 5); if (Greenfoot.isKeyDown("right")) setRotation(getRotation() + 5); } Test It Test It Proton Wave Can Be Released Too Fast by Holding Down the z Key Managing Delays private static final int gunReloadTime = 5; private static final int protonReloadTime = 200; private int reloadDelayCount; private int protonDelayCount; // Minimum delay in firing gun. // Minimum delay in proton wave bursts. // How long ago we fired gun the last time. // How long ago we fired proton wave the last time. private GreenfootImage rocket = new GreenfootImage("rocket.png"); private GreenfootImage rocketWithThrust = new GreenfootImage("rocketWithThrust.png"); A Delay Count of 200 Seems Reasonable Managing Delays /* * Do what a rocket's gotta do. (Which is: mostly flying about, and turning, * accelerating and shooting when the right keys are pressed.) */ public void act() { move (); checkKeys(); checkCollision(); reloadDelayCount++; protonDelayCount++; } Managing Delays /* * Release a proton wave (if it is loaded). */ private void startProtonWave() { if (protonDelayCount >= protonReloadTime) { ProtonWave wave = new ProtonWave(); getWorld().addObject (wave, getX(), getY()); protonDelayCount = 0; } } The method only produces a new Proton Wave if sufficient time as passed since the last one Test It Interacting with Objects in Range List getObjectsInRange (int radius, Class cls) Check for ProtonWave Collisions /* * Act for the proton wave is: grow and check whether we hit anything. */ public void act() { checkCollision(); grow(); } /* * Explode all intersecting asteroids. */ private void checkCollision() { } ProtonWave Collisions import greenfoot.*; // (World, Actor, GreenfootImage, and Greenfoot) import java.util.List; … … … /* * Explode all intersecting asteroids. */ private void checkCollision() { int range = getImage().getWidth() / 2; List<Asteroid> asteroids = getObjectsInRange (range, Asteroid.class); for (Asteroid a : asteroids) { } } Asteroid Hit Method Parameter to determine the amount of damage /* * Hit this asteroid dealing the given amount of damage. */ public void hit(int damage) { stability = stability - damage; if(stability <= 0) breakUp (); } Every Asteroid has “hit points” in the form of an integer variable called stability. An Asteroid breaks up if the variable reaches 0 or less. ProtonWave Collissions /* The damage this wave will deal */ private static final int DAMAGE = 30; … … … /* * Explode all intersecting asteroids. */ private void checkCollision() { int range = getImage().getWidth() / 2; List<Asteroid> asteroids = getObjectsInRange(range, Asteroid.class); for (Asteroid a : asteroids) { a.hit (DAMAGE); } } Extra: Hit Invincibility The ProtonWave currently completely destroys all Asteroids it comes in contact with. Meaning it destroys both the Asteroids it hits and the Asteroids that are then spawned. Depending on how we want our game to function, this might not be desirable. How can we make sure that the ProtonWave does not destroy the new Asteroids? Summary of Programming Techniques