Space Rocks Arcade Shooter for Android

My latest Android game is now available from the Amazon App Store and as a direct download on my website.
My site: spacerocksmobile
Amazon: Amazon.com
Google Play: https://play.google.com/store/apps/details?id=com.atr.spacerocks


Enjoy…

6 Likes

Hi @Tryder

When running i am getting :

java.lang.NoClassDefFoundError: java.util.Objects

Exception thrown in Thread [GLThread 1920,5,main]

java.lang.NoClassDefFoundError: java.util.Objects
at com.simsilica.lemur.core.ComponentStack.hasLayer(ComponentStack.java: 153)
at com.simsilica.lemur.core.ComponentStack.setComponent(ComponentStack.java:223)

at com.simsilica.lemur.core.GuiControl.setComponent(GuiControl.java:328)

at com.simsilica.lemur.Panel.setPadding(Panel.java:283)

at com.atr.spacerocks.state.Initialize.<init>(Initialize.java:166)

at com.atr.spacerocks.SpaceRocks.simpleInitApp(SpaceRocks.java:149)

at com.jme3.app.SimpleApplication.initialize(SimpleApplication.java:220)

at com.jme3.app.AndroidHarnessFragment.java:556)

at com.jme3.system.android.OGLESContext.onDrawFrame(OGLESContext.java:328)
at android.opengl.GLSurfaceView$GLThread.guardedRun(GLSurfaceView.java:1468)
at android.opengl.GLSurfaceView$GLThread.run (GLSurfaceView.java:1222)

I am on Android 4.0.3

Seems like Android only got this class from 4.4 on. I’ve found a couple of threads about it online and that seems to be when it was added… though in one place I saw it in the 4.2 codebase. So who knows?

For reference:
https://groups.google.com/forum/#!topic/javafxandroid/QmqMk0Jk1yw
https://android.googlesource.com/platform/frameworks/base.git/+/android-4.2.2_r1/core/java/com/android/internal/util/Objects.java

1 Like

I uploaded a new version that removes the usage of the Objects class from the specified method. If you could please head over to my website and download the ‘direct’ linked one and let me know if it works, I don’t know if Objects was used anywhere else in Lemur or not. If it works I’ll upload the new version to Amazon.

Nevermind I just did a search and it’s used a bunch more times in Lemur, it’ll take me a bit to clean it all out.

Tested it on my brother’s phone with Android 6 and it runs OK. :slight_smile:

According to that one thread, you should just be able to add it to compat.jar.

Seems like the easier route than further forking Lemur.

And note that according to this:
https://developer.android.com/about/dashboards/index.html

…for the majority of devices out there, it won’t even be an issue.

1 Like

Okay @Ali_RS, you certainly don’t have to, but if you’d like to test out the new version I just uploaded to the ‘direct’ link on my website I would appreciate it.

@pspeed I didn’t change a whole lot, not sure if this will work for Android pre-4.4, but you were already using com.google.common.base.Objects instead of java.util.Objects in most places anyway so I just updated the few remaining places to use com.google.common.base.Objects too.

It just feels weird to modify source code for something that affects less than 10% of mobile devices, doesn’t affect desktop at all, and has an (apparently) easy work-around that is already android specific.

I was a little surprised to see @Ali_RS had an error like that, Amazon tests it on a lot of devices, some dating back to 2012 and Space Rocks ran on all of them, even Fire TV and Fire TV stick.

Quite fun to play. I’d love to have show FPS option, because my phone seems to have FPS problems in jme games and I’d like to see if there’s something wrong with my app or it applies to all jme apps.
Also if you’d like someone to publish it on Google Play for you, I’m pretty sure there are plenty people (including me) with google play dev accounts.

@Tryder i tested it on my phone and now it runs without problem. thanks
Game looks very nice and i really like the GUI.
Nice to see lemur on android.
btw, are you using groovy styles ?

On my phone I get a solid 60fps and it’s not a powerhouse, it’s on the cheaper end nowhere near Galaxy or iPhone in terms of price. My tablet runs around 30ish fps with particle detail on low, though the bulk of the workload is the physics engine I think, Dyn4j. I limit the number of actors in the physics simulation, shouldn’t really hit any higher than 64 and isn’t likely to get that high, should hover around 32 for the most part.

As for Google Play I can swing the $25 entry fee, just didn’t want to blow the money and have Google reject the game for whatever reason. Now that Amazon has accepted it I’m more confident that Google would too, I might put it up there, I might not time will tell.

@Ali_RS No I am not using Groovy, did all my styles in Java. Thanks for testing, I’ll get the new version uploaded to Amazon.

Edit: The new version is up on Amazon, usually takes an hour or so before it’s live on the site.

On my phone even this game feels laggy a little, but it’s not so obvious than in LSF app. This is why I can’t really tell what the fps is.
My phone is mid range and should be able to run the game just fine, but I suspect there is something wrong with graphic chip drivers since sometimes my LSF app works at 60FPS but most of the time it doesn’t.

That is the thing with Android I suppose, a lot of different hardware out there with all sorts of custom OS jobs and you never know for sure the build quality of the OS modifications. It looks like my phone uses a version of Cyanogen, I noticed a few references to CyanogenMod in LogCat while debugging Space Rocks.

Came with 4.4 Kitkat and was later updated to 5.0.

By the by, my high score is 1545 :slight_smile:

Hi @Tryder.

I would like to congratulate you on this android game. This game was very well done and very very smooth with
exceptional graphics on my phone. (Galaxy Note4).

Some things that really stood out:

  1. Smooth graphics of text
  2. Control of ship
  3. Particle effects…Loved the portals
  4. 3D menu’s… very nice

What I like to see improved:

  1. Larger text of score in play screen
  2. Acceleration and deceleration of ship.
  3. Larger joystick.
  4. I would love to see it on google play store with leader board services like google play services or facebook.

Other than that, very very cool game.

1 Like

Thanks I really appreciate your feedback.

1 Like

So, uh, what’s the black hole spawn rate? I’ve shot every yellow crystal I’ve seen but it just won’t show up.

My experience was that text is hard to read when there’s a lot of it and it’s swinging around like crazy like in the tutorial. The framerate was stable with occasional lag spikes, but that’s sort of expected on a phone like mine - an LG G3 which is underpowered for its 1080p screen.

Other than that, great job!

What’s the tech behind the curved bars if I may ask? The only way I can picture doing that is by making a shader that discards everything above a certain y value but I don’t think that’s how you did it. Well unless there was some sort of angle math involved that gives the bended endpoint look. :stuck_out_tongue:

The first black hole spawns 60 seconds in, after that they spawn 30-90 seconds after the previous one was spawned.

You are correct, there is some sort of angle math being used for the curved meters. The curved meter is a custom mesh that I generate based on a desired angle and height. The UV coordinates generated when the mesh is created are used for two things, anti-aliasing and setting the alpha value for any y-axis UV coordinate above a “Percent” variable to zero. That part is done in the shader.

public class CurvedMeter extends Mesh {
    private final ColorRGBA col1;
    private final ColorRGBA col2;
    private final ColorRGBA meterColor;
    
    private final float meterAngle;
    private final float meterEndAngle;
    private final float gapAngle;
    private final float gap;
    private final float meterWidth;
    private final float valueWidth;
    private final int side;
    private final float radius;
    
    private final int div;
    
    private float width = 0;
    private float height = 0;
    
    /**
     * 
     * @param angle Overall angle of the meter and value
     * @param endAngle The height of the meter's end caps
     * @param gapAngle Gap between the meter's end caps and
     * meter value
     * @param gap Gap between the meter and value
     * @param divisions Number of subdivisions for the curve
     * @param meterWidth Width of the meter
     * @param valueWidth Width of the value
     * @param scale Scale of the overall meter and value
     * @param height The height of the meter
     * @param valCol1 Bottom color of the value
     * @param valCol2 Top color of the value
     * @param meterColor Color of the meter
     */
    public CurvedMeter(float angle, float endAngle, float gapAngle, float gap,
            int divisions, float meterWidth, float valueWidth,
            float scale, float height, ColorRGBA valCol1, ColorRGBA valCol2,
            ColorRGBA meterColor) {
        meterAngle = angle * FastMath.DEG_TO_RAD;
        meterEndAngle = endAngle * FastMath.DEG_TO_RAD * Math.abs(scale);
        this.gapAngle = gapAngle * FastMath.DEG_TO_RAD * Math.abs(scale);
        this.gap = gap * scale;
        div = divisions;
        this.meterWidth = meterWidth * scale;
        this.valueWidth = valueWidth * scale;
        this.side = scale >= 0 ? 1 : -1;
        
        this.radius = (height / (2 * FastMath.sin(meterAngle / 2))) * this.side;
        col1 = valCol1;
        col2 = valCol2;
        this.meterColor = meterColor;
        
        createMesh();
    }
    
    public float getRadius() {
        return radius;
    }
    
    public float getWidth() {
        return width;
    }
    
    public float getHeight() {
        return height;
    }
    
    private void createMesh() {
        int mCol = meterColor.asIntABGR();
        ColorRGBA vCol = col1.clone();
        
        Vector3f v = new Vector3f();
        Quaternion quat = new Quaternion();
        
        FloatBuffer verts = BufferUtils.createVector3Buffer((div * 4) + 8);
        FloatBuffer tex = BufferUtils.createVector2Buffer((div * 4) + 8);
        FloatBuffer tex2 = BufferUtils.createVector2Buffer((div * 4) + 8);
        ByteBuffer col = BufferUtils.createByteBuffer(((div * 4) + 8) * 4);
        ShortBuffer indices = BufferUtils.createShortBuffer(((div - 1) * 12) + 12);
        
        //Bottom meter cap
        float angle = -meterAngle / 2;
        quat.fromAngles(0, 0, angle);
        v.set(radius, 0, 0);
        quat.mult(v, v);
        verts.put(v.x);
        verts.put(v.y);
        verts.put(v.z);
        col.putInt(mCol);
        tex.put(1);
        tex.put(-1);
        tex2.put(1);
        tex2.put(0);
        
        height = Math.abs(v.y * 2);
        
        v.set(radius - meterWidth - gap - valueWidth, 0, 0);
        quat.mult(v, v);
        verts.put(v.x);
        verts.put(v.y);
        verts.put(v.z);
        col.putInt(mCol);
        tex.put(0);
        tex.put(-1);
        tex2.put(0);
        tex2.put(0);
        
        width = Math.abs(radius - v.x);
        
        angle += meterEndAngle;
        quat.fromAngles(0, 0, angle);
        v.set(radius, 0, 0);
        quat.mult(v, v);
        verts.put(v.x);
        verts.put(v.y);
        verts.put(v.z);
        col.putInt(mCol);
        tex.put(1);
        tex.put(-1);
        tex2.put(1);
        tex2.put(meterEndAngle / meterAngle);
        
        v.set(radius - meterWidth - gap - valueWidth, 0, 0);
        quat.mult(v, v);
        verts.put(v.x);
        verts.put(v.y);
        verts.put(v.z);
        col.putInt(mCol);
        tex.put(0);
        tex.put(-1);
        tex2.put(0);
        tex2.put(meterEndAngle / meterAngle);
        
        //top meter cap
        angle = (meterAngle / 2) - meterEndAngle;
        quat.fromAngles(0, 0, angle);
        v.set(radius, 0, 0);
        quat.mult(v, v);
        verts.put(v.x);
        verts.put(v.y);
        verts.put(v.z);
        col.putInt(mCol);
        tex.put(1);
        tex.put(-1);
        tex2.put(1);
        tex2.put((meterAngle - meterEndAngle) / meterAngle);
        
        v.set(radius - meterWidth - gap - valueWidth, 0, 0);
        quat.mult(v, v);
        verts.put(v.x);
        verts.put(v.y);
        verts.put(v.z);
        col.putInt(mCol);
        tex.put(0);
        tex.put(-1);
        tex2.put(0);
        tex2.put((meterAngle - meterEndAngle) / meterAngle);
        
        angle = meterAngle / 2;
        quat.fromAngles(0, 0, angle);
        v.set(radius, 0, 0);
        quat.mult(v, v);
        verts.put(v.x);
        verts.put(v.y);
        verts.put(v.z);
        col.putInt(mCol);
        tex.put(1);
        tex.put(-1);
        tex2.put(1);
        tex2.put(1);
        
        v.set(radius - meterWidth - gap - valueWidth, 0, 0);
        quat.mult(v, v);
        verts.put(v.x);
        verts.put(v.y);
        verts.put(v.z);
        col.putInt(mCol);
        tex.put(0);
        tex.put(-1);
        tex2.put(0);
        tex2.put(1);
        
        //meter
        angle = -(meterAngle / 2) + meterEndAngle;
        float step = (meterAngle - (meterEndAngle * 2)) / (div - 1);
        float wholeAngle = meterEndAngle;
        for (int i = 0; i < div; i++) {
            quat.fromAngles(0, 0, angle);
            v.set(radius, 0, 0);
            quat.mult(v, v);
            verts.put(v.x);
            verts.put(v.y);
            verts.put(v.z);
            col.putInt(mCol);
            tex.put(1);
            tex.put(-1);
            tex2.put(1);
            tex2.put(wholeAngle / meterAngle);
            
            v.set(radius - meterWidth, 0, 0);
            quat.mult(v, v);
            verts.put(v.x);
            verts.put(v.y);
            verts.put(v.z);
            col.putInt(mCol);
            tex.put(0);
            tex.put(-1);
            tex2.put(0);
            tex2.put(wholeAngle / meterAngle);
            
            angle += step;
            wholeAngle += step;
        }
        
        //meter value
        angle = -(meterAngle / 2) + meterEndAngle + gapAngle;
        step = (meterAngle - ((meterEndAngle + gapAngle) * 2)) / (div - 1);
        for (int i = 0; i < div; i++) {
            float perc = (float)i / (div - 1);
            perc = side > 0 ? perc : 1 - perc;
            GMath.smoothRGBA(perc, vCol, col1, col2);
            int iCol = vCol.asIntABGR();
            
            quat.fromAngles(0, 0, angle);
            v.set(radius - meterWidth - gap, 0, 0);
            quat.mult(v, v);
            verts.put(v.x);
            verts.put(v.y);
            verts.put(v.z);
            col.putInt(iCol);
            tex.put(1);
            tex.put(perc);
            tex2.put(1);
            tex2.put(perc);
            
            v.set(radius - meterWidth - gap - valueWidth, 0, 0);
            quat.mult(v, v);
            verts.put(v.x);
            verts.put(v.y);
            verts.put(v.z);
            col.putInt(iCol);
            tex.put(0);
            tex.put(perc);
            tex2.put(0);
            tex2.put(perc);
            
            angle += step;
        }
        
        Short index = 8;
        indices.put((short)1);
        indices.put((short)0);
        indices.put((short)2);
        indices.put((short)2);
        indices.put((short)3);
        indices.put((short)1);

        indices.put((short)5);
        indices.put((short)4);
        indices.put((short)6);
        indices.put((short)6);
        indices.put((short)7);
        indices.put((short)5);

        for (int i = 0; i < div - 1; i++) {
            indices.put((short)(index + 1));
            indices.put(index);
            indices.put((short)(index + 2));
            indices.put((short)(index + 2));
            indices.put((short)(index + 3));
            indices.put((short)(index + 1));

            index = (short)(index + 2);
        }

        index = (short)(index + 2);

        for (int i = 0; i < div - 1; i++) {
            indices.put((short)(index + 1));
            indices.put(index);
            indices.put((short)(index + 2));
            indices.put((short)(index + 2));
            indices.put((short)(index + 3));
            indices.put((short)(index + 1));

            index = (short)(index + 2);
        }
        
        verts.flip();
        VertexBuffer vb = new VertexBuffer(VertexBuffer.Type.Position);
        vb.setupData(VertexBuffer.Usage.Static, 3, VertexBuffer.Format.Float, verts);
        setBuffer(vb);
        
        tex.flip();
        vb = new VertexBuffer(VertexBuffer.Type.TexCoord);
        vb.setupData(VertexBuffer.Usage.Static, 2, VertexBuffer.Format.Float, tex);
        setBuffer(vb);
        
        tex2.flip();
        vb = new VertexBuffer(VertexBuffer.Type.TexCoord2);
        vb.setupData(VertexBuffer.Usage.Static, 2, VertexBuffer.Format.Float, tex2);
        setBuffer(vb);
        
        col.flip();
        vb = new VertexBuffer(VertexBuffer.Type.Color);
        vb.setupData(VertexBuffer.Usage.Static, 4, VertexBuffer.Format.UnsignedByte, col);
        vb.setNormalized(true);
        setBuffer(vb);
        
        indices.flip();
        vb = new VertexBuffer(VertexBuffer.Type.Index);
        vb.setupData(VertexBuffer.Usage.Static, 3, VertexBuffer.Format.UnsignedShort,
                indices);
        setBuffer(vb);
        
        updateBound();
    }
}

Basically this determines the radius of the circle required to display the requested angle of the circle at the requested height. I use the term meter to refer to, with Space Rocks as an example, the white area of the whole mesh that displays the maximum/minimum extents of the value and the term value to refer to the part of the mesh that represents the actual displayed value, the purple area of the overall mesh.

Pretty much every part of curved meter mesh is variable so you can change the width of the meter, the width of the value, the angle, the height, etc…

I made it that way so I could change it up until I got something I liked. Also because the entire UI is density independent so everything stays the same physical size across all devices, screen sizes and resolutions.

P.S. If you want a meter that curves to the left you pass in a negative scale.

5 Likes