Sunday, September 27, 2015

State / Screen management in Entity Component Systems


My entity/component system is happily humming along and, despite some performance concerns I initially had, everything is working fine.



However, I've realized that I missed a crucial point when starting this thing: how do you handle different screens?


At the moment, I have a GameManager class which owns a component manager and entity manager. When I create an entity, the entity manager assigns it an ID and makes sure it's tracked.


When I modify the components that are assigned to an entity. an UpdateEntity method is called, which alerts each of the systems that they may need to add or remove the entity from their respective entity lists.


A problem with this is that the collection of entities operated on by each system is determined solely by the individual Systems, typically based on a "required component" filter. (An entity has to have a Renderable component to be rendered, for instance.)


In this situation, I can't just keep collections of entities per screen and only Update/Draw those collections. They'd have to either be added and removed depending on their applicability to the current screen, which would cause their associated components to be removed, or enable/disable entities in a group per screen to hide what's not supposed to be visible.


These approaches seem like really, really crappy kludges.


What's a good way to handle this? A pretty straightforward way that comes to mind is to create a separate GameManager (which in my implementation owns all of the systems, entities, etc.) per screen, which means that everything outside of the device context would be duplicated. That's bothersome because some things are always visible, or I might want to continue to display the game under a translucent menu window.


Another option would be to add a "layer" key to the GameManager class, which could be checked against a displayable layer stack held by the game manager. *System.Draw() would be called for each active layer, in the required order as determined by the stack. When the systems request an iterator for their respective entity collections, it would be pre-filtered to a (cached) set of those entities that participate in the active layer. Those collections could be updated from the same UpdateEntity event that's already used to maintain each system's entity collections.


Still, kinda feels like a hack. If I've coded myself into a corner, feel free to throw tomatoes as long as they're labeled with a helpful suggestion.


Hooray for learning curves.




Answer



After trying a few different approaches, I implemented the following, and it's working quite well.


Briefly:


I have a GameManager class which is responsible for maintaining systems, components and resources.


public class GameManager : Disposable
{
public EntityManager EntityManager { get; private set; }
}

GameManager is the game's primary interface to the engine. It provides methods for creating entities, etc. The relevant part here is EntityManager (abbreviated, relevant methods shown):



public class EntityManager
{
public List Entities { get; private set; }
internal Dictionary Caches { get; private set; }

private Channel _nextChannel = null;

internal EntityCache CreateCache(string name, params Type[] filter)
{
name = name.Trim().ToLower();


if (Caches.ContainsKey(name))
throw new Exception("A cache of the same name already exists");

var cache = new EntityCache();
cache.Filter = filter;

Caches.Add(name, cache);

return cache;

}

private bool MatchesFilter(IEntity entity, IEnumerable filter)
{
return filter.All(t => entity.Is(t));
}

internal void PreUpdate(float dt)
{
if (_nextChannel != null)

{
foreach (var kv in Caches)
{

var cache = kv.Value;

cache.ActiveItems.Clear();
cache.ActiveItems.AddRange(
cache.Items.Where(
(entity) =>

{
var enable = !entity.Is() || entity.As() == _nextChannel;
entity.Enabled = enable;
return enable;
}
)
);
}
_nextChannel = null;
}


}
///
/// update the caches by adding or removing the specified entity, based on its assigned components
///

///
internal void Update(IEntity entity)
{
if (!Entities.Contains(entity))
Entities.Add(entity);


if (Manager.Components.ContainsKey(entity))
{
var components = Manager.Components[entity];

foreach (var cache in Caches.Values)
{
var matches = MatchesFilter(entity, cache.Filter);
var contains = cache.Items.Contains(entity);


var active = entity.Is() ? (entity.As() == _nextChannel) : true;

if (matches && !contains)
{
cache.Items.Add(entity);
if (active)
cache.ActiveItems.Add(entity);

}
else if (!matches && contains)

{
cache.Items.Remove(entity);
}

}
}
}

public void SetChannel(Channel channel)
{

_nextChannel = channel;
}

}

EntityManager maintains filtered caches of entities:


internal class EntityCache
{
///
/// list of component types that this filter requires

///

public Type[] Filter { get; internal set; }

///
/// all items in the system that match this filter (set by EntityManager)
///

public List Items { get; private set; }

///
/// Items that are active (should be updated, drawn, etc.)

///

public List ActiveItems { get; private set; }

internal EntityCache()
{
Items = new List();
ActiveItems = new List();
}
}


Also, a Channel component is used to group entities into groups/channels/screens. (Channel exposes a ChannelId property used to separate groups.)


When EntityManager starts its Update cycle, it checks if a channel change request has been received. If so, it rebuilds the ActiveItems lists. Entities that have a Channel component attached are added to the ActiveItems collection if their channel is the same as the next selected channel. Entities that do not have a Channel component are always active (good for global input processors, debug displays, loggers, etc.)


Systems only operate on the ActiveItems collections during the Draw and Update cycles.


So, when initializing a game, I might do this:


var playChannel = new Channel("play");
var menuChannel = new Channel("menu");

var ship = GameManager.Create("ship");
...add ship rendering components...
ship.Add(playChannel);


var splashLogo = GameContent.Create("splash-screen-logo");
...add logo rendering components...
splashLogo.Add(menuChannel);

Manager.EntityManager.SetChannel("menu");

The last line updates the ActiveItems collections, so on the next cycle, any entities with an attached Channel with ID "menu" will be drawn and updated, but everything else will be ignored. An input processor switches channels when required.


I also added a small serialization-friendly state machine which handles navigation between screens, but that's another topic.


I initially had each System maintain its own entity caches, but I found myself creating systems which required the same component filters, so those caches would be duplicated. Moving them into an EntityManager provided a centralized interface to all of the entities in the game, made implementation of the screen/channel system fairly trivial, and made it much easier to remove entities and components from the game when necessary.



Hope this helps someone!


No comments:

Post a Comment

Simple past, Present perfect Past perfect

Can you tell me which form of the following sentences is the correct one please? Imagine two friends discussing the gym... I was in a good s...