I've been working on a MUD in Clojure a bit lately. Initially I started with ECS using immutable data because I wanted automatic concurrency. Systems declared what components they needed to work, and the core of the system would examine what components each system used and automatically parallelize them. Systems could say if they only read it, or updated it, or appended it (two systems appending but not reading or updating the same data can run concurrently), if they could use stale data, etc.
Components were just a key in my game state hash map, which used immutable data, so if something said it only read (but didn't write) a component, but it was lying, there would be no bug - the core is responsible for actual updates returned from systems, not the systems themselves.
But then... where do you stick "globals"? Sometimes systems need access to common non-component data. You could hack it and stick it in components, which felt weird.
So I kinda changed my design. Now, at the core, I have systems. And systems declare what keys in game state it needs, similar to how they did with components. But it doesn't have to be only for components. It can be any key or "key path" (nested maps). And systems are automatically ran concurrently based upon this.
I happen to arrange entity data as components, because that increases the concurrency of the systems. But it doesn't have to. Sometimes I split things up for the sake of concurrency.
This is also really easy to test. You just have functions that take data spitting out data.
Components were just a key in my game state hash map, which used immutable data, so if something said it only read (but didn't write) a component, but it was lying, there would be no bug - the core is responsible for actual updates returned from systems, not the systems themselves.
But then... where do you stick "globals"? Sometimes systems need access to common non-component data. You could hack it and stick it in components, which felt weird.
So I kinda changed my design. Now, at the core, I have systems. And systems declare what keys in game state it needs, similar to how they did with components. But it doesn't have to be only for components. It can be any key or "key path" (nested maps). And systems are automatically ran concurrently based upon this.
I happen to arrange entity data as components, because that increases the concurrency of the systems. But it doesn't have to. Sometimes I split things up for the sake of concurrency.
This is also really easy to test. You just have functions that take data spitting out data.