[Solved] Minimap, cloaking devices and stealth

Hi

I’m trying to wrap my head around a new topic for my game development: a minimap. In my game, it should be a top-down minimap as close as possible to this one:

https://steamuserimages-a.akamaihd.net/ugc/440574362338374555/DF5560CC4AC6AF677232E07357F58C7BA4494C23/

I guess a new viewport, with an orthognal camera could get me far. Anyone with experience in doing a minimap?

Then comes my first ‘headache’ - players can have stealth, meaning they should not show up on the radar, but they should show up in the normal camera. And vice versa, players can have cloaking devices, where they are hidden from normal sight, but still show up on the minimap. Or they can have both or none of those combos.

If I do different cameras/viewports, can I cull them in one or the other by some means?

My level is entirely tilebased, if that makes any difference (perhaps when considering rendering ‘something’ to texture and displaying that texture - instead of doing another viewport…). Any and all input most welcome.

1 Like

If you save the level’s layout in a file, you could parse it and create a mini-map that way.

Then for the ship’s positions, you would need to scale their position relative to the minimap.

For the hidden/cloaked ships, i would simply set a game property letting the minimap render know what to consider visible and not.

That’s what I would do :slight_smile:

I don’t know if viewports are in any way more efficient than rendering the data that is already available to you in a map form. I’d be interested to know myself. I would have thought sampling the noise map for the terrain at a different scale and using the entity set (which is being iterated anyway) would be fine, with the benefit of full control over what and how it is seen.

Probably you want the scene in your minimap to be a different scene than the one the players see.

If you are using an ES, just use a different app state for those that doesn’t show invisible ships. Probably you want simplified models and stuff anyway.

I dont have a ‘terrain’ per say (not in the sense jME defines it). You can see my ‘level’ here:

It is ES based, and I’m confident I can find the data that I need to use, but how to actually use it is more the question.

With two ViewPorts I would run into issues because with how different I want the minimap (or radar) to look (lighting for instance).

I guess a texture is more the way to go (at least for now). As you say, simplified models.

I don’t understand what this means. A second ViewPort is like a second screen inside your screen. You can put whatever you want in it… different lighting, etc… So I don’t know what limitation you mean.

It’s got its own camera, scene, everything… if you want.

Ah, that I was not aware of. Good to know! I thought they ‘looked’ at the same scene.

When you mentioned this, do you mean

  1. XRadar appstate: Retrieves all with Position/BodyPosition (yes, Sim-et-es based) and shows all (because Client has Xradar ability to see even stealthed)
  2. Normal appstate: Retrieves both Position/BodyPosition and also Stealth components, to filter those ‘out from the view’.

I think he means rendering the same scene from a top down view (orthographic?) by adding an additional camera and rendering it like that, but I think we are all in agreement at this point, anyway.

Just in case by “he” you meant “me”.

…no, I mean two separate scenes. One rendered optimized for 3D view. Another rendered optimized for tiny 2D view. Two separate root nodes, two separate cameras, etc…

In an ES sense, for the 3D scene there is probably a MovelViewState or something that collects all of the positioned + modeled game objects and creates visuals for them. For the 2D map view, you will have a similar state that collects a different set of entities as necessary.

Errie, I am working on this at the moment also.

After reading everything I can find on jme I came to the conclusion that using a custom filter on the second viewport may work.

How to write a custom filter is the part I am trying to figure out now. I may be going down the wrong rabbit hole though.

Here’s how I got it looking at the moment. I chose the texture path, seemed more fitting for my purpose:

And here’s a source snippet if anyone wants to sneak a peek:

package infinity.client;

import com.jme3.app.Application;
import com.jme3.app.state.BaseAppState;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.texture.Texture;
import com.simsilica.lemur.Container;
import com.simsilica.lemur.GuiGlobals;
import com.simsilica.lemur.component.BorderLayout;
import com.simsilica.lemur.component.QuadBackgroundComponent;
import com.simsilica.lemur.input.InputMapper;
import infinity.Main;
import infinity.MainGameFunctions;
import infinity.client.view.ModelViewState;
import infinity.client.view.ModelViewState.Mob;
import infinity.client.view.ModelViewState.MobContainer;
import infinity.client.view.ModelViewState.ModelContainer;
import infinity.client.view.PaintableTexture;
import java.awt.Color;
import java.util.HashSet;
import org.dyn4j.geometry.Vector2;

/**
 * For information on how to do this, check out:
 * https://www.unknowncheats.me/forum/general-programming-and-reversing/135529-implement-simple-radar.html
 * or look at page 212 in the book:
 * 'Game-Programming-Algorithms-and-Techniques-A-Platform-Agnostic-Approach'
 *
 * @author Asser Fahrenholz
 */
public class RadarState extends BaseAppState {

    //Needed these to initialize GUI element
    private Container window;
    private QuadBackgroundComponent bg;
    int radarWidth = 300;
    int radarHeight = 300;

    Vector2 radarCenter = new Vector2(radarWidth / 2, radarWidth / 2);

    private int camHeight;
    private int camWidth;

    private HashSet<Vector2> testCoords = new HashSet<>();

    //Needed these to draw texture
    private ModelViewState mvs;
    ModelContainer models;
    MobContainer mobs;
    Spatial playerSpatial;

    //Texture to draw on
    private PaintableTexture texture;

    public static final float RADARRANGE = 100;
    private final float radarRadius = 150;
    private Vector2 playerCoords;

    //Colors:
    private final Color colorMap = Color.WHITE;
    private final Color colorMobs = Color.WHITE;
    private final Color colorMe = Color.WHITE;
    private final Color colorBackground = Color.DARK_GRAY;

    //ColorRGBAs:
    private final ColorRGBA colorMapRGBA = ColorRGBA.White;
    private final ColorRGBA colorMobsRGBA = ColorRGBA.White;
    private final ColorRGBA colorMeRGBA = ColorRGBA.White;
    private final ColorRGBA colorBackgroundRGBA = ColorRGBA.Green;//this.copyWithAlpha(ColorRGBA.LightGray, 0);

    public RadarState() {
        setEnabled(false);
    }

    public void close() {
        setEnabled(false);
    }

    public void toggleEnabled() {
        setEnabled(!isEnabled());
    }

    @Override
    protected void initialize(Application app) {

        mvs = getState(ModelViewState.class);
        models = mvs.getModelContainer();
        mobs = mvs.getMobContainer();
        //playerSpatial = mvs.getPlayerSpatial();

        camWidth = getApplication().getCamera().getWidth();
        camHeight = getApplication().getCamera().getHeight();

        InputMapper inputMapper = GuiGlobals.getInstance().getInputMapper();
        inputMapper.addDelegate(MainGameFunctions.F_RADAR, this, "toggleEnabled");
        window = new Container(new BorderLayout());

        //Label title = window.addChild(new Label("Radar", new ElementId("title")));
        //title.setInsets(new Insets3f(2, 2, 0, 2
        this.resetTexture();

        bg = new QuadBackgroundComponent(texture.getTexture());

        window.setBackground(bg);

        //Can be used to set alpha on entire map gui element
        window.setAlpha(0.5f, true);

        //loadTestBlips();
    }

    @Override
    protected void cleanup(Application app) {
        InputMapper inputMapper = GuiGlobals.getInstance().getInputMapper();
        inputMapper.removeDelegate(MainGameFunctions.F_RADAR, this, "toggleEnabled");
    }

    @Override
    protected void onEnable() {
        // Setup the panel for display
        Node gui = ((Main) getApplication()).getGuiNode();

        window.setPreferredSize(new Vector3f(radarWidth, radarHeight, 0));

        window.setLocalTranslation(camWidth - radarWidth, radarHeight,
                0);

        gui.attachChild(window);
        GuiGlobals.getInstance().requestFocus(window);
    }

    @Override
    protected void onDisable() {
        window.removeFromParent();
    }

    @Override
    public void update(float tpf) {
        if (playerSpatial == null) {
            playerSpatial = mvs.getPlayerSpatial();
            if (playerSpatial == null) {
                return;
            }
        }
        playerCoords = convertToVec2(playerSpatial.getWorldTranslation());

        this.resetTexture();

        for (Spatial s : models.getArray()) {
            Vector2 vec = this.convertToVec2(s.getWorldTranslation());

            Vector2 playerToBlip = vec.copy().subtract(playerCoords);
            if (playerToBlip.getMagnitude() <= RADARRANGE) {
                Vector2 scaledBlipToPlayerVec = this.getScaledBlipToPlayerVector(playerToBlip.copy());
                addBlipToRadar(scaledBlipToPlayerVec, colorMap);
            }
        }

        for (Mob m : mobs.getArray()) {
            //Visibility on a mob defines whether it's in range related to ZONES in the network distributor of positions
            //It does not relate to x-radar/stealth etc.
            if (m.isVisible()) {

                Vector2 vec = this.convertToVec2(m.getSpatial().getWorldTranslation());

                Vector2 playerToBlip = vec.copy().subtract(playerCoords);
                if (playerToBlip.getMagnitude() <= RADARRANGE) {
                    Vector2 scaledBlipToPlayerVec = this.getScaledBlipToPlayerVector(playerToBlip.copy());
                    addBlipToRadar(scaledBlipToPlayerVec, colorMobs);
                }
            }
        }

        for (Vector2 vec : testCoords) {
            Vector2 playerToBlip = vec.copy().subtract(playerCoords);
            if (playerToBlip.getMagnitude() <= RADARRANGE) {
                Vector2 scaledBlipToPlayerVec = this.getScaledBlipToPlayerVector(playerToBlip.copy());
                addBlipToRadar(scaledBlipToPlayerVec, colorMe);
            }
        }
        bg.setTexture(texture.getTexture());
    }

    private Vector2 getScaledBlipToPlayerVector(Vector2 playerToBlip) {

        playerToBlip.multiply(1f / RADARRANGE);
        playerToBlip.multiply(radarRadius);
        return playerToBlip;
    }

    private void addBlipToRadar(Vector2 scaledRelativeBlip, Color color) {

        texture.setPixel((int) (radarCenter.x + scaledRelativeBlip.x), (int) (radarCenter.y + scaledRelativeBlip.y), color);

    }

    private void loadTestBlips() {
        testCoords.add(new Vector2(10, 10));
        testCoords.add(new Vector2(20, 20));
        testCoords.add(new Vector2(10, 30));
        testCoords.add(new Vector2(10, -10));
        testCoords.add(new Vector2(-10, -20));
    }

    private Vector2 convertToVec2(Vector3f vec3f) {
        return new Vector2(vec3f.x, vec3f.y);
    }

    private void resetTexture() {
        texture = new PaintableTexture(radarWidth, radarHeight);
        texture.setBackground(colorBackground);
        texture.setMagFilter(Texture.MagFilter.Bilinear);
    }

    private ColorRGBA copyWithAlpha(ColorRGBA color, float alpha) {
        ColorRGBA newColor = color.clone();
        newColor.set(color.r, color.g, color.b, alpha);
        return newColor;
    }
}
1 Like

So you are manually painting a texture instead of letting JME draw it?

I guess that works. Seems rather limiting, though… and a lot of extra code… but maybe it fits your needs.

I don’t know the JME alternatives right now. I’m open to suggestions and generally curious, so any key words you got for me would be appreciated.

Ok, well, I’m not going to repeat what I already said about a separate viewport, separate scene.

So, good luck.

I opted for the texture path because I didn’t want the minimap to be camera looking down upon the scene - stylewise, the texture makes more sense to me.

If I can make another viewport+camera do that, then I’m in - but I simply can’t connect those dots just now.

Ah, it’s beginning to dawn on me what you’re communicating to me (I think).

  1. I could do a totally seperate Scene
  2. have the entities look differently then they do in the 3D scene (without lighting for example)
  3. have the camera be orthogonal

and probably more

1 Like

Exactly.

For example, your entities could even have an Icon component or MapSymbol component that defines how they look on the map versus in a regular scene. Objects without that component wouldn’t even appear on the map. And that component could represent a dot, a little triangle, a flag, whatever.

Totally different view.

2 Likes