Skip to content

Expert Review (Technische documentatie)

K5: OO-programming

Bij deze project heb ik gebruik gemaakt van 3 technieken van object-georiënteerd programmeren: abstraction, inheritance, and encapsulation. Hier beneden staat hoe ik heb die gebruikt.

Abstraction

Bij abstraction heb ik aan een specefieke regel gehouden: de parameters zijn public alleen als ze buiten de class gebruikt zijn, zoals hier uitgebeeld: parameters zoals tiles, typelist, clusters en moves zijn private, en dus buiten de class Tilegrid zijn niet gezien of gebruikt, terwijl selectedTile, score en gameover zijn wel gebruikt (of zullen gebruikt worden) buiten de class, waardoor ze zijn public.

class TileGrid {

    //Hier zijn de coördinaten van de tileGrid
    #x;
    #y;
    //Hier is  de parameter voor de grootte van de tegels
    #tileSize;
    #tiles;
    //Hier zijn de breedte en de hoogte van de canvas.
    #width;
    #height;
    //Hier is de nummer van de huidige level
    #level;
    //Hier zijn de arrays met types die gebruikt worden voor een specefieke level
    #typelist = [1, 2, 3, 4];
    #typelist2 = [1, 2, 3, 4, 6, 6];
    #typelist3 = [1, 2, 3, 4, 6, 8];
    #typelist4 = [1, 2, 3, 4, 8, 8];

    //Het staat de coördinaten voor de tile die nu is geselecteerd
    selectedTile = {
        selected: false,
        column: 0,
        row: 0,
    }

    //In deze lijst worden later clusters gezet
    #clusters = [];
    //In deze lijst staan alle beschikbare moves
    #moves = [];

    //Hier staan de coördinaten voor de huidige twee tegels die geswapt worden
    #currentmove = {
        x1: 0,
        y1: 0,
        x2: 0,
        y2: 0
    }

    //Hier staat de score van de speler.
    score = 0;
    //Hier staat de spelers aanvalskracht
    playerdamage = 0;

    //Hier wordt bepaald welek functie is de grid aan het uitvoeren.
    #gamestates = {
        init: 0,
        ready: 1,
        resolve: 2
    }
    #gamestate = this.#gamestates.init;

    //Deze parameters zijn voor animaties bedoeld
    #animationstate = 0;
    #animationtime = 0;
    #animationtimetotal = 180;
    //Hier wordt gecontroleerd of de bepaalde twee tegels kunnen met elkaar omgewisseld worden
    canSwap(x1, y1, x2, y2) {
        if ((Math.abs(x1 - x2) == 1 && y1 == y2) ||
            (Math.abs(y1 - y2) == 1 && x1 == x2)) {
            return true;
        }
        return false;

    }
    //Hier worden de coördinaten van de huidige move opgeslagen
    touchSwap(c1, r1, c2, r2) {
        this.#currentmove = { x1: c1, y1: r1, x2: c2, y2: r2 };
        this.selectedTile.selected = false;
        this.#animationstate = 2;
        this.#animationtime = 0;
        this.#gamestate = this.#gamestates.resolve;
    }
}
//Deze functie wordt uitgevoerd als de speler zijn vinger beweegd
function touchMoved(event) {
    let rect = canvas.getBoundingClientRect();
    if (drag == true && tileGrid.selectedTile.selected == true) {
        const tile = tileGrid.getTileAtPosition(getPosition(event));
        if (tile != null && tile.type < 6) {
            if (tileGrid.canSwap(tile.x, tile.y, tileGrid.selectedTile.column, tileGrid.selectedTile.row)) {
                tileGrid.touchSwap(tile.x, tile.y, tileGrid.selectedTile.column, tileGrid.selectedTile.row);
                movesleft -= 1;
            }
        }
    }
}
Abstraction

Inheritance

De class Character heeft twee subclasses: Hero en Enemy. Hero heeft dezelfde parameters en functies als zijn parent class, terwijl enemy voegd zijn eigen parameters toe, zoals moves, en eigen funcites, zoals updateDamage();

class Character {
    #x
    #y
    #role
    #hp
    #maxhp
    _damage
    _spritesheet
    #size
    #color


    //De functies bewegen worden uitgevoerd afhakelijk van of de personage valt aan, geraakt wordt, of doodgaat.
    attack(enemy) {
        if (this.#hp > 0) {
            this.#animationstate = 2;
            this.#animationtime = 0;
            enemy.hp -= this._damage;
            enemy.hurt();
        }
    }

    hurt() {
        this.#animationstate = 3;
        this.#animationtime = 0;
    }

    death() {
        this.#animationstate = 4;
    }

    //De twee functies bewgen zorgen voor het tekenen van de personages
    draw(sprite) {
        let frame;
        let time = Math.floor(this.#animationtime / (this.#animationtimetotal / Math.round(sprite.width / 128)));
        const distance = 128;
        let spritex;
        const spritesize = this.#size * 1.5;
        if (this.#role == "pc") {
            frame = distance * time;
            spritex = this.#x;
        }
        else if (this.#role == "npc") {
            frame = sprite.width - distance * (1 + time);
            spritex = this.#x - (spritesize - this.#size) / 2;
        }
        fill(255, 0, 0);
        stroke(0);
        strokeWeight(5);
        rect(this.#x, this.#y, this.#size, 30, 10);
        if (this.#hp > 0) {
            fill(0, 255, 0);
            stroke(0);
            strokeWeight(3);
            rect(this.#x, this.#y, this.#size * (this.#hp / this.#maxhp), 30, 10);
        }
        if (this.#animationstate == 2) {
            fill(this.#color);
            textSize(100);
            textFont(gameManager.getFont("medieval"));
            text(this._damage, this.#x + this.#size / 3, this.#y - this.#size);
        }
        image(sprite, spritex, this.#y - spritesize, spritesize, spritesize, frame, 0, sprite.height, sprite.height);
        if ((frame >= sprite.width && this.#role == "pc") || (frame < 0 && this.#role == "npc")) {
            if (this.#animationstate > 0 && this.#animationstate < 4) {
                this.#animationstate = 1;
                frame = 0;
                this.#animationtime = 0;
            }
            else if (this.#animationstate == 4) {
                this.#animationstate = 0;
            }
        }
    }

    update(deltaTime) {
        if (this.#animationstate > 0) {
            this.#animationtime += deltaTime;
            if (this.#animationstate == 1) {
                this.draw(this._spritesheet.idle);
            }
            else if (this.#animationstate == 2) {
                this.draw(this._spritesheet.attack);
            }
            else if (this.#animationstate == 3) {
                this.draw(this._spritesheet.hurt);
            }
            else if (this.#animationstate == 4) {
                this.draw(this._spritesheet.dead);
            }
        }
    }

    constructor(x, y, role, hp, size, color) {
        this.#x = x;
        this.#y = y;
        this.#role = role;
        this.#hp = hp;
        this.#maxhp = hp;
        this.#size = size;
        this.#color = color
        this.#animationstate = 1;
    }
}
class Hero extends Character {

    //Hier wordt de held aangemaakt
    constructor(x, y, size) {
        super(x, y, "pc", 500, size, color(0, 0, 255));
        this._damage = 0;
        this._spritesheet = {
            idle: gameManager.getImage("knight_idle"),
            attack: gameManager.getImage("knight_attack"),
            hurt: gameManager.getImage("knight_hurt"),
            dead: gameManager.getImage("knight_dead")
        }
    }
}
class Enemy extends Character {
    #moves
    #mindmg
    #maxdmg

    get moves() {
        return this.#moves
    }
    //Hier wordt de willekeurige aanvalskracht gemaakt tussen minimale en maximale waarde
    #getRndInteger(min, max) {
        return Math.floor(Math.random() * (max - min + 1)) + min;
    }
    updateDamage() {
        this._damage = this.#getRndInteger(this.#mindmg, this.#maxdmg);
    }

    constructor(type, x, y) {
        if (type == "skeleton") {
            super(x, y, "npc", 400, 300, color(255, 0, 0));
            this.#moves = 3;
            this.#mindmg = 50;
            this.#maxdmg = 75;
            this._spritesheet = {
                idle: gameManager.getImage("skeleton_idle"),
                attack: gameManager.getImage("skeleton_attack"),
                hurt: gameManager.getImage("skeleton_hurt"),
                dead: gameManager.getImage("skeleton_dead")
            }
        }
        else if (type == "slime") {
            super(x, y, "npc", 250, 150, color(255, 0, 0));
            this.#moves = 3;
            this.#mindmg = 25;
            this.#maxdmg = 50;
            this._spritesheet = {
                idle: gameManager.getImage("slime_idle"),
                attack: gameManager.getImage("slime_attack"),
                hurt: gameManager.getImage("slime_hurt"),
                dead: gameManager.getImage("slime_dead")
            }
        }
    }
}
Inheritance

Encapsulation

Er zijn drie soorten modifiers die toegang tot parameters voor andere files beperken: public, private, en protected. Public parameters kunnen worden gezien en bewerkt binnen en buiten de class. Private parameters (met #-prefix) kunnen gezien en bewerkt worden alleen binnen dezelfde class. Protected (met _-prefix) werkt net zoals private, alleen de subclasses hebben ook toegang tot deze parameters.

class Tile {
    #x;
    #y;
    #gridx;
    #gridy;
    #size;
    _image;
    #visible;
    _type;
    #shift;
}
class NormalTile extends Tile {
    updateImage(){
        if (this._type == 1){
            this._image = gameManager.getImage("diamond");
        }
        if (this._type == 2){
            this._image = gameManager.getImage("emerald");
        }
        if (this._type == 3){
            this._image = gameManager.getImage("ruby");
        }
        if (this._type == 4){
            this._image = gameManager.getImage("sapphire");
        }
    }
}
Je kan wel waardes van private parameters lezen en veranderen als je voor deze parameters getters en setters maakt. Getters om te lezen en setters om te veranderen. Sommigen hebben beiden, sommigen hebben alleen getters en dus kunnen alleen gelezen worden, en sommigen hebben geen getters en setters, en dus kunnen niet bereikt worden buiten hun class. Ook met getters en setters beslis je hoe deze waarden zullen gelezen en aangepast worden (zoals gezien bij position).
class Tile {

    get x() {
        return this.#x;
    }

    set x(value) {
        this.#x = value;
    }

    get y() {
        return this.#y;
    }

    set y(value) {
        this.#y = value;
    }

    get image(){
        return this._image;
    }

    get position() {
        let pos = createVector(this.#x, this.#y).mult(this.#size);
        return pos.add(this.#gridx, this.#gridy);
    }

    get visible() {
        return this.#visible;
    }

    set visible(value) {
        this.#visible = value;
    }
}
Encapsulation

Bronnen

Janssen, T. (2023, 18 maart). OOP concept for beginners: What is inheritance? Stackify. https://stackify.com/oop-concept-inheritance/
Janssen, T. (2023b, mei 1). OOP concept for Beginners: What is encapsulation. Stackify. https://stackify.com/oop-concept-for-beginners-what-is-encapsulation/
Janssen, T. (2023b, mei 1). OOP concept for Beginners: What is abstraction? Stackify. https://stackify.com/oop-concept-abstraction/# Kantor, I. (z.d.). Private and protected properties and methods. https://javascript.info/private-protected-properties-methods
EisenbergEffect. (2023, 3 oktober). Public, private, and protected class visibility patterns in JavaScript. Medium. https://eisenbergeffect.medium.com/public-private-and-protected-class-visibility-patterns-in-javascript-a23a29229430
Parwinder. (2020, 5 september). Classes in JS: public, private and protected. DEV Community. https://dev.to/bhagatparwinder/classes-in-js-public-private-and-protected-1lok
JavaScript accessors. (z.d.). https://www.w3schools.com/js/js_object_accessors.asp

K6: Relationele database

Ik ben nog niet ver gegaan met de database, maar ik heb al progress gemaakt. Bijvoorbeeld ik kan nu database gebruiken voor de login systeem. Dit heb ik gedaan door user.php te modificeren. Nu slaat die bestand username en wachtwoord van de speler, en zoekt de speler door gebruik van username en wachtwoord. Ook heb ik de database gemodificeerd door nieuwe tabellen en columns toe te voegen.

Huidige_Database

// Handle request type logic
switch ($requestMethod) {
    case 'PUT':
        // nothing yet  
        break;
    case 'POST':
        // README: https://lornajane.net/posts/2008/accessing-incoming-put-data-from-php
        $params = file_get_contents('php://input'); // Access the request PUT data
        $paramsArray = json_decode($params, true);
        createPlayer($paramsArray);
        break;
    case 'GET':
        findPlayer();
        break;
    default:
        handleError('Invalid HTTP request method');
        break;

}
function findPlayer()
{

    if (isset($_GET['code'])) {
        $playerUniqueCode = $_GET["code"];
        $dbReturnID = 0;
        $dbAanmaakDatum = 0;

        // Search for player in the database based on GET param playerName 
        try {
            $dbConnect = new DatabaseConnection();

            $playerUniqueCode = mysqli_real_escape_string($dbConnect->getConnection(), $playerUniqueCode);

            $statement = "
            SELECT 
                id, aanmaakDatumTijd
            FROM
                Blok2_Speler
            WHERE
                uniqueCode = '" . $playerUniqueCode . "';
            ";


            // Execute query-statement on the database
            $result = $dbConnect->executeQuery($statement);

            if (!$result) {
                $errorMsg = $dbConnect->getConnection()->error;
                handleError('' . $errorMsg);
            } else {
                if ($result->num_rows > 0) {
                    $row = $result->fetch_assoc();
                    $dbReturnID = $row['id'];
                    $dbAanmaakDatum = $row['aanmaakDatumTijd'];
                    // Return JSON structured data with requested/needed information about the existing player
                    echo '{"responseType":"ok", "aanmaakDatumTijd":"' . $dbAanmaakDatum . '", "id":' . $dbReturnID . '}';
                } else {
                    // No records found, return error notifying the user about this.
                    handleNotFound('user with uniqueCode ' . $playerUniqueCode);
                }
            }

            // Free the result set
            $result->free();

        } catch (Exception $e) {
            $dbReturnID = -1;
            $errorMsg = 'Failed to query the database. Err: ' . $e->getMessage();
            handleError('' . $errorMsg);
        }

    } else {
        handleError('No unqiue player code received via GET, or use POST with param createPlayer : true to create a new player.');
    }

}

function createPlayer($paramsPostArray)
{
    $errorMsg = NULL;
    $bCreatePlayer = NULL;
    $bUsername = NULL;
    if (isset($paramsPostArray["createPlayer"], $paramsPostArray["userName"])) {

        $uniqueDBCode = -1;
        $dbReturnId = -1;
        $bCreatePlayer = $paramsPostArray["createPlayer"];
        $bUsername = $paramsPostArray["userName"];

        if ($bCreatePlayer == true) {

            // Add a new player to the database and return player data (row id, uniqueCode, aanmaakDatum)
            try {
                $dbConnect = new DatabaseConnection();

                // Generate a unique token of 23 characters with a 'player_' prefix (30 characters)
                $uniqueDBCode = uniqid('player_', true);

                $statement = "
                INSERT INTO Blok2_Speler
                    (uniqueCode, aanmaakDatumTijd, Username)
                VALUES
                    ('" . $uniqueDBCode . "', now(), '" . $bUsername . "');
                ";

                // Execute query-statement on the Database
                $bSucces = $dbConnect->executeQuery($statement);

                if (!$bSucces) {
                    $errorMsg = $dbConnect->getConnection()->error;
                    handleError('Insert failed: ' . $errorMsg);
                } else {
                    $dbReturnId = $dbConnect->getConnection()->insert_id;
                    // Return JSON structured data with requested/needed information about the new player
                    echo '{"responseType":"ok", "uniqueCode": "' . $uniqueDBCode . '"}';
                }

            } catch (Exception $e) {
                $dbReturnId = -1;
                $errorMsg = 'Failed to query the database. Err: ' . $e->getMessage();
                handleError('' . $errorMsg);
            }
        }
    } else {
        handleError('No createPlayer POST param received');
    }
}
class AnalyticsTrackerManager {

    playerID;
    newplayer = false;
    constructor() {
        //get a unique id and store it in the localstorage.
        //retrieve from localStorage if available, otherwise create a new one.
        this.#initialize();
    }

    async #initialize() {
        const playerGUID = localStorage.getItem("playerGUID");
        if (playerGUID === null) {
            this.newplayer = true;
        } else {
            this.#retrievePlayerData(playerGUID);
        }
    }

    #retrievePlayerData(uniqueCode) {
        let url = `https://oege.ie.hva.nl/~akimovm/blok2/analytics/user.php?code=${uniqueCode}`;
        httpGet(url, 'json', (responce)  => {
            this.playerID = responce.id;
            this.newplayer = false;
        })
    }

    storePlayerData() {
        // create a p5 httppost connection
        let username = document.getElementById("userName").value;
        const postData = { 
            "createPlayer": true, 
            "userName": username 
        };
        const url = `https://oege.ie.hva.nl/~akimovm/blok2/analytics/user.php`;
        httpPost(url, 'json', postData, (result)  => {
            const playerUniqueCode = result.uniqueCode;
            localStorage.setItem("playerGUID", playerUniqueCode);
            this.#retrievePlayerData(playerUniqueCode);
        }, function (error) { throw new Error(error); });
    }
}
class GameManager {
    #analyticsTrackerManager;
    #assetManager;

    constructor() {
        this.#analyticsTrackerManager = new AnalyticsTrackerManager();
        this.#assetManager = new AssetManager();
        window.gameManager = this;
    }

    savePlayer(){
        this.#analyticsTrackerManager.storePlayerData();
    }

    get newplayer(){
        return this.#analyticsTrackerManager.newplayer;
    }
    get playerID(){
        return this.#analyticsTrackerManager.playerID;
    }
}
let start = true;
function draw() {
    if (start == true) {
        StartScreen();
    }
}
function StartScreen() {
    textSize(96);
    fill(255, 215, 0);
    textFont(font);
    text('Jewel', width / 3, 200);
    text('Quest', width / 3, 300);
    if (gameManager.newplayer == true) {
        EnterName();
    }
    else {
        let playButton = document.createElement("button");
        playButton.setAttribute("type", "button");
        playButton.innerHTML = "Play";
        playButton.setAttribute("onclick", "enterTheGame()");
        playButton.setAttribute("class", "startbutton");
        if (loginappend == true) {
            playButton.style.fontSize = "96px";
            canvaswrap.appendChild(playButton);
            loginappend = false;
        }
    }
}
function EnterName() {
    textSize(56);
    textFont(font);
    fill(255);
    text('Please enter your name', width / 10, 650);
    let nameInput = document.createElement("input");
    nameInput.setAttribute("type", "text");
    nameInput.setAttribute("id", "userName");
    nameInput.setAttribute("placeholder", "Username");
    nameInput.setAttribute("oninput", "turnButtonOn(submitButton)");
    nameInput.setAttribute("autocomplete", "off");
    let submitButton = document.createElement("button");
    submitButton.setAttribute("type", "button");
    submitButton.setAttribute("disabled", "true")
    submitButton.innerHTML = "Confirm";
    submitButton.setAttribute("onclick", "enterTheGame()");
    submitButton.setAttribute("class", "startbutton");
    submitButton.setAttribute("id", "submitButton");
    if (loginappend == true) {
        canvaswrap.appendChild(nameInput);
        canvaswrap.appendChild(submitButton);
        loginappend = false;
    }
}

function enterTheGame() {
    if (gameManager.newplayer == true) {
        gameManager.savePlayer();
    }
    let inputbar = document.getElementById("userName");
    if (inputbar != null) {
        inputbar.remove();
    }
    removeHTMLElements("startbutton");
    start = false;
}

Bronnen

Reference | P5.js. (z.d.). https://p5js.org/reference/#/p5/httpPost
Reference | P5.js. (z.d.-b). https://p5js.org/reference/#/p5/httpGet
Here, S. N. (z.d.). Stappenplan opzetten PHP en het gebruik van een database - HBO-ICT Match 3. https://propedeuse-hbo-ict.dev.hihva.nl/onderwijs/opdrachtomschrijvingen-jaar1-blok2/gd-opdracht/lesplan_php/
PHP isset() function. (z.d.). https://www.w3schools.com/php/func_var_isset.asp
PHP Mysqli Real_Escape_String() function. (z.d.). https://www.w3schools.com/php/func_mysqli_real_escape_string.asp
Window localStorage property. (z.d.). https://www.w3schools.com/jsref/prop_win_localstorage.asp

K7: Documentatie met UML

Om UML Diagrammen te maken heb ik gebruik gemaakt van (een VSC extentie voor) UMLet, een tool om UML diagrammen te maken. Nadat ik heb die gemaakt, heb ik die geëxporteerd als png’s en hier geplakt. Hier beneden zijn Use Case en Class diagrammen voor de huidige code en in de toekomst zal het aangepast worden.
Use_Case Class_Tile Class_Character

Bronnen

Propedeuse, T. (z.d.). UML - Knowledgebase. https://knowledgebase.hbo-ict-hva.nl/1_beroepstaken/software/ontwerpen/uml/0_uml/
Propedeuse, T. (z.d.-b). Use case diagram - Knowledgebase. https://knowledgebase.hbo-ict-hva.nl/1_beroepstaken/software/ontwerpen/uml/uml_use_case_diagram/
Propedeuse, T. (z.d.-b). UML Class Diagram - Knowledgebase. https://knowledgebase.hbo-ict-hva.nl/1_beroepstaken/software/ontwerpen/uml/uml_class_diagram/
Lucid Software. (2023, 10 augustus). UML class diagrams [Video]. YouTube. https://www.youtube.com/watch?v=6XrL5jXmTwM
Lucid Software. (2023b, september 21). UML use case diagrams [Video]. YouTube. https://www.youtube.com/watch?v=4emxjxonNRI


Last update: January 18, 2024