Collectable Items, Persistence and ECS Architectures

Started by
2 comments, last by Zipster 8 years, 9 months ago

My current project is a 2D platformer where the whole game is made up of a series of "rooms" (like a level of Shovel Knight, but without a world map to split it up).

My rooms are currently defined like this (pseudo code):


room = {
    tilemap = "tilemap.tmx",
    init = {
         // Perform the initialisation of anything not in the tilemap, e.g. enemies and collectable items
    },
    // Other irrelevant stuff
}

Each time you enter a room, the init code is run and all of the enemies/collectables you killed/picked up/whatever are all back in their original place. This is exactly how I want this to work.

At the moment saving the game is simple, I just serialize out the state of the player, which room they are on and that's it.

The problem I'm having is that I'm now at the point where I want to add one-off collectables (like the morph ball in the Metroid games) or one-off enemies (e.g. an extra hard one that spawns only once), once you've collected/defeated them, they are gone - and when you enter/re-enter a room they shouldn't be re-spawned along with everything else.

I'm struggling to come up with a concept to handle this without some kind of global manager to look after everything which feels a bit wrong to me.

I'm working with an ECS architecture (as a purely educational exercise, I don't want to go into the merits of ECS vs. non-ECS), and I know that I shouldn't try and force everything into it, but it feels like this is a problem that I should be able to solve with it.

As a rough stab at it I was thinking of the following:

  • a Persistence component which stores a key
  • a PersistenceSystem which keeps track of these keys and their state
  • when a Persistence component is added to an entity, if the PersistenceSystem says the entity shouldn't exist anymore, destroy it (this is to handle the case when I reload a room)
  • when I save the game I serialize the keys and their states from the PersistenceSystem along with my other data

Is this is a sensible approach? Is this something that should sit outside of the ECS? How have other people tackled this problem?

Advertisement


Is this is a sensible approach? Is this something that should sit outside of the ECS? [...]

IMHO your approach is fine because it allows the level designer to deal with the problem in a consistent way (assuming that s/he is familiar with ECS, of course ;) ) and it seems to fit smoothly into the technical point of view, too.

The only issue I personally would have with that particular solution is that the term "persistence" is broader. I.e. if I would hit a Persistence component I would understand it as a (perhaps even) collection of attributes that persist a room switch. It seems not clear why "persistence"


[...]How have other people tackled this problem?

Well, I do not have this exact problem because in my engine the overall state is persistent anyway. So it includes the persistence but without the need of an explicit Persistence component. A spawn point that is never disabled and triggered on a "entering room" signal then will spawn each time, and one that is disabled does not.

I'd just give the player's state a vector for his "collection" (invisible) of unique entity type IDs.

For collectible items:
- When touched by the player, they add their own entity type ID to the player's collection and then kill themselves.
- Next time when created, if the entity sees the player already has its type ID, it self-destructs silently on map load.

For unique boss-like enemies:
- When killed by the player, add their own entity type ID to the player's collection, etc...
- Next time when created, etc...


You could use the same system for other kinds of non-unique collectibles as well, like gems/coins/whatever.
Just make the 'collection' actually an unordered map of (typeID, count)
Coin:
 - OnCreation : { do nothing }
 - OnTouch : { player.collection[coinEntityType]++; destroySelf(); }
And you could have doors that open when you need X amount of coins, or that open after you defeat enemy Y.
LockedDoor (entityTypeToMonitorID, numRequired)
OnCreation : { start off closed }
OnTick()
{
    bool shouldBeOpen = (player.collection[entityTypeToMonitorID] >= numRequired);

     if(closed && shouldBeOpen)
     {
          this->beginOpenAnimation();
     }
}

//Opens once you collect one or more 'bossEnemyTypeIDs' (gained from killing that boss).
LockedDoor bossLockedDoor(bossEnemyTypeID, 1);

//Opens once you collect 250 coins.
LockedDoor doorLockedByCoins(coinTypeID, 250); 
What this doesn't easily handle is collectibles that don't respawn but have multiple instances throughout the world (for example, Metroid Life Tanks or Rocket Extensions). With this system, you'd have to make each lifetank have a different EntityTypeID or something, which could get annoying during development - but with some additional thought, you could extend this system to make that kind of collectible easier.

To me at least, it's very strange to persist state for non-existent entities, especially for the sole purpose of indicating that the entity shouldn't exist. It's also weird to create entities when you know beforehand that they're just going to be destroyed. Waste of cycles and resources aside, you run the risk of other systems detecting or interacting with these entities in that limbo state and modifying your gamestate in unexpected and difficult-to-debug ways.

haegarr hinted at the notion of spawn points, which is how I've solved this sort of problem in the past. Spawners (as I like to call them) are logic-only entities that exist solely to create other entities. In fact, they can be used to manage the entire lifetime. Instead of placing enemies and collectibles directly into the level, you place spawners that are configured to create the enemies are collectibles. When a room is entered, the spawners are created and they check for and load their persisted data. This data includes, but is not limited to, whether or not they've already spawned their entity (or entities), and whether or not it/they can spawn again (generally speaking, this could be a "max spawn count" parameter with a special "infinity" setting that means no limit). When you leave a room, the spawner state is persisted, and the spawner can chose to destroy its entities based on a flag or other setting (i.e. "auto-destroy if room left"). If the entity isn't destroyed, then it and its link to the spawner can also be persisted. Let's say that you have a boss that only appears once, but you don't want its health to reset when the player leaves the room. The "only appears once" is handled by the persistent spawner, the health is managed by persisting the boss state (as long as it's alive), and once the boss is killed, it no longer leaves a trace of data behind.

And of course, since spawners are just entities themselves, you can theoretically have spawners that create spawners, which then create entities (or perhaps more spawners!). The possibilities start to explode from there, but I'd still caution against too many levels of indirection; things can quickly get out of hand.

This topic is closed to new replies.

Advertisement