2
\$\begingroup\$

I'm working on a turn based rpg and have an action/queue system setup that I'm happy with. However, I am having trouble implementing a feature and I am here for some advice. I want to have effects/abilities that alter other outcomes of actions/effects and am not sure how to structure things to allow for this.

I'll provide an example to illustrate what I am trying to achieve.

Say we have two characters in combat. The attacker needs to roll 1d20 and add the result to their current strength to see if they make a hit. If that value is equal to or greater than the defender's toughness the attack is successful and the defender takes damage.

def Attack(attacker, defender):
    roll = Dice.roll()
    roll += attacker.strength

    if roll >= defender.toughness:
        defender.damage(attacker.weapon.power)

Now lets say the attacker has a piece of gear on that says if they get a critical hit (a roll of 20) they hit the monster no matter what and deal one point of damage. How do I inject this code into the attack function and have it ignore the portion of the function after the dice is rolled (the armor's effect is replacing the rest of the attack)?

I've thought of breaking the code into chunks

def Critical_Hit_Buff(roll, defender):
    if roll == 20:
        defender.damage(1)
        return False
    return True

def Attack(attacker, defender):
    do_continue = True
    roll = Dice.roll()

    #check if any buffs from armor/etc. activate
    for buff in attacker.buffs('attack'):
        do_continue = buff(roll, defender)

    if do_continue:
        roll += attacker.strength

        if roll >= defender.toughness:
             defender.damage(attacker.weapon.power)

But I really don't like having to carry around the continue variable everywhere and further I don't know if an action is not continuing because the action failed (some buff/debuff prevents the attack from being successful) or if it was because some effect replaced the attack event (this might be relevant if I give the player notifications that about if the action was successful or not).

Any advice?

\$\endgroup\$
2
  • 1
    \$\begingroup\$ It is hard to suggest a software architecture when one sees only that one use-case for your system. If I suggest you something, I might suggest something which works very elegantly in this particular case but does not work at all with your many other use-cases. Can you add some more information about what other kinds of effects and actions you want in your game? \$\endgroup\$
    – Philipp
    Commented Sep 16, 2018 at 3:28
  • \$\begingroup\$ The For Honor team gave a good talk at GDC last year about how they implemented modifiers and used them extensively. It may have some useful inspirations, though unfortunately the full video is still behind a paywall. The slides are free, but not as descriptive as the full talk. \$\endgroup\$
    – DMGregory
    Commented Sep 16, 2018 at 11:23

2 Answers 2

3
\$\begingroup\$

Split the actions, such as "main weapon melee attack" in the OP's example into smaller actions and conditions. Recombine them using a behaviour tree. For example:

'main weapon melee attack action split into smaller sub actions

Normally the purpose of a behaviour tree is to implement AI scripting and to allow easily composing AI behaviour. Here we will use the behaviour tree somewhat different1:

  1. Just like in the typical AI the behaviour tree is evaluated to execute the scripted behaviour.
  2. We converted code into data. Data can be easily manipulated from the running game. For this specific weapon we can replace the above tree with:Inserted extra sub actions for the special weapon
  3. This leaves one issue: when should we modify a sub action tree and how should we modify the tree? To solve this issue we make the tree searchable and design a simple query language for it. So that we can find any tree that contains the 'roll (attack dice)' action. We trigger the weapon's tree modifying action2 when such an action tree is about to be executed.
    The tree modifying action can replace the 'roll (attack dice)' node by a sequence of 'roll (attack dice)' and 'is crit?' or insert the 'is crit?' subtree after the 'roll (attack dice)' in the parent sequence of 'roll (attack dice)'.

1 You could still use it for you AI system as well. The possibilities and features will grow: weapons or spells that manipulate behaviour of computer played monsters and NPCs.
2 If you like, the modify tree action can also be encoded as a behaviour tree (but this recursive level of abstraction might make some people dizzy).

\$\endgroup\$
1
  • \$\begingroup\$ Every use of behavior trees I have seen so far was for AI. But using them to describe game mechanics is a really interesting idea. I might try something like this in some project one day. \$\endgroup\$
    – Philipp
    Commented Sep 17, 2018 at 14:41
1
\$\begingroup\$

After trying a few things I think I found a method I am happy with. I've broken the single Attack() function into several smaller functions. Each action has a default followup action that occurs when the current action is finished. When buffs are checked, "followup" is also passed in and can be altered. If a buff alters the outcome of the event it sets followup to a different function. From here the new action can either branch off into another new actions or, when it is done, return back to the default followup that would occur after the Damage function.

def Attack(attacker, defender):
    followup = Damage
    roll = Dice.roll()

    #a buff can edit what happens next
    for buff in attacker.buffs('attack'):
        followup = buff(attacker, defender, roll, followup)

    followup(attacker, defender, roll)

def Damage(attacker, defender, roll)
    followup = Counter_Attack
    roll += attacker.strength

    if roll >= defender.toughness:
        defender.damage(attacker.weapon.power)

    followup(attacker, defender)

def Critical_Hit_Buff(attacker, defender, roll, followup):
    def Critical_Damage(attacker, defender, roll)
        followup = Counter_Attack #same followup action as Damage
        defender.damage(1)

        followup(attacker, defender)

    if roll == 20:
        return Critical_Damage(attacker, defender, roll) #return new followup created by the armor buff
    return followup #return default followup

The above example assumes that there is at most one followup action that could occur when the buffs are checked (any others would overwrite the previous). However, it's possible to expand this to account for multiple paths.

\$\endgroup\$
2
  • \$\begingroup\$ Looks good! I'm glad you found a method that works for you! I'm currently looking into adding a tag to make it easier to browse questions about this general topic - you can find an incomplete list in this meta thread in case it's useful for any further inspirations. We're also trying to figure out what to call such a tag, so I'd be interested in your input. :) \$\endgroup\$
    – DMGregory
    Commented Sep 16, 2018 at 20:13
  • \$\begingroup\$ Awesome! I think effect-modifier or ability-modifier would both be good names. \$\endgroup\$ Commented Sep 16, 2018 at 20:28

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .