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.
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.
|
|
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 👇
|
|
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:
|
|
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:
|
|
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:
|
|
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
👇
|
|
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:
|
|
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.