Tools: Easy Tools

This page assumes you’ve read and implemented these three systems into your game, at least for the first couple.

Heat Map

A heat map simply shows where players did certain events across multiple playtests. Let’s assume we’re writing this for a Mega Man style game. You can jump. You can shoot. I can think of a few other events, too:

  • Jump
  • Shoot
  • Take damage
  • Gain health
  • Die

We could create a class like this:

class HeatMapRecorder
{
    HeatMapRecorder()
    {
        ConnectEvent(GetPlayer(), MotionEvent, OnMotionEvent);
        ConnectEvent(GetPlayer(), DamageEvent, OnHealEvent);
        ConnectEvent(GetPlayer(), HealEvent, OnDamageEvent);
    }
    
    ~HeatMapRecorder()
    {
        DisconnectEvent(GetPlayer(), MotionEvent, OnMotionEvent);
        DisconnectEvent(GetPlayer(), DamageEvent, OnHealEvent);
        DisconnectEvent(GetPlayer(), HealEvent, OnDamageEvent);
    }

    void OnMotionEvent(MotionEvent& e)
    {
        WriteEventToFile(Color::Blue, e.Entity->GetComponent<Transform>()->GetPosition());
    }
    
    void OnHealEvent(DamageEvent& e)
    {
        WriteEventToFile(Color::Green, e.Entity->GetComponent<Transform>()->GetPosition());
    }
    
    void OnDamageEvent(HealEvent& e)
    {
        WriteEventToFile(Color::Red, e.Entity->GetComponent<Transform>()->GetPosition());
    }
    
    void WriteEventToFile(const Color& aColor, const glm::vec3& aLocation)
    {
        <code to write the color value and location to file>;
    }
}

Note that this code has a lot more it could be doing to be more efficient, like buffering first and then writing to file every second or so. I kept it this way to just get the idea across!

Now we have a file of colors and locations on the map. If we have a bunch of playtesters run through the game, we can get a lot of data. Now we can render each location with a tiny box of the given color. If another box of that color was near it, we make the box bigger instead. There’s a lot more we can do as well:

  • Filter out certain colors so you can see specific events (deaths, paths, jumps, special moves).
  • Filter by the size of particular portions on the map to see common areas.
  • Add a run ID to the colors so you can filter by run.
  • Add timing data so you can see how long it took people to get to certain spots (ex. after 2 minutes, what does the heat map look like?).
  • If you have the ability to store this in a database, you can read and write to it instead of saving it all locally. You can even get live updates. This is known as Telemetry.

Metrics

Events don’t just have to be player actions. We can also mark when the player has achieved or started something:

  • Level start/end
  • Encounter start/end
  • Cutscene start/end
  • Death start/end

We can use those events to provide bookends for certain events. Simply store a string named “level_” and change it when you start/end a level. For example, let’s say the player died. We have a lot of information we can collect about that death!

  • What level were they on?
  • What encounter (if any) where they in?
  • How long since level/encounter start was this?
  • How many deaths were there during this session?
  • Game specific metrics.

This works for non-physics/combat based games as well! How long were they in a certain menu? What was the first button they pushed on the main menu? Did they ever go to the options screen?

You can record all this information and start processing it into different metrics.

  • What menu do players spend the most time on?
  • What section to players die the quickest on?
  • What line of dialogue is skipped the fastest?
  • What line of dialogue is waited on the most?
  • What level do players quit on?

These can be saved to file in your own file type, or you can save them to csv (Comma Separated Values). That’ll let you open them in Google Sheets or Excel and analyze it as a team. You can even do some deeper data analysis using their tools as well!

Random Input Player

One day, you’re going to be playtesting your game with someone you may not know. They’re going to hit some crazy key combination you didn’t think of like pause break + 7 and your game is going to scream. That’s why it’s important to have a system designed to find odd cases you wouldn’t think to do.

The quick implementation of this is to launch the game and practice sliding your face from the left to right of the keyboard. A more elegant solution might look like this:

void Update(float dt) 
{
    timer_ -= dt;
    if (timer_ <= 0.0f)
    {
        PlayRandomInput();
        timer_ = InputRate;
    }
}

PlayRandomInput would simply work with your input manager and register key presses. It’s a good idea to check if the input was down/triggered first so that you can release it if it is pressed again.

There’s many specific events we may want to mock as well. Let’s assume we’re playing an FPS with a rocket launcher. We may want to mock explosions, spawning enemies, firing weapons, placing physics objects and much more. A particular bug might only involve a number of these things happening at once!

This is where data driving makes everything awesome. Here’s an example test we made for Zero Day (our Junior/Senior game) called ThousandFramesOfPain:

NumEditsToDo = 1000
DelayBetweenInputs = 0.016
InputsPerBurst = 1
IsDeterministic = true
InputType = Planes
VoxelEditPosRadius = 149
VoxelEditSize = 30

That test use to be hard. HA! Try loading in multiple levels at the same time.

It’s dead simple to read that in and configure the system to do exactly what you want it to. You don’t have to invent a language! X = Y is more than enough! Our system had a txt file that it would read to find out which “.rnd” file (shown above) we wanted it to load in for the random input system. Now, new tests don’t even require recompilation.

There’s one variable there that’s pretty important: IsDeterministic. If you are using rand() for you random input system, your random input system is too random. Let’s say you hit a bug with that configuration. Next time the game plays, it probably won’t happen. Why? If the seed is based on time, it’ll be different. Even if you set the seed, it can still end up different! What if a slight bit of lag causes your particle effects to do one extra use of rand()? It could completely change the results! That’s pretty worthless. The solution is to create our own random function just for this system!

int Engine::RandomInputSystem::DetermRand()
{
    if (isDeterministic_)
    {
        static long seed = 37;
        static long hold = seed;
        seed ^= seed << 13;
        seed ^= seed >> 17;
        seed ^= seed << 5;
        hold += seed;
        hold %= 0x7fff;
        return abs(hold);
    }
    else
    {
        return rand();
    }
}

I’m pretty sure I don’t do drugs, but this function makes me question that. In short, this takes a function static seed, shifts the bits around a bunch, adds that to the number we’re going to return and clamps it to 0x7fff. That 0x7fff is the maximum value of RAND_MAX. After that, it makes sure it didn’t wrap around to the negatives and returns it.

clang-format

There is never a reason to have a debate about style in your code except in one situation; You are setting up clang-format for the first time. If you are using Visual Studio 2017, you can install it into visual studio and have it run every time you save. If that gets annoying, you can simply hit Ctrl + R D to activate it for the current file.

Visual Studio has so many hot keys that they had to make a way to add more. Their solution is hitting two keys in a row while holding a modifier key. If you see Ctrl + R D, read it as holding Ctrl while you hit R and then D.

Keep in mind that if you are on a school computer, you may need to ask a Space TA or IT for permission to download and install Clang Format. You may also need to update your version of Visual Studio and restart, so try to make it easy on them and be ready for them to enter their password. Make sure you get it for your entire team if this is the case!

Go to Tools -> Extensions and Updates here:

A window will pop up. Open up Online -> Visual Studio Marketplace:

On the top right, search for “ClangFormat” and hit enter:

Click “Download” and the following should show up at the bottom of the window:

Close out of Visual Studio completely and a VSIX installer should pop up:

Simply click “Modify” and it will be installed for you. Reopen visual studio and go to Tools -> Options:

A window will pop up. On the left side, scroll down to LLVM/Clang and click on ClangFormat.

Use these settings:

In the top level directory of your project (next to the sln), add a file named _clang-format (no file type!). Add the following code to it:

BasedOnStyle: WebKit
Language: Cpp
Standard: Cpp11

DisableFormat: false

AccessModifierOffset: -4
AlignAfterOpenBracket: true
AlignConsecutiveAssignments: true
AlignConsecutiveDeclarations: false
AlignEscapedNewlines: true
AlignOperands: true
AlignTrailingComments: true
AllowAllArgumentsOnNextLine: false
AllowAllConstructorInitializersOnNextLine: true
AllowShortBlocksOnASingleLine: true
AllowShortCaseLabelsOnASingleLine: true
AllowShortFunctionsOnASingleLine: false
AllowShortIfStatementsOnASingleLine: false
AllowShortLoopsOnASingleLine: false
AlwaysBreakBeforeMultilineStrings: false
AlwaysBreakTemplateDeclarations: true
AlwaysBreakAfterDefinitionReturnType: None
AlwaysBreakAfterReturnType: None
BinPackArguments: false
BreakBeforeBraces: Custom
BraceWrapping:
  AfterEnum: true
  AfterClass: true
  AfterStruct: true
  AfterControlStatement: true
  AfterFunction: true
  AfterNamespace: true
  AfterObjCDeclaration: true
  AfterStruct: true
  SplitEmptyFunction: true
  AfterExternBlock: true
  AfterUnion: true
  BeforeCatch: true
  BeforeElse: true
  SplitEmptyFunction: true
  SplitEmptyRecord: true
  SplitEmptyNamespace: true
  IndentBraces: false
BreakBeforeBinaryOperators: true
BreakBeforeTernaryOperators: true
BreakConstructorInitializersBeforeComma: true
ColumnLimit: 80
CommentPragmas: "DO_NOT_FORMAT"
Cpp11BracedListStyle: false
DerivePointerAlignment: false
ExperimentalAutoDetectBinPacking: false
IncludeCategories:
  - Regex:           '^<.*'
    Priority:        1
  - Regex:           '^".*'
    Priority:        2
IndentCaseLabels: true
IndentWidth: 4
IndentWrappedFunctionNames: false
KeepEmptyLinesAtTheStartOfBlocks: false
MacroBlockBegin: "MACRO_START"
MacroBlockEnd: "MACRO_END"
MaxEmptyLinesToKeep: 1
NamespaceIndentation: All
ObjCBlockIndentWidth: 4
ObjCSpaceAfterProperty: true
ObjCSpaceBeforeProtocolList: false
PointerAlignment: Left
SpaceAfterCStyleCast: false
SpaceBeforeAssignmentOperators: true
SpaceBeforeParens: ControlStatements
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 1
SpacesInCStyleCastParentheses: false
SpacesInContainerLiterals: false
SpacesInParentheses: false
SpacesInSquareBrackets: false
UseTab: Never
TabWidth: 4

This is YAML code that will format your code pretty closely to Mead’s style. Use this webpage to make adjustments that your team wants.

The link above is a little strange with how they tell you about enums. If you see one like:

DRTBS_None (in configuration: None) 

Make sure you put “None” and not “DRTBS_None” in the _clang-format file!

I highly suggest using the file at this link (scroll down to the C++) to work with your team on finding a style everyone agrees on. Just hit Ctrl + R D to apply it to the current page!

Now, every save you do will automatically reformat your code to your team’s style!

Recap

There are a lot of programmers in the world. If you can think of a tool, it probably already exists in some form. Seek them out!

Now, if the whole function static business in there didn’t make any sense to you, the next section is going to go over it as we tended to use it a lot.