view game/Engine.java @ 138:9eb791e2fa17

Optimize board updating logic, so that the old placed tiles need not to be redrawn from scratch on each screen update, as they do not change usually.
author Matti Hamalainen <ccr@tnsp.org>
date Fri, 25 Nov 2011 11:04:09 +0200
parents a33fdb1de11c
children be9cc2ee3c16
line wrap: on
line source

/*
 * Ristipolku Game Engine
 * (C) Copyright 2011 Matti 'ccr' Hämäläinen <ccr@tnsp.org>
 */
package game;

import java.awt.*;
import java.awt.geom.*;
import java.awt.event.*;
import java.awt.image.*;
import java.awt.event.*;
import java.awt.font.*;
import javax.imageio.*;
import javax.swing.*;
import java.util.*;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.io.*;
import game.*;
import javax.sound.sampled.*;


class AboutBox extends IDMWidget
{
    BufferedImage aboutImg;
    boolean aboutSecret;

    IDMPoint currPos, currOffs;
    Paint textPaint;
    Font textFont;
    FontMetrics textMetrics;


    public AboutBox()
    {
        try {
            ResourceLoader res = new ResourceLoader("graphics/girl.jpg");
            aboutImg = ImageIO.read(res.getStream());
        }
        catch (IOException e)
        {
        }

        aboutSecret = false;

        setPos(150f, 150f);
        setSize(675f, 420f);
    }

    public void setTextFont(Font font, FontMetrics metrics)
    {
        textFont = font;
        textMetrics = metrics;
    }

    public void setTextPaint(Paint paint)
    {
        textPaint = paint;
    }

    public void setCurrPos(IDMPoint npos)
    {
        currPos = npos;
        currOffs = new IDMPoint(0, 0);
    }

    public void setCurrPos(float x, float y)
    {
        setCurrPos(new IDMPoint(x, y));
    }

    public void drawString(Graphics2D g, String text)
    {
        Paint savePaint = g.getPaint();
        g.setPaint(textPaint);
        g.setFont(textFont);

        int i = 0;
        while (i < text.length())
        {
            int p = text.indexOf("\n", i);
            boolean linefeed;
            String str;
            if (p >= i)
            {
                str = text.substring(i, p);
                i = p + 1;
                linefeed = true;
            }
            else
            {
                str = text.substring(i);
                i += str.length();
                linefeed = false;
            }

            g.drawString(str, currPos.x + currOffs.x, currPos.y + currOffs.y);

            if (linefeed)
            {
                currOffs.x = 0;
                currOffs.y += textMetrics.getHeight();
            }
            else
            {
                currOffs.x += textMetrics.stringWidth(str);
            }
        }

        g.setPaint(savePaint);
    }

    public void paint(Graphics2D g)
    {
        int x = getScaledX(), y = getScaledY(),
            w = getScaledWidth(), h = getScaledHeight();

        g.setPaint(new Color(0.0f, 0.0f, 0.0f, 0.7f));
        g.fill(new RoundRectangle2D.Float(x, y, w, h, 10, 10));

        setTextFont(G.fonts[3], G.metrics[3]);
        setTextPaint(Color.white);

        setCurrPos(x + 20, y + 30);
        drawString(g, "RistiPolku (CrossPaths) v"+ G.version +"\n");

        setTextFont(G.fonts[1], G.metrics[1]);
        if (aboutSecret)
        {
            g.drawImage(aboutImg, x + 20, y + 55,
                aboutImg.getWidth(), aboutImg.getHeight(), null);

            setCurrPos(x + 225, y + 75);
            drawString(g,
                "Dedicated to my\n" +
                "favorite woman\n" +
                "in the world.");

            setCurrPos(x + 370, y + 175);
            drawString(g, "- Matti");
        }
        else
        {
            setTextPaint(Color.yellow);
            drawString(g, "(c) Copyright 2011 Matti 'ccr' Hämäläinen\n");

            setTextPaint(Color.white);
            drawString(g, "Programming project for Java-course\n" +
                          "T740306 taught by Kari Laitinen." +
                          "\n \n");

            setTextPaint(Color.red);
            drawString(g, "Controls:\n");

            IDMPoint old = currOffs.copy();

            setTextPaint(Color.white);
            drawString(g,
            "Arrow keys / mouse wheel\n"+
            "Enter / mouse click\n"+
            "Space bar\n");

            currPos.x += 330;
            currOffs.y = old.y;
            drawString(g,
            "- Rotate piece\n" +
            "- Place/lock piece\n" +
            "- Swap piece\n");

            currPos.x -= 330;
            setTextPaint(Color.green);
            drawString(g,
            "\nObjective: Create a path long as possible by rotating\n"+
            "and placing pieces. More points will be awarded for\n"+
            "advancing the path by several segments per turn."
            );
        }
    }

    public boolean keyPressed(KeyEvent e)
    {
        if (e.getKeyCode() == KeyEvent.VK_L)
        {
            aboutSecret = true;
        }
        else
        {
            clicked();
            aboutSecret = false;
        }
        return true;
    }

    public void clicked()
    {
        parent.remove(this);
    }
}


class GameBoard extends IDMWidget
{
    static final int boardSize = 9;
    static final int boardMiddle = 4;
    Piece[][] board;
    float pscale, ptime;

    public boolean flagGameOver, flagBoardIsDirty;
    int gameScore;

    Piece currPiece, nextPiece;
    int currX, currY, currPoint;

    Sound sndPlaced;

    private final ReentrantReadWriteLock pointLock = new ReentrantReadWriteLock();
    private ArrayList<AnimatedPointElement> pointElems;

    public GameBoard(IDMPoint pos, float ps)
    {
        super(pos);
        pscale = ps;

//        sndPlaced = G.smgr.getSound("sounds/placed.wav");

        pointElems = new ArrayList<AnimatedPointElement>();

        startNewGame();
    }

    public void startNewGame()
    {

        board = new Piece[boardSize][boardSize];
        board[boardMiddle][boardMiddle] = new Piece(PieceType.START);

        currX = boardMiddle;
        currY = boardMiddle;
        currPoint = 0;

        currPiece = null;
        nextPiece = new Piece(PieceType.ACTIVE);

        flagGameOver = false;
        flagBoardIsDirty = true;
        pieceFinishTurn();
        gameScore = 0;
    }

    public void paintBackPlate(Graphics2D g)
    {
        g.setPaint(new Color(0.0f, 0.0f, 0.0f, 0.2f));
        g.setStroke(new BasicStroke(5.0f));
        g.draw(new RoundRectangle2D.Float(getScaledX(), getScaledY(),
            boardSize * pscale, boardSize * pscale,
            pscale / 5, pscale / 5));
    }

    public void paintBoard(Graphics2D g, boolean drawCurrent)
    {
        for (int y = 0; y < boardSize; y++)
        for (int x = 0; x < boardSize; x++)
        if (board[x][y] != null)
        {
            if ((drawCurrent && board[x][y] == currPiece) ||
                (!drawCurrent && board[x][y] != currPiece))
            {
                board[x][y].paint(g,
                    getScaledX() + (x * pscale),
                    getScaledY() + (y * pscale),
                    pscale - pscale / 10);
            }
        }
    }

    public void paint(Graphics2D g)
    {
        paintBoard(g, true);

        Lock read = pointLock.readLock();
        read.lock();
        try
        {
            for (AnimatedPointElement elem : pointElems)
            {
                elem.paint(g);
            }
        }
        finally
        {
            read.unlock();
        }


        if (!flagGameOver)
        {
            if (nextPiece != null)
            {
                // Draw next piece
                AffineTransform save = g.getTransform();
                nextPiece.paint(g, 830, 325, 90);
                g.setTransform(save);
            }
        }
        else
        {
            // Game over text
            String text = "Game Over!";
            int textWidth = G.metrics[2].stringWidth(text);
            g.setFont(G.fonts[2]);

            g.setPaint(new Color(0.0f, 0.0f, 0.0f, 0.5f));
            g.drawString(text, (G.screenDim.width - textWidth) / 2 + 5, G.screenDim.height / 2 + 5);

            double f = Math.sin(ptime * 0.1) * 4.0;
            g.setPaint(Color.white);
            g.drawString(text, (G.screenDim.width - textWidth) / 2 + (float) f, G.screenDim.height / 2 + (float) f);
        }

        // Score
        g.setFont(G.fonts[2]);
        g.setPaint(Color.white);
        g.drawString(""+ String.format("%05d", gameScore), G.screenDim.width - 230, 220);

    }

    public boolean contains(float x, float y)
    {
        return (x >= getScaledX() &&
                y >= getScaledY() &&
                x < getScaledX() + boardSize * pscale &&
                y < getScaledY() + boardSize * pscale);
    }

    public boolean isBoardDirty()
    {
        if (flagBoardIsDirty)
        {
            flagBoardIsDirty = false;
            return true;
        }
        else
            return false;
    }

    public void animate(float time)
    {
        ptime = time;
        for (int y = 0; y < boardSize; y++)
        for (int x = 0; x < boardSize; x++)
        if (board[x][y] != null)
        {
            board[x][y].animate(time);
            if (board[x][y] != currPiece && board[x][y].active)
                flagBoardIsDirty = true;
        }

        Lock write = pointLock.writeLock();
        write.lock();
        try
        {
            ArrayList<AnimatedPointElement> tmp = new ArrayList<AnimatedPointElement>();

            for (AnimatedPointElement elem : pointElems)
            {
                elem.animate(time);
                if (elem.active)
                    tmp.add(elem);
            }

            pointElems = tmp;
        }
        finally
        {
            write.unlock();
        }
    }

    public void pieceRotate(Piece.RotateDir dir)
    {
        if (currPiece != null && !flagGameOver)
        {
            currPiece.rotate(dir);
        }
    }

    // Change coordinates based on the "outgoing"
    // piece connection point.
    private void pieceMoveTo(int point)
    {
        switch (point)
        {
            case 0: currY--; break;
            case 1: currY--; break;

            case 2: currX++; break;
            case 3: currX++; break;

            case 4: currY++; break;
            case 5: currY++; break;

            case 6: currX--; break;
            case 7: currX--; break;
        }
    }

    public void pieceCreateNew()
    {
        currPiece = nextPiece;
        currPiece.changed();
        nextPiece = new Piece(PieceType.ACTIVE);
        flagBoardIsDirty = true;
    }

    public void pieceSwapCurrent()
    {
        if (!flagGameOver)
        {
            Piece tmp = currPiece;
            currPiece = nextPiece;
            nextPiece = tmp;
            board[currX][currY] = currPiece;
            currPiece.changed();
            nextPiece.changed();
        }
    }

    // Check one piece, set connections, find the new placement
    // based on piece rotations etc.
    private boolean pieceCheck(Piece piece)
    {
        if (piece == null)
        {
            // Create new piece
            pieceCreateNew();
            board[currX][currY] = currPiece;
            return true;
        }
        else
        if (piece.getType() == PieceType.START)
        {
            if (currPiece != null)
            {
                // Hit center starting piece, game over
                flagGameOver = true;
                return true;
            }
            else
            {
                // Start piece as first piece means game is starting
                pieceMoveTo(currPoint);
                pieceCreateNew();
                board[currX][currY] = currPiece;
                return true;
            }
        }

        // Mark the current piece as locked
        piece.setType(PieceType.LOCKED);

        // Solve connection (with rotations) through the piece
        currPoint = piece.getRotatedPoint(piece.getMatchingPoint(currPoint));

        // Mark connection as active
        piece.setConnectionState(currPoint, true);

        // Solve exit point (with rotations)
        currPoint = piece.getAntiRotatedPoint(piece.getConnection(currPoint));

        // Move to next position accordingly
        pieceMoveTo(currPoint);
        return false;
    }

    // Finish one move/turn of the game, resolve path and find placement
    // of the next piece, or set "game over" state if required.
    public void pieceFinishTurn()
    {
        boolean finished = false;
        int connections = 0;

        if (currPiece != null)
        {
            G.smgr.play(sndPlaced);
        }

        while (!finished)
        {
            if (currX >= 0 && currX < boardSize && currY >= 0 && currY < boardSize)
            {
                int oldX = currX, oldY = currY;
                connections++;
                finished = pieceCheck(board[currX][currY]);

                if (!finished)
                {
                    Lock write = pointLock.writeLock();
                    write.lock();
                    try
                    {
                        pointElems.add(new AnimatedPointElement(
                            new IDMPoint(
                            getScaledX() + ((oldX + 0.5f) * pscale),
                            getScaledY() + ((oldY + 0.5f) * pscale)),
                            "" + connections));
                    }
                    finally
                    {
                        write.unlock();
                    }
                }

            }
            else
            {
                // Outside of the board, game over
                finished = true;
                flagGameOver = true;
            }
        }

        // Compute and add score
        gameScore += connections * connections;

        // If game over, clear the game
        if (flagGameOver)
        {
            currPiece = null;
        }
    }

    public boolean mouseWheelMoved(MouseWheelEvent e)
    {
        int notches = e.getWheelRotation();

        if (notches < 0)
            pieceRotate(Piece.RotateDir.LEFT);
        else
            pieceRotate(Piece.RotateDir.RIGHT);

        return true;
    }

    public void clicked()
    {
        if (!flagGameOver)
            pieceFinishTurn();
    }

    public boolean keyPressed(KeyEvent e)
    {
        if (flagGameOver)
            return false;

        switch (e.getKeyCode())
        {
            case KeyEvent.VK_LEFT:
            case KeyEvent.VK_UP:
                pieceRotate(Piece.RotateDir.LEFT);
                return true;

            case KeyEvent.VK_RIGHT:
            case KeyEvent.VK_DOWN:
                pieceRotate(Piece.RotateDir.RIGHT);
                return true;

            case KeyEvent.VK_ENTER:
                pieceFinishTurn();
                return true;
        }
        return false;
    }
}


public class Engine extends JPanel
                    implements Runnable, KeyListener,
                    MouseListener, MouseWheelListener
{
    long startTime;
    float gameClock, gameFrames;

    Thread animThread;
    boolean animEnable = false;

    GameBoard lauta = null;
    InputStream musa;
    IDMContainer widgets;
    AboutBox aboutBox;

    public Engine()
    {
        // Initialize globals
        System.out.print("Engine() constructor\n");

        // Sound system
        G.smgr = new SoundManager(new AudioFormat(22050, 16, 1, true, false), 1);

        // Load resources
        try
        {
            ResourceLoader res = new ResourceLoader("graphics/board.jpg");
            G.lautaBG = ImageIO.read(res.getStream());

            try {
                res = new ResourceLoader("graphics/font.ttf");

                G.fonts = new Font[G.numFonts];
                G.fonts[0] = Font.createFont(Font.TRUETYPE_FONT, res.getStream());
                G.fonts[1] = G.fonts[0].deriveFont(24f);
                G.fonts[2] = G.fonts[0].deriveFont(64f);
                G.fonts[3] = G.fonts[0].deriveFont(32f);
            }
            catch (FontFormatException e)
            {
                System.out.print("Could not initialize fonts.\n");
            }

            res = new ResourceLoader("sounds/gamemusic.wav");
            musa = res.getStream();
        }
        catch (IOException e)
        {
            JOptionPane.showMessageDialog(null,
                e.getMessage(),
                "Initialization error",
                JOptionPane.ERROR_MESSAGE);

            System.out.print(e.getMessage());
        }

        // Create IDM GUI widgets
        widgets = new IDMContainer();

        lauta = new GameBoard(new IDMPoint(95, 130), 63);
        widgets.add(lauta);

        widgets.add(new BtnSwapPiece(767f, 450f));
        widgets.add(new BtnAbout    (767f, 550f));
        widgets.add(new BtnNewGame  (767f, 630f));

        aboutBox = new AboutBox();

        // Game
        startNewGame();

        // Initialize event listeners
        addKeyListener(this);
        addMouseListener(this);
        addMouseWheelListener(this);

        // Start playing background music
        G.smgr.play(musa);

        // Get initial focus
        if (!hasFocus())
        {
            System.out.print("Engine(): requesting focus\n");
            requestFocus();
        }

    }

    public void startNewGame()
    {
        gameClock = 0;
        gameFrames = 0;
        startTime = new Date().getTime();
        lauta.startNewGame();
    }

    public void paintComponent(Graphics g)
    {
        Graphics2D g2 = (Graphics2D) g;
        boolean scaleChanged = false,
                updateBoard = lauta.isBoardDirty();

        // Rescale if parent component size has changed
        Dimension dim = getSize();
        if (G.screenDim == null || !dim.equals(G.screenDim))
        {
            float dw = dim.width / 1024.0f,
                  dh = dim.height / 768.0f;

            // Rescale IDM GUI widgets
            widgets.setScale(dw, dh);
            G.screenDim = dim;

            // Rescale background image
            // Rescale fonts
            G.fonts[1] = G.fonts[0].deriveFont(24f * dw);
            G.fonts[2] = G.fonts[0].deriveFont(64f * dw);
            G.fonts[3] = G.fonts[0].deriveFont(32f * dw);

            System.out.print("scale changed\n");
            scaleChanged = true;
            updateBoard = true;
        }
        
        if (updateBoard)
        {
            System.out.print("updateBoard()\n");
            G.lautaBGScaled = new BufferedImage(dim.width, dim.height, BufferedImage.TYPE_INT_RGB);
            Graphics2D gimg = G.lautaBGScaled.createGraphics();
            gimg.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
                                  RenderingHints.VALUE_INTERPOLATION_BICUBIC);

            gimg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                                RenderingHints.VALUE_ANTIALIAS_ON);

            gimg.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
                                RenderingHints.VALUE_TEXT_ANTIALIAS_ON);

            gimg.drawImage(G.lautaBG, 0, 0, dim.width, dim.height, null);
            lauta.paintBackPlate(gimg);
            lauta.paintBoard(gimg, false);
        }

        // Get font metrics against current Graphics2D context
        if (G.metrics == null || scaleChanged)
        {
            G.metrics = new FontMetrics[G.numFonts];
            for (int i = 0; i < G.numFonts; i++)
                G.metrics[i] = g2.getFontMetrics(G.fonts[i]);
        }

        // Draw background image, pieces, widgets
        g2.drawImage(G.lautaBGScaled, 0, 0, null);

        // Use antialiasing when rendering the game elements
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                            RenderingHints.VALUE_ANTIALIAS_ON);

        g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
                            RenderingHints.VALUE_TEXT_ANTIALIAS_ON);

        widgets.paint(g2);


        // Frames per second counter
        g2.setFont(G.fonts[1]);
        long currTime = new Date().getTime();
        g2.drawString("fps = "+ ((gameFrames * 1000) / (currTime - startTime)), G.screenDim.width - 120, 20);

        gameFrames++;
    }

    public void startThreads()
    {
        System.out.print("startThreads()\n");
        if (animThread == null)
        {
            animThread = new Thread(this);
            animEnable = true;
            animThread.start();
        }
    }

    public void stopThreads()
    {
        System.out.print("stopThreads()\n");

        // Stop animations
        if (animThread != null)
        {
            animThread.interrupt();
            animEnable = false;
            animThread = null;
        }

        // Shut down sound manager
        G.smgr.close();
    }

    public void mouseEntered(MouseEvent e)
    {
        widgets.mouseEntered(e);
    }
    public void mouseExited(MouseEvent e)
    {
        widgets.mouseExited(e);
    }

    public void mousePressed(MouseEvent e)
    {
        if (widgets.containsObject(aboutBox))
            aboutBox.mousePressed(e);
        else
            widgets.mousePressed(e);
    }

    public void mouseReleased(MouseEvent e)
    {
        if (widgets.containsObject(aboutBox))
            aboutBox.mouseReleased(e);
        else
            widgets.mouseReleased(e);
    }

    public void mouseClicked(MouseEvent e)
    {
        if (!hasFocus())
        {
            System.out.print("Requesting focus\n");
            requestFocus();
        }
    }

    public void mouseWheelMoved(MouseWheelEvent e)
    {
        lauta.mouseWheelMoved(e);
    }

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

    public void keyPressed(KeyEvent e)
    {
        // Handle keyboard input
        if (widgets.containsObject(aboutBox))
            aboutBox.keyPressed(e);
        else
            widgets.keyPressed(e);
    }

    public void run()
    {
        while (animEnable)
        {
            // Progress game animation clock
            gameClock++;

            // Animate components
            lauta.animate(gameClock);
            if (lauta.nextPiece != null)
                lauta.nextPiece.animate(gameClock);

            // Repaint with a frame limiter
            if (gameClock % 4 == 1)
                repaint();

            // Sleep for a moment
            try {
                Thread.sleep(10);
            }
            catch (InterruptedException x) {
            }
        }
    }

    class BtnNewGame extends IDMButton
    {
        public BtnNewGame(float x, float y)
        {
            super(x, y, KeyEvent.VK_ESCAPE, G.fonts[1], "New Game");
        }

        public void clicked()
        {
            startNewGame();
        }
    }

    class BtnSwapPiece extends IDMButton
    {
        public BtnSwapPiece(float x, float y)
        {
            super(x, y, KeyEvent.VK_SPACE, G.fonts[1], "Swap");
        }

        public void clicked()
        {
            lauta.pieceSwapCurrent();
        }
    }

    class BtnAbout extends IDMButton
    {
        public BtnAbout(float x, float y)
        {
            super(x, y, KeyEvent.VK_A, G.fonts[1], "About");
        }

        public void clicked()
        {
            if (!widgets.containsObject(aboutBox))
                widgets.add(aboutBox);
        }
    }
}