A Very Basic Golf Game

The aim of this post is to introduce you to physics and rendering in JavaScript with the wonderful p5.js and Matter.js libraries. By the end of it, assuming you follow along (try CodePen if you’re feeling lazy), you’ll have built a very basic game where you click to launch a ball towards a hole. If the ball lands in the hole, you’ll get a congratulatory pop-up. Just like real golf!

Setting things up

To make this basic golf game, you’ll need an HTML file to open in your browser, a JavaScript file to run the game (I named mine sketch.js), and the p5.js (rendering) and Matter.js (physics) libraries to access nifty functions that will make your life much easier. Fortunately, both p5.js and Matter.js are available via CDN, so all you need to do is reference their URLs in script tags in your HTML. Here’s how your index.html should look:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8"/>
    <title>Golf</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.9.0/p5.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.14.2/matter.min.js"></script>
    <script src="sketch.js"></script>
  </head>
  <body></body>
</html>

Rendering the green, the ball, and the hole

If you open up index.html in your browser right now, you’ll see a blank white page. Boring. Let’s fix that.

Open sketch.js in your editor of choice and add the following two functions:

function setup() {

}

function draw() {

}

These functions are both provided by the p5.js library and the way they work is fairly simple. The setup() function is where you set things up. It runs a single time when the page is loaded. The draw() function runs sixty times per second, though, so that’s where we’ll be putting the code that draws things that might move (like the ball). P5.js handles the execution of both of these functions in the background, so you only need to worry about defining them.

Refresh the page. Still nothing? Not for long!

function setup() {
  createCanvas(400, 600)
  background(color(3, 135, 29))

  fill(255)
  circle(200, 500, 30)

  fill(0)
  circle(200, 50, 30)
}

These function calls should be fairly self explanatory. createCanvas(width, height) creates a canvas, background(color) sets the background color (referencing RGB values in this case), circle(x, y, diameter) creates a circle at the x and y coordinates you specify (with [0, 0] at the top left corner). fill(color) is slightly more confusing, but it basically defines the colour of all the shapes created after it’s called. We call it twice here because we want the ball to be white and the hole to be black. Refresh the page. You should see something like this: a green rectangle in portrait orientation with a white circle at the bottom and black circle at the top

This is great, but we can’t interact with it in any way. We’ve put all our code in the setup() function which has kindly drawn our shapes for us, but isn’t any way to make the shapes move! This is where Matter.js comes in and things get a bit more complicated.

Adding physics and interactivity

In order to get our ball to bounce off walls and slow down according to friction, we’ll use the Matter.js physics engine to control its movement and keep track of where it is.

const ball = Matter.Bodies.circle(200, 500, 15)

const engine = Matter.Engine.create()
const world = engine.world
Matter.Engine.run(engine)
Matter.World.add(world, ball)

function setup() {
  createCanvas(400, 600)
}

function draw() {
  background(color(3, 135, 29))

  fill(0)
  circle(200, 50, 30)

  fill(255)
  circle(ball.position.x, ball.position.y, ball.circleRadius * 2)
}

A few things have changed here. The ball is now created with Matter.Bodies.circle(x, y, radius), and everything is being rendered in the draw() function. The background() and circle() functions need to be in draw() because you want a clean slate every frame (preventing after-images of every frame previous), and the second circle() function call references ball.position, which is an attribute provided by Matter.js to keep track of where things (which it calls bodies) are.

Refresh the page. You should now have something like this: a green rectangle in portrait orientation with a black circle at the top and a white circle at the bottom which falls out of view

This might seem like one step forward and two steps back. Let’s add a wall at the bottom of the canvas to stop the ball from dropping out of view.

const ball = Matter.Bodies.circle(200, 500, 15)

const static = {isStatic: true}
const bottomWall = Matter.Bodies.rectangle(200, 625, 400, 50, static)

const engine = Matter.Engine.create()
const world = engine.world
Matter.Engine.run(engine)
Matter.World.add(world, [ball, bottomWall])

The parameters for the rectangle method can be found in the Matter.js documentation, and the {isStatic: true} object just tells the engine that you want that body to be fixed in place. You may have noticed, however, that the ball stays on bottomWall only for a moment before falling through. This seems to be an unavoidable issue with Matter.js, but we don’t really want gravity pulling the ball towards the bottom of the screen anyway. We can remove that gravitational effect with world.gravity.y = 0 which you can include just after defining your world variable.

With gravity disabled we appear to be back where we were before we added Matter.js, so let’s move on to adding some user interactivity. First, though, lets add the remaining walls.

const ball = Matter.Bodies.circle(200, 500, 15)

const static = {isStatic: true}
const bottomWall = Matter.Bodies.rectangle(200, 625, 400, 50, static)
const topWall = Matter.Bodies.rectangle(200, -25, 400, 50, static)
const leftWall = Matter.Bodies.rectangle(-25, 300, 50, 600, static)
const rightWall = Matter.Bodies.rectangle(425, 300, 50, 600, static)
const bodies = [ball, bottomWall, topWall, leftWall, rightWall]

bodies.forEach(body => body.restitution = 0.8)

const engine = Matter.Engine.create()
const world = engine.world
world.gravity.y = 0
Matter.Engine.run(engine)
Matter.World.add(world, bodies)

Now that we have all of our walls and we’ve given them all a restitution value (which means the ball with bounce off them), we’re ready to get the ball moving on click.

The goal is to be able to click and have the ball move towards where your mouse is. The speed of the ball will be calculated by checking how far away your mouse is from the ball. The further away you click, the faster the ball will move. Matter.js can calculate the angles for us, but we’ll need to calculate the distance ourselves. Add this function to the bottom of your file:

function distanceBetween(vectorA, vectorB) {
  // Pythagorean theorem time!
  return Math.sqrt(Math.pow(vectorA.x - vectorB.x, 2) + Math.pow(vectorA.y - vectorB.y, 2))
}

We’ll want to run these calculations whenever we click, and fortunately p5.js provides a function called mouseClicked() for precisely this purpose. Add the following code at the bottom of your file:

function mouseClicked() {
  const mousePosition = {x: mouseX, y: mouseY}

  const distance = distanceBetween(ball.position, mousePosition)
  const angle = Matter.Vector.angle(ball.position, mousePosition)
  const forceMultiplier = distance / 50 + 1
  const force = 0.002 * forceMultiplier > 0.02 ? 0.02 : 0.002 * forceMultiplier

  Matter.Body.applyForce(ball, ball.position, {
    x: cos(angle) * force,
    y: sin(angle) * force
  })
}

Again, we only have to worry about defining the function because p5.js will run it in the background whenever it recognises that you’ve clicked your mouse. It also helpfully provides the values mouseX and mouseY, which we covert into a vector for matter.js to use in calculations. The ternary statement in the assignment of force is there to make sure that the force never gets too high, and Matter.Body.applyForce gets the ball to move. You should now have a mostly-functional (very basic) golf game! a green rectangle in portrait orientation with a black circle at the top and a white circle at the bottom which moves towards the mouse when it is clicked, bouncing off the side of the frame

Winning

The only thing left to do is to add a definition of winning, so that we can check whether we’ve won. In my experience, putting a golf ball into a hole successfully has two criteria. You need to aim it in the right direction, but you also need to make sure you hit it softly enough that it actually falls in. Our checkIfWin() function will use our existing distanceBetween() function to check whether the ball is over the hole, and it will reference the speed property of the ball (provided by matter.js) to make check if it’s moving slowly enough. You’ll need to define the checkIfWin() function and reference it in draw() so that the game will constantly check whether you’ve won. Feel free to tinker with the numbers in checkIfWin() to make the game easier or harder as you see fit.

function draw() {
  background(color(3, 135, 29))

  fill(0)
  circle(200, 50, 30)

  fill(255)
  circle(ball.position.x, ball.position.y, ball.circleRadius * 2)

  checkIfWin()
}

function checkIfWin() {
  if (distanceBetween(ball.position, {x: 200, y: 50}) < 10 && ball.speed < 1.8) {
    alert("You win!")
  }
}

a green rectangle in portrait orientation with a black circle at the top and a white circle at the bottom which moves towards the black circle. Upon the collision of the white circle with the black circle, a pop-up message appears which reads: "You win!"

And that’s it. You’ve made a very basic golf game! Hopefully you’ve also learned something. If you weren’t following along and you’d like to see all the finished code in one place to see how it fits together, you can check it out on CodePen :)