0
\$\begingroup\$

I have developed a basic lunar lander-style game using Slick2D and dyn4j and am now redeveloping it using libgx for various reasons. I am having problems drawing the background (i.e. everything, but the main spaceship) using libgdx and am not sure if I am doing this correctly.

In the Slick2D version of the game I have 3 layers to the background, which are all 4096 by 1440 pixel Image objects (this to allow for suffient variety in layers 2 and 3 as per below). The layers are:

  • Layer 1 = stars (black background, with white pixels that vary in brightness so that they appear to twinkle)
  • Layer 2 = mountains (black background with deep blue filled landscape) (static image)
  • Layer 3 = foreground (black background with foreground landscape, a simple line drawing, with landing sites that vary in brightness over time)

I map where the ship (and therefore the screen) sits within the background images and then extract portions of the images to paint on the screen (this allows for wrapping across the top, the sides or the corners of the background images (note only the stars wrap across the top/bottom – the mountains and foreground only need to wrap side-to-side and across corners). I then extract a portion of the image using the getSubImage() method and draw this to the screen using the draw() method, e.g. as follows:

stars.getImage().getSubImage(x1, y1, width, height).draw(0, 0);

In separate method calls in each update() loop I update the star and foreground images so that the stars and landing sites vary in intensity. This is done by drawing directly to the background Images.

Using this approach the game performs at over 400FPS using Slick2D and dyn4j if I allow it to run unrestricted.

In the libgdx version I have created the backgrounds as Pixmap objects and then extracted portions by drawing a subsection to a new Pixmap object, which is the same size as the screen. I then convert this to a Texture and draw it to the screen using a SpriteBatch. Having read the documentation, it seems like this might be a very inefficient way to do things and, in practice, is too slow using just the star background (layer 1) without either of the other two layers.

There doesn’t appear to be a way to do this using Textures since I want to be able to draw to these to vary star and landing site intensity over time (unless I fundamentally re-write the game and use Sprites to draw the flashing stars and landing sites and just use a flat, static background Texture for everything else...)

So, my question is what is the correct way to do this using libgdx so that I get an effective level of performance? Can I replicate the logic that I have used in the Slick2D version of the game or do I need to do a fundamental re-think?

Note the game is designed for the desktop only and is not intended to be run an Android or iOS.

\$\endgroup\$
1

1 Answer 1

0
\$\begingroup\$

After a lot of research and continuing development using Slick2D, I have decided that the only way to do this in libGDX would be a fundamental re-write. After some tests, it also turns out that this is a Good Thing.

The solution I have tried is to hold the data for the graphics in arrays, use TreeSet indexes to be able to quickly locate which items should be on screen at any one time and then draw these once per loop. Per loop, this removes the getSubImage() call and repaint of this sub-image to the main graphics canvas. Having tested the approach with both the old graphics-based class and the new array/TreeSet-based class the difference in performance is significant. Using the graphics approach the game runs unchecked at about 205 FPS. Using the new array/TreeSet approach it runs at around 280 FPS.

I've included an edited version of my Stars class here to show how this works in practice. This is the simplest case since the stars are simply dots on screen. It gets more complicated for other shapes, but the logic is essentially the same except for having to check for objects being partially on-screen, i.e. check all vertices. I've also included a small class below that I use for the TreeSet indices used to search for on-screen objects. Both use dyn4j (physics engine) co-ordinates for the stars and also for the edges of the screen (picked up as an Observer of another class) to calculate what should be shown on-screen.

public final class Stars implements PlanetObserver {

    private Color dots[];                                   // Colours for different star intensities

    private final Settings settings;
    private final int bWIDTH;
    private final int bHEIGHT;
    private final int sWIDTH;
    private final int sHEIGHT;

    private int scrLeftX;                                   // Where screen is in world co-ordinates
    private int scrTopY;

    private int[][] star_array;                             // Array of stars
    private TreeSet<Index> xIndex;                          // Index of x values
    private TreeSet<Index> yIndex;                          // Index of x values
    private static final Integer MAX_STARS = 500;

    /* Static placeholders for values held in star_array include:
     x co-ordinate of star
     y co-ordinate of star
     intensity of star brightness
     speed of star update
     direction of update - up or down
     */
    private static final int STAR_X = 0;
    private static final int STAR_Y = 1;
    private static final int STAR_INTENSITY = 2;
    private static final int STAR_SPEED = 3;
    private static final int UP_DOWN = 4;
    private static final int DOWN = 0;
    private static final int UP = 1;

    public Stars(Settings setts) {
        settings = setts;                                   // Global settings class
        bWIDTH = settings.getbWIDTH();                      // Width of buffer for star field
        bHEIGHT = settings.getbHEIGHT();                    // Height of buffer for star field
        sWIDTH = settings.getsWIDTH();                      // Current screen width
        sHEIGHT = settings.getsHEIGHT();                    // Current screen height
        init();
    }

    public void init() {
        settings.getPlanetObj().registerObserver(this);     // Register for scrTopLeftX/Y updates

        // Create star images - saves recreating them each cycle
        dots = new Color[11];
        dots[0] = new Color(110, 145, 230, 0f);             // Match sky colour at 1000m altitude
        for (int i = 1; i < 11; i++) {
            dots[i] = new Color(110, 145, 230, (float) (i / 10f));
        }

        // Create the star field
        star_array = new int[MAX_STARS][5];                 // Array of star values: x, y, intensity and update speed
        xIndex = new TreeSet<>();                           // Set up new indices for x and y axes
        yIndex = new TreeSet<>();
        Random rand = new Random();                         // Random number generator
        rand.setSeed(System.currentTimeMillis());           // Seed random with current time

        for (int i = 0; i < MAX_STARS; i++) {
            star_array[i][STAR_X] = rand.nextInt(bWIDTH);   // x co-ordinate
            star_array[i][STAR_Y] = rand.nextInt(bHEIGHT);  // y co-ordinate
            star_array[i][STAR_INTENSITY] = rand.nextInt(11);// intensity 0 to 10, where 0 = black and 10 = white
            star_array[i][STAR_SPEED] = rand.nextInt(3);    // glow speed 0 to 2, where 0 = slow, 1 = medium, 2 = fast
            star_array[i][UP_DOWN] = rand.nextInt(2);       // Direction of glow travel - 0=down, 1=up
            // Add entries to indices for x and y values
            xIndex.add(new Index(star_array[i][STAR_X], i));
            yIndex.add(new Index(star_array[i][STAR_Y], i));
        }
    }

    public void update() {
        // Loop through star array and increment/decrement intensity as appropriate
        for (int[] star : star_array) {
            if (star[UP_DOWN] == UP) {
                star[STAR_INTENSITY]++;
                if (star[STAR_INTENSITY] > 10) {
                    star[STAR_INTENSITY] = 9;
                    star[UP_DOWN] = DOWN;
                }
            } else {
                star[STAR_INTENSITY]--;
                if (star[STAR_INTENSITY] < -5) {            // Minus numbers allow for period where str is 'off'
                    star[STAR_INTENSITY] = -4;
                    star[UP_DOWN] = UP;
                }
            }
        }
    }

    public void render(Graphics g) {
        // Calculate screen bottom-edge Y taking into account values > bHEIGHT
        int scrBtmY = scrTopY - sHEIGHT;
        if (scrBtmY >= bHEIGHT) {
            scrBtmY = scrBtmY % bHEIGHT;
            if (FastMath.abs(scrBtmY) == bHEIGHT) {         // Wrap-around bug at y == bHEIGHT
                scrBtmY = 0;
            }
        }

        // Get x and y co-ordinate values that are in range of on-screen World co-ordinates
        Index index;
        index = new Index(scrLeftX, 0);
        Index xLower = this.xIndex.ceiling(index);
        if (xLower == null) {
            xLower = this.xIndex.last();
        }

        if (scrLeftX + sWIDTH <= bWIDTH) {
            index = new Index(scrLeftX + sWIDTH, 0);
        } else {
            index = new Index((scrLeftX + sWIDTH) - bWIDTH, 0);
        }
        Index xUpper = this.xIndex.floor(index);
        if (xUpper == null) {
            xUpper = this.xIndex.first();
        }

        index = new Index(scrBtmY, 0);
        Index yLower = this.yIndex.ceiling(index);
        if (yLower == null) {
            yLower = this.yIndex.last();
        }

        if (scrBtmY + sHEIGHT <= bHEIGHT) {             // Check if range of y values wraps around top of buffer
            index = new Index(scrBtmY + sHEIGHT, 0);
        } else {
            index = new Index((scrBtmY + sHEIGHT) - bHEIGHT, 0);
        }
        Index yUpper = this.yIndex.floor(index);
        if (yUpper == null) {
            yUpper = this.yIndex.first();
        }

        // Extract y value pointers to star_array into  a temporary TreeSet
        TreeSet<Integer> yPointer = new TreeSet<>();
        SortedSet ySet = new TreeSet();
        if (yLower.xyValue < yUpper.xyValue) {
            ySet = this.yIndex.subSet(yLower, yUpper);
        } else {                                            // Wrapping occured - extract 2 ranges and join
            ySet.addAll(this.yIndex.tailSet(yLower, true));
            ySet.addAll(this.yIndex.headSet(yUpper, true));
        }
        for (Iterator it = ySet.iterator(); it.hasNext();) {
            Index i = (Index) it.next();
            yPointer.add(i.pointer);
        }

        // Create list of x value index elements that might be on-screen
        ArrayList<Integer> result = new ArrayList<>();
        SortedSet xSet = new TreeSet();
        if (xLower.xyValue < xUpper.xyValue) {
            xSet = this.xIndex.subSet(xLower, xUpper);
        } else {                                            // Wrapping occured - extract 2 ranges and join
            xSet.addAll(this.xIndex.tailSet(xLower, true));
            xSet.addAll(this.xIndex.headSet(xUpper, true));
        }

        /* Check if x value pointers exist in y pointer list - if
         so then point is on screen: store in results list
         */
        for (Iterator it = xSet.iterator(); it.hasNext();) {
            Index i = (Index) it.next();
            if (yPointer.contains(i.pointer)) {
                result.add(i.pointer);
            }
        }

        // Draw the stars on-screen that are in the results list
        for (Integer pointer : result) {
            if (star_array[pointer][STAR_INTENSITY] >= 0) {
                g.setColor(dots[star_array[pointer][STAR_INTENSITY]]);
                int x = star_array[pointer][STAR_X] - scrLeftX;
                int y = sHEIGHT - (star_array[pointer][STAR_Y] - scrBtmY);
                g.drawLine(x, y, x, y);
            }
        }
    }

    @Override
    public void updateScreen(float topLeftX, float topLeftY, int xoffset, int yoffset) {
        scrLeftX = (int) topLeftX;
        scrTopY = (int) topLeftY;
    }
}

.. and here is the class for the Index elements used in the TreeSets:

public final class Index implements Comparator<Index>, Comparable<Index> {

    public final int xyValue;
    public final int pointer;

    public Index(int val, int p) {
        xyValue = val;
        pointer = p;
    }

    @Override
    public int compare(Index o1, Index o2) {
        return o1.xyValue - o2.xyValue;
    }

    @Override
    public int compareTo(Index o) {
        return this.xyValue - o.xyValue;        
    }
}
\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .