1

im working on a RPG (combat system) and have a question about a specific part of the design / code.

First I have to explain some background information about the system:

Every time a character deals damage to an other character (weapon attack, spell damage etc) the buffs / status effects of the attacker and the target can modify the damage. This is handled by events the attacker raises a "DealingDamage" event, which has the needed information as (mutable) event parameter and the target raises a "TakingDamage" event with the same event parameter.

For example a attack buff on the attacker could add a event handler for the "DealingDamage" event of the character and modify the damage value while a defensive buff on the target could add a event handler for the "TakingDamage" event of the character and reduce the damage value.

The flow would be similar to this:

  • Something (weapon, ability, spell) wants to deal damage, it has the following information a user (character), target (character) and a "base dmg"
  • Then "it" calls a damage method which handles the actual damage (events etc)
  • The damage method invokes the events of the attacker (to buff the attack)
  • Then the damage method invokes the events of the target (to modify the damage final damage)
  • Then the final damage is dealt to the target

Now the actual question: Im not sure where the "damage" method should be.

A, It could be a static method which takes the attacker, target and "base" dmg as arguments(but then it would need acces to some privat stuff, for example to raise the events)

B, It could be a non static "TakeDamage" method which is called on the target for example target.TakeDamage(baseDamage, sourceCharacter), but then the method (which is called on the target) would raise events of the source character which feels weird or am I just overthinking it?

Since the "final" damage will be dealt to the target, I prefere version B, because the method will actually modify a value (the health) of the target character.

Which version do you like better or does someone have a additional idea?

(Similar methods/events will be added for healing, but I wanted to keep the example "short", because it's already long enough)

2

B, It could be a non static "TakeDamage" method which is called on the target for example target.TakeDamage(baseDamage, sourceCharacter), but then the method (which is called on the target) would raise events of the source character which feels weird or am I just overthinking it?

You are not overthinking it. You are simply looking at it from a specific perspective, which ends up leading to various levels of coupling between classes, or, as you put it, making it necessary to access private stuff of other classes, or raising events of other classes. This is clearly a sub-optimal design.

If you "leave" the perspective aside and just manage it in a more "perspective-less" way, you may find the end result easier to manage in the long run. Short answer is that no partakers raise the event(s) themselves. The event is not even a character event. It is a general (domain) event that includes as much relevant information, as possible, and characters simply respond to it accordingly.

Take a look (I hope I have not made too much of a mess):


public interface IBattleEvent
{
    //Any information that might be a global fit. Otherwise, you can simply leave
    //this empty for type-safety in the following constraints. Or you can skip it
    //(and the type constraints in the following classes) entirely, if you wish.
}

public interface IBattleEventPublisher
{
    //Strict interface method
    void Subscribe<T>(IEventHandler<T> handler, object token) where T : IBattleEvent;

    //Convenience method, if defining a handler class is too fussy!
    void Subscribe<T>(Action<T> handler, object token) where T : IBattleEvent;
}

public interface IBattleEventDispatcher
{
    void Dispatch<T>(T battleEvent) where T : IBattleEvent;

    //Or maybe, with a token... aimed at specific listeners...
    void Dispatch<T>(T battleEvent, object token) where T : IBattleEvent;
}

//A damage event example.
public class WeaponDamageBattleEvent : IBattleEvent
{
    public ICharacter SourceCharacter { get; }
    public ICharacter TargetCharacter { get; }

    public IWeapon UsedWeapon { get; }
}

//A better damage event example.
public class CastingSpellDamageBattleEvent : IBattleEvent
{
    public ICharacter SourceCharacter { get; }
    public ICharacter TargetCharacter { get; }

    public ISpell UsedSpell { get; }
}

//Accompanied by the -ing variant (cf. OnDamaging).
public class PreparingSpellDamageBattleEvent : IBattleEvent
{
    private bool _cancel = false;

    public ICharacter SourceCharacter { get; }
    public ICharacter TargetCharacter { get; }

    public ISpell UsedSpell { get; }

    public bool Cancelled => _cancel;

    //Make this a method, so that it cannot be revoked along the
    //event listener handling sequence, once it has been applied.
    public void Cancel()
    {
        _cancel = true;
    }

}

public class MageCharacter : ICharacter
{
    private int _hp = 1000; //For illustration purposes, obviously!

    private int _mana = 1000; //For illustration purposes, obviously!

    public MageCharacter(/* ... */)
    {
        //Event subscription could also take place here.
    }

    //Method to include character in game..
    public void Spawn(IBattleEventPublisher eventPublisher)
    {
        eventPublisher.Subscribe<PreparingSpellDamageBattleEvent>(PrepareSpellDamage);

        eventPublisher.Subscribe<CastingSpellDamageBattleEvent>(CastSpellDamage);

    }

    void PrepareSpellDamage(PreparingSpellDamageBattleEvent spellDamageEvent)
    {
        if (spellDamageEvent.SourceCharacter == this)
        {
            int neededMana = GetNeededMana(spellDamageEvent.UsedSpell);

            //Spell cannot work if it needs more mana than is available.
            if (neededMana > _mana)
                spellDamageEvent.Cancel();
        }
    }

    void CastSpellDamage(CastingSpellDamageBattleEvent spellDamageEvent)
    {

        if (spellDamageEvent.TargetCharacter == this)
        {
            //Take damage in private, by reading data from the event and
            //acting accordingly.
            int baseDamage = spellDamageEvent.UsedSpell.BaseDamage;

            //modify as needed, based on source character or target character
            //characteristics.
        }
        else if (spellDamageEvent.SourceCharacter == this)
        {
            _mana -= GetNeededMana(spellDamageEvent.UsedSpell);
        }

        //Log, maybe???
    }
}

I understand this may appear to be a bit convoluted, and I am probably messing up what should be a rather simple example. Maybe you can borrow some ideas from the concept. However, I wanted to make a case for targeting the main question: who dispatches the event.

Well, this could be anything of your choice. The idea is that neither the target, nor the source character raises the event. The BattleEventDispatcher raises the events and this might be (has to be, actually) some Gameplay "orchestrator" class, or a class that observes events on the UI. I don't know any more details of your case, but a very rough example would be: character walks around ==> other character is visible ==> characters meet ==> weapon is wielded ==> hit-test succeeds ==> event is raised.

In short, whichever class is the one that determines that an attack is about to take place, is the class that should raise the event. If any of the participating classes undertakes that task, one often ends up unknowingly violating the Single Responsibility Principle.

0

Using events may work. But I don't think I would do it like that at all. When using events you are depending on the order in which modifiers are handled.

Instead you could create a ICombatSystem interface which contains a HandleAttack(IDamageSource source, IDamageTarget target). IDamageSource could contain a GetDamageAndModifiers() which could return Modifiers which your ICombatSystem implementation could then reorder and apply according a certain strategy.

The handle attack could for instance then be implemented to get base damage and modifiers. Character modifers, Weapon modifiers, state modifiers (Are we shooting an arrow under water?). Also after determining attack you could do the same with defensive stuff and finally the combat system could call ApplyDamage(dmg).

BTW I specifically use IDamageTarget and IDamageSource. Because combat, IMHO, could be between more than just a character and character, e.g. character kicking in a door.

  • Thanks for the answer, I will think more about it later. I just wanted to add something (I think I might should have added that to the question) Im using a custom event system instead of the "default" C# events, one of the main reasons was, that the custom event system can add event handler with a sort order to control in which order they are called (so for example status effects can add their event handler with a different order depending on their priority, so that one buff is handled before the other, if needed) – R1PFake Nov 29 '19 at 14:03
0

The trouble with OOP and games is that you have no limitations on your potential requirements.

For example you might have a character that takes less damage from other characters that have eaten a pie in the last 24h.

Its hard to design objects in such a way that you would only have to modify that single characters class inorder to achieve that game mechanic.

Your DoDamage and TakeDamage methods are the classic way to "attack" the problem, but you still need to define a structure to your combat mechanic such as hitpoints and modifiers which will of a nescesity limit what features you can add within that structure.

Once you have that structure where you put the methods is less important. Although personally I do favour events in a UI heavy application as you can hang multiple UI stuff, blood spatter, explosions, staggering etc off the event without having to edit the 'game logic'

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.