After talking with @Sunflash93 on twitter (find their blog here), I got to thinking I should post about how I designed the ability system in my game Z-Com. See my previous blog post for more info about the game.

In my coding adventures, I like to attempt to stick to the SOLID principles of OOD as outlined here. Namely, my favorite and arguably the easiest of the principles to smell is the Single Responsibility Principle. In short, A class should have one, and only one, reason to change. This can and should be applied to all facets of OO programming, including game development.

When first trying to flesh out the details of how I would do abilities, I knew I wanted a few things:

  • Single target abilities
  • AOE/Multi target abilities
  • Friendly abilities (heals)
  • Damage/Healing over time abilities
  • Constant effects (increases your speed by x for y seconds)

Starting with single target abilities, I thought perhaps I would just do it all in one class (Ability). A TacticalEntity (my movable player and zombie object) would get a list of abilities that one could fire at will (zombies through their AI, and players through some GUI). What is so wrong with this approach? For starters, it would would fine for a single target or aoe instant ability,  but how would a single method on a single instance of an ability apply a damage over time effect (e.g. Does 10 damage per second for 4 seconds)? You have to apply something to an entity and have it stick: AbilityEffect.

That was the biggest revelation for me: separate the responsibility of applying effects and the actual damage/healing effect to a different object altogether. Ok great… now I can just put a collection of effects on a TacticalEntity and an ability can apply effects to the entity! Wait… who is responsible for removing the effects once they expire? For that matter, who is responsible for keeping track of all the effects?

Of course the effect could probably have handled all of this, and the entity itself could have removed effects from itself when they expire, but that’s not the responsibility of the TacticalEntity. It already has a lot of code and does enough. That is where EffectManager comes along. It’s a static class that has an Activity() method that gets called every frame, and it gets the honor of keeping track of a Dictionary<TacticalEntity, List> which holds all effects that are applied to all TacticalEntities.

In both of the examples above, adhering to the Single Responsibility Principle drove me to make decisions which keep my code more concise and maintainable. Any time you hear yourself saying you can use a single class to do multiple different things, you should ask yourself if it would work better split into separate responsibilities.

I haven’t done an AOE ability yet, but that is all about targetting and figuring out which entities to apply effects to outside of the whole ability/effect/manager classes as described above. Without rambling any further, here is the code as it stands right now:

BasicAbility.cs:

    public class BasicAbility : IAbility
    {
        public BasicAbility(List effects)
        {
            Effects = effects;
        }

        public void execute(TacticalEntity source, List destination)
        {
            foreach (TacticalEntity entity in destination)
            {
                execute(source, entity);
            }
        }

        public void execute(TacticalEntity source, TacticalEntity destination)
        {
            Projectile projectile = Factories.ProjectileFactory.CreateNew();
            projectile.Position = source.Position;
            projectile.SourceEntity = source;
            projectile.TargetEntity = destination;
            projectile.SpriteAnimate = true;
            projectile.Ability = this;
        }

        public List Effects
        {
            get;
            private set;
        }
    }

AbilityEffect.cs:

    public class AbilityEffect : IAbilityEffect
    {
        private bool ConstantEffectsApplied = false;

        public AbilityEffect()
        {
            ConstantEffectsApplied = false;
        }

        public AbilityEffect(bool tickImmediately, int healthPerTick, int speedEffect, int defenseEffect, float aggroRadiusEffect, int strengthEffect, int totalticks, string name)
        {
            TickImmediately = tickImmediately;
            HealthEffectPerTick = healthPerTick;
            SpeedEffectWhileActive = speedEffect;
            DefenseEffectWhileActive = defenseEffect;
            AggroRadiusEffectWhileActive = aggroRadiusEffect;
            StrengthEffectWhileActive = strengthEffect;
            TotalTicks = totalticks;
            Name = name;
            ConstantEffectsApplied = false;
        }

        public AbilityEffect(TacticalEntity source, TacticalEntity affectedEntity, IAbilityEffect that)
            : this(that.TickImmediately, that.HealthEffectPerTick, that.SpeedEffectWhileActive, that.DefenseEffectWhileActive, that.AggroRadiusEffectWhileActive, that.StrengthEffectWhileActive, that.TotalTicks, that.Name)
        {
            AffectedEntity = affectedEntity;
            SourceEntity = source;
        }

        public int HealthEffectPerTick
        {
            set;
            get;
        }

        public int SpeedEffectWhileActive
        {
            set;
            get;
        }

        public int DefenseEffectWhileActive
        {
            set;
            get;
        }

        public float AggroRadiusEffectWhileActive
        {
            set;
            get;
        }

        public int StrengthEffectWhileActive
        {
            set;
            get;
        }

        private int _TotalTicks;
        public int TotalTicks
        {
            get
            {
                return _TotalTicks;
            }
            private set
            {
                _TotalTicks = value;
                TicksRemaining = value;
            }
        }

        public int TicksRemaining
        {
            get;
            private set;
        }

        public string Name
        {
            get;
            set;
        }

        public bool Active
        {
            get
            {
                return TicksRemaining > 0;
            }
        }

        public TacticalEntity AffectedEntity
        {
            get;
            set;
        }

        public void ApplyConstantEffects()
        {
            AffectedEntity.strengthEffects += StrengthEffectWhileActive;
            AffectedEntity.defenseEffects += DefenseEffectWhileActive;
            AffectedEntity.speedEffects += SpeedEffectWhileActive;
            AffectedEntity.aggroCircleRadiusEffects += AggroRadiusEffectWhileActive;
        }

        public void RemoveConstantEffects()
        {
            AffectedEntity.strengthEffects -= StrengthEffectWhileActive;
            AffectedEntity.defenseEffects -= DefenseEffectWhileActive;
            AffectedEntity.speedEffects -= SpeedEffectWhileActive;
            AffectedEntity.aggroCircleRadiusEffects -= AggroRadiusEffectWhileActive;
        }

        public void ApplyEffectTick()
        {
            if (!ConstantEffectsApplied)
            {
                ApplyConstantEffects();
                ConstantEffectsApplied = true;
            }
            if (Active)
            {
                AffectedEntity.health += HealthEffectPerTick;
                --TicksRemaining;
            }
        }

        public IAbilityEffect Clone(TacticalEntity source, TacticalEntity entity)
        {
            return new AbilityEffect(SourceEntity, entity, this);
        }

        public TacticalEntity SourceEntity
        {
            get;
            private set;
        }

        public bool TickImmediately { get; set; }
    }

EffectManager.cs:

    public static class EffectManager
    {
        private static Dictionary<TacticalEntity, List> entityEffects = new Dictionary<TacticalEntity, List>(20);
        private static double lasttick = TimeManager.CurrentTime;

        public static void AddEffectsToEntity(TacticalEntity source, TacticalEntity entity, List effects)
        {
            if (!entityEffects.ContainsKey(entity))
            {
                entityEffects.Add(entity, new List(effects.Count));
            }
            foreach (IAbilityEffect effect in effects)
            {
                AddEffectToEntity(source, entity, effect);
            }
        }

        public static void AddEffectToEntity(TacticalEntity source, TacticalEntity entity, IAbilityEffect effect)
        {
            IAbilityEffect newEffect = effect.Clone(source, entity);
            List effects;
            if (!entityEffects.ContainsKey(entity))
            {
                effects = new List(1);
                entityEffects.Add(entity, effects);
            }
            else
            {
                effects = entityEffects[entity];
            }
            effects.Add(newEffect);

            if (newEffect.Active && newEffect.TickImmediately)
            {
                newEffect.ApplyEffectTick();
            }
        }

        public static void Activity()
        {
            if ((TimeManager.CurrentTime - lasttick) > 1.0)
            {
                lasttick = TimeManager.CurrentTime;
                foreach (KeyValuePair<TacticalEntity, List> pair in entityEffects)
                {
                    TickAndRemoveInactiveEffects(pair);
                }
            }
        }

        private static void TickAndRemoveInactiveEffects(KeyValuePair<TacticalEntity, List> pair)
        {
            foreach (IAbilityEffect effect in pair.Value)
            {
                effect.ApplyEffectTick();
            }

            for (int x = pair.Value.Count - 1; x >= 0; --x)
            {
                if (!pair.Value[x].Active)
                {
                    pair.Value[x].RemoveConstantEffects();
                    pair.Value.RemoveAt(x);
                }
            }
        }
    }

And the call to attack another entity:

        public virtual void attack(TacticalEntity attackableEntity)
        {
            if (this.currentAbility != null &&
                this.attackCircle.CollideAgainst(attackableEntity.hitCircle))
            {
                this.currentAbility.execute(this, attackableEntity);                
            }
        }

Leave a Reply

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