Advertisement
Drakim

Rubber Ducky Conversation

Feb 27th, 2012
313
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 9.79 KB | None | 0 0
  1. ==BASIC==
  2. Component based entities is not the only way one can avoid hard coding much of the game logic, but I feel that it's the only system that goes far enough while also maintaining good performance. Other systems typically provides a wide spectrum of fine tuning capabilities, but never true modularity. They often end up promoting bad design (such as cramming too much functionality into the 'update' method of an entity).
  3.  
  4. =The entity API=
  5. class Entity {
  6. function attachComponent(component):Void
  7. function removeComponentByType(type):Bool
  8. function getComponentByType(type):T
  9. function attachEvent(string,Dynamic->Void):ComponentEvent
  10. function removeEvent(string,componentevent):Bool
  11. function dispatchEvent(string,data):Void
  12. }
  13.  
  14. =Typical component class trash example=
  15. class MyComponent implements Component {
  16. static function append(entity):Void //standard method for attaching a component to an entity
  17. var property1:Int
  18. var property2:Float
  19. function helpermethod1():Void
  20. function helpermethod2(_value:Int):Int
  21. function eventmethod1(_data:Dynamic):Void
  22. function eventmethod2(_data:Dynamic):Void
  23. }
  24.  
  25. Let's deconstruct this to see what it really means. What it essentially boils down to is that we have strictly typed objects called components. The components can communicate with each other in two ways.
  26.  
  27. The first way is component A fetching component B with getComponent() and interacting with it. This is strictly typed. The components can cache access to each other to allow lighting fast manipulation. However, since everything needs to be so strict and predictable, it pretty much locks things tight. If we ever modify the existing structure of B then A will most likely throw an error.
  28.  
  29. =Example=
  30. function modifyOtherComponent(entity:Entity) {
  31. var other:B = entity.getComponent('othercomponent',B); //Need to know the class of component we get
  32. other.specialvalue += 10; //Here we need to know exactly what B's properties are called
  33. //if somebody modified component B so that it doesn't have .specialvalue anymore this code fails
  34. }
  35.  
  36.  
  37. The second way is for component A to dispatch an event with dispatchEvent(keyword) on the parent Entity. This is more dynamic. For this event to actually do anything, then component B must have attached an event-method to that keyword. Component A has no way to know if component B has actually attached such an event-method, and if component B has not, nothing will simply happen after the event was dispatched. It is also possible that component C has attached it's own event-method to that keyword, also being triggered without component A realizing. Futhermore, there is no way for component B (or C) to respond to the dispatched event, so they can't tell A if they were successful at doing what they are supposed to do. If such a response mechanism is absolutely needed, it can be done by making component B (or C) dispatch an event back with a different keyword that A can detect.
  38.  
  39. =Example=
  40. function modifyUnknownComponent(entity:Entity) {
  41. entity.dispatchEvent('barrel_roll',{repeat:10})
  42. //Will component B see this? Does it react to a barrel_roll? Who knows!
  43. }
  44.  
  45. These two methods of communication cannot be unified. If you made a system that does both you'd get something very verbose and slow. That being said, this two tier system is quite nice and should be kept. Due to the caching the first method of communication is really fast. The only penalty compared to really hard-coding everything would be that you are doing position.x instead of just x (and helper-methods can even be inlined). Events aren't quite as fast, but dispatching events on a keyword that has no event-methods is still quite cheap, and there are methods mentioned at the end of this document for how to speed up certain cases where events can be heavy.
  46.  
  47. ==ADVANCED==
  48.  
  49. Events provide a lot more modularity than simply allowing components to interact with each other without knowing each other's type and structure. Components can provide a priority value when attaching event-methods, to ensure that they are triggered before other event-methods. Futhermore, the data object that is sent along with the dispatchEvent() method is shared among all event-methods, meaning that it can be modified before it reaches the other event-methods.
  50.  
  51. See for instance how this "damaged" event can be manipulated on the way down the chain:
  52.  
  53. 1. Event 'damaged' is dispatched with {amount:50,source:entity1} on entity2
  54. 2. An event-method attached to 'damaged' with priority 1 triggers first
  55. *This is the Event-method of component Armor, which subtracts 20 from data.amount;
  56. 3. An event-method attached to 'damaged' with priority 0 triggers second
  57. *This is the Event-method of component Life which subtracts data.amount from Life.health;
  58.  
  59. Result: 30 health points is subtracted from this entity. The Armor module is completely transparent to the Life module, and it would be easy to add more modules to the chain, such as a Debuff that makes you take 25% more damage, to further modify how the event unfolds.
  60.  
  61. Event-methods can also call a secret method called entity.stopEvent(), which stops all Event-methods further down the chain from getting triggered at all. If you have played StarCraft, you can probably see how this can be used to implement a Protoss style shield to the entity. The energy shield Component would simply subtract damage from it's own shield value, and stopEvent() whenever it blocks the damage entirely, leaving the unit unscratched. But when the shield value is not enough to block the damage, the shield component avoids calling stopEvent().
  62.  
  63. There are almost limitless possibilities for how we can make components respond to these events. A SpikedArmor component could return a 'damage' event to the source entity with 20% of the damage that was caused, creating a "damage returned" effect (just be careful it doesn't go in an infinitive loop). Additional values like element:'ice' can be added to the data object without interfering with how earlier components that doesn't care about elements works, as they will simply ignore the value.
  64.  
  65. Making an unit invulnerable is now extremely easy and doesn't require you to open up your Entity class to make changes to it's internal takeDamage() method. All you need is a high priority 'damaged' event-method that nullifies all damage done or calls stopEvent();
  66.  
  67. ==SUPER ADVANCED OPTIMIZATION==
  68.  
  69. Say that you have a beehive boss that summons bees with an extreme amount of Components. It's come to the point where it's causing a slowdown. What can we do?
  70.  
  71. Say that component A, B, C, D, E, F and G have registered an event-method on the event 'update' for every bee, and the amount of method calls is slowing down our game. The way we optimize this is to make a new component X, which attaches an event-method to the event 'update'. In it's event-method, we put A.eventmethod1();B.eventmethod1();C.eventmethod1();D.eventmethod1();E.eventmethod1();F.eventmethod1();stopEvent();
  72.  
  73. We also make sure to mark these eventmethods as inline in the respective components. Inline won't help in the dispatch system because it's possible to inline from such a point of view, but it will work if it's all wrapped up inside component X, and it will help us greatly reduce the amount of method calls, while maintaining the same functionality. The only gotcha here is that we have to be careful if we remove a component like C from the entity, as component X will still be calling it. This could be solved by a entity.dispatchEvent('componentRemoved',{component:C}); and having component X listen for this.
  74.  
  75.  
  76. ==MORE SUPER ADVANCED OPTIMIZATION==
  77. One common thing in games is health regeneration. You want the unit to recover a slight bit of health each tick. Things like this easily pollute the 'update' method of the entity, as you add in health regeneration, mana regeneration, strength recovery, cooldowns, etc.
  78.  
  79. But at the same time, modular design can fall short. We can easily make a HealthRegeneration component and give it to all entities on 'update', but it is quite costy. For 300 units across the map that's 300 method calls, multiplied by every stat we want to recover over time.
  80.  
  81. However, our components allow us to work our logic in a different flow, avoiding this expensive spam of method calls. Instead of assigning one event-method per entity, we can assign a single event-method to the world entity. This event-method contains a loop, where it adds the amount of health that each entity is supposed to get. When a HealthRegeneration component is given to an entity, it simply takes that entity and places it into the loop, rather than attaching to the entity's 'update' method.
  82.  
  83. This way, we have reduced those 300 method calls into a single method call plus a loop.
  84.  
  85. But let's say that we wanted to have a StopRegeneration component that prevents the unit from gaining health, as part of some debuff. Now our fancy loop system is preventing the StopRegeneration from working properly. How do we solve this?
  86.  
  87. With another secret method, entity.getEventList(string). In our regeneration loop, we can, before adding health to the entity, call entity.getEventList('regeneration') to quickly see if there are any other event-methods that wants to do something when regeneration event happens. If there are any such event-methods, and they have a higher priority than our own regeneration event, then we do dispatchEvent('regeneration') for that particular Entity. Basically we are doing a graceful fallback, where we do it the old fashioned way if, and only if, it is necessary. After we have done this for that one entity, we go back to the loop for all the other entities that doesn't have the StopRegeneration component.
  88.  
  89. The key here is that all this is done in the component we are trying to optimize. We never have to go into our StopRegeneration component to fix up this issue, it's all in HealthRegeneration.
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement