Wednesday, March 6, 2013

The Game Map Pt 2 (Level 2)

This will be a pretty short update, but kind of fun.  The goal is to be able to click-drag to move around the world, tell which cell is being clicked on, scroll to zoom in/out, and draw only the map tiles within our camera's view.

I'm going to implement the first three pieces in PlayerInputSystem.java, and the last part in MapRenderSystem.java.

First let's look at PlayerInputSystem.java
package com.gamexyz.systems;

import com.artemis.Aspect;
import com.artemis.ComponentMapper;
import com.artemis.Entity;
import com.artemis.annotations.Mapper;
import com.artemis.systems.EntityProcessingSystem;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.InputProcessor;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.math.Vector3;
import com.gamexyz.components.Player;
import com.gamexyz.components.Position;
import com.gamexyz.utils.MapTools;

public class PlayerInputSystem extends EntityProcessingSystem implements InputProcessor {
 @Mapper ComponentMapper<Position> pm;
 
 private OrthographicCamera camera;
 private Vector3 mouseVector;
 
 @SuppressWarnings("unchecked")
 public PlayerInputSystem(OrthographicCamera camera) {
  super(Aspect.getAspectForAll(Player.class));
  this.camera=camera;
 }
 
 @Override
 protected void initialize() {
  Gdx.input.setInputProcessor(this);
 }

 @Override
 protected void process(Entity e) {
  mouseVector = new Vector3(Gdx.input.getX(),Gdx.input.getY(),0);
  camera.unproject(mouseVector);
  
 }

 @Override
 public boolean keyDown(int keycode) {
  return false;
 }

 @Override
 public boolean keyUp(int keycode) { 
  return false;
 }

 @Override
 public boolean keyTyped(char character) {
  return false;
 }

 @Override
 public boolean touchDown(int screenX, int screenY, int pointer, int button) {
  int x = (int)((mouseVector.x - 6f) / (float)MapTools.col_multiple);
  int y = (int)((mouseVector.y - (float)MapTools.row_multiple*(x%2)/2) / (float)MapTools.row_multiple);
  return false;
 }

 @Override
 public boolean touchUp(int screenX, int screenY, int pointer, int button) {
  return false;
 }

 @Override
 public boolean touchDragged(int screenX, int screenY, int pointer) {
  Vector3 delta = new Vector3(-camera.zoom*Gdx.input.getDeltaX(), camera.zoom*Gdx.input.getDeltaY(),0);
  camera.translate(delta);
  
  return false;
 }

 @Override
 public boolean mouseMoved(int screenX, int screenY) {
  return false;
 }

 @Override
 public boolean scrolled(int amount) {
  if ((camera.zoom > 0.2f || amount == 1) && (camera.zoom < 8 || amount == -1)) camera.zoom += amount*0.1;
  return false;
 } 
}
The touchDown() method computes which cell is being clicked, and stores the coordinates in x,y.  It looks a little hideous because you have to be careful whether you are in an even or odd column.  Remember, because it's a hex map, if you are in an odd column all the cells are drawn down a little lower.

touchDragged() handles clicking and dragging.  It's kind of awesome that libgdx just comes with built in methods for Gdx.input.getDeltaX() and Y.  I multiply them both by camera.zoom, because when we are zoomed very far in or very far out, the distance the camera moves should change accordingly.  camera.translate() just moves the x,y, and z coordinate of the camera, but we're not touching z, so that component is 0.

scrolled() handles scrolling, and if you scroll up it zooms in (up to 0.2) whereas if you scroll down it scrolls out (up to 8).  You can adjust those points, but be wary of what can happen if you get a negative zoom!  Also, the fact that zoom changes by amount*0.1 sets how fine tuned you can adjust the zoom.  If it were 0.01, you would have finer control.

If you implement these straight away, you might notice something funny... I sure did!  The HUD which displays FPS and so on stays at a fixed point on the MAP, not on the screen.  As you zoom out, it gets smaller and smaller.  As you zoom in it does the same.  That is silly, so let's fix it!

The problem in HudRenderSystem.java is that we run batch.setProjectionMatrix(camera.combined).  This lets things move as you move your camera, exactly what we want to avoid.  Get rid of this line.  In fact, you don't really need anything to do with a camera, so this is what my new HudRenderSystem looks like (I know I left the camera as an argument, but I was just too lazy to change the main code where I initialize it).
package com.gamexyz.systems;

import com.artemis.systems.VoidEntitySystem;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.Texture.TextureFilter;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.gamexyz.GameXYZ;

public class HudRenderSystem extends VoidEntitySystem {

 private SpriteBatch batch;
 private BitmapFont font;

 public HudRenderSystem(OrthographicCamera camera) {
 }

 @Override
 protected void initialize() {

  batch = new SpriteBatch();

  Texture fontTexture = new Texture(Gdx.files.internal("fonts/normal_0.png"));
  fontTexture.setFilter(TextureFilter.Linear, TextureFilter.MipMapLinearLinear);
  TextureRegion fontRegion = new TextureRegion(fontTexture);
  font = new BitmapFont(Gdx.files.internal("fonts/normal.fnt"), fontRegion, false);
  font.setUseIntegerPositions(false);
 }

 @Override
 protected void begin() {
  batch.begin();
 }

 @Override
 protected void processSystem() {
  batch.setColor(1, 1, 1, 1);
  font.draw(batch, "FPS: " + Gdx.graphics.getFramesPerSecond(), 20, GameXYZ.WINDOW_HEIGHT - 20);
  font.draw(batch, "Active entities: " + world.getEntityManager().getActiveEntityCount(), 20, GameXYZ.WINDOW_HEIGHT - 40);
  font.draw(batch, "Total created: " + world.getEntityManager().getTotalCreated(), 20, GameXYZ.WINDOW_HEIGHT - 60);
  font.draw(batch, "Total deleted: " + world.getEntityManager().getTotalDeleted(), 20, GameXYZ.WINDOW_HEIGHT - 80);
 }
 
 @Override
 protected void end() {
  batch.end();
 }
}

With that, our HUD should stay up where it belongs... silly HUD...

The last bit we want right now is frustum culling: to only draw the tiles in our camera's view.  Libgdx makes this pretty easy too, but to give you super clear idea of what a frustum is, here's wikipedia.
What we're going to do is get the coordinates of the corners of the far plane, and use them to limit what we render.  In MapRenderSystem, where we defined int x0, x1, y0, and y1, change the code to look like this:
  // Get bottom left and top right coordinates of camera viewport and convert
  // into grid coordinates for the map
  int x0 = MathUtils.floor(camera.frustum.planePoints[0].x / (float)MapTools.col_multiple) - 1;
  int y0 = MathUtils.floor(camera.frustum.planePoints[0].y / (float)MapTools.row_multiple) - 1;
  int x1 = MathUtils.floor(camera.frustum.planePoints[2].x / (float)MapTools.col_multiple) + 2;
  int y1 = MathUtils.floor(camera.frustum.planePoints[2].y / (float)MapTools.row_multiple) + 1;
  
  // Restrict the grid coordinates to realistic values
  if (x0 % 2 == 1) x0 -= 1;
  if (x0 < 0) x0 = 0;
  if (x1 > gameMap.width) x1 = gameMap.width;
  if (y0 < 0) y0 = 0;
  if (y1 > gameMap.height) y1 = gameMap.height; 
And voila!  We probably don't notice a huge difference yet, but when we start playing with HUGE maps later on, this will mean a world of difference.  It handles zooming in and out as well.

This is almost starting to look like it could become a game.  Almost...

You have gained 50 XP.  Progress to Level 3: 250/600

No comments:

Post a Comment