Advanced Logging with the Strategy Pattern

A clean and easy to maintain system for debugging bugs and errors in production builds

by on 7 minute read


Have you ever tried debugging a problem with your game that only seems to happen in production?

Maybe your game is wrapped in Capacitor for mobile and a user, client, or the Quality Assurance (QA) team finds a bug that is hard to reproduce… at least for you. But they can make it happen like clockwork. 🤷‍♂️

The Developer Tools Console or Debugger is not an option so what do you do?

One way is to build a secret in-game log console but you don't want to have to write 2 log statements every time log something.

What you need is an advanced logging solution using the Strategy Pattern to decide which logger or loggers to use depending on the environment.

This article will show you how to do that.

Strategy Pattern

The Strategy Pattern is a behavior pattern similar to the State Pattern.

The main difference is that the Strategy Pattern is mostly involved with interchangeable ways of doing something while the State Pattern deals with managing the internal state of objects.

Don't worry if that definition isn't crystal clear. Implementing the advanced logger in this article will help you better understand the Strategy Pattern.

We'll be using TypeScript and Phaser 3 in the example code but the concepts are the same for other languages and frameworks.

TypeScript and Phaser 3 are a great combination for making games on the web. Check out our free ebook to learn more 👇

Learn to make an Infinite Runner in Phaser 3 with TypeScript!

Drop your email into the box below to get this free 90+ page book and join our newsletter.

Learn more about the book here.

Create a Logger

Let's start by creating a Logger class that we'll use to log messages to the Console instead of console.log().

The key to this Logger class is that it will have no concrete logging logic. The actual work of logging will be handled by loggers that get added to a Logger instance.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import ILogger from './ILogger'

export default class Logger
{
	private loggersByName = new Map<string, ILogger>()

	add(name: string, logger: ILogger)
	{
		this.loggersByName.set(name, logger)
	}

	remove(name: string)
	{
		this.loggersByName.delete(name)
	}

	log(message: string)
	{
		this.loggersByName.forEach(logger => {
			logger.log(message)
		})
	}
}

Notice that we are using the Map data structure instead of a plain object or hashmap on line 3.

This may look odd for JavaScript developers and completely normal for those familiar with C#, Java, C++, or similar. We decided on using Map but you don't have to.

The important thing is that we are holding a mapping of keys to ILogger instances that can be added and removed at runtime using the add() and remove() methods.

Then the log() method goes through each ILogger instance and instructs them to perform the work of logging.

The ILogger interface looks like this 👇

1
2
3
4
export default interface ILogger
{
	log(message: string)
}

Chances are you'll only need a single instance of the Logger class and you can do that however you want.

One way is to export a shared instance from Logger.ts like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
export default class Logger
{
	// ...
}

const sharedInstance = new Logger()

export {
	sharedInstance
}

Then you can import sharedInstance as logger where you need it.

Create a Console Logging Strategy

The Logger class doesn't do any actual logging so we'll need concrete loggers that implement ILogger.

Let's start with a simple ConsoleLogger that logs to the browser's Console:

1
2
3
4
5
6
7
8
9
import ILogger from './ILogger'

export default class ConsoleLogger implements ILogger
{
	log(message: string)
	{
		console.log(message)
	}
}

Notice that this simply wraps console.log(). Nothing fancy.

What's more important is that this ConsoleLogger is a logging strategy. We can use this with the Logger in a Phaser 3 Scene like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import Phaser from 'phaser'

import Logger from './Logger'
import ConsoleLogger from './ConsoleLogger'

export default class LoggerScene extends Phaser.Scene
{
	private logger!: Logger

	constructor()
	{
		super('logger')
	}

	init()
	{
		this.logger = new Logger()
		this.logger.add('console', new ConsoleLogger())
	}

    create()
    {
		this.logger.log('hello, world!')
    }
}

The result of this.logger.log('hello, world!') will be the same as console.log('hello, world!').

So far this seems like a lot of extra code for little benefit!

But don't worry because the magic of the Strategy Pattern comes when you have multiple strategies.

Logging to an In-Game Console

We won't go into hiding/showing an in-game console or making it pretty. For simplicity, we'll just use an HTMLTextAreaElement that will show messages.

First, we can create an InGameLogger similar to the ConsoleLogger 👇

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import Phaser from 'phaser'
import ILogger from './ILogger'

export default class InGameLogger implements ILogger
{
	private readonly textarea: Phaser.GameObjects.DOMElement

	constructor(textarea: Phaser.GameObjects.DOMElement)
	{
		this.textarea = textarea
	}

	log(message: string)
	{
		const node =  this.textarea.node as HTMLTextAreaElement

		node.value += `${message}\n`
	}
}

The key thing to note is that we are taking in a Phaser.GameObjects.DOMElement in the constructor on line 8.

Then the log() method appends the given message to the textarea that is assumed to be an HTMLTextAreaElement.

Here's how we can use the InGameLogger from a Scene:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import Phaser from 'phaser'

import Logger from './Logger'
import InGameLogger from './InGameLogger'

export default class LoggerScene extends Phaser.Scene
{
	private logger!: Logger

	constructor()
	{
		super('logger')
	}

	init()
	{
		this.logger = new Logger()
	}

    create()
    {
		const { width, height } = this.scale

		const textarea = this.add.dom(width * 0.5, height * 0.5, 'textarea', {
			width: width * 0.8,
			height: height * 0.7
		})

		this.logger.add('in-game', new InGameLogger(textarea))

		this.logger.log('hello, world!')
    }
}

Most of this is similar to the previous ConsoleLogger example.

The main difference is that we are making a textarea with this.add.dom() and then using it to create an InGameLogger instance.

The example will result in hello, world! appended to a Textarea element. Deciding how to hide/show or make it prettier is up to you!

Now you can use NODE_ENV or however you determine environment to decide which logging strategy to use for different builds.

A side benefit of this system is that it lets you easily adhere to the best practice of avoiding the use of console.log() in production. Just don't add the ConsoleLogger for production builds! 🍰

Real World Logging Strategy

There's a limitless number of different loggers you can create for different scenarios but we'll leave you with 1 more that is very useful in production to detect problems.

The ConsoleLogger and InGameLogger are good for development and QA testing but it doesn't give you information about issues that only players are having.

One solution is to have a cloud logger that connects to a real-time log aggregation service like Sumo Logic.

You can use this to detect errors from backend deploys, live-ops events, or similar by hooking into window.onerror and logging those errors to the cloud while players are playing the game!

Next Steps

There is a lot more you can do with this logging system that is specific to the needs of your game. Using the Strategy Pattern helps keep the code clean and easy to maintain. 🤗

Another way to use the pattern is to have different strategies in the ConsoleLogger depending on what should be logged. We used message: string but you'll want to log many things in practice including Array objects in which case maybe console.table() should be used?

Think about how the Strategy Pattern can help you do that. 🤔

If you enjoyed this article then be sure to subscribe to our newsletter for more game development tips and techniques! 👇

Don't miss any future game development content

Subscribers get exclusive tips & techniques not found on the blog!

Join our newsletter. It's free. We don't spam. Spamming is for jerks.

Phaser 3 patterns debug logging strategy pattern

Want tips and techniques more suited for you?


You may also like...


Video Guides


Beginner Guides


Articles Recommended For You

Fix Stretched Image Distortions in Phaser 3 with 9-Slice Scaling

by on

Are you having image distortion problems when scaling to make a button or panel graphic bigger? Multiple versions of the …

5 minute read

Command Pattern to Undo Player Actions

by on

Are you looking for a clean and reusable way to implement undo for player actions? Perhaps you are making a turn-based …

15 minute read

State Pattern for Character Movement in Phaser 3

by on

Writing clean and well-organized code is something all game developers aspire to. We want code to be reusable and easy …

7 minute read

Memory Match in Modern Javascript with Phaser 3 - Part 6

by on

If you've got the basics of Phaser 3 in modern JavaScript down then it might be time to try making something a bit more …

8 minute read

Didn't find what you were looking for?


comments powered by Disqus