7
\$\begingroup\$

I was wondering if someone could quickly skim over this for me really quickly and tell me if there's anything I could improve code-wise. What I have right now seems pretty big and clustered, and I'm almost positive that there's a way it can be compressed and made more efficient, and maybe even faster. I'm also hopeful for a way to remove the threads in my code if possible. Thanks for the help :P

By the way, this game is an attempted remake of The World's Hardest Game by Snubby Land and Armor Games, and it can be played here: http://armorgames.com/play/1043/the-worlds-hardest-game

Game.java

package net.thedanpage.worldshardestgame;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dialog;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.font.FontRenderContext;
import java.awt.font.TextLayout;
import java.awt.geom.AffineTransform;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.Timer;

import kuusisto.tinysound.Music;
import kuusisto.tinysound.Sound;
import kuusisto.tinysound.TinySound;

public class Game extends JPanel implements ActionListener {

    /** An instance of the game. */
    private static Game game;

    /** The timer used for the game's clock. */
    private Timer t = new Timer(5, this);

    /** Used for logging information during the game. */
    public final static Logger logger = Logger.getLogger(Game.class.getName());

    static String logFilePath = System.getProperty("user.home")
              + "/worldshardestgame/logs/" +  new SimpleDateFormat("YY-MM-dd").format(new Date()) + ".log";

    private static final long serialVersionUID = 1L;

    /** The frame that the panel goes in. */
    static JFrame frame = new JFrame();

    /** The enum instance used for switching the state of the game. */
    static final int INTRO = 0, MAIN_MENU = 1, LEVEL_TITLE = 2, LEVEL = 3;

    /** The integer used for the game state. */
    static int gameState = INTRO;

    /** Used for when the instructions should be shown. */
    private boolean showIntro = false;


    /** This is the level that the player is on. */
    static int levelNum = 0;

    /** A player class, used to get information about the player. */
    private Player player = new Player();

    /** The data of the current level. This should be given data in initLevel(). */
    static GameLevel level = new GameLevel();

    /** Controls whether the game has sound or not. */
    static boolean muted = false;

    /** Images for indicating volume. */
    private final Image VOLUME_BLACK = new ImageIcon(ClassLoader.getSystemResource(
            "net/thedanpage/worldshardestgame/resources/volume_black.png")).getImage();
    private final Image VOLUME_BLACK_MUTE = new ImageIcon(ClassLoader.getSystemResource(
            "net/thedanpage/worldshardestgame/resources/volume_black_mute.png")).getImage();
    private final Image VOLUME_WHITE = new ImageIcon(ClassLoader.getSystemResource(
            "net/thedanpage/worldshardestgame/resources/volume_white.png")).getImage();
    private final Image VOLUME_WHITE_MUTE = new ImageIcon(ClassLoader.getSystemResource(
            "net/thedanpage/worldshardestgame/resources/volume_white_mute.png")).getImage();

    /** Background music. */
    static Thread bgMusic = new Thread() {
        public void run() {
            TinySound.init();
            Music bgmusic = TinySound.loadMusic(ClassLoader.getSystemResource(
                    "net/thedanpage/worldshardestgame/resources/music.ogg"));
            bgmusic.play(true);
        }
    };

    Thread endIntro = new Thread() {
        public void run() {
            try {
                Thread.sleep(1500);
            } catch (InterruptedException e) {
                TextFileWriter.appendToFile(logFilePath, e.getMessage());
            }
            gameState = MAIN_MENU;
            easyLog(logger, Level.INFO, "Game state set to MAIN_MENU");
        }
    };

    static boolean doLogging = false;

    private static int totalLevels = 0;


    //Intro objects

    /** True if the intro text should move down. */
    private boolean fadeOutIntro = false;

    /** The opacity of the intro text. */
    private int introTextOpacity = 0;

    /** A whoosh sound. */
    Sound drone = TinySound.loadSound(ClassLoader.getSystemResource("net/thedanpage/worldshardestgame/resources/drone.ogg"));

    /** A bell sound. */
    Sound bell = TinySound.loadSound(ClassLoader.getSystemResource("net/thedanpage/worldshardestgame/resources/bell.wav"));





    public void paintComponent(final Graphics g) {
        super.paintComponent(g);

        update(g);
        render(g);

        //Start the timer
        t.start();

        Toolkit.getDefaultToolkit().sync();
    }





    /** Update the game.
     * 
     * @param g
     * */
    public void update(Graphics g) {

        if (gameState == INTRO) {

            if (introTextOpacity == 0 && !fadeOutIntro) {
                drone.play();
            }

            if (introTextOpacity < 255 && !fadeOutIntro) {
                introTextOpacity += 255/10;
                if (introTextOpacity > 255) introTextOpacity = 255;
            }

            if (introTextOpacity == 225) {
                new Thread() {
                    public void run() {
                        try {
                            Thread.sleep(3500);
                        } catch (InterruptedException e) {
                            TextFileWriter.appendToFile(logFilePath, e.getMessage());
                        }
                        fadeOutIntro = true;
                        bgMusic.start();
                    }
                }.start();
            }

            if (fadeOutIntro) {
                if (introTextOpacity > 0) {
                    introTextOpacity -= 255/20;
                    if (introTextOpacity < 0) introTextOpacity = 0;
                }
            }

            if (fadeOutIntro && introTextOpacity == 0 && !endIntro.isAlive()) {
                endIntro.start();
            }





        } else if (gameState == MAIN_MENU) {

            if (showIntro) {

                if (Input.enter.isPressed) {
                    showIntro = false;
                    gameState = LEVEL_TITLE;
                    easyLog(logger, Level.INFO, "Game state set to LEVEL_TITLE");

                    player.reset();

                    levelNum = 1;
                    level.init(player, levelNum);

                    //Wait 1.75 seconds then start the level.
                    new Thread() {
                        public void run() {
                            try { Thread.sleep(1750); } catch (InterruptedException e) { TextFileWriter.appendToFile(logFilePath, e.getMessage()); }
                            gameState = LEVEL;
                            easyLog(logger, Level.INFO, "Game state set to LEVEL");
                        }
                    }.start();
                }
            } else {

                //Click to start the first level
                if (Input.mousePressed && Input.mouseCoords.x > 304 && Input.mouseCoords.y < 323
                        && Input.mouseCoords.x < 515 && Input.mouseCoords.y > 192) {
                    showIntro = true;
                    bell.play();
                }   
            }

        } else if (gameState == LEVEL) {

            if (Input.mouseOnWindow && Input.mouseCoords.x <= 65 && Input.mouseCoords.y <= 22
                    && Input.mousePressed) {
                gameState = MAIN_MENU;
                easyLog(logger, Level.INFO, "Game state set to MAIN_MENU");
            }
        }
    }





    /** Draw the game's graphics.
     * 
     * @param g
     */
    private void render(Graphics g) {
        Graphics2D g2 = (Graphics2D) g;

        if (gameState == INTRO) {

            //Background
            g2.setPaint(new GradientPaint(0, 0, new Color(213, 213, 255), 0, 600, Color.WHITE));
            g2.fillRect(0, 0, 800, 600);

            g2.setFont(new Font("Tahoma", Font.BOLD, 50));
            g2.setColor(new Color(0, 0, 0, introTextOpacity));
            drawCenteredString("Made by Dan95363", 400, 250, g2);

        } else if (gameState == MAIN_MENU) {

            if (showIntro) {
                //Instructions
                g2.setFont(new Font("Tahoma", Font.BOLD, 20));
                g2.setColor(Color.BLACK);
                drawString("You are the red square. Avoid the blue circles and collect the\n" +
                           "yellow circles. Once you have collected all of the yellow\n" +
                           "circles, move to the green beacon to complete the level.\n" +
                           "Some levels consist of more than one beacon; the\n" +
                           "intermediary beacons act as checkpoints. You must complete\n" +
                           "all 30 levels in order to submit your score. Your score is a\n" +
                           "reflection of how many times you have died; the less, the better.", 30, 40, g2);

                g2.setColor(Color.BLUE);
                drawCenteredString("Press enter to continue", 400, 350, g2);
            } else {
                //Background
                g2.setPaint(new GradientPaint(0, 0, new Color(213, 213, 255), 0, 600, Color.WHITE));
                g2.fillRect(0, 0, 800, 600);

                //Draw and outline the title
                g2.setPaint(Color.BLACK);
                g2.setFont(new Font("SansSerif", Font.BOLD, 32));
                g2.drawString("The world's...", 40, 60);
                g2.setPaint(new Color(66, 117, 192));
                g2.setFont(new Font("SansSerif", Font.BOLD, 80));
                g2.drawString("HARDEST GAME", 40, 145);
                g2.setPaint(Color.BLACK);
                drawTextOutline("HARDEST GAME", 40, 145, 5, g2);

                g2.setFont(new Font("SansSerif", Font.BOLD, 60));

                //Gradient of "play game" text depending on the mouse location
                if (Input.mouseCoords.x > 284 && Input.mouseCoords.y < 343
                        && Input.mouseCoords.x < 515 && Input.mouseCoords.y > 192) {
                    g2.setPaint(new GradientPaint(0, 175, new Color(220, 220, 220), 0, 255, new Color(190, 60, 60)));
                } else {
                    g2.setPaint(new GradientPaint(0, 175, Color.WHITE, 0, 255, Color.RED));
                }

                //Draw and outline the "play game" text
                drawCenteredString("PLAY", 400, 255, g2);
                drawCenteredString("GAME", 400, 320, g2);
                g2.setColor(Color.BLACK);
                drawTextOutline("PLAY", 315, 255, 3, g2);
                drawTextOutline("GAME", 302, 320, 3, g2);
            }

        } else if (gameState == LEVEL) {

            if (levelNum != 0) {
                level.drawTiles(g);

                level.drawCoins(g);

                level.drawDots(g);
                level.updateDots();

                player.draw(g);
                player.update(level);

                g.setColor(Color.BLACK);
                g.fillRect(0, 0, 800, 22);

                g.setColor(Color.WHITE);
                g.setFont(new Font("Tahoma", Font.BOLD, 18));
                drawRightJustifiedString("Deaths: " + player.getDeaths(), 750, 17, g);
                drawCenteredString(levelNum + "/" + totalLevels, 400, 17, g);

                if (Input.mouseOnWindow && Input.mouseCoords.x <= 65 && Input.mouseCoords.y <= 22) {
                    g.setColor(Color.LIGHT_GRAY);
                }
                g.drawString("MENU", 0, 17);

                if (muted) {
                    g.drawImage(VOLUME_WHITE_MUTE, 760, -12, null);
                } else {
                    g.drawImage(VOLUME_WHITE, 760, -12, null);
                }
            }

        } else if (gameState == LEVEL_TITLE) {
            //Background
            g2.setPaint(new GradientPaint(0, 0, new Color(213, 213, 255), 0, 600, Color.WHITE));
            g2.fillRect(0, 0, 800, 600);

            //Draw the title text
            g2.setFont(new Font("Tahoma", Font.BOLD, 48));
            g.setColor(Color.BLACK);
            int textY = 200;
            for (String s : level.getTitle().split("\n")) {
                drawCenteredString(s, 400, textY += g.getFontMetrics().getHeight(), g);
            }
        }

        if (gameState != LEVEL) {
            if (muted) {
                g.drawImage(VOLUME_BLACK_MUTE, 760, -12, null);
            } else {
                g.drawImage(VOLUME_BLACK, 760, -12, null);
            }
        }

        g.dispose();
    }





    public void actionPerformed(ActionEvent arg0) {
        repaint();
    }





    /** Draw a string centered on its x axis.
     * 
     * @param text
     *      the text to be drawn
     * @param x
     *      the x coordinate of the text
     * @param y
     *      the y coordinate of the text
     * @param g
     *      the graphics the text will be drawn with
     */
    private void drawCenteredString(String s, int w, int h, Graphics g) {
        FontMetrics fm = g.getFontMetrics();
        int x = (w*2 - fm.stringWidth(s)) / 2;
        g.drawString(s, x, h);
    }





    /** Draw a string centered on its x axis.
     * 
     * @param text
     *      the text to be drawn
     * @param x
     *      the x coordinate of the text
     * @param y
     *      the y coordinate of the text
     * @param g2
     *      the 2D graphics the text will be drawn with
     */
    private void drawCenteredString(String s, int w, int h, Graphics2D g2) {
        FontMetrics fm = g2.getFontMetrics();
        int x = (w*2 - fm.stringWidth(s)) / 2;
        g2.drawString(s, x, h);
    }





    /** Draw a right-justified string.
     * 
     * @param text
     *      the text to be drawn
     * @param x
     *      the x coordinate of the text
     * @param y
     *      the y coordinate of the text
     * @param g2
     *      the 2D graphics the text will be drawn with
     */
    private void drawRightJustifiedString(String s, int w, int h, Graphics g) {
        FontMetrics fm = g.getFontMetrics();
        int x = (w - fm.stringWidth(s));
        g.drawString(s, x, h);
    }





    /** Draw the outline of a string of text.
     * 
     * @param text
     *      the text to be drawn
     * @param x
     *      the x coordinate of the text
     * @param y
     *      the y coordinate of the text
     * @param thickness
     *      the thickness of the outline
     * @param g2
     *      the 2D graphics the text will be drawn with
     */
    private void drawTextOutline(String text, int x, int y, int thickness, Graphics2D g2) {
        TextLayout tl = new TextLayout(text, g2.getFont(), new FontRenderContext(null,false,false));
        AffineTransform textAt = new AffineTransform();
        textAt.translate(x, y);
        g2.setStroke(new BasicStroke(thickness));
        g2.draw(tl.getOutline(textAt));
        g2.setStroke(new BasicStroke());
    }





    /** Draw a string, with the use of \n implemented.
     * 
     * @param text
     *      the text to be drawn
     * @param x
     *      the x coordinate of the text
     * @param y
     *      the y coordinate of the text
     * @param g
     *      the graphics the text will be drawn with
     */
    private void drawString(String text, int x, int y, Graphics g) {
        for (String line : text.split("\n"))
            g.drawString(line, x, y += g.getFontMetrics().getHeight());
    }





    /**
     * Convert an exception to a String with full stack trace
     * 
     * @param ex
     *            the exception
     * @return A string with the full stacktrace error text
     */
    public static String getStringFromStackTrace(Throwable ex) {
        if (ex == null) {
            return "";
        }
        StringWriter str = new StringWriter();
        PrintWriter writer = new PrintWriter(str);
        try {
            ex.printStackTrace(writer);
            return str.getBuffer().toString();
        } finally {
            try {
                str.close();
                writer.close();
            } catch (IOException e) {
                // ignore
            }
        }
    }





    /**
     * Easily log a string of text, and write it to the log file
     * 
     * @param logger
     *      The logger for the string to be logged with
     * @param level
     *      The level of the logger
     * @param s
     *      The string of text to be logged
     */
    static void easyLog(Logger logger, Level level, String s) {
        if (doLogging) {
            logger.setLevel(level);

            if (level == Level.CONFIG) logger.config(s);
            else if (level == Level.FINE) logger.fine(s);
            else if (level == Level.FINER) logger.finer(s);
            else if (level == Level.FINEST) logger.finest(s);
            else if (level == Level.INFO) logger.info(s);
            else if (level == Level.SEVERE) logger.severe(s);
            else if (level == Level.WARNING) logger.warning(s);

            else {
                logger.setLevel(Level.WARNING);
                logger.warning("Logging error");
            }

            TextFileWriter.appendToFile(logFilePath, new SimpleDateFormat(
                    "MMM dd, YYYY h:mm:ss a").format(new Date())
                    + " net.thedanpage.worldshardestgame easyLog\n" + level + ": " + s);
        }
    }





    public static void main(String[] args) {

        int option = JOptionPane.showConfirmDialog(
                new Dialog(frame, true),
                "Would you like to enable logging to " + System.getProperty("user.home") + "/worldshardestgame/logs?",
                "Setup",
                JOptionPane.YES_NO_OPTION);
        if (option == JOptionPane.YES_OPTION) Game.doLogging = true;
        else Game.doLogging = false;

        if (Game.doLogging) {

            //Create directory for logs if it does not exist
            if (!new File(System.getProperty("user.home") + "/worldshardestgame/logs").isDirectory()) {
                new File(System.getProperty("user.home") + "/worldshardestgame/logs").mkdirs();
            }

            if (new File(Game.logFilePath + ".zip").exists()) {
                LogZipper.unzip(
                    System.getProperty("user.home") + "/worldshardestgame/logs", Game.logFilePath + ".zip");
                new File(Game.logFilePath + ".zip").delete();
            }

            try {
                if (new File(Game.logFilePath).exists() && new BufferedReader(new FileReader(Game.logFilePath)).readLine() != null) {
                    TextFileWriter.appendToFile(Game.logFilePath, "\n");
                }
            } catch (IOException e) {
                Game.easyLog(Game.logger, Level.WARNING, Game.getStringFromStackTrace(e));
            }
        }

        try {
            while (new File(ClassLoader
                    .getSystemResource("net/thedanpage/worldshardestgame/resources/maps/level_" + (totalLevels+1) + ".txt").toURI())
                            .exists()) {
                totalLevels++;
            }
        } catch (Exception e) {
            System.out.println("Total levels: " + totalLevels);
        }

        Game.easyLog(Game.logger, Level.INFO, "Starting The World's Hardest Game");

        TinySound.init();
        Game.easyLog(Game.logger, Level.INFO, "TinySound initialized");

        if (Game.muted) TinySound.setGlobalVolume(0);

        Input.init();
        Game.easyLog(Game.logger, Level.INFO, "Input initialized");

        frame.setTitle("World's Hardest Game");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(new Dimension(800, 622));
        frame.setResizable(false);
        frame.setLocationRelativeTo(null);

        game = new Game();
        frame.add(game);

        frame.setIconImage(new ImageIcon(ClassLoader.getSystemResource("net/thedanpage/worldshardestgame/resources/favicon.png")).getImage());
        frame.setVisible(true);
    }

}

GameLevel.java

package net.thedanpage.worldshardestgame;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.geom.Area;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Scanner;
import java.util.logging.Level;

public class GameLevel {

    @Override
    public String toString() {
        return "GameLevel [spawnPoint=" + spawnPoint + ", id=" + id
                + ", levelTitle=" + levelTitle + ", tileMap=" + tileMap
                + ", dots=" + dots + ", coins=" + coins + ", levelArea="
                + levelArea + "]";
    }



    /** Spawn point of the level. */
    private Point spawnPoint;

    /** The ID of the level. The first level has an ID of 1. */
    private int id;

    /** The message that is displayed before the level. */
    private String levelTitle;

    /** A list of all of the level's tiles. */
    private ArrayList<Tile> tileMap;

    /** A list of all of the level's dots. */
    public ArrayList<Dot> dots;

    /** A list of all of the level's coins. **/
    public ArrayList<Coin> coins;

    /** The area of the level, not including background tiles. */
    Area levelArea;

    public GameLevel() {
        this.levelArea = new Area();
        this.tileMap = new ArrayList<Tile>();
        this.dots = new ArrayList<Dot>();
        this.spawnPoint = new Point(20, 20);
        this.id = -1;
        this.levelTitle = "\"Intimidating message\nhere\"";
    }

    public GameLevel(Point spawn, int id) {
        this.levelArea = new Area();
        this.tileMap = new ArrayList<Tile>();
        this.dots = new ArrayList<Dot>();
        this.spawnPoint = spawn;
        this.id = id;
        this.levelTitle = "\"Intimidating message\nhere\"";
    }

    /**
     * @return spawnPoint
     */
    public Point getSpawnPoint() {
        return this.spawnPoint;
    }

    /**
     * @return id
     */
    public int getID() {
        return this.id;
    }

    /**
     * @return tileMap
     */
    public ArrayList<Tile> getTileMap() {
        return this.tileMap;
    }

    /**
     * @return levelTitle
     */
    public String getTitle() {
        return this.levelTitle;
    }



    /** Draw the tiles based on a text file in the maps package. 
     * 
     * 
     * */
    public void drawTiles(Graphics g) {

        Graphics2D g2 = (Graphics2D) g;

        try {
            /*for (Tile t : this.tileMap) {
                //Background
                if (t.getType() == 0) {
                    g.setColor(new Color(180, 181, 254));
                    g.fillRect(t.getX(), t.getY(), 40, 40);
                }
            }*/

            g.setColor(new Color(180, 181, 254));
            g.fillRect(0, 22, 800, 622);

            //Border around level
            g2.setColor(Color.BLACK);
            g2.fill(this.levelArea);

            for (Tile t : this.tileMap) {

                t.draw(this, g);

            }
        } catch (Exception e) {
            System.out.println("File not found.");
            TextFileWriter.appendToFile(Game.logFilePath, Game.getStringFromStackTrace(e));
        }
    }



    public void drawDots(Graphics g) {
        for (Dot dot : this.dots) dot.draw(g);
    }



    public void updateDots() {
        if (this.dots != null)
            for (Dot dot : this.dots) dot.update();
    }



    public void drawCoins(Graphics g) {
        if (this.coins != null)
            for (Coin coin : this.coins) coin.draw(g);
    }



    public boolean allCoinsCollected() {
        if (this.coins != null) {
            for (Coin coin : this.coins) {
                if (!coin.collected) return false;
            }
        }
        return true;
    }



    /**
     * Load the current level data from
     * net.thedanpage.worldshardestgame.resources.maps
     */
    public void init(Player player, int levelNum) {

        if (ClassLoader.getSystemResource("net/thedanpage/worldshardestgame/resources/maps/level_" + levelNum + ".txt") != null)
            Game.easyLog(Game.logger, Level.INFO, "The file for level " + levelNum + " has been found");
        else Game.easyLog(Game.logger, Level.SEVERE, "The file for level " + levelNum + " could not be found");

        Game.easyLog(Game.logger, Level.INFO, "Level " + Game.levelNum + " is being initialized");

        //Clears the tile data
        this.tileMap = new ArrayList<Tile>();

        //Clears the dot data
        this.dots = new ArrayList<Dot>();

        //Clears the coin data
        this.coins = new ArrayList<Coin>();

        //Clears the level area data
        this.levelArea = new Area();

        //Resets the level title
        this.levelTitle = "\"Intimidating message\nhere\"";

        try {
            this.spawnPoint = new Point(
                            Integer.parseInt(PropLoader
                                    .loadProperty("spawn_point",
                                            "net/thedanpage/worldshardestgame/resources/maps/level_" + levelNum + ".properties")
                                    .split(",")[0]) * 40 + 20,
                            Integer.parseInt(PropLoader
                                    .loadProperty("spawn_point",
                                            "net/thedanpage/worldshardestgame/resources/maps/level_" + levelNum + ".properties")
                                    .split(",")[1]) * 40 + 20);

            this.id = Integer.parseInt(PropLoader.loadProperty("level_id",
                    "net/thedanpage/worldshardestgame/resources/maps/level_"
                            + levelNum + ".properties"));

            this.levelTitle = PropLoader.loadProperty("level_title", 
                    "net/thedanpage/worldshardestgame/resources/maps/level_"
                            + levelNum + ".properties").toString();

            String coinData = null;

            //Retrieves the coin data
            if (PropLoader.loadProperty("coins", 
                    "net/thedanpage/worldshardestgame/resources/maps/level_"
                            + levelNum + ".properties") != "null") {
                coinData = PropLoader.loadProperty("coins", 
                        "net/thedanpage/worldshardestgame/resources/maps/level_"
                                + levelNum + ".properties");
            }

            if (coinData != null) {
                coinData = coinData.replaceAll("\\Z", "");

                if (coinData.contains("-")) {

                    String[] coins = coinData.split("-");
                    for (String s : coins) this.coins.add(new Coin((int) (Double.parseDouble(s.split(",")[0]) * 40),
                            (int) (Double.parseDouble(s.split(",")[1]) * 40)));

                } else this.coins.add(new Coin((int) (Double.parseDouble(coinData.split(",")[0]) * 40),
                        (int) (Double.parseDouble(coinData.split(",")[1]) * 40)));
            }
            Game.easyLog(Game.logger, Level.INFO, "All coins have been added");

            //Retrieves the tile data
            InputStreamReader isr = new InputStreamReader(ClassLoader
                    .getSystemResource(
                            "net/thedanpage/worldshardestgame/resources/maps/level_"
                                    + levelNum + ".txt").openStream());
            String content = "";
            Scanner scanner = new Scanner(isr);
            content = scanner.useDelimiter("\\Z").next();
            scanner.close();

            Game.easyLog(Game.logger, Level.INFO, "Level data:\n\n" + content);

            content = content.replaceAll("\n", "");

            for (int i = 0; i < content.length(); i++) {
                if (i > 299)
                    break;
                else
                    this.tileMap.add(new Tile((i % 20) * 40, (i / 20) * 40,
                            Character.getNumericValue(content.charAt(i))));
            }
            this.levelArea = new Area();
            for (Tile t : this.tileMap) {
                if (t.getType() != 0) {
                    this.levelArea.add(new Area(
                            new Rectangle(t.getX() - 3, t.getY() - 3 + 22, 46, 46)));
                }
            }
        } catch (Exception e) {
            Game.easyLog(Game.logger, Level.SEVERE, "Map unable to be loaded:\n" + Game.getStringFromStackTrace(e));
        }
        //Retrieves the dot data
        try {
            InputStreamReader isr = new InputStreamReader(ClassLoader
                    .getSystemResource(
                            "net/thedanpage/worldshardestgame/resources/maps/level_"
                                    + levelNum + ".txt").openStream());
            Scanner scanner = new Scanner(isr);
            String content = scanner.useDelimiter("\\Z").next();
            String[] lines = content.split("\n");
            scanner.close();
            for (int i=19; lines[i] != null; i++) {
                String line = lines[i];
                String[] dotData = line.replaceAll(" ", "").split("-");
                this.dots.add(new Dot(
                            Integer.parseInt(dotData[0]),
                            Integer.parseInt(dotData[1]),
                            new Point(Integer.parseInt(dotData[2].split(",")[0]),
                                      Integer.parseInt(dotData[2].split(",")[1])),
                            new Point(Integer.parseInt(dotData[3].split(",")[0]),
                                      Integer.parseInt(dotData[3].split(",")[1])),
                            Double.parseDouble(dotData[4]),
                            Boolean.parseBoolean(dotData[5]),
                            Boolean.parseBoolean(dotData[6])
                        ));
            }
            Game.easyLog(Game.logger, Level.INFO, "All dots have been added");
        } catch (Exception e) {
            if (e.getClass().getName() != "java.lang.ArrayIndexOutOfBoundsException")
                Game.easyLog(Game.logger, Level.SEVERE, "Dots unable to be loaded:\n" + Game.getStringFromStackTrace(e));
        }
        if (this.tileMap.size() == 300) Game.easyLog(Game.logger, Level.INFO, "All tiles have been added");
        else Game.easyLog(Game.logger, Level.WARNING, "Not all tiles were added");

        player.respawn(this);
    }

}

Player.java

package net.thedanpage.worldshardestgame;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.Shape;
import java.util.ArrayList;
import java.util.logging.Level;

import kuusisto.tinysound.Sound;
import kuusisto.tinysound.TinySound;

public class Player {

    /** The X coordinate of the player. */
    private int x;

    /** The Y coordinate of the player. */
    private int y;

    /**
     * The X coordinate of the player, snapped to the grid of 40x40 tiles.
     * snapX = x/40
     */
    private int snapX;

    /**
     * The Y coordinate of the player, snapped to the grid of 40x40 tiles.
     * snapY = y/40
     */
    private int snapY;

    /** True if the player is colliding with a tile above them. */
    private boolean collidingUp;

    /** True if the player is colliding with a tile below them. */
    private boolean collidingDown;

    /** True if the player is colliding with a tile to their left. */
    private boolean collidingLeft;

    /** True if the player is colliding with a tile to their right. */
    private boolean collidingRight;

    /** The smack sound. */
    Sound smack;

    /** The number of times the player has died. */
    private int deaths;

    /** True if the player has been hit and is not allowed to move. */
    private boolean dead;

    /** The opacity of the player. */
    private double opacity;

    public Player() {
        this.x = 400;
        this.y = 300;
        this.snapX = x/40;
        this.snapY = y/40;
        this.collidingUp = false;
        this.collidingDown = false;
        this.collidingLeft = false;
        this.collidingRight = false;
        this.deaths = 0;
        this.dead = false;
        this.opacity = 255;
    }





    public Player(int x, int y) {
        this.x = x;
        this.y = y;
        this.snapX = x/40;
        this.snapY = y/40;
        this.collidingUp = false;
        this.collidingDown = false;
        this.collidingLeft = false;
        this.collidingRight = false;
        this.deaths = 0;
        this.dead = false;
        this.opacity = 255;
    }





    public void draw(Graphics g) {
        g.setColor(new Color(0, 0, 0, (int) opacity));
        g.fillRect(x - 15, y - 15 + 22, 31, 31);
        g.setColor(new Color(255, 0, 0, (int) opacity));
        g.fillRect(x-12, y-12 + 22,
                   25, 25);
    }





    Tile getRelativeTile(GameLevel level, int x1, int y1, int xOff, int yOff) {
        for (Tile t : level.getTileMap()) {
            if (x1/40 + xOff == t.getSnapX() && y1/40 + yOff == t.getSnapY()) {
                return t;
            }
        }
        return null;
    }





    Tile getTile(GameLevel level) {
        for (Tile t : level.getTileMap()) {
            if (this.x/40 == t.getSnapX() && this.y/40 == t.getSnapY()) {
                return t;
            }
        }
        return null;
    }





    boolean doesIntersect(Rectangle a, Rectangle b) {
        return (a.x + a.width < b.x || a.x > b.x + b.width
                || a.y + a.height < b.y || a.y > b.y + b.height);
    }





    public Rectangle getBounds() {
        return new Rectangle(this.x - 15, this.y - 15, 31, 31);
    }





    void checkCollisionUp(GameLevel level) {
        if (getRelativeTile(level, this.x - 14, this.y + 24, 0, -1) != null &&
                getRelativeTile(level, this.x - 14, this.y + 24, 0, -1).getType() == 0 ||
                getRelativeTile(level, this.x + 15, this.y + 24, 0, -1) != null &&
                getRelativeTile(level, this.x + 15, this.y + 24, 0, -1).getType() == 0) {
            this.collidingUp = true;
            return;
        }
        this.collidingUp = false;
    }





    void checkCollisionDown(GameLevel level) {
        if (getRelativeTile(level, this.x - 14, this.y - 24, 0, 1) != null &&
                getRelativeTile(level, this.x - 14, this.y - 24, 0, 1).getType() == 0 ||
                getRelativeTile(level, this.x + 15, this.y - 24, 0, 1) != null &&
                getRelativeTile(level, this.x + 15, this.y - 24, 0, 1).getType() == 0) {
            this.collidingDown = true;
            return;
        }
        this.collidingDown = false;
    }





    void checkCollisionLeft(GameLevel level) {
        if (getRelativeTile(level, this.x + 24, this.y - 15, -1, 0) != null &&
                getRelativeTile(level, this.x + 24, this.y - 15, -1, 0).getType() == 0 ||
                getRelativeTile(level, this.x + 24, this.y + 14, -1, 0) != null &&
                getRelativeTile(level, this.x + 24, this.y + 14, -1, 0).getType() == 0) {
            this.collidingLeft = true;
            return;
        }
        this.collidingLeft = false;
    }





    void checkCollisionRight(GameLevel level) {
        if (getRelativeTile(level, this.x - 24, this.y - 15, 1, 0) != null &&
                getRelativeTile(level, this.x - 24, this.y - 15, 1, 0).getType() == 0 ||
                getRelativeTile(level, this.x - 24, this.y + 15, 1, 0) != null &&
                getRelativeTile(level, this.x - 24, this.y + 15, 1, 0).getType() == 0) {
            this.collidingRight = true;
            return;
        }
        this.collidingRight = false;
    }





    void respawn(GameLevel level) {
        this.x = level.getSpawnPoint().x;
        this.y = level.getSpawnPoint().y;
        if (level.coins != null) {
            for (Coin coin : level.coins) coin.collected = false;
        }
    }





    boolean collidesWith(Shape other) {
        return this.getBounds().getBounds2D().intersects(other.getBounds2D());
    }





    public void update(GameLevel level) {
        this.snapX = this.x / 40;
        this.snapY = this.y / 40;

        if (level.coins != null) {
            for (Coin coin : level.coins) {
                if (this.collidesWith(coin.getBounds()) && !coin.collected) {
                    coin.collected = true;

                    //Coin sound
                    TinySound.init();
                    TinySound.loadSound(Player.class.getClassLoader()
                            .getResource("net/thedanpage/worldshardestgame/resources/ding.wav")).play();
                }
            }
        }

        if (level.getTileMap() != new ArrayList<Tile>()) {

            if (level.allCoinsCollected()) {

                for (Tile t : level.getTileMap()) {

                    if (t.getType() == 3 && this.collidesWith(t.getBounds())) {

                        Game.levelNum ++;
                        level.init(this, Game.levelNum);
                        Game.gameState = Game.LEVEL_TITLE;
                        Game.easyLog(Game.logger, Level.INFO, "Game state set to LEVEL_TITLE");

                        //Wait 1.75 seconds then start the level.
                        new Thread() {
                            public void run() {
                                try {
                                    Thread.sleep(1750);
                                } catch (InterruptedException e) {
                                    Game.easyLog(Game.logger, Level.SEVERE, Game.getStringFromStackTrace(e));
                                }
                                Game.gameState = Game.LEVEL;
                                Game.easyLog(Game.logger, Level.INFO, "Game state set to LEVEL");
                            }
                        }.start();
                    }
                }
            }
        }

        checkCollisionUp(level);
        checkCollisionDown(level);
        checkCollisionLeft(level);
        checkCollisionRight(level);

        if (this.dead) {
            this.opacity -= 255/75;

            if (this.opacity < 0) this.opacity = 0;

            if (this.opacity == 0) {
                this.dead = false;
                this.opacity = 255;
                this.respawn(level);
            }
        } else {
            if (Input.up.isPressed && !this.collidingUp) this.y --;
            if (Input.down.isPressed && !this.collidingDown) this.y ++;
            if (Input.left.isPressed && !this.collidingLeft) this.x --;
            if (Input.right.isPressed && !this.collidingRight) this.x ++;
        }

        if (this.x > 800) this.x = 0;
        if (this.x < 0) this.x = 800;
        if (this.y > 600) this.y = 0;
        if (this.y < 0) this.y = 600;

        if (!this.dead) {
            for (Dot dot : level.dots) {
                if (this.collidesWith(dot.getBounds())) {
                    this.deaths ++;
                    this.dead = true;

                    if (!Game.muted) {
                        //Play the smack sound
                        TinySound.init();
                        TinySound.loadSound(ClassLoader.getSystemResource(
                                "net/thedanpage/worldshardestgame/resources/smack.wav")).play();
                    }
                }
            }
        }
    }





    public int getX() {
        return this.x;
    }



    public int getY() {
        return this.y;
    }



    public int getSnapX() {
        return this.snapX;
    }



    public int getSnapY() {
        return this.snapY;
    }



    public int getWidth() {
        return (int) this.getBounds().getWidth();
    }



    public int getHeight() {
        return (int) this.getBounds().getHeight();
    }



    public boolean isCollidingLeft() {
        return this.collidingLeft;
    }



    public boolean isCollidingRight() {
        return this.collidingRight;
    }



    public boolean isCollidingUp() {
        return this.collidingUp;
    }



    public boolean isCollidingDown() {
        return this.collidingDown;
    }



    public int getDeaths() {
        return this.deaths;
    }



    public boolean isDead() {
        return this.dead;
    }



    public void setDead(boolean dead) {
        this.dead = dead;
    }



    public double getOpacity() {
        return this.opacity;
    }



    public void reset() {
        this.x = 400;
        this.y = 300;
        this.snapX = x/40;
        this.snapY = y/40;
        this.collidingUp = false;
        this.collidingDown = false;
        this.collidingLeft = false;
        this.collidingRight = false;
        this.deaths = 0;
        this.dead = false;
        this.opacity = 255;
    }





    @Override
    public String toString() {
        return "Player [x=" + x + ", y=" + y + ", snapX=" + snapX + ", snapY="
                + snapY + ", collidingUp=" + collidingUp + ", collidingDown="
                + collidingDown + ", collidingLeft=" + collidingLeft
                + ", collidingRight=" + collidingRight + ", deaths=" + deaths
                + ", dead=" + dead + "]";
    }
}

TextFileWriter.java

package net.thedanpage.worldshardestgame;

//  JavaFileAppendFileWriterExample.java
//  Created by <a href="http://alvinalexander.com" title="http://alvinalexander.com">http://alvinalexander.com</a>

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;

public class TextFileWriter {

    static BufferedWriter bw = null;

    public static void appendToFile(String filepath, String s) {
        try {
            // APPEND MODE SET HERE
            bw = new BufferedWriter(new FileWriter(filepath, true));
            bw.write(s);
            bw.newLine();
            bw.flush();
        } catch (IOException e) {
            System.out.println(e);
        } finally { // always close the file
            if (bw != null)
                try {
                    bw.close();
                } catch (IOException ioe2) {
                    // just ignore it
                }
        } // end try/catch/finally
    }

} // end class

LogZipper.java

package net.thedanpage.worldshardestgame;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

public class LogZipper {
    public static void zipLog() {
        byte[] buffer = new byte[1024];

        try {

            FileOutputStream fos = new FileOutputStream(
                    Game.logFilePath + ".zip");
            ZipOutputStream zos = new ZipOutputStream(fos);
            ZipEntry ze = new ZipEntry(Game.logFilePath.replace(
                    System.getProperty("user.home")
                            + "/worldshardestgame/logs/", ""));
            zos.putNextEntry(ze);
            FileInputStream in = new FileInputStream(Game.logFilePath);

            int len;
            while ((len = in.read(buffer)) > 0) {
                zos.write(buffer, 0, len);
            }

            in.close();
            zos.closeEntry();

            // remember close it
            zos.close();

            if (new File(Game.logFilePath).exists()) new File(Game.logFilePath).delete();

        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    public static void unzipLog() {
        try {
            ZipFile zipFile = new ZipFile(Game.logFilePath + ".zip");
            Enumeration<?> enu = zipFile.entries();
            while (enu.hasMoreElements()) {
                ZipEntry zipEntry = (ZipEntry) enu.nextElement();

                String name = zipEntry.getName();

                File file = new File(name);
                if (name.endsWith("/")) {
                    file.mkdirs();
                    continue;
                }

                File parent = file.getParentFile();
                if (parent != null) {
                    parent.mkdirs();
                }

                InputStream is = zipFile.getInputStream(zipEntry);
                FileOutputStream fos = new FileOutputStream(file);
                byte[] bytes = new byte[1024];
                int length;
                while ((length = is.read(bytes)) >= 0) {
                    fos.write(bytes, 0, length);
                }
                is.close();
                fos.close();

            }
            zipFile.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void unzip(String destinationFolder, String zipFile) {
        File directory = new File(destinationFolder);

        // if the output directory doesn't exist, create it
        if(!directory.exists()) 
            directory.mkdirs();

        // buffer for read and write data to file
        byte[] buffer = new byte[2048];

        try {
            FileInputStream fInput = new FileInputStream(zipFile);
            ZipInputStream zipInput = new ZipInputStream(fInput);

            ZipEntry entry = zipInput.getNextEntry();

            while(entry != null){
                String entryName = entry.getName();
                File file = new File(destinationFolder + File.separator + entryName);

                System.out.println("Unzipping file " + entryName + " to " + file.getAbsolutePath());

                // create the directories of the zip directory
                if(entry.isDirectory()) {
                    File newDir = new File(file.getAbsolutePath());
                    if(!newDir.exists()) {
                        boolean success = newDir.mkdirs();
                        if(success == false) {
                            System.out.println("Problem creating Folder");
                        }
                    }
                }
                else {
                    FileOutputStream fOutput = new FileOutputStream(file);
                    int count = 0;
                    while ((count = zipInput.read(buffer)) > 0) {
                        // write 'count' bytes to the file output stream
                        fOutput.write(buffer, 0, count);
                    }
                    fOutput.close();
                }
                // close ZipEntry and take the next one
                zipInput.closeEntry();
                entry = zipInput.getNextEntry();
            }

            // close the last ZipEntry
            zipInput.closeEntry();

            zipInput.close();
            fInput.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
\$\endgroup\$
0
3
\$\begingroup\$

try-with-resources

    static BufferedWriter bw = null;

You make this a class field. So it is common to every object of this class. Why?

    public static void appendToFile(String filepath, String s) {
        try {
            // APPEND MODE SET HERE
            bw = new BufferedWriter(new FileWriter(filepath, true));
            bw.write(s);
            bw.newLine();
            bw.flush();
        } catch (IOException e) {
            System.out.println(e);
        } finally { // always close the file
            if (bw != null)
                try {
                    bw.close();
                } catch (IOException ioe2) {
                    // just ignore it
                }
        } // end try/catch/finally
    }

One would think that this would check if bw is already instantiated before operating on bw. But it doesn't. It writes over it and keeps going. If it doesn't care about previous values of bw, why save them? This holds a reference to a closed BufferedWriter until the next time it needs one. Then it creates a new object and sends the old one for garbage collection. Why wait? Why not just send the old object for garbage collection as soon as you are done using it?

Why not just replace the whole thing with

    public static void appendToFile(String filepath, String s) {
        // APPEND MODE SET on the FileWriter
        try (BufferedWriter bw = new BufferedWriter(new FileWriter(filepath, true))) {
            bw.write(s);
            bw.newLine();
            bw.flush();
        } catch (IOException e) {
            System.out.println(e);
        }
    }

Now the try-with-resources handles closing the file. No manual finally block needed. That alone cuts almost half the code of the method. This also limits the scope to just the four places it is used.

Since we always create a new BufferedWriter, this just makes it a local variable. No class field needed.

Also, you may want to change the comment to explain why append mode is set on the FileWriter rather than simply saying that append mode is set. We can see that the FileWriter object is created in append mode if we read the JavaDoc on calling it with a Boolean second argument. What I still don't know though is why that matters. I'd much prefer to see

        // append to FileWriter so that ...

With the ... replaced with a real explanation.

As is, I can see that you found this important but I have no idea why. The current comment tells me nothing that the code doesn't. I would expect a method called appendToFile to use append mode. In fact, it leaves me a bit confused, as I don't know which statement is supposed to set append mode until I read the code.

Don't Repeat Yourself (DRY)

    void checkCollisionUp(GameLevel level) {
        if (getRelativeTile(level, this.x - 14, this.y + 24, 0, -1) != null &&
                getRelativeTile(level, this.x - 14, this.y + 24, 0, -1).getType() == 0 ||
                getRelativeTile(level, this.x + 15, this.y + 24, 0, -1) != null &&
                getRelativeTile(level, this.x + 15, this.y + 24, 0, -1).getType() == 0) {
            this.collidingUp = true;
            return;
        }
        this.collidingUp = false;
    }

You have four of these methods. All of them have the same basic pattern. You may want to consider @SimonForsberg's Direction enum pattern for this. But even without that, you could make this code considerably simpler.

    void checkCollisionUp(GameLevel level) {
        collidingUp = hasCollision(level, x - 14, y + 24, x + 15, y + 24, 0, -1);
    }

I prefer not using the this. unless needed to disambiguate. That of course is up to you. It works both ways. I'm just pointing out that I don't use it.

Then of course you need

    bool hasCollision(GameLevel level, int x1, int y1, int x2, int y2, int xOff, int yOff) {
        Tile relative1 = getRelativeTile(level, x1, y1, xOff, yOff);
        Tile relative2 = getRelativeTile(level, x2, y2, xOff, yOff);

        return relative1 != null && relative1.getType() == 0
            || relative2 != null && relative2.getType() == 0;
    }

Now there are five simple methods rather than four complex and repetitive methods.

I prefer to put operators at the beginning of the lines rather than at the end. This makes it easier to scan down the left side and get all the critical information.

I don't like the numbered variables, but I don't have better names for them. Perhaps topLeft and bottomRight or small and big or close and far.

This only calls each method once instead of calling both twice.

I'm not crazy about all the -14, +15, and +24 in that code. I would rather those were defined as constants in some way. I note that while it is -14 and +15 three out of four times, the other time it is -15 and +15. Is that deliberate? Or a mistake? I haven't tried to investigate it enough to tell.

If getRelativeTile were a static function, then hasCollision could be one too.

Set before the constructor

    /** True if the player is colliding with a tile above them. */
    private boolean collidingUp;

    /** True if the player is colliding with a tile below them. */
    private boolean collidingDown;

    /** True if the player is colliding with a tile to their left. */
    private boolean collidingLeft;

    /** True if the player is colliding with a tile to their right. */
    private boolean collidingRight;

    /** The smack sound. */
    Sound smack;

    /** The number of times the player has died. */
    private int deaths;

    /** True if the player has been hit and is not allowed to move. */
    private boolean dead;

    /** The opacity of the player. */
    private double opacity;

    public Player() {
        this.x = 400;
        this.y = 300;
        this.snapX = x/40;
        this.snapY = y/40;
        this.collidingUp = false;
        this.collidingDown = false;
        this.collidingLeft = false;
        this.collidingRight = false;
        this.deaths = 0;
        this.dead = false;
        this.opacity = 255;
    }





    public Player(int x, int y) {
        this.x = x;
        this.y = y;
        this.snapX = x/40;
        this.snapY = y/40;
        this.collidingUp = false;
        this.collidingDown = false;
        this.collidingLeft = false;
        this.collidingRight = false;
        this.deaths = 0;
        this.dead = false;
        this.opacity = 255;
    }

You can get this down to

    /** True if the player is colliding with a tile above them. */
    private boolean collidingUp = false;

    /** True if the player is colliding with a tile below them. */
    private boolean collidingDown = false;

    /** True if the player is colliding with a tile to their left. */
    private boolean collidingLeft = false;

    /** True if the player is colliding with a tile to their right. */
    private boolean collidingRight = false;

    /** The number of times the player has died. */
    private int deaths = 0;

    /** True if the player has been hit and is not allowed to move. */
    private boolean dead = false;

    /** The opacity of the player. */
    private double opacity = 255;

    public Player(int x, int y) {
        this.x = x;
        this.y = y;
        this.snapX = x/40;
        this.snapY = y/40;
    }

    public Player() {
        this(400, 300);
    }

This does three things to get the code down. First, I get rid of a smack variable that is unused. Second, for those fields that get set the same regardless of how the constructor is called, I initialize those at declaration time. So the constructors don't need to do so. Third, I call one constructor from the other. This allows me to set default values for the parameters. And it means that we don't have to repeat the snap logic in both constructors.

Java doesn't care about method order, so you can do it either way. I prefer to have called methods before calling methods when possible. You may prefer a different order.

\$\endgroup\$
1
  • \$\begingroup\$ Thanks for the tips! I realize that I'm seeing this pretty late now, but the advice is really helpful :P Unfortunately, I've pretty much abandoned this project, but I am working on a 2D tile game that I will definitely use these tips for. You mentioned not using repetition, and I've been trying to implement that into this new project by combining multiple similar functions into one common function. Also, I've been using an enum for the game state, and functions within the enums. Sorry I couldn't specifically apply this, but it definitely helped with my general knowledge. Merry xmas btw :) \$\endgroup\$ – Dan95363 Dec 25 '16 at 17:16
0
\$\begingroup\$

If you're looking to get rid of threads in the code, I would recommend to create a loop with a case system for game modes. (Note: I only read the first part of the code due to time limits so I'm sorry if I missed something similar.)

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.