How to Code a 2D Game Engine using Java

How to Code a 2D Game Engine using Java

Game engines provide a framework and toolset for building video games across multiple platforms. While powerful commercial engines like Unity and Unreal Engine are widely used in the industry, developing your own game engine is a valuable learning experience that provides insight into computer graphics, performance optimization, cross-platform development, and other key concepts.

In this in-depth guide, we‘ll walk through the process of architecting and coding a 2D game engine in Java from scratch, covering everything from low-level rendering with OpenGL to higher-level systems for game object management, physics simulation, and scene editing. By the end, you‘ll have a solid understanding of what goes into a game engine and how the different pieces fit together. Let‘s get started!

Architecture Overview

Before diving into the code, it‘s important to understand the high-level architecture of a game engine. The engine is essentially a collection of interconnected systems that work together to bring a game to life. Some key systems include:

  • The rendering engine
  • The physics engine
  • The audio engine
  • The input system
  • The memory management system
  • The entity-component system (game object model)
  • The asset management pipeline
  • The GUI and tools framework

Game Engine Architecture
Example game engine architecture, courtesy of Engine Architecture by Jason Gregory

How you structure and decouple these systems is critical for maintainability and extensibility as your engine grows in complexity. A well-designed engine should allow you to easily swap out different implementations of each system or add new features without massive rewrites.

Setting Up the Project

For our Java game engine, we‘ll use the Lightweight Java Game Library (LWJGL) for low-level APIs. LWJGL provides access to native libraries like OpenGL and OpenAL for graphics and audio, as well as input handling via GLFW.

To set up an LWJGL project:

  1. Download the latest LWJGL release
  2. Extract the contents somewhere in your project directory
  3. Add the jars from the extracted folder to your IDE‘s build path configuration
  4. Add the natives folder for your platform to the classpath

With LWJGL added as a dependency, we can start writing engine code.

The Game Loop

At the heart of every game engine is the game loop – a process that continuously updates the game state and renders each frame. A basic game loop looks like:

void run() {
    init();
    while (!glfwWindowShouldClose(window)) {
        float dt = getDeltaTime();
        update(dt);
        render();
        glfwPollEvents();
    }    
}

The getDeltaTime function calculates the elapsed time since the last frame, which we pass to update to make sure game physics and animations run at a consistent speed regardless of framerate.

glfwPollEvents processes any queued window events like mouse movement or key presses. We‘ll register callback functions to update engine state on input events.

The Rendering Engine

Rendering is often the most complex part of a game engine. To draw 2D graphics, we‘ll use OpenGL, a cross-platform graphics API. Modern OpenGL requires the use of shaders – small programs written in GLSL that run on the GPU.

Here‘s an example vertex shader:

#version 330 core

layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;

uniform mat4 uProjection;
uniform mat4 uView;
uniform mat4 uModel;

out vec2 fTexCoord;

void main() {
    fTexCoord = aTexCoord;
    gl_Position = uProjection * uView * uModel * vec4(aPos, 1.0); 
}

And a simple fragment shader:

#version 330 core

in vec2 fTexCoord;

uniform sampler2D uTexture;
uniform vec3 uTint;

out vec4 color;

void main() {
    color = texture(uTexture, fTexCoord) * vec4(uTint, 1.0);
}

To draw objects with OpenGL:

  1. Set up Vertex Array Objects (VAOs) and Vertex Buffer Objects (VBOs) with mesh data
  2. Load textures from image files
  3. Set shader uniforms for the projection, view, and model matrices
  4. Call glDrawElements with the VAO and vertex count

However, calling glDraw for each object would be very inefficient. Instead, we can use batched rendering – loading the vertex data for many objects into a single buffer and drawing it all at once. The shader can use per-instance data like model matrices and texture IDs uploaded to GPU memory.

With batched rendering and texture atlases, it‘s possible to draw tens of thousands of objects in a single draw call. This is crucial for CPU-bound games to maintain good performance.

Some other key parts of a rendering engine include:

  • Managing multiple cameras and viewports
  • Culling objects that are offscreen to avoid wasted rendering
  • Sorting objects by Z-order for proper transparency blending
  • Post-processing effects like bloom, color grading, etc.
  • Particle systems for effects like fire, smoke, magic spells
  • Optimizing shaders to minimize redundant GPU state changes

The Physics Engine

Another key system is the physics engine, which handles things like collision detection and rigid body dynamics simulation. Writing a robust physics engine from scratch is very complex, so we‘ll use an existing library: JBox2D, a Java port of the popular C++ Box2D engine.

Some key physics engine concepts:

  • A World object that holds all the physics entities and simulates them over time
  • Body objects that have properties like position, velocity, and collider shapes
  • Fixture objects attached to bodies that define the shape and material properties
  • Joints and constraints that limit degrees of freedom between bodies
  • Collision callbacks that detect and resolve contacts between colliders

To integrate physics into our engine, we‘ll create a PhysicsSystem that wraps a JBox2D world. Each frame, we‘ll update the physics world, then sync the positions of the physics bodies to their corresponding entities in the game world.

Some other common physics engine features include:

  • Raycasting for line-of-sight checks
  • Trigger callbacks when objects enter or exit collision zones
  • Collision layers and masks to optimize collision checking
  • Support for kinematic bodies, which are moved by game logic instead of physics simulation

Entity Component System

Game objects in modern engines are often built using an Entity Component System (ECS) architecture. The core idea is a complete decoupling of object data from behavior.

  • An Entity is just an ID. It has no data or logic of its own.
  • A Component is a piece of data attached to an entity, like position or sprite.
  • A System operates on entities that have a certain set of components.

Some example components:

class Transform implements Component {
    Vector2f position;
    float rotation;
    Vector2f scale;
}

class Sprite implements Component {
    Texture texture;
    Vector4f color;
}

class Collider implements Component {
    Body body;
    Fixture fixture;
}

And systems:

class PhysicsSystem implements System {
    public void update(float dt) {
        world.step(dt, VELOCITY_ITERATIONS, POSITION_ITERATIONS);
        for (Entity e : entities) {            
            Transform t = e.getComponent(Transform.class);
            Collider c = e.getComponent(Collider.class);            
            Vec2 pos = c.body.getPosition();
            t.position.set(pos.x, pos.y);
            t.rotation = c.body.getAngle();
        }
    }
}

class RenderSystem implements System {
    public void render() {
        for (Entity e : entities) {
            Transform t = e.getComponent(Transform.class);
            Sprite s = e.getComponent(Sprite.class);
            renderSprite(s, t);
        }
    }
}

This approach has several benefits:

  • Avoids deep, inflexible inheritance hierarchies
  • Improves cache usage by packing related data together
  • Easy to reuse functionality across entity types
  • Allows extending entities with new abilities without invasive code changes

To implement a basic ECS:

  1. Create an Entity class that‘s essentially a wrapper around an integer ID
  2. Create a Component interface that all components must implement
  3. Create a System abstract class that operates on a list of entities
  4. Give entities methods to add, remove, and retrieve their attached components
  5. Each frame, update the relevant systems

The Scene Graph

On top of the ECS architecture, most game engines use a tree data structure called a scene graph to organize game objects spatially and logically. The scene graph makes it easy to modify the position or rendering order of whole branches of objects. It‘s also very useful for frustum and occlusion culling optimizations.

Here‘s a (simplified) example of scene graph nodes:

class SceneNode {
    private final Transform localTransform = new Transform();
    private Transform worldTransform;

    private final List<SceneNode> children = new ArrayList<>();
    private SceneNode parent;

    public void attachChild(SceneNode child) {
        children.add(child);
        child.parent = this;
    }

    public void detachChild(SceneNode child) {
        children.remove(child);
        child.parent = null;
    }   

    public void update(float dt) {
        for (SceneNode child : children) {
            child.update(dt);
        }
    }
}

class Entity extends SceneNode {
    private final Map<Class<?>, Component> components = new HashMap<>();
    private final List<SceneNode> children = new ArrayList<>();

    public <T extends Component> T getComponent(Class<T> type) {        
        return type.cast(components.get(type));
    }

    public <T extends Component> Entity addComponent(T c) {
        components.put(c.getClass(), c);
        return this;
    }
}

The scene graph should be updated each frame, with transforms propagating down from parents to children. The renderer can then traverse the graph and draw objects in the correct order based on their world transform.

The Asset Pipeline

Games often have lots of assets like textures, 3D models, audio files, and scripts. An asset pipeline is responsible for discovering assets, loading them from files or over the network, and translating them into optimal runtime formats.

Some key parts of an asset system:

  • A central AssetManager class that handles all asset loading and caching
  • Async background loading to avoid stalling the game
  • Reference counting to share assets between multiple objects
  • Hot reloading of modified assets at runtime (for faster iteration)
  • Preprocessing like resizing textures to power-of-two dimensions or generating mipmaps
class AssetManager {
    private final Map<String, Asset> assets = new HashMap<>();

    public <T extends Asset> T load(String path) {
        if (assets.containsKey(path)) {
            return (T) assets.get(path);
        } else {
            Asset asset = loadAssetFromFile(path);
            assets.put(path, asset);
            return (T) asset;
        }
    }

    private Asset loadAssetFromFile(String path) {
        // Async load + process the asset file, then cache it
    }
}

The Editor

Making games requires spending lots of time tuning data in the engine. To speed up iteration times, most engines integrate GUI tools for visually editing game objects, scripting behaviors, defining animation state machines, building levels, and more.

For our Java engine, we can use Dear ImGui, an immediate mode GUI toolkit, to create tools that integrate directly with live game data. Here‘s an example of how to make an editor window with ImGui:

public void renderEditorUI() {
    ImGui.begin("Object Properties");

    Entity selectedEntity = selectedNode.getEntity();
    Transform transform = selectedEntity.transform;

    float[] pos = {transform.position.x, transform.position.y};
    if (ImGui.dragFloat2("Position", pos)) {
        transform.position.set(pos[0], pos[1]);
    }

    float[] color = selectedEntity.getComponent(Sprite.class).color.toArray();
    if (ImGui.colorEdit4("Color", color)) {
        selectedEntity.getComponent(Sprite.class).setColor(color);        
    }

    ImGui.end();
}

This will create a window with editable position and color fields for the currently selected entity. Changes in the UI will immediately be reflected in the game world.

Some other useful editor features:

  • Scene hierarchy tree for parenting objects
  • Drag and drop support for rearranging the hierarchy
  • Property grids for editing component values
  • Undo/redo stack for reversing changes
  • Project file browser for managing assets
  • Toolbar with common commands like translate, rotate, scale
  • Multiple layout panels, like 4-up, grid, tabbed, etc.

Conclusion

As you can see, even a basic 2D game engine requires integrating a wide range of complex systems. Rendering, physics, scripting, GUI tools, cross-platform asset loading – each of these are deep disciplines in their own right. And we‘ve only just scratched the surface of more advanced topics like skeletal animation, visual effects, networking, and multiplayer.

Fortunately, by using solid architecture practices and leveraging open source libraries, we can create a working engine foundation that can be extended with more features over time. A well-designed, modular engine can be just as important for game development velocity as the right creative direction or art style.

I hope this in-depth guide gave you a solid overview of what goes into engineering a game engine and the kind of work professional game developers do every day. Building an engine from scratch is a huge undertaking, but a very rewarding one. With the foundation we‘ve laid out here, you‘re well equipped to dig deeper into each of the systems and start bringing your own game ideas to life. Happy coding!

References & Further Reading

Similar Posts