Skip to content

Engine Architecture

Kyle Robinson edited this page Jan 28, 2023 · 12 revisions

Software Architecture & Patterns

Midnight Harvest's engine implements a variety of programming patterns to best adhere to the SOLID design philosophy. Some of these patterns include the following.

  • State Machines - (Level System)
  • Fuzzy State Machines - (AI System)
  • Component System - (Entity Components)
  • Event System - (For Decoupling Classes)

Class Diagram

Below is a rough class diagram that illustrates on a high level how the classes are intertwined. Looking at this, the Level class is acting as a container for all of the editors, which each in turn binds all the relevant subsystems for modification.

Event System

Throughout the codebase, an event system is used to completely decouple systems that would otherwise be directly reliant on each other such that one class could react to the modification of the other, creating a chain of inheritance which would be very difficult to manage for larger projects.

As such, the event system eliminates this problem. Any class can use an instance of the event system to fire events throughout the rest of the codebase without ever knowing the destination.

Below is an example of how player input can be handled, where events are fired from the Input class when a key is registered. Any classes that are actively listening for the input events can react accordingly, but this approach removes the need for these classes to know of each other's existence.

// Input.cpp

if ( m_keyboard.KeyIsPressed( 'W' ) )
    EventSystem::Instance()->AddEvent( EVENTID::PlayerUp );

if ( m_keyboard.KeyIsPressed( 'A' ) )
    EventSystem::Instance()->AddEvent( EVENTID::PlayerLeft );

if ( m_keyboard.KeyIsPressed( 'S' ) )
    EventSystem::Instance()->AddEvent( EVENTID::PlayerDown );

if ( m_keyboard.KeyIsPressed( 'D' ) )
    EventSystem::Instance()->AddEvent( EVENTID::PlayerRight );

Then, after registering a class as a listener, which can be done by inheriting from the Listener class, the developer can add events as clients, which should be done upon immediate initialization of the class, to ensure that the events are processed correctly.

Once the class bind a selection of events to listen for, the HandleEvent function will handle the processing of the events when they are fired from elsewhere in the codebase. In this, the player movement events are fired from the Input class.

// Entity.cpp

void Entity::AddToEvent() noexcept
{
    EventSystem::Instance()->AddClient(EVENTID::PlayerUp, this);
    EventSystem::Instance()->AddClient(EVENTID::PlayerLeft, this);
    EventSystem::Instance()->AddClient(EVENTID::PlayerDown, this);
    EventSystem::Instance()->AddClient(EVENTID::PlayerRight, this);
}

void Entity::UpdatePlayer( const float dt )
{
    EVENTID eventId = m_entityController->GetEventId();
    switch ( eventId )
    {
    case EVENTID::PlayerUp: m_physics->AddForce({ 0.0f, -movementFactor }); break;
    case EVENTID::PlayerLeft: m_physics->AddForce({ -movementFactor, 0.0f }); break;
    case EVENTID::PlayerDown: m_physics->AddForce({ 0.0f, movementFactor }); break;
    case EVENTID::PlayerRight: m_physics->AddForce({ movementFactor, 0.0f }); break;
    default: break;
    }
}

If you have any questions regarding the use of these systems, please reach out to a member of Nine Byte Warriors for advice.


Page Author: Kyle Robinson

Clone this wiki locally