Making Geometry Dash with Renderspice


Geometry Dash is a great game, even with just its simple physics and gamemodes, it can be a very fun game to play. In this tutorial, I am only going to create 4 of the gamemodes.

Before we start coding, we need to actually know how Geometry Dash actually works. Geometry Dash is a basic game with simple to understand physics. The main gamemode is a cube, where you can only jump when on the ground, another gamemode that we will create is a ship, which flies through the air. The third gamemode we will be creating is a ball, which changes gravity direction when clicked. The fourth and final gamemode we will be attempting to create is the UFO, which is just like the cube, but can jump midair.

Now that we have the basics down we can start coding.


Setup

Assuming we have setup our HTML and CSS files, and added the script in, which can all be found in our documentation, we can get started.

In this part, I setup a simple environment with a player and the ground. I set the player to apply collisions, so that we would not fall through the ground.

const rs = new Renderspice('canvas');
rs.camera.background = "skyblue";
rs.camera.w = 25;
rs.camera.h = 25 / rs.aspectRatio;

let player = rs.box(-5, -3, 1, 1);
player.collisionBody = true;
player.fillColor = "green";
player.gravity = 30;

let ground = rs.box(0, -20.5, 50, 34);
ground.fillColor = "lightblue";

Main Parts

Now that we have our scene setup, we can start to work on the basic logic, for one, we need the player to move forward. We also need to move the ground with the player, so that the player does not fall over an edge. We can put this into preRender:

player.velocity.x = 6;

rs.preRender = () => {
    rs.camera.x = ground.x = player.x + 5;
}

Lets also add jumping into our preProcessing:

rs.preProcessing = () => {
    // Jumping
    let input = rs.keys[32].down || rs.mouse.down;
    if(input && player.collisionBody.colliding) {
        player.velocity.y += 12;
    }
}

Lets also make a simple game over function, that will write "Game over" in the middle of the screen.

function gameOver() {
    let text = rs.text("Game Over", rs.camera.x, rs.camera.y, "Segoe UI", 72);
    text.fillColor = "#800";
}

Spikes and Blocks

Now that we have the simple basics down, with a moving, jumping player, we can start work on the obstacles and level itself.

In Geometry Dash, there are 2 main elements, spikes and blocks. Blocks are the least deadliest, the player can jump on top of them and fall off of them and be fine, however when they run straight into a block, they die. Spikes are much deadlier, if the player touches them at all, they will die. Let's add this into our code.

player.onCollide = () => {
    // Check for spikes
    for(let id of player.collisionBody.colliding) {
        if(rs.getObject(id).shape === 'triangle') {
            gameOver();
        }
    }
}

Right now our onCollide function only checks for collisions with spikes, but what about head-on collisions with blocks?

This might seem like a challenging problem, but it becomes easier when we think about it critically. When we collide head-on with a block, the collision resolution will stop the player from moving, so if we can tell if the player has no velocity, we can know for certain that it is colliding with a block.

So all we need to do is check whether our player is still moving or not.

player.onCollide = () => {
    // Check for spikes
    for(let id of player.collisionBody.colliding) {
        if(rs.getObject(id).shape === 'triangle') {
            gameOver();
        }
    }

    // Check for head-on collisions
    if(player.velocity.x == 0) {
        gameOver();
    }
}

Now this is all we need to start creating levels.


Creating Levels

Instead of creating entire layouts for new levels, something that would take a very long time, I decided to come up with a simpler approach. I decided on a modular endless runner design. This takes a bunch of smaller layouts that can be used universally, and randomly select one to put in front of the player, making it seem like an endless layout. For this I decided to make 3 different layouts for a cube that were about 20 units long.

Here are the modules for copy and pasting.

let m1 = rs.tag('module1');
m1.add(
    rs.box(-10, -3, 2, 1),
    rs.triangle(-10.5, -2.1, 0.8, 0.8),
    rs.box(-9.5, -2, 1, 1),
    rs.triangle(-8.5, -3.25, 1, 0.5),
    rs.triangle(-7.5, -3.25, 1, 0.5),
    rs.box(-6, -3, 2, 1),
    rs.box(-4.5, 1, 2, 1),
    rs.triangle(-5, 0, 1, 1).rotate(Math.PI),
    rs.triangle(-4, 0.25, 1, 0.5).rotate(Math.PI),
    rs.box(4, -2.5, 2, 2),
    rs.triangle(2.5, -3, 1, 1),
    rs.box(9, -0.5, 1, 1),
    rs.triangle(5.5, -3.25, 1, 0.5),
    rs.triangle(6.5, -3.25, 1, 0.5),
    rs.triangle(7.5, -3.25, 1, 0.5),
    rs.triangle(8.5, -3.25, 1, 0.5),
);

let m2 = rs.tag('module2');
m2.add(
    rs.box(-9, -2.5, 1, 2),
    rs.box(-6, -3, 1, 1),
    rs.box(-3, -2, 1, 3),
    rs.box(0, -1, 1, 5),
    rs.box(3, -2, 1, 3),
    rs.box(6, -1, 1, 5),
    rs.box(9, -1.5, 1, 4),
    rs.triangle(9, 1, 1, 1)
)

let m3 = rs.tag('module3');
m3.add(
    rs.triangle(-9, -3, 1, 1),
    rs.triangle(-8, -3, 1, 1),
    rs.box(-7, -2.5, 1, 2),
    rs.triangle(-1, -3, 1, 1),
    rs.triangle(0, -3, 1, 1),
    rs.triangle(1, -3, 1, 1),
    rs.triangle(8, -3, 1, 1),
    rs.triangle(9, -3, 1, 1)
);

let modules = [m1, m2, m3];

Currently we have an array containing 3 tags. If we ran this right now, the objects would be directly in front of the player, something we don't want, so let's move them up by 100.

for(let module of modules) module.y = 100;

Now we are all set to add in modules.

To add a module, the player needs to be at a certain x position, so that we can duplicate the tag and put it some value in front of the player. Lets set this variable to nextLevel. We need to move the duplicated object to nextLevel plus the width of the camera, so that the camera cannot see the duplication. The next thing is to pick a random module, this can be done with Math.random().

Let's also add a lifespan to the tag, so that it will be removed once the player has passed it. We also need to reset the timeAlive value so that the tag doesn't get deleted when the other ones do.

The last thing we need is to have a distance between each duplication. Since each module is about 20 units wide, we should set the distance to be 30, so that there are 10 units of space between the end of the last module and the start of the next. We can put all of this code into rs.preProcessing.

let nextLevel = 0;

rs.preProcessing = () => {
    // Jumping
    let input = rs.keys[32].down || rs.mouse.down;
    if(input && player.collisionBody.colliding) {
        player.velocity.y += 12;
    }

    // Duplicating modules
    if(player.x > nextLevel) {
        let random = Math.floor(Math.random() * modules.length);
        let nextModule = modules[random].duplicate();
        nextModule.x += nextLevel + 30;
        nextModule.y = 0;
        nextModule.timeAlive = 0;
        nextModule.lifespan = 16;
        nextLevel += 30;
    }
}

Conclusion

This is all that we need to create Geometry Dash in Renderspice. The final thing to add before running the code is

rs.renderLoop();

This is our final product: Geometry Dash in Renderspice