Copying a deeply nested Javascript / Typescript object by value vs reference in game development.

- 5 mins
Hello1
ViewSource code

The perils of pass by reference vs value in Javascript

Over the past few weeks I've been working on a game using the input framework we created during the course (Link below). This has mostly been smooth sailing and pretty fun except every now and then I come across a particular type of bug that has at times taken hours to find and solve 😔.

The worst part was that I was mostly aware of what was going on to cause the bug but despite this it was still tricky to locate the issue when it arised during development.

Before I jump into what it is exactly that made me drop working on the game and write this post, I want you to try something for me.

In your browser right click the page and select inspect, this should bring up developer tools side window. Inside this window tap on the tab named console which will bring you to the browser's console. If you are using a browser different to chrome the way to find the console may differ, just search how to use browser console for <insert browser name here> if you need help.

Okay in the console tap on the > character and type the commands one by one below and press enter.

const initialWallet = {amount: 0}
const wallet1 = initialWallet
const wallet2 = initialWallet
wallet1.amount = 1
console.log(wallet1)
//{amount: 1}

We've created an initialWallet constant and two wallet objects which are both initialised to the initialWallet of {amount: 0}. Next we increment the amount in wallet1 by 1 and log the current wallet1 state. Here we see the output is 1.

Finally lets print to the console the state of wallet2, I mean there's basically no point because we haven't updated it since it was initialised right... right?

console.log(wallet2)
//{amount: 1}

Huh 🙃, wallet2's amount has been updated as well, what's going on?

Now to some of you this may seem reasonable and expected, this is not really a bug as I described it earlier but is actually intentional and an outcome of the way Javascript handles variable object assignment.

By default Javascript and Typescript assign objects by reference not value.

In the simple example above if you are aware of this it's not too difficult to deduce what's going on. However I ran into this effect multiple times while creating the input course and as the code was fairly complicated it took me a while to realise what was happening. As mentioned even once I was aware it took a while to bottle up and distill exactly when this occurs in Javascript and how to prevent it.

If you've been following on with my previous tutorials you may have noticed that I am trying to make my game examples (as much as possible) follow a functional programming style. In fact for my job I use mostly languages that assign objects / variables by value not by reference and so I prefer to avoid reference based assignment in my Typescript games. As you can see it can lead to all sorts of unintended side effects such as above which can wreak havoc on any program and cause all sorts of odd bugs during game development. For me this took the form of player2 being controlled by player1's controls 🙀 or mimmicking player1's position.

After the third time this effect had reared it's ugly head in my game I realised that some assumptions I had were wrong and my techniques to break reference assignment (which I will discuss in this post) were still producing undesired bugs in my game. That is why I decided to write this post to hopefully save you time debugging and explain ways to mitigate this effect by assigning object by value instead of by reference.

Value vs reference object assignment in Javascript

Before we jump into the hands-on example I want to quickly describe the differences between reference and value based assignment.

As Javascript uses reference based object assignment, for the line below we are not creating a new object in memory with values equal to initialWallet rather we are creating a pointer to the original initialWallet.

const wallet1 = initialWallet

Next we assigned wallet2 in the same way.

const wallet2 = initialWallet

Now both wallet1 and wallet2 are pointing to exactly the same address in memory which points to the initialWallet object.

This means that anytime we update wallet1's properties, the compiler will navigate to the pointer which points to the initialWallet and its properties will be updated instead which is what was shown above.

A language that assigns objects by value (Such as Java or Scala) will instead make a copy or clone of the initial object and store that copy somewhere different in memory. In this way both wallet1 and wallet2 can be updated individually without effecting eachother!

There are ways to get Javascript to behave in this way which I'll cover in the example below.

Pass by reference

First lets start by re-creating the problem ourselves using the game input starter, see below for how to setup this project if this is your first time.

This project gives us a basic game shell runtime with a game loop and input system. Checkout my course below if you're interested in how the input starter project was created.

We'll start by outputting two squares to the canvas to represent player1 and player2 (tap the example above to see the finished product). To begin with we will use Javascript's default object reference assignment.

There's not too much code to this example but there is a few initial parts to setup which are described below.

At the top of the GameRuntime.ts file lets add two new types as below.

type PlayerState = {x: number, y: number, direction: 'left'| 'right'}

type Players = {
  player1Equal: PlayerState,
  player2Equal: PlayerState,
}

Normally I'd add these to a separate file but will keep it simple for this example. Here we introduce two new types. We have a PlayerState which holds a x, y position and current direction left | right, this will be to make the square move back and forth to simulate a real game.

We're also going to skip updating the GameState as we have done in previous tutorials as this might make the reference effect harder to conceptualise.

In the GameRuntime class add the following code.

static playerStartingPositionEqual: PlayerState = {x: 0, y: 70, direction: 'right'}
static player1Equal: PlayerState = GameRuntime.playerStartingPositionEqual

Next we will create three helper methods that I will describe below, add them to GameRuntime.

private static getDirection = (player: PlayerState): 'right' | 'left' => {
  const player1OutOfBounds = player.x > 100 || player.x < 0
  return player1OutOfBounds ? (player.direction === 'right' ? 'left' : 'right') : player.direction
}

private static updatePlayer = (player: PlayerState) => {
  const newDirection = GameRuntime.getDirection(player)
  const newPlayerPosition = player.x + (newDirection === 'right' ? 1 : -1)

  player.direction = newDirection
  player.x = newPlayerPosition
}

private static drawPlayers = (context: CanvasRenderingContext2D, player1: PlayerState, player2: PlayerState, player1Size: number, player2Size: number) => {
  context.fillStyle = 'blue'
  context.fillRect(player1.x, player1.y, player1Size, player1Size)
  context.fillStyle = 'red'
  context.fillRect(player2.x, player2.y, player2Size, player2Size)
}

The getDirection method simply returns the players current direction. The players direction is only changed to the opposite direction when the player goes out of bounds. In this case if x < 0 or x > 100.

The updatePlayer method will mutate the player's directon and x value based on the result from getDirection, the player will then move 1 unit towards the way they are facing +1 when facing 'right', -1 when facing left.

The examples will always compare two players and different methods of assigning objects to see the results.

The drawPlayers method will draw the two players. Player1 will be drawn as a blue square and player2 will be drawn as a red square half the size (In order to see it when it overlaps with player1).

Next we want to update the draw method to take a Players type as below.

private static draw = (context: CanvasRenderingContext2D, canvasWidth: number, canvasHeight: number, gameState: GameState, players: Players) => {
  ...
}

Below the resetCanvas call in the draw method add the following code.

const player1SquareSize = 20
const player2SquareSize = 10

context.fillText('Player1: Blue square, Player2: Red square', 0, 20)

context.fillText(`Player1Equal: ${JSON.stringify(players.player1Equal)}`, 0, 50)
context.fillText(`Player2Equal: ${JSON.stringify(players.player2Equal)}`, 0, 60)
GameRuntime.drawPlayers(context, players.player1Equal, players.player2Equal, player1SquareSize, player2SquareSize)

Here we are setting the two players size and outputting their current state to the canvas. We are also drawing the two players using the drawPlayers method.

In the GameRuntime class's loop method. add the following code and update the draw method call to pass the new players object.

const player2Equal = GameRuntime.playerStartingPositionEqual
GameRuntime.updatePlayer(GameRuntime.player1Equal)

const players = {
  player1Equal: GameRuntime.player1Equal,
  player2Equal: player2Equal,
}

GameRuntime.draw(context, canvasWidth, canvasHeight, GameRuntime.gameState, players)

At this stage we can now run the app npm run serve and observe the results. This example simualtes the wallet example above. The running app should show a blue and red square moving back and forth. The red player2 square will copy the blue player1 squares behaviour. This is expected although not desired as Javascript is assigning by reference when we initialise the player1Equal object.

In the next sections we'll look at how changing how player1 is assigned it's initial state will effect player2 and discuss ways to avoid this effect.

Using Object.assign()

Javascript has a Object.assign method that will perform a shallow copy of an object, we'll look at what the term shallow means later.

Update Players type to include the below new fields, in this case the assigned keyword is used to indicate that we have will use the Object.assign method.

player1Assigned: PlayerState, 
player2Assigned: PlayerState, 

Add a new player1Assigned static constant inside the GameRuntime class as below.

static playerStartingPositionAssigned: PlayerState = {x: 0, y: 120, direction: 'right'}
static player1Assigned: PlayerState = Object.assign({}, GameRuntime.playerStartingPositionAssigned)

Notice how the Object.assign method is used to initialise the player1Assigned constant. This method takes a source argument of type object and a target argument of type object (the object we want to copy).

Inside the loop method add the following code.

const player2Assigned = GameRuntime.playerStartingPositionAssigned
GameRuntime.updatePlayer(GameRuntime.player1Assigned)

const players = {
  player1Equal: GameRuntime.player1Equal,
  player2Equal: player2Equal,
  player1Assigned: GameRuntime.player1Assigned,
  player2Assigned: player2Assigned,
}

In the draw method add the following code to draw the new assigned player state and squares.

context.fillText(`Player1Assigned: ${JSON.stringify(players.player1Assigned)}`, 0, 100)
context.fillText(`Player2Assigned: ${JSON.stringify(players.player2Assigned)}`, 0, 110)
GameRuntime.drawPlayers(context, players.player1Assigned, players.player2Assigned, player1SquareSize, player2SquareSize)

Now run the app and this time the player2 red square should remain fixed in its original position as desired. This is because when we use the Object.assign method we are not assigning the object by reference.

Using the spread operator

In Javascript the spread operator denoted by ... can be used to spread an objects properties onto a resulting object. See the example below.

const wallet1 = {amount: 0, colour: 'brown'}
const wallet2 = {...wallet1, colour: 'black'}

In the above example the spread operator is used to spread or copy wallet1's properties to wallet 2 as a new object, we are also updating the colour property to black just to show this pattern. I used this pattern all the time (you may have noticed) and this is the way I have been assigning objects by value instead of reference during my tutorials.

The code changes for the spread operator follow exactly the same process as the Object.assign and so I won't repeat the exact steps. Have a go at adding the spread operator example and confirm you can add another 2 squares and that the new blue is indeed static.

Spread and Object assign don't work for deeply nested objects

This brings me onto the assumption that I had regarding the spread operator. Before this I was happily applying the spread operator whenever I thought a sneaky reference assignment was occurring to break it and for a while this was working fine.

My objects quickly became deeply nested objects containing more nested objects and I started to see some strange side effects.

After the third time I read up on the spread operator to find that it does break the reference of deeply nested objects meaning that any object that was nested would still reference its original reference even if the spread operator is used.

Lets show this by example, again I won't go through all the code changes but the pattern is exactly the same as before only this time we will use deeply nested objects.

Update the Players type with four new fields equal to below.

player1NestedAssigned: {config: PlayerState},
player2NestedAssigned: {config: PlayerState},
player1NestedSpread: {config: PlayerState},
player2NestedSpread: {config: PlayerState},

Note that this time these fields are nested with another config field that points to a PlayerState.

Add the following code to the loop method to update player1Nested.

static playerStartingPositionNestedAssigned: {config: PlayerState} = {config: {x: 0, y: 220, direction: 'right'}}
static player1NestedAssigned = Object.assign({}, GameRuntime.playerStartingPositionNestedAssigned)

static playerStartingPositionNestedSpread: {config: PlayerState} = {config: {x: 0, y: 270, direction: 'right'}}
static player1NestedSpread = {...GameRuntime.playerStartingPositionNestedSpread}

Add the following lines to the draw method.

const player2NestedAssigned = GameRuntime.playerStartingPositionNestedAssigned
GameRuntime.updatePlayer(GameRuntime.player1NestedAssigned.config)
const player2NestedSpread = GameRuntime.playerStartingPositionNestedSpread
GameRuntime.updatePlayer(GameRuntime.player1NestedSpread.config)

Run the app and notice that once again the player2 red square is chasing the player1 blue square and referencing it's position exactly.

It turns out Object.assign doesn't break the reference of deeply nested objects either.

JSON Javascript value reference hack

After searching about this issue I like many others discovered a hack that uses the JSON utility in Javascript to stringify and then parse an object to remove it's references. I was interested to know if the typescript compiler would be happy with this method, let's try it out now below.

Update the Players type with two new fields equal to below.

player1NestedJson:  {config: PlayerState}, 
player2NestedJson: {config: PlayerState},

Add a new player1NestedJson static constant inside the GameRuntime class as below.

static playerStartingPositionNestedJson: {config: PlayerState} = {config: {x: 0, y: 320, direction: 'right'}}
static player1NestedJson = JSON.parse(JSON.stringify(GameRuntime.playerStartingPositionNestedJson))

Note the use of JSON.parse(JSON.stringify()) this basically turns our object into a string and then back into JSON which effectively stores the new object in memory and no longer references the old one.

As a challenge add the rest of the code and then run the app to confirm that this does indeed make the red square stay in it's initial position.

Note this hack does modify the original object and removes any functions on the object it can also throw errors if the object contains recursive fields which is partly the reason it receives the hack title.

As I've been following the functional programming style I don't often assign functions to objects but the fact that this method can throw errors had me searching for another way.

Lodash and Rambda

There are libraries out there that support deeply nested copying or cloning. Two such libraries are Lodash and ramda which support a wide range of useful operations, their differences can be read about in this stack overflow post Differences between Lodash and Ramda.

If you feel like a spicy challenge you could have fun with recursion and try create your deeply nested clone algorithm!

Structured clone

Fortunately the Javascript community were also not happy with the JSON hack and as of es2021 a method called structuredClone provides a way to clone or copy deeply nested objects. Functions are still removed and structuredClone can still throw errors but it supports recursive objects.

Lets update the project to try this now. First of all we need to update the project to support es2022 latest ecmascript (Javascript). We can do this by updating the typescript dependency to "typescript": "^4.9.4" in the package.json file and running npm install.

Next update tsconfig.json's compilerOptions.module field to ES2022, I plan to update the next starter project milestone to incorporate these changes if I don't discover any issues with using the new ES2022 module.

You know the drill update the Players type with two new fields equal to below.

player1NestedClone: {config: PlayerState}, 
player2NestedClone: {config: PlayerState},

Add a new player1NestedClone static constant inside the GameRuntime class as below.

static playerStartingPositionNestedClone: {config: PlayerState} = {config: {x: 0, y: 370, direction: 'right'}}
static player1NestedClone = structuredClone(GameRuntime.playerStartingPositionNestedClone)

Note the use of structuredClone in the player1 initialisation. Now go ahead and update the rest of the relevant code to output player1 and player2 using the structuredClone and run the app. If you have any trouble you can take a look at the reference code using the link above.

Notice again how the red square stays in place and is not tempted by the alluring blue square. 🥳

Summary

Okay I'm glad that's now addressed, my advice would be to use the spread operator for shallow (not deeply nested) object assignment (as it's likely more performant than structuredClone) and structuredClone if possible and fallback to the JSON hack for deeply nested objects. Also in general try avoid assigning any values by reference as it can quickly get out of hand, especially for game development.

Also although we didn't apply the different initialisations when assigning player2 to it's starting state in general it's best practice to use these whenever we assign objects, I skipped it for player2 to highlight the fact that applying it once breaks the reference, however if we introduced a third player and didn't use a value based assignment then player3 would copy player2 etc.

Also also bit of a strange request but please let me know in the comment box below whether you're enjoying these posts, any feedback, where you're posting from etc as this will help me adjust my posts, make my day and also confirm the comment box is working (Pretty sure it is 😂).