Hello and welcome to the second part of Slick2D Path to glory, the road that will lead you from total slick2D newcomer into a veteran coding warrior.
In this tutorial we will make a falling brick game cloned after tetris, also a cute little penguin called Tux will appear in this game.
Here's some screenshots.
Webstart «comming soon»
Note: These tutorials are meant for instructional purposes and may no longer be in a 100% working state. However, the main theory behind programming in Slick is covered. The code in these tutorials are best read as pseudo-code.
Everybody knows tetris, a simple game yet so enthralling that most people can play it for several hours in a row. Its just one of those games that beg to be played, over and over again. Its also a simple game to make, that can teach us a lot. Its a story of pieces falling inside a pit that only empties itself whenever lines are full of pieces.
As I covered the in the last Tutorial, a game is all about the game loop. Remember?
Well for tetris, the game loop goes something like this.
So what are the definitions of the game? The pit will be 20 squares in depth by 10 in width. The pieces are called Tetraminos and, as the name implies are made of 4 squares (tetra means four hence the name TETRis). The pieces are as follow:
The O, J, I, L, S, Z and T pieces (from top right to bottom left) as they are know in the lawforbidden world of Tetramaniacs.
The piece will be inserted at the top center of the pit and slowly go down the pit until there it collides with another block or to the bottom of the pit.
Time for your first exercise. Try and create your own solution of piece construction. Create a solution for creating pieces and allowing them to drop on the pit. Now test your solution against this:
1. Does your definition of a piece allows for part of the piece to be destroyed when completing a line? How do you define which of the four blocks are destroyed and a piece is still a piece after one of its blocks is destroyed?
After this lets get back to making our game.
What are game states? This is the wiki definition : state based games. Gamestates are one of those patterns that are already known by anyone that makes a game. When you insert into your game a introduction video, main menu, options menu, gameplay, game over screen and ending game video, your game class will be so big that it will probably be very difficult to manage the code. So what do we do? Instead of having a big game class with every code possible, we use a sort of mini-games that implement every one of these sections of the game. Lets analyze for a moment how these sections are constructed in order to let the game flow.
As you can see there is a “flow” between all of these sections. This is something called a state-machine, a flow made of states that execute a job. This job is our game, and each state represent a part of our game. These states can be implemented through the GameState Interface.
Our game will have two states, the MainMenuState and the GamePlayState, the first will implement the stuff needed for rendering the main menu and the gameplayState will implement the falling block gameplay related stuff.
Lets make the skeleton for our game
MainMenuState:
package slick.path2glory.blockgame; 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 MainMenuState extends BasicGameState { int stateID = -1; MainMenuState( int stateID ) { this.stateID = stateID; } @Override public int getID() { return 0; } public void init(GameContainer gc, StateBasedGame sbg) throws SlickException { } public void render(GameContainer gc, StateBasedGame sbg, Graphics gc) throws SlickException { } public void update(GameContainer gc, StateBasedGame sbg, int delta) throws SlickException { } }
GameplayState
package slick.path2glory.blockgame; 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 GameplayState extends BasicGameState { int stateID = -1; MainMenuState( int stateID ) { this.stateID = stateID; } @Override public int getID() { return 0; } public void init(GameContainer gc, StateBasedGame sbg) throws SlickException { } public void render(GameContainer gc, StateBasedGame sbg, Graphics gc) throws SlickException { } public void update(GameContainer gc, StateBasedGame sbg, int delta) throws SlickException { } }
These two classes implement each of our states, as you can see they are just like BasicGame. The reason behind this is just that the gamecontainer instead of having just one BasicGame to render, has a list of them and we can control which one the gameContainer is rendering to screen.
And this is our main class:
package slick.path2glory.blockgame; import org.newdawn.slick.AppGameContainer; import org.newdawn.slick.GameContainer; import org.newdawn.slick.SlickException; import org.newdawn.slick.state.StateBasedGame; /** * * @author Spiegel * */ public class SlickBlocksGame extends StateBasedGame { public static final int MAINMENUSTATE = 0; public static final int GAMEPLAYSTATE = 1; public SlickBlocksGame() { super("SlickBlocks"); this.addState(new MainMenuState(MAINMENUSTATE)); this.addState(new GameplayState(GAMEPLAYSTATE)); this.enterState(MAINMENUSTATE); } public static void main(String[] args) throws SlickException { AppGameContainer app = new AppGameContainer(new SlickBlocksGame()); app.setDisplayMode(800, 600, false); app.start(); } @Override public void initStatesList(GameContainer gameContainer) throws SlickException { this.getState(MAINMENUSTATE).init(gameContainer, this); this.getState(GAMEPLAYSTATE).init(gameContainer, this); } }
As you can see I defined every game state Id. GameContainer identifies game states by the ID returned by the getID method in GameState. Since I didnt want to hard code it, I created the states ID in the main class and pass the correct ID to the gameState as it is being constructed.
CODE REVIEW
this.addState(new MainMenuState(MAINMENUSTATE)); this.addState(new GameplayState(GAMEPLAYSTATE)); this.enterState(MAINMENUSTATE);
In the constructor we create the states and add them to the gamecontainer statelist.
Also we define that the first state to start rendering is the MAINMENUSTATE.
public void initStatesList(GameContainer gameContainer) throws SlickException { this.getState(MAINMENUSTATE).init(gameContainer, this); this.getState(GAMEPLAYSTATE).init(gameContainer, this); }
Also in this method we can define what states are to be initialized or not.
Lets create our first state, the main menu. The main menu will be a simple menu, without any sort of complex UI. We are going to use two Images as buttons, adding a cool scale effect when the mouse is over the buttons. Also we will show the highscores on this page.
Lets add the images to the main menu, last time we added a image to the background and an image of a plane, this time we will do something different. Because a single entity may be created using several images, loading all of these to independent Images objects may slow down the initialization of the game, so Slick2D enables us to load a single Image that can be fractionated to several others.
These will be the images we will use:
Yes its tux, Its the famous images of him with a fly swatter, I changed it to hold a cube mad with the textures of the pieces we will be using and made the title on GIMP.
Lets put them in action.
Lets add these variables.
Image background = null; Image startGameOption = null; Image exitOption = null; float startGameScale = 1; float exitScale = 1;
And to init method
background = new Image("data/menu.jpg"); // load the menu images Image menuOptions = new Image("data/menuoptions.png"); startGameOption = menuOptions.getSubImage(0, 0, 377, 71); exitOption = menuOptions.getSubImage(0, 71, 377, 71);
As usual my data directory may be altered to reflect where your images are in your disc. This is all business as usual except for the last two lines. Loading a texture sheet into one image we can use it afterwards to generate several images from that sheet. This way we only load one big image to memory and then use it to spawn several others, also because the image is not replicated when spawning new images there is no significant extra burden in the memory used. Using the method getSubImage we can use part of an loaded image to generate a new one.
So now we have loaded all images, lets render them.
To render we add:
// render the background background.draw(0, 0); // Draw menu startGameOption.draw(menuX, menuY, startGameScale); exitOption.draw(menuX, menuY+80, exitScale);
This part is simple, whenever we click on the start game Image the game should jump the gameplay state, when clicking exit the game should end. Also when the mouse is within the Image location, the image will scale itself to show the option is highlighted.
This will be the step of our scale.
float scaleStep = 0.0001f;
And on the update method we add the following:
Input input = gc.getInput(); int mouseX = input.getMouseX(); int mouseY = input.getMouseY(); boolean insideStartGame = false; boolean insideExit = false; if( ( mouseX >= menuX && mouseX <= menuX + startGameOption.getWidth()) && ( mouseY >= menuY && mouseY <= menuY + startGameOption.getHeight()){ insideStartGame = true; }else if( ( mouseX >= menuX && mouseX <= menuX+ exitOption.getWidth()) && ( mouseY >= menuY+80 && mouseY <= menuY+80 + exitOption.getHeight()) ){ insideExit = true; } if(insideStartGame){ if(startGameScale < 1.05f) startGameScale += scaleStep * delta; if ( input.isMouseButtonDown(Input.MOUSE_LEFT_BUTTON) ){ sb.enterState(SlickBlocksGame.GAMEPLAYSTATE); } }else{ if(startGameScale > 1.0f) startGameScale -= scaleStep * delta; if ( input.isMouseButtonDown(Input.MOUSE_LEFT_BUTTON) ) gc.exit(); } if(insideExit) { if(exitScale < 1.05f) exitScale += scaleStep * delta; }else{ if(exitScale > 1.0f) exitScale -= scaleStep * delta; }
Its not the sharpest code but it gets the job done with the simple tools we have now. Also lots of floating numbers there, but its just fluff to show that images can be used for some cool effects.
Lets add a simple sound to the menu. Get some WAV, any wav or OGG any of those will do and put it on your data directory.
add the variable
Sound fx = null;
Not in the init method add:
fx = new Sound("data/pulse.wav");
an finally on the update method add:
if(insideStartGame) { ... if ( input.isMouseButtonDown(Input.MOUSE_LEFT_BUTTON) ){ // Add this line here fx.play(); sb.enterState(SlickBlocksGame.GAMEPLAYSTATE); }...
Now everytime we click on the Start Game we will hear a sound playing. As you can see loading is as simple as loading a wav/ogg file and then just using the play method to make the sound be heard. We will talk more about sounds in the next tutorial, for now we will leave it as this.
Finally we end the Menu state by adding some highscores. Every game needs a challenge, and as far as falling brick clones go… highscores is the ultimate challenge, to have the biggest score in the table is the way to go, so lets add them to our game.
Our highscore class is a simple one. A singleton class that can be used by both states safely. Its a simple ArrayList of Integers because im to lazy to do it with a simple array, and because it gives me an excuse to insert another exercise here.
The following class data is not persistent, that means that once we exit the game our highscores are lost. Change the highscores in order to make them save and load at the correct times. Think about the games you played and all cases that could happen and how the data should be saved to prevent losses of information.
Since there is no Slick stuff here I'll just show the code and let you go thought it.
package slick.path2glory.blockgame; import java.util.ArrayList; public class Highscores { private static Highscores instance = null; ArrayList<Integer> scores = null; private Highscores(int size) { scores = new ArrayList<Integer>(); for(int i = 0; i < size; i++) scores.add(new Integer(0)); } public static Highscores getInstance() { if(instance == null) instance = new Highscores(10); return instance; } public boolean addScore(int score) { for(int idx = 0; idx < scores.size(); idx++) { if(score > scores.get(idx)) { scores.add(idx, new Integer(score)); scores.remove(scores.size()-1); return true; } } return false; } public ArrayList<Integer> getScores() { return scores; } }
And that's it you should now have a fully functional menu with a cool effect on the menu items.
Check the bottom of the page for the full source code of all classes.
Ah yes, the gameplay, sure making menus is rather nice but lets face… it its not a game. Now we are going to make the meat of our game.
Lets start by adding a background image and the rest of the images for our game, this is the same as the last section so I wont explain more than its needed.
and
By the way you cant see the transpareny correctly in the wiki, just download it and use it against a black background.
Add these class variables
Image gameHUD = null; Image transparentGameHUD = null;
And on init add
Image allImages = new Image("data/gameplaytextures.png"); gameHUD = new Image("data/hudblockgame.jpg"); transparentGameHUD = allImages.getSubImage(29, 0, 287, 573); ArrayList<Image> blockImages = new ArrayList<Image>(); for(int i = 0; i < 4; i++) blockImages.add(allImages.getSubImage(0, i*28, 28, 28));
One simple note, as we did in the last sections, we loaded a single image and then separated them into several in order to use them separately. First we load the game HUD in order to have something in which the game will be played, and the we load an image with a sort of transparency for the HUD and after the blocks are inserted into and ArrayList. Dont worry about the scope of the arraylist, we will be using that real soon.
Finnaly add this to render
public void render(GameContainer gc, StateBasedGame sb, Graphics g) throws SlickException { // Paint HUD gameHUD.draw(0,0); transparentGameHUD.draw(48, 15);
If you run the game you will see the pit and a sort of glass effect over the pit area, our blocks will go in between those layers. This is a good time to write up about layers in slick2D, there are none. That's right you don't have support for those (at least not at the time of writing). As you draw stuff in the render method the way slick renders them is in a FIFO way. The first thing you tell him to render, will be the one that slick renders, he is rather obedient. So you'll have to manage the order in which you choose to render the images on screen. Lets say that we draw the transparencyHUD before the gameHud itself, you would not see anything because after slick ended painting the first image, it would pain another right on top of it. So be rather careful when issuing draw commands, you need to know what is going to be rendered before and after the command you are issuing.
Our game needs pieces, lots of them, so we need a way to create them rather quick, and in a random order. We need a sort of factory of pieces. But fist lets define what a piece is? A piece is a set of four blocks, order in some way, that can rotate itself, maintaning the struture of the 4 initial blocks. This may seem a bit stupid, but a piece is not a “real” piece, its just a collection of block that exhibit a piece-like behaviour until it lands on the pit bottom. The piece its nothing more than a set of logic rules that are applied to four blocks.
Warning This is not the best or worst or only way to make the falling brick clone logic, its just one of them.
How do we define the rotations of the piece? I used a simple way to describe each piece, for every piece there is a central block, all the other blocks locations are described through the central block location.
Lets say for the T piece,
that the central block is the blue block. We will define an array of positions {blue, yellow, green, red}.
So the blue block is at pos (0,0), the green at pos (-1,0), and yellow at pos (1,0) and red at pos (0,-1). Now for all 4 rotations there is a line that describes the locations of each block exactly this way, making a 4×4 matrix of locations (tuples).
The piece class is simple, it receives a rotation Matrix 4×4 in size each element containing a Tuple class, rotating the piece right or left is just a matter of choosing the correct line. The method getPosOfBlock return the position relative to the central block of the choosen block.
The beauty of this design is that a single piece class can describe all the different pieces by changing the rotation matrix.
package slick.path2glory.blockgame; public class Piece { private String name; Tuple [][] rotationMatrice; int currentRotation; public Piece(String name, Tuple [][] rotationMatrice) { this.name = name; currentRotation = 0; this.rotationMatrice = rotationMatrice; } public Tuple getPosOfBlock( int blockNumber ) { return rotationMatrice[currentRotation][blockNumber]; } public void rotateRight() { currentRotation = ++currentRotation % rotationMatrice.length; } public void rotateLeft() { currentRotation = --currentRotation % rotationMatrice.length; } public static class Tuple{ public int x; public int y; public Tuple(int x, int y) { this.x = x; this.y = y; } } }
Now we have a piece container, we need a simple way of generating pieces, just like a factory of pieces.
package slick.path2glory.blockgame; import slick.path2glory.blockgame.Piece.Tuple; public class PieceFactory { private int counter = 0; /** Creates a new instance of PieceFactory */ public PieceFactory( ) { } public Piece generateRandomPiece() { java.util.Random random = new java.util.Random(); switch(random.nextInt(7)) { case 0: return generatePiece('t'); case 1: return generatePiece('s'); case 2: return generatePiece('z'); case 3: return generatePiece('o'); case 4: return generatePiece('i'); case 5: return generatePiece('l'); case 6: return generatePiece('j'); } return null; } // Rotation Matrices static Tuple [][] TRotationMatrice = { {new Tuple(0,0), new Tuple(-1,0), new Tuple(1,0), new Tuple(0,-1)}, {new Tuple(0,0), new Tuple(0,1), new Tuple(0,-1), new Tuple(-1,0)}, {new Tuple(0,0), new Tuple(1,0), new Tuple(-1,0), new Tuple(0,1)}, {new Tuple(0,0), new Tuple(0,-1), new Tuple(0,1), new Tuple(1,0)} }; static Tuple [][] SRotationMatrice = { {new Tuple(0,0), new Tuple(1,0), new Tuple(0,-1), new Tuple(-1,-1)}, {new Tuple(0,0), new Tuple(0,1), new Tuple(1,0), new Tuple(1,-1)}, {new Tuple(0,0), new Tuple(1,0), new Tuple(0,-1), new Tuple(-1,-1)}, {new Tuple(0,0), new Tuple(0,1), new Tuple(1,0), new Tuple(1,-1)} }; static Tuple [][] ZRotationMatrice = { {new Tuple(0,0), new Tuple(-1,0), new Tuple(0,-1), new Tuple(1,-1)}, {new Tuple(0,0), new Tuple(0,1), new Tuple(-1,0), new Tuple(-1,-1)}, {new Tuple(0,0), new Tuple(-1,0), new Tuple(0,-1), new Tuple(1,-1)}, {new Tuple(0,0), new Tuple(0,1), new Tuple(-1,0), new Tuple(-1,-1)} }; static Tuple [][] ORotationMatrice = { {new Tuple(0,0), new Tuple(1,0), new Tuple(0,-1), new Tuple(1,-1)}, {new Tuple(0,0), new Tuple(1,0), new Tuple(0,-1), new Tuple(1,-1)}, {new Tuple(0,0), new Tuple(1,0), new Tuple(0,-1), new Tuple(1,-1)}, {new Tuple(0,0), new Tuple(1,0), new Tuple(0,-1), new Tuple(1,-1)} }; static Tuple [][] IRotationMatrice = { {new Tuple(0,0), new Tuple(-1,0), new Tuple(1,0), new Tuple(2,0)}, {new Tuple(0,0), new Tuple(0,-1), new Tuple(0,1), new Tuple(0,2)}, {new Tuple(0,0), new Tuple(-1,0), new Tuple(1,0), new Tuple(2,0)}, {new Tuple(0,0), new Tuple(0,-1), new Tuple(0,1), new Tuple(0,2)} }; static Tuple [][] LRotationMatrice = { {new Tuple(0,0), new Tuple(1,0), new Tuple(-1,0), new Tuple(-1,-1)}, {new Tuple(0,0), new Tuple(0,-1), new Tuple(0,1), new Tuple(-1,1)}, {new Tuple(0,0), new Tuple(-1,0), new Tuple(1,0), new Tuple(1,1)}, {new Tuple(0,0), new Tuple(0,1), new Tuple(0,-1), new Tuple(1,-1)} }; static Tuple [][] JRotationMatrice = { {new Tuple(0,0), new Tuple(-1,0), new Tuple(1,0), new Tuple(1,-1)}, {new Tuple(0,0), new Tuple(0,1), new Tuple(0,-1), new Tuple(-1,-1)}, {new Tuple(0,0), new Tuple(1,0), new Tuple(-1,0), new Tuple(-1,1)}, {new Tuple(0,0), new Tuple(0,-1), new Tuple(0,1), new Tuple(1,1)} }; public Piece generatePiece(char pieceType) { String name = pieceType + "Shape" + counter++; switch(pieceType) { case 't': { return new Piece(name, TRotationMatrice); } case 's': { return new Piece(name, SRotationMatrice); } case 'z': { return new Piece(name, ZRotationMatrice); } case 'o': { return new Piece(name, ORotationMatrice); } case 'i': { return new Piece(name, IRotationMatrice); } case 'l': { return new Piece(name, LRotationMatrice); } case 'j': { return new Piece(name, JRotationMatrice); } } return null; } }
This class is pretty straightforward, the method generateRandomPiece, creates a random piece of any of the eight different types.
As you can see the rotation matrices are defined inside this class and when a piece is created, the correct piece rotations is inserted.
Now we have a simple way of making any kind of pieces, and rotating them.
We need to put it in the GameStateClass
public class GameplayState extends BasicGameState{ ... PieceFactory pieceFactory = null; ... @Override public void init(GameContainer gc, StateBasedGame sb) throws SlickException { ... pieceFactory = new PieceFactory(); ... } }
Onto the pit! This is where all the action is, the piece fall here and lines get filled and destroyed.
A pit is nothing more than an array 10×20 that contains ints. Why ints? Because its a easy way of differentiating the contents of the pit, works on a text based game and even in slick2D.
The pit class doesn't have much to tell, the blocks that already landed on the pit are described by integer 0,1,2 or 3 and empty spots are -1. This was my convention, yours may differ. The piece is tested against the pit, this means that we test the current position and rotation of the piece, if the piece can be inserted the game continues, if not the piece stops and a new piece is inserted.
/* * To change this template, choose Tools | Templates * and open the template in the editor. */ package slick.path2glory.blockgame; import java.util.ArrayList; import org.newdawn.slick.geom.Vector2f; import slick.path2glory.blockgame.Piece.Tuple; public class Pit { int height; int width; ArrayList<int[]> pit = null; public Pit(int width, int height) { this.width = width; this.height = height; pit = new ArrayList<int[]>(); makeCleanPit(); } public void makeCleanPit() { pit.clear(); for(int y = 0; y < height; y++) { addNewLine(); } } public boolean insertPieceAt(Piece piece, int x, int y) { Tuple insertPos = new Tuple(x,y); if(isPieceInsertableIn(piece, insertPos )) { for(int i = 0; i < 4; i++) setBlockAt( i, new Tuple(piece.getPosOfBlock(i).x + insertPos.x, piece.getPosOfBlock(i).y + insertPos.y ) ); return true; } return false; } public void setBlockAt(int value, Piece.Tuple pos) { setBlockAt(value, (int)pos.x, (int)pos.y); } public final void setBlockAt(int value, int x, int y) { pit.get(y)[x] = value; } public int getBlockAt(Vector2f pos) { return getBlockAt((int)pos.x, (int)pos.y); } public int getBlockAt(int x, int y) { return pit.get(y)[x]; } public boolean isPieceInsertableIn( Piece piece, Tuple pos ) { //return ( pos.x >= 0 && pos.x < size.x) && (pos.y >= 0 && pos.y < size.y); boolean isInsertable = true; for(int idx = 0; idx < 4 && isInsertable; idx++) { Tuple blockPos = piece.getPosOfBlock( idx ); isInsertable &= ( pos.x + blockPos.x >= 0 && pos.x + blockPos.x < width) && (pos.y + blockPos.y >= 0 && pos.y + blockPos.y < height) && ( getBlockAt( (int)(pos.x + blockPos.x), (int)(pos.y + blockPos.y)) == -1 ); } return isInsertable; } public boolean isLineFull(int index) { boolean isFull = true; for(int idx = 0; idx < width && isFull; idx++) { isFull &= ( getBlockAt(idx, index) != -1 ); } return isFull; } public int getNumberOfLines(){ return height; } public int getNumberOfColumns(){ return width; } public boolean hasFullLines() { for(int idx = 0; idx < height; idx++) { if(isLineFull(idx)) { return true; } } return false; } public void eraseLine(int index) { pit.remove( pit.get( index ) ); addNewLine(); } private void addNewLine() { int [] line = new int[width]; for(int x = 0; x < width; x++) line[x] = -1; pit.add(line); } }
Now that we have a way to create pieces and a pit to insert pieces into, we turn our attention to the GamePlayState class.
What you probably do not know is that after we separated our game into states, we have to do the same for the gameplay. The gameplay is also a state machine, these are the states: GAME_STARTING, INSERT_NEW_PIECE, MOVING_PIECE, LINE_DESTRUCTION, PAUSE_GAME_STATE, HIGHSCORE_STATE, GAME_OVER.
So lets setup the class for this:
public class GameplayState extends BasicGameState{ ... private enum STATES { START_GAME_STATE, NEW_PIECE_STATE, MOVING_PIECE_STATE, LINE_DESTRUCTION_STATE, PAUSE_GAME_STATE, HIGHSCORE_STATE, GAME_OVER_STATE } private STATES currentState = null;
This defines all of our game states.
@Override
public void enter(GameContainer gc, StateBasedGame sb) throws SlickException
{
super.enter(gc, sb);
pit.makeCleanPit();
currentState = STATES.START_GAME_STATE;
score = 0;
}
One thing about game states, we have these two little methods, called enter and leave that are called whenever we enter or leave a state. Its nice to be able to initialize some variables here, like our currentState, and cleaning the pit, as well as resetting the score.
public void update(GameContainer gc, StateBasedGame sb, int delta) throws SlickException { switch(currentState) { case START_GAME_STATE: currentState = STATES.NEW_PIECE_STATE; deltaCounter = 500; break; case NEW_PIECE_STATE: generateNewPiece(); break; case MOVING_PIECE_STATE: updatePiece( gc, sb, delta); break; case LINE_DESTRUCTION_STATE: checkForFullLines(gc, sb, delta); currentState = STATES.NEW_PIECE_STATE; break; case HIGHSCORE_STATE: break; case PAUSE_GAME_STATE: break; case GAME_OVER_STATE: Highscores.getInstance().addScore(score); sb.enterState(SlickBlocksGame.MAINMENUSTATE); break; } }
And this our update method reflecting the inner gameplay state machine.
Lets add the rest of the code
Let's add the rest of the code.
public void checkForFullLines( GameContainer gc, StateBasedGame sb, int delta ) { int linesDestroyed = 0; for(int lineIdx = 0; lineIdx < pit.getNumberOfLines(); ) { if( pit.isLineFull(lineIdx) ) { pit.eraseLine(lineIdx); linesDestroyed++; }else lineIdx++; } switch(linesDestroyed) { case 0 : score += 10; break; case 1 : score += 100; break; case 2 : score += 300; break; case 3 : score += 600; break; case 4 : score += 1000; break; } }
This is a simple check to see how many lines were destroyed and score is given accordingly.
private void generateNewPiece() { if(currentPiece == null) nextPiece = pieceFactory.generateRandomPiece(); currentPiece = nextPiece; cursorPos = new Tuple( 5, 19); if(pit.isPieceInsertableIn(currentPiece, cursorPos )) { nextPiece = pieceFactory.generateRandomPiece(); currentState = STATES.MOVING_PIECE_STATE; }else{ currentState = STATES.GAME_OVER_STATE; } }
This method allows us to generate a new piece after the last landed on the pit. It always starts the piece at the middle top of the screen. If the new piece cant be inserted than its game over.
int deltaCounter = 500; int inputDelta = 0;
DeltaCounter is the time a piece needs to wait before falling a setp in the pit and inputDelta is just a variables to stop the user from giving input for X milliseconds, because if you cant control the pace of the input the pieces rotate really fast.
private void updatePiece( GameContainer gc, StateBasedGame sb, int delta ) { Tuple newCursorPos = new Tuple( cursorPos.x, cursorPos.y); //is it falling down time? deltaCounter -= delta; inputDelta -= delta; if( deltaCounter < 0) { newCursorPos.y -= 1; if(!pit.isPieceInsertableIn(currentPiece, newCursorPos)) { pit.insertPieceAt(currentPiece, (int)cursorPos.x, (int)cursorPos.y); blockFX.play(); currentState = STATES.LINE_DESTRUCTION_STATE; return; } deltaCounter= 500; } //verificar o input Input input = gc.getInput(); if(inputDelta < 0) { if(input.isKeyDown(Input.KEY_LEFT)) { newCursorPos.x -= 1; if(!pit.isPieceInsertableIn(currentPiece, newCursorPos)) newCursorPos.x += 1; else inputDelta = 100; } if(input.isKeyDown(Input.KEY_RIGHT)) { newCursorPos.x += 1; if(!pit.isPieceInsertableIn(currentPiece, newCursorPos)) newCursorPos.x -= 1; else inputDelta = 100; } if(input.isKeyDown(Input.KEY_UP)) { currentPiece.rotateRight(); if(!pit.isPieceInsertableIn(currentPiece, newCursorPos)) currentPiece.rotateLeft(); else inputDelta = 150; } if(input.isKeyDown(Input.KEY_DOWN)) { newCursorPos.y -= 1; if(!pit.isPieceInsertableIn(currentPiece, newCursorPos)){ newCursorPos.y += 1; } else inputDelta = 10; } } cursorPos = new Tuple(newCursorPos.x, newCursorPos.y); }
It may seem big, but this method controls everything when the piece is moving. Another design I decided to take was to have a cursor position that represents the central block of the piece, this makes my moving decision really easy since its just a simple add or subtract to the cursor. All the piece is constructed against this position. Also think of the pit as a layer of already landed blocks, the current piece (the one the player is moving) inst actually inserted into the pit, just like a piece o transparent paper, the piece is put over the pit to see if the current action is possible. As you can see we replicate the cursor position, make the changes to it according to the actions we want and if all goes well the actual cursor position is updated. The checks are made and if not possible taken back, for example, we are rotating the piece near the left of the pit, the piece is rotated, checked against the pit that tells us that the piece cannot fit the pit like this, and then the rotation is dismissed.
Finally we render our scene. Remember how I said that slick2D does not have layers, well now we have to be careful where we put the drawing commands.
So what do we want to render? Since all the logic is taken care of, we only need to render the contents of the pit, the current piece and the scores.
int pitWidth = 0; int pitDepth = 0; static int PIT_X = 52; static int PIT_Y = 18; int blockSize = 0; ... public void init(GameContainer gc, StateBasedGame sb) throws SlickException { ... blockSize = 28; pitWidth = 10; pitDepth = 20; pit = new Pit(pitWidth, pitDepth); ... @Override public void render(GameContainer gc, StateBasedGame sb, Graphics g) throws SlickException { // Paint HUD gameHUD.draw(0,0); // draw pit for(int lineIdx = 0; lineIdx < pit.getNumberOfLines(); lineIdx++) { for(int columnIdx = 0; columnIdx < pit.getNumberOfColumns(); columnIdx++) { int blockType = pit.getBlockAt(columnIdx, lineIdx); if(blockType != -1) { blockImages.get(blockType).draw(columnIdx*blockSize+PIT_X, PIT_Y + (blockSize* ( pit.getNumberOfLines() - lineIdx - 1))); } } } // draw currentPiece if(currentPiece != null) { for(int i = 0; i < 4; i++) { Tuple blockPos = currentPiece.getPosOfBlock(i); blockImages.get(i).draw( PIT_X + (blockPos.x + cursorPos.x) * blockSize, PIT_Y + ( pit.getNumberOfLines() - 1 -( blockPos.y + cursorPos.y)) * blockSize ); } } // draw nextPiece if(nextPiece != null) { for(int i = 0; i < 4; i++) { Tuple blockPos = nextPiece.getPosOfBlock(i); blockImages.get(i).draw( PIT_X + 336 + (blockPos.x) * blockSize, PIT_Y + 56 + ( ( blockPos.y )) * blockSize ); } } trueTypeFont.drawString(600, 80, String.valueOf(score), Color.orange ); transparentGameHUD.draw(48, 15); }
Well and that's it… now we have a working copy of a falling brick clone. but still its really basic, all this work and its still really basic, no levels, no speed enhancement, etc. Time for some…
And that's it, this concludes the second tutorial on Slick2D, there was no BIG coding improvement over the other, but we made a full length game and you should stop to enjoy it a bit.
package slick.path2glory.blockgame; import java.util.ArrayList; import org.newdawn.slick.AppGameContainer; import org.newdawn.slick.GameContainer; import org.newdawn.slick.SlickException; import org.newdawn.slick.state.StateBasedGame; /** * * @author Spiegel * */ public class SlickBlocksGame extends StateBasedGame { public static final int MAINMENUSTATE = 0; public static final int RESOURCELOADERSTATE = 1; public static final int GAMEPLAYSTATE = 2; public SlickBlocksGame() { super("SlickBlocks"); this.addState(new MainMenuState(MAINMENUSTATE)); this.addState(new GameplayState(GAMEPLAYSTATE)); this.enterState(MAINMENUSTATE); } public static void main(String[] args) throws SlickException { AppGameContainer app = new AppGameContainer(new SlickBlocksGame()); app.setDisplayMode(800, 600, false); app.start(); } @Override public void initStatesList(GameContainer gameContainer) throws SlickException { this.getState(MAINMENUSTATE).init(gameContainer, this); this.getState(GAMEPLAYSTATE).init(gameContainer, this); //tentar criar um novo state } }
package slick.path2glory.blockgame; import java.awt.Font; import java.util.ArrayList; import org.newdawn.slick.Color; import org.newdawn.slick.GameContainer; import org.newdawn.slick.Graphics; import org.newdawn.slick.Image; import org.newdawn.slick.Input; import org.newdawn.slick.SlickException; import org.newdawn.slick.Sound; import org.newdawn.slick.TrueTypeFont; import org.newdawn.slick.state.BasicGameState; import org.newdawn.slick.state.StateBasedGame; public class MainMenuState extends BasicGameState{ Image background = null; Image startGameOption = null; Image exitOption = null; int stateID = 0; Highscores highscores = null; private static int menuX = 410; private static int menuY = 160; float startGameScale = 1; float exitScale = 1; Sound fx = null; public MainMenuState(int stateID ) { this.stateID = stateID; } @Override public int getID() { return stateID; } TrueTypeFont trueTypeFont = null; public void init(GameContainer arg0, StateBasedGame arg1) throws SlickException { background = new Image("data/menu.jpg"); // Load the menu images Image menuOptions = new Image("data/menuoptions.png"); startGameOption = menuOptions.getSubImage(0, 0, 377, 71); exitOption = menuOptions.getSubImage(0, 71, 377, 71); //-------------------------------------------------- fx = new Sound("data/Desktop/Pictures/pulse.wav"); //-------------------------------------------------- Font font = new Font("Verdana", Font.BOLD, 20); trueTypeFont = new TrueTypeFont(font, true); highscores = Highscores.getInstance(); } public void render(GameContainer arg0, StateBasedGame arg1, Graphics arg2) throws SlickException { // render the background background.draw(0, 0); // Draw menu startGameOption.draw(menuX, menuY, startGameScale); exitOption.draw(menuX, menuY+80, exitScale); // Draw Highscores int index = 1; int posY = 300; ArrayList<Integer> highScoreList = highscores.getScores(); for(Integer score : highScoreList ) { trueTypeFont.drawString(20, posY, " " + ( index < highScoreList.size() ? "0" + index : "" + index) + " ." + score, Color.orange); index++; posY += 20; } } float scaleStep = 0.0001f; public void update(GameContainer gc, StateBasedGame sb, int delta) throws SlickException { Input input = gc.getInput(); int mouseX = input.getMouseX(); int mouseY = input.getMouseY(); boolean insideStartGame = false; boolean insideExit = false; if( ( mouseX >= menuX && mouseX <= menuX + startGameOption.getWidth()) && ( mouseY >= menuY && mouseY <= menuY + startGameOption.getHeight()) ) { insideStartGame = true; }else if( ( mouseX >= menuX && mouseX <= menuX+ exitOption.getWidth()) && ( mouseY >= menuY+80 && mouseY <= menuY+80 + exitOption.getHeight()) ) { insideExit = true; } if(insideStartGame) { if(startGameScale < 1.05f) startGameScale += scaleStep * delta; if ( input.isMouseButtonDown(Input.MOUSE_LEFT_BUTTON) ){ fx.play(); sb.enterState(SlickBlocksGame.GAMEPLAYSTATE); } }else{ if(startGameScale > 1.0f) startGameScale -= scaleStep * delta; if ( input.isMouseButtonDown(Input.MOUSE_LEFT_BUTTON) ) gc.exit(); } if(insideExit) { if(exitScale < 1.05f) exitScale += scaleStep * delta; }else{ if(exitScale > 1.0f) exitScale -= scaleStep * delta; } } }
package slick.path2glory.blockgame; import java.util.ArrayList; public class Highscores { private static Highscores instance = null; ArrayList<Integer> scores = null; private Highscores(int size) { scores = new ArrayList<Integer>(); for(int i = 0; i < size; i++) scores.add(new Integer(0)); } public static Highscores getInstance() { if(instance == null) instance = new Highscores(10); return instance; } public boolean addScore(int score) { for(int idx = 0; idx < scores.size(); idx++) { if(score > scores.get(idx)) { scores.add(idx, new Integer(score)); scores.remove(scores.size()-1); return true; } } return false; } public ArrayList<Integer> getScores() { return scores; } }
package slick.path2glory.blockgame; public class Piece { private String name; Tuple [][] rotationMatrice; int currentRotation; public Piece(String name, Tuple [][] rotationMatrice) { this.name = name; currentRotation = 0; this.rotationMatrice = rotationMatrice; } public Tuple getPosOfBlock( int blockNumber ) { return rotationMatrice[currentRotation][blockNumber]; } public void rotateRight() { currentRotation = ++currentRotation % rotationMatrice.length; } public void rotateLeft() { currentRotation = --currentRotation % rotationMatrice.length; } public static class Tuple{ public int x; public int y; public Tuple(int x, int y) { this.x = x; this.y = y; } } }
package slick.path2glory.blockgame; import slick.path2glory.blockgame.Piece.Tuple; public class PieceFactory { private int counter = 0; /** Creates a new instance of PieceFactory */ public PieceFactory( ) { } public Piece generateRandomPiece() { java.util.Random random = new java.util.Random(); switch(random.nextInt(7)) { case 0: return generatePiece('t'); case 1: return generatePiece('s'); case 2: return generatePiece('z'); case 3: return generatePiece('o'); case 4: return generatePiece('i'); case 5: return generatePiece('l'); case 6: return generatePiece('j'); } return null; } // Rotation Matrixes static Tuple [][] TRotationMatrice = { {new Tuple(0,0), new Tuple(-1,0), new Tuple(1,0), new Tuple(0,-1)}, {new Tuple(0,0), new Tuple(0,1), new Tuple(0,-1), new Tuple(-1,0)}, {new Tuple(0,0), new Tuple(1,0), new Tuple(-1,0), new Tuple(0,1)}, {new Tuple(0,0), new Tuple(0,-1), new Tuple(0,1), new Tuple(1,0)} }; static Tuple [][] SRotationMatrice = { {new Tuple(0,0), new Tuple(1,0), new Tuple(0,-1), new Tuple(-1,-1)}, {new Tuple(0,0), new Tuple(0,1), new Tuple(1,0), new Tuple(1,-1)}, {new Tuple(0,0), new Tuple(1,0), new Tuple(0,-1), new Tuple(-1,-1)}, {new Tuple(0,0), new Tuple(0,1), new Tuple(1,0), new Tuple(1,-1)} }; static Tuple [][] ZRotationMatrice = { {new Tuple(0,0), new Tuple(-1,0), new Tuple(0,-1), new Tuple(1,-1)}, {new Tuple(0,0), new Tuple(0,1), new Tuple(-1,0), new Tuple(-1,-1)}, {new Tuple(0,0), new Tuple(-1,0), new Tuple(0,-1), new Tuple(1,-1)}, {new Tuple(0,0), new Tuple(0,1), new Tuple(-1,0), new Tuple(-1,-1)} }; static Tuple [][] ORotationMatrice = { {new Tuple(0,0), new Tuple(1,0), new Tuple(0,-1), new Tuple(1,-1)}, {new Tuple(0,0), new Tuple(1,0), new Tuple(0,-1), new Tuple(1,-1)}, {new Tuple(0,0), new Tuple(1,0), new Tuple(0,-1), new Tuple(1,-1)}, {new Tuple(0,0), new Tuple(1,0), new Tuple(0,-1), new Tuple(1,-1)} }; static Tuple [][] IRotationMatrice = { {new Tuple(0,0), new Tuple(-1,0), new Tuple(1,0), new Tuple(2,0)}, {new Tuple(0,0), new Tuple(0,-1), new Tuple(0,1), new Tuple(0,2)}, {new Tuple(0,0), new Tuple(-1,0), new Tuple(1,0), new Tuple(2,0)}, {new Tuple(0,0), new Tuple(0,-1), new Tuple(0,1), new Tuple(0,2)} }; static Tuple [][] LRotationMatrice = { {new Tuple(0,0), new Tuple(1,0), new Tuple(-1,0), new Tuple(-1,-1)}, {new Tuple(0,0), new Tuple(0,-1), new Tuple(0,1), new Tuple(-1,1)}, {new Tuple(0,0), new Tuple(-1,0), new Tuple(1,0), new Tuple(1,1)}, {new Tuple(0,0), new Tuple(0,1), new Tuple(0,-1), new Tuple(1,-1)} }; static Tuple [][] JRotationMatrice = { {new Tuple(0,0), new Tuple(-1,0), new Tuple(1,0), new Tuple(1,-1)}, {new Tuple(0,0), new Tuple(0,1), new Tuple(0,-1), new Tuple(-1,-1)}, {new Tuple(0,0), new Tuple(1,0), new Tuple(-1,0), new Tuple(-1,1)}, {new Tuple(0,0), new Tuple(0,-1), new Tuple(0,1), new Tuple(1,1)} }; public Piece generatePiece(char pieceType) { String name = pieceType + "Shape" + counter++; switch(pieceType) { case 't': { return new Piece(name, TRotationMatrice); } case 's': { return new Piece(name, SRotationMatrice); } case 'z': { return new Piece(name, ZRotationMatrice); } case 'o': { return new Piece(name, ORotationMatrice); } case 'i': { return new Piece(name, IRotationMatrice); } case 'l': { return new Piece(name, LRotationMatrice); } case 'j': { return new Piece(name, JRotationMatrice); } } return null; } }
package slick.path2glory.blockgame; import java.util.ArrayList; import org.newdawn.slick.geom.Vector2f; import slick.path2glory.blockgame.Piece.Tuple; public class Pit { int height; int width; ArrayList<int[]> pit = null; public Pit(int width, int height) { this.width = width; this.height = height; pit = new ArrayList<int[]>(); makeCleanPit(); } public void makeCleanPit() { pit.clear(); for(int y = 0; y < height; y++) { addNewLine(); } } public boolean insertPieceAt(Piece piece, int x, int y) { Tuple insertPos = new Tuple(x,y); if(isPieceInsertableIn(piece, insertPos )) { for(int i = 0; i < 4; i++) setBlockAt( i, new Tuple(piece.getPosOfBlock(i).x + insertPos.x, piece.getPosOfBlock(i).y + insertPos.y ) ); return true; } return false; } public void setBlockAt(int value, Piece.Tuple pos) { setBlockAt(value, (int)pos.x, (int)pos.y); } public final void setBlockAt(int value, int x, int y) { pit.get(y)[x] = value; } public int getBlockAt(Vector2f pos) { return getBlockAt((int)pos.x, (int)pos.y); } public int getBlockAt(int x, int y) { return pit.get(y)[x]; } public boolean isPieceInsertableIn( Piece piece, Tuple pos ) { //return ( pos.x >= 0 && pos.x < size.x) && (pos.y >= 0 && pos.y < size.y); boolean isInsertable = true; for(int idx = 0; idx < 4 && isInsertable; idx++) { Tuple blockPos = piece.getPosOfBlock( idx ); isInsertable &= ( pos.x + blockPos.x >= 0 && pos.x + blockPos.x < width) && (pos.y + blockPos.y >= 0 && pos.y + blockPos.y < height) && ( getBlockAt( (int)(pos.x + blockPos.x), (int)(pos.y + blockPos.y)) == -1 ); } return isInsertable; } public boolean isLineFull(int index) { boolean isFull = true; for(int idx = 0; idx < width && isFull; idx++) { isFull &= ( getBlockAt(idx, index) != -1 ); } return isFull; } public int getNumberOfLines(){ return height; } public int getNumberOfColumns(){ return width; } public boolean hasFullLines() { for(int idx = 0; idx < height; idx++) { if(isLineFull(idx)) { return true; } } return false; } public void eraseLine(int index) { pit.remove( pit.get( index ) ); addNewLine(); } private void addNewLine() { int [] line = new int[width]; for(int x = 0; x < width; x++) line[x] = -1; pit.add(line); } }
package slick.path2glory.blockgame; import java.awt.Font; import java.util.ArrayList; import org.newdawn.slick.Color; import org.newdawn.slick.GameContainer; import org.newdawn.slick.Graphics; import org.newdawn.slick.Image; import org.newdawn.slick.Input; import org.newdawn.slick.SlickException; import org.newdawn.slick.Sound; import org.newdawn.slick.TrueTypeFont; import org.newdawn.slick.state.BasicGameState; import org.newdawn.slick.state.StateBasedGame; import slick.path2glory.blockgame.Piece.Tuple; public class GameplayState extends BasicGameState{ private int stateId = 0; PieceFactory pieceFactory = null; Pit pit = null; Tuple cursorPos; Piece currentPiece = null; Piece nextPiece = null; Image gameHUD = null; Image transparentGameHUD = null; TrueTypeFont trueTypeFont = null; Sound blockFX = null; int score = 0; int deltaCounter = 500; int inputDelta = 0; int pitWidth = 0; int pitDepth = 0; static int PIT_X = 52; static int PIT_Y = 18; int blockSize = 0; ArrayList<Image> blockImages = null; private enum STATES { START_GAME_STATE, NEW_PIECE_STATE, MOVING_PIECE_STATE, LINE_DESTRUCTION_STATE, PAUSE_GAME_STATE, HIGHSCORE_STATE, GAME_OVER_STATE } private STATES currentState = null; public GameplayState(int stateId) { this.stateId = stateId; } @Override public int getID() { return stateId; } @Override public void init(GameContainer gc, StateBasedGame sb) throws SlickException { Image allImages = new Image("data/gameplaytextures.png"); gameHUD = new Image("data/hudblockgame.jpg"); transparentGameHUD = allImages.getSubImage(29, 0, 287, 573); blockImages = new ArrayList<Image>(); for(int i = 0; i < 4; i++) blockImages.add(allImages.getSubImage(0, i*blockSize, blockSize, blockSize)); pieceFactory = new PieceFactory(); blockSize = 28; pitWidth = 10; pitDepth = 20; pit = new Pit(pitWidth, pitDepth); blockFX = new Sound("data/pulse.wav"); Font font = new Font("Verdana", Font.BOLD, 40); trueTypeFont = new TrueTypeFont(font, true); } @Override public void enter(GameContainer gc, StateBasedGame sb) throws SlickException { super.enter(gc, sb); pit.makeCleanPit(); currentState = STATES.START_GAME_STATE; score = 0; } @Override public void render(GameContainer gc, StateBasedGame sb, Graphics g) throws SlickException { // Paint HUD gameHUD.draw(0,0); // draw pit for(int lineIdx = 0; lineIdx < pit.getNumberOfLines(); lineIdx++) { for(int columnIdx = 0; columnIdx < pit.getNumberOfColumns(); columnIdx++) { int blockType = pit.getBlockAt(columnIdx, lineIdx); if(blockType != -1) { blockImages.get(blockType).draw(columnIdx*blockSize+PIT_X, PIT_Y + (blockSize* ( pit.getNumberOfLines() - lineIdx - 1))); } } } // draw currentPiece if(currentPiece != null) { for(int i = 0; i < 4; i++) { Tuple blockPos = currentPiece.getPosOfBlock(i); blockImages.get(i).draw( PIT_X + (blockPos.x + cursorPos.x) * blockSize, PIT_Y + ( pit.getNumberOfLines() - 1 -( blockPos.y + cursorPos.y)) * blockSize ); } } // draw nextPiece if(nextPiece != null) { for(int i = 0; i < 4; i++) { Tuple blockPos = nextPiece.getPosOfBlock(i); blockImages.get(i).draw( PIT_X + 336 + (blockPos.x) * blockSize, PIT_Y + 56 + ( ( blockPos.y )) * blockSize ); } } trueTypeFont.drawString(600, 80, String.valueOf(score), Color.orange ); transparentGameHUD.draw(48, 15); } @Override public void update(GameContainer gc, StateBasedGame sb, int delta) throws SlickException { switch(currentState) { case START_GAME_STATE: currentState = STATES.NEW_PIECE_STATE; deltaCounter = 500; break; case NEW_PIECE_STATE: generateNewPiece(); break; case MOVING_PIECE_STATE: updatePiece( gc, sb, delta); break; case LINE_DESTRUCTION_STATE: checkForFullLines(gc, sb, delta); currentState = STATES.NEW_PIECE_STATE; break; case HIGHSCORE_STATE: break; case PAUSE_GAME_STATE: break; case GAME_OVER_STATE: Highscores.getInstance().addScore(score); sb.enterState(SlickBlocksGame.MAINMENUSTATE); break; } } public void checkForFullLines( GameContainer gc, StateBasedGame sb, int delta ) { int linesDestroyed = 0; for(int lineIdx = 0; lineIdx < pit.getNumberOfLines(); ) { if( pit.isLineFull(lineIdx) ) { pit.eraseLine(lineIdx); linesDestroyed++; }else lineIdx++; } switch(linesDestroyed) { case 0 : score += 10; break; case 1 : score += 100; break; case 2 : score += 300; break; case 3 : score += 600; break; case 4 : score += 1000; break; } } private void updatePiece( GameContainer gc, StateBasedGame sb, int delta ) { Tuple newCursorPos = new Tuple( cursorPos.x, cursorPos.y); //verificar se está na altura de cair deltaCounter -= delta; inputDelta -= delta; if( deltaCounter < 0) { newCursorPos.y -= 1; if(!pit.isPieceInsertableIn(currentPiece, newCursorPos)) { pit.insertPieceAt(currentPiece, (int)cursorPos.x, (int)cursorPos.y); blockFX.play(); currentState = STATES.LINE_DESTRUCTION_STATE; return; } deltaCounter= 500; } //verificar o input Input input = gc.getInput(); if(inputDelta < 0) { if(input.isKeyDown(Input.KEY_LEFT)) { newCursorPos.x -= 1; if(!pit.isPieceInsertableIn(currentPiece, newCursorPos)) newCursorPos.x += 1; else inputDelta = 100; } if(input.isKeyDown(Input.KEY_RIGHT)) { newCursorPos.x += 1; if(!pit.isPieceInsertableIn(currentPiece, newCursorPos)) newCursorPos.x -= 1; else inputDelta = 100; } if(input.isKeyDown(Input.KEY_UP)) { currentPiece.rotateRight(); if(!pit.isPieceInsertableIn(currentPiece, newCursorPos)) currentPiece.rotateLeft(); else inputDelta = 150; } if(input.isKeyDown(Input.KEY_DOWN)) { newCursorPos.y -= 1; if(!pit.isPieceInsertableIn(currentPiece, newCursorPos)){ newCursorPos.y += 1; } else inputDelta = 10; } } cursorPos = new Tuple(newCursorPos.x, newCursorPos.y); } private void generateNewPiece() { if(currentPiece == null) nextPiece = pieceFactory.generateRandomPiece(); currentPiece = nextPiece; cursorPos = new Tuple( 5, 19); if(pit.isPieceInsertableIn(currentPiece, cursorPos )) { nextPiece = pieceFactory.generateRandomPiece(); currentState = STATES.MOVING_PIECE_STATE; }else{ currentState = STATES.GAME_OVER_STATE; } } }