Published on

Making a JavaScript Browser Game

Authors
  • avatar
    Name
    Jyotir Sai
    Twitter
    Engineering Student

There are many examples of successful browser games such as agar.io, slither.io, diep.io, skribbl.io, and more. I wanted to learn more about making games for the web, so I decided to make my own basic browser game called Aim Duel. The concept is fairly simple and not exactly original, two players click on a series of targets and the person who clicks them the quickest wins! The full code is available on my Github. You can also play it here!.

The part of this project I was most interested in learning was the networking side i.e. making the game multiplayer. Therefore, I tried to keep everything game related as simple as possible. Since I wanted to keep things simple, I decided to go with the 2d HTML rendering context rather than using something like WebGL. My targets would also just be circles that randomly appear across the screen.

My plan was to first make a single player version of the game and then add multiplayer. Before I started the game, I wanted to add webpack so I wouldn't have to worry about manually ordering my scripts to satisfy dependencies. I split the webpack configurations into development and production, where the production config focused on optimizing the build for production.

// webpack.prod.js
...
module.exports = merge(common, {
  mode: "production",
  entry: "./src/client/index.js",
  output: {
    filename: "main.[contenthash].js",
    path: path.resolve(__dirname, "dist"),
  },
  optimization: {
    minimizer: [
      new OptimizeCssAssetsPlugin(),
      new TerserPlugin(),
      new HtmlWebpackPlugin({
        filename: "index.html",
        template: "./src/client/html/index.html",
        minify: {
          removeAttributeQuotes: true,
          collapseWhitespace: true,
          removeComments: true,
        },
      }),
    ],
  },
  plugins: [
    new CleanWebpackPlugin(),
    new MiniCssExtractPlugin({ filename: "[name].[contenthash].css" }),
  ],
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, "css-loader"],
      },
    ],
  },
})

Once I had that configured, I went ahead and started the single player game. Here's how the game would work: user loads the page --> menu appears --> player clicks start game --> a random target appears --> user clicks target --> another target appears. The game will also calculate the time it takes the user to click on the target and after 10 targets the game ends and the user is presented with their average "reaction" time.

The main menu has 3 buttons: Play Solo, Create Room, and Join Room. The Play Solo button will activate the single player game.

When the user clicks on the Play Solo button, the menu is hidden and we randomly generate a coordinate. Initially, I wrote all the game logic on the client side which proved to be a mistake since I wanted to add multiplayer. When I started the multiplayer, I realized that if I wanted to generate targets for both players, I would have to generate their coordinates on the server and then send them to the client to be rendered. So I added express.js to take care of the server side logic.

// server.js
...
const app = express()
const PORT = process.env.PORT
app.use(express.static("public"))

if (process.env.NODE_ENV === "development") {
  const compiler = webpack(webpackConfig)
  app.use(webpackDevMiddleware(compiler))
} else {
  app.use(express.static("dist"))
}

const server = app.listen(PORT)
console.log(`Server is running on port ${PORT}`)

const io = socketio(server)

When the user clicks the Play Solo button, we initialize the game on the client by doing things such as hiding the game menu, initializing the canvas, and adding an event listener to listen to user clicks. We also emit a message to our server via socket.io that the Play Solo button has been clicked.

// server.js
...
io.on("connection", (socket)=>{
    socket.on("playSolo", handlePlaySolo);

    function handlePlaySolo() {
        state = initGame();
        emitGameState(state);
    }
})
// Game.js
function initGame() {
  const state = createGameState()
  randomTarget(state)

  return state
}

function createGameState() {
  return {
    target: {
      x: null,
      y: null,
    },
    playerOneTimes: [],
  }
}

function randomTarget(state) {
  target = {
    x: Math.random(),
    y: Math.random(),
  }
  state.target = target
}

The initGame function initializes our game state object which simply keeps track of the current target x and y position, as well as the reaction time of the player. It also generates the initial target using the randomTarget function.

// server.js
function emitGameState(state) {
  socket.emit('gameState', JSON.stringify(state))
}

After the game state is initiated and the target coordinates are rendered, the emitGameState function sends the game state object back to the client.

// index.js a.k.a client
...
socket.on("gameState", handleGameState)

function handleGameState(gameState) {
  state = JSON.parse(gameState);
  renderTarget(state.target.x, state.target.y);
}

Once the client receives the game state object, we pass the server generated target positions to the renderTarget function which draws a circle on our canvas with those coordinates.

To figure out whether the user has clicked inside the target, we'll have to use some math. Since we added the event listener, whenever the user clicks we can access the event in our handleOnClick function. The event includes the x and y position of where the user has clicked. We want to know if the user has clicked within the circle, so we want to check if x^2 + y^2 <= r^2, where r is the radius of the target.

// index.js
function handleOnClick(event) {
  if (Object.keys(state).length != 0) {
    const diffX = event.clientX - state.target.x * canvas.width
    const diffY = event.clientY - state.target.y * canvas.height
    const deltaSquared = Math.pow(diffX, 2) + Math.pow(diffY, 2)
    if (deltaSquared <= Math.pow(20, 2)) {
      reactionTime = Date.now() - startTime
      socket.emit('clickedTarget', reactionTime)
      renderBackground()
    }
  }
}

Once the user clicks on the target, we emit their reaction time back to the server.

io.on('connection', (socket) => {
  socket.on('clickedTarget', handleClickedTarget)

  function handleClickedTarget(reactionTime) {
    state.playerOneTimes.push(reactionTime)
    if (state.playerOneTimes.length >= 10) {
      socket.emit('gameOver', JSON.stringify(state))
    } else {
      randomTarget(state)
    }
    emitGameState(state)
  }
})

The reaction time is added to the player one times array in the game state object. Once the number of reaction times hits 10, we emit a game over message to the client along with the current state.

// index.js
socket.on('gameOver', handleGameOver)

function handleGameOver(state) {
  removeEventListener('click', handleOnClick)
  renderBackground()
  state = JSON.parse(state)
  avgReactionTime = calculateReactionTime(state.playerOneTimes)
  gameOverDiv.classList.remove('hidden')
  rTimeText.innerText = avgReactionTime + ' ms'
}

In the handleGameOver function we receive the state from the server which we use to calculate the average reaction time and then display it to the user.

That's it for the single player game!