Starter Tutorial 8 - Hello Intersection


« Previous: Starter Tutorial 7 - Hello MousePick
Next: Starter Tutorial 9 - Hello Terrain »


(Tip: Up-to-date source files for the tutorials are always in the repository)


This program introduces SoundNode, Skybox, and SoundAPIController. You will learn how to play sounds, create text, and make your own controllers and input handler. This also introduces rudimentary collision detection.

Code Sample

import java.net.URL;
import java.util.Random;
import java.util.logging.Logger;
 
import com.jme.app.SimpleGame;
import com.jme.bounding.BoundingSphere;
import com.jme.image.Texture;
import com.jme.input.KeyInput;
import com.jme.input.action.InputActionEvent;
import com.jme.input.action.KeyInputAction;
import com.jme.math.Vector3f;
import com.jme.renderer.ColorRGBA;
import com.jme.scene.Controller;
import com.jme.scene.Skybox;
import com.jme.scene.Spatial;
import com.jme.scene.Text;
import com.jme.scene.TriMesh;
import com.jme.scene.shape.Sphere;
import com.jme.scene.state.CullState;
import com.jme.scene.state.MaterialState;
import com.jme.util.TextureManager;
import com.jme.util.resource.ResourceLocatorTool;
import com.jme.util.resource.SimpleResourceLocator;
import com.jmex.audio.AudioSystem;
import com.jmex.audio.AudioTrack;
 
/**
 * Started Date: Jul 24, 2004 <br>
 * <br>
 * Demonstrates intersection testing, sound, and making your own controller.
 * 
 * @author Jack Lindamood
 */
public class HelloIntersection extends SimpleGame {
	private static final Logger logger = Logger
			.getLogger(HelloIntersection.class.getName());
 
	/** Material for my bullet */
	MaterialState bulletMaterial;
 
	/** Target you're trying to hit */
	Sphere target;
 
	/** Location of laser sound */
	URL laserURL;
 
	/** Location of hit sound */
	URL hitURL;
 
	/** Used to move target location on a hit */
	Random r = new Random();
 
	/** A sky box for our scene. */
	Skybox sb;
 
	/**
	 * The sound tracks that will be in charge of maintaining our sound effects.
	 */
	AudioTrack laserSound;
	AudioTrack targetSound;
 
	public static void main(String[] args) {
		HelloIntersection app = new HelloIntersection();
		app.setConfigShowMode(ConfigShowMode.AlwaysShow);
		app.start();
	}
 
	protected void simpleInitGame() {
		setupSound();
 
		/** Create a + for the middle of the screen */
		Text cross = Text.createDefaultTextLabel("Crosshairs", "+");
 
		// 8 is half the width of a font char
		/** Move the + to the middle */
		cross.setLocalTranslation(new Vector3f(display.getWidth() / 2f - 8f,
				display.getHeight() / 2f - 8f, 0));
		statNode.attachChild(cross);
		target = new Sphere("my sphere", 15, 15, 1);
		target.setModelBound(new BoundingSphere());
		target.updateModelBound();
		rootNode.attachChild(target);
 
		/** Create a skybox to suround our world */
		setupSky();
 
		// Attach the skybox to our root node, and force the rootnode to show
		// so that the skybox will always show
		rootNode.attachChild(sb);
		rootNode.setCullHint(Spatial.CullHint.Never);
 
		/**
		 * Set the action called "firebullet", bound to KEY_F, to performAction
		 * FireBullet
		 */
		input.addAction(new FireBullet(), "firebullet", KeyInput.KEY_F, false);
 
		/** Make bullet material */
		bulletMaterial = display.getRenderer().createMaterialState();
		bulletMaterial.setEmissive(ColorRGBA.green.clone());
 
		/** Make target material */
		MaterialState redMaterial = display.getRenderer().createMaterialState();
		redMaterial.setDiffuse(ColorRGBA.red.clone());
		target.setRenderState(redMaterial);
	}
 
	private void setupSound() {
		/** Set the 'ears' for the sound API */
		AudioSystem audio = AudioSystem.getSystem();
		audio.getEar().trackOrientation(cam);
		audio.getEar().trackPosition(cam);
 
		/** Create program sound */
		targetSound = audio.createAudioTrack(getClass().getResource(
				"/jmetest/data/sound/explosion.ogg"), false);
		targetSound.setMaxAudibleDistance(1000);
		targetSound.setVolume(1.0f);
		laserSound = audio.createAudioTrack(getClass().getResource(
				"/jmetest/data/sound/laser.ogg"), false);
		laserSound.setMaxAudibleDistance(1000);
		laserSound.setVolume(1.0f);
	}
 
	private void setupSky() {
		sb = new Skybox("skybox", 200, 200, 200);
 
		try {
			ResourceLocatorTool.addResourceLocator(
					ResourceLocatorTool.TYPE_TEXTURE,
					new SimpleResourceLocator(getClass().getResource(
							"/jmetest/data/texture/")));
		} catch (Exception e) {
			logger.warning("Unable to access texture directory.");
			e.printStackTrace();
		}
 
		sb.setTexture(Skybox.Face.North, TextureManager.loadTexture(
				"north.jpg", Texture.MinificationFilter.BilinearNearestMipMap,
				Texture.MagnificationFilter.Bilinear));
		sb.setTexture(Skybox.Face.West, TextureManager.loadTexture("west.jpg",
				Texture.MinificationFilter.BilinearNearestMipMap,
				Texture.MagnificationFilter.Bilinear));
		sb.setTexture(Skybox.Face.South, TextureManager.loadTexture(
				"south.jpg", Texture.MinificationFilter.BilinearNearestMipMap,
				Texture.MagnificationFilter.Bilinear));
		sb.setTexture(Skybox.Face.East, TextureManager.loadTexture("east.jpg",
				Texture.MinificationFilter.BilinearNearestMipMap,
				Texture.MagnificationFilter.Bilinear));
		sb.setTexture(Skybox.Face.Up, TextureManager.loadTexture("top.jpg",
				Texture.MinificationFilter.BilinearNearestMipMap,
				Texture.MagnificationFilter.Bilinear));
		sb.setTexture(Skybox.Face.Down, TextureManager.loadTexture(
				"bottom.jpg", Texture.MinificationFilter.BilinearNearestMipMap,
				Texture.MagnificationFilter.Bilinear));
		sb.preloadTextures();
 
		CullState cullState = display.getRenderer().createCullState();
		cullState.setCullFace(CullState.Face.None);
		cullState.setEnabled(true);
		sb.setRenderState(cullState);
 
		sb.updateRenderState();
	}
 
	class FireBullet extends KeyInputAction {
		int numBullets;
 
		public void performAction(InputActionEvent evt) {
			logger.info("BANG");
			/** Create bullet */
			Sphere bullet = new Sphere("bullet" + numBullets++, 8, 8, .25f);
			bullet.setModelBound(new BoundingSphere());
			bullet.updateModelBound();
			/** Move bullet to the camera location */
			bullet.setLocalTranslation(new Vector3f(cam.getLocation()));
			bullet.setRenderState(bulletMaterial);
			/**
			 * Update the new world locaion for the bullet before I add a
			 * controller
			 */
			bullet.updateGeometricState(0, true);
			/**
			 * Add a movement controller to the bullet going in the camera's
			 * direction
			 */
			bullet.addController(new BulletMover(bullet, new Vector3f(cam
					.getDirection())));
			rootNode.attachChild(bullet);
			bullet.updateRenderState();
			/** Signal our sound to play laser during rendering */
			laserSound.setWorldPosition(cam.getLocation());
			laserSound.play();
		}
	}
 
	class BulletMover extends Controller {
		private static final long serialVersionUID = 1L;
		/** Bullet that's moving */
		TriMesh bullet;
 
		/** Direciton of bullet */
		Vector3f direction;
 
		/** speed of bullet */
		float speed = 10;
 
		/** Seconds it will last before going away */
		float lifeTime = 5;
 
		BulletMover(TriMesh bullet, Vector3f direction) {
			this.bullet = bullet;
			this.direction = direction;
			this.direction.normalizeLocal();
		}
 
		public void update(float time) {
			lifeTime -= time;
			/** If life is gone, remove it */
			if (lifeTime < 0) {
				rootNode.detachChild(bullet);
				bullet.removeController(this);
				return;
			}
			/** Move bullet */
			Vector3f bulletPos = bullet.getLocalTranslation();
			bulletPos.addLocal(direction.mult(time * speed));
			bullet.setLocalTranslation(bulletPos);
			/** Does the bullet intersect with target? */
			if (bullet.getWorldBound().intersects(target.getWorldBound())) {
				logger.info("OWCH!!!");
				targetSound.setWorldPosition(target.getWorldTranslation());
 
				target.setLocalTranslation(new Vector3f(r.nextFloat() * 10, r
						.nextFloat() * 10, r.nextFloat() * 10));
 
				lifeTime = 0;
 
				targetSound.play();
			}
		}
	}
 
	/**
	 * Called every frame for updating
	 */
	protected void simpleUpdate() {
		// Let the programmable sound update itself.
		AudioSystem.getSystem().update();
		// Move the skybox into position
		sb.getLocalTranslation().set(cam.getLocation().x, cam.getLocation().y,
				cam.getLocation().z);
	}
 
	@Override
	protected void cleanup() {
		super.cleanup();
		if (AudioSystem.isCreated()) {
			AudioSystem.getSystem().cleanup();
		}
	}
}

Whew! This is a deep program and our first one that actually does something kind of fun!

Setting Up The Sounds

OK, first new thing is the sound:

/**
 * The sound tracks that will be in charge of maintaining our sound effects.
 */
AudioTrack laserSound;
AudioTrack targetSound;

Sounds have to be rendered just like nodes in a scene graph. ProgrammableSound extends SoundSpatial extends Object. So because programSound isn’t a “Spatial” in the sense of a TriMesh or Geometry or Node, it can’t be rendered with the regular rendering environment. It is rendered with a sound system, while objects are rendered with a display system. In this system, we are going to use a Programmable sound. A programmable sound is a node that has various links to URLs in it. Each URL is a sound that can be fired on an event. The event ID for our laser sound is 1. The number isn’t as important as being unique. We’ll get more into using it later, but look at how I setup the sound:

/** Set the 'ears' for the sound API */
AudioSystem audio = AudioSystem.getSystem();
audio.getEar().trackOrientation(cam);
audio.getEar().trackPosition(cam);

The first line creates a sound system. Sound isn’t enabled by default. We are creating a sound system that is specific to the renderer we are using. So if we’re using LWJGL it will create a LWJGL usable sound system. Next we setup the ‘ears’ for our sound by placing them where the camera is. Afterwards, we create our sound:

/** Create program sound */
targetSound = audio.createAudioTrack(getClass().getResource(
		"/jmetest/data/sound/explosion.ogg"), false);
targetSound.setMaxAudibleDistance(1000);
targetSound.setVolume(1.0f);

We say here our sound can be heard up to 140 units away and won’t loop. We do the same with our laser sound, but don’t give it a max distance.

laserSound = audio.createAudioTrack(getClass().getResource(
		"/jmetest/data/sound/laser.ogg"), false);
laserSound.setMaxAudibleDistance(1000);
laserSound.setVolume(1.0f);

Next, we tie the sounds into the Sound nodes:

/** locate laser and register it with the prog sound. */
laserURL = HelloIntersection.class.getClassLoader().getResource(
 "jmetest/data/sound/laser.ogg");
hitURL = HelloIntersection.class.getClassLoader().getResource(
 "jmetest/data/sound/explosion.ogg");
// Ask the system for a program id for this resource
int programid = SoundPool.compile(new URL[] { laserURL });
int hitid = SoundPool.compile(new URL[] { hitURL });
// Then we bind the programid we received to our laser event id.
laserSound.bindEvent(laserEventID, programid);
targetSound.bindEvent(hitEventID, hitid);
snode.attachChild(laserSound);
snode.attachChild(targetSound);

SoundPool.compile takes an array of URLs and compiles them into a “SoundPool” that we can use in our program. Those sounds are identified by an integer. After compiling, we tell snode that whenever event laserEventID is called, it should play the sounds in SoundPool that are identified by programid. Whenever the event hitEventID is triggered, we should play the sounds identified by hitid. We only use one sound, but we could easily have a laser sound followed by a … bang tied into our programid by using an array of 2 URLs.

The Crosshair

After our sounds, we put our crosshair on the screen:

/** Create a + for the middle of the screen */
Text cross = Text.createDefaultTextLabel("Crosshairs", "+");
 
// 8 is half the width of a font char
/** Move the + to the middle */
cross.setLocalTranslation(new Vector3f(display.getWidth() / 2f - 8f,
		display.getHeight() / 2f - 8f, 0));
statNode.attachChild(cross);

Text extends Geometry. So this means Text needs a name. We call it “crosshairs”. Text just displays text on the screen. This one will display a “+”. The translation of the text isn’t its real world coordinates like other things, but is its actual screen coordinates. The Z of setLocalTranslation is ignored by Text so 0 is fine. I set my + to the middle of the screen in this example. You will notice I don’t attach it too rootNode but to statNode. statNode is created in SimpleGame and has one child by default. That child is the text you see at the bottom of the screen. I attach my cross to statNode because statNode has a special TextureState already in it.

Let’s look at the statNode's texture state for a second:

Text will look at the ASCII character you give it, and match it to a place on this picture, and display a rectangle for each letter. Taking advantage of this, you can easily modify a picture file to have any text you want to create your own custom fonts for your games!

The Skybox

After creating the +, I create the skybox for my world:

/** Create a skybox to surround our world */
sb = new Skybox("skybox", 200, 200, 200);
 
try {
	ResourceLocatorTool.addResourceLocator(
			ResourceLocatorTool.TYPE_TEXTURE,
			new SimpleResourceLocator(getClass().getResource(
					"/jmetest/data/texture/")));
} catch (Exception e) {
	logger.warning("Unable to access texture directory.");
	e.printStackTrace();
}
 
sb.setTexture(Skybox.Face.North, TextureManager.loadTexture(
		"north.jpg", Texture.MinificationFilter.BilinearNearestMipMap,
		Texture.MagnificationFilter.Bilinear));
		...West...
		...South...
		...East...
		...Up...
		...Down...
sb.preloadTextures();

This simply creates a box that is 200x200x200 units big. The box is made so that textures can appear on the inside of it. Then I load the texture directory using the ResourceLocatorTool (jME API) for use by the TextureManager (jME API).

CullState cullState = display.getRenderer().createCullState();
cullState.setCullFace(CullState.Face.None);
cullState.setEnabled(true);
sb.setRenderState(cullState);

CullState (jME API) determines which side of an object is not to be rendered. In this case all faces are visible, so that we can be inside of the box and still see the textures. FIXME Is this correct?

sb.updateRenderState();
 
// Attach the skybox to our root node, and force the rootnode to show
// so that the skybox will always show
rootNode.attachChild(sb);
rootNode.setCullHint(Spatial.CullHint.Never);

First off we load all textures, so we don't have to do it while the player moves around. Secondly we attach the Skybox to the rootNode and tell the renderer to never cull the Skybox. FIXME: Isn't this superfluous?

Firing Bullets

The final new thing in simpleInitGame is when I bind a key to an action:

input.addKeyboardAction("firebullet", KeyInput.KEY_F,
new FireBullet());

input is a standard property of SimpleGame. It is of class InputHandler (jME API). By default, SimpleGame gives input the information it needs to do simple keyboard and mouse movements in our world. Here, I am adding KEY_F to be the action called “firebullet” which is handled by my own FireBullet object. If you’ve done action binding for AWT, this is similar. This simply states that when you press the F key, FireBullet.performAction will be called. Let's see what it looks like:

/** Create bullet */
Sphere bullet = new Sphere("bullet" + numBullets++, 8, 8, .25f);
bullet.setModelBound(new BoundingSphere());
bullet.updateModelBound();
/** Move bullet to the camera location */
bullet.setLocalTranslation(new Vector3f(cam.getLocation()));
bullet.setRenderState(bulletMaterial);

Whenever the F key is pressed, first I create my bullet. Next I move it to the camera’s position (after all that’s where the bullet will come from) and then give it a greenish color:

/**
* Update the new world locaion for the bullet before I add a
* controller
*/
bullet.updateGeometricState(0, true);

I call the function updateGeometricState(0, true) because I need to update the bullet’s geometric information (more importantly its boundingsphere). This gives the bullet the correct worldbounds so that the immediately following update() for BulletMover() will know the correct location for the bullet. The true simply states that bullet is starting the updateGeometricState call. This is needed because the function is recursive for all of bullet’s children (if it was able to have any). Behind our backs, every frame SimpleGame calls rootNode.updateGeometricState(time_per_frame, true) to move and update all of rootNode’s children. Next, I give the bullet a controller to move it:

/**
 * Add a movement controller to the bullet going in the camera's
 * direction
 */
bullet.addController(new BulletMover(bullet, new Vector3f(cam
		.getDirection())));

This moves the bullet in the camera’s direction. We’ll get into BulletMover later, but for now lets finish our F key press:

rootNode.attachChild(bullet);
bullet.updateRenderState();

Next we attach the nodes and call updateRenderState() on the bullet. Again, updateRenderState() is called behind our backs in SimpleGame after simpleInitGame(). The function takes all the render states we are trying to apply to a spatial and actually applies them, as well as any children states. Without updating our render states for our bullet, it would look red because it doesn’t know about the green.

Finally, we signal to laserSound that it first update it's position and then play:

/** Signal our sound to play laser during rendering */
laserSound.setWorldPosition(cam.getLocation());
laserSound.play();

Moving The Bullet

Now, lets look at BulletMover. Notice it is a controller just like KeyframeController (jME API). Its purpose is to move a spatial in a given direction. Every time updateGeometricState() is called on the bullet (to which the BulletMover is attached) or bullet’s parent (rootNode), the function update is called on all of bullet’s controllers. Lets look at update():

public void update(float time) {

The time is the time between successive frames. Why? Because in that’s the float value given to updateGeometricState() in SimpleGame’s update method. Next, I see if I can remove my bullet:

lifeTime -= time;
/** If life is gone, remove it */
if (lifeTime < 0) {
	rootNode.detachChild(bullet);
	bullet.removeController(this);
	return;
}

If the bullet has been alive for more than 5 seconds, I remove it. Simple enough, right? Next, I move the bullet in its direction:

/** Move bullet */
Vector3f bulletPos = bullet.getLocalTranslation();
bulletPos.addLocal(direction.mult(time * speed));
bullet.setLocalTranslation(bulletPos);

The float time is the value given to update() and the float speed is just a constant for the bullet that makes it move faster. Next, I check to see if the bullet has hit my target:

/** Does the bullet intersect with target? */
if (bullet.getWorldBound().intersects(target.getWorldBound())) {

Note that I am checking if the bullet’s worldBound intersects the target’s worldbound. A BoundingVolume for a mesh can be bigger than the actual mesh, which means that two bounds can intersect while the objects don’t. If this were a real game, I would add a more complex intersection method after this one to see if the two TriMesh objects intersect, but for now we’re keeping it simple. If the bounds intersect, I simply move the target to a random location and kill the bullet.

Update And Cleanup

The only new things left are in simpleUpdate and cleanup.

First lets have a look at simpleUpdate:

// Let the programmable sound update itself.
AudioSystem.getSystem().update();
// Move the skybox into position
sb.getLocalTranslation().set(cam.getLocation().x, cam.getLocation().y,
		cam.getLocation().z);

I have to update my sounds just like SimpleGame does for my rootNode, and so that’s what I’m doing here. Update the skybox location to keep surrounding the player.

super.cleanup();
if (AudioSystem.isCreated()) {
	AudioSystem.getSystem().cleanup();
}

The SimpleGame method cleanup is extended to clean up the AudioSystem as well.

Scene Graph

Here are two graphs of the game:


fpsNode has font TextureState
“fps” shows frame data in SimpleGame Cross “+”


rootNode
target “my sphere” has redMaterial sb “skybox” has Clouds texture bullet (0+ of these) has bulletMaterial
For each bullet

/var/www/wiki/data/pages/starter/hello_intersection.txt · Last modified: 2010/02/13 10:02 by zathras  
Recent changes · Show pagesource · Login

Recent changes RSS feed Creative Commons License Donate Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki

subscribe to jME latest jme headlines


site design by bleedcrimson designs © 2008