germ worm

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(
    "&nbspS &nbspC &nbspO &nbspR &nbspE &nbsp: &nbsp1 &nbsp"
  );
  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(
          "&nbspS &nbspC &nbspO &nbspR &nbspE &nbsp: &nbsp1 &nbsp"
        );
        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(
      "&nbspS &nbspC &nbspO &nbspR &nbspE &nbsp: &nbsp" +
        (worm.length + 1) +
        " &nbsp"
    );
  }
}

//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.

Leave a Reply