Nibbles in Java Gnome

In this part of the Java Gnome programming tutorial, we will create a Nibbles game clone.

Nibbles

Nibbles is an older classic video game. It was first created in late 70s. Later it was brought to PCs. In this game the player controls a snake. The objective is to eat as many apples as possible. Each time the snake eats an apple, its body grows. The snake must avoid the walls and its own body.

Development

The size of each of the joints of a snake is 10px. The snake is controlled with the cursor keys. Initially, the snake has three joints. The game starts immediately. When the game is finished, we display "Game Over" message in the statusbar widget.

board.java
package com.zetcode;

import java.io.FileNotFoundException;

import java.util.Timer;
import java.util.TimerTask;

import org.freedesktop.cairo.Context;

import org.gnome.gdk.Color;
import org.gnome.gdk.EventExpose;
import org.gnome.gdk.EventKey;
import org.gnome.gdk.Keyval;
import org.gnome.gdk.ModifierType;
import org.gnome.gdk.Pixbuf;
import org.gnome.gtk.DrawingArea;
import org.gnome.gtk.Justification;
import org.gnome.gtk.Label;
import org.gnome.gtk.StateType;
import org.gnome.gtk.Widget;


public class Board extends DrawingArea implements Widget.ExposeEvent {

    private final int WIDTH = 300;
    private final int HEIGHT = 300;
    private final int DOT_SIZE = 10;
    private final int ALL_DOTS = 900;
    private final int RAND_POS = 29;
    private final int DELAY = 140;
    private final int PERIOD = 80;

    private int x[] = new int[ALL_DOTS];
    private int y[] = new int[ALL_DOTS];

    private int dots;
    private int apple_x;
    private int apple_y;

    private boolean left = false;
    private boolean right = true;
    private boolean up = false;
    private boolean down = false;
    private boolean inGame = true;

    private Timer timer;

    private Pixbuf dot;
    private Pixbuf apple;
    private Pixbuf head;
    
    private Label statusbar;

    public Board(Label statusbar) {
        
        this.statusbar = statusbar;

        connect(new SnakeKeyListener());

        modifyBackground(StateType.NORMAL, Color.BLACK);

        try {
            dot = new Pixbuf("dot.png");
            apple = new Pixbuf("apple.png");
            head = new Pixbuf("head.png");

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }

        connect(this);
        setCanFocus(true);

        initGame();
    }
    
    
    public Timer getTimer() { return timer; }


    public void initGame() {

        dots = 3;

        for (int z = 0; z < dots; z++) {
            x[z] = 50 - z * 10;
            y[z] = 50;
        }

        locateApple();

        timer = new Timer();
        timer.scheduleAtFixedRate(new ScheduleTask(), DELAY, PERIOD);

    }


    public void drawObjects(Context cr) {


        if (inGame) {

            cr.setSource(apple, apple_x, apple_y);
            cr.paint();

            for (int z = 0; z < dots; z++) {
                if (z == 0) {
                    cr.setSource(head, x[z], y[z]);
                    cr.paint();
                } else {
                    cr.setSource(dot, x[z], y[z]);
                    cr.paint();
                }
            }

        } else {
            gameOver();
        }
    }


    public void gameOver() {
        
        timer.cancel();

        statusbar.setJustify(Justification.LEFT);
        statusbar.setAlignment(0f, 0.5f);
        statusbar.setLabel("Game Over");
    }


    public void checkApple() {

        if ((x[0] == apple_x) && (y[0] == apple_y)) {
            dots++;
            locateApple();
        }
    }

    public void move() {

        for (int z = dots; z > 0; z--) {
            x[z] = x[(z - 1)];
            y[z] = y[(z - 1)];
        }

        if (left) {
            x[0] -= DOT_SIZE;
        }

        if (right) {
            x[0] += DOT_SIZE;
        }

        if (up) {
            y[0] -= DOT_SIZE;
        }

        if (down) {
            y[0] += DOT_SIZE;
        }
    }

    public void checkCollision() {

        for (int z = dots; z > 0; z--) {

            if ((z > 4) && (x[0] == x[z]) && (y[0] == y[z])) {
                inGame = false;
            }
        }

        if (y[0] > HEIGHT) {
            inGame = false;
        }

        if (y[0] < 0) {
            inGame = false;
        }

        if (x[0] > WIDTH) {
            inGame = false;
        }

        if (x[0] < 0) {
            inGame = false;
        }
    }

    public void locateApple() {
        int r = (int) (Math.random() * RAND_POS);
        apple_x = ((r * DOT_SIZE));
        r = (int) (Math.random() * RAND_POS);
        apple_y = ((r * DOT_SIZE));
    }

    public boolean onExposeEvent(Widget widget, EventExpose eventExpose) {
    
        Context cr = new Context(widget.getWindow());
        drawObjects(cr);
        
        return false;
    }


    class ScheduleTask extends TimerTask {

        public void run() {

            if (inGame) {
                checkApple();
                checkCollision();
                move();
            }
            queueDraw();
        }
    }

    class SnakeKeyListener implements Widget.KeyPressEvent {

        public boolean onKeyPressEvent(Widget widget, EventKey eventKey) {
            
            final Keyval key;
            final ModifierType mod;
            
            key = eventKey.getKeyval();
            mod = eventKey.getState();

            if ((key == key.Left) && (!right)) {
                left = true;
                up = false;
                down = false;
            }

            if ((key == key.Right) && (!left)) {
                right = true;
                up = false;
                down = false;
            }

            if ((key == key.Up) && (!down)) {
                up = true;
                right = false;
                left = false;
            }

            if ((key == key.Down) && (!up)) {
                down = true;
                right = false;
                left = false;
            }
            
            return false;
        }
    }
}

First we will define some globals used in our game.

The WIDTH and HEIGHT constants determine the size of the Board. The DOT_SIZE is the size of the apple and the dot of the snake. The ALL_DOTS constant defines the maximum number of possible dots on the Board. The RAND_POS constant is used to calculate a random position of an apple. The DELAY constant determines the speed of the game.

private int x[] = new int[ALL_DOTS];
private int y[] = new int[ALL_DOTS];

These two arrays store x, y coordinates of all possible joints of a snake.

The initGame() method initialises variables, loads images and starts a timeout function.

In the move() method we have the key algorithm of the game. To understand it, look at how the snake is moving. You control the head of the snake. You can change its direction with the cursor keys. The rest of the joints move one position up the chain. The second joint moves where the first was, the third joint where the second was etc.

for (int z = dots; z > 0; z--) {
    x[z] = x[(z - 1)];
    y[z] = y[(z - 1)];
}

This code moves the joints up the chain.

if (left) {
    x[0] -= DOT_SIZE;
}

Move the head to the left.

In the checkCollision() method, we determine if the snake has hit itself or one of the walls.

for (int z = dots; z > 0; z--) {
    if ((z > 4) && (x[0] == x[z]) && (y[0] == y[z])) {
        inGame = false;
    }
}

We finish the game if the snake hits one of its joints with the head.

if (y[0] > HEIGHT) {
    inGame = false;
}

We finish the game if the snake hits the bottom of the Board.

The locateApple() method locates an apple randomly on the form.

int r = (int) (Math.random() * RAND_POS);

We get a random number from 0 to RAND_POS - 1.

apple_x = ((r * DOT_SIZE));
...
apple_y = ((r * DOT_SIZE));

These line set the x, y coordinates of the apple object.

In the onKeyPressEvent() method of the Board class, we determine which keys the player hit.

if ((key == key.Left) && (!right)) {
    left = true;
    up = false;
    down = false;
}

If we hit the left cursor key, we set left variable to true. This variable is used in the move() method to change coordinates of the snake object. Notice also that when the snake is heading to the right, we cannot turn immediately to the left.

nibbles.java
package com.zetcode;

import java.util.Timer;

import org.gnome.gdk.Event;
import org.gnome.gtk.Gtk;
import org.gnome.gtk.Label;
import org.gnome.gtk.VBox;
import org.gnome.gtk.Widget;
import org.gnome.gtk.Window;
import org.gnome.gtk.WindowPosition;


/**
 * ZetCode Java Gnome tutorial
 *
 * This program creates a Nibbles game clone.
 *
 * @author jan bodnar
 * website zetcode.com
 * last modified March 2009
 */

public class GNibbles extends Window {

    Board board;
    Label statusbar;

    public GNibbles() {
    
        setTitle("Nibbles");
        
        initUI();
        
        setDefaultSize(320, 320);
        setPosition(WindowPosition.CENTER);
        
        showAll();
    }
    
    public void initUI() {
     
        VBox vbox = new VBox(false, 0);
     
        statusbar = new Label("");
        board = new Board(statusbar);
        
        vbox.packStart(board);
        vbox.packStart(statusbar, false, false, 0);
        
        add(vbox);
        
        connect(new Window.DeleteEvent() {
            public boolean onDeleteEvent(Widget source, Event event) {
                Timer timer = board.getTimer();
                timer.cancel();
                Gtk.mainQuit();
                return false;
            }
        });
    }

    public static void main(String[] args)  {
        Gtk.init(args);
        new GNibbles();        
        Gtk.main();
    }
}

In this class, we set up the Nibbles game. Notice that we get the timer object from the board. This is to perform a clean exit.

Nibbles
Figure: Nibbles

This was the Nibbles computer game programmed using the Java Gnome programming library.