I am making a dungen crawler game. I want several character classes for both player characters and enemies. Currently, my class hierarchy looks like this:
and I have run into a problem. Whenever I decide to add another player class I need to edit a bunch of code to include the added class. Is there a way to bypass this? For example: All players would have PlayerController script on them, and the player’s behaviour would be based on some enum variable…
I was thinking of something like this:
public enum PlayerClass {KNIGHT,RANGER,WIZARD,...,}
public class PlayerController : Character
{
public PlayerClass playerClass = PlayerClass.KNIGHT;
void Awake()
{
switch (playerClass)
{
case PlayerClass.KNIGHT:
someGenericTypeVariable = new Knight();
}
case PlayerClass.RANGER:
someGenericTypeVariable = new Ranger();
}
}
//later I would call class-specific behaviours like someGenericTypeVariable.Skill1();
but I don’t know if that’s even possible. Any ideas?
Or to recap: How can I have multiple player and enemy classes without editing a bunch of my code whenever I add new characrter class?
You are not using the inheritance model to your advantage. First, I would have something like this:
Actor (abstract)
Player: Actor
NPC: Actor
The character types of Ranger versus Wizard etc, I would handle as configurations of properties/components of either an Player instance and/or NPC instance (not sure if you can play all types of characters or let them be NPC’s).
I would implement all common behavior in the Actor class, and only implement behavior specific to Players or NPCs in their respective classes. and I would use virtual methods implemented at the Player and NPC class level to handle differences in behavior between Players and NPCs.
Doing all this will allow you to eliminate your switch statements and will dramatically reduce or even eliminate the code you need to write to add a new class.
That is what I’m doing. The difference is: I have player-specific behaviour in PlayerController class and ‘class’-specific behaviour in Knight, Ranger etc. and that is leading to all those switch statements.
As I mentioned the class specific behavior can be implemented as configured properties and components on an instance of either the player class or the NPC class. This configuration can be done in the inspector and then each character class instance saved as a prefab.
Then, for example, there is no need to have a switch in the awake function to initialize the instance based on its class characteristics. Each prefab instance will already configured to be a particular class. Then when you want to create a new class, you configure it and save it as a prefab.
When building the specific characteristics for a wizard or whatever in game, then all you are doing are setting specific properties on an instance of a wizard prefab.
Another benefit of doing this is that when it comes to saving/serializing player and NPC instances, you are only saving and reinitializing based on a small amount of data.
Lastly, fundamentally if you ever end up with switch statements in a OOP hierarchy, or worse downcasting to detect type, then the model is surely flawed.
Do you mean having every behaviour for every class in PlayerController class?
If so, this leads to some variables not being used for a given character class. For example: let’s say we have a Knight who has only melee attacks, and a Mage who spawns projectiles. Now, I assume we put the projectile variable as public and set the reference in the inspector. However, Knight does not need such variable. Wouldn’t it bother you to have several (or maybe many) unused variables in each prefab’s inspector?
I thought about this scenario when I wrote my first replies, but decided to leave off it. I’m of two minds on this, and not knowing anything about your game, I’ll give you both opinions in the context of ranged weapon character versus a magic wielder. Regarding melee combat, I think that is a behavior that nearly all character classes could have in some fashion and therefore would implement the behavior and properties high up in the OOP hierarchy, and then disallow it where needed.
Back to my example, if magic wielding is fairly simple, you could treat magic wielding and firing a range weapon with the same methods and just have the difference be simple configuration. Essentially shooting or casting a spell would be something like prepare to fire/cast, fire/cast (if ammo/manna is sufficient), cooldown. If you fire a range weapon, you fire a prefab that implements, say a crossbow bolt. If your a magic wielder, you cast a fireball prefab. Both hit something and potentially do damage. Maybe the crossbow bolt is just a simple mesh (or sprite) and the fireball is a particle system.
On the other hand, if magic wielding is a more complex endeavor, say you have to learn spells, and have a spellbook, can only cast a spell every so often etc. Then I would probably have a more complex OOP hierarchy where a magic wielder is a separate class, and which itself contains the logic for handling magic wielding.
I do agree that you also want to avoid having class variables that are extraneous for a particular character class. In a simple OOP approach it won’t be possible to avoid this. But you could avoid this also by taking a component-based approach. A player, or an NPC, can have an attached weapon system component (which implements a ranged weapon’s behavior and properties). A mage has an attached magic system component (which implements casting spells). Every character class that can engage in melee combat has a melee combat component. The base player or NPC class queries its installed components to know what it can do, and thus what it is. If you took this approach, the character creation system would essentially spit our a prefab based on a whole bunch of behavior selections, traits and properties which are implemented as attached components.
Another reason I like this kind of component approach is that you have your character classes mostly defined in data rather than code. In a really good implementation, you could get to the point that a character class can be added without even recompiling. And then this opens your game up to data driven modding. I am certainly not saying that this would be easy to do, but it certainly would be elegant. If it were my game, it would make this a long term goal and start with something simple along the configured component approach rather than the detailed OOP model approach.
Lastly, I still believe that a basic OOP approach has a lot of value. There is a lot of common behavior that could be implemented in that structure and propagated through inheritance.