/* * 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.amaze.ui; import javax.microedition.io.ConnectionNotFoundException; 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.model.GameModel; import com.nokia.example.amaze.model.MarbleModel; import com.nokia.example.amaze.model.Maze; import com.nokia.example.amaze.model.MyTimer; /** * The main UI class. */ public class MazeCanvas extends GameCanvas implements Runnable, OrientationListener, CameraAnimator.Listener { // Constants private final static int MILLIS_PER_TICK = 5; // Milliseconds to wait in the main loop private final static float DEFAULT_TOP_CAMERA_ANGLE = 75f; private final static float PAUSE_CAMERA_ANGLE = 45f; private final static float DEFAULT_ZOOM = 1.0f; public final static int MIN_ZOOM = -70; // As close as you can zoom public final static int MAX_ZOOM = -MIN_ZOOM; // As far as you can zoom public final static float ANGLE_STEP = 8.0f; // Amount to rotate on each step private final static float MAX_TILT_COEFFICIENT = 3.0f; private final static float TILT_THRESHOLD = 0.1f; private final static int LEVEL_TIME = 60; // Initial level time, in seconds private final static int MIN_UPDATE_INTERVAL = 20; // Milliseconds private final static int MIN_FPS_STATS_COUNT = 20; // Game states public final static int NOT_STARTED = 0; // When in start menu public final static int TRANSITION_ANIMATION_ONGOING = 1; public final static int ONGOING = 2; public final static int PAUSED = 3; public final static int LEVEL_FINISHED = 4; public final static int GAME_OVER = 5; private final static int MARGIN = 6; public final static int TEXT_COLOR = 0xff81daff; // 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 GameModel _gameModel = null; private InteractionManager _interactionManager = null; private Maze _maze = null; private MarbleModel _marbleModel = null; 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 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; // The length of single steps to directions X and Z // Stored to avoid extra trigonometric calculations private float _stepLengthX = 0.0f; private float _stepLengthZ = -GameModel.DEFAULT_STEP_LENGTH_PER_AXIS; private volatile int _loopCounter = 1; private long _drawingTime = 0; 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; } /* * Methods from GameCanvas. * Implementation moved to InteractionManager to reduce the size of this * class. */ protected void keyPressed(int key) { _interactionManager.onKeyPressed(key); } protected void pointerPressed(int x, int y) { _interactionManager.onPointerPressed(x, y); } protected void pointerReleased(int x, int y) { _interactionManager.onPointerReleased(x, y); } protected void pointerDragged(int x, int y) { _interactionManager.onPointerDragged(x, y); } protected void pointerRepeated(int x, int y) { _interactionManager.onPointerRepeated(x, y); } /** * From Runnable. */ public void run() { _graphics = getGraphics(); long tempTime = System.currentTimeMillis(); long tempTimeForPause = 0; while (_running) { long startTime = System.currentTimeMillis(); if (isShown()) { MyTimer.instance().update(startTime); if (_gameState == ONGOING) { //--------------------------------------------------------- // 1. Do operations that need to be done as often as possible //--------------------------------------------------------- _timeUsed = startTime - _levelStartTime; if (_timeToBeat - _timeUsed <= 0) { // Out of time! End the game. endGame(); } if (!_povMode) { // Update the marble position float[] position = _marbleModel.position(); _marble.setTranslation( position[0] + MarbleModel.DEFAULT_SIZE, position[1], position[2] + MarbleModel.DEFAULT_SIZE); } //--------------------------------------------------------- // 2. Do operations that need to be done less often //--------------------------------------------------------- if (startTime - tempTime > MIN_UPDATE_INTERVAL) { //----------------------------------------------------- // 3. Do operations that need to be done every // MIN_UPDATE_INTERVAL milliseconds //----------------------------------------------------- // Move the marble and update the camera if (_povMode) { moveMarbleInPovMode(); } else { moveMarble(); } updateCamera(); //----------------------------------------------------- // 4. Do operations that don't need to be done very // MIN_UPDATE_INTERVAL milliseconds //----------------------------------------------------- if (_loopCounter % 2 == 0) { tiltBoard(); } if (_loopCounter % 9 == 0) { // Check if the marble reached the end if (!_gameModel.goalReached() && _gameModel.isAtEnd()) { // We're there! _gameModel.setGoalReached(true); finishLevel(); } } // Update the counter if (_loopCounter > 9) { _loopCounter = 1; } else { _loopCounter++; } tempTime = startTime; } } // if (_gameState == ONGOING) // Camera animator is updated all the time if running if (_cameraAnimator.running()) { _cameraAnimator.update(); if (_cameraAnimator.animationType() == CameraAnimator.PAUSE_ANIMATION && startTime - tempTimeForPause > MIN_UPDATE_INTERVAL) { shiftBackground(-1); tempTimeForPause = startTime; } } // Update graphics every time updateGraphics(); } // if (isShown()) // Wait for a little and give the other threads the chance to run long timeTaken = System.currentTimeMillis() - startTime; if (timeTaken < MILLIS_PER_TICK) { synchronized (this) { try { wait(MILLIS_PER_TICK - timeTaken); } catch (InterruptedException e) { e.printStackTrace(); } } } else { Thread.yield(); } } } /** * From OrientationListener. */ 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()); _interactionManager.setCalibration(InteractionManager.UNDEFINED); } } /** * From CameraAnimator.Listener. * A lot of the transition logic is done here. */ 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(); } } } /** * Initializes the game canvas. */ public void init() { _gameModel = GameModel.instance(); _interactionManager = new InteractionManager(this, _gameModel); _maze = _gameModel.maze(); _marbleModel = _gameModel.marble(); _topCamera = new Camera(); _povCamera = new Camera(); setFullScreenMode(true); _width = getWidth(); _height = getHeight(); _menu = new Menu(_interactionManager, _width, _height); // Get the Graphics3D instance _graphics3d = Graphics3D.getInstance(); // Create buttons _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); _interactionManager.onUIInitialized(); Orientation.addOrientationListener(this); showStartMenu(); } /** * 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; } /** * @return True if the background is painted, false otherwise. */ public final boolean hasBackground() { return _hasBackground; } /** * Starts a new game. */ public void startNewGame() { _gameModel.setGoalReached(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; } _interactionManager.reset(); DeviceControl.setLights(0, 100); // Prevent screen lock TipBox.showTips(); // This only works when called the first time } /** * Resumes the current game. */ public void resume() { System.out.println("MazeCanvas::resume()"); if (_gameState == PAUSED) { _levelStartTime = System.currentTimeMillis() - _timeUsed; _cameraAnimator.stopAnimation(); } else { System.out.println("MazeCanvas::resume(): No game to resume!"); } } /** * Pauses the game. */ public void pause() { if (_gameState == PAUSED) { // Already paused return; } // Create the pause menu _menu.clear(); _menu.addItem("Resume"); _menu.addItem((_hasBackground ? "Set background off" : "Set background on")); _menu.addItem((_debugMode ? "Switch debug mode off" : "Switch debug mode on")); _menu.addItem("Restart game"); Transform targetTransform = new Transform(); targetTransform.postRotate(PAUSE_CAMERA_ANGLE, -1f, 0f, 0f); targetTransform.postTranslate(0f, 0f, DEFAULT_ZOOM * GameModel.MAZE_SIDE_LENGTH); _cameraAnimator.startAnimation(CameraAnimator.PAUSE_ANIMATION, targetTransform, null); // Resume will result in re-calibration _interactionManager.setCalibration(InteractionManager.UNDEFINED); // Update the game state _gameState = PAUSED; } /** * 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); } /** * 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); } /** * @return True if in portrait, false otherwise. */ public final boolean isPortrait() { return _isPortrait; } /** * Sets the info dialog visibility. * @param visible If true, the info dialog is shown, otherwise it is hidden. */ public void setInfoDialogVisible(final boolean visible) { if (visible) { if (_infoDialog == null) { _infoDialog = new InfoDialog(); } } else { _infoDialog = null; } } /** * @return True if the info dialog is visible, false otherwise. */ public final boolean infoDialogVisible() { return (_infoDialog != null); } /** * Tries to open the Nokia Developer Projects webpage. If successful, this * app will be terminated. */ public void openProjectsLink() { System.out.println("MazeCanvas::openProjectsLink()"); try { if (_midlet.platformRequest("http://projects.developer.nokia.com/amaze")) { quit(); } } catch (ConnectionNotFoundException cnfe) {} } /** * Exits the application. */ public final void quit() { _midlet.quitApp(); } /** * @return The game state. */ public final int gameState() { return _gameState; } /** * @return The menu instance. */ public final Menu menu() { return _menu; } /** * @return The top camera. */ public final Camera topCamera() { return _topCamera; } /** * @return The camera animator instance. */ public final CameraAnimator cameraAnimator() { return _cameraAnimator; } /** * 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(); } /** * Reset the zoom. If the current zoom is the default, the previous zoom * level is restored. */ public void resetZoom() { 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::resetZoom(): Resetting the default zoom."); _previousZoom = _zoom; _zoom = DEFAULT_ZOOM; } else { // Restore the previous zoom System.out.println("MazeCanvas::resetZoom(): Restoring the previous zoom."); _zoom = _previousZoom; } calculateRelativeZoomAndCameraOffset(); updateCamera(); } /** * @param angle The point-of-view angle to set. */ public void setPovAngleY(final float angle) { _povAngleY = angle; _stepLengthX = GameModel.DEFAULT_STEP_LENGTH_PER_AXIS * (float)Math.sin(Math.toRadians(_povAngleY)); _stepLengthZ = GameModel.DEFAULT_STEP_LENGTH_PER_AXIS * (float)Math.cos(Math.toRadians(_povAngleY)); } /** * @return The point-of-view angle. */ public final float povAngleY() { return _povAngleY; } /** * Rotates the point-of-view angle based on the given difference. * @param diff The rotation difference. */ public void rotatePovAngleY(final float diff) { _povAngleY += diff; _stepLengthX = GameModel.DEFAULT_STEP_LENGTH_PER_AXIS * (float)Math.sin(Math.toRadians(_povAngleY)); _stepLengthZ = GameModel.DEFAULT_STEP_LENGTH_PER_AXIS * (float)Math.cos(Math.toRadians(_povAngleY)); } // Methods for getting the steps on both axis public final float stepLengthX() { return _stepLengthX; } public final float stepLengthZ() { return _stepLengthZ; } /** * @return True if the point-of-view mode is on, false if the top camera * mode is on. */ public final boolean povMode() { return _povMode; } /** * 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(); } /** * @param id * @param button */ public void setButtonPressed(int id) { _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); } } /** * @return The ID of the button pressed, or -1 if no button is pressed. */ public final int buttonPressed() { if (_iconButtons[IconButton.EXIT].pressed()) { return IconButton.EXIT; } if (_iconButtons[IconButton.INFO].pressed()) { return IconButton.INFO; } if (_iconButtons[IconButton.PAUSE].pressed()) { return IconButton.PAUSE; } return -1; } /** * 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); } } /** * @return The average measured FPS rate or -1 if not calculated. */ public final float averageFps() { if (_fpsCount < MIN_FPS_STATS_COUNT) { return -1f; } return _averageFps; } /** * 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); } } /** * Decides whether it is allowed to move to a given place or not. This * prevents the player walking through walls and going outside the maze. * @param stepX The distance to move on X axis. * @param stepZ The distance to move on Z axis. * @return - 0 if no collision * - 1 if collision on X axis * - 2 if collision on Z axis * - 3 if collision on both axis */ public final int detectCollision(final float stepX, final float stepZ) { float[] position = _marbleModel.position(); float x = position[0] + stepX; float z = position[2] + stepZ; // First check if the target is inside the maze area float mazeSize = GameModel.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; } return marbleCollidesAt(x, z); } /** * 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) { 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; } /** * 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 * GameModel.MAZE_SIDE_LENGTH); _topCamera.setTransform(transform); updateCamera(); // Start the pause animation _cameraAnimator.startAnimation(CameraAnimator.PAUSE_ANIMATION); } /** * 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()"); _gameModel.setGoalReached(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(); _interactionManager.reset(); } /** * 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, GameModel.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)(-_interactionManager.ax()); final float tiltY = (float)(-_interactionManager.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() { final double ax = _interactionManager.ax(); final double ay = _interactionManager.ay(); // Check if we should be turning to some direction if (Math.abs(ax) > InteractionManager.ACCELERATION_THRESHOLD) { rotatePovAngleY((float)ax * InteractionManager.ACCELERATION_COEFFICIENT_X); shiftBackground((int)(-ax * InteractionManager.ACCELERATION_COEFFICIENT_X) * 3); } // Check if we should be moving back or forth final float absCalibratedAy =(float)Math.abs(ay); if (absCalibratedAy > InteractionManager.ACCELERATION_THRESHOLD) { final int mark = (ay > 0) ? 1 : -1; final float stepLength = GameModel.DEFAULT_STEP_LENGTH_PER_AXIS * (absCalibratedAy * InteractionManager.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 int collision = detectCollision(stepX, 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. */ private final void moveMarble() { // Calculate the velocity float[] velocity = _marbleModel.calculateVelocity(_interactionManager.ax(), _interactionManager.ay()); // Check for collisions int collision = -1; int safety = 0; // To prevent forever loop while (collision != 0 && safety < 4) { collision = detectCollision(velocity[0], velocity[2]); 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(velocity[0], 0, velocity[2]); } /** * Draws the graphics. */ private final void updateGraphics() { if (_debugMode || _fpsCount < MIN_FPS_STATS_COUNT) { _drawingTime = System.currentTimeMillis(); } // Draw the graphics draw3D(_graphics); draw2D(_graphics); if (_debugMode || _fpsCount < MIN_FPS_STATS_COUNT) { // Calculate the FPS _drawingTime = System.currentTimeMillis() - _drawingTime; if (_drawingTime > 0) { _fps = 1000 / _drawingTime; } if (_debugMode) { // Draw the FPS on the screen _graphics.setColor(TEXT_COLOR); _graphics.drawString("FPS: " + _fps + " (" + ((_fpsCount < MIN_FPS_STATS_COUNT) ? -1 : _averageFps) + ")", 42, _height, Graphics.BOTTOM | Graphics.LEFT); } 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 < 25) { // Remove the background to boost the performance. setBackground(false); } _firstTime = false; } } } } // Flush the buffer to the screen flushGraphics(); } /** * Draws 2D graphics e.g. information on top of the screen. * @param graphics */ private void draw2D(Graphics graphics) { graphics.setColor(TEXT_COLOR); if (_debugMode) { // Draw the accelerometer sensor readings in the bottom graphics.drawString("AX: " + (float)_interactionManager.ax(), 42, _height - 30, Graphics.BOTTOM | Graphics.LEFT); graphics.drawString("AY: " + (float)_interactionManager.ay(), 42, _height - 15, 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); _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!" } } /** * Paints the scene. * @param graphics */ private void draw3D(Graphics graphics) { boolean bound = false; try { // Bind the target _graphics3d.bindTarget(graphics); bound = true; // Advance the animation _world.animate((int)(System.currentTimeMillis() - _levelStartTime)); // Do the rendering _graphics3d.render(_world); } finally { // Release the target if (bound) { _graphics3d.releaseTarget(); } } } }