Creating a 2D Game with LibGDX: A Complete Guide

LibGDX is a powerful, open-source framework for building cross-platform games and applications in Java. A single codebase can deploy to Windows, macOS, Linux, Android, iOS, and even web browsers. This guide will walk you through setting up a LibGDX project and building a simple 2D game.


1. Project Setup: Using the LibGDX Project Generator

The easiest way to start is with the official setup tool.

  1. Download the Generator: Go to the LibGDX Project Generator.
  2. Run the Tool: Download the gdx-setup.jar file and run it:
    bash java -jar gdx-setup.jar
  3. Configure Your Project:
    • Name: My2DGame
    • Package: com.mygame
    • Game Class: My2DGame
    • Destination: Choose your project folder.
    • Subprojects: Select Desktop, Android, iOS, Html. For learning, Desktop is sufficient.
    • Extensions: For a simple 2D game, you don't need any initially. You can add them later.
  4. Generate: Click "Generate" to create your project structure.

2. Understanding the Project Structure

Your generated project will look like this:

My2DGame/
├── core/
│   ├── src/com/mygame/My2DGame.java  # <-- Your MAIN game class
│   └── build.gradle
├── desktop/
│   ├── src/com/mygame/DesktopLauncher.java # <-- Desktop launcher
│   └── build.gradle
├── android/
├── html/
├── ios/
└── build.gradle
  • core/: This is where all your shared game logic and assets go. You will spend most of your time here.
  • desktop/, android/, etc.: These are platform-specific launchers. The desktop/src/DesktopLauncher.java is the entry point for running your game on a PC.

3. Core Concepts: The Application Lifecycle

The main game class (e.g., My2DGame.java) extends ApplicationAdapter and overrides key lifecycle methods:

package com.mygame;
import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
public class My2DGame extends ApplicationAdapter {
SpriteBatch batch;
Texture img;
@Override
public void create () {
// Called once when the game starts. Initialize resources here.
batch = new SpriteBatch();
img = new Texture("badlogic.jpg"); // Place an image in core/assets/
}
@Override
public void render () {
// Called every frame (60 times per second). Your game logic goes here.
// 1. Clear the screen
Gdx.gl.glClearColor(0.15f, 0.15f, 0.2f, 1); // Dark blue color
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
// 2. Draw the image
batch.begin();
batch.draw(img, 50, 50); // Draw at position (50, 50)
batch.end();
}
@Override
public void dispose () {
// Called when the game is closed. Free up resources here.
batch.dispose();
img.dispose();
}
}

Key Components:

  • SpriteBatch: The most important object for 2D rendering. It draws images (sprites) to the screen efficiently. You begin a batch, draw everything you need, then end the batch.
  • Texture: Represents an image loaded into GPU memory (e.g., PNG, JPG).

4. Building a Simple Game: "Catch the Drop"

Let's create a simple game where the player controls a bucket to catch raindrops.

Step 1: Create the Game Class (core/src/com/mygame/DropGame.java)

package com.mygame;
import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.ScreenUtils;
import com.badlogic.gdx.utils.TimeUtils;
public class DropGame extends ApplicationAdapter {
private OrthographicCamera camera;
private SpriteBatch batch;
// Game Objects
private Texture bucketTexture;
private Texture dropTexture;
private Rectangle bucket;
// Raindrops
private Array<Rectangle> raindrops;
private long lastDropTime;
// Constants
private final int BUCKET_WIDTH = 64;
private final int BUCKET_HEIGHT = 64;
private final int DROP_SIZE = 64;
@Override
public void create() {
// Load textures
bucketTexture = new Texture(Gdx.files.internal("bucket.png"));
dropTexture = new Texture(Gdx.files.internal("drop.png"));
// Create camera and set the world view to 800x480
camera = new OrthographicCamera();
camera.setToOrtho(false, 800, 480);
batch = new SpriteBatch();
// Initialize bucket
bucket = new Rectangle();
bucket.x = 800 / 2 - BUCKET_WIDTH / 2;
bucket.y = 20;
bucket.width = BUCKET_WIDTH;
bucket.height = BUCKET_HEIGHT;
// Initialize raindrops array
raindrops = new Array<>();
spawnRaindrop();
}
private void spawnRaindrop() {
Rectangle raindrop = new Rectangle();
raindrop.x = MathUtils.random(0, 800 - DROP_SIZE);
raindrop.y = 480;
raindrop.width = DROP_SIZE;
raindrop.height = DROP_SIZE;
raindrops.add(raindrop);
lastDropTime = TimeUtils.nanoTime();
}
@Override
public void render() {
// Clear screen
ScreenUtils.clear(0, 0, 0.2f, 1);
// Update camera
camera.update();
batch.setProjectionMatrix(camera.combined);
// Handle Input
handleInput();
// Spawn new raindrops
if (TimeUtils.nanoTime() - lastDropTime > 1000000000) { // 1 second
spawnRaindrop();
}
// Update raindrop positions
for (Iterator<Rectangle> iter = raindrops.iterator(); iter.hasNext(); ) {
Rectangle raindrop = iter.next();
raindrop.y -= 200 * Gdx.graphics.getDeltaTime(); // Move downward
// Remove raindrops that are off-screen
if (raindrop.y + DROP_SIZE < 0) iter.remove();
// Check for collision with bucket
if (raindrop.overlaps(bucket)) {
iter.remove();
// TODO: Add score
}
}
// Draw everything
batch.begin();
batch.draw(bucketTexture, bucket.x, bucket.y);
for (Rectangle raindrop : raindrops) {
batch.draw(dropTexture, raindrop.x, raindrop.y);
}
batch.end();
}
private void handleInput() {
// Move bucket with mouse/touch
if (Gdx.input.isTouched()) {
Vector3 touchPos = new Vector3();
touchPos.set(Gdx.input.getX(), Gdx.input.getY(), 0);
camera.unproject(touchPos);
bucket.x = touchPos.x - BUCKET_WIDTH / 2;
}
// Move bucket with keyboard
if (Gdx.input.isKeyPressed(Input.Keys.LEFT)) bucket.x -= 400 * Gdx.graphics.getDeltaTime();
if (Gdx.input.isKeyPressed(Input.Keys.RIGHT)) bucket.x += 400 * Gdx.graphics.getDeltaTime();
// Keep bucket within screen bounds
if (bucket.x < 0) bucket.x = 0;
if (bucket.x > 800 - BUCKET_WIDTH) bucket.x = 800 - BUCKET_WIDTH;
}
@Override
public void dispose() {
bucketTexture.dispose();
dropTexture.dispose();
batch.dispose();
}
}

Step 2: Add Assets

  1. Download two simple PNG images (e.g., bucket.png and drop.png).
  2. Place them in the core/assets/ directory of your project.

Step 3: Update the Desktop Launcher

Modify desktop/src/com/mygame/DesktopLauncher.java to launch your new game:

package com.mygame;
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application;
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration;
public class DesktopLauncher {
public static void main(String[] arg) {
Lwjgl3ApplicationConfiguration config = new Lwjgl3ApplicationConfiguration();
config.setTitle("Catch the Drop");
config.setWindowedMode(800, 480);
config.setForegroundFPS(60);
new Lwjgl3Application(new DropGame(), config);
}
}

5. Key LibGDX Concepts Explained

  • OrthographicCamera: A 2D camera that defines what part of your game world is visible. Essential for scrolling games or different screen resolutions.
  • Rectangle: Used for collision detection. The overlaps() method checks if two rectangles intersect.
  • Gdx.input: The interface for handling user input (touch, mouse, keyboard).
  • Gdx.graphics.getDeltaTime(): Returns the time in seconds since the last frame. Use this to make movement frame-rate independent.
  • TimeUtils: Provides time-related utilities for game timing.

6. Running and Packaging

To Run on Desktop:

./gradlew desktop:run
# Or on Windows:
gradlew desktop:run

To Build a Desktop JAR:

./gradlew desktop:dist

The JAR file will be in desktop/build/libs/.


Next Steps and Best Practices

  1. Add a Game State Manager: Separate your main menu, game screen, and pause screen into different classes.
  2. Implement an Asset Manager: Load all your textures, sounds, and music in a centralized way to avoid reloading.
  3. Use Scenes2D UI: For complex UIs (buttons, labels, menus), LibGDX's Scenes2D is very powerful.
  4. Add Sound: Use Gdx.audio.newSound() for short effects and Gdx.audio.newMusic() for background music.
  5. Handle Different Screen Sizes: Use a Viewport (like FitViewport or ExtendViewport) with your camera for proper scaling.

Conclusion

LibGDX provides a robust, well-documented foundation for building 2D games in Java. Starting with simple concepts like the game loop, sprite rendering, and input handling, you can gradually build up to complex, polished games. The cross-platform nature means your game can reach a wide audience with minimal extra effort. The official LibGDX Wiki is an excellent resource for diving deeper into advanced topics.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper