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

the construction of the map

The construction of the map took me quite a lot of time, mostly because of its strange coordinate system. I wanted to create a hexagonal map, rectangle shaped, which is consisted of separate 3D hexagonal tiles, and has non-static dimensions. The initial picture of the map in my mind resembled the following image.

map2

The tiles never touch each other, they always have a small definite distance that separates them. This way, their 3D nature is more visible, and also I like it better. Furthermore, each column of tiles cannot be placed exactly next to its column beside it. There is an offset of half a tile’s height that must be considered for the columns next to each other. In this coordinate system, the x coordinate increases while moving to the right, while the y coordinate increases while move up.

Rendering the map was quite easy:

Repeat for all columns:

  1. Render the column
  2. move to the right
  3. add a vertical offset in case of odd column

So far so good. The problems began when I tried to name the tiles, and later on define the movements of the units on them. One function that is going to be used for sure when dealing with the units, is the distance calculation. By distance, I mean the number of tiles a unit must traverse to reach from a tile to another .The type of coordinate system shown above, proved to be really frustrating as far as it concerns the calculation of the distance between two tiles. The famous Manhattan distance does not apply of course in hexagonal maps. I concluded that this coordinate system is no good because if there was a function or algorithm able to calculate this distance, I couldn’t find it.

map3Searching on the net for other approaches, I found out that most of the bibliography suggests a different type of coordinate system. Amit Patel has done a great job explaining hexagon grids, it as well as providing some very useful links. I wish I had seen it earlier.

Unfortunately this kind of map cannot be used as it is, because I need a rectangle shaped map. But, here’s a thought: What if I could render the map as I initially imagined it, but somehow be able to change the values of the coordinates, so as to match this coordinate system?

Let’s name the axis with the slope ‘x’ and the vertical axis ‘y’. You can notice that the x coordinate remains the same, every column number(x) still increases as we move to the right. This does not happen with the y coordinate. When moving steadily horizontically to the right, the y coordinate does not remain the same. In fact, when moving to the right, y decreases.

This is why a special mapping is required. The implementation of this mapping is done with the use of coordOffset variable which increases by one in case of odd columns. The general idea is that the value of coordOffset will be abstracted from each row coordinate(y), as we move to the right.

This whole procedure may look a little pointless for the time being, but it will surely have a great impact afterwards, when playing around with the units and implementing the interaction with tiles using picking, a very interesting process that I plan on showing on a next post.

In the end, this coordinate mapping will lead to a map that will look like the image below. The same coordinate system as before is used, but on a rectangle shaped map. Which is great!

map4

Here is most of the source code used for creating the map (some stuff were cut off as they are irrelevant at this point).

package gr.jmanji.toywars.objects;

import java.util.ArrayList;
import org.lwjgl.opengl.GL11;
import gr.jmanji.toywars.unit.UnitManager;
import gr.jmanji.toywars.TileCoords;
import static gr.jmanji.toywars.constants.Appearance.*;
import static gr.jmanji.toywars.constants.Logic.*;

/**
* The map of the game. On this map, the units can move and attack. It consists
* of hexagonal tiles, so the units can move in six directions. The map is
* rectangle shaped and the size of it can vary, which is convenient for
* simple levels.
*
* @author jmanji
*/
public class Map{
    private final float TILE_SIZE = 3.0f; // the size(diameter) of the tile
    private final float OFFSET = 0.1f * TILE_SIZE; // the distance between the
                                                   // the tiles
    // tiles available
    private final Tile normalTile = new Tile(NORMAL_WOOD_TEXTURE, TILE_SIZE);
    private final Tile blueTile = new Tile(BLUE_WOOD_TEXTURE, TILE_SIZE);
    private final Tile redTile = new Tile(RED_WOOD_TEXTURE, TILE_SIZE);

    // used for placing the tiles
    private final float tileHeight = normalTile.getWidth();
    private final float vDistance = tileHeight + OFFSET;
    private final float hDistance = TILE_SIZE/2 + normalTile.getSide()/2.0f + OFFSET;

    // dimenions of the map
    private int xDim;
    private int yDim;

/**
 * Create a map of rectange shape with any dimensions.
 *
 * @param xDim The x dimension of the map
 * @param yDim The y dimension of the map
 */
public Map(int xDim, int yDim){
    this.yDim = yDim;
    this.xDim = xDim;
}
/**
 * Render the map.
 *
 * @param manager The units' manager
 */
public void render(){
    int coordOffset = 0; // variable abstracted from y coordinate

    GL11.glPushMatrix();
        // move up a bit to place the map on the floor
        GL11.glTranslatef(0.0f, 0.0f, normalTile.getHeight());

        // render first by column (for each row)
        for (int i=0; i<xDim; i++){

            // for odd columns, increase coordOffset
            if (i%2!=0)
                coordOffset++;

            // for each column, keep current matrix, render the column,
            // and move to the right
            GL11.glPushMatrix();
                // render the column. The size varies, depending on the
                // distance(in tiles) from the initial(0) column
                for (int j=-coordOffset; j<yDim-coordOffset; j++){
                    normalTile.render();
                    // move up to the next row
                    GL11.glTranslatef(0.0f, vDistance, 0.0f);
                }
            GL11.glPopMatrix();

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

            // provide an offset for placing the column, positive for odd
            // columns, negative for even
            if (i%2!=0)
                GL11.glTranslatef(0.0f, tileHeight/2.0f + OFFSET/2.0f, 0.0f);
            else
                GL11.glTranslatef(0.0f, -tileHeight/2.0f - OFFSET/2.0f, 0.0f);
        }
    GL11.glPopMatrix();
}

Written by jmanji

April 25, 2009 at 1:57 pm

Posted in source code

Tagged with , ,

my 3D tile

leave a comment »

In the previous post, I showed how I created a simple hexagon. In this post, I will try to demonstrate the creation of a tile of my map. The tile I want to make consists of a hexagon and six rectangles. Think of it like a hex nut but without the hole of course. Again, I have made a Rectangle class which draws a simple rectangle, with normal and texture coordinates. The texture coordinates are applied according to a factor variable. I will not put the whole Rectangle class here, just the rendering function.

public void render(){
    GL11.glBegin(GL11.GL_TRIANGLE_STRIP);
        GL11.glNormal3f(0.0f, 0.0f, 1.0f);
            // top left
            GL11.glTexCoord3f(0.0f, factor, 0.0f);
            GL11.glVertex3f(-width, height, 0.0f);
            // bottom left
            GL11.glTexCoord3f(0.0f, 0.0f, 0.0f);
            GL11.glVertex3f(-width, -height, 0.0f);
            // top right
            GL11.glTexCoord3f(factor, factor, 0.0f);
            GL11.glVertex3f(width, height, 0.0f);
            // bottom right
            GL11.glTexCoord3f(factor, 0.0f, 0.0f);
            GL11.glVertex3f(width, -height, 0.0f);
    GL11.glEnd();
}

As it is known, OpenGL can render triangles faster than any other polygon primitive. Whenever possible, you should construct everything using triangles, as this is the optimized primitive for the majority of graphics cards. This is also why this rectangle consists of two triangles.

The interesting thing here is the factor variable. In OpenGL, texture coordinates vary from 0 to 1 within the texture. If a coordinate goes beyond 1, then the texture will be rendered again, as many times needed, in order to fit in the polygon primitive. At least this is the default behaviour. So, factor here shows how many times we want the texture to be rendered on the triangle. This can come in handy if the texture is smaller or bigger than the one we want. But I guess it would be easier and faster for OpenGL to change the texture on a image manipulation program like gimp and then just map it normally.

The rendering of the tile:

...
tileID = GL11.glGenLists(1);
GL11.glNewList(tileID, GL11.GL_COMPILE);
    texture.bind();
    hexagon.render();
    renderSides();
GL11.glEndList();
...
private void renderSides(){
    float angle = 0.0f;

    GL11.glPushMatrix();
        // render the rectangles as watching them from top
        GL11.glTranslatef(0.0f, 0.0f, -SIDE_HEIGHT/2);
        GL11.glRotatef(90.0f, 1.0f, 0.0f, 0.0f);
        for (int numVertices = 0; numVertices < 6; numVertices++){
            angle += Math.toDegrees(STEP);
            // go from the center of the hexagon to the
            // center of each side rectangle
            GL11.glPushMatrix();
                GL11.glRotatef(angle, 0.0f, 1.0f, 0.0f);
                GL11.glTranslatef(0.0f, 0.0f, hexagon.getHeight()/2);
                rectangle.render();
            GL11.glPopMatrix();
        }
    GL11.glPopMatrix();
}

SIDE_HEIGHT is the height of each side rectangle and STEP is the same as in Hexagon class. Because many rotations and transformations are performed here, the whole rendering of the tile is put in a display list(in the constructor of the class). When we want to render it, we just do glCallList(tileID) and we are done. It is also important here that  after the rendering, we are positioned exactly where we started, so glPushMatrix() and glPopMatrix() are also needed between the side rectangles.

Behold, my wooden tile!

tiles4

Written by jmanji

April 6, 2009 at 4:55 pm

Posted in source code

Tagged with , , ,

making a hexagon

with one comment

Several games like the one I’m trying to make, have hexagonal tiles for their maps, while others use square tiles. I will go with the first approach, because I like it better and it seems to provide deeper gameplay. I considered that before constructing the map, and before even making a single tile, I should keep things simple and make a hexagon class. Then this could be used for the tile(’cause I’m thinking about making it a little more complicated than a 2D shape) and whatever other purposes.

So, making a hexagon proved to be for me a little more tricky than I initially thought. I chose triangle fans over triangles so as to improve performance(less number of vertices). The whole class is presented below:

package gr.jmanji.toywars.shapes;

import org.lwjgl.opengl.GL11;

/**
 * Draws a simple 2D hexagon, according to a size. Only geometry and texture
 * coordinates are defined here. The shape of the hexagon has the base
 * positioned horizontically, and looks like this:
 *
<pre>
    _
   / \
   \_/</pre>
*
 * @author jmanji
 */
public class Hexagon {
    private final float STEP = (2.0f*(float)Math.PI/6.0f); // STEP=1/6th of a circle
    private float radius; // the radius of the hexagon(half the diameter)
    private float height; // height of the hexagon is the vertical distance from
                          // the top side to the bottom side.
    private float side;   // the size of a hexagon's side

    /**
     * Create a new Hexagon
     *
     * @param size The diameter of the hexagon
     */
    public Hexagon(float size){
        radius = size/2;
        // 2*radius*(cos/sin of half the angle)
        height = size * ((float)Math.cos(STEP/2.0f));
        side = size * ((float)Math.sin(STEP/2.0f));
    }

    /**
     * Get the height of the hexagon
     *
     * @return the height of the hexagon. This is actually the
     * vertical distance from the top side to the bottom side.
     */
    public float getHeight(){
        return height;
    }

    /**
    * Get the size of a hexagon's side
    *
    * @return the size of the each hexagon's side
    */
    public float getSide(){
        return side;
    }

    /**
    * Renders the hexagon with normal and texture coordinates
    */
    public void render() {
        float angle = 0.0f; // angle(in radians) for each vertex
        float x, y;

        GL11.glBegin(GL11.GL_TRIANGLE_FAN);
            GL11.glNormal3f(0.0f, 0.0f, 1.0f);
            GL11.glTexCoord3f(0.5f, 0.5f, 0.0f);
            GL11.glVertex3f(0.0f, 0.0f, 0.0f);

            // divide the circle up into 6 sections(starting from top right)
            for (int numVertices = 0; numVertices < 6; numVertices++){
                x = (float)Math.cos(angle);
                y = (float)Math.sin(angle);
                angle += STEP;

                // map the hexagon coordinates to image coordinates(0...1)
                GL11.glTexCoord3f((x+1)/2.0f, (y+1)/2.0f, 0.0f);
                GL11.glVertex3f(radius*x, radius*y, 0.0f);
            }
            // last vertex closes the fan (defined explicitely for best precision)
            GL11.glTexCoord3f(1.0f, 0.5f, 0.0f);
            GL11.glVertex3f(radius, 0.0f, 0.0f);
        GL11.glEnd();
    }
}

The idea is simple: Go to the center of the hexagon and then draw triangles having one vertex at the center and the other 2(defined by the step) on the perimeter of the imaginable circle.
The formation of the hexagon step by step:

hex_formationOne interesting line in the source code is this:

GL11.glTexCoord3f((x+1)/2.0f, (y+1)/2.0f, 0.0f);

When setting the hexagon’s vertices, we must also provide the texture coordinates(if we want a texture). In OpenGL the coordinates on a texture can go from 0 to 1, but here the vertex coordinates range from -radius to +radius, so we can’t just use these. The cos and sin coordinates will always be from -1 to 1, so we can use these values and map them on the texture. This now works, no matter what the size of he hexagon might be.

Written by jmanji

March 31, 2009 at 9:21 am

Posted in source code

Tagged with , ,

Follow

Get every new post delivered to your Inbox.