Komodo

Komodo

Komodo was spawned out of a desire to learn popular game engine design, like composition over inheritance and ECS. Komodo is an attempt at making an engine that places the developer first, providing simple control over 2D and 3D game development.

Usage

The recommended way to start with Komodo is by using a project template found in the Komodo.Templates package.

The project layout generally consists of two or more projects.

  1. One or multiple <Platform> projects (such as DesktopGL) which consists of platform-specific and creation of starting entities.
  2. Common project where all major game logic should reside.

Content Pipeline

A Komodo project also will need a Content directory in the top-level directory of the project for placing assets and the MonoGame mgcb file. Komodo users will need to use the MonoGame Content Pipeline for compiling assets. Releases can be found here and a tutorial can be found here. Ignore the sections detailing the code needed to load in the asset files, as Komodo will do this for you given a path to an applicable asset for SpriteComponent, TextComponent, Drawable3DComponent, and SoundComponent.

Platform Project

In each platform project, a Game instance needs to be created. Once Game.Run() is called, control will not come back to the Startup class, so make sure at least one Entity with a BehaviorComponent.

A suggested pattern involves creating a Game instance, setting up platform-specific default inputs, and creating a root Entity with a root BehaviorComponent that will generate all the starting entities and their components.

using DesktopGL.Behaviors;
using Komodo.Core;
using Komodo.Core.ECS.Entities;
using Komodo.Core.Engine.Input;

namespace DesktopGL
{
    public static class Startup
    {
        public static Game Game { get; private set; }
        
        [STAThread]
        static void Main()
        {
            using (Game = new Game()) {
                SetupInputs();

                var rootEntity = new Entity(Game);
                rootEntity.AddComponent(new StartupBehaviorComponent());

                Game.Run();
            }
        }

        private static void SetupInputs()
        {
            InputManager.AddInputMapping("left", Inputs.KeyLeft, 0);
            InputManager.AddInputMapping("right", Inputs.KeyRight, 0);
            InputManager.AddInputMapping("up", Inputs.KeyUp, 0);
            InputManager.AddInputMapping("down", Inputs.KeyDown, 0);
            InputManager.AddInputMapping("quit", Inputs.KeyEscape, 0);
        }
    }
}

If a Common project is being used, the <Platform> project should rarely be more complicated than this. Consider including graphics configuration code to the <Platform> project, selecting a resolution, whether or not to display fullscreen, etc.

Common Project

The Common project is where the majority of the game will be created through BehaviorComponents.

BehaviorComponents generally will initialize the content for an Entity, loading the content from disk or generating the assets programatically.

Once initialized, a BehaviorComponents will continue to receive Update(GameTime gametime) calls unless disabled, allowing the BehaviorComponent to continue interacting with other entities and components.

Here is an example BehaviorComponent which creates an FPS counter as a TextComponent. This example assumes a CameraComponent and a Render2DSystem are also a part of the parent Entity of the FPSCounterBehavior.

using Komodo.Core.ECS.Components;
using Komodo.Lib.Math;
using System;

using Color = Microsoft.Xna.Framework.Color;
using GameTime = Microsoft.Xna.Framework.GameTime;

namespace Common.Behaviors
{
    public class FPSCounterBehavior : BehaviorComponent
    {
        public TextComponent CounterText { get; set; }

        public override void Initialize()
        {
            base.Initialize();

            CounterText = new TextComponent(
                "fonts/font",
                Color.Black,
                Game.DefaultSpriteShader,
                ""
            )
            {
                Position = Vector3.Zero
            };
            Parent.AddComponent(CounterText);
        }

        public override void Update(GameTime gameTime)
        {
            CounterText.Text = $"{Math.Round(Game.FramesPerSecond)} FPS";
        }
    }
}

Cameras and Render Systems

All Entity objects which have components which can be rendered must have either a Render2DSystem or Render3DSystem referenced by them.

If an Entity will have a TextComponent, it will also need a Render2DSystem.

If an Entity will have a Drawable3DComponent, it will also need a Render3DSystem.

If an Entity has both a TextComponent and a Drawable3DComponent, it would need both a Render2DSystem and a Render3DSystem.

Each Render2DSystem or Render3DSystem renders using a single CameraComponent. A CameraComponent is set as the active camera for it’s parent Entity by calling CameraComponent.SetActive(). This member method will set the CameraComponent as the active camera for the Entity’s Render2DSystem and Render3DSystem.

It is not necessary to create a CameraComponent for each Entity, as sharing the Render2DSystem and Render3DSystem between Entity objects will cause them to render with the same CameraComponent.

By having the flexibility to use a different CameraComponent per entity, UIs can be easily drawn without relation to the CameraComponent folowing a player.

// Render systems for the perspective scene
var render2DSystem = Game.CreateRender2DSystem();
var render3DSystem = Game.CreateRender3DSystem();

// An entity with a SpriteComponent will need a Render2DSystem
var playerSpriteEntity = new Entity(Game)
{
    Position = new Vector3(0f, 0f, 0f),
    Render2DSystem = render2DSystem,
};
playerSpriteEntity.AddComponent(new PlayerBehavior(0));

// An entity with a Drawable3DComponent will need a Render3DSystem
var cubeEntity = new Entity(Game)
{
    Position = new Vector3(20f, 0f, 0f),
    Render3DSystem = render3DSystem,
};
cubeEntity.AddComponent(new CubeBehavior());

// Pass in both the 2D and 3D render systems to the perspective camera.
var perspectiveCameraEntity = new Entity(Game)
{
    Position = new Vector3(0f, 0f, 200f),
    Render2DSystem = render2DSystem,
    Render3DSystem = render3DSystem,
};
var perspectiveCamera = new CameraComponent()
{
    Position = new Vector3(0, 0, 0f),
    FarPlane = 10000000f,
    IsPerspective = true,
    Zoom = 1f,
    // Will point at the playerSpriteEntity as both the camera and the playerSpriteEntity move
    Target = playerSpriteEntity,
};
perspectiveCameraEntity.AddComponent(perspectiveCamera);
perspectiveCameraEntity.AddComponent(new CameraBehavior(perspectiveCamera));
// Sets the camera as the active camera for the render systems
perspectiveCamera.SetActive();

// Create a new Render2DSystem for the UI scene
render2DSystem = Game.CreateRender2DSystem();
var fpsCounterEntity = new Entity(Game)
{
    Render2DSystem = render2DSystem,
};
fpsCounterEntity.AddComponent(new FPSCounterBehavior());
var orthographicCameraEntity = new Entity(Game)
{
    Position = new Vector3(0f, 0f, 0f),
    Render2DSystem = render2DSystem,
};
var orthographicCamera = new CameraComponent()
{
    FarPlane = 10000000f,
    IsPerspective = false,
    Position = new Vector3(0f, 0f, 100f),
};
orthographicCameraEntity.AddComponent(orthographicCamera);
orthographicCamera.SetActive();

This setup will render the FPS counter without regard for the position of the entities with the perspectiveCamera. Using multiple render systems allows for segmented management of how Entity objects should be individually rendered.