PATH TO GLORY : SlickOut

Introduction

Welcome to the second slick2d game tutorial… a breakout clone nicely called… slickout. This tutorial will help you not only get more out of your slick2d but also to understand some base concepts in game development.

For those 2 or 3 of you that dont know breakout.. shame… shame on you, google it and play some of the clones out there. The game revolves on the tennis concept where a paddle has to ump against a ball without letting it fall of the screen, there are bricks in the level that are destroyed upon contact with the paddle, to advance a level the player must destroy all bricks.

Simple… yet addictive…

About me

I'm Tiago “Spiegel” Costa, the sole programmer/gamedesigner/sound engineer in FoxholeStudios. I've been developing small games since 2007, and I am now trying to create my first commercial game.

Screenshots

slickout_game_image1.jpg

slickout_game_image2.jpg

What you will learn

Slick2d

  • Using animations
  • Using InputListeners
  • Using sounds as music
  • Using Slick2d shapes for collision detection
  • Advance uses of direction of Vector2f class

Game development

  • Level loading from a file
  • Collision detection
  • Structuring your game

Before starting

Before starting

Last tutorial, slickblocks, was a bit of mess and I want to return to it again as soon as I finish this tutorial. The mess came from not being thought out as a tutorial, it was a game I made that I tought I could turn it into a tutorial. My mistake. This wont happen with slickout. Slickout will be divided into sections, after the end of each you will have a part of your game running.

The tutorial is divided into the following sections:

  1. Load “”→ Enter→Play(Level loading from file)
  2. Moving on a collision Course (Paddle & ball Movement, and collision handling)
  3. Its always nice to score (refining gameplay)
  4. What's in the menu (menu's)

Let's begin then.

DISCLAIMER

All the classes were made during the writing and exactly at the writing of this document, so while they (probably) are garanteed to work, you should use the code at the bottom as reference.

Also please download all the resources at the bottom of the page and add it to a data folder in your classpath.

The classes were compiled using javac 1.6, and it uses java tags (@Override) if you dont have a tag enabled compilation ambient, please remove the tags. Also the classes were designed as I go so they cant be seen as the best way of doing this game, there's probably a better way of doing all this, but I couldnt think about it while my two children run about in the house screaming.

Altought I have finished the tutorial, I'll probably come back and change somethings from time to time and if people ask me to. If you find something wrong please correct it, or notify me.

Implementation

Section 1: Load “” → Enter → Play

The main purpose (game development-wise) of this tutorial is to show how a level can be read from a file and injected into gameplay. If you think about your favorite games (almost all of them) you can recgonize two different parts of the game, the gameplay and the levels. Lets pick up a classic here for examples, good old pacman.

Heres a snapshot of the gameplay:

  • Pac-Man's game play is an avatar (pacman) eating dots on corridors, making waka-waka sounds;
  • Dots earn the Pac points
  • there are 4 ghosts that chase him
  • on collision with ghost pacman dies and a life is lost
  • A game is lost if no more lives are left
  • some dots are special (the big ones) and will make the ghosts vunurable and allow the pac to eat them
  • once a ghost is eaten it returns to the pen (from where they leave on level start) and to their normal state
  • A fruit will appear from time to time to add points

There, the whole pacman game gameplay described in 8 simple setences. As you can see here there is no level description, nothing here wil tell you how level 1 differs from level 2 or 3, because gameplay wise level 1 is exactly the same as level 2 or level 100.

There is a distinction between the gameplay and the data of a level that runs on top of that gameplay. Gameplay never differs from level to level (yes even in your latest FPS these rules apply). So what generally is done is to create a game-engine that will house all of your game rules into a single place and then feed it the data that will turn those rules into what is known as a level.

The image below shows how a level information for a level 1, can be fed to a game engine and perform as a game level, you while can trade level 1 for level 2 and have a new level running, there is no need to change the rules because they are the same.

slickout_game_engine_flow.jpg Figure 1 – Setting up information driven games

This concept can be applied to slickout, because the gameplay is always the same:

  • The player controls a paddle in an horizontal axis on the bottom of the screen, within the limits of the screen of course
  • A ball rebounds into any surface (including the paddle) existent in the game
  • The game area is confined at the left, right and top, but not at the bottom
  • The ball may fall of the screen at the bottom and the player loses a life
  • If all player lives are lost the game ends
  • A set of bricks exist in the game at the top half of the screen
  • Upon collision between the bricks and the ball the brick will lose its ingrety
  • When a brick reaches zero integraty the brick is destroyed and the player receives an amount of points
  • When all the bricks are destroyed the level ends

These are our rules, the ones we must implement in order to be fed level data and run any level you may wish and thinkof.

Level structure on file

So now that we've understood that what needs to be done is a game engine, what it the shape of the data fed to that game engine? I want a simple text file, easy to read, easy to edit to be our level data holder.

So first of all we need to know what to load, so here's a list of all the levels assets:

  • Background image
  • Left bumper
  • Right bumper
  • Top Bumper
  • Paddle
  • Ball
  • lists of bricks
    • Red Brick (1 hit)
    • Blue Brick (2 hit)
    • Green Brick (2 hit)
    • Purple Brick (3 hits)
    • Yellow brick (3 hits)
    • Gray Brick (unbreakable)

This is the entire level description so the file should reflect that, so we need a way to refer an object and place it within the game

ObjectName| [TYPE]; [PATH]; [TYPE_OPTIONS]|PositionX, Position Y| [COLLISION_TYPE]; [COLLISION_SHAPE]; [COLLISION_SHAPE_OPTIONS] | R,G,B; hits

This single line will help us define an entire level, let's analise it.

ObjectName

The name of the object that its being treated

[TYPE]; [PATH]; [TYPE_OPTIONS]

The type of the object, for the sanity sake we will only allow the IMAGE and ANIMATION types.

The path of the type.

The Image has no options

The animation has tw, th and loop, that represent the number of tiles vertically and horizontally

PositionX, Position Y

The x,y position of the element.

The x,y position of the element

[COLLISION_TYPE]; [COLLISION_SHAPE]; [COLLISION_SHAPE_OPTIONS]

The type of the collider The shape of the collider, for sanity reasons we will allow only RECTANGLE and CIRCLE

The options of the shape, rectangle having deviationx, deviationy, width, height and the circle, deviationx, deviationy, radius.

R,G,B; hits

if it is a brick then a couple of more fields are in order, the red, green, blue for defining the color of the brick and the number of hits until it dies.

So lets have a look at a couple of examples of this definition:

Ball

ball | IMAGE; /data/ball.png | 0, 0 | 1; CIRCLE; 10,10,10

This will create a ball image at 0,0 with a collision shape enclosing a radius of 10.

Paddle

paddle | ANIMATION; /data/paddle.png ; 5,1 | 300, 500 |2; RECTANGLE; 0,0,100, 50

This will create a animation at 300, 500 with a rectangle of 100,50 enclosing it.

With the single elements defined, its time to define the entire struture of the file, so picking up the list of assets its just a simple transposition to the file:

# Background
# Left bumper
# Right bumper
# Top bumper
# Paddle
# Ball
# Bricks
# …
# Bricks

in each of these a structure of a object element appears, lets see a simple 2 brick level

# Background
background|IMAGE;data/carbonfiber.png|0, 0
# Left bumper
leftBumper|IMAGE;data/leftbumper.png|-10,0|1;RECTANGLE;10,0,20,600
# Right bumper
rightBumper|IMAGE;data/rightbumper.png|760,0|1;RECTANGLE;20,0,20,600
# Top bumper
topBumper|IMAGE;data/topbumper.png|40,-1|1;RECTANGLE;-40,10,800,20
# Paddle
paddle|ANIMATION;data/padanimation.png;100,20,1000|300,570|1;RECTANGLE;0,0,100,20
# Ball
ball|IMAGE;data/ball.png|337,550|1;CIRCLE;10,10,10|0.5
# Bricks
blue|ANIMATION;data/brickanimationblue.png;50,20,500|100,100|1;RECTANGLE;0,10,50,20|0,0,255;1
red|ANIMATION;data/brickanimationred.png;50,20,500|150,100|1;RECTANGLE;0,0,50,20|255,0,0;1
blue|ANIMATION;data/brickanimationyellow.png;50,20,500|200,100|1;RECTANGLE;0,0,50,20|0,0,255;1
blue|ANIMATION;data/brickanimationblue.png;50,20,500|250,100|1;RECTANGLE;0,0,50,20|0,0,255;1
blue|ANIMATION;data/brickanimationred.png;50,20,500|300,100|1;RECTANGLE;0,0,50,20|0,0,255;1

As you can see it is a very simple level structure file, but is goes to show you sometimes you dont need complex XML files to make a level structure file, just a plain old txt :P.

One more note, the # in the file shall represent a comment and will be ignored.

Level loader in-game

Before even making slick2d gamestructure lets make the level loader. The loader will load the level from an inputstream that will hold the file and will generate an instanciated object with the following interface:

package tutorials.slickout.gameplay.level;
 
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.geom.Vector2f;
import org.newdawn.slick.state.StateBasedGame;
 
public interface ILevelObject {
 
	public String getName();
 
	public void setPosition(Vector2f position);
 
	public Vector2f getPosition();
 
	public void render( Graphics graphics );
 
	public void update(GameContainer gc, StateBasedGame sbg, int delta);
}

This will represent all the objects in out game. Also for collisions a small collidable interface:

package tutorials.slickout.gameplay.level;
 
import org.newdawn.slick.geom.Shape;
 
public interface ICollidableObject {
      public Shape getNormalCollisionShape();
 
	public Shape getCollisionShape();
 
	public int getCollisionType();
 
	public boolean isCollidingWith(ICollidableObject collidable);
}

The difference between the normal collisionshape and the collisionshape is that, the normal collisionshape will give us the shape without the position of the object, the getcollisionshape method will give us the collisionshape adapted to the position of the object.

Image & Animation Objects

So now we can create two of our objects, the Image object:

package tutorials.slickout.gameplay.level;
 
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.Image;
import org.newdawn.slick.geom.Vector2f;
import org.newdawn.slick.state.StateBasedGame;
 
public class ImageObject implements ILevelObject {
 
	protected String name;
	protected Image image;
	protected Vector2f position;
 
	public ImageObject(String name, Image image, Vector2f position){
		this.name = name;
		this.image = image;
		this.position = position;
	}
 
	@Override
	public String getName() {
		return name;
	}
 
	@Override
	public Vector2f getPosition() {
		return position;
	}
 
	@Override
	public void setPosition(Vector2f position) {
		this.position = position;
	}
 
 
	@Override
	public void render(Graphics graphics) {
		image.draw(position.x, position.y);
	}
 
 
	@Override
	public void update(GameContainer gc, StateBasedGame sbg, int delta) {
	}
 
}

and the animation object

package tutorials.slickout.gameplay.level;
 
import org.newdawn.slick.Animation;
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.geom.Vector2f;
import org.newdawn.slick.state.StateBasedGame;
 
public class AnimationObject implements ILevelObject {
 
	protected String name;
	protected Animation animation;
	protected Vector2f position;
 
	public AnimationObject(String name, Animation animation, Vector2f position){
		this.name = name;
		this.position = position;
		this.animation = animation;
	}
 
	@Override
	public String getName() {
		return name;
	}
 
	@Override
	public Vector2f getPosition() {
		return position;
	}
 
	@Override
	public void setPosition(Vector2f position) {
		this.position = position;
	}
 
 
	@Override
	public void render(Graphics graphics) {
		animation.draw(position.x, position.y);
	}
 
 
	@Override
	public void update(GameContainer gc, StateBasedGame sbg, int delta) {
		animation.update(delta);
	}
 
}

And their collidable counter parts:

package tutorials.slickout.gameplay.level;
 
import org.newdawn.slick.Graphics;
import org.newdawn.slick.Image;
import org.newdawn.slick.geom.Shape;
import org.newdawn.slick.geom.Transform;
import org.newdawn.slick.geom.Vector2f;
 
 
public class CollidableImageObject extends ImageObject implements ICollidableObject {
 
	protected Shape collisionShape;
	protected int collisionType;
 
	public CollidableImageObject(String name, Image image, Vector2f position, Shape collisionShape, int collisionType) {
		super(name, image, position);
 
		this.collisionShape = collisionShape;
		this.collisionType = collisionType;
	}
 
	@Override
	public void setPosition(Vector2f position){
		super.setPosition(position);
	}
 
	@Override
	public Shape getNormalCollisionShape() {
		return collisionShape;
	}
 
	@Override
	public Shape getCollisionShape() {
		return collisionShape.transform(Transform.createTranslateTransform(position.x, position.y));
	}
 
	@Override
	public int getCollisionType() {
		return collisionType;
	}
 
	@Override
	public void render(Graphics graphics) {
		image.draw(position.x, position.y);
 
		graphics.draw(getCollisionShape());
	}
 
	@Override
	public boolean isCollidingWith(ICollidableObject collidable){
		return this.getCollisionShape().intersects(collidable.getCollisionShape());
	}
}

and animation again:

package tutorials.slickout.gameplay.level;
 
import org.newdawn.slick.Animation;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.geom.Shape;
import org.newdawn.slick.geom.Transform;
import org.newdawn.slick.geom.Vector2f;
 
public class CollidableAnimationObject extends AnimationObject implements
		ICollidableObject {
 
	private Shape collisionShape;
	private int collisionType;
 
	//private Vector2f deviation;
 
	public CollidableAnimationObject(String name, Animation animation, Vector2f position, Shape collisionShape, int collisionType) {
		super(name, animation, position);
 
		this.collisionShape = collisionShape;
		this.collisionType = collisionType;
	}
 
	@Override
	public void setPosition(Vector2f position) {
		super.setPosition(position);
	}
 
 
	@Override
	public Shape getNormalCollisionShape() {
		return collisionShape;
	}
 
 
	@Override
	public Shape getCollisionShape() {
		return collisionShape.transform( Transform.createTranslateTransform(position.x, position.y));
	}
 
	@Override
	public int getCollisionType() {
		return collisionType;
	}
 
	public Animation getAnimation() {
		return animation;
	}
 
	@Override
	public void render(Graphics graphics) {
		super.render(graphics);
 
		graphics.draw(getCollisionShape());
	}
 
	@Override
	public boolean isCollidingWith(ICollidableObject collidable){
		return 		this.getCollisionShape().intersects(collidable.getCollisionShape());
	}
}

By this point we will have all we need to render anything on screen. Now for each implementation of the objects, we need a ball, a paddle and a brick implementation, bumpers and background may be implemented using the classes above.

Each object shall be completely independent, so it will house an internal state and a render and an update method.

Lets build the skeletons of our objects.

Ball Object

For the ball object we shall need a speed and direction values.

package tutorials.slickout.gameplay.level;
 
import org.newdawn.slick.Image;
import org.newdawn.slick.geom.Shape;
import org.newdawn.slick.geom.Vector2f;
 
public class Ball extends CollidableImageObject {
 
	protected float ballSpeed;
	protected Vector2f direction;
 
	public Ball(String name, 
				Image image, 
				Vector2f position, 
				float ballSpeed, 
				Vector2f initialDirection,
				Shape collisionShape, 
				int collisionType) {
		super(name, image, position, collisionShape, collisionType);
 
		this.ballSpeed = ballSpeed;
		this.direction = initialDirection.copy();
	}
 
	public void setDirection(Vector2f direction){
		this.direction = direction.copy();
	}
 
	public Vector2f getDirection(){
		return direction;
	}
 
	public void setSpeed(float speed){
		ballSpeed = speed;
	}
 
	public float getSpeed(){
		return ballSpeed;
	}
}
</java>
 
** Paddle Object **
 
For the paddle object there is no need for new values right now.
 
<code java>
package tutorials.slickout.gameplay.level;
 
import org.newdawn.slick.Animation;
import org.newdawn.slick.geom.Shape;
import org.newdawn.slick.geom.Vector2f;
 
public class Paddle extends CollidableAnimationObject {
 
	public Paddle(String name, Animation animation, Vector2f position,
			Shape collisionShape, int collisionType) {
		super(name, animation, position, collisionShape, collisionType);
	}
}

Brick object

Our final object is the brick object that will hold the color and the number of hits that the brick can take before being destroyed.

package tutorials.slickout.gameplay.level;
 
import org.newdawn.slick.Color;
import org.newdawn.slick.Image;
import org.newdawn.slick.geom.Shape;
import org.newdawn.slick.geom.Vector2f;
 
public class Brick extends CollidableImageObject {
 
	protected int hitNumber;
	protected Color color;
 
	public Brick(String name, Image image, Vector2f position, int hitNumber, Color color,
			Shape collisionShape, int collisionType) {
		super(name, image, position, collisionShape, collisionType);
 
		this.hitNumber = hitNumber;
	}
 
	public Color getColor(){
		return color;
	}
 
	public void incrementHit(){
		hitNumber++;
	}
 
	public void decreaseHit(){
		hitNumber--;
	}
 
	public int getHitsLeft(){
		return hitNumber;
	}
}

After the level is loaded we must organized the data in a simple way, this justifies the next interface:

package tutorials.slickout.gameplay.level;
 
import java.util.List;
 
public interface ILevel {
 
	public ImageObject getBackground();
 
	public List<Ball> getBalls();
 
	public Paddle getPaddle();
 
	public CollidableImageObject getLeftBumper();
 
	public CollidableImageObject getRightBumper();
 
	public CollidableImageObject getTopBumper();
 
	public List<Brick> getBricks();	
 
	public Ball addNewBall();
}

This is the level data reprensentation. Of course in order to make the game advance we shall need to add moe functional stuff to the interface, but since all we need right now is to load the data from the files to memory and then render it;

Now we need to load data from the file and fill these classes. We shall need an implementation for the ILevel interface, below is a simple implementation for the level, it willl house only the getters and setters for the ILevel objects.

package tutorials.slickout.gameplay.level;
 
import java.io.InputStream;
import java.util.List;
 
public class LevelImpl implements ILevel {
 
	protected ImageObject background;
	protected CollidableImageObject rightBumper;
	protected CollidableImageObject leftBumper;
	protected CollidableImageObject topBumper;
 
	protected List<Brick> bricks;
	protected List<Ball> balls;
	protected Paddle paddle;
 
	private LevelImpl(){
		balls = new ArrayList<Ball>();
	}
 
	@Override
	public final ImageObject getBackground() {
		return background;
	}
 
	@Override
	public final List<Ball> getBalls() {
		return balls;
	}
 
	@Override
	public final List<Brick> getBricks() {
		return bricks;
	}
 
	@Override
	public final CollidableImageObject getLeftBumper() {
		return leftBumper;
	}
 
	@Override
	public final Paddle getPaddle() {
		return paddle;
	}
 
	@Override
	public final CollidableImageObject getRightBumper() {
		return rightBumper;
	}
 
	@Override
	public final CollidableImageObject getTopBumper() {
		return topBumper;
	}
 
 
	public final void setBackground(ImageObject background) {
		this.background = background;
	}
 
	public final void setRightBumper(CollidableImageObject rightBumper) {
		this.rightBumper = rightBumper;
	}
 
	public final void setLeftBumper(CollidableImageObject leftBumper) {
		this.leftBumper = leftBumper;
	}
 
	public final void setTopBumper(CollidableImageObject topBumper) {
		this.topBumper = topBumper;
	}
 
	public final void setBricks(List<Brick> bricks) {
		this.bricks = bricks;
	}
 
	public final void setBalls(List<Ball> balls) {
		this.balls = balls;
	}
 
	public final void setPaddle(Paddle paddle) {
		this.paddle = paddle;
	}
 
}

I didnt go into much detail in these implementations since they are straightforward. Now that we will do the actual work, i'll try and explain all of it.

First of all you may have noticed that the class constructor is private, this arises because no-one will be able to construct a level, just itself with a factory pattern (check factory pattern). For that we shall need a static method that will return a constructed level.

Add to the LevelImpl class the following method:

public static ILevel loadLevel(InputStream is) throws SlickException{
	ILevel level = null;
 
	// add level loading here
 
	return level;
}

Lets load our level from our file, we will assume that the inputstream will hold the file contents and that someone already loaded the file into the inputstream.

Lets create a static method to load an image from a set of arguments in a string array form, this will follow the <NAME> | IMAGE ; <PATH> | 0,0;

Argument 0 - <Name> Argument 1 – IMAGE; <PATH> Argument 2 – <X,Y>

private static ImageObject createImage( String[] args ) throws SlickException {
		// background| IMAGE; /data/background.jpg | 0, 0
		String name = args[0];
		String[] imageData = args[1].split(";");
 
		if( !imageData[0].trim().equalsIgnoreCase("IMAGE") ){
			throw new SlickException("Invalid image");
		}
 
		String path = imageData[1];
		String[] coords = args[2].split(",");
		Vector2f position = new Vector2f(Integer.parseInt(coords[0].trim()), Integer.parseInt(coords[1].trim()));
 
		return new ImageObject(name, new Image(path), position);
	}

This will create an ImageObject from those arguments.

Now lets add up a level, creating a CollidableImageObject the same way, following <NAME> | IMAGE ; <PATH> | 0,0 | <collision type>; [RECTANGLE or CIRCLE] ; <Shape properties>

Argument 0 - <Name> Argument 1 – IMAGE; <PATH> Argument 2 – <X,Y> Argument 3 - <collision type>; [RECTANGLE or CIRCLE] ; <Shape properties>

private static CollidableImageObject createCollidableImage( String[] args ) throws SlickException{
		ImageObject image = createImage(args); 
 
		String[] collisionData = args[3].split(";");
 
		int collisionType = Integer.parseInt( collisionData[0] );
 
		Shape shape = null;
 
		if(collisionData[1].trim().equalsIgnoreCase("RECTANGLE")){
			String[] size = collisionData[2].split(",");
			shape = new Rectangle(Integer.parseInt(size[0]), Integer.parseInt(size[1]), Integer.parseInt(size[2]), Integer.parseInt(size[3]));
		}else if(collisionData[1].trim().equalsIgnoreCase("CIRCLE")){
			String[] size = collisionData[2].split(",");
			shape = new Circle(Integer.parseInt(size[0]), Integer.parseInt(size[1]), Integer.parseInt(size[2])); 	
		}
 
		return new CollidableImageObject(image.getName(), 
										 image.getImage(), 
										 image.getPosition(), 
										 shape, 
										 collisionType);
	}

Now the same of the animation:

private static CollidableAnimationObject createCollidableAnimation(String[] args) throws SlickException{
		// blue| ANIMATION; /data/brickanimation.png; 100,20,100 | 100, 100 | 1; RECTANGLE; 0,0, 50, 20
		String name = args[0];
 
		String[] imageData = args[1].split(";");
 
		if(!imageData[0].trim().equalsIgnoreCase("ANIMATION")){
			throw new SlickException("Animation tag is invalid");
		}
 
		String[] animationData = imageData[2].split(",");
 
		SpriteSheet ss = new SpriteSheet(new Image(imageData[1]), Integer.parseInt(animationData[0]), Integer.parseInt(animationData[1]));
 
		Animation animation = new Animation(ss, Integer.parseInt(animationData[2]));
 
		String[] coords = args[2].split(",");
		Vector2f position = new Vector2f(Integer.parseInt(coords[0]), Integer.parseInt(coords[1]));
 
		String[] collisionData = args[3].split(";");
 
		int collisionType = Integer.parseInt( collisionData[0] );
 
		Shape shape = null;
 
		if(collisionData[1].trim().equalsIgnoreCase("RECTANGLE")){
			String[] size = collisionData[2].split(",");
			shape = new Rectangle(Integer.parseInt(size[0]), Integer.parseInt(size[1]), Integer.parseInt(size[2]), Integer.parseInt(size[3]));
		}else if(collisionData[1].trim().equalsIgnoreCase("CIRCLE")){
			shape = new Circle(position.x, position.y, Float.parseFloat(collisionData[2])); 	
		}
 
		return new CollidableAnimationObject(name, animation, position, shape, collisionType);
	}

Finally the Real level objects:

Paddle

private static Paddle createPaddle(String[] args) throws SlickException {
		CollidableAnimationObject animation = createCollidableAnimation(args);
 
		return new Paddle(animation.getName(), animation.getAnimation(), animation.getPosition(), animation.getNormalCollisionShape(), animation.getCollisionType());
	}

Ball

public Ball addNewBall(){
 
		Ball ball = null;
		try {
			ball = createBall( ballArgs);
			balls.add( ball );
		} catch (SlickException e) {
			e.printStackTrace();
		}
 
		return ball;
	}
 
	private static Ball createBall(String[] args) throws SlickException {
 
		CollidableImageObject image = createCollidableImage(args);
 
		return new Ball(image.getName(), image.getImage(), image.getPosition(), Float.parseFloat(args[4]), new Vector2f(0,0), image.getNormalCollisionShape(), image.getCollisionType());
	}

Bricks

private static Brick createBrick(String[] args) throws SlickException{
 
		CollidableAnimationObject brickAnimation = createCollidableAnimation(args);
 
		String[] brickData = args[4].split(";");
		String[] colorData = brickData[0].split(","); 
		int numberOfHits = Integer.parseInt(brickData[1]);
		Color color = new Color( Integer.parseInt(colorData[0]), Integer.parseInt(colorData[1]), Integer.parseInt(colorData[2]) );
 
		return new Brick(brickAnimation.getName(), 
						 brickAnimation.getAnimation(), 
						 brickAnimation.getPosition(), 
						 numberOfHits, 
						 color, 
						 brickAnimation.getNormalCollisionShape(), 
						 brickAnimation.getCollisionType());
	}

Now we need a way to read a line and create the argument list for all the methods.

The following method reads a BufferedReader and iterates each line until it finds a valid line for obtaining the arguments.

private static String[] readNextValidLine(BufferedReader br) throws SlickException {
		boolean read = false;
 
		String[] args = null;
 
		while(!read){
			String line = null;
			try {
				line = br.readLine();
			} catch (IOException e) {
				throw new SlickException("Could not read level file line", e);
			}
 
			if(!( line.startsWith("#") || line.isEmpty() )){
				read = true;
 
				args = line.split("\\|");
			}
		}
 
		return args;
	}

And finaly to get back to our reader method we must change the loadLevel method to:

public static ILevel loadLevel(InputStream is) throws SlickException{
		LevelImpl level = new LevelImpl();
 
		BufferedReader br = new BufferedReader(new InputStreamReader(is));
 
		// background
		level.setBackground( createImage( readNextValidLine( br ) ) );
 
		// Left Bumper
		level.setLeftBumper( createCollidableImage( readNextValidLine( br ) ) );
 
		// Right Bumper
		level.setRightBumper( createCollidableImage( readNextValidLine( br ) ) );
 
		// top Bumper
		level.setTopBumper( createCollidableImage( readNextValidLine( br ) ) );
 
		// Paddle
		level.setPaddle( createPaddle( readNextValidLine( br ) ) );
 
		// Ball
		// Ball
		level.setBallArgs( readNextValidLine( br ) );
 
		// Bricks
		try {
			List<Brick> bricks = new ArrayList<Brick>();
 
			while(br.ready()){
				bricks.add( createBrick( readNextValidLine( br ) ) );
			}
 
			level.setBricks(bricks);
		} catch (IOException e) {
			throw new SlickException("Could not load Bricks", e);
		}
		return level;
	}

Wait.. what is that with the balls??? Well because you do not want to have a ball object but the hability to create balls on demand.

So we need to add three more things a new member:

protected String[] ballArgs;

And two more methods:

private void setBallArgs(String[] ballArgs) {
		this.ballArgs = ballArgs;
	}

and

	public Ball addNewBall() throws SlickException{
		Ball ball = createBall( ballArgs) ;
		balls.add( ball );
 
		return ball;
	}

This will enable us to create new balls on demand.

So now we can create levels on demand by using the loadlevel with the files using the struture.

Now lets render stuff…

Spiegel, Spiegel on the wall, who's the renderest of them all...

No that we háve a full blown level readed from file, we're going to need to render those objects.

  • Create the gameplay state

First of all lets create a basic game state.

Well call it GamePlayState and will extend the Slick2d BasicGameState. We'll put it in the tutorials.slickout.gameplay package:

package tutorials.slickout.gameplay;
 
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
 
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.state.BasicGameState;
import org.newdawn.slick.state.StateBasedGame;
 
import tutorials.slickout.gameplay.level.Ball;
import tutorials.slickout.gameplay.level.Brick;
import tutorials.slickout.gameplay.level.ILevel;
import tutorials.slickout.gameplay.level.LevelImpl;
 
public class GameplayState extends BasicGameState {
 
	@Override
	public int getID() {
		return 0;
	}
 
	@Override
	public void init(GameContainer arg0, StateBasedGame arg1)
			throws SlickException {
 
	}
 
	@Override
	public void render(GameContainer gc, StateBasedGame sbg, Graphics gr)
			throws SlickException {
	}
 
	@Override
	public void update(GameContainer gc, StateBasedGame sbg, int delta)
			throws SlickException {
	}
 
}

So now we need to add a new level reader to this game state. Since we want to reuse the game state for all the levels we need a setLevel method, we'll add a string that will hold the name of the file to load to the gameplay game state class, as well as level structure:

private ILevel level;
private String levelFile;

Now for the method:

	public void setLevelFile(String file){
		levelFile = file;
	}

And now we override the enter method from basic game state to load the file every time the game enter this game state.

public void enter(GameContainer gc, StateBasedGame sbg)
			throws SlickException {
 
		if(levelFile == null){
			throw new SlickException("No level to load"); 
		}
 
		try {
			level = LevelImpl.loadLevel( new FileInputStream(new File(levelFile)) );
		} catch (FileNotFoundException e) {
			throw new SlickException("Could not load file");
		}
	}

Now lets add this little state to the main game, in the initStatesList method add:

	public void initStatesList(GameContainer gc) throws SlickException {
 
		GameplayState state = new GameplayState();
 
		state.setLevelFile("data/level1.lvl");
 
		addState(state);
	}

Now lets slow down a bit, if you run the game it will now actually do something, it will try to read the level1.lvl from the classpath data folder, but there is no level1.lvl. So.. lets add it then.

Create a folder data in the classpath of your project and add the following stuff:

  • Create a text file called Level1.lvl, add the following inside:
# Background
background|IMAGE;data/carbonfiber.png|0, 0
# Left bumper
leftBumper|IMAGE;data/leftbumper.png|-10,0|1;RECTANGLE;20,600
# Right bumper
rightBumper|IMAGE;data/rightbumper.png|760,0|1;RECTANGLE;20,600
# Top bumper
topBumper|IMAGE;data/topbumper.png|40,-1|1;RECTANGLE;800,20
# Paddle
paddle|ANIMATION;data/padanimation.png;100,20,1000|300,570|1;RECTANGLE;150,20
# Ball
ball|IMAGE;data/ball.png|337,550|1;CIRCLE;10|0.5
# Bricks
blue|ANIMATION;data/brickanimationblue.png;50,20,500|100,100|1;RECTANGLE;50,20|0,0,255;1
red|ANIMATION;data/brickanimationred.png;50,20,500|150,100|1;RECTANGLE;50,20|255,0,0;1
blue|ANIMATION;data/brickanimationyellow.png;50,20,500|200,100|1;RECTANGLE;50,20|0,0,255;1
blue|ANIMATION;data/brickanimationblue.png;50,20,500|250,100|1;RECTANGLE;50,20|0,0,255;1
blue|ANIMATION;data/brickanimationred.png;50,20,500|300,100|1;RECTANGLE;50,20|0,0,255;1
  • 2.Add the following images to your data folder:

carbonfiber.png, leftbumper.png, rightbumper.png, topbumper.png, padanimation.png, ball.png, brickanimationred.png, brickanimationblue.png, brickanimationyellow.png

You can find all the resources at the end of this tutorial.

Now if it was all ok, running the game will display a black screen, it should not caught any exception.

Now for the fun part, rendering stuf…

  • Render the level

Now we have all the objects of our game in a level struture but nothing shows, we can fix that by goint to gameplay state class and fill the render method with :

@Override
	public void render(GameContainer gc, StateBasedGame sbg, Graphics gr)
			throws SlickException {
 
		// Background
		level.getBackground().render(gr);
 
		// Bumpers
		level.getLeftBumper().render(gr);
 
		level.getRightBumper().render(gr);
 
		level.getTopBumper().render(gr);
 
		// Bricks
		for ( Brick brick : level.getBricks() ){
			brick.render(gr);
		}
 
		// Paddle
		level.getPaddle().render(gr);
 
		// Ball
		for ( Ball ball : level.getBalls()){
			ball.render(gr);
		}
	}

This will make our game take life and you'll probably see the following:

initial_gameplay.jpg

figure 2 - Initial Gameplay

Ok. Now we have a level loading from a file, to our own skeleton (with a bit of muscle in already) of a game engine.

This concludes the first part of our tutorial, next we will make the paddle and ball move around.

Section 2: Moving on a collision course

In this section. We will add some movement to our game, as well as collisions between elements. Section 2 will be divided into two parts, paddle and ball movement, collision handling.

Yet... it moves

In the tetris tutorial the input was done during the update phase, but that is not very useful. Lets imagine that the update cicle takes a couple of miliseconds to execute, that may give the user a window to press a button of click the mouse and not be registered during the game. Its not even hard to do this, just put something that holds the HDD hostage and everything in your machine halts to a full stop, including your game. Also since the keyboard controller has several keys associated to a single output, you may experience that holding four or more keys simultaneously only grabs the key events for two or three keys, losing all the others.

So you can use inputlisteners to obtain the key events when they happen and reduce (almost to 100%) the loss of key presses.

So what is a KeyListener in slick2d? A listener is just a class that receives the events associated with that particular input. For example, Key Listeners receive an event that represents a key pressed and another when a key is released.

Slick2d presents us with three listeners, a controller listener, a mouse listener and a Key listener.

We will now focus on the mouse listener to obtain the mouse input.

So how can we create a Mouse Listener? Easy as pie… just make a class implement MouseListener and register that class into the game input. Its that easy.

For the gameplay only the pad should have input, so we will extend the pad, as our MouseListener.

An this is how the paddle class will look like now.

public class Paddle extends CollidableAnimationObject implements MouseListener {
 
	public Paddle(String name, Animation animation, Vector2f position,
			Shape collisionShape, int collisionType) {
		super(name, animation, position, collisionShape, collisionType);
	}
 
	@Override
	public void mouseClicked(int button, int x, int y, int clickCount) {
	}
 
	@Override
	public void mouseMoved(int oldx, int oldy, int newx, int newy) {
	}
 
	@Override
	public void mousePressed(int button, int x, int y) {
	}
 
	@Override
	public void mouseReleased(int button, int x, int y) {
	}
 
	@Override
	public void mouseWheelMoved(int change) {
	}
 
	@Override
	public void inputEnded() {
 
	}
 
	@Override
	public boolean isAcceptingInput() {
		return false;
	}
 
	@Override
	public void setInput(Input arg0) {	
	}
 
}

Each of these methods represent an event that will be called every time that the mouse makes them, so every time a mouse moves, a mouse moved event is issued and the MouseMoved method is callled, every time you click on a button, the event is launched and the MousePressed method called.

In order to have the mouse events be processed by the paddle class we need to register it in the input of our game. Going back to the gameplay state, just add to the end of the enter method the following line:

gc.getInput().addMouseListener(level.getPaddle());

And we are set to go, now our class will receive the input… Well not so, because the slick2d input manager checks the method IsReceivingInput() in order to send input to that manager and we have our method returning false. Change it to this:

	@Override
	public boolean isAcceptingInput() {
		return true;
	}

Now for some magic, lets change the mouseMove to :

@Override
	public void mouseMoved(int oldx, int oldy, int newx, int newy) {
		if(newx > 10 && newx<690){
			position.x = newx;
		}
	}

And run the game, if no problem arises you should control the pad with your mouse. Its a simple yet effective way of making the pad move.

But now what? There is something missing… perhaps a ball… but how do we now when to create it? Which leads us once again in gameplay states. We need a couple of them:

  1. Ball Launch
  2. Normal Game
  3. Life lost
  4. Level Over
  5. Game over

And this is the normal flow:

slickout_game_flow.jpg

figure 3 - Slickout gameplay flow

This is how the update and render should behave on each state:

Ball Launch

Create a ball at the middle of the pad. Ball and paddle are attached to each other and moving the paddle will move the ball. The words, “Level # - Get Ready” should be displayed in the game On any mouse button clicking the ball should be launched in a random angle of 45º to 135º , the game will advance to Normal Game State.

Normal Game

The ball will rebound on any collidable surfaces, such as bricks, the paddle, and the top, left and right bumpers. Upon the ball leaving the bottom area go to the life lost state.

If no bricks are left, go to the level over state. Each brick hit will generate 100 points.

Life lost

If lives are more than zero, go back to the ball launch state. If lives are zero, go to the game over state.

Level Over

Show the score to the user in the middle of the screen. Wait 5 seconds then move to next level

Game over

Show the score to the user in the middle of the screen. Wait 5 seconds then move to main menu level.

Code wise this is what will happen, we will add two private members to the gameplay state class.

private static enum LEVEL_STATES { BALL_LAUNCH, NORMAL_GAME, LIFE_LOST, LEVEL_OVER, GAME_OVER };
 
private LEVEL_STATES currentState;

And change the update method to reflect all the states:

@Override
	public void update(GameContainer gc, StateBasedGame sbg, int delta)
			throws SlickException {
 
		switch(currentState){
		case BALL_LAUNCH:
			break;
		case NORMAL_GAME:
			break;
		case LIFE_LOST:
			break;
		case LEVEL_OVER:
			break;
		case GAME_OVER:
			break;
		}
	}

Now all we have to do is to fill up the gaps, lets start with ball_launch:

Add the ball launch state code

case BALL_LAUNCH:{
			if( level.getBalls().size() == 0 ){
				Ball ball = level.addNewBall();
				ball.setSpeed(0.0f);
				ball.setDirection(new Vector2f(0,0));
			}
 
			Ball ball = level.getBalls().get(0);
 
			if(level.getPaddle().getState() == Paddle.PAD_STATE.STICKY){
 
				Vector2f position = level.getPaddle().getPosition().copy();
 
				position.x += 40;
				position.y -= 20;
 
				ball.setPosition( position );
			}else{
				// set the normal state to the paddle
				level.getPaddle().setState(Paddle.PAD_STATE.NORMAL);
				// add movement and direction to the ball
				ball.setSpeed(0.5f);
 
				Vector2f direction = new Vector2f(0,1);
				Random r = new Random();
 
				direction.add( r.nextInt(45) * (r.nextBoolean()? -1 : 1) );
				ball.setDirection(direction);
				// change the game state
				currentState = LEVEL_STATES.NORMAL_GAME;
			}
			break;}

perhaps this one needs a couple of explanations, but we still need to do some more stuff, first of all we need to add states to the paddle as well. A normal and a Sticky State, when the paddle is on the second state, the balls should stick to it, on the normal state they should bounce.

Add this to the paddle class:

public static enum PAD_STATE {NORMAL, STICKY};
private PAD_STATE currentState;

and

public Paddle(String name, Animation animation, Vector2f position,
			Shape collisionShape, int collisionType) {
		super(name, animation, position, collisionShape, collisionType);
 
		currentState = PAD_STATE.STICKY;
	}
 
	public PAD_STATE getState(){
		return currentState;
	}
 
	public void setState(PAD_STATE newState){
		currentState = newState;
	}

and

@Override
	public void mousePressed(int button, int x, int y) {
		if(currentState == PAD_STATE.STICKY){
			currentState = PAD_STATE.NORMAL;
		}
	}

So lets explain the earlier code a bit:

if( level.getBalls().size() == 0 ){
				Ball ball = level.addNewBall();
				ball.setSpeed(0.0f);
				ball.setDirection(new Vector2f(0,0));
			}

When there is no balls create a new ball and set it to zero speed and direction.

if(level.getPaddle().getState() == Paddle.PAD_STATE.STICKY){
				Vector2f position = level.getPaddle().getPosition().copy();
 
				position.x += 40;
				position.y -= 20;
 
				ball.setPosition( position );
			}

Next if the paddle state is STICKY then we obtain the paddle position and apply a minor translation (40,20) to the ball.

else{
				// set the normal state to the paddle
				level.getPaddle().setState(Paddle.PAD_STATE.NORMAL);
				// add movement and direction to the ball
				ball.setSpeed(0.5f);
 
				Vector2f direction = new Vector2f(0,1);
				Random r = new Random();
 
				direction.add( r.nextInt(45) * (r.nextBoolean()? -1 : 1) );
				ball.setDirection(direction);
				// change the game state
				currentState = LEVEL_STATES.NORMAL_GAME;
			}

As you can seen, if the player clicks the mouse, turning the paddle state to NORMAL the ball is set with speed 0.5 and direction (0,1), which means that the ball will be launched in the y-axis up. Next we add a minor deviation of 45 degrees randomly to any side so that the ball is lauched in a diferent direction at any ball launch.

If you try it now it will make the ball stick to the pad and be launched on any mouse click.

Launched is a strong word, it stays put on the same place. That is because there is no update to the ball on the normal state. Lets add it up

case NORMAL_GAME:{
			// update all balls
			List<Ball> removals = null;
 
			for(Ball ball : level.getBalls()){
				ball.update(gc, sbg, delta);
 
				if(ball.getPosition().y > gc.getHeight()){
					if(removals == null){
						removals = new ArrayList<Ball>();
					}
					removals.add(ball);
				}
			}
 
			if(removals != null){
				for(Ball ball : removals){
					level.getBalls().remove(ball);
				}
			}
 
			// check if ball drops from screen
			if(level.getBalls().size() == 0){
				currentState = LEVEL_STATES.LIFE_LOST;
			}
 
			// perform collisions
 
			break;}

Lets explain this code a bit, first of all we obtain all the balls and update them, if the result for that update is that the ball is outside the gamearea then put that ball for removal. After all the balls have been updated, balls in the removals list are removed from the game. If there is zero balls in game the game advances to life lost.

There is only another thing to do to end (or start) the ball movement and that is (you probably guessed) the update for the ball. Just add the following to the ball class:

	@Override
	public void update(GameContainer gc, StateBasedGame sbg, int delta) {
		position.x += direction.x * delta * ballSpeed;
		position.y -= direction.y * delta * ballSpeed;
	}

This will add a movement based on its direction and speed as well as the delta between updates. Changing any one of these three will result in a different movement.

So now we have a small sample of the game, the ball is lauched and it flies off the screen. That's not what we intended, we need to have it rebound on everything. Lets add collisions to this game (finally).

Captain, we're on a collision course

Probably one of the most sought out algorithms of any young game developer, collision detection is something that most people find hard. Suprinsingly its not very hard to do collision detection.

Almost every game implements a physic engine of sorts. Our will implement only a ball flying with no attrition (so speed never drops) and rebounds on every surface by the same amount of angle, as seen in the picture below:

slickout_rebound_angle.jpg

figure 4 - Rebound angle

So we need to do two things:

  1. Encounter collision
  2. Decide the outcome of the collision

The first is straigthforward, we will test all objects in the game to find if anything that is colliding with the ball. The second is to change its direction angle as shown above.

Altough its not very efficient to test all objects against all objects, since this game is very small we can get away with it and still have an efficient game.

But before we define collisions, lets jut make simple upgrades to all of our collidableObjects.

Now for this we will need a collision manager.

A collision Manager is something that handles all collisions for us (altough we actually have to program it anyway). A collision manager is made of two things, the first a set of collidable objects each containing its collidable type (for example, balls may have type 1 and bricks type 2, paddle and bumpers type 3), second, a set of handlers that handle collisions between two types.

Lets define the basic interface for the handler:

package tutorials.slickout.gameplay.collision;
 
import tutorials.slickout.gameplay.level.ICollidableObject;
 
public interface ICollisionHandler {
 
	public int getCollider1Type();
 
	public int getCollider2Type();
 
	public void performCollision(ICollidableObject collidable1, ICollidableObject collidable2);
}

This is all we need for now. Then for the skeleton of the manager:

/**
 * Performs basic collisions between collidable types using slick2d shapes.
 * 
 * @author Tiago "Spiegel" Costa
 *
 */
public class CollisionManager {
 
	// the list of objects per type
	private Map<Integer, List<ICollidableObject>> collidables;
	// the list of collisions per type
	private Map<Integer, List<Integer>> collisionsTypes;
	// the list handlers for collisions
	private Map<String, ICollisionHandler> collisionHandlers;
 
	/**
	 * Creates a new CollisionManager
	 */
	public CollisionManager(){
		collidables 		= new HashMap<Integer, List<ICollidableObject>>() ;
		collisionsTypes 	= new HashMap<Integer, List<Integer>>() ;
		collisionHandlers 	= new HashMap<String, ICollisionHandler>() ;
	}
 
}

Each map means a specific thing. The collidables Map<Integer, List<ICollidableObject» collidables represents all the collidable objects for a specific type in the game. As an example, if the brick are type 3, the there will be an entry for <type3, List<All bricks». The Map<Integer, List<Integer» collisionsTypes represent the collision between types, for exampe, if ball is type 1, and collides with pad( type2) and bumpers (type2) and bricks (type3) then there will be an entry for <1, List<2,3» in this map, this will make it easier to get all collisions types for a type, the using the first map (collidables) we can obtain all the objects from those types and compare it for collisions. Finnally there is the last map, a list of handlers for a specific key, this key is composed by the two types in the collision by <smaller type>-<bigger type> and will be implemented by the following method in the class:

		/**
	 * Generates a key for the two collision types in the form of
	 * <smaller type>-<bigger type>
	 * @param type1 the first type
	 * @param type2 the second type
	 * @return a string with the key in the given format
	 */
	public static String getKey(int type1, int type2){
		// generate key <smaller type>-<bigger type>
		return (type1 < type2) ? type1+"-"+type2 : type2+"-"+type1; 
	}

Using the second and first maps, we can obtain a collision between a type and a list of collidable object from several types. Running through them all, will get us for each collision a pair <first type, second type> than can be used to obtain a key, and after a handler for that kind of collision. But first we need to populate the maps, the following methods insert all the needed data into the manager, enabling us to perform collisions afterwards.

Adding objects to the collision bag:

/**
	 * Adds a new object for the collision bag, from this point on this object will be checked
	 * for collisions.
	 * 
	 * @param collidable the collidable object to add
	 */
	public void addCollidable(ICollidableObject collidable){
		// obtain the entry for this type
		List<ICollidableObject> collidableList = collidables.get(collidable.getCollisionType());
 
		//if there is no entry for this type add one
		if(collidableList == null){
			collidableList = new ArrayList<ICollidableObject>();
			collidables.put(collidable.getCollisionType(), collidableList);
		}
 
		// and an entry to the list
		collidableList.add(collidable);
	}

Removing objects from the collision bag:

/**
	 * Removes an object from the collidable bag, from this point on the object 
	 * shall not be used for collisions.
	 * 
	 * If there is no entry for this object nothing is done.
	 * 
	 * @param collidable the collidable object to remove
	 */
	public void removeCollidable(ICollidableObject collidable){
		// obtain the entry for this type
		List<ICollidableObject> collidableList = collidables.get(collidable.getCollisionType());
 
		// if the entry exists remove the object from the list (if possible)
		if(collidableList != null){
			collidableList.remove(collidable);
		}
	}

Adding a new collision handler to the manager to enable collisions between two types:

	/**
	 * Adds a new collision handler to the manager. From this point on all the types
	 * concerning this handler (type1 and type2) will perform collisions.
	 * 
	 * @param handler the handler to add
	 */
	public void addHandler(ICollisionHandler handler){
		// generate the key
		String key = getKey(handler.getCollider1Type(), handler.getCollider2Type());
 
		// add the handler to the map
		collisionHandlers.put( key, handler );
 
		// add the collision type1 to type 2
		addTypesToCollision(handler.getCollider1Type(), handler.getCollider2Type());
		// add the collision type2 to type 1
		addTypesToCollision(handler.getCollider2Type(), handler.getCollider1Type());
	}
 
	private void addTypesToCollision(int type1, int type2){
		// obtain collision type entry
		List<Integer> typeCollisions = collisionsTypes.get(type1);
 
		// if there is no entry create one
		if(typeCollisions == null){
			typeCollisions = new ArrayList<Integer>();
			collisionsTypes.put(type1, typeCollisions);
		}
		// add collision to list
		typeCollisions.add(type2);
	}

Now all we have to do is to perform the collision checks.

We need a support class for the collisions, add it to the collisionmanager:

class CollisionData{
		public ICollisionHandler handler;
		public ICollidableObject object1;
		public ICollidableObject object2;
	}

This is the method for finding and processing all collisions:

public void processCollisions(){
 
		// prepare a set of all keys to collide
		Set<String> allCollisionKeys = new HashSet<String>();
 
		// prepare a list of collisions to handle
		List<CollisionData> collisions = new ArrayList<CollisionData>();
 
		Set<Integer> types = collisionsTypes.keySet();
 
		// obtain every type for collision
		for(Integer type : types){
			// obtain for each type the type it collides with
			List<Integer> collidesWithTypes = collisionsTypes.get(type);
 
			for(Integer collidingType : collidesWithTypes){
				// if the pair was already treated ignore it else treat it
				if( !allCollisionKeys.contains(getKey(type, collidingType)) ){
					// obtain all object of type
					List<ICollidableObject> collidableForType = collidables.get(type);
					// obtain all object of collidingtype
					List<ICollidableObject> collidableForCollidingType = collidables.get(collidingType);
 
					for( ICollidableObject collidable : collidableForType ){
						for( ICollidableObject collidesWith : collidableForCollidingType ){
							if(collidable.isCollidingWith(collidesWith)){
								CollisionData cd = new CollisionData();
								cd.handler = collisionHandlers.get(getKey(type, collidingType));
								cd.object1 = collidable;
								cd.object2 = collidesWith;
 
								collisions.add(cd);
							}
						}
					}
 
					allCollisionKeys.add(getKey(type, collidingType));
				}				
			}
		}
 
		for(CollisionData cd : collisions){
			cd.handler.performCollision(cd.object1, cd.object2);
		}
	}

Lets explain this method a bit, after all you have four for all chained together and everyone as said that it is bad, but this is actually a O(N) order algorithm, and since you have to iterate through all the collidable entities than its the best order you get.

But lets explain it a bit, this algorithm grabs all registered types with handlers (the only ones that will collide) and for each type obtains a list of the types it collides with. And for each the collidable objects tha go with it. One by one the objects are tested for collision, if they collide the handler is added to the a collision list. The key for the two types colliding is then added to a list of already collided types and will not be processed again if it appears again. Example: type 1 colliding with type 2, will make two lists, <1, <2» and <2, <1», so with this algorithm collision between 1 and 2 will only be catched once.

And now lets make things collide.

In the gameplay class add the following in the enter method:

try {
			level = LevelImpl.loadLevel( new FileInputStream(new File(levelFile)) );
		} catch (FileNotFoundException e) {
			throw new SlickException("Could not load file");
		}
 
		collisionManager = new CollisionManager();
 
		collisionManager.addCollidable(level.getLeftBumper());
		collisionManager.addCollidable(level.getRightBumper());
		collisionManager.addCollidable(level.getTopBumper());
 
		collisionManager.addCollidable(level.getPaddle());
 
		for(Brick brick : level.getBricks()){
			collisionManager.addCollidable(brick);
		}

This will register all the collidable objects existent after load in the manager. Now for the ball register on creation:

if(level.getPaddle().getState() == Paddle.PAD_STATE.STICKY){
 
				Vector2f position = level.getPaddle().getPosition().copy();
 
				position.x += 40;
				position.y -= 20;
 
				ball.setPosition( position );
			}else{
				// set the normal state to the paddle
				level.getPaddle().setState(Paddle.PAD_STATE.NORMAL);
				// add movement and direction to the ball
				ball.setSpeed(0.5f);
 
				collisionManager.addCollidable(ball);
 
				Vector2f direction = new Vector2f(0,1);
				Random r = new Random();
 
				direction.add( r.nextInt(45) * (r.nextBoolean()? -1 : 1) );
				ball.setDirection(direction);
				// change the game state
				currentState = LEVEL_STATES.NORMAL_GAME;
			}

And update the normal state:

case NORMAL_GAME:{
			// update all balls
			List<Ball> removals = null;
 
			for(Ball ball : level.getBalls()){
				ball.update(gc, sbg, delta);
 
				if(ball.getPosition().y > gc.getHeight()){
					if(removals == null){
						removals = new ArrayList<Ball>();
					}
					removals.add(ball);
				}
			}
 
			if(removals != null){
				for(Ball ball : removals){
					level.getBalls().remove(ball);
 
					collisionManager.removeCollidable(ball);
				}
			}
 
			// check if ball drops from screen
			if(level.getBalls().size() == 0){
				currentState = LEVEL_STATES.LIFE_LOST;
			}
 
			// perform collisions
			collisionManager.processCollisions();

Now all we need is a handler to handle the collisions, we're going to need two of them, one for collisions between bumpers/Paddle and ball, and another for bricks and ball.

Bumper/Paddle and Ball

package tutorials.slickout.gameplay.level.collision;
 
import java.util.Random;
 
import org.newdawn.slick.geom.Shape;
import org.newdawn.slick.geom.Vector2f;
 
import tutorials.slickout.gameplay.collision.ICollisionHandler;
import tutorials.slickout.gameplay.level.Ball;
import tutorials.slickout.gameplay.level.ICollidableObject;
 
public class BumperAndPadBallCollisionHandler implements ICollisionHandler {
 
	private Random r ;
 
	public BumperAndPadBallCollisionHandler(){
		r = new Random();
	}
 
	@Override
	public int getCollider1Type() {
		return 1;
	}
 
	@Override
	public int getCollider2Type() {
		return 2;
	}
 
	@Override
	public void performCollision(ICollidableObject collidable1,
			ICollidableObject collidable2) {
 
		// check to see if collision is still applicable
		// sometimes the collision may be resolved by other handlers somehow
		if(!collidable1.isCollidingWith(collidable2)){
			return;
		}
 
		Ball ball = null;
		ICollidableObject object = null;
 
		// Cast the correct objects		
		if(collidable1 instanceof Ball){
			ball = (Ball) collidable1;
			object = collidable2;
		}else{
			ball = (Ball) collidable2;
			object = collidable1;
		}
 
		// obtain a copy of the direction
		Vector2f direction = ball.getDirection().copy(); 
		// reverse it
		direction.set(direction.x*-1, direction.y*-1);
 
		// backtrack the position of the ball until it no longer collides with
		// the paddle/brick
		do {
			Vector2f pos = ball.getPosition();
			ball.setPosition(new Vector2f( pos.x + direction.x, pos.y - direction.y) );
		}while(ball.isCollidingWith(object));
 
		// obtain the shapes of the objects
		Shape ballShape 	= ball.getCollisionShape();
		Shape objectShape   = object.getCollisionShape();
 
		// obtain a fresh direction of the ball
		direction = ball.getDirection().copy(); 
 
		// define the new direction
		if(ballShape.getMinY() > objectShape.getMaxY() || ballShape.getMaxY() < objectShape.getMinY()){
			direction.set(direction.x, -direction.y);
		}else direction.set(-direction.x, direction.y);
 
		// set the new ball direction
		ball.setDirection(direction);
	}
 
}

Brick and Ball

package tutorials.slickout.gameplay.level.collision;
 
import java.util.Random;
 
import org.newdawn.slick.geom.Shape;
import org.newdawn.slick.geom.Vector2f;
 
import tutorials.slickout.gameplay.collision.CollisionManager;
import tutorials.slickout.gameplay.collision.ICollisionHandler;
import tutorials.slickout.gameplay.level.Ball;
import tutorials.slickout.gameplay.level.Brick;
import tutorials.slickout.gameplay.level.ICollidableObject;
import tutorials.slickout.gameplay.level.ILevel;
 
public class BrickBallCollisionHandler implements ICollisionHandler {
 
	private Random r ;
 
	private ILevel levelData;
	private CollisionManager manager;
 
	public BrickBallCollisionHandler(ILevel levelData, CollisionManager manager){
		r = new Random();
 
		this.levelData = levelData;
		this.manager = manager;
	}
 
	@Override
	public int getCollider1Type() {
		return 3;
	}
 
	@Override
	public int getCollider2Type() {
		return 2;
	}
 
	@Override
	public void performCollision(ICollidableObject collidable1,
			ICollidableObject collidable2) {
 
		// check to see if collision is still applicable
		// sometimes the collision may be resolved by other handlers somehow
		if(!collidable1.isCollidingWith(collidable2)){
			return;
		}
 
		Ball ball = null;
		Brick brick = null;
 
		// Cast the correct objects		
		if(collidable1 instanceof Ball){
			ball = (Ball) collidable1;
			brick = (Brick) collidable2;
		}else{
			ball = (Ball) collidable2;
			brick = (Brick) collidable1;
		}
 
		// obtain a copy of the direction
		Vector2f direction = ball.getDirection().copy(); 
		// reverse it
		direction.set(direction.x*-1, direction.y*-1);
 
		// backtrack the position of the ball until it no longer collides with
		// the brick
		do {
			Vector2f pos = ball.getPosition();
			ball.setPosition(new Vector2f( pos.x + direction.x, pos.y - direction.y) );
		}while(ball.isCollidingWith(brick));
 
		// obtain the shapes of the objects
		Shape ballShape 	= ball.getCollisionShape();
		Shape objectShape   = brick.getCollisionShape();
 
		// obtain a fresh direction of the ball
		direction = ball.getDirection().copy(); 
 
		// define the new direction
		if(ballShape.getMinY() > objectShape.getMaxY() || ballShape.getMaxY() < objectShape.getMinY()){
			direction.set(direction.x, -direction.y);
		}else direction.set(-direction.x, direction.y);
 
		// add -10º to 10º degrees random to each bump in the bricks
		direction.add( r.nextInt(10) * (r.nextBoolean()? -1 : 1) );
 
		// set the new ball direction
		ball.setDirection(direction);
 
		// since the brick was hit, decrease the number of hits to be detroyed
		brick.decreaseHit();
 
		// if the brick has reached the last hit
		if(brick.getHitsLeft() == 0){
			// remove the brick from the level list
			levelData.getBricks().remove(brick);
			// remove it from the collision manager
			manager.removeCollidable(brick);
		}
 
	}
 
}

This is the only thing you will need to perform a collision… You probably tought that it took more? We'll no… like I said its not rocket science (its computer science), its not that hard to do simple collision checks. And since almost all 2d games hve simple collision checks you may use this the collision manager to perform any collision at all. It even works for any Slick2d Shape you can create.

But lets continue, the actual meat of the collision check is withing Slick2d. Shapes in slick2d have a isIntereseting method that is actually very fast and well implemented (Thanks Kev) and works for all shapes. So iterating on top of that, the only thing we need is to make something (a manager) that veryfies if several shapes are colliding with each other using slick2d below.

The concept of handlers, is not new and its great to reuse code, for example, the pad and the bumpers have the same behaviour, so there is no need to have a collision between the ball and the paddle and another for collisions between the ball and the bumpers, they can be treated the same way. Also if tomorow you would want to add an industructible brick to the list of bricks, just add a brick with type one and presto! You have a indestructable brick.

Now lets make our game perform collisions, in the gameplay class, enter method add :

collisionManager.addHandler(new BumperAndPadBallCollisionHandler());
		collisionManager.addHandler(new BrickBallCollisionHandler(level, collisionManager));

As you can see there is only one handler of each type in the system at any time, its one handler that performs the same way to every pair of those specific collidable types in the game.

In the update method, in the normal state add at the bottom:

			// check for bricks left
			if(level.getBricks().size() == 0){
				gc.getInput().removeMouseListener(level.getPaddle());
 
				currentState = LEVEL_STATES.LEVEL_OVER;
			}

So now after the ball destroys all bricks the game state will advance and we will lose control over the pad.

Lets try our game for a bit now, if all goes right you should have a moving paddle that will launch a ball on mouse click, the ball should rebound of anything and the brocks should take some hits and be detroyed. We're getting there but we're still a long way making a full game, we're going against the evil part in games… polishing a game.

Exercises: Try and create a full level in your file. Add several types of bricks and hit numbers Add a new brick that is indistructable but does not count as a brick

Section 3: Its always nice to score

We've done all the gameplay aspect of the game, it will launch a ball, move the paddle, collide against everything, destroy bricks. Now what? Well scores and lives for one thing.

Cats have nine lives, but my game avatar has a lot more

Lets implement the concept of the livesand score. Alsmot every game of the old days had them, you had 3, 5, 7, but you had to have lives, then came the health meter and these days its almost impossible to have a game with the concept of lives, we have a concept of infinite lives and to succed its almost only a matter of time. Games like contra or final figt yould have you value your gamming lives as much as you did value your children (some cases even more). Having 3 lives in contra was like saying Im already dead but Im going to try anyway (good times), games back then were for the ones with skill not with time. But I digress… lives & score.. lets add them to the game.

The concept of lives and score is transversal to any level, and should be passed on to each level or exist in a global concept. We're adopting the second one. A new class called PlayerInfo will house that information.

package tutorials.slickout.playerinfo;
 
public class PlayerInfo {
	private static PlayerInfo _instance = null;
 
	public static PlayerInfo getCurrentPlayerInfo(){
		return _instance;
	}
 
	public static PlayerInfo createNewCurrentPlayerInfo(){
		_instance = new PlayerInfo();
 
		return getCurrentPlayerInfo();
	}
 
	private String name;
	private int lives;
	private int score;
 
 
	private PlayerInfo(){
		name ="AAA";
		lives = 3;
		score = 0;
	}
 
 
	public final String getName() {
		return name;
	}
 
 
	public final int getLives() {
		return lives;
	}
 
 
	public final int getScore() {
		return score;
	}
 
 
	public final void setName(String name) {
		this.name = name;
	}
 
 
	public final void incrementLives() {
		this.lives++;
	}
 
	public final void decrementLives() {
		this.lives--;
	}
 
	public final void addScore(int score) {
		this.score += score;
	}
 
	public final void decreaseScore(int score) {
		this.score -= score;
	}
}

Using the createNewPlayerInfo static method a new instance is created that can be acessed at any time by the method getCurrentPlayerInfo static method.

Lets create a simple PlayerInfo for us to play, we'll remove it as soon as we create a menu. We're adding it to the SlickoutGame class.

Change the method initStatesList to :

public void initStatesList(GameContainer gc) throws SlickException {
 
		PlayerInfo.createNewCurrentPlayerInfo();
 
		GameplayState state = new GameplayState();
 
		state.setLevelFile("data/level1.lvl");
 
		addState(state);
	}

Now for the gameplay state:

Lets add a new member to the class.

private PlayerInfo playerInfo;

And in the enter method add:

playerInfo = PlayerInfo.getCurrentPlayerInfo();

At the bootom of the render method add:

gr.drawString("Lives: " + playerInfo.getLives(), 700, 10);
		gr.drawString("Score: " + playerInfo.getScore(), 500, 10);

And finally, add in the brickBallHandler:

// since the brick was hit, decrease the number of hits to be detroyed
		brick.decreaseHit();
 
		PlayerInfo.getCurrentPlayerInfo().addScore(100);
 
		// if the brick has reached the last hit
		if(brick.getHitsLeft() == 0){
			// remove the brick from the level list
			levelData.getBricks().remove(brick);
			// remove it from the collision manager
			manager.removeCollidable(brick);
 
			PlayerInfo.getCurrentPlayerInfo().addScore(250);
		}

Run the game and now you have scores… Its always nice to score… fun to.

Next for the lives concept, in the normal state in update add to the bottom:

// check if ball drops from screen
			if(level.getBalls().size() == 0){
				currentState = LEVEL_STATES.LIFE_LOST;
			}

and add two new members to the class:

private int counter = 0;
	private String message = null;

add to the render:

gr.drawString("Lives: " + playerInfo.getLives(), 700, 10);
		gr.drawString("Score: " + playerInfo.getScore(), 500, 10);
 
		if(message != null){
			gr.drawString(message, 300, 300);
		}

and finally add, in the normal state of the update method:

case LIFE_LOST:
			playerInfo.decrementLives();
 
			if(playerInfo.getLives() == 0){
				currentState = LEVEL_STATES.GAME_OVER;
				counter = 3000;
			}else{
				currentState = LEVEL_STATES.BALL_LAUNCH;
				level.getPaddle().setState(PAD_STATE.STICKY);
			}
			break;
		case LEVEL_OVER:
			if(counter == 3000){
				gc.getInput().removeMouseListener(level.getPaddle());
				message = "CONGRATULATIONS!\nLevel completed!";
			}
			counter -= delta;
 
			if(counter < 0){
				// jump to level selection screen
			}
			break;
		case GAME_OVER:
			if(counter == 3000){
				gc.getInput().removeMouseListener(level.getPaddle());
				message = "GAME OVER\nSCORE :" + PlayerInfo.getCurrentPlayerInfo().getScore();
			}
			counter -= delta;
 
			if(counter < 0){
				// jump to main menu screen
			}
			break;
 
		}

This will implement all of our game, lives, score and game structure. Running the game will now enable the concept of lives and score. Also a pretty message will appear on the game screen once you clear the level or its game over.

So now all we need is a shiny main menu and a level selector.

Section 4: Whats in the menu

Finally the last part, soon it will all be over. So now he have our fully implemented gameplay, we now need a couple of stuff like menus and stuff like that.

So what do we need?

  • A main menu
  • A pre-level selection screen

First of all lets destroy the PlayerInfo we created befor, go to the SlickoutGame class and delete that entry. Next we Create a basic Gamestate:

package tutorials.slickout.mainmenu;
 
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.Image;
import org.newdawn.slick.MouseListener;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.state.BasicGameState;
import org.newdawn.slick.state.StateBasedGame;
 
import tutorials.slickout.playerinfo.PlayerInfo;
 
public class MainMenuGameState extends BasicGameState implements MouseListener{
 
	@Override
	public int getID() {
		return 0;
	}
 
	@Override
	public void enter(GameContainer container, StateBasedGame game)
			throws SlickException {
	}
 
	@Override
	public void init(GameContainer container, StateBasedGame game)
			throws SlickException {
	}
 
	@Override
	public void render(GameContainer container, StateBasedGame game, Graphics g)
			throws SlickException {
	}
 
	@Override
	public void update(GameContainer container, StateBasedGame game, int delta)
			throws SlickException {
	}
}

Next we're going to add the following memebrs:

private Image background;
	private Image selector;
	private int selection; 
	private int optionSelected;
	private int topScore;

And initialize it on the init method:

background = new Image("data/mainmenu.jpg");
		selector = new Image("data/selector.png");

And the render:

@Override
	public void render(GameContainer container, StateBasedGame game, Graphics g)
			throws SlickException {
		background.draw();
 
		if(selection == 1){
			selector.draw(158, 310);
			selector.draw(694, 310);
 
			PlayerInfo.createNewCurrentPlayerInfo();
		}else if(selection == 2){
			selector.draw(158, 474);
			selector.draw(694, 474);
		}
 
		// TODO: Log this
		g.drawString("TOPSCORE : " + topScore, 10, 10) ;
	}

Now lets add some mouse support, any gamestate is already a input Listener so it has all methods ready for you to override:

public void mouseMoved(int oldx, int oldy, int newX, int newY){
	}
 
	public void mouseClicked(int button, int x, int y, int clickCount){
	}

There is no need to register the listener because Slick does this for us already. Now for the meat :

public void mouseMoved(int oldx, int oldy, int newX, int newY){
 
		if(newX > 228 && newX < 702){
// start game
			if ( newY > 308 && newY < 389){
				selection = 1;
// exit game
			}else if ( newY > 475 && newY < 544){
				selection = 2;
			}else {
				selection = -1;
			}
		}
	}

This shows that if the mouse passes over the Startgame, the selection Id is 1, if exit game selection id is 2, -1 if none is selected, those positions are just the screen positions for the options

And

public void mouseClicked(int button, int x, int y, int clickCount){
		optionSelected = selection;
	}

This means that when the mouse is clicked the options selected is the same as the selection made by the mouse position. So when the mouse if over the startmenu and is clicked then the selected option becomes the startmenu.

Now we do all this because we have to register what option is selected, so that the update method knows what to do. A minor note, we could do the update part of the code, in the listener methods directly and it would work, but I like to separate both, the listener methods change the internal state of the gamestate while the update method uses that internal state to make any necessary changes to the world. Now the update method:

@Override
	public void update(GameContainer container, StateBasedGame game, int delta)
			throws SlickException {
		if(optionSelected == 1){
			game.enterState(2);
		}else if(optionSelected == 2){
			System.exit(0);
		}
	}

If the start game is selected it will be launced to the level selection gamestate(id 2), if the exit game is selected the game will exit.

Finally we need to initialize the gamestate internal state in the enter method:

@Override
	public void enter(GameContainer container, StateBasedGame game)
			throws SlickException {
		selection = -1;
		optionSelected = selection;
 
		if ( PlayerInfo.getCurrentPlayerInfo() != null){
			topScore = ( topScore > PlayerInfo.getCurrentPlayerInfo().getScore() ) ? topScore : PlayerInfo.getCurrentPlayerInfo().getScore(); 
		}
	}

Now to add this to our game, change the initStatesList method , in SlickoutGame, class to the following:

public void initStatesList(GameContainer gc) throws SlickException {
 
		PlayerInfo.createNewCurrentPlayerInfo();
 
		addState(new MainMenuGameState());
 
		GameplayState state = new GameplayState();
 
		addState(state);
	}

This concludes our main menu state.

A pre-level selection screen

Now for the pre level selection screen. The level selector will display different game levels, that will be selectable. When no more levels are selected, a fireworks display will be shown as a congratulatory screen for finishing the game.

Before we start, lets change the playerInfo a bit, first of all comment the static generators:

/*
	private static PlayerInfo _instance = null;
 
	public static PlayerInfo getCurrentPlayerInfo(){
		return _instance;
	}
 
	public static PlayerInfo createNewCurrentPlayerInfo(){
		_instance = new PlayerInfo();
 
		return getCurrentPlayerInfo();
	}*/

Then change the visibility of the constructor to public:

public PlayerInfo(){
		name ="AAA";
		lives = 3;
		score = 0;
	}

Now lets add a new class, a GameInfo class:

package tutorials.slickout;
 
import tutorials.slickout.playerinfo.PlayerInfo;
 
 
public class GameInfo {
private static GameInfo _instance = null;
 
	private PlayerInfo playerInfo;
	private boolean[] completedLevels;
	private boolean allLevelsCompleted;
 
	public static GameInfo getCurrentGameInfo(){
		return _instance;
	}
 
	public static GameInfo createNewGameInfo(){
		_instance = new GameInfo();
 
		return getCurrentGameInfo();
	}
 
	private GameInfo(){
		playerInfo = new PlayerInfo();
 
		completedLevels = new boolean[6];
 
		for(boolean level : completedLevels){
			level = false;
		}
 
		allLevelsCompleted = false;
	}
 
	public PlayerInfo getPlayerInfo(){
		return playerInfo;
	}
 
	public void setCompletedLevel(int levelId){
		completedLevels[levelId-1] = true;
 
		allLevelsCompleted = true;
 
		for(boolean level : completedLevels){
			allLevelsCompleted &= level;
		}
	}
 
	public boolean isLevelCompleted(int levelId){
		return completedLevels[levelId-1];
	}
 
	public boolean allLevelsCompleted(){
		return allLevelsCompleted;
	}
 
 
}

This will hold our game information from now on, change MainMenuGameState to:

	@Override
	public void enter(GameContainer container, StateBasedGame game)
			throws SlickException {
		selection = -1;
		optionSelected = selection;
 
		if ( GameInfo.getCurrentGameInfo() != null){
			topScore = ( topScore > GameInfo.getCurrentGameInfo().getPlayerInfo().getScore() ) ? topScore : GameInfo.getCurrentGameInfo().getPlayerInfo().getScore(); 
		}
	}

and

@Override
	public void render(GameContainer container, StateBasedGame game, Graphics g)
			throws SlickException {
		background.draw();
 
		if(selection == 1){
			selector.draw(158, 310);
			selector.draw(694, 310);
 
			GameInfo.createNewGameInfo();
		}else if(selection == 2){
			selector.draw(158, 474);
			selector.draw(694, 474);
		}
 
		g.drawString("TOPSCORE : " + topScore, 10, 10) ;
	}

Now for the gameplayState, change the enter method to :

@Override
	public void enter(GameContainer gc, StateBasedGame sbg)
			throws SlickException {
 
		// load level
		if(levelFile == null){
			throw new SlickException("No level to load"); 
		}
 
		try {
			level = LevelImpl.loadLevel( new FileInputStream(new File(levelFile)) );
		} catch (FileNotFoundException e) {
			throw new SlickException("Could not load file");
		}
 
		collisionManager = new CollisionManager();
 
		collisionManager.addCollidable(level.getLeftBumper());
		collisionManager.addCollidable(level.getRightBumper());
		collisionManager.addCollidable(level.getTopBumper());
 
		collisionManager.addCollidable(level.getPaddle());
 
		for(Brick brick : level.getBricks()){
			collisionManager.addCollidable(brick);
		}
 
		collisionManager.addHandler(new BumperAndPadBallCollisionHandler());
		collisionManager.addHandler(new BrickBallCollisionHandler(level, collisionManager));
 
		gc.getInput().addMouseListener(level.getPaddle());
 
		currentState = LEVEL_STATES.BALL_LAUNCH;
 
		playerInfo = GameInfo.getCurrentGameInfo().getPlayerInfo();
	}

And the GAME_OVER case in the update method to:

case LEVEL_OVER:
			if(counter == 3000){
				gc.getInput().removeMouseListener(level.getPaddle());
				message = "CONGRATULATIONS!\nLevel completed!";
			}
			counter -= delta;
 
			if(counter < 0){
				// jump to level selection screen
			}
			break;

Finally the brickballcollisionhandler change in the perform collision method:

GameInfo.getCurrentGameInfo().getPlayerInfo().addScore(100);
 
		// if the brick has reached the last hit
		if(brick.getHitsLeft() == 0){
			// remove the brick from the level list
			levelData.getBricks().remove(brick);
			// remove it from the collision manager
			manager.removeCollidable(brick);
 
			GameInfo.getCurrentGameInfo().getPlayerInfo().addScore(250);
		}

Now, lets add the levelselector gamestate. Once again well start with the skeleton for our gamestate:

package tutorials.slickout.levelselector;
 
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.state.BasicGameState;
import org.newdawn.slick.state.StateBasedGame;
 
public class LevelSelector extends BasicGameState {
 
	@Override
	public int getID() {
		// level selector id
		return 2;
	}
 
	@Override
	public void init(GameContainer container, StateBasedGame game)
			throws SlickException {
	}
 
	@Override
	public void render(GameContainer container, StateBasedGame game, Graphics g)
			throws SlickException {
	}
 
	@Override
	public void update(GameContainer container, StateBasedGame game, int delta)
			throws SlickException {
	}
 
}

Lets add the members:

private Image background;
	private Image ended;
	private int option;
	private int optionSelected;
	private int counter;

And the init method:

@Override
	public void init(GameContainer container, StateBasedGame game)
			throws SlickException {
 
		background = new Image("data/levelselection.jpg");
		ended = new Image("data/levelend.png");
 
		counter = 5000;
	}

As well as the enter method:

	@Override
	public void enter(GameContainer container, StateBasedGame game)
			throws SlickException {
 
		optionSelected = -1;
	}

Now we want to have a mouse listener available so lets change the class to:

public class LevelSelector extends BasicGameState implements MouseListener {

and add the following methods:

@Override
	public void mouseClicked(int button, int x, int y, int clickCount){
		optionSelected = option;
	}
 
	@Override
	public void mouseMoved(int oldx, int oldy, int newx, int newy){
		int line = 0;
		if( newy > 150 && newy < 325 ){
			line = 0;
		}else if( newy > 375  && newy < 550 ){
			line = 1;
		}
 
		int row = -1;
 
		for(int i = 0; i < 3; i++){
			int minx = 50 + (250 * i);
			int maxx = minx+200;
 
			if(newx > minx && newx< maxx){
				row = i+1;
			}
		}
 
		option = (line*3)+row;
	}

This will create a selection of the six levels into an integer.

Next the render:

@Override
	public void render(GameContainer container, StateBasedGame game, Graphics g)
			throws SlickException {
 
		background.draw();
 
		for(int i = 0; i < 6; i++){
			if ( GameInfo.getCurrentGameInfo().isLevelCompleted(i+1)){
				int x = 50 + (250 * ((i < 3) ? i : i-3));
				int y = 150+(225* ((i < 3 ) ? 0 : 1));
				ended.draw(x, y);
			}
		}
 
		if(GameInfo.getCurrentGameInfo().allLevelsCompleted()){
			g.drawString("CONGRATULATIONS... YOU'VE FINISHED THIS TUTORIAL", 200, 300);
		}
	}

This method will render hexagons on top of the levels already passed.

And finally the update:

	@Override
	public void update(GameContainer container, StateBasedGame game, int delta)
			throws SlickException {
 
		if(!GameInfo.getCurrentGameInfo().allLevelsCompleted()){
			if(optionSelected != -1 && !GameInfo.getCurrentGameInfo().isLevelCompleted(optionSelected)){
				String levelfile = "data/level"+optionSelected+".lvl";
				// obtain the game state
				GameplayState gameplay = (GameplayState) game.getState(1);
 
				gameplay.setLevelFile(levelfile);
 
				GameInfo.getCurrentGameInfo().setCompletedLevel(optionSelected);
 
				game.enterState(1);
			}
		}else{
			counter -= delta;
 
			if(counter < 0){
				game.enterState(0);
			}
		}
 
 
	}

This method check the selected option, opens the file “level#.lvl”, pass it on to the gameplay state and then change state to the gameplay to go to the actual game. Finally if all levels are done, a message will be displayed, waits for 5 seconds and jumps to the menu.

Now all we need to do is to add this state to the game, go to the slickout class and change the initstateslist to:

	public void initStatesList(GameContainer gc) throws SlickException {
 
		addState(new MainMenuGameState());
 
		GameplayState state = new GameplayState();
 
		addState(new LevelSelector());
 
		addState(state);
	}

And we're done with the game. If everything goes well the whole game will launch and you can play six levels.

This concludes the second part of our 4 part game development tutorials. This as a big one, but we covered a lot of ground here.

Resources

ball

bricks

Background

Bumpers

Paddle

Menu mainmenu.jpg

levelselection.jpg

Level files

The game needs 6 level files to work from level1.lvl to level6.lvl:

This is a simple file with just one brick, feel free to create new levels using this as template.

# Background
background|IMAGE;data/carbonfiber.png|0, 0
# Left bumper
leftBumper|IMAGE;data/leftbumper.png|-10,0|1;RECTANGLE;10,0,20,600
# Right bumper
rightBumper|IMAGE;data/rightbumper.png|760,0|1;RECTANGLE;20,0,20,600
# Top bumper
topBumper|IMAGE;data/topbumper.png|40,-1|1;RECTANGLE;-40,10,800,20
# Paddle
paddle|ANIMATION;data/padanimation.png;100,20,1000|300,570|1;RECTANGLE;0,0,100,20
# Ball
ball|IMAGE;data/ball.png|337,550|2;CIRCLE;10,10,10|0.5
# Bricks
red|ANIMATION;data/brickanimationred.png;50,20,500|35,75|3;RECTANGLE;0,0,50,20|255,0,0;1
red|ANIMATION;data/brickanimationred.png;50,20,500|90,75|3;RECTANGLE;0,0,50,20|255,0,0;1
red|ANIMATION;data/brickanimationred.png;50,20,500|145,75|3;RECTANGLE;0,0,50,20|255,0,0;1
red|ANIMATION;data/brickanimationred.png;50,20,500|200,75|3;RECTANGLE;0,0,50,20|255,0,0;1
red|ANIMATION;data/brickanimationred.png;50,20,500|255,75|3;RECTANGLE;0,0,50,20|255,0,0;1
red|ANIMATION;data/brickanimationred.png;50,20,500|310,75|3;RECTANGLE;0,0,50,20|255,0,0;1
red|ANIMATION;data/brickanimationred.png;50,20,500|365,75|3;RECTANGLE;0,0,50,20|255,0,0;1
red|ANIMATION;data/brickanimationred.png;50,20,500|420,75|3;RECTANGLE;0,0,50,20|255,0,0;1
red|ANIMATION;data/brickanimationred.png;50,20,500|475,75|3;RECTANGLE;0,0,50,20|255,0,0;1
red|ANIMATION;data/brickanimationred.png;50,20,500|530,75|3;RECTANGLE;0,0,50,20|255,0,0;1
red|ANIMATION;data/brickanimationred.png;50,20,500|585,75|3;RECTANGLE;0,0,50,20|255,0,0;1
red|ANIMATION;data/brickanimationred.png;50,20,500|640,75|3;RECTANGLE;0,0,50,20|255,0,0;1
red|ANIMATION;data/brickanimationred.png;50,20,500|695,75|3;RECTANGLE;0,0,50,20|255,0,0;1

blue|ANIMATION;data/brickanimationblue.png;50,20,500|35,100|3;RECTANGLE;0,0,50,20|255,0,0;2
blue|ANIMATION;data/brickanimationblue.png;50,20,500|90,100|3;RECTANGLE;0,0,50,20|255,0,0;2
blue|ANIMATION;data/brickanimationblue.png;50,20,500|145,100|3;RECTANGLE;0,0,50,20|255,0,0;2
blue|ANIMATION;data/brickanimationblue.png;50,20,500|200,100|3;RECTANGLE;0,0,50,20|255,0,0;2
blue|ANIMATION;data/brickanimationblue.png;50,20,500|255,100|3;RECTANGLE;0,0,50,20|255,0,0;2
blue|ANIMATION;data/brickanimationblue.png;50,20,500|310,100|3;RECTANGLE;0,0,50,20|255,0,0;2
blue|ANIMATION;data/brickanimationblue.png;50,20,500|365,100|3;RECTANGLE;0,0,50,20|255,0,0;2
blue|ANIMATION;data/brickanimationblue.png;50,20,500|420,100|3;RECTANGLE;0,0,50,20|255,0,0;2
blue|ANIMATION;data/brickanimationblue.png;50,20,500|475,100|3;RECTANGLE;0,0,50,20|255,0,0;2
blue|ANIMATION;data/brickanimationblue.png;50,20,500|530,100|3;RECTANGLE;0,0,50,20|255,0,0;2
blue|ANIMATION;data/brickanimationblue.png;50,20,500|585,100|3;RECTANGLE;0,0,50,20|255,0,0;2
blue|ANIMATION;data/brickanimationblue.png;50,20,500|640,100|3;RECTANGLE;0,0,50,20|255,0,0;2
blue|ANIMATION;data/brickanimationblue.png;50,20,500|695,100|3;RECTANGLE;0,0,50,20|255,0,0;2

yellow|ANIMATION;data/brickanimationyellow.png;50,20,500|35,125|3;RECTANGLE;0,0,50,20|255,0,0;3
yellow|ANIMATION;data/brickanimationyellow.png;50,20,500|90,125|3;RECTANGLE;0,0,50,20|255,0,0;3
yellow|ANIMATION;data/brickanimationyellow.png;50,20,500|145,125|3;RECTANGLE;0,0,50,20|255,0,0;3
yellow|ANIMATION;data/brickanimationyellow.png;50,20,500|200,125|3;RECTANGLE;0,0,50,20|255,0,0;3
yellow|ANIMATION;data/brickanimationyellow.png;50,20,500|255,125|3;RECTANGLE;0,0,50,20|255,0,0;3
yellow|ANIMATION;data/brickanimationyellow.png;50,20,500|310,125|3;RECTANGLE;0,0,50,20|255,0,0;3
yellow|ANIMATION;data/brickanimationyellow.png;50,20,500|365,125|3;RECTANGLE;0,0,50,20|255,0,0;3
yellow|ANIMATION;data/brickanimationyellow.png;50,20,500|420,125|3;RECTANGLE;0,0,50,20|255,0,0;3
yellow|ANIMATION;data/brickanimationyellow.png;50,20,500|475,125|3;RECTANGLE;0,0,50,20|255,0,0;3
yellow|ANIMATION;data/brickanimationyellow.png;50,20,500|530,125|3;RECTANGLE;0,0,50,20|255,0,0;3
yellow|ANIMATION;data/brickanimationyellow.png;50,20,500|585,125|3;RECTANGLE;0,0,50,20|255,0,0;3
yellow|ANIMATION;data/brickanimationyellow.png;50,20,500|640,125|3;RECTANGLE;0,0,50,20|255,0,0;3
yellow|ANIMATION;data/brickanimationyellow.png;50,20,500|695,125|3;RECTANGLE;0,0,50,20|255,0,0;3

yellow|ANIMATION;data/brickanimationyellow.png;50,20,500|35,150|3;RECTANGLE;0,0,50,20|255,0,0;3
yellow|ANIMATION;data/brickanimationyellow.png;50,20,500|90,150|3;RECTANGLE;0,0,50,20|255,0,0;3
yellow|ANIMATION;data/brickanimationyellow.png;50,20,500|145,150|3;RECTANGLE;0,0,50,20|255,0,0;3
yellow|ANIMATION;data/brickanimationyellow.png;50,20,500|200,150|3;RECTANGLE;0,0,50,20|255,0,0;3
yellow|ANIMATION;data/brickanimationyellow.png;50,20,500|255,150|3;RECTANGLE;0,0,50,20|255,0,0;3
yellow|ANIMATION;data/brickanimationyellow.png;50,20,500|310,150|3;RECTANGLE;0,0,50,20|255,0,0;3
yellow|ANIMATION;data/brickanimationyellow.png;50,20,500|365,150|3;RECTANGLE;0,0,50,20|255,0,0;3
yellow|ANIMATION;data/brickanimationyellow.png;50,20,500|420,150|3;RECTANGLE;0,0,50,20|255,0,0;3
yellow|ANIMATION;data/brickanimationyellow.png;50,20,500|475,150|3;RECTANGLE;0,0,50,20|255,0,0;3
yellow|ANIMATION;data/brickanimationyellow.png;50,20,500|530,150|3;RECTANGLE;0,0,50,20|255,0,0;3
yellow|ANIMATION;data/brickanimationyellow.png;50,20,500|585,150|3;RECTANGLE;0,0,50,20|255,0,0;3
yellow|ANIMATION;data/brickanimationyellow.png;50,20,500|640,150|3;RECTANGLE;0,0,50,20|255,0,0;3
yellow|ANIMATION;data/brickanimationyellow.png;50,20,500|695,150|3;RECTANGLE;0,0,50,20|255,0,0;3

blue|ANIMATION;data/brickanimationblue.png;50,20,500|35,175|3;RECTANGLE;0,0,50,20|255,0,0;2
blue|ANIMATION;data/brickanimationblue.png;50,20,500|90,175|3;RECTANGLE;0,0,50,20|255,0,0;2
blue|ANIMATION;data/brickanimationblue.png;50,20,500|145,175|3;RECTANGLE;0,0,50,20|255,0,0;2
blue|ANIMATION;data/brickanimationblue.png;50,20,500|200,175|3;RECTANGLE;0,0,50,20|255,0,0;2
blue|ANIMATION;data/brickanimationblue.png;50,20,500|255,175|3;RECTANGLE;0,0,50,20|255,0,0;2
blue|ANIMATION;data/brickanimationblue.png;50,20,500|310,175|3;RECTANGLE;0,0,50,20|255,0,0;2
blue|ANIMATION;data/brickanimationblue.png;50,20,500|365,175|3;RECTANGLE;0,0,50,20|255,0,0;2
blue|ANIMATION;data/brickanimationblue.png;50,20,500|420,175|3;RECTANGLE;0,0,50,20|255,0,0;2
blue|ANIMATION;data/brickanimationblue.png;50,20,500|475,175|3;RECTANGLE;0,0,50,20|255,0,0;2
blue|ANIMATION;data/brickanimationblue.png;50,20,500|530,175|3;RECTANGLE;0,0,50,20|255,0,0;2
blue|ANIMATION;data/brickanimationblue.png;50,20,500|585,175|3;RECTANGLE;0,0,50,20|255,0,0;2
blue|ANIMATION;data/brickanimationblue.png;50,20,500|640,175|3;RECTANGLE;0,0,50,20|255,0,0;2
blue|ANIMATION;data/brickanimationblue.png;50,20,500|695,175|3;RECTANGLE;0,0,50,20|255,0,0;2

blue|ANIMATION;data/brickanimationblue.png;50,20,500|35,200|3;RECTANGLE;0,0,50,20|255,0,0;2
blue|ANIMATION;data/brickanimationblue.png;50,20,500|90,200|3;RECTANGLE;0,0,50,20|255,0,0;2
blue|ANIMATION;data/brickanimationblue.png;50,20,500|145,200|3;RECTANGLE;0,0,50,20|255,0,0;2
blue|ANIMATION;data/brickanimationblue.png;50,20,500|200,200|3;RECTANGLE;0,0,50,20|255,0,0;2
blue|ANIMATION;data/brickanimationblue.png;50,20,500|255,200|3;RECTANGLE;0,0,50,20|255,0,0;2
blue|ANIMATION;data/brickanimationblue.png;50,20,500|310,200|3;RECTANGLE;0,0,50,20|255,0,0;2
blue|ANIMATION;data/brickanimationblue.png;50,20,500|365,200|3;RECTANGLE;0,0,50,20|255,0,0;2
blue|ANIMATION;data/brickanimationblue.png;50,20,500|420,200|3;RECTANGLE;0,0,50,20|255,0,0;2
blue|ANIMATION;data/brickanimationblue.png;50,20,500|475,200|3;RECTANGLE;0,0,50,20|255,0,0;2
blue|ANIMATION;data/brickanimationblue.png;50,20,500|530,200|3;RECTANGLE;0,0,50,20|255,0,0;2
blue|ANIMATION;data/brickanimationblue.png;50,20,500|585,200|3;RECTANGLE;0,0,50,20|255,0,0;2
blue|ANIMATION;data/brickanimationblue.png;50,20,500|640,200|3;RECTANGLE;0,0,50,20|255,0,0;2
blue|ANIMATION;data/brickanimationblue.png;50,20,500|695,200|3;RECTANGLE;0,0,50,20|255,0,0;2

red|ANIMATION;data/brickanimationred.png;50,20,500|35,225|3;RECTANGLE;0,0,50,20|255,0,0;1
red|ANIMATION;data/brickanimationred.png;50,20,500|90,225|3;RECTANGLE;0,0,50,20|255,0,0;1
red|ANIMATION;data/brickanimationred.png;50,20,500|145,225|3;RECTANGLE;0,0,50,20|255,0,0;1
red|ANIMATION;data/brickanimationred.png;50,20,500|200,225|3;RECTANGLE;0,0,50,20|255,0,0;1
red|ANIMATION;data/brickanimationred.png;50,20,500|255,225|3;RECTANGLE;0,0,50,20|255,0,0;1
red|ANIMATION;data/brickanimationred.png;50,20,500|310,225|3;RECTANGLE;0,0,50,20|255,0,0;1
red|ANIMATION;data/brickanimationred.png;50,20,500|365,225|3;RECTANGLE;0,0,50,20|255,0,0;1
red|ANIMATION;data/brickanimationred.png;50,20,500|420,225|3;RECTANGLE;0,0,50,20|255,0,0;1
red|ANIMATION;data/brickanimationred.png;50,20,500|475,225|3;RECTANGLE;0,0,50,20|255,0,0;1
red|ANIMATION;data/brickanimationred.png;50,20,500|530,225|3;RECTANGLE;0,0,50,20|255,0,0;1
red|ANIMATION;data/brickanimationred.png;50,20,500|585,225|3;RECTANGLE;0,0,50,20|255,0,0;1
red|ANIMATION;data/brickanimationred.png;50,20,500|640,225|3;RECTANGLE;0,0,50,20|255,0,0;1
red|ANIMATION;data/brickanimationred.png;50,20,500|695,225|3;RECTANGLE;0,0,50,20|255,0,0;1

red|ANIMATION;data/brickanimationred.png;50,20,500|35,250|3;RECTANGLE;0,0,50,20|255,0,0;1
red|ANIMATION;data/brickanimationred.png;50,20,500|90,250|3;RECTANGLE;0,0,50,20|255,0,0;1
red|ANIMATION;data/brickanimationred.png;50,20,500|145,250|3;RECTANGLE;0,0,50,20|255,0,0;1
red|ANIMATION;data/brickanimationred.png;50,20,500|200,250|3;RECTANGLE;0,0,50,20|255,0,0;1
red|ANIMATION;data/brickanimationred.png;50,20,500|255,250|3;RECTANGLE;0,0,50,20|255,0,0;1
red|ANIMATION;data/brickanimationred.png;50,20,500|310,250|3;RECTANGLE;0,0,50,20|255,0,0;1
red|ANIMATION;data/brickanimationred.png;50,20,500|365,250|3;RECTANGLE;0,0,50,20|255,0,0;1
red|ANIMATION;data/brickanimationred.png;50,20,500|420,250|3;RECTANGLE;0,0,50,20|255,0,0;1
red|ANIMATION;data/brickanimationred.png;50,20,500|475,250|3;RECTANGLE;0,0,50,20|255,0,0;1
red|ANIMATION;data/brickanimationred.png;50,20,500|530,250|3;RECTANGLE;0,0,50,20|255,0,0;1
red|ANIMATION;data/brickanimationred.png;50,20,500|585,250|3;RECTANGLE;0,0,50,20|255,0,0;1
red|ANIMATION;data/brickanimationred.png;50,20,500|640,250|3;RECTANGLE;0,0,50,20|255,0,0;1
red|ANIMATION;data/brickanimationred.png;50,20,500|695,250|3;RECTANGLE;0,0,50,20|255,0,0;1

FULL CODE

SlickOutGame

/* ************************************************************************************
 * Copyright (c) 2010, FoxholeStudios
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, 
 * are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this list 
 * of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice, this 
 * list of conditions and the following disclaimer in the documentation and/or other 
 * materials provided with the distribution.
 * Neither the name of FoxholeStudios nor the names of its contributors may be used 
 * to endorse or promote products derived from this software without specific prior 
 * written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 
 * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 
 * DAMAGE.
 * ************************************************************************************/
 
package tutorials.slickout;
 
import org.newdawn.slick.AppGameContainer;
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.state.StateBasedGame;
 
import tutorials.slickout.gameplay.GameplayState;
import tutorials.slickout.levelselector.LevelSelector;
import tutorials.slickout.mainmenu.MainMenuGameState;
import tutorials.slickout.playerinfo.PlayerInfo;
 
 
public class SlickOutGame extends StateBasedGame {
	public SlickOutGame() {
		super("Slick Tutorials : SlickOut");
	}
 
	public void initStatesList(GameContainer gc) throws SlickException {
 
		addState(new MainMenuGameState());
 
		GameplayState state = new GameplayState();
 
		addState(new LevelSelector());
 
		addState(state);
	}
 
	public static void main(String[] args) throws SlickException
    {
         AppGameContainer app = new AppGameContainer(new SlickOutGame());
 
         // Application properties
         app.setDisplayMode(800, 600, false);
         //app.setSmoothDeltas(true);
 
         app.start();
    }
 
 
}

GameInfo

/* ************************************************************************************
 * Copyright (c) 2010, FoxholeStudios
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, 
 * are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this list 
 * of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice, this 
 * list of conditions and the following disclaimer in the documentation and/or other 
 * materials provided with the distribution.
 * Neither the name of FoxholeStudios nor the names of its contributors may be used 
 * to endorse or promote products derived from this software without specific prior 
 * written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 
 * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 
 * DAMAGE.
 * ************************************************************************************/
 
package tutorials.slickout;
 
import tutorials.slickout.playerinfo.PlayerInfo;
 
 
public class GameInfo {
private static GameInfo _instance = null;
 
	private PlayerInfo playerInfo;
	private boolean[] completedLevels;
	private boolean allLevelsCompleted;
 
	public static GameInfo getCurrentGameInfo(){
		return _instance;
	}
 
	public static GameInfo createNewGameInfo(){
		_instance = new GameInfo();
 
		return getCurrentGameInfo();
	}
 
	private GameInfo(){
		playerInfo = new PlayerInfo();
 
		completedLevels = new boolean[6];
 
		for(boolean level : completedLevels){
			level = false;
		}
 
		allLevelsCompleted = false;
	}
 
	public PlayerInfo getPlayerInfo(){
		return playerInfo;
	}
 
	public void setCompletedLevel(int levelId){
		completedLevels[levelId-1] = true;
 
		allLevelsCompleted = true;
 
		for(boolean level : completedLevels){
			allLevelsCompleted &= level;
		}
	}
 
	public boolean isLevelCompleted(int levelId){
		return completedLevels[levelId-1];
	}
 
	public boolean allLevelsCompleted(){
		return allLevelsCompleted;
	}
 
 
}
 

GameplayState

/* ************************************************************************************
 * Copyright (c) 2010, FoxholeStudios
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, 
 * are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this list 
 * of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice, this 
 * list of conditions and the following disclaimer in the documentation and/or other 
 * materials provided with the distribution.
 * Neither the name of FoxholeStudios nor the names of its contributors may be used 
 * to endorse or promote products derived from this software without specific prior 
 * written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 
 * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 
 * DAMAGE.
 * ************************************************************************************/
 
package tutorials.slickout.gameplay;
 
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
 
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.geom.Vector2f;
import org.newdawn.slick.state.BasicGameState;
import org.newdawn.slick.state.StateBasedGame;
 
import tutorials.slickout.GameInfo;
import tutorials.slickout.gameplay.collision.CollisionManager;
import tutorials.slickout.gameplay.level.Ball;
import tutorials.slickout.gameplay.level.Brick;
import tutorials.slickout.gameplay.level.ILevel;
import tutorials.slickout.gameplay.level.LevelImpl;
import tutorials.slickout.gameplay.level.Paddle;
import tutorials.slickout.gameplay.level.Paddle.PAD_STATE;
import tutorials.slickout.gameplay.level.collision.BrickBallCollisionHandler;
import tutorials.slickout.gameplay.level.collision.BumperAndPadBallCollisionHandler;
import tutorials.slickout.playerinfo.PlayerInfo;
 
public class GameplayState extends BasicGameState {
 
	private ILevel level;
	private String levelFile;
	private PlayerInfo playerInfo;
 
	private int counter = 0;
	private String message = null;
 
	private static enum LEVEL_STATES { BALL_LAUNCH, NORMAL_GAME, LIFE_LOST, LEVEL_OVER, GAME_OVER };
 
	private LEVEL_STATES currentState;
 
	private CollisionManager collisionManager;
 
	@Override
	public int getID() {
		return 1;
	}
 
	@Override
	public void init(GameContainer arg0, StateBasedGame arg1)
			throws SlickException {
	}
 
	@Override
	public void enter(GameContainer gc, StateBasedGame sbg)
			throws SlickException {
 
		// load level
		if(levelFile == null){
			throw new SlickException("No level to load"); 
		}
 
		try {
			level = LevelImpl.loadLevel( new FileInputStream(new File(levelFile)) );
		} catch (FileNotFoundException e) {
			throw new SlickException("Could not load file");
		}
 
		collisionManager = new CollisionManager();
 
		collisionManager.addCollidable(level.getLeftBumper());
		collisionManager.addCollidable(level.getRightBumper());
		collisionManager.addCollidable(level.getTopBumper());
 
		collisionManager.addCollidable(level.getPaddle());
 
		for(Brick brick : level.getBricks()){
			collisionManager.addCollidable(brick);
		}
 
		collisionManager.addHandler(new BumperAndPadBallCollisionHandler());
		collisionManager.addHandler(new BrickBallCollisionHandler(level, collisionManager));
 
		gc.getInput().addMouseListener(level.getPaddle());
 
		currentState = LEVEL_STATES.BALL_LAUNCH;
 
		playerInfo = GameInfo.getCurrentGameInfo().getPlayerInfo();
	}
 
	@Override
	public void render(GameContainer gc, StateBasedGame sbg, Graphics gr)
			throws SlickException {
 
		// Background
		level.getBackground().render(gr);
 
		// Bumpers
		level.getLeftBumper().render(gr);
 
		level.getRightBumper().render(gr);
 
		level.getTopBumper().render(gr);
 
		// Bricks
		for ( Brick brick : level.getBricks() ){
			brick.render(gr);
		}
 
		// Paddle
		level.getPaddle().render(gr);
 
		// Ball
		for ( Ball ball : level.getBalls()){
			ball.render(gr);
		}
 
		gr.drawString("Lives: " + playerInfo.getLives(), 700, 10);
		gr.drawString("Score: " + playerInfo.getScore(), 500, 10);
 
		if(message != null){
			gr.drawString(message, 300, 300);
		}
	}
 
	@Override
	public void update(GameContainer gc, StateBasedGame sbg, int delta)
			throws SlickException {
 
		switch(currentState){
		case BALL_LAUNCH:{
			if( level.getBalls().size() == 0 ){
				Ball ball = level.addNewBall();
				ball.setSpeed(0.0f);
				ball.setDirection(new Vector2f(0,0));
				// TODO: Catalog this
				message = "Get Ready!";
			}
 
			Ball ball = level.getBalls().get(0);
 
			if(level.getPaddle().getState() == Paddle.PAD_STATE.STICKY){
 
				Vector2f position = level.getPaddle().getPosition().copy();
 
				position.x += 40;
				position.y -= 20;
 
				ball.setPosition( position );
			}else{
				// set the normal state to the paddle
				level.getPaddle().setState(Paddle.PAD_STATE.NORMAL);
				// add movement and direction to the ball
				ball.setSpeed(0.5f);
 
				collisionManager.addCollidable(ball);
 
				Vector2f direction = new Vector2f(0,1);
				Random r = new Random();
 
				direction.add( r.nextInt(45) * (r.nextBoolean()? -1 : 1) );
				ball.setDirection(direction);
				// change the game state
				currentState = LEVEL_STATES.NORMAL_GAME;
 
				// TODO: Catalog this
				message = null;
			}
			break;}
		case NORMAL_GAME:{
			// update all balls
			List<Ball> removals = null;
 
			for(Ball ball : level.getBalls()){
				ball.update(gc, sbg, delta);
 
				if(ball.getPosition().y > gc.getHeight()){
					if(removals == null){
						removals = new ArrayList<Ball>();
					}
					removals.add(ball);
				}
			}
 
			if(removals != null){
				for(Ball ball : removals){
					level.getBalls().remove(ball);
 
					collisionManager.removeCollidable(ball);
				}
			}
 
			// perform collisions
			collisionManager.processCollisions();
 
			// check for bricks left
			if(level.getBricks().size() == 0){
				currentState = LEVEL_STATES.LEVEL_OVER;
				counter = 3000;
			}
 
			// check if ball drops from screen
			if(level.getBalls().size() == 0){
				currentState = LEVEL_STATES.LIFE_LOST;
			}
 
			break;}
		case LIFE_LOST:
			playerInfo.decrementLives();
 
			if(playerInfo.getLives() == 0){
				currentState = LEVEL_STATES.GAME_OVER;
				counter = 3000;
			}else{
				currentState = LEVEL_STATES.BALL_LAUNCH;
				level.getPaddle().setState(PAD_STATE.STICKY);
			}
			break;
		case LEVEL_OVER:
			if(counter == 3000){
				gc.getInput().removeMouseListener(level.getPaddle());
				message = "CONGRATULATIONS!\nLevel completed!";
			}
			counter -= delta;
 
			if(counter < 0){
				// jump to level selection screen
				sbg.enterState(2);
			}
			break;
		case GAME_OVER:
			if(counter == 3000){
				gc.getInput().removeMouseListener(level.getPaddle());
				message = "GAME OVER\nSCORE :" + GameInfo.getCurrentGameInfo().getPlayerInfo().getScore();
			}
			counter -= delta;
 
			if(counter < 0){
				sbg.enterState(0);
			}
			break;
 
		}
	}
 
	public void setLevelFile(String file){
		levelFile = file;
	}
 
}

CollisionManager

/* ************************************************************************************
 * Copyright (c) 2010, FoxholeStudios
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, 
 * are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this list 
 * of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice, this 
 * list of conditions and the following disclaimer in the documentation and/or other 
 * materials provided with the distribution.
 * Neither the name of FoxholeStudios nor the names of its contributors may be used 
 * to endorse or promote products derived from this software without specific prior 
 * written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 
 * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 
 * DAMAGE.
 * ************************************************************************************/
 
package tutorials.slickout.gameplay.collision;
 
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
 
import tutorials.slickout.gameplay.level.ICollidableObject;
 
/**
 * Performs basic collisions between collidable types using slick2d shapes.
 * 
 * @author Tiago "Spiegel" Costa
 *
 */
public class CollisionManager {
 
	// the list of objects per type
	private Map<Integer, List<ICollidableObject>> collidables;
	// the list of collisions per type
	private Map<Integer, List<Integer>> collisionsTypes;
	// the list handlers for collisions
	private Map<String, ICollisionHandler> collisionHandlers;
 
	/**
	 * Creates a new CollisionManager
	 */
	public CollisionManager(){
		collidables 		= new HashMap<Integer, List<ICollidableObject>>() ;
		collisionsTypes 	= new HashMap<Integer, List<Integer>>() ;
		collisionHandlers 	= new HashMap<String, ICollisionHandler>() ;
	}
 
	/**
	 * Adds a new object for the collision bag, from this point on this object will be checked
	 * for collisions.
	 * 
	 * @param collidable the collidable object to add
	 */
	public void addCollidable(ICollidableObject collidable){
		// obtain the entry for this type
		List<ICollidableObject> collidableList = collidables.get(collidable.getCollisionType());
 
		//if there is no entry for this type add one
		if(collidableList == null){
			collidableList = new ArrayList<ICollidableObject>();
			collidables.put(collidable.getCollisionType(), collidableList);
		}
 
		// and an entry to the list
		collidableList.add(collidable);
	}
 
	/**
	 * Removes an object from the collidable bag, from this point on the object 
	 * shall not be used for collisions.
	 * 
	 * If there is no entry for this object nothing is done.
	 * 
	 * @param collidable the collidable object to remove
	 */
	public void removeCollidable(ICollidableObject collidable){
		// obtain the entry for this type
		List<ICollidableObject> collidableList = collidables.get(collidable.getCollisionType());
 
		// if the entry exists remove the object from the list (if possible)
		if(collidableList != null){
			collidableList.remove(collidable);
		}
	}
 
	/**
	 * Adds a new collision handler to the manager. From this point on all the types
	 * concerning this handler (type1 and type2) will perform collisions.
	 * 
	 * @param handler the handler to add
	 */
	public void addHandler(ICollisionHandler handler){
		// generate the key
		String key = getKey(handler.getCollider1Type(), handler.getCollider2Type());
 
		// add the handler to the map
		collisionHandlers.put( key, handler );
 
		// add the collision type1 to type 2
		addTypesToCollision(handler.getCollider1Type(), handler.getCollider2Type());
		// add the collision type2 to type 1
		addTypesToCollision(handler.getCollider2Type(), handler.getCollider1Type());
	}
 
	private void addTypesToCollision(int type1, int type2){
		// obtain collision type entry
		List<Integer> typeCollisions = collisionsTypes.get(type1);
 
		// if there is no entry create one
		if(typeCollisions == null){
			typeCollisions = new ArrayList<Integer>();
			collisionsTypes.put(type1, typeCollisions);
		}
		// add collision to list
		typeCollisions.add(type2);
	}
 
	/**
	 * Generates a key for the two collision types in the form of
	 * <smaller type>-<bigger type>
	 * @param type1 the first type
	 * @param type2 the second type
	 * @return a string with the key in the given format
	 */
	public static String getKey(int type1, int type2){
		// generate key <smaller type>-<bigger type>
		return (type1 < type2) ? type1+"-"+type2 : type2+"-"+type1; 
	}
 
	public void processCollisions(){
 
		// prepare a set of all keys to collide
		Set<String> allCollisionKeys = new HashSet<String>();
 
		// prepare a list of collisions to handle
		List<CollisionData> collisions = new ArrayList<CollisionData>();
 
		Set<Integer> types = collisionsTypes.keySet();
 
		// obtain every type for collision
		for(Integer type : types){
			// obtain for each type the type it collides with
			List<Integer> collidesWithTypes = collisionsTypes.get(type);
 
			for(Integer collidingType : collidesWithTypes){
				// if the pair was already treated ignore it else treat it
				if( !allCollisionKeys.contains(getKey(type, collidingType)) ){
					// obtain all object of type
					List<ICollidableObject> collidableForType = collidables.get(type);
					// obtain all object of collidingtype
					List<ICollidableObject> collidableForCollidingType = collidables.get(collidingType);
 
					// for each pair from type1 -> type2 verify if the collision happens 
					for( ICollidableObject collidable : collidableForType ){
						for( ICollidableObject collidesWith : collidableForCollidingType ){
							if(collidable.isCollidingWith(collidesWith)){
								// if collision happens add the collision handler to the array
								CollisionData cd = new CollisionData();
								cd.handler = collisionHandlers.get(getKey(type, collidingType));
								cd.object1 = collidable;
								cd.object2 = collidesWith;
 
								collisions.add(cd);
							}
						}
					}
 
					// set these both types as processed
					allCollisionKeys.add(getKey(type, collidingType));
				}				
			}
		}
 
		// process all collisions
		for(CollisionData cd : collisions){
			cd.handler.performCollision(cd.object1, cd.object2);
		}
	}
 
	class CollisionData{
		public ICollisionHandler handler;
		public ICollidableObject object1;
		public ICollidableObject object2;
	}
}

ICollisionHandler

/* ************************************************************************************
 * Copyright (c) 2010, FoxholeStudios
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, 
 * are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this list 
 * of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice, this 
 * list of conditions and the following disclaimer in the documentation and/or other 
 * materials provided with the distribution.
 * Neither the name of FoxholeStudios nor the names of its contributors may be used 
 * to endorse or promote products derived from this software without specific prior 
 * written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 
 * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 
 * DAMAGE.
 * ************************************************************************************/
 
package tutorials.slickout.gameplay.collision;
 
import tutorials.slickout.gameplay.level.ICollidableObject;
 
public interface ICollisionHandler {
 
	public int getCollider1Type();
 
	public int getCollider2Type();
 
	public void performCollision(ICollidableObject collidable1, ICollidableObject collidable2);
}

AnimationObject

/* ************************************************************************************
 * Copyright (c) 2010, FoxholeStudios
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, 
 * are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this list 
 * of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice, this 
 * list of conditions and the following disclaimer in the documentation and/or other 
 * materials provided with the distribution.
 * Neither the name of FoxholeStudios nor the names of its contributors may be used 
 * to endorse or promote products derived from this software without specific prior 
 * written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 
 * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 
 * DAMAGE.
 * ************************************************************************************/
 
package tutorials.slickout.gameplay.level;
 
import org.newdawn.slick.Animation;
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.geom.Vector2f;
import org.newdawn.slick.state.StateBasedGame;
 
public class AnimationObject implements ILevelObject {
 
	protected String name;
	protected Animation animation;
	protected Vector2f position;
 
	public AnimationObject(String name, Animation animation, Vector2f position){
		this.name = name;
		this.position = position;
		this.animation = animation;
	}
 
	@Override
	public String getName() {
		return name;
	}
 
	@Override
	public Vector2f getPosition() {
		return position;
	}
 
	@Override
	public void setPosition(Vector2f position) {
		this.position = position;
	}
 
 
	@Override
	public void render(Graphics graphics) {
		animation.draw(position.x, position.y);
	}
 
 
	@Override
	public void update(GameContainer gc, StateBasedGame sbg, int delta) {
		animation.update(delta);
	}
 
}

Ball

/* ************************************************************************************
 * Copyright (c) 2010, FoxholeStudios
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, 
 * are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this list 
 * of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice, this 
 * list of conditions and the following disclaimer in the documentation and/or other 
 * materials provided with the distribution.
 * Neither the name of FoxholeStudios nor the names of its contributors may be used 
 * to endorse or promote products derived from this software without specific prior 
 * written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 
 * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 
 * DAMAGE.
 * ************************************************************************************/
 
package tutorials.slickout.gameplay.level;
 
import org.newdawn.slick.Animation;
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.geom.Vector2f;
import org.newdawn.slick.state.StateBasedGame;
 
public class AnimationObject implements ILevelObject {
 
	protected String name;
	protected Animation animation;
	protected Vector2f position;
 
	public AnimationObject(String name, Animation animation, Vector2f position){
		this.name = name;
		this.position = position;
		this.animation = animation;
	}
 
	@Override
	public String getName() {
		return name;
	}
 
	@Override
	public Vector2f getPosition() {
		return position;
	}
 
	@Override
	public void setPosition(Vector2f position) {
		this.position = position;
	}
 
 
	@Override
	public void render(Graphics graphics) {
		animation.draw(position.x, position.y);
	}
 
 
	@Override
	public void update(GameContainer gc, StateBasedGame sbg, int delta) {
		animation.update(delta);
	}
 
}

Brick

/* ************************************************************************************
 * Copyright (c) 2010, FoxholeStudios
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, 
 * are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this list 
 * of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice, this 
 * list of conditions and the following disclaimer in the documentation and/or other 
 * materials provided with the distribution.
 * Neither the name of FoxholeStudios nor the names of its contributors may be used 
 * to endorse or promote products derived from this software without specific prior 
 * written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 
 * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 
 * DAMAGE.
 * ************************************************************************************/
 
package tutorials.slickout.gameplay.level;
 
import org.newdawn.slick.Animation;
import org.newdawn.slick.Color;
import org.newdawn.slick.geom.Shape;
import org.newdawn.slick.geom.Vector2f;
 
public class Brick extends CollidableAnimationObject {
 
	protected int hitNumber;
	protected Color color;
 
	public Brick(String name, Animation animation, Vector2f position, int hitNumber, Color color,
			Shape collisionShape, int collisionType) {
		super(name, animation, position, collisionShape, collisionType);
 
		this.hitNumber = hitNumber;
	}
 
	public Color getColor(){
		return color;
	}
 
	public void incrementHit(){
		hitNumber++;
	}
 
	public void decreaseHit(){
		hitNumber--;
	}
 
	public int getHitsLeft(){
		return hitNumber;
	}
}

CollidableAnimationObject

/* ************************************************************************************
 * Copyright (c) 2010, FoxholeStudios
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, 
 * are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this list 
 * of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice, this 
 * list of conditions and the following disclaimer in the documentation and/or other 
 * materials provided with the distribution.
 * Neither the name of FoxholeStudios nor the names of its contributors may be used 
 * to endorse or promote products derived from this software without specific prior 
 * written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 
 * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 
 * DAMAGE.
 * ************************************************************************************/
 
package tutorials.slickout.gameplay.level;
 
import org.newdawn.slick.Animation;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.geom.Shape;
import org.newdawn.slick.geom.Transform;
import org.newdawn.slick.geom.Vector2f;
 
public class CollidableAnimationObject extends AnimationObject implements
		ICollidableObject {
 
	private Shape collisionShape;
	private int collisionType;
 
	//private Vector2f deviation;
 
	public CollidableAnimationObject(String name, Animation animation, Vector2f position, Shape collisionShape, int collisionType) {
		super(name, animation, position);
 
		this.collisionShape = collisionShape;
		this.collisionType = collisionType;
	}
 
	@Override
	public void setPosition(Vector2f position) {
		super.setPosition(position);
	}
 
 
	@Override
	public Shape getNormalCollisionShape() {
		return collisionShape;
	}
 
 
	@Override
	public Shape getCollisionShape() {
		return collisionShape.transform( Transform.createTranslateTransform(position.x, position.y));
	}
 
	@Override
	public int getCollisionType() {
		return collisionType;
	}
 
	public Animation getAnimation() {
		return animation;
	}
 
	@Override
	public void render(Graphics graphics) {
		super.render(graphics);
 
		graphics.draw(getCollisionShape());
	}
 
	@Override
	public boolean isCollidingWith(ICollidableObject collidable){
		return this.getCollisionShape().intersects(collidable.getCollisionShape());
	}
 
}

CollidableImageObject

/* ************************************************************************************
 * Copyright (c) 2010, FoxholeStudios
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, 
 * are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this list 
 * of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice, this 
 * list of conditions and the following disclaimer in the documentation and/or other 
 * materials provided with the distribution.
 * Neither the name of FoxholeStudios nor the names of its contributors may be used 
 * to endorse or promote products derived from this software without specific prior 
 * written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 
 * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 
 * DAMAGE.
 * ************************************************************************************/
 
package tutorials.slickout.gameplay.level;
 
import org.newdawn.slick.Graphics;
import org.newdawn.slick.Image;
import org.newdawn.slick.geom.Shape;
import org.newdawn.slick.geom.Transform;
import org.newdawn.slick.geom.Vector2f;
 
 
public class CollidableImageObject extends ImageObject implements ICollidableObject {
 
	protected Shape collisionShape;
	protected int collisionType;
 
	public CollidableImageObject(String name, Image image, Vector2f position, Shape collisionShape, int collisionType) {
		super(name, image, position);
 
		this.collisionShape = collisionShape;
		this.collisionType = collisionType;
	}
 
	@Override
	public void setPosition(Vector2f position){
		super.setPosition(position);
	}
 
	@Override
	public Shape getNormalCollisionShape() {
		return collisionShape;
	}
 
	@Override
	public Shape getCollisionShape() {
		return collisionShape.transform(Transform.createTranslateTransform(position.x, position.y));
	}
 
	@Override
	public int getCollisionType() {
		return collisionType;
	}
 
	@Override
	public void render(Graphics graphics) {
		image.draw(position.x, position.y);
 
		graphics.draw(getCollisionShape());
	}
 
	@Override
	public boolean isCollidingWith(ICollidableObject collidable){
		return this.getCollisionShape().intersects(collidable.getCollisionShape());
	}
}

CollidableImageObject

/* ************************************************************************************
 * Copyright (c) 2010, FoxholeStudios
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, 
 * are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this list 
 * of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice, this 
 * list of conditions and the following disclaimer in the documentation and/or other 
 * materials provided with the distribution.
 * Neither the name of FoxholeStudios nor the names of its contributors may be used 
 * to endorse or promote products derived from this software without specific prior 
 * written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 
 * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 
 * DAMAGE.
 * ************************************************************************************/
 
package tutorials.slickout.gameplay.level;
 
import org.newdawn.slick.Graphics;
import org.newdawn.slick.Image;
import org.newdawn.slick.geom.Shape;
import org.newdawn.slick.geom.Transform;
import org.newdawn.slick.geom.Vector2f;
 
 
public class CollidableImageObject extends ImageObject implements ICollidableObject {
 
	protected Shape collisionShape;
	protected int collisionType;
 
	public CollidableImageObject(String name, Image image, Vector2f position, Shape collisionShape, int collisionType) {
		super(name, image, position);
 
		this.collisionShape = collisionShape;
		this.collisionType = collisionType;
	}
 
	@Override
	public void setPosition(Vector2f position){
		super.setPosition(position);
	}
 
	@Override
	public Shape getNormalCollisionShape() {
		return collisionShape;
	}
 
	@Override
	public Shape getCollisionShape() {
		return collisionShape.transform(Transform.createTranslateTransform(position.x, position.y));
	}
 
	@Override
	public int getCollisionType() {
		return collisionType;
	}
 
	@Override
	public void render(Graphics graphics) {
		image.draw(position.x, position.y);
 
		graphics.draw(getCollisionShape());
	}
 
	@Override
	public boolean isCollidingWith(ICollidableObject collidable){
		return this.getCollisionShape().intersects(collidable.getCollisionShape());
	}
}

ICollidableObject

/* ************************************************************************************
 * Copyright (c) 2010, FoxholeStudios
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, 
 * are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this list 
 * of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice, this 
 * list of conditions and the following disclaimer in the documentation and/or other 
 * materials provided with the distribution.
 * Neither the name of FoxholeStudios nor the names of its contributors may be used 
 * to endorse or promote products derived from this software without specific prior 
 * written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 
 * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 
 * DAMAGE.
 * ************************************************************************************/
 
package tutorials.slickout.gameplay.level;
 
import org.newdawn.slick.geom.Shape;
import org.newdawn.slick.geom.Vector2f;
 
public interface ICollidableObject {
 
	public void setPosition(Vector2f position);
 
	public Vector2f getPosition();
 
	public Shape getNormalCollisionShape();
 
	public Shape getCollisionShape();
 
	public int getCollisionType();
 
	public boolean isCollidingWith(ICollidableObject collidable);
}

ILevel

/* ************************************************************************************
 * Copyright (c) 2010, FoxholeStudios
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, 
 * are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this list 
 * of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice, this 
 * list of conditions and the following disclaimer in the documentation and/or other 
 * materials provided with the distribution.
 * Neither the name of FoxholeStudios nor the names of its contributors may be used 
 * to endorse or promote products derived from this software without specific prior 
 * written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 
 * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 
 * DAMAGE.
 * ************************************************************************************/
 
package tutorials.slickout.gameplay.level;
 
import java.util.List;
 
import org.newdawn.slick.SlickException;
 
public interface ILevel {
 
	public ImageObject getBackground();
 
	public List<Ball> getBalls();
 
	public Paddle getPaddle();
 
	public CollidableImageObject getLeftBumper();
 
	public CollidableImageObject getRightBumper();
 
	public CollidableImageObject getTopBumper();
 
	public List<Brick> getBricks();
 
	public Ball addNewBall();
	/*
	public void addBall(Ball newBall);
 
	public void removeBall(Ball ball);
 
	public int getBallCount();
 
	public int
	*/ 
}

ILevelObject

/* ************************************************************************************
 * Copyright (c) 2010, FoxholeStudios
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, 
 * are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this list 
 * of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice, this 
 * list of conditions and the following disclaimer in the documentation and/or other 
 * materials provided with the distribution.
 * Neither the name of FoxholeStudios nor the names of its contributors may be used 
 * to endorse or promote products derived from this software without specific prior 
 * written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 
 * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 
 * DAMAGE.
 * ************************************************************************************/
 
package tutorials.slickout.gameplay.level;
 
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.geom.Vector2f;
import org.newdawn.slick.state.StateBasedGame;
 
public interface ILevelObject {
 
	public String getName();
 
	public void setPosition(Vector2f position);
 
	public Vector2f getPosition();
 
	public void render( Graphics graphics );
 
	public void update(GameContainer gc, StateBasedGame sbg, int delta);
}

ImageObject

/* ************************************************************************************
 * Copyright (c) 2010, FoxholeStudios
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, 
 * are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this list 
 * of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice, this 
 * list of conditions and the following disclaimer in the documentation and/or other 
 * materials provided with the distribution.
 * Neither the name of FoxholeStudios nor the names of its contributors may be used 
 * to endorse or promote products derived from this software without specific prior 
 * written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 
 * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 
 * DAMAGE.
 * ************************************************************************************/
 
package tutorials.slickout.gameplay.level;
 
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.Image;
import org.newdawn.slick.geom.Vector2f;
import org.newdawn.slick.state.StateBasedGame;
 
public class ImageObject implements ILevelObject {
 
	protected String name;
	protected Image image;
	protected Vector2f position;
 
	public ImageObject(String name, Image image, Vector2f position){
		this.name = name;
		this.image = image;
		this.position = position;
	}
 
	@Override
	public String getName() {
		return name;
	}
 
	@Override
	public Vector2f getPosition() {
		return position;
	}
 
	@Override
	public void setPosition(Vector2f position) {
		this.position = position;
	}
 
	public Image getImage(){
		return image;
	}
 
 
	@Override
	public void render(Graphics graphics) {
		image.draw(position.x, position.y);
	}
 
 
	@Override
	public void update(GameContainer gc, StateBasedGame sbg, int delta) {
	}
 
}

LevelImpl

/* ************************************************************************************
 * Copyright (c) 2010, FoxholeStudios
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, 
 * are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this list 
 * of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice, this 
 * list of conditions and the following disclaimer in the documentation and/or other 
 * materials provided with the distribution.
 * Neither the name of FoxholeStudios nor the names of its contributors may be used 
 * to endorse or promote products derived from this software without specific prior 
 * written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 
 * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 
 * DAMAGE.
 * ************************************************************************************/
 
package tutorials.slickout.gameplay.level;
 
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
 
import org.newdawn.slick.Animation;
import org.newdawn.slick.Color;
import org.newdawn.slick.Image;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.SpriteSheet;
import org.newdawn.slick.geom.Circle;
import org.newdawn.slick.geom.Rectangle;
import org.newdawn.slick.geom.Shape;
import org.newdawn.slick.geom.Vector2f;
 
public class LevelImpl implements ILevel {
 
	protected ImageObject background;
	protected CollidableImageObject rightBumper;
	protected CollidableImageObject leftBumper;
	protected CollidableImageObject topBumper;
 
	protected List<Brick> bricks;
	protected List<Ball> balls;
 
	protected Paddle paddle;
 
	protected String[] ballArgs;
 
 
	public static ILevel loadLevel(InputStream is) throws SlickException{
		LevelImpl level = new LevelImpl();
 
		BufferedReader br = new BufferedReader(new InputStreamReader(is));
 
		// background
		level.setBackground( createImage( readNextValidLine( br ) ) );
 
		// Left Bumper
		level.setLeftBumper( createCollidableImage( readNextValidLine( br ) ) );
 
		// Right Bumper
		level.setRightBumper( createCollidableImage( readNextValidLine( br ) ) );
 
		// top Bumper
		level.setTopBumper( createCollidableImage( readNextValidLine( br ) ) );
 
		// Paddle
		level.setPaddle( createPaddle( readNextValidLine( br ) ) );
 
		// Ball
		level.setBallArgs( readNextValidLine( br ) );
 
		// Bricks
		try {
			List<Brick> bricks = new ArrayList<Brick>();
 
			while(br.ready()){
				bricks.add( createBrick( readNextValidLine( br ) ) );
			}
 
			level.setBricks(bricks);
		} catch (IOException e) {
			throw new SlickException("Could not load Bricks", e);
		}
		return level;
	}
 
	private void setBallArgs(String[] ballArgs) {
		this.ballArgs = ballArgs;
	}
 
	private static CollidableAnimationObject createCollidableAnimation(String[] args) throws SlickException{
		// blue| ANIMATION; /data/brickanimation.png; 100,20,100 | 100, 100 | 1; RECTANGLE; 0,0, 50, 20
		String name = args[0];
 
		String[] imageData = args[1].split(";");
 
		if(!imageData[0].trim().equalsIgnoreCase("ANIMATION")){
			throw new SlickException("Animation tag is invalid");
		}
 
		String[] animationData = imageData[2].split(",");
 
		SpriteSheet ss = new SpriteSheet(new Image(imageData[1]), Integer.parseInt(animationData[0]), Integer.parseInt(animationData[1]));
 
		Animation animation = new Animation(ss, Integer.parseInt(animationData[2]));
 
		String[] coords = args[2].split(",");
		Vector2f position = new Vector2f(Integer.parseInt(coords[0]), Integer.parseInt(coords[1]));
 
		String[] collisionData = args[3].split(";");
 
		int collisionType = Integer.parseInt( collisionData[0] );
 
		Shape shape = null;
 
		if(collisionData[1].trim().equalsIgnoreCase("RECTANGLE")){
			String[] size = collisionData[2].split(",");
			shape = new Rectangle(Integer.parseInt(size[0]), Integer.parseInt(size[1]), Integer.parseInt(size[2]), Integer.parseInt(size[3]));
		}else if(collisionData[1].trim().equalsIgnoreCase("CIRCLE")){
			shape = new Circle(position.x, position.y, Float.parseFloat(collisionData[2])); 	
		}
 
		return new CollidableAnimationObject(name, animation, position, shape, collisionType);
	}
 
	private static Paddle createPaddle(String[] args) throws SlickException {
		CollidableAnimationObject animation = createCollidableAnimation(args);
 
		return new Paddle(animation.getName(), animation.getAnimation(), animation.getPosition(), animation.getNormalCollisionShape(), animation.getCollisionType());
	}
 
	public Ball addNewBall(){
 
		Ball ball = null;
		try {
			ball = createBall( ballArgs);
			balls.add( ball );
		} catch (SlickException e) {
			e.printStackTrace();
		}
 
		return ball;
	}
 
	private static Ball createBall(String[] args) throws SlickException {
 
		CollidableImageObject image = createCollidableImage(args);
 
		return new Ball(image.getName(), image.getImage(), image.getPosition(), Float.parseFloat(args[4]), new Vector2f(0,0), image.getNormalCollisionShape(), image.getCollisionType());
	}
 
	private static String[] readNextValidLine(BufferedReader br) throws SlickException {
		boolean read = false;
 
		String[] args = null;
 
		while(!read){
			String line = null;
			try {
				line = br.readLine();
			} catch (IOException e) {
				throw new SlickException("Could not read level file line", e);
			}
 
			if(!( line.startsWith("#") || line.isEmpty() )){
				read = true;
 
				args = line.split("\\|");
			}
		}
 
		return args;
	}
 
	private static ImageObject createImage( String[] args ) throws SlickException {
		// background| IMAGE; /data/background.jpg | 0, 0
		String name = args[0];
		String[] imageData = args[1].split(";");
 
		if( !imageData[0].trim().equalsIgnoreCase("IMAGE") ){
			throw new SlickException("Invalid image");
		}
 
		String path = imageData[1];
		String[] coords = args[2].split(",");
		Vector2f position = new Vector2f(Integer.parseInt(coords[0].trim()), Integer.parseInt(coords[1].trim()));
 
		return new ImageObject(name, new Image(path), position);
	}
 
	private static CollidableImageObject createCollidableImage( String[] args ) throws SlickException{
		ImageObject image = createImage(args); 
 
		String[] collisionData = args[3].split(";");
 
		int collisionType = Integer.parseInt( collisionData[0] );
 
		Shape shape = null;
 
		if(collisionData[1].trim().equalsIgnoreCase("RECTANGLE")){
			String[] size = collisionData[2].split(",");
			shape = new Rectangle(Integer.parseInt(size[0]), Integer.parseInt(size[1]), Integer.parseInt(size[2]), Integer.parseInt(size[3]));
		}else if(collisionData[1].trim().equalsIgnoreCase("CIRCLE")){
			String[] size = collisionData[2].split(",");
			shape = new Circle(Integer.parseInt(size[0]), Integer.parseInt(size[1]), Integer.parseInt(size[2])); 	
		}
 
		return new CollidableImageObject(image.getName(), 
										 image.getImage(), 
										 image.getPosition(), 
										 shape, 
										 collisionType);
	}
 
	private static Brick createBrick(String[] args) throws SlickException{
 
		CollidableAnimationObject brickAnimation = createCollidableAnimation(args);
 
		String[] brickData = args[4].split(";");
		String[] colorData = brickData[0].split(","); 
		int numberOfHits = Integer.parseInt(brickData[1]);
		Color color = new Color( Integer.parseInt(colorData[0]), Integer.parseInt(colorData[1]), Integer.parseInt(colorData[2]) );
 
		return new Brick(brickAnimation.getName(), 
						 brickAnimation.getAnimation(), 
						 brickAnimation.getPosition(), 
						 numberOfHits, 
						 color, 
						 brickAnimation.getNormalCollisionShape(), 
						 brickAnimation.getCollisionType());
	}
 
 
	private LevelImpl(){
		balls = new ArrayList<Ball>();
	}
 
	@Override
	public final ImageObject getBackground() {
		return background;
	}
 
	@Override
	public final List<Ball> getBalls() {
		return balls;
	}
 
	@Override
	public final List<Brick> getBricks() {
		return bricks;
	}
 
	@Override
	public final CollidableImageObject getLeftBumper() {
		return leftBumper;
	}
 
	@Override
	public final Paddle getPaddle() {
		return paddle;
	}
 
	@Override
	public final CollidableImageObject getRightBumper() {
		return rightBumper;
	}
 
	@Override
	public final CollidableImageObject getTopBumper() {
		return topBumper;
	}
 
 
	public final void setBackground(ImageObject background) {
		this.background = background;
	}
 
	public final void setRightBumper(CollidableImageObject rightBumper) {
		this.rightBumper = rightBumper;
	}
 
	public final void setLeftBumper(CollidableImageObject leftBumper) {
		this.leftBumper = leftBumper;
	}
 
	public final void setTopBumper(CollidableImageObject topBumper) {
		this.topBumper = topBumper;
	}
 
	public final void setBricks(List<Brick> bricks) {
		this.bricks = bricks;
	}
 
	public final void setBalls(List<Ball> balls) {
		this.balls = balls;
	}
 
	public final void setPaddle(Paddle paddle) {
		this.paddle = paddle;
	}
 
}

<java code> /* * Copyright © 2010, FoxholeStudios * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright notice, this list * of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this * list of conditions and the following disclaimer in the documentation and/or other * materials provided with the distribution. * Neither the name of FoxholeStudios nor the names of its contributors may be used * to endorse or promote products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH * DAMAGE. * /

package tutorials.slickout.gameplay.level;

import org.newdawn.slick.Animation; import org.newdawn.slick.Input; import org.newdawn.slick.MouseListener; import org.newdawn.slick.geom.Shape; import org.newdawn.slick.geom.Vector2f;

public class Paddle extends CollidableAnimationObject implements MouseListener {

public static enum PAD_STATE {NORMAL, STICKY};

private PAD_STATE currentState;

public Paddle(String name, Animation animation, Vector2f position,
		Shape collisionShape, int collisionType) {
	super(name, animation, position, collisionShape, collisionType);
	
	currentState = PAD_STATE.STICKY;
}

public PAD_STATE getState(){
	return currentState;
}

public void setState(PAD_STATE newState){
	currentState = newState;
}
@Override
public void mouseClicked(int button, int x, int y, int clickCount) {
}
@Override
public void mouseMoved(int oldx, int oldy, int newx, int newy) {
	if(newx > 10 && newx<690){
		setPosition(new Vector2f( newx, getPosition().y) );
	}
}
@Override
public void mousePressed(int button, int x, int y) {
	if(currentState == PAD_STATE.STICKY){
		currentState = PAD_STATE.NORMAL;
	}
}
@Override
public void mouseReleased(int button, int x, int y) {
}
@Override
public void mouseWheelMoved(int change) {
}
@Override
public void inputEnded() {

}
@Override
public boolean isAcceptingInput() {
	return true;
}
@Override
public void setInput(Input arg0) {	
}

} </code>

BrickBallCollisionHandler

/* ************************************************************************************
 * Copyright (c) 2010, FoxholeStudios
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, 
 * are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this list 
 * of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice, this 
 * list of conditions and the following disclaimer in the documentation and/or other 
 * materials provided with the distribution.
 * Neither the name of FoxholeStudios nor the names of its contributors may be used 
 * to endorse or promote products derived from this software without specific prior 
 * written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 
 * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 
 * DAMAGE.
 * ************************************************************************************/
 
package tutorials.slickout.gameplay.level.collision;
 
import java.util.Random;
 
import org.newdawn.slick.geom.Shape;
import org.newdawn.slick.geom.Vector2f;
 
import tutorials.slickout.GameInfo;
import tutorials.slickout.gameplay.collision.CollisionManager;
import tutorials.slickout.gameplay.collision.ICollisionHandler;
import tutorials.slickout.gameplay.level.Ball;
import tutorials.slickout.gameplay.level.Brick;
import tutorials.slickout.gameplay.level.ICollidableObject;
import tutorials.slickout.gameplay.level.ILevel;
import tutorials.slickout.playerinfo.PlayerInfo;
 
public class BrickBallCollisionHandler implements ICollisionHandler {
 
	private Random r ;
 
	private ILevel levelData;
	private CollisionManager manager;
 
	public BrickBallCollisionHandler(ILevel levelData, CollisionManager manager){
		r = new Random();
 
		this.levelData = levelData;
		this.manager = manager;
	}
 
	@Override
	public int getCollider1Type() {
		return 3;
	}
 
	@Override
	public int getCollider2Type() {
		return 2;
	}
 
	@Override
	public void performCollision(ICollidableObject collidable1,
			ICollidableObject collidable2) {
 
		// check to see if collision is still applicable
		// sometimes the collision may be resolved by other handlers somehow
		if(!collidable1.isCollidingWith(collidable2)){
			return;
		}
 
		Ball ball = null;
		Brick brick = null;
 
		// Cast the correct objects		
		if(collidable1 instanceof Ball){
			ball = (Ball) collidable1;
			brick = (Brick) collidable2;
		}else{
			ball = (Ball) collidable2;
			brick = (Brick) collidable1;
		}
 
		// obtain a copy of the direction
		Vector2f direction = ball.getDirection().copy(); 
		// reverse it
		direction.set(direction.x*-1, direction.y*-1);
 
		// backtrack the position of the ball until it no longer collides with
		// the brick
		do {
			Vector2f pos = ball.getPosition();
			ball.setPosition(new Vector2f( pos.x + direction.x, pos.y - direction.y) );
		}while(ball.isCollidingWith(brick));
 
		// obtain the shapes of the objects
		Shape ballShape 	= ball.getCollisionShape();
		Shape objectShape   = brick.getCollisionShape();
 
		// obtain a fresh direction of the ball
		direction = ball.getDirection().copy(); 
 
		// define the new direction
		if(ballShape.getMinY() > objectShape.getMaxY() || ballShape.getMaxY() < objectShape.getMinY()){
			direction.set(direction.x, -direction.y);
		}else direction.set(-direction.x, direction.y);
 
		// add -10º to 10º degrees random to each bump in the bricks
		direction.add( r.nextInt(10) * (r.nextBoolean()? -1 : 1) );
 
		// set the new ball direction
		ball.setDirection(direction);
 
		// since the brick was hit, decrease the number of hits to be detroyed
		brick.decreaseHit();
 
		GameInfo.getCurrentGameInfo().getPlayerInfo().addScore(100);
 
		// if the brick has reached the last hit
		if(brick.getHitsLeft() == 0){
			// remove the brick from the level list
			levelData.getBricks().remove(brick);
			// remove it from the collision manager
			manager.removeCollidable(brick);
 
			GameInfo.getCurrentGameInfo().getPlayerInfo().addScore(250);
		}
 
	}
 
}

BumperAndPadBallCollisionHandler

/* ************************************************************************************
 * Copyright (c) 2010, FoxholeStudios
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, 
 * are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this list 
 * of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice, this 
 * list of conditions and the following disclaimer in the documentation and/or other 
 * materials provided with the distribution.
 * Neither the name of FoxholeStudios nor the names of its contributors may be used 
 * to endorse or promote products derived from this software without specific prior 
 * written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 
 * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 
 * DAMAGE.
 * ************************************************************************************/
 
package tutorials.slickout.gameplay.level.collision;
 
import java.util.Random;
 
import org.newdawn.slick.geom.Shape;
import org.newdawn.slick.geom.Vector2f;
 
import tutorials.slickout.gameplay.collision.ICollisionHandler;
import tutorials.slickout.gameplay.level.Ball;
import tutorials.slickout.gameplay.level.ICollidableObject;
 
public class BumperAndPadBallCollisionHandler implements ICollisionHandler {
 
	private Random r ;
 
	public BumperAndPadBallCollisionHandler(){
		r = new Random();
	}
 
	@Override
	public int getCollider1Type() {
		return 1;
	}
 
	@Override
	public int getCollider2Type() {
		return 2;
	}
 
	@Override
	public void performCollision(ICollidableObject collidable1,
			ICollidableObject collidable2) {
 
		// check to see if collision is still applicable
		// sometimes the collision may be resolved by other handlers somehow
		if(!collidable1.isCollidingWith(collidable2)){
			return;
		}
 
		Ball ball = null;
		ICollidableObject object = null;
 
		// Cast the correct objects		
		if(collidable1 instanceof Ball){
			ball = (Ball) collidable1;
			object = collidable2;
		}else{
			ball = (Ball) collidable2;
			object = collidable1;
		}
 
		// obtain a copy of the direction
		Vector2f direction = ball.getDirection().copy(); 
		// reverse it
		direction.set(direction.x*-1, direction.y*-1);
 
		// backtrack the position of the ball until it no longer collides with
		// the paddle/brick
		do {
			Vector2f pos = ball.getPosition();
			ball.setPosition(new Vector2f( pos.x + direction.x, pos.y - direction.y) );
		}while(ball.isCollidingWith(object));
 
		// obtain the shapes of the objects
		Shape ballShape 	= ball.getCollisionShape();
		Shape objectShape   = object.getCollisionShape();
 
		// obtain a fresh direction of the ball
		direction = ball.getDirection().copy(); 
 
		// define the new direction
		if(ballShape.getMinY() > objectShape.getMaxY() || ballShape.getMaxY() < objectShape.getMinY()){
			direction.set(direction.x, -direction.y);
		}else direction.set(-direction.x, direction.y);
 
		// set the new ball direction
		ball.setDirection(direction);
	}
 
}

LevelSelector

/* ************************************************************************************
 * Copyright (c) 2010, FoxholeStudios
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, 
 * are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this list 
 * of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice, this 
 * list of conditions and the following disclaimer in the documentation and/or other 
 * materials provided with the distribution.
 * Neither the name of FoxholeStudios nor the names of its contributors may be used 
 * to endorse or promote products derived from this software without specific prior 
 * written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 
 * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 
 * DAMAGE.
 * ************************************************************************************/
 
package tutorials.slickout.levelselector;
 
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.Image;
import org.newdawn.slick.MouseListener;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.state.BasicGameState;
import org.newdawn.slick.state.StateBasedGame;
 
import tutorials.slickout.GameInfo;
import tutorials.slickout.gameplay.GameplayState;
 
public class LevelSelector extends BasicGameState implements MouseListener {
 
	private Image background;
	private Image ended;
	private int option;
	private int optionSelected;
	private int counter;
 
	@Override
	public int getID() {
		return 2;
	}
 
	public void reset(){
 
	}
 
	@Override
	public void init(GameContainer container, StateBasedGame game)
			throws SlickException {
 
		background = new Image("data/levelselection.jpg");
		ended = new Image("data/levelend.png");
 
		counter = 5000;
	}
 
	@Override
	public void enter(GameContainer container, StateBasedGame game)
			throws SlickException {
 
		optionSelected = -1;
	}
 
	@Override
	public void render(GameContainer container, StateBasedGame game, Graphics g)
			throws SlickException {
 
		background.draw();
 
		for(int i = 0; i < 6; i++){
			if ( GameInfo.getCurrentGameInfo().isLevelCompleted(i+1)){
				int x = 50 + (250 * ((i < 3) ? i : i-3));
				int y = 150+(225* ((i < 3 ) ? 0 : 1));
				ended.draw(x, y);
			}
		}
 
		if(GameInfo.getCurrentGameInfo().allLevelsCompleted()){
			g.drawString("CONGRATULATIONS... YOU'VE FINISHED THIS TUTORIAL", 200, 300);
		}
	}
 
	@Override
	public void update(GameContainer container, StateBasedGame game, int delta)
			throws SlickException {
 
		if(!GameInfo.getCurrentGameInfo().allLevelsCompleted()){
			if(optionSelected != -1 && !GameInfo.getCurrentGameInfo().isLevelCompleted(optionSelected)){
				String levelfile = "data/level"+optionSelected+".lvl";
				// obtain the game state
				GameplayState gameplay = (GameplayState) game.getState(1);
 
				gameplay.setLevelFile(levelfile);
 
				GameInfo.getCurrentGameInfo().setCompletedLevel(optionSelected);
 
				game.enterState(1);
			}
		}else{
			counter -= delta;
 
			if(counter < 0){
				game.enterState(0);
			}
		}
 
 
	}
 
 
	@Override
	public void mouseClicked(int button, int x, int y, int clickCount){
		optionSelected = option;
	}
 
	@Override
	public void mouseMoved(int oldx, int oldy, int newx, int newy){
		int line = 0;
		if( newy > 150 && newy < 325 ){
			line = 0;
		}else if( newy > 375  && newy < 550 ){
			line = 1;
		}
 
		int row = -1;
 
		for(int i = 0; i < 3; i++){
			int minx = 50 + (250 * i);
			int maxx = minx+200;
 
			if(newx > minx && newx< maxx){
				row = i+1;
			}
		}
 
		option = (line*3)+row;
	}
 
}

MainMenuGameState

/* ************************************************************************************
 * Copyright (c) 2010, FoxholeStudios
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, 
 * are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this list 
 * of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice, this 
 * list of conditions and the following disclaimer in the documentation and/or other 
 * materials provided with the distribution.
 * Neither the name of FoxholeStudios nor the names of its contributors may be used 
 * to endorse or promote products derived from this software without specific prior 
 * written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 
 * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 
 * DAMAGE.
 * ************************************************************************************/
 
package tutorials.slickout.mainmenu;
 
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.Image;
import org.newdawn.slick.MouseListener;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.state.BasicGameState;
import org.newdawn.slick.state.StateBasedGame;
 
import tutorials.slickout.GameInfo;
import tutorials.slickout.playerinfo.PlayerInfo;
 
public class MainMenuGameState extends BasicGameState implements MouseListener{
 
	private Image background;
	private Image selector;
	private int selection; 
	private int optionSelected;
	private int topScore;
 
	@Override
	public int getID() {
		return 0;
	}
 
	@Override
	public void enter(GameContainer container, StateBasedGame game)
			throws SlickException {
		selection = -1;
		optionSelected = selection;
 
		if ( GameInfo.getCurrentGameInfo() != null){
			topScore = ( topScore > GameInfo.getCurrentGameInfo().getPlayerInfo().getScore() ) ? topScore : GameInfo.getCurrentGameInfo().getPlayerInfo().getScore(); 
		}
	}
 
	@Override
	public void init(GameContainer container, StateBasedGame game)
			throws SlickException {
		background = new Image("data/mainmenu.jpg");
		selector = new Image("data/selector.png");
	}
 
	@Override
	public void render(GameContainer container, StateBasedGame game, Graphics g)
			throws SlickException {
		background.draw();
 
		if(selection == 1){
			selector.draw(158, 310);
			selector.draw(694, 310);
 
			GameInfo.createNewGameInfo();
		}else if(selection == 2){
			selector.draw(158, 474);
			selector.draw(694, 474);
		}
 
		g.drawString("TOPSCORE : " + topScore, 10, 10) ;
	}
 
	@Override
	public void update(GameContainer container, StateBasedGame game, int delta)
			throws SlickException {
		if(optionSelected == 1){
			// TODO log
			game.enterState(2);
		}else if(optionSelected == 2){
			System.exit(0);
		}
	}
 
	public void mouseMoved(int oldx, int oldy, int newX, int newY){
 
		if(newX > 228 && newX < 702){
			if ( newY > 308 && newY < 389){
				selection = 1;
			}else if ( newY > 475 && newY < 544){
				selection = 2;
			}else {
				selection = -1;
			}
		}
	}
 
	public void mouseClicked(int button, int x, int y, int clickCount){
		optionSelected = selection;
	}
}

PlayerInfo

/* ************************************************************************************
 * Copyright (c) 2010, FoxholeStudios
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, 
 * are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this list 
 * of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice, this 
 * list of conditions and the following disclaimer in the documentation and/or other 
 * materials provided with the distribution.
 * Neither the name of FoxholeStudios nor the names of its contributors may be used 
 * to endorse or promote products derived from this software without specific prior 
 * written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 
 * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 
 * DAMAGE.
 * ************************************************************************************/
 
package tutorials.slickout.playerinfo;
 
public class PlayerInfo {
 
	/*
	private static PlayerInfo _instance = null;
 
	public static PlayerInfo getCurrentPlayerInfo(){
		return _instance;
	}
 
	public static PlayerInfo createNewCurrentPlayerInfo(){
		_instance = new PlayerInfo();
 
		return getCurrentPlayerInfo();
	}*/
 
	private String name;
	private int lives;
	private int score;
 
 
	public PlayerInfo(){
		name ="AAA";
		lives = 3;
		score = 0;
	}
 
 
	public final String getName() {
		return name;
	}
 
 
	public final int getLives() {
		return lives;
	}
 
 
	public final int getScore() {
		return score;
	}
 
 
	public final void setName(String name) {
		this.name = name;
	}
 
 
	public final void incrementLives() {
		this.lives++;
	}
 
	public final void decrementLives() {
		this.lives--;
	}
 
	public final void addScore(int score) {
		this.score += score;
	}
 
	public final void decreaseScore(int score) {
		this.score -= score;
	}
}
 
03_-_slickout.txt · Last modified: 2010/08/27 11:56 by spiegel
chimeric.de = chi`s home Creative Commons License Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0