nadako / Ash-Haxe

Port of Ash entity framework to Haxe

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Changed EntityState components not added in order

scriptorum opened this issue · comments

If you create a new EntityState for an FSM, the components are added to the entity one at a time, and in arbitrary order. This has led to some issues with my use optional components and the nodeAdded signal. For example, here's a node:

class ImageNode extends Node<ImageNode>
{
    public var position:Position;
    public var image:Image;
}

I subscribe to the ImageNode list's nodeAdded signal. When a new ImageNode is added, I can create the display object that is actually responsible for rendering the Image on the screen. Here's a normal entity created:

var e = new Entity();
var pos = new Position(10,10);
e.add(new Layer(30));
e.add(pos);
e.add(new Image("alive.png"));
ash.addEntity(e);

This addition is detected by my event handler and a display object is created. The Layer component is optional; if one is attached to the entity, the display object will assign itself to the layer encapsulated by the Layer component. In this case above, this works, because all the components get added to the entity before the entity is added to the engine. All the components are guaranteed to be there before nodeAdded signals are dispatched.

And here's an example of an FSM state that modifies the entity, when fsm.changeState("dead") is called:

fsm.createState("dead")
  .add(Layer).withInstance(new Layer(20))
  .add(Position).withInstance(pos)
  .add(Image).withInstance(new Image("dead.png"));

Layer and Image are new instances so the old versions of these components are removed and new ones added by the changeState method. This happens one component at a time and in arbitrary order. So sometimes the Layer object is present when the nodeAdded signal is made, and sometimes it is not, because Image was added first. If either components were added in the order specified, or a they were bulk added such that all components got created before any NodeList signals could be dispatched, this would not be an issue.

Since this is probably an issue in Richard's code as well, I don't know that you want to address it, but I thought I'd mention it to you. Alternatives right now seem to be adding the Layer component to the node (making it mandatory), detect the late-addition of the Layer component and adjust the layer then, or stop using nodeAdded for new node detection, instead using a system to loop through all nodes lacking a Display component, and construct the display on the spot.

Hey, thanks for the detailed report. To be honest, I personally didn't use the FSM stuff myself, I just ported it for the sake of completeness. :) But I'll look into it for sure.

Though for now I could say it's not the best practice to make your code rely on component adding/nodelist creating order. I believe that's Richard's idea as well, but I can't say for sure. We need to bring this discussion to the original Ash discussion group (https://groups.google.com/forum/?hl=en&fromgroups#!forum/ash-framework).

Regarding your case, I'd say that layer could be property of an Image component which could have a default value, so you don't need to specify it every time. I use that technique in HaxeDungeons and it works well for now (because every renderable entity has its own pre-defined and constant layer to be added to), though it may not be the best example, because that code's trash actually :)

You're right. I think the issue is I'm responding to immediate signals rather than acting in sequence using systems. If you act in sequence, you can make a batch of changes like a transaction without worrying about it being interrupted. I took a look at the Ash discussion group and found a post where Richard Lord advocates that.

...I have a very strict rule, which is essential to keep the game code clean, and that is that systems may never act out of sequence. Systems run when the game updates them and at no other time. That means that systems may never respond to a signal.
https://groups.google.com/d/msg/ash-framework/NHTVqvc7upI/MeDQNWkCvn8J

I'm sure you've read that, but I'm posting it because it's a discovery to me. :) Richard endorses systems signaling components but not the other way around. He also suggests boolean flags to pass state information to systems. I just tried this with my inventory system and it worked great, adding a "changed" Bool to my Inventory component. My InventoryView -- which is updated by the RenderSystem -- responds to that flag and resets it if found. I'm going to replace my NodeList signals with this shortly.

By the way, HaxeDungeons was serendipitously helpful in teaching me the Ash framework, as I was working on a Haxe roguelike too. I was doing this for 7DRL, but that seems to have turned into 30DRL because I'm still working on it. :) I originally implemented turn-based systems by creating my own SystemList and updating these systems separately from the real-time systems when the simulation advanced. This turned out to be unnecessary and I've since merged them. The turn-based systems are always responding to something transient, like an Action component I've added in response to player input, so they just sit idle when the components they respond to are removed. The only hacky thing I'm doing is removing the player's Velocity component after it is resolved, so turn-based movement halts until the next input, but there are ways to make that cleaner.

Thanks for all your work on the Ash port, this has been great.

BTW, HaxeDungeons doesn't follow Richard's recommendations at all. I still haven't found a comfortable way to avoid out-of-sequence system execution without much boilerplate and I'm honestly still not sure that what Richard advocates for is a no-brainer way to go, especially for a turn-based game like a roguelike.

Are you planning to share your code on github? I'm very excited to check out another Haxe roguelike project, especially when it's based on component system!

I probably will release it, once it gets to a playable state, or just before the start of the next Ludum Dare, whichever comes first. ;) I would like to use some of this code for a 48H jam, and as I understand it you have to share any base code that you might reuse in the solo compo. The game is not playable right now, it's acting more like a sandbox for messing with components. I'll let you know when I put it up.

One idea I had to make turn/real integration easier was to special case 0 as a time parameter. I haven't tried this yet, but I'm still not doing any heavy real-time stuff.

engine.update(0); // advance the turn

Each system could then evaluate this parameter, something like:

public function update(time:Float)
{
   if(time <= 0)
      // turn based updates
   else // real-time updates
}

I just got rid of a bunch of signals from my code. Now instead of responding to a nodeAdded signal -- which I was doing to create a corresponding display object -- I'm just looking for a missing Display object during the update:

for(node in engine.getNodeList(GridNode))
    updateNode(node.entity, GridView);
for(node in engine.getNodeList(ImageNode))
    updateNode(node.entity, ImageView);
[...snip...]
private function updateNode(entity:Entity, viewClass:Class<View>)
{
    if(!entity.has(Display))
    {
        var view:View = Type.createInstance(viewClass, [entity]);
        HXP.world.add(view); // Currently using HaxePunk
        entity.add(new Display(view));
    }
    entity.get(Display).view.nodeUpdate();
}

I still have a signal that checks for the deleting of a entity containing a Display, so it can remove the corresponding display object, but I don't think that's a terrible way to handle it. I could add a "Destroyed" component and detect it in the loop, but that seems unnecessary.

Hey, I put my code up on GitHub. It's a mess. :) See you at Ludum Dare!

https://github.com/scriptorum/DeadGrinder