Search Unity

  1. Unity 6 Preview is now available. To find out what's new, have a look at our Unity 6 Preview blog post.
    Dismiss Notice
  2. Unity is excited to announce that we will be collaborating with TheXPlace for a summer game jam from June 13 - June 19. Learn more.
    Dismiss Notice

RPG Class/Job Structure c#

Discussion in 'Scripting' started by Collin__Patrick, Apr 9, 2016.

  1. Collin__Patrick

    Collin__Patrick

    Joined:
    May 20, 2015
    Posts:
    188
    I am creating an RPG where the player can control multiple character that are each unique in their own way. I am faced with a problem of implementing Character classes such as Fighter,Wizard, and Rouge that can then branch off into other classes of that type. I have been scavenging around the internet for about a week now and I cannot seem to find anything that actually fits my needs. Everything I find only gives hypothetical examples in words and not in code. It is kind of hard to learn any new concepts if a code example is not present. Now I call out to the Unity RPG Gurus out there for your help. I don't necessarily need help crating my own code but I need help being pointed in the right direction with structure examples I can follow.
     
  2. LeftyRighty

    LeftyRighty

    Joined:
    Nov 2, 2012
    Posts:
    5,148
    have you done much with inheritance before?
     
  3. Collin__Patrick

    Collin__Patrick

    Joined:
    May 20, 2015
    Posts:
    188
    I have been told several times before that it is best practice, and more efficient not to use much inheritance in unity.
     
  4. ftremblay

    ftremblay

    Joined:
    Nov 8, 2015
    Posts:
    2
    What you could do is a C# class for your character and compose it with different behavior interfaces. I don't know what are your needs, but in my games I usually go with composition.

    Code (CSharp):
    1. class Character {
    2.   IAttackBehaviour AttackBehaviour;
    3.  
    4.   void Attack(){
    5.     AttackBehaviour.Attack();
    6.   }
    7. }
    So I think that you should not know which type of character your are instantiate by the class name like Fighter, Wizard. It should be a Character that behave like a wizard or a fighter.
     
  5. Collin__Patrick

    Collin__Patrick

    Joined:
    May 20, 2015
    Posts:
    188
    And If I want a Character to be able change classes?
     
  6. GarthSmith

    GarthSmith

    Joined:
    Apr 26, 2012
    Posts:
    1,240
    I'm going to call them "jobs" instead of "character classes" (ala Final Fantasy Tactics) because I get classes mixed with C# classes.

    Inheritance is good. It's taught a lot in schools. I would suggest this if you don't have a lot of jobs.

    Composition is probably better for some things. Especially many times in games. Like @ftremblay suggests, your Character should contain an IAttackBehaviour. (ICharacterClass or ICharacterJob might make more sense as a name.) Your different jobs would implement this interface. To change the job a character has, you would need to replace the ICharacterJob that the Character holds.

    ICharacterJob is just an interface. Fighter, Wizard, Rogue would implement ICharacterJob. The ICharacterJob that Character holds would determine their job.

    If you want multiclassing, then you can make characters hold a List<ICharacterJob> so they can be multiple jobs at once.
     
    Enerex and Kartik0330 like this.
  7. Laperen

    Laperen

    Joined:
    Feb 1, 2016
    Posts:
    1,065
    Seems like a concept problem, so unfortunately you are going to get more hypothetical examples. I'm going to make a few assumptions first. This RPG has the player controlling a team of heroes. The player never directly takes control of any of these heroes, instead orders are given to them, much like an RTS.

    So these hero characters at the very least will need to be able to
    - move, but no need to jump
    - use skills, with an array or list of them which can have skills easily put in and taken/switched out.
    - affect health, from damage and heals from skills
    we are going to ignore statistics, equipment bonuses, elemental resistances and whatever other factor for now.

    So movement is pretty straight forward, doubt there will be much problems.

    For skills, I would create skill prefabs, made up of components like damage, auxilary effect 1, auxilart effect 2, etc.
    each skill will have its own behavior, like a fireball will move from hero start point, to cursor end point, it has nothing to do with the hero itself.

    For affecting health, I would create a stats script to manage a list of serializable classes I have named resources. So lets say my mage will have health and mana, while my fighter will have health and stamina, the paladin will have health stamina and mana, the priest will have health stamina mana and faith. I can add as many resource types as I want to the list. Then I have a reaction script which will have functions to add/remove from the resource script, based on damage taken, based on a heal from a skill, based on what resources a skill consumes, etc.

    The concept behind it is very much like TheSims really. The character is a mere reactor to the events around it. There is no attack behavior, only attack objects telling the character what animation to play, maybe where to go if needed. Your "different character classes" under such a framework would be the kind of skills they have, the type of resources the hero has at its disposal, and the amount of each said resource the individual hero has

    Once the framework has been setup, then maybe you can start working on items and buffs and such, have statistics and percentage increase amounts or however you are leveling up your characters.
     
    Enerex likes this.
  8. Collin__Patrick

    Collin__Patrick

    Joined:
    May 20, 2015
    Posts:
    188
    I am kinda following. Could I get a written example of assigning a "Job" and changing "Jobs"?
     
  9. GarthSmith

    GarthSmith

    Joined:
    Apr 26, 2012
    Posts:
    1,240
    To add:

    Inheritance would be if your Unit is a Wizard that extends Character. A wizard is a character. (Not suggested here. This will make it hard to change a unit's job.)

    Composition is you have a Unit, and they CONTAIN a job. A unit *isn't* a Wizard, but the Unit might CONTAIN wizard code. If the Unit CONTAINS a wizard, then we know the Unit can do wizard stuff.
    Code (csharp):
    1. public class Unit
    2. {
    3.     public ICharacterJob Job;
    4.  
    5.     public void Attack()
    6.     {
    7.         Job.Attack();
    8.     }
    9. }
    10.  
    11. // Shared functions between jobs.
    12. public interface ICharacterJob
    13. {
    14.     void Attack();
    15.     // If different jobs have different abilities, might a list of available commands.
    16.     // Ability[] Abilities { get; }
    17. }
    18.  
    19. public class Wizard : ICharacterJob
    20. {
    21.     public void Attack() { // cast spell here }
    22. }
    23.  
    Code (csharp):
    1. // To change a Unit's job type
    2. Unit someUnit = new Unit();
    3. someUnit.Job = new Wizard(); // now a wizard.
    4. // Bard implements ICharacterJob to sing on attack
    5. someUnit.Job = new Bard(); // now a bard.
     
    Last edited: Apr 9, 2016
    Fajlworks likes this.
  10. Collin__Patrick

    Collin__Patrick

    Joined:
    May 20, 2015
    Posts:
    188
    So pretty much what I am doing is my character has a base stat that always changes. The classes that each character is assigned has a stat modifier that adds to the character base stat and the class also includes the class name, class weapon type, class strengths, class weaknesses. The style of combat is much like Fire Emblem where I control a large group of "Heroes".
     
  11. GarthSmith

    GarthSmith

    Joined:
    Apr 26, 2012
    Posts:
    1,240
    So the ICharacterJob might look like this:
    Code (csharp):
    1. public interface ICharacterJob
    2. {
    3.    float StatModifier { get; }
    4.    string JobName { get; }
    5.    // Not sure how you id strengths and weaknesses.
    6.    // Assuming string ids for this example.
    7.    string[] Strengths { get; }
    8.    string[] Weaknesses { get; }
    9. }
    Then your Hero would check their own Jobs property to see their JobName or strengths or other job specific data.

    Feel free to call it ICharacterClass (or whatever) if that's easier for you!
     
  12. Collin__Patrick

    Collin__Patrick

    Joined:
    May 20, 2015
    Posts:
    188
    So like I was saying to Laperen, I want to be able to get integers and strings and assign them in a character sheet depending on the class. The game is turn based so the calculations for damage and damage taken are elsewhere. In order to do the calculations I add the base character stat and the modifier from the class. How would I use the interfaces to assign variables.
     
  13. GarthSmith

    GarthSmith

    Joined:
    Apr 26, 2012
    Posts:
    1,240
    Something like this, though I haven't seen your code so I'm guessing what it might look like.
    Code (csharp):
    1. public void Unit
    2. {
    3.     // Assign this to provide Unit with Character Class specific-data.
    4.     public ICharacterJob Job;
    5.  
    6.     // I assume you have something similar to this already
    7.     public float AttackRating { get; private set; }
    8.  
    9.     public void Attack()
    10.     {
    11.         // calculate damage by combining this Unit's stat with the Character Class stat modifier.
    12.         float totalAttackRating = AttackRating * Job.StatModifier;
    13.  
    14.         // Attack as normal using totalAttackRating instead of just AttackRating.
    15.     }
    16. }
    When you assign the Unit's Job, you are providing it with all the character class specific data they would ever need.
     
  14. Collin__Patrick

    Collin__Patrick

    Joined:
    May 20, 2015
    Posts:
    188
    So this is what I have so far.
    Code (CSharp):
    1. public interface ICharacterClass
    2. {
    3.     string ClassName { get; }
    4.     string ClassDescription { get; }
    5.     string ClassStrength { get; }
    6.     string ClassWeakness { get; }
    7.     string ClassWeaponType { get; }
    8.  
    9.     int Strength { get; }
    10.     int Magic { get; }
    11.     int Speed { get; }
    12.     int Agility { get; }
    13.     int Defense { get; }
    14.     int Luck { get; }
    15.     int Health { get; }
    16.  
    17. }
    If I put this interface on mt FighterClass script, where would the Fighter class script then get its variable.
     
    Last edited: Apr 9, 2016
  15. Laperen

    Laperen

    Joined:
    Feb 1, 2016
    Posts:
    1,065
    What i have mentioned is merely a framework concept. Under this framework, it is the skills which take precedence over the individual hero's properties. Skills become the primary mode of interacting with the world. Without imposed restriction you would be able to assign a mage's skill to a warrior. you can add in your modifiers later, but I would do it like this
    Code (CSharp):
    1. public class Stats: MonoBehaviour
    2.    public GameObject[] skills;
    3.    public int strength;
    4.    public int stamina;
    5.    public int dexterity;
    6.    public int intelligence;
    7.  
    8.    public void CastSkill(int index){
    9.        GameObject skill = Instantiate(skills[index], ...);
    10.        skill.GetComponent<Skill>().CastSkill(this.GetComponent<Stats>());
    11.    }
    12. }
    in the skill itself would then have its own multiplier from the stats
    Code (CSharp):
    1. public class Skill: MonoBehaviour {
    2.     private float damage;
    3.  
    4.     public void CastSkill (Stats stats){
    5.         damage = stats.str + 10;
    6.         //movement behavior etc etc...
    7.     }
    8.     void OnTriggerEnter(Collider collide){
    9.         if(collide.gameObject.GetComponent<ReactionScript>() != null){
    10.             Stats enemyStats = collide.gameObject.GetComponent<Stats>();
    11.             collide.gameObject.GetComponent<ReactionScript>().TakeDamage(damage, enemyStats);
    12.         }
    13.     }
    14. }
    and finally, the reaction script on the victim will react to the skill, calculating if it's hit and take the damage.
    Code (CSharp):
    1. public class ReactionScript: MonoBehaviour{
    2.     private Stats stats;
    3.  
    4.     void Start(){
    5.         stats = this.GetComponent<Stats>();
    6.     }
    7.     public void TakeDamage(float dmg, Stats enemy){
    8.         if(stats.dex > enemy.dex){
    9.             //miss
    10.         } else {
    11.             //take damage
    12.         }
    13.     }
    14. }
     
    Last edited: Apr 9, 2016
    Enerex and GarthSmith like this.
  16. GarthSmith

    GarthSmith

    Joined:
    Apr 26, 2012
    Posts:
    1,240
    I'm shortening this a little bit, but you should still get the idea.
    Code (csharp):
    1. public class Fighter : ICharacterClass
    2. {
    3.     public string ClassName { get { return "Fighter"; } }
    4.     public string ClassDescription { get { return "I'm a fighter, not a lover."; } }
    5.     public int Strength { get { return 10; } }
    6.     public int  Health { get { return 100; } }
    7. }
    Code (csharp):
    1. Unit someUnit = new Unit();
    2. someUnit.Job = new Fighter(); // Now someUnit has access to all Fighter data.
    What @Laperen says is good too. You can take composition really far and not even have "Units" but components like "Thing that can get hit and lose health." or "Thing that can cast a spell" or "Thing that has stats" then just focus on the interaction between components. It's not important to know that something is a Unit. Usually knowing if something can attack, or can be damaged, etc. is enough. This makes it really easy to change the behavior of a thing just by removing or adding components.

    The biggest problem I had learning how to component things out like that is that the lower division CompSci classes I took (a while ago now) didn't even cover Components and Composition so it was a foreign concept to me and I found it difficult to implement for a while, and existing code I had used Inheritance so much it was difficult to refactor. Each game I make seems to use Composition more and Inheritance less though, so it's definitely very useful.
     
    Enerex likes this.
  17. Collin__Patrick

    Collin__Patrick

    Joined:
    May 20, 2015
    Posts:
    188
    Ok now it makes sense.
     
  18. Collin__Patrick

    Collin__Patrick

    Joined:
    May 20, 2015
    Posts:
    188
    Where would I put:
    Code (CSharp):
    1. Unit someUnit = new Unit();
    2. someUnit.Job = new Fighter();
    Would I put it in the Character Sheet or elsewhere?
     
  19. GarthSmith

    GarthSmith

    Joined:
    Apr 26, 2012
    Posts:
    1,240
    I would put it wherever you create a Unit. How are you creating Units right now? How will you determine what character class a Unit has?

    I'm not sure what you mean exactly by Character Sheet. Your existing code might help if you need more details.
     
  20. Collin__Patrick

    Collin__Patrick

    Joined:
    May 20, 2015
    Posts:
    188
    Each of my Character sheets inherits this Character script:
    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3.  
    4. public abstract class Character : MonoBehaviour {
    5.     public string characterName;
    6.     public string className;
    7.     public string classDescription;
    8.     public string classWeaponType;
    9.     public string classWeaponStrength;
    10.     public string classWeaponWeakness;
    11.  
    12.     public static int level;
    13.     public static int exp;
    14.     public static int neededExp;
    15.  
    16.     public int classStrength;
    17.     public int classMagic;
    18.     public int classSpeed;
    19.     public int classAgility;
    20.     public int classDefense;
    21.     public int classLuck;
    22.     public int classHealth;
    23.  
    24.     public int baseStrength;
    25.     public int baseMagic;
    26.     public int baseSpeed;
    27.     public int baseAgility;
    28.     public int baseDefense;
    29.     public int baseLuck;
    30.     public int baseHealth;
    31. }
    Then the Character sheet decides the baseStats and Level information:
    Code (CSharp):
    1. public class ZepherCharacterSheet : Classes{
    2.  
    3.     int strength;
    4.     int magic;
    5.     int speed;
    6.     int agility;
    7.     int defense;
    8.     int luck;
    9.     int health;
    10.  
    11.     void Start(){
    12.      
    13.         baseStrength = 5;
    14.         baseMagic = 5;
    15.         //baseSpeed = 5;
    16.         baseAgility = 5;
    17.         baseDefense = 5;
    18.         baseLuck = 5;
    19.         baseHealth = 5;
    20.  
    21.         //The character name.
    22.         characterName = "Zepher";
    23.  
    24.         //current level and exp
    25.         level = 1;
    26.         exp = 0;
    27.         neededExp = 100;
    28.  
    29.         //calculates the final value of each stat
    30.         strength = baseStrength + classStrength;
    31.         magic = baseMagic + classMagic;
    32.         speed = classSpeed; //Speed is only determined by the class speed value.
    33.         agility = baseAgility + classAgility;
    34.         defense = baseDefense + classDefense;
    35.         luck = baseLuck + classLuck;
    36.         health = baseHealth + classHealth;
    37.  
    38.         //Testing if variables are being assigned and working
    39.         Debug.Log (characterName);
    40.         Debug.Log (className);
    41.         Debug.Log (classDescription);
    42.         Debug.Log (classWeaponType);
    43.         Debug.Log (classWeaponStrength);
    44.         Debug.Log (classWeaponWeakness);
    45.         Debug.Log (strength);
    46.         Debug.Log (magic);
    47.         Debug.Log (speed);
    48.         Debug.Log (agility);
    49.         Debug.Log (defense);
    50.         Debug.Log (luck);
    51.         Debug.Log (health);
    52.  
    53.         AddExp (10000);
    54.  
    55.     }
    56.     //Calculates and determins if this character levels up and how many times they do when exp is given.
    57.     public static void AddExp(int EXP){
    58.         exp += EXP;
    59.         Debug.Log (" gained " + EXP + " EXP!");
    60.         while (exp >= neededExp) {
    61.             level ++;
    62.             Debug.Log("Zepher leveled up to " + level);
    63.             exp -= neededExp;
    64.             neededExp += 50;
    65.         }
    66.         Debug.Log (exp);
    67.         Debug.Log (neededExp);
    68.     }
    69.  
    70. }
    If there is a better or more efficient way of doing this then please tell me.
     
  21. GarthSmith

    GarthSmith

    Joined:
    Apr 26, 2012
    Posts:
    1,240
    Instead of setting strength in Start(), I would make it a getter so it's always up to date.
    Code (csharp):
    1. public int strength
    2. {
    3.     get { return Jobs.Strength + baseStrength; }
    4. }
    In your Character class, I would remove all the class specific stuff and just add the ICharacterClass property.

    Getting a little off topic, you can also just have a string CharacterClassId and look up Wizard info from a singleton or static class if the Wizard info is the same for everyone in the entire game. (Ie: Flyweight Pattern.)
     
    ROAMA likes this.
  22. Collin__Patrick

    Collin__Patrick

    Joined:
    May 20, 2015
    Posts:
    188
    Ok with this revised script how would I set the job to Fighter?
    Code (CSharp):
    1.  
    2. public class ZepherCharacterSheet : Classes{
    3.  
    4.     public ICharacterClass CurrentClass;
    5.  
    6.  
    7.     public int strength
    8.     {
    9.         get { return CurrentClass.Strength + baseStrength; }
    10.     }
    11.     public int magic
    12.     {
    13.         get { return CurrentClass.Magic + baseMagic; }
    14.     }
    15.     public int speed
    16.     {
    17.         get { return CurrentClass.Speed; }
    18.     }
    19.     public int agility
    20.     {
    21.         get { return CurrentClass.Agility + baseAgility; }
    22.     }
    23.     public int defense
    24.     {
    25.         get { return CurrentClass.Defense + baseDefense; }
    26.     }
    27.     public int luck
    28.     {
    29.         get { return CurrentClass.Luck + baseLuck; }
    30.     }
    31.     public int health
    32.     {
    33.         get { return CurrentClass.Health + baseHealth; }
    34.     }
    35.  
    36.  
    37.     void Start(){
    38.        
    39.         baseStrength = 5;
    40.         baseMagic = 5;
    41.         //baseSpeed = 5;
    42.         baseAgility = 5;
    43.         baseDefense = 5;
    44.         baseLuck = 5;
    45.         baseHealth = 5;
    46.  
    47.         //The character name.
    48.         characterName = "Zepher";
    49.  
    50.         //current level and exp
    51.         level = 1;
    52.         exp = 0;
    53.         neededExp = 100;
    54.  
    55.         //Testing if variables are being assigned and working
    56.         Debug.Log (characterName);
    57.         Debug.Log (CurrentClass.ClassName);
    58.         Debug.Log (CurrentClass.ClassDescription);
    59.         Debug.Log (CurrentClass.ClassWeaponType);
    60.         Debug.Log (CurrentClass.ClassStrength);
    61.         Debug.Log (CurrentClass.ClassWeakness);
    62.         Debug.Log (strength);
    63.         Debug.Log (magic);
    64.         Debug.Log (speed);
    65.         Debug.Log (agility);
    66.         Debug.Log (defense);
    67.         Debug.Log (luck);
    68.         Debug.Log (health);
    69.  
    70.         AddExp (10000);
    71.  
    72.     }
    73.     //Calculates and determins if this character levels up and how many times they do when exp is given.
    74.     public static void AddExp(int EXP){
    75.         exp += EXP;
    76.         Debug.Log (" gained " + EXP + " EXP!");
    77.         while (exp >= neededExp) {
    78.             level ++;
    79.             Debug.Log("Zepher leveled up to " + level);
    80.             exp -= neededExp;
    81.             neededExp += 50;
    82.         }
    83.         Debug.Log (exp);
    84.         Debug.Log (neededExp);
    85.     }
    86.  
    87. }
    88.  
     
    Last edited: Apr 9, 2016
  23. GarthSmith

    GarthSmith

    Joined:
    Apr 26, 2012
    Posts:
    1,240
    If all ZepherCharacterSheet is a MonoBehaviour and Zephers are wizards, you can set it in void Start().
    Code (csharp):
    1. void Start()
    2. {
    3.     CurrentClass = new Wizard();
    4.  
    5.     // The rest of the Start() code you already had.
    6. }
    If you want to change it later on, you need to get a reference to your ZepherCharacterSheet before you can change it.
    Code (csharp):
    1. // Assuming the character sheet is on the same gameObject as whatever runs this code.
    2. ZepherCharacterSheet zepher = GetComponent<ZepherCharacterSheet>();
    3. zepher.CurrentClass = new Bard();
    If you need to detect from the player when to do this, might want to look up how to run scripts from a UI Button. If the ZepherCharacterSheet is on another object, then you'll need to figure out how to get a reference to that script.
     
  24. Collin__Patrick

    Collin__Patrick

    Joined:
    May 20, 2015
    Posts:
    188
    One last question. I changed my character sheet so its the same script on each character and i assign the variables in the inspector. As far as I know, it is impossible to get an interface to show up in the inspector. Is there a work around such as converting a string value into an interface type?
     
  25. GarthSmith

    GarthSmith

    Joined:
    Apr 26, 2012
    Posts:
    1,240
    If you need to assign things in the inspector, then use a MonoBehaviour or ScriptableObject. Lots of ways to do this. Here's an example.
    Code (csharp):
    1. public class MonoCharacterClass : MonoBehaviour, ICharacterClass
    2. {
    3.     [SerializeField] string MClassName;
    4.     [SerializeField] string MClassDescription;
    5.     [SerializeField] int MStrength;
    6.     [SerializeField] int MHealth;
    7.  
    8.     public string ClassName { get { return MClassName; } }
    9.     public string ClassDescription { get { return MClassDescription; } }
    10.     public int Strength { get { return MStrength; } }
    11.     public int Health { get { return MHealth; } }
    12. }
    Then you can assign it in the inspector. This makes it easy to edit classes in the inspector. Changing a character class is as easy as dragging-and-dropping in the inspector. Here's example code.
    Code (csharp):
    1. public class ThingWithCharacterClass : MonoBehaviour
    2. {
    3.     // Assign in inspector
    4.     [SerializeField] MonoCharacterClass MCurrentClass;
    5.  
    6.     public ICharacterClass CurrentClass { get { return MCurrentClass; } }
    7. }
    Note: The [SerializeField] let's the inspector assign private fields. Using the public keyword instead is also common.
     
    Last edited: Apr 10, 2016
  26. Collin__Patrick

    Collin__Patrick

    Joined:
    May 20, 2015
    Posts:
    188
    And I would still use
    Code (CSharp):
    1. someUnit.Job = new Fighter();
    If I wanted to change the job while in runtime to something different from what I set in the inspector?
     
  27. ericbegue

    ericbegue

    Joined:
    May 31, 2013
    Posts:
    1,353
    Hi,

    Have a look at Panda BT. It's a script-based Behaviour Tree engine. With this tool you can define behaviours at runtime by simply generating a string that contains the source code of a BT script and it will get compiled and executed at runtime.

    Furthermore, you can mix and match different BT scripts on the same GameObject, so that you can give it a unique behaviour. The package contains an example that illustrates this purpose. The example "Shooter" is a third person shooter in which the enemies have different behaviours (you can think of them as different classes) yet they are sharing the same C# scripts, it's only their BT scripts that are different.

    You can have more information on the website:

    http://www.pandabehaviour.com

    I'm the author, so if you have any question about using Panda BT, fire!

    You're also welcome on this thread for suggestions:

    http://forum.unity3d.com/threads/ai-scripting-panda-bt-behaviour-tree-scripting.388429/