- Home /
Class hierarchy design and class comumication question.
Hi there.
I've read some best practices and watched videos about different patterns, but they did not give me the answer I would want to hear.
Say, I have:BaseCharacter
class which holds character movement logics and statesHumanCharacter
and AnimalCharacter
derived from the base class,EnemyCharacter
and PlayerCharacter
derived from HumanCharacter
And a couple of interfaces:ICharacter
, IHuman : ICharacter
and IAnimal : ICharacter
.
Also, I have an inventory class, weapon holder class (for managing the weapon in character's hands), an empty melee combat class (I'm going to build some melee system but I don't know if I need to make refactoring in my current system, so I'm kinda stuck for now).
The question is: how should I manage all of this?
For now my HumanCharacter
class has some gun specific properties (isAiming, isShooting, transforms that are parents for weapon equipped...), inventory and a simple parkour system. All this made by references to WeaponManager
, Inventory
and Parkour
classes in HumanCharacter.cs
. These are public properties so in example my ShootScript (instantiating bullets, raycasting, recoil) could determine if the character could and want to shoot, which is depends on his parkour state (in ShootScript:
if (Character.Parkour.State == ParkourStates.None)
and his moving state from base character class
&& Character.MoveState != MoveStates.Run)
Shoot ();
The same time I get ParkourState in HumanCharacter
to change it's inAction
state so BaseCharacter
could set it's movement settings accordingly.
The same time Parkour
have its own reference to the IHuman
interface so it could change Character's state, example:
Human.Parkour.State = ParkourStates.Climb;
Also, ShootScript
has its own reference to the character to get his current equipped weapon info, move weapon holder transform by recoil force, etc.
So, is it ok to have such a cross references? Though in first case it's a class references and in second they are interfaces, but still I don't know, am I doing it more or less in proper way or maybe I should think in more OOP/COP ways, MVC patterns, etc.
P.S.: Also, I would be glad to hear about using events. I've tried it and I fear my code could become too much non-organizable with such amount of events and their listeners here and there in every class. Should I stop to be afraid of it?
PPS: If I decide to use events, I then most likely will need access to certain properties of the character, so I would send this
in event args giving a 'whole' character object to the listener. I've read that it's not good and I should minimize any data I send to other classes. Are these opinions rightfull? Why can't I send just this
and let subscriber to determine which property of the character it wants to use?
Answer by JVene · Aug 20, 2018 at 06:42 PM
Kudos on you for thinking about your design. So many new students of Unity (rather particularly) think in terms of scripts (which isn't really a programming concept, they're text files).
While there are many purposes for classes, and you have the basic idea, a few guiding ideas may be useful. The first 'modern' computers (around the 50's forward) were programmed in a one dimension, single list of instructions. When the first languages were devised this lead to functions (C# calls them methods, but these older versions were without classes). Functions are a somewhat natural result of a CPU's ability to execute a call instruction (calling a function), which most of the earliest CPU's didn't actually have. Functions allowed for a second dimension of organization, greatly expanding the ambition of software targets while keeping code organized. The Unix and Linux operating systems are built in that way.
Objects (classes in C# and several similar languages, especially where instantiated), offer a 3rd dimension to organization of code. If you consider the one dimensional list, naturally oriented from a "top" expanding downward, functions would represent columns (the second dimension), while classes would wrap that 2d layout into pages. With the slightest clever effort, pages can become chapters, then books, the collection of books on shelves, entire libraries, etc.
There are hints for early students as to when to create a class. This is often when code is repeated. Sometimes that merely hints to a function, but where there is data associated with the concept it is a class.
As you have done so far, another means of designing classes is to encapsulate concepts. This is about all the motivation one has before code is written, but, as you've inquired, one must take care not to be trigger happy about the design. There is a significant point of intersection between what a class does and the concept it represents, but you may well find that mutates as you write the code. This has occurred to you when you inquire, for example, about events. There's no reason to be concerned about events, but there's always concern about organization of code to be readable, maintainable and efficient.
There can, and likely will, be quite a volume of objects. That should be reflective of that complexity you're trying to organize. An example comes from my point about repeated code hinting to an object or a method (function). A repeated bit of code is most likely best put, at least, into a function so there's one place to edit, expand and debug that code. There is naturally some associated parameters to that function, but when there are several variables (the function signature becomes too complicated), and especially where those variables represent grouped collection, it is likely an object. If one simply decided to start first making classes for all bits of repeated code, then the classes may not actually be organizing something significant, they may be just complicating the matter.
The opposite view of that tendency is the "uber object". In this case there's a class with a long, winding list of member variables. There are valid occasions when that is just the way a concept works, but frequently what we see in student or intermediate level code is a tendency to just dump concepts into an existing class, as if to resist creating classes because it's more work. Unfortunately the truth is that when such a mess of a class actually represents several different concepts at once, there's no organization to help future work, and while the short term convenience of merely expanding a class into some uber giant returns diminished returns. It seems clear that's not your problem, but it is common.
You've sensed there's a balance to this, which tells me your in a phase of moving upward in skill, and studying carefully. I regard that as the most important aspect of your post.
Objects (classes) function like components in a machine. If you consider a modern car, you realize there are components that fit together with clearly defined interfaces. The alternator bolts on, a belt is routed over its pulley, the wires are attached to the battery. The internal operation of the alternator is of no concern to the rest of the engine. Factually, when most mechanics are faced with a malfunctioning alternator, there is little reason to disassemble it to repair it, it is simply replaced (this was not quite the case in early cars).
When thinking about what a class should be, think about how it would function if it were part of a machine. If cars were built the way software was written in early computers of the 50's, there could be a bug in the radio that would blow a tire. That seems absurd for a real car, because it is obvious the radio has nothing to do with the tires, but where there are no separations between components that becomes a real possibility. One (of many) primary purpose of the class is to be the outer case of a component, the shell that separates it from the rest of the machine being built out of logic and language.
Such components are black boxes. They may have a few connections for wires, a button or dial to adjust (maybe several), some readouts to monitor - that is, an interface to the user (the programmer, which may well be yourself). The interior is private, which is one reason it operates like a real machine. The less one can fiddle with the interior of a component, the less that can go wrong (assuming the component works well).
Early and intermediate students tend to confuse themselves over the fact they are the only programmer involved. The classes are theirs, it's their code, so they tend to ignore the notion of private, internal parts of a class. They'll liberally write classes that read and write to each other's internals. This is because they're not thinking in terms of that interface between components of a machine. It takes a while to practice the notion, and then see why it offers leverage. This is the nature of your inquiring about exchanging "this" among classes. It isn't the exchange of "this" that is at issue. The usage if that reference is what is at issue. When a family of classes must operate with each other, they must 'know' each other (often by exchanging 'this'), but what matters is how they use that connection to communicate with each other. In particular, one must be clear about what a class does, what it encapsulates in the shell of the class, and what it should not be doing for other classes.
Unity uses a version of the entity component system, a patter of design you'll recognize from the use of GetComponent. Here, Unity leverages the simple notion of a Transform into as complex a composite object as is required. Unity students naturally adopt to this without necessarily using that system in their own classes. It is not always necessary to attach classes the way Unity does. Sometimes it is quite simply that a class has member objects, the way Transform owns a Vector3 and a Quaternion.
It is best not to overthink the obstacles before you work on the code. You can end up assuming a great deal you simply can't predict until you write the code, gain experience and subsequently might have greater vision in years to come. Instead, begin with the core relationships you've defined, consider everything you write an experimental proposition (an idea that one maintains throughout a career), and be prepared to refactor and redesign as you notice what emerges.
A pair of classes like Vector3 and Quaternion have a rather obvious relationship, and the implied Transform rather obviously emerges from their pairing. It is rare that object designs are so clear before you write. You may well have (somewhat) working code you notice is getting out of control with complexity, then after review realize you have two or three classes mashed together in some uberclass that needs to be broken apart. That may be little more than moving methods from one place to another, with a few adjustments in code that reflects their interfaces. Embrace this practice or you'll forever assume you should overthink design before you can write code.
As you can see, there's a lot to the subject, you've asked so many questions, each of which lead to a book chapter, that I probably should have suggested a few C# books, but I don't have a good list, I'm not fond of C# (I'm a C++ developer primarily, but I work in several languages as must we all). I've been a programmer/developer for over 30 years, so any language based on the C/C++/Java concepts is a quick adoption for me. You're following a path most of us recognize, and you'll need to continue study, experiment, analyze and remember what worked best, must like any engineering or scientific endeavor.
Thanks for the answer!
Yes, I'm awared of the incapsulation paradigm and am trying to use it wherever it possible, but sometimes I just can't figure out how would I make a property private and still let classes to work together. So, looks like it's just a matter of time and experience.
Also, I actually don't like the Unity's visual drag'n'drop system. I am trying not to use a lot of $$anonymous$$onoBehaviours attaching all of them. Don't know why but I like to use 'pure' C# classes to divide logic and 'view' (not long ago I realized that it would be called some kind of $$anonymous$$VC pattern :) ).
So, as you said, it's not that bad to give 'this' to other classes, unless it provide them too much freedom to switch and change internal things. That's fine. I'll be trying to secure the class from external threats, and use events for communication. Ok, now it's time to get back to work and break some eggs so I would cook an omelet in the end.
Your post was very motivating and encouraging. Thanks a lot again!
Glad you liked that. Indeed, people who learn C# from Unity do tend to assume a great deal about Unity is C#, when in reality they are writing a C# application that consumes the Unity framework.
The classic means of keeping data private but allowing classes to work with each other is either through properties or methods. While a property itself may be public, it controls both read and write access through code, which is often sufficient for control. The whole reason to make any information private is when it is part of the internal functioning of the class. For Vector3, for example, the x, y and z properties are easily accessed by code, but other properties like magnitude trigger a calculation. I often think Unity should have made the x, y, z and w properties of the Quaternion private, because hardly anyone understands what they actually mean, so reading them or worse setting them usually produces unintended results.
Consider the reason you'd be motivated to make a member variable public. Say, for example, a character may be damaged by a weapon. One might think it convenient that when the weapon registers a collision, and therefore a method in the weapon is operating, that it would be simpler just to alter the damage member of the character being struck. While that can be made to work, there's no control. The weapon may have to therefore consider the character's other attributes, like shields or health. If the damage is a property then setting damage would at least be forced to work through code of the property in order to apply damage, but if the weapon had no access to the member variables describing damage, the weapon would use a public method to apply damage. The character's member for taking on damage would be a better place to understand how the damage is to be applied, and what myriad consequences and actions may have to take place when that damage occurs (beyond mere score keeping, animations, changes to textures, loosing a limb or whatever).
Well, it sounds quite similar to how I'm planning to deal damage. For the health management I have a Stats
class which is a property of the charcter and it has a method TakeDamage (float dmg)
. I think that I would get character from a collision trigger in weapon script and call Character.Stats.TakeDamage (weaponDmg)
from the weapon script. And then calculate the damage in Stats
and trigger an event OnDamageTaken()
to inform the character himself so he could play animations etc.
Your answer
Follow this Question
Related Questions
Component-based design: Do I have the right idea? 1 Answer
How to avoid one engine class controlling the game flow getting too large 3 Answers
Modular ability creation in editor 1 Answer
grab variable from an instance of a game object? 1 Answer
How to make this script load next level after audio stops playing 3 Answers