Here you will find the source code for the Kitty Hero game, as described in the upcoming book Graphic Guide to Java
Copyright 2025 Antony Lees
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
Main Program
private Cat cat;
private TilePool tilePool;
private Scoreboard scoreboard;
private LivesBoard livesBoard;
private int speed;
private static final int NUMBER_OF_COLUMNS = 5;
private static final int LOWEST_SPEED = 5;
private static final int HIGHEST_SPEED = 20;
void setup() {
size(600, 1000, P3D);
speed = LOWEST_SPEED;
tilePool = new TilePool();
tilePool.createTiles(height, width, NUMBER_OF_COLUMNS);
int catSize = tilePool.getTileSize();
cat = new Cat(catSize);
// initial position
cat.setX(width/2);
cat.setY(height/2);
// score
scoreboard = new Scoreboard();
scoreboard.setValue(0);
// lives
livesBoard = new LivesBoard();
livesBoard.setValue(9);
}
void draw() {
background(0);
//set speed but keep within limits
this.speed = constrain(scoreboard.getValue()/2, LOWEST_SPEED, HIGHEST_SPEED);
pushMatrix(); // PUSH MATRIX
rotateX(radians(50)); // ROTATE
// tiles
for(Tile tile : tilePool.getTiles()) {
// only display if there are lives remaining
if (livesBoard.getValue() > 0) {
tile.drawTile();
// check each tile for hits
if(tilePool.isTargetHit(cat.getX(), cat.getY(), tile)) {
scoreboard.increase();
tilePool.markHit(tile);
}
if(tilePool.isHoleHit(cat.getX(), cat.getY(), tile)) {
livesBoard.reduce();
tilePool.markHit(tile);
}
tile.setY(tile.getY() + speed); // move tiles
}
}
tilePool.recycleTiles(height);
// cat
lights();
cat.drawCat();
popMatrix(); // POP MATRIX
// boards
fill(255);
int tileSize = tilePool.getTileSize();
// score
textSize(tileSize/2.5);
text(scoreboard.getDisplayString(), 10, tileSize/2);
// lives
text(String.format(livesBoard.getDisplayString()), width-(tileSize*1.5), tileSize/2);
}
void mouseMoved() {
cat.setX(mouseX);
cat.setY(mouseY);
}
Cat class
public class Cat {
private int x;
private int y;
private final int catColour = color(255, 200, 0);
private final int catSize;
private final int tailLength;
private final int tailThickness;
private final float[][] tailSegments;
// single-argument constructor
public Cat(int catSize) {
this.x = 0;
this.y = 0;
this.catSize = catSize;
this.tailLength = catSize/4;
this.tailThickness = catSize/7;
// tail segments
this.tailSegments = new float[tailLength][2];
}
public void drawCat() {
pushMatrix(); // start cat
drawBody();
drawHead();
drawEars();
drawTail();
popMatrix(); // end cat
}
private void drawBody() {
// draw body in separate matrix so can rotate
pushMatrix();
noStroke();
translate(this.x, this.y);
rotateX(frameCount/20.0); // roll
sphereDetail(7);
fill(this.catColour);
sphere(this.catSize * 0.4) ;
popMatrix();
}
private void drawHead() {
noStroke();
translate(this.x, this.y - (this.catSize/2));
sphereDetail(8);
sphere(this.catSize * 0.2);
}
private void drawEars() {
// left
pushMatrix();
translate(-this.catSize*0.1, -this.catSize*0.05, this.catSize*0.15);
rotateY(0.5);
rotateX(4);
sphereDetail(1);
sphere(this.catSize*0.1);
popMatrix();
// right
pushMatrix();
translate(this.catSize*0.1, -this.catSize*0.05, this.catSize*0.15);
rotateY(3);
rotateX(2);
sphereDetail(1);
sphere(this.catSize*0.1);
popMatrix();
}
private void drawTail() {
sphereDetail(8);
// tail end
tailSegments[this.tailLength-1][0] = (sin(frameCount * 0.2) * 10);
tailSegments[this.tailLength-1][1] = this.catSize/2;
// rest of tail
for (int i = 0; i < tailLength-1; i++) {
this.tailSegments[i][0] = this.tailSegments[i+1][0];
this.tailSegments[i][1] = this.tailSegments[i+1][1] + (this.tailThickness/2);
pushMatrix();
translate(this.tailSegments[i][0], this.tailSegments[i][1]);
sphere(this.tailThickness);
popMatrix();
}
}
public void setX(int x) {
this.x = x;
}
public void setY(int y) {
this.y = y;
}
public int getX() {
return this.x;
}
public int getY() {
return this.y;
}
}
Tile class
public class Tile {
public static final int STANDARD_TILE = 0;
public static final int HOLE_TILE = 1;
public static final int HOLE_HIT_TILE = 2;
public static final int TARGET_TILE = 3;
private final int standardTileColour = color(255); //white
private final int holeTileColour = color(0); // black
private final int holeHitTileColour = color(255, 0, 0); // red
private final int targetTileColour = color(255, 255, 0); // yellow
private int x;
private int y;
private int tileSize;
private int tileType;
public Tile(int x, int y, int tileSize, int tileType) {
this.x = x;
this.y = y;
this.tileSize = tileSize;
this.tileType = tileType;
}
public void drawTile() {
stroke(0); // black lines
fill(standardTileColour); // default tile colour
if (this.tileType == HOLE_TILE) {
fill(holeTileColour);
}
if (this.tileType == HOLE_HIT_TILE) {
fill(holeHitTileColour);
}
if (this.tileType == TARGET_TILE) {
fill(targetTileColour);
}
square(this.x, this.y, tileSize);
}
public void setY(int y) {
this.y = y;
}
public void moveByY(int moveAmount) {
this.y += moveAmount;
}
public int getX() {
return this.x;
}
public int getY() {
return this.y;
}
public int getTileSize() {
return this.tileSize;
}
public int getTileType() {
return this.tileType;
}
public void setTileType(int tileType) {
this.tileType = tileType;
}
}
TilePool class
public class TilePool {
private ArrayList<Tile> tiles;
private int tileSize;
public void createTiles(int height, int width, int numberOfColumns) {
this.tiles = new ArrayList(); // start with clean array list
this.tileSize = width/numberOfColumns/2;
int numberOfRows = (height/tileSize) * 2;
int tileXOffset = (width-tileSize * numberOfColumns)/2;
// initialise tiles
for (int i = 0; i < numberOfRows; i++) {
// set y coordinates
int y = -i * tileSize;
//int y = -i * this.tileSize;
for (int j = 0; j < numberOfColumns; j++) {
// set x coordinate
int x = j * tileSize + tileXOffset;
tiles.add(new Tile(x, y, tileSize, getRandomTileType()));
}
}
}
private boolean isHit(int x, int y, Tile tile) {
if (y >= tile.getY() && y <= tile.getY() + tile.getTileSize() && x >= tile.getX() && x <= tile.getX() + tile.getTileSize()) {
return true;
}
return false;
}
public boolean isTargetHit(int x, int y, Tile tile) {
// immediately reply if not a target tile
if (tile.getTileType() != Tile.TARGET_TILE) {
return false;
}
return isHit(x, y, tile);
}
public boolean isHoleHit(int x, int y, Tile tile) {
// immediately reply if not a target tile
if (tile.getTileType() != Tile.HOLE_TILE) {
return false;
}
return isHit(x, y, tile);
}
public void markHit(Tile tile) {
if(tile.getTileType() == Tile.TARGET_TILE) {
tile.setTileType(Tile.STANDARD_TILE);
} else if (tile.getTileType() == Tile.HOLE_TILE) {
tile.setTileType(Tile.HOLE_HIT_TILE);
}
}
// recycle tiles off the screen
public void recycleTiles(int height) {
// get y coord of last tile
int y = 0;
for (Tile tile : tiles) {
if (tile.getY() < y) {
y = tile.getY();
}
}
// moves tiles to the end (x coord can stay the same)
int newY = y - this.tileSize;
for (Tile tile : tiles) {
// if off the screen
if (tile.getY() > height) {
tile.setY(newY);
tile.setTileType(getRandomTileType());
}
}
}
// generate a random type of tile
private int getRandomTileType() {
int tileType = Tile.STANDARD_TILE;
// randomised tile chances
if (random(0, 5) > 4) {
tileType = Tile.HOLE_TILE;
}
if (random(0, 50) > 45) {
tileType = Tile.TARGET_TILE;
}
return tileType;
}
public ArrayList<Tile> getTiles() {
return this.tiles;
}
public int getTileSize() {
return this.tileSize;
}
}
DisplayBoard class
public abstract class DisplayBoard {
private int value;
public void setValue(int value) {
this.value = value;
}
public int getValue() {
return this.value;
}
public void increase() {
this.value += 1;
}
public void reduce() {
this.value -= 1;
}
public void resetValue() {
this.value = 0;
}
public abstract String getDisplayString();
}
Scoreboard class
public class Scoreboard extends DisplayBoard {
@Override
public String getDisplayString() {
return String.format("Score: %1s", getValue());
}
}
LivesBoard class
public class LivesBoard extends DisplayBoard {
@Override
public String getDisplayString() {
return String.format("Lives: %1s", getValue());
}
}
