Sunday, July 26, 2015

Some Notes on Game Structure

I'm at a point now where I've got a few nice building blocks, and I can start turning them into more of a system.  I'd like to now move toward building commands, abilities, movement, etc.  I'll need a proper system to manage these pieces, and you'll see why thinking about them holistically is both necessary and hard.

For this post, I'm just going to think out loud stream of consciousness style to gather some thoughts on how I'm going to structure the command system from here.  Keep in mind my Unreal experience is quite low, so this might be a terrible way to do things.  But alas, we must move forward somehow, and we learn best when we get burned!

First, let's start with some requirements.

  • From the user's point of view, it'll be a standard tactics game style of UI.  Select a character, select an ability, go into targeting mode, select a target, confirm the target, execute the ability.  
  • Different abilities may have slightly different targeting rules and steps.
  • It'd be nice if the system can support an action history because I like seeing that in my tactics games.  
  • The same system should work for players and AI.  
  • I'd like it to be very easy to construct new abilities from simple building blocks.  
  • Finally, I want it to follow good engineering principles: solid code reuse, low coupling, etc.

There are different approaches you can take when designing a system like this.  I frequently use a bottom-up approach where I imagine what it would look like to build a few different things using this system that doesn't exist, then I look at the pieces I now require and move up a step.  I find that this approach makes it easier to understand real-world use cases.  A top down approach has a tendency to miss real-world cases, leaving you in the awkward position of saying the user can't do what is desired or hacking the system to make it work.

So let's see how we would build a Fireball ability.  This ability can target a location and explode, dealing instant damage to everything in range and applying a Burning debuff to them.  Let's write some pseudocode!

function Fireball::Execute
    damage = CalculateDamage()
    characters = target.GetCharactersNearTargetPoint(explosionRange)
    for each character in characters
        ApplyDamage(character, damage)
        AddEffect(character, Burning)

function Fireball::CalculateDamage()
    return baseDamage + scalingDamage * myCharacterLevel

function Target::GetCharactersNearTargetPoint(range)
    return SearchCharacters(targetPoint, range)

Okay, so I've highlighted some pieces worth talking about.

Target: The ability needs to know where to apply its effect.  A target object seems like a nice way to encapsulate that concept.

MyCharacterLevel: The ability also needs to know some information about who is using the ability.

ExplosionRange: To reduce coupling, we pass in any relevant ability information to the targeting object so it doesn't need to know anything about the ability. 

Note: my first draft had me passing the ability into the target, but that caused a two-way dependency, which is generally icky.  By pushing the data into the target rather than having the target pull the data out of the ability, that makes all flow unidirectional. Conceptually I like this, too -- an ability needs to have a target in order for it to execute, but a target can stand alone and doesn't need an ability to exist.

This suggests a basic class structure already: A character has an ability. An ability has a target (hold onto this thought).  

As of right now, an ability also has a reference to its character. This goes against what I just said about explosionRange, but for some reason this coupling bothers me a bit less.  Probably because conceptually an ability naturally feels like it only exists as a result of a character and also because the Unreal Component structure is such that the Character will already be accessible to any Abilities Component inside it.  I'm going to move on for now, and we can revise this later if I feel gross about it once I understand the rest of the system more.

So now that we know what an ability is, how do we execute it?

The user has a selected character that we keep track of.  There's also some UI that maps each ability, so when I press a button or hotkey, I activate that ability (which is itself a multi-step process).  We'll call this request to use an ability a Command.  Not only does Command work conceptually, but we're literally using the Command design pattern as well. 

Interestingly enough, this article on the Command pattern showed up on my Twitter feed this morning, so I even have a relevant link to give for those not familiar with the pattern:http://gameprogrammingpatterns.com/command.html.  I know, it's unfashionable to talk about design patterns these days, but I still stand by them when used appropriately, especially when it comes to easily communication through a shared language.  But I digress...

I like the idea that a Command brings together an Ability and a Target and tells them to combine (execute).  This tells me that we'd create a target object, run a state machine until it's valid and locked in, then pass that Target into the Ability when telling it to execute.  That thought I told you to hold onto above: an Ability only has a Target when it's executing (i.e. we pass it in as a parameter).

Let's bring it back to Unreal now.  When the user hits that ability hotkey, we'll need to construct a Command.  The PlayerController can probably handle that construction.  It'll then pass in the ability.

Once the ability is passed in, the command will ask the ability to construct a target object.  We go factory style on the target because, as we'll discover later, the exact subclass of Target depends on the ability.

I'm going to apologize right now for my inconsistent capitalization and just stop trying to pick a style. It makes sense in my head, okay?

Once we have the command, the PlayerController would hold onto it as the current active command. It would also bind some events on that command so it can respond as the state machine updates.  It would also immediately ask the command to begin targeting.

The command then forwards the targeting request to the targeting object, along with any per-command information it needs (such as caster location).  That causes the targeting object to show and update as the user's mouse is moved around. The ability had already fed in information like whether or not it requires line of sight, the types of characters that are valid, etc. The targeting object uses all that information to determine whether or not the target is valid, which it uses when updating the UI.

Once the user clicks, the targeting object fires an event that the target has been acquired.  The command was listening for this event and will advance the state machine to the lock in phase.  Some UI will get updated and show a confirmation frame.  If confirmed, that will then cause the command to execute, which calls the Execute function on its ability, passing in the target.

For external systems, such as UI frames, I imagine the PlayerController sends an event any time the active command changes.  Those systems can then register directly on the active command so they can respond to changes in lock in and targeting state.

Finally, let's talk about the actual class hierarchies.  I know this is getting long, but I need these notes to help gather my own thoughts. :)

We have a Command, and I'm going to look slightly ahead and have a base class for Command and a subclass for CommandCharacterAbility.  This way I can use commands for other things if necessary.  For example, what if Save was a command?  Or how about that most useful of debugging tools -- cheat commands?

Command
    void Activate()
    void Execute()

CommandCharacterAbility extends Command
    void BeginTargeting()
    Ability* ability
    Target* target
   
As we said before, characters have a set of abilities.  They're not references here because characters own the abilities.

Character
   Ability[] abilities

Next the ability itself.  Abilities can all be executed.  They can also create targets.  In this way, AbilityFireball would construct a target of type TargetPointAOE while AbilityHeal would create a TargetCharacter. The Command and other classes could then operate on that target without caring what kind of target it is.  Yay polymorphism.

Right now I have the abilities deriving straight from Ability.  If I discover there are common ability archetypes, we could add another layer there.

Finally, the IsTargetValid function is worthy of special mention.  Imagine we disallow you to cast Heal on targets with full health.  I'd still like the targeting system to consider that a valid target, but I don't want you to be able to Lock In the ability.  If we simply prevented you from targeting at all, there'd be no opportunity to tell you why you can't cast the spell.  Also, that kind of complex validation where we look at character status doesn't feel right inside the Target class.  I'd rather custom validation like that occur inside the ability.  So there are two styles of validation: whether you can select the target and whether you can choose the selected target.

Ability
    void Execute(Target*)
    Target* CreateTarget()
    bool IsTargetValid(Target*)

AbilityFireball extends Ability
    override void Execute(Target*)
    override Target* CreateTarget() // Type: TargetPointAOE

AbilityMove extends Ability
    override void Execute(Target*)
    override IsTargetValid(Target*)
    override Target* CreateTarget() // Type: TargetPoint

AbilityHeal extends Ability
    override void Execute(Target*)
    override IsTargetValid(Target*)
    override Target* CreateTarget() // Type: TargetCharacter

Finally, we have the target.  The base target has a bunch of common properties, such as the origin position, range, etc.  Each subtype would draw a different kind of targeting UI and have different accessors for getting the data you'd need out of it.  For example, TargetCharacter has a GetTargetCharacter() function for obvious reasons, but that function doesn't make sense for TargetPointAOE.  The abilities know what their TargetClass is, so they'll be equipped to cast down during execution.

Target
    bool IsValid()
    void LockIn()
    Vector originPosition
    float range
    bool requiresLOS
    bool friendly

TargetPoint extends Target
    override bool IsValid()
    position GetTargetPoint()

TargetPointAOE extends TargetPoint   
    override bool IsValid()
    Character*[] GetCharacters()
    float aoeRange

TargetCharacter extends Target
    override bool IsValid()
    Character* GetTargetCharacter()
    void SetAllowedTypes(bitmask)

I still have some questions on this architecture.  For example, while targeting a Move, it would be nice to draw a path on the ground.  Who is responsible for that?  The Target object doesn't have enough information on that because different Characters may move differently.  I'll have to think about that some more.

I'm also a bit afraid at the number of parameters that'll go into the target classes.  Maybe it should just have a reference to the ability?  RequiresLOS, Friendly, and Range all seem like properties of an Ability, and duplicating them all down into the target just for some self-imposed encapsulation may cause more problems than it's worth.

So this was a massive post and far larger than I intended, but I think it highlights the amount of consideration needed when designing something as core as a command/ability/targeting system in your game.  Sure, it'd be super easy to just start writing code or blueprints, but decisions I make early will have a massive impact on what is easy and what is hard in the future.  A little forethought now can help make sure I don't design myself into a corner.

I'll also say that I lied a bit at the top of this post -- it wasn't stream of consciousness.  As I thought about it and discovered different considerations, I would go back and make changes.

This process of make a design, ask questions about it, revise, poke holes in it, etc. is very normal.  In fact, writing up this blog (i.e. explaining it) was very helpful in that process.  The act of explanation, whether on a blog or with a teammate, is probably your #1 tool when tech designing.  It's amazing how you find errors when you go through the exercise of putting words to it.  I spent most of the day on this post, and I feel like I made great progress on the game despite never opening Unreal.

That said, the threat of overengineering is real, especially on a personal learning project where the scope will be limited.  Perhaps it's time to start building so I can find those holes using real world problems?

I'll be sure to compare various stages of implementation with these notes to see how close we got.

No comments:

Post a Comment