concept:
The midterm of this course required us to implement everything we had learned, ranging from the simplicity of shapes to the complexity of object-oriented programming. I intended to provide the skills I learned throughout these past seven weeks while also implementing my touch of art and creativity. That being said, this required me to code a playable game and make it aesthetically pleasing in its own art style. I chose to aim towards a retro-pixelated black-and-white art style that would make the user delve into a nostalgic experience when playing the game. I decided to get inspired by the most prominent game in video game history, snake. However, in my game (germ-worm), the player is in a black-and-white world where they control a worm in need of collecting germs to reach the highest possible score while exploring the setting available to play, making it a more complex or more manageable experience.
code:
// HAMAD ALSHAMSI // GERM WORM: MIDTERM PROJECT // code //game state variable var mode = 0; //set object/player variables let worm = []; //variables for worm and player let player; let germ; //set game's rules variables let gameExtent = 20; let edgeExtent = 1; let collectibles = 10; let playState = true; let direction, frameChange, scoreCount; let portionValue, collectiblesValue, frameValue; let portionSlider, lengthSlider, multiplierSlider, speedSlider; //classify worm portions and class Portion { constructor(x, y) { this.x = x; this.y = y; this.wormLength = gameExtent - edgeExtent * 2; } //used to test if player is offscreen testEdges() { return this.x < 0 || this.x > width || this.y < 0 || this.y > height; } //used to test player self-collision testWorm() { for (let portio of worm) { if (this.x == portio.x && this.y == portio.y && direction) return true; } return false; } //used to test player germ-collision testGerm() { return this.x == germ.x && this.y == germ.y; } //update position to player's direction input update() { switch (direction) { case "up": player.y -= gameExtent; break; case "right": player.x += gameExtent; break; case "down": player.y += gameExtent; break; case "left": player.x -= gameExtent; break; } //test offscreen or self-collision, if so, lose. else test germ-collision if (this.testEdges() || this.testWorm()) { //implemet pixelated font textFont(fontPixel); fill(60); push(); translate(width / 2, height / 2); textSize(52); text("GAME OVER", -150, 0); textSize(18); text("press ' SPACE ' to restart", -134, 30); pop(); playState = false; } else if (this.testGerm()) germ.consume(); } //first worm section drawn using image asset alongside a white background create() { noStroke(); fill(255); image(wormIMG, this.x, this.y, this.wormLength, this.wormLength); } } //classify germs class Germ { constructor(x, y) { this.x = x; this.y = y; this.wormLength = gameExtent - edgeExtent * 2; this.collectibles = 2; } //when worm collides with germ, increase length. then pick random spot for new germ consume() { collectibles += this.collectibles; this.x = floor(random(width / gameExtent)) * gameExtent + edgeExtent; this.y = floor(random(height / gameExtent)) * gameExtent + edgeExtent; } //draw germ in new spot using image from asset create() { image(germIMG, this.x, this.y, this.wormLength, this.wormLength); } } //load images from asset file function preload() { menuIMG = loadImage("assets/menuImage.png"); germIMG = loadImage("assets/germImage.png"); wormIMG = loadImage("assets/wormImage.png"); fontPixel = loadFont("assets/PixelDigivolve-mOm9.ttf"); } function setup() { //define default game state mode = 0; //center shapes and images rectMode(CENTER); imageMode(CENTER); //set canvas size createCanvas(400, 600); //create new germ let x = floor(random(width / gameExtent)) * gameExtent + edgeExtent; let y = floor(random(height / gameExtent)) * gameExtent + edgeExtent; germ = new Germ(x, y); //view score counter scoreCount = createP( " S  C  O  R  E  :  1  " ); scoreCount.style("color", "white"); scoreCount.style("font-size", "15px"); scoreCount.style("background-color", "#5c5c5c"); scoreCount.position(5, 560); //worm size slider portionValue = createP("size: 20"); portionValue.position(50, 610); portionValue.style("font-size", "15px"); portionValue.style("margin:2px"); portionValue.style("color", "#5c5c5c"); portionSlider = createSlider(10, 50, 20, 10); portionSlider.position(10, 630); //worm length slider lengthP = createP("length: 0"); lengthP.position(50, 660); lengthP.style("font-size", "15px"); lengthP.style("margin:2px"); lengthP.style("color", "#3c3c3c"); lengthSlider = createSlider(1, 30, 1, 1); lengthSlider.position(10, 680); //score per germ slider collectiblesValue = createP("multiplier: 1"); collectiblesValue.position(290, 610); lengthP.style("font-size", "15px"); collectiblesValue.style("margin:2px"); collectiblesValue.style("color", "#3c3c3c"); multiplierSlider = createSlider(1, 10, 1, 1); multiplierSlider.position(270, 630); //frames per second slider frameValue = createP("speed: 8"); frameValue.position(300, 660); lengthP.style("font-size", "15px"); frameValue.style("margin:2px"); frameValue.style("color", "#3c3c3c"); speedSlider = createSlider(1, 20, 8, 1); speedSlider.position(270, 680); //set default framerate frameRate(8); //prnt rules in console.log print( "arrow keys --> MOVE. \nspace --> RESTART. \nclick --> START." ); } function keyPressed() { //different arrows correspond to different directions if (frameChange) { switch (key) { case "ArrowUp": if (direction != "down") direction = "up"; break; case "ArrowRight": if (direction != "left") direction = "right"; break; case "ArrowDown": if (direction != "up") direction = "down"; break; case "ArrowLeft": if (direction != "right") direction = "left"; break; case " ": player = new Portion(width / 2 + edgeExtent, height / 2 + edgeExtent); germ.x = floor(random(width / gameExtent)) * gameExtent + edgeExtent; germ.y = floor(random(height / gameExtent)) * gameExtent + edgeExtent; //display score scoreCount.html( " S  C  O  R  E  :  1  " ); direction = undefined; worm = []; playState = true; break; } frameChange = false; } } //game state display function draw() { if (mode == 0) { menuGame(); } else if (mode == 1) { startGame(); } } //command lines responsible before the game starts function menuGame() { background(255); push(); translate(width / 2, height / 2); image(menuIMG, 0, 0); pop(); } //command lines responsible for when the game starts function startGame() { frameChange = true; //update information on sliders accordingly portionValue.html( portionValue.html().split(" ")[0] + " " + portionSlider.value() ); lengthP.html(lengthP.html().split(" ")[0] + " " + lengthSlider.value()); collectiblesValue.html( collectiblesValue.html().split(" ")[0] + " " + multiplierSlider.value() ); frameValue.html(frameValue.html().split(" ")[0] + " " + speedSlider.value()); if (!direction) { //if game not run, update using sliders info background(255); gameExtent = portionSlider.value(); edgeExtent = gameExtent / 20; collectibles = lengthSlider.value() - 1; frameRate(speedSlider.value()); player = new Portion( floor(width / gameExtent / 2) * gameExtent + edgeExtent, floor(height / gameExtent / 2) * gameExtent + edgeExtent ); germ.x = floor(random(width / gameExtent)) * gameExtent + edgeExtent; germ.y = floor(random(height / gameExtent)) * gameExtent + edgeExtent; germ.wormLength = gameExtent - edgeExtent * 2; germ.collectibles = multiplierSlider.value(); player.create(); } else if (playState) { background(255); //copies player into a worm array and replaces index to show worm moving motion worm.push(new Portion(player.x, player.y)); //if germ is present, erase it. else kill worm if (collectibles) collectibles--; else worm.shift(); //update game scores and display player.update(); player.create(); germ.create(); for (let portio of worm) portio.create(); scoreCount.html( " S  C  O  R  E  :  " + (worm.length + 1) + "  " ); } } //launch game by clicking function mousePressed() { if (mode == 0) { launchGame(); } } function launchGame() { mode = 1; } // references // game rule variables inspired from "GeoCrafter57" on YouTube // color palette from "https://colorhunt.co/palettes/grey" // images from "https://www.alamy.com/stock-photo/star-pixel-video-game-play.html"
method:
Initially, having an object, a worm, move through the screen in a manner that is up, down, right, and left was an essential aspect of the game. Then, it was necessary to include a collectible germ that would also serve as a point system. These goals were managed by classifying both the ‘worm’ and ‘germ’ and implementing them into arrays and indexes to stylize further the motion presenting an illusion of a worm-like movement. Then, adding a difficulty aspect where the user can freely adjust the difficulty level was an enjoyable feature, as it required sliders, a concept never delved into before. Finally, adding menu and end screens containing instructions would be the icing on the cake. To do that, I fully customized an entire image in Photoshop that compliments the pixelated theme present in the game. These images possessed easy-to-follow instructions such as an icon of arrows with “move,” a space bar with “restart, and a click with “start” phrases next to them.
sketch:
The following pictures preview the menu, game, and end screens of the game. Additionally, images of the gameplay with the various rules are previewed to present the difficulty range offered.
menu screen
gameplay screen
end screen
future improvements:
Presenting a worm-customization aspect allowing the user to customize the shape of the worm would be a fun and interactive feature. Additionally, adding a high-score element to possibly implement a competitive aspect to the game would be extremely fun.