Jump to content

Rick's Blog

  • entries
    65
  • comments
    96
  • views
    26,334

Component Architecture Part Deux


Rick

3,869 views

 Share

In my other blog (https://www.leadwerks.com/community/blogs/entry/1908-top-10-celebrities-who-use-component-architecture/) I talked about the component architecture I was using. I've since teamed up with Roland on this idea and together we have fleshed it out more. It's not 100% ready to be released yet but I realized that we've come a long way and that where we are today might seem confusing to someone if they weren't along for the ride. So I thought I'd use this blog to start down the journey we went on.

The main goal for our journey was as follows:

- Components will NEVER depend on other components. Decoupling as much as possible was priority #1. So the idea of events and actions(functions) is still at the core of the system. Now components need to work together obviously but the lowest level of dependencies the system has is via the arguments that events send along to actions. If you're hooking an action to an event you need to know what args that    event is sending you. This is primitive data vs classes and it's as decoupled as you can get while still allowing interactions between components.
    
Coroutines!

One day Josh asked about coroutines and if anyone knew anything about them. I've used coroutines in the past so I replied and did some examples (still need to finish that for him). Then one day on my way home from work it hit me. Since we have a common communication method in having actions (functions) being called from events we had a centralized place where all component actions (functions) were being called (the event class). This meant incorporating coroutines into our system was simple. Thanks to Roland for fleshing the idea out, all component actions are now coroutines automatically created (no work on the component creator's part). This was very exciting as it meant action functionality that required sequential coding could be done right in the action directly. This helped eliminate the need for most components needing an update() method that might not be doing anything except in certain situations. Situations that are now all handled in an action itself. It also meant a lot less state variables were needed to manage this sequential code. Roland had a doAddHealth() action where he instantly added the value passed in to the players health. This resulted in a snapping of the UI to the new health value. While that clearly works and you can do it that way, the test of our system was to make that health slowly increase to the final value over time, giving it a nice visual animation of increasing health. We were able to do it directly in that doAddHealth() function with about 2-3 more lines of code. It was insanely easy and much more logical in nature. Roland had never used coroutines before and he was able to get it working in a couple mins because it's just very logical and intuitive to work with when you don't have to deal with the details of the coroutine setup and management. You simple work with coroutine.yield() inside your action and that's all you really need to understand. This is an idea I'd like to see Josh think about with normal LE script functions as I think it can make life easier. Tim and I are working on a turn based game. Everything in the game is basically a sequence of actions over time so this idea of all actions are coroutines has been HUGE for our game. More on that at the end of Aug.

Entity to Entity Communication!

We fleshed out the entity to entity communication. My last blog talked about component communication with other components inside the same entity (the event system). This works great because all the components are inside the same entity and can access each others events and actions to hook up. But how can a component in entity A do something because a component in entity B wants it to, without those components knowing anything about each other? It was a challenge to get the right idea for this. We wanted to stay with the idea of events for this but we also didn't want entities to know about each other's internals.Decoupling of entities is important or else you end up with dependency hell and tigh coupling. We ended up with giving entities a SendMessage() function and an onReceiveMessage event. So from entity B we can hook up one of it's components events to the entity action SendMessage. The arguments for these required 2 special variables. Dest and Message. Dest is the entity to send the Message to. To get the Dest entity you're component is doing some kind of picking or GetNeighbors(). Dest can be 1 entity or a table of entities. On the receiving entity(s) the onReceiveMessage event is raised so that you can hook it's component actions to a received message. So all communication is done via the event system in some way.

This introduced 2 needed features to our event system. When you get an event onReceiveMessage is raised no matter what event you got. However you'd only want certain events to trigger certain component actions. This requires some kind of routing of string messages to actions being called. We did this currently with a filter function on the event's subscribe() method. When the event is raised and a filter function exists it'll call the function passing in the arguments of the event and if the function returns true, raise call the action method. If false it wont call the action method. So generally what you do is pass a function that checks the args.Message for the value you want to call the action.

self.onReceiveMessage:subscribe(self.healthComponent, self.healthComponent.doHurt, function(args)
        if args.Message == "hurt" then
            return true
        end
        
        return false
    end)

In the above event hookup when the Message is "hurt" then we hook it up to our healthComponent doHurt action. Because this is a very common thing to do, it can be bloated to have to define the function that does exactly this but for different string messages, you can just pass a string instead of a function to make it more streamlined:

self.onReceiveMessage:subscribe(self.healthComponent, self.healthComponent.doHurt, "hurt")

As you can see communication is NOT done inside the component. We didn't want components handling communication. We view component functionality and how that functionalty is communicated as 2 different types of coding. Communication is done inside the event hookups and functionality is done inside the component. Because of this and staying with the SendMessage/onReceiveMessage idea, we introduced another idea to the event subscribe() function. Another callback that is called before the action is fired. This also passes in the args and exists to let you modify the args before the action is called. This is used mostly when hooking a component event to SendMessage so that at that point you can give the string Message value. This way the component itself isn't concerning itself with the message which helps keep it more generic. This makes communication implementation specific to YOUR game and not the component. The component is just doing it's ONE job and raising events. That's it. What that means to your game is up to you to code. An example of this is:

-- nil is the routing/filter function we talked about above which we don't need because we are sending out not receiving in
self.inputComponent.onPicked:subscribe(self, self.SendMessage, nil, function(args)
        args.Message = "use"
    end)

The inputComponent will raise an onPicked event when an entity is picked by left clicking. It doesn't care what you want to do with that. That's your part of coding your game and is game specific. It will fill in the args.Dest value with the entity but we need a place outside the component to specify what we want our message to be for our game. The supplied function does just that. It let's us create the Message variable on the args and fill it in. On the receiving side then it's up to us to hook up to that entities components when the message is "use" like above in the onReceiveMessage examples. This idea of 2 types of coding I think really helps create more separation and isolation of code which helps with maintainability and reusability. If components were to define the Message value inside then their influence starts to leak out as another component needs to be programmed to deal with that exact Message. We don't want that. We want the messages to be game specific and decided on by the user of the component system not the component creators. There is an alternative syntax to the above code where instead of a function you can specify a table. This table will be merged into the args parameter.

self.inputComponent.onPicked:subscribe(self, self.SendMessage, nil, { Message = "use" })

So to summarize entity communication, when sending messages the arguments callback function (or table that gets merged) is useful. When receiving messages the filter/routing callback function (or string message) is useful.

Cool Side Effects!

And interesting side effect to to the event system is that they are raised in the order they were subscribed to. You can use that to your advantage if you want to modify the args in any way between multiple components. Tim and I use this concept in our game. When we get a "hurt" message come into a character entity we first pass it through a Stats component which stores information about the player armor and other defenses. The args has a value property on it that is how much damage we should take, but by first running through our Stats component we can reduce that value by our armor. The 2nd component it's hooked up to is the health component which will reduce our health by the value property but now it's less because our Stats component reduced it. Since args is a table and tables are passed by reference the change to an args property in one component is visible to subsequent components.
 

Summary

Having a common communication protocol between components and entities has been a big help in structuring my code for maintainability, adding new features, and reusability. One of the benefits of Lua is that table properties can be accessed via their string name. So something you might notice about event hookups to actions given knowing that Lua table properties can be accessed via string names, is that hooking up events is really just configuration. Even though the above examples is code that code can be made very generic where the property names are string values stored in a file. For example the above inputComponent hookup is the same as:

self["inputComponent"]["onPicked"]:subscribe(self, self["SendMessage"], nil, { Message = "use" })

Because it's a common structure all event hookups follow the same pattern. So imagine this information is coming from a json file and that json file is built from a visual editor. You would code your components in a code editor but hookup your components in a visual editor as you select the event source, event, action source and action from drop down menus. Thanks to Roland, we will have more to come on that in the next few months...

 

  • Upvote 7
 Share

10 Comments


Recommended Comments

Very cool stuff Rick and Roland. I would also love to see more enthusiasm on such a communication framework. I think adding a basic diagram on the flow of the message and the structures it passes through will aid in explaining what you are doing. For instance a character with a health component.

 

Link to comment
1 hour ago, AggrorJorn said:

Very cool stuff Rick and Roland. I would also love to see more enthusiasm on such a communication framework. I think adding a basic diagram on the flow of the message and the structures it passed through, for instance a character with a health component, would aid in explaining how your solution works.

 

I can make one. Give me some hours :)

An FPS player that can pick health items and have a health meter 

  • Upvote 1
Link to comment

So here is a small example of a game with a first person player with a camera. You can move the player forwards, backwards and turn him around. The camera will detect any items in front of the cursor within a limited range. If you left click on such an item when its detected it will go away, add health to the player and update a health meter showing current health.

Here is a screenshot of the example (here with two different kinds of items to pick and two meters, but besides that its the same)

lcssample.jpg.903c7d59c66eb7543e35e3e7246ab5eb.jpg

Instead of diving into coding the player with all its operations to handle we won't in fact make any player coding. Instead we will divide things into components with their own responsibilities. The components is hold together by a runtime object created by LCS and is nothing you don't need to care about. So what is a component then. Its a LUA class that has Events and Actions and is completely isolated and independent of other components. This means it can easily be replaced with another component that behaves in another way. It can also without any changes be used in other projects.

So how would we go about this then. First of all we have some entity's in our scene.

  • Health items to pick up (the boxes)
  • Player (a simple invisible cylinder) including a FPS camera
  • HealthMeter ( GUI showing current health)

Normally we the would have started making Scripts for those items above. But were not this time :o In fact none of the Entities in the scene will have a Script. Instead we are going to write Component scripts that can be used here or in some other game.

So what components could our player need then? That's up you depending on how you want to design things. Here is a suggested approach by me.

  • Input component that handles user input
  • Controller component that handles movement of the player and camera
  • Health component that keeps track of current health
  • Cam component which is the FPS camera

Further on the Health item needs a HealthItem component that reacts to a pickup and a HealtMeter component that serve as a GUI for current health. You don't need to write any Script code for the Entities in the Scene, only component needs some action by you. Each entity will be connected to a GameObject (which is generated in runtime and does not need any coding). This GameObject ties the components together and handles all things to get the communication going. The system is defined by supplying a setup written in JSON format. However you wont need to do that either as I'm currently working on a design application for setting things up and that will be available when we release the system.

ed.PNG.2f1b7a23177a99948e6a0487dc7d78e0.PNG

Anyway.. so now when we know what components we need, its time to think a bit on what they should to and how they will interact with the surroundings. So lets get started. I won't go through every component but I think you get the idea

Input

Should handle input from the user and generate events accordingly. An event can be subscribed to by any other component and sends some information about the event that happened. All events starts with 'on'. A subscriber of an event must have an action that will be triggered by the event. All actions starts with 'do'.

Events

  • onForward is sent when user presses 'W'
  • onBackward is sent when user presses 'S'
  • onClick is sent when user clicks left mouse button

Controller

The controller is responsible for moving the player

Events

  • onMove is sent when the player moves (contains information about current position)

Actions

  • doMoveForward moves the player forward
  • doMoveBackward moves the player backward
  • doTurn turns the player around its Y-axis (needs an angle)

Cam

Is simply the camera which is rotated by moving the mouse.

Events

onPick is sent if the doClick action is called when an pickable entity is in front of the camera. The entity is sent with the event.

onTurn is sent when the user rotates the camera by moving the mouse

Actions

doMove moves the camera to a given position

Here is a drawing showing our system now. The yellow parts are entities in the scene (map), the red ones are GameObjects created in runtime holding the components and finally the blue ones are the components which we have to write code for.

lcs1.PNG.fd945ffbf9bd178de727df90a5ae8f8a.PNGL

 Lets have a look at the code for one of our components to see what it looks like

controller.thumb.PNG.509a17b410340949c64f511445fb86cf.PNG

The Controller class must have following functions to be treated as a controller by the LCS system

  • init called by LCS to create the controller
  • attach called by LCS when its attached to its entity and GameObject

Besides those two, a component can (optionally) have following functions called in the same way as in normal Scripts. If one or more of those exist they are called automatically. 

  • update called on UpdateWorld
  • updatePhysics called on UpdatePhysics
  • draw called on Draw
  • drawEach called on DrawEach
  • postRender called on PostRender
  • detach called on Detach

In the init function you can see how an Event is created. That is all needed to create an Event. 

self.onMove = EventManager:create()

If you then look in the update function you can see how the event is sent by calling 'raise'

self.onMove:raise({Pos=newpos})

This event a contains a position. All event information are sent as tables. In this case a table member Pos set to the current position.

Further there are some actions that can respond to events from outside world. Lets look at one of the actions 

function Controller:doMoveForward()
	self.move = self.moveSpeed 
end

The action is just a normal function that may or may not have an argument. It does not need to return anything. One exiting thing is that all Actions are automatically executed as coroutines which has really cool advantages. I wont go into that here but I thought its worth mentioning.

Here is a code snippet from the Cam component showing an action that can handle an onMove event that has an argument.

function Cam:doMove(args)
	self.camera:SetPosition(args.Pos,true)
end

The args is the table sent with the event ( remember self.onMove:raise( {Pos=newpos} ) ). The args table will contain the member given when the event was raised.

Okay. Lets see how all this works then

lcs2.PNG.86ad986915b451162b817be3be26bcb4.PNG

In the diagram above you can see the components and the events in action.

  1. User presses W and Input:onForward event is sent to the Controller:doMoveForward action which make the player move forward
     
  2. The Controller raises the onMove event which is received  by the Cam:DoMove action which moves the camera
     
  3. User presses S and Input:onBackward event is sent to the Controller:doMoveBackward action which make the player move forward
     
  4. The Controller raises the onMove event which is received  by the Cam:DoMove action which moves the camera
     
  5. User moves mouse left and the Cam:onTurn event is raised giving the angle of rotation the movement caused. This event is caught by the Controller:doTurn action which rotates the player in the same angle
     
  6. User left clicks the mouse which raises the onClick event which is caught by the Cam:doClick action. If the camera now has an item directly in focus it raises an onPick event which then is caught by the entity's component HealtItem:doPicked.
     
  7. In doPicked the HealthItem will hide it self and send a message back to the player telling that it needs to 'add.health' among with the amount of health.
     
  8. The message will end up in the Health component (by magic :) ) and the health will be increased. 
     
  9. As the health has changed the Health:onHealthChange event will be raised and is caught by the HealthMeter:doSetHealth action which will show the new health 

The communication between Events and Actions is set as hookups. A hookup means that a component subscribes to an Event. Take our onMove/doMove example above. Setting up a hookup for this is simple

self.Controller.onMove:subscribe( self.Cam, self.Cam.doMove )

But that's done for you by the LCS system in the runtime GameComponent holding the Components for the player. How, you may wonder? This is done by creating a LCS project file (in JSON format) defining the GameObjects, Components and Hookups.

Going into this in detail goes a bit out of scope for this 'short' description, but I can show a cut from that file

{
	"source": "Controller",
	"source_event": "Move",
	"destination": "Cam",
	"destination_action": "Move",
	"arguments": "",
	"filter": "",
	"post": ""
}

This creates the hookup between the Controller and the Cam. This is only a simple example but you can add functions right here also for adding more arguments, making event filters and more.

Code creation. One nice feature is when adding a new component by defining it in the LCS project file, LCS will create the LUA file for the component with everything added. The only thing you will need is to add some code inside the Action functions.

As a last thing I will show how easy it is to take advantage of the actions which are coroutines (by magic). Here is how I raise the health level in the HealthMeter to be smooth

function HealthMeter:doSetHealth(args)

	local value = 1
	if args.Health < self.health then value = -1 end
	
	local goal = Math:Round(Math:Clamp(args.Health, 0, self.max) * self.valueSize)
	while self.pixelValue ~= goal do
		self.pixelValue = self.pixelValue  + value
		coroutine.yield() -- give system some time
	end
	self.power = args.Health
	-- done!
end

 

Hope this have given you some more insight into the LCS system.

PS: Some details has been left out here to increase readability 

  • Upvote 3
Link to comment

Thanks for the extended explanation Roland. I will have to sit down for this and give it a thorough read. Really cool to see how you guys are creating this framework.

  • Upvote 1
Link to comment

I'm going away for a roadtrip tomorrow and won't be back for three weeks. First thing when I'm back is to finish the LCS design editor and then start making some docs and tutorials. After that we will need some beta testers (you have already showed an interest in that). Anyone else is welcome. Will probably be in end of August if God and Thor is on our side. All this will be free when released.

  • Upvote 2
Link to comment
21 minutes ago, mdgunn said:

I'd be interested too!

 

Great. All you who are interested just tell here 

  • Upvote 1
Link to comment

We have a long term goal of allowing, from the editor, pulling down and publishing from/to github components that people create for this system. GitHub has a web API and if people use a certain tag on thier components on there we can easily query those and display them in a list to pick from. Sort of like a package manager. However, getting multiple people to use the system will help better define it before we do that. At that point we would want to have everything solid for a version 1 release.

We think that it's really easy to create actions and events and then visually hook those up together to make more complex functionality. 

  • Upvote 2
Link to comment

I sat down last evening to give it a thorough read and I am really impressed. I also love the component editor system. This saves a lot of manual typing to register all components.

What I like the most is that this completely overhauls my default thinking process about the game entities. The losely coupled structure is perfect and I think that this is the biggest downside to most of the code that I currently write. Everything needs a reference to objects in the scene or other scripts. This just makes management so much more easy. 

@Rick That API/components collections sounds awesome. I can allready see dozens of tiny components that can be useful for this. Any ETA on the project beta? nvm, end of august.

  • Upvote 1
Link to comment

I'll give a real world example that can help people get in the event action component mindset.

Tjheldna and I are making an rpg turned based fighting game. Think old school final fantasy like. So you have your characters and enemy characters. When it's one of your characters turn you select an ability and then select a target to do that ability on. Ultimately a message is sent to that entity to do damage to it. I call that message "do.damage". The target entity gets onReceivedMessage event raised whenever it gets a message. We then hook up components actions we want to do functionality to this entities onReceiveMessage.

So in my example case I'm getting a "do.damage" message. So my thought process was I need health in order to take damage so I create a health component. It's a pretty basic component to start with. Simply has a health variable, and an action I named doReduceHealth(args). The arguments table that came along with the "do.damage" message has a 'value' variable with the amount of damage to be done. So inside doReduceHealth() action I reduce my health variable by args.value. I then add the health component to my character entity and hook up its doReduceHealth() action to the entities onReceiveMessage event and part of that subscription I have a filter function that returns true when the args.Message == "do.damage" so that when onReceiveMessage is raised it'll only call my health components doReduceHealth() action when the message we received is "do.damage".  Now my health is being reduced!

So we were rocking that for some time and all going well. The next part shows the lego like idea of this system. It's very much like snapping legos together ifeach lego was a piece of functionality. Because this is an rpg we need stats like armor, strength, etc. In our specific case one enemy has an aura that reduces all damage by 50%. So another stat would be damage reduction. So I know I need these stats thst kind of all play together in some way. So I create a Stat component. Inside I create a variable named damgeReduction which holds a percent value. The point of this component at this point in time is to reduce the damage trying to be done to me. So I create an action named doReduceDamage(args). One good side effect to our system is that actions are called in the order they were hooked up to the event. So clearly I need to reduce the damage coming into use from the "do.damage" event our entity gets before the health component does the damage to our health. So before the health event hookup of the "do.damage" message event I hook up this stats doReduceDamage() action. The other benefit to this system is that arguments to actions is a table and tables are passed by reference. Which means if I modify a variable value that is part of thst table in one component action, the other component actions thst get called after that will see those changes. Perfect! Since our stats doReduceDamage action is called first I'll reduce the args.value number by whatever our stats damageReduction variable is. Now when the health components doReduceHealth action is called the args.value variable is lower. I just easily and in an isolated way snapped on functionality.

The last day or so I've made posts trying to figure out how to draw to a texture and then put that texture on a sprite. The reason I'm doing this is because I need to show the player how much damage was done. I needed the value of the damage being done to show up on screen above the characters head and then rise up a little over time. So guess what? New component! I created a RiseText component. It has an action named doRise(args). I hooked this up to the entities onReceiveMessage event and raise it when it gets the "do.damage" message but it's hooked up AFTER the stats actions so that args.value is already modified and the true value that is being done to that entity's health component. This is where the benefit of coroutines come in. doRise() action of this component does everything right inside itself. It creates the needed variables for buffer, material, etc. draws the text and then loops and tweens the text upward while yielding out. It's nice and more natural to be able to do that all in that one actionvs having to set state variables and do it in that components update() function. An added benefit to why it's nice is because when an action is raised a unique coroutine is created that wraps it. This means that even if the same action is called multiple times each one is ran in its own contained state. So one instance of that action might not be finished while another is called again but because they are wrapped in thier own unique coroutine they don't effect each other. If we didn't have this then calling the doRise() while another rising text was still happening in its update() function would cause thst one to stop so it can do the new request of rising text. If we didn't want that then we'd have to get into possibly storing multiple instances of possible rising text ourself. What a pain. With actions being wrapped into unique coroutine objects we don't have to worry and we can code more naturally.

So I hope this illustrates the mindset of this system. Snapping smaller fairly isolated pieces of functionality together to get complex functionality. Maintaining each component is extremely easy since thier code is usually fairly small and it's contained in its own "class" and file. Removing its effects is simply commenting out its event hookups which are usually just a couple of lines of code.

The only "coupling" components have is through the arguments. You need to know what variable names are part of the args table from within a components action when you hook up to an event. It's as loose of coupling you can get. It doesn't have anything to directly do with components structure themselves. I remember when josh first started this version of Leadwerks where he allowed multiple scripts to be attached. He found himself querying the other attached scripts to see if a certain script existed and then directly calling its functions. He didn't like that and he was right. That sort of defeats the purpose of components being isolated and creates a spaghetti mess of code. People in Unity still do this and I think it's a mess, and bug prone issue. Once you start working with hooking up actions to events things just become so much easier to work with.

Learning what size scope to make your components (not too big not too small in scope) takes a little practice and feel but once you get the feeling down you are off and rolling.

  • 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...