Midterm Project: Uncharted

Uncharted

Concept:

For my midterm I made a game called Uncharted. I am a big One Piece fan, and the idea came from the thing the whole series is built on, a sea that nobody has properly mapped and a crew crossing it one island at a time. It also grew straight out of my Assignment 3 piece, which was a generative sea chart where a ship sails from island to island and draws its route behind itself. Once I had that working I kept thinking it was already most of a game that nobody was playing.

So in the game you set sail on a sea that has never existed before, because every map is generated fresh when you press play. Your ship sails from island to island on its own, and every island is guarded by a monster. When the ship reaches an island the monster rises out of the water and you have to click it to beat it before it smashes your ship. Every time it hits you, you lose a heart. Beat every monster and you reach the treasure, lose all your hearts and you are shipwrecked.

Main Menu

The part I like most is that the monsters are randomly generated too. One might have one big eye and horns, the next might be purple with three eyes and spikes down its back, and each one gets its own random name like “Gnash the Grumpy” or “Skullop the One Eyed”. So you never know what is waiting on an island until you get there, which is kind of the whole point of an uncharted sea.

Design:

The whole game is drawn to look like an old nautical chart. Parchment colour, faded stains on the paper, tiny dashes for open water, a compass rose in the corner and a double line border, all from my Assignment 3 piece. There are no image files in the game at all, everything you see is drawn with shapes in code, including the ship, the islands and every monster.

For the main menu I did not want a flat background, so the map is actually still running underneath it with the ship sailing, and the menu is a sheet of parchment laid over the top at 210 out of 255 alpha so you can just about see it moving. The title is drawn twice, once in dark brown 3 pixels down and to the right and once in red on top, so it looks like it is cut into the paper. I used a One Piece style font for the title only and Metamorphous for everything else, because I tried a few fonts and most display fonts fall apart at the small sizes the instructions need.

For the How to Play screen I remembered the Week 4 reading by Norman and tried to make sure the game tells you what to do without me having to explain it. The buttons go solid red when your mouse is over them so you know they can be clicked, every island that still has a monster on it has a little “?” marker over it, and the top of the screen always shows your hearts and which island you are heading to. In the fight there is a bar that fills up showing when the monster is about to strike, and the monster’s mouth opens wider the closer it gets, so you can read the danger off its face as well as the bar.

How to play screen.

I also added an Options screen where you can pick Easy, Normal or Hard and turn the sound on or off. Under the difficulty buttons it tells you what that setting actually changes, so you are not picking blind.

Options screen
Game Map

For the monsters I wanted them to feel alive even though they are just ellipses and triangles. Their pupils follow your mouse, they blink every few seconds, and their tentacles wiggle. When you hit one it flashes white. When you beat one there is a little cutscene where its eyes turn into X’s, it shudders, keels over to one side and sinks while bubbles come up and rings spread across the water, and then “BEATEN” fades in with its name.

For the winning screen, every monster you beat is lined up small across the screen with X eyes, like a collection. For the losing screen it shows you the monster that sank you and which island you got to.

Highlighted Code:

this.eyes = floor(random(1, 4));  // 1, 2 or 3
this.eyeSize = random(22, 32);
if (this.eyes === 1) {
  this.eyeSize = this.eyeSize * 1.5;   // one big eye looks scarier
}
if (this.eyes === 3) {
  this.eyeSize = this.eyeSize * 0.8;   // or they dont fit
}

// 0 = nothing on top, 1 = horns, 2 = spikes
this.top = floor(random(3));
this.hornLength = random(20, 40);

this.tentacles = floor(random(3, 7));
this.teeth = floor(random(2, 6));

let title = titles[floor(random(titles.length))];
if (this.eyes === 1 && random(100) < 50) {
  title = "the One Eyed";
}

I am proud of this because it is what makes every monster different. This is in the constructor of my Monster class, so all the random choices get made once when the monster is created, and after that the object just remembers them. That is the same lesson I learned in Assignment 3 with the islands, because if the random() was in show() instead, the monster would get a new face every single frame and flicker. floor() turns the random decimal into a whole number so I get exactly 1, 2 or 3 eyes, and the same trick picks horns or spikes, how many tentacles and how many teeth. Its skin colour and eye colour get picked out of a 2D array of colours the same way. My favourite bit is the name, because a one eyed monster has a 50 percent chance of being called “the One Eyed”, so sometimes the name actually matches what it looks like.

function hitMonster() {
  if (beatenTimer === 0) {
    if (fighting.isHit(mouseX, mouseY)) {
      fighting.hp = fighting.hp - 1;
      // console.log(fighting.hp);
      fighting.flash = 8;
      playSound(hitSound);
      // a hit pushes its strike back, so fast clicking matters
      fighting.attack = fighting.attack - 0.1;
      if (fighting.attack < 0) {
        fighting.attack = 0;
      }
      if (fighting.hp <= 0) {
        fighting.defeated = true;
        fighting.attack = 0;
        beatenTimer = 170;
        playSound(sinkSound);
      }
    } else {
      splashX = mouseX;
      splashY = mouseY;
      splashTime = 20;
      playSound(splashSound);
      // missing gives it a head start on normal and hard
      fighting.attack = fighting.attack + missCostFor[difficulty];
    }
  }
}

 

This is the whole fight in one function and it runs every time you click. isHit() is a method on the Monster class that uses dist() to check if the click was close enough to the middle of the monster. If it was, the monster loses a health point, flashes white, the hit sound plays, and its strike bar gets pushed back a bit, which is what makes fast clicking matter. If that was its last health point it gets marked as defeated and the sinking cutscene starts. If you missed, there is a splash where you clicked, and on Normal and Hard the miss actually pushes its strike bar forward. That one line is what made the game harder, because before it you could just spam click everywhere and win.

How This Was Made?

The map: This is from my Assignment 3 piece and works the same way. Every island is a pile of about twelve overlapping ellipses at random sizes, drawn four times over in different colours to make the shallows, beach, land and forest. Islands get placed using dist() so they do not land on each other, and the whole bottom left corner is kept empty for the compass. There are 4 islands for sure and then 4 more that each have a 60 percent chance, so you get between 4 and 8.

Screens: One variable called screen holds which page you are on, “menu”, “howto”, “options”, “play”, “fight”, “win” or “lose”, and draw() checks it and calls the right function. This is the game states technique from Week 6.2. Each screen also changes what a click does inside mousePressed().

The ship stopping at islands: In Assignment 3 the ship just kept sailing forever. For the game I added a docked true or false to the Ship class. When it reaches the end of a route it stays sat on the island and docked becomes true, and draw() notices that and starts the fight with the monster for that island. When you beat it, setSail() moves it on to the next route. On the menu the ship does not stop at all, so the background keeps sailing on its own.

Monsters: This is my fourth class, Monster.js, and there is one monster made for every route when the map is made, stored in an array where monsters[0] guards the second island, monsters[1] guards the third and so on. Each one is drawn with push(), translate() and scale() so I can draw it big in the fight and small on the win screen with the same code. The tentacles are rows of circles getting smaller with a sin() wiggle, the pupils use map() to follow your mouse, and the blink uses % so it closes its eyes for 8 frames out of every 220.

The fight: The monster rises out of the water at the start, sways side to side using sin() so you have to aim, and its attack number goes from 0 to 1. When it reaches 1 you lose a heart, the screen shakes using translate() with random numbers, and the screen flashes red. The sea is drawn after the monster so it looks like it is coming out of the water, and it goes all the way to the bottom of the screen.

Death cutscene: When a monster dies it shakes for 24 frames, then rotate() tips it over to one side, which is randomly left or right, while it sinks. The bubbles are four parallel arrays, the same way the paper stains work, and they only get drawn while they are still under the surface so they look like they pop. The “BEATEN” text fades in using map() on its alpha.

Difficulty: The difficulty settings are parallel arrays, so heartsFor[difficulty] is how many hearts you get. Easy gives you 5 hearts and slower monsters, Normal gives you 3 and misses cost you, and Hard gives you 2 with faster tougher monsters. Monsters also get stronger the further you sail, because each one is made with a level number that adds to its health and speed.

Sound: There are 8 sounds, a roar when the monster rises, a hit, a splash when you miss, a crash when it hits you, bubbles when it sinks, a win and lose tune, and waves looping in the background. They all load at the same time using Promise.all from Week 6.2 inside an async function, so setup() waits for them with await before the game starts.

Fonts: Both fonts load with await loadFont() in their own async function, the same way as the sounds.

Fullscreen: Pressing F calls fullscreen() from Week 6.2. My game is drawn for a 660 by 560 canvas, so instead of resizing everything, a few lines in style.css stretch the canvas to fill the screen while keeping its shape. The game still thinks it is 660 by 560 underneath so all the clicks still line up.

Buttons: The buttons are just rect() with a mouseIsOver() function that checks if mouseX and mouseY are inside the box and returns true or false, and mousePressed() uses it to decide which button you clicked.

Problems:

The first big problem was that when you beat a monster it would sink straight through the water and come out the bottom onto the paper, because I had only drawn the sea as a band 100 pixels tall. The fix was just making the sea go all the way to the bottom of the screen, and I made it slightly see through so you can still see the dark shape sinking underneath, which ended up looking better than what I was going for in the first place.

Fullscreen did not work at first. Every time you pressed F it tried to exit fullscreen instead of going in. It turned out fullscreen() with nothing in the brackets does not give back true or false, it gives back the fullscreen element or nothing, so checking it with === false was always wrong. Changing it to a plain if and else fixed it. Then pressing F during a voyage also sent you back to the menu, because I already had any key going back to the menu, so the F check had to go first.

Clicking the sea on the map used to make a brand new map, which was fine for Assignment 3, but in a game it meant one accidental click in the middle of a voyage wiped all your progress. I took it out and left it commented out in the code.

The sea sound would not play even though there were no errors. In the version of p5.sound we use, loop() only turns looping on and does not actually start the sound, so it needs loop() and then play(). Also browsers do not let a page make noise until you click something, so the waves start on your first click.

When I was moving the code into the p5 editor I kept getting “Ship is not defined”. It was because I had uploaded the .js files with Upload File instead of making them with Create File, so they were sitting there as assets and the sketch never actually ran them. I also had a font error with “pending” in the link, which was because I had not saved the sketch after uploading the font.

The game was also way too easy at first because you could just click everywhere as fast as you could. Making misses push the monster’s strike forward, and adding the three difficulty settings, fixed that. To check the difficulties were actually different, it was tested with an automatic player that clicks about three times a second and hits about 70 percent of the time. On Easy it won with all 5 hearts left, on Normal it lost on island 6 out of 7, and on Hard it lost on island 3 out of 8.

Reflection:

Overall I am really happy with how it turned out, especially because it started as a generative art piece and ended up as an actual game you can win or lose. I am most proud of the monsters, because every one of them comes out different and they still all look like they belong in the same game. The death cutscene is my favourite part to watch.

There are a few things from my draft that I did not get to. I wanted the player to be able to make their own character and design their own flag, which would replace the little red triangle on the ship’s mast. I would also like the fights to be different for different monsters, like some that throw things at you that you have to dodge instead of all of them being clicking fights. I would also like to swap my sounds for real recorded ones.

Resources:

Class slides

Example sketches from the slides

p5.js reference

JavaScript and CSS (MDN), for the parts we did not cover in class

Fonts

  • One Piece manga font from Font Meme, a fan made font based on the One Piece logo, used for the game title only.
  • Metamorphous by James Grieshaber, Sorkin Type Co, from Google Fonts under the SIL Open Font License, used for everything else.

Sounds

  • All 8 sound effects were generated with code by Claude for this.

Inspiration and visual reference

  • One Piece by Eiichiro Oda, for the idea of an unmapped sea and a crew crossing it island by island.
  • Old hand drawn nautical charts, for the compass rose, the border and the little dashes that show open water.
  • My own Assignment 3 piece, Uncharted, which the map, the islands, the routes and the ship come from.

Leave a Reply