0
\$\begingroup\$

I'm working on a grand strategy game similar to those made by Paradox Game Studios with their Clausewitz engine. I'm trying to create the world map from an image. On that you can see the areas, and every area has a unique color. I made a JSON file from this image and then the stored object contained every area with their points, their neighbours and their borders and in the game I just had to load this JSON file. I made a stage and added a custom object what would draw my map area by area, and pixel by pixel but with this method the FPS was really small (I think 8 was the max). Now I don't have any clue how I should make it work.

If there's any document about this I'd like to read it.

Image: enter image description here

Map (which will be deserialized with JSON; I'm using Jackson JSON because I prefer that much more)

    public class Map implements Serializable {

    private static final long serialVersionUID = -518329203219605551L;

    public Area[] areas = new Area[0];

    public int width;
    public int height;

    public Area byID(int id) {
        for (Area area : areas) {
            if (area.id == id) {
                return area;
            }
        }
        return null;
    }

    public Array<Area> areas() {
        return new Array<>(areas);
    }

    private void add(Area a) {
        Area[] array = new Area[areas.length + 1];
        for (int i = 0; i < areas.length; i++) {
            array[i] = areas[i];
        }
        array[areas.length] = a;
        areas = array;
    }

    private int indexOf(int rgb) {
        for (int i = 0; i < areas.length; i++) {
            if (areas[i].rgb == rgb) {
                return i;
            }
        }
        return -1;
    }

    public Area area(int x, int y) {
        for (Area area : areas) {
            if (area.hasPoint(x, y)) {
                return area;
            }
        }
        return null;
    }

    public static void fix(Vector2 vector, float width, float height) {
        if (0 > vector.x) {
            vector.x = width - vector.x;
        }
        if (0 <= width) {
            vector.x = vector.x - width;
        }
    }

    public static Array<Vector2> neighbours(Vector2 vector, float width, float height) {
        Array<Vector2> vectors = new Array<>();
        for (int dx = -1; dx <= 1; dx++) {
            for (int dy = -1; dy <= 1; dy++) {
                Vector2 v = new Vector2(vector.x + dx, vector.y + dy);
                fix(v, width, height);
                if (vector.y >= 0 && vector.y < height) {
                    vectors.add(v);
                }
            }
        }
        return vectors;
    }

    public static Map fromPNG(BufferedImage img) { // used only for build the JSON file 
        try {
            Map map = new Map();
            map.width = img.getWidth();
            map.height = img.getHeight();
            System.out.println(map.width + " * " + map.height);
            for (int y = 0; y < img.getHeight(); y++) {
                for (int x = 0; x < img.getWidth(); x++) {
                    int rgb = img.getRGB(x, y);
                    Area area = map.byID(rgb);
                    if (area == null) {
                        area = new Area();
                        area.id = rgb;
                        area.rgb = rgb;
                        map.add(area);
                        System.out.println("New area: " + area.id);
                    }
                    Vector2 p = new Vector2(x, y);
                    fix(p, img.getWidth(), img.getHeight());
                    area.add(p.x, p.y);
                    for (Vector2 point : neighbours(p, img.getWidth(), img.getHeight())) {
                        int c = new Color(img.getRGB((int) point.x, (int) point.y)).getRGB();
                        if (c != rgb) {
                            area.add(c, x, y);
                        }
                    }
                }
                System.out.println("Row: " + y + " is done.");
            }
            System.out.println("Converting ids to readable");
            for (int i = 0; i < map.areas.length; i++) {
                map.areas[i].id = i;
                Set<Integer> set = new HashSet<>(map.areas[i].neighbors.keySet());
                HashMap<Integer, List<Vector2>> copy = new HashMap<>();
                for (Entry<Integer, List<Vector2>> entry : map.areas[i].neighbors.entrySet()) {
                    copy.put(map.indexOf(entry.getKey()), entry.getValue());
                }
                map.areas[i].neighbors.putAll(copy);
                for (Integer id : set) {
                    map.areas[i].neighbors.remove(id);
                }
            }
            System.out.println("Flipping coords");
            for (Area area : map.areas) {
                for (Vector2 p : area.points) {
                    p.y = map.height - 1 - p.y;
                }
            }
            return map;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

}

Area object (which you can see in the Map class)

public class Area implements Serializable {

    public static class VectorSerializer extends JsonSerializer<Vector2> {
        @Override
        public void serialize(Vector2 v, JsonGenerator jgen, SerializerProvider sp) throws IOException {
            jgen.writeStartObject();
            jgen.writeNumberField("x", v.x);
            jgen.writeNumberField("y", v.y);
            jgen.writeEndObject();
        }
    }

    public static class VectorDeserializer extends JsonDeserializer<Vector2> {
        @Override
        public Vector2 deserialize(JsonParser jp, DeserializationContext dc)
                throws IOException, JsonProcessingException {
            JsonNode node = jp.getCodec().readTree(jp);
            return new Vector2((float) node.get("x").asDouble(), (float) node.get("y").asDouble());

        }
    }

    private static final long serialVersionUID = 2288613202778397975L;
    public Set<Vector2> points = new HashSet<>();
    public int id = -1;
    public int rgb;
    public HashMap<Integer, List<Vector2>> neighbors = new HashMap<>();
    public boolean naval = false;

    private Color color;
    private java.awt.Color java;

    public Color color() {
        if(color==null) {
            java = new java.awt.Color(rgb);
            String hex = String.format("#%02x%02x%02x", java.getRed(), java.getGreen(), java.getBlue());
            color = Color.valueOf(hex);
        }
        return color;
    }

    private Rectangle rect;

    public float x() {
        return rect.x;
    }

    public float y() {
        return rect.y;
    }

    public float w() {
        return rect.width;
    }

    public float h() {
        return rect.height;
    }

    public Rectangle rect() {
        return rect;
    }

    public void bounds(float x, float y, float w, float h) {
        rect = new Rectangle(x, y, w, h);
    }

    void add(float x, float y) {
        points.add(new Vector2(x, y));
    }

    void add(int rgb, int x, int y) {
        List<Vector2> list = neighbors.getOrDefault(rgb, new ArrayList<>());
        list.add(new Vector2(x, y));
        neighbors.put(rgb, list);
    }

    public boolean hasPoint(float x, float y) {
        for (Vector2 p : points) {
            if (p.x == x && p.y == y) {
                return true;
            }
        }
        return false;
    }

}

The Game Screen

public class GameScreen implements Screen {

    private CW game;
    private Save save;
    private Stage stage;
    private OrthographicCamera camera;

    public GameScreen(CW game, Save save) {
        this.save = save;
        this.game = game;
        this.camera = game.cam();
        camera.setToOrtho(false, save.map().width, save.map().height);
        game.viewport().setWorldSize(save.map().width, save.map().height);
        game.viewport().apply(true);
        this.stage = new Stage(game.viewport());
        InputMultiplexer input = new InputMultiplexer(stage, new InputAdapter() {
        });
        Gdx.input.setInputProcessor(input);
        stage.addActor(new WorldMap(this.save.map(), camera));
    }

    @Override
    public void show() {

    }

    @Override
    public void render(float delta) {
        handleCamera();
        camera.update();
        game.batch().setProjectionMatrix(camera.combined);
        Gdx.gl20.glClearColor(0f, 0f, 0f, 1f);
        Gdx.gl20.glClear(GL20.GL_COLOR_BUFFER_BIT);
        stage.act(Gdx.graphics.getDeltaTime());
        stage.draw();
        System.out.println(Gdx.graphics.getFramesPerSecond()); // TODO remove
    }

    private void handleCamera() {
        if (Gdx.input.isKeyPressed(Keys.LEFT)) {
            camera.translate(1, 0);
        } else if (Gdx.input.isKeyPressed(Keys.RIGHT)) {
            camera.translate(-1, 0);
        } else if (Gdx.input.isKeyPressed(Keys.UP)) {
            camera.translate(0, 1);
        } else if (Gdx.input.isKeyPressed(Keys.DOWN)) {
            camera.translate(0, -1);
        }
    }

    @Override
    public void resize(int width, int height) {

    }

    @Override
    public void pause() {

    }

    @Override
    public void resume() {

    }

    @Override
    public void hide() {

    }

    @Override
    public void dispose() {
        stage.dispose();
    }

}

The Actor:

public class WorldMap extends Actor {

    private Map map;
    private OrthographicCamera cam;

    public WorldMap(Map map, OrthographicCamera cam) {
        this.map = map;
        this.cam = cam;
        setBounds(0, 0, map.width, map.height);
    }

    private ShapeRenderer shape = new ShapeRenderer();

    @Override
    public void draw(Batch batch, float parentAlpha) {
        batch.end();
        shape.begin(ShapeType.Filled);
        for (Area area : map.areas) {
            shape.setColor(area.color());
            for (Vector2 p : area.points) {
                if (cam.frustum.pointInFrustum(p.x, p.y, 0)) {
                    shape.rect(p.x, p.y, 1, 1);
                }
            }
        }
        shape.end();
        batch.begin();
    }

}

Thanks!

\$\endgroup\$
  • \$\begingroup\$ Welcome to GDSE. Please edit to include an example image & indicate which Paradox game you are referring to. \$\endgroup\$ – Pikalek Aug 26 at 12:20
  • \$\begingroup\$ If you'd like help fixing your slow drawing method, it would help to show us a minimal sample of the code responsible, so we can spot opportunities to improve it. A brief walkthrough of your JSON format could help too, since that could be pretty much anything. \$\endgroup\$ – DMGregory Aug 26 at 12:22
  • \$\begingroup\$ @Pikalek I assume FiveShipHUN means the Paradox grand strategy games like Crusader Kings, Hearts of Iron or Europa Universalis. I edited the question accordingly. If I am wrong, feel free to revert. \$\endgroup\$ – Philipp Aug 26 at 13:26
  • \$\begingroup\$ Yes, thank you and sorry for being not so clear. \$\endgroup\$ – FiveShipHUN Aug 26 at 13:31
2
\$\begingroup\$

In WorldMap.draw you are reconstructing the map texture every frame by drawing a bunch of 1px by 1px rects.

This is a very expensive way of drawing bitmaps, because it not just runs on the CPU instead of the GPU but also uses a very inefficient method (calling a method to fill rectangles instead of changing the color values of a pixel buffer).

If possible, it would be good to see if you can do your map rendering algorithm on the GPU by passing the raw data to a shader. But there might be good reasons to implement your map rendering algorithm on the CPU, for example, because it's too complicated for a shader or because you lack the necessary shader programming know-how. In that case you should:

  1. render your map to a Pixmap (which offers you a more appropriate drawPixel(x, y, color) method)
  2. use that pixmap to create a Texture. (the difference between a pixmap and a texture is that pixmaps are stored in system RAM while textures are uploaded to GPU RAM)
  3. just draw that texture on each subsequent call to draw. Do not recreate the pixmap or texture unless the map data has changed. Reuse the same texture on every frame.
\$\endgroup\$
  • \$\begingroup\$ Can you please show me code examples? I know (or at least I think I know) what are shaders but I never used them. Anyway thanks the suggestion! \$\endgroup\$ – FiveShipHUN Aug 26 at 13:42
  • \$\begingroup\$ @FiveShipHUN No, I can not show you code examples right now, but I linked to some relevant sections of the documentation which should tell you what you need to know. \$\endgroup\$ – Philipp Aug 26 at 13:44
  • \$\begingroup\$ Thank you a lot! I'm going to check them. \$\endgroup\$ – FiveShipHUN Aug 26 at 13:47
  • \$\begingroup\$ I've just tried out and this seems to work. Thanks a lot again! \$\endgroup\$ – FiveShipHUN Aug 26 at 14:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.