Quantcast
Channel: Dark Views » Games
Viewing all articles
Browse latest Browse all 21

Writing Games with Processing: Cleaning Up

$
0
0

While the code for our game is simple enough, it’s starting to get messy. movePlayer() contains code to move the player, the enemies and the collision detection.

Also, the game ends when you win. There is no way to restart. I could add that but it would make the code … ugly. Time to refactor.

Refactoring, in a nutshell, means to change the internal structure of the code without changing the behavior.

Sounds futile? Nope. The goal of refactoring to make the code easier to maintain and extend.

Right now, the some code related to enemies is in the Enemy class but code for the player is spread all over the place. Time to create a Player class.

class Player {
  int x = 320, y = 240;

  void draw() {
    fill(235,220,160);
    rect(x, y, tileSize, tileSize);
    
    fill(0);
    textSize(tileSize-2);
    textAlign(CENTER, CENTER);
    text("P", x+tileSize/2, y+tileSize/2-2);
  }
  
  void move(int dx, int dy) {
    dx *= tileSize;
    dy *= tileSize;
    
    int newX = x + dx;
    int newY = y + dy;
    if(newX >= 0
      && newX < width
      && newY >= 0
      && newY < height
    ) {
      x = newX;
      y = newY;
    }
  }
}

That looks much better. Next, we’ll have several states that the game can be in: Intro, game running, game over, high score.

They all have similar properties: They respond to keys and they draw something on the screen. With the current code, that would be clumsy to implement.

So I add a new class:

class State {
  void setup() {}
  void draw() {}
  void keyPressed() {}
}

Now, I can move all the code to run the game into a new class MainGame which extends State:

class MainGame extends State {
  ArrayList<Enemy> enemies = new ArrayList<Enemy>();
  Player player;
  
  void addEnemy(int x, int y, String name) {
    Enemy enemy = new Enemy(name);
    enemy.x = x;
    enemy.y = y;
    enemies.add(enemy);
  }
  
  void drawEnemies() {
   for(Enemy e: enemies) {
      e.draw();
    }
  }

  void setup() {
    player = new Player();
  
    addEnemy(20, 20, "Kenny");
    addEnemy(600, 20, "Benny");
  }

  void drawBackground() {
    fill(174, 204, 27);
    rect(0, 0, width, height);
      
    fill(0);
    textAlign(LEFT, TOP);
    textSize(20);
    text("Scrore: "+score, 2, 2);
  }
  
  void draw() {
    drawBackground();
    player.draw();
    drawEnemies();
  }
  
  void moveEnemies() {
    for(Enemy e: enemies) {
      e.hunt(player.x, player.y);
    }
  }
  
  void checkCollisions() {
    ArrayList<Enemy> enemiesToCheck = new ArrayList<Enemy>();
    
    for(Enemy e: enemies) {
      if(e.closeTo(player.x, player.y)) {
        gameOver(e);
        return;
      }
      
      for(Enemy e2: enemiesToCheck) {
        if(e.closeTo(e2.x, e2.y)) {
          e.fighting = true;
          e2.fighting = true;
        }
      }
      
      enemiesToCheck.add(e);
    }
    
    int notFighting = 0;
    for(Enemy e: enemies) {
      if(!e.fighting) {
        notFighting ++;
      }
    }
    
    score ++;
    if(notFighting == 0) {
      youWon(enemies.size());
    }
  }
  
  void movePlayer(int dx, int dy) {
    player.move(dx, dy);
    moveEnemies();
    checkCollisions();
  }
  
  void keyPressed() {
    if(key == CODED) {
      if(keyCode == UP) {
        movePlayer(0, -1);
      } else if(keyCode == DOWN) {
        movePlayer(0, 1);
      } else if(keyCode == LEFT) {
        movePlayer(-1, 0);
      } else if(keyCode == RIGHT) {
        movePlayer(1, 0);
      }
    } else if(key == ' ') {
      movePlayer(0, 0);
    }  
  }  
}

Next, we can move the code for “game over” into a new GameOverWonState and GameOverLostState:

class GameOverWonState extends State {
  MainGame main;
  int numberOfEnemies;
  
  GameOverWonState(MainGame main, int numberOfEnemies) {
    this.main = main;
    this.numberOfEnemies = numberOfEnemies;
  }
  
  void draw() {
    main.draw();
  
    textAlign(CENTER, TOP);
    textSize(40);
    color outline = color(255,255,255);
    color fill = color(255,0,0);
    textWithOutline("YOU WON!", width/2, 200, outline, fill);
    
    textSize(20);
    textWithOutline("The cute platypus outsmarted "+numberOfEnemies+" crocodiles!", width/2, 240, outline, fill);    
  }
}

class GameOverLostState extends State {
  MainGame main;
  Enemy e;
  
  GameOverLostState(MainGame main, Enemy e) {
    this.main = main;
    this.e = e;
  }
  
  void draw() {
    main.draw();
  
    textAlign(CENTER, TOP);
    textSize(40);
    color outline = color(255,255,255);
    color fill = color(255,0,0);
    textWithOutline("GAME OVER", width/2, 200, outline, fill);
    
    textSize(20);
    textWithOutline("The poor platypus was eaten by "+e.name+"!", width/2, 240, outline, fill);
  }
}

While this is much more code than before, it has the advantage that I don’t have to stop the main loop anymore. Which means I can add a nice death or victory animation with little effort.

More importantly, I can (after another refactoring) restart the game:

class GameOverState extends State {
  MainGame main;
  String mainMessage;
  String subMessage;
  
  GameOverState(MainGame main, String mainMessage, String subMessage) {
    this.main = main;
    this.mainMessage = mainMessage;
    this.subMessage = subMessage;
  }

  void draw() {
    main.draw();
  
    textAlign(CENTER, TOP);
    textSize(40);
    color outline = color(255,255,255);
    color fill = color(255,0,0);
    textWithOutline(mainMessage, width/2, 200, outline, fill);
    
    textSize(20);
    textWithOutline(subMessage, width/2, 240, outline, fill);
    
    color outline = color(0,0,0);
    color fill = color(255,255,255);

    textSize(20);
    textWithOutline("Try again (Y/N)?", width/2, 280, outline, fill);
  }
  
  void keyPressed() {
    if(key == 'y' || key == 'Y') {
      current = new MainGame();
      current.setup();
    } else if(key == 'n' || key == 'N') {
      exit();
    }
  } 
}

class GameOverWonState extends GameOverState {
  GameOverWonState(MainGame main, int numberOfEnemies) {
    super(main, "YOU WON!", "The cute platypus outsmarted "+numberOfEnemies+" crocodiles!");
  }
}

class GameOverLostState extends GameOverState {
  GameOverLostState(MainGame main, Enemy e) {
    super(main, "GAME OVER", "The poor platypus was eaten by "+e.name+"!");
  }
}

To move to these states, I implement two new methods in MainGame:

  void gameOver(Enemy e) {
    current = new GameOverLostState(this, e);
  }
  
  void youWon() {
    current = new GameOverWonState(this, enemies.size()); 
  }

While trying this version, I noticed that I forgot to reset the score. Actually, the scoring is part of the main game loop, so I can move all the code related to it into MainGame.

Also, I noticed that I sometimes forgot to call setup() when changing states. A new method fixes all this:

void changeState(State next) {
  current = next;
  current.setup();
}

To clean up things even more, I’m splitting the code into several tabs. Since the project has now reached a certain complexity, I’m moving to version control with Git. If you don’t know what Git or distribute version control is, I suggest to read this article. Or you can try it online.

The whole project can be cloned from here. The version of this blog post is tagged with v8.

With the new cleaned up code, we can easily add an intro screen.

Previous: Highscore!
First post: Getting Started


Tagged: Game, Games, Processing

Viewing all articles
Browse latest Browse all 21

Trending Articles