Thursday, August 26, 2010

Fantasy RPG applet in Java, the early stages

For the past week I've been dabbling at home with building an old school Fantasy RPG, and I've been enjoying the hell out of it. It isn't playable yet by a longshot, as so far I've only got a combat framework and map scroller functional. For what the first real build of this will look like, think Bard's Tale 2. Think Ultima 4. At least, that's what I'm thinking now, as turning this into Diablo or World of Warcraft is still a long way off.

I got the itch to design a game after toying around with Google's App Engine framework. I wrote a slideshow unwrapper for my wife to use when browsing on Ebay or Etsy, which lets you paste in several URLs with slideshows in them, and builds a single page containing all the images. App Engine's "fetch url" function and the ability to execute arbitrary Java code made that a snap. While learning the App Engine ropes, I found that it can serve an applet, and receive an arbitrary serialized object and store it in Google's "data store".

In other words, you can play a game in an applet, and save the game state to the data store.

I don't think that can be extended to be an MMORPG, since keeping ports open for always-on two way communication isn't supported, but something approaching a MUD wouldn't be too far fetched.

As it turns out, Java objects extend perfectly into defining RPG concepts. Stats (strength, dex, hit points, etc.) can be an object. "Item" can be an abstract class, where Armor, Weapon, and Scroll can be concrete extensions of Item. "Action" can be abstract, with Script, Combat, and Retail as concrete extensions. Fun stuff. Here is a screenshot of an early experiment with combat where a level 3 player with a sword and shield goes to battle with a level 1 skeleton armed, sadly, with only a bone:



For displaying the player on a world map, I want to leave him centered while the world moves around him. Right now this is all simple icon-based with no animation (the first couple screenshots below show only solid color icons, the last is using the icon set from Ultima 4), but that can be extended if I ever get the game mechanics fleshed out and complete. Knowing Jack about animation, that should be fun if I ever get there.

The scrolling turned out to be an interesting puzzle. I have an image representing the world map, and superimposed on that is the player icon, it whatever position he happens to be in the world. Both of those, in turn, are placed on a larger image that is only a black background. When the larger "scene" image is displayed, the player's position is always in the center of the display pane. Here are a couple screenshots showing how that is going to work (you're the red guy):





...and the Java code that makes it happen. If you're a budding game developer, feel free to snag and extend:

package cea.applet;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Point;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.image.BufferedImage;
import javax.swing.JApplet;
import javax.swing.JPanel;

public class LApplet extends JApplet implements KeyListener {

    // Image array representing "cells" on the board
    Image[][] icons;

    // Image representing the absolute board
    Image board;

    // The player's icon
    Image player;

    // The board, plus the player at his position, plus a black background
    Image scene;

    // Where the player currently is. To cut down on multiplication, this is the absolute position
    // on the board image to display the player.
    Point playerPos;

    // How many cells per side are on our square board
    private static final int boardIconsPerSide = 26;

    // How many pixels per side are on our square icons
    private static final int iconSide = 24;

    // Number of pixels on the board's side
    private final int boardSide = boardIconsPerSide * iconSide;

    // Where the upper and left corner of the board can be found on the scene image
    private final int offset = -boardSide / 2;

    @Override
    public void init() {

        // Three solid-color icons representing different types of terrain
        Image green = getIcon(Color.GREEN);
        Image blue = getIcon(Color.BLUE);
        Image grey = getIcon(Color.GRAY);

        // Basic red icon representing the player
        player = getIcon(Color.RED);

        // Begin the player on the board's center square
        int startPos = (boardIconsPerSide / 2) * iconSide;
        playerPos = new Point(startPos, startPos);

        // Enumerate the icons at each point on the board. The positions for each color here are arbitrary
        icons = new Image[boardIconsPerSide][boardIconsPerSide];
        for (int y = 0; y < boardIconsPerSide; y++) {
            for (int x = 0; x < boardIconsPerSide; x++) {
                if (y < 3 && x < 3) icons[x][y] = blue;
                else if (y > 6 && x > 6) icons[x][y] = grey;
                else icons[x][y] = green;
            }
        }

        // Create the board image based on the above icon array
        setBoard();

        // Set up the Applet's panel and keyboard listener
        JPanel p = new JPanel();
        p.setBounds(0, 0, boardSide, boardSide);
        p.setBackground(Color.WHITE);
        p.setVisible(true);
        add(p);
        addKeyListener(this);
    }


    // Returns a solid-color image square
    public Image getIcon(Color c) {
        BufferedImage b = new BufferedImage(iconSide, iconSide, BufferedImage.TYPE_3BYTE_BGR);
        Graphics g = b.getGraphics();
        g.setColor(c);
        g.fillRect(0, 0, iconSide, iconSide);
        g.dispose();
        return b;
    }
    

    // Sets up the board image
    public void setBoard() {
        board = new BufferedImage(boardSide, boardSide, BufferedImage.TYPE_3BYTE_BGR);
        Graphics g = board.getGraphics();
        for (int y = 0; y < boardIconsPerSide; y++) {
            for (int x = 0; x < boardIconsPerSide; x++) {
                g.drawImage(icons[x][y], x * iconSide, y * iconSide, null);
            }
        }
        setScene();
    }


    // Draw the board, player at his position, and set against a black background to allow us to
    // scroll and still leave the player in the center of the display panel
    public void setScene() {
        if (scene == null) {
            int sideX3 = boardSide * 3;
            scene = new BufferedImage(sideX3, sideX3, BufferedImage.TYPE_3BYTE_BGR);
            Graphics g = scene.getGraphics();
            g.setColor(Color.BLACK);
            g.fillRect(0, 0, sideX3, sideX3);
            g.dispose();
        }
        Graphics g = scene.getGraphics();
        g.translate(boardSide, boardSide);
        g.drawImage(board, 0, 0, null);
        g.drawImage(player, playerPos.x, playerPos.y, null);
        g.dispose();
        repaint();
    }


    @Override
    public void paint(Graphics g) {
        // Explicitly set the paint area to the size of the board to prevent overflow when the player moves
        g.setClip(0, 0, boardSide, boardSide);

        // Draw the scene so that the player is centered in the display panel
        g.drawImage(scene, offset - playerPos.x, offset - playerPos.y, null);
    }


    // Let the player move his avatar with arrow keys
    public void keyPressed(KeyEvent e) {
        switch(e.getKeyCode()) {
            case KeyEvent.VK_LEFT:
                if (playerPos.x > 0) playerPos.x-= iconSide;
                break;
            case KeyEvent.VK_RIGHT:
                if (playerPos.x < (boardSide - iconSide)) playerPos.x+= iconSide;
                break;
            case KeyEvent.VK_UP:
                if (playerPos.y > 0) playerPos.y-= iconSide;
                break;
            case KeyEvent.VK_DOWN:
                if (playerPos.y < (boardSide - iconSide)) playerPos.y+= iconSide;
        }
        setScene();
    }

    public void keyReleased(KeyEvent e) { }
    public void keyTyped(KeyEvent e) { }
}

Finally, here is a later build showing actual icons in place of solid colors. A little corny, but it illustrates that graphics can be easily swapped and extended, leaving gameplay mechanics untouched.

No comments:

Post a Comment