Thursday, August 18, 2011

Deceptively Simple Prototypes

I decided to work on some very simple types of snippets related to game mechanics. Small little almost but not quite games! Definitely not polished.

This one I came up with is insanely simple. It's only purpose is just to focus on a process for prototyping different game mechanics. This isn't really a mechanic you would directly use as-is in a game. The core mechanic here being "click" the moving re-sizing circle as close to the center as possible to get maximum points.

Evolution
I tend to like exercises to come up with ways to change a currently working game or in this case snippet to turn it into something else. To sort of evolve it into it's own thing. There are a number of things I could add or change to enhance this and eventually make it into a game. It would be a casual arcade style game. I am not sure how well this idea of taking a simple mechanic and expanding on the gameplay works to create a fully polished game, but in my opinion it's better than just trying to brainstorm and come up with a game even if you're a designer and are good at visualizing the end product.

Prototypes
From my perspective it's definitely worth it. The experience of coding any gameplay mechanics no matter how small is useful. It forces you to turn the ideas in your head into something tangible and to work out coding issues you WILL have later on in development. Almost every game development team I have been on has always focused on the graphics and architecture first. The successful teams either use an existing engine or separate the graphics and architectural code from the game logic such that they are independent.

Your real drive is to get to the good stuff, the fun aspects of game making, the actual design. Coming up with battle systems, interesting weapons, big explosions, and detailed adventure areas, using all your creative juices. I have been tap-dancing around writing any game logic code for years. But I have always found small simple examples to be the best way to explore new areas in coding. So I am pushing myself to create as many small examples of different types of mechanics as possible. And I will of course post them here.

The Snippet
This snippet wasn't hard to get setup and rendering, one of the reasons I enjoy using Java for game development. It's basically create a window and a thread that continuously renders to the screen capture input via mouse position and do stuff when the buttons released.

Here's the 200+ line snippet.
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.util.HashSet;
import java.util.Set;

import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;

public class SimpleGame extends JComponent {

    public static class ScoreEffect {

        private static final double MAX_TIME  = 300;

        private final long          starttime = System.currentTimeMillis();

        private final int           score;

        private final int           x;

        private final int           y;

        protected ScoreEffect(int score, int x, int y) {
            this.score = score;
            this.x = x;
            this.y = y;
        }

        public void draw(Graphics2D init) {
            Graphics2D g = (Graphics2D) init.create();
            long cur = System.currentTimeMillis();
            double scale = ((cur - starttime) / MAX_TIME) + 1;
            g.translate(x, y);
            g.scale(1.5 * scale, 1.5 * scale);
            g.drawString(String.format("+%d", score), 0, 0);
            g.dispose();
        }

        public boolean isMax() {
            return System.currentTimeMillis() - starttime > MAX_TIME;
        }

        public static ScoreEffect valueOf(int score, int x, int y) {
            return new ScoreEffect(score, x, y);
        }
    }

    private static final int       MAX_HIT_COUNT = 20;
    private double                 direction     = 1;
    private double                 x             = 0;
    private double                 y             = 0;

    private double                 rsize;
    private int                    mouseX        = 0;
    private int                    mouseY        = 0;

    protected int                  score;

    private int                    hitCount      = MAX_HIT_COUNT;

    private boolean                isGameOn;

    private final Set<ScoreEffect> scores;

    public SimpleGame() {
        this.setPreferredSize(new Dimension(300, 200));
        this.setDoubleBuffered(true);

        scores = new HashSet<ScoreEffect>();

        (new Thread() {
            Object lock = new Object();

            @Override
            public void run() {
                synchronized (lock) {
                    while(true) {
                        repaint();

                        checkLooseCondition();

                        try {
                            lock.wait(10);
                        } catch (InterruptedException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                    }
                }
            }
        }).start();

        Dimension size = getPreferredSize();
        if(x == 0) {
            x = size.width / 2;
        }

        if(y == 0) {
            y = size.height / 2;
        }

        addMouseMotionListener(new MouseMotionAdapter() {
            @Override
            public void mouseMoved(MouseEvent e) {
                mouseX = e.getX();
                mouseY = e.getY();
            }
        });

        addMouseListener(new MouseAdapter() {
            @Override
            public void mouseReleased(MouseEvent e) {
                if(!isGameOn) {
                    isGameOn = true;
                } else {
                    double distance = Math.pow(mouseX - (x + rsize / 2), 2)
                            + Math.pow(mouseY - (y - rsize / 2), 2);
                    if(distance <= 400) {
                        int addPoints = (int) (10 * (distance / 400.0d)) + 1;
                        scores.add(ScoreEffect.valueOf(addPoints,
                                e.getX(),
                                e.getY()));
                        score += addPoints;
                        x = 0;
                        direction = 1;
                    }
                }
            }
        });
    }

    protected void checkLooseCondition() {
        if(hitCount <= 0) {
            isGameOn = false;
        }
    }

    @Override
    public void paintComponent(Graphics init) {
        Graphics2D g = (Graphics2D) init.create();

        if(isGameOn) {
            rsize = Math.abs(getWidth() / 2 - x);

            rsize /= getWidth() / 2;

            rsize = 1 - rsize;

            rsize *= 20;

            if(x <= 0) {
                direction = 1;
            }

            if(x >= getWidth() - rsize) {
                --hitCount;
                direction = -1;
            }

            x += direction * 2;
            g.setColor(Color.black);
            g.drawOval((int) x, (int) (y - rsize / 2), (int) rsize, (int) rsize);

            g.drawString(String.format("Score: %d", score), 10, 25);
            String hits = String.format("Hits Left: %d/%d",
                    hitCount,
                    MAX_HIT_COUNT);
            g.drawString(hits, getWidth() - 10
                    - g.getFontMetrics().stringWidth(hits), 25);

            Set<ScoreEffect> remove = new HashSet<ScoreEffect>();
            for(ScoreEffect e : scores) {
                e.draw(g);
                if(e.isMax()) {
                    remove.add(e);
                }
            }

            scores.removeAll(remove);
        } else {
            String gameon = "Game Over - click to restart!";
            g.drawString(gameon, getWidth() / 2
                    - g.getFontMetrics().stringWidth(gameon) / 2, getHeight()
                    / 2 - g.getFontMetrics().getDescent());

            String lastScore = String.format("Last Score: %d", score);
            g.drawString(lastScore,
                    getWidth() / 2 - g.getFontMetrics().stringWidth(lastScore)
                            / 2,
                    getHeight() / 2 - g.getFontMetrics().getDescent() + 30);

        }
        g.dispose();
    }

    /**
     * @param args
     */
    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                JFrame frame = new JFrame("Hit Me");
                SimpleGame g = new SimpleGame();
                frame.add(g);
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

}

No comments:

Post a Comment