Technische documentatie¶
In deze documentatie wordt kort uitgelegd waarom een stuk code is toegevoegd en wat de code doet.
UML Class Diagram¶
Het huidige class diagram is als volgt:
Maken van de tilemap¶
// in TileGrid class
tileMap = [
[1, 2, 2, 1, 2, 2, 1, 1],
[1, 2, 0, 1, 2, 2, 1, 1],
[1, 1, 1, 1, 1, 2, 1, 1],
[1, 2, 2, 1, 2, 0, 1, 1],
[1, 2, 1, 1, 1, 2, 1, 1],
[1, 1, 1, 1, 2, 2, 1, 1],
[1, 2, 0, 2, 1, 2, 1, 1],
[1, 1, 2, 1, 2, 0, 1, 1],
[1, 1, 2, 1, 1, 2, 1, 1],
[1, 2, 1, 1, 2, 2, 1, 1],
[2, 1, 2, 1, 1, 2, 1, 2],
[1, 2, 2, 1, 1, 2, 2, 1],
]
generateTileGrid() {
//tiles is a 2D array, meaning that it is an array of arrays.
//see https://www.freecodecamp.org/news/javascript-2d-arrays/ for more information about 2D arrays.
this.tiles = new Array();
//generate tile grid here and place tiles in the 2D #tile array.
if (!this.tileArrayMade) {
for (let x = 0; x < this.#width; x++) {
for (let y = 0; y < this.#height; y++) {
if (!this.tiles[x]) {
this.tiles[x] = new Array();
}
if (this.tileMap[x][y] == 0) {
this.tiles[x][y] = new NonMovingTile(this.tileSize, x, y, this.tileNumber)
} else if (this.tileMap[x][y] == 1) {
// Add 3 randomized normalTile's
this.tileNumber = Math.floor(Math.random() * 3) + 1
this.tiles[x][y] = new NormalTile(this.tileSize, x, y, this.tileNumber);
} else if (this.tileMap[x][y] == 2) {
// Add 3 randomized specialTile's
this.tileNumber = Math.floor(Math.random() * 3) + 4
this.tiles[x][y] = new SpecialTile(this.tileSize, x, y, this.tileNumber);
}
}
}
this.tileArrayMade = true;
this.checkForMatches();
score = 0; // Checks for matches before game starts to create a normal starting tileGrid and resets score
}
}
Hier wordt de tilemap en dus het speelveld gemaakt, waarin 0 staat voor een “non movable tile”, de nummers 1 worden gerandomized naar 1, 2 of 3, terwijl de nummers 2 gerandomized worden naar 4, 5 en 6. De nummers staan daarna gelijk aan een symbool volgens onderstaande code. Er wordt een object gemaakt met als eigenschappen de grootte, de x en y coordinaten en de tileNumber die het symbool aangeeft. Deze wordt met behulp van een 2d array in de tilegrid gedaan.
if (tileNumber == 6) {
image = gameManager.getImage("Poisonous Golden Dagger");
} else if (tileNumber == 5) {
image = gameManager.getImage("Golden Dagger");
} else if (tileNumber == 4) {
image = gameManager.getImage("Dagger");
} else if (tileNumber == 3) {
image = gameManager.getImage("Bow");
} else if (tileNumber == 2) {
image = gameManager.getImage("Shield");
} else if (tileNumber == 1) {
image = gameManager.getImage("diamond");
}
Tileswap mechanic¶
/**
* Using targetX and targetY, the x and y coordinates of the position that the player has pressed,
* we can then check this position in the array and use the tile we get from this in the moveObject method
* to start moving the tile.
*/
checkClickedOnTileGrid(targetX, targetY) {
this.targetX = targetX;
this.targetY = targetY;
for (let x = 0; x < this.#width; x++) {
for (let y = 0; y < this.#height; y++) {
let tile = this.tiles[x][y]
if (this.firstTile && tile.x == this.targetX && tile.y == this.targetY) {
this.firstObjectX = tile.x;
this.firstObjectY = tile.y;
this.firstTile = false;
break;
}
if (!this.firstTile && tile.x == this.targetX && tile.y == this.targetY) {
this.secondObjectX = tile.x;
this.secondObjectY = tile.y;
this.firstTile = true;
this.moveObject();
}
}
}
}
/**
* Checks if the tilenumber is 0, which is the nonMovableTile. The method then checks if the pieces are adjacent and if, so it switches the tiles.
*/
moveObject() {
const firstTileNumber = this.tiles[this.firstObjectX][this.firstObjectY].getTileNumber();
const secondTileNumber = this.tiles[this.secondObjectX][this.secondObjectY].getTileNumber();
if (firstTileNumber !== 0 && secondTileNumber !== 0) {
const distanceXCoord = Math.abs(this.tiles[this.firstObjectX][this.firstObjectY].x - this.tiles[this.secondObjectX][this.secondObjectY].x);
const distanceYCoord = Math.abs(this.tiles[this.firstObjectX][this.firstObjectY].y - this.tiles[this.secondObjectX][this.secondObjectY].y);
// The code below checks for every part of the array that's 1 block from the first block selected.
if ((distanceXCoord === 1 && distanceYCoord === 0) || (distanceXCoord === 0 && distanceYCoord === 1)) {
// Swap tile positions in the tiles array.
const tempTile = this.tiles[this.firstObjectX][this.firstObjectY];
this.tiles[this.firstObjectX][this.firstObjectY] = this.tiles[this.secondObjectX][this.secondObjectY];
this.tiles[this.secondObjectX][this.secondObjectY] = tempTile;
// Used to update the tile positions
this.tiles[this.firstObjectX][this.firstObjectY].x = this.firstObjectX;
this.tiles[this.firstObjectX][this.firstObjectY].y = this.firstObjectY;
this.tiles[this.secondObjectX][this.secondObjectY].x = this.secondObjectX;
this.tiles[this.secondObjectX][this.secondObjectY].y = this.secondObjectY;
}
this.timer = this.timerDefault
this.turnUsed = true;
}
}
De checkClickedOnTileGrid functie zorgt ervoor dat een tile geselect kan worden, als vervolgens een 2e tile wordt aangevinkt die direct naast de selected tile is worden ze geswapt met de moveObject functie. De 1e tile wordt opgeslagen in een tijdelijke variabele waarna de 1e tile vervangen wordt met de 2e tile, de 2e tile wordt hierna vervangen met de tijdelijke variabele waarna ze verwisselt zijn. Zie ook de p5js wiki voor uitleg over bijvoorbeeld tijdelijke waardes en en memory management.
Swap delay¶
countdownTimer() {
this.timer -= 1
if (this.timer <= 0) {
this.checkForMatches()
this.timer = this.timerDefault
this.turnUsed = false;
}
}
countdownTimerAfterCheck() {
this.timer2 -= 0.7
if (!this.secondcheck && this.timer2 <= 0) {
this.checkForMatches()
this.timer2 = this.timerDefault
this.secondcheck = true;
}
if (this.secondcheck && this.timer2 <= 0) {
this.checkForMatches()
this.timer2 = this.timerDefault
this.turnEnded = false;
this.secondcheck = false;
}
}
countdownTimer() {
this.timer -= 1
if (this.timer <= 0) {
this.checkForMatches()
this.timer = this.timerDefault
this.turnUsed = false;
}
}
De code hierboven zorgt ervoor dat er na het swappen een kleine rust is voordat er voor matches wordt gecheckt. Op deze manier krijgt de speler het duidelijk te zien en weet de speler het ook als het maken van de match een domino effect veroorzaakt van meer matches. Dit gebeurt door een timer die even wacht en daarna pas de checkForMatches functie aanroept. We hebben de code op de onderstaande website bekeken om de timer functie goed werkend te krijgen. Link
Vinden van matches¶
checkForMatches() {
this.checkHorizontalMatches();
this.checkVerticalMatches();
}
checkHorizontalMatches() {
for (let x = 0; x < this.tiles.length-2; x++) {
for (let y = 0; y < this.tiles[0].length; y++) {
if(this.tiles[x][y].tileNumber == this.tiles[x+1][y].tileNumber && this.tiles[x][y].tileNumber == this.tiles[x+2][y].tileNumber) {
var matchLength = 3;
if(x != this.tiles.length - 3) {
if(this.tiles[x][y].tileNumber == this.tiles[x+3][y].tileNumber) {
var matchLength = 4;
}
if(x != this.tiles.length - 4) {
if(this.tiles[x][y].tileNumber == this.tiles[x+4][y].tileNumber) {
var matchLength = 5;
}
}
}
this.removeHorizontalLane(this.tiles[x][y].x, this.tiles[x][y].y, matchLength);
score += 10;
this.timer2 = this.timerDefault
this.turnEnded = true;
}
}
}
}
checkVerticalMatches() {
for (let x = 0; x < this.tiles.length; x++) {
for (let y = 0; y < this.#height - 2; y++) {
if(this.tiles[x][y].tileNumber == this.tiles[x][y+1].tileNumber && this.tiles[x][y].tileNumber == this.tiles[x][y+2].tileNumber) {
var matchLength = 3;
if(y != this.#height - 3) {
if(this.tiles[x][y].tileNumber == this.tiles[x][y+3].tileNumber) {
var matchLength = 4;
}
if(y != this.#height && y != this.#height - 4) {
if(this.tiles[x][y].tileNumber == this.tiles[x][y+4].tileNumber) {
var matchLength = 5;
}
}
}
for(let n = 0; n < matchLength; n++) {
this.removeVerticalLane(this.tiles[x][y].x, this.tiles[x][y].y + matchLength - 1, matchLength);
}
score += 10;
this.timer2 = this.timerDefault
this.turnEnded = true;
}
}
}
}
Weghalen van rijen¶
removeHorizontalLane(x, y, i) {
for(let m = x; m < x + i; m++) {
for(let n = y; n > 0; n--) {
if(this.tiles[m][n-1].tileNumber != 0 && this.tiles[m][n].tileNumber != 0) {
this.tiles[m][n] = new NormalTile(this.tileSize, m, n, this.tiles[m][n-1].tileNumber);
}
else if(this.tiles[m][n-2].tileNumber != 0 && this.tiles[m][n].tileNumber != 0) {
this.tiles[m][n] = new NormalTile(this.tileSize, m, n, this.tiles[m][n-2].tileNumber);
}
}
for(let n = 0; n < this.#width; n++) {
this.tiles[m][0] = new NormalTile(this.tileSize, m, 0, Math.floor(random(1, 6)));
}
}
}
removeVerticalLane(x, y, i) {
for(let n = y; n > 0; n--) {
if(this.tiles[x][n].tileNumber != 0 && this.tiles[x][n-1].tileNumber != 0) {
this.tiles[x][n] = new NormalTile(this.tileSize, x, n, this.tiles[x][n-1].tileNumber);
}
else if(this.tiles[x][n].tileNumber != 0 && this.tiles[x][n-2].tileNumber != 0) {
this.tiles[x][n] = new NormalTile(this.tileSize, x, n, this.tiles[x][n-2].tileNumber);
}
}
this.tiles[x][0] = new NormalTile(this.tileSize, x, 0, Math.floor(random(1, 6)));
}
Swipe controls¶
function handleSwipeAction() {
let differenceX = endPosX - startPosX;
let differenceY = endPosY - startPosY;
// Max distance swiped in between 2 connected tiles (Left end to right end) still checks for the closest tile connection.
if (differenceX < 160 && differenceX > 80) {
differenceX = 80;
} else if (differenceY < 160 && differenceY > 80) {
differenceY = 80;
} else if (differenceX > -160 && differenceX < -80) {
differenceX = -80;
} else if (differenceY > -160 && differenceY < -80) {
differenceY = -80;
}
// if the differences aren't 0 (they can be negative as well), and the differences are below 81 (so 80 and lower)
if ((!differenceX == 0 || !differenceY == 0) &&
(differenceX < 81 && differenceY < 81) && (differenceX > -81 && differenceY > -81)) {
const firstTile = tileGrid.getTileAtPosition(createVector(startPosX, startPosY));
const secondTile = tileGrid.getTileAtPosition(createVector(endPosX, endPosY));
tileGrid.checkClickedOnTileGrid(firstTile.x, firstTile.y);
tileGrid.checkClickedOnTileGrid(secondTile.x, secondTile.y);
}
}
document.addEventListener('mousedown', e => {
isClick = true; // Sets isClick to true to change later if the mouse moves too much after holding down the mouse button.
startPosX = e.clientX;
startPosY = e.clientY;
});
// If the mouse has moved more then the specified number below (5), the mousedown counts as a click.
document.addEventListener('mousemove', e => {
if (Math.abs(e.clientX - startPosX) > 5 || Math.abs(e.clientY - startPosY) > 5) {
isClick = false;
}
});
document.addEventListener('mouseup', e => {
endPosX = e.clientX;
endPosY = e.clientY;
if (isClick) { // if the click is a normal click and not a swipe, it uses the normal way to move tiles.
const tile = tileGrid.getTileAtPosition(createVector(e.clientX, e.clientY));
tileGrid.checkClickedOnTileGrid(tile.x, tile.y);
} else { // If it's a swipe it uses the function below.
handleSwipeAction();
}
isClick = false;
});
document.addEventListener('touchstart', e => {
isClick = true; // Sets isClick to true to change later if the player touch moves too much after holding down on the screen.
startPosX = e.touches[0].clientX;
startPosY = e.touches[0].clientY;
});
document.addEventListener('touchmove', e => {
// If the touch moves significantly, consider it as a swipe
if (Math.abs(e.touches[0].clientX - startPosX) > 5 || Math.abs(e.touches[0].clientY - startPosY) > 5) {
isClick = false;
e.preventDefault();
}
}, {passive: false} );
document.addEventListener('touchend', e => {
endPosX = e.changedTouches[0].clientX;
endPosY = e.changedTouches[0].clientY;
if (isClick) { // if the touch isn't a swipe but just a touch, it uses a different function to move the tiles.
const tile = tileGrid.getTileAtPosition(createVector(e.changedTouches[0].clientX, e.changedTouches[0].clientY));
tileGrid.checkClickedOnTileGrid(tile.x, tile.y);
e.preventDefault();
} else { // if it's a swipe it uses the function below.
handleSwipeAction();
}
isClick = false; // reset the isClick variable
});
Toggleable fps counter en start game button¶
function displayPreGameScreen() {
displayStartButton();
displayFPSButton();
}
function displayStartButton() {
startButton = createButton("Start game");
startButton.position(windowWidth / 2 - startButtonWidth / 2, 400);
startButton.size(startButtonWidth, startButtonHeight);
startButton.mousePressed(startGame);
startButton.style("background-color", "black");
startButton.style("color", "white");
}
function displayFPSButton() {
fpsButton = createButton("Display FPS");
fpsButton.position(windowWidth / 2 - startButtonWidth / 2, 600);
fpsButton.size(startButtonWidth, startButtonHeight);
fpsButton.mousePressed(toggleFPS);
fpsButton.style("background-color", "black");
fpsButton.style("color", "white");
}
function toggleFPS() {
if(showFPS) {
showFPS = false;
document.getElementById("fps").innerHTML = "";
}
else {
showFPS = true;
}
}
function calculateFPS() {
delta = (Date.now() - lastCalledTime) / 1000;
lastCalledTime = Date.now();
fps = 1 / delta;
fps = floor(fps);
}
function displayFPS() {
if(frameCount % 15 == 0) { // updates the fps 4 times a second
document.getElementById("fps").innerHTML = fps;
}
}
function startGame() {
mode = 1;
}
De fps wordt uitgerekent met behulp van tijdverschillen om het gemiddelde uit te rekenen, dit wordt 4 keer per seconde geupdate om het leesbaar te maken. (Anders verandert de FPS te snel om te lezen)
Als de startButton wordt aangeklikt, wordt de startGame functie gecalled en wordt de mode veranderd naar 1. Mode 0 is het beginscherm met de start button en fps button, mode 1 bevat ook het speelveld. We hebben hiervoor de volgende bronnen gebruikt:
Github Growing With The Web p5js references