Concept definition, or how to lose time and sanity.

Today we talk about something that, we, game devs, can be really bad at : correctly expressing and implementing design concepts.
This is one of the main source of frustration and need for refactoring when the design evolve, making small changes takes weeks or months, instead of hours.

This article is mainly targeted at gameplay programmers and designers, but I hope everyone can find something for them in it.

The Example.

Now let’s put you in the body of a programmer on a third person RPG. Things are going nicely, and you are tasked to add some enemy AIs to fight against your player character.
But first you need to implement the target detection system, so that your AIs know which actors they must combat.

Watch Dogs Legion

So you check in with your game designer, so they can give you a list of conditions for an actor to be considered a threat.
“Let’s do something simple first”, they say to you, “the AI should target anyone that is not of their faction.
Simple enough.
You implement a nice Threat Detection Component and one method is there to check just that.

bool ThreatDetectionComponent::IsThisAThreat(Actor _actor)
{
    // m_myActor is the AI, _actor is someone else ( player, another AI, etc )
    if( m_myActor.getFaction() != _actor.getFaction() )
    {
        return true;
    }
    return false;
}

And you just made a terrible mistake.

That single, seemingly harmless if-line is a trap, because it represents something that should be a whole design concept, but without explicitly defining and isolating it.
All of which can have huge ramifications down the line.

You have conflated two features that should only be correlated.
Affiliation ( what is my faction ? ) and Relationships ( Who are my ennemies ? ) in this example.

So what are the consequences with this :

Well, first because the test is so simple, everyone might start using the same one everywhere.
An UI Programmer could check the factions of the actors to change the color of their icon on the minimap, for instance.
Or a game designer will compare the two faction in the visual scripting editor.
People might start adding that test every time they need to differentiate between an ally and an enemy, to play barks, to do a look at, etc.
And soon, you have hundreds of faction checks like this everywhere in the code ( and maybe in the data ).

And then, one day, a mission designer comes and talks to you.
“Hey, so in mission 12, it turns out that character X is a spy, and even thought they look like a Faction2 guard, they need to start fighting with all of the others Faction2.”
or
“Did you hear ? after Mission 6, all of the different gangs unite and need to fight together.”

and you don’t know it yet, but your world just started to crumble.

To implement those two requests, you ( and most likely lots of other devs ) would have to go look into the code, through the hundreds of factions check, trying to sort the ones you need to actually change from the ones that are legit ( like for selecting the right outfit for instance ), and then changing the code everywhere to take into consideration the new conditions.
It takes days, it’s tedious work, and because people missed some, or changed some that shouldn’t have, bugs are created ( “JIRA-54662 : CharacterX in Mission 12 is supposed to be a friend once revealed as a spy, but they are still red on the minimap” ) and everyone is unhappy.
And that’s assuming you can do it right !
But maybe there is a milestone days away, so you do not not have time to do all that work. So you add a work-around, to hack those exotic behaviors, but now other systems have bugs and your hack is propagating because you need to fix those too.

How to help avoid all that.*

* I say help avoid because of course, game dev being game dev there is no magical solution, just little tricks that might be of use.

Well let’s start with the design requirements.

“The AI should target anyone that is not of their faction.”

That phrasing is misleading because it already talks about factions, but what happen if we rephrase it to :

“The AI should target anyone that is not an ally”.

The next obvious question should be :
-“ok, but who are considered not an ally ?”
– ” People with a different faction”.

It might seem like exactly the same thing, because the result would be the same, but it introduces a whole new concept ! Relationships.
And because it’s been introduced, the programmer might think about a different implementation which would be ultimately better and more resiliant to changes.

The method could become :

bool ThreatDetectionComponent::IsThisAThreat(Actor _actor) 
{     
    // m_myActor is the AI, _actor is someone else ( player, another AI, etc )     
    if( GetRelationship(m_myActor, _actor) == Relationship.Hostile )     
    {         
         return true;     
    }     
    return false; 
}

Relationship GetRelationship(Actor _actorA, Actor _actorB)
{
    if(_actorA.getFaction() != _actorB.getFaction())
    {
         return Relationship.Hostile;
    }
    return Relationship.Ally;
}

So now, the UI Programmer or the game designer could use that same GetRelationShip() method, which is basic code reusability.
But more importantly they are now using the same concept.

You can also more easily expand the system, making it way more maintenable because it has been well defined and isolated.
You can add Relationship matrixes between the factions, and add easy global or per actor overrides to handle the two exotic cases from above.

The example is a bit simplified and over the top for clarity purposes but we actually do that all the time, sometimes for big things that should definitely be their own feature.
And sometimes with smaller stuff.

You might have seen some “IsAPlayer” condition in a design doc, or in the code, and most of the time we don’t actually care if the actor is the main character, we just use this because it’s easier.
” If the weapon owner is the player actor, I want to use the high-res muzzle effect in my FPS” is incorrect.
The real reason you want to use that asset is because the weapon is close and big in the viewport, and that would also be true if you add a feature where your player can control an armed drone, or a spider tank.

The SpiderTank mini game in Watchdogs 1


So your test should be :

“Is the weapon owner attached to the camera” instead.

Of course I’m not saying you should anticipate every possible design change, new features and exotic game mode before implementing a feature, that is impossible. But it always pays off to try to ask if everything is well defined, and shortcuts are not taken ( or at least identified ).
In both examples, the implementation changes are minimal, and yet the pay-offs can be huge.

Take-Away

When you design a feature, ask yourself a few questions to try and rephrase things if possible.
Ideally these questions should be answered by the designer, but gameplay programmers should be able to help as they have to actually bother with the implementation in most cases.
Those questions can be :

  • Have I expressed my feature as a need ( why ) and not a solution ( how ) ?
  • What are the other systems/features/concepts that my feature is relying on ?
  • What are the parameters or conditions of my feature and are they expressed as such ?

Making sure you take the time to express a feature correctly won’t be a guarantee on anything, but it will help design and architecture your game in what is hopefully a better way.

Leave a Reply

Your email address will not be published. Required fields are marked *

Prev
Should devs crunch ? Detailed analysis.

Should devs crunch ? Detailed analysis.

No

Next
Card Games AIs, quick overview of different algorithms.

Card Games AIs, quick overview of different algorithms.

The tweet below from @TrumpSc sparked a bit of a discussion about the AI