0
\$\begingroup\$

Right now, I have a Player.cs class that handles button pushes, movement speed, damage, Update() etc. At the moment, all of the logic for button presses triggering an ability, and the logic surrounding that ability lives in my player.class. I'd like to take that logic and put it into an Abilities.cs class. All I want in my player at most would be:

public override void Update()
{
    if (button.pushed == "berserkKey")
    {
       ability.berserk();
    }
}

... where for example berserk is a stacking buff but none of that logic is in my player class anymore.

For additional context, I plan to implement a Classes.cs class which will hold the info on classes (warrior/archer/etc) which will know exactly what the class and therefor player can use. The code above will likely abstract out even more when that is the case.

The problem is that my Player class already inherits from another class. How do I go about designing my Classes and Abilities classes in a way that my player can use them in the example above?

I'm also open to a completely different way if my initial thought here isn't ideal.

\$\endgroup\$
0

2 Answers 2

1
\$\begingroup\$

I'd recommend you solve this with composition over inheritance.

Let's say we have an ability interface:

public interface IAbility {
    void UseAbility();
}

Now my berserk ability can be a MonoBehaviour component that implements this interface:

public class BerserkAbility : MonoBehaviour, IAbility {

    public float duration = 5f;
    public float damageModifier = 1.5f;
    public float defenseModifier = 2.0f;

    public void UseAbility() {
        // TODO: do some berserk stuff.
    }
}

Now your player control class can look something like this:

public class PlayerControl : MonoBehaviour {

    public IAbility primaryAbility;
    public IAbility secondaryAbility;

    void Update() {
        if (button.pushed == "primaryAbility" && primaryAbility != null) {
            primaryAbility.UseAbility();
        }

        if (button.pushed == "secondaryAbility" && secondaryAbility != null) {
            secondaryAbility.UseAbility();
        }
    }
}

...and it doesn't need to know just what those abilities are, just that it's allowed to use them.

Your class advancement script can then attach new ability components to your player and wire them up to the control script as your character gains new class abilities.

\$\endgroup\$
7
  • \$\begingroup\$ Thanks. I haven't implemented interfaces before so I will brush up on that and get back to this. \$\endgroup\$ Apr 27, 2020 at 0:22
  • \$\begingroup\$ You can accomplish similar things with events or delegates too, so don't feel like this is the only option. Just one that's simple to outline. ;) \$\endgroup\$
    – DMGregory
    Apr 27, 2020 at 0:23
  • \$\begingroup\$ I spent about 4 hours trying to figure this out. Part of the problem might be that I'm modding Terraria, not using unity. They have a class concept of ModPlayer that contains some things that I need to use in the berserk ability itself. The thing is, I keep getting "object is not set to an instance". I reverted to trying to pass the Player and Mod references to my new berserk ability directly when I create it in my Player code. These are then assigned in the berserk class code to use built in terraria code to start the buff: player.AddBuff(mod.BuffType("BerserkBuff")) . This is the error line \$\endgroup\$ Apr 27, 2020 at 8:10
  • 1
    \$\begingroup\$ all of this information should probably go into your initial question. \$\endgroup\$ Apr 27, 2020 at 8:17
  • \$\begingroup\$ Thanks, I'll update after work. I'm still stuck. \$\endgroup\$ Apr 27, 2020 at 19:14
0
\$\begingroup\$

Ok, this is how I solved this ultimately. I found three distinct things I wanted to define by classes:

  1. The player (already done by default)
  2. The player's class (warrior, rogue, etc)
  3. The abilities used by classes (berserk, stealth, etc)

This is how I did it, starting with #1 The Player:

        PlayerClass playerClass;
        public static ModHotKey ability1 = new ModHotKey("F");
        ...

        Update()
        {
        ...
            else if (choice == "Warrior")
            {
                playerClass = new WarriorClass(player, 100);
            }
        ...
        }

        KeyTriggers(TriggersSet triggersSet)
        {
            if (ability1.JustPressed)
        {
            playerClass.UseAbility(ability1);
        }

    }

PlayerClass is a mostly empty class that guides what methods should be called when inherited. Example WarriorClass inheriting from PlayerClass:

public class WarriorClass : PlayerClass
{
    int level;
    Player player = new Player();
    //Ability placeholders
    public IAbility primaryAbility;
    public IPassive primaryPassive;


    //Constructor. Needs player reference and level to set skills and stats correctly (when progression is a thing)
    public WarriorClass(Player player,  int level)
    {
        this.player = player;
        this.level = level;
        //Set ability manually for now
        primaryAbility = new BerserkAbility();
        primaryPassive = new LifeRegenPassive();
    }

    public override void DoPassives()
    {
        primaryPassive.DoPassive(player);
    }

    public override void UseAbility(ModHotKey key)
    {
        if (key == MinionTest.ability1)
        {
            primaryAbility.UseAbility(player);
        }
    }
}

It doesn't seem like much now that I've finished the entire design, but it will hold WarriorSpecific things like its progression and possibly talent tree (or maybe those will be classes themselves!).

Next up, the abilities the classes use. Notice above that the warrior doesn't know what ability it is going to use in UseAbility() until the constructor is called. Here is some ability code:

    public class BerserkAbility : IAbility
{
    int lifeLost;

    public void UseAbility(Player player)
    {
        //If the buff is off completely... start the buff on stack one
        if (player.HasBuff(ModContent.BuffType<BerserkBuff1>()) == false && player.HasBuff(ModContent.BuffType<BerserkBuff2>()) == false && player.HasBuff(ModContent.BuffType<BerserkBuff3>()) == false && player.HasBuff(ModContent.BuffType<BerserkBuff4>()) == false && player.HasBuff(ModContent.BuffType<BerserkBuff5>()) == false)
        {
            //Lose 10% of your life
            lifeLost = player.statLifeMax2 / 10;
            player.statLife = player.statLife - (int)lifeLost;

            //Start first stack of berserk for 10 seconds
            player.AddBuff(ModContent.BuffType<BerserkBuff1>(), 600, false);
        }
        //If it's already at stack 1, start stack two...
        else if (player.HasBuff(ModContent.BuffType<BerserkBuff1>()))
        ...

Where UseAbility (maybe a poor choice due to not being unique project wide) is required due to the interface IAbility interface. If you are stuck on Buffs above, those are already a distinct object or class in the mod library. IAbility is just this:

namespace MinionTest.Abilities
{
    public interface IAbility
    {
        void UseAbility(Player player);
    }

    //Should add cooldown here later
}

Why did I user class inheritance for player classes and an interface for abilities? I'm not really sure and wanted to try both.

Essentially what I end up with is a player that hits a key and asks the warrior to do the correct ability, and the warrior asks the ability how to do it. This adds three main benefits I see right now:

  1. Really nice, readable code and code architecture. You know exactly what file and class to edit to change a class or ability.
  2. It allows for game design choices like choosing a new class on the fly in-game, and talent trees that may change up what abilities your class can use.
  3. The possibility to be extended incredibly easily. People can work on an ability in and of itself. A person can work on a class in and of itself, and plug and play abilities to it.
\$\endgroup\$
2
  • \$\begingroup\$ This flagged our duplicate answer checker. If you post two different questions about the same issue, so that they could be answered by a copy-and-paste of the same text, then they probably shouldn't be two separate questions. What you might want to do is edit your existing question instead to make sure it clearly expresses what you really need solved. \$\endgroup\$
    – DMGregory
    May 1, 2020 at 12:25
  • \$\begingroup\$ I could see that. It is the same answer but I needed two different questions asked to figure out inheritance and interfaces, which I used both of. Therefore this one answer answered both. \$\endgroup\$ May 1, 2020 at 22:17

Not the answer you're looking for? Browse other questions tagged .