Entities in Space is a game prototype to support my bachelor thesis about the comparison of architectural differences between modern OOP approaches and DOD. This repository contains a full implementation of a simple game with Unity DOTS ECS and modern OOP approaches respectively. The game will also collect various statistics like frame timings, average FPS and active actors, to allow a performance comparison between ECS and OOP.
Entities in Space is a 2D action shooter game with mouse only controls and takes inspiration from various bullet hell titles. The design of the game deliberately seeks to highlight the performance advantages that can be gained by using the DOD approach through producing a high number of concurrently active objects. The player ship will orient in the direction of the mouse pointer. The left mouse button will fire the thrusters, the right mouse button will fire the weapons. Enemies will spawn in waves and will drop a collectible upon being destroyed. On collection of a scaling number of collectibles, an upgrade for the player ship can be chosen. The enemies become stronger with each new wave, and the amount of enemies that spawn increases.
- Full implementation in both Unity DOTS ECS and modern OOP (DI, behavior trees, reactive patterns).
- Comprehensive performance metrics collection (frame times, percentile lows, entity counts).
- Configurable game parameters for benchmarking.
- Bullet-hell gameplay with wave-based enemy spawning.
- Three enemy types, two player weapons and three different upgrades.
Frameworks & Libraries:
- VContainer, UniTask, R3 (modern OOP patterns)
- Unity DOTS (Entities, Physics, Rendering)
- FMOD (Audio)
Engine: Unity 6 (6000.0.41f1) with URP 17.0.4
Licensing: The MIT license of this repository applies only to the original source code and game prototype. All external dependencies and redistributed assets (textures, audio clips, code, etc.) retain their original licenses as specified in CREDITS.md. Downloading this repository does not grant additional redistribution rights beyond what the original licenses permit.
See CREDITS.md for full license information on redistributed external dependencies.
Many thanks to Krotos Studio, Gabriel Aguiar, Ozan Karakoc and Seaside Studios for allowing the inclusion of their licensed asset files as part of this academic project.
- Create a checkout of this repository.
- Download and install Unity Hub and Unity 6 (6000.0.41f1).
- Import the project on Unity Hub and open the Unity Editor.
- Choose File->Build Profiles, click the Build button and choose a target folder. Note: It might be possible to open and build the project for different Unity 6 versions. Attempts to automatically convert the project might work out of the box or fail miserably. Caution advised. Older Unity versions pre Unity 6 will be incompatible.
The pre-built release version 1.0.0 for Windows 11 is available on the Releases page. The configuration assumes that this archive is extracted to "C:\EntitiesInSpace".
The build folder contains an AppConfig.json file. Before launching the game, the variable "ResultDirectory" needs to be set to a valid folder. The performance reports will be stored there after the conclusion of each play session.
Upon game start, a settings screen can be accessed via the "Settings" button in the main menu. The following settings can be configured:
- Game Mode: Allows selecting ECS or OOP and starts the corresponding implementation on game start.
- Enemy Strength: The starting level of the enemies. Both hitpoints and weapons are set to this level. Enemy hitpoint and weapon levels increase with each subsequent wave.
- Player Strength: The starting level of the player. Both hitpoints and weapons are set to this level. Player hitpoints and weapon level can also be increased by selecting upgrades.
- Spawn Settings - Frequency: The interval in which enemies spawn. On Wave 1 there will be 50 spawns per wave and this number increases with each subsequent wave.
- Spawn Setting - Volume: A factor that defines the volume of enemies on each spawn. Default is 20 enemies per spawn on wave 1, and this base number is multiplied by the factor. The base level increases with each subsequent wave.
- Game Timer: When switched, the game terminates after the timer expires. This is useful for running benchmark tests.
- Background Type: Allows selecting the background type. It is possible to choose from procedural, texture and solid color. Procedural looks best but performs worst, texture has medium impact on performance and looks okay and solid color has the best performance but looks boring.
Assets/GameData # All data objects, like player and enemy prefabs, behavior tree definitions, etc.
Assets/Scenes # All scene files that are used for the game
Assets/Scripts/
├── Core/ # Code shared between the ECS and OOP implementations
├── ECS/ # DOTS ECS implementation
│ ├── Authoring/ # Authoring components
│ ├── Data/ # Data components (IComponentData)
│ ├── Managed/ # OOP code and hybrid bridge
│ └── Systems/ # ECS Systems
├── OOP/ # OOP implementation
│ ├── Audio/
│ ├── Collectibles/
│ ├── Combat/
│ └── ...
└── Test/ # Contains examples for unit tests
Settings/ # All project settings, like the game configuration, URP settings, etc.
UI/ # UI Toolkit uxml files
Note: This only includes the most important folders.
Quick start: Both implementations use the MainLoop class as the entry point into the game logic. See EcsSceneScope/OopSceneScope and naming conventions below for navigation.
vContainer is consequently used to maintain all dependencies outside of the ECS world. In the settings folder, the definition of the RootScope context can be found. This contains all global registrations like the scene service, game settings and game configuration as well as the benchmark recording functionality. To allow the game to pause during the display of in-game menus, there is also a class registered in the root context that provides a central place to hook into the pausable Update() loop. In general, the Scope files provide a great overview over the existing functionality. Apart from the RootScope, there is the MainMenuScope, EcsSceneScope and OopSceneScope on scene level, and for the OOP implementation there is further the PlayerScope and EnemyScope on instance level.
The main entry point into the game is the MainMenu scene. This scene provides the settings screen and a start game button that will trigger the load of the configured scene, depending on the Game Mode that has been chosen in the settings.
Throughout the whole software, a common naming scheme is applied:
- Service describes classes that are injected as dependencies into other classes and expose functionality for other classes to be used.
- Processor describes systems that process data independently of other systems. These are implemented as ISystem structs.
- Manager describes managed systems that bridge the ECS world with the managed parts of the application. These are implemented as classes that inherit from SystemBase.
- Controller describes OOP entry points that trigger functionality when conditions are fulfilled, like spawning an explosion when an actor dies.
- Configuration describes static configuration that cannot be changed at runtime.
- Settings describes dynamic configuration that can be changed at runtime through the settings menu.
- View describes abstractions from the Unity UI system that provide access to presentation and a source for user interaction
- Model describes classes that utilize the views to expose their functionality to the rest of the software
- Data describes ECS IComponentData-structs that contain data
- Tag describes empty ECS IComponentData-structs
It is not possible to implement all functionality with pure ECS (things like UI in particular are very hard to realize with DOD approaches), therefore also here vContainer is used to maintain all OOP classes in the DI container. The EcsSceneScope is the main entry point into the ECS implementation. Here, all required MonoBehaviour dependencies can be populated in the inspector and are injected into the DI container. The automatic world creation is disabled, this is to allow only executing the world in ECS mode and prevent any performance impact that running the ECS world would have on the OOP implementation. To provide a clean interface to the World, the world is stored in the WorldContainer, which exposes all necessary functionality for the OOP code to interact with the world through the IWorldContainer interface. The flow of the game is controlled through the MainLoop class. This class will trigger all the user interaction, start, pause and unpause the game, and end the game on timer expiry or player ship destruction. The MainLoop is also the entry point into the game logic and a great place to start when trying to get an overview over the code base.
The ECS implementation is strictly separated into Authoring, Data and Systems. As per the DOD design philosophy, all functionality is broken down into independent systems as far as possible. The main design goal is to achieve parallel Burst jobs for data processing. The spawning of player and enemies happens through the PlayerService and EnemySpawner from the managed part of the application. To get an understanding of the flow of data and how the systems interact, these classes are a great place to start.
To resolve problems with system ordering and race conditions with respect to the creation and destruction of entities, a frame is divided into three parts: Update-Phase, Reaction-Phase and Cleanup-Phase. All Systems are decorated with the [DisableAutoCreate]-attribute, and will be injected into the world as part of the EntitiesInSpaceSystemBootstrap. In this class, it is possible to get an overview over the existing systems and their execution order. For structural changes, each phase has it's own entity-command-buffer system which also serves as the sync point to synchronize structural changes across the phases. This class is a great starting point to get an overview over the existing systems.
Also for OOP, the best place to start reading is the OopSceneScope and the MainLoop class, that contains functionality analogous to the ECS implementation. The OOP implementation tries to leverage the architectural advantages that can be gained from using async/await with the help of UniTask, as well as Reactive Programming with R3. For the distribution and processing of data, the reactive approach is heavily utilized and the use of Update() methods is avoided wherever possible.
The implementation consequently abstracts from the Unity API. MonoBehaviour is not referenced from the business logic at all, all points where the interaction with the Unity engine happens are declared as part of the OopSceneScope and Unity components are wrapped with thin wrapper classes that only expose the required parts of the API.
As with the ECS implementation, a great place to start reading is the OopSceneScope, the MainLoop, as well as the PlayerService and EnemySpawner. On top of the scene scope, there is the described instance scopes to allow the enemy and player objects local components. These scopes give a great overview over the implementation of these actors.
All VisualEffect-Graphs that are being used are fed through GPU buffers. The different elements that require particles, such as explosions, projectiles, collectibles, are all implemented to be rendered exclusively on the GPU. On the CPU, only the position will be mirrored where it is required to perform collision detection. To get an overview over this implementation, the following classes need to be inspected:
- Core IParticleServce, ParticleService # This is the implementation of the GPU buffer functionality
- OOP: ProjectileService, CollectibleService, DropController, ExplosionController
- ECS: ProjectileProcessor, ParticleManager, DropDataProcessor, ExplosionProcessor