Monkey Blaster (Toolforger edition), p1

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 and PlayerShip.
  • 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 and Sound are covered by JME’s AssetLoader.
  • We do not need the code in MathUtil and Extensions.

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.
 * &lt;p&gt;
 * 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.&lt;br&gt;
 * 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.

4 Likes

thoughts on the tutorial:

  • id slim down the inital paragaphs to just one paragraph that explains the tutorial and lists the main classes/concepts/features explored in the tutorial. A lot of stuff you mention might be better served lower down in the tutorial after the newcomer has seen some code (otherwise it might just seem like jibber jabber)

  • I really like the style of the tutorials in the wiki that show all the code, then analyze chunks of a code one at a time and talks about just that segment. (just a personal style/preference suggestion for what helps me learn stuff)

  • I’m guessing this will eventually be in the jmonkey wiki? whenever that happens I think all the mentions of XNA should be kind of put in their own sidenote in a different font as being just a note to people transitioning from XNA or mono.

thoughts on the code:

-I think you should remove the complicated constructor and do all that code in SimpleApplication. With the constructor youre kind of defining limitations to the “screen object” control. Without that constructor you could use this control with any spatial and not just “node with a picture child”. I know you said youre not focusing on “best practices” since its a tutorial, but I think the way you make the class kinds of misleads people on how Controls are generally used. (I’m sure this could possibly be debated some)

  • youve written your own clamp() method, but one already exists at FastMath.clamp(), this would be a good chance to show new users this incredibly convenient math class.

  • you have non thread safe public methods on your control. (setRotation(), setVelocity() and the like) that could be potentially called (by accident) outside of the game loop. I think you should add comments to them that say not thread safe, or make them thread safe by deferring the actual rotation code to the update loop.

Otherwise, Good job =D , i wish i had more game examples/tutorials when i first started getting in to jmonkey. these sort of tutorials are certainly the kind of things jmonkey needs!

1 Like

Thanks for the feedback :slight_smile:

The tutorial is currently targetted as an alternate to the XNA tutorial, so I’m following the existing tutorial’s structure to make it easy to compare.
If this goes to the wiki, I agree that other aspects should be highlighted.

Code-analysis-code is a good practice.
I have been planning to take the implementation comments out and turn them into analysis sections, I didn’t find the time to do that yet (I still have to put up the remaining classes).

Yeah, the constructors are ugly. OTOH setters would be a ton of boilerplate code, both in the class and in call sites, and it would introduce the risk of forgetting to initialize something.
The PlayerShip constructor is going to be much nicer though :slight_smile:
I’ve been thinking about turning the coordinate pairs into Vector2f or Vector3f objects to cut on the number of parameters. It would be somewhat less efficient on Android if the GC is bad (OTOH there isn’t that much object construction going on per second, so it’s not THAT important).

Thanks for the pointer to FastMath.clamp, I’d have used that if I hadn’t overlooked it.

Thread safety - hmm, yes, the angle and the position should probably be collected and transferred to the scene graph only inside update().
Though everything in these classes is going to be called from inside the update loop anyway. The only exception is the PlayerShip object that’s created in the main program BEFORE the update loop starts… I’m not 100% sure how to proceed with this without complicating the code (with “complicating” I mean peppering the members with volatile markers and synchronized sections).
Maybe it’s enough to mention that the whole controller object is running inside the rendering thread so we’re avoiding all concurrency issues, and link to the concurrency page in the JME wiki.

Thanks again, this has been very useful feedback.

@toolforger said: The only exception is the PlayerShip object that's created in the main program BEFORE the update loop starts...

How are you doing that? In main()? simpleInitApp is run on the render thread. main() is not run on Android or Applets.

Oh. No main() in Android? Now that’s making some things fall in place for me, I didn’t know that.

And yes, PlayerShip is created in simpleInitApp(), so everything is fine :slight_smile:

when you set up your project for android in jme, the sdk automatically creates some basic code to just start your game as a proper android application.

The way it starts your game is by using java reflection to get the constructor of simple application with no parameters, and call newInstance(). So you also need to make sure your SimpleApplication uses a constructor with no parameters… also i’m pretty sure it ignores the “AppSettings” of simple application (maybe someone else can confirm though)

Hoping to be able to contribute something of some sort, as a student to the teacher I’d say:

The beginner tutorials that show you code and then talk about the code bit by bit, are, as already mentioned, quite kickass-like…

Although if I remember correctly there were a few places I had to frown a couple of times because I didn’t like something about it… if I remember I will come back I guess…

The overview BEFORE the code I personally think is nicer here than in the beginner tuts…

  • I came (and kinda still come) as a very inexperienced person, and getting to type in code and see it in action quick and then go over what the different parts did was great, but at times the code became jibber-jabber becuase there wasn’t enough jibber-jabber before the code…

-.-.-.--.-.-.--.-.-.-_-.-.-.-
In general, for studying stuff, this is my attempt at writing what I think, I have the best time with :
*/ A quick */ flash-overview that gives some quick first-impressions,
*/ followed by */ some hands-on experience,
*/ followed by */ study of what is done in the hands-on section,
*/ followed by */ some play-around,
*/ followed by */ study of play-around,
*/ followed by */ correction of errors in play-around
, */ followed by */ investigating the ideas inspired.

Attempt at explanation:
… The first time you see something, you’re not ready to interact with it … Like when you see someone throw a punch the first few times, you flinch but then you’ve seen it and then you kick their arse… (in games… as well as IRL I guess)

  • Herefore, the flash overview
    Then, you wanna mess around with stuff if it catches your interest…
  • hands-on exp
    … long story short, this feels like the most ‘natural’ learning process to me, I think it has merit in some sense… I may have missed something … I just wanted to post this in the hopes I may inspire some awesome teaching… cuz then I don’t have to spend so much god damn time learning… which sucks… (spending (too much) time that is…) :slight_smile:

Hope I wasn’t to much of a “wtf” post with this… :wink:

Yeah, I see what you mean, and I agree it’s a worthy goal.

As soon as I can edit the top post, I’ll do more of interspersing code with explanations.
Taking code snippets out of their class risks introducing bugs, so I’ll format the explanations as comments inside the code; the final version will properly separate it.

I can’t change too much of the overall structure, as the result is still supposed to be roughly structured along the lines of the XNA tutorial to keep the two comparable.

I modified a few things in the introductory text, deleted text is struck out, new text is underlined.
Also, there are new classes, PlayerShip and the main program, MonkeyBlaster.

Comments welcome.

Agh… somehow the line breaks got mangled. I’ll have to revisit the page.

[java]public PlayerShip(AssetManager assetManager, InputManager inputManager,
Node guiNode, Vector2f screenSize, Vector2f position) {
super(assetManager, “Player”, guiNode, screenSize, position, RADIUS);
}[java]

it should be

[java]public PlayerShip(AssetManager assetManager, InputManager inputManager, Node guiNode, Vector2f screenSize, Vector2f position)
{
super(assetManager, “Player”, guiNode, screenSize.x,screenSize.y, position.x,position.y, RADIUS);
}[/java]

as required parameter is of type (…,float,float,float…)
and found (Vector2f,vector2f,…)

Ah, I just changed the interface in my code.
Obviously, I updated one code snipped and forgot to adapt the other.
Thanks for the heads-up, I’ll fix this with the next installment.

There’s now an iOS version of this Tuts+ tutorial.Your optimized version could be a really good starting point for the jME3 Android/iOS version of this tutorial. If you decide to take a crack at it, I think there’s a good chance they’d want to add it to their site.