Source

lib.js

/** Class representing an animation based on a spritesheet and json. */
class Animation {
    #spritesheet = undefined;
    #frames = undefined;

    #animationLoops = {};
    #currentAnimationLoop = "";
    #currentAnimationFrame = 0;

    /**
     * Create a new Animation.
     * @param {string} spritesheetImagePath - The path to an image representing a spritesheet.
     * @param {string} framesJSONPath - The path to a JSON file holding all frames data for the spritesheet.
     */
    constructor(spritesheetImagePath, framesJSONPath, speed = 20) {
        this.#spritesheet = loadImage(spritesheetImagePath);
        this.#frames = loadJSON(framesJSONPath);

        setInterval(() => {
            if (this.#currentAnimationLoop in this.#animationLoops) {
                this.#currentAnimationFrame = (this.#currentAnimationFrame + 1) % this.#animationLoops[this.#currentAnimationLoop].length;
            }
        }, speed);
    }

    /**
     * Add an animationloop to the animation. An animation loop consists of several frames that are defined in the framesJSON file for this animation.
     * Framenumbers are 0-based. 
     * Frames can be reused in different animationloops.
     * @param {string} loopName - The name for this animationloop. This name must be unique!
     * @param {number} frameNumbers - the framenumbers for this animationloop, as defined (in order) in the framesJSON file for this animation.
     */
    AddAnimationLoop(loopName, ...frameNumbers) {
        this.#animationLoops[loopName] = frameNumbers;
    }

    /**
     * Set the current animation loop to a previously-defined animation-loop.
     * @param {string} value - the name of the animation-loop that should run from now on.
     */
    set CurrentAnimationLoop(value) {
        if (this.#currentAnimationLoop == value) {
            return;
        }
        if (value in this.#animationLoops) {
            this.#currentAnimationLoop = value;
            this.#currentAnimationFrame = 0;
        }
    }

    /**
     * Draws the current animation-frame for the currently selected animation-loop.
     * @param {number} w - the draw-width for this animation
     * @param {number} h - the draw-height for this animation
     */
    Draw(w, h) {
        if (this.#currentAnimationLoop in this.#animationLoops &&
            this.#frames[this.#animationLoops[this.#currentAnimationLoop][this.#currentAnimationFrame]]) {
            image(this.#spritesheet, 0, 0, w, h, 
                this.#frames[this.#animationLoops[this.#currentAnimationLoop][this.#currentAnimationFrame]].frame.x,
                this.#frames[this.#animationLoops[this.#currentAnimationLoop][this.#currentAnimationFrame]].frame.y,
                this.#frames[this.#animationLoops[this.#currentAnimationLoop][this.#currentAnimationFrame]].frame.width,
                this.#frames[this.#animationLoops[this.#currentAnimationLoop][this.#currentAnimationFrame]].frame.height);
        }
    }
}
class Collider {
    #center;
    #offset;
    #extents;
    #originalExtents;
    #parent;

    #hit = {
        left: false,
        right: false,
        top: false,
        bottom: false
    };

    constructor(_parent, _center, _extents, _offset) {
        this.#parent = _parent;
        this.#center = _center;
        this.#extents = _extents;
        this.#originalExtents = _extents.copy();

        if (_offset === undefined)
            this.#offset = createVector(0, 0);
        else
            this.#offset = _offset;
    }

    get HitLocation() {
        return this.#hit;
    }

    get Center() {
        return this.#center;
    }

    get Extents() {
        return this.#extents;
    }

    get Offset() {
        return this.#offset;
    }

    get Min() {
        return createVector(this.Center.x + this.#offset.x - this.#extents.x, this.Center.y + this.#offset.y - this.#extents.y);
    }

    get Max() {
        return createVector(this.Center.x + this.#offset.x + this.#extents.x, this.Center.y + this.#offset.y + this.#extents.y);
    }

    get Right() {
        return this.Center.x + this.#offset.x + this.#extents.x / 2;
    }

    get Left() {
        return this.Center.x + this.#offset.x - this.#extents.x / 2;
    }

    get Top() {
        return this.Center.y + this.#offset.y - this.#extents.y / 2;
    }

    get Bottom() {
        return this.Center.y + this.#offset.y + this.#extents.y / 2;
    }

    get Size() {
        return createVector(this.#extents.x * 2, this.#extents.y * 2);
    }

    Hit(overlap, ...otherColliders) {
        this.#hit.left = false;
        this.#hit.right = false;
        this.#hit.top = false;
        this.#hit.bottom = false;

        let collidersHit = [];

        if (overlap === true) {
            otherColliders.forEach(otherCollider => {
                if (this !== otherCollider && !this.#parent.Removed) {
                    if (this.Overlap(otherCollider) === true) {
                        collidersHit.push(otherCollider);
                    }
                }
            });
        }
        else {
            otherColliders.forEach(otherCollider => {
                if (this !== otherCollider && !this.#parent.Removed) {
                    let displacement = this.Collide(otherCollider);

                    if (displacement.x !== 0 || displacement.y !== 0) {
                        if (!this.#parent.Immovable) {
                            this.#parent.Displace(displacement);
                        }
                        collidersHit.push(otherCollider);

                        if (displacement.x > 0)
                            this.#hit.left = true;
                        if (displacement.x < 0)
                            this.#hit.right = true;
                        if (displacement.y < 0)
                            this.#hit.bottom = true;
                        if (displacement.y > 0)
                            this.#hit.top = true;
                    }
                }
            });
        }

        return collidersHit.map(collider => collider.#parent);
    }

    UpdateCollider() {
        this.#extents.x = this.#parent.Width;
        this.#extents.y = this.#parent.Height;
    }

    Overlap(other) {
        //box vs box
        if (other instanceof Collider && !other.#parent.Removed && !this.#parent.Removed) {
            let difference = other.MinkowskiDifference(this);

            if (difference.Min.x <= 0 &&
                difference.Max.x >= 0 &&
                difference.Min.y <= 0 &&
                difference.Max.y >= 0) {
                return true;
            }
            else
                return false;
        }
        return false;
        // //box vs circle
        // else if (other instanceof CircleCollider) {

        //     //find closest point to the circle on the box
        //     let pt = createVector(other.Center.x, other.Center.y);

        //     //I don't know what's going o try to trace a line from centers to see
        //     if (other.Center.x < this.left())
        //         pt.x = this.left();
        //     else if (other.Center.x > this.right())
        //         pt.x = this.right();

        //     if (other.Center.y < this.top())
        //         pt.y = this.top();
        //     else if (other.Center.y > this.bottom())
        //         pt.y = this.bottom();

        //     let distance = pt.dist(other.Center);

        //     return distance < other.radius;
        // }
    }

    Collide(other) {
        if (other instanceof Collider) {
            let md = other.MinkowskiDifference(this);

            if (md.Min.x <= 0 &&
                md.Max.x >= 0 &&
                md.Min.y <= 0 &&
                md.Max.y >= 0) {
                let boundsPoint = md.ClosestPointOnBoundsToPoint(createVector(0, 0));

                return boundsPoint;
            }
            else
                return createVector(0, 0);
        }
    }

    MinkowskiDifference(other) {
        let topLeft = this.Min.sub(other.Max);
        let fullSize = this.Size.add(other.Size);

        let difference = new Collider(null, topLeft.add(fullSize.div(2)), fullSize.div(2));

        return difference;
    }


    ClosestPointOnBoundsToPoint(point) {
        // test x first
        let minDist = abs(point.x - this.Min.x);
        let boundsPoint = createVector(this.Min.x, point.y);

        if (abs(this.Max.x - point.x) < minDist) {
            minDist = abs(this.Max.x - point.x);
            boundsPoint = createVector(this.Max.x, point.y);
        }

        if (abs(this.Max.y - point.y) < minDist) {
            minDist = abs(this.Max.y - point.y);
            boundsPoint = createVector(point.x, this.Max.y);
        }

        if (abs(this.Min.y - point.y) < minDist) {
            minDist = abs(this.Min.y - point.y);
            boundsPoint = createVector(point.x, this.Min.y);
        }

        return boundsPoint;
    }
}
/**
 * Class that represents a Game. Extend this class to create a baseclass for your Game.
 */
class Game {
    /**
     * Creates a new Game instance.
     */
    constructor() {
        GameManager.GetInstance().GameInstance = this;
    }
}
/** Singleton class that manages the game and handles various p5 functionalities. */
class GameManager {
    static #INSTANCE;

    /**
     * Function to retrieve the current instance of the GameManager. Always use this function instead of the constructor!
     * When no instance is available, one is created.
     * @returns The singleton instance of the GameManager.
     */
    static GetInstance() {
        if (GameManager.#INSTANCE == undefined) {
            GameManager.#INSTANCE = new GameManager();
            GameManager.#INSTANCE.Init();
        }
        return GameManager.#INSTANCE;
    }

    #allGameObjects = [];
    #collisionLayers = {};
    #gameInstance = undefined;
    #debugUpdateTimer = 0;
    #debugText = "";

    static keysPressed = [];

    /**
     * The instance of the game that is playing.
     * @param {Game} value - An instance of a game that the gameManager should manage.
     */
    set GameInstance(value) {
        this.#gameInstance = value;
    }

    /**
     * Initializes the canvas and game values.
     */
    Init() {
        createCanvas(Settings.GameWidth, Settings.GameHeight);

        if (Settings.Debug === true || Settings.ShowStats === true) {
            this.#UpdateDebugText();
            setInterval(() => { this.#UpdateDebugText() }, 500);
        }
    }

    /**
     * Removes a GameObject from the scene.
     * @param {GameObject} gameObject - The GameObject to be removed from the scene.
     */
    RemoveGameObject(gameObject) {
        this.#allGameObjects = this.#allGameObjects.filter(go => go != gameObject);

        for (const layerId in this.#collisionLayers) {
            let index = this.#collisionLayers[layerId].indexOf(gameObject);
            if (index >= 0) {
                this.#collisionLayers[layerId].splice(index, 1);
            }
        }
    }

    /**
     * Adds a GameObject to the current game.
     * @param {GameObject} gameObject - The GameObject to be added to the scene.
     */
    AddGameObject(gameObject) {
        gameObject.Index = this.#allGameObjects.length;
        this.#allGameObjects.push(gameObject);
    }

    /**
     * Puts the given GameObject on the given Collision Layer. Collision Layers are defined in Settings/Settings.js
     * @param {GameObject} gameObject - The GameObject to be added to the collision layer
     * @param {Settings.collisionLayer} collisionLayer - Collision layer defined in the Settings file
     */
    AddGameObjectToCollisionLayer(gameObject, collisionLayer) {
        for (const layerId in this.#collisionLayers) {
            let index = this.#collisionLayers[layerId].indexOf(gameObject);
            if (index >= 0) {
                if (layerId === collisionLayer) {
                    return;
                }
                else {
                    this.#collisionLayers[layerId].splice(index, 1);
                }
            }
        }

        if (collisionLayer in this.#collisionLayers === false) {
            this.#collisionLayers[collisionLayer] = [];
        }

        this.#collisionLayers[collisionLayer].push(gameObject);
    }

    /**
     * Sets up a Game instance, defined by the type of Game in Settings/Settings.js
     */
    static Setup() {
        GameManager.GetInstance().#gameInstance = new Settings.GameClass();
    }

    /**
     * Static function that functions as a replacement for p5.draw. This calls Update from the GameManager Instance.
     */
    static Draw() {
        GameManager.GetInstance().Update();
    }

    /**
     * Static function that functions as a replacement for p5.kyePressed. This calls KeyPressed from the current Game instance.
     */
    static KeyPressed() {
        GameManager.keysPressed.push(keyCode);
        this.#gameInstance.KeyPressed();
    }

    /**
     * Update draws all GameObjects in the correct order and draws debug information in case Settings/Settings.js/Debug was set to true.
     */
    Update() {
        push();
        background(Settings.BackgroundColor);
        if (this.#gameInstance) {
            this.#gameInstance.Update();
        }
        this.#allGameObjects.sort((a, b) => a.Depth == b.Depth ? 0 : a.Depth > b.Depth ? 1 : -1);
        this.#allGameObjects.forEach(go => go.Display());

        for (const layerA in Settings.LayerInteractions) {
            let value = Settings.LayerInteractions[layerA];
            value.forEach(layerB => {
                if (this.#collisionLayers[layerA] && this.#collisionLayers[layerB]) {
                    this.#collisionLayers[layerA].forEach(go => go.Collide(...this.#collisionLayers[layerB]));
                }
            });
        }

        this.#allGameObjects.forEach(go => { 
            if (go.Collider) {
                let gameObjectsWithCollider = this.#allGameObjects.filter(f => f.Collider && f != go)
                go.Overlap(...gameObjectsWithCollider);
            }
        });

        if (Settings.ShowGrid) {
            this.#DrawGridOverlay();
        }
        pop();

        if (Settings.Debug === true || Settings.ShowStats === true) {
            push();
            textAlign(RIGHT, TOP);
            text(this.#debugText, width - 10, 0);
            pop();
        }

        if (GameManager.keysPressed.length > 0) {
            GameManager.keysPressed = [];
        }
    }

    /**
     * Updates the debug text that is shown in the upper right corner.
     */
    #UpdateDebugText() {
        this.#debugText = `
        ${"debugMode On"}
        ${Math.round(frameRate())} fps
        `;
    }

    /**
     * Draws a Grid overlay, where each square is sized according to Settings/Settings.js/GridSize
     */
    #DrawGridOverlay() {
        push();
        stroke('rgba(0,255,0, 0.25)');
        for (let i = 0; i < 100; ++i) {
            line(0, i * Settings.GridSize, 100 * Settings.GridSize, i * Settings.GridSize);
        }
        for (let i = 0; i < 100; ++i) {
            line(i * Settings.GridSize, 0, i * Settings.GridSize, 100 * Settings.GridSize);
        }
        pop();
    }
}

// window.preload = GameManager.Setup;
window.setup = GameManager.Setup;
window.draw = GameManager.Draw;
window.keyPressed = GameManager.KeyPressed;

/**
 * Global function to check if a key (by keycode) was pressed in the previous frame.
 * @param {number} key - keyCode to check if the key went down in the previous frame
 * @returns true if the key went down in the previous frame.
 */
p5.prototype.keyWentDown = function (key) {
    return GameManager.keysPressed.includes(key);
};

/**
 * Copies a source image as a pattern to a target image.
 * @param {p5.Image} src The source image on which the fill image is based.
 * @param {number} dw Width of the target image
 * @param {number} dh Height of the target image
 * @param {number} sw Width of the source image
 * @param {number} sh Height of the source image
 * @returns An image that contains the source image as a pattern
 */
p5.prototype.createFillImage = function (src, dw, dh, sw, sh) {
    let img = createImage(dw, dh);

    src.resize(sw, sh);
    for (let i = 0; i < dw; i += src.width) {
        for (let j = 0; j < dh; j += src.height) {
            img.copy(src, 0, 0, src.width, src.height, i, j, src.width, src.height);
        }
    }
    return img;
}
/** Class representing an object in your game. */
class GameObject {
    #width = 100;
    #height = 100;

    #position;
    #previousPosition;
    #newPosition;
    #deltaPosition;

    #index = 0;
    #depth = 0;
    #life = -1;
    #visible = true;
    #debug = false;
    #removed = false;

    #velocity;
    #maxSpeed = -1;
    #immovable = false;
    #collider = undefined;
    #collisionLayer = undefined;

    /** 
     * Create a GameObject
     * @param {number} x - X position
     * @param {number} y - Y position
     * @param {number} width - The width of the GameObject
     * @param {number} height - The height of the GameObject 
     */
    constructor(x, y, width, height) {
        this.#position = createVector(x, y);
        this.#previousPosition = createVector(x, y);
        this.#newPosition = createVector(x, y);
        this.#deltaPosition = createVector(0, 0);

        this.Width = width;
        this.Height = height;
        this.#width = width;
        this.#height = height;

        this.#velocity = createVector(0, 0);

        GameManager.GetInstance().AddGameObject(this);
    }

    /**
     * Property to check if the GameObject was removed from the scene
     * @return {boolean} true if the GameObject was removed from the scene
     */
    get Removed() { 
        return this.#removed;
    }

    /**
     * The collider that is currently used on this GameObject
     * @return {Collider} The collider for this GameObject
     */
    get Collider() { 
        return this.#collider;
    }

    /**
     * The location where the collider for this GameObject was hit. (left, right, top, bottom)
     * @return {Collider} The side on which the current collider was hit.
     */
    get Hit() {
        return this.#collider.HitLocation;
    }

    /**
     * Changes the collision layer for this GameObject. The collision layer determines with which other GameObject this GameObject can interact. 
     * collision layers are set in the Settings.js
     * @param {string} value - The side on which the current collider was hit.
     */
    set CollisionLayer(value) {
        GameManager.GetInstance().AddGameObjectToCollisionLayer(this, value);
        this.#collisionLayer = value;
    }

    /**
     * Determines how many frames this GameObject will remain alive (before being removed automatically). 
     * @param {number} value - How many frames this GameObject will remain alive. -1 will keep this GameObject alive indefinite (default).
     */
    set Life(value) {
        this.#life = value;
    }

    /**
     * An immovable object cannot be displaced by another GameObject.
     * @return {Boolean} Returns true if this GameObject is immovable.
     */
    get Immovable() {
        return this.#immovable;
    }

    /**
     * An immovable object cannot be displaced by another GameObject.
     * @param {Boolean} value - set it to true to make this GameObject immovable. False allows it to be displaced on collision with other GameObjects (default).
     */
    set Immovable(value) {
        this.#immovable = value;
    }

    /**
     * When debug is set to true, debug values are shown on screen. (e.g. render-depth, collisionbox, ...)
     * @param {Boolean} value - True to show debug values on screen.
     */
    set Debug(value) {
        this.#debug = value;
    }

    /**
     * The depth value determines the rendering order of each GameObject. GameObjects with identical depth values have no predetermined rendering order.
     * @param {number} value - integer value that represents the rendering order for this GameObject. A higher depth value renders a GameObject on top.
     */
    set Depth(value) {
        this.#depth = value;
    }

    /**
     * The depth value determines the rendering order of each GameObject. GameObjects with identical depth values have no predetermined rendering order.
     * @returns {number} integer value that represents the rendering order for this GameObject. A higher depth value renders a GameObject on top.
     */
    get Depth() {
        return this.#depth;
    }

    /**
     * The index shows in what order a GameObject was added to the game. The first GameObject that was added gets index 0, the next 1, ...
     * @param {number} value - integer value that represents the order in which this GameObject was added to the GameObject.
     */
    set Index(value) {
        this.#index = value;
    }

    /**
     * The width of the GameObject on screen in pixels.
     * @returns {number} The width of this GameObject in pixels.
     */
    get Width() {
        return this.#width;
    }

    /**
     * The width of the GameObject on screen in pixels.
     * @param {number} value - The width of this GameObject in pixels.
     */
    set Width(value) {
        this.#width = value;
    }

    /**
     * The height of the GameObject on screen in pixels.
     * @returns {number} The height of this GameObject in pixels.
     */
    get Height() {
        return this.#height;
    }

    /**
     * The height of the GameObject on screen in pixels.
     * @param {number} value - The height of this GameObject in pixels.
     */
    set Height(value) {
        this.#height = value;
    }

    /**
     * A p5.Vector (x, y) representing the velocity of the GameObject on the X and Y axis.
     * @returns {p5.Vector} returns the current velocity of this GameObject.
     */
    get Velocity() {
        return this.#velocity;
    }

    /**
     * A p5.Vector (x, y) representing the velocity of the GameObject on the X and Y axis.
     * @param {p5.Vector} value - the new current velocity of this GameObject.
     */
    set Velocity(value) {
        this.#velocity.x = value.x;
        this.#velocity.y = value.y;
    }

    /**
     * The speed of this GameObject, the same as the magnitude of the Velocity Vector.
     * @returns {number} The speed of the current GameObject.
     */
    get Speed() {
        return this.#velocity.mag();
    }

    /**
     * A p5.Vector (x, y) that represents the position of the current GameObject on the X and Y axis in pixels.
     * @param {p5.Vector} value - The new position for the GameObject to be rendered at.
     */
    set Position(value) {
        this.#position.x = value.x;
        this.#position.y = value.y;
    }

    /**
     * A p5.Vector (x, y) that represents the position of the current GameObject on the X and Y axis in pixels.
     * @returns {p5.Vector} The position of the current GameObject.
     */
    get Position() {
        return this.#position;
    }

    /**
     * The angle of direction of the velocity vector in radians.
     * @returns {number} The angle of direction of the velocity vector in radians.
     */
    get Direction() {
        let direction = atan2(this.#velocity.y, this.#velocity.x);

        if (isNaN(direction))
            direction = 0;

        return direction;
    }

    /**
     * Moves the GameObject according to a given displacement.
     * @param {p5.Vector} displacement - The displacement vector that determines where and how much the current GameObject should be moved.
     */
    Displace(displacement) {
        this.#position.add(displacement);
        this.#previousPosition = createVector(this.#position.x, this.#position.y);
        this.#newPosition = createVector(this.#position.x, this.#position.y);
    }

    /**
     * Sets a maximum size for the velocity vector. The GameObject cannot move faster than this speed.
     * @param {number} max - The maximum speed at which the GameObject can move when using the Velocity.
     */
    LimitSpeed(max) {
        //update linear speed
        let speed = this.Speed;

        if (abs(speed) > max) {
            //find reduction factor
            let k = max / abs(speed);
            this.#velocity.x *= k;
            this.#velocity.y *= k;
        }
    }

    /**
     * Sets the speed and direction for the current GameObject.
     * @param {number} speed - The speed at which the GameObject should start moving.
     * @param {number} angle - The angle of direction where the GameObject should move to.
     */
    SetSpeed(speed, angle) {
        let a = radians(angle);
        this.#velocity.x = cos(a) * speed;
        this.#velocity.y = sin(a) * speed;
    }

    /**
     * Adds some speed in a certain direction. 
     * @param {number} speed The speed that should be added to the current speed (determined by the Velocity).
     * @param {number} angle The angle of direction where the GameObject should move to.
     */
    AddSpeed(speed, angle) {
        let a = radians(angle);
        this.#velocity.x += cos(a) * speed;
        this.#velocity.y += sin(a) * speed;
    };

    /**
     * Creates a default collider based on the GameObject's width and height, and assigns it to the current GameObject.
     */
    SetDefaultCollider() {
        this.#collider = new Collider(this, this.#position, createVector(this.Width, this.Height));
    }

    /**
     * Removes the currently set collider from the GameObject. If no collider was set before, this does nothing.
     */
    RemoveCollider() { 
        this.#collider = undefined;
    }

    /**
     * Check if the current GameObject overlaps with any of a list of GameObjects. When it overlaps, OnOverlap gets called.
     * @param  {...any} otherSprites - List of GameObjects. The functions checks which of these GameObjects overlaps with the current GameObject (ignoring itself).
     * @returns true if an overlap was found, false if not
     */
    Overlap(...otherSprites) {
        let spritesHit = this.#collider.Hit(true, ...otherSprites.map(sprite => sprite.#collider)); // the collider hit function needs an array of colliders, not sprites
        if (spritesHit.length > 0)
            this.OnOverlap(spritesHit);
        return spritesHit.length > 0;
    }

    /**
     * Function that gets called when an overlap with this GameObject is detected. Override this function to check for overlaps.
     * @param {GameObject[]} spritesHit - Array of GameObjects that overlap with this GameObject
     */
    OnOverlap(spritesHit) { }

    /**
     * Check if the current GameObject collides with any of a list of GameObjects. When it collides, OnCollide gets called.
     * @param  {...any} otherSprites - List of GameObjects. The functions checks which of these GameObjects collides with the current GameObject (ignoring itself).
     * @returns true if a collision was found, false if not
     */
    Collide(...otherSprites) {
        let spritesHit = this.#collider.Hit(false, ...otherSprites.map(sprite => sprite.#collider)); // the collider hit function needs an array of colliders, not sprites
        if (spritesHit.length > 0)
            this.OnCollide(spritesHit);
        return spritesHit.length > 0;
    }

    /**
     * Function that gets called when a collision with this GameObject is detected. Override this function to check for collisions.
     * @param {GameObject[]} spritesHit - Array of GameObjects that overlap with this GameObject
     */
    OnCollide(spritesHit) { }

    /**
     * Removes the current GameObject from the scene. The GameObject may not be removed from memory instantly, but the Removed property is set to true.
     */
    Remove() {
        this.RemoveCollider();
        this.#removed = true;
        GameManager.GetInstance().RemoveGameObject(this);
        delete this;
    }

    /**
     * Update gets called every frame. Override this function to implement your own visualisations and/or frame-actions (input, movement, ...).
     */
    Update() {
        noStroke();
        fill("magenta");
        rect(0, 0, this.#width, this.#height);
    }

    /**
     * Display gets called every frame, and prepares the GameObject to be drawn correctly. Do not call this directly, or override this!
     */
    Display() {
        if (this.#visible && !this.#removed) {
            if (this.Collider) { 
                this.Collider.UpdateCollider();   
            }
            //if there has been a change somewhere after the last update
            //the old position is the last position registered in the update
            if (this.#newPosition !== this.#position)
                this.#previousPosition = createVector(this.#newPosition.x, this.#newPosition.y);
            else
                this.#previousPosition = createVector(this.#position.x, this.#position.y);

            if (this.#maxSpeed !== -1)
                this.LimitSpeed(this.#maxSpeed);

            this.#position.x += this.#velocity.x;
            this.#position.y += this.#velocity.y;

            this.#newPosition = createVector(this.#position.x, this.#position.y);

            this.#deltaPosition.x = this.#position.x - this.#previousPosition.x;
            this.#deltaPosition.y = this.#position.y - this.#previousPosition.y;

            //self destruction countdown
            if (this.#life > 0)
                this.#life--;
            if (this.#life === 0)
                this.Remove();

            push();

            colorMode(RGB);
            noStroke();
            rectMode(CENTER);
            ellipseMode(CENTER);
            imageMode(CENTER);

            translate(this.#position.x, this.#position.y);
            this.Update();
            pop();

            //draw debug info
            if (Settings.Debug || this.#debug) {
                this.#DrawDebugInfo();
            }

        }
    }

    /**
     * Draws debug information for this GameObject.
     */
    #DrawDebugInfo() {
        let ctx = document.querySelector("canvas").getContext("2d");
        ctx.save();

        let pos = this.Collider ? this.Collider.Center : this.Position;
        let w = this.Collider ? this.Collider.Size.x / 2 : 0;
        let h = this.Collider ? this.Collider.Size.y / 2 : 0;

        ctx.lineWidth = 2;
        ctx.strokeStyle = "#00FF00";
        ctx.strokeRect(pos.x - w / 2, pos.y - h / 2, w, h);

        ctx.beginPath();
        ctx.moveTo(pos.x - 10, pos.y);
        ctx.lineTo(pos.x + 10, pos.y);
        ctx.stroke();

        ctx.beginPath();
        ctx.moveTo(pos.x, pos.y - 10);
        ctx.lineTo(pos.x, pos.y + 10);
        ctx.stroke();

        ctx.fillStyle = "#00FF00";
        ctx.font = '16px sans-serif';
        ctx.fillText(this.#index + '', pos.x + 4, pos.y - 2);
        ctx.fillText(this.#depth + '', pos.x + 4, pos.y + 15);

        ctx.restore();
    }
}