toyWars

a strategy game wannabe

picking

leave a comment »

pointerPicking is a technique used to interact with the elements in a game. Up to now, everything we wanted to render was perceived and designed in 3D and then rendered in 2D on the screen. When we want to interact with the scene, the opposite procedure must take place. The 2D coordinates of the mouse pointer on the screen must be translated to the 3D coordinates the user intends to select. As you can imagine, this can be quite a hard task to do.

Fortunately, OpenGL provides a good mechanism for picking. I won’t go through all the details, a very helpful article can be found here. The brief idea is:

  1. Render the object
  2. Push a “name” in the name stack
  3. Render the object again, in selection mode
  4. Pop the name
  5. The object can now be selected, when the mouse is over it

At step 3, a special more simple rendering of the objects under selection should be used. This rendering is never actually seen on screen, so it should be as light as it can get. This means that states like textures, lighting, blending etc can be safely disabled.  Game programmers even use a less complicated geometry structure for each model in selection mode.

In my selection rendering  of the map below, 2 variables are pushed in the name stack. Every tile, in order to be identified, must be rendered (for selection) giving its x and y coordinates. So, for each row and each column of the map, I provide the corresponding values in the name stack.

public void renderSelection(){
    int coordOffset = 0;

    GL11.glPushMatrix();
    GL11.glTranslatef(-getWidth()/2, -getHeight()/2, 0.0f);
    GL11.glTranslatef(0.0f, 0.0f, normalTile.getHeight());

    // Render first by column (for each row)
    for (int i=0; i<xDim; i++){
        // Push the x coordinate of the unit for picking
        GL11.glPushName(i);
            if (i%2!=0)
                coordOffset++;

         GL11.glPushMatrix();
            // Render a column
            for (int j=-coordOffset; j<yDim-coordOffset; j++){
                // Push the y coordinate of the unit for picking
                GL11.glPushName(j);
                    // Render only hexagons in selection mode, for efficiency
                    // GL11.glCallList(tileID);
                    tile.renderBase();
                GL11.glPopName();

                // Move to the next row
                GL11.glTranslatef(0.0f, vDistance, 0.0f);
            }
        GL11.glPopName();

        GL11.glPopMatrix();

        // Move to the next column
        GL11.glTranslatef(hDistance, 0.0f, 0.0f);

        // Add the offset
        if (i%2!=0)
            GL11.glTranslatef(0.0f, tileWidth/2.0f + OFFSET/2.0f, 0.0f);
        else
            GL11.glTranslatef(0.0f, -tileWidth/2.0f - OFFSET/2.0f, 0.0f);
    }

    GL11.glPopMatrix();
}

And the Picker class itself:

package gr.jmanji.toywars.mouse;

import java.nio.IntBuffer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import org.lwjgl.opengl.GL11;
import org.lwjgl.util.glu.GLU;
import gr.jmanji.toywars.state.GameWindow;
import gr.jmanji.toywars.TileCoords;

/**
 * Mouse picker. Gets the mouse input and returns the element it
 * points to.
 *
 * @author jmanji
 */
public class Picker {

    private IntBuffer selBuffer;
    private int hits;

    private int xSelected;
    private int ySelected;

    /**
     * Makes the game available for picking (when in 3D mode)
     *
     * @param xMouse The x coordinate of the mouse on the screen
     * @param yMouse The y coordinate of the mouse on the screen
     */
    public void startPicking3D(int xMouse, int yMouse) {
        startPickingGeneric(xMouse, yMouse);

        GLU.gluPerspective(65.0f, GameWindow.getWidth() / GameWindow.getHeight(),
                           0.1f, 150.0f);

        GL11.glMatrixMode(GL11.GL_MODELVIEW);
        GL11.glLoadIdentity();
    }

    /**
     * Makes the game available for picking (when in 2D mode)
     *
     * @param xMouse The x coordinate of the mouse on the screen
     * @param yMouse The y coordinate of the mouse on the screen
     */
    public void startPicking2D(int xMouse, int yMouse) {
        startPickingGeneric(xMouse, yMouse);

        GL11.glOrtho(0, GameWindow.getWidth(), 0, GameWindow.getHeight(), -1, 1);

        GL11.glMatrixMode(GL11.GL_MODELVIEW);
        GL11.glLoadIdentity();
    }

    /**
     * Makes the game available for picking (generic)
     *
     * @param xMouse The x coordinate of the mouse on the screen
     * @param yMouse The y coordinate of the mouse on the screen
     */
    private void startPickingGeneric(int xMouse, int yMouse){
        // The selection buffer
        selBuffer = ByteBuffer.allocateDirect(1024).order(ByteOrder.nativeOrder()).
                    asIntBuffer();
        IntBuffer vpBuffer = ByteBuffer.allocateDirect(64).
                             order(ByteOrder.nativeOrder()).asIntBuffer();
        // Size of the viewport. [0] Is <x>, [1] Is <y>, [2] Is <width>, [3] Is <height>
        int[] viewport = new int[4];

        // Get the viewport info
        GL11.glGetInteger(GL11.GL_VIEWPORT, vpBuffer);
        vpBuffer.get(viewport);

        // Set the buffer that OpenGL uses for selection to our buffer
        GL11.glSelectBuffer(selBuffer);

        // Change to selection mode
        GL11.glRenderMode(GL11.GL_SELECT);

        // Initialize the name stack (used for identifying which object was selected)
        GL11.glInitNames();

        GL11.glMatrixMode(GL11.GL_PROJECTION);
        GL11.glPushMatrix();
        GL11.glLoadIdentity();

        // Create 5x5 pixel picking region near cursor location
        GLU.gluPickMatrix((float) xMouse, (float) yMouse,
                          5.0f, 5.0f, IntBuffer.wrap(viewport));
    }

    /**
     * Stops the picking mode
     */
    public void stopPicking(){
        GL11.glMatrixMode(GL11.GL_PROJECTION);
	GL11.glPopMatrix();
        GL11.glMatrixMode(GL11.GL_MODELVIEW);

        hits = 0;
        hits = GL11.glRenderMode(GL11.GL_RENDER);
    }

    /**
     * Gets the tile the mouse points to
     *
     * @return TileCoords object with the coordinates of the selected tile
     */
    public TileCoords getSelectedTile(){

        int[] buffer = new int[256];
        xSelected = -1000;
        ySelected = -1000;

        selBuffer.get(buffer);

        if (hits > 0) {
              // If there were more than 0 hits
              xSelected = buffer[3]; // Make our selection the first object
              ySelected = buffer[4];
              int depth = buffer[1]; // Store how far away it is
              for (int i = 1; i < hits; i++) {
                    // Loop through all the detected hits
                    // If this object is closer to us than the one we have selected
                    if (buffer[i * 4 + 1] < (int) depth) {
                        xSelected = buffer[i * 4 + 3]; // Select the closest object
                        ySelected = buffer[i * 4 + 4];
                        depth = buffer[i * 4 + 1]; // Store how far away it is
                     }
              }
        }
        if (xSelected == -1000 || ySelected == -1000)
            return null;

        return new TileCoords(xSelected, ySelected);
    }

}

The process of the hit records is very well documented on the website I mentioned above. The interface to use this current implementation is:

  1. start picking ( startPicking3D(xMouse, yMouse) )
  2. render stuff for selection
  3. stop picking ( stopPicking() )
  4. get results ( getSelectedUnit() )

Written by jmanji

September 28, 2010 at 2:31 am

Posted in source code

Tagged with , , ,

0.3.1 version is out

with 4 comments

This release conforms to the latest editions of LWJGL (2.5) and Jouvieje’s great model loading API (ModelWorldEngine-1.1.3). Some other minor changes have been made, like improved UI, better Help screen and adding an exe file. You can download it from here.

Written by jmanji

August 31, 2010 at 3:37 pm

Posted in toyWars

Tagged with ,

toyWars is out!

leave a comment »

At last, the first beta version of the game is out! You can try it by downloading toyWars 0.2.1 from here. Also the widget on the sidebar will contain the new releases from now on.  toyWars is a Java based game, so to play it you must have Java (JRE) installed in your system of course. You can change some of the basic settings of the game through the config.xml file. To run toyWars, simply execute the run.bat file for Windows or run.sh for Linux. Please feel free to comment on anything.

Written by jmanji

February 25, 2010 at 9:00 am

Posted in source code

set the camera

leave a comment »

old_camera2What’s point of having a 3D game, if you cannot move and rotate the camera, right? It is usually nice for the user to be able to change the angle and position from which he watches the point of interest, which in our case is the map. So, the concept of a camera that changes its position and angle, according to the user’s mouse input, is what I tried to implement here.

The camera should have the ability to move on the surface of the map, and make every tile of it visible to the user. Furthermore, it should be able to rotate 360 degrees horizontally and about 90 degrees vertically around a focused spot. The camera will be tied to the map, and it will not be able to move far from its boundaries.

For debugging reasons mostly, the camera will work in two modes. The first one will be the normal mode, as described above, and the second will be the free mode. While in this mode, the camera will be able to move to every direction and be placed at every position of the room.

The source code that my game uses for its camera, is the following class:

package gr.jmanji.toywars;

import org.lwjgl.input.Keyboard;
import org.lwjgl.input.Mouse;
import org.lwjgl.opengl.GL11;
import gr.jmanji.toywars.objects.Room;
import gr.jmanji.toywars.objects.Map;

/**
 * The camera of the game. Using this camera the user can change his view
 * of the map.
 *
 * @author jmanji
 */
public class Camera {

    private static final float MOUSE_SENSITIVITY = 0.5f;
    private static final float KEY_SENSITIVITY = 0.17f;
    private static final float ZOOM_SENSITIVITY = 0.22f;
    private static final float WHEEL_SENSITIVITY = 3.0f;
    private static final float MIN_ANGLE = 80.0f; //180
    private static final float MAX_ZOOM_IN = 2.5f;//0;
    private static final float MAX_ZOOM_OUT = 28.0f;// 26.0f;
    private static final float ZOOM_OFFSET = 0.2f;
    private static final float ZOOM_OFFSET_SPEED = 0.065f;

    // Angle coordinates of the camera
    private float xAngle = -33.5f;
    private float yAngle = 53.0f;

    // Position coordinates of the camera
    private float xPos = 4.6f;
    private float yPos = -14.3f;
    private float zPos = 0.0f;

    private float zoom = 18.0f;

    // Maximum x and y room coordinates
    private float maxXMap;
    private float maxYMap;

    // Maximum x and y map coordinates
    private float maxXRoom;
    private float maxYRoom;
    private float maxZRoom;

    private boolean zoomIn;
    private boolean zoomOut;
    private boolean zoomInSmooth;
    private boolean zoomOutSmooth;
    private float finalZoom;

    private boolean rightClick = false;
    private float xCurrent = 0.0f;
    private float xPrevious = 0.0f;
    private float xDiff = 0.0f;
    private float yCurrent = 0.0f;
    private float yPrevious = 0.0f;
    private float yDiff = 0.0f;

    private boolean freeMode = false;

    /**
	 * Create a camera that will move on the map.
     *
     * @param room The room of the game
     * @param map The map of the game
	 */
    public Camera(Room room, Map map){
        maxXMap = map.getWidth();
        maxYMap = map.getHeight();

        maxXRoom = room.getLength();
        maxYRoom = room.getWidth();
        maxZRoom = room.getHeight();
    }

    /**
	 * Get user input, both from keyboard and mouse.
	 */
    public void getInput(){

        // Catch user's mouse input
        getMouseInput();

        // Catch user's key input
        getKeyInput();

        // Provide a smooth effect
        smoothZoom();

    }

    /**
	 * Get user input from mouse. This feedback from the user determines
     * the viewing angle. Some constrains are put here, the camera must not
     * be able to turn to any angle.
	 */
    private void getMouseInput(){

        // If right click buttton is pressed (works when mouse is grabbed )
        if(Mouse.isButtonDown(1) && rightClick) {

            xCurrent = Mouse.getX();
            yCurrent = Mouse.getY();

            xDiff = xCurrent - xPrevious;
            yDiff = yCurrent - yPrevious;

            xPrevious = xCurrent;
            yPrevious = yCurrent;

            xAngle = xAngle + xDiff*MOUSE_SENSITIVITY;

            if (xAngle > 360.0f)
                xAngle = xAngle - 360.0f;
            else if (xAngle < -360.0f)
                xAngle = xAngle + 360.0f;

            // Vertical movement of the mouse
            yAngle = yAngle + yDiff*MOUSE_SENSITIVITY;

            float minAngle;
            if (freeMode)
                minAngle = 180.0f;
            else
                minAngle = MIN_ANGLE;

            // Don't go below floor
            yAngle = Math.min(yAngle, minAngle);

            // Don't go up and opposite
            yAngle = Math.max(yAngle, 0.0f);

            rightClick = true;
        }
        else if(Mouse.isButtonDown(1) && !rightClick) {
            xPrevious = Mouse.getX();
            yPrevious = Mouse.getY();
            rightClick = true;
        }
        else
            rightClick = false;

        // Check if wheel has been scrolled
        int wheelMovement = Mouse.getDWheel();
        // If scrolled up
        if (wheelMovement > 0){
            zoomIn = true;
            zoomOutSmooth = false;
            zoom -= ZOOM_SENSITIVITY * WHEEL_SENSITIVITY;
        }
        else if (zoomIn){
            finalZoom = zoom - ZOOM_OFFSET;
            zoomInSmooth = true;
            zoomIn = false;
        }

        // If scrolled down
        if (wheelMovement < 0){
            zoomOut = true;
            zoomInSmooth = false;
            zoom += ZOOM_SENSITIVITY * WHEEL_SENSITIVITY;
        }
        else if (zoomOut){
            finalZoom = zoom + ZOOM_OFFSET;
            zoomOutSmooth = true;
            zoomOut = false;
        }

    }

    /**
	 * Get user input from keyboard. This feedback from the user determines
     * the position of the camera. Some constrains are put here, the camera
     * must not be able to move to any position.
	 */
    private void getKeyInput(){

        // If PgUp is pressed
        if (Keyboard.isKeyDown(Keyboard.KEY_PRIOR)) {
            zoomIn = true;
            zoomOutSmooth = false;
            zoom -= ZOOM_SENSITIVITY;
        }
        else if (zoomIn){
            finalZoom = zoom - ZOOM_OFFSET;
            zoomInSmooth = true;
            zoomIn = false;
        }

        // If PgDn is pressed
        if (Keyboard.isKeyDown(Keyboard.KEY_NEXT)) {
            zoomOut = true;
            zoomInSmooth = false;
            zoom += ZOOM_SENSITIVITY;
        }
        else if (zoomOut){
            finalZoom = zoom + ZOOM_OFFSET;
            zoomOutSmooth = true;
            zoomOut = false;
        }

        // If direction buttons are pressed
        if (Keyboard.isKeyDown(Keyboard.KEY_UP)) {
            xPos += (float)(Math.sin(Math.toRadians(xAngle))*KEY_SENSITIVITY);
            yPos += (float)(Math.cos(Math.toRadians(xAngle))*KEY_SENSITIVITY);
            zPos += (float)(Math.cos(Math.toRadians(yAngle))*KEY_SENSITIVITY);
        }
        if (Keyboard.isKeyDown(Keyboard.KEY_DOWN)) {
            xPos -= (float)(Math.sin(Math.toRadians(xAngle))*KEY_SENSITIVITY);
            yPos -= (float)(Math.cos(Math.toRadians(xAngle))*KEY_SENSITIVITY);
            zPos -= (float)(Math.cos(Math.toRadians(yAngle))*KEY_SENSITIVITY);
        }
        if (Keyboard.isKeyDown(Keyboard.KEY_RIGHT)) {
            xPos += (float)(Math.sin(Math.toRadians(90.0f-xAngle))*KEY_SENSITIVITY);
            yPos -= (float)(Math.cos(Math.toRadians(90.0f-xAngle))*KEY_SENSITIVITY);
        }
        if (Keyboard.isKeyDown(Keyboard.KEY_LEFT)) {
            xPos -= (float)(Math.sin(Math.toRadians(90.0f-xAngle))*KEY_SENSITIVITY);
            yPos += (float)(Math.cos(Math.toRadians(90.0f-xAngle))*KEY_SENSITIVITY);
        }

    }

    /**
	 * Make a smoothing effect when zooming. The final zoom level will be
     * extended a little bit more
	 */
    private void smoothZoom(){
        // If a zoom smoothing effect is required
        if (zoomInSmooth || zoomOutSmooth){
            if (finalZoom > MAX_ZOOM_IN || finalZoom < MAX_ZOOM_OUT){
                if (zoomInSmooth){
                    // If the final zoom level hasn't been reached yet
                    if (finalZoom + 0.0001 < zoom)
                        zoom -= ( (zoom - finalZoom) * ZOOM_OFFSET_SPEED);
                    else
                        zoomInSmooth = false;
                }
                else if (zoomOutSmooth){
                    // If the final zoom level hasn't been reached yet
                    if (finalZoom - 0.0001 > zoom)
                        zoom += ( (finalZoom - zoom) * ZOOM_OFFSET_SPEED);
                    else
                        zoomOutSmooth = false;
                }
            }
        }
    }

    /**
	 * Move the camera. Place the camera according to the user's zoom level,
     * angle and position.
	 */
    public void move(){

        if (!freeMode)
            moveNormal();
        else
            moveFree();
    }

    /**
	 * Move the camera in normal mode
	 */
    private void moveNormal(){

        // Limit the zoom level
        zoom = Math.max(zoom, MAX_ZOOM_IN);
        zoom = Math.min(zoom, MAX_ZOOM_OUT);

        // Check for map boundaries
        xPos = Math.max(xPos, -maxXMap/2);
        yPos = Math.max(yPos, -maxYMap/2);

        xPos = Math.min(xPos, maxXMap/2);
        yPos = Math.min(yPos, maxYMap/2);

        GL11.glTranslatef(0.0f, 0.0f, -zoom);
        GL11.glRotatef(-yAngle,1.0f,0.0f,0.0f);
        GL11.glRotatef(xAngle,0.0f,0.0f,1.0f);
        GL11.glTranslatef(-xPos, -yPos, 0.0f);
    }

    /**
	 * Move the camera in free mode
	 */
    private void moveFree(){

        // Check for room boundaries
        xPos = Math.max(xPos, -maxXRoom/2 + 1.0f);
        yPos = Math.max(yPos, -maxYRoom/2 + 1.0f);
        zPos = Math.max(zPos-zoom, -maxZRoom + 1.0f) + zoom;

        xPos = Math.min(xPos, maxXRoom/2 - 1.0f);
        yPos = Math.min(yPos, maxYRoom/2 - 1.0f);
        zPos = Math.min(zPos-zoom, -1.0f) + zoom;

        GL11.glRotatef(-yAngle, 1.0f, 0.0f, 0.0f);
        GL11.glRotatef(xAngle, 0.0f, 0.0f, 1.0f);
        GL11.glTranslatef(-xPos, -yPos, zPos - zoom);//- zoom
    }

    /**
	 * Change the camera mode
	 */
    public void changeMode(){
        freeMode = !freeMode;
    }

}

It shouldn’t be a big deal to get the user input. I am using LWJGL methods to catch user’s mouse and key input. The challenging part is what you do with them, after you have them. Mouse.getX() and Mouse.getY() are used to get the the number of pixels the mouse(pointer) has been moved (note: I found out that Mouse.getDX() and Mouse.getDY() behave differently when the mouse is grabbed, so I made the code more generic). Thus, this can be translated into angle for the rotation of the camera. The movement of the camera requires some simple trigonometric knowledge. I had to study there a little and try different things, to get it to work.

One thing I am happy to have implemented, is the smoothing effect of the camera, after a zoom effect is required. You can notice that in almost every 3D strategy game. When you zoom in or out on a map, the movement of the camera does not stop instantly, but the transition from zooming to stopping is smooth. smoothZoom function does that for me.

After you get the final angles, positions and zoom levels of the camera, you will have to place the camera according to these. move function is responsible for that. According to the camera mode, the behavior of the movement will be different. The order of translations and rotations here is of course very important, and only if you are a master of OpenGL, you will get it right the first time (that’s why it took me more than 4 days to figure it out).

With this camera, I can move around the room and the map, both in normal and free mode. Here is how they look, up to now:

camera1

camera2

Written by jmanji

August 18, 2009 at 1:06 pm

Posted in LWJGL, math, source code

Follow

Get every new post delivered to your Inbox.