User Interface

Introduction

As discussed in the previous devlog, the user interface (UI) is a fundamental aspect of a videogame. Conveying information to the player allows them to observe how the game reacts to their input and how interactions between game entities unfold. Ultimately, UI helps players understand the mechanics that rule the game world.

Similarly to input systems, it’s crucial to hit a balance on how much information is conveyed when designing UI. On one hand, a user interface that conveys little to no information makes the game confusing to the player, which in turn makes the game experience frustrating. On the other hand, a user interface that drowns the player with intel is overwhelming and hard to follow. Players are most likely to ignore most of the information on screen because it’s too much to keep track of.

Alt text
I guess a PhD. is required to play WoW. Taken from [1]

The design of user interfaces is both a technical and artistic choice. On the tech side, you have to consider the tools you are working with and the resources they provide for UI design. Also, the capabilities of your target platform to render the interface may be limited. On the art side, the visual style of the UI is driven by the art direction of the videogame. Elements like fonts, images, menus, buttons, etc. need to match the vibe of the rest of the game to provide a consistent experience. Additionally, the user experience or UX is another huge topic related to UI that has to be studied carefully. After all, a well-thought-out UX ensures the UI is intuitive and satisfying to interact with.

UI design requires lots of considerations. Therefore, there’s not a unique, correct way I can teach you to create UI for your game. We’ll stick to review examples on how to do it for Chess so you can recreate it or use it as inspiration for other games.

In my experience, in most games, coding UI is way less difficult than programming rules and mechanics. This means there won’t be any clever coding techniques or convoluted algorithms in this devlog.

We’ll take advantage of the graphical capabilities and simple usage of the p5.js and p5.quadrille.js libraries to draw figures on the screen. You’ll find links to the full API of both libraries in the references section.

Creating a canvas

In the previous devlog, we briefly went over the canvas element. This HTML element is provided by the p5.js library and it is required to render any graphics on the screen.

The main drawing canvas of the application is typically created with the createCanvas() function. Every program can have one main canvas only. Hence, it is not possible to have multiple canvases with this function.

For my application, I want to give the flexibility of drawing multiple chess boards on the screen or running more than one chess game at once. Also, I want my chess program to be imported as a library on top of other p5 programs. This means a main canvas will most likely be created already. We will definitely need more than one canvas.

Fortunately, p5.js provides the createGraphics() function. This function creates an extra canvas that’s independent from the main one. The contents of the extra canvas will be drawn on top of the main canvas. For this reason, a main canvas still must be created with createCanvas(). If a portion of the extra canvas goes beyond the main canvas, it won’t be visible.

Sketch Code
let graphics;

function setup() {
    //create main canvas
    createCanvas(300, 300);
    background(180);

    //create extra canvas
    graphics = createGraphics(100, 100);
    graphics.background(100);
    graphics.circle(graphics.width / 2, graphics.height / 2, 20);
}

function draw() {
    //draw extra canvas
    background(180);
    image(graphics, mouseX, mouseY);
}

Board UI

Let’s start with the main element of the game: the chess board. The Board class holds a reference to a quadrille that represents the board and its pieces. We can render this quadrille with the use of drawQuadrille().

drawQuadrille() receives a couple of drawing parameters besides the quadrille object that will be drawn. With the use of these parameters, we can change the board’s position, size, color, etc.

Let’s put this into practice to draw our board.

class Board{
    #board; //board with pieces in symbol representation

    /**
 * Creates a new chess board
 * @param {string} inputFen FEN of board
 */
    constructor(inputFen) {
        this.#board = new Quadrille(inputFen);
 }

    /**
 * Draws board
 */
    draw(graphics) {
        graphics.drawQuadrille(this.#board, {
            x: BOARD_UI_SETTINGS.LOCAL_POSITION.x,
            y: BOARD_UI_SETTINGS.LOCAL_POSITION.y,
            cellLength: BOARD_UI_SETTINGS.SQUARE_SIZE
 });
 }
}

Here we draw a quadrille named board that contains the chess board. This quadrille’s position is defined by the x and y parameters. Its size is defined by the cellLength parameter.

The graphics variable passed to draw() is the target extra canvas on which we will draw the board.

This board is missing the checkered pattern of any chessboard. Conveniently, if a quadrille is created without any constructor parameters, it will return an 8x8 quadrille with a chessboard pattern.

class Board{
    #board;//board with pieces in symbol representation
    #boardBackground;//checkered pattern

    /**
 * Creates a new chess board
 * @param {string} inputFen FEN of board
 */
    constructor(inputFen) {
        this.#board = new Quadrille(inputFen);
        this.#boardBackground = new Quadrille();
 }

    /**
 * Draws board
 */
    draw(graphics) {
        graphics.drawQuadrille(this.#boardBackground,{
            x: BOARD_UI_SETTINGS.LOCAL_POSITION.x,
            y: BOARD_UI_SETTINGS.LOCAL_POSITION.y,
            cellLength: BOARD_UI_SETTINGS.SQUARE_SIZE });

        graphics.drawQuadrille(this.#board, {
            x: BOARD_UI_SETTINGS.LOCAL_POSITION.x,
            y: BOARD_UI_SETTINGS.LOCAL_POSITION.y,
            cellLength: BOARD_UI_SETTINGS.SQUARE_SIZE
 });
 }
}

We can change the color of the squares by modifying the Quadrille.whiteSquare and Quadrille.blackSquare properties.

class Board{
    #board;//board with pieces in symbol representation
    #boardBackground;//checkered pattern

    /**
 * Creates a new chess board
 * @param {string} inputFen FEN of board
 */
    constructor(inputFen) {
        this.#board = new Quadrille(inputFen);
        Quadrille.whiteSquare = BOARD_UI_SETTINGS.WHITE_SQUARE_COLOR;
        Quadrille.blackSquare = BOARD_UI_SETTINGS.BLACK_SQUARE_COLOR;
        this.#boardBackground = new Quadrille();
 }

    /**
 * Draws board
 */
    draw(graphics) {
        graphics.drawQuadrille(this.#boardBackground,{
            x: BOARD_UI_SETTINGS.LOCAL_POSITION.x,
            y: BOARD_UI_SETTINGS.LOCAL_POSITION.y,
            cellLength: BOARD_UI_SETTINGS.SQUARE_SIZE });

        graphics.drawQuadrille(this.#board, {
            x: BOARD_UI_SETTINGS.LOCAL_POSITION.x,
            y: BOARD_UI_SETTINGS.LOCAL_POSITION.y,
            cellLength: BOARD_UI_SETTINGS.SQUARE_SIZE
 });
 }
}

Let’s make the board’s outline match the black squares’ color.

class Board{
    #board;//board with pieces in symbol representation
    #boardBackground;

    /**
 * Creates a new chess board
 * @param {string} inputFen FEN of board
 */
    constructor(inputFen) {
        this.#board = new Quadrille(inputFen);
        Quadrille.whiteSquare = BOARD_UI_SETTINGS.WHITE_SQUARE_COLOR;
        Quadrille.blackSquare = BOARD_UI_SETTINGS.BLACK_SQUARE_COLOR;
        this.#boardBackground = new Quadrille();
 }

    /**
 * Draws board
 */
    draw(graphics) {
        graphics.drawQuadrille(this.#boardBackground,{
            x: BOARD_UI_SETTINGS.LOCAL_POSITION.x,
            y: BOARD_UI_SETTINGS.LOCAL_POSITION.y,
            cellLength: BOARD_UI_SETTINGS.SQUARE_SIZE });

        graphics.drawQuadrille(this.#board, {
            x: BOARD_UI_SETTINGS.LOCAL_POSITION.x,
            y: BOARD_UI_SETTINGS.LOCAL_POSITION.y,
            cellLength: BOARD_UI_SETTINGS.SQUARE_SIZE,
            outline: color(BOARD_UI_SETTINGS.OUTLINE)
 });
 }
}

Finally, we can change how pieces in the board are rendered by creating a function that defines how quadrille cell values are displayed.

class Board{
    /**
 * Draws board
 */
    draw(graphics) {
        graphics.drawQuadrille(this.#boardBackground,{
            x: BOARD_UI_SETTINGS.LOCAL_POSITION.x,
            y: BOARD_UI_SETTINGS.LOCAL_POSITION.y,
            cellLength: BOARD_UI_SETTINGS.SQUARE_SIZE });

        let piecesDisplay = ({ graphics, value, cellLength = Quadrille.cellLength } = {}) => {}

        graphics.drawQuadrille(this.#board, {
            x: BOARD_UI_SETTINGS.LOCAL_POSITION.x,
            y: BOARD_UI_SETTINGS.LOCAL_POSITION.y,
            cellLength: BOARD_UI_SETTINGS.SQUARE_SIZE,
            outline: color(BOARD_UI_SETTINGS.OUTLINE)
 });
 }
}

Inside piecesDisplay(), we can use any p5.js graphical function to draw the cell values as we want. The cell values of the board quadrille are text characters that represent each piece. Therefore, we’ll use p5.js functions related to typography.

class Board{
    /**
 * Draws board
 */
    draw(graphics) {
        graphics.drawQuadrille(this.#boardBackground,{
            x: BOARD_UI_SETTINGS.LOCAL_POSITION.x,
            y: BOARD_UI_SETTINGS.LOCAL_POSITION.y,
            cellLength: BOARD_UI_SETTINGS.SQUARE_SIZE });

        let piecesDisplay = ({ graphics, value, cellLength = Quadrille.cellLength } = {}) => {
            graphics.textAlign(CENTER, CENTER);
            graphics.textSize(BOARD_UI_SETTINGS.PIECES_SIZE); 
            graphics.fill(color(BOARD_UI_SETTINGS.PIECES_COLOR));
            graphics.text(value, cellLength / 2, cellLength / 2);
 }

        graphics.drawQuadrille(this.#board, {
            x: BOARD_UI_SETTINGS.LOCAL_POSITION.x,
            y: BOARD_UI_SETTINGS.LOCAL_POSITION.y,
            cellLength: BOARD_UI_SETTINGS.SQUARE_SIZE,
            outline: color(BOARD_UI_SETTINGS.OUTLINE)
 });
 }
}

Here we change the color and size of the text with fill() and textSize(), respectively. Then, we put the pieces in the center with textAlign(). Then, we render the text characters with text().

We pass this display function as a drawing parameter to drawQuadrille(). The appropriate parameter is stringDisplay because the cell values are text characters. The piecesDisplay function will be called on every non-empty cell. The value parameter corresponds to the content of each cell.

class Board{
    /**
 * Draws board
 */
    draw(graphics) {
        graphics.drawQuadrille(this.#boardBackground,{
            x: BOARD_UI_SETTINGS.LOCAL_POSITION.x,
            y: BOARD_UI_SETTINGS.LOCAL_POSITION.y,
            cellLength: BOARD_UI_SETTINGS.SQUARE_SIZE });

        let piecesDisplay = ({ graphics, value, cellLength = Quadrille.cellLength } = {}) => {
            graphics.textAlign(CENTER, CENTER);
            graphics.textSize(BOARD_UI_SETTINGS.PIECES_SIZE); 
            graphics.fill(color(BOARD_UI_SETTINGS.PIECES_COLOR));
            graphics.text(value, cellLength / 2, cellLength / 2);
 }

        graphics.drawQuadrille(this.#board, {
            x: BOARD_UI_SETTINGS.LOCAL_POSITION.x,
            y: BOARD_UI_SETTINGS.LOCAL_POSITION.y,
            cellLength: BOARD_UI_SETTINGS.SQUARE_SIZE,
            outline: color(BOARD_UI_SETTINGS.OUTLINE),
            stringDisplay: piecesDisplay
 });
 }
}

Sketch Code

const FEN = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR';
const SQUARE_SIZE = 50;
const CANVAS_SIZE = 400;
const WHITE_SQUARE_COLOR = '#ffffff';
const BLACK_SQUARE_COLOR = '#44c969';
const PIECES_SIZE = 35;
const PIECES_COLOR = '#000000';

let board;
let boardBackground;
let piecesDisplay = ({ graphics, value, cellLength = Quadrille.cellLength } = {}) => {
    graphics.textAlign(CENTER, CENTER);
    graphics.textSize(PIECES_SIZE); 
    graphics.fill(color(PIECES_COLOR));
    graphics.text(value, cellLength / 2, cellLength / 2);
}

function setup() {
    createCanvas(CANVAS_SIZE, CANVAS_SIZE);
    board = new Quadrille(FEN);
    Quadrille.whiteSquare = WHITE_SQUARE_COLOR;
    Quadrille.blackSquare = BLACK_SQUARE_COLOR;
    boardBackground = new Quadrille();

}

function draw() {
    drawQuadrille(boardBackground, {cellLength: SQUARE_SIZE});
    drawQuadrille(board, {
        cellLength: SQUARE_SIZE,
        outline: color(BLACK_SQUARE_COLOR),
        stringDisplay: piecesDisplay
 });
}

There are various display functions in the p5.quadrille.js library depending on the contents of the quadrille, like stringDisplay, numberDisplay, colorDisplay, arrayDisplay, etc. Make sure the function you assign to these display functions uses the value variable accordingly.

Input UI

Let’s look at a simple example of how to implement UI when the player makes a move.

class MoveInputUI{

}

First of all, we can subscribe to the various events of the MoveInput class so the UI reacts to the player’s input.

class MoveInputUI{
    /**
 * @param {MoveInput} moveInput 
 */
    constructor(moveInput) {
        //set events
        moveInput.addInputEventListener(
            MoveInput.inputEvents.onMoveStartSet, 
            this.#onMoveStartSet.bind(this)
 );

        moveInput.addInputEventListener(
            MoveInput.inputEvents.onMoveDestinationSet,
             this.#onMoveDestinationSet.bind(this)
 );

        moveInput.addInputEventListener(
            MoveInput.inputEvents.onMoveInput,
            this.#onMoveInput.bind(this)
 );
        moveInput.addInputEventListener(
            MoveInput.inputEvents.onMoveCanceled, 
            this.#onMoveCanceled.bind(this)
 );
 }

 #onMoveStartSet(event) {}
 #onMoveDestinationSet(event) {}
 #onMoveInput(event) {}
 #onMoveCanceled(event) {}
}

The UI will do the following for each event:

  • On Move Start Set     - Highlight the square selected as the start     - Show the available moves for the piece that was selected
  • On Move Destination Set     - Highlight the square selected as the destination
  • On Move Input     - Highlight the start and destination square if the move was legal     - Show nothing if the move was illegal     - Hide available moves
  • On Move Canceled     - Show nothing

We’ll create a function that highlights a square with a specific color. The fill() function allows us to fill a cell of a quadrille with a color instance.

class MoveInputUI{
    #UIQuadrille;

    /**
 * @param {MoveInput} moveInput 
 */
    constructor(moveInput) {

        this.#UIQuadrille = createQuadrille(NUMBER_OF_FILES, NUMBER_OF_RANKS);

        //set events
        // ...
 }

 #highlightSquare(rank, file, color) {
        let row = 8 - rank;
        let column = file - 1;
        this.#UIQuadrille.fill(row, column, color);
 }
}

UIQuadrille is the quadrille in which we will draw everything. It will render on top of the board.

Now, we’ll create a function that clears the quadrille so we can show nothing. The clear() function accomplishes that by emptying all the cells of the quadrille.

class MoveInputUI{
 #clear() {
        this.#UIQuadrille.clear();
 }
}

Let’s implement the onMoveStartSet event. The available moves will be retrieved from the Game object instance.

class MoveInputUI{

    #game;
    #UIQuadrille;
    #colorForSelectedSquare;

    /**
 * @param {MoveInput} moveInput 
 */
    constructor(game, moveInput) {

        this.#game = game;
        this.#UIQuadrille = createQuadrille(NUMBER_OF_FILES, NUMBER_OF_RANKS);
        this.#colorForSelectedSquare = color(MOVE_INPUT_UI_SETTINGS.COLOR_FOR_SELECTED_SQUARES);

        //set events
        // ...
 }

 #onMoveStartSet(event) {
        let selectedSquare = event.detail.square;
        //highlight selected square
        this.#highlightSquare(selectedSquare.rank, selectedSquare.file, this.#colorForSelectedSquare);

        //draw available moves
        //for each legal move
        for (let move of this.#game.legalMoves) {
            //if this move's start square matches the selected square
            if (move.startRank === selectedSquare.rank && move.startFile === selectedSquare.file) {
                //highlight the destination square as an available move
                this.#highlightSquare(move.endRank, move.endFile, this.#colorForAvailableMoves);
 }
 }
 }
}

The event object provides the selected square. We fill that square with a predefined color. Then, we go through the list of legal moves. Every legal move whose start square matches the selected square is an available move for the piece in the selected square. Therefore, we fill the destination square of the legal move with another color.

Moving on with onMoveDestinationSet, we’ll just fill the selected square again.

class MoveInputUI{
 #onMoveDestinationSet(event) {
        let selectedSquare = event.detail.square;
        //fill selected square
        this.#highlightSquare(
            selectedSquare.rank,
            selectedSquare.file,
            this.#colorForSelectedSquare
 );
 }
}

Then, for onMoveCanceled, we’ll clear the UI quadrille:

class MoveInputUI{
 #onMoveCanceled(event) {
        this.#clear();
 }
}

Lastly, in the onMoveInput event, we check if the move is legal. We clear the UI if it isn’t. Otherwise, we remove the squares colored as available moves because a move has been chosen. However, we keep highlighting the squares selected as start and destination.

The replace() function easily replaces all cells colored as available moves with no color.

class OnMoveInput{
 #onMoveInput(event) {
        let result = this.#game.isMoveLegal(event.detail.move);
        //if input move is legal
        if (result.isLegal) {
            //remove color of squares colored as available moves
            this.#UIQuadrille.replace(this.#colorForAvailableMoves, null);
 } else {
            //clear UI
            this.#clear();
 }
 }
}

We observe an error where past selected squares keep the gray color even when a new move is being performed. To fix this, when a start square is selected, we clear the quadrille beforehand.

class MoveInputUI{
 #onMoveStartSet(event) {

        //clear quadrille beforehand
        this.#clear();

        //fill selected square
        let selectedSquare = event.detail.square;
        this.#highlightSquare(selectedSquare.rank, selectedSquare.file, this.#colorForSelectedSquare);

        //draw available moves
        //for each legal move
        for (let move of this.#game.legalMoves) {
            //if this move's start square matches the selected square
            if (move.startRank === selectedSquare.rank && move.startFile === selectedSquare.file) {
                //highlight the destination square as an available move
                this.#highlightSquare(move.endRank, move.endFile, this.#colorForAvailableMoves);
 }
 }
 }
}

Conclusion

We’ve seen that the p5.js and p5.quadrille.js libraries are really useful for implementing user interfaces for games and other applications. The p5.quadrille.js is particularly handy for board games and grid-based projects.

The examples we went through are simple. However, the potential of these libraries goes beyond what we did here. The p5.js library supports many more elements for more complex user interfaces, like buttons, checkboxes, images, toggles, sliders, etc. Likewise, the p5.quadrille.js library offers tools for advanced graphics manipulation, like rasterization, image filtering, sampling, sorting, and 3D rendering.

The sky is the limit! I hope these examples give you an idea of how to implement UI on your project. Be creative and feel free to explore and use these resources for your next project.

References