Hugo Manrique

Minecraft as a Game Engine

Minecraft has become a widely used tool in lots of environments, but this game is capable of much more in terms of animation.

The fact that Minecraft's growth has no boundaries cannot be disputed. This game is currently used in lots of environments such as in education, but what people don't tend to think is that this game is capable of much more in terms of animation. In this post I will try to show you how to use Minecraft as a game engine with little code involved. Let's begin!

How does it work?

The first step on learning how to use a rendering engine is learning the data structures it uses, the tick loop, etc.

Minecraft processes changes in the world every 50 milliseconds, which is 1/20th of a second. Minecraft as a game uses the LWJGL library which uses OpenGL, a common rendering engine used in a lot of games nowadays.

The game also renders in regions of 16x16 blocks called chunks with some distance limits that we will have to take into account.

By the end of this post, you will be able to create your own engine, and although it can be run in a server, some methods in this post are latency sensitive such as the block gravity and the cinematics.

What should our game engine contain?

According to the Wikipedia page for "Game Engine", most of them include:

  • A rendering engine (We already have this)
  • A physics engine
  • A Sound engine (playable custom sounds)
  • Animation creator
  • Artificial intelligence
  • Tons of things Minecraft already implements for us

We will start with the easier concepts, and as we progress, we will start to add more code.

Mods vs Plugins

We could develop the engine as a mod (a client side modification) or as a plugin for the Spigot platform (a fork of the original Minecraft server that focuses on performance). I think Minecraft has overcomplicated how mods are made, forcing us the developers to use the Forge library, MCP, unreadable obfuscated function names...

Another point for not using a mod in this case is that I have little (or none) experience with Forge coding for the reasons stated above.

World rendering

Hypixel Warlords game map with Shaders

The first thing a player sees in a game is the terrain that is surrounding him. Minecraft has extensive settings to modify this experience such as FOV (field of view), rendering distance, quality of animations...

Visual modifications to the game can only be created client side by heavily modifying the dark rendering engine used in the client (the previous photo shows the Shaders mod creating custom shades), and as so, we won't focus on this.

The game has some clashing settings for our engine; for example, the FOV modification affects the character speed feeling, and as so, we will have to stick with block-based rendered objects. Other compromises/problems you will face while using Minecraft related code will be transparency and biome dynamic coloring for textures.

Resource Packs

Since 1.6.1, Minecraft allows content creators to modify all the textures available in the game and sounds, even giving us the option to add new assets.

If you want to create a shooter kind of game, we will need to have gun (pixelated) textures, and you can see lots of examples in famous multiplayer servers of the game:

Custom guns in CvC, a Hypixel game

But this doesn't end here! Since 1.8, Mojang has given texture creators the ability to give depth and custom dimensions to their models through a complex JSON based engine. As always, there's someone that makes this process easier, and that's @Sphax84 with his program Cubik Studio, a voxel based editor that can export your models into Minecraft JSON, with advanced functions such as in-game relative hand placing, rotations and imports and exports from real models in .STL (3D printings), .OBJ (Mesh) and many more.

This allows us to give more detail to our engine by not limiting our textures to predefined voxels and go above the normal possibilities the old texture pack system gave us. Here are some examples:

3D modelled karts

Medieval weapon

Cinematics

Okay, enough talking. Let's start with code. Minecraft doesn't have cinematics support, but the Spectator gamemode makes the player experience during a cinematic more inversive.

We will create an API that will accept a list of locations and will make a smooth transition for spectators. The method that is usually used to create the smooth teleports is a lerp function, which uses Linear interpolation. The important code is in the lerp() function, the rest is just classes to make it easier for the developers to add dynamic locations:

public class Cinematic {
    private final JavaPlugin plugin;
    private final World world;
    private final List<CinematicView> views;

    public Cinematic(JavaPlugin plugin, List<CinematicView> views, World world) {
        this.plugin = plugin;
        this.views = views;
        this.world = world;
    }

    // TODO Add getters
}
public class CinematicTask implements Runnable {
    private final JavaPlugin plugin;
    private final Player player;
    private final Cinematic cinematic;

    private int delay = 1;
    private float timer;
    private int index;

    private CinematicView current;
    private CinematicView next;

    private Vector pos1;
    private Vector pos2;
    private Vector nextPos;

    private int taskId;

    public CinematicTask(JavaPlugin plugin, Player player, Cinematic cinematic) {
        this.plugin = plugin;
        this.player = player;
        this.cinematic = cinematic;

        this.nextPos = new Vector();
    }

    public void start() {
        this.index = 0;
        loadView();

        Location loc = player.getLocation();

        loc.setX(pos1.getX());
        loc.setY(pos1.getY());
        loc.setZ(pos1.getZ());

        loc.setWorld(cinematic.getWorld());

        player.teleport(loc);
        player.setFlySpeed(0F);
        player.setGameMode(GameMode.SPECTATOR);

        this.timer = 0;

        // run() every tick
        taskId = Bukkit.getScheduler().scheduleSyncRepeatingTask(plugin, this, 0L, 0L);
    }

    public void run() {
        if (!player.isOnline()) {
            cancel();
            return;
        } else {
            player.setGameMode(GameMode.SPECTATOR);
        }

        player.closeInventory();
        timer += current.getSpeed() * delay;

        lerpNext();

        Vector playerPos = player.getLocation().toVector();
        float distance = (float) nextPos.distance(playerPos);

        nextPos.subtract(playerPos).normalize().multiply(distance * current.getSpeed());
        player.setVelocity(nextPos);

        // Arrived at view
        if (timer >= 1) {
            player.setFlySpeed(0);
            timer--;

            if (++index >= cinematic.getViews().size() - 1) {
                exit();
                return;
            }

            loadView();
        }
    }

    private void loadView() {
        current = getView(index);
        next = getView(index + 1);

        pos1 = current.getPosition();
        pos2 = next.getPosition();
    }

    // Utility methods
    public CinematicView getView(int index) {
        return cinematic.getViews().get(index);
    }

    public void lerpNext() {
        nextPos.setX(lerp(pos1.getX(), pos2.getX(), timer));
        nextPos.setY(lerp(pos1.getY(), pos2.getY(), timer));
        nextPos.setZ(lerp(pos1.getZ(), pos2.getZ(), timer));
    }

    private static float lerp(double pos1, double pos2, float timer) {
        return (float) (pos1 + timer * (pos2 - pos1));
    }

    private void cancel() {
        Bukkit.getScheduler().cancelTask(taskId);
    }
}
public class CinematicView {
    private Vector position;
    private float speed = 0.01F;

    public CinematicView(Location location) {
        this(location.toVector());
    }

    public CinematicView(Vector position) {
        this.position = position;
    }

    // Getters here
}

As you can see, the actual code is in the CinematicTask class, where it loads the current and the next locations and, every tick, calculates the next position using the lerp()function in line 110. You could even make this more powerful by adding text to each position, but that's up to you.

You can now create your own Cinematic class, and initialize the cinematic for a Player using:

new CinematicTask(plugin, player, this).start();

Sound

The resource pack system isn't limited to textures! Sounds are also implemented with this system, and they don't have many limitations. The only sound format Minecraft supports is .ogg(Wiki page), but there are plenty of converters online.

Again, this is a client side modification, and as so, we can't do anything about it. Although one thing that should receive a mention is that you can add custom .ogg files and then play them from the server specifying the folder structure as so:

player.playSound("folder.structure.file");

Animation

Creating animations is hard, especially because there aren't many tools available to automate things. There are two kind of animations we will focus on:

  • Item animations
  • Entity animations

The first uses the resource pack system explained above. It simply uses frame based animations and Cubik Creator is also compatible with this kind of assets.

The second is the most difficult one to achieve. Whether you are trying to move a wheel or sit down, you will need to use Armor stands, a powerful entity added in 1.8 that allow us to push the limits of the "voxelness" of Minecraft. Here are some examples:

Target minigame in Epicube

The Bukkit API provides powerful methods and allow us to create cool animations using Euler angles. Let's try to spawn some swords and a delivery man opening a special chest:

public class SwordAnimation {
    // Config variables
    private static final float SEPARATOR = 2;
    private static final float RAD_PER_SEC = 1.5F;
    private static final float RAD_PER_TICK = RAD_PER_SEC / 20F;

    private Location center;
    private double radius;

    private List<ArmorStand> swords;

    public SwordAnimation(Location center, double radius) {
        this.center = center;
        this.radius = radius;
        swords = Lists.newArrayList();
    }

    public void start(JavaPlugin plugin) {
        for (double angle = 0; angle < Math.PI * 2; angle += SEPARATOR) {
            spawnStand(angle);
        }

        new BukkitRunnable(){
            int tick = 0;

            public void run() {
                ++tick;

                for (int i = 0; i < swords.size(); i++) {
                    ArmorStand stand = swords.get(i);
                    Location loc = getLocationInCircle(RAD_PER_TICK * tick + SEPARATOR * i);

                    // Entity falling bug
                    stand.setVelocity(new Vector(1, 0, 0));

                    stand.teleport(loc);
                }
            }
        }.runTaskTimer(plugin, 0L, 1L);
    }

    private void spawnStand(double angle) {
        Location loc = getLocationInCircle(angle);

        ArmorStand stand = loc.getWorld().spawn(loc, ArmorStand.class);

        // TODO Customize by hiding, adding items, rotation...

        swords.add(stand);
    }

    private Location getLocationInCircle(double angle) {
        double x = center.getX() + radius * Math.cos(angle);
        double z = center.getZ() + radius * Math.sin(angle);

        return new Location(center.getWorld(), x, center.getY(), z);
    }
}

And here's the result after adding rotation, hiding the armor stands and creating the delivery man: (This was created 5 months ago)

(YouTube video)

Artificial Intelligence

The internal Minecraft server contains a powerful AI library used with Creatures (an internal class that represent mobs) that allows you to add custom Pathfinder goals that define how the mob will behave. Let's try to create a mob that will follow the nearest player.

We will need to use NMS in this case, and I recommend you to create the classes in this Spigot tutorial before proceding, as I will assume you already have them.

The type of goal we will need is the PathfinderGoalFollow. We can now proceed:

public class Follower extends EntityZombie {
    public Follower(World world) {
        super(((CraftWorld) world).getHandle());

        // TODO Get the fields as in the tutorial

        // Follow entities at 1.2 speed
        goalSelector.a(0, new PathfinderGoalFollow(this, 1.2, false));

        // Look at player from 8 blocks
        this.goalSelector.a(1, new PathfinderGoalLookAtPlayer(this, EntityHuman.class, 8.0F));
    }
}

Ocelots are creatures that implement this goal

As you have noticed, this isn't even a quarter of the required code to create a custom entity (that's why I included the Spigot tutorial). But that's why we are here, to create an API over the overcomplicated NMS code required for this to work. Minecraft gives us lots of premade pathfinder goals, but you can always create your own. This gives us infinite possibilities to control the movement of entities.

Physics

We all love Minecraft generation, don't we?

This one is the last, and probably the most fun. Every game engine has gravity, hitboxes and movement properties. Since Minecraft 1.9, hitboxes are actually solid, so they prevent two players from sticking together. That's some job already done for us, but let's see how to implement gravity components such as -9.81m/s^2 (the gravitational acceleration in Earth) and 4m/s^2(negative gravity, also known as Anti-gravity) which is impossible in our current model of the Universe; but who cares, this is Minecraft.

We all know that Minecraft doesn't respect gravity rules for placed blocks (it does for entities), so we will have to override the block placing and create a falling block entity with the placed block texture:

public class Gravity implements Listener {
    private static final Vector GRAVITY = new Vector(0, -9.81F, 0);

    private JavaPlugin plugin;

    public Gravity(JavaPlugin plugin) {
        this.plugin = plugin;
        plugin.getServer().getPluginManager().registerEvents(this, plugin);
    }

    @EventHandler
    public void onPlace(BlockPlaceEvent e) {
        if (e.isCancelled() || !e.canBuild()) {
            return;
        }

        e.setCancelled(true);

        Block block = e.getBlockPlaced();
        Location loc = e.getBlock().getLocation();

        MaterialData data = new MaterialData(block.getType(), block.getData());

        FallingBlock entity = loc.getWorld().spawnFallingBlock(loc, data);
        entity.setVelocity(GRAVITY);
    }
}

That one was easy! We just prevent the BlockPlaceEvent (this can cause issues if we aren't running the server locally), and create a FallingBlockwith the same data as the placed block. You should also remove the block from the player's hand.

Final notes

Woah! You made it to the end. I hope you all learned what things game engines should implement to make the life of game developers easier and how to implement some of these techniques in Minecraft. I know you were waiting for this word the entire post, so here you go: Gameception :video_game:

As you have seen, some things cannot be implemented server-side, but being able to create a custom physics engine in under 20 lines of code is awesome! I implemented all of this ideas in a Game API months ago when I was trying to make a Battlefield realistic server based game in Minecraft, but I lost the motivation on this project, but I got a bit bored of it.

You can find all the (complete) code used in this tutorial in this GitHub repo.

Special thanks to @SantiTR_ for this post idea, make sure to check out his work.

© 2019 Hugo Manrique