/** * Copyright (c) 2012-2013 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.amaze.ui; import javax.microedition.lcdui.Command; import javax.microedition.lcdui.CommandListener; import javax.microedition.lcdui.Displayable; import javax.microedition.lcdui.Font; import javax.microedition.lcdui.Graphics; import javax.microedition.lcdui.game.GameCanvas; import javax.microedition.m3g.Appearance; import javax.microedition.m3g.Background; import javax.microedition.m3g.Camera; import javax.microedition.m3g.Graphics3D; import javax.microedition.m3g.Mesh; import javax.microedition.m3g.Node; import javax.microedition.m3g.RayIntersection; import javax.microedition.m3g.Transform; import javax.microedition.m3g.World; import com.nokia.mid.ui.DeviceControl; import com.nokia.mid.ui.orientation.Orientation; import com.nokia.mid.ui.orientation.OrientationListener; import com.nokia.example.amaze.Main; import com.nokia.example.amaze.gestures.DoubleTapDetector; import com.nokia.example.amaze.gestures.SafeGestureEvent; import com.nokia.example.amaze.gestures.SafeGestureInteractiveZone; import com.nokia.example.amaze.gestures.SafeGestureListener; import com.nokia.example.amaze.gestures.SafeGestureRegistrationManager; import com.nokia.example.amaze.model.MarbleModel; import com.nokia.example.amaze.model.Maze; import com.nokia.example.amaze.model.MyTimer; import com.nokia.example.amaze.sensors.AccelerationProvider; /** * The main UI class. */ public class MazeCanvas extends GameCanvas implements Runnable, CommandListener, OrientationListener, CameraAnimator.Listener, AccelerationProvider.Listener, DoubleTapDetector.Listener, SafeGestureListener, Menu.Listener { // Constants /* Ticks coefficient is used to define the normal speed of the game * regardless of the performance of the phone i.e. it is used to normalise * the changes to the model. In this case it can be understood as the * desired frame rate (or the duration of a single frame to be more * precise). */ public static final float TICKS_COEFFICIENT = 1000.0f / 30.0f; private static final int MILLIS_TO_SLEEP = 5; // Milliseconds to wait in the main loop public static final int MAZE_CORRIDOR_COUNT = 10; public static final float MAZE_SIDE_LENGTH = 200f; // Defines the overall size of the maze private static final float WALL_HEIGHT = 10.f; // Height of the walls private static final int POSITION_TOLERANCE = 12; private static final float DEFAULT_STEP_LENGTH_PER_AXIS = 3.0f; // Amount to advance on each step public static final float ACCELERATION_THRESHOLD = 1.5f; public static final float ACCELERATION_COEFFICIENT_X = 1.5f; public static final float ACCELERATION_COEFFICIENT_Y = 0.3f; private static final int MIN_CALIBRATION_VALUE = -6; public static final int UNDEFINED = -20; private static final int ZOOM_INTERVAL = 10; private static final float DEFAULT_TOP_CAMERA_ANGLE = 75f; private static final float PAUSE_CAMERA_ANGLE = 45f; private static final float DEFAULT_ZOOM = 1.0f; private static final int MIN_ZOOM = -70; // As close as you can zoom private static final int MAX_ZOOM = -MIN_ZOOM; // As far as you can zoom private static final float MAX_TILT_COEFFICIENT = 3.0f; private static final float TILT_THRESHOLD = 0.1f; private static final int LEVEL_TIME = 60; // Initial level time, in seconds private static final int MIN_FPS_STATS_COUNT = 20; // Game states public static final int NOT_STARTED = 0; // When in start menu public static final int TRANSITION_ANIMATION_ONGOING = 1; public static final int ONGOING = 2; public static final int PAUSED = 3; public static final int LEVEL_FINISHED = 4; public static final int GAME_OVER = 5; private static final String RESUME_MENU_ITEM_TEXT = "Resume"; private static final String SET_BACKGROUND_PREFIX_MENU_ITEM_TEXT = "Set background "; private static final String SWITCH_DEBUG_MODE_PREFIX_MENU_ITEM_TEXT = "Switch debug mode "; private static final String RESTART_GAME_MENU_ITEM_TEXT = "Restart game"; public static final int TEXT_COLOR = 0xff81daff; public static final int MARGIN = 5; // Members private final Main _midlet; private WorldBuilder _builder; private Menu _menu; private final MenuItem _blinkingMenuItem = new MenuItem("Tap screen to start game"); private Thread _mainThread = null; private Maze _maze = null; private final MarbleModel _marbleModel; private Graphics3D _graphics3d; // Graphics 3D global instance private World _world; private Camera _topCamera = null; private Camera _povCamera = null; private Background _background = null; private Graphics _graphics = null; private CameraAnimator _cameraAnimator = null; private Appearance _wallClearAppearance = new Appearance(); private Appearance _wallAppearance = new Appearance(); private Mesh _marble = null; // Simple mesh representing the marble private IconButton[] _iconButtons = new IconButton[4]; private InfoDialog _infoDialog = null; private DoubleTapDetector _doubleTapDetector = null; private final Command _exitAndBackCommand = new Command("", Command.EXIT, 1); private double _calibration = UNDEFINED; private double _ax; private double _ay; private boolean _goalReached = false; private float _zoom = DEFAULT_ZOOM; private float _previousZoom = MIN_ZOOM; private float _relativeZoom = 0.5f; private float _cameraOffset = 1; private float _povAngleY = 0.0f; private boolean _povMode = true; private float _prevTiltX = 0f; private float _prevTiltY = 0f; private volatile boolean _running = true; // Flag to keep the drawing going private int _gameState = NOT_STARTED; private volatile int _loopCounter = 1; private volatile float _fps = 0; private float _averageFps = 0; private int _fpsCount = 0; // For calculating the average private volatile long _levelStartTime = 0; private volatile long _timeUsed = 0; private volatile long _timeToBeat = 0; private int _level = 1; private int _width = 0; private int _height = 0; private boolean _isPortrait = true; private boolean _hasBackground = true; private boolean _firstTime = true; public boolean _debugMode = false; /** * Constructor. * @param _midlet The parent MIDlet. */ public MazeCanvas(Main midlet) { super(true); _midlet = midlet; _marbleModel = new MarbleModel(); setCommandListener(this); } /** * Initializes the game canvas. */ public void init() { setFullScreenMode(true); _width = getWidth(); _height = getHeight(); _maze = new Maze(MAZE_CORRIDOR_COUNT, MAZE_SIDE_LENGTH, WALL_HEIGHT); _marbleModel.setPosition(0f, 2 * WALL_HEIGHT + 3f, 0f); _doubleTapDetector = new DoubleTapDetector(this); // Register to listen to the pinch event // Note: If the device doesn't have multitouch support, this will cause // unhandled exception. SafeGestureRegistrationManager.setListener(this, this); SafeGestureInteractiveZone gestureZone = new SafeGestureInteractiveZone(); gestureZone.setGesture(SafeGestureInteractiveZone.GESTURE_PINCH); gestureZone.setRectangle(0, 0, _width, _height); SafeGestureRegistrationManager.register(this, gestureZone); _topCamera = new Camera(); _povCamera = new Camera(); _menu = new Menu(this, _width, _height); // Get the Graphics3D instance _graphics3d = Graphics3D.getInstance(); // Create buttons if (!Main.HAS_ONE_KEY_BACK) { _iconButtons[IconButton.EXIT] = new IconButton(IconButton.EXIT); } _iconButtons[IconButton.INFO] = new IconButton(IconButton.INFO); _iconButtons[IconButton.PAUSE] = new IconButton(IconButton.PAUSE); _iconButtons[IconButton.VIEW_MODE] = new IconButton(IconButton.VIEW_MODE); // Create the world and the maze _world = new World(); _world.addChild(_topCamera); _world.addChild(_povCamera); _world.setActiveCamera(_topCamera); _builder = new WorldBuilder(); _background = _builder.createBackground(_world); _builder.createFloor(_world); _builder.createWallAppearances(_wallAppearance, _wallClearAppearance); _marble = _builder.createMarble(_world); _povCamera.setPerspective(60.f, (float)getWidth() / (float)getHeight(), 0.1f, 1000.f); _povMode = false; _povAngleY = -180f; calculateRelativeZoomAndCameraOffset(); _cameraAnimator = new CameraAnimator(this, _world, _topCamera); createNewLevel(); setRunning(true); AccelerationProvider.getProvider(this); Orientation.addOrientationListener(this); showStartMenu(); } /** * This method implements main (game) loop. * @see java.lang.Runnable#run() */ public void run() { _graphics = getGraphics(); long tempTime = 0; long ticks = 0; while (_running) { long time = System.currentTimeMillis(); if (isShown()) { MyTimer.instance().update(time); if (_gameState == ONGOING) { _timeUsed = time - _levelStartTime; if (_timeToBeat - _timeUsed <= 0) { // Out of time! End the game. endGame(); } updateModel((int)ticks); updateCamera(); if (time - tempTime > 5000) { DeviceControl.setLights(0, 100); // Prevent screen lock tempTime = time; } } // if (_gameState == ONGOING) // Camera animator is updated all the time if running if (_cameraAnimator.running()) { _cameraAnimator.update((int)ticks); if (_cameraAnimator.animationType() == CameraAnimator.PAUSE_ANIMATION && time - tempTime > 20) { shiftBackground(-1); tempTime = time; } } // Tilting the board does not need to be done very often if (_loopCounter % 3 == 0) { tiltBoard(); } // Update the loop counter if (_loopCounter > 9) { _loopCounter = 1; } else { _loopCounter++; } // Draw the graphics draw3D(_graphics, (int)ticks); draw2D(_graphics); // Flush the buffer to the screen flushGraphics(); } // if (isShown()) // Wait for a little and give the other threads the chance to run try { Thread.sleep(MILLIS_TO_SLEEP); } catch (InterruptedException e) { } ticks = System.currentTimeMillis() - time; if (isShown() && (_debugMode || _fpsCount < MIN_FPS_STATS_COUNT)) { // Calculate the FPS _fps = ticks > 0 ? 1000 / ticks : 60; if (_fpsCount < MIN_FPS_STATS_COUNT) { _averageFps += _fps; _fpsCount++; if (_fpsCount == MIN_FPS_STATS_COUNT) { // Enough figures calculated. Calculate now the average. _averageFps /= _fpsCount; if (_firstTime && _averageFps > 1) { if (_averageFps < 20) { // Remove the background to boost the performance setBackground(false); } _firstTime = false; } } } } // if (_debugMode || _fpsCount < MIN_FPS_STATS_COUNT) } // while (_running) } /** * Updates the model i.e. calculates the new position of the marble. */ private final void updateModel(final int ticks) { if (_povMode) { moveMarbleInPovMode(); } else { // Update the marble position float[] position = _marbleModel.position(); _marble.setTranslation(position[0] + MarbleModel.DEFAULT_SIZE, position[1], position[2] + MarbleModel.DEFAULT_SIZE); moveMarble(ticks); } // This does not need to be done very so often if (_loopCounter == 9) { // Check if the marble reached the end if (!_goalReached) { final float[] position = _marbleModel.position(); if (_maze.isAtTheEnd(position[0], position[2], POSITION_TOLERANCE)) { // We're there! _goalReached = true; finishLevel(); } } } } //------------------------------------------------------------------------- // UI interaction implementation -> //------------------------------------------------------------------------- /** * @see javax.microedition.lcdui.Canvas#pointerPressed(int, int) */ protected void pointerPressed(int x, int y) { if (_infoDialog != null) { return; } if (TipBox.visible()) { TipBox.hide(); return; } if (_gameState == MazeCanvas.NOT_STARTED || _gameState == MazeCanvas.PAUSED || _gameState == MazeCanvas.GAME_OVER) { if (!Main.HAS_ONE_KEY_BACK && x > _width - 42 && y > _height - 42) { // Exit button pressed setButtonPressed(IconButton.EXIT); } else if (x < 42 && y > _height - 42) { // Info button pressed setButtonPressed(IconButton.INFO); } else { switch (_gameState) { case NOT_STARTED: { startNewGame(); break; } case PAUSED: { _menu.onPressed(x, y); break; } case GAME_OVER: { _cameraAnimator.startAnimation( CameraAnimator.LEVEL_RESET_ANIMATION_STEP); break; } } } return; } if (_doubleTapDetector.onTapped(x, y)) { // Double tap was detected return; } if (x > _width - 42 && y > _height - 42) { // Pause button pressed setButtonPressed(IconButton.PAUSE); } else if (x < 42 && y > _height - 42) { // View mode button pressed // Do nothing yet } } /** * @see javax.microedition.lcdui.Canvas#pointerReleased(int, int) */ protected void pointerReleased(int x, int y) { if (_gameState == NOT_STARTED || _gameState == PAUSED || _gameState == GAME_OVER) { if (_infoDialog != null) { // Info dialog is visible if (y > _infoDialog.linkTextY() - MARGIN && y < _infoDialog.linkTextY() + _infoDialog.linkTextHeight() + MARGIN) { Main.openLink(InfoDialog.LINK_URL); } else { _infoDialog = null; if (Main.HAS_ONE_KEY_BACK && _gameState == PAUSED) { removeCommand(_exitAndBackCommand); } } return; } if (!Main.HAS_ONE_KEY_BACK && _iconButtons[IconButton.EXIT].pressed() && x > _width - 42 && y > _height - 42) { // Exit button was tapped, do quit _midlet.quitApp(); } else if (_iconButtons[IconButton.INFO].pressed() && x < 42 && y > _height - 42) { // Show info dialog if (_infoDialog == null) { _infoDialog = new InfoDialog(_midlet); if (Main.HAS_ONE_KEY_BACK) { addCommand(_exitAndBackCommand); } } } else { _menu.onReleased(x, y); } setButtonPressed(UNDEFINED); return; } if (_gameState == ONGOING) { if (_iconButtons[IconButton.PAUSE].pressed() && x > _width - 42 && y > _height - 42) { // Do pause pause(); } else if (x < 42 && y > _height - 42) { // Toggle view mode toggleViewMode(); } } setButtonPressed(UNDEFINED); } /** * @see javax.microedition.lcdui.Canvas#pointerDragged(int, int) */ protected void pointerDragged(int x, int y) { if (_gameState == PAUSED && _infoDialog != null) { _menu.onPressed(x, y); } } /** * @see AccelerationProvider.Listener#onDataReceived(double, double, double) */ public void onDataReceived(double ax, double ay, double az) { if (_gameState != ONGOING) { return; } if (_calibration == UNDEFINED) { // Do calibrate now based on the current reading if (_isPortrait) { setCalibration(-ay); } else { setCalibration(-ax); } } if (_isPortrait) { _ax = ax; _ay = ay + _calibration; } else { _ax = ax + _calibration; _ay = ay; } } /** * Reset the zoom. If the current zoom is the default, the previous zoom * level is restored. * * @see DoubleTapDetector.Listener#onDoubleTapDetected() */ public void onDoubleTapDetected() { if (_povMode) { // No zoom in point-of-view mode return; } if (_zoom != DEFAULT_ZOOM) { // Store the current zoom level and reset to default System.out.println("MazeCanvas.onDoubleTapDetected(): Resetting the default zoom."); _previousZoom = _zoom; _zoom = DEFAULT_ZOOM; } else { // Restore the previous zoom System.out.println("MazeCanvas.onDoubleTapDetected(): Restoring the previous zoom."); _zoom = _previousZoom; } calculateRelativeZoomAndCameraOffset(); updateCamera(); } /** * From SafeGestureListener. * * Handles pinch gestures and changes the distance of the camera based on * the gesture. * * @see SafeGestureListener#gestureAction(java.lang.Object, * SafeGestureInteractiveZone, SafeGestureEvent) */ public void gestureAction(Object container, SafeGestureInteractiveZone gestureInteractiveZone, SafeGestureEvent gestureEvent) { if (!_povMode && gestureEvent.getType() == SafeGestureInteractiveZone.GESTURE_PINCH) { if (gestureEvent.getPinchDistanceChange() < 0) { // If the gesture was inwards, scale smaller doZoom(ZOOM_INTERVAL); } else if (gestureEvent.getPinchDistanceChange() > 0) { // If the gesture was outwards, scale larger doZoom(-ZOOM_INTERVAL); } } } /** * @see Menu.Listener#onMenuItemSelected(int) */ public void onMenuItemSelected(int index, MenuItem item) { if (_gameState == MazeCanvas.PAUSED && item != null) { final String text = item.text(); if (text.equals(RESUME_MENU_ITEM_TEXT)) { // Resume resume(); } else if (text.startsWith(SET_BACKGROUND_PREFIX_MENU_ITEM_TEXT)) { // Toggle background setBackground(!_hasBackground); item.setText(SET_BACKGROUND_PREFIX_MENU_ITEM_TEXT + (_hasBackground ? "off" : "on")); } else if (text.startsWith(SWITCH_DEBUG_MODE_PREFIX_MENU_ITEM_TEXT)) { // Toggle debug mode _debugMode = !_debugMode; item.setText(SWITCH_DEBUG_MODE_PREFIX_MENU_ITEM_TEXT + (_debugMode ? "off" : "on")); } else if (text.equals(RESTART_GAME_MENU_ITEM_TEXT)) { // Restart the game startNewGame(); } } } /** * Sets the accelerometer calibration value for the Y axis. * @param calibrationY The value to set. */ public void setCalibration(final double calibration) { _calibration = calibration; if (_calibration < MIN_CALIBRATION_VALUE && _calibration != UNDEFINED) { _calibration = MIN_CALIBRATION_VALUE; } } /** * Resets the input state. */ public void resetMarbleVelocity() { _marbleModel.setVelocity(new float[] { 0f, 0f, 0f }); } //------------------------------------------------------------------------- // <- UI interaction implementation //------------------------------------------------------------------------- /* * @see javax.microedition.lcdui.CommandListener#commandAction(javax.microedition.lcdui.Command, javax.microedition.lcdui.Displayable) */ public void commandAction(Command command, Displayable displayble) { if (command == _exitAndBackCommand) { switch (_gameState) { case NOT_STARTED: if (_infoDialog != null) { // Hide the info dialog _infoDialog = null; } else { _midlet.quitApp(); } break; case ONGOING: pause(); break; case PAUSED: if (_infoDialog != null) { // Hide the info dialog _infoDialog = null; removeCommand(_exitAndBackCommand); } break; case GAME_OVER: _midlet.quitApp(); break; default: break; } } } /** * @see com.nokia.mid.ui.orientation.OrientationListener#displayOrientationChanged(int) */ public void displayOrientationChanged(int newDisplayOrientation) { System.out.println("MazeCanvas.displayOrientationChanged(): " + newDisplayOrientation); boolean orientationChanged = false; if (newDisplayOrientation == Orientation.ORIENTATION_LANDSCAPE && !_povMode && _isPortrait) { orientationChanged = true; _isPortrait = false; } else if (!_isPortrait) { orientationChanged = true; _isPortrait = true; } if (orientationChanged) { _iconButtons[IconButton.PAUSE].setIsPortrait(_isPortrait); _topCamera.setTransform(defaultTopCameraTransform()); setCalibration(UNDEFINED); } } /** * A lot of the transition logic is done here. * @see com.nokia.example.amaze.ui.CameraAnimator.Listener#onAnimationFinished(int) */ public void onAnimationFinished(int animationType) { System.out.println("MazeCanvas.onAnimationFinished(): " + animationType); switch (animationType) { case CameraAnimator.PAUSE_ANIMATION: { // Start a new animation to go to the current game view transitionToCurrentViewMode(); break; } case CameraAnimator.TRANSITION_ANIMATION_TO_POV: { _world.setActiveCamera(_povCamera); _marble.setRenderingEnable(false); // Do not render the marble in POV mode _gameState = ONGOING; updateCamera(); // Trigger a new average FPS calculation _averageFps = 0; _fpsCount = 0; break; } case CameraAnimator.TRANSITION_ANIMATION_TO_TOP: { _world.setActiveCamera(_topCamera); _marble.setRenderingEnable(true); _gameState = ONGOING; updateCamera(); // Trigger a new average FPS calculation _averageFps = 0; _fpsCount = 0; break; } case CameraAnimator.LEVEL_RESET_ANIMATION_STEP: { if (_gameState == LEVEL_FINISHED) { // Advance a level nextLevel(); transitionToCurrentViewMode(); } else { // Game over, create a new level and go to the start view createNewLevel(); showStartMenu(); } break; } } // switch (animationType) } /** * Starts/stops the game thread. * @param running If true, will start the thread/drawing. If false, will * stop the game thread. */ public synchronized void setRunning(final boolean running) { _running = running; if (_running) { if (_mainThread == null) { _mainThread = new Thread(this); _mainThread.start(); } } } /** * To control the background setting. Painting the background has a * significant impact on performance with low-performing devices. * @param on If true, will paint the background. */ public void setBackground(final boolean on) { if (on && _background == null) { // Create the background _background = _builder.createBackground(_world); } else if (!on && _background != null) { // Clear the background _world.setBackground(null); _background = null; } _hasBackground = on; // Trigger a new average FPS calculation _averageFps = 0; _fpsCount = 0; } /** * Constructs and shows the start menu. */ private void showStartMenu() { _gameState = NOT_STARTED; // Create the start menu _menu.clear(); MyTimer.instance().removeListener(_blinkingMenuItem); _blinkingMenuItem.setText("Tap screen to start game"); MyTimer.instance().addListener(_blinkingMenuItem, true, MenuItem.BLINK_INTEVAL); _menu.addItem(_blinkingMenuItem); // Set the camera final float width = (float)_width; final float height = (float)_height; _topCamera.setPerspective(60.f, width / height, 0.1f, 1000.f); Transform transform = new Transform(); transform.postRotate(PAUSE_CAMERA_ANGLE, -1f, 0f, 0f); transform.postTranslate(0f, 0f, DEFAULT_ZOOM * MAZE_SIDE_LENGTH); _topCamera.setTransform(transform); updateCamera(); // Start the pause animation _cameraAnimator.startAnimation(CameraAnimator.PAUSE_ANIMATION); if (Main.HAS_ONE_KEY_BACK) { addCommand(_exitAndBackCommand); } } /** * Starts a new game. */ public void startNewGame() { _goalReached = false; _level = 1; _levelStartTime = System.currentTimeMillis(); _timeToBeat = LEVEL_TIME * 1000; _timeUsed = 0; if (_gameState == NOT_STARTED) { // Starting the game from the start menu (view) // No need to create a new level MyTimer.instance().removeListener(_blinkingMenuItem); } else { createNewLevel(); } if (_cameraAnimator.running()) { _cameraAnimator.stopAnimation(); } else { _gameState = ONGOING; } resetMarbleVelocity(); TipBox.showTips(_height); // This only works when called the first time if (Main.HAS_ONE_KEY_BACK) { addCommand(_exitAndBackCommand); } } /** * Pauses the game. */ public void pause() { if (_gameState == PAUSED) { // Already paused return; } // Create the pause menu _menu.clear(); _menu.addItem(RESUME_MENU_ITEM_TEXT); _menu.addItem(SET_BACKGROUND_PREFIX_MENU_ITEM_TEXT + (_hasBackground ? "off" : "on")); _menu.addItem(SWITCH_DEBUG_MODE_PREFIX_MENU_ITEM_TEXT + (_debugMode ? "off" : "on")); _menu.addItem(RESTART_GAME_MENU_ITEM_TEXT); Transform targetTransform = new Transform(); targetTransform.postRotate(PAUSE_CAMERA_ANGLE, -1f, 0f, 0f); targetTransform.postTranslate(0f, 0f, DEFAULT_ZOOM * MAZE_SIDE_LENGTH); _cameraAnimator.startAnimation(CameraAnimator.PAUSE_ANIMATION, targetTransform, null); // Resume will result in re-calibration setCalibration(UNDEFINED); // Update the game state _gameState = PAUSED; if (Main.HAS_ONE_KEY_BACK) { removeCommand(_exitAndBackCommand); } } /** * Resumes the current game. */ public void resume() { System.out.println("MazeCanvas.resume()"); if (_gameState == PAUSED) { _levelStartTime = System.currentTimeMillis() - _timeUsed; _cameraAnimator.stopAnimation(); if (Main.HAS_ONE_KEY_BACK) { addCommand(_exitAndBackCommand); } } else { System.out.println("MazeCanvas.resume(): No game to resume!"); } } /** * Ends the current game and shows the game over view. */ public void endGame() { _gameState = GAME_OVER; _menu.clear(); MenuItem item = new MenuItem("Game Over"); item.setDisabled(true); _menu.addItem(item); _menu.addItem("You reached level " + _level); _blinkingMenuItem.setText("Tap screen to continue"); MyTimer.instance().addListener(_blinkingMenuItem, true, MenuItem.BLINK_INTEVAL); _menu.addItem(_blinkingMenuItem); _cameraAnimator.startAnimation(CameraAnimator.PAUSE_ANIMATION); if (Main.HAS_ONE_KEY_BACK) { addCommand(_exitAndBackCommand); } } /** * Finishes the level and prepares to start a new one. This method should be * called when the marble gets to the end of the maze. */ public void finishLevel() { _gameState = LEVEL_FINISHED; _menu.clear(); _menu.addItem("Level " + _level + " finished!"); _cameraAnimator.startAnimation(CameraAnimator.LEVEL_RESET_ANIMATION_STEP); } /** * Modifies the current zoom value. * @param diff The difference to current zoom. */ public void doZoom(final float diff) { _zoom += diff; if (_zoom < MIN_ZOOM) { _zoom = MIN_ZOOM; } else if (_zoom > MAX_ZOOM) { _zoom = MAX_ZOOM; } calculateRelativeZoomAndCameraOffset(); updateCamera(); } /** * Switches from top view to normal view and vice versa. */ public void toggleViewMode() { _povMode = !_povMode; System.out.println("MazeCanvas.toggleViewMode(): To " + (_povMode ? "POV" : "TOP")); if (_povMode) { _iconButtons[IconButton.VIEW_MODE].setPressed(true); } else { _iconButtons[IconButton.VIEW_MODE].setPressed(false); } // Do the transition animation. transitionToCurrentViewMode(); } /** * Sets the button with the given ID in pressed state. If the ID does not * match any of the buttons, all buttons are set in unpressed state. * @param id The ID of the buttonn. */ public void setButtonPressed(int id) { if (!Main.HAS_ONE_KEY_BACK) { _iconButtons[IconButton.EXIT].setPressed(false); } _iconButtons[IconButton.INFO].setPressed(false); _iconButtons[IconButton.PAUSE].setPressed(false); if (id == IconButton.EXIT || id == IconButton.INFO || id == IconButton.PAUSE) { _iconButtons[id].setPressed(true); } } /** * Shifts the background to support the illusion when the camera rotates. * @param delta The amount the background is shifted. A positive value * shifts the background to the right and negative to the * left. */ public final void shiftBackground(final int delta) { if (_background != null) { _background.setCrop(_background.getCropX() + delta, 0, _width, _height); } } /** * Updates the properties of the active camera. Note that this is very * expensive operation! */ public void updateCamera() { final float[] position = _marbleModel.position(); if (_povMode) { _povCamera.setOrientation(_povAngleY, 0f, 1f, 0f); _povCamera.setTranslation(position[0], 10f, position[2]); } else { // Follow the marble with the camera but if the view is zoomed out // i.e. the maze and the marble are smaller, then try to show more // of the maze. _topCamera.setTranslation(position[0] / _cameraOffset, _zoom, position[2] / _cameraOffset); } } /** * Checks the possible collision between the marble and the walls. * @param x * @param z * @return - 0 if no collision * - 1 if collision on X axis * - 2 if collision on Z axis * - 3 if collision on both axis */ private final int marbleCollidesAt(final float x, final float z) { // First check if the target is inside the maze area float mazeSize = MAZE_SIDE_LENGTH / 2 - 2; int outside = 0; if (x - MarbleModel.DEFAULT_SIZE_HALVED <= -mazeSize || x + MarbleModel.DEFAULT_SIZE_HALVED > mazeSize) { outside = 1; } if (z - MarbleModel.DEFAULT_SIZE_HALVED <= -mazeSize || z + MarbleModel.DEFAULT_SIZE_HALVED > mazeSize) { outside += 2; } if (outside > 0) { System.out.println("MazeCanvas.canMoveTo(): Would go outside of the maze."); return outside; } RayIntersection intersection = new RayIntersection(); final float minimumDistance = MarbleModel.DEFAULT_SIZE + 0.5f; if (_world.pick(-1, x, MarbleModel.DEFAULT_SIZE_HALVED, z, MarbleModel.DEFAULT_SIZE_HALVED, 0.1f, MarbleModel.DEFAULT_SIZE_HALVED, intersection)) { Node selected = intersection.getIntersected(); if (selected instanceof Mesh) { float distance = intersection.getDistance(); if (distance < minimumDistance) { float[] position = _marbleModel.position(); int retval = 0; // Check X axis if (_world.pick(-1, x, MarbleModel.DEFAULT_SIZE_HALVED, position[2], MarbleModel.DEFAULT_SIZE_HALVED, 0.1f, MarbleModel.DEFAULT_SIZE_HALVED, intersection)) { distance = intersection.getDistance(); if (distance < minimumDistance) { retval = 1; } } // Check Z axis if (_world.pick(-1, position[0], MarbleModel.DEFAULT_SIZE_HALVED, z, MarbleModel.DEFAULT_SIZE_HALVED, 0.1f, MarbleModel.DEFAULT_SIZE_HALVED, intersection)) { distance = intersection.getDistance(); if (distance < minimumDistance) { retval += 2; } } return retval; } } } return 0; } /** * Creates a new level. The old level, if one exists, will be destroyed. */ private void createNewLevel() { System.out.println("MazeCanvas.createNewLevel()"); _builder.createNewMaze(_world, _maze, _wallAppearance); // Set the marble's initial location _marbleModel.setPosition(_maze.startPosition()); } /** * Recreates the maze and re-initializes the values for the next level. */ private void nextLevel() { System.out.println("MazeCanvas.nextLevel()"); _goalReached = false; _level++; _levelStartTime = System.currentTimeMillis(); _timeToBeat = LEVEL_TIME * 1000 + _timeToBeat - _timeUsed // Add the time left from previous level - (_level - 1) * 5000; // Take 5 seconds off on each level if (_timeToBeat < 10000) { // Keep it ten seconds minimum _timeToBeat = 10000; } _timeUsed = 0; createNewLevel(); updateCamera(); resetMarbleVelocity(); } /** * Returns the default top camera transform. */ private Transform defaultTopCameraTransform() { Transform transform = new Transform(); if (_isPortrait) { transform.postRotate(DEFAULT_TOP_CAMERA_ANGLE, -1f, 0f, 0f); } else { transform.postRotate(90, -1f, 0f, 0f); transform.postRotate(90 - DEFAULT_TOP_CAMERA_ANGLE, 0f, -1f, 0f); } transform.postTranslate(0f, 0f, MAZE_SIDE_LENGTH); return transform; } /** * Starts a camera animation based on the current view mode setting. */ private void transitionToCurrentViewMode() { if (_povMode) { _cameraAnimator.startAnimation(CameraAnimator.TRANSITION_ANIMATION_TO_POV, _povCamera); } else { Transform transform = defaultTopCameraTransform(); float[] orientation = new float[4]; orientation[0] = 0; orientation[1] = 0; orientation[2] = 0; orientation[3] = 1; _cameraAnimator.startAnimation(CameraAnimator.TRANSITION_ANIMATION_TO_TOP, transform, orientation); } } /** * Calculates and returns the relative zoom [0.0 - 1.0], where 0 is the * maximum zoom in and 1 the maximum zoom out. * @return The relative zoom. */ private final void calculateRelativeZoomAndCameraOffset() { final int scale = MazeCanvas.MAX_ZOOM - MazeCanvas.MIN_ZOOM; _relativeZoom = (_zoom - (float)MazeCanvas.MIN_ZOOM) / scale; _cameraOffset = _relativeZoom * 4; if (_cameraOffset < 1) { _cameraOffset = 1; } } /** * Tilts the board based on the current accelerometer sensor readings. */ private final void tiltBoard() { // Tilt the board based on the accelerometer readings final float tiltX = (float)(-_ax); final float tiltY = (float)(-_ay); boolean updateTilt = false; if (Math.abs(tiltX - _prevTiltX) > TILT_THRESHOLD) { _prevTiltX = tiltX; updateTilt = true; } if (Math.abs(tiltY - _prevTiltY) > TILT_THRESHOLD) { _prevTiltY = tiltY; updateTilt = true; } if (updateTilt) { // Calculate the tilt coefficient based on the current zoom final float coefficient = _relativeZoom * MAX_TILT_COEFFICIENT; // Update the camera angle based on the accelerometer values _topCamera.setOrientation(_prevTiltY * coefficient, 1f, 0f, 0f); _topCamera.postRotate(_prevTiltX * coefficient, 0f, 0f, 1f); } } /** * Moves the marble when in POV mode. * * Note that this method does not update the camera. */ private final void moveMarbleInPovMode() { // Check if we should be turning to some direction if (Math.abs(_ax) > ACCELERATION_THRESHOLD) { _povAngleY += (float)_ax * ACCELERATION_COEFFICIENT_X; shiftBackground((int)(-_ax * ACCELERATION_COEFFICIENT_X) * 3); } // Check if we should be moving back or forth final float absCalibratedAy =(float)Math.abs(_ay); if (absCalibratedAy > ACCELERATION_THRESHOLD) { final int mark = (_ay > 0) ? 1 : -1; final float stepLength = DEFAULT_STEP_LENGTH_PER_AXIS * (absCalibratedAy * ACCELERATION_COEFFICIENT_Y); final float stepX = mark * stepLength * (float)Math.sin(Math.toRadians(_povAngleY)); final float stepZ = mark * stepLength * (float)Math.cos(Math.toRadians(_povAngleY)); final float[] position = _marbleModel.position(); final int collision = marbleCollidesAt(position[0] + stepX, position[2] + stepZ); if (collision == 0) { _marbleModel.move(stepX, 0, stepZ); } } } /** * Calculates the velocity of the marble based on the current accelerometer * sensor readings and moves the marble based on the velocity and the * collision detection. * * Note that this method does not update the camera. * * @param ticks The milliseconds since last time the method was called. */ private final void moveMarble(final int ticks) { // Calculate the velocity float[] velocity = _marbleModel.calculateVelocity(_ax, _ay, ticks); // Check for collisions int collision = -1; int safety = 0; // To prevent forever loop final float[] position = _marbleModel.position(); final float ticksCoefficient = (float)ticks / MazeCanvas.TICKS_COEFFICIENT; while (collision != 0 && safety < 4) { collision = marbleCollidesAt( position[0] + velocity[0] * ticksCoefficient, position[2] + velocity[2] * ticksCoefficient); if (collision == 1 || collision == 3) { // X axis or both velocity[0] = -velocity[0] * MarbleModel.DEFAULT_BOUNCE_DRAG; } else if (collision == 2 || collision == 3) { // Z axis or both velocity[2] = -velocity[2] * MarbleModel.DEFAULT_BOUNCE_DRAG; } safety++; } // Set the velocity and move the marble _marbleModel.setVelocity(velocity); _marbleModel.move(ticks); } /** * Draws 2D graphics e.g. information on top of the screen. * @param graphics */ private void draw2D(Graphics graphics) { graphics.setColor(TEXT_COLOR); final Font defaultFont = Font.getDefaultFont(); graphics.setFont(defaultFont); if (_debugMode) { // Draw the accelerometer sensor readings in the bottom String ax = String.valueOf(_ax); String ay = String.valueOf(_ay); ax = ax.length() > 4 ? ax.substring(0, 4) : ax; ay = ay.length() > 4 ? ay.substring(0, 4) : ay; graphics.drawString("A: [" + ax + ", " + ay + "]", 42, _height - defaultFont.getHeight(), Graphics.BOTTOM | Graphics.LEFT); // Draw the FPS graphics.drawString("FPS: " + _fps + " (" + ((_fpsCount < MIN_FPS_STATS_COUNT) ? -1 : _averageFps) + ")", 42, _height, Graphics.BOTTOM | Graphics.LEFT); } if (_isPortrait && (_gameState == ONGOING || _gameState == PAUSED || _gameState == GAME_OVER)) { // Draw the current level in the top-left corner StringBuffer buffer = new StringBuffer(); buffer.append("Level ").append(_level); graphics.drawString(buffer.toString(), MARGIN, MARGIN, Graphics.TOP | Graphics.LEFT); // Draw the time left in the top-right corner buffer.delete(0, buffer.length()); buffer.append("Time left: "); buffer.append((_timeToBeat - _timeUsed) / 1000); graphics.drawString(buffer.toString(), _width - MARGIN, MARGIN, Graphics.TOP | Graphics.RIGHT); } if (_gameState == NOT_STARTED || _gameState == PAUSED || _gameState == GAME_OVER) { if (_infoDialog == null) { // Draw the menu _menu.paint(graphics); // Draw the info and exit button _iconButtons[IconButton.INFO].paint(graphics, MARGIN, _height - 36); if (!Main.HAS_ONE_KEY_BACK) { _iconButtons[IconButton.EXIT].paint(graphics, _width - 36, _height - 36); } } else { _infoDialog.paint(graphics); } } else if (_gameState == ONGOING) { // Draw the view mode and pause buttons _iconButtons[IconButton.PAUSE].paint(graphics, _width - 36, _height - 36); if (_isPortrait) { _iconButtons[IconButton.VIEW_MODE].paint(graphics, MARGIN, _height - 36); if (TipBox.visible()) { // Draw the tip box TipBox.paint(graphics); } } } else if (_gameState == LEVEL_FINISHED) { _menu.paint(graphics); // Shows "Level X finished!" } } /** * Renders the 3D objects of the scene. * @param graphics The Graphics instance. * @param ticks The time elapsed since last rendering in milliseconds. */ private final void draw3D(final Graphics graphics, final int ticks) { boolean bound = false; try { // Bind the target _graphics3d.bindTarget(graphics); bound = true; // Advance the animation _world.animate(ticks); // Do the rendering _graphics3d.render(_world); } finally { // Release the target if (bound) { _graphics3d.releaseTarget(); } } } }