Note: This is a work in progress. I’ll be collecting improvement proposals and integrating them into this first posting.
I’m intentionally ignoring best practices until they show merit within the context of the tutorial. Those that have merit only for larger projects will be left out entirely; I’ve been planning to add a “remaining challenges” page where these are listed as exercises people could do on their own.
Best practices being skipped on page 1 are:
Separating the world model from the scene graph. It would help separate the game logic from the display logic, at the cost of complicating everything. Not worth it since the scene IS the game here. <span style=“text-decoration:line-through;”>Should be explained on page 6.</span> I plan to outline how to go about that separation on page 6.
An entity system. <span style=“text-decoration:line-through;”>It does not pay off for a handful of different game objects, particularly if the set of members for each class is predetermined and you can set up the class hierarchy to match that.
I want to include a primitive, handmade ES as soon as the entity zoo starts to show how that’s useful. I haven’t yet determined when that will be, but if necessary I’ll artificially introduce something that forces an entity system.</span> Now that I understand entity systems better, I doubt I’m able to write a good tutorial on it; there’s more to it than I originally though. I’ll simply punt on the subject; I’ll probably write a short paragraph with a link to the usual pages, and possible a sentence or two about how one would restructure the tutorial to use an ES.
(The XNA tutorial applies the term “Entity” to things that <span style=“text-decoration:underline;”>definitely</span> aren’t entities as defined in the context of an entity system,<span style=“text-decoration:line-through;”> of that I’m pretty sure</span>.)
AppStates. There are simply no states to switch until quite far into the tutorial.
We’ll get two AppStates late in the tutorial: in-game and highscore screen. The tutorial page that introduces the highscore screen will introduce the AppState concept (and mention that F5 also switches an AppState, so people know it’s not just the big mode changes that call for an AppState).
(I’ll add more missing best practices as astute readers will detect their absence.)
Oh, and remarks that aren’t supposed to go into the final tutorial will be in italics, just like this text.
Here we go. <span style=“text-decoration:line-through;”>Alpha version, it’s just being typed in and not yet proofread in any way - I guess fixing conceptual problems is more important initially.</span> First half is supposed to be complete, with maybe some cleanup missing, second half is alpha version.
All and any feedback welcome.
This is a JME version of the neon vector shooter tutorial found at Make a Neon Vector Shooter in XNA: Basic Gameplay
Overview
As in the original tutorial, we will create a twin-control shooter: The WASD keys will move the ship, the mouse will determine the direction we’re shooting at. This is a bit of a grey area; I have little experience using anything but keyboard and mouse, so I’m just doing keyboard and mouse. Is JME even able to support a gamepad? I’d prefer to add a dual-touch control for Android, but I have no smartphone to test on so I’ll simply pick up what anybody proposes to do for that. <span style=“text-decoration:underline;”>How to do alternate controls, both on the desktop and on Android, is still an open issue.</span>
We use a number of classes to accomplish this:
-
ScreenObject
: The base class for enemies, bullets, and the player’s ship. Entities can move and are visible as part of JME’s scene graph. -
Bullet
andPlayerShip
. -
MonkeyBlaster
: The main class of the game. Initializes everything, then starts the game loop.
This is a lot less classes than what’s needed for the XNA version. This is because JME is providing readymade data structures and mechanisms that the XNA version had to implement on its own:
-
EntityManager
is covered by JME’s scene graph. - We are doing one thing differently: Input is handled directly in the PlayerShip class.
-
Art
andSound
are covered by JME’sAssetLoader
. - We do not need the code in
MathUtil
andExtensions
.
Note that these differences should not be overestimated. Every framework has areas where it’s better and others where it is worse. Also, some differences are only relevant for game initialization and get eclipsed quickly as the game grows.
Screen Objects and the Player’s Ship
Create a new JME project. Rename the Main
class to something different if you wish. I called it MonkeyBlaster
and moved it to the org.toolforger.demo.monkeyblaster
package.
Now let’s start by creating a base class for our screen objects.
[java]package org.toolforger.demo.monkeyblaster;
import com.jme3.asset.AssetManager;
import com.jme3.asset.TextureKey;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector2f;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.control.AbstractControl;
import com.jme3.texture.Texture2D;
import com.jme3.ui.Picture;
public abstract class ScreenObject extends AbstractControl {
public ScreenObject(AssetManager assetManager, String name, Node guiNode,
float screenWidth, float screenHeight, float x, float y,
float radius) {
// Texture
String texturePath = "Textures/" + name + ".png";
boolean flipY = false;
TextureKey textureKey = new TextureKey(texturePath, flipY);
Texture2D texture = (Texture2D) assetManager.loadTexture(textureKey);
float width = texture.getImage().getWidth();
float height = texture.getImage().getHeight();
// Picture
final boolean useAlpha = true;
Picture picture = new Picture(name);
picture.setTexture(assetManager, texture, useAlpha);
picture.setWidth(width);
picture.setHeight(height);
picture.setPosition(width / -2.0f, height / -2.0f);
// node
node = new Node(name);
node.attachChild(picture);
guiNode.attachChild(node);
node.addControl(this);
// screen extent
this.screenWidth = screenWidth;
this.screenHeight = screenHeight;
// position
setPosition(x, y);
// radius
this.radius = radius;
}
// /////////////////////////////////////////////////////////////////////////
// Link to jME3 scene graph
final private Node node;
public Spatial getRoot() {
return node;
}
public void delete() {
node.removeFromParent();
}
// /////////////////////////////////////////////////////////////////////////
// XY position
public void move(float deltaX, float deltaY) {
node.move(deltaX, deltaY, 0f);
adjustForNonlinearities();
}
public void setPosition(float x, float y) {
node.setLocalTranslation(x, y, 0);
adjustForNonlinearities();
}
public float getX() {
return node.getLocalTranslation().x;
}
public float getY() {
return node.getLocalTranslation().y;
}
// /////////////////////////////////////////////////////////////////////////
// Border handling
protected float screenWidth;
protected float screenHeight;
/**
* React to any nonlinearities that go beyond simply continuing to fly in a
* straight line.
* <p>
* The default implementation simply clamps the coordinates to within 0 to
* screen size.
*/
protected void adjustForNonlinearities() {
float nodeX = getX();
float newX = FastMath.clamp(nodeX, 0f, screenWidth);
float nodeY = getY();
float newY = FastMath.clamp(nodeY, 0f, screenHeight);
if (nodeX != newX || nodeY != newY) {
node.setLocalTranslation(newX, newY, 0);
}
}
// /////////////////////////////////////////////////////////////////////////
// Rotation
public void rotate(float deltaAngle) {
node.rotate(0f, 0f, deltaAngle);
}
private Quaternion rotation = new Quaternion();
public void setRotation(float angle) {
rotation.fromAngles(0f, 0f, angle);
node.setLocalRotation(rotation);
}
/**
* Rotate to "look alongside" the given direction vector.<br>
* Do nothing if direction is the zero vector.
*/
public void setRotation(Vector2f direction) {
if (direction.x != 0f || direction.y != 0f) {
setRotation(FastMath.atan2(direction.y, direction.x));
}
}
// /////////////////////////////////////////////////////////////////////////
// Velocity
private Vector2f velocity = new Vector2f();
public void setVelocity(float vx, float vy) {
if (vx != velocity.x || vy != velocity.y) {
// New velocity is actually different from previous velocity
velocity.set(vx, vy);
setRotation(velocity);
}
}
// /////////////////////////////////////////////////////////////////////////
// Collision
/**
* Collision radius
*/
public final float radius;
// /////////////////////////////////////////////////////////////////////////
// Control implementation
@Override
protected void controlUpdate(float tpf) {
move(velocity.x * tpf, velocity.y * tpf);
}
@Override
protected void controlRender(RenderManager rm, ViewPort vp) {
// Nothing special to do before rendering.
}
}
[/java]
ScreenObject
extends AbstractControl
.
This makes sure that JME3 will call into this object whenever the associated scene graph node is visible, so we can do position updates, check for player input, and do whatever is needed to update things for the next frame.
We do not have any explicit drawing code, this is handled by the JME rendering loop for us. All we do is to create a Node
object in the scene graph and enter it as a child of a scene graph node.
One slightly atypical thing is that we are using guiNode
instead of the normal rootNode
.
Most JME games are using full 3D, and for that, you need to control camera position, viewing angle etc. explicitly.
For our 2D shooter, the GUI node already has everything set up so that everything positioned in the Z plane will be shown on the correct screen coordinates.
We’re now ready for the player’s ship:
[java]package org.toolforger.demo.monkeyblaster;
import com.jme3.asset.AssetManager;
import com.jme3.input.InputManager;
import com.jme3.input.controls.ActionListener;
import com.jme3.math.Vector2f;
import com.jme3.scene.Node;
public class PlayerShip extends ScreenObject implements ActionListener {
public static int RADIUS = 10;
public static float SPEED = 800f;
public static float SHOOT_INTERVAL = 0.1f;
public static int SHOT_COUNT = 2;
public PlayerShip(AssetManager assetManager, InputManager inputManager,
Node guiNode, Vector2f screenSize, Vector2f position) {
super(assetManager, "Player", guiNode, screenSize, position, RADIUS);
}
@Override
public void onAction(String name, boolean isPressed, float tpf) {
// Do nothing here (yet)
}
}
[/java]
<bold>Showing it all</bold>
We need a main program.
JME programs are built by constructing an Application
object and start()
ing it.
There is a standard SimpleApplication
class. It offers us a 2D scene already positioned properly so that the standard camera will always see it, and a statistics display telling us frames per second and, more importantly, number of OpenGL objects that the application uses - the actual numbers there aren’t that interesting, but if one of these numbers is permanently increasing you know that you didn’t properly clean up some data structures.
SimpleApplication
also offers a “fly-by camera” that you can use to fly through whatever 3D scene we are constructing. Since we’re doing just 2D, and since it eats the WASD keys that we’ll want to move the player ship with later, we switch the fly-by camera off.
Here’s the main program:
[java]package org.toolforger.demo.monkeyblaster;
import com.jme3.app.SimpleApplication;
import com.jme3.math.Vector2f;
import com.jme3.system.AppSettings;
public class MonkeyBlaster extends SimpleApplication {
public static void main(String[] args) {
new MonkeyBlaster().start();
}
public static int SCREEN_WIDTH = 800;
public static int SCREEN_HEIGHT = 600;
public static Vector2f SCREEN_SIZE = new Vector2f(SCREEN_WIDTH,
SCREEN_HEIGHT);
public static Vector2f SCREEN_CENTER = new Vector2f(SCREEN_WIDTH / 2f,
SCREEN_HEIGHT / 2f);
public MonkeyBlaster() {
// JME wants an AppSettings object that contains some global
// initializations. It will usually ask the player about these, but we
// can also set it all up programmatically.
// Since players are supposed to just blast away without much ado, let's
// do that programmatically!
settings = new AppSettings(true); // Start with default settings
settings.setWidth(SCREEN_WIDTH); // Windowed mode with that size
settings.setHeight(SCREEN_HEIGHT);
settings.setTitle("Monkey Blaster!"); // Window title
showSettings = false; // No settings screen before the game starts
setSettings(settings);
}
@Override
public void simpleInitApp() {
// JME has a standard fly-by camera so that people can fly through
// whatever 3D scene they are building and watch it all.
// We don't need that for 2D, so we disable it:
getFlyByCamera().setEnabled(false);
// Populate the scene. We only need a player ship.
new PlayerShip(assetManager, inputManager, guiNode, SCREEN_SIZE,
SCREEN_CENTER);
}
}
[/java]
Try it out! You should now see this:
I know, I know, It’s a lot of code for just getting a few lines on the screen, so let’s make it move around next.