Learn to Code with Games: Hellblade Senua's Sacrifice
The 2nd post in the learn to code with games blog series. Replicate the pattern matching game in Hellblade: Senua's Sacrifice with canvas and Javascript.
In my last Learn to Code with Games post we talked about replicating the hunger meter in The Long Dark. Today’s post is about replicating a feature from Hellblade: Senua’s Sacrifice. Hellblade is one of the most harrowing journeys into a mentally ill person’s mind that I have ever seen in a video game. If you haven’t played it, I highly recommend checking it out. You don’t even have to worry about getting addicted because the game has a concrete beginning, middle, and end. One of the unique aspects of Hellblade is a mini puzzle game that involves finding a shape in nature that matches a shape carved into the various runes in the world.
I decided to recreate a simple version of this mini puzzle game with Javascript in Glitch. You can look at it right away here or give it a shot yourself first. In this tutorial we will be using HTML5 Canvas and vanilla Javascript. We will load a background image of some trees and the user will control a triangle shape on top of it and try to find where the same shape can be found among the trees. When they hover the triangle shape in the right spot that matches up where the trees form that shape, the triangle will change color. You could use a more complex shape, but I wanted to keep it simple by using a triangle for this tutorial.
Thankfully the HTML is very simple, just two things we need to do. First we need to do is create a canvas element with <canvas> and give it width, height, and an id as shown below. The width and height should be roughly the size of our image. We will use the id to identify the canvas in Javascript. The entire game will pretty much happen within this canvas, which allows for advanced graphics manipulation that you cannot do with other HTML elements.
The picture we are using for this exercise
Second we need to add our tree background image so our canvas can access the image data. However I will also add a hidden class because otherwise we will see our image twice, since it’s going to appear inside our canvas. We want to give our image an id as well, since the canvas also needs to access it. I called it “trees” because well, its an image of trees. The below code will go inside your <body> tags.
<img id="trees" class="hidden" src="https://cdn.glitch.com/eb083ff0-5e3b-41d0-be19-711a1dcd89f5%2FDSC0063-1024x680.jpg?v=1589402686658"/> canvas width="800" height="600" id="canvas"></canvas> <script>Our Javascript will go here, or in a .js file if you prefer </script>
Then in order to make your image be hidden, you will want to add this inside your <head> tags.
<style> .hidden { display: none; } </style>
Worry not, even though the image is hidden our magical canvas will still be able to access the data to display it in all its beauty. Wonderful! Now our HTML file is set and we can focus on the Javascript. The first step is to identify our canvas and get the context, which is what lets us run functions to actually change what is displaying.
let context; let img; let canvas; window.onload = function() { canvas = document.getElementById("canvas"); context = canvas.getContext("2d"); img = document.getElementById("trees"); context.drawImage(img, 0, 0); };
I’m declaring the image, canvas, and context variables at the top because we are going to need to access them throughout the code. Having a window.onload makes sure that we don’t try to fetch the canvas before it is loaded into our browser. In the first line of the function, we are getting our canvas, which we need in order to get our context. Then we are getting our image and drawing it to the canvas with context.drawImage. This function takes our image, and then the x and y coordinates (which start from 0 at the top left corner, so in this case our image will take up the whole canvas). If our context was in 3d space instead of 2d, we would also add a third value for our z index, the perspective plane.
So what’s next? Let’s think a little about what we data we need in order for this to work. So far all we have is our tree background image in a canvas. We want there to be a shape that the user can move around on top of the image. While allowing the user to drag the shape around would be nice, the easiest option is to just make the shape follow the user’s mouse around.
In order to do that, we will need to get the coordinates of the users mouse. This is actually the trickiest part, because canvas is not very sophisticated with the data it provides by default. We have to do some math to account for the location of the canvas on the window. The function below will do that for you.
function getPosition(el) { var xPosition = 0; var yPosition = 0; while (el) { xPosition += (el.offsetLeft - el.scrollLeft + el.clientLeft); yPosition += (el.offsetTop - el.scrollTop + el.clientTop); el = el.offsetParent; } return { x: xPosition, y: yPosition }; }
This function accepts the canvas element and returns the x and y coordinates of the canvas in relation to the browser window. We will call this function inside window.onload to get our canvas position, which will then be used to get an accurate mouse position. Don’t worry too much if you don’t understand all of it. If we were using another framework such as P5js this extra math wouldn’t be necessary at all.
The important part is next. We are going to add what’s called an event listener, which is a function that will get called every time the window detects a user interaction. We can define what user interaction we are listening for. In this case it will be moving the mouse. While we’re at it let’s also call our getPosition function to get our canvas position and add our mouse coordinate variables to the top, since we will need to access them soon.
let context; let mouseX = 0; let mouseY = 0; let canvasPos; let img; let canvas; window.onload = function() { canvas = document.getElementById("canvas"); canvasPos = getPosition(canvas); // getting our canvas position context = canvas.getContext("2d"); img = document.getElementById("trees"); context.drawImage(img, 0, 0); canvas.addEventListener("mousemove", setMousePosition, false); //the line above is listening for when the user moves their mouse, and will call the function "setMousePosition" };
Olay so now we have an event listener but this code will not run because the function setMousePosition doesn’t exist yet. That is where most of the magic is going to happen. We will need to redraw our shape every time the mouse moves. We will also need to check if the shape is in the spot where it matches the pattern, so we can tell the user they have found it! You can add this function below window.onload.
function setMousePosition(e) { mouseX = e.clientX - canvasPos.x; mouseY = e.clientY - canvasPos.y; }
The above code will get us the current coordinates of the users mouse on the canvas. We are passing in e which stands for the element that is being passed into the function, in this case our canvas element. The subtraction is happening to account for the offset of the canvas position on the browser window, as mentioned earlier. Now we can actually draw our shape!
function setMousePosition(e) { mouseX = e.clientX - canvasPos.x; mouseY = e.clientY - canvasPos.y; context.beginPath(); // tell canvas you want to begin drawing lines context.moveTo(mouseX, mouseY); // move where the cursor starts the line context.lineTo(mouseX - 25, mouseY + 125); // draw first line context.lineTo(mouseX + 25, mouseY + 125); // draw second line context.fillStyle = "#FF6A6A"; //set the color context.fill(); //fill shape with color }
As you can probably tell from my comments on the code above , there are several steps to drawing a shape. First we have to tell the canvas we want to draw lines with context.beginPath and then we need to move our cursor. Since we want our triangle to follow the mouse, we move our cursor to the same coordinates.
I want my triangle to be a bit elongated, so when I define the end coordinates of my first line I want them to be just a little bit to the left (-25) and farther down (+125). To keep my mouse centered to the top of my triangle, I set my other line coordinates to be the same amount, but in the other direction on the x coordinate (+25). The final line goes back to our original coordinates, so you don’t need any additional code to complete the triangle shape. Now we can set the fill style to the hexadecimal code for a sort of salmon-y color. You have to call the fill function in order for that color to actually be applied to your shape.
That’s not right….
We’re getting close but if you run the code now you might see something is a little strange! Instead of having a triangle that follows our mouse we seem to be painting the canvas. That is because the canvas is constantly drawing more triangles every time we move our mouse and the canvas isn’t getting cleared. Luckily clearing the canvas is pretty easy.
function setMousePosition(e) { mouseX = e.clientX - canvasPos.x; mouseY = e.clientY - canvasPos.y; // add the lines below context.clearRect(0, 0, canvas.width, canvas.height); //clearing canvas context.drawImage(img, 10, 10); //drawing our image again since that got cleared out context.beginPath(); context.moveTo(mouseX, mouseY); context.lineTo(mouseX - 25, mouseY + 125); context.lineTo(mouseX + 25, mouseY + 125); context.fillStyle = "#FF6A6A"; context.fill(); }
The clearRect function takes four values, x and y coordinates which define the upper left corner of the rectangle, as well as a height and width. If we provided something smaller than the canvas height and width only a portion of our canvas would get cleared, but we want to clear all of it. Of course this clears our image as well so we need to draw that back to the canvas again. This all needs to happen before we draw our triangle or it will get covered up by our image.
Now you should have a lovely little elongated salmon triangle floating around on top of our forest image, following our mouse obediently. There is only one thing left to do. We need to give the user some indication when they have “discovered” the pattern. There are a lot of fancy things that could be done here. We could display some text to tell the user they have found the pattern. We could add some fancy animation like in the actual Hellblade game. But for the sake of brevity and to give you freedom to experiment with canvas on your own, lets just change the color of our triangle. This code will be added to the bottom of our setMousePosition function.
if(mouseX > 625 && mouseX < 630) { if(mouseY > 10 && mouseY < 20) { context.fillStyle = "#FFFFFF"; context.fill(); } }
Here we are checking our mouseX and mouseY coordinates to see if they match with the coordinates where we know our shape is in the image. You may notice there is a range of 5 pixels in both the x and y coordinates, because it is actually quite difficult to get your mouse on 1 or 2 specific pixels.
I took the liberty of figuring out the coordinates for the image in our tutorial, but if you want to do this with a different image or a different shape you will need to add some console.log statements to your mouseX and mouseY so you can gauge where the shape should change colors. I’m changing the color to a simple white, though you can obviously change it to whatever color you choose. Check out my version on Glitch here.
Thats it! Now you can plug in any image and see if your friends can figure out if they can find the pattern. It’s obviously not too difficult with the shape and image I provided, but it can certainly be made more difficult with a larger image or a more unusual shape. I recommend checking out the following tutorials if you are interested in expanding your knowledge of drawing shapes and images with the canvas element:
Drawing Shapes
https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Drawing_shapes
Transform + Text
https://eloquentjavascript.net/17_canvas.html
Build a Drawing App
http://www.williammalone.com/articles/create-html5-canvas-javascript-drawing-app/
Working with Video
http://html5doctor.com/video-canvas-magic/
If you enjoyed this article, consider following me on Twitter @nadyaprimak or if you need more tips on breaking into the tech industry, you can read my book “Foot in the Door” in paperback or Kindle now.
This blog post was originally published at www.nadyaprimak.com
Read more about:
BlogsAbout the Author
You May Also Like