Home CPSC 330

Game Programming 1

Introduction

Game programming is a demanding area of software development. Many games need:

The combination of these facets makes game development challenging.


Game Programming Basics

Games typically are structured differently from other applications. Rather than run a set procedure from start to end, or wait passively for user events, games are usually driven by a game loop:


while(running) {
  // get input from user and/or network
  
  // run AI

  // move game objects

  // check for collisions
  
  // draw draw game objects

  // play sounds
}

Each iteration through this loop is called a "frame".


2D Graphics

2D graphics in Java can be displayed using the Graphichs class.

The Graphics class is abstract, and cannot be created. Instead, we create a class that extends JComponent, and override the paintComponent method.

The following example is a simple 2D graphics program:


import javax.swing.*;
import java.awt.*;

class GameWorld extends JComponent {
  public void paintComponent(Graphics g) {
    g.setColor(Color.black);
    g.drawLine(0, 0, 100, 100);
  }
}

public class Simple {
  public static void main(String args[]) {
    // create and set up the window.
    JFrame frame = new JFrame("Graphics Example!");

    // make the program close when the window closes
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    // add the GameWorld component
    frame.add(new GameWorld());

    // display the window.
    frame.setSize(500, 500);
    frame.setVisible(true);
  }    
}

The coordinates start in the upper left hand corner at (0, 0). They increase to the bottom (Y) and right (X).


Game Objects

A typical pattern in game programming is to contain a list of "game objects" where each one keeps some state, can be drawn to the screen, and can be updated.

As a simple example, the following code displays a simple snow scene.


import javax.swing.*;
import java.awt.*;
import java.util.*;

class Flake {
  private int x, y, dx, dy;

  public Flake() {
    Random r = new Random();
    x = r.nextInt(500);
    y = r.nextInt(500);
    dx = r.nextInt(3) - 1;
    dy = r.nextInt(3) + 3;
  }

  public void draw(Graphics g) {
    g.fillRect(x, y, 3, 3);
  }

  public void update() {
    x += dx;
    y += dy;

    if(y < 0) y = 500;
    if(y > 500) y = 0;
    if(x < 0) x = 500;
    if(x > 500) x = 0;
  }
}


class GameWorld extends JComponent {
  private ArrayList<Flake> snow;

  public GameWorld() {
    snow = new ArrayList<Flake>();
    for(int i = 0; i < 100; i++) {
      snow.add(new Flake());
    }
  }

  public void paintComponent(Graphics g) {
    /* set the color to light blue */
    g.setColor(new Color(100, 150, 255));
    g.fillRect(0, 0, 500, 500);

    g.setColor(Color.white);
    for(Flake f : snow) {
      f.draw(g);
    }

    /* now update */
    for(Flake f : snow) {
      f.update();
    }
    
    /* force an update */
    revalidate();
    repaint();

    /* sleep for 1/20th of a second */
    try {
      Thread.sleep(50);
    } catch(InterruptedException e) {
      Thread.currentThread().interrupt();
    }
  }
}


public class Snow {
  public static void main(String args[]) {
    // create and set up the window.
    JFrame frame = new JFrame("Snow!");

    // make the program close when the window closes
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    // add the GameWorld component
    frame.add(new GameWorld());

    // display the window.
    frame.setSize(500, 500);
    frame.setVisible(true);
  }    
}

Each snow flake tracks its position, can update its position, and draw itself to the screen.

If, as in most real games, there are multiple types of game objects, they can be put in an inheritance hierarchy.


Framerate Independent Movement

There is one problem in the above program, which is that it will run at different speeds based on how fast the user's computer is. We would like to have it run at the same speed no matter what.

One way to do this is to keep track of the elapsed time since the last frame, and use that to decide how far to move the snow flakes. That is done in the following example:


import javax.swing.*;
import java.awt.*;
import java.util.*;

class Flake {
  private double x, y, dx, dy;

  public Flake() {
    Random r = new Random();
    x = r.nextFloat() * 500;
    y = r.nextFloat() * 500;

    /* these are now pixels / second instead of pixels per frame */
    dx = r.nextFloat()*50 - 25;
    dy = r.nextFloat()*50 + 100;
  }

  public void draw(Graphics g) {
    g.fillRect((int)x, (int)y, 3, 3);
  }

  public void update(double dt) {
    x += (dx * dt);
    y += (dy * dt);

    if(y < 0) y = 500;
    if(y > 500) y = 0;
    if(x < 0) x = 500;
    if(x > 500) x = 0;
  }
}


class GameWorld extends JComponent {
  private ArrayList<Flake> snow;
  private long elapsed;

  public GameWorld() {
    elapsed = new Date().getTime();
    snow = new ArrayList<Flake>();
    for(int i = 0; i < 100; i++) {
      snow.add(new Flake());
    }
  }

  public void paintComponent(Graphics g) {
    /* set the color to light blue */
    g.setColor(new Color(100, 150, 255));
    g.fillRect(0, 0, 500, 500);

    g.setColor(Color.white);
    for(Flake f : snow) {
      f.draw(g);
    }

    /* now update */
    long time_now = new Date().getTime();
    double seconds = (time_now - elapsed) / 1000.0f;
    elapsed = time_now;
    System.out.println(seconds);
    for(Flake f : snow) {
      f.update(seconds);
    }
    
    /* force an update */
    revalidate();
    repaint();
    /* sleep for 1/20th of a second */
    try {
      Thread.sleep(50);
    } catch(InterruptedException e) {
      Thread.currentThread().interrupt();
    }
  }
}


public class Snow2 {
  public static void main(String args[]) {
    // create and set up the window.
    JFrame frame = new JFrame("Snow!");

    // make the program close when the window closes
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    // add the GameWorld component
    frame.add(new GameWorld());

    // display the window.
    frame.setSize(500, 500);
    frame.setVisible(true);
  }    
}

This program might run more or less smoothly on different computers, but the snow will fall at the same rate.


Loading Images

While drawing basic geometric shapes is fun, most games use images loaded from files. Java's graphics library supports this too as the following example shows:


import javax.swing.*;
import javax.imageio.*;
import java.awt.*;
import java.io.*;

class GameWorld extends JComponent {
  private Image mario;

  GameWorld() {
    try {
      mario  = ImageIO.read(new File("r1.png"));
    } catch(Exception e) {
      mario = null;
    }
  }

  public void paintComponent(Graphics g) {
    /* draw mario on the screen
     * the x and y coordinates refer to the upper left-hand corner
     * the null argument is an "ImageObserver" that can be used to
     * keep track of conversions done on the image */
    g.drawImage(mario, 100, 100, null);
  }
}

public class ImageExample {
  public static void main(String args[]) {
    // create and set up the window.
    JFrame frame = new JFrame("Graphics Example!");

    // make the program close when the window closes
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    // add the GameWorld component
    frame.add(new GameWorld());

    // display the window.
    frame.setSize(500, 500);
    frame.setVisible(true);
  }    
}

For this example, you'll need the r1.png file.

By loading and displaying multiple images, we can build complex scenes.


User Input

Kayboard input can be done for graphical components as well. To do this, we must create a class that implements the KeyListener interface.

This interface contains methods that are called when keys are pressed, released, or "typed". The KeyListener object must be added to a frame using the addKeyListener method.

This example shows how this can be done:


import javax.swing.*;
import javax.imageio.*;
import java.awt.*;
import java.io.*;
import java.awt.event.*;
import java.util.*;

class Character {
  private static final double SPEED = 50.0;
  private Image mario;
  private double x, y, dx, dy;

  public Character() {
    try {
      mario  = ImageIO.read(new File("r1.png"));
    } catch(Exception e) {
      mario = null;
    }

    x = y = 100;
    dx = dy = 0;
  }

  public void draw(Graphics g) {
    /* draw mario on the screen */
    g.drawImage(mario, (int)x, (int)y, null);
  }

  /* stop mario */
  public void stop() {
    dx = dy = 0;
  }

  /* left/up/right/down */
  public void left() {dx = -SPEED;}
  public void up() {dy = -SPEED;}
  public void right() {dx = SPEED;}
  public void down() {dy = SPEED;}

  /* update him */
  public void update(double dt) {
    x += (dx * dt);
    y += (dy * dt);

    if(y < 0) y = 500;
    if(y > 500) y = 0;
    if(x < 0) x = 500;
    if(x > 500) x = 0;
  }
}

class GameWorld extends JComponent implements KeyListener {
  private Character mario;
  private long elapsed;

  public GameWorld() {
    elapsed = new Date().getTime();
    mario = new Character();
  }

  public void keyTyped(KeyEvent e) {
    /* don't need to do anything */
  }

  public void keyPressed(KeyEvent e) {
    if (e.getKeyCode() == KeyEvent.VK_RIGHT ) {
      mario.right();
    } else if (e.getKeyCode() == KeyEvent.VK_LEFT ) {
      mario.left();
    } else if (e.getKeyCode() == KeyEvent.VK_UP ) {
      mario.up();
    } else if (e.getKeyCode() == KeyEvent.VK_DOWN ) {
      mario.down();
    }
  }

  public void keyReleased(KeyEvent e) {
    mario.stop();
  }

  public void paintComponent(Graphics g) {
    mario.draw(g);

    /* now update */
    long time_now = new Date().getTime();
    double seconds = (time_now - elapsed) / 1000.0f;
    elapsed = time_now;
    mario.update(seconds);

    /* force an update */
    revalidate();
    repaint();
    /* sleep for 1/20th of a second */
    try {
      Thread.sleep(50);
    } catch(InterruptedException e) {
      Thread.currentThread().interrupt();
    }
  }
}

public class InputExample {
  public static void main(String args[]) {
    // create and set up the window.
    JFrame frame = new JFrame("Graphics Example!");

    // make the program close when the window closes
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    // add the GameWorld component
    GameWorld g = new GameWorld();
    frame.add(g);
    frame.addKeyListener(g);

    // display the window.
    frame.setSize(500, 500);
    frame.setVisible(true);
  }    
}

Animation

Mario doesn't look that good moving across the screen standing still. To make him look like he's running, we can create a simple animation by flipping between multiple images.

To make this work, we need to keep track of a few things:

Sometimes a single image is used, but we read from different portions of it. Images with several different images or frames in them are called sprite sheets.

This example shows a simple version of an animated sprite:


import javax.swing.*;
import javax.imageio.*;
import java.awt.*;
import java.io.*;
import java.awt.event.*;
import java.util.*;

class Character {
  private static final double SPEED = 50.0;
  private double x, y, dx, dy;

  /* now we have an array of images */
  private Image [] mario;
  private int current = 0;
  private boolean right = true;
  private boolean moving = false;

  /* time between flips in the animation */
  private static final double FLIP_TIME = 0.125;

  /* time since last flip */
  private double timer = 0.0;


  public Character() {
    /* load all the images */
    try {
      mario = new Image[4];
      mario[0]  = ImageIO.read(new File("r1.png"));
      mario[1]  = ImageIO.read(new File("r2.png"));
      mario[2]  = ImageIO.read(new File("l1.png"));
      mario[3]  = ImageIO.read(new File("l2.png"));
    } catch(Exception e) {
      mario = null;
    }

    x = y = 100;
    dx = dy = 0;
  }

  public void draw(Graphics g) {
    /* add to the index if going left */
    int add = 0;
    if(!right) add = 2;
    
    /* draw mario on the screen */
    g.drawImage(mario[current + add], (int)x, (int)y, null);
  }

  /* stop mario */
  public void stop() {
    dx = dy = 0;
    moving = false;
  }

  /* left/up/right/down */
  public void left() {moving = true; right = false; dx = -SPEED;}
  public void up() {moving = true; dy = -SPEED;}
  public void right() {moving = true; right = true; dx = SPEED;}
  public void down() {moving = true; dy = SPEED;}

  /* update him */
  public void update(double dt) {
    x += (dx * dt);
    y += (dy * dt);

    if(y < 0) y = 500;
    if(y > 500) y = 0;
    if(x < 0) x = 500;
    if(x > 500) x = 0;

    /* update animation */
    if(moving) {
      timer += dt;
      if(timer > FLIP_TIME) {
        timer = 0;
        current = (current + 1) % 2;
      }
    }
  }
}


class GameWorld extends JComponent implements KeyListener {
  private Character mario;
  private long elapsed;

  public GameWorld() {
    elapsed = new Date().getTime();
    mario = new Character();
  }

  public void keyTyped(KeyEvent e) {
    /* don't need to do anything */
  }

  public void keyPressed(KeyEvent e) {
    if (e.getKeyCode() == KeyEvent.VK_RIGHT ) {
      mario.right();
    } else if (e.getKeyCode() == KeyEvent.VK_LEFT ) {
      mario.left();
    } else if (e.getKeyCode() == KeyEvent.VK_UP ) {
      mario.up();
    } else if (e.getKeyCode() == KeyEvent.VK_DOWN ) {
      mario.down();
    }
  }

  public void keyReleased(KeyEvent e) {
    mario.stop();
  }

  public void paintComponent(Graphics g) {
    mario.draw(g);

    /* now update */
    long time_now = new Date().getTime();
    double seconds = (time_now - elapsed) / 1000.0f;
    elapsed = time_now;
    mario.update(seconds);

    /* force an update */
    revalidate();
    repaint();
    /* sleep for 1/20th of a second */
    try {
      Thread.sleep(50);
    } catch(InterruptedException e) {
      Thread.currentThread().interrupt();
    }
  }
}

public class Animation {
  public static void main(String args[]) {
    // create and set up the window.
    JFrame frame = new JFrame("Graphics Example!");

    // make the program close when the window closes
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    // add the GameWorld component
    GameWorld g = new GameWorld();
    frame.add(g);
    frame.addKeyListener(g);

    // display the window.
    frame.setSize(500, 500);
    frame.setVisible(true);
  }    
}

Collision Detection

Collision detection depends greatly on the types of objects in question. Typically doing collision detection on complex objects is too expensive, so approximations are done. For example, bounding boxes or bounding circles are used in 2D games, and bounding spheres or volumes are used.

This example does collision detection on circles.


import javax.swing.*;
import java.awt.*;
import java.util.*;

class Ball {
  private double x, y, dx, dy;
  private static final int DIAMETER = 25;
  private Color color;

  public Ball() {
    Random r = new Random();
    x = r.nextFloat() * 500;
    y = r.nextFloat() * 500;

    /* these are now pixels / second instead of pixels per frame */
    dx = r.nextFloat()*300 - 150;
    dy = r.nextFloat()*300 -150;

    color = new Color(r.nextInt(256), r.nextInt(256), r.nextInt(256));
  }

  public void draw(Graphics g) {
    g.setColor(color);
    g.fillOval((int)x, (int)y, DIAMETER, DIAMETER);
  }

  boolean collides(Ball other) {
    /* if it's the same ball, it didn't "collide */
    if(this == other) {
      return false;
    }

    /* find the center point of each */
    double cx = x + DIAMETER / 2;
    double cy = y + DIAMETER / 2;
    double ox = other.x + DIAMETER / 2;
    double oy = other.y + DIAMETER / 2;

    /* find the distance between the center points */
    double dist = Math.sqrt((cx - ox)*(cx - ox) + (cy - oy)*(cy - oy));

    /* check if the distance is less than the diameter */
    if(dist < DIAMETER) {
      return true;
    } else {
      return false;
    }
  }

  public void update(double dt, ArrayList<Ball> balls) {
    /* update the position */
    x += (dx * dt);
    y += (dy * dt);

    /* check collisions with the walls */
    if(y < 0) dy = Math.abs(dy);
    if((y + DIAMETER) > 500) dy = -Math.abs(dy);
    if(x < 0) dx = Math.abs(dx);
    if((x + DIAMETER) > 500) dx = -Math.abs(dx);

    /* check if we hit another ball */
    for(Ball b : balls) {
      if(collides(b)) {
        /* reverse position */
        dx *= -1;
        dy *= -1;

        /* change color */
        Random r = new Random();
        color = new Color(r.nextInt(256), r.nextInt(256), r.nextInt(256));
      }
    }
  }
}


class GameWorld extends JComponent {
  private ArrayList<Ball> balls;
  private long elapsed;

  public GameWorld() {
    elapsed = new Date().getTime();
    balls = new ArrayList<Ball>();
    for(int i = 0; i < 10; i++) {
      balls.add(new Ball());
    }
  }

  public void paintComponent(Graphics g) {
    /* set the color to light blue */
    g.setColor(Color.black);
    g.fillRect(0, 0, 500, 500);

    g.setColor(Color.white);
    for(Ball f : balls) {
      f.draw(g);
    }

    /* now update */
    long time_now = new Date().getTime();
    double seconds = (time_now - elapsed) / 1000.0f;
    elapsed = time_now;
    for(Ball f : balls) {
      f.update(seconds, balls);
    }
    
    /* force an update */
    revalidate();
    repaint();
  }
}


public class Collisions {
  public static void main(String args[]) {
    // create and set up the window.
    JFrame frame = new JFrame("Collisions!");

    // make the program close when the window closes
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    // add the GameWorld component
    frame.add(new GameWorld());

    // display the window.
    frame.setSize(510, 530);
    frame.setVisible(true);
  }    
}

It is important to not check for collisions between an object and itself!

Games with large numbers of objects cannot do pairwise collision detection. Instead they use optimizations to not compare objects that are far apart etc.


Playing Sounds

Sound can add a huge dimension to a game, but is quite tricky to get right.

The example below shows how we can play a sound file whenever the balls collide:


import javax.swing.*;
import java.awt.*;
import java.util.*;
import javax.sound.sampled.*;
import java.io.*;

class Sound {
  public static void playSound(String name) {
    try {
      Clip clip = AudioSystem.getClip();
      AudioInputStream is = AudioSystem.getAudioInputStream(new File(name));
      clip.open(is);
      clip.start();
    } catch(Exception e) {
    }
  }
}

class Ball {
  private double x, y, dx, dy;
  private static final int DIAMETER = 25;
  private Color color;

  public Ball() {
    Random r = new Random();
    x = r.nextFloat() * 500;
    y = r.nextFloat() * 500;

    /* these are now pixels / second instead of pixels per frame */
    dx = r.nextFloat()*300 - 150;
    dy = r.nextFloat()*300 -150;

    color = new Color(r.nextInt(256), r.nextInt(256), r.nextInt(256));
  }

  public void draw(Graphics g) {
    g.setColor(color);
    g.fillOval((int)x, (int)y, DIAMETER, DIAMETER);
  }

  boolean collides(Ball other) {
    /* if it's the same ball, it didn't "collide */
    if(this == other) {
      return false;
    }

    /* find the center point of each */
    double cx = x + DIAMETER / 2;
    double cy = y + DIAMETER / 2;
    double ox = other.x + DIAMETER / 2;
    double oy = other.y + DIAMETER / 2;

    /* find the distance between the center points */
    double dist = Math.sqrt((cx - ox)*(cx - ox) + (cy - oy)*(cy - oy));

    /* check if the distance is less than the diameter */
    if(dist < DIAMETER) {
      return true;
    } else {
      return false;
    }
  }

  public void update(double dt, ArrayList<Ball> balls) {
    /* update the position */
    x += (dx * dt);
    y += (dy * dt);

    /* check collisions with the walls */
    if(y < 0) dy = Math.abs(dy);
    if((y + DIAMETER) > 500) dy = -Math.abs(dy);
    if(x < 0) dx = Math.abs(dx);
    if((x + DIAMETER) > 500) dx = -Math.abs(dx);

    /* check if we hit another ball */
    for(Ball b : balls) {
      if(collides(b)) {
        /* reverse position */
        dx *= -1;
        dy *= -1;

        /* change color */
        Random r = new Random();
        color = new Color(r.nextInt(256), r.nextInt(256), r.nextInt(256));

        /* play a sound */
        Sound.playSound("sound.aiff");
      }
    }
  }
}


class GameWorld extends JComponent {
  private ArrayList<Ball> balls;
  private long elapsed;

  public GameWorld() {
    elapsed = new Date().getTime();
    balls = new ArrayList<Ball>();
    for(int i = 0; i < 10; i++) {
      balls.add(new Ball());
    }
  }

  public void paintComponent(Graphics g) {
    /* set the color to light blue */
    g.setColor(Color.black);
    g.fillRect(0, 0, 500, 500);

    g.setColor(Color.white);
    for(Ball f : balls) {
      f.draw(g);
    }

    /* now update */
    long time_now = new Date().getTime();
    double seconds = (time_now - elapsed) / 1000.0f;
    elapsed = time_now;
    for(Ball f : balls) {
      f.update(seconds, balls);
    }
    
    /* force an update */
    revalidate();
    repaint();
  }
}


public class Collisions2 {
  public static void main(String args[]) {
    // create and set up the window.
    JFrame frame = new JFrame("Collisions!");

    // make the program close when the window closes
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    // add the GameWorld component
    frame.add(new GameWorld());

    // display the window.
    frame.setSize(510, 530);
    frame.setVisible(true);
  }    
}

This code only seems to work with AIFF format files. To run this, you need a file called sound.aiff.

There are a few problems with the way sounds are handeled above. For one, the code above reloads the sound file each time it is played - which is wasteful. It also could play the file multiple times at the same time which is not a good idea.

The code below fixes both of these problems:


class Sound {
  private Clip clip;
  private AudioInputStream is;
  private long last_play = 0;

  public Sound(String name) {
    try {
      clip = AudioSystem.getClip();
      is = AudioSystem.getAudioInputStream(new File(name));
      clip.open(is);
    } catch(Exception e) {
    }
  }

  public void play() {
    long now = new Date().getTime();
    if((now - last_play) < 250) {
      return;
    }
    try {
      clip.stop();
      clip.setFramePosition(0);
      clip.start();
      last_play = now;
    } catch(Exception e) {
    }
  }
}

There are many free sound effect files available from http://www.freesound.org/.

Audacity is a good, free program for creating, editing, and converting sound files.

Copyright © 2018 Ian Finlayson | Licensed under a Creative Commons Attribution 4.0 International License.