-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[proposal] Dispatch events upon adding and removing components #30
Comments
I think we need something like this. We could later add some sort of priority level or whatnot. I wonder if assigning components in |
Can confirm. I've attempted to do the same in Echoes, and it causes one specific error due to order of operations. Before getting to that error, I'm going to try making some examples. //Test case 1
entities.onEntityUnmatched( function (entity:{ component:Int }) {
trace(entity.component); //should not be null yet
entity.component = null;
} ); For a very simple case, consider this potential infinite loop. Because In Echoes, this isn't a problem. Each archetype stores a list of matched entities plus a set of match/unmatch listeners. And they have a With all that said, this example is pretty unrealistic. Here's one that might realistically come up: //Test case 2
entities.onEntityUnmatched( function (entity:{ yin:Yin, yang:Yang }) {
entity.yin = null;
entity.yang = null;
} ); Say you have two components that must exist as a pair, so whenever one is removed, you'd want to remove the other. Without a solution to the infinite loop problem, both If using Echoes's solution, this setup would end up calling Another possibility is to put off processing Hmm. What about this? //Test case 3
entities.onEntityUnmatched( function (entity:{ yin:Yin, ?yang:Yang }) {
entity.yang = null;
} );
entities.onEntityUnmatched( function (entity:{ ?yin:Yin, yang:Yang }) {
entity.yin = null;
} ); I was thinking that maybe adding restrictions could help. I thought the rule could be "an unmatch listener can't remove any of the required components, but can still remove optional ones." But no, these two listeners will trigger each other. Both solutions I mentioned still work, but I don't think this restriction opens up anything new. Now the other side of the coin. What if we add new components? //Test case 4
entities.onEntityUnmatched( function (entity:{ caterpillar:Caterpillar, ?butterfly:Butterfly }) {
if (entity.butterfly == null)
entity.butterfly = new Butterfly( entity.caterpillar );
} );
entities.onEntityMatched( function (entity:{ ?caterpillar:Caterpillar, butterfly:Butterfly }) {
trace(entity.caterpillar);
entity.caterpillar = null;
} );
//Test case 4.1
var green = {};
entities.createEntity( green );
green.caterpillar = new Caterpillar( 0x00FF00 );
green.butterfly = new Butterfly( green.caterpillar );
//Test case 4.2
var yellow = {};
entities.createEntity( yellow );
yellow.caterpillar = new Caterpillar( 0xFFFF00 );
yellow.caterpillar = null; So here we have a goal of replacing one component with another. We can either do this by adding the new component and having it automatically remove the old, or removing the old and having it automatically add the new. (I've found use cases for both.) In case 4.1, we start by adding the In case 4.2, we start by removing the No infinite loops here, just slightly unexpected behavior. This all stems from the order of operations I mentioned in my first post: match listeners must be called after adding the component, while unmatch listeners must be called before removing it. When a match listener is called by an unmatch listener, the old component still hasn't been removed. And this brings us to the case where Echoes fails. //Test case 5
entities.onEntityUnmatched( function (entity:{ caterpillar:Caterpillar, ?butterfly:Butterfly }) {
if (entity.butterfly == null)
entity.butterfly = new Butterfly( entity.caterpillar );
} );
var timer = new haxe.Timer( 1000 );
timer.run = function():Void {
entities.foreachEntity( function (entity:{ caterpillar:Caterpillar, butterfly:Butterfly }) {
trace( 'Warning: forgot to clean up caterpillar ${entity.caterpillar.id}' );
entity.caterpillar = null;
} );
}; If I converted this code to run in Echoes, it would crash when attempting to retrieve
After over a year of use, this has been the only edge case I've found. Realistically, the conditions are unlikely to come to pass. You have to add one component in response to another's removal, implying the two don't coexist, and then call I think the alternative I proposed (where you postpone component assignments until after all listeners return) covers every case. The only problem is implementing it. |
I think the difficulty with postponing component assignments is we want this code to work as one would expect: entities.onMatchedEntity(function(entity:{ b:Int }) {
entity.b *= 2;
});
entities.onMatchedEntity(function(entity:{ a:Int, ?b:Int }) {
if (entity.b == null)
entity.b = 5;
trace(entity.b); // Should be 10
});
entities.createEntity({ a: 0 }); It should trace I gave some thought to this based on your feedback coming from Echos: If Let's run this through the different cases: //Test case 1
entities.onEntityUnmatched( function (entity:{ component:Int }) {
trace(entity.component); //should not be null yet
entity.component = null;
} ); Setting //Test case 2
entities.onEntityUnmatched( function (entity:{ yin:Yin, yang:Yang }) {
entity.yin = null;
entity.yang = null;
} ); Same as above, both //Test case 3
entities.onEntityUnmatched( function (entity:{ yin:Yin, ?yang:Yang }) {
entity.yang = null;
} );
entities.onEntityUnmatched( function (entity:{ ?yin:Yin, yang:Yang }) {
entity.yin = null;
} ); This one gets more interesting. Considering an entity with both components, setting //Test case 4
entities.onEntityUnmatched( function (entity:{ caterpillar:Caterpillar, ?butterfly:Butterfly }) {
if (entity.butterfly == null)
entity.butterfly = new Butterfly( entity.caterpillar );
} );
entities.onEntityMatched( function (entity:{ ?caterpillar:Caterpillar, butterfly:Butterfly }) {
trace(entity.caterpillar);
entity.caterpillar = null;
} ); Test case 4.1:Once Test case 4.2:Once //Test case 5
entities.onEntityUnmatched( function (entity:{ caterpillar:Caterpillar, ?butterfly:Butterfly }) {
if (entity.butterfly == null)
entity.butterfly = new Butterfly( entity.caterpillar );
} );
var timer = new haxe.Timer( 1000 );
timer.run = function():Void {
entities.foreachEntity( function (entity:{ caterpillar:Caterpillar, butterfly:Butterfly }) {
trace( 'Warning: forgot to clean up caterpillar ${entity.caterpillar.id}' );
entity.caterpillar = null;
} );
}; Considering an entity Some more cases:entities.onEntityMatched(function(entity:{ a:Int }) {
trace("Matched " + entity.a);
});
entities.onEntityUnmatched(function(entity:{ a:Int }) {
trace("Unmatched " + entity.a);
});
entities.createEntity({ a: 33 }); // traces "Matched 33"
entities.foreachEntity(function(entity:{ a:Int }) {
entity.a = 99; // traces "Unmatched 33" then "Matched 99"
}); Re-assigning a component should trigger This approach seems more technical to implement but I think it also feels more natural. An issue I see with it, however, is when we want to invoke entities.onEntityUnmatched(function(entity:{ a:Int, b:Int }) {
});
entities.foreachEntity(function(entity:{ a:Int, b:Int }) {
entity.a = null;
entity.b = null;
});
The callback |
If you think you can pull this off efficiently, it seems like it should work well. My main concern is, it sounds like it would involve constructing a new anonymous structure, to pass to the listener. If you're already doing that, no big deal, but if not, make sure it performs well.
Ok, I need to clarify a bit. You only need to postpone operations that happen within an unmatch listener*, and only until the listener returns. When you set You'd probably track this using a static variable somewhere. And I guess that leaves two problems. If the listener throws an error, the count might not decrement, and then the whole library breaks. Sometimes it's possible to keep going after an error, but not anymore. Second, if you're inside a listener, there's still the problem that you can't access variables you just set. *It's specifically unmatch listeners because of the whole "removed component must still be accessible" thing. Since you put off removing the component, then if any other mutations happen in the meantime, they're out of order, and you get something akin to a race condition. Since your solution updates the underlying representation before calling the listener, everything stays in order and there shouldn't be any issues. At least, not any that resemble a race condition. I say go for it. |
Events are an important part of any game, and it's only natural to want them for ECS programming. There are plenty of situations where you'll want to know about new components being added and old ones being removed.
foreachEntity
is good, but can't do everything.My suggestion is to add two new functions to
EntityGroup
:onEntityMatched
(calls a function when an entity begins to match an archetype) andonEntityUnmatched
(calls a function when an entity stops matching an archetype). Here's an example of how they might look in action:Note that both systems need to know the value of
display
. ThusonEntityMatched
has to run after setting the new value, butonEntityUnmatched
has to run before.The text was updated successfully, but these errors were encountered: