Concept
‘Save the Ocean’ is an adaptation of the famous snakes game but with a new concept. It aims to raise awareness of the plastic pollution in our oceans around the world and with this intent in mind, I developed this game where the player controls a diver (instead of a snake) and uses arrow keys on the keyboard to move around the ocean – the canvas. The diver’s mission is to collect as much plastic waste as possible while avoiding obstacles – the monsters.
The Game
Fullscreen view here: https://editor.p5js.org/ramshabilal/full/uDXF9FUDA
Key elements of the game
Start Page
The game has a start page that shows the cover of the game along with a start button, which, when pressed moves onto the instructions page. Game music also begins playing on this page. An image I designed for the game is used as a background while the button is added using shapes and text.
Instructions Page
With an online image as a background, this page highlights the key information and instructions needed to understand how the game is to be played. A button ‘Go’ when pressed takes the player to the main game page where the game is to be played.
Main Game Page
The main game page has:
- The diver: using a segment class I made, the diver object can be moved around using keyboard navigations.
- Monsters: Monsters are generated at random positions (coordinates saved in arrays) on the canvas and if the diver collides with them, 10 points are deducted from the player’s “Life” – game stops when Life gets to zero.
- Plastic Waste: These are the green plastic bottles that the user has to collect to save the ocean. Whenever plastic is collected, 10 points are added to the user’s score. At the end of the game, the high score is updated – if the user exceeds the previous high score, they break their own record. This layer is added to compel users to play again.
- Informational text: Score, high score, and life are displayed for the user to see.
Game over page
This is the page displayed when the game ends i.e. life = zero. Here, the score and high score with the corresponding message are displayed. If the user breaks their high score record, they are informed here as well. Music also changes on this page. The user can also press a button to go back to the start – the game is reset using a reset game function and the user can start again (without rerunning the code).
Difficulty
The difficulty of the game increases with time – as time passes, the game speeds up and the number of monsters in the ocean also increases.
Some of my design considerations
A color scheme was designed for the game – blue and purple were the dominant colors in this theme to create a coherent and complete look and experience for the user and to fit the “ocean” theme of the game.
Initially, this was my cover for the game:
However, to reveal more about what the game will be and to match the theme and colors, I changed it to this design I made:
Buttons changed color when the mouse hovered over them to indicate they need to be pressed.
The main music played in the game was chosen to match the ocean theme. Moreover, pop and coin sound effects were added which corresponded to the collisions with monster and plastic waste respectively.
A simple yet fun font was used uniformly throughout the game to create a coherent and fun look while keeping everything minimal and easy to follow.
Code I am proud of
As I developed the game, unique challenges popped up that required creative solutions. Developing these solutions was the part I was most proud of as it required problem-solving.
One example is when I wanted to create more than one monster at a time as opposed to the one monster-at-a-time thing I had initially. For this, I created arrays to store random coordinates and loops to create the monsters.
function createMonsters(){
//for every monster, update its speed and if it collides with the walls, reverse their direction of motion
for (let i = 0; i < numOfMonsters; i++){
image(monsterImg, monsterXs[i], monsterYs[i], monsterWidth, monsterHeight);
monsterXs[i] += monsterSpeeds[i];
if (monsterXs[i] < 0 || monsterXs[i]+monsterWidth > width)
{
monsterSpeeds[i] = -monsterSpeeds[i];
}
}
}
Another challenge I faced was that when the user would play again, the conditions would not reset. For this, firstly i introduced game states to smoothly switch between different stages of the game and also created a reset page function to refresh the game to start conditions.
function stopPage(){
background('rgb(100,229,255)');
image(gameOverImg, 0,0,width,height);
fill('white');
textAlign(CENTER);
textFont(scoreFont);
textSize(24);
if (score > highscore)
{
print('score is ' + score + ' highscore is ' + highscore);
text('Congratulations. You helped free the ocean of harmful plastic and you just beat your previous highscore!', 0, height/2, width);
fill('gold')
text('New Highscore = ' + score, 10, height*2/3, width);
}
else
{
fill('white');
text('Congratulations. You helped free the ocean of harmful plastic!',10, height/2, width);
text('Score = ' + score + '\n\nHighscore = '+ highscore, 10, height*2/3, width);
}
fill('red')
ellipse(width/2, 530,200,80);
fill('white')
text('Play Again', 295, 538);
if (mouseX > width/2-100 && mouseX < width/2+100 && mouseY > 530-40 && mouseY < 530+40){
fill('gold');
ellipse(width/2, 530,200,80);
fill('white')
text('Play Again', 295, 538);
if (mouseIsPressed){
reset = true;
gameOverSong.stop();
gameState='start';
}
}
}
function resetGame(){
gameOverSong.stop();
song.loop();
life =100;
if (score > highscore){
highscore = score;
}
score = 0;
startPlaying=false;
x1=0;
x2=width;
direction='start';
newSeg.setX(width/2);
newSeg.setY(height/2);
for (i = numOfMonsters; i > 2;i--)
{
monsterSpeeds.splice(i, 1);
monsterXs.splice(i, 1);
monsterYs.splice(i, 1);
}
numOfMonsters = 3;
step =3;
for (i =0; i<numOfMonsters; i++)
{
monsterSpeeds[i] = random(-2,2);
monsterXs[i] = random(0,width);
monsterYs[i] = random(0,height);
}
}
Also, while resetting, to go back to the original number of monsters, I splice their arrays keeping only some monsters and reset their speeds to the initial range (while keeping it random).
I also used other boolean flags to control the flow of the game and as a whole, I am very proud of the entire code – functions, class, etc – that I wrote.
//seg is class for the diver and step is the speed of the diver
let newSeg, segWidth = 60, step = 3;
let direction = 'start', gameState = 'start';
//booleans to store various game states/reset etc. startPlaying checks if game level has started, reset checks if the game is started again to reset the variables, playgameover stores if the game over song should be played or not
let startPlaying = false, reset = true, playgameover = true;
//for images and songs
let img, diverImg, instrImg, bgImg, plasticImg, monsterImg, gameOverImg;
let song, gameOverSong, coinSong,powSong;
let cnv;
//for scrolling background
let x1 = 0, x2, scrollSpeed = 1;
//for the green plastic bottles
let plasticX = 100, plasticY = 200, plasticWidth =70, plasticHeight = 70;
//to save randomly chosen x and y coordinates for the monsters
let monsterXs = [], monsterYs = [];
let monsterWidth =60, monsterHeight = 60;
//initially num of monsters and array to store their randomly chosen speeds (within a range)
let numOfMonsters = 3, monsterSpeeds =[];
let highscore = 0, score = 0, life = 100;
//preload required images, songs, and fonts
function preload() {
img = loadImage('images/1.png');
song = loadSound('music.mp3');
gameOverSong = loadSound('music/gameover.mp3');
coinSong = loadSound('music/coin.mp3');
powSong = loadSound('music/pow.mp3');
bgImg = loadImage("images/game_bg.png");
plasticImg = loadImage("images/plastic.png");
monsterImg = loadImage("images/monster.png");
gameOverImg = loadImage("images/gameOverImg.png");
instrImg = loadImage("images/instructions.jpg");
diverImg = loadImage("images/diver-1.png");
regularFont ='Georgia';
scoreFont = loadFont("fonts/Sunrise.ttf");
}
//class to represent the segment/object for the diver
class Segment {
constructor(x, y){
this.x = x;
this.y = y;
noStroke();
fill(55);
ellipse(this.x, this.y, segWidth);
image(diverImg, this.x-55, this.y-segWidth, 100,80);
}
//methods to change direction of diver
moveRight(){
if (this.x > width){
this.x = 0;
}else {
this.x += step;
}
noFill();
ellipse(this.x, this.y,segWidth);
image(diverImg, this.x-55, this.y-35,100,80);
}
moveLeft(){
if (this.x < 0){
this.x = width;
}
else
{
this.x -= step;
}
//flip the image when diver moves left
noFill();
ellipse(this.x, this.y,segWidth);
push();
//flipping along x-axis to create turning effect
scale(-1,1);
image(diverImg, -(this.x+55), this.y-35,100,80);
pop();
}
moveUp(){
if (this.y < 0){
this.y = height;
}
else{
this.y -= step;
}
//fill(55);
noFill();
ellipse(this.x, this.y,segWidth);
image(diverImg, this.x-55, this.y-35,100,80);
}
moveDown(){
if (this.y > height){
this.y = 0;
}
else {
this.y += step;
}
noFill();
ellipse(this.x, this.y,segWidth);
image(diverImg, this.x-segWidth, this.y-35,100,80);
}
updatePosition()
{
if (direction === 'right')
{
this.moveRight();
}
if (direction === 'left')
{
this.moveLeft();
}
if (direction === 'up')
{
this.moveUp();
}
if (direction === 'down')
{
this.moveDown();
}
}
//get and set methods
getX(){
return this.x;
}
getY(){
return this.y;
}
blink(){
fill(random(200,255));
ellipse(this.x,this.y, segWidth);
}
setX(x){
this.x=x;
}
setY(y){
this.y=y;
}
}
//during set up, play song, initialize the x and y coordinates for monsters, and create diver segment
function setup() {
cnv = createCanvas(600, 600);
// frameRate(20);
song.loop();
//for scrolling background
x2=width;
newSeg = new Segment(width/2, height/2);
for (i =0; i<numOfMonsters; i++)
{
append(monsterXs,random(0,width));
append(monsterYs,random(0,height));
append(monsterSpeeds, random(-2,2));
}
//increase speed of monsters and diver and increment number of monsters every 20 seconds when game is playing
setInterval(increaseSpeed, 20000);
}
function draw() {
//draw according to the state of the game
//reset game whenever game is started again
if (gameState === 'start'){
if (reset === true){
resetGame();
reset = false;
}
playgameover=true; //flag to check if game over song should be played or not
startPage(); //if game started, show start page
}
else if (gameState === 'play'){
playGame();
}
//switch song when game/level is over and show the stop page
else if (gameState === 'stop'){
song.stop();
if(playgameover === true){
gameOverSong.loop();
playgameover = false;
}
stopPage();
}
}
function startPage(){
textAlign(LEFT, BASELINE);
image(img, 0, 0,600,600);
noFill();
//when mouse enters the canvas - display the start button
if (mouseX< 580 && mouseX > 20 && mouseY < 580 && mouseY > 20)
{
fill('rgba(10,108,180,0.85)');
ellipse(width*4/5, height*5/6, 150,100);
fill('white');
textFont(scoreFont);
textSize(32);
text('Start',425, 513);
}
//when mouse hovers over start button, change its color
if (mouseX < width*4/5+75 && mouseX > width*4/5-75 && mouseY < height*5/6 + 50 && mouseY > height*5/6-50)
{
fill('#6338B1');
ellipse(width*4/5, height*5/6, 170,120);
textFont(scoreFont);
textSize(38);
fill('white')
text('Start',415, 513);
//if start button is pressed, change game state
if (mouseIsPressed)
{
gameState = 'play'
}
}
fill(255, 60, 100);
}
function playGame(){
smooth();
background('rgb(100,229,255)');
showInstructions();
if (startPlaying === true){
startPlayingGame();
}
}
function showInstructions(){
background('rgb(100,117,255)')
fill(255)
square(25,25,550);
image(instrImg, 30,30,540,540);
textSize(20);
textFont(scoreFont)
fill('white');
textAlign(CENTER);
text('Instructions', width/2, 100);
textSize(18);
textAlign(LEFT,BASELINE);
text ('Welcome!\n\nToday, you are a diver. Your mission is to \ncollect as much plastic waste from the \nocean as possible to protect marine life.\n\nMove around using the left, right, up, and \ndown arrow keys on your keyboard to \ncollect trash.\n\nEvery piece of trash collected = +10 points\n\nBeware! The ocean has monsters! Life = 100. \nEach encounter with a monster = -10 life.\nCome on! Let us free the ocean from plastic \nwaste!', 70, 130, width);
ellipse(width/2,555, 80);
fill(0);
text('GO', width/2-12,560)
if (mouseX > width/2-50 && mouseX < width/2+50 && mouseY > 540-50 && mouseY < 540+50)
{
fill('rgb(221,166,252)');
ellipse(width/2,555, 80);
fill(255);
text('GO', width/2-12,560)
if(mouseIsPressed)
{
startPlaying = true;
}
}
}
function startPlayingGame()
{
//scrolling background
background('rgb(100,229,255)');
image(bgImg, x1, 0, width, height);
image(bgImg, x2,width, height);
//starting text
text('Use arrow keys to start moving', width/4, height/3);
fill(55);
ellipse(width/2, height/2, segWidth-20);
//if player starts moving, begin updating position - start game
if (direction === 'up' || direction === 'down' || direction === 'left' || direction === 'right' )
{
background('rgb(100,229,255)');
image(bgImg, x1, 0, width, height);
image(bgImg, x2, 0, width, height);
x1 -= scrollSpeed;
x2 -= scrollSpeed;
if (x1 < -width){
x1 = width;
}
if (x2 < -width){
x2 = width;
}
newSeg.updatePosition();
updatePlastic();
createMonsters();
checkPlasticCollisions();
checkMonsterCollisions();
displayScoreandLife();
checkLife();
}
}
function keyPressed() {
if (keyCode === RIGHT_ARROW) {
direction = 'right';
//print('right')
} else if (keyCode === LEFT_ARROW) {
direction = 'left';
//print('left');
} else if (keyCode === UP_ARROW) {
direction = 'up';
//print('up')
}
else if (keyCode === DOWN_ARROW) {
direction = 'down';
//print('down')
}
}
function updatePlastic(){
image(plasticImg, plasticX, plasticY, plasticWidth, plasticHeight);
}
//for every monster, update its speed and if it collides with the walls, reverse their direction of motion
function createMonsters(){
for (let i = 0; i < numOfMonsters; i++){
image(monsterImg, monsterXs[i], monsterYs[i], monsterWidth, monsterHeight);
monsterXs[i] += monsterSpeeds[i];
if (monsterXs[i] < 0 || monsterXs[i]+monsterWidth > width)
{
monsterSpeeds[i] = -monsterSpeeds[i];
}
}
}
//if plastic collected, generate a new one on the canvas at a random location and update score
function checkPlasticCollisions(){
//print(newSeg.getX(), " and ", plasticX);
if (newSeg.getX() > plasticX && newSeg.getX() < plasticX + plasticWidth && newSeg.getY() > plasticY && newSeg.getY() < plasticY + plasticHeight)
{
coinSong.play();
score += 10;
plasticX = random(0,width-plasticWidth);
plasticY = random(0,height-plasticHeight);
image(plasticImg, plasticX, plasticY, plasticWidth, plasticHeight);
}
}
//if monster collides collected, generate a new one on the canvas at a random location and decrement life
function checkMonsterCollisions(){
for (let i = 0; i < numOfMonsters; i++){
if (newSeg.getX() > monsterXs[i] && newSeg.getX() < monsterXs[i] + monsterWidth && newSeg.getY() > monsterYs[i] && newSeg.getY() < monsterYs[i] + monsterHeight)
{
powSong.play();
newSeg.blink();
monsterXs[i] = random(0,width-monsterWidth);
monsterYs[i] = random(0,height-monsterHeight);
life -=10;
}
}
}
//when life reaches zero, end the game
function checkLife(){
if (life === 0){
gameState = 'stop';
}
}
function displayScoreandLife(){
fill('rgb(138,63,196)')
textFont(scoreFont);
textSize(18);
text('Highscore: ' + highscore + ' Score: '+ score, 10,20);
text('Life remaining: '+ life, 390,20);
}
function stopPage(){
background('rgb(100,229,255)');
image(gameOverImg, 0,0,width,height);
fill('white');
textAlign(CENTER);
textFont(scoreFont);
textSize(24);
if (score > highscore)
{
print('score is ' + score + ' highscore is ' + highscore);
text('Congratulations. You helped free the ocean of harmful plastic and you just beat your previous highscore!', 0, height/2, width);
fill('gold')
text('New Highscore = ' + score, 10, height*2/3, width);
}
else
{
fill('white');
text('Congratulations. You helped free the ocean of harmful plastic!',10, height/2, width);
text('Score = ' + score + '\n\nHighscore = '+ highscore, 10, height*2/3, width);
}
fill('red')
ellipse(width/2, 530,200,80);
fill('white')
text('Play Again', 295, 538);
if (mouseX > width/2-100 && mouseX < width/2+100 && mouseY > 530-40 && mouseY < 530+40){
fill('gold');
ellipse(width/2, 530,200,80);
fill('white')
text('Play Again', 295, 538);
if (mouseIsPressed){
reset = true;
gameOverSong.stop();
gameState='start';
}
}
}
function resetGame(){
gameOverSong.stop();
song.loop();
life =100;
if (score > highscore){
highscore = score;
}
score = 0;
startPlaying=false;
x1=0;
x2=width;
direction='start';
newSeg.setX(width/2);
newSeg.setY(height/2);
for (i = numOfMonsters; i > 2;i--)
{
monsterSpeeds.splice(i, 1);
monsterXs.splice(i, 1);
monsterYs.splice(i, 1);
}
numOfMonsters = 3;
step =3;
for (i =0; i<numOfMonsters; i++)
{
monsterSpeeds[i] = random(-2,2);
monsterXs[i] = random(0,width);
monsterYs[i] = random(0,height);
}
}
function increaseSpeed(){
if(startPlaying === true){
step +=0.5;
append(monsterXs,random(monsterWidth,width-monsterWidth));
append(monsterYs,random(monsterHeight,height-monsterHeight));
append(monsterSpeeds, random(-2,2));
numOfMonsters+=1;
for (i =0; i < numOfMonsters; i++ ){
if (monsterSpeeds[i]<0){
monsterSpeeds[i]-=0.5;
}
else{
monsterSpeeds[i]+=0.5
}
}
}
}
Problems/Future Improvements
Some problems I mentioned earlier. Additionally, I was not able to stop the ‘game over’ song and it kept playing repeatedly at the same time. I figure out that it was due to the draw function being repeatedly called so I set up a flag to check if the song was already playing.
For the future, I can add coins/life randomly on the canvas so the user can “gain” life as well. Also, I can also make the plastic move as well. Different difficulty levels can also be designed instead of one continuous level with increasing difficulty. Also, the diver’s collision with the monster or plastic is not the most accurate and can be made more accurate for a more visually appealing effect.
As a whole, I really enjoyed revamping the snakes game to make one that I enjoy and to also raise awareness on an issue I care about.