November 29 2021

The "KISS" Transaction Pattern


In this post, I’ll teach you how to protect your most sensitive code from the classic “sloppy programming” by using the KISS Transaction Pattern for Unity.

When Do You Need the Transaction Pattern in Unity?

With traditional programming, it’s quite easy to screw up your most sensitive data, which turns into frustration/regrets or worse, getting canned from work.

I’m talking about the fragility of executing code like this:

_player.SetLevel(_player.Level + 1);
_player.SetName(userInput)
_player.AddItem(loot[0])

Honestly, it is not so much about the input we pass (that’s another issue)…

It’s more about the fact that anyone can just call these functions anytime, anywhere, for whatever reason and mess your data up.

But we need these setters! How could we live without them?

Indeed, you need setters; how’re you going to do a level up otherwise?

But my point is that you don’t need write-access to sensitive data 95% of the time. And guess what, this is sensitive data you are ought to protect.

Messing with sensitive data just once is enough to ruin someone’s (virtual?) life. Especially if we’re talking about multiplayer games.

I guess no one other than hackers likes corrupted savegames.

At least not me. That’s why I wrote this blog post ;)

Let’s get into the meat of the post: today I’m going to help you increase the DEF stat of your sensitive data by applying the KISS Transaction Pattern.

So, What’s This Transaction Pattern in Game Development?

Here’s our primary quest for today:

We are going to forbid write-access to your most sensitive-data… except to the 1% of your codebase that truly needs it.

So 99% of your code will only have “getter” access. Because last time I checked it’s hard to mess anything up if you only got read access, right?

Only carefully crafted processes will be allowed to modify your sensitive data. The concept of whitelisting, that is.

Let’s assume Jeff is playing our “never-finished MMO” and wants to change his name to Jefferson. Just because some sort of virtual trauma.

Here’s the question for you: \wWhich code should ever be allowed to call SetName?

Here are my two cases:

  • A) During character creation
  • B) When a game master (GB) manually approves your name change

So yeah, we want ALL our codebase to only have read-access to the name… except for these two cases.

Before I show you the solution, here’s more data you really want to protect:

  • Level/xp: should only be attained through beating down characters or doing quests
  • Inventory: add items only during merchant purchases, quests or looting. Can you guess why it was so easy to make dupes in Diablo 2?
  • Health: can only be increased through healing spells or resting and decreased by falling or enemy damage.

You see the common denominator?

We decide ahead of time WHEN exactly we want to allow these changes.

And for this, the transaction pattern will kindly help us build this whitelist.

Let’s see how.

How to Use the Transaction Pattern in Unity — The “KISS” Way

Aiai… too many approaches to the transaction pattern.

And I don’t like most of them. Again, they’re often overengineered and complexity is NOT the solution to your problems.

Let me say that again out loud: complexity only brings you fresher problems.

Simple in ”KISS” means:

  • Simpler to explain to other developers.
  • Simple for your future self to remember the classic “how did it work this thing again?”.
  • Simpler to maintain, less overhead & frustration to use and extend.

KISS is the acronym I stand for: Keep Things Simple(,) Stupid.

As with the other KISS Patterns, let’s build a simple solution, shall we?

These are the key sentences you must burn into your mind just like OLED-TVs burn-in reveal who watched too much p***nhub:

The KISS Transaction Pattern separates read-access (in the interface) from write-access (on its specific class). Your codebase only gets access to your read-only interface, which allows you to execute carefully crafted transactions that have write-access.

Got it?

Probably not. I also prefer examples over definitions, so let’s give this a good kick.

Let’s assume we want to protect the Name property of your player. This is how the player would look like:

public interface IPlayerProfile
{
  string Name { get; }
  
  void ExecuteTransaction(PlayerTransaction transaction);
}

public class PlayerProfile : IPlayerProfile
{
  public string Name { get; set; }
  
  public void ExecuteTransaction(PlayerTransaction transaction)
  {
  	transaction.Execute(this);
  }
}

Saw that miracle?

  • IPlayerProfile: read-only interface referenced across your entire codebase.
  • PlayerProfile: class with write-access only referenced by approved transactions.

And now the trick is that the interface allows you to execute transactions that will eventually get write-access. Like this:

public abstract class PlayerTransaction
{
  public abstract void Execute(PlayerProfile playerProfile);
}

public class AdminChangeNameTransaction : PlayerTransaction
{
  private string _newName;
  
  public AdminChangeNameTransaction(string newName)
  {
  	_newName = newName;
  }
  
  public override void Execute(PlayerProfile playerProfile)
  {
  	playerProfile.Name = _newName;
  }
}

Key facts to remember:

  • You construct transactions with any parameters you might need, e.g. the new player name.
  • Transactions do their work in the Execute method, which get write-access to the player data.

Finally, your game admins can now use the transaction to safely change the player name like this:

[Inject] IPlayerProfile _playerProfile;	// Note this is the interface

void OnAdminChangeNameButtonPressed()
{
  _playerProfile.ExecuteTransaction(new AdminChangeNameTransaction(nameInput.text));
}

Juicy!

(Noticed how well this plays with the KISS Dependency Injection Pattern?)

Now you can go ahead and:

  1. Identify situations that require changes on your sensitive data.
  2. Implement transactions for these scenarios.

Some developers don’t understand the benefits of an approach like this. So here are some on top of my head:

  • Simplicity: fewer bugs, fewer exploits, less overhead & frustration for YOU.
  • Cohesion: you can now safely group operations that make sense together. For example, IncreaseXPTransaction does not only add XP, but might also increase the level when enough xp is accumulated.
  • Mental structure: it forces you to think ahead of time who will you grant write-access to for your sensitive data.

However!!

Not everything is cute rainbows & butterflies. There’s also a sucky dark thing about this transaction pattern…

A Warning on the Transaction Pattern

The KISS Transaction Pattern for Unity works wonders because it’s simple and makes your programming style safer. Period.

But there’s a single warning I must give you about this specific implementation: it is dangerous for your project performance.

Why?

Because these example transactions allocate memory on the heap and fragment your memory by default.

Here, see for yourself.

Memory allocations of the Default Transaction Pattern

You might think you can fix that by converting them into a struct, but no, that will only make things worse.

The result is clear: slower framerate and occasional freezes that your players won’t particularly enjoy.

It is totally ok that you use this sample implementation during your prototyping phase. But if you’re working on a production project that you eventually want to ship, you should use the production-ready, high-performance KISS Transaction Pattern.

To gain access to a detailed lesson on the production-ready KISS Transaction Pattern with ZERO allocations, join the Performance Taskforce & unleash the beast. You’ll find all the content in module 2021.09 after you join.

Ruben (The Gamedev Guru)

The Gamedev Guru Logo

Performance Labs SL
Paseo de la Castellana 194, Ground Floor B
28046 Madrid, Spain

This website is not sponsored by or affiliated with Facebook, Unity Technologies, Gamedev.net or Gamasutra.

The content you find here is based on my own opinions. Use this information at your own risk.
Some icons provided by Icons8