Concept
For this project, I created a Tetris. Tetris is a simple game, in which users move or rotate blocks to fill a game field. Once a horizontal line is created with blocks, blocks on the line will be removed and the user will gain a certain amount of points. Users will gain points if:
- Removes blocks by making a horizontal line of blocks.
- Uses hard drop
- Extra points if multiple lines are removed at once
Although I am using a high-resolution screen, I wanted to give a “retro” appearance to the game (8bit). To achieve this effect, I used 8-bit style fonts and images.
Code
Coding the game was indeed complex because the game required not only game logic, which I partially implemented, but also a friendly user interface. For this reason, I used 3 js. files, each responsible of running the code (sketch.js), displaying game interface (Page.js), and running game (Game.js).
Before making an actual “gaming” part of the project, I made the game interface of the game first. For this project, I used Press Start 2P font downloaded from https://fonts.google.com/specimen/Press+Start+2P.
1. sketch.js
<2022/10/3> UPDATE 1
This file manages how different pages will be displayed based on the user input. Depending on the user input, the game will show the main page, game page, instruction page, interrupt page, or game.
function draw() { background(0); if (!escapePressed) { //if user did not press ESC if (currentPageIndex == 0) { //MAIN PAGE displayMain(); if (mySelectorMain != null) { //draw selector object mySelectorMain.drawSelector(); } else { //if selector is new, create new selector object mySelectorMain = new selector(150, 340, 21); } } else if (currentPageIndex == 1) { //START GAME selected startGame(); //start game based on Game.js } else if (currentPageIndex == 2) { //HOW TO PLAY selected displayInstructions(); //display instructions } } else { if (currentPageIndex == 1) { //when user is in the middle of the game displayReturn("EXIT GAME AND RETURN TO MAIN?"); if (mySelectorESC != null) { //draw selector object mySelectorESC.drawSelector(); } else { //if selector is new, create new selector object mySelectorESC = new selector(230, 310, 21); } } else if (currentPageIndex == 2) { //when user is reading instructions displayReturn("QUIT READING AND RETURN TO MAIN?"); if (mySelectorESC != null) { //draw selector object mySelectorESC.drawSelector(); } else { //if selector is new, create new selector object mySelectorESC = new selector(230, 310, 21); } } } }
Code above will display appropriate pages based on the current index of the game and ESC input(0: MAIN PAGE, 1: GAME, 2: INSTRUCTION). If ESC is pressed, the game will display RETURN TO HOME PAGE with appropriate texts.
//When keyboard input is given function keyPressed() { if (keyCode == DOWN_ARROW) { if (currentPageIndex == 0) { /*Main page. Move selector accordingly. Selector will move to HOW TO PLAY when it was previously at START GAME, and vice versa*/ if (mySelectorMain.y == 340) mySelectorMain.y = 400; else mySelectorMain.y = 340; } else if ( (currentPageIndex == 1 || currentPageIndex == 2) && escapePressed ) { /*if ESC is pressed in the middle of the game or in the instruction, move selector accordingly.Selector will move to NO when it was previously at YES, and vice versa*/ if (mySelectorESC.y == 310) mySelectorESC.y = 370; else mySelectorESC.y = 310; } } if (keyCode == UP_ARROW) { if (currentPageIndex == 0) { /*Main page. Move selector accordingly. Selector will move to HOW TO PLAY when it was previously at START GAME, and vice versa*/ if (mySelectorMain.y == 340) mySelectorMain.y = 400; else mySelectorMain.y = 340; } else if ( (currentPageIndex == 1 || currentPageIndex == 2) && escapePressed ) { /*if ESC is pressed in the middle of the game or in the instruction, move selector accordingly.Selector will move to NO when it was previously at YES, and vice versa*/ if (mySelectorESC.y == 310) mySelectorESC.y = 370; else mySelectorESC.y = 310; } } if (keyCode == ENTER) { if (currentPageIndex == 0) { //user selects START GAME. Change currentPageIndex to 1 if (mySelectorMain.y == 340) currentPageIndex = 1; //user selects HOW TO PLAY. Change currentPageIndex to 2 else currentPageIndex = 2; } else if ( (currentPageIndex == 1 || currentPageIndex == 2) && escapePressed ) { /*ESC is pressed in the middle of the game or in the instruction. If user selects YES, change currentPageIndex to 0 (returning to MAIN PAGE); if user selects NO, simply return to the current page*/ if (mySelectorESC.y == 310) { //YES currentPageIndex = 0; escapePressed = false; } else { //NO //user selects NO escapePressed = false; } } //Reset selector objects' position (DEFAULT: First option) mySelectorMain = null; mySelectorESC = null; } //If ESC is pressed, change escapePressed to true; if (keyCode == ESCAPE) { if (currentPageIndex != 0) escapePressed = true; } }
Code above deals with user inputs and how the game will react to each input. For this version of the game, user has an option to select different options using up/down arrows and enter. Variables with name including “selector” are objects of triangles that is shown next to the options. It will be further explained in PAGE.JS section.
2. Page.js
<2022/10/3> UPDATE 1
This file manages how each page will visually displayed to the user. It is where all positions and alignments of window/texts are shown. The page starts with preload() to load fonts and images that will be used through out the game.
function preload() { font = loadFont("assets/8Bit.ttf"); //load textfont to p5js blockImg = loadImage("assets/block.png"); //load single block unit image to p5js }
Most of the codes in this page manages positions and alignments of the visual elements for each page (MAIN, GAME, INSTRUCTION, and INTTERUPTION).
//MAIN Page function displayMain() { currentPageIndex = 0; //display main title of the game: TETRIS setFontStyle(titleFontSize); text("TETRIS", width / 2, 250); //display options setFontStyle(normalFontSize); text("START GAME", width / 2, 350); text("HOW TO PLAY", width / 2, 410); } //Instruction Page function displayInstructions() { setFontStyle(titleFontSize - 40); text("HOW TO PLAY", width / 2, 95); createWindow(width / 2, 320, 530, 370); setFontStyle(normalFontSize); text( "↑ : ROTATE BLOCKS\n\n\n← : MOVE BLOCKS LEFT\n\n\n→ : MOVE BLOCKS RIGHT\n\n\n↓ : SOFT DROP\n\n\nSPACE : HARD DROP", width / 2, 180 ); setFontStyle(normalFontSize - 7); if (millis() - currentMillis > 800) { currentMillis = millis(); blink = !blink; } if (blink) { fill(0); stroke(0); } else { fill(255); stroke(255); } text("PRESS ESC TO RETURN", width / 2, 560); } //Game function displayGameBackground(score, level, lines) { createWindow(width / 4 + 25, height / 2, width / 2 + 6, height - 60 + 6); setFontStyle(normalFontSize); text("——— NEXT ———", 462.5, 50); //Next blocks createWindow(400, 125, 106, 106); createWindow(525, 125, 106, 106); //Score, level, line cleared createWindow(462.5, 385, 231, 376); setFontStyle(normalFontSize); text("SCORE", 462.5, 245); text(score, 462.5, 290); //display score text("LEVEL", 462.5, 358.3); text(level, 462.5, 403.3); //display level of the round text("LINES", 462.5, 481.7); text(lines, 462.5, 526.7); //display total # of lines cleared in the round } //Return to Main Question Page function displayReturn(_text) { //Create window createWindow(width / 2, height / 2, 400, 300); setFontStyle(normalFontSize); //Text Question text(_text, width / 2, 220, 350); //Options text("YES", width / 2, 320); text("NO", width / 2, 380); } //default setting of the font function setFontStyle(size) { strokeWeight(1); textFont(font); //set font style fill(255); textWrap(WORD); //wrap text textAlign(CENTER); //align text to center textSize(size); //change text size } //create a rectangular window where texts will be displayed function createWindow(x, y, w, h) { fill(0); stroke(255); //white border strokeWeight(6); //border weight of the rectangle rectMode(CENTER); //rectangle is centered rect(x, y, w, h); }
setFontStlye(size) and createWindow(x,y,w,h) are functions, where size is textSize, x and y are coordinates of the rectangle’s/window’s center, and w and h are width and height of the window.
3. Game.JS
<2022/10/3> UPDATE 1
This file contains all the game logics and runs Tetris game. The game consists of 2 main parts: block objects and game field array. Block objects are used to display blocks in motion. Game field array is what the program use to visualize the game. Once blocks cannot further be moved, it will be stored into game field array, so no further calculations will be needed in the future.
Game logic follow procedures listed below:
- Create an empty game field array. (0: empty space, 1: something is present)
board = []; for (let i = 0; i < boardHeight + 1; i++) { board[i] = []; for (let j = 0; j < boardWidth; j++) { board[i][j] = 0; } }
- User selects START GAME. Game Starts based on SCRIPT.JS
else if (currentPageIndex == 1) { //START GAME selected startGame(); }
- Load background layout of the game (shapes and texts) based on PAGE.JS
function startGame() { displayGameBackground(0, 1, 0, 1); //Code is partially shown for demonstration }
- Generate, display, and move block object. Types of the block is randomly generated from the array that contains 7 different block types. Up until now, block will move only vertically down every time interval. When the block reaches bottom, or cannot be further moved, a new random block will be generated.
let blockTypeArr = ["I", "J", "L", "O", "S", "T", "Z"]; //Block Type Array //GAME function startGame() { displayGameBackground(0, 1, 0, 1); //display background interface if (generateBlock) { //If new block is needed, create a random shaped block myBlock = new block(int(random(0, 11)), 10, random(blockTypeArr), 4); generateBlock = false; } myBlock.drawBlock(); myBlock.blockDown(); displayBoard(); }
Most of the game logic is handled by block class below. For more, read the commented lines.
//block object for tetris blocks class block { constructor(_x, _y, _type, speed) { this.x = _x; this.y = _y; this.type = _type; this.shapeArr = []; this.speed = 1; } /*this class method will fill shapeArr based on the type of the block. shapeArr is a 4*4 array, in which 0: empty, 1: block*/ shapeType(_type) { let shapeArr = []; /*example L block 0 0 0 0 0 1 0 0 0 1 0 0 0 1 1 0*/ for (let i = 0; i < 4; i++) { shapeArr[i] = [0, 0, 0, 0]; } if (_type == "I") { shapeArr[0][0] = 1; shapeArr[1][0] = 1; shapeArr[2][0] = 1; shapeArr[3][0] = 1; } else if (_type == "J") { shapeArr[1][2] = 1; shapeArr[2][2] = 1; shapeArr[3][1] = 1; shapeArr[3][2] = 1; } else if (_type == "L") { shapeArr[1][1] = 1; shapeArr[2][1] = 1; shapeArr[3][1] = 1; shapeArr[3][2] = 1; } else if (_type == "O") { shapeArr[2][1] = 1; shapeArr[2][2] = 1; shapeArr[3][1] = 1; shapeArr[3][2] = 1; } else if (_type == "S") { shapeArr[2][1] = 1; shapeArr[2][2] = 1; shapeArr[3][0] = 1; shapeArr[3][1] = 1; } else if (_type == "T") { shapeArr[2][1] = 1; shapeArr[3][0] = 1; shapeArr[3][1] = 1; shapeArr[3][2] = 1; } else if (_type == "Z") { shapeArr[2][1] = 1; shapeArr[2][2] = 1; shapeArr[3][2] = 1; shapeArr[3][3] = 1; } this.shapeArr = shapeArr; } //Checks if block can be moved in a given direction validMove(dX, dY) { for (let i = 0; i < 4; i++) { for (let j = 0; j < 4; j++) { if ( /*ignores all empty spaces. 1s must always be within the boundary of game field and must not overlap with non-empty, or 1s, when moved*/ this.shapeArr[i][j] == 1 && (board[this.y + i + 1][this.x + j] != 0 || this.y + i + dY >= boardHeight || this.x + j + dX < 0 || this.x + j + dX >= boardWidth) ) { return false; } } } return true; } //Move block down for every time interval blockDown() { if (this.validMove(0, 1)) { if (millis() - counter > 400 / this.speed) { this.y++; counter = millis(); } } else { //Once block cannot be moved down further, update game field for (let i = 0; i < 4; i++) { for (let j = 0; j < 4; j++) { if (this.shapeArr[i][j] == 1) board[this.y + i][this.x + j] = 1; } } //generate new block generateBlock = true; } } //Move Block left or right. left when direction is -1, right when direction is 1 blockLeftRight(dX) { if (direction == -1) { } } //draw blocks. for each 1s in the shapeArr, block image will be placed drawBlock() { this.shapeType(this.type); blockImg = blockImg.get(0, 0, 20, 20); push(); translate(25, 30); for (let i = 0; i < 4; i++) { for (let j = 0; j < 4; j++) { if (this.shapeArr[i][j] == 1) image(blockImg, blockSize * (this.x + j), blockSize * (this.y + i)); } } pop(); } } //visually display the gameboard. function displayBoard() { blockImg = blockImg.get(0, 0, 20, 20); push(); translate(25, 30); for (let i = 0; i < boardHeight; i++) { for (let j = 0; j < boardWidth; j++) { if (board[i][j] == 1) image(blockImg, blockSize * j, blockSize * i); } } pop(); }
Future Improvements
There are tons of more things to implement in this game:
-
- Moving blocks horizontally using key inputs.
- Rotating blocks using key inputs.
- This means more logic must be added to the movement validation method.
- Add conditions that will check if game is over or not.
- Add conditions that will check if user had made a horizontal line using blocks. Then, the program must be able to remove that line and shift whole game filed down by the number of removed lines unit(s).
- The game will not reset even if user returns to the main page in the middle of the game. This can be fixed by resetting every variable at the start of each game.