Closed vmwxiong closed 3 years ago
Hmmm, another issue I'm seeing is that you are required to provide your own custom deserialization functions for each component. This adds some pretty significant overhead to creating a new component type, which you'll likely end up having dozens or hundreds of. It also introduces risk in modifications, since it becomes fairly easy for a developer to modify a component while forgetting to modify its custom deserializer.
I'd propose having some sensible defaults for deserialization. Take some opinionated stance on what components should look like, and use that as a starting point when saving/loading entities. After that, you can add hooks for more custom deserialization of specialized component types for more complex use cases that people might have.
Hi @vmwxiong , thank you for your very good feedback. I think what you are pointing out is one of the missing pieces of the component life-cycle, one which I am currently trying to figure out myself.
I think you might want to have something like an in-line deserializer that lets a designer create an entity with an object literal or a piece of json.
The current save-format was developed with small file size in mind, the idea being that there would be a GUI-based editor later on which is used to create the entities. Having a second format, which is geared towards humans, might be a solution for people who are not tech-savvy and still want to quickly edit a text file. I like your second example, where a component's JSON representation might use the type name as key and the value delivers the component's data. It would be fairly easy to implement, too! I just have to think about the API changes needed. TSaveFormat
would need to get compatibility and then IEntityBuilder
could expose some method to import it the way it's done in your example:
const entity = world.buildEntity().fromObject({
background: {},
glyph: {},
mineable: {},
name: {},
position_2d: {}
});
// or
const entity = world.buildEntity().fromJSON('{ ... }');
Maybe even with methods to import a whole bunch of entities stored in such a JSON file (in an array).
This adds some pretty significant overhead to creating a new component type, which you'll likely end up having dozens or hundreds of. It also introduces risk in modifications, since it becomes fairly easy for a developer to modify a component while forgetting to modify its custom deserializer.
Having default code here is very hard, because the ECS needs to instantiate an object with an generic type from a serialized representation and make sure that the type contract is honored. I am exploring the foundations of prototypes and types with this project myself, so I'd need time to find a good solution. If you have experience, I would be happy to get any hint, or even PR.
All in all, you are right. the current API is difficult to learn and use. I think about extending the current design (especially component registration) in a way which is minimal, so that a user can just register the constructor for data-only structs and the ECS will know how to handle that, but leave it up to the user if they actually also need a serializer and deserializer.
What do you think of the above ideas?
The current save-format was developed with small file size in mind
Personally, I think this goal is a little unintuitive considering the size and complexity of a game that would bother implementing ECS. If it's important to you though, you could maybe make this entire framework more modular and implement things as extensions or add-ons, but that's quite a lot of work for something that may not end up being used in practical situations.
Maybe even with methods to import a whole bunch of entities stored in such a JSON file
Definitely a fan of this direction! Making things easy and practical to use off the shelf goes a long way. Even if you have capabilities to implement custom loading/saving, picking some reasonable, opinionated defaults can make it a lot more appealing to jump in and start using something like this. ECS isn't particularly complex on its own right, and there's a pretty low bar before someone just decides it's more work to integrate this than to run their own basic ECS solution.
Having default code here is very hard
I'm afraid I'm no typescript expert myself, so I'm probably of little help here, but I've found the Typescript discord to be immensely helpful in answering tricky, interesting problems around typing like this. Bet they'd be more than willing to lend a hand.
in a way which is minimal, so that a user can just register the constructor for data-only structs
Yes! Another great direction. Anything that makes every really dead simple to pick up and start is a great addition.
On a related note, I thought of another syntax that might work for entity building:
world.createEntity<Counter, Health, Inventory>({...}, {...}, {...});
Slightly less programmer-ese by removing the new()
calls, but still allows for type checking each config. Certainly less than ideal for a designer to ever have to deal with though.
(Actually this probably doesn't work now that I think of it)
Speaking of sensible defaults, with regards to your component registration, you can just add some overloads which will handle default cases for you.
registerComponent(Counter);
can automatically construct a default serializer and deserializer.
registerComponent(Counter, {serializer: ..., deserializer: ...})
would accept anything else, plus any other options you might want to add later on.
Personally, I think this goal is a little unintuitive considering the size and complexity of a game that would bother implementing ECS.
I don't think that the format must be intuitive, since ideally no one will touch the raw data in there. On the other hand, having a world with thousands of entities with various components each could quickly blow up in file size, so I at least minimized the format overhead in a simple way. I was thinking about a binary format, too, but I don't see too much gain out of it right now (I would first need to get some other things in place). The format itself is at the moment not the top priority, until I get real feedback that it has to be improved :)
implement things as extensions or add-ons
I am not sure if a simple ECS would need an ecosystem like that. I guess it would be more confusing just to be able to change out the save format, so I would rather have it simple and opinionated. Which does not contradict having importers or exporters for human-readable data!
you can just add some overloads which will handle default cases for you.
My thoughts exactly! I will take this on next, since it is overly complex the way it is right now.
Hi @vmwxiong , I added a prefab un-/loader, which uses the format from above. You can check out the code changes in a0352a096769d763f4b8d1aff03964d582efdb47 and take a look at this prefab for a possible example. MenuButton
would be a component in this case, and the object associated would contain the component's data. The prefab can be loaded using world.loadPrefab()
(don't forget to maintain the world afterwards). The prefab could, of course, be written in JSON. The API takes an array of objects, so the JSON would have to be JSON.parsed before passing it, but I don't think that's a problem.
This project looks super promising! I've been looking around for ECS implementations for the web, and so far none of them seemed to expose components first, as opposed to returning entire entity references from any given query. Excited to see where this goes.
Only feedback I had initially was that it seems like it might be a bit clunky to manually create entities. From a collaboration perspective, you'll frequently have designers with a weak technical background creating enemies or powerups for a game, and they need to be able to quickly throw together a new entity without code-heavy syntax. Granted, this feedback is just going off of example entity creation you have in your readme, so there might be some undocumented ways to create entities that I'm just missing. Just as an idea, I think you might want to have something like an in-line deserializer that lets a designer create an entity with an object literal or a piece of json.
For instance:
Or even something like
Both of these examples might be awful though, ultimately the goal is to create an API that is approachable for non-technical contributors to a game.