Friday, August 19, 2011

roguelike tutorial 02: input, output, modes, and screens

Nearly all games follow the same basic main loop:

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

30 comments:

  1. I like your idea, well done :D

    This first "programming" stuff is very useful, waiting for the next :D

    ReplyDelete
  2. @Marte: Thanks. I'm happy with the Screen interface and how that turned out too. Some other things turned into little messes, but that's what little projects and postmortems are for.

    ReplyDelete
  3. Keep coding!! I'm also linking your blog to mine :D

    ReplyDelete
  4. Your tutorial is really awesome! I've been looking for something like this for a long time, I will pass it on for sure!

    One thing I'm confused about is how come we have to pass the user input into the ApplicationMain class?

    Thanks for all the hard work you put into this, I'm learning a lot and having fun at the same time.

    Thanks again!!

    ReplyDelete
  5. @aode19, Something has to implement KeyListener in order to be receive the KeyEvent when the user presses a key. I just make the ApplicationMain class the KeyListener. So the java runtime passes the KeyEvent to ApplicationMain which passes the KeyEvent to the current screen to handle it.

    ReplyDelete
  6. This comment has been removed by the author.

    ReplyDelete
  7. Thank you so much for this tutorial. I have clawing my eyes out looking for a proper roguelike tutorial.

    The only resources I found in my search:

    -I found a partial 3 page one written in C++ that uses a depricatd library, and it only got as far as making a static map. The author stopped writing after that ...

    -I also found the libtcod library, and their tutorial is for Python...a language which i refuse to learn. I really can't stand it's whitespace style and indention rules. That and its all interpreter based... I haven't used one of those since Qbasic back in 1990..and I don't plan on going back. They do have c/c++ bindings but there documentation/community refrences is offline as their server crashed and the author straight up said, well I have it , but It wasn't profitable for me to host so your sh#t out of luck... Frnakly I am not going to use a lib when they can't even put their 3 years worth of docs and community writing back up.

    - The only thing close to this was a C++ tutorial called : "Terror in a Ascii Dungoen", it was awesome but I can't get the complete tutorial as it was written back in the early 2000s, and the site is now offline...uggh...

    SO again, thank you so much for this. I took java for 3 years in college but went on to work for a website company that does a crm suite. Never had a need for java since but its all coming back to me. Again thanks for this!!!

    ReplyDelete
  8. Am I the only one getting yelled at by netbeans about StartScreen.displayOutput not being abstract, and something about @override?

    ReplyDelete
    Replies
    1. Nevermind, I put a semicolon after the method declaration.

      Delete
  9. Hi Trystan,

    I know this tutorial of yours has been around for quite some time, but I finally had some free time and decided to give my own project another attempt. I come from a classic VB background, and recently switched to VB.NET, and although there is QUITE a lot to learn, especially when going down a more OO route, I'm kind of awestruck by everything .NET has to offer. I think this will be fun, albeit somewhat challenging!

    Anyways, I was struggling for weeks coming up with a framework for dealing with game states, and after digesting everything here in this article for [ugh... has it really been...] 4 days, I have borrowed your idea of using a "screen" interface to handle the different gamestates that will be needed for my project. Maybe I'm just ignorant for the time being, but this implementation of yours really seems like a good idea to me (that is, after knowing what the heck is really going on)! I couldn't quite work out how Java handles input, but I was still able to adapt the concept to my own project rather easily (I'm using a console for input/output, by the way).

    So yeah, thank you for the pretty cool stuff here, even though I'm a bit late. You might expect some more praise as I work my way further throughout your tutorial. :)

    ReplyDelete
    Replies
    1. Thanks Seismos!

      I've been happy with the screen interface too. One variation I did on a recent project of mine was to use a screen stack. User input goes straight to the topmost screen and when it's time to draw, each screen is drawn from the bottom of the stack to the top. enterScreen puts a new screen on the top of the stack, exitScreen pops the topmost screen off the stack and switchScreen pops then pushes a new screen. This way individual screens don't have to keep track of the previous screen for drawing and returning to.

      Delete
  10. Hey!

    So I love the framework you have provided. However, I am having a few issues. The screen is not being redrawn and only writing over, and not all of the symbols are showing up as they should. I have both typed the code myself and downloaded yours and it still is not working. Am I doing something wrong? or could it be different since I am on ubuntu?

    ReplyDelete
    Replies
    1. I'm guessing repaint isn't calling terminal.clear().

      Delete
    2. I don't know. I downloaded the source to use directly and that works fine. Thanksngpr your response!

      Delete
    3. Hi,
      as a new year resolution I'm following the tutorial and converting the code to Scala.
      I had exactly the same problem with the screen not been redrawn correctly. I checked the downloaded Java source and it works perfectly.
      I solved the issue by replacing the asciiPanel.jar I had downloaded from the direct link in the first part of the tutorial with the one found in the downloaded sources. So I guess there is a problem with the jar linked here: https://github.com/downloads/trystan/AsciiPanel/asciiPanel.jar

      Thank you for this great tutorial.

      Delete
    4. Hi,

      first of all: thank you for the great tutorial, really enjoying it so far. I'm a Web developer and write quite a lot of PHP and JavaScript but I still prefer Java's syntax over everything else even though I only learned a little bit of Java in university.

      So, I've encountered the same problem as Matt and Jelmo had: the asciiPanel.jar you've linked to at the top of the first article (the one on GitHub) seems to be different from the one you've included in the code zips you provide at the end of each article. The one from GitHub doesn't clear the terminal, it just lays the screens over one another (you can see through them).

      The asciiPanel.jar from the code zips does redraw the screen correctly, however, it seems to be very slow in comparison. While the one from GitHub changes screens without any noticeable delay (using the code from the second part here), the one from the code zips takes about 0.5 - 1 seconds before the screen changes. I'm on a highest-spec MacBook Retina 15" which is quite the beast (for a Mac) so I don't think this is because of slow hardware. Although, Java tends to be slower on OS X than Windows/Linux iirc...

      Anyway, just wanted to tell you so you can look into that if you'd like to. Thanks again for the great tutorial! :)

      Delete
  11. Hello! I just started looking at these, and I have a quick question:

    in Eclipse I put the AsciiPanel.jar into a source folder called lib.
    After that I set the build path and whatnot, but, even though it seems to work everywhere else,
    in this class, I get the bug "screen cannot be resolved". Any way to fix this?

    ReplyDelete
    Replies
    1. I'm not sure whats causing that. I'd guess that either the jar isn't in the classpath or you're missing the import statement for it.

      Delete
  12. Excellent tutorial! I'm a beginner Java coder and I'm interested in making a roguelike but what I was looking for were the basic structures of how a game should be made and what not to do when making a game. I love how you made clear how to build the basic beginnings here. I'm definitely bookmarking this. Thanks!

    ReplyDelete
  13. I've just started this and I love it already. Thanks a ton!

    ReplyDelete
  14. Picking up Java to start a new project and ran across your tutorial. Having tinkered with roguelike development in the past this is turning out to be a fun introduction to the language. Thanks!

    ReplyDelete
  15. Hi,

    my Applet don't want to take KeyEvent. I have add setFocusable(true); in my AppletMain() and now it works. But someone can tell me why i need to add this line. In the source of this blog, there is no setFocusable(true);? ...

    ReplyDelete
    Replies
    1. I had the same issue. My main application worked fine, but the applet wouldn't take keyboard commands until I placed that line. I have no clue why it does that though, I'd love to know.

      Delete
  16. Problem: I get an error saying
    "ApplicationMain cannot be converted to KeyListener()"
    on the line of
    "addKeyListener(this)".

    (I'm compiling through the command line and the little arrow points at the 'this' keyword)

    I know the code is old and I'm using the most recent jdk 1.8.something.91. I was googling around but couldn't find anything I understood clearly. I'm not new to programming, but I don't know java APIs, so I'm kind of clueless on how to fix that, while keeping the same functionalities intact...

    Would you or anyone else happen to know how to fix it?
    Thanks in advance.

    ReplyDelete
    Replies
    1. Nevermind.
      Forgot to 'implments KeyListener'...

      Delete
  17. First, thank you very much for the tutorial. Unfortunately, when adding all the files to Eclipse, I see the following errors:
    Incompatible conditional operand types PlayScreen and LoseScreen LoseScreen.java line 14
    Incompatible conditional operand types PlayScreen and StartScreen StartScreen.java line 13
    Incompatible conditional operand types PlayScreen and WinScreen WinScreen.java line 13

    Despite a longish time used to search for a solution, I haven't found one. Could anyone give me a hint? The project compiles but only shows the initial screen, spewing the above error after a key is pressed.

    ReplyDelete
    Replies
    1. LoseScreen and WinScreen need to extend PlayScreen and PlayScreen needs to extend StartScreen
      public class PlayScreen extends StartScreen implements Screen
      public class WinScreen extends PlayScreen implements Screen
      public class LoseScreen extends PlayScreen implements Screen

      Delete
  18. This comment has been removed by the author.

    ReplyDelete
  19. It has been pretty necessary for the students to follow out all those concerning piece guides which are indeed said to be of utmost importance and guides.

    ReplyDelete
  20. Im getting an input == null in my ApplicationMain and I have no idea why. It seems to be happening when I instantiate the ApplicationMain in the main method.

    ReplyDelete