Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Organizing code for player/enemy stats - classes, namespaces, or something else?

Discussion in 'Scripting' started by AssembledVS, Jul 13, 2016.

  1. AssembledVS

    AssembledVS

    Joined:
    Feb 23, 2014
    Posts:
    248
    How should I approach organizing stats code?

    Ideally, I'd like to break enemy stats, for example, into groups like HealthData, DefenseData, StatusData, etc. all in one file. These groups, which are currently nested classes in my code, sometimes refer to one another's data, like calculating defense based on data from UpgradeData. Also, health-related methods are in HealthData, defense-related methods are in DefenseData, etc., and I'd like to call methods by first pointing to the group, like healthData.DamageHealth(), to help keep the code organized and easy to access from other scripts.

    How would using namespaces instead work and what would an example look like?

    Please let me know how I can improve my approach.

    BaseStats.cs
    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3.  
    4. /// <summary>
    5. /// This class is the base class for enemy, neutral, and destructible object stats.
    6. /// </summary>
    7. public class BaseStats : MonoBehaviour
    8. {
    9.     // declare instances of each class; done here because it is done only once per gameobject
    10.     public HealthData healthData = new HealthData();
    11.     public DefenseData defenseData = new DefenseData();
    12.     public AttackData attackData = new AttackData();
    13.     public StatusData statusData = new StatusData();
    14.  
    15.     void Awake()
    16.     {
    17.         // pass in script's class to each class
    18.         healthData.SetParent(this);
    19.         defenseData.SetParent(this);
    20.         attackData.SetParent(this);
    21.     }
    22.  
    23.     // ########################################################################################## //
    24.     // ---HEALTH---
    25.     /// <summary>
    26.     /// This class stores the object's health stats and related methods.
    27.     /// </summary>
    28.  
    29.     public class HealthData
    30.     {
    31.         private float _current;
    32.         public float current
    33.         {
    34.             get { return _current; }
    35.             private set
    36.             {
    37.                 _current = value;
    38.  
    39.                 if ((current <= 0) && (parent.statusData.isObjDead == false))
    40.                 {
    41.                     print("enemy killed");
    42.                     parent.statusData.SetDeathStatus(true);
    43.                 }
    44.             }
    45.         }
    46.  
    47.         public float max
    48.         { get; private set; }
    49.  
    50.         // constructor with default values
    51.         public HealthData()
    52.         {
    53.             current = 200;
    54.         }
    55.  
    56.         // -------------------------------------------------------------------------------------- //
    57.         // create reference to BaseStats parent class to access instances of class
    58.         private BaseStats parent;
    59.  
    60.         /// <summary>
    61.         /// This method allows this class to reference other classes' global instances. It should
    62.         /// only be set within the script itself.
    63.         /// </summary>
    64.  
    65.         public void SetParent(BaseStats parent)
    66.         {
    67.             // set parent of this class to script's class
    68.             this.parent = parent;
    69.         }
    70.  
    71.         // -------------------------------------------------------------------------------------- //
    72.         /// <summary>
    73.         /// This method calculates damage and change the object's health from events like hits.
    74.         /// </summary>
    75.  
    76.         public float DamageHealth(float damageAmount, int damageType, bool ignoreArmor)
    77.         {
    78.             float damageAmountCalculated;
    79.  
    80.             // calculate damage amount with defense modifiers
    81.             if (ignoreArmor == false)
    82.             {
    83.                 switch (damageType)
    84.                 {
    85.                     case 1: // 1 = vsArmor vs shielded
    86.                         if (parent.defenseData.defenseType == DefenseData.DefenseType.shielded)
    87.                         {
    88.                             // if shielded, take less damage from vsArmor weapon
    89.                             damageAmountCalculated = damageAmount * parent.defenseData.modifier *
    90.                                                      parent.defenseData.typeModifier;
    91.                         }
    92.  
    93.                         else
    94.                         {   // otherwise, take normal damage
    95.                             damageAmountCalculated = damageAmount * parent.defenseData.modifier;
    96.                         }
    97.  
    98.                         break;
    99.  
    100.                     case 2: // 2 = vsShielded vs armor
    101.                         if (parent.defenseData.defenseType == DefenseData.DefenseType.armored)
    102.                         {
    103.                             // if armored, take less damage from vsShielded weapon
    104.                             damageAmountCalculated = damageAmount * parent.defenseData.modifier *
    105.                                                      parent.defenseData.typeModifier;
    106.                         }
    107.  
    108.                         else
    109.                         {   // otherwise, take normal damage
    110.                             damageAmountCalculated = damageAmount * parent.defenseData.modifier;
    111.                         }
    112.  
    113.                         break;
    114.  
    115.                     default:    // 0 = vsNull; no extra modifier
    116.                         damageAmountCalculated = damageAmount * parent.defenseData.modifier;
    117.                         break;
    118.                 }
    119.             }
    120.  
    121.             // ignoring armor, so no defense modifiers
    122.             else
    123.             {
    124.                 damageAmountCalculated = damageAmount;
    125.             }
    126.  
    127.             // get current health before it is changed below
    128.             float oldValue = current;
    129.  
    130.             // change actual health
    131.             current -= damageAmountCalculated;
    132.  
    133.             return damageAmountCalculated;
    134.         }
    135.  
    136.         // -------------------------------------------------------------------------------------- //
    137.         /// <summary>
    138.         /// This method should only be used to directly change the object's's health.
    139.         /// </summary>
    140.  
    141.         public void SetHealthDirect(float changeTo)
    142.         {
    143.             // get current health before it is changed below
    144.             float oldValue = current;
    145.  
    146.             // change actual health
    147.             current = changeTo;
    148.  
    149.             //EventManagerPlayerStats.OnPlayerHealthChangeMethod(current - oldValue);
    150.         }
    151.     }
    152.  
    153.     // ########################################################################################## //
    154.     /// <summary>
    155.     /// This class stores the object's defense stats and related methods.
    156.     /// </summary>
    157.  
    158.     // ---DEFENSE---
    159.     public class DefenseData
    160.     {
    161.         public float amount                             // defense amount
    162.         { get; private set; }
    163.  
    164.         public float modifier                           // defense modifier with bonuses
    165.         { get; private set; }
    166.  
    167.         public enum DefenseType                         // enum for type of defense
    168.         { unarmored = 0, armored = 1, shielded = 2 };
    169.  
    170.         public DefenseType defenseType                  // type of defense
    171.         { get; private set; }
    172.  
    173.         public float typeModifier                       // defense type modifier
    174.         { get; private set; }
    175.  
    176.         // constructor with default values
    177.         public DefenseData()
    178.         {
    179.             amount = 0;
    180.             modifier = (100 - amount) / 100.0f;
    181.  
    182.             defenseType = DefenseType.unarmored;
    183.  
    184.             switch (defenseType)
    185.             {
    186.                 case DefenseType.unarmored:
    187.                     typeModifier = 1.0f;
    188.                     break;
    189.  
    190.                 case DefenseType.armored:
    191.                     typeModifier = 0.25f;
    192.                     break;
    193.  
    194.                 case DefenseType.shielded:
    195.                     typeModifier = 0.25f;
    196.                     break;
    197.             }
    198.         }
    199.  
    200.         // -------------------------------------------------------------------------------------- //
    201.         // create reference to BaseStats parent class to access instances of class
    202.         private BaseStats parent;
    203.  
    204.         /// <summary>
    205.         /// This method allows this class to reference other classes' global instances. It should
    206.         /// only be set within the script itself.
    207.         /// </summary>
    208.  
    209.         public void SetParent(BaseStats parent)
    210.         {
    211.             // set parent of this class to script's class
    212.             this.parent = parent;
    213.         }
    214.     }
    215.  
    216.     // ########################################################################################## //
    217.     /// <summary>
    218.     /// This class stores the object's attack stats and related methods.
    219.     /// </summary>
    220.  
    221.     // ---ATTACK---
    222.     public class AttackData
    223.     {
    224.         public float amount                             // attack amount
    225.         { get; private set; }
    226.  
    227.         // constructor with default values
    228.         public AttackData()
    229.         {
    230.             amount = 25;
    231.         }
    232.  
    233.         // -------------------------------------------------------------------------------------- //
    234.         // create reference to BaseStats parent class to access instances of class
    235.         private BaseStats parent;
    236.  
    237.         /// <summary>
    238.         /// This method allows this class to reference other classes' global instances. It should
    239.         /// only be set within the script itself.
    240.         /// </summary>
    241.  
    242.         public void SetParent(BaseStats parent)
    243.         {
    244.             // set parent of this class to script's class
    245.             this.parent = parent;
    246.         }
    247.     }
    248.  
    249.     // ########################################################################################## //
    250.     /// <summary>
    251.     /// This class stores the object's status stats and related methods.
    252.     /// </summary>
    253.  
    254.     // ---STATUS---
    255.     public class StatusData
    256.     {
    257.         public string objName                           // obj's name
    258.         { get; private set; }
    259.  
    260.         public enum ObjClass                            // enum for obj's class/obj type
    261.         { enemy = 0, neutral = 1, destructable = 2 };
    262.  
    263.         public ObjClass objClass                        // obj's class/obj type
    264.         { get; private set; }
    265.  
    266.         public bool isObjKillable                       // whether obj is killable or not
    267.         { get; private set; }
    268.  
    269.         public bool isObjDead                           // obj death status
    270.         { get; private set; }
    271.  
    272.         public bool isObjInvulnerable                   // obj invulnerability status from effects
    273.         { get; private set; }                               // and others
    274.  
    275.         // constructor with default values
    276.         public StatusData()
    277.         {
    278.             isObjKillable = true;
    279.             isObjDead = false;
    280.             isObjInvulnerable = false;
    281.         }
    282.  
    283.         // -------------------------------------------------------------------------------------- //
    284.         // create reference to BaseStats parent class to access instances of class
    285.         private BaseStats parent;
    286.  
    287.         /// <summary>
    288.         /// This method allows this class to reference other classes' global instances. It should
    289.         /// only be set within the script itself.
    290.         /// </summary>
    291.  
    292.         public void SetParent(BaseStats parent)
    293.         {
    294.             // set parent of this class to script's class
    295.             this.parent = parent;
    296.         }
    297.  
    298.         // -------------------------------------------------------------------------------------- //
    299.         /// <summary>
    300.         /// This method change's the obj's killable status.
    301.         /// </summary>
    302.  
    303.         public void SetKillableStatus(bool isKillable)
    304.         {
    305.             isObjKillable = isKillable;
    306.         }
    307.  
    308.         // -------------------------------------------------------------------------------------- //
    309.         /// <summary>
    310.         /// This method changes the obj's death status.
    311.         /// </summary>
    312.  
    313.         public void SetDeathStatus(bool isDead)
    314.         {
    315.             isObjDead = isDead;
    316.         }
    317.  
    318.         // -------------------------------------------------------------------------------------- //
    319.         /// <summary>
    320.         /// This method changes the obj's vulnerability status.
    321.         /// </summary>
    322.  
    323.         public void SetInvulnerabilityStatus(bool isInvulnerable)
    324.         {
    325.             isObjInvulnerable = isInvulnerable;
    326.         }
    327.     }
    328. }
     
  2. xjjon

    xjjon

    Joined:
    Apr 15, 2016
    Posts:
    587
    Well if you want to use a namespace to organize your code, I don't think it would really help. It will let you access stuff by putting the identified ahead of it. For example.. make the namespace Stats.. then you access it by: Stats.BaseStats.HealthData

    I think instead you should split everything into their own files. No purpose in having all these classes in one file.

    So do HealthData.cs, DefenseData.cs, etc.
     
  3. AssembledVS

    AssembledVS

    Joined:
    Feb 23, 2014
    Posts:
    248
    Thank you. What would the benefit be of a single file? Why do you think that is a better way of organizing stats?
     
    thenolaconnor likes this.
  4. xjjon

    xjjon

    Joined:
    Apr 15, 2016
    Posts:
    587
    Well generally there's no reason to put multiple classes in the same file.
    It makes it easier to maintain if your classes are in their own files.
    You can swap out code, rewrite, and delete easily without having to worry about a whole bunch of other code.
    When your project gets big it gets hard to find stuff especially if the files are not organized well, so it's best to keep everything well organized.
     
    landon912 likes this.
  5. AssembledVS

    AssembledVS

    Joined:
    Feb 23, 2014
    Posts:
    248
    Thanks for this. I understand your argument for splitting code into multiple scripts. Do you think that it is worth it even if it will require me to have many more script references in other scripts?

    Is one class per script considered good practice in Unity?

    I would appreciate any other thoughts!
     
    Last edited: Jul 15, 2016
  6. xjjon

    xjjon

    Joined:
    Apr 15, 2016
    Posts:
    587
    Yes, I think many will appreciate when classes are split into different files. You'll thank yourself too when your project is larger, it's much easier to find old code this way.

    You won't really be making more references, still the same amount. In your BaseStats class you make references to Health, Defense, Attack, and Status, which you would still be doing even if your classes were split up. Actually you wouldn't even need to rewrite any of the code.

    In general name your files the same as your class name.

    So you would have

    BaseStats.cs
    HealthData.cs
    DefenseData.cs
    AttackData.cs
    StatusData.cs

    And then to further organize this, you could put these scripts in their own folder, etc.
     
  7. landon912

    landon912

    Joined:
    Nov 8, 2011
    Posts:
    1,579
    It won't increase your "script references" because that's not a thing in this context.

    You have object references, and those will stay the same because you still have the same number of classes. In Unity there is no benefit in grouping classes together because it will have nearly no effect on the end product.

    In case you've gotten confused reading about code layout being important due to fitting into the I-cache or likewise, this only applies in C/C++ and other low level languages.

    Otherwise, there is no glaring issues I saw on a quick skim over.
     
  8. AssembledVS

    AssembledVS

    Joined:
    Feb 23, 2014
    Posts:
    248
    Thank you. Maybe I'm missing something here - wouldn't it be the case that an enemy, for example, would need each one of these HealtData.cs, DefenseData.cs, etc. scripts attached? Or should I be inheriting when splitting up into multiple scripts? Or am I simply making a new instance, like HealthData healthData = new HealthData(parameters);?

    Thanks. Maybe I'm understanding a bit more now - I'm not attaching each class/script like HealthData.cs on the enemy, but making a new instance in code?
     
  9. xjjon

    xjjon

    Joined:
    Apr 15, 2016
    Posts:
    587
    No, to explain at a high level, the program does not need multiple 'copies' of the script. If you make 100 HealthData objects in unity, it doesn't need to make 100 copies if HealthData.cs, there is still only one.

    When you initialize an object, there is a reference to it. The size of each of these objects is the size of the variables in that class + some small amount for book keeping. For example: lets say an int is 4 bytes.

    now you have this class

    UnitStats.cs

    which has there variables

    Code (csharp):
    1.  
    2.  
    3. int health;
    4. const int maxHealth;
    5.  
    6. int armor;
    7. int defense;
    8.  
    9. public int getHealth(){
    10.  
    11. return health;
    12. }
    13.  
    14.  
    This object holds 4 ints, so 4x4bytes = 16 bytes + book keeping for one UnitStats object. So you see there may be functions, etc, in there, but the object will be the variables. So even if you split them into multiple scripts, it's still the same amount of size.

    You can read more about the C# compiling and runtime process here.

    But generally I think you may be overthinking this, don't worry about how much space these scripts are taking. You should organize the code in a way that's easy for you to access and read. This lets you easily maintain and work on your code, so you spend more time making games and less time looking for your old functions and reading walls of texts!
     
  10. orb

    orb

    Joined:
    Nov 24, 2010
    Posts:
    3,032
    On namespaces: Start every project by making one for that project. Put all your custom code inside this namespace to avoid being bitten by clashing names when you start using other people's code. I'm sure the number of projects which had a custom class called SceneManager is pretty huge.

    If your class names consist of one or two common words you have a reason to stick them in a namespace. For example, ProjectWonderBread.StatusData is unambiguous, and not likely to clash with assets from the store, or future UnityEngine classes. Having no namespace and naming your class Unit or Action, on the other hand, is likely to cause trouble. A little extra verbosity saves you a ton of headaches later.

    And like xijon said above, don't overthink it. Just write a class per file, organise as neatly as necessary in folders (but not so deep you spend more time clicking and searching than using them), and keep your stuff away from borrowed stuff. I like to put all 3rd party script assets in their own folder at the root.
     
  11. AssembledVS

    AssembledVS

    Joined:
    Feb 23, 2014
    Posts:
    248
    Thank you for this clear explanation! No, I know that there is only one script and that it is not duplicated. I was thinking about the number of scripts like having HealthData, DefenseData, etc. being their own scripts instead of in one big script. The question is more about organization and ease of use than anything else.
    Thank you. This is a good rule to remember about namespaces. I've heard this said several times in different places so I should probably take your advice.

    Personally, I have a separate [GAME] folder where all of my scripts go. All other assets' scripts and other scripts that are not my own go outside of this folder.
     
  12. AssembledVS

    AssembledVS

    Joined:
    Feb 23, 2014
    Posts:
    248
    I'm trying to understand the following:

    So every piece of code that I write and is part of my project should be inside of a namespace, the same namespace? So each script will pretty much look like this:

    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3.  
    4. namespace CustomProjectName
    5. {
    6.     public class TestingA : MonoBehaviour
    7.     {
    8.     }
    9. }
    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3.  
    4. namespace CustomProjectName
    5. {
    6.     public class TestingB : MonoBehaviour
    7.     {
    8.     }
    9. }
    etc.?

    And if everything is already within namespace, will I ever use the 'using' directive for this namespace within my project?
     
    Last edited: Jul 16, 2016
  13. landon912

    landon912

    Joined:
    Nov 8, 2011
    Posts:
    1,579
    Pretty much. You'd also want to get in a good habit of grouping subsystems together in a lower namespace for organization, to not clutter your autocorrects, and to further prevent inner name clashing. Then, if a class is referencing another class in a separate namespace you'd need to add a using statement or fully qualify the name.

    Example:

    Code (CSharp):
    1. namespace Project
    2. {
    3.    namespace System { class SystemClass{} }
    4. }
    To use this in a seperate namespace:

    Code (CSharp):
    1. using Project.System;
    2.  
    3. namespace Anything
    4. {
    5.     class Anything2 { SystemClass test; }
    6. }
    7.  
    8. ///////OR fully qualify the name with no need to import a namespace
    9.  
    10. namespace Anything
    11. {
    12.     class Anything2 { Project.System.SystemClass test; }
    13. }
     
    Last edited: Jul 16, 2016
    orb likes this.
  14. AssembledVS

    AssembledVS

    Joined:
    Feb 23, 2014
    Posts:
    248
    I understand, thank you. I don't have too many subsystems yet so I won't think about lower namespaces right now, but I'll consider this advice going forward.

    Are there any tools that I can use to change all of my code to be part of the namespace instead of changing one by one? I use Visual Studio 2015 with Power Tools - any extensions or other programs available to let me do this? Or something built within Visual Studio?
     
  15. landon912

    landon912

    Joined:
    Nov 8, 2011
    Posts:
    1,579
    Pretty sure ReSharper can handle this. I'm away from the computer, but try selecting multiple classes, then right click into the refactor menu see if modify namespace is there. Or, press Shift-Ctrl-R. You may have to do it for each class, but the plugin handles updating references and generating the boiler plate code.
     
  16. AssembledVS

    AssembledVS

    Joined:
    Feb 23, 2014
    Posts:
    248
    Thanks. I've seen ReSharper - expensive, maybe worth it in the future.
     
  17. AssembledVS

    AssembledVS

    Joined:
    Feb 23, 2014
    Posts:
    248
    I'm trying to move the code into separate classes. Ideally, the BaseStats script will have references to HealthData, DefenseData, etc., and will be the only one of these scripts attached to the game object. These are the issues that I'm running into:

    HealthData, DefenseData, etc. all derive from Monobehaviour. I cannot make new instances of them in BaseStats with the "new" keyword for this reason, forcing me to use AddComponent. Is this the right approach? I did not really intend for these classes to be added to the game object, but then, why am I deriving from a Monobehaviour, right?

    If not attaching HealthData, DefenseData, etc. to any objects, how do I have these classes refer to one another's data? For example, the DamageHealth method within HealthData needs data from DefenseData within its very method definition.

    Again, I want all of the references to be within BaseStats so that I'm not juggling multiple scripts when I want to access something within DefenseData, or StatusData, etc.
     
  18. xjjon

    xjjon

    Joined:
    Apr 15, 2016
    Posts:
    587
    I would recommend removing dependencies to Unity from your 'data' classes.
    What I mean by this removing this Mono Behavior dependency from classes such as health, defense, etc.
    You don't need to have calls to Update, Start, Init, etc in these classes.

    You can have a monobehaviour class called StatHolder or BaseStat that holds reference to all these data classes, then just add StatHolder/BaseStat as a component to your gameobject.

    There's many benefits for doing this and at a high level, you can reuse your code now in any project / game, it removes dependency to Unity, and also you can now serialize your data easily.
    You can also now make constructors for your data :p

    Code (csharp):
    1.  
    2.  
    3. public class BaseStats : MonoBehavior {
    4.  
    5. HealthData healthData;
    6. DefenseData defenseDate;
    7.  
    8. void Start(){
    9. healthData = new HealthData();
    10. defenseData = new DefenseData():
    11.  
    12. }
    13.  
    14.  
    Then your data could be:

    Code (csharp):
    1.  
    2.  
    3. public class HealthData{
    4.  
    5. public HealthData(){
    6. //default constructor
    7. }
    8.  
    9.  
    10. }
    11.  
    12.  
     
    AssembledVS and landon912 like this.
  19. AssembledVS

    AssembledVS

    Joined:
    Feb 23, 2014
    Posts:
    248
    Thanks! This is what I decided to do. Each base class, like HealthData, has instances of the other base classes that allows me to reference the different data.

    So the structure looks something like this:

    StatsBase : Monobehaviour has instances of DefenseData, HealthData, StatusData, and AttackData, which have instances of one another. These classes do not derive from anything.

    I'm now just trying to make sure that working with an instance of an instance (?) is fine. This is my code:

    StatsBase.cs
    Code (CSharp):
    1. public class StatsBase : MonoBehaviour
    2. {
    3.     // create instances of stat data classes
    4.     public HealthData healthData = new HealthData();
    5.     public DefenseData defenseData = new DefenseData();
    6.     public AttackData attackData = new AttackData();
    7.     public StatusData statusData = new StatusData();
    8. }
    HealthData.cs
    Code (CSharp):
    1. public class HealthData
    2. {
    3.     // instances of other data classes for referencing
    4.     public StatusData statusData = new StatusData();
    5.     public DefenseData defenseData = new DefenseData();
    6.  
    7.     // max health
    8.     public float max
    9.     { get; protected set; }
    10.  
    11.     // current health
    12.     protected float _current;
    13.     public float current
    14.     {
    15.         get { return _current; }
    16.         protected set
    17.         {
    18.             _current = value;
    19.  
    20.             if ((current <= 0) && (statusData.isObjDead == false))
    21.             {
    22.                 statusData.SetDeathStatus(true);
    23.             }
    24.         }
    25.     }
    26.  
    27.     // constructor with default values
    28.     public HealthData()
    29.     {
    30.         current = 200;
    31.     }
    32.  
    33.     // ------------------------------------------------ //
    34.     /// <summary>
    35.     /// This method calculates damage and change the object's health from events like hits.
    36.     /// </summary>
    37.  
    38.     public virtual float DamageHealth(float damageAmount, int damageType, bool ignoreArmor)
    39.     {
    40.         float damageAmountCalculated;
    41.  
    42.         // calculate damage amount with defense modifiers
    43.         if (ignoreArmor == false)
    44.         {
    45.             switch (damageType)
    46.             {
    47.                 case 1: // 1 = vsArmor vs shielded
    48.                     if (defenseData.defenseType == DefenseData.DefenseType.shielded)
    49.                     {
    50.                         // if shielded, take less damage from vsArmor weapon
    51.                         damageAmountCalculated = damageAmount * defenseData.modifier *
    52.                                                  defenseData.typeModifier;
    53.                     }
    54.  
    55.                     else
    56.                     {   // otherwise, take normal damage
    57.                         damageAmountCalculated = damageAmount * defenseData.modifier;
    58.                     }
    59.  
    60.                     break;
    61.  
    62.                 case 2: // 2 = vsShielded vs armor
    63.                     if (defenseData.defenseType == DefenseData.DefenseType.armored)
    64.                     {
    65.                         // if armored, take less damage from vsShielded weapon
    66.                         damageAmountCalculated = damageAmount * defenseData.modifier *
    67.                                                  defenseData.typeModifier;
    68.                     }
    69.  
    70.                     else
    71.                     {   // otherwise, take normal damage
    72.                         damageAmountCalculated = damageAmount * defenseData.modifier;
    73.                     }
    74.  
    75.                     break;
    76.  
    77.                 default:    // 0 = vsNull; no extra modifier
    78.                     damageAmountCalculated = damageAmount * defenseData.modifier;
    79.                     break;
    80.             }
    81.         }
    82.  
    83.         // ignoring armor, so no defense modifiers
    84.         else
    85.         {
    86.             damageAmountCalculated = damageAmount;
    87.         }
    88.  
    89.         // get current health before it is changed below
    90.         float oldValue = current;
    91.  
    92.         // change actual health
    93.         current -= damageAmountCalculated;
    94.  
    95.         return damageAmountCalculated;
    96.     }
    97.  
    98.     // ------------------------------------------------ //
    99.     /// <summary>
    100.     /// This method increases the object's health from events like healing.
    101.     /// </summary>
    102.  
    103.     public virtual void IncreaseHealth(float increaseAmount)
    104.     {
    105.         // get current health before it is changed below
    106.         float oldValue = current;
    107.  
    108.         // change actual health
    109.         current += increaseAmount;
    110.     }
    111.  
    112.     // ------------------------------------------------ //
    113.     /// <summary>
    114.     /// This method should only be used to directly change the object's's health.
    115.     /// </summary>
    116.  
    117.     public virtual void SetHealthDirect(float changeTo)
    118.     {
    119.         // get current health before it is changed below
    120.         float oldValue = current;
    121.  
    122.         // change actual health
    123.         current = changeTo;
    124.     }
    125. }
    As you can see, HealthData has instances of DefenseData and StatusData, and then the Monobehaviour StatsBase has instances of HealthData, DefenseData, StatusData, and AttackData. I haven't tested yet, as I'm int the process of updating other things as well, so I'd like to be sure that I'm not going down the wrong path here.
     
  20. xjjon

    xjjon

    Joined:
    Apr 15, 2016
    Posts:
    587
    You shouldn't need to create instances of the other base classes in each base class.

    So healthData doesn't need to create new DefenseData, StatusData, etc.

    I'm assuming you are using these stats for a unit, say the player.

    So the player holds BaseStats :: MonoBehaviour

    The BaseStat then holds each of the base classes, such as health, defense, status. So that part you have right.

    But you shouldn't be instantiating new instances of each base class in each base class, that will be an endless string of code that will never finish. (i.e. Health Data -> makes a new Status and Defense -> which each makes 3 new references to each of those, etc)

    If your healthData needs to access something from defenseData, you can pass in reference to the baseStats in the constructor.

    Example:

    Code (csharp):
    1.  
    2.  
    3. public class HealthData{
    4.  
    5. private BaseStat baseStat;
    6.  
    7. public HealthData(BaseStat baseStat){
    8.    this.baseStat = baseStat;
    9.  
    10. // you can now access the other stats by using baseStat.defenseData, baseStata.statusData, etc
    11. }
    12.  
    13. }
    14.  
    15.  
    For a little more reading up on what I mentioned in the last post, you can learn about MVC Model. At a high level, it's a software design pattern where you separate the data, the logic, and the interaction.

    It seems you are interested in how to organize and design better structured code / software, so I would also recommend looking into topics related to design principles. More specifically the 'single-purpose principle', which would apply to much of what you are doing here.
     
    AssembledVS likes this.
  21. AssembledVS

    AssembledVS

    Joined:
    Feb 23, 2014
    Posts:
    248
    Thanks for the reply and extra research material. I'll take a look at MVC and read up on the single purpose principle.

    I see what you mean about the endless code. I'm having some difficulty figuring out how to order and use instances of non-MonoBehaviour classes so this is great help.

    Giving a BaseStats parameter to the constructor within HealthData now requires me to pass that in when creating a new instance of the HealthData class within BaseStats.cs. How would I pass in the BaseStats instance into the constructor when creating a new instance of HealthData within the BaseStats script? Using 'this'?

    EDIT: 'This' seems to work just fine. No NullReference exceptions, etc.

    BaseStats.cs
    Code (CSharp):
    1. public class BaseStats : MonoBehaviour
    2. {
    3.     public HealthData healthData;
    4.     public DefenseData defenseData;
    5.     public AttackData attackData;
    6.     public StatusData statusData;
    7.  
    8.     void Awake()
    9.     {
    10.         // create instances of stat data classes
    11.         healthData = new HealthData(this);
    12.         defenseData = new DefenseData();
    13.         attackData = new AttackData();
    14.         statusData = new StatusData();
    15.     }
    16. }
    Also, HealthData healthData = new HealthData(this); doesn't compile if placed outside of any method, Awake() in this case. I know that this code, which passes in the class itself as a parameter, must go into a function and not be in the global initializer, but why? For example, this does not compile:

    Code (CSharp):
    1. public class BaseStats : MonoBehaviour
    2. {
    3.     protected BaseStats baseStats();
    4.  
    5.     // create instances of stat data classes
    6.     public HealthData healthData = new HealthData(this); // DOES NOT COMPILE
    7.     public DefenseData defenseData = new DefenseData();
    8.     public AttackData attackData = new AttackData();
    9.     public StatusData statusData = new StatusData();
    10. }
     
    Last edited: Aug 3, 2016
  22. AssembledVS

    AssembledVS

    Joined:
    Feb 23, 2014
    Posts:
    248
    I have written another class, HealthDataPlayer, that inherits from HealthData. I am having trouble figuring out what to do with the constructor:

    HealthDataPlayer.cs
    Code (CSharp):
    1. public class HealthDataPlayer : HealthData
    2. {
    3.     private PlayerStats playerStats;
    4.  
    5.     // health amount per life unit
    6.     public float lifeUnitIncrement
    7.     { get; private set; }
    8.  
    9.     // attained num of max life units
    10.     public int maxAttainedLifeUnitCount
    11.     { get; private set; }
    12.  
    13.     // total num of max life units attainable in entire game
    14.     public int maxPossibleLifeUnitCount
    15.     { get; private set; }
    16.  
    17.     // redefine health for player, as it is determined differently than for other objects
    18.     public HealthDataPlayer(PlayerStats playerStats) // ERROR
    19.     {
    20.         /* set above declared playerStats to value of passed in parameter; parameter is the class
    21.          * itself that is being passed in from another script via this constructor */
    22.         this.playerStats = playerStats;
    23.  
    24.         lifeUnitIncrement = 50.0f;
    25.         maxAttainedLifeUnitCount = 3;
    26.         maxPossibleLifeUnitCount = 5;
    27.         current = lifeUnitIncrement * maxAttainedLifeUnitCount;
    28.         max = lifeUnitIncrement * maxAttainedLifeUnitCount;
    29.     }
    30. }
    The error is: There is no argument given that corresponds to the required formal parameter 'baseStats' of 'HealthData.HealthData(BaseStats)'. I understand the error: I'm passing the wrong argument in, 'playerStats' instead of 'baseStats'.

    The HealthData constructor gives some default values for everything that is not the player and gets a reference to BaseStats, which holds references to DefenseData and StatusData, which are necessary for HealthData's code.

    HealthData.cs
    Code (CSharp):
    1. public class HealthData
    2. {
    3.     protected BaseStats baseStats;
    4.  
    5.     // max health
    6.     public float max
    7.     { get; protected set; }
    8.  
    9.     // current health
    10.     protected float _current;
    11.     public float current
    12.     {
    13.         get { return _current; }
    14.         protected set
    15.         {
    16.             _current = value;
    17.  
    18.             if ((current <= 0) && (baseStats.statusData.isObjDead == false))
    19.             {
    20.                 baseStats.statusData.SetDeathStatus(true);
    21.             }
    22.         }
    23.     }
    24.  
    25.     // constructor
    26.     public HealthData(BaseStats baseStats)
    27.     {
    28.         /* set above declared baseStats to value of passed in parameter; parameter is the class
    29.          * itself that is being passed in from another script via this constructor */
    30.         this.baseStats = baseStats;
    31.  
    32.         current = 200;
    33.     }
    34.  
    35.     // ------------------------------------------------ //
    36.     /// <summary>
    37.     /// This method calculates damage and change the object's health from events like hits.
    38.     /// </summary>
    39.  
    40.     public virtual float DamageHealth(float damageAmount, int damageType, bool ignoreArmor)
    41.     {
    42.         float damageAmountCalculated;
    43.  
    44.         // calculate damage amount with defense modifiers
    45.         if (ignoreArmor == false)
    46.         {
    47.             switch (damageType)
    48.             {
    49.                 case 1: // 1 = vsArmor vs shielded
    50.                     if (baseStats.defenseData.defenseType == DefenseData.DefenseType.shielded)
    51.                     {
    52.                         // if shielded, take less damage from vsArmor weapon
    53.                         damageAmountCalculated = damageAmount * baseStats.defenseData.modifier *
    54.                                                  baseStats.defenseData.typeModifier;
    55.                     }
    56.  
    57.                     else
    58.                     {   // otherwise, take normal damage
    59.                         damageAmountCalculated = damageAmount * baseStats.defenseData.modifier;
    60.                     }
    61.  
    62.                     break;
    63.  
    64.                 case 2: // 2 = vsShielded vs armor
    65.                     if (baseStats.defenseData.defenseType == DefenseData.DefenseType.armored)
    66.                     {
    67.                         // if armored, take less damage from vsShielded weapon
    68.                         damageAmountCalculated = damageAmount * baseStats.defenseData.modifier *
    69.                                                  baseStats.defenseData.typeModifier;
    70.                     }
    71.  
    72.                     else
    73.                     {   // otherwise, take normal damage
    74.                         damageAmountCalculated = damageAmount * baseStats.defenseData.modifier;
    75.                     }
    76.  
    77.                     break;
    78.  
    79.                 default:    // 0 = vsNull; no extra modifier
    80.                     damageAmountCalculated = damageAmount * baseStats.defenseData.modifier;
    81.                     break;
    82.             }
    83.         }
    84.  
    85.         // ignoring armor, so no defense modifiers
    86.         else
    87.         {
    88.             damageAmountCalculated = damageAmount;
    89.         }
    90.  
    91.         // get current health before it is changed below
    92.         float oldValue = current;
    93.  
    94.         // change actual health
    95.         current -= damageAmountCalculated;
    96.  
    97.         return damageAmountCalculated;
    98.     }
    99.  
    100.     // ------------------------------------------------ //
    101.     /// <summary>
    102.     /// This method increases the object's health from events like healing.
    103.     /// </summary>
    104.  
    105.     public virtual void IncreaseHealth(float increaseAmount)
    106.     {
    107.         // get current health before it is changed below
    108.         float oldValue = current;
    109.  
    110.         // change actual health
    111.         current += increaseAmount;
    112.     }
    113.  
    114.     // ------------------------------------------------ //
    115.     /// <summary>
    116.     /// This method should only be used to directly change the object's's health.
    117.     /// </summary>
    118.  
    119.     public virtual void SetHealthDirect(float changeTo)
    120.     {
    121.         // get current health before it is changed below
    122.         float oldValue = current;
    123.  
    124.         // change actual health
    125.         current = changeTo;
    126.     }
    127. }
    BaseStats holds the actual instances of each class and is attached to an enemy/other game object:

    BaseStats.cs
    Code (CSharp):
    1. public class BaseStats : MonoBehaviour
    2. {
    3.     public HealthData healthData;
    4.     public DefenseData defenseData;
    5.     public AttackData attackData;
    6.     public StatusData statusData;
    7.  
    8.     void Awake()
    9.     {
    10.         // create instances of stat data classes
    11.         healthData = new HealthData(this);
    12.         defenseData = new DefenseData();
    13.         attackData = new AttackData();
    14.         statusData = new StatusData();
    15.     }
    16. }
    But my HealthDataPlayer class calculates the player's life differently from that of other objects and needs that additional code. I think that I should also should be passing in BaseStats' player counterpart, PlayerStats, not BaseStats as a reference. I'm unsure whether this assumption is correct.

    How can I change my approach to get what I want? Is using inheritance in the way that I'm using it good for this? (There is some shared data and methods, but others must be changed.)
     
    Last edited: Aug 3, 2016
  23. AssembledVS

    AssembledVS

    Joined:
    Feb 23, 2014
    Posts:
    248
    Does anyone have any ideas?
     
  24. xjjon

    xjjon

    Joined:
    Apr 15, 2016
    Posts:
    587
    You can pass the instance of baseStats by using this keyword. (i.e. new HealthData(this))

    If you need a player health data that is different in logic, you could abstract HealthData and then implement two versions of it, one for the player and one for the other enemies / etc.

    So you could have an abstract class HealthData

    then two classes PlayerHealthData and EnergyHealthData and override the abstract methods that you need to change.

    Same can apply for BaseStats. If you need to have multiple implementations of it, you could create an abstract BaseStats class and then two other ones (PlayerBaseStats) and EnemyBaseStats.

    You'll be able to get through the invalid parameters now since they both derive from baseStats.
     
    AssembledVS likes this.
  25. AssembledVS

    AssembledVS

    Joined:
    Feb 23, 2014
    Posts:
    248
    Thanks for this. I'm finding all of this information very helpful!