splatteroids game preview

Pure JavaScript Asteroids Clone with Enemy Ships Source Code

There are many acceptable JavaScript game engines out nowadays, but often you can get good performance from writing your own simple engine or renderer depending on your use case. The code for this project will be on my GitHub linked below.

What goes into writing a game engine?

Ideally, we want to handle a few important things.

  1. States, whether that be states of objects (alive, dead, moving, the type of enemy)
  2. Rendering
  3. Spawnable objects (with previously mentioned states)
  4. Input
  5. Save data

We approach this task with an object-oriented mindset instead of a functional programming mindset. Although there are a few global variables such as the overall running game state or the object pool arrays, most of the memory or information we need to remember occurs on a per-object basis.

We will be using a ‘Canvas‘ to draw our simple asteroid graphics. Writing a 3d renderer in JS is a much more complex task, although libraries like threeJS exist to get you started.

To begin with, we want to define a Vector2D class that we can reuse throughout our game. I’m familiar with Unity so I imagine an implementation similar to their engine’s GameObject setup, but any class that can read / write an X and Y will work.

var Vec2D = (function() {
var create = function(x, y) {
        var obj = Object.create(def);
        obj.setXY(x, y);

        return obj;
    };

    var def = {
        _x: 1,
        _y: 0,

        getX: function() {
            return this._x;
        },

        setX: function(value) {
            this._x = value;
        },

        getY: function() {
            return this._y;
        },

        setY: function(value) {
            this._y = value;
        },

        setXY: function(x, y) {
            this._x = x;
            this._y = y;
        },

        getLength: function() {
            return Math.sqrt(this._x * this._x + this._y * this._y);
        },

        setLength: function(length) {
            var angle = this.getAngle();
            this._x = Math.cos(angle) * length;
            this._y = Math.sin(angle) * length;
        },

        getAngle: function() {
            return Math.atan2(this._y, this._x);
        },

        setAngle: function(angle) {
            var length = this.getLength();
            this._x = Math.cos(angle) * length;
            this._y = Math.sin(angle) * length;
        },

        add: function(vector) {
            this._x += vector.getX();
            this._y += vector.getY();
        },

        sub: function(vector) {
            this._x -= vector.getX();
            this._y -= vector.getY();
        },

        mul: function(value) {
            this._x *= value;
            this._y *= value;
        },

        div: function(value) {
            this._x /= value;
            this._y /= value;
        }
    };

    return {
        create: create
    };
}());       

This will allow us to reference positions easier. It’s vital to implement a few capabilities for our renderer. One important need is to be able to draw an object to our canvas at a specified position, and have the capability to clear said canvas, preparing for the next frame the game renders.

To draw a line, we can write JavaScript such as:

var c = document.getElementById("canvas");
var ctx = c.getContext("2d");
ctx.moveTo(0, 0);
ctx.lineTo(200, 100);
ctx.stroke();

And if we wanted to clear our canvas, we can use clearRect:

ctx.clearRect(0, 0, canvas.width, canvas.height);

We can define a render function to handle our different objects.

window.getAnimationFrame =
    window.requestAnimationFrame ||
    window.webkitRequestAnimationFrame ||
    window.mozRequestAnimationFrame ||
    window.oRequestAnimationFrame ||
    window.msRequestAnimationFrame ||
function(callback) {
    window.setTimeout(callback, 16.6);
};
render(){
    context.clearRect(0,0,screenWidth,screenHeight);
    renderShips();
    renderAsteroids();
    renderBullets();
    getAnimationFrame(loop);
}

renderShips(){
    ship.renderSelf();
    for (int i = 0; i < enemies.length; i++)
    enemies.renderSelf();
}
...etc

Then an example render self function:

renderSelf: function() {
    if (this.hasDied)
        return;
    context.save();
    context.translate(this.pos.getX() >> 0, this.pos.getY() >> 0);
    context.rotate(this.angle);
    context.strokeStyle = playerColor;
    context.lineWidth = (Math.random() > 0.9) ? 4 : 2;
    context.beginPath();
    context.moveTo(10, 0);
    context.lineTo(-10, -10);
    context.lineTo(-10, 10);
    context.lineTo(10, 0);
    context.stroke();
    context.closePath();

    context.restore();
}

Which would render our object assuming a class holding some variables with our Vector2 class we described earlier.

var Ship = (function() {
var create = function(x, y, ref) {
    var obj = Object.create(def);
    obj.ref = ref;
    obj.angle = 0;
    obj.pos = Vec2D.create(x, y);
    obj.vel = Vec2D.create(0, 0);
    obj.thrust = Vec2D.create(0, 0);
    obj.invincible = false;
    obj.hasDied = false;
    obj.radius = 8;
    obj.idleDelay = 0;
    obj.isSpectating = false;

    return obj;
};
...etc

We are handling rendering and state management from inside an object now. All that just for a triangle.

player ship

We aren’t done yet. Next we need to handle Input. The goal with creating object classes is reusability and extensibility. We don’t need to spawn multiple instances of an input, so we can handle that globally. Your Input function may look something like this:

window.onkeydown = function(e) {
    switch (e.keyCode) {
        //key A or LEFT
        case 65:
        case 37:
            keyLeft = true;
            break;
            //key W or UP
        case 87:
        case 38:
            keyUp = true;
            break;
            //key D or RIGHT
        case 68:
        case 39:
            keyRight = true;
            break;
            //key S or DOWN
        case 83:
        case 40:
            keyDown = true;
            break;
            //key Space
        case 32:
        case 75:
            keySpace = true;
            break;
            //key Shift
        case 16:
            keyShift = true;
            break;
    }

    e.preventDefault();
};

window.onkeyup = function(e) {
    switch (e.keyCode) {
        //key A or LEFT
        case 65:
        case 37:
            keyLeft = false;
            break;
            //key W or UP
        case 87:
        case 38:
            keyUp = false;
            break;
            //key D or RIGHT
        case 68:
        case 39:
            keyRight = false;
            break;
            //key S or DOWN
        case 83:
        case 40:
            keyDown = false;
            break;
            //key Space
        case 75:
        case 32:
            keySpace = false;
            break;
            //key Shift
        case 16:
            keyShift = false;
            break;
    }

    e.preventDefault();
};

e.preventDefault() will stop users from accidentally hitting keys such as ctrl + L and losing focus from the window, or jumping the page with Space, for instance.

function updateShip() {
    ship.update();

    if (ship.hasDied) return;

    if (keySpace) ship.shoot();
    if (keyLeft && keyShift) ship.angle -= 0.1;
    else if (keyLeft) ship.angle -= 0.05;
    if (keyRight && keyShift) ship.angle += 0.1;
    else if (keyRight) ship.angle += 0.05;

    if (keyUp) {
        ship.thrust.setLength(0.1);
        ship.thrust.setAngle(ship.angle);
    } else {
        ship.vel.mul(0.94);
        ship.thrust.setLength(0);
    }

    if (ship.pos.getX() > screenWidth) ship.pos.setX(0);
    else if (ship.pos.getX() < 0) ship.pos.setX(screenWidth);

    if (ship.pos.getY() > screenHeight) ship.pos.setY(0);
    else if (ship.pos.getY() < 0) ship.pos.setY(screenHeight);
}

...etc

function checkDistanceCollision(obj1, obj2) {
    var vx = obj1.pos.getX() - obj2.pos.getX();
    var vy = obj1.pos.getY() - obj2.pos.getY();
    var vec = Vec2D.create(vx, vy);

    if (vec.getLength() < obj1.radius + obj2.radius) {
        return true;
    }

    return false;
}

...etc

Once we have the ability to render a reusable object to a canvas and read / write a position that can be checked, we use that as a template to create other objects (particles, asteroids, other ships).

hexagon asteroid
enemy ship example

You can make interesting graphics with just basic shapes. We handle collision by assigning either an xWidth and yWidth + xOffset and yOffset, OR a radius. This again would be assigned to the object itself to keep track of.

asteroids game example

Further Techniques

If we can control the rendering manually we can leave an ‘afterimage’ on our canvas before rendering the next frame as opposed to clearing it entirely. To do this, we can manipulate the canvas’ global alpha.

// Get the canvas element and its 2D rendering context
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
// Set the initial alpha value
let alpha = 0.1; // You can adjust this value to control the fading speed
// Function to create the afterimage effect
function createAfterimage() {
    // Set a semi-transparent color for the shapes
    ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`;
    // Fill a rectangle covering the entire canvas
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    // Decrease alpha for the next frame
    alpha *= 0.9; // You can adjust this multiplier for a different fade rate
    // Request animation frame to update
    requestAnimationFrame(createAfterimage);
}
// Call the function to start creating the afterimage effect
createAfterimage();

And a simple localStorage can be used to save scores.

function checkLocalScores() {
    if (localStorage.getItem("rocks") != null) {
        visualRocks = localStorage.getItem("rocks");
    }
    if (localStorage.getItem("deaths") != null) {
        visualDeaths = localStorage.getItem("deaths");
    }
    if (localStorage.getItem("enemyShips") != null) {
        visualEnemyShips = localStorage.getItem("enemyShips");
    }
    updateVisualStats();
}
function saveLocalScores() {
    localStorage.setItem("rocks", visualRocks);
    localStorage.setItem("deaths", visualDeaths);
    localStorage.setItem("enemyShips", visualEnemyShips);
}

End Result

You can see and play the game here.

Source code is here. ✨


It helps me if you share this post

Published 2023-11-30 23:51:07

One comment on “Pure JavaScript Asteroids Clone with Enemy Ships Source Code”

Leave a Reply

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