Hour 20. Entity Grouping


What You’ll Learn in This Hour

• Adding an entity grouping system to the engine

• Making the necessary engine enhancements

• Adding behavior to the game entities

• Demonstrating entity grouping


This hour covers a very big subject that is high in concept but fairly low in complexity, leading up to collision detection in the next hour. Entity grouping is the use of a list for game entities that is automatically updated by the engine. The way this works is that animations or behaviors are used to specify the activity of an entity (that is, sprite), which means “fire and forget” sprites. Give a sprite entity a simple behavior and “cut it loose” to wreak havoc—entirely on its own, so to speak. We’ll follow this up in the next hour by covering collision detection, which works well with entity grouping—because, as it turns out, it’s fairly easy to check for collisions after you have a grouping system in place.

Entity Grouping

The concept behind entity grouping is extremely simple. We don’t need to borrow any algorithms or develop any techniques; all that is required is a LinkedList and some creativity. You were introduced to entity grouping in the previous hour when the asteroid sprites were managed using a list. The basic code will be used again here, but it will be moved inside Engine.java with some required properties and helper methods. The approach this hour is to build the example as we go along, while learning about grouping. So, let’s begin with some work on the core engine again (which hasn’t been touched for a while now).

Engine Enhancements

Open up the core engine class in Engine.java. You may follow along while perusing the finished project under H20 in the sources, or make the changes yourself to the base H19 engine library project if you prefer to build things as a good learning experience. Because the engine source code is rather lengthy, we’ll just note the changes rather than repeating the entire code listing. For reference, go back to Hour 15, “Building an Android Game Engine.”

Add this new private property to the list of privates at the top of the class:

private LinkedList<Sprite> p_group;

To use this class, an import is needed. While we’re at it, the iterator class will be imported as well.

import java.util.LinkedList;
import java.util.ListIterator;

Next, initialize the LinkedList object in the class constructor under public Engine():

p_group = new LinkedList<Sprite>();

Now for the new group update and draw code. The run() method in Engine.java is the thread update method, as you’ll recall from back in Hour 8, “Bringing Your Game to Life with Looping.” There are only a few minor additions to the method that will bring to life our entity group. Note the changes in bold.

@Override
    public void run() {
        Log.d("Engine","Engine.run start");

        Timer frameTimer = new Timer();
        int frameCount=0;
        int frameRate=0;
        long startTime=0;
        long timeDiff=0;

        while (p_running) {
            // Process frame only if not paused
            if (p_paused) continue;

            // Calculate frame rate
            frameCount++;
            startTime = frameTimer.getElapsed();
            if (frameTimer.stopwatch(1000)) {
                frameRate = frameCount;
                frameCount = 0;

                //reset touch input count
                p_numPoints = 0;
            }

            // Call abstract update method in sub-class
            update();

            // Rendering section, lock the canvas
            // Only proceed if the SurfaceView is valid
            if (beginDrawing()) {

                p_canvas.drawColor(Color.BLACK);

                /**
                 * NEW CODE
                 * Draw the group entities with transforms
                 */
                ListIterator<Sprite> iter = p_group.listIterator();
                while (iter.hasNext()) {
                    Sprite spr = (Sprite)iter.next();
                    spr.animate();
                    spr.draw();
                }

                // Call abstract draw method in sub-class
                draw();

                int x = p_canvas.getWidth()-150;
                p_canvas.drawText("ENGINE", x, 20, p_paintFont);
                p_canvas.drawText(toString(frameRate) + " FPS", x, 40,
                    p_paintFont);
                p_canvas.drawText("Pauses: " + toString(p_pauseCount),
                    x, 60, p_paintFont);

                // Complete the rendering process by unlocking the canvas
                endDrawing();
            }

            // Calculate frame update time and sleep if necessary
            timeDiff = frameTimer.getElapsed() - startTime;
            long updatePeriod = p_sleepTime - timeDiff;
            if (updatePeriod > 0) {
                try {
                    Thread.sleep( updatePeriod );
                }
                catch(InterruptedException e) {}
            }
        }
        Log.d("Engine","Engine.run end");
        System.exit(RESULT_OK);
    }

Further down in the code of Engine.java, add the following new methods to the Engine class:

/**
     * Entity grouping methods
     */

    public void addToGroup(Sprite sprite) {
        p_group.add(sprite);
    }

    public void removeFromGroup(Sprite sprite) {
        p_group.remove(sprite);
    }

    public void removeFromGroup(int index) {
        p_group.remove(index);
    }

That’s all we need at this point to do rudimentary entity grouping inside the engine. Now, let’s take a look at some new animation classes we’ll need for the example this hour, and then we’ll take a look at the working demo.

Throb Animation Update

The ThrobAnimation class introduced in the previous hour needs some tweaking. It is still useful as a one-time animation sequence, but I want to give it a new feature that will cause the throb scale effect to loop if desired. This can be done with an additional constructor parameter to flag a repeat. The changes are noted in bold. While making the minor modifications, I’ve taken the opportunity to add another constructor and improve the logic code a bit as well. This way, the class can be used as originally intended, or the repeat flag can be used. In C++, this would be a simple matter of using an optional parameter, but in Java we have to use an overload.

package game.engine;
import android.renderscript.Float2;

public class ThrobAnimation extends Animation {
    private float p_startScale, p_endScale, p_speed;
    private boolean p_started, p_repeat;

    public ThrobAnimation(float startScale, float endScale, float speed) {
        this(startScale, endScale, speed, false);
    }

    public ThrobAnimation(float startScale, float endScale, float speed,
            boolean repeat) {
        p_started = false;
        animating = true;
        this.p_startScale = startScale;
        this.p_endScale = endScale;
        this.p_speed = speed;
        this.p_repeat = repeat;
    }

    @Override
    public Float2 adjustScale(Float2 original) {
        Float2 modified = original;
        if (!p_started) {
            modified.x = p_startScale;
            modified.y = p_startScale;
            p_started = true;
        }
        modified.x += p_speed;
        modified.y += p_speed;
        if (modified.x > p_endScale) {
            p_speed *= -1;
        }
        else if (modified.x < p_startScale) {
            if (!p_repeat)
                animating = false;
            else
                p_speed *= -1;
        }
        return modified;
    }
}

Warp Behavior Update

In the previous hour, I introduced an animation subclass called WarpRect. We’re going to rename and enhance this class to make it more useful. The new name will be WarpBehavior. Going along with this theme, any animation that is more about logic than appearance will use the term Behavior instead of Animation in the class name. This is important because you will most likely create a lot of such classes over time and there is clearly a difference between the two types. In addition to the name change, this class also features a new constructor overload (for convenience, not new functionality).

An interesting use of this class involves warping objects around the perimeter of the entire screen. Because the origin or pivot point of a sprite is at the upper-left corner, there is a difference in logic between warping from the left or top and the right or bottom. To balance out the discrepancy, it is helpful to specify a negative value for the left and top edges. This will have the effect of showing an object sliding off the edge of the screen rather than popping off.

package game.engine;
import android.graphics.Point;
import android.graphics.Rect;
import android.renderscript.Float2;

public class WarpBehavior extends Animation {
    private Rect p_bounds;
    private Float2 p_velocity;
    private Point p_size;

    public WarpBehavior(Rect bounds, int w, int h, Float2 velocity) {
        this(bounds, new Point(w,h), velocity);
    }

    public WarpBehavior(Rect bounds, Point size, Float2 velocity) {
        animating = true;
        p_bounds = bounds;
        p_velocity = velocity;
        p_size = size;
    }

    @Override
    public Point adjustPosition(Point original) {
        Point modified = original;
        modified.x += p_velocity.x;
        modified.y += p_velocity.y;

        if (modified.x < p_bounds.left)
            modified.x = p_bounds.right-p_size.x;
        else if (modified.x > p_bounds.right-p_size.x)
            modified.x = p_bounds.left;

        if (modified.y < p_bounds.top)
            modified.y = p_bounds.bottom-p_size.y;
        else if (modified.y > p_bounds.bottom-p_size.y)
            modified.y = p_bounds.top;

        return modified;
    }
}

Fence Behavior

We will introduce a new behavior class this hour! Remember, anytime you are tempted to write reusable gameplay logic, consider putting that code into a subclass of Animation and add it to the engine rather than writing custom code in the game loop! For this behavior, I want to limit the player’s ship to the boundary of the screen. It is simple logic to prevent a sprite from going off the screen; it’s sort of the opposite of the warp behavior. As such, we might call this fencing, or corralling.

Here is the FenceBehavior class. It is extremely simple in nature—just stop an object if it reaches the fence border, and move it back behind the fence if necessary.

package game.engine;
import android.graphics.Point;
import android.graphics.Rect;

public class FenceBehavior extends Animation {
    private Rect p_fence;

    public FenceBehavior(Rect fence) {
        p_fence = fence;
        animating = true; //just means it is active
    }

    @Override
    public Point adjustPosition(Point original) {
        Point modified = original;

        if (modified.x < p_fence.left)
            modified.x = p_fence.left;
        else if (modified.x > p_fence.right)
            modified.x = p_fence.right;
        if (modified.y < p_fence.top)
            modified.y = p_fence.top;
        else if (modified.y > p_fence.bottom)
            modified.y = p_fence.bottom;

        return modified;
    }
}

Entity Grouping Demo

Now we want the main project to test the new entity grouping functionality of the engine. Obviously, there is much work yet to be done to make the grouping system versatile for a large game project, but it’s a good start. For quick reference, Figure 20.1 shows the output of the project. Note that the figure shows what appear to be stationary sprites; in fact, the asteroids are moving quite fast and are being warped by the WarpBehavior class, and the ship is moving because of user input. Note also that the background should now be black instead of blue. That is due to a quick change while we were working in the Engine class. In the future, it will be helpful to add a support method to the engine that will make it possible to change the background color or possibly use a background image.

Image

Figure 20.1. The Entity Grouping Demo program.


By the Way

When you make changes to the code in Engine.java or any other class file in the engine library project, you have to Save All and then use Project, Clean to rebuild the engine for the changes to show up in the code editor.


Here is the source code for the example. It is astonishing how short this code listing is, given how much activity is happening on the screen. We have asteroids zooming across the screen while animating. The player’s ship is moving in response to user touch input while also cycling the scale in a subtle way to help with the sensation of movement. This isn’t a complex example, but it’s a lot of behavior in a small package thanks to the new animation and behavior features in the engine. We are truly working with some advanced gameplay code now at a fairly high level. In fact, this type of code is only one level away from script code.

/**
 * H20 Entity Grouping Demo
* Requires game.engine.Engine to build.
 */
package android.program;
import java.util.Random;
import android.graphics.*;
import android.renderscript.*;
import game.engine.*;

public class Game extends game.engine.Engine {
    Canvas canvas;
    Paint paint;
    game.engine.Timer timer; //avoid conflict with java.util.Timer
    Random rand;
    Texture asteroid_image;
    Sprite ship;
    Texture ship_image;

    public Game() {
        canvas = null;
        paint = new Paint();
        timer = new Timer();
        rand = new Random();
        asteroid_image = null;
        ship = null;
        ship_image = null;
    }

    public void init() {
        setScreenOrientation(Engine.ScreenModes.LANDSCAPE);
    }

    public void load() {

        asteroid_image = new Texture(this);
        if (!asteroid_image.loadFromAsset("asteroid_sheet.png")) {
            fatalError("Error loading asteroid_sheet");
        }

        int w = getScreenWidth();
        int h = getScreenHeight();
        Rect boundary = new Rect(-60,-60,w,h);

        for (int n=0; n<20; n++) {
            Sprite asteroid = new Sprite(this, 60, 60, 8);
            asteroid.setTexture(asteroid_image);
            asteroid.position = new Point(w+rand.nextInt(w), rand.nextInt(h-60));
            asteroid.addAnimation(new FrameAnimation(0, 63, 1));
            Float2 vel = new Float2(rand.nextFloat()*-50.0f, 0);
            asteroid.addAnimation(new WarpBehavior(boundary, 60, 60, vel));
            asteroid.setFrame(rand.nextInt(63));
            asteroid.setScale(rand.nextFloat()*2.0f);
            addToGroup(asteroid);
        }

        ship = new Sprite(this);
        ship_image = new Texture(this);
        if (!ship_image.loadFromAsset("ship3.png")) {
            fatalError("Error loading ship3");
        }
        ship.setTexture(ship_image);
        ship.position = new Point(100, h/2);
        ship.addAnimation(new ThrobAnimation(0.8f, 1.2f, 0.001f, true));
        ship.addAnimation(new FenceBehavior(new Rect(0,0,200,h-64)));
    }

    public void draw() {
        canvas = getCanvas();
        drawText("Entity Grouping Demo", 10, 20);
        ship.draw();
    }

    public void update() {
        Point touch=null;
        int inputs = getTouchInputs();

        if (timer.stopwatch(20)) {
            if (inputs > 0) {
                touch = getTouchPoint(0);
                if (touch.x < 200) {
                    if (touch.y < getScreenHeight()/2-20) {
                        ship.position.y -= 5.0f;
                    }
                    else if (touch.y > getScreenHeight()/2+20) {
                        ship.position.y += 5.0f;
                    }
                }
            }

            ship.animate();
        }
    }
}

Figure 20.2 shows the player’s spaceship featured in the demo. It has been prerotated by 90 degrees to face the right. Because that is the orientation the sprite will always be in for this sort of game, it makes sense to make changes to the art in advance to cut down on code manipulation that would otherwise be required. Consider doing this anytime you are working on a game. Try to avoid messy code to get art where you want it, and pre-edit images in advance to keep your code cleaner. This also benefits by being easier to modify later.

Image

Figure 20.2. The player’s spaceship sprite.

Figure 20.3 shows the asteroid animation sheet/atlas for reference. We have used this sprite before. It’s not especially interesting, just useful for testing new game code when you want a scifi setting. In this hour’s example, the asteroid sprites are being used as a background without interaction to make the ship appear to be flying through space.

Image

Figure 20.3. The asteroid sheet/atlas.

Summary

This hour introduced a new entity grouping system to the engine that makes it fairly easy to add automated behaviors to a game with a minimum of source code. The engine is truly beginning to feel like a functional engine rather than just a graphics wrapper for Android.

Q&A

Q. The animation system in this rudimentary Android game engine seems to support a lot more than just frame animation, it can be used for game logic, too, such as moving things around. Should this functionality be moved into a more appropriate class, such as a class called Behavior? Discuss the pros and cons.

A. Answers will vary.

Q. The new entity system that automatically moves sprites does not seem to let the programmer do anything with the objects manually. This seems to be a problem, or is that intentional? Discuss the issue.

A. Answers should reflect a need to “get” objects out of the entity system as needed, and this functionality is not yet included.

Workshop

Quiz

1. Which class handles the list of automated entities in the engine?

2. Which class makes it easier to loop through a list and is preferred when using a list?

3. When iterating through a list, which method returns the next object in the list?

Answers

1. java.util.LinkedList

2. java.util.ListIterator

3. next()

Activities

The WarpBehavior class makes some assumptions that could be customized with additional parameters or constructor overloads. See what interesting things you can do with the class to give it even more useful functionality.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.16.51.157