Dive In: JSnake
Continuing on the Dive In series, we are going one step further and creating a small game using animation: a clone of the classic video game Snake. As always, this will be as small as possible and many many changes can be made (which I'll once again try to point out). We'll start from design all the way to implementing and testing it using the easy Water Fall model. You can run the game and download the source code at my JavaCorner.
So, without further ado, let's start!
So, our first step is to define how our game will work, what can the player do, and generally describe the functionality using Use Cases.
- Action: Game Logic (Goal)
Description: The player starts with a basic single red square head that constantly moves in the right direction. He uses the arrow keys to change the direction either up, down, left or right. The head cannot stop moving. A green square (food) appears randomly on the board. If the head touches the food, a orange square (tail) appears right after the head. The food disappears and reappears at another random spot. The place in which the snake moves is a square board with walls on each side. The player must avoid hitting the walls and it's own tail or else the game ends. The goal is to eat as much food as possible without hitting.
Since our game is just a small snake game, we will only need this Use Case.
Using a single flow chart, we will design our game logic as simple as possible. Here is an example:
So, let's begin explaining this diagram to find our variables and classes.
When the game begins, we initially set the direction of our snake to the right. After that we start the main loop. The first thing to do inside the loop is move our snake's head one step in the current direction. After we have done this we check to see if the current position of the head collides with a wall . If it does, then game over. If it doesn't we check to see if the same position collides with a tail. If it does, again, game over. If it doesn't we check to see if it collides with food. It it does, we add a tail. If it doesn't we look for user input. If the user pressed on any arrow key, we change the direction of our snake. Either way, we continue our loop from the beginning.
As we can see, I have colored the various variables, methods and classes to make it easier to identify what we need here. So, we clearly need a variable that will store our current direction. This can be done either with a Class, an Enum, an integer or any way you like. I'll choose the easiest, an integer called direction. 0 = Top, 1 = left, 2 = bottom, 3 = right.
Now, the next thing we need is to define our snake. We know that our snake consists of a head and a tail (which consists of a number of tail parts). All of them are squares. The head is a red one and we need to store its current position. Each tail part is an orange one, and follows the tail part that comes before it or, for the first tail part, the head. So basically our snake is a chain of snake parts (either head or tail) that each follow the one before it. I don't know about you, but whenever I here about chains, I think of Linked Lists. So what we are basically going to do is create a Linked List of Snake Parts. So, we create a class called SnakePart. It should have it's x and y position and a tail which is also a SnakePart class. We only need a reference of the head. What happens is, we begin by telling the head to move to a specific position calling a method (move). This method will also call the tails method move to move to the previous position of the head (if there is a tail). The tail's method will move to that position and, again, call its tails move method to move to its previous position (again, if it exists) and so on so forth. This chain reaction will update all the snake parts in their new positions.
Next is the food. The food is easy. We only need a Point object that will tell us in which position the food is. When the head of the snake overlaps the food, we call a method (createFood) which will place the food to a random new position. That's all! Well, no it isn't. What if the random new position is on a SnakePart? For the sake of simplicity, I'll skip this part.
We also have the walls. Because this game will be as simple as possible, there isn't anything special about the walls. We simply look if the head is going to hit the walls (which means either x or y will be either -1 or the maximum size of our board) and if it does, the game is over. We will talk about implementing a more sophisticated wall in the Maintenance.
Now, all these objects should be wrapped around some classes. So, GameData will have a head, the direction, the food, and two constants to determine the width and height of our board BOARD_WIDTH and BOARD_HEIGHT respectively. The methods that will loop one step and do collision detections would be in another class, TheGame. So, this class would have a GameData object, a wallCollision method, a tailCollision method, a foodCollision method, a mainLoop method and a createFood method. Finally, we would have a GUI class that visually shows the game. Our class diagram would look something like this:
So, we finally begin coding. Let's look at some parts of the code individually and then stick them all together. You can read the source code side-by-side with this section:
Since we did our class diagram, we first start creating all our classes with their attributes and empty methods. GameDate can simply stop right there because it's basically a Data Type. But it would be better and safer to create modifiers and accessors (getters and setters) and a constructor for basic initialization. I'll only implement a constructor which will place the head in the middle of the board, set the direction to the right and create a Point for the food.
The SnakePart isn't difficult either. We only store the current position and the tail (if there is any). We create getters and setters for the tail (which we will be needing later on) and two constructors. The first constructor will have a Point and a SnakePart for parameters, which will set the SnakePart's attributes respectively. The second constructor will only have a Point parameter because we really need the starting point of our SnakePart.
Now comes the good part, the TheGame class. Here we will set all our business logic, meaning, the loop, the collision detections and the food positioning. Probably the easiest part is the positioning of the food, which changes the food Point into random integer values from 0 to GameData.BOARD_WIDTH/HEIGHT - 1 . The wall collision is easy too. We simply look at the current heads position and check to see if the x or y is -1 or GameData.BOARD_WIDTH/HEIGHT. If yes, return true, otherwise false. The food collision is even easier. We just compare the y's and x's of the head and food for equality. If both are equal, then we return true, otherwise false. Our final collision detection method is to detect a tail collision. Since our snake is a linked list of SnakeParts, we take one tail at a time and compare their positions with the head's position. If their equal, we have a collision!
So far we created some Data Types and basic functionality. Now it's time to create our game loop. It's actually fairly simple. All we have to do is move the head to the current direction and do collision detection. We begin with a switch statement for each direction and call the heads move method depending on which direction we are moving. Next we call the wallCollision and if it returns true, we throw a new Exception("Game Over") (this can be done with various ways, but I find this the easiest. Probably a more proper way would be to throw a custom Exception, i.e. new GameOverException("Hit a wall"). We then call tailCollision and handle the returning true (if it does) the same way and finally we call the foodCollision. If this returns true, we add a new SnakePart to the head (setting the head's tail to the new SnakePart and setting the new SnakePart's tail to the previous tail of the head) and call createFood.
Now, the most interesting part of the program is creating the loop. Again, for the sake of simplicity, I'll create a really basic game loop but explain where the code could be improved.
The first thing to do is create a class that extends JPanel. This will be our canvas, so let's call it Canvas. It will contain all the looping and drawing of our game. We start our Canvas by defining a constructor. The constructor will set the preferred size of the canvas (i.e. GameData.BOARD_WIDTH/HEIGHT * 10) and ask for focus. Asking for focus will allow us to listen for any keyboard events, so, naturally, we then add a KeyListener. In our anonymous inner class that will handle the key events, we switch the keycode of the key event for an UP, LEFT, DOWN, RIGHT key press. We then simply change the value of the GameData.direction to the appropriate one as defined earlier. That's the end of our constructor.
The next thing to do is create the code which will handle the actual loop. Let's call it run for now and I'll explain later why I called it that way. When this method is called, our loop begins. It will update the state of the game, render the scene and wait for 200 milliseconds. This continues on until the game ends. We do this by declaring a boolean running and loop while it is true. The running variable will change when the game will be over (i.e. collision with wall). If we try to execute the code as it is and call run, the game will stop responding because the loop runs in the same Thread as the rest of our program and doesn't let anything else to be processed. To avoid this, we create a new Thread and tell it to do this job. We do this by implementing the Interface Runnable. This Interface only has one method to implement, the run, which we already declared previously. Now, when we want to start the game loop, we create a new Thread and pass to the constructor this class (Canvas). Then call the Thread's method start() and the run method will be automatically called by the Thread. Now everything is done in the background and there is place for additional processing to be done.
In the run method, we update our game state. Since we have already done this part at the TheGame class, all we have to do is call our TheGame method mainLoop() and the state of the game will be updated. After the state has been updated, we need to draw the scene. We will use a trick called double buffering. This allows us to render our scene in a second image (buffer) while the first is being displayed to the screen and avoid flickering and other artifacts. To do this, we create a method called render and do all the drawing in an image that we created (img) calling JComponent's createImage(int width, int height) method. To start drawing, we need a Graphics object. Every Image object has a method called getGraphics() that returns it's own Graphics object which we use in order to draw on that image. Long story short, we get the Graphics object of our img and start rendering our scene. We draw each part of the snake the same way we did when we where detecting collision, through iteration (getting each SnakePart's tail and drawing it).
After we have done rendering, we must tell that to our Canvas and it should display it on the screen. We can do this by calling it's repaint method but because this method relies on the operating system and not always the response time is the same, we could implement another trick called active rendering. What we do is call a method (paintScreen) that will basically force our rendered image (img) to be drawn on our Canvas. This is done by calling our Canvas's getGraphics method and drawing the image on it.
Finally, we have to somehow pause our game for a certain period otherwise the game will run so fast it will basically end before it started. This part is probably the most tricky part of all. In order for us to have a certain Frames Per Second we must calculate the time it took to update the state of the game and render the scene and then pause it for the appropriate amount of time. We must calculate this down to nanoseconds in order to have the best results. Java has a method in the System class called nanoTime which returns the most precise available time. Unfortunately, this depends mostly on the Operating System so we get different values for different Operating Systems. Other ways of accomplishing this is using Third Party libraries such as Java3D. Once again, for the sake of simplicity, I'll just call Thread's method sleep(int milliseconds) to pause the current Thread for 200 milliseconds.
And that's that!
As for the testing of this game, we should have created at least for the SnakePart, the TheGame and the GameData some Unit Tests just to make sure we don't have any critical mistakes and allow us to improve our game in the safe side. After that, it's time to have a lot of fun playing the game over and over again to make sure everything is OK. In our source code for example, a food may be placed at the same position as any SnakePart we have in our game making it invisible. This should be avoided. Also, the head turns orange for the first refresh of the game when the snake eats a food, this could easily be changed. Enjoy testing!
After you have added proper getters and setters and probably added better code, here are some ideas on how to plan your next step.
What about adding a custom stage? You could add another type of object in the game called Wall, which you would store all of them in a List and checking to see if there is any collision with the head. You could also create a class named Stages and have a static method getStage(int stageNum) to return various stages with custom walls
What about a scoring system? You could add each food the snake eats to the score and, even more, you could add custom food that doesn't add tail, but simply adds to the score.
How about making the snake move faster every time it eats one (or more) pieces? Or the snake could have an initial size.
A more sophisticated loop would be perhaps a good challenge. I'm having trouble finding books for creating a game in Java, although I know "Killer Game Programming in Java" by O'Reilly.
And what about handling pause and resume events? What if the window is hidden behind another? Full screen support?
Infinite possibilities! Enjoy!