One thing that I’ve felt very strongly while thinking about the game is making communication a first class citizen in the controls. I want users to be able to communicate at anytime with almost no barrier to entry.

I’d go as far to say that even having to press [Enter] before being able to send a chat message is too high of a barrier to entry. I want a player to be able to type a few characters, point and click their mouse, type a few more, do something else, rinse and repeat. Their pending message should just sit there, letting them add or remove to it at their leisure regardless of what they’re doing in game.

To contrast, many games will have you press [Enter] to begin messaging, and then not let you do anything else until you finish your message and send it. This limit discourages casual chatting, and instead turns chat into a more transactional activity. Typing out a full message prevents you from taking any other actions until you send it, encouraging shorter and more infrequent messages.

I think that the freedom to send messages in the middle of any other activity without interruption has huge implications for socializing within a game, and so that’s the direction that I’m headed with chat.

Of course this means that the game control possibilities will suffer as a consequence. All of the alphanumeric keys are already assigned, so we’re limited control wise right from the beginning.

I’m actually fine with this trade-off as I’d like for the game to work just as well on touch devices as a keyboard. The fewer controls the better. We’ll just have to be that much more creative with our game mechanics.

Any who, I pushed up a chat system where messages appear above a player’s head. You can poke around at game.chinedufn.com, or read on if you’re interested in a high level overview of the implementation.

Two game clients chatting

Player in the bottom window walking by and saying hello

Implementation Overview

Our client listens for key presses on document.body and updates its pending message accordingly. Our pending message (and all other data) tracked in a single chinedufn/solid-state instance.

function AddCharacter (ClientState, character) {
  var currentChatText = ClientState.get().chat.pendingMessage
  currentChatText = currentChatText + character
  ClientState.set('chat.pendingMessage', currentChatText)
}

function DeleteCharacter (ClientState) {
  var currentChatText = ClientState.get().chat.pendingMessage
  currentChatText = currentChatText.slice(0, currentChatText.length - 1)
  ClientState.set('chat.pendingMessage', currentChatText)
}

When a client is ready to send a message and they hit [Enter]. Their message gets batched along with other commands from the current game tick and then eventually gets sent to the server.

function SendPendingMessage (ClientState, EventSink) {
  EventSink.ws({message: ClientState.get().chat.pendingMessage})
  ClientState.set('chat.pendingMessage', '')
}

Our server receives the message and emits an event to have this message added to game state.

// ...
} else if (pendingCommands.message) {
  targets.chat.emit('setOverheadMessage', clientId, pendingCommands.message)
}
// ...

We use chinedufn/client-ketchup to calculate patches for each clients in order to represent their game state changes from this game tick. Our new overhead message will be included in each nearby client’s state patch.

function AddPendingUpdate (GameState, CST, clientId, updateJSON) {
  var existingPendingUpdate = GameState.get().clients[clientId].pendingUpdate
  // The player can only receive updates once per game tick
  // The existing pending update is cleared after every game tick
  if (!existingPendingUpdate) {
    var patches = CST.update(clientId, updateJSON)
    GameState.set('clients.' + clientId + '.pendingUpdate', patches)
  }
}

Clients use their local view of game state in order to render to the transparent div that overlays the game canvas.

We calculate the 3d coordinates right above the speaking client’s head and then convert that position to clip space. Here’s a good tutorial on rendering text relative to your world.

// calculate-chat-position.js

var create = require('gl-mat4/create')
var transformMat4 = require('gl-vec4/transformMat4')
var makeCamera = require('../../../../render/camera/make-camera.js')

var mat4Translate = require('gl-mat4/translate')
var mat4Multiply = require('gl-mat4/multiply')
var mat4Scale = require('gl-mat4/scale')
var mat4Invert = require('gl-mat4/invert')

module.exports = CalculateChatPosition

// Determine the chat text's position in clip space
// TODO: Use a thunk
// TODO: normalize with HealthBar position calculator
function CalculateChatPosition (canvasDimensions, aboveClientHeadPos, cameraData) {
  var camera = makeCamera(cameraData)
  var viewMatrix = []
  mat4Invert(viewMatrix, camera.matrix)

  var matrix = create()
  mat4Translate(matrix, matrix, aboveClientHead)
  mat4Multiply(matrix, viewMatrix, matrix)

  mat4Multiply(matrix, cameraData.perspective, matrix)

  var clipSpace = []
  transformMat4(clipSpace, [0, 0, 0, 1], matrix)

  clipSpace[0] /= clipSpace[3]
  clipSpace[1] /= clipSpace[3]

  var left = (clipSpace[0] * 0.5 + 0.5) * canvasDimensions.width
  var top = (clipSpace[1] * -0.5 + 0.5) * canvasDimensions.height

  var absolutePosition = { left: left, top: top }

  return absolutePosition
}


After a few game ticks the server deletes the message, and this gets reflected in our clients’ local states. This means that their UI render function no longer displays the message.

// Our client UI render function transforms game state into a virtual-dom

var h = require('virtual-dom/h')

var CanvasOverlay = require('../canvas-overlay/canvas-overlay.js')
var ChatPanel = require('../chat-panel/chat-panel.js')
var MainPanel = require('../main-panel/main-panel.js')

module.exports = RenderHTMLInterface

// Render the HTML portion of the game UI
function RenderHTMLInterface (state) {
  return h('div', {
    style: { display: 'flex', height: '560px', width: '900px' }
  }, [
    h('div', {
      style: { display: 'flex', flexDirection: 'column', height: '560px', width: '680px' }
    }, [
      CanvasOverlay.render(h, state),
      ChatPanel.render(h, state)
    ]),
    MainPanel.render(h, state)
  ])
}

What’s Next

We’re going to move onto modeling and rendering the first real game asset, a tree. For this we’ll be pushing a few updates to chinedufn/wavefront-obj-parser that should help with a few aspects of .obj files that we’ll need to handle.

Should be fun!

- CFN