3
\$\begingroup\$

I am currently experimenting a bit with C# and as a way of learning I am making a very simple 2D game (asteroids-ish).

I am using the monogame framework and the general plan of attack was to have all data "flow one way", if that makes sense? In the sense that I have multiple layers of objects such as this:

CoreGame.Update(gametime) -> 
   Scene.Update(gametime) -> 
      GameManager.Update(gametime) (if scene is game scene) -> 
         EntityManager.Update(gametime) -> 
            Entity.Update(gametime) -> 
                Component.Update(gametime)

Where every subsequent layer has a many-to-one relationship with the previous layer (an Entity can have many components, an entity manager can have many entities, but an entity can only have one entity manager).

I also use a simple ECS scheme as can probably be inferred. However the "data only runs one way" thinking hits a snag when events occur such as a spaceship firing a laser for example, which requires the EntityManager to be informed to add this entity to its list of entities etc, sounds must be generated etc. But the actual event occurs at the component level so data would have to travel "upwards".

So I figured I would make a messaging system that dispatches a message to the layer above it, which the layer then handles that message or forwards it to the next layer. The message itself would be added to a concurrent queue of Actions which are emptied and invoked in each update call for each layer.

Some code to show what I mean:

namespace SpaceGame.Engine.ECS {

    public class EntityManager : IRecv<EntityManagerMsg> {
        #region properties
        private ConcurrentQueue<Action> _recvActions;
        public MessageSendingService MsgService { get; set; }
        public Dictionary<Guid, GameEntity> Entities;
        public ConcurrentQueue<Action> RecvActions { get => this._recvActions; set => this._recvActions = value; }
        private Camera camera;
        #endregion

        public EntityManager(ref Camera camera) {
            this.MsgService = new MessageSendingService();
            this.Entities = new Dictionary<Guid, GameEntity>();
            this.RecvActions = new ConcurrentQueue<Action>();
            this.Camera = camera;
            var player = AssetManager.CreatePlayer ( new Vector2 (400,300) );
            this.AddEntity ( player );
        }

        public void AddEntity (GameEntity entity) {
            entity.MsgService.Subscribe(OnRecv);
            this.Entities.Add(entity.Id, entity);
            return;
        }

        public void RemoveEntity(Guid id) {
            if (this.Entities.ContainsKey(id)) {
                this.Entities[id].MsgService.UnSubscribe(OnRecv);
                this.Entities.Remove(id);     
            }
        }

        public void Update (GameTime gameTime) {
            while (_recvActions.TryDequeue (out var msg)) {
                msg.Invoke();
            }
            Parallel.ForEach(this.Entities, KeyValuePair => {
                KeyValuePair.Value.Update(gameTime);
            });
            
            //will handle collisions here
        }

        public void Draw (SpriteBatch sb) {
            foreach(GameEntity entity in this.Entities.Values) {         
                entity.Draw(sb);
            }
        }

        public void HandleIncomingMessages (EntityManagerMsg msg) {
            switch (msg) {
                case SpawnBallMsg spawnBallMsg:
                   var ball = AssetManager.CreateBall(
                      new Vector2(
                         spawnBallMsg.XPos, 
                         spawnBallMsg.YPos), 
                      new Vector2(
                         spawnBallMsg.XSpeed * 6, 
                         spawnBallMsg.YSpeed * 6),
                      spawnBallMsg.Owner
                    );
                    this.RecvActions.Enqueue( () => this.AddEntity (ball));
                    return;

                case IsKilledMsg killedMsg:
                    this.RecvActions.Enqueue( () => this.RemoveEntity(killedMsg.EntityId));
                    return;

                case SpawnFighter fighterMsg:
                    var fighter = AssetManager.CreateFighter(fighterMsg.Id, new Vector2(fighterMsg.XPos, fighterMsg.YPos));
                    this.RecvActions.Enqueue( () => this.AddEntity(fighter));
                    return;
                default:
                    return;
            }
        }

        public void OnRecv (object source, Msg msg) {
            if (msg is EntityManagerMsg emsg) {
                HandleIncomingMessages(emsg);
            }
            else {
                MsgService.ForwardMsg(msg);
            }
        }
    }
}

This is an example of a layer which implements the IRecv message interface and has a has a msg service composition.

Finally the code for the message passing:

namespace SpaceGame.Engine.MessageSystem {
    public interface IRecv<T> where T : Msg {
        ConcurrentQueue<Action> RecvActions { get; set; }
        void HandleIncomingMessages(T msg);
        void OnRecv(object source, Msg msg);
    }
}

namespace SpaceGame.Engine.MessageSystem {
    public class MessageSendingService {
        #region properties
        private EventHandler<Msg> Msgs { get; set; }
        #endregion
        protected void OnDispatch(Msg msg) {
            this.Msgs?.Invoke(this, msg);
        }

        protected void OnDispatchAsync(Msg msg) {
            Task.Factory.StartNew(() => this.OnDispatch(msg));
        }

        public void Dispatch(Msg msg) {
            OnDispatchAsync(msg);
        }

        public void ForwardMsg(Msg msg) {
            OnDispatch(msg);
        }

        public void Subscribe(Action<object, Msg> func) {
            EventHandler<Msg> a = func.Invoke;
            this.Msgs += a;
        }

        public void UnSubscribe(Action<object, Msg> func) {
            EventHandler<Msg> a = func.Invoke;
            this.Msgs -= a;
        }
    }
}

And the messages are structured like this (removed some messages for brevity but you get the point).

namespace SpaceGame.Engine.MessageSystem {

    abstract public class Msg : EventArgs {}
    
    abstract public class EntityManagerMsg : Msg {}

    public class IsKilledMsg : EntityManagerMsg {
        public Guid EntityId { get; set; }
        public IsKilledMsg(Guid eId) {
          this.EntityId = eId;
        }
    }

    abstract public class EntityMsg : Msg {}
    abstract public class GameManagerMsg : Msg {}

    public class ExitGame : Msg {
        public ExitGame() {}
    }
}

So in this case a StatComponent would then call dispatch new IsKilledMsg(this.Entity.Id) which would send it from the StatComponent -> Entity where the entity would see that it is not an EntityMsg and forward it to the EntityManager who would then insert the command to its queue to remove it from its entity list.

Every message is also sent asynchronously, I have tested this a bit and it seems to work fine but is there a problem with this design? Or is it a rational way of making this game? Any improvements, potential issues or other ways of doing it that are better?. I know it is a long post and I apologize, but if you read through it I am most appreciative.

\$\endgroup\$
2
\$\begingroup\$

Some general advice:

You need to think about the pros/cons of each step in your design.

For example, you've said you would like all the data to "flow one way"

  • Are there benefits to doing this?
  • Do the benefits outweigh the problems you have listed?

If I were in your shoes, I'd opt for the simplest solution I could think of. If simple solutions don't work, then you can start looking for complicated ones.

In the words of Bill Gates, “I choose a lazy person to do a hard job. Because a lazy person will find an easy way to do it.”

Problem specific answer

You've mentioned that your message system works asynchronously/concurrently. I'd absolutely avoid having asynchronous operations unless they are needed. There's plenty of resources out there listing difficulties in concurrent programming.

A message system itself is not a bad idea. I wouldn't recommend using one in a simple game though; it seems like an overkill. See here for an overview of a game message system.

With the laser spawning issue, you could have a Queue in your entity manager that holds any new entities spawned in that frame.

At the end of the frame, the entity manager can move the entities from the Queue to whatever data structure the active entities are stored in.

You've mentioned that you have multiple entity managers, which adds a complication; since entities would then need a reference to their entity manager. The solution to this depends on how you've divided the entity managers up:

  • If it's one entity manager per type of entity, you can have a global reference that any entity can access. Creating a new entity is as simple as pushing it onto the relevant entity manager.
  • If it's one entity manager per level, you can set up a global getter that points to the entity manager that relates to the current level.

This way, no entity has to store a reference to its entity manager.

Finally, your component-based design is a popular architecture. Unreal Engine is a commerical engine that uses it. However, it isn't without issues. For a simple game as you describe, I would go for an inheritance-based design. I would imagine that all your entities have many overlapping properties, which is what inheritance-based design is effective at. Your entity base class can have properties such as location, sprite, and collision box.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Hello thank you for your response. I realize that this is laughably over-designed for what it is. I more wanted to sort of make it extendible for the future if I wanted to make it more complex/re-usable for an entirely different game. I am wondering however, how would the asynchronous behaviour be a problem in this instance as the messages themselves are virtually immutable and I am adding them to a threadsafe queue? \$\endgroup\$ – kiooikml Sep 3 at 19:35
  • 1
    \$\begingroup\$ No problem @kiooikml! It's understandable to want to design perfect low-level systems from the start. Unfortunately it's difficult to do, as it involves having solving problems that haven't arisen yet! The nature of programming is to find a solution to a problem, and then revisit it later when a new problem arises. I suggest you extensively modularise your systems. For example, you can create an API that can add entities, and under-the-hood it might simply add the entity directly to the entity manager for the time being, but in the future you can change it to use a message system. \$\endgroup\$ – 0liveradam8 Sep 3 at 19:43
  • 1
    \$\begingroup\$ I'll use Unreal Engine as an example again; they haven't simply created a great engine from scratch. They continually develop their game Fortnite, and push new features back to the engine if they think it will be usable in other games. \$\endgroup\$ – 0liveradam8 Sep 3 at 19:46
  • 1
    \$\begingroup\$ Regarding your question about asynchronous behaviour being an issue. It isn't an issue, you can use that approach, given that you are aware of issues surrounding concurrent programming (see my answer). However, it's simply unnecessary. You need to ask yourself "What would happen if I didn't use an asynchronous approach?" The benefit is a much simpler message system. I can't see any drawbacks from avoiding the async approach judging by your original post. \$\endgroup\$ – 0liveradam8 Sep 3 at 19:54
  • 1
    \$\begingroup\$ Ok I get you, the reasoning for the asynchronous nature of it was the intention of maybe adding a simple multiplayer mode. But as it is now, you are correct it really does not add anything, it may even slow the entire thing down from the overhead by running threads. \$\endgroup\$ – kiooikml Sep 3 at 20:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.