javascripthtmlcanvascollision-detectionaabb

How to Resolve Axis Aligned Rectangle to Rectangle Bounding Box Collision in Javascript?


I've been working on a game recently where the game has walls that the player and other entities can collide with, but the only problem I have is that I don't know how to resolve Rectangle to Rectangle collision.

Here is the code:

class Mouse {
    constructor() {
        throw new Error(`new Mouse() is not allowed.\nTry using the Mouse.init() method instead.`);
    }

    static x = 0;
    static y = 0;

    static movement = {
        x: 0,
        y: 0
    }

    static pressed = false;

    static setMousePosition(e) {
        this.x = e.pageX;
        this.y = e.pageY;
        this.movement.x = e.movementX;
        this.movement.y = e.movementY;
    }

    static init() {
        window.addEventListener("mousedown", (e) => { this.setMousePosition(e); this.pressed = true; });
        window.addEventListener("mouseup", (e) => { this.pressed = false; });
        window.addEventListener("mousemove", (e) => { this.setMousePosition(e); });
    }
}

/**
 * @description A set of helper functions to make drawing on a 2d canvas easier.
 */
class Draw {
    /**
     * @param context The canvas context to use for drawing.
     */
    constructor(context) {
        this.ctx = context;
    }

    /**
     * @description Clears the specified rectangular area, making it fully transparent.
     */
    clear(x, y, width, height) {
        this.ctx.clearRect(x, y, width, height);
    }

    rectangle(x, y, width, height, roundness = 0, fill = true, stroke = false, options = {}) {
        this.ctx.save();
        Object.assign(this.ctx, options);
        this.ctx.beginPath();
        this.ctx.roundRect(x, y, width, height, roundness);
        if (fill) this.ctx.fill();
        if (stroke) this.ctx.stroke();
        this.ctx.closePath();
        this.ctx.restore();
    }

    arc(x, y, radius, a1, a2 = Math.PI * 2, fill = true, stroke = false, options = {}, counterClockwise = false) {
        this.ctx.save();
        Object.assign(this.ctx, options);
        this.ctx.beginPath();
        this.ctx.arc(x, y, radius, a1, a2, counterClockwise);
        if (fill) this.ctx.fill();
        if (stroke) this.ctx.stroke();
        this.ctx.closePath();
        this.ctx.restore();
    }

    text(text, x, y, fill = true, stroke = false, options = {}, maxWidth = undefined) {
        this.ctx.save();
        Object.assign(this.ctx, options);
        if (fill) this.ctx.fillText(text, x, y, maxWidth);
        if (stroke) this.ctx.strokeText(text, x, y, maxWidth);
        this.ctx.restore();
    }

    path(path, fill = false, stroke = true, options = {}) {
        this.ctx.save();
        Object.assign(this.ctx, options);
        this.ctx.beginPath();
        if (fill) this.ctx.fill(path);
        if (stroke) this.ctx.stroke(path);
        this.ctx.closePath();
        this.ctx.restore();
    }

    grid(x, y, width, height, cellSize, options = {}) {
        this.ctx.save();
        Object.assign(this.ctx, options);
        this.ctx.beginPath();

        for (var cx = x; cx <= x + width; cx += cellSize) {
            this.ctx.moveTo(cx, y);
            this.ctx.lineTo(cx, y + height);
        }

        for (var cy = y; cy <= y + height; cy += cellSize) {
            this.ctx.moveTo(x, cy);
            this.ctx.lineTo(x + width, cy);
        }

        this.ctx.stroke();
        this.ctx.closePath();
        this.ctx.restore();
    }

    text(text, x, y, fill = true, stroke = false, options = {}) {
        this.ctx.save();
        Object.assign(this.ctx, options);
        if (fill) this.ctx.fillText(text, x, y);
        if (fill) this.ctx.strokeText(text, x, y);
        this.ctx.restore();
    }
}

function random(min, max) {
    return Math.random() * (max - min) + min;
}

function degreesToRadians(degrees) {
    return degrees * Math.PI / 180;
}

var tankClass = {
    basic: function (tank) {
        return [
            new Gun(0, -tank.height * 0.15, tank.width * 0.9, tank.height * 0.3, tank, 0)
        ];
    },

    doubleShot: function (tank) {
        return [
            new Gun(0, -tank.height * 0.12, tank.width * 0.9, tank.height * 0.24, tank, -4),
            new Gun(0, -tank.height * 0.12, tank.width * 0.9, tank.height * 0.24, tank, 4),
            new Gun(0, -tank.height * 0.15, tank.width * 0.9, tank.height * 0.3, tank, 0)
        ];
    }
}

class Player {
    constructor(x, y, width, height, color, bc, startingWeapons = "basic") {
        this.initX = x;
        this.initY = y;
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
        this.color = color;
        this.bc = bc;
        this.velX = 0;
        this.velY = 0;
        this.acceleration = 0.3;
        this.gunAngle = 0;
        this.weapons = this.setWeapons(startingWeapons);
        this.bullets = [];
        this.currentReloadTime = 0;
        this.reloadTime = 60;
        this.recoilX = 0;
        this.safeZone = {
            x: x - 100,
            y: y - 100,
            width: width + 100,
            height: height + 100
        }
    }

    setWeapons(weaponString) {
        var newWeapons = tankClass[weaponString];
        return newWeapons(this);
    }

    reset() {
        this.x = this.initX;
        this.y = this.initY;
        this.velX = 0;
        this.velY = 0;
        this.gunAngle = 0;
    }

    setSafeZone(x, y, width, height) {
        this.safeZone.x = x;
        this.safeZone.y = y;
        this.safeZone.width = width;
        this.safeZone.height = height;
    }
}

class Enemy {
    constructor(x, y, width, height, color, bc, startingWeapons = "basic") {
        this.initX = x;
        this.initY = y;
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
        this.color = color;
        this.bc = bc;
        this.velX = 0;
        this.velY = 0;
        this.gunAngle = 0;
        this.weapons = this.setWeapons(startingWeapons);
        this.bullets = [];
        this.currentReloadTime = 0;
        this.reloadTime = 60;
        this.recoilX = 0;
    }

    setWeapons(weaponString) {
        var newWeapons = tankClass[weaponString];
        return newWeapons(this);
    }

    reset() {
        this.x = this.initX;
        this.y = this.initY;
        this.velX = 0;
        this.velY = 0;
        this.gunAngle = 0;
    }
}

class Gun {
    constructor(x, y, width, height, parent, rotation = 0) {
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
        this.parent = parent;
        this.color = "#606060";
        this.bc = "#404040";
        this.rotation = degreesToRadians(rotation);
    }
}

class Bullet {
    constructor(x, y, radius, color, bc, parent, velX, velY) {
        this.x = x;
        this.y = y;
        this.radius = radius;
        this.color = color;
        this.bc = bc;
        this.parent = parent;
        this.velX = velX;
        this.velY = velY;
    }
}

var levels = {
    level0: {
        map: [
            [0, 0, 1, 1, 0, 0],
            [0, 0, 1, 0, 0, 0]
        ]
    }
}

class Info_Level {
    constructor(gridSize = 64) {
        this.GRID_SIZE = gridSize;

        this.map = {
            walls: []
        };
    }

    load(map) {
        var tileOffsetX = 0;
        var tileOffsetY = 0;
    
        for (var i = 0; i < map.length; i++) {
            for (var j = 0; j < map[i].length; j++) {
                if (map[i][j] === 1) {
                    this.createWall(tileOffsetX, tileOffsetY, this.GRID_SIZE, this.GRID_SIZE, "#000000");
                }
    
                if (map[i][j] === 2) {
                    this.createWall(tileOffsetX, tileOffsetY, this.GRID_SIZE, this.GRID_SIZE, "#000000");
                }
    
                tileOffsetX += this.GRID_SIZE;
            }
    
            tileOffsetX = 0;
            tileOffsetY += this.GRID_SIZE;
        }
    
        tileOffsetY = 0;
        tileOffsetX = 0;
    }

    createWall(x, y, width, height, color, id = "") {
        this.map.walls.push({
            x: x,
            y: y,
            width: width,
            height: height,
            color: color,
            id: id
        });
    }
}

var infoLevel = new Info_Level(64);

infoLevel.load(levels.level0.map);

var friction = 0.85;

var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");

var vWidth = window.innerWidth;
var vHeight = window.innerHeight;

var player = new Player(vWidth / 2 - 33, vHeight / 2 - 27.5, 75, 60, "#608060", "#204020", "basic");

var keysDown = [];

var enemies = [];

// enemies.push(new Enemy(0, 0, 75, 60, "#806060", "#402020", "basic"));

var draw = new Draw(ctx);

var fps = 60;

function resizeCanvas(canvasElement, width, height) {
    vWidth = width;
    vHeight = height;
    canvasElement.width = vWidth;
    canvasElement.height = vHeight;
}

resizeCanvas(canvas, window.innerWidth, window.innerHeight);

function updateTank(tank, isEnemy = false) {
    if (tank.currentReloadTime >= 0) {
        tank.currentReloadTime--;
    }

    if (tank.recoilX < 0) {
        tank.recoilX += 0.5;
    }

    if (tank.x < 0) {
        tank.x = 0;
    }

    if (tank.y < 0) {
        tank.y = 0;
    }

    if (tank.x + tank.width > vWidth) {
        tank.x = vWidth - tank.width;
    }

    if (tank.y + tank.height > vHeight) {
        tank.y = vHeight - tank.height;
    }

    tank.velX *= friction;
    tank.velY *= friction;

    tank.x += tank.velX;
    tank.y += tank.velY;

    /* This is where the collision detection of the player and a wall happen. */
    for (var i = 0; i < infoLevel.map.walls.length; i++) {
        var wall = infoLevel.map.walls[i];
        if (rectangleToRectangleCollision(tank, wall)) {
            tank.velX = 0;
            tank.velY = 0;
        }
    }

    if (isEnemy == true) {
        var enemyAV = Math.atan2((player.y + player.height / 2) - (tank.y + tank.height / 2), (player.x + player.width / 2) - (tank.x + tank.width / 2));
        tank.gunAngle = enemyAV;
        if (rectangleToRectangleCollision(player, tank) == false) {
            tank.velX += Math.cos(enemyAV) * 0.3;
            tank.velY += Math.sin(enemyAV) * 0.3;
        }

        if (tank.currentReloadTime <= 0) {
            for (var i = 0; i < tank.weapons.length; i++) {
                var gun = tank.weapons[i];
                shootBullet(gun, tank);
            }

            tank.currentReloadTime = tank.reloadTime;
        }
    }

    ctx.save();
    ctx.translate(tank.x + tank.width / 2, tank.y + tank.height / 2);
    ctx.rotate(Math.atan2(tank.velY / 2, tank.velX / 2));
    draw.rectangle(-tank.width / 2, -tank.height / 2, tank.width, tank.height, 2, true, true, { fillStyle: tank.color, strokeStyle: tank.bc, lineWidth: tank.width / tank.height * 1.75 });
    ctx.restore();

    for (var i = 0; i < tank.bullets.length; i++) {
        var bullet = tank.bullets[i];

        bullet.x += bullet.velX;
        bullet.y += bullet.velY;

        draw.arc(bullet.x, bullet.y, bullet.radius, 0, 2 * Math.PI, true, true, { fillStyle: bullet.color, strokeStyle: bullet.bc, lineWidth: tank.width / tank.height * 1.75 });
    }

    for (var i = 0; i < tank.weapons.length; i++) {
        var gun = tank.weapons[i];

        if (gun.x < 0) {
            gun.x += gun.width / 240;
        }

        ctx.save();
        ctx.translate(tank.x + tank.width / 2, tank.y + tank.height / 2);
        ctx.rotate(tank.gunAngle + gun.rotation);
        draw.rectangle(gun.x, gun.y, gun.width, gun.height, 2, true, true, { fillStyle: gun.color, strokeStyle: gun.bc, lineWidth: tank.width / tank.height * 1.75 });
        ctx.restore();
    }

    ctx.save();
    ctx.translate(tank.x + tank.width / 2, tank.y + tank.height / 2);
    ctx.rotate(tank.gunAngle);
    draw.rectangle(-tank.width / 2 * 0.6 + tank.recoilX, -tank.height / 2 * 0.7, tank.width * 0.6, tank.height * 0.7, 2, true, true, { fillStyle: tank.color, strokeStyle: tank.bc, lineWidth: tank.width / tank.height * 1.75 });
    ctx.restore();
}

function shootBullet(gun, tank) {
    var shootS = new Audio("./assets/shoot.wav");
    shootS.play();
    gun.x -= gun.width / 12;
    tank.recoilX = -tank.width / 16;
    var rawVX = Math.cos(tank.gunAngle + gun.rotation);
    var rawVY = Math.sin(tank.gunAngle + gun.rotation);
    var velX = (rawVX + random(-0.02, 0.02)) * 5;
    var velY = (rawVY + random(-0.02, 0.02)) * 5;
    tank.bullets.push(new Bullet(tank.x + tank.width / 2 + (rawVX * (tank.width - (tank.height / 2))), tank.y + tank.height / 2 + (rawVY * (tank.width - (tank.height / 2))), gun.height / 2, "#ff0000", "#800000", tank, velX, velY));
}

function main() {
    if (keysDown["w"]) {
        player.velY -= player.acceleration;
    }

    if (keysDown["a"]) {
        player.velX -= player.acceleration;
    }

    if (keysDown["s"]) {
        player.velY += player.acceleration;
    }

    if (keysDown["d"]) {
        player.velX += player.acceleration;
    }

    if (Mouse.pressed) {
        if (player.currentReloadTime <= 0) {
            for (var i = 0; i < player.weapons.length; i++) {
                var gun = player.weapons[i];
                shootBullet(gun, player);
            }
            player.currentReloadTime = player.reloadTime;
        }
    }

    player.setSafeZone(player.x + player.width / 2 - 100, player.y + player.height / 2 - 100, 200, 200);


    ctx.save();
    draw.clear(0, 0, vWidth, vHeight);

    updateTank(player, false);

    for (var i = 0; i < enemies.length; i++) {
        updateTank(enemies[i], true);
    }

    for (var i = 0; i < infoLevel.map.walls.length; i++) {
        var wall = infoLevel.map.walls[i];
        draw.rectangle(wall.x, wall.y, wall.width, wall.height, 0, true, false, { fillStyle: wall.color });
    }

    draw.text("Add Collision Resolution To These Black Boxes", 0, 10, true, true, { textBaseline: "top", textAlign: "left", fillStyle: "#ffffff", strokeStyle: "#000000", font: "Bold 30px Arial" });

    ctx.restore();
}

window.onload = function () {
    Mouse.init();
    setInterval(main, 1000 / fps);
}

function rectangleToRectangleCollision(obj1, obj2) {
    if (obj1.x + obj1.width > obj2.x && obj1.y + obj1.height > obj2.y
        && obj1.x < obj2.x + obj2.width && obj1.y < obj2.y + obj2.height) {
        return true;
    }

    return false;
}

window.onresize = function () {
    resizeCanvas(canvas, window.innerWidth, window.innerHeight);
}

document.addEventListener("keydown", (e) => {
    keysDown[e.key] = true;
});

document.addEventListener("keyup", (e) => {
    keysDown[e.key] = false;
});

document.addEventListener("mousemove", () => {
    player.gunAngle = Math.atan2(Mouse.y - (player.y + player.height / 2), Mouse.x - (player.x + player.width / 2));
});
*, *:before, *:after {
    font-family: roboto, Arial, Helvetica, sans-serif, system-ui;
    padding: 0px 0px;
    margin: 0px 0px;
    box-sizing: border-box;
}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>CD</title>
</head>
<body>
    <canvas id="canvas"></canvas>
</body>
</html>
WASD to move, click to shoot.

Note: I am not using any JavaScript libraries or plugins, as I like to make things from scratch.


Solution

  • Turns out this wasn't as hard as I thought this was, so I figured it out on my own.

    Here is the code:

    function rectangleToRectangleCollisionResolution(obj1, obj2) {
      var vx = (obj1.x + obj1.width / 2) - (obj2.x + obj2.width / 2);
      var vy = (obj1.y + obj1.height / 2) - (obj2.y + obj2.height / 2);
    
      if (Math.abs(vx / obj2.width) > Math.abs(vy / obj2.height)) {
        if (vx < 0) {
          obj1.x = obj2.x - obj1.width;
        } else {
          obj1.x = obj2.x + obj2.width;
        }
      } else {
        if (vy < 0) {
          obj1.y = obj2.y - obj1.height;
        } else {
          obj1.y = obj2.y + obj2.height;
        }
      }
    }
    
    class Mouse {
      constructor() {
        throw new Error(`new Mouse() is not allowed.\nTry using the Mouse.init() method instead.`);
      }
    
      static x = 0;
      static y = 0;
    
      static movement = {
        x: 0,
        y: 0
      }
    
      static pressed = false;
    
      static setMousePosition(e) {
        this.x = e.pageX;
        this.y = e.pageY;
        this.movement.x = e.movementX;
        this.movement.y = e.movementY;
      }
    
      static init() {
        window.addEventListener("mousedown", (e) => {
          this.setMousePosition(e);
          this.pressed = true;
        });
        window.addEventListener("mouseup", (e) => {
          this.pressed = false;
        });
        window.addEventListener("mousemove", (e) => {
          this.setMousePosition(e);
        });
      }
    }
    
    /**
     * @description A set of helper functions to make drawing on a 2d canvas easier.
     */
    class Draw {
      /**
       * @param context The canvas context to use for drawing.
       */
      constructor(context) {
        this.ctx = context;
      }
    
      /**
       * @description Clears the specified rectangular area, making it fully transparent.
       */
      clear(x, y, width, height) {
        this.ctx.clearRect(x, y, width, height);
      }
    
      rectangle(x, y, width, height, roundness = 0, fill = true, stroke = false, options = {}) {
        this.ctx.save();
        Object.assign(this.ctx, options);
        this.ctx.beginPath();
        this.ctx.roundRect(x, y, width, height, roundness);
        if (fill) this.ctx.fill();
        if (stroke) this.ctx.stroke();
        this.ctx.closePath();
        this.ctx.restore();
      }
    
      arc(x, y, radius, a1, a2 = Math.PI * 2, fill = true, stroke = false, options = {}, counterClockwise = false) {
        this.ctx.save();
        Object.assign(this.ctx, options);
        this.ctx.beginPath();
        this.ctx.arc(x, y, radius, a1, a2, counterClockwise);
        if (fill) this.ctx.fill();
        if (stroke) this.ctx.stroke();
        this.ctx.closePath();
        this.ctx.restore();
      }
    
      text(text, x, y, fill = true, stroke = false, options = {}, maxWidth = undefined) {
        this.ctx.save();
        Object.assign(this.ctx, options);
        if (fill) this.ctx.fillText(text, x, y, maxWidth);
        if (stroke) this.ctx.strokeText(text, x, y, maxWidth);
        this.ctx.restore();
      }
    
      path(path, fill = false, stroke = true, options = {}) {
        this.ctx.save();
        Object.assign(this.ctx, options);
        this.ctx.beginPath();
        if (fill) this.ctx.fill(path);
        if (stroke) this.ctx.stroke(path);
        this.ctx.closePath();
        this.ctx.restore();
      }
    
      grid(x, y, width, height, cellSize, options = {}) {
        this.ctx.save();
        Object.assign(this.ctx, options);
        this.ctx.beginPath();
    
        for (var cx = x; cx <= x + width; cx += cellSize) {
          this.ctx.moveTo(cx, y);
          this.ctx.lineTo(cx, y + height);
        }
    
        for (var cy = y; cy <= y + height; cy += cellSize) {
          this.ctx.moveTo(x, cy);
          this.ctx.lineTo(x + width, cy);
        }
    
        this.ctx.stroke();
        this.ctx.closePath();
        this.ctx.restore();
      }
    
      text(text, x, y, fill = true, stroke = false, options = {}) {
        this.ctx.save();
        Object.assign(this.ctx, options);
        if (fill) this.ctx.fillText(text, x, y);
        if (fill) this.ctx.strokeText(text, x, y);
        this.ctx.restore();
      }
    }
    
    function random(min, max) {
      return Math.random() * (max - min) + min;
    }
    
    function degreesToRadians(degrees) {
      return degrees * Math.PI / 180;
    }
    
    var tankClass = {
      basic: function(tank) {
        return [
          new Gun(0, -tank.height * 0.15, tank.width * 0.9, tank.height * 0.3, tank, 0)
        ];
      },
    
      doubleShot: function(tank) {
        return [
          new Gun(0, -tank.height * 0.12, tank.width * 0.9, tank.height * 0.24, tank, -4),
          new Gun(0, -tank.height * 0.12, tank.width * 0.9, tank.height * 0.24, tank, 4),
          new Gun(0, -tank.height * 0.15, tank.width * 0.9, tank.height * 0.3, tank, 0)
        ];
      }
    }
    
    class Player {
      constructor(x, y, width, height, color, bc, startingWeapons = "basic") {
        this.initX = x;
        this.initY = y;
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
        this.color = color;
        this.bc = bc;
        this.velX = 0;
        this.velY = 0;
        this.acceleration = 0.3;
        this.gunAngle = 0;
        this.weapons = this.setWeapons(startingWeapons);
        this.bullets = [];
        this.currentReloadTime = 0;
        this.reloadTime = 60;
        this.recoilX = 0;
        this.safeZone = {
          x: x - 100,
          y: y - 100,
          width: width + 100,
          height: height + 100
        }
      }
    
      setWeapons(weaponString) {
        var newWeapons = tankClass[weaponString];
        return newWeapons(this);
      }
    
      reset() {
        this.x = this.initX;
        this.y = this.initY;
        this.velX = 0;
        this.velY = 0;
        this.gunAngle = 0;
      }
    
      setSafeZone(x, y, width, height) {
        this.safeZone.x = x;
        this.safeZone.y = y;
        this.safeZone.width = width;
        this.safeZone.height = height;
      }
    }
    
    class Enemy {
      constructor(x, y, width, height, color, bc, startingWeapons = "basic") {
        this.initX = x;
        this.initY = y;
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
        this.color = color;
        this.bc = bc;
        this.velX = 0;
        this.velY = 0;
        this.gunAngle = 0;
        this.weapons = this.setWeapons(startingWeapons);
        this.bullets = [];
        this.currentReloadTime = 0;
        this.reloadTime = 60;
        this.recoilX = 0;
      }
    
      setWeapons(weaponString) {
        var newWeapons = tankClass[weaponString];
        return newWeapons(this);
      }
    
      reset() {
        this.x = this.initX;
        this.y = this.initY;
        this.velX = 0;
        this.velY = 0;
        this.gunAngle = 0;
      }
    }
    
    class Gun {
      constructor(x, y, width, height, parent, rotation = 0) {
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
        this.parent = parent;
        this.color = "#606060";
        this.bc = "#404040";
        this.rotation = degreesToRadians(rotation);
      }
    }
    
    class Bullet {
      constructor(x, y, radius, color, bc, parent, velX, velY) {
        this.x = x;
        this.y = y;
        this.radius = radius;
        this.color = color;
        this.bc = bc;
        this.parent = parent;
        this.velX = velX;
        this.velY = velY;
      }
    }
    
    var levels = {
      level0: {
        map: [
          [0, 0, 1, 1, 0, 0],
          [0, 0, 1, 0, 0, 0]
        ]
      }
    }
    
    class Info_Level {
      constructor(gridSize = 64) {
        this.GRID_SIZE = gridSize;
    
        this.map = {
          walls: []
        };
      }
    
      load(map) {
        var tileOffsetX = 0;
        var tileOffsetY = 0;
    
        for (var i = 0; i < map.length; i++) {
          for (var j = 0; j < map[i].length; j++) {
            if (map[i][j] === 1) {
              this.createWall(tileOffsetX, tileOffsetY, this.GRID_SIZE, this.GRID_SIZE, "#000000");
            }
    
            if (map[i][j] === 2) {
              this.createWall(tileOffsetX, tileOffsetY, this.GRID_SIZE, this.GRID_SIZE, "#000000");
            }
    
            tileOffsetX += this.GRID_SIZE;
          }
    
          tileOffsetX = 0;
          tileOffsetY += this.GRID_SIZE;
        }
    
        tileOffsetY = 0;
        tileOffsetX = 0;
      }
    
      createWall(x, y, width, height, color, id = "") {
        this.map.walls.push({
          x: x,
          y: y,
          width: width,
          height: height,
          color: color,
          id: id
        });
      }
    }
    
    var infoLevel = new Info_Level(64);
    
    infoLevel.load(levels.level0.map);
    
    var friction = 0.85;
    
    var canvas = document.getElementById("canvas");
    var ctx = canvas.getContext("2d");
    
    var vWidth = window.innerWidth;
    var vHeight = window.innerHeight;
    
    var player = new Player(vWidth / 2 - 33, vHeight / 2 - 27.5, 75, 60, "#608060", "#204020", "basic");
    
    var keysDown = [];
    
    var enemies = [];
    
    // enemies.push(new Enemy(0, 0, 75, 60, "#806060", "#402020", "basic"));
    
    var draw = new Draw(ctx);
    
    var fps = 60;
    
    function resizeCanvas(canvasElement, width, height) {
      vWidth = width;
      vHeight = height;
      canvasElement.width = vWidth;
      canvasElement.height = vHeight;
    }
    
    resizeCanvas(canvas, window.innerWidth, window.innerHeight);
    
    function updateTank(tank, isEnemy = false) {
      if (tank.currentReloadTime >= 0) {
        tank.currentReloadTime--;
      }
    
      if (tank.recoilX < 0) {
        tank.recoilX += 0.5;
      }
    
      if (tank.x < 0) {
        tank.x = 0;
      }
    
      if (tank.y < 0) {
        tank.y = 0;
      }
    
      if (tank.x + tank.width > vWidth) {
        tank.x = vWidth - tank.width;
      }
    
      if (tank.y + tank.height > vHeight) {
        tank.y = vHeight - tank.height;
      }
    
      tank.velX *= friction;
      tank.velY *= friction;
    
      tank.x += tank.velX;
      tank.y += tank.velY;
    
      for (var i = 0; i < infoLevel.map.walls.length; i++) {
        var wall = infoLevel.map.walls[i];
        if (rectangleToRectangleCollision(tank, wall)) {
          rectangleToRectangleCollisionResolution(tank, wall);
        }
      }
    
      if (isEnemy == true) {
        var enemyAV = Math.atan2((player.y + player.height / 2) - (tank.y + tank.height / 2), (player.x + player.width / 2) - (tank.x + tank.width / 2));
        tank.gunAngle = enemyAV;
        if (rectangleToRectangleCollision(player, tank) == false) {
          tank.velX += Math.cos(enemyAV) * 0.3;
          tank.velY += Math.sin(enemyAV) * 0.3;
        }
    
        if (tank.currentReloadTime <= 0) {
          for (var i = 0; i < tank.weapons.length; i++) {
            var gun = tank.weapons[i];
            shootBullet(gun, tank);
          }
    
          tank.currentReloadTime = tank.reloadTime;
        }
      }
    
      ctx.save();
      ctx.translate(tank.x + tank.width / 2, tank.y + tank.height / 2);
      ctx.rotate(Math.atan2(tank.velY / 2, tank.velX / 2));
      draw.rectangle(-tank.width / 2, -tank.height / 2, tank.width, tank.height, 2, true, true, {
        fillStyle: tank.color,
        strokeStyle: tank.bc,
        lineWidth: tank.width / tank.height * 1.75
      });
      ctx.restore();
    
      for (var i = 0; i < tank.bullets.length; i++) {
        var bullet = tank.bullets[i];
    
        bullet.x += bullet.velX;
        bullet.y += bullet.velY;
    
        draw.arc(bullet.x, bullet.y, bullet.radius, 0, 2 * Math.PI, true, true, {
          fillStyle: bullet.color,
          strokeStyle: bullet.bc,
          lineWidth: tank.width / tank.height * 1.75
        });
      }
    
      for (var i = 0; i < tank.weapons.length; i++) {
        var gun = tank.weapons[i];
    
        if (gun.x < 0) {
          gun.x += gun.width / 240;
        }
    
        ctx.save();
        ctx.translate(tank.x + tank.width / 2, tank.y + tank.height / 2);
        ctx.rotate(tank.gunAngle + gun.rotation);
        draw.rectangle(gun.x, gun.y, gun.width, gun.height, 2, true, true, {
          fillStyle: gun.color,
          strokeStyle: gun.bc,
          lineWidth: tank.width / tank.height * 1.75
        });
        ctx.restore();
      }
    
      ctx.save();
      ctx.translate(tank.x + tank.width / 2, tank.y + tank.height / 2);
      ctx.rotate(tank.gunAngle);
      draw.rectangle(-tank.width / 2 * 0.6 + tank.recoilX, -tank.height / 2 * 0.7, tank.width * 0.6, tank.height * 0.7, 2, true, true, {
        fillStyle: tank.color,
        strokeStyle: tank.bc,
        lineWidth: tank.width / tank.height * 1.75
      });
      ctx.restore();
    }
    
    function shootBullet(gun, tank) {
      var shootS = new Audio("./assets/shoot.wav");
      shootS.play();
      gun.x -= gun.width / 12;
      tank.recoilX = -tank.width / 16;
      var rawVX = Math.cos(tank.gunAngle + gun.rotation);
      var rawVY = Math.sin(tank.gunAngle + gun.rotation);
      var velX = (rawVX + random(-0.02, 0.02)) * 5;
      var velY = (rawVY + random(-0.02, 0.02)) * 5;
      tank.bullets.push(new Bullet(tank.x + tank.width / 2 + (rawVX * (tank.width - (tank.height / 2))), tank.y + tank.height / 2 + (rawVY * (tank.width - (tank.height / 2))), gun.height / 2, "#ff0000", "#800000", tank, velX, velY));
    }
    
    function main() {
      if (keysDown["w"]) {
        player.velY -= player.acceleration;
      }
    
      if (keysDown["a"]) {
        player.velX -= player.acceleration;
      }
    
      if (keysDown["s"]) {
        player.velY += player.acceleration;
      }
    
      if (keysDown["d"]) {
        player.velX += player.acceleration;
      }
    
      if (Mouse.pressed) {
        if (player.currentReloadTime <= 0) {
          for (var i = 0; i < player.weapons.length; i++) {
            var gun = player.weapons[i];
            shootBullet(gun, player);
          }
          player.currentReloadTime = player.reloadTime;
        }
      }
    
      player.setSafeZone(player.x + player.width / 2 - 100, player.y + player.height / 2 - 100, 200, 200);
    
    
      ctx.save();
      draw.clear(0, 0, vWidth, vHeight);
    
      updateTank(player, false);
    
      for (var i = 0; i < enemies.length; i++) {
        updateTank(enemies[i], true);
      }
    
      for (var i = 0; i < infoLevel.map.walls.length; i++) {
        var wall = infoLevel.map.walls[i];
        draw.rectangle(wall.x, wall.y, wall.width, wall.height, 0, true, false, {
          fillStyle: wall.color
        });
      }
    
      ctx.restore();
    }
    
    window.onload = function() {
      Mouse.init();
      setInterval(main, 1000 / fps);
    }
    
    function rectangleToRectangleCollision(obj1, obj2) {
      if (obj1.x + obj1.width > obj2.x && obj1.y + obj1.height > obj2.y &&
        obj1.x < obj2.x + obj2.width && obj1.y < obj2.y + obj2.height) {
        return true;
      }
    
      return false;
    }
    
    window.onresize = function() {
      resizeCanvas(canvas, window.innerWidth, window.innerHeight);
    }
    
    document.addEventListener("keydown", (e) => {
      keysDown[e.key] = true;
    });
    
    document.addEventListener("keyup", (e) => {
      keysDown[e.key] = false;
    });
    
    document.addEventListener("mousemove", () => {
      player.gunAngle = Math.atan2(Mouse.y - (player.y + player.height / 2), Mouse.x - (player.x + player.width / 2));
    });
    *,
    *:before,
    *:after {
      font-family: roboto, Arial, Helvetica, sans-serif, system-ui;
      padding: 0px 0px;
      margin: 0px 0px;
      box-sizing: border-box;
    }
    <!DOCTYPE html>
    <html lang="en">
    
    <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>CD</title>
    </head>
    
    <body>
      <canvas id="canvas"></canvas>
    </body>
    
    </html>