Home CPSC 220

Graphics Programming

 

2D Graphics

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

The Graphics class is abstract, is not created directly. Instead, we create a class that extends the JComponent class. This allows objects of this class to be added to a GUI program.

If that class overrides the "paintComponent" method, then that code will be used to draw the component to the window.

The following example is a simple 2D graphics program which demonstrates this:


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

// the gameworld is a component, so it can be added to a GUI program
class GameWorld extends JComponent {
    @Override
    // overriding this method changes how it is shown
    public void paintComponent(Graphics g) {
        // just draw a black line on the window
        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 Graphics object passed to paintComponent can be used to draw various 2D shapes. This example uses it to draw a single black line.

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 thing to do 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.

Each snow flake is represented as an object with a position and speed. Each snow flake is drawn to the screen and updates so it moves:


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

// a snow flake is a simple game object which can move around and be drawn
class Flake {
    // snow flakes have a position and speed
    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 {
    // we keep an array of snow flake objects
    private Flake snow [];

    // initialize the array
    public GameWorld(int count) {
        snow = new Flake [count];
        for (int i = 0; i < count; i++) {
            snow[i] = new Flake();
        }
    }

    // draw the component
    @Override
    public void paintComponent(Graphics g) {
        // set the color to light blue and draw a rectangle
        g.setColor(new Color(100, 150, 255));
        g.fillRect(0, 0, 500, 500);

        // draw each of the snow flakes to the screen
        g.setColor(Color.white);
        for (Flake f : snow) {
            f.draw(g);
        }

        // update each snow flake
        for (Flake f : snow) {
            f.update();
        }

        // force an update - because the snow flakes have moved
        revalidate();
        repaint();

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

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(100));

        // 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.

Because the GameWorld object changes, it has to tell Swing that it needs to be repainted. This is done by calling both "revalidate" and "repaint". Many types of GUI elements are static and so the default in Swing is to not redraw anything. Games and other graphics programs need to be redrawn more often!

We also call the Thread.sleep() function to slow the program down. This takes a number of milliseconds as a parameter and pauses for that long. It might throw and InterruptedException which has to be caught.


 

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.*;

// a snow flake is a simple game object which can move around and be drawn
class Flake {
    // position and speed are now doubles
    private double x, y, dx, dy;

    // position the snow flake randomly
    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;
    }

    // draw the snow flake at its position casted as an int
    public void draw(Graphics g) {
        g.fillRect((int) x, (int) y, 3, 3);
    }

    // update takes the seconds since the last update
    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 {
    // the array of snow flakes
    private Flake snow[];
    
    // the time the last time the update runs
    private long last_time;

    // build the game world with a certain number of flakes
    public GameWorld(int count) {
        // store the current time
        last_time = new Date().getTime();
        
        // init snow flakes
        snow = new Flake[count];
        for (int i = 0; i < count; i++) {
            snow[i] = new Flake();
        }
    }

    @Override
    public void paintComponent(Graphics g) {
        // set the color to light blue and draw the sky
        g.setColor(new Color(100, 150, 255));
        g.fillRect(0, 0, 500, 500);
        
        // draw each of the snow flakes to the screen
        g.setColor(Color.white);
        for (Flake f : snow) {
            f.draw(g);
        }

        // calculate the number of seconds since the last update
        long time_now = new Date().getTime();
        long elapsed_ms = time_now - last_time;
        double dt = elapsed_ms / 1000.0;
        
        // update each flake with this elapsed time
        for (Flake f : snow) {
            f.update(dt);
            
        }
        
        // save the current time as the last time
        last_time = time_now;

        // force an update
        revalidate();
        repaint();
    }
}

public class SmoothSnow {

    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(1000));

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

This uses doubles instead of ints for the flakes position and speed. The flakes update function takes the number of seconds that have elapsed as a parameter. This is used in calculating the flakes new positions.

The Date class has a method called "getTime" which returns the number of milliseconds since January 1st 1970. This is not likely useful in its own right, but the difference between two of these values gives us the number of elapsed milliseconds.

This is used to find the number of seconds elapsed and to update all of the snow flakes.

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


 

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 {
    // store an image
    private Image mario;

    // load the image from a file
    GameWorld() {
        try {
            mario = ImageIO.read(new File("mario.png"));
        } catch (Exception e) {
            // if not found, set it to null
            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 Mario {
    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 mario.png file.

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

Copyright © 2024 Ian Finlayson | Licensed under a Creative Commons BY-NC-SA 4.0 License.