Search code examples
javaswinggame-looproguelike

Java and Swing with a game loop


(Not so) short question here. I am working on making a simple roguelike using Swing as the UI rather than the console (Which makes it easier to work with in Eclipse, among other things) but I seem to have hit a roadblock.

What I am having trouble with is that when I enter the game loop, the UI will not display properly. I get an ugly window frame that gives me the 'solitaire' effect during the entire thing and while running it quickly grows in RAM usage.

Am I missing something critical about Swing here? Is it mandatory that I use Swing's concurrency setup to do this? If so, what would be the best approach?

The full code is below:

package roguelike;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.LinkedList;

import javax.swing.*;
import javax.swing.text.*;

public class roguelike {
    Color c_black = new Color(0x000000);
    Color c_white = new Color(0xffffff);
    Color c_red = new Color(0xff0000);
    Color c_blue = new Color(0x0000ff);
    Color c_green = new Color(0x00ff00);

    UI ui = null;
    Player me = null;
    Map gamemap = null;
    LinkedList<Character> keyqueue = new LinkedList<Character>();

    int charheight = 20;
    int charwidth = 80;

    public static void main(String[] args){
        SwingUtilities.invokeLater(new Runnable(){
            public void run(){
                roguelike game = new roguelike();
                game.run();
            }
        });
    }
    public roguelike(){
        gamemap = new Map();
        ui = new UI(charheight,charwidth,"Monospaced");
        me = new Player();
    }
    private class UI extends JFrame implements KeyListener{
        private static final long serialVersionUID = 9065411532125953842L;
        JPanel disp_screen = null;
        JTextPane disp_screen_text = null;
        StyledDocument disp_doc = null;
        Font mono_norm = null;
        int pxheight = 0;
        int pxwidth = 0;
        String[][] ctemp = new String[charheight][charwidth];
        String[][] stemp = new String[charheight][charwidth];


        public UI(int h,int w,String fontname){
            setVisible(true);
            charheight = h;
            charwidth = w;
            addKeyListener(this);

            this.setResizable(false);
            mono_norm = new Font(fontname,0,25);
            initScreen(h,w);
            add(disp_screen);
            makeStyles();
            try {
                disp_doc.insertString(0, "12345678901234567890123456789012345678901234567890123456789012345678901234567890\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20", disp_doc.getStyle("Default"));
            } catch (BadLocationException e) {
                e.printStackTrace();
            }

            Dimension temp = disp_screen.getSize();
            this.setSize(temp);
            this.setDefaultCloseOperation(EXIT_ON_CLOSE);           


            pack();
            try {
                disp_doc.remove(0, disp_doc.getLength());
            } catch (BadLocationException e) {
                e.printStackTrace();
            }
            renderMap(gamemap.getText(),gamemap.getStyles());

        }
        @Override
        public void keyPressed(KeyEvent e) {
            // TODO Auto-generated method stub

        }
        @Override
        public void keyReleased(KeyEvent e) {
            // TODO Auto-generated method stub

        }
        @Override
        public void keyTyped(KeyEvent e) {
            keyqueue.add(e.getKeyChar());
        }
        private void initScreen(int height, int width){
            FontMetrics metric = getFontMetrics(mono_norm);
            String temp = null;

            //generate a string of the right width from which to grab metrics
            for(int x=0;x<=width;x++){
                temp += '.';
            }
            pxwidth = SwingUtilities.computeStringWidth(metric, temp);
            pxheight = (metric.getHeight())*charheight;

            disp_screen = new JPanel();
            disp_screen_text = new JTextPane();
            disp_screen_text.setEditable(false);
            disp_screen_text.setAlignmentX(LEFT_ALIGNMENT);
            disp_screen_text.setAlignmentY(TOP_ALIGNMENT);
            disp_screen_text.setFont(mono_norm);
            disp_screen_text.setBackground(c_black);
            disp_screen_text.setForeground(c_green);
            disp_doc = disp_screen_text.getStyledDocument();


            disp_screen.add(disp_screen_text);
            disp_screen.setAlignmentX(LEFT_ALIGNMENT);
            disp_screen.setAlignmentY(TOP_ALIGNMENT);
            disp_screen.setLayout(new BoxLayout(disp_screen,BoxLayout.Y_AXIS));
        }
        private void makeStyles(){
            //The default style removes all special formatting and returns the text to standard nonsense
            Style sty_default = disp_doc.addStyle("Default", null);
            StyleConstants.setFontFamily(sty_default, "Monospaced");
            StyleConstants.setFontSize(sty_default, 18);
            StyleConstants.setForeground(sty_default, c_green);
            StyleConstants.setBackground(sty_default, c_black);
            StyleConstants.setItalic(sty_default, false);
            StyleConstants.setBold(sty_default, false);
            //StyleConstants.setSpaceAbove(sty_default, 0);
            //StyleConstants.setSpaceBelow(sty_default, 0);

            //The following styles apply certain effects. They are meant to be set without replacing styles
            Style sty_bold = disp_doc.addStyle("Bold", disp_doc.getStyle("Default"));
            StyleConstants.setBold(sty_bold,true);

            Style sty_ital = disp_doc.addStyle("Italic", disp_doc.getStyle("Default"));
            StyleConstants.setItalic(sty_ital, true);

        }
        private void clearMap(){
            try {
                disp_doc.remove(0, disp_doc.getLength());
            } catch (BadLocationException e1) {
                e1.printStackTrace();
            }
            try {
                //CLEAR THE MAP
                //For every row...
                for(int y=0;y<charheight;y++){
                    //For every column location in a row...
                    for(int x=0;x<charwidth;x++){
                        disp_doc.insertString(disp_doc.getLength(),".", disp_doc.getStyle("Default"));
                    }
                    disp_doc.insertString(disp_doc.getLength(),"\n", disp_doc.getStyle("Default"));
                }
            } catch (BadLocationException e){
                    e.printStackTrace();
            }
        }
        public void renderMap(String[][] chars, String[][] styles){
            System.out.print("Rendering map...");
            clearMap();
            ctemp = chars;
            stemp = styles;

            try {

                //For every row...
                for(int y=0;y<charheight;y++){
                    //For every column location in a row...
                    for(int x=0;x<charwidth;x++){
                        if(ctemp[y][x] != null){
                            if(stemp[y][x] == "D"){
                                disp_doc.remove((y*charwidth)+x, 1);
                                disp_doc.insertString((y*charwidth)+x,ctemp[y][x], disp_doc.getStyle("Default"));
                            } else if(stemp[y][x] == "B"){
                                disp_doc.remove((y*charwidth)+x, 1);
                                disp_doc.insertString((y*charwidth)+x,ctemp[y][x], disp_doc.getStyle("Bold"));
                            } else if(stemp[y][x] == "I"){
                                disp_doc.remove((y*charwidth)+x, 1);
                                disp_doc.insertString((y*charwidth)+x,ctemp[y][x], disp_doc.getStyle("Italic"));
                            } else{
                                disp_doc.remove((y*charwidth)+x, 1);
                                disp_doc.insertString((y*charwidth)+x,ctemp[y][x], disp_doc.getStyle("Default"));
                            }
                        }
                    }
                }
            } catch (BadLocationException e){
                e.printStackTrace();
                System.err.print(e.getCause());
            }
        }
    }

    //Handles the virtualized information of characters on the map. Does NOT handle display of map on screen
    //Displaying the screen happens by passing required data to the ui member
    private class Map{
        //Holds an array of map characters
        String[][] text = new String[charheight][charwidth];
        //Holds an array of styles associated with each map character
        String[][] styles = new String[charheight][charwidth];


        public Map(){
        }
        public String[][] getText(){

            return text;
        }
        public String[][] getStyles(){
            return styles;
        }
        public void putch(String thing, String styledef,int y, int x){
            text[y][x] = thing;
            styles[y][x] = styledef;
        }
    }

    private class Player{
        //Player location, [y][x]
        int[] location = {0,0};
        String sym = "@";
        public Player(){
        }
        public int[] getLocation(){
            return location;
        }
        public String getSymbol(){
            return sym;
        }
        public void setLocation(int y, int x){
            location[0]=y;
            location[1]=x;
        }
        public void setSymbol(String newsym){
            sym = newsym;
        }
        public void move(int dir){
            //Movement will be handled in an 8 directional fashion
            //North is 1, like below
            //     812
            //     703
            //     654
            //////////////////////
            switch(dir){
                case 0:
                    break;
                case 1:
                    location[0] += 1;
                    break;
                case 2:
                    location[0] += 1;
                    location[1] += 1;
                    break;
                case 3:
                    location[1] += 1;
                    break;
                case 4:
                    location[0] -= 1;
                    location[1] += 1;
                    break;
                case 5:
                    location[0] -= 1;
                    break;
                case 6:
                    location[0] -= 1;
                    location[1] -= 1;
                    break;
                case 7:
                    location[1] -= 1;
                    break;
                case 8:
                    location[0] += 1;
                    location[1] -= 1;
                default:
                    System.err.print("ERROR! "+dir+" is not a valid direction!");
                    break;
            }
        }
    }

    /////////////////////////////////////////////////////
    /////////////FUNCTIONS///////////////////////////////
    /////////////////////////////////////////////////////
    public void run(){
        boolean running = true;
        while(running){
            //Render Map
            gamemap.putch(me.getSymbol(), "D", me.getLocation()[0], me.getLocation()[1]);
            ui.renderMap(gamemap.getText(), gamemap.getStyles());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //Wait for player input
            if(keyqueue.isEmpty()){
                continue;
            }
            //Act on input
            else{
                char input = keyqueue.getFirst();
                keyqueue.removeFirst();
                if(input == 'w'){
                    me.move(1);
                }
                if(input =='a'){
                    me.move(7);
                }
                if(input == 's'){
                    me.move(5);
                }
                if(input == 'd'){
                    me.move(3);
                }
            }
        }

    }
}

Solution

  • Swing apps must be event-driven - your game.run() call will block Swing from doing anything else useful. Note that "events" could be user events, timers, or probably other things.

    If your design needs game.run() or something like it to run and never return, it could be done on another thread (or even a SwingWorker background thread), but all UI components must be accessed on the Swing thread.

    A recommendation: your keyboard handling could easily be done with a KeyListener you write, attached to an event handler on the window or other component, and then will be called whenever the user presses the key - i.e. you do not need to poll with this design.