I've switched Zity from using OOP and inheritance to a component based architecture. This means that there hasn't been a whole lot of new content, but a lot has gone on under the hood with how Zity is programmed. Basically I've been working my way BACK to where I was a month ago using this new architecture and thought it's been quick, I don't have much to show yet. What I can do, however, is explain OOP and components in case anyone is interested.
Allow me to shed some light on the subject.
Object Oriented Programming (OOP)
OOP is a programming methodology (rules basically) based around this concept of objects. An object can be anything in your game/program that exists by itself. For instance in a game like Zity I would have Car, Player, Zombie, Gun, and Bullet objects. Each of these self-contained 'things' is an object and would like this in code:class Car{
public string Model { get; set;}
public int Year { get; set;}
public int TopSpeed { get; set;}
public float CurrentGas { get; set;}
// And so on...
}
Each object is responsible for all of the data (information) about itself. So in the previous example the car object contained information about it's make and model, it's capabilities, and how much gas it currently has. A zombie object might contain health information, how much damage it does, and what items it's carrying.
As an example think of a tree. What is this tree made up of? Well it's got leaves, branches, colors, a height, an age, thickness, roots, and maybe nests (but nests themselves might be it's own object). All of the things I mentioned could/would be properties in a tree object. You think of the properties as something the object is made of.
In OOP each object is different but some objects may share similar data. For instance my Zombie object has health information (a number from 0 through 100). My Player object would also need health information (again, 0 through 100) in the same way the zombies do. Instead of programming this functionality for both the Zombie and Player separately, we use something called inheritance to let them both copy the functionality.
In inheritance an object can 'inherit' functionality and properties from another object that it derives from. Derives is a just a fancy word meaning that one object can link to another object, kind of like how you've got powdered doughnuts which are 'derived' from regular doughnuts (they just have powder on them). What this means is that since my Zombie and Player need health they mine as well share that functionality and derive from another object I'd create called Creature. The Creature object would have all the information that EVERY Creature (Human, Zombie, Animal) needs, such as health, stamina, damage, and a position. My Player and Zombie objects would then inherit, derive, from my creature object and both gain the same properties and methods without me having to re write the same code for each object.
Here is my example in C#
class Creature
{
public int Health {get; set;}
public int Damage {get; set;}
public int XPosition {get; set;}
public int YPosition {get; set;}
public int Stamina {get; set;}
}
class Zombie : Creature
{
//Zombie now has all the properties of Creature..
//.. and can still implement it's own specific properties (Decay, etc..)
}
class Player : Creature
{
//Player now has all the properties of Creature..
//.. and can still implement it's own specific properties (Hunger, etc..)
}
Now that the Zombie and Player have each inherited from Creature they are free to implement the details specific to their individual objects.
So OOP seems great right? You can even have objects inherit from each other multiple times and end up with a big list like this: Sprite, Creature, Humanoid, Player; where Player inherits from Humanoid, Humanoid inherits from Creature, and Creature from sprite. The system seems fairly easy at first and it makes sense in our brains as we've always thought of real life objects in a similar way. The trouble comes when inheritance gets really extended. Suddenly you have functionality you're duplicating in an object because you can't inherit from the other object that has it since you don't want what that object inherits from and AHHHH. It can get really messy really quick if you aren't careful
This is where Component architecture comes in.
Component System
I had originally been using standard OOP on Zity and managed to make it work despite it's short-comings and general code bloat (although I'm no expert in OOP anyhow). As I went about working on the semi-block editor however (see previous post), I hit a series of road blocks stemming from my objects inheritance. My main issue was that each of my map objects (couches, fridges, light switches, lamps) all needed additional info particular to themselves and while inheritance can do this it was becoming a nightmare with duplicated functionality and me needing to cast my objects and determine what type they were every time I wanted to change them.I had heard about component architecture from my research into Unity and other engines in the past but I had never tried implementing it myself. I decided to give it a go however as my current architecture was simply driving me insane. So over the course of a week or two I came up with some ideas and some base classes (stole some ideas from other engines) and looked at some pseudo code before coming up with my own component based system.
How (my) Component Systems Work -
There is no single, perfect way to create a component system and there are a lot of opinions and ideas out there about which one is best, but the one I opted for was the most basic and straightforward. Remember the objects from before? In component programming you have to think of them differently than in OOP.
Instead of thinking of properties (information) as being a part of an object, you have to think of our real life objects (Car, Zombie, Player) in terms of what they are made up of and what they do. Imagine an empty bucket with the title Zombie on the top and another empty bucket next to it with the title Player. Now next to these sheets of paper you have a big pile of small note-cards and on each one is written something that the Zombie and/or Player would need. Taking from our earlier example there would be a note-card with the word Health written on it. This note card represents all the properties and functionality needed to access and manipulate Health data.
So you take this Health card and make a quick copy, placing each one into the Zombie and Player buckets. Now each of these buckets contains information on Health that can be accessed by pulling the card out and looking at it. So you continue by adding in a Position object that let's us know where the Zombie and Player are. You then realize you want your zombies to have something your players don't need, AI (Artificial Intelligence). So you pull out the AI card and only throw that one into the Zombie bucket. What you've just done is given each object, Zombie and Player, different functionality based on what they need. Taking this functionality away is as simple as grabbing the note-card out and disposing of it and adding more functionality in just takes throwing a note-card into the bucket. This is the core of component design.
In OOP the properties, such as the Health information, would be coded right into the object's class file. This would mean that if we had a bunch of different properties such as AI, Rendering, Physics, Input and so on, they would all be put right onto each object. The downside to this is that you are locking in each object with certain functionality. There is no flexibility, you can't just remove health information from the player or change out an AI module. There's hacks to get around this limitation but for the most part they are just a cheater form of component systems.
With components, objects do not have any direct properties or functionality related to the game. All they are is buckets waiting to be filled. So instead of having each object be locked in to the properties you gave it, the objects can add and remove properties at will through the components. The way you accomplish that is by creating a base object called the GameObject. This object only has functionality it needs to add/remove/update/and communicate with it's components.
An example GameObject:
class GameObject
{
private List<IComponent> Components;
public void AddComponent(IComponent c)
{
Components.Add(c);
}
public void RemoveComponent(IComponent c)
{
Components.remove(c);
}
public T GetComponent<T>() //The T is the type of component
{
foreach(IComponent c in Components)
{
if(c.GetType() == typeof(t))
{
return (T)(object)c;
}
}
return default(T);
}
public void update()
{
foreach(IComponent c in Components)
{
c.Update();
}
}
}
All the GameObject is responsible for is managing it's components, making sure they're been updated, passing along events to each component that wants them. The components themselves can be thought of as little blocks of properties and functionality. For instance if I wanted my Player to participate in physics I would need to create a Physics component and attach it to him. The Physics component would be responsible for updating itself and any other components the Player has that would want to be updated by it (such as position).
To accomplish this the first thing that I need to do is create a Manager class, in this case a PhysicsManager. My PhysicsManager has all the methods in place to add new objects into it's simulation and remove them at will. The manager will update each physics object and then run it's simulation every frame. All my physics component needs to do is add itself to the PhysicsManager and it would be up and running. If I wanted an object that happened to have a weird shape and I needed to account for that, I would simply create a new Physics component to accomadate that shape.
Some example components:
PhysicsRectangle,
PhysicsCircle,
AIAggresive
AIPassive
SpriteRenderer,
TileRenderer,
Position,
Health,
ArmoredHealth,
These should give you an idea of how I can modify each component to fit my needs. If I want my Zombie to stop being aggressive and to run away I can just swap out AIAggressive with AIScared (or something of the like).
The key thing is that each component needs to handle itself. It uses the update method to do any modifications it needs to and it uses global information and managers to accomplish whatever it must.
Here is an example of an IComponent class (the base component that all components inherit from)
abstract class IComponent
{
//A property that links back to the GameObject that..
//contains this component.
public GameObject Owner;
//All these methods can be overriden by the components..
//.. that inherit IComponent to provide functionality.
public virtual void Init()
{
//Allows the component to do some setup stuff
}
public virtual void OnCollision(GameObject with)
{
//Will be called whenever a GameObject's Physics..
//.. Component collides with something.
//Useful for doing damage and other modifications.
}
public virtual void Update()
{
//Called once per frame for the component to do..
//.. any updating it needs.
}
}
The virtual methods are there for each component to override if need be. They are what allow communication between components. I have a few more virtual methods on my own base class to handle when a new component is added or an existing one removed.
Here is an example of a component inheriting from IComponent.
class CirclePhysicsComponent: IComponent
{
private PhysicsBody Body;
public PhysicsComponent(int radius)
{
//Create a new body based on the radius parameter
Body = new BodyFactory.CreateCircle(radius);
}
public override void Init()
{
//Hook the PhysicsBody into the simulation.
PhysicsManager.AddBody(Body);
}
public override void Update()
{
//If the Owner of this component has a Position object we..
//.. want to update it.
Position c = Owner.GetComponent<Position>();
if(c != null)
{
c.X = Body.Position.X;
c.Y = Body.Position.Y;
}
}
}
Keep in mind all this code is in C# and is as basic as I could make it. My current setup has more complete components and some methods and properties different than how I explained here. This should be a good starting point though if anyone is interested in components.
This was the last of my catch-up posts so now I'll be moving into a groove of a new post every weekend (most likely Sunday afternoons) detailing what I've accomplished (or haven't) that week. Thanks for reading!
No comments:
Post a Comment