Loading…

What The Heck Just Happened…to The Peacenet?

The Peacenet’s been going on a huge rollercoaster the past few weeks and it’s been kind of a mess. In the screenshot above you can see a blank MonoGame window that indicates that it is The Peacenet. So what the heck just happened?

Long story short…

…We’re rewriting, again. I’d mentioned before that we were going to revive the Peace Engine – and while that’s certainly the case, that’s a story for another time. The Peacenet however is being written on its own. We’ve noticed some patterns (ha, ha) with development of The Peacenet that have become a noticeable problem.

The Longhorn Effect…

A big issue I’ve been noticing with The Peacenet’s development – and been personally responsible for – is something called “the Longhorn Effect.

Essentially this is what happened with Windows Longhorn/Windows Vista (hence the name.) Microsoft put a ton of features into Longhorn, a lot of things got implemented that were out of the project’s scope, the code was a mess, the program was buggy as hell, everything got hard to maintain and BAM, the Reset happened. Well this is that.

With peacenet we’ve kept on planning these huge features, trying to implement a ton of things that were ALREADY PROVIDED FOR US by the framework (both Unreal and MG), now our code’s a mess, it’s buggy too, it’s hard to maintain, and bam, we get annoyed and frustrated and go to something else. It’s our own recurring Longhorn Effect!

So what’s happening?

The window in the screenshot above is a .NET Core project I’m working on. It’s called Peacenet but it’s really just a framework ontop of MG. The scope of it is to implement…

  • an event system
  • a module manager
  • a GUI.

These are the only things the framework needs. An event system, a module manager, and a GUI. And behind that blank window is… exactly that. (Minus the GUI, that’s coming later.)

So here’s what I have so far.

The Event System

This addresses a major issue with old Peace Engine which was how it handled input events. it used MonoGame.Extended.Input to actually manage and dispatch input events – which was fine – until we wanted to dispatch other kinds of events. Now we have two completely different ways of receiving events which are incompatible. Whereas now, I’ve written a central event manager class that acts as a MonoGame service. Here’s it’s code.

using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;

namespace BitPhoenix.Peacenet.Core
{
    public class EventManager
    {
        private Logger _logger = null;
        private Queue<Event> _eventQueue = null;
        private object _mutex = new object();
        private List<IEventDispatcher> _dispatchers = new List<IEventDispatcher>();

        public void Listen<TEvent>(Func<TEvent, bool> handler) where TEvent : Event
        {
            var dispatcher = new EventDispatcher<TEvent>(handler);
            _dispatchers.Add(dispatcher);
        }

        public void StopListening<TEvent>(Func<TEvent, bool> handler) where TEvent : Event
        {
            _dispatchers.Remove(_dispatchers.First(x => (x as EventDispatcher<TEvent>).Handler == handler));
        }

        public void Submit(Event e)
        {
            lock (_mutex)
            {
                _logger.Log(e.ToString());
                _eventQueue.Enqueue(e);
            }
        }

        internal EventManager(RunLoop runLoop)
        {
            runLoop.Services.AddService(this);
            _logger = runLoop.Services.GetService<Logger>();
            _logger.Log("Registered event manager...");

            _eventQueue = new Queue<Event>();
        }

        private void Dispatch(Event e)
        {
            _logger.Log(e.ToString());
            foreach(var dispatcher in _dispatchers)
            {
                if(dispatcher.Dispatch(e))
                {
                    break;
                }
            }
        }

        internal void Update(GameTime gameTime)
        {
            if(!gameTime.IsRunningSlowly)
            {
                lock (_mutex)
                {
                    int events = _eventQueue.Count;
                    for (int i = 0; i < 20 && i < events; i++)
                    {
                        var e = _eventQueue.Dequeue();
                        Dispatch(e);
                    }
                }
            }
        }
    }
}

So let’s deconstruct what this code is doing and how we can use it internally.

The Event Class

Every event bases off this “Event” class:

using System;
using System.Collections.Generic;
using System.Text;

namespace BitPhoenix.Peacenet.Core
{
    public abstract class Event
    {
        public bool Handled { get; private set; }

        public void Handle()
        {
            Handled = true;
        }
    }
}

All this is, is an abstract object that can be handled. Call “Handle()” on it and it’s handled. Nothing more. From there you can base different kinds of events – “KeyEvent,” “MouseEvent,” etc – that can be submitted to the event manager. Like this:

using System;

namespace BitPhoenix.Peacenet.Core
{
    public abstract class MouseEvent : Event
    {
        public int X { get; }
        public int Y { get; }

        public MouseEvent(int x, int y)
        {
            X = x;
            Y = y;
        }
    }

    public sealed class MouseMoveEvent : MouseEvent
    {
        public int DeltaX { get; }
        public int DeltaY { get; }

        public MouseMoveEvent(int x, int y, int deltaX, int deltaY)
            : base(x, y)
        {
            DeltaX = deltaX;
            DeltaY = deltaY;
        }
    }
}

Event Submission (Input Manager)

In the above example we create a “MouseEvent” and a “MouseMoveEvent.” All MouseEvents must hold the mouse’s X and Y position, so it gets its own base class with a constructor and properties for those values. Then we create a MouseMoveEvent which takes in a delta X and Y value for the mouse and holds that information as its own. Then, for example, in the Input Manager class, we can do this.

using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework.Input;
using System.Text;

namespace BitPhoenix.Peacenet.Core
{
    public sealed class InputManager
    {
        private GameWindow _window = null;
        private Logger _logger = null;
        private EventManager _eventManager = null;
        private int _mouseX = 0;
        private int _mouseY = 0;

        internal InputManager(RunLoop runLoop)
        {
            runLoop.Services.AddService(this);

            _window = runLoop.Window ?? throw new InvalidOperationException("Game window must be created first.");
            _logger = runLoop.Services.GetService<Logger>() ?? throw new InvalidOperationException("Logger must be initialized first.");
            _eventManager = runLoop.Services.GetService<EventManager>() ?? throw new InvalidOperationException("Event manager must be registered first.");
            _logger.Log("Input manager registered...");

            runLoop.IsMouseVisible = true;

            InitDefaults();
        }

        private void InitDefaults()
        {
            var mouse = Mouse.GetState();
            var keyboard = Keyboard.GetState();

            _mouseX = mouse.Position.X;
            _mouseY = mouse.Position.Y;
        }

        private void FireMouseEvents(MouseState mouse)
        {
            if(_mouseX != mouse.Position.X || _mouseY != mouse.Position.Y)
            {
                int xDelta = mouse.Position.X - _mouseX;
                int yDelta = mouse.Position.Y - _mouseY;

                _mouseX = mouse.Position.X;
                _mouseY = mouse.Position.Y;

                if(_mouseX >= 0 && _mouseY >= 0 && _mouseX <= _window.ClientBounds.Width && _mouseY <= _window.ClientBounds.Height)
                {
                    _eventManager.Submit(new MouseMoveEvent(_mouseX, _mouseY, xDelta, yDelta));
                }
            }
        }

        internal void Update(GameTime gameTime)
        {
            var mouse = Mouse.GetState();

            FireMouseEvents(mouse);
        }
    }
}

I ommitted a ton of code from the actual Input Manager but the idea’s there. We test for mouse movements each frame, check if the mouse is on-screen, and send new MouseMoveEvents to the event manager – providing the cursor’s current and delta positions. This accomplishes a lot:

  • All events are centralized in the Event Manager. Anything can submit to it. Anything can receive from it. It determines when things get sent out.
  • The event manager doesn’t exactly need to KNOW who it’s dispatching to, just whether or not an event of x type can be dispatched to a handler of y type and whether said handler actually handled the event. We can also decide to stop sending events while the game’s running slowly – to give it a chance to catch up.
  • The event manager is generic, meaning as long as the event is an event, it can be submitted and it can be received.

Event Listeners

So we now have a centralized event system. And this is how easy it is to use:

using System;

namespace BitPhoenix.Peacenet.Core
{
    public class CoolService
    {
        internal CoolService(RunLoop runLoop)
        {
            runLoop.Services.AddService(this);

            _logger = runLoop.Services.GetService<Logger>() ?? throw new InvalidOperationException("Logger must be registered first.");
            _eventManager = runLoop.Services.GetService<EventManager>() ?? throw new InvalidOperationException("Event manager must be registered first.");

            _eventManager.Listen<MouseMoveEvent>(MouseMove);
        }

        private bool MouseMove(MouseMoveEvent e)
        {
            _logger.Log("Received mouse move!");
            return true;
        }
    }
}

So we grab the event manager service and throw if it’s not registered yet. (Same with logger but that’s just so we can log to the console and debugger with more detailed info about where the log came from). Then we tell it “We’re going to listen to this kind of event (MouseMoveEvent) and handle it with this function (MouseMove).” THAT’S ALL YOU NEED TO DO. Just tell it what events you want to receive and give it a function to send ’em to.

Module Manager

This leads me to the Module Manager – which is work in progress. But, let’s look at the Input Manager, Cool Service, and Event Manager. There’s a bit of a problem with them. That is that they’re internal, and have to be manually constructed. They also have to manually retrieve any service they depend on. This is how MonoGame’s service locator works. These are the problems right now.

  • You need a RunLoop so you can register your service with it, and so you can request other services.
  • The Run Loop needs to spawn these services in order, otherwise you’ll get missing services. Can’t do that if you’re, say, defining the service in a different assembly.
  • That’s a lot of boilerplate. Yuck.

Wouldn’t it be nice if this could be automated and managed by a service for us? That’ll be the job of the Module Manager. That’ll be discussed in my next post.

Leave a Reply