From e100931c1f5b595de3645f1cb350bfe57f2f9567 Mon Sep 17 00:00:00 2001 From: "nor..67" Date: Thu, 14 Feb 2013 15:00:13 +0000 Subject: [PATCH] - add first version of WorldOfInception git-svn-id: https://jmonkeyengine.googlecode.com/svn/trunk@10406 75d07b2b-3a1a-0410-a2c5-0572b91ccdca --- .../test/jme3test/games/WorldOfInception.java | 483 ++++++++++++++++++ 1 file changed, 483 insertions(+) create mode 100644 engine/src/test/jme3test/games/WorldOfInception.java diff --git a/engine/src/test/jme3test/games/WorldOfInception.java b/engine/src/test/jme3test/games/WorldOfInception.java new file mode 100644 index 000000000..ee7548ec0 --- /dev/null +++ b/engine/src/test/jme3test/games/WorldOfInception.java @@ -0,0 +1,483 @@ +/* + * Copyright (c) 2009-2012 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package jme3test.games; + +import com.jme3.app.Application; +import com.jme3.app.SimpleApplication; +import com.jme3.app.state.AbstractAppState; +import com.jme3.app.state.AppStateManager; +import com.jme3.bullet.BulletAppState; +import com.jme3.bullet.collision.shapes.CollisionShape; +import com.jme3.bullet.collision.shapes.MeshCollisionShape; +import com.jme3.bullet.collision.shapes.SphereCollisionShape; +import com.jme3.bullet.control.RigidBodyControl; +import com.jme3.bullet.debug.DebugTools; +import com.jme3.input.KeyInput; +import com.jme3.input.controls.AnalogListener; +import com.jme3.input.controls.KeyTrigger; +import com.jme3.material.Material; +import com.jme3.math.ColorRGBA; +import com.jme3.math.FastMath; +import com.jme3.math.Ray; +import com.jme3.math.Vector3f; +import com.jme3.scene.Geometry; +import com.jme3.scene.Mesh; +import com.jme3.scene.Node; +import com.jme3.scene.Spatial; +import com.jme3.scene.shape.Sphere; +import java.util.Random; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * WorldOfInception - Find the galaxy center ;) + * + * @author normenhansen + */ +public class WorldOfInception extends SimpleApplication implements AnalogListener { + + //Assumptions: POI radius in world == 1, only one player, vector3f hash describes enough worlds + private static final Logger logger = Logger.getLogger(WorldOfInception.class.getName()); + private static final Random random = new Random(System.currentTimeMillis()); + private static final float scaleDist = 10; + private static final float poiRadius = 100; + private static final int poiCount = 30; + private static Material poiMaterial; + private static Mesh poiMesh; + private static Material ballMaterial; + private static Mesh ballMesh; + private static CollisionShape poiHorizonCollisionShape; + private static CollisionShape poiCollisionShape; + private static CollisionShape ballCollisionShape; + private InceptionLevel currentLevel; + private final Vector3f walkDirection = new Vector3f(); + private static DebugTools debugTools; + + public WorldOfInception() { + //base level vector position hash == seed + super(new InceptionLevel(null, Vector3f.ZERO)); + currentLevel = super.getStateManager().getState(InceptionLevel.class); + currentLevel.takeOverParent(); + currentLevel.getRootNode().setLocalScale(Vector3f.UNIT_XYZ); + currentLevel.getRootNode().setLocalTranslation(Vector3f.ZERO); + } + + public static void main(String[] args) { + WorldOfInception app = new WorldOfInception(); + app.start(); + } + + @Override + public void simpleInitApp() { + //set far frustum only so we see the outer world longer + cam.setFrustumFar(10000); + cam.setLocation(Vector3f.ZERO); + debugTools = new DebugTools(assetManager); + rootNode.attachChild(debugTools.debugNode); + poiMaterial = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); + poiMaterial.setTexture("ColorMap", assetManager.loadTexture("Interface/Logo/Monkey.jpg")); + poiMesh = new Sphere(16, 16, 1f); + + ballMaterial = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); + ballMaterial.setTexture("ColorMap", assetManager.loadTexture("Interface/Logo/Monkey.jpg")); + ballMaterial.setColor("Color", ColorRGBA.Red); + ballMesh = new Sphere(16, 16, 1.0f); + + poiHorizonCollisionShape = new MeshCollisionShape(new Sphere(128, 128, poiRadius)); + poiCollisionShape = new SphereCollisionShape(1f); + ballCollisionShape = new SphereCollisionShape(1f); + setupKeys(); + } + + private void setupKeys() { + inputManager.addMapping("StrafeLeft", new KeyTrigger(KeyInput.KEY_A)); + inputManager.addMapping("StrafeRight", new KeyTrigger(KeyInput.KEY_D)); + inputManager.addMapping("Forward", new KeyTrigger(KeyInput.KEY_W)); + inputManager.addMapping("Back", new KeyTrigger(KeyInput.KEY_S)); + inputManager.addMapping("StrafeUp", new KeyTrigger(KeyInput.KEY_Q)); + inputManager.addMapping("StrafeDown", new KeyTrigger(KeyInput.KEY_Z), new KeyTrigger(KeyInput.KEY_Y)); + inputManager.addMapping("Space", new KeyTrigger(KeyInput.KEY_SPACE)); + inputManager.addMapping("Return", new KeyTrigger(KeyInput.KEY_RETURN)); + inputManager.addMapping("Esc", new KeyTrigger(KeyInput.KEY_ESCAPE)); + inputManager.addMapping("Up", new KeyTrigger(KeyInput.KEY_UP)); + inputManager.addMapping("Down", new KeyTrigger(KeyInput.KEY_DOWN)); + inputManager.addMapping("Left", new KeyTrigger(KeyInput.KEY_LEFT)); + inputManager.addMapping("Right", new KeyTrigger(KeyInput.KEY_RIGHT)); + inputManager.addListener(this, "StrafeLeft", "StrafeRight", "Forward", "Back", "StrafeUp", "StrafeDown", "Space", "Reset", "Esc", "Up", "Down", "Left", "Right"); + } + + public void onAnalog(String name, float value, float tpf) { + Vector3f left = rootNode.getLocalRotation().mult(Vector3f.UNIT_X.negate()); + Vector3f forward = rootNode.getLocalRotation().mult(Vector3f.UNIT_Z.negate()); + Vector3f up = rootNode.getLocalRotation().mult(Vector3f.UNIT_Y); + //TODO: properly scale input based on current scaling level + tpf = tpf * (10 - (9.0f * currentLevel.getCurrentScaleAmount())); + if (name.equals("StrafeLeft") && value > 0) { + walkDirection.addLocal(left.mult(tpf)); + } else if (name.equals("StrafeRight") && value > 0) { + walkDirection.addLocal(left.negate().multLocal(tpf)); + } else if (name.equals("Forward") && value > 0) { + walkDirection.addLocal(forward.mult(tpf)); + } else if (name.equals("Back") && value > 0) { + walkDirection.addLocal(forward.negate().multLocal(tpf)); + } else if (name.equals("StrafeUp") && value > 0) { + walkDirection.addLocal(up.mult(tpf)); + } else if (name.equals("StrafeDown") && value > 0) { + walkDirection.addLocal(up.negate().multLocal(tpf)); + } else if (name.equals("Up") && value > 0) { + //TODO: rotate rootNode, needs to be global + } else if (name.equals("Down") && value > 0) { + } else if (name.equals("Left") && value > 0) { + } else if (name.equals("Right") && value > 0) { + } else if (name.equals("Esc")) { + stop(); + } + } + + @Override + public void simpleUpdate(float tpf) { + currentLevel = currentLevel.getCurrentLevel(); + currentLevel.move(walkDirection); + walkDirection.set(Vector3f.ZERO); + } + + public static class InceptionLevel extends AbstractAppState { + + private final InceptionLevel parent; + private final Vector3f inParentPosition; + private SimpleApplication application; + private BulletAppState physicsState; + private Node rootNode; + private Vector3f playerPos; + private InceptionLevel currentActiveChild; + private InceptionLevel currentReturnLevel; + private float curScaleAmount = 0; + + public InceptionLevel(InceptionLevel parent, Vector3f inParentPosition) { + this.parent = parent; + this.inParentPosition = inParentPosition; + } + + @Override + public void update(float tpf) { + super.update(tpf); + if (currentReturnLevel != this) { + return; + } + debugTools.setYellowArrow(new Vector3f(0, 0, -2), playerPos.divide(poiRadius)); + float curLocalDist = getPlayerPosition().length(); + // If we are outside the range of one point of interest, move out to + // the next upper level + if (curLocalDist > poiRadius + FastMath.ZERO_TOLERANCE) { //DAFUQ normalize? + if (parent == null) { + //TODO: could add new nodes coming in instead for literally endless space + logger.log(Level.INFO, "Hit event horizon"); + currentReturnLevel = this; + return; + } + //give to parent + logger.log(Level.INFO, "give to parent");; + parent.takeOverChild(inParentPosition.add(playerPos.normalize())); + application.getStateManager().attach(parent); + currentReturnLevel = parent; + return; + } + + AppStateManager stateManager = application.getStateManager(); + // We create child positions based on the parent position hash so we + // should in practice get the same galaxy w/o too many doubles + // with each run with the same root vector. + Vector3f[] vectors = getPositions(poiCount, inParentPosition.hashCode()); + for (int i = 0; i < vectors.length; i++) { + Vector3f vector3f = vectors[i]; + //negative rootNode location is our actual player position + Vector3f distVect = vector3f.subtract(playerPos); + float distance = distVect.length(); + if (distance <= 1) { + checkActiveChild(vector3f); + float percent = 0; + curScaleAmount = 0; + this.scaleAsParent(percent, playerPos, distVect); + currentActiveChild.scaleAsChild(percent, distVect); + logger.log(Level.INFO, "Give over to child {0}", currentActiveChild); + currentActiveChild.takeOverParent(); + stateManager.detach(this); + currentReturnLevel = currentActiveChild; + return; + } else if (distance <= 1 + scaleDist) { + debugTools.setRedArrow(Vector3f.ZERO, distVect); + checkActiveChild(vector3f); + //TODO: scale percent nicer for less of an "explosion" effect + float percent = 1 - mapValue(distance - 1, 0, scaleDist, 0, 1); + curScaleAmount = percent; + rootNode.getChild(i).setCullHint(Spatial.CullHint.Always); + this.scaleAsParent(percent, playerPos, distVect); + currentActiveChild.scaleAsChild(percent, distVect); + currentReturnLevel = this; + return; + } else if (currentActiveChild != null && currentActiveChild.getPositionInParent().equals(vector3f)) { + //TODO: doing this here causes problems when close to multiple pois + logger.log(Level.INFO, "Detach child {0}", currentActiveChild); + rootNode.getChild(i).setCullHint(Spatial.CullHint.Inherit); + stateManager.detach(currentActiveChild); + currentActiveChild = null; + } + } + curScaleAmount = 0; + rootNode.setLocalScale(1); + rootNode.setLocalTranslation(playerPos.negate()); + debugTools.setRedArrow(Vector3f.ZERO, Vector3f.ZERO); + debugTools.setBlueArrow(Vector3f.ZERO, Vector3f.ZERO); + debugTools.setGreenArrow(Vector3f.ZERO, Vector3f.ZERO); + } + + private void checkActiveChild(Vector3f vector3f) { + AppStateManager stateManager = application.getStateManager(); + if (currentActiveChild == null) { + currentActiveChild = new InceptionLevel(this, vector3f); + stateManager.attach(currentActiveChild); + logger.log(Level.INFO, "Attach child {0}", currentActiveChild); + } else if (!currentActiveChild.getPositionInParent().equals(vector3f)) { + logger.log(Level.INFO, "Switching from child {0}", currentActiveChild); + stateManager.detach(currentActiveChild); + currentActiveChild = new InceptionLevel(this, vector3f); + stateManager.attach(currentActiveChild); + logger.log(Level.INFO, "Attach child {0}", currentActiveChild); + } + } + + public static float linear2decibel(float x) { + return (float) (20.0 * Math.log(x) / Math.log(10)); + } + + public static float decibel2linear(float x) { + return (float) Math.pow(10.0, x / 20.0); + } + + private void scaleAsChild(float percent, Vector3f dist) { + float childScale = mapValue(percent, 1.0f / poiRadius, 1); + Vector3f distToHorizon = dist.normalize(); + Vector3f scaledDistToHorizon = distToHorizon.mult(childScale * poiRadius); + Vector3f rootOff = dist.add(scaledDistToHorizon); + debugTools.setBlueArrow(Vector3f.ZERO, rootOff); + getRootNode().setLocalScale(childScale); + getRootNode().setLocalTranslation(rootOff); + //prepare player position already + Vector3f playerPosition = dist.normalize().mult(-poiRadius); + setPlayerPosition(playerPosition); + } + + private void scaleAsParent(float percent, Vector3f playerPos, Vector3f dist) { + float scale = mapValue(percent, 1.0f, poiRadius); + Vector3f distToHorizon = dist.subtract(dist.normalize()); + Vector3f offLocation = playerPos.add(distToHorizon); + Vector3f rootOff = offLocation.mult(scale).negate(); + rootOff.addLocal(dist); + debugTools.setGreenArrow(Vector3f.ZERO, offLocation); + getRootNode().setLocalScale(scale); + getRootNode().setLocalTranslation(rootOff); + } + + public void takeOverParent() { + //got playerPos from scaleAsChild before + getPlayerPosition().normalizeLocal().multLocal(poiRadius); + currentReturnLevel = this; + } + + public void takeOverChild(Vector3f playerPos) { + this.playerPos.set(playerPos); + currentReturnLevel = this; + } + + public InceptionLevel getLastLevel(Ray pickRay) { + // TODO: get a level based on positions getting ever more accurate, + // from any given position + return null; + } + + private void initData() { + getRootNode(); + physicsState = new BulletAppState(); + physicsState.startPhysics(); + physicsState.getPhysicsSpace().setGravity(Vector3f.ZERO); + //horizon + physicsState.getPhysicsSpace().add(new RigidBodyControl(poiHorizonCollisionShape, 0)); + int hashCode = inParentPosition.hashCode(); + Vector3f[] positions = getPositions(poiCount, hashCode); + for (int i = 0; i < positions.length; i++) { + Vector3f vector3f = positions[i]; + Geometry poiGeom = new Geometry("poi", poiMesh); + poiGeom.setLocalTranslation(vector3f); + poiGeom.setMaterial(poiMaterial); + RigidBodyControl control = new RigidBodyControl(poiCollisionShape, 0); + //!!! Important + control.setApplyPhysicsLocal(true); + poiGeom.addControl(control); + physicsState.getPhysicsSpace().add(poiGeom); + rootNode.attachChild(poiGeom); + + } + //add balls after so first 10 geoms == locations + for (int i = 0; i < positions.length; i++) { + Vector3f vector3f = positions[i]; + Geometry ball = getRandomBall(vector3f); + physicsState.getPhysicsSpace().add(ball); + rootNode.attachChild(ball); + } + + } + + private Geometry getRandomBall(Vector3f location) { + Vector3f localLocation = new Vector3f(); + localLocation.set(location); + localLocation.addLocal(new Vector3f(random.nextFloat() - 0.5f, random.nextFloat() - 0.5f, random.nextFloat() - 0.5f).normalize().mult(3)); + Geometry poiGeom = new Geometry("ball", ballMesh); + poiGeom.setLocalTranslation(localLocation); + poiGeom.setMaterial(ballMaterial); + RigidBodyControl control = new RigidBodyControl(ballCollisionShape, 1); + //!!! Important + control.setApplyPhysicsLocal(true); + poiGeom.addControl(control); + float x = (random.nextFloat() - 0.5f) * 100; + float y = (random.nextFloat() - 0.5f) * 100; + float z = (random.nextFloat() - 0.5f) * 100; + control.setLinearVelocity(new Vector3f(x, y, z)); + return poiGeom; + } + + private void cleanupData() { + physicsState.cleanup(); + //TODO: remove all objects? + physicsState = null; + rootNode = null; + } + + @Override + public void initialize(AppStateManager stateManager, Application app) { + super.initialize(stateManager, app); + //only generate data and attach node when we are actually attached (or picking) + initData(); + application = (SimpleApplication) app; + application.getRootNode().attachChild(getRootNode()); + application.getStateManager().attach(physicsState); + } + + @Override + public void cleanup() { + super.cleanup(); + //detach everything when we are detached + application.getRootNode().detachChild(rootNode); + application.getStateManager().detach(physicsState); + cleanupData(); + } + + public Node getRootNode() { + if (rootNode == null) { + rootNode = new Node("ZoomLevel"); + if (parent != null) { + rootNode.setLocalScale(1.0f / poiRadius); + } + } + return rootNode; + } + + public Vector3f getPositionInParent() { + return inParentPosition; + } + + public Vector3f getPlayerPosition() { + if (playerPos == null) { + playerPos = new Vector3f(); + } + return playerPos; + } + + public void setPlayerPosition(Vector3f vec) { + if (playerPos == null) { + playerPos = new Vector3f(); + } + playerPos.set(vec); + } + + public void move(Vector3f dir) { + if (playerPos == null) { + playerPos = new Vector3f(); + } + playerPos.addLocal(dir); + } + + public float getCurrentScaleAmount() { + return curScaleAmount; + } + + public InceptionLevel getCurrentLevel() { + return currentReturnLevel; + } + } + + public static Vector3f[] getPositions(int count, long seed) { + Random rnd = new Random(seed); + Vector3f[] vectors = new Vector3f[count]; + for (int i = 0; i < count; i++) { + vectors[i] = new Vector3f((rnd.nextFloat() - 0.5f) * poiRadius, + (rnd.nextFloat() - 0.5f) * poiRadius, + (rnd.nextFloat() - 0.5f) * poiRadius); + } + return vectors; + } + + /** + * Maps a value from 0-1 to a range from min to max. + * + * @param x + * @param min + * @param max + * @return + */ + public static float mapValue(float x, float min, float max) { + return mapValue(x, 0, 1, min, max); + } + + /** + * Maps a value from inputMin to inputMax to a range from min to max. + * + * @param x + * @param inputMin + * @param inputMax + * @param min + * @param max + * @return + */ + public static float mapValue(float x, float inputMin, float inputMax, float min, float max) { + return (x - inputMin) * (max - min) / (inputMax - inputMin) + min; + } +}