while the game isn't over:
show stuff to the user
get user input
respond to user input
Roguelikes are no different. But showing stuff to the user, getting user input, and responding to the input doesn't always mean the same thing. Usually we're showing the world and waiting for a player's command but sometimes we're showing a list of spells and waiting for the user to tell us which one to cast or maybe we're showing the player how he died and asking if he wants to play again. Said another way, sometimes we're in "play" mode, sometimes in "select spell" mode, and sometimes "you lost" mode. Each mode has a different way of handling input and output, and I've found that having a different class for each mode with it's own input and output logic is a good way of handling that — much better than having a big mess of if statements and mode-related variables.
Each mode will be represented by a different screen. Each screen displays output on our AsciiPanel and responds to user input; this abstraction can be represented as a simple Screen interface.
package rltut.screens;
import java.awt.event.KeyEvent;
import asciiPanel.AsciiPanel;
public interface Screen {
public void displayOutput(AsciiPanel terminal);
public Screen respondToUserInput(KeyEvent key);
}
The displayOutput method takes an AsciiPanel to display itself on and the respondToUserInput takes the KeyEvent and can return the new screen. This way pressing a key can result in looking at a different screen.
The first screen players will see is the StartScreen. This is just a screen that displays some info and sets us in "play" mode when the user hits enter.
package rltut.screens;
import java.awt.event.KeyEvent;
import asciiPanel.AsciiPanel;
public class StartScreen implements Screen {
public void displayOutput(AsciiPanel terminal) {
terminal.write("rl tutorial", 1, 1);
terminal.writeCenter("-- press [enter] to start --", 22);
}
public Screen respondToUserInput(KeyEvent key) {
return key.getKeyCode() == KeyEvent.VK_ENTER ? new PlayScreen() : this;
}
}
The PlayScreen class will be responsible for showing the dungeon and all it's inhabitants and loot — but since we don't have that yet we'll just tell the player how much fun he's having. It will also respond to user input by moving the player and either setting us in "win" mode if we won the game, or "lose" mode if we lost.
package rltut.screens;
import java.awt.event.KeyEvent;
import asciiPanel.AsciiPanel;
public class PlayScreen implements Screen {
public void displayOutput(AsciiPanel terminal) {
terminal.write("You are having fun.", 1, 1);
terminal.writeCenter("-- press [escape] to lose or [enter] to win --", 22);
}
public Screen respondToUserInput(KeyEvent key) {
switch (key.getKeyCode()){
case KeyEvent.VK_ESCAPE: return new LoseScreen();
case KeyEvent.VK_ENTER: return new WinScreen();
}
return this;
}
}
Yes, using escape and enter to lose and win is pretty lame, but we know it's temporary and we can swap it out for real stuff later.
The WinScreen will eventually display how awesome our brave hero is and ask if they'd like to play again. But not yet.
package rltut.screens;
import java.awt.event.KeyEvent;
import asciiPanel.AsciiPanel;
public class WinScreen implements Screen {
public void displayOutput(AsciiPanel terminal) {
terminal.write("You won.", 1, 1);
terminal.writeCenter("-- press [enter] to restart --", 22);
}
public Screen respondToUserInput(KeyEvent key) {
return key.getKeyCode() == KeyEvent.VK_ENTER ? new PlayScreen() : this;
}
}
The LoseScreen will eventually display how lame our foolish hero was and ask if they'd like to play again. But not yet.
package rltut.screens;
import java.awt.event.KeyEvent;
import asciiPanel.AsciiPanel;
public class LoseScreen implements Screen {
public void displayOutput(AsciiPanel terminal) {
terminal.write("You lost.", 1, 1);
terminal.writeCenter("-- press [enter] to restart --", 22);
}
public Screen respondToUserInput(KeyEvent key) {
return key.getKeyCode() == KeyEvent.VK_ENTER ? new PlayScreen() : this;
}
}
That's it for the screens. Each one is only a dozen or so lines and does only a few simple things. That's a really good sign: small classes with few related responsibilities are good. Very good.
The ApplicationMain class needs to be updated though. It now has to display the current screen when the window repaints and pass user input to the current screen. It's generally best to avoid changing current code but this is only delegating input and output to other things, exactly what ApplicationMain is for. If we were changing it so it handles game logic then I'd be worried, but this is ok.
package rltut;
import javax.swing.JFrame;
import asciiPanel.AsciiPanel;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import rltut.screens.Screen;
import rltut.screens.StartScreen;
public class ApplicationMain extends JFrame implements KeyListener {
private static final long serialVersionUID = 1060623638149583738L;
private AsciiPanel terminal;
private Screen screen;
public ApplicationMain(){
super();
terminal = new AsciiPanel();
add(terminal);
pack();
screen = new StartScreen();
addKeyListener(this);
repaint();
}
public void repaint(){
terminal.clear();
screen.displayOutput(terminal);
super.repaint();
}
public void keyPressed(KeyEvent e) {
screen = screen.respondToUserInput(e);
repaint();
}
public void keyReleased(KeyEvent e) { }
public void keyTyped(KeyEvent e) { }
public static void main(String[] args) {
ApplicationMain app = new ApplicationMain();
app.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
app.setVisible(true);
}
}
Don't forget to update the AppletMain class to handle input and delegate everything to it's current screen.
I'm sure you want to have an @ running around quaffing potions and saying dragons but what we've done so far is very good. Not only are we ready for inventory screens, targeting screens, help screens, and any other screen you can think of, but we've already begin thinking about how user input and output is different than actual gameplay and should be in it's own separate code. Let me repeat, bold, and italicize that: It's good when input, output, and gameplay logic are separate. Once you start mixing them, everything goes downhill. In fact, all we really need to do now is implement the guts of PlayScreen. We'll start that next.
download the code
No comments:
Post a Comment