ecsx-framework/ECSx

Support for turn-based apps

Opened this issue ยท 6 comments

An idea which came up was the option to remove automatic tick events, while exposing a function to manually trigger a tick at the end of the turn.

After further consideration, a full-manual mode might not be the way to go. Even in turn-based scenarios, we probably don't want to totally shut off the Manager. The best way forwards here might be to write a clean, efficient implementation of a turns mechanic using standard ECS design principles, and include it in the documentation as a guide.

Nezteb commented

@APB9785 does that mean I should close #60? ๐Ÿ˜„

Hi @APB9785! Do you have any updates on this? I'm ultra curious about how that looks like in ECSx.

I'm testing some approaches for a turn-based MMO that uses LiveView and right now I'm mainly relying on a single GenServer to control battle states - it's mostly event-based and it's working pretty well tbh. So, I'd very much like to understand how ECSx could help in these specific cases (considering a more data-drive approach).

It goes without saying, but props to the excellent work on ECSx. Cheers!

@thiagomajesk Sorry for the late reply! I appreciate the kind words, and your interest in ECSx. ๐Ÿ™‚

  • However you organize your players, whether it's into rooms, games, lobbies, instances, realms, etc... Assuming each of those is an Entity, you can add an ActivePlayer or ActiveTeam Component, which contains the value of whose turn it is
  • If you have more than two players/teams per turn rotation, you could give them NextPlayer Components storing the Entity of the next player after them, or a TurnRotationPosition storing an integer, etc. So your turn System will know who gets to be the next active player/team.
  • If you just have one big game happening - i.e. it isn't divided into rooms or instances - a Tag can be used such as IsActive which marks the active player/team.

Hope this gives you some ideas while we consider an "official approach" for the documentation

Thanks for your reply, @APB9785!

So, if I got it right because the game world will always be modeled using components, the turns have to be contained within the main game loop as well, which means the game never really stops running. Ok, that makes sense because it's how the architecture is supposed to work, but now I'm wondering if you have done any experiments with systems that have variable tick rates (or maybe systems that only run when some events happen).

Let me give you an example: Say you have a forest with some trees that can be taken down by players, and once those trees have been taken down, they'll respawn after 30 minutes. In this case, I could have a TreeRespawn system that only has to run 30 minutes after I have attached the tag IsTakenDown to a tree.

I know you can mix events with ECS, but I'm essentially thinking more in terms of resource usage and I think this could be very helpful for building turn-based games. Have you seen something like this while studying to build ECSx? I'm not sure if this is a completely different pattern or something that you can build on top of an ECS engine.

@thiagomajesk

systems that only run when some events happen

This could just be an Elixir function which gets called at the time and place where the event is happening

For example, let's say you want players to have Hit Points and the player dies when HP reduces to zero. You could take one of two options:

  • Have a Death System which runs toward the end of every tick, checking for players with zero HP and running your kill_player logic whenever it finds one
  • Every time a player's HP is reduced, check if it went down to zero, and if so, call kill_player right there in the AttackDamage or Collision or whatever System is causing the damage

There are some tradeoffs to either approach - I would lean towards the former, since ECSx can now search for specific values in constant time with index: true (i.e. it is extremely fast to check for all HitPoints components where the value is exactly 0). A nice benefit of the former approach is that your logic is organized neatly into a single System, whereas with the latter approach, you have multiple systems sharing the same code.

There is also a "third way" so to speak, in cases where searching for a specific value is not possible, but you want to keep the aforementioned code organization benefit. When the event happens (like the player dropping to zero HP) you can create a Tag such as ReadyToDie for the player and then your Death system simply reads ReadyToDie.get_all() each tick, which is almost always an empty table.

In this case, I could have a TreeRespawn system that only has to run 30 minutes after I have attached the tag IsTakenDown to a tree

Early in development, we tried including an option for only running systems periodically, but decided against this because it makes it more difficult to accurately monitor your application's performance with ECSx.LiveDashboard. We highly encourage everyone to use the LiveDashboard; it can be a huge benefit to see the "big picture" as you add more and more Systems into your app.

There is a workaround, however, which I would recommend to use sparingly for the reason mentioned above. Since you have an Elixir app, with a regular application.ex supervision tree, nothing is stopping you from adding your own processes outside ECSx, such as a GenServer which tracks a 30 min cooldown. The limitation is that your GenServer will not have any write access to your Components. Therefore you must consider the GenServer (and any other processes you spin up outside of ECSx) to be a Client (similar to a LiveView) and use ClientEvents to delegate the respawn task to an ECSx System.

For this specific case, I personally would add a RespawnAt component with a timestamp 30 minutes in the future, whenever the tree is taken down. Then the TreeRespawn system checks just these components (every tick) to see if the time is up. This should not become a performance concern until the number of trees grows to a very large amount, and at that point there are still some techniques to squeeze out more, such as using integer timestamps with search + index: true