Resolved How to make these classes work.

gjaros

Member
Joined
Aug 27, 2022
Messages
5
Programming Experience
3-5
This is something I had to write (to make something work, and it did) and it made me cringe.

C#:
string advancement = obj.name == "Player" ? obj.GetComponent<Player>().config.Advancement : obj.GetComponent<Enemy>().config.Advancement;

What I would like to be able to do is something simple like:

C#:
string advancement = obj.config.advancement.

Player and Enemy are separate classes. The config member is a class instance of EnemyConfig and PlayerConfig (both named "config"), respectively. Both EnemyConfig and PlayerConfig inherit from the class Config.

The thoughts behind my design so far and background:
  • This is a game I'm working on in Unity.
  • Player and Enemy have a lot in common as far as state (config) but not much else. Their behavior is completely different. I would like them to inherit from a common class too but I don't know how I would implement that with this config issue I'm having.
  • Config has two purposes.
    • Hold state and remove clutter from the classes that instance it.
    • Is assigned by using NewtonSoft's Json Convert to make objects easily configurable.
  • With those two points in mind I would like to keep Config even though the Player/Enemy class could just hold all those members themselves.
Here is my code, simplified:

Configs:
public class Config
{
    public string Name { get; set; }
    public State Stats { get; set; }
    public string Advancement { get; set; }
}

public class EnemyConfig : Config
{
    public List<string> Skills { get; set; }
    public Characteristic Characteristics { get; set; }
    public Movement Moves { get; set; }
    public Behavior Behaviors { get; set; }
}

public class PlayerConfig : Config
{
    public List<Hotkey> Hotkeys { get; set; }
}
Enemy Class:
public class Enemy : MonoBehaviour
{
    public EnemyConfig config;   

    private EnemyMovement movement;
    private EnemyAttack attack;

    //Stuff
}
Player Class:
public class Player : MonoBehaviour
{
    [SerializeField] public Camera playerCamera;

    public PlayerConfig config;

    private PlayerMovement movement;
    private PlayerAttack attack;

    public Health health;
    public Madra madra;
    private Inventory inventory;

    public readonly Vector2 castPoint = new Vector2(1.0f, 1.0f);
    
    public List<SkillCast> skillCasts;
    public List<Buff> buffs;

    private readonly float sightCooldown = 0.5f;
    private float sightCooldownTimer = 0f;
    
    //Stuff
}

How should I approach this using anything C# has? I've looked around at abstract classes, inheritance, interfaces but I don't know how it should be done.
 
What's the type of obj?

Anyway, if you really don't like:
C#:
string advancement = obj.name == "Player" ? obj.GetComponent<Player>().config.Advancement : obj.GetComponent<Enemy>().config.Advancement;

You could do:
C#:
var config = obj.name == "Player" ? obj.GetComponent<Player>().config : obj.GetComponent<Enemy>().config;
string advancement = config.Advancement;

Anyway, as a general rule of thumb, if you are needing to determine the type of an object to determine what action to do, it's a code smell that you aren't make full use of polymorphism and that Strategy Pattern maybe a good fit.
 
On thinking some more, you could do something like:
C#:
interface IHaveConfig
{
    public Config Config { get; }
}

:

public Player : MonoBehaviour, IHaveConfig
{
    public Config Config { get; } = new PlayerConfig();
    :
}

public Enemy : MonoBehaviour, IHaveConfig
{
    public Config Config { get; } = new EnemyConfig();
    :
}

:

string advancement = null;
if (obj is IHaveConfig haveConfig)
{
    advancement = haveConfig.config.Advancement;
}
 
What's the type of obj?

Anyway, if you really don't like:
C#:
string advancement = obj.name == "Player" ? obj.GetComponent<Player>().config.Advancement : obj.GetComponent<Enemy>().config.Advancement;

You could do:
C#:
var config = obj.name == "Player" ? obj.GetComponent<Player>().config : obj.GetComponent<Enemy>().config;
string advancement = config.Advancement;

Anyway, as a general rule of thumb, if you are needing to determine the type of an object to determine what action to do, it's a code smell that you aren't make full use of polymorphism and that Strategy Pattern maybe a good fit.

They are plain C# classes. The Monobehavior doesn't factor in with what I'm trying to do.

Maybe you could tell me how to make full use of it (polymorphism)? Again, I'm looking to leverage something about C# that I'm unaware of to make this work. I want to refactor these classes (Player, Enemy, and the Config) so that I can just get config from either of them without getting an error about Player.config != Enemy.Config like in the example....

C#:
string advancement = obj.config.advancement.

I see what you did, it's a small breakdown of the problem but it doesn't cut out the verboseness. I want these objects to be comparable so that I don't get an implicit conversion error.
 
I want these objects to be comparable so that I don't get an implicit conversion error.
Making options comparable doesn't automatically impart automatic implicit conversions.

To make thing comparable, you can implement IEquatable and/or IComparable, but all that does is just let you do comparisons.

If you want implicit conversions, you'll need to declare them.
 
They are plain C# classes.
That doesn't answer the question. What is its type? You must have declared a type for it. Or if you used var to let the compiler determine the type, you can hover over the variable and Visual Studio will tell you what type the compiler thinks the type is.
 
As aside, there an error in my post #2. This:
C#:
var config = obj.name == "Player" ? obj.GetComponent<Player>().config : obj.GetComponent<Enemy>().config;
should be:
C#:
Config config = obj.name == "Player" ? obj.GetComponent<Player>().config : obj.GetComponent<Enemy>().config;
because your original code had two different PlayerConfig and EnemyConfig. But since both have a common base class, I use polymorphism and just ask for their common base class.

Anyway, in my post #3, I also use polymorphism via interfaces.
 
Or as another alternative to accessing the config in post #3:
C#:
string advancement = ((IHaveConfig) obj).Config.Advancement;
 
Doing some quick experiments, the following seems to work:

C#:
using System;

var player = new Player();
var enemy = new Enemy();

var name = true ? player.Name : enemy.Name;
var setting = true ? player.Config.Setting : enemy.Config.Setting;

Entity entity = true ? player : enemy;
name = entity.Name;
setting = entity.Config.Setting;

class GameBase
{
}

abstract class Entity : GameBase
{
    public string Name { get; set; }
    public abstract Config Config { get; }
}

class Player : Entity
{
    public override Config Config { get; } = new PlayerConfig();
}

class Enemy : Entity
{
    public override Config Config { get; } = new EnemyConfig();
}

class Config
{
    public string Setting { get; set; }
}

class PlayerConfig : Config
{
}

class EnemyConfig : Config
{
}

Pretend that GameBase is MonoBehavior.
 
On thinking some more, you could do something like:
C#:
interface IHaveConfig
{
    public Config Config { get; }
}

:

public Player : MonoBehaviour, IHaveConfig
{
    public Config Config { get; } = new PlayerConfig();
    :
}

public Enemy : MonoBehaviour, IHaveConfig
{
    public Config Config { get; } = new EnemyConfig();
    :
}

:

string advancement = null;
if (obj is IHaveConfig haveConfig)
{
    advancement = haveConfig.config.Advancement;
}
This is what I was looking for! I knew an interface was probably what I needed but as I toyed around with it I got frustrated because I didn't know to implement it this way. I appreciate the help! I'll marked this solved.
 
Doing some quick experiments, the following seems to work:

C#:
using System;

var player = new Player();
var enemy = new Enemy();

var name = true ? player.Name : enemy.Name;
var setting = true ? player.Config.Setting : enemy.Config.Setting;

Entity entity = true ? player : enemy;
name = entity.Name;
setting = entity.Config.Setting;

class GameBase
{
}

abstract class Entity : GameBase
{
    public string Name { get; set; }
    public abstract Config Config { get; }
}

class Player : Entity
{
    public override Config Config { get; } = new PlayerConfig();
}

class Enemy : Entity
{
    public override Config Config { get; } = new EnemyConfig();
}

class Config
{
    public string Setting { get; set; }
}

class PlayerConfig : Config
{
}

class EnemyConfig : Config
{
}

Pretend that GameBase is MonoBehavior.
Okay, it doesn't quite work. I'm getting this error, where config doesn't have a definition for one of the EnemyConfig members. How do I modify the interface so it works?

The screenshot attached shows all the relevant scripts and where the error is happening.
 

Attachments

  • Capture.PNG
    Capture.PNG
    263.5 KB · Views: 10
Eating dinner right now. Look up down casting.
 
In the future, post code, not screenshots. Yes, the screenshot maybe useful if there is an error you are trying to highlight, but the code and error (in code tags). Like now, I'd like to give you the fix, but I'll have to keep flipping between the screenshot and this edit box to try doing that. If you had posted the code in code tags, I could have just copied the relevant line and pasted it in my reply.
 
Using down casting you would do something like:
C#:
if (((EnemyConfig) config).Behaviors.WhenHealthLow.Contains("stationary"))

Down casting is usually good when there are just one or two locations that you need to go from a base class to a derived class. For your case where you are more than likely going to need it often, it'll be better to do something like:
C#:
public class Enemy : MonoBehaviour, IConfig
{
    public EnemyConfig { get; private set; }
    public override Config => EnemyConfig;

    :

    public void Start()
    {
        EnemyConfig = ... // whatever work you need to do to get the initial EnemyConfig object.
        :
    }

    :
}
 
Using down casting you would do something like:
C#:
if (((EnemyConfig) config).Behaviors.WhenHealthLow.Contains("stationary"))

Down casting is usually good when there are just one or two locations that you need to go from a base class to a derived class. For your case where you are more than likely going to need it often, it'll be better to do something like:
C#:
public class Enemy : MonoBehaviour, IConfig
{
    public EnemyConfig { get; private set; }
    public override Config => EnemyConfig;

    :

    public void Start()
    {
        EnemyConfig = ... // whatever work you need to do to get the initial EnemyConfig object.
        :
    }

    :
}
Perfect! I was trying to add casting but I didn't do it like you did. Now it works!

Thank you so much for all your help. I'm so glad there are people like you out there willing to spend their leisure time helping others. :)
 
Back
Top Bottom