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