4
\$\begingroup\$

Please, if you find a better title, tell me because I really didn't know what to put in here!

I like to guess how I could implement a game, and to be sure that my perception is working, I plan to develop small games. For instance, I plan to build a FFTA-like game, a FTL-like game, a tower-defense game, and so on. As a base, I created a bunch of rpg-classes (Warrior, Wizard, ...), and I want to use all of them in each of my futur game.

The only sure thing I know about what should be in common for every game is the stat system:

  • Strength
  • Constitution
  • Agility
  • Accuracy
  • Intelligence
  • Charisma
  • Luck

I will continue with what I plan to do, but I am not really sure about my choices:

namespace General
{
    public interface IStatsOwner
    {
        int Strength { get; }
        int Constitution { get; }
        int Agility { get; }
        int Accuracy { get; }
        int Intelligence { get; }
        int Charisma { get; }
        int Luck { get; }
    }

    public abstract class WarriorBase : IStatsOwner
    {
        public int Strength { get { return 5; } }
        public int Constitution { get { return 4; } }
        public int Agility { get { return 3; } }
        public int Accuracy { get { return 3; } }
        public int Intelligence { get { return 2; } }
        public int Charisma { get { return 3; } }
        public int Luck { get { return 1; } }
    }

    public abstract class WizardBase : IStatsOwner
    {
        public int Strength { get { return 2; } }
        public int Constitution { get { return 3; } }
        public int Agility { get { return 3; } }
        public int Accuracy { get { return 3; } }
        public int Intelligence { get { return 5; } }
        public int Charisma { get { return 4; } }
        public int Luck { get { return 1; } }
    }

    public interface IGame
    {
        Type WarriorType { get; }
        Type WizardType { get; }
    }
}

namespace FFTALike
{
    public interface IDamageable
    {
        void TakeDamages();
    }

    public class Warrior : General.WarriorBase, IDamageable
    { 
        public void TakeDamages() { Console.WriteLine($"Taking damages with CON={Constitution}"); }
    }

    public class Wizard : General.WizardBase, IDamageable
    {
        public void TakeDamages() { Console.WriteLine($"Taking damages with CON={Constitution}"); }
    }

    public Game : IGame
    {
        public Type WarriorType { get { return typeof(Warrior); } }
        public Type WizardType { get { return typeof(Wizard); } }
    }
}

namespace TowerDefense
{
    public interface ILocatable
    {
        Location Location { get; set; } // Location(int X, int Y)
    }

    public interface IShooter
    {
        void Shoot();
    }

    public class Warrior : WarriorBase, ILocatable, IShooter
    {
        public Location Location { get; set; }
        public void Shoot() { Console.WriteLine($"Shooting with STR={Strength}"); }
    }

    public class Wizard : WizardBase, ILocatable, IShooter
    {
        public Location Location { get; set; }
        public void Shoot() { Console.WriteLine($"Shooting with STR={Strength}"); }
    }

    public Game : IGame
    {
        public Type WarriorType { get { return typeof(Warrior); } }
        public Type WizardType { get { return typeof(Wizard); } }
    }
}

What I feel uncomfortable with is:

  • I don't enforce the fact that IGame.WarriorType inherits WarriorBase
  • I duplicate TakeDamages(), Location { get; set; } and Shoot() (I actually have 20 rpg-classes, so I isocuplate these implementations)

Please note that I want the final implementation to fit with SOLID principles, because I want these projects to be training projects.

\$\endgroup\$

closed as off-topic by t3chb0t, Mast, Quill, Ludisposed, chux Nov 2 '18 at 4:37

This question appears to be off-topic. The users who voted to close gave this specific reason:

  • "Lacks concrete context: Code Review requires concrete code from a project, with sufficient context for reviewers to understand how that code is used. Pseudocode, stub code, hypothetical code, obfuscated code, and generic best practices are outside the scope of this site." – t3chb0t, Mast, Quill, Ludisposed
If this question can be reworded to fit the rules in the help center, please edit the question.

  • 1
    \$\begingroup\$ This doesn't look like working code to me, and I don't see concrete game-specific requirements, so I'm afraid this is off-topic here. Either way, I think you're trying to generalize too early and too much, and you're doing too much at the type-level - a data-driven approach seems more suitable here. \$\endgroup\$ – Pieter Witvoet Oct 31 '18 at 16:16
  • \$\begingroup\$ About public interface ILocatable // I don't know how to say :p - I find this is a perfect name for that interface! Exactly as it should be ;-) \$\endgroup\$ – t3chb0t Oct 31 '18 at 16:30
  • 2
    \$\begingroup\$ This isn't doing anything yet. How would you use this? If you have no usage for it yet, you're too early with wanting to generalize it. \$\endgroup\$ – Mast Oct 31 '18 at 17:05
  • \$\begingroup\$ Regarding my note about 'doing too much at the type level', Eric Lippert wrote an interesting series about that: ericlippert.com/2015/04/27/wizards-and-warriors-part-one \$\endgroup\$ – Pieter Witvoet Nov 5 '18 at 8:30
4
\$\begingroup\$

I'm having trouble visualizing what your intention here is.

Let's talk about your code itself, then I'll move on to a slight redesign:

C# has a nice-new language feature called "Expression-Bodied members", basically, this:

public int Strength => 5;

That's equivalent, but much shorter than your version:

public int Strength { get { return 5; } }

So, I would recommend moving to something like that, if you want to keep this design.

But, I would redesign this:

Everything has the same stats, right? They all need the stats, they just might have different values?

public class Stats
{
    public int Strength { get; }
    public int Constitution { get; }
    public int Agility { get; }
    public int Accuracy { get; }
    public int Intelligence { get; }
    public int Charisma { get; }
    public int Luck { get; }
}

So we're using another new language feature here: auto-implemented readonly properties. Basically: don't put code in the get, and these can only be set by the constructor. Of course, we need a constructor:

public Stats(int strength, int constitution, int agility, int accuracy, int intelligence, int charisma, int luck)
{
    Strength = strength;
    Constitution = constitution;
    Agility = agility;
    Accuracy = accuracy;
    Intelligence = intelligence;
    Charisma = charisma;
    Luck = luck;
}

Now, because everyone has their own specific stats, but they all follow the same structure, we use this class in the object:

public abstract class Entity
{
    public Stats Stats { get; }
    public Entity(Stats stats)
    {
        Stats = stats;
    }
}

So, because everybody is the same type of "Entity" with different attributes, we then move on to the Warrior / Wizard / etc.:

public abstract class Warrior : Entity
{
    public Warrior()
        : base(new Stats(5, 4, 3, 3, 2, 3, 1))
    { }
}

public abstract class Wizard : Entity
{
    public Wizard()
        : base(new Stats(2, 3, 3, 3, 5, 4, 1))
    { }
}

Now you can use the ILocatable etc. in the specific games, but the basics are all the same across-the-board. We build a more extracted model: the things that have to be there are, and are enforced.

With this part:

public interface IGame
{
    Type WarriorType { get; }
    Type WizardType { get; }
}

We can do some trickery:

public abstract class Game<TWarrior, TWizard>
    where TWarrior : Warrior
    where TWizard : Wizard
{
    public Type WarriorType => typeof(TWarrior);
    public Type WizardType => typeof(TWizard);
}

Now, I don't know why you need the WarriorType or WizardType, you probably don't need them in this version, because we put those types on the Game class.

We obviously haven't handled the TakeDamages or Shoot, right? We need to find a way to deal with that on a game-by-game basis: enter ECS (entity-component-system).

We built the entities, they have stats (components), now we need "systems" to handle the components.

public class ActualWizard : Wizard { }
public class ActualWarrior : Warrior { }

public class ActualGame : Game<ActualWizard, ActualWarrior>
{
    public class ShootSystem
    {
        public static void Shoot(Entity source, Entity target)
        {
            Console.WriteLine($"Shooting with STR={source.Stats.Strength}");
        }
    }
}

So, now you would call ShootSystem.Shoot(myWarrior, myWizard);, which would be where you handle all of the logic to actually shoot the wizard with the warrior. This also means that all entities can use the same function, whatever they may be. (Paladin, etc.)

var myGame = new ActualGame();
var myWarrior = new ActualWarrior();
var myWizard = new ActualWizard();
myGame.ShootSystem.Shoot(myWarrior, myWizard);

You would add any stats specific to warriors to ActualWarrior, same with ActualWizard, but you don't put the logic there, you build a System for it.

\$\endgroup\$
  • \$\begingroup\$ Comments are not for extended discussion; this conversation has been moved to chat. \$\endgroup\$ – rolfl Nov 2 '18 at 12:58

Not the answer you're looking for? Browse other questions tagged or ask your own question.