Scene.java

/*
 * Copyright © 2012 Nokia Corporation. All rights reserved.
 * Nokia and Nokia Connecting People are registered trademarks of Nokia Corporation.
 * Oracle and Java are trademarks or registered trademarks of Oracle and/or its
 * affiliates. Other product and company names mentioned herein may be trademarks
 * or trade names of their respective owners.
 * See LICENSE.TXT for license information.
 */
package com.nokia.example.ghosts;

import java.io.IOException;
import java.util.Timer;
import java.util.TimerTask;

import javax.microedition.lcdui.Alert;
import javax.microedition.lcdui.Canvas;
import javax.microedition.lcdui.Display;
import javax.microedition.lcdui.Font;
import javax.microedition.lcdui.Graphics;
import javax.microedition.lcdui.Image;
import javax.microedition.midlet.MIDlet;

/**
 * This class implements the game logic.
 *
 * This is a simple game where the player should shoot a ghost by moving the crosshair
 * over the screen and firing. The crosshair is controlled by an on-screen gamepad
 * which emulates an analog stick and fire button on multipoint touch-enabled Canvas.
 * Game records the score and high-score.
 * A progress bar is displayed, showing the remaining time before score reset.
 */
public class Scene extends Canvas
{
    protected MIDlet midlet;
    private Image exitImage;
    private Image exitPressedImage;
    private Image shotImage;
    private Image missImage;
    private boolean exitButtonPointerDown = false;
    private Timer inactivityTimer;
    private Point firePosition = new Point();
    private boolean shot = false;
    private boolean miss = false;
    private int level = 1;
    private int score = 0;
    private int hiScore = 0;
    private Font font;
    private Ghost ghost;
    private Crosshair crosshair;
    private Gamepad gamepad;

    public Scene(MIDlet midlet)
    {
        super();
        this.setFullScreenMode(true);
        this.midlet = midlet;
        try
        {
            this.exitImage = Image.createImage("/images/Exit.png");
            this.exitPressedImage = Image.createImage("/images/ExitPressed.png");
            this.shotImage = Image.createImage("/images/Shot.png");
            this.missImage = Image.createImage("/images/Miss.png");

            Size sceneSize = new Size(
                this.getWidth(),
                this.getHeight()
            );
            this.ghost = new Ghost(sceneSize);
            this.crosshair = new Crosshair(sceneSize);
            this.gamepad = new Gamepad(sceneSize);
        }
        catch (IOException e)
        {
            Display.getDisplay(this.midlet).setCurrent(
                new Alert("Cannot load graphics."), this
            );
            e.printStackTrace();
        }

        // Is used to draw score
        this.font = Font.getFont(Font.FACE_SYSTEM, Font.STYLE_BOLD, Font.SIZE_MEDIUM);
        // The initial fire position
        this.firePosition = this.crosshair.getCrosshairPosition();
        // The game's world clock
        this.inactivityTimer = new Timer();
        this.inactivityTimer.schedule(
            new TimerTask()
        {
            // This timer method updates all parts of the game and repaints the game
            public void run()
            {
                // Update the ghost's clock
                boolean ghostRelocated = ghost.handleClockTick();
                // Ghost wasn't hit, and it was relocated, that means the player failed
                if (!shot && ghostRelocated)
                {
                    // Reset game
                    score = 0;
                    level = 0;
                }
                if (ghostRelocated)
                {
                    shot = false;
                    miss = false;
                    level++;
                }

                // Update crosshair position
                crosshair.handleClockTick(gamepad.getAnalogStickPosition());
                repaint();
            }
        },
        0,
        20
        );
    }

    protected void paint(Graphics graphics)
    {
        // Black background
        graphics.setColor(0x000000);
        graphics.fillRect(0, 0, this.getWidth(), this.getHeight());

        // Draw the ghost
        this.ghost.paint(graphics);

        // Draw the shot image
        if (this.shot)
        {
            graphics.drawImage(
                this.shotImage,
                this.firePosition.x,
                this.firePosition.y,
                Graphics.HCENTER | Graphics.VCENTER
            );
        }

        if (this.miss)
        {
            graphics.drawImage(
                this.missImage,
                this.firePosition.x,
                this.firePosition.y,
                Graphics.HCENTER | Graphics.VCENTER
            );
        }

        // Draw the crosshair
        this.crosshair.paint(graphics);

        // Draw the gamepad
        this.gamepad.paint(graphics);

        // Draw the exit button
        graphics.drawImage(
            this.exitButtonPointerDown ? this.exitPressedImage : this.exitImage,
            this.getWidth(),
            0,
            Graphics.RIGHT | Graphics.TOP
        );

        // Draw remaining time
        // Bar shows percentage of time remaining, the maximum is 100.
        int barMaxHeight = 100;
        int barHeight = 0;
        if (!this.shot)
        {
            barHeight = this.ghost.getPercentOfTimeUsed();
        }
        final int barWidth = 20;
        int barX = this.getWidth() - barWidth - 10;
        int barY = (this.getHeight() - barMaxHeight) / 2;
        graphics.setColor(0xffff00);
        graphics.drawRect(barX, barY, barWidth, barMaxHeight);
        graphics.setColor(0x00ff00);
        graphics.fillRect(barX + 1, barY + (barMaxHeight - barHeight) + 1, barWidth - 1, barHeight - 1);

        // Draw score
        graphics.setColor(0xffff00);
        graphics.setFont(this.font);
        String scoreString = Integer.toString(this.score) + " / " + Integer.toString(this.hiScore);
        graphics.drawString(
            scoreString,
            this.getWidth() - this.exitImage.getWidth() - 30,
            10,
            Graphics.TOP | Graphics.RIGHT
        );
    }

    protected void pointerPressed(int x, int y)
    {
        int pointerId = this.getPointerEventId();
        // Check whether the exit button was tapped
        this.exitButtonPointerDown = this.exitButtonHitTest(pointerId, x, y);

        boolean wasFirePressed = this.gamepad.isFireButtonPressed();
        // Pass pointer event to gamepad
        this.gamepad.pointerPressed(pointerId, x, y);
        if (!wasFirePressed && this.gamepad.isFireButtonPressed())
        {
            // Fire only when the fire button was released and pressed again
            this.handleFire();
        }
    }

    protected void pointerDragged(int x, int y)
    {
        int pointerId = this.getPointerEventId();
        // Update exit button state
        this.exitButtonPointerDown = this.exitButtonHitTest(pointerId, x, y);
        // Pass pointer event to gamepad
        this.gamepad.pointerDragged(pointerId, x, y);
    }

    protected void pointerReleased(int x, int y)
    {
        int pointerId = this.getPointerEventId();
        if (this.exitButtonPointerDown && this.exitButtonHitTest(pointerId, x, y))
        {
            // The exit button was pressed and released, exit the game.
            this.exit();
        }
        else
        {
            this.exitButtonPointerDown = false;
        }
        // Pass pointer event to gamepad
        this.gamepad.pointerReleased(pointerId, x, y);
    }

    /**
     * Returns the number of the pointer which caused last pointer event.
     * This method should be called only from pointerPressed(), pointerDragged() and
     * pointerReleased(), otherwise its return value has no meaning.
     */
    private int getPointerEventId()
    {
        String idString = System.getProperty("com.nokia.pointer.number");
        int id = 0;
        if (idString != null)
        {
            id = Integer.parseInt(idString);
        }
        return id;
    }

    private boolean exitButtonHitTest(int pointerId, int x, int y)
    {
        return pointerId == 0
               && x > (this.getWidth() - this.exitImage.getWidth())
               && y < this.exitImage.getHeight();
    }

    private void handleFire()
    {
        // After a successful shot, ignore fire events until the next ghost relocation
        if (this.shot)
        {
            return;
        }

        // Remember the fire position
        this.firePosition = this.crosshair.getCrosshairPosition();

        // Check whether the ghost was shot and hit
        if (this.ghost.hitTest(this.firePosition.x, this.firePosition.y))
        {
            // The ghost was shot
            this.shot = true;
            this.miss = false;
            this.ghost.setShot(true);
            this.score += 5 * this.level;
            if (this.score > this.hiScore)
            {
                this.hiScore = score;
            }
        }
        else
        {
            // The player missed
            this.miss = true;
        }
    }

    private void exit()
    {
        this.inactivityTimer.cancel();
        Display.getDisplay(this.midlet).setCurrent(null);
        this.midlet.notifyDestroyed();
    }
}