S.O.L.I.D. in UnrealScript
- Feralidragon
- Skaarj Lord
- Posts: 223
- Joined: 15 Jul 2008, 22:41
- Location: Liandri
Subject: S.O.L.I.D. in UnrealScript
Post Posted: 15 Mar 2018, 15:16
For a while already, I mentioned and advised some folks around to read and apply SOLID principles to how they build their code in their mods.
I was only meaning to talk about this much later on, probably several months from now, after I got my hands in UnrealScript again for real, so I could present better and proven examples, but then it was mentioned that these should be elaborated now, and I think as long there's interest on this subject, doing it now may as well create an interesting discussion and get other programmers to understand, or even criticize, these principles and how to apply them in UnrealScript.
So, what is S.O.L.I.D. ?
It's an acronym which stands for 5 key principles in OOP (Object-Oriented Programming), and they were coined by Robert C. Martin, and have been proven to actually work.
Generally programmers dealing with OOP languages use these principles to guide them into using the best approach to model their classes after how they want them to work, but always in a way that the code is easily maintainable and extensible, something that does not break at the slightest touch.
These 5 principles are the following:
S - Single Responsibility Principle (SRP)
O - Open/Closed Principle (OCP)
L - Liskov Substitution Principle (LSP)
I - Interface Segregation Principle (ISP)
D - Dependency Inversion Principle (DIP)
The first thing to take in mind even before understanding any of these, these are just principles, and NOT laws, which means that while they ought to be followed, at least in my opinion, it does not mean that they can always be applied or that should be. So while ideally you should have all your code following these principles, it does not strictly mean that it has always to.
Also, keep in mind that these are not the only principles code should abide to, there is a number of other principles to take into account when programming anything: DRY, KISS, and others, which won't be touched in this topic, and there is a set of different things called "design patterns" which are just common practices which tend to follow these principles, but in a more specific way, at the implementation level.
Furthermore these principles are towards OOP languages, therefore while other languages may abide by similar principles, different language paradigms may require a completely different set of principles, and even within OOP some of these principles may be hard to downright impossible to exert, such in the case of UnrealScript itself, at least up until Unreal Engine 2.
From here, what I am going to do is to explain each one in a separate post, so each post can be linked to individually.
- Feralidragon
- Skaarj Lord
- Posts: 223
- Joined: 15 Jul 2008, 22:41
- Location: Liandri
Subject: S.O.L.I.D. in UnrealScript
Post Posted: 15 Mar 2018, 15:19
SRP is, for me, the most important principle of them all, and which, unfortunately, is blatantly violated in the Unreal Engine itself (at least the older ones).
It essentially states the following:
A class should have one and only one reason to change, meaning that a class should have only one job.
In other words, this principle states that a class should do just one specific thing.
Rather than being a Swiss Army knife, a class should only be a knife, screwdriver, corkscrew, scissors, etc, but nothing more beyond that.
If you think in terms of testing for example, if you change something like the scissors from a Swiss Army knife, not only you have to test the scissors, now you have to test all the other tools it comes with to check if they still work, if they still open, if you didn't mess up any other mechanism that made all the tools work.
However, if you had split this huge Swiss Army knife into each separate specific tool with just 1 job each, if you changed or improved just the scissors, you don't need to change or test anything on the other tools, because they are independent from each other.
Similarly, when it comes to classes, rather than building everything in one big class, this principle dictates that it's often preferable to split this big class into many smaller ones, each one doing a specific thing.
It makes no sense however to split a big class just for the sake of splitting. While a big class is often a sign of a violation of this principle, it's not always the case, all that there is to it is that a class should be responsible for just one single thing in the entire system, and the simpler the responsibility, the better.
I think the best example I can give you to show exactly this, instead of creating an abstract "foobar" kind of example using this principle, is to give you a clear example of an existing class in the engine which stands as the polar opposite of this principle: Actor.
The Actor class is what's generally called as a God class. While it may sound cool for some, it's the most terrifying way of defining and building a class, and it stands as the most massive and blatant violation of this exact principle.
God classes are classes which alone are responsible for most of the program, if not the entire program, by holding most or even all the functions/methods, most of the properties/members, etc, and which instead of having subclasses to expand with new functionality (simpler to more complex), you have subclasses only using part of the functionality already implemented in such a class (more complex to simpler), while not using the overwhelming majority of the remaining functionality, but still having all its overhead.
Why is Actor a God class?
Let's take a look at what Actor is capable of doing:
http://www.madrixis.de/undox/Source_engine/actor.html
An Actor is capable of: physics, collision, lighting, mesh rendering, texture rendering, sounds, replication, and a LOT more stuff as well.
To not even mention the methods, to do things like execute commands, triggering all sorts of events, tracing, get configuration values, iterators, rendering on the canvas, broadcast messages, give damage, and so on.
In other words, it does too much stuff in the same class, it has too many responsibilities.
And on top of it all, a lot of this stuff is not even used by most of them, such as lighting, and many actors such as Light, BlockAll, Trigger, and many others specialize themselves in using a small part of the functionality already provided by the Actor parent class, rather than being them to implement such a specialized functionality.
How would an Actor look like if it followed SRP?
Well, the best way to understand how it would look like is to look at another engine, such as Unity, which does things right on this regard, where something like lighting, is actually a light component (or class), which can simply be added or attached to another object to give it lighting capabilities.
Therefore, consider the following exemplification of how an Actor could look like if it was better designed by following this principle (along with design by composition):
Code: Select all
abstract class Actor extends Object;
//some basic properties every actor must really have, such as the 3D location in the world
private var Actor nextActor;
final function add(Actor actor)
{
if (actor == none) {
return;
} else if (nextActor != none) {
actor.nextActor = nextActor;
}
nextActor = actor;
}
function tick(float deltaTime)
{
local Actor a;
for (a = nextActor; a != none; a = a.nextActor) {
//update stuff like 3D location
}
}
Then a light could be something just like this:
Code: Select all
class Light extends Actor;
var() enum EType
{
T_None,
T_Steady,
...
T_TexturePaletteLoop
} Type;
var() enum EEffect
{
E_None,
E_TorchWaver,
...
E_Rotor,
E_Unused
} Effect;
var() byte Brightness, Hue, Saturation;
var() byte Radius, Period, Phase, Cone;
var() bool bActorShadows;
var() bool bCorona;
var() bool bLensFlare;
Even the "Light" prefixes are no longer necessary to define the names of the properties, looking far better that way, proving that Light does belong to its own class, with the sole responsibility to lit things up.
While a collision could look like this:
Code: Select all
class Collision extends Actor;
var() const float Radius;
var() const float Height;
native(283) final function bool setSize(float radius, float height);
And now you can see that now you start having actors specialized in doing a single job: Collision can only collide, while Light can only lit things up.
A Collision cannot lit things up, nor a Light is able to collide with anything, since it's not their job to do so.
However, with this approach, should a need arise that a Collision needs generate light, then a Light could just be used by a Collision class, rather than implementing light in its own code, the latter of which would make the collision actor to be responsible for both colliding and lighting, violating this principle.
And from there, you could also create your own light-weight Actor such as:
Code: Select all
class MyActor extends Actor;
function beginPlay()
{
local Light l;
local Collision c;
l = Spawn(class'Light');
l.Radius = 240;
l.Brightness = 128;
l.Hue = 72;
add(l);
c = Spawn(class'Collision');
c.Height = 8;
c.Radius = 32;
add(c);
}
As you may have noticed by now, isolating the responsibility of colliding into a separate class would also allow for a single actor to have multiple collisions.
And of course, in case of Unreal Engine in general, this could be something that you could potentially define in the default properties instead, and all the UnrealScript could be actually native code if Epic did it this way, something I think later engine iterations allow to with a specific syntax but in a similar fashion.
Having that said however, the Actor class cannot become the above, so we are all forever bound to the Swiss Army knife of classes forever in this engine, but that should not prevent you from following this and other principles in what you build for yourselves.
And this finishes the first principle, SRP.
If you have anything to add, correct or even criticize, or if you feel like a better example could be used, please feel free to provide any feedback you deem necessary.
- Feralidragon
- Skaarj Lord
- Posts: 223
- Joined: 15 Jul 2008, 22:41
- Location: Liandri
Subject: Re: S.O.L.I.D. in UnrealScript
Post Posted: 15 Mar 2018, 15:20
OCP is a principle which, luckily, is fairly well enforced by the original classes in UnrealScript, given that is what allows us to do mods far more easily in the first place, and it essentially states the following:
Objects or entities should be open for extension, but closed for modification.
In other words, once you've finished a class to serve a specific purpose, if then you need to modify its source code later, namely modifying existing methods for instance, so the class is able to do something more such as accounting for new classes which were created in the meanwhile, meaning that it became impossible to do so as is or by extending it or even by using another class, you may have just failed at following this principle.
At which point you may ask: does this mean that any source code modification violates this principle?
No.
This principle addresses only a very specific kind of source code modification, and forbids it, and which can be translated to the following: do not hard code or otherwise hard limit anything in your class.
Consider the following example:
Code: Select all
class ArmorCounter extends Actor
{
var private int count;
function beginPlay()
{
local Pickup p;
foreach AllActors(class'Pickup', p) {
if (p.Class == class'Armor' || p.Class == class'Armor2') {
count++;
}
}
}
}
This actor simply counts the number of armor pickups existent in the map, and this is the exact kind of code which violates OCP.
Why?
For instance, let's say that now you decide to extend Armor2 there to create your own armor pickup, simply because you want it to have a new skin, or a new mesh, or something else entirely, but it's still armor nonetheless.
And ArmorCounter which is meant to count the number of armor pickups in the map should be able to account for this new one as well, right?
However, as you may notice, in order for the new armor to be counted, the original class now has to be directly modified, like this:
Code: Select all
if (p.Class == class'Armor' || p.Class == class'Armor2' || p.Class == class'NewShinyArmor') {
count++;
}
And as OCP itself states, this cannot happen, given that what was just done above was a modification to the existing source of ArmorCounter just to account for an extension of another class.
This is the exact kind of required modification that this principle aims to prevent.
So, how should the ArmorCounter class be designed to follow OCP?
As you may have probably already guessed by now, it would look like something like this:
Code: Select all
class ArmorCounter extends Actor
{
var private int count;
function beginPlay()
{
local Pickup p;
foreach AllActors(class'Pickup', p) {
if (Armor(p) != none || Armor2(p) != none) {
count++;
}
}
}
}
or even:
Code: Select all
class ArmorCounter extends Actor
{
var private int count;
function beginPlay()
{
local Pickup p;
foreach AllActors(class'Pickup', p) {
if (p.isA('Armor') || p.isA('Armor2')) {
count++;
}
}
}
}
also works.
Why?
Because now we may safely extend the Armor2 class to our new one with our own shiny new skin and such, but now the ArmorCounter doesn't require any changes whatsoever to account for it, hence being closed for modification, but open for extension (whether this extension is from itself or another class).
And this finishes the second principle, OCP.
If you have anything to add, correct or even criticize, or if you feel like a better example could be used, once again please feel free to provide any feedback you deem necessary.
- Feralidragon
- Skaarj Lord
- Posts: 223
- Joined: 15 Jul 2008, 22:41
- Location: Liandri
Subject: Re: S.O.L.I.D. in UnrealScript
Post Posted: 15 Mar 2018, 15:21
LSP is a principle which the game itself pretty much follows most of the time, but it's very easy to violate from the moment you create a subclass.
It's a principle introduced by Barbara Liskov (hence the name), and it states the following:
Let q(x) be a property provable about objects x of type T.
Then q(y) should be provable for objects y of type S where S is a subtype of T.
Don't worry if you did not get that definition at all, it took me a few re-reads as well to understand exactly what it meant.
In other words, if you have a class T, and then you extend from it as a new class S (class S extends T), and if you have a function or method somewhere (it does not matter where) defined as q, which may only receive objects x of class T (function q(T x)), then it must be able to safely receive objects y of class S and have S still behave the same as T.
Thus far, this may sound like simple polymorphism, for which languages (such as UnrealScript) generally ensure type safety by enforcing that only T and subtypes or subclasses of T are allowed to be given, whenever you define a function or method parameter of being of type or class T.
However, this principle goes deeper than simple polymorphism, as it addresses a more subtle characteristic of a type or class, for which languages (such as UnrealScript too) generally are not able to enforce whatsoever, and is down to the developers to enforce it instead: behavior.
Behavior is what this principle is all about, and what it really dictates is the following: a subclass must follow the same core behavior as its parent class.
Translating this into a more UnrealScript-oriented view of this principle, consider the following class:
Code: Select all
class Counter extends Actor
{
var private int count;
function increment()
{
count++;
}
function int getCount()
{
return count;
}
function reset()
{
count = 0;
}
}
This is a simple class which acts as a counter, and which increments a count by 1 every time the increment method is called.
Then the current count can be retrieved by calling getCount, and it can be reset through reset.
And this defines the behavior of what this Counter class is meant to do.
Now consider the following class:
Code: Select all
class MyActor extends Actor
{
var private Counter counter;
function setCounter(Counter c)
{
c.reset();
counter = c;
}
function timer()
{
if (counter != none)
{
counter.increment();
if (counter.getCount() == 10) {
//do stuff
}
}
}
}
This is just a class for which you can set a Counter instance by using setCounter, and its count is reset and it's then used in the timer method, where it keeps getting incremented until its count reaches 10.
Up until this point everything is fine.
However, now consider the following Counter subclasses:
Code: Select all
class MyCounter1 extends Counter
{
function increment() {}
}
class MyCounter2 extends Counter
{
function increment()
{
super.increment();
super.increment();
super.increment();
}
}
class MyCounter3 extends Counter
{
function int getCount()
{
return 10;
}
}
Every single one of the classes above violates LSP.
Why?
The reason is simple: if instead of a Counter instance, I set a MyCounter1, MyCounter2 or MyCounter3 instance through setCounter, MyActor would be broken, given that each one of those 3 subclasses has a very different behavior than the one expected by MyActor:
- MyCounter1 prevents the count to be incremented entirely, therefore timer in MyActor will never finish;
- MyCounter2 increments in steps of 3, therefore timer in MyActor will never finish either, since getCount will never return 10;
- MyCounter3 always returns the same count (10), therefore timer in MyActor will finish right away, instead of going through a full count of 10.
Each one of these 3 behaviors completely contradicts the behavior established in their parent class (Counter), therefore will fail to meet the expectations from any class which uses them and expected not only a Counter type, but a Counter behavior as well, at the very least.
And while it could be argued that the " == 10" could be " >= 10" instead, even because increment can be called from anywhere else in the code, and not exclusively by MyActor, the point here is that increment itself, along the other methods, should not have a different behavior by itself than the one set by the class it was first defined in.
These on the other hand:
Code: Select all
class MyCounter1 extends Counter
{
function increment()
{
super.increment();
//do other stuff
}
}
class MyCounter2 extends Counter
{
function incrementBy3()
{
increment();
increment();
increment();
}
}
follow LSP perfectly, given that if any of these is given in setCounter, even though MyCounter2 could still break MyActor upon the call of incrementBy3, due to " == " instead of " >= " in the condition, the behavior MyActor expects from increment, getCount and reset is still fully honored, and that's what this principle is all about.
One possible way to ensure LSP is followed in cases like this, is to declare the methods as final, especially the ones which deal directly with private properties (such as count in this case) and are never meant to be overridden or extended in any way by subclasses, however in UnrealScript it is the case that, more often than not, non-final methods and public properties are declared, making this a very important principle to follow whenever possible.
And this finishes the third principle, LSP.
If you have anything to add, correct or even criticize, or if you feel like a better example could be used, once again please feel free to provide any feedback you deem necessary.
- Feralidragon
- Skaarj Lord
- Posts: 223
- Joined: 15 Jul 2008, 22:41
- Location: Liandri
Subject: S.O.L.I.D. in UnrealScript
Post Posted: 15 Mar 2018, 15:22
ISP is a principle which is pretty much impossible to follow in UnrealScript, at least from the get-go, given that the language itself is missing a critical feature for this principle: interfaces.
However, I believe it's still worth being mentioned and explained, especially considering that while the language itself does not have the needed feature to enable it, it's still possible to emulate the behavior from an interface in UnrealScript to some extent.
And it states the following:
A client should never be forced to implement an interface that it doesn't use or clients shouldn't be forced to depend on methods they do not use.
So, first off: what is an interface?
Many OOP languages, and also UnrealScript in Unreal Engine 3, support something called interfaces.
An interface is something close to a class, but instead it only declares functions or methods to be implemented by other classes, and defines them with no default implementation whatsoever.
Any class which implements an interface, must implement every single method declared in that interface, and a class may implement multiple interfaces.
What is the purpose of an interface?
An interface is meant to add more capabilities to a class, and do so in a way which is recognizable from other classes and functions that such a class has those specific capabilities, by making an interface act like a contract which is recognized and strictly followed by every class which implements it.
While a class defines what an object is, an interface defines what an object is able to do.
For anyone who has never heard of interfaces before, this may indeed sound very confusing to grasp at first, but it will get much clearer through some examples.
But since there's no such a thing as interfaces in UnrealScript, the following examples show an "what if" scenario in case UnrealScript did support interfaces at all:
Code: Select all
interface Describable
{
function string getDescription();
}
This interface defines just a single method to implement: getDescription, and it gives a class which implements it the capability of returning a description.
And as you can see, it has absolutely no code within the body of the method itself, only the declaration, because it cannot and is not meant to have one, this method is only meant to be implemented in classes themselves.
Now let's say that I intend to create a projectile subclass which has a description of its own, so I implement this interface in it, as such:
Code: Select all
class MyAwesomeProjectile extends Projectile implements Describable
{
function string getDescription()
{
return "This awesome projectile is awesome.";
}
}
By saying that I am implementing the Describable interface (through implements Describable), I am now forced to actually implement the getDescription method the interface declared, and from there on I can retrieve a description from this projectile class.
Easy enough, right?
But we could as easily not implement the interface at all and just declare and implement the same method in this new projectile class, right?
Indeed.
However, the power of an interface is revealed when you want another completely unrelated class to also be able to return a description.
For example:
Code: Select all
class MyPawn extends Pawn implements Describable
{
function string getDescription()
{
return "This is just a normal pawn, meh.";
}
}
As you can see, I just created a pawn subclass above, and made it also implement the Describable interface.
And thus far this still looks rather useless, right?
So now, the real power is revealed: what if the only thing I care about any class whatsoever, within a function or method, is whether or not it has a description?
Code: Select all
class Broadcaster extends Actor
{
function string broadcastDescription(Describable obj)
{
broadcastMessage(obj.getDescription());
}
}
In the above example, broadcastDescription is set to accept any object which implements the Describable interface, and as you can see, here Describable is now used as the type of obj, just like a class, in order to allow this:
Code: Select all
function doStuff()
{
local SomeActor someActor;
local MyAwesomeProjectile proj;
local MyPawn p;
...
someActor.broadcastDescription(proj);
someActor.broadcastDescription(p);
}
Although MyPawn (as p) is completely unrelated to MyAwesomeProjectile (as proj), since they implement the same interface, which is the interface expected to be implemented by whichever object is given to broadcastDescription, the code would compile and work with no issues whatsoever.
And this finishes what an interface is.
Getting back to the principle itself (ISP), it's only about how interfaces should be defined.
In most, if not all, OOP languages which support interfaces, classes may implement multiple interfaces at the same time, they are not limited to just one.
However, an easy mistake to make, and which this principle aims to prevent, is the declaration of too many unrelated methods in a single interface, for example:
Code: Select all
interface TeamDescribable
{
function string getDescription();
function byte getTeam();
}
This interface adds the capability to a class to both retrieve a description and a team.
While this sounds like an useful interface to apply to classes which are team based and have a description, ultimately a description has nothing to do with a team, therefore classes for which only a team makes sense to be returned would still be required to implement getDescription as well, even if to just return an empty value (empty string), and vice-versa.
However, by segregating this interface into 2 separate ones, like this:
Code: Select all
interface Describable
{
function string getDescription();
}
interface Teamable
{
function byte getTeam();
}
allows each class to only implement what they really aim to implement.
This way, a class which only has a team, but no description, would only need to implement the Teamable interface above, like so:
Code: Select all
class SomeTeamActor extends Actor implements Teamable
{
function byte getTeam()
{
return 1;
}
}
while a class which only has a description, but no team, would only need to implement the Describable interface above, like so:
Code: Select all
class SomeDescriptionActor extends Actor implements Describable
{
function string getDescription()
{
return "This is my description.";
}
}
while a class which has both a description and a team, can implement both the Describable and Teamable interfaces above at the same time, like so:
Code: Select all
class SomeTeamDescriptionActor extends Actor implements Describable, Teamable
{
function string getDescription()
{
return "This is my team description.";
}
function byte getTeam()
{
return 1;
}
}
and in the example given above with the Broadcaster class, which receives an object which implements the Describable interface in the broadcastDescription method, it would accept both SomeDescriptionActor and SomeTeamDescriptionActor objects since they implement that interface, but would deny (in compile time) SomeTeamActor objects since they do not implement the expected interface.
Similarly, a function which only accepts Teamable objects, would accept both SomeTeamActor and SomeTeamDescriptionActor, and deny SomeDescriptionActor.
And this finishes the forth principle, ISP.
If you have anything to add, correct or even criticize, or if you feel like a better example could be used, once again please feel free to provide any feedback you deem necessary.
In a last note, as first mentioned above, while UnrealScript does not have the ability to create interfaces, which would be extremely useful on many levels, hence being the cornerstone of many OOP languages nowadays, they may effectively be emulated to some extent by using classes and some mechanisms to dictate type safety and "isA" relationships.
While it would never be comparable to the real thing, both in performance and powerfulness, it would still open the doors to a more powerful style of programming, and could potentially solve many issues concerning dependency management, package mismatching (in a way) and enable classes to have the same set of capabilities without being necessarily inherited from the same parent class, although the way to do it approximates more to "design by composition" than actual interfaces, but a similar principle would still apply.
However, given that such a subject falls outside of the scope addressed here, and the fact that this very post is already a quite lengthy read as it is, I may address it in another separate topic someday, depending on the overall level of interest on this specific subject.
- Feralidragon
- Skaarj Lord
- Posts: 223
- Joined: 15 Jul 2008, 22:41
- Location: Liandri
Subject: S.O.L.I.D. in UnrealScript
Post Posted: 15 Mar 2018, 15:24
DIP is a principle which is somewhat followed in UnrealScript, in some cases more, and in others less, and it states the following:
Entities must depend on abstractions not on concretions.
In other words, a class should not depend on another specific concrete class, and rather it should depend on either an abstract class or an interface.
While interfaces are often preferable for this principle, and while I already explained what an interface is in the previous principle, I will focus this explanation on abstract classes instead given that's what can already be done in UnrealScript.
So, first off, abstract vs concrete classes, what is the difference?
An abstract class is a class which is not meant and cannot be instantiated (spawned) given that it's a class which is meant to represent a base, representing a broader but specific kind or set of classes.
And while it may have a base implementation of some methods and some properties, it generally has methods which are meant to be implemented by subclasses, as the real implementation and usage of such a class is done by extending it into something more concrete: a concrete class.
For example: Decoration, Projectile, Pawn, these are all abstract classes, which by themselves have no substance and aren't usable, as they only represent decoration classes, projectile classes and pawn classes respectively, but no decoration, projectile or pawn in specific.
While a concrete class is a class which is meant to be instantiated (spawned) and it has a specific implementation, as it represents a specific thing which is meant to have an active role in the code or program.
They generally implement what an abstract class has set to be implemented by their subclasses, but are not necessarily required to extend from an abstract class.
For example: the Barrel class is a concrete implementation of the Decoration class, as it represents a barrel, which is something very specific, but it belongs to a bigger and more abstract group of things called decoration, hence being an implementation of Decoration.
What this principle addresses is what the code should always depend on, and it states that the code should depend on abstract, and not on concrete.
Why?
Let's take the following example:
Code: Select all
class RedTeamChecker extends Actor
{
function bool isSameTeam(byte team)
{
return team == 0;
}
}
This class does a very concrete thing: it has a method isSameTeam to check if a given team corresponds to the red team.
Now, let's say we have the following:
Code: Select all
class Door extends Actor
{
var private RedTeamChecker teamChecker;
function setRedTeamChecker(RedTeamChecker team_checker)
{
teamChecker = team_checker;
}
function bool canOpen(byte team)
{
return teamChecker.isSameTeam(team);
}
}
This class represents a door, which has a method canOpen to check if a given team can open it or not, and that decision is retrieved from a team checker actor, which this class depends on, which in this case is the RedTeamChecker class, which is a concrete class which only checks for the red team specifically.
So, what's the problem with this?
This is answered with another question: what if I want the door to be able to be opened by the blue team instead?
Or any other team, like green or yellow/gold, or even any other at all that doesn't exist yet?
Well, the obvious answer would be "just extend the RedTeamChecker class into a BlueTeamChecker class and override the isSameTeam method", right?
However, the problem with that approach is that it violates the Liskov Substitution Principle (LSP, the third principle), since a RedTeamChecker should clearly always be a check concerning the red team, and never any other team since the setRedTeamChecker clearly does not expect a class to validate any other team other than red.
Ok... then how about "create a new BlueTeamChecker on the side extending from Actor instead, with similar code, and add a new method setBlueTeamChecker to Door"?
While LSP wouldn't be violated this way, it would violate another principle instead: the Open/Closed Principle (OCP, the second principle), since this means that the Door code would require to be changed to account for every new variation of a team checker class.
Either way, by making Door depend on a concrete class, in order to account for any new changes and extensions, other principles would be violated, and the only way to not violate them is to not change the code at all and keep it concrete and immutable. Which means you're screwed.
So, let's see now an example using an abstraction instead:
Code: Select all
abstract class TeamChecker extends Actor
{
function bool isSameTeam(byte team);
}
Here an abstract TeamChecker class was declared, and, as you can see, the isSameTeam method has no implementation whatsoever.
This is because this class represents a team checker in an abstract way, and for this class it doesn't matter what specific team it needs to check, that's the responsibility of a concrete class to extend it and implement such a concrete detail.
From there, now if we structure the Door class this way instead:
Code: Select all
class Door extends Actor
{
var private TeamChecker teamChecker;
function setTeamChecker(TeamChecker team_checker)
{
teamChecker = team_checker;
}
function bool canOpen(byte team)
{
return teamChecker.isSameTeam(team);
}
}
While it will never actually receive an instance from the TeamChecker class itself, it can receive and accept anything which is a concrete implementation of it.
And this is what depending on an abstraction is.
From here, concrete classes could be created to extend TeamChecker in order to be used with the Door, like so:
Code: Select all
class RedTeamChecker extends TeamChecker
{
function bool isSameTeam(byte team)
{
return team == 0;
}
}
class BlueTeamChecker extends TeamChecker
{
function bool isSameTeam(byte team)
{
return team == 1;
}
}
Any of them could be given as team checkers to a Door, without violating any of the other principles.
And it would be easy to create more team checkers for other teams, even for completely new teams which do not yet exist in the game, and this is why classes should always depend on abstractions, and never on concretions.
And this finishes the fifth principle, DIP.
If you have anything to add, correct or even criticize, or if you feel like a better example could be used, once again please feel free to provide any feedback you deem necessary.
- Buff Skeleton
- >:E
- Posts: 4175
- Joined: 15 Dec 2007, 00:46
Subject: Re: S.O.L.I.D. in UnrealScript
Post Posted: 17 Mar 2018, 01:15
- Sat42
- Skaarj Warlord
- Posts: 995
- Joined: 14 Jul 2013, 16:42
- Contact:
Subject: Re: S.O.L.I.D. in UnrealScript
Post Posted: 18 Mar 2018, 11:06
Waffnuffly wrote:It's tarydium-doped smoothies. Drunk by the player, I mean. The player is tripping balls. The whole game actually takes place in a large city and the player thinks he's on an alien world.
- Jet v4.3.5
- Skaarj Elder
- Posts: 1247
- Joined: 24 Dec 2007, 17:40
- Contact:
Subject: Re: S.O.L.I.D. in UnrealScript
Post Posted: 27 Mar 2018, 01:13
Writeup on Template Design Pattern
- Function in base class (probably using 'final' keyword). This function always does what the base type and sub-types should do when called. It also calls the function in the next item...
- An empty function in the base class that any sub-class can override. This function is always called virtually from the 'final'-ized function defined in the base class. You can then augment the original functionality of the 'final'-ized function with expanded code in this overridden method. It also removes the need to call 'super.*' methods explicitly in overrides as is sometimes done.
Code: Select all
class Counter extends Actor
{
var private int count;
final function increment()
{
count++;
incrementExtension();
}
final function int getCount()
{
return count;
}
final function reset()
{
count = 0;
resetExtension();
}
function void incrementExtension() {/* do nothing in the base class*/}
function void resetExtension() {/* do nothing in the base class */}
}
class MyCounter4 extends Counter
{
// override template method
function void incrementExtension()
{
Console.Log("Counter Incremented");
}
// override template method
function void resetExtension()
{
Console.Log("Counter Reset");
}
}
You can add whatever code you need in place of the logging calls.
- Feralidragon
- Skaarj Lord
- Posts: 223
- Joined: 15 Jul 2008, 22:41
- Location: Liandri
Subject: Re: S.O.L.I.D. in UnrealScript
Post Posted: 29 Mar 2018, 14:51
This one ended up being quite a challenge to explain in the context of UnrealScript (since there's currently none), unlike my initial estimate, and it ended a bit more lengthy than the others, which is also why it took a lot longer to post.
Having that said, only one principle is left to explain, and is probably the easiest one to explain, so it will come soon.
_______________________________________________________________________________________
Concerning the template design pattern, it is probably the most used pattern across the game, even because there's generally no good base alternative due to the lack of some critical OOP features (like interfaces and abstract methods).
However, the template design pattern has severe problems of its own, which come from mixing up implementation with the definition of methods to implement, which range from accidentally "breaking" the class, to be extremely confusing and overwhelming to read up the class just to know which methods are meant to be implemented, besides looking ugly as hell ("incrementExtension" instead of just "increment" is an eye-sore in my opinion, and even a code smell I would say).
So, there's an alternate pattern I came up with, which separates working code from methods to be implemented in the most clean way possible.
I say that "I came up with it", but that's only because I thought it up on my own, however it's possible that there's already a name or a set of names for it somewhere (I am actually pretty sure there is), but at least I didn't find any pattern that fits what I did upon my own search (maybe you guys know it and can enlighten me in that regard ).
This pattern consists in what I call a "component" and a "prototype": the "component" is the class which has all the code that defines that class, and it is with instances of such a class that you actually "work" with, by calling its public methods and whatnot, just like a normal component class.
A "prototype" however actually configures and "implements" that "component", meaning that all the methods for implementation are defined in that prototype.
Then, upon a "component" instantiation, you just provide the "prototype" you want it to use (as an instance or the class itself), and that "prototype" then shapes up the "component" from within, without the "component" itself looking any different from outside when used in any given context.
Maybe this is just a variation of the "facade pattern" or the "proxy pattern", with "dependency injection" thrown in, maybe something else entirely, I am not quite sure to be honest, but I am yet to find someone using something like this, and it worked so damn well it's not even funny, because it solves a ton of problems at the same time: guarantees isolation, you can use the same method names for the same things in both, you can have multiple prototypes for the same component (imagine an "input" component like I have in my PHP framework, and then each prototype is a specific type of input, like integer, boolean, etc), and you can even throw in the "factory pattern" as well by making the component a potential mini-factory by using prototype "names" instead of instances or classes.
And the best thing, while I originally did this in PHP, due to the nature of it, it also fits UnrealScript like a glove, and I intend to use it there as well once I get to build UnrealScript stuff again.
Probably one of these days I may make a post about this to show with examples exactly what I mean.
- Buff Skeleton
- >:E
- Posts: 4175
- Joined: 15 Dec 2007, 00:46
Subject: Re: S.O.L.I.D. in UnrealScript
Post Posted: 29 Mar 2018, 17:46
One reason I could see is replication -- it's much easier to handle client/server replication when all the core functions are already present on both machines in all actors. Having to invoke a separate object and then transfer that to all clients (when it can't be simulated) might add some overhead to a network transfer that needs to be as quick and simple as possible since it's an FPS where every ms counts.
With the LSP, would replication be impacted by having separate objects for every sort of customization, or am I not understanding it fully and it's actually a non-issue?
- Buff Skeleton
- >:E
- Posts: 4175
- Joined: 15 Dec 2007, 00:46
Subject: Re: S.O.L.I.D. in UnrealScript
Post Posted: 29 Mar 2018, 17:55
- Feralidragon
- Skaarj Lord
- Posts: 223
- Joined: 15 Jul 2008, 22:41
- Location: Liandri
Subject: Re: S.O.L.I.D. in UnrealScript
Post Posted: 30 Mar 2018, 02:47
Buff Skeleton wrote:I have to wonder about the SRP vs. Actor god-class architecture now. Why do you think Actor was written this way? Inexperience? Or was there maybe a legit reason for it?
One reason I could see is replication -- it's much easier to handle client/server replication when all the core functions are already present on both machines in all actors. Having to invoke a separate object and then transfer that to all clients (when it can't be simulated) might add some overhead to a network transfer that needs to be as quick and simple as possible since it's an FPS where every ms counts.
You may want to check Unity on that regard.
Their replication works in a very similar way as in Unreal Engine, except that everything that has to do with replication is also handled by separate components which you just add to components you want to add replication capabilities to.
As for the reason why Tim did it that way, I think the reason is simply because it was simpler and faster to do it that way back then, and even maybe they didn't really know any better, given that we're talking about a time where the number of programmers was severely smaller than it is today, so there was much less scrutiny over writing bad code, and there wasn't this level of widespread information, principles and patterns to go by.
Even I only started to hear about all of this stuff a few years ago, several years after I started programming, even at a professional level.
From there, in later iterations of the engine he either didn't want or perhaps didn't have the luxury to choose to just rebuild the engine, and proof of that is even in Unreal Engine 4, where many of the base C++ structures and classes you have present in Unreal Engine 1 are still there, so UE4 is not that different from UE1 at its core.
But there is evidence that at least someone at Epic noticed this problem and so in Unreal Engine 2 or 3 (if I recall) they actually created something close to "components" that you could add to actors and such, as if they were trying a way to at least follow SRP to some extent with those.
They even tried to separate some of the weapons functionality into different actors in UT2003/UT2004 for example.
However, while SRP is a principle that should be followed and solves a lot of problems concerning extension, maintenance and overall memory-efficiency (and even CPU if well built), it has a trade-off: it's more complex to bootstrap from and it requires a different mindset on what "adding functionality" means, as you're building a lot of small lego pieces first to then assemble them together in a bigger construction, rather than molding the construction from the get-go in clay as a single big entity with a lot of details to it.
And sometimes building the bigger construction with some clay instead of little lego pieces is just easier and works, the problem comes later however when you have to change or at least affect whole chunks of it just to change small tiny things, or to build a different construction all together, and by then is too late and you realize you should have done it with lego.
And judging by the fact that UnrealScript has classes with several thousands lines of code, and even "states", I think this represents very well the kind of mindset that was being used to build all of these.
Here Tim just went with pure inheritance, by assembling what he probably believed to be common enough traits to define an Actor, and then just using some of them in subclasses.
And truth is, Actor is actually not that terrible of a class, but it does way too much nonetheless, making the most of what it offers completely useless in the context of many of its subclasses.
In terms of replication and other common functionality, they are no different than a class that only handles collision or lighting, they only represent a single thing each.
But make no mistake: that's not how replication works in this engine.
All the actors and such that you possibly need already exist in both the server and clients, this is actually a requirement for replication to even work.
Then, when you need to spawn an actor, and replicate it across the network, the engine is not replicating its entire structure, it only replicates something akin to an "event" providing just enough identifiers to each client on what class they need to instantiate (spawn), and then the client itself spawns the actor on its own in the same way it would spawn in an offline game, and from there, only data that has changed is replicated and applied (like properties set in a way that they differ from their defaults), and that's why replication is rather efficient.
Therefore, with SRP it would be no different in that regard: all the classes and such that you need, are already with you as a client, the server doesn't have to send them over again.
What would happen with SRP however, is that the server might need to notify the clients to spawn a greater number of actors for example, however the amount of data would still be essentially the same as each actor would have less properties to change in the same proportion.
With SRP, you could still have replication embed into the main actor itself, as being a feature of the engine and UnrealScript itself, which technically wouldn't violate this principle.
But even if replication was a different class altogether, and actors didn't have any replication by default until you actually attached a replication object to them, you would still end up pretty much with the same level of efficiency, because you're just moving where the code runs from, but still affecting the same things and having a similar flow.
As a matter of fact, under SRP, the entire replication framework could be optimized to a point where it could become far more efficient than what it already is.
How? By deciding which actors actually need replication or can simply be spawned in the client in their own, or even be shared and only start actual replication through replicate-on-write mechanisms.
But this is probably a subject for another day since this reply already got way too big already.
So, bottom line, replication could be something written on the side as its own class or set of classes which could then be applied to actors you want to have replication capabilities on (or even any object really), just like Unity does for instance, and instead of loosing performance, this would actually open the doors for massive optimizations to take place.
Buff Skeleton wrote:With the LSP, would replication be impacted by having separate objects for every sort of customization, or am I not understanding it fully and it's actually a non-issue?
In terms of LSP, it only addresses a specific issue: a subclass betraying the core functionality of its parent class.
Violating that principle generally means broken classes which should not be broken.
Customization should be made through extension, in a way that follows LSP, meaning that the core functionality should be intact, therefore LSP doesn't impact replication negatively at all, it actually prevents it from breaking in the first place.
It essentially dictates that if object A behaves in a certain way, and object B extends from it, then no matter what sort of customization is made in B, it still has to behave the same as A.
This guarantees that, in replication for example, if A replicates things in a certain way, B must also replicate the same things in the same way at the very least, and may simply replicate more things in a different way.
It's all about extending without breaking.
- Buff Skeleton
- >:E
- Posts: 4175
- Joined: 15 Dec 2007, 00:46
Subject: Re: S.O.L.I.D. in UnrealScript
Post Posted: 30 Mar 2018, 04:08
- Feralidragon
- Skaarj Lord
- Posts: 223
- Joined: 15 Jul 2008, 22:41
- Location: Liandri
Subject: Re: S.O.L.I.D. in UnrealScript
Post Posted: 30 Mar 2018, 13:39
One of these days I may write about replication as well. I once started a series of tutorials at ut99.org several years ago, but I never continued them.
Replication is actually a LOT simpler than it looks at first, but all guides and tutorials there are, are quite cryptic at best, and complicate something which is actually much simpler to explain.
But it's something which is better explained with pictures or even a video showing what's going on there, and there does not seem to be any of the sort yet.
Also, sorry for my long ass reply above.
As I scrolled down I noticed how freaking huge my reply was, and it must have been tiring reading all of that. I probably could have made it a lot shorter, holy damn.
Who is online
Users browsing this forum: No registered users and 3 guests