WorldBuilder.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.amaze.ui;

import java.util.Enumeration;
import java.util.Vector;
/**
 * Copyright (c) 2012 Nokia Corporation.
 */

import javax.microedition.lcdui.Image;
import javax.microedition.m3g.Appearance;
import javax.microedition.m3g.Background;
import javax.microedition.m3g.CompositingMode;
import javax.microedition.m3g.Image2D;
import javax.microedition.m3g.Mesh;
import javax.microedition.m3g.Node;
import javax.microedition.m3g.PolygonMode;
import javax.microedition.m3g.Texture2D;
import javax.microedition.m3g.Transform;
import javax.microedition.m3g.World;

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;

/**
 * Helper class containing methods to create the world content. This
 * functionality is separated into its own class so that MazeCanvas
 * wouldn't be so full.
 */
public class WorldBuilder {
	// Constants
	private static final int MAX_NODES_TO_DESTROY_COUNT = 70;
	
	// Filenames of graphical assets
	private static final String BACKGROUND_IMAGE_FILENAME = "/graphics/background.png";
	private static final String MARBLE_IMAGE_FILENAME = "/graphics/marble.png";
	private static final String FLOOR_IMAGE_FILENAME = "/graphics/floor.png";
	private static final String WALL_IMAGE_FILENAME = "/graphics/wall.png";
	private static final String GOAL_IMAGE_FILENAME = "/graphics/goal.png";
	
	// Members
	private Node[] _nodesToDestroy = null;
	
	/**
	 * Constructor.
	 */
	public WorldBuilder() {
		_nodesToDestroy = new Node[MAX_NODES_TO_DESTROY_COUNT];
	}

	/**
	 * Sets up the maze.
	 * @param world
	 * @param maze
	 * @param wallAppearance
	 * @param wallClearAppearance
	 */
	public void createNewMaze(World world,
			 			  	  Maze maze,
			 			  	  Appearance wallAppearance)
	{
		// Destroy the previous maze if one exists
		destroyMaze(world);
		
		// Generate a new maze
		maze.createNew(GameModel.MAZE_CORRIDOR_COUNT);
		
	    // Create the planes based on the generated maze
	    Enumeration wallsEnum = createPlanes(maze);
	    
	    // Release resources not needed anymore
	    maze.clear();
	    
	    // Create meshes of the planes and add them to the world
	    while (wallsEnum.hasMoreElements()) {
	    	Mesh wallMesh = ((Plane)wallsEnum.nextElement()).createMesh();
	    	wallMesh.setAppearance(0, wallAppearance);
	    	world.addChild(wallMesh);
	    	addNodeToDestroy(wallMesh);
	    }
	    
	    // Create the goal mark to the end of the maze
	    createGoalMark(world, maze);
	}
	
	/**
	 * Creates the background.
	 * @param world
	 * @return The created background instance.
	 */
	public Background createBackground(World world) {
		Background background = new Background();
	    Image backgroundImage = Main.makeImage(BACKGROUND_IMAGE_FILENAME);
	    
	    if (backgroundImage != null) {
	    	background.setImage(new Image2D(Image2D.RGB, backgroundImage));
	    	background.setImageMode(Background.REPEAT, Background.REPEAT);
	    	world.setBackground(background);
	    }
	    
	    return background;
	}

	/**
	 * Creates the wall appearances.
	 * @param wallAppearance
	 * @param wallClearAppearance
	 */
	public void createWallAppearances(Appearance wallAppearance,
			  						  Appearance wallClearAppearance)
	{
		// The walls need perspective correction enabled
		PolygonMode wallPolygonMode = new PolygonMode();
		wallPolygonMode.setPerspectiveCorrectionEnable(true);

		// Build the wall semi-transparent appearance
		wallClearAppearance.setPolygonMode(wallPolygonMode);
	    
		// This is to make a wall semi-transparent
		CompositingMode wallClearCompositeMode = new CompositingMode();
		wallClearCompositeMode.setBlending(CompositingMode.ALPHA_ADD);
		wallClearAppearance.setCompositingMode(wallClearCompositeMode);

	    // Build the normal wall appearance
	    wallAppearance.setPolygonMode(wallPolygonMode);

	    // Load the wall texture
	    Image wallTextureImage = Main.makeImage(WALL_IMAGE_FILENAME);
	    
	    if (wallTextureImage != null) {
	    	Texture2D wallTexture = null;
	    	wallTexture = new Texture2D(new Image2D(Image2D.RGB, wallTextureImage));
	    	
	    	// The texture is repeated
	    	wallTexture.setWrapping(Texture2D.WRAP_REPEAT,
	                                Texture2D.WRAP_REPEAT);//Texture2D.WRAP_CLAMP);
	    	wallTexture.setBlending(Texture2D.FUNC_REPLACE);
	    	wallTexture.setFiltering(Texture2D.FILTER_LINEAR,
	                                 Texture2D.FILTER_NEAREST);
	    	// Set the texture
	    	wallAppearance.setTexture(0, wallTexture);
	    	wallClearAppearance.setTexture(0, wallTexture);
	    }		
	}
	
	/**
	 * Creates the floor.
	 * @param world
	 */
	public void createFloor(World world) {
	    float floorSide = GameModel.MAZE_SIDE_LENGTH / 2;

	    // define the location and size of the floor
	    Transform floorTransform = new Transform();
	    floorTransform.postRotate(90.0f, -1.0f, 0.0f, 0.0f);
	    floorTransform.postScale(floorSide, floorSide, 1.0f);

	    // The floor appearance. Basically a texture repeated many times
	    Appearance floorAppearance = new Appearance();
	    
	    // The floor needs that perspective correction is enabled
	    PolygonMode floorPolygonMode = new PolygonMode();
	    floorPolygonMode.setPerspectiveCorrectionEnable(true);
	    floorAppearance.setPolygonMode(floorPolygonMode);

	    // load the texture
	    Texture2D floorTexture = null;
	    Image floorTextureImage = Main.makeImage(FLOOR_IMAGE_FILENAME);
	    
	    if (floorTextureImage != null) {
	    	floorTexture = new Texture2D(
	          new Image2D(Image2D.RGB, floorTextureImage));
	    	
	    	// the texture is repeated many times
	    	floorTexture.setWrapping(Texture2D.WRAP_REPEAT,
	                                 Texture2D.WRAP_REPEAT);
	    	floorTexture.setBlending(Texture2D.FUNC_REPLACE);
	    	floorTexture.setFiltering(Texture2D.FILTER_LINEAR,
	                                  Texture2D.FILTER_NEAREST);
	    	floorAppearance.setTexture(0, floorTexture);
	    }
	    
	    Plane floor = new Plane(floorTransform, 10);

	    // build the mesh
	    Mesh floorMesh = floor.createMesh();
	    floorMesh.setAppearance(0, floorAppearance);
	    
	    // the floor is not pickable
	    floorMesh.setPickingEnable(false);
	    
	    // add to the world
	    world.addChild(floorMesh);
	}
	
	/**
	 * Creates the goal mark.
	 * @param world
	 * @param maze
	 */
	private void createGoalMark(World world, Maze maze) {
	    Appearance appearance = new Appearance();
	    
	    CompositingMode compositingMode = new CompositingMode();
	    compositingMode.setBlending(CompositingMode.ALPHA);
	    appearance.setCompositingMode(compositingMode);
	    
	    Texture2D texture = null;
	    Image textureImage = Main.makeImage(GOAL_IMAGE_FILENAME);
	    
	    if (textureImage != null) {
	    	texture = new Texture2D(
	          new Image2D(Image2D.RGBA, textureImage));
	    	
	    	// The texture is not repeated
	    	texture.setWrapping(Texture2D.WRAP_CLAMP,
	                            Texture2D.WRAP_CLAMP);
	    	texture.setBlending(Texture2D.FUNC_REPLACE);
	    	texture.setFiltering(Texture2D.FILTER_NEAREST,
	                             Texture2D.FILTER_NEAREST);
	    	appearance.setTexture(0, texture);
	    }

	    // Create the goal mesh
	    Plane goalMarkPlane = createGoalMark(maze);
	    Mesh goalMarkMesh = goalMarkPlane.createMesh();
	    goalMarkMesh.setAppearance(0, appearance);
	    goalMarkMesh.setPickingEnable(false); // Not pickable

	    world.addChild(goalMarkMesh);
	    addNodeToDestroy(goalMarkMesh);
	}

	/**
	 * Creates the marble.
	 * @param world
	 * @return The newly created marble as a Mesh instance.
	 */
	public Mesh createMarble(World world) {
	    Transform transform = new Transform();
	    transform.postRotate(90.0f, -1.0f, 0.0f, 0.0f);
	    transform.postScale(MarbleModel.DEFAULT_SIZE,
	    					MarbleModel.DEFAULT_SIZE, 1.0f);

	    Appearance appearance = new Appearance();
	    CompositingMode compositingMode = new CompositingMode();
	    compositingMode.setBlending(CompositingMode.ALPHA);
	    appearance.setCompositingMode(compositingMode);
	    
	    Texture2D texture = null;
	    Image marbleImage = Main.makeImage(MARBLE_IMAGE_FILENAME);
	    
	    if (marbleImage != null) {
	    	texture = new Texture2D(new Image2D(Image2D.RGBA, marbleImage));

	    	// The texture is not repeated
	    	texture.setWrapping(Texture2D.WRAP_CLAMP,
	                            Texture2D.WRAP_CLAMP);
	    	texture.setBlending(Texture2D.FUNC_REPLACE);
	    	texture.setFiltering(Texture2D.FILTER_NEAREST,
	                             Texture2D.FILTER_NEAREST);
	    	appearance.setTexture(0, texture);
	    }
	    
	    Plane marblePlane = new Plane(transform, 1);
	    Mesh marble = marblePlane.createMesh();
	    marble.setAppearance(0, appearance);
	    marble.setRenderingEnable(false);
	    marble.setPickingEnable(false);

	    world.addChild(marble);
	    
	    return marble;
	}
	
	/**
	 * Destroys the maze planes and goal mark.
	 * @param world The world from which to remove the nodes.
	 */
	public void destroyMaze(World world) {
		if (_nodesToDestroy != null) {
			for (int i = 0; i < MAX_NODES_TO_DESTROY_COUNT; ++i) {
				if (_nodesToDestroy[i] != null) {
					world.removeChild(_nodesToDestroy[i]);
				}
			}
			
			_nodesToDestroy = null;
		}
	}

	/**
	 * Adds a reference of the given node to the internal array so that it can
	 * be removed from the world instance and deleted later (when a new maze
	 * needs to be generated).
	 * @param node The node to destroy later.
	 */
	private boolean addNodeToDestroy(Node node) {
		if (_nodesToDestroy == null) {
			_nodesToDestroy = new Node[MAX_NODES_TO_DESTROY_COUNT];
			_nodesToDestroy[0] = node;
			return true;
		}
		
		for (int i = 0; i < MAX_NODES_TO_DESTROY_COUNT; ++i) {
			if (_nodesToDestroy[i] == null) {
				_nodesToDestroy[i] = node;
				return true;
			}
		}
		
		// The array is full
		System.out.println("WorldBuilder::addNodeToDestroy(): The array is full!");
		return false;
	}
		
	/**
	 * Creates a plane located at the end of the maze.
	 * @return The goal plane.
	 */
	private Plane createGoalMark(Maze maze) {
		Transform markTransform = new Transform();
		markTransform.postTranslate(maze.origin() + maze.goalX()
								    * maze.spaceBetweenPlanes(),
									maze.height() / 2 + 0.2f,
									-maze.origin() - 5f);
		markTransform.postScale(10f, 10f, 10f);
		markTransform.postRotate(90f, -1f, 0f, 0f);
		return new Plane(markTransform, 1f);
	}
	
	/**
	 * Creates the horizontal and vertical planes and puts them in an
	 * enumeration. Note that after calling this method the maze array becomes
	 * unusable as it is released (set to null).
	 * @param maze The model of the maze.
	 * @return The enumeration of the components in the planes vector.
	 */
	private Enumeration createPlanes(Maze maze) {
		long[] mazeArray = maze.array();
		
		if (mazeArray == null || mazeArray.length == 0) {
			return null;
		}
		
		float spaceBetweenPlanes = maze.spaceBetweenPlanes();
		float mazeOrigin = maze.origin();
		float mazeHeight = maze.height();
		Vector allPlanes = new Vector();
		
		for (int i = 0; i < mazeArray.length; i++) {
			int startX = -1;
			
			for (int j = 0; j < mazeArray.length; j++) {
				long shift = (0x1L << j);
        
				if ((mazeArray[i] & shift) == shift && startX == -1) {
					startX = j;
					continue;
				}
        
				if ((((mazeArray[i] & shift) == 0) || (j == (mazeArray.length - 1)))
						&& (startX >= 0))
				{
					int steps = j - startX;
					
					// Don't create walls of side 1 since they will be created
					// on the other direction
					if (steps == 1) {
						startX = -1;
						continue;
					}

					// compensate that the last item is always 1
					if (j == (mazeArray.length - 1)) {
						steps++;
					}
          
					Transform planeTransform = new Transform();

					// Divided by 2 since the original square is of side 2
					float wallWidth = (maze.spaceBetweenPlanes() * (steps - 1) / 2);

					// Move to the correct position
					planeTransform.postTranslate(
							mazeOrigin + spaceBetweenPlanes * startX + wallWidth,
							mazeHeight, mazeOrigin + spaceBetweenPlanes * i);

					// Give the actual size
					planeTransform.postScale(wallWidth, mazeHeight, 1f);
					allPlanes.addElement(new Plane(planeTransform, 1f));
					startX = -1;
				}
			}
		}
		
		for (int i = 0; i < mazeArray.length; i++) {
			int startY = -1;
			long shift = (0x1L << i);
      
			for (int j = 0; j < mazeArray.length; j++) {
				if ((mazeArray[j] & shift) == shift && startY == -1) {
					startY = j;
					continue;
				}
        
				if ((((mazeArray[j] & shift) == 0) || (j == (mazeArray.length - 1)))
						&& (startY >= 0))
				{
					int steps = j - startY;
					
					if (steps == 1) {
						startY = -1;
						continue;
					}
          
					if (j == (mazeArray.length - 1)) {
						steps++;
					}
          
					Transform planeTransform = new Transform();

					// Divided by 2 since the original square is of side 2
					float wallWidth = (spaceBetweenPlanes * (steps - 1) / 2);

					// Translate to the correct position
					planeTransform.postTranslate(
							mazeOrigin + spaceBetweenPlanes * i, mazeHeight,
							mazeOrigin + spaceBetweenPlanes * startY + wallWidth);

					// Rotate 90 degrees since this is a vertical wall
					planeTransform.postRotate(90f, 0f, 1f, 0f);
          
					// Give the correct size
					planeTransform.postScale(wallWidth, mazeHeight, 1f);
					allPlanes.addElement(new Plane(planeTransform, 1f));
					startY = -1;
				}
			}
		}
		
		return allPlanes.elements();
	}	
}