1
\$\begingroup\$

NOTE: I am currently not using any frameworks or other game dev tools like Unity - this is purely written using c# 6.0 in Visual Studio 2015. I would be interested to learn about any frameworks or tools that have an implementation of this system for learning purposes, but ultimately I wish to design my own system.

For the past several weeks, I've been working on building (the engine for) an RPG game. The work done has purely been based around things like what happens when a player attacks an enemy, etc. More recently, I've been trying to figure out how to implement the stats system.

If you're familiar with EverQuest, this is the sort of system I'm going for, namely:

  • Players and enemies share the same common stats.
  • Stats are capped (based on level or otherwise). Example: crit chance cannot exceed 100, Stamina cannot exceed 250 at level 40.
  • Stats are calculated into the basic Damage, Defense and Health stats. Example: Stamina increases the Health stat by X per point.
  • Stats are affected by things like potions, friendly spells, debuffs, item effects, etc.
  • There is a maximum number of effects that can be used at one time (ex. 10)
  • Item effects may increase a stat past the cap

There are other details that I'd ultimately like to consider, but this is the primary goal: A player (or enemy) has a set of statistics that, at any given time, can be changed by a (maximum) number of statistic effects.

Consider the following example:

A player with 100 Health and 10 Strength has a calculated Damage stat of 18 (arbitrary). He then receives a buff, increasing his Strength to 14. If he then drinks a potion that gives Strength + 2, his strength should be recalculated to 16 (base strength + buff + potion).

The system I am trying to design should be able to calculate the Damage stat to its new value, regardless of the source of the stat change. Whether it's a potion, spell or otherwise that changes the stat, the stats should ultimately be represented as one single calculated value.

Similarly, when the timer on an item effect expires, the buff should be removed and its effects should fall off.

What I have so far

I have created a base class, holding the individual statistics, and using OnPropertyChanged() to recalculate base stats when a secondary stat changes:

public int Defense //base stat, no change event fired
{
    get { return this._defense; }
    set { this._defense = value; }
}

public int Stamina //affects health
{
    get { return this._stamina; }
    set { this._stamina = value; OnPropertyChanged(nameof(Stamina)); }
}

This works for the time being, but I've already run into snags trying to add item effects into the equation. At this point, I'm simply stuck on the implementation and am not sure about the "proper" way to proceed further.

Edit:

Based on comments to the question, I'm narrowing the scope to be less broad. I would like assistance designing the Statistics class to facilitate the requirements given above.

public void ApplyStatEffects()
{
    Statistics playerStats = new Statistics
    {
        Health = 10,
        Damage = 3,
        CritChance = 20
    };

    Statistics buffStats = new Statistics
    {
        Health = 5
        //only declare properties that are changing
    };

    //apply a blanket update to the base stats based on current effects
    //playerStats += buffStats;
}

Ultimately, what I would like to do is take one Statistics object (base) and apply the effects of other Statistics objects to base. It should also support things like buff timers and stat caps, BUT:

I'm not sure how to design the properties to make this easier. A few thoughts:

  • I could use a KeyValuePair<> with an enum to represent the stats
  • Make every Statistic it's own object with properties string name; and T (object?) value.
  • Roll the current properties into a List or Array.

However, some properties are int, where others are double. Additionally, some may be capped in a range 0-100 while others might be capped at x * level.

How should I implement my Statistics class to make it easier to apply one or many stat changes like Health + 10, Damage - 2 from multiple sources?

\$\endgroup\$
  • \$\begingroup\$ Have you seen this? gamedev.stackexchange.com/questions/29982/… \$\endgroup\$ – MichaelHouse Mar 11 '16 at 22:39
  • \$\begingroup\$ @Byte56 Nope! but thank you for the link - I'll check it out \$\endgroup\$ – levelonehuman Mar 11 '16 at 22:40
  • \$\begingroup\$ It might help to narrow the scope of your question a bit. I think if you found your answer in that other question, a large portion of your question here would be answered. \$\endgroup\$ – MichaelHouse Mar 11 '16 at 22:47
  • \$\begingroup\$ @Byte56 I tried to make the question less broad, but I'm not quite sure if I succeeded. I've looked at the linked question and my own question is now essentially "how do I design the base class to support the implementation given in the other question?" \$\endgroup\$ – levelonehuman Mar 14 '16 at 13:30
  • \$\begingroup\$ Unless your intention is to learn basics of game engines, go use a game engine. Otherwise it means unnecessary work. \$\endgroup\$ – S. Tarık Çetin Mar 14 '16 at 18:11
2
\$\begingroup\$

As with most programming problems there is more than one way to skin a cat, but here is a small example program that will roughly implement the type of behaviour you are after, it should be enough for you to work with in your own project:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace StatTesting
{
    class Program
    {
        public static Random ran = new Random();

        static void Main(string[] args)
        {
            var player = new GameEntity();

            Console.WriteLine("Defense: {0}", player.Defense);
            Console.WriteLine("Damage: {0}", player.Damage);
            Console.WriteLine("Health: {0}", player.Health);  

            var buffDescriptions = new string[11] { "Striking", "Thundering", "Hammering", "Piercing", "Slashing", "Vampiric", "Icing", "Flaming", "Shocking", "Noxious", "Acidic" };
            for (var i = 0; i < 11; i++)
            {
                var buffDesc = buffDescriptions[i];
                var buffValue = ran.Next(1, 51);
                var duration =  ran.Next(5, 20);

                var buff = new Buff(buffValue, buffDesc, duration);

                if(player.AddDamageBuff(buff))
                    Console.WriteLine("Buff '{0}' Applied, damage Value modified by {1}, damage is now {2}, duration: {3}", buffDesc, buffValue, player.Damage, duration);
            }

            Console.WriteLine("Finished adding damage buffs.");

            // simulate ticks with Thread.Sleep(1000) in a loop
            // in a game this would be replaced with an .Update() function within the game loop
            var timeAlive = 0;
            while (timeAlive < 25)
            {
                Console.WriteLine("tick...{0}", timeAlive);

                // remove all expired buffs:
                player.DamageBuffs.RemoveAll(item => item.DurationInSeconds <= timeAlive);

                // display remaining buffs:
                Console.WriteLine("Damage Score: {0}", player.Damage);

                // increase time alive:
                timeAlive++;
                Thread.Sleep(1000);
            }

            Console.WriteLine("All buffs expired...");
            var x = Console.ReadLine();

        }
    }

    public class GameEntity
    {
        // value based on level or some other metric:
        private int MAX_UNBUFFED_STAT = 40;

        private int damage;
        private int defense;
        private int health;

        public RestrictedList<Buff> DamageBuffs;
        public RestrictedList<Buff> DefenseBuffs;
        public RestrictedList<Buff> HealthBuffs;

        public int Damage
        {
            get
            {
                // get the base damage amount:
                var damageScore = damage < MAX_UNBUFFED_STAT ? damage : MAX_UNBUFFED_STAT;

                //apply any status effects:
                damageScore += DamageBuffs.Select(t => t.StatModifier).Sum();

                // return total damage value:
                return damageScore;
            }
            set { damage = value; }
        }

        public int Defense
        {
            get
            {
                // get the base defense amount:
                var defenseScore = defense < MAX_UNBUFFED_STAT ? defense : MAX_UNBUFFED_STAT;

                //apply any status effects:
                defenseScore += DefenseBuffs.Select(b => b.StatModifier).Sum();

                // return total damage value:
                return defenseScore;
            }
            set { defense = value; }
        }

        public int Health
        {
            get
            {
                // get the base health amount:
                var healthScore = health < MAX_UNBUFFED_STAT ? health : MAX_UNBUFFED_STAT;

                //apply any status effects:
                healthScore += HealthBuffs.Select(b => b.StatModifier).Sum();

                // return total damage value:
                return healthScore;
            }
            set { health = value; }
        }

        public GameEntity()
        {
            // initialise list of damage buffs:
            DamageBuffs = new RestrictedList<Buff>();
            DefenseBuffs = new RestrictedList<Buff>();
            HealthBuffs = new RestrictedList<Buff>();

            // set base stats:
            this.damage = 20;
            this.defense = 35;
            this.health = 50;
        }

        public bool AddDamageBuff(Buff buff)
        {
            try
            {
                // try and add this buff to player:
                this.DamageBuffs.Add(buff);

                return true;
            }
            catch (OverCapacityException oex)
            {
                Console.WriteLine("Buffs at maximum capacity!");
            }
            catch (Exception ex)
            {
                Console.WriteLine("Well, I wasn't expecting that... {0}", ex.Message);
            }

            return false;
        }
    }

    public class RestrictedList<T> : List<T>
    {
        private int maxCapacity = -1;

        public RestrictedList(int capacity = 10)
        {
            this.maxCapacity = capacity;
        }

        public new void Add(T value)
        {
            if (this.Count < maxCapacity)
            {
                base.Add(value);
                return;
            }

            throw new OverCapacityException("The RestrictedList is at maximum capacity.");
        }
    }

    public class OverCapacityException : Exception
    {
        public OverCapacityException(string message)
            : base(message) { }
    }

    public struct Buff
    {
        public Buff(int statModifier, string description, int durationInSeconds = 10)
        {
            StatModifier = statModifier;
            Description = description;
            DurationInSeconds = durationInSeconds;
        }

        public int StatModifier;
        public string Description;
        public int Id;
        public int DurationInSeconds;
    }
}
\$\endgroup\$
  • 1
    \$\begingroup\$ Why don't you use arrays for fixed capacity, instead of fixing capacity of a list? \$\endgroup\$ – S. Tarık Çetin Mar 14 '16 at 18:13
  • \$\begingroup\$ I hadn't thought about calculating all effects on a property inside the getter - my thought process was all about applying a blanket update to the stats class. Thanks for the input, this will definitely give me some more to think about. \$\endgroup\$ – levelonehuman Mar 14 '16 at 21:38
  • \$\begingroup\$ @SamedTarıkÇETİN agreed, that would be a more efficient way of implementing a solution to the problem; I just chose to use a custom restricted list for the purposes of demonstration... \$\endgroup\$ – Neil Hibbert Mar 15 '16 at 8:17
  • \$\begingroup\$ I don't think the inheritance on List<T> is that neat. Casting or even assigning to List<T> will just allow you to add more items than the capacity. \$\endgroup\$ – Caramiriel Mar 15 '16 at 8:53

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.