Putting it all together

We will now begin to fill in the stub methods with some life. The game loop is a good starting point; it is our driving engine that keeps the game world updated and rendered in a continuous way. After this, we will add some sprites and verify that the updating and rendering mechanism is working fine. In order to manipulate the world and game objects, controls are added to receive and react on user input. Finally, the CameraHelper class will be implemented to allow us to move around freely in the game world and to select a game object of our choice that the camera is supposed to follow.

Note

The additions and modifications in code listings will be highlighted.

Building the game loop

The game loop will reside in the CanyonBunnyMain class' render() method. Before we can add the new code, we have to import the following packages to gain access to some classes that we are going to use:

import com.badlogic.gdx.Application;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;

After this, add the following code to create():

  @Override
  public void create () {
    // Set Libgdx log level to DEBUG
    Gdx.app.setLogLevel(Application.LOG_DEBUG);
    // Initialize controller and renderer
    worldController = new WorldController();
    worldRenderer = new WorldRenderer(worldController);
  }

First, we set the log level of LibGDX's built-in logger to debug the mode in order to print out everything to the console that might be logged during runtime. Do not forget to change the log level to something more appropriate such as LOG_NONE or LOG_INFO before publishing your game.

After this, we simply create a new instance of WorldController and WorldRenderer and save them in their respective member variables.

To continuously update and render the game world to the screen, add the following code to render():

  @Override
  public void render() {
    // Update game world by the time that has passed
    // since last rendered frame.
    worldController.update(Gdx.graphics.getDeltaTime());
    // Sets the clear screen color to: Cornflower Blue
    Gdx.gl.glClearColor(0x64/255.0f, 0x95/255.0f, 0xed/255.0f, 0xff/255.0f);
    // Clears the screen
    Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

    // Render game world to screen
    worldRenderer.render();
  }

The game world is incrementally updated using delta times. Luckily, LibGDX already does the math and housekeeping behind this for us, so all we need to do is to query the value by calling getDeltaTime() from the Gdx.graphics module and passing it to update() of WorldController. After this, LibGDX is instructed to execute two direct OpenGL calls using the Gdx.gl module. The first call glClearColor() sets the color white to a light blue color using red, green, blue, and alpha (RGBA) values written in a hexadecimal notation. Each color component needs to be expressed as a floating-point value ranging between 0 and 1 with a resolution of 8 bits. This is the reason why we are also dividing each color component by the value of 255.0f (8 bit = 28 = 256 = 0..255 distinct levels per color component).

Note

Some prefer a hexadecimal notation, while others prefer a decimal notation. Here is an example of setting the same color in a decimal notation if you prefer to do so:

Gdx.gl.glClearColor(
  100/255.0f, 149/255.0f, 237/255.0f, 255/255.0f);

The second call glClear() uses the color white we set before to fill in the screen, and therefore erase all of the screen's previous contents. The last step renders the new frame of the updated game world to the screen.

Note

You should never reverse the order of code execution, as shown in the preceding listing. For example, you could first try to render and then update the game world. Now, in this case, the displayed game world will always lag one frame behind of its actual state. The change is very subtle and might even go unnoticed. This, of course, depends on many factors. If it is an action game that requires fast reactions, it will probably be much more noticeable as compared to a slow-paced cardboard game with enough pauses to bridge the time gap until the screen eventually shows the true game world state.

Next, add the following code to resize():

  @Override
  public void resize (int width, int height) {
    worldRenderer.resize(width, height);
  }

Whenever a resize event occurs, the resize() method of the ApplicationListener interface will be called. As this event is related to rendering, we want it to be handled in WorldRenderer; therefore, simply hand over the incoming values to its own resize() method.

The same is almost true for the code to be added in dispose():

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

Whenever a dispose event occurs, it is passed on to the renderer.

There is one more tiny addition to improve the code for execution on Android devices. As you learned in Chapter 2, Cross-platform Development – Build Once, Deploy Anywhere, there are system events on Android to pause and resume its applications. In case of an incoming pause or resume event, we also want the game to either stop or continue updating our game world accordingly. To make this work, we need a new member variable called paused. Hence, add the following line of code to the class:

  private boolean paused;

Then, modify the create() and render()methods, as shown in the following code snippet:

  @Override
  public void create () {
      // Set Libgdx log level to DEBUG
      Gdx.app.setLogLevel(Application.LOG_DEBUG);
      // Initialize controller and renderer
      worldController = new WorldController();
      worldRenderer = new WorldRenderer(worldController);
      // Game world is active on start
      paused = false;
  }

  @Override
  public void render () {
      // Do not update game world when paused.
      if (!paused) {
        // Update game world by the time that has passed
        // since last rendered frame.
        worldController.update(Gdx.graphics.getDeltaTime());
      }
      // Sets the clear screen color to: Cornflower Blue
      Gdx.gl.glClearColor(0x64/255.0f, 0x95/255.0f, 0xed/255.0f, 0xff/255.0f);
      // Clears the screen
      Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

      // Render game world to screen
      worldRenderer.render();
    }

Lastly, add the following code to pause() and resume() in order to let the game respond to these events by setting paused to the correct state:

  @Override
  public void pause () {
      paused = true;
  }

  @Override
  public void resume () {
      paused = false;
  }

We have now reached a stage in our development process where it is worthwhile to take a quick look at whether everything works as expected. Run the game on a platform of your choice to test it. The following is the screenshot of the game running on Windows:

Building the game loop

You should see a window entirely filled with a blue color. Seriously, the result is not very exciting yet, nor does it resemble anything like a game. However, all the work we have done so far gives us a foundation on which we can continue to build our next extensions for the game.

Adding the test sprites

Let's now add some test code to try out the mechanism we built for updating and rendering. We will do this by adding some simple test sprites that are procedurally generated at runtime.

First, add the following imports to WorldController:

import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.graphics.Pixmap.Format;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.Sprite;
import com.badlogic.gdx.math.MathUtils;

After this, add the following code:

  public Sprite[] testSprites;
  public int selectedSprite;

  public WorldController () {
    init();
  }

  private void init () {
    initTestObjects();
  }

  private void initTestObjects() {
    // Create new array for 5 sprites
    testSprites = new Sprite[5];
    // Create empty POT-sized Pixmap with 8 bit RGBA pixel data
    int width = 32;
    int height = 32;
    Pixmap pixmap = createProceduralPixmap(width, height);
    // Create a new texture from pixmap data
    Texture texture = new Texture(pixmap);
    // Create new sprites using the just created texture
    for (int i = 0; i < testSprites.length; i++) {
      Sprite spr = new Sprite(texture);
      // Define sprite size to be 1m x 1m in game world
      spr.setSize(1, 1);
      // Set origin to sprite's center
      spr.setOrigin(spr.getWidth() / 2.0f, spr.getHeight() / 2.0f);
      // Calculate random position for sprite
      float randomX = MathUtils.random(-2.0f, 2.0f);
      float randomY = MathUtils.random(-2.0f, 2.0f);
      spr.setPosition(randomX, randomY);
      // Put new sprite into array
      testSprites[i] = spr;
    }
    // Set first sprite as selected one
    selectedSprite = 0;
  }

  private Pixmap createProceduralPixmap (int width, int height) {
    Pixmap pixmap = new Pixmap(width, height, Format.RGBA8888);
    // Fill square with red color at 50% opacity
    pixmap.setColor(1, 0, 0, 0.5f);
    pixmap.fill();
    // Draw a yellow-colored X shape on square
    pixmap.setColor(1, 1, 0, 1);
    pixmap.drawLine(0, 0, width, height);
    pixmap.drawLine(width, 0, 0, height);
    // Draw a cyan-colored border around square
    pixmap.setColor(0, 1, 1, 1);
    pixmap.drawRectangle(0, 0, width, height);
    return pixmap;
  }
  
  public void update (float deltaTime) {
    updateTestObjects(deltaTime);
  }

  private void updateTestObjects(float deltaTime) {
    // Get current rotation from selected sprite
    float rotation = testSprites[selectedSprite].getRotation();
    // Rotate sprite by 90 degrees per second
    rotation += 90 * deltaTime;
    // Wrap around at 360 degrees
    rotation %= 360;
    // Set new rotation value to selected sprite
    testSprites[selectedSprite].setRotation(rotation);
  }
}

The new code adds two new member variables, testSprites and selectedSprite. The first one holds instances of the Sprite objects. We chose to add five sprites for our test. The second variable holds the index of the currently selected sprite that is stored in the array. The Sprite class can be used to display textures. As we do not have any textures added to our project yet, we will generate one for our test on the fly using the Pixmap class. Pixmap holds the actual pixel data (in a map of bytes) to represent any image. Its class provides some basic drawing methods that we will use in this code to draw a 32 x 32 pixel-sized transparent red box with a yellow "X" crossing it diagonally and a cyan border. The final pixel data is then put in a new Texture object. This object is eventually attached to each new Sprite we create so that it will show our handcrafted image when rendered.

The following is an image of the procedurally-generated test sprite:

Adding the test sprites

Each sprite's size is set to 1 x 1 meter. Remember that we defined our visible world size to be 5 x 5 meters at the beginning of this chapter. So, these sprites will be exactly one-fifth of the size in our game world. This fact is very important to understand because it is not the dimension of pixels in your image that defines the size of your game objects. Everything needs to be defined in virtual meters that relate to the visible game world.

The origin of the sprite is set to its center point. This allows us to rotate the sprites around itself without an added translation effect. The position is set randomly between two meters in negative and positive directions. Additionally, we set the index to 0 (the first element in the array) for the initially selected sprite. In updateTestObjects(), we refer to the selected sprite to rotate it on each update cycle. This allows us to easily see which of the shown sprites is currently the selected one.

All that we have achieved so far is to add the logic to create and modify the game world with its objects, but none of them are rendered to the screen yet. This is what we will change next.

First, add one new line to also import the Sprite class in WorldRenderer, as follows:

import com.badlogic.gdx.graphics.g2d.Sprite;

After this, add the following code to WorldRenderer:

  public WorldRenderer (WorldController worldController) {
    this.worldController = worldController;
    init();
  }
  
  private void init () {
    batch = new SpriteBatch();
    camera = new OrthographicCamera(Constants.VIEWPORT_WIDTH, Constants.VIEWPORT_HEIGHT);
    camera.position.set(0, 0, 0);
    camera.update();
  }
  public void render () {
  renderTestObjects();
}

  private void renderTestObjects() {
     batch.setProjectionMatrix(camera.combined);
     batch.begin();
     for(Sprite sprite : worldController.testSprites) {
       sprite.draw(batch);
     }
     batch.end();
  }

  public void resize (int width, int height) {
    camera.viewportWidth = (Constants.VIEWPORT_HEIGHT / height) * width;
    camera.update();
  }

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

The first action in WorldRenderer is to store the reference to WorldController when it is instantiated. This is necessary for the renderer to access the game objects that are managed by the controller. In init(), a new SpriteBatch object is created, which will be used for all our rendering tasks. Before we can start to render objects, we need to create a camera and define its viewport properly. The camera's viewport defines the size of the captured game world it is looking at. It works basically the same as a real camera. Obviously, when looking through a camera, you cannot see anything else except the area it is currently pointed at. For example, if you want to see what is to the left of it, you will have to move your camera to the left, which holds true for both real and virtual cameras. We are using the width and height defined in Constants to set the viewport.

In the event of a resized displaying area, resize() will be called by LibGDX. This is our chance to adapt to the new display dimensions and redefine how the game world should be rendered in this case. The code we added in resize() calculates the aspect ratio between our desired visible world height and the currently available display height. The answer is then multiplied with the available display width to find the new viewport width for the camera. The resulting effect of this calculation is that the world's visible height will always be kept to its full extent, while the world's width will scale according to the calculated aspect ratio. It is very important to not forget to call camera.update() whenever changes are made to the camera to let them take effect.

The rendering of the game world takes place in the render() method. The SpriteBatch class offers two methods called begin() and end(). These methods are used to start and end a new batch of drawing commands. Before any drawing command can be executed, it is mandatory to call begin(). In renderTestObjects(), we loop through all the sprites by accessing the previously stored reference to WorldController and calling the Sprite class' draw() method to draw it. After all drawing commands have been executed, we end the batch with the corresponding call to end(), which is just as mandatory as begin().

All done! You can now run the game to try it out. One of the sprites should be constantly rotating around its center point, which tells us that this must be the currently selected sprite.

Here is a screenshot of the game with the rendered test sprites running on Windows:

Adding the test sprites

Adding the game world's debug controls

During development, having debug controls built into an application to be able to directly manipulate certain behaviors can be a very powerful feature. Debug controls are what gamers usually call game cheats, although this is a very elastic term. What is certain is that it will make your life as a developer a lot easier and more fun too. Just be sure to remove or disable all debug controls before publishing your game as long as you do not intend them to be available to the user.

There are two ways to handle the input events. We will make use of both of them shortly to demonstrate when and how to use them. The debug controls we are going to implement will allow us to do the following operations:

  • Move a selected sprite into any of the four directions (left, right, up, or down)
  • Reset the game world to its initial state
  • Cycle through the list of sprites to select the other ones

The first of the three requirements is quite different to the other two in respect of continuity. For example, when holding down a key for a move action, you would expect this action to be repeatedly executed while the key is still being pressed. In contrast, the other two actions are characterized by being nonrecurring events. This is because you usually don't want to reset the game or cycle through the list of sprites a hundred times per second when the respective key is pressed and even held for a longer period of time.

Let's begin with the movement of a selected sprite that uses the continuous execution approach. Add the following line of code to WorldController to import a new class that holds all the available key constants that are supported by LibGDX:

import com.badlogic.gdx.Input.Keys;

Then, add the following code:

  public void update (float deltaTime) {
    handleDebugInput(deltaTime);
    updateTestObjects(deltaTime);
  }

  private void handleDebugInput (float deltaTime) {
    if (Gdx.app.getType() != ApplicationType.Desktop) return;

    // Selected Sprite Controls
    float sprMoveSpeed = 5 * deltaTime;
    if (Gdx.input.isKeyPressed(Keys.A)) moveSelectedSprite(-sprMoveSpeed, 0);
    if (Gdx.input.isKeyPressed(Keys.D)) moveSelectedSprite(sprMoveSpeed, 0);
    if (Gdx.input.isKeyPressed(Keys.W)) moveSelectedSprite(0, sprMoveSpeed);
    if (Gdx.input.isKeyPressed(Keys.S)) moveSelectedSprite(0, -sprMoveSpeed);
  }

  private void moveSelectedSprite (float x, float y) {
    testSprites[selectedSprite].translate(x, y);
  }

The new code adds two new methods, handleDebugInput() and moveSelectedSprite() to the class. The handleDebugInput() method is added as the topmost call to ensure that the available user inputs are handled first before other update logic is executed. Otherwise, similar to the order of updating and rendering in the game loop, it might introduce some sort of lagging behind the user input and response to such an event. This method also takes the delta time as an argument.

It is used for the same purpose as it is used in updateTestObjects()—to apply incremental updates in relation to the time that has passed since the last frame was rendered. As a measure of precaution, the handling of our debug controls is skipped if the game is not run on a system that is identified as desktop by LibGDX. In this way, if we were to only target Android for our game, we could leave all the code for the debug controls in the game without having to worry about it any time later.

The Gdx.input module provides an isKeyPressed() method that can be used to find out whether a key is (still) pressed. You have to use LibGDX's Keys class for valid constants to receive the correct results. So what we basically did here is ask whether any possible combination of the keys A, D, W, and S is currently pressed. If a condition returns true, meaning that the key is really pressed, moveSelectedSprite() is called. The method requires two values that indicate the direction and magnitude of the desired motion that is to be applied to the selected sprite. The magnitude here is sprMoveSpeed with a constant value of 5 meters multiplied by the delta time, which means that our sprite will effectively be able to move at a speed of 5 meters per second.

You can now start the game at the desktop and try it out. Press any of the keys (A, D, W, or S) to move around the selected sprite in the game world.

The next controls to implement are the keys to reset the game world and to select the next sprite. Once again, add another line of code to WorldController to import a new class, as follows:

import com.badlogic.gdx.InputAdapter;

The InputAdapter class is a default implementation of the InputProcessor interface that provides various methods to handle input events. We want to use the adapter variant instead of the InputProcessor. This is because it is a convenient way of not being forced to implement all the interface methods when you know that you are not going to implement most of them anyway. It would be perfectly valid, of course, to still use the InputProcessor interface since it is just a matter of taste. Derive WorldController from InputAdapter by changing the existing class like this:

public class WorldController extends InputAdapter {
  // ...
}

Then, add the following code snippet to the existing class:

  private void init () {
    Gdx.input.setInputProcessor(this);
    initTestObjects();
  }

The WorldController class serves a second purpose from now on by also being an instance of the InputProcessor interface that can receive input events. LibGDX needs to be told about where it should send the received input events. This is done by calling setInputProcessor() from the Gdx.Input module. As WorldController is also our InputProcessor, we can simply pass it into this method.

Now that LibGDX will send all the input events to our listener, we need to actually implement an event handler for each event we are interested in. In our case, this will only be the event where a key was released. These events are handled in keyUp(). Override the adapter's default implementation of this method with the following code:

  @Override
  public boolean keyUp (int keycode) {
    // Reset game world
    if (keycode == Keys.R) {
      init();
      Gdx.app.debug(TAG, "Game world resetted");
    }
    // Select next sprite
    else if (keycode == Keys.SPACE) {
      selectedSprite = (selectedSprite + 1) % testSprites.length;
      Gdx.app.debug(TAG, "Sprite #" + selectedSprite + " selected");
    }
    return false;
  }

The code will check whether keycode contains the code for either R or the Space bar. If it is R, the initialization method init() of WorldController is called. This results in an internal restart of the game as if the whole game was restarted. If the Space bar was pressed, the index stored in selectedSprite is incremented by one. The modulo operator (%) that is followed by the size of the array is used to wrap around the incremented value if it exceeds the maximum value allowed.

Note

This handler method is called only when there is an event. This is the huge difference as compared to the previous way we were handling user input. Both ways are correct, as it only depends on your situation and how you need the input in question to be handled.

You can now start the game and try it out on your desktop. You should be able to reset the game world with the R key. The test sprites should be shuffling around on every executed reset action. Selecting another sprite using the Space bar should make the previously selected one stop rotating and in turn, let the newly selected sprite start rotating. You can also use the keys A, D, W, and S that will only move the currently selected sprite one at a time.

Adding the CameraHelper class

We now want to implement a helper class called CameraHelper that will assist us to manage and manipulate certain parameters of the camera we use to render the game world.

Here is the implementation of CameraHelper:

package com.packtpub.libgdx.canyonbunny.util;

import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.g2d.Sprite;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Vector2;

public class CameraHelper {
  private static final String TAG = CameraHelper.class.getName();

  private final float MAX_ZOOM_IN = 0.25f;
  private final float MAX_ZOOM_OUT = 10.0f;

  private Vector2 position;
  private float zoom;
  private Sprite target;

  public CameraHelper () {
    position = new Vector2();
    zoom = 1.0f;
  }

  public void update (float deltaTime) {
    if (!hasTarget()) return;

    position.x = target.getX() + target.getOriginX();
    position.y = target.getY() + target.getOriginY();
  }
  public void setPosition (float x, float y) {
    this.position.set(x, y);
  }
  public Vector2 getPosition () { return position; }

  public void addZoom (float amount) { setZoom(zoom + amount); }
  public void setZoom (float zoom) {
    this.zoom = MathUtils.clamp(zoom, MAX_ZOOM_IN, MAX_ZOOM_OUT);
  }
  public float getZoom () { return zoom; }

  public void setTarget (Sprite target) { this.target = target; }
  public Sprite getTarget () { return target; }
  public boolean hasTarget () { return target != null; }
  public boolean hasTarget (Sprite target) {
    return hasTarget() && this.target.equals(target);
  }

  public void applyTo (OrthographicCamera camera) {
    camera.position.x = position.x;
    camera.position.y = position.y;
    camera.zoom = zoom;
    camera.update();
  }
}

The helper class stores the current position and zoom value for the camera. Furthermore, it can follow one game object at a time when set as a target by calling setTarget(). The target can also be set to null to make the camera stop following at all. To find out what the last set target is, you can call getTarget(). Usually, you will want to do this for null checks or to find out whether the set target is a certain sprite. These checks are wrapped into the hasTarget() method and can be used either with or without a sprite argument to find out whether a certain target has been set if any. The update() method should be called on every update cycle to let it update the camera position whenever needed. The applyTo() method should always be called at the beginning of the rendering of a new frame as it takes care of updating the camera's attributes.

Adding the camera debug controls using CameraHelper

The last step in this chapter will be to add the camera debug controls using the CameraHelper class. This will greatly improve your debugging abilities just because you can freely move around the game world, zoom in and out to/from game objects, and follow any game object.

Before we can use CameraHelper, we have to import it in WorldController as follows:

import com.packtpub.libgdx.canyonbunny.util.CameraHelper;

After this, add the following code to WorldController:

  public CameraHelper cameraHelper;

  private void init () {
    Gdx.input.setInputProcessor(this);
    cameraHelper = new CameraHelper();
    initTestObjects();
  }

  public void update (float deltaTime) {
    handleDebugInput(deltaTime);
    updateTestObjects(deltaTime);
    cameraHelper.update(deltaTime);
  }

  @Override
  public boolean keyUp (int keycode) {
    // Reset game world
    if (keycode == Keys.R) {
      init();
      Gdx.app.debug(TAG, "Game world resetted");
    }
    // Select next sprite
    else if (keycode == Keys.SPACE) {
      selectedSprite = (selectedSprite + 1) % testSprites.length;
      // Update camera's target to follow the currently
      // selected sprite
      if (cameraHelper.hasTarget()) {
        cameraHelper.setTarget(testSprites[selectedSprite]);
      }
      Gdx.app.debug(TAG, "Sprite #" + selectedSprite + " selected");
    }
    // Toggle camera follow
    else if (keycode == Keys.ENTER) {
      cameraHelper.setTarget(cameraHelper.hasTarget() ? null : testSprites[selectedSprite]);
      Gdx.app.debug(TAG, "Camera follow enabled: " + cameraHelper.hasTarget());
    }
    return false;
  }

The WorldController class now has an instance of CameraHelper that is initialized in init() and appended at the end of update(). Remember to continuously call update() of CameraHelper on every update cycle to ensure that its internal calculations are also performed. In the keyUp() method, we add two new functionalities. The first one is that the target of the camera helper is updated according to a newly selected sprite. Secondly, when the Enter key is pressed, the target is toggled on and off. Additionally, add the following code to WorldRenderer:

  public void renderTestObjects () {
    worldController.cameraHelper.applyTo(camera);
    batch.setProjectionMatrix(camera.combined);
    batch.begin();
    for(Sprite sprite : worldController.testSprites) {
      sprite.draw(batch);
    }
    batch.end();
  }

The applyTo() method should be called on each frame right at the beginning in the renderTestObjects() method of WorldRenderer. It will take care of correctly setting up the camera object that is passed.

You can now start the game on your desktop and try it out. To enable the camera follow feature, simply press the Enter key to toggle the state. A message is also logged to the console that informs you about the current state it is in. When the camera follow feature is enabled, use the A, D, W, and S keys to move the selected sprite. However, the difference now is that the camera is following you to every location until the camera follow feature is disabled again.

The last change to the code deals with adding a lot of new keys to control the camera in various ways.

Add the following code to WorldController:

  private void handleDebugInput (float deltaTime) {
    if (Gdx.app.getType() != ApplicationType.Desktop) return;

    // Selected Sprite Controls
    float sprMoveSpeed = 5 * deltaTime;
    if (Gdx.input.isKeyPressed(Keys.A)) moveSelectedSprite(-sprMoveSpeed, 0);
    if (Gdx.input.isKeyPressed(Keys.D)) moveSelectedSprite(sprMoveSpeed, 0);
    if (Gdx.input.isKeyPressed(Keys.W)) moveSelectedSprite(0, sprMoveSpeed);
    if (Gdx.input.isKeyPressed(Keys.S)) moveSelectedSprite(0, -sprMoveSpeed);

    // Camera Controls (move)
    float camMoveSpeed = 5 * deltaTime;
    float camMoveSpeedAccelerationFactor = 5;
    if (Gdx.input.isKeyPressed(Keys.SHIFT_LEFT)) camMoveSpeed *= camMoveSpeedAccelerationFactor;
    if (Gdx.input.isKeyPressed(Keys.LEFT)) moveCamera(-camMoveSpeed, 0);
    if (Gdx.input.isKeyPressed(Keys.RIGHT)) moveCamera(camMoveSpeed, 0);
    if (Gdx.input.isKeyPressed(Keys.UP)) moveCamera(0, camMoveSpeed);
    if (Gdx.input.isKeyPressed(Keys.DOWN)) moveCamera(0, -camMoveSpeed);
    if (Gdx.input.isKeyPressed(Keys.BACKSPACE)) cameraHelper.setPosition(0, 0);

    // Camera Controls (zoom)
    float camZoomSpeed = 1 * deltaTime;
    float camZoomSpeedAccelerationFactor = 5;
    if (Gdx.input.isKeyPressed(Keys.SHIFT_LEFT)) camZoomSpeed *= camZoomSpeedAccelerationFactor;
    if (Gdx.input.isKeyPressed(Keys.COMMA)) cameraHelper.addZoom(camZoomSpeed);
    if (Gdx.input.isKeyPressed(Keys.PERIOD)) cameraHelper.addZoom(-camZoomSpeed);
    if (Gdx.input.isKeyPressed(Keys.SLASH)) cameraHelper.setZoom(1);
  }

  private void moveCamera (float x, float y) {
    x += cameraHelper.getPosition().x;
    y += cameraHelper.getPosition().y;
    cameraHelper.setPosition(x, y);
  }

There are two new code blocks to control the moving and zooming features of the camera in handleDebugInput(). They look and also work similar to the block above them that deals with the controls for the selected sprite. The keys are checked for their state and if pressed, the respective action is taken.

The camera controls to move are as follows:

  • The arrow keys left, right, up, and down control the movement of the camera
  • The magnitude of motion is set to 500 percent when the Shift key is pressed
  • Pressing the Backspace key resets the camera position to the origin (0, 0) of the game world

The camera controls to zoom are as follows:

  • The comma (,) and period (.) keys control the zoom level of the camera
  • The magnitude of motion is set to 500 percent when the Shift key is pressed
  • The forward slash (/) key resets the zoom level to 100 percent (the original position)

The moveCamera() method is used to execute relative movements of the camera similar to what moveSelectedSprite() is doing by calling sprite's translate() method.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset