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