Jump to content

Rick's Blog

  • entries
    65
  • comments
    96
  • views
    26,334

Top 10 celebrities who use component architecture!


Rick

4,233 views

 Share

The last blog I posted was Nov 2013. The blog section seemed stale for for a week or so so thought I'd share a change in design I recently did for our Dead Anyway game to spark conversation and ideas.

 

Our player script was just getting too massive and was doing too many different things directly inside of it. Adding features or modifying existing features was scary as hell. All the "helper" variables were adding up and the amount of hunting for what I needed in the script was pissing me off. So I decided to re-look at component architecture. After putting this into practice it's really makes programming more fun and changing things a lot easier and less fearful.

 

The first task was to look at this monster player code and break it down into domains. That meant looking at the high level functionality.

  • Play sounds
  • Take input
  • Camera controls
  • Movement controls
  • Inventory
  • FPS arms
  • HUD
  • Handle stats like health, hunger, thirst, etc

All of this was happening right in 1 script. Not cool. So I first hand to break all this functionality into their own scripts. However all this functionality works off each other. Even though they are separate when to do what and code X needs to know stuff about Y still exists. So if we want to break code out into it's own domain (and scripts) and reduce coupling on each other to avoid making fragile code AND they need to know about each other in various ways how would I do that?

 

Enter events. I treat each component as it's own little library that is designed to do it's one specific thing related to the game. So it of course has functions that act on it's data. But how would those functions get called? They would get called when events from other components were raised. So now a component has events to tell the outside world that something happened inside this component and functions to act on this components state. It doesn't care how it's functions get called and it doesn't care who is listening to it's events. It's blind to the outside world.

 

An example is the PlayerSound component. It loads sounds in it's init function and then it has functions like WalkForward(), WalkBackward(), StopWalking(), StrafeLeft(), StrafeRight(), Jump(), Eat(), Drink(). These functions simply play the right sounds. The PlayerSound code doesn't care about the input code and when you're in the PlayerSound script you don't either. Your mindset is just all about sound code at that point. You're thinking about what functions do I need for sound, and what possible events should I fire that have to do with sound? The sound script right now is about 100 lines of code (but it will grow in the future). It's nice and contained as it's own component.

 

Every component is like this. PlayerInput converts actions to events. It doesn't care who uses those events. It also has functions to map keys to actions. Who is calling those functions? Who cares. It's not my concern when I'm in coding the PlayerInput component. I just know I want to do that functionality at some point.

 

So how does this all get wired up you may ask? That's the interesting part. Your player script, instead of being an if nested disaster, simply becomes configuration code of linking component events to component functions (I call them actions). I noticed something interesting happen when I did this. Looking at the player script revealed what it's doing much simpler. It provided a nice high level overview of what the player is doing at a glance. I didn't have to read state variables and loops and branch statements to figure it out. I saw it all revealed to me. I feel like it works better with our brains. We think, when this happens do x, y, z. That's the component architecture with events exactly. That's all you see 1 line at a time. When this happens do this. Very little interpretation is needed.

 

Here is an example of what my player script looks like now:

 

PlayerLooting.onLooting:Subscribe(PlayerHUD, PlayerHUD.ShowProgressBar)
PlayerLooting.onLooting:Subscribe(PlayerCamera, PlayerCamera.DisableUpdate)
PlayerLooting.onLooting:Subscribe(PlayerController, PlayerController.DisableUpdate)
PlayerLooting.onCancelLooting:Subscribe(PlayerController, PlayerController.EnableUpdate)
PlayerLooting.onCancelLooting:Subscribe(PlayerCamera, PlayerCamera.EnableUpdate)
PlayerLooting.onCancelLooting:Subscribe(PlayerHUD, PlayerHUD.HideProgressBar)

 

 

The PlayerLooting component has an event onLooting and onCancelLooting and those other components are subscribing to those events and telling those PlayerLooting events what functions they could call when that event is raised from within the PlayerLooting component.

 

You can plainly see the HUD shows the progressbar and the camera and controller controls are disabled. If we cancel looting we hide the progressbar and the controls are enable again.

 

Want to add a sound when looting? Just think about how easy that is to think about. No hunting for the right section and state in the old giant player script that is 1000+ lines. You simply:

  1. Add a loot sound variable and load a loot sound inside the PlayerSound script (you know exactly where to do this. if it's a sound it's done in PlayerSound component. easy right?)
  2. Make a function in PlayerSound to play said sound
  3. Make a function in PlayerSound to stop said sound
  4. Link the onLooting event to the PlayerSound play looting function
  5. Link the onCancelLooting event to the PlayerSound stop looting function

It's much more compartmentalized than the traditional design of putting all that stuff into the player script.

 

 

Here is the event code that I use:

 

if EventManager ~= nil then return end

EventManager = {}

function EventManager:Create(owner)
local obj = {}

obj.handlers = {}
obj.owner = owner

for k, v in pairs(EventManager) do
obj[k] = v
end

return obj
end

function EventManager:Subscribe(owner, method)
table.insert(self.handlers, { owner = owner, method = method })
end

function EventManager:Raise(args)
for i = 1, #self.handlers do
self.handlers[i].method(self.handlers[i].owner, self.owner, args)
end
end

 

 

Inside a component to create an event you simply do:

 

-- note I just wanted to use Event but Josh exposed that for his UI stuff so I ended up with EventManager instead
self.onmoveForward = EventManager:Create(self)  

-- events have a table that is an argument that will get passed to the function subscribed to events
-- interestingly enough because it's a table, it's passed by reference so inside your action function if
-- you change a value the component that raised the event can read that to perhaps act accordingly.
-- I do that in a few instances
self.onmoveFoward:Raise({ shiftKeyDown = false })

 

Then to subscribe to the event in the player script where all the components come together you simply call the Subscribe() function on said event passing in the component object and the component objects function/action to get called when that event is raised:

 

PlayerInput.onmoveFoward:Subscribe(PlayerSound, PlayerSound.PlayMoveFowardSound)

 

My future for this is to have an editor that reads all component events/actions and visually allows you to hook up events to actions. Then the player script simply reads the output file of said editor to hook things up. Then we'd have a nice visual of how our game works at a high level.

 

This was a long one but I'm really excited about making the switch. It was an easy transition to make and it makes adding/changing functionality really simple, and it gives me a high level overview of what's going on with the player which was increasingly getting more complex.

 

If you read this far on a weekend then go outside :)

  • Upvote 8
 Share

16 Comments


Recommended Comments

Very interesting reading. I really like your idea, especially with the events. Im using a similar approach by having some classes act as every generators where classes can register them self as event listeners. The listeners then inherits abstract event interfaces with methods like 'onMouseClick' ... If understood your system correctly we are more or less having the same approach.

Link to comment

Component design really doesn't use inheritance at all. The idea is inheritance is less flexible and causes more issues. Components are about composition.

 

What does your player class look like? This generally is a good test since the player can generally do more than anything else in a game.

  • Upvote 1
Link to comment

I'm probably missing something. You have a component that can act upon its own data by methods called from the outside world. The component can also send events to the outside world (the user of the component). How is this done? Either by callbacks or listeners that have a method that can handle (act on) such an event. Is there a third option? That sounds interesting. I'm using a combination of LUA and C++. The event-components are in C++ and uses inheritance, goes something like this (a simple fictional sample to illustrate how I'm doing it). Is there a better way to do this I'm certainly willing to adapt that into my code.

 

 


//  inherits this to be a listener for MouseInput events
class MouseEventListener
{
public:
   // called when use left clicks mouse
   virtual void onMouseLeftClick() = 0;
   // called when use right clicks mouse
   virtual void onMouseRightClick() = 0;
};
// A mouse input component
class MouseInput
{
   vector<MouseEventListener*> _listeners;

   MouseInput() {}

   // register a new listener
   void register( MouseEventListener* listener )
   {
    _listeners.push_back(listener);
   }

   // unregister a  listener
   void unregister( MouseEventListener* listener )
   {
    remove( _listeners.begin(), _listeners.end(), listener );
   }

   // must be called frequently
   void update()
   {
    if( Window::GetCurrent()->MouseHit(0) )
    {
	    for each ( auto listener in _listeners )
	    {
		    listener->onMouseLeftClick();
	    }
    }
    if( Window::GetCurrent()->MouseHit(1) )
    {
	    for each ( auto listener in _listeners )
	    {
		    listener->onMouseRightClick();
	    }
    }
   }
   // reset the mouse
   void reset()
   {
    Window::GetCurrent()->FlushMouse();
   }
};
class Player:
   public MouseEventListener
{
   // Mouse input component
   MouseInput _mouse;

public:
   // MouseEventListener
   void onMouseLeftClick()
   {
    // handle left mouse clicks
   }
   void onMouseRightClick()
   {
    // handle right mouse clicks
   }

   // Player
   void init()
   {
    // start listening to mouse
    // you can stop doing this by
    // _mouse.unregister(this)
    _mouse.register(this);
   }

   void update()
   {
    _mouse.update();
   }
};


Link to comment
You have a component that can act upon its own data by methods called from the outside world. The component can also send events to the outside world (the user of the component). How is this done?

 

Generally by events or messages being broadcasted to all other components and the components who care about those messages do whatever they need to do with them to change their internal state and/or raise events/messages.

 

To be clear the player is a game object not a component. The game object has a collection of components and it really only acts as a place to configure how the components interact.

 

Also component design really shines when things get complicated and you have a lot of code. So with your small example we have to imagine a lot since there isn't a lot of functionality but we can do that.

 

So the question I ask about your example is what is the player class doing with mouse input? Why does it need to know that in the intimate way that it does (inheritance, even though you're using it as an interface it's still inheritance)? Your comments in your mouse functions seem to suggest the player class itself will act on the mouse input. What is it going to do with it? Is the reason you didn't show me your actual player class because it's doing a lot of stuff in it and too big and would be confusing for someone to follow? If that is the case, and you don't mind, I'd love to see it to perhaps explain why component design could help it.

  • Upvote 1
Link to comment

Aaah... Good questions Rick. Now I understand where you are going. Very very nice approach. To answer you question about the mouse events (and all other events) I can confirm that I'm an idiot :D :D. As its now events are caught by the player and used for controlling other components. That might be needed in some rare cases, but what you are saying is of course a much much better way of doing it. Lets say the MouseInput send events about mouse movement (onmousemove..). Those events could as an example be used to control the camera rotation. So instead of catching the mouse movement event in the Player code and rotate the camera (or call some Camera-object method), its better to have a Camera-Object that catches the MouseEvents directly. The player does not have to be involved in this. Am I on the right track here Rick??

 

Regarding my Player its like you Player was before doing the cleanup you are describing... which means some 1000 lines of code. To much to describe here and of course a BIG hint that I also have to make some cleanup by using Components. Thanks for you interesting view on this. I really appreciated it.

 

My player .cpp is far to big and complex to share here. However I can show the header file and as you see its to complex now. You can see the events handlers for various events (the methods starting with on.....). I will go through this as see what I can come up with

 

class Player :
   public GameItem,
   public GameTimerClient,
   public ToolBarListener,
   public GameActorClient,
   public AreaClient,
   public PickDetectorClient,
   public CollisionDetectorClient,
   public CarryListener
{

   const float WalkSpeed = 1.5;
   const float MaxWalkDistace = 6.0;
   const float WalkDecay = 0.1;
   const float MouseSense = 15.0;
   const float MinPitch = -60.0;
   const float MaxPitch = 60.0;
   const float PitchSmooth = 4.0;
   const float PitchSpeed = 20.0;
   const float Turnsmooth = 4.0;
   const int   HealthSpeed = 2;
   const int   HealthAdd = 2;
   const int   HealthDec = 1;
   const float DoubleClickMsec = 400;
   const float JumpForce = 8;
   const int   StartDelay = 4;
   const float CameraFOV = 70;
   const float CameraMinRange = 0.5;
   const float CameraMaxRange = 500;
   const float EyeHeight = 1.8;
   const int   MaxCarryWeight = 20;
   const float CarryDrop = 2;
   const float CarryMaxDiff = 0.5;

   //  --- LUA
   UserInterface* _userinterface;						  
   std::string _pick_decal_material;						  
   std::string _pick_decal_red_material;
   //  --- INTERNAL
   bool _is_started;
   long _startdelay;
   bool _firstTime;
   Leadwerks::Vec3 _start_pos;
   Leadwerks::Font* _bigFont;
   bool _wait_for_left_mouse_release;
   Leadwerks::Listener* _soundlistener;
   Leadwerks::Sound* _music;
   Leadwerks::Source* _soundsource;
   GameTimer _timer;
   Leadwerks::Entity* _pointed_at;
   Leadwerks::Entity* _socket;
 //  --- Health
   int _cur_health;
   long _healthTimer;
   std::vector<int> _healthDec;
   //  --- Messages to user
   MessageWindow _msgwin;

   //  --- Health & Toolbar
   HealthBar _healthbar;
   ToolBar _toolbar;
   //  --- Cursor
   Cursor  _cursor;
   //  --- Picking items
   PickDetector _pick_detector;
   //  --- Colliding with things
   CollisionDetector _collision_detector;
   //  --- Carry things
   GameItem* _carriedItem;
   Leadwerks::Vec3 _carryrotation;
   Leadwerks::Vec3 _carryposition;
   int _carry_collsion;
   float _lastMZ;
   void grabItem(GameItem* gi);
   void carryItem();
   void dropItem();
   // --- Tool
   Leadwerks::Pivot* _toolhandle;
   // --- FlashLight
   FlashLight _flashlight;
   // --- Camera
   Leadwerks::Camera* _camera;
   Leadwerks::Vec3 _camera_rot;
   float _cameraPitch;			
   Leadwerks::Vec2 _mousediff;
   void _on_started();
   void _pick_detection();
   void _triggerhandler(const Leadwerks::PickInfo& pickinfo);
   void _checkpointhandler(const Leadwerks::PickInfo& pickinfo);
   // Mouse double click
   bool _leftclicked;
   long _lastclick;
   // Walking
   float _move;
   float _walkdistance;
   bool _walking;
   Leadwerks::Vec3  _walkstart;

   // Screen size
   int _scx;
   int _scy;
   void updateCam();
   void updateMove();
   void die();
protected:
   void onCollision(Leadwerks::Entity* e, const Leadwerks::Vec3& pos, const Leadwerks::Vec3& normal, float speed);
   void onUpdateWorld();
   void onUpdatePhysics() {}
   void onPostRender(Leadwerks::Context* ct);
public:
   Player(Leadwerks::Entity* entity);
   ~Player();				
   //  --- GameItem
   void init(LuaBridge& lb);
   void onRestore();
   void onactivated();
   //  --- GameTimerClient
   void onMsec100();
   void onSec();
   void onMinute();
   //  --- ToolbarClient
   void onToolbarFull(ToolBar* tb);
   void onToolbarAdded(ToolBar* tb, Tool* t);
   void onToolbarSelected(ToolBar* tb, Tool* t);
   void onToolbarUsing(ToolBar* tb, Tool* t);
   void onToolbarShown(ToolBar* tb);
   void onToolbarHidden(ToolBar* tb);
   void onToolbarSelectChange(ToolBar* tb, Tool* t);
   //  --- HealthClient
   void onZeroHealth(Health* h);
   void onWarnHealth(Health* h);
   void onOkHealth(Health* h);
   void onFullHealth(Health* h);
   //  --- GameActorClient
   void onActorAtStart(GameActor* a);
   void onActorAtEnd(GameActor* a);
   void onActorDone(GameActor* a);
   void onActorPicked(GameActor* a);
   //  --- AreaClient
   void onAreaEnter(Area* a);
   void onAreaLeave(Area* a);
   //  --- CarryClient
   void onCarryGrabbed(Carry* item);
   void onCarryDropped(Carry* item);
   //  --- PickDetectorClient
   bool onPickedHealth(Health* item);
   bool onPickedTool(Tool* item);
   bool onPickedCarry(Carry* item);
   bool onPickedEnergy(ToolEnergy* item);
   bool onPickedActor(GameActor* item);
   bool onPointingAt(GameItem* ent);
   //  --- CollisionDetectorClient
   bool onCollidedHealth(Health* item);
   bool onCollidedTool(Tool* item);
   bool onCollidedCarry(Carry* item);
   bool onCollidedEnergy(ToolEnergy* item);
   bool onCollidedActor(GameActor* item);
   bool onCollidedArea(Area* item);
   // Inherited via CarryListener
   virtual void onCarryCollision(Carry * item, const Leadwerks::Vec3 & pos) override;
};

Link to comment
The player does not have to be involved in this. Am I on the right track here Rick??

 

That's correct. The player class, which is a game object, shouldn't care about those events because ideally the player class isn't handling them. It wouldn't be controlling the camera. Instead it would have a camera component inside of it and the camera component is the class that creates the camera and gets the events it needs (mouse input) and fires events that other components may be interested in.

 

Your player class then becomes the central hub where you add the components it needs and hook up the events between them.

 

The good news is your comments on functions in your header class help show you the different domains your player class is doing. Those are good hints on how to break out components. Each component should be it's own domain (group of similar logic). So in your case:

 

I don't know what Toolbar is but I can tell it should be it's own component because you have a fair amount of functions supporting it. Health should be it's own component. Carrying it's own component, pick and collision it's own component as well.

 

You are in the exact same boat I was in, and I'm sure a lot of people are in, and I'm telling you when you refactor to use the component design it's like a lightbulb goes off and it becomes fun to make changes/additions because it becomes so easy.

 

Step 1 would be split your domains into their own components. Just create a component class for that specific domain. Now when you're staring at this empty class (PlayerCameraComponent for example), just think of it as it's own library that doesn't know anything else about the other components in your game. It's job is to deal with the camera period. Anything that has to do with the camera happens here. This should create the LE camera entity and then act on that camera entity. What functions does it need to do that? What events should it fire off so outside components can use that.

 

Actions generally are easier to think about at this time. You generally think about another components events when inside a different component and you find yourself saying "I need to know when the camera is in this state". That's your trigger that tells you the event the camera needs. You can right then and there go to your camera class and add the event and fire it when it needs to be fire. Done! It's that simple. You worry about hooking it up later then. It's so nice to be able to do that because it's kind of how we think.

 

Now, the question is how will you do messaging. With events you'd have an event class (I made one a long time ago in C++, I think you mentioned at one point you've used it before) or you do what unity does and you send messages from within a component to the game object.

 

If you go the route of events (I like this way as it's less wasteful and mapping becomes centralized which helps see at a high level what's doing on):

 

// notice how you don't event have a player class in this case. you don't need it as a game object is just a collection of components and the components do ALL the work so no need for a player type class.
GameObject* player = new GameObject();

CameraComponent* camera = new CameraComponent(player);
player->AddComponent(camera);

InputComponent* input = new InputComponent(player);
player->AddComponent(new InputComponent());

// hook up the event
input->onmousemove.Subscribe(camera, CameraComponent::RotateCamera);

 

 

 

 

If you go the route of SendMessage();

 

class Component{
  GameObject* owner;
  public Component(GameObject* o) : owner(o){}
};

class CameraComponent: Component{
  public CameraComponent(GameObject* o) : Component(o){}
}

class GameObject{
  list<Component*> components;

  void SendMessage(msg, args){
     foreach(auto c in components){
        c->ReceiveMessage(msg, args);
     }
  }
}

 

It's been awhile since I've done C++ so I probably screwed syntax up but you get the idea.

 

While the send message approach seems simpler I don't like it. In my view components shouldn't be left to map messages to functionality. For one it takes all that mapping and segments it in different places. Making it hard to see a 10,000 foot level of the interactions that make up that game object. Having it centralized really does help see how your game object behaves at a high level which is handy. I also look at components as separate libraries and it's cleaner if they just raise events and have functions that work on it's internal state. Event args is how you can share details about it's internal state. If you need to know the mouse position inside the camera's Rotate function that is hooked to the input's mouse move event, then the argument (handle this like .NET does where your functions take specific argument classes) can pass that data. So we know that the function inside the camera class should have a parameter of MouseArgs class. The camera class doesn't care how it got that information though right. It just knows it got it and it uses it.

 

The good news is you have all the functionality already. You just need to get it organized better and component design helps with that. So refactoring this stuff is fairly simple.

 

Avoid this trap: I noticed I had an issue with naming my component functions very specific the events I knew they would be hooked up to. Try to avoid doing that. Component functions (actions) should be named for what they do internally to that component. The events are named to what they do. So don't name the camera function MouseMove(). That doesn't make any sense when you think about the camera functionality itself. What's really happening is that the camera is rotating. We know that we're going to do that rotation with the input component but put that out of your head because really anything could call that function and pass in the parameters it needs. Anything!

 

Why is that important? Well, the side benefit of doing all of this is your components are now easier to test. You can do unit tests if you want. In your tests you make an instance of that 1 component, and start calling it's functions passing in whatever data you want to see if you get the right result (examine it's internal state or did an event get called when it should have). That in itself is huge and opens up an entirely new door which will make your code way more stable and make changes way more trustworthy that you didn't screw something up. Generally you'll build up tests and after a change you run them all at the same time and if they all pass you have confidence that you didn't screw anything up.

  • Upvote 1
Link to comment

Thanks Rick. You have really opened my eyes to this new concept. Also many thanks for taking the time to more or less type down a mini tutorial. Now with this new concept in mind I will start to break the player into components. I will keep you guys informed on how that goes, either here or in my blog. Reading this blog post by you Rick almost gave me religious experience, so thanks again

Link to comment

FYI, each component generally will have an Update() function to it and can be calls in the game objects Update() function.

 

Like a lot of things in life not everything works perfectly. Generally you don't want to have an event that is raised every frame inside the Update() function of a component, but when you break out the controller and camera you'll find the controller needs the Y rotation value from the camera so the best way to do this without making them aware of each other would be to have the camera fire and event in it's update every frame where it passes as it's argument it's Y rotation value. Then have the controller component subscribe to that event so it can take that Y rotation value and store it off to an internal variable so it can use it inside it's Update() function when you pass it to SetInput(). Ideally events aren't raised every frame but this would be an exception.

 

 

Also I would say have your GameObject take an LE entity and store that as well. Since your components gets a reference to the GameObject it can then have access to the LE entity. The Controller component would need this since it's calling functions on that entity like SetInput().

  • Upvote 1
Link to comment

The religious experience for me was when I went to add functionality and how easy it was.

 

1) The class component I added the functionality was JUST that. It wasn't all this other **** that isn't about the functionality I needed to add like when you have the massive player class. This was a huge relief and minimized searching and fear that I'd break something else. Not having to worry about the state of all this other ****.

 

2) Seeing all my events to actions hooked up in 1 spot showed me how my game worked at a high level. This was an amazing realization. The high level idea of my game didn't really even need comments. You could easily see how it worked with the events assigned to actions. You don't get this when you have a giant player class. You have to go hunting for functions but then they are all over the place and figuring out the high level of what that player class does is actually really difficult.

 

When you have all the event hooks in one function you can simply go down line by line (no loops/if's/function calls to follow) and just say what you see on that line and that tells you the functionality that exists for that player/game object. I was shocked by that. That was incredible. So easy for someone else to pick up quickly or you when you revisit something 6 months from now.

  • Upvote 1
Link to comment

Just because I like talking about this I'll give my mindset with component design when I need to add a new feature so others can see the thought process.

 

In our game the player can get drunk. That's the feature. Now we start breaking out how to add that feature to the game.

 

First, this instantly becomes it's own component I call PlayerDrunknessComponent. It manages what getting drunk means. I then think about if this was a library on it's own how would I handle it. Well I would have a Drink() function that's for sure. But maybe different drinks affect how fast you get drunk. So I would probably pass an argument to the Drink() function that tells me what the drink is. I can now test this on it's own which is nice.

 

I then think about what would I like this component to tell the outside world? Well the functionality I want is that we have different drunk levels. Maybe 0-5. Every 2 beers raises the drunk level by 1. So I should probably tell the outside world every time the drunk level changes so I'll have an onchangeDrunkLevel event.

 

So now I have a way to get drunk with my Drink() function and I can tell the outside world about my drunkness. At this point the PlayerDrunknessComponent is completely self contained and ignorant about the outside world.

 

Then I start thinking about what will call this Drink() function? Well my inventory component has an onUse event that passes the item that is trying to get used. That seems like a great way to get drunk right? So I hook up the inventories onUse event to my Drink() function. Now I may have to go back and refactor the params since events will send 2 arguments in my case (the sender of the event and 1 args table that holds other data specific to that event).

 

The interesting thing is how is the onUse event of the inventory getting called? I don't care right now. I just know it's called when an item in the inventory is right clicked because that's what I deemed use to be. So all I care about is that the event exists and gets called and passes the item trying to be used and that meets what I need for this.

 

Interestingly enough I have a Thirst and Hunger component that hooks into onUse as well. The Thirst component has a Drink() function as well that's hooked into onUse. It checks if the item being clicked was a drink and if so it decreases our thirst and sets one arg to true which will tell the inventory component to delete that item once it comes back from raising it's onUse event. That works out great because when I drink alcohol I want my thirst to be reduced too!

 

So this is how I get drunk. I right click an item in the inventory and if that item has alcohol (a check inside drunkness component against the item in question) I can get drunk. But just setting a drunk level isn't really doing anything. What shows the player they are drunk?

 

Well let's randomly rotate the camera to show the head bobbing around. Let's screw around with the controller strafe value so even though the player is moving forward we are adding random strafe values to represent staggering around. Then let's add some shader blurring effect too!

 

By talking about what we want we already see what components need to be affected. When we made those components we weren't thinking about the drunk feature so we probably didn't put any functions for that. So let's go into each component and add function(s) that will handle this. Again, we can test these out in a unit test if we want. Then we hook the onDrunkLevelChange to all these components functions/actions and the parameter would be what the drunk level is. Each component can read this drunk level and act accordingly by increasing/decreasing the effects.

 

We also want to sober up over time so our drunkness component will look at that in it's Update() function. As time passes without having alcohol we'll reduce our drunk level and raise the event to tell the other components.

 

So that is the thought process with adding a feature. Notice the nice thing is that you can add each effect separately to each component over time if you want. Add the staggering to the controller component first. That could pass as drunk for now. At another time add it to the camera for head bobbing. Nice and easy tasks that can be created and checked off with various degrees of what the drunk feature looks like.

 

That's my thought process with component design. Just wanted to show people what goes on with making a new feature.

  • Upvote 2
Link to comment

This is the most interesting blog post I have read in years. I really get so many ideas on how to make my design better by reading this. I here by declare Rick my hero of the month if not the year. As some of you know I have been programming for many many years and because of that I have got stuck in old habits (like my player above) without even thinking if there's a better way to do it. Of course the component concept has come in my way now and then but in a more abstract and non-game format. Now Rick's words on this subject woke my interest as I know he's a clever guy and it was presented in a more hands-on format. I will absolutely adopt to this starting immediately and if I get drunk Rick is responsible :)

  • Upvote 3
Link to comment

Another bonus is you can have a master subscriber. You can record all the raised events with time-stamps and play them back. Now you have a built in recording tool.

  • Upvote 2
Link to comment

This is pure gold. I love component based behaviour and in C++ I still stick to a similar approach: http://www.leadwerks.com/werkspace/blog/34/entry-1077-component-based-engine-design/

 

Rick, you did a great job explaining this concept. I would love turning this in to a set of advanced Lua tutorials if that is okey with you Rick. Getting more people enthusiastic about this can create a new wave of usefull scripts since they can be easily chained together.

  • Upvote 3
Link to comment

@AggrorJorn Yeah for sure. I was thinking about how when a person is new to game dev or coding this is a hard concept to convince a person to use. It's not straight forward and seems like overkill UNLESS the person feels the pain of the more brute force way of coding themselves. You allude to this in your blog post on how Josh isn't a fan of stuff like this and is a more direct programming person. Direct programming I think is the best way to teach a new engine for sure as it gets to the meat without any kind of framework around it, but once a person gets those basics they generally quickly see how unruly and complex to maintain direct programming can be.

 

As soon as a persons game starts becoming more complex and they see how fragile their brute force way of coding becomes then they either give up, thinking it's their fault and they just can't do it game dev, or they start searching for a better way to structure their game.

 

So I feel like really explaining WHY coding this way is so important. Otherwise it can just seem like unnecessary complexity (even though in the long run it's less complex). It's a hard concept to get people to understand why they need it if they've never felt the pain.

  • Upvote 2
Link to comment
Guest
Add a comment...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...