EntityManager | A modular and reusable entity-component system

entitymanager-banner

A modular and reusable entity-component system



Overview of this resource

EntityManager is a flexible module that allows you to create reusable entities from any instance while attaching components and properties that enhance their functionality. Entities act as data containers, while components provide methods and logic that can be accessed globally anywhere, anytime.

EntityManager is a hybrid architecture combining ECS principles with OOP: primarily ECS for entity-component management, but enriched with OOP patterns to provide reusable functions and behaviors.


Key features

  • Create and manage entities of any type.
  • Attach components that add reusable behaviors and functionality.
  • Retrieve entities and their components from anywhere in your game.
  • Simplify game logic by modularizing features like leaderstats, health systems, AI behaviors, and more, with the use of custom components.
  • Unlike your typical ECS module, EntityManager provides a simple and intuitive framework that you can use to manage your entities and create custom components.
  • Out of the box experience with annotations and guides.


Why should I use entity-component systems over OOP?

Entity-component systems are often chosen over object-oriented programming in performance-critical applications. Here are some key reasons why ECS might be preferred:

  • ECS structures data in a way that benefits cache locality, leading to faster processing.
  • OOP uses deep inheritance hierarchies, which can become rigid and hard to maintain.
  • ECS stores components in contiguous memory, improving data-oriented design and reducing cache misses.
  • ECS decouples behavior from data, making it easier to add, remove, or modify functionality without breaking existing code.


How does EntityManager target these points?

EntityManager has a strong foundation and effectively implements ECS principles, key strengths that make EntityManager stand out:

  • EntityManager is performance-oriented, entities store components in a modular way, reducing inheritance issues.
  • Cached entities improve lookup efficiency, avoiding unnecessary recreation.
  • EntityManager uses custom annotations to improve clarity and helps developers understand expected types.
  • Entities can add and remove components dynamically, allowing flexibility.
  • Provides a modular framework that allows developers to mix and match components without rigid dependencies. This makes it easy to add new features, replace systems, or optimize specific parts of the game without rewriting entire sections of code.
  • An intuitive framework that provides clear structure, readable API, and predictable behavior, this reduces development time and makes debugging much easier.
  • Components are stored in a dictionary with systems for batch updates and global iteration over specific components (e.g., physics, AI).
  • Contains event-driven systems for entities and components to react dynamically (e.g., OnInitialized, OnDestroyed, etc.).

Key features EntityManager fails to cover as of now:

  • Client-side integration is untested, while it may work, features like server-locked entities and network synchronization are planned for future updates.


Getting started with EntityManager

Installation

  1. Get the module from the Roblox Creator Marketplace
  2. Manual Installation
    • If you prefer, you can manually source the code from the GitHub repository (coming soon)
    • After sourcing the code, place each module in its appropriate location within your game
  3. Install using the Wally package manager (coming soon)

Basic Usage

Here’s a quick example of how to use EntityManager in your game to automatically load player’s data when they join the game using the pre-packed MultiStats component:

First, let’s create a new server script inside of ServerScriptService, now let’s import our globals:

-- Globals
local ServerScriptService = game:GetService("ServerScriptService")
local Players = game:GetService("Players")

-- Require necessary modules
local Entity = require(ServerScriptService.Entity)
local Components = require(ServerScriptService.Entity.Components)

Additionally, if you want to include component-specific annotations, you can also require your component:

local MultiStats = require(ServerScriptService.Entity.Components.MultiStats)

Now, let’s create our PlayerAdded listener:

Players.PlayerAdded:Connect(function(player)
	-- PlayerAdded listener
end)

Let’s create our first entity, using the Entity.new() function:

Players.PlayerAdded:Connect(function(player)
	-- Initialize the entity with latest id and player's identifier
	local entityManager = Entity.new(Entity:GetNextId(), player.Name)
end)

Now, let’s give our entity an identity, by changing it’s properties and initializing it:

Players.PlayerAdded:Connect(function(player)
	-- Initialize the entity with latest id and player's identifier
	local entityManager = Entity.new(Entity:GetNextId(), player.Name)
	entityManager.Properties.Instance = player
	entityManager:Initialize()
end)

It’s about time we get our first component using the GetComponent() method, we will be using local component: MultiStats.Component to let our script know which annotations to use for type-checking:

Players.PlayerAdded:Connect(function(player)
	-- Initialize the entity with latest id and player's identifier
	local entityManager = Entity.new(Entity:GetNextId(), player.Name)
	entityManager.Properties.Instance = player
	entityManager:Initialize()
	
	-- Get the pre-packed MultiStats component
	local component: MultiStats.Component = entityManager:GetComponent("MultiStats")
end)

Modules found under the Components module are treated as components which should automatically be loaded on any entity upon initialization. To prevent this, you can place the modules under Extras and load them manually as needed using the Load() method.

Now let’s use our component’s AddCoins() method:

Players.PlayerAdded:Connect(function(player)
	-- Initialize the entity with latest id and player's identifier
	local entityManager = Entity.new(Entity:GetNextId(), player.Name)
	entityManager.Properties.Instance = player
	entityManager:Initialize()
	
	-- Get the pre-packed MultiStats component
	local component: MultiStats.Component = entityManager:GetComponent("MultiStats")
	component:AddCoins(100)
end)

If we run the game, now we should have 100 coins!

image

Now, let’s use our component’s CanAfford() method to check if our player can afford a 500 coin item:

Players.PlayerAdded:Connect(function(player)
	-- Initialize the entity with latest id and player's identifier
	local entityManager = Entity.new(Entity:GetNextId(), player.Name)
	entityManager.Properties.Instance = player
	entityManager:Initialize()
	
	-- Get the pre-packed MultiStats component
	local component: MultiStats.Component = entityManager:GetComponent("MultiStats")
	component:AddCoins(100)
	
	-- Check if player can afford a 500 coins priced item
	print(component:CanAfford(500))
end)

As expected, the output is false, our player only has 100 coins, so how would they be able to afford a 500 coin item?

Let’s continue by creating a new part, and adding a new script inside of it to see how we would retrieve entities and their components:

We’ll start off by requiring our modules and services:

-- Globals
local ServerScriptService = game:GetService("ServerScriptService")
local Players = game:GetService("Players")

-- Require necessary modules
local Entity = require(ServerScriptService.Server.Modules.Entity)

This time, we won’t be using the Components module, as we don’t need to load any components, just retrieve an existing one on the entity.

Let’s create our touch listener:

-- Touched event
script.Parent.Touched:Connect(function(part)
	-- Touch listener
end)

Let’s find our player:

-- Touched event
script.Parent.Touched:Connect(function(part)
	if part.Parent and part.Parent:FindFirstChild("Humanoid") then
		local character = part.Parent
		local player = Players:GetPlayerFromCharacter(character)
		
		if player then
			-- We have a player!
		end
	end
end)

Now let’s get this player’s entity object using the Entity:Get() method:

-- Touched event
script.Parent.Touched:Connect(function(part)
	if part.Parent and part.Parent:FindFirstChild("Humanoid") then
		local character = part.Parent
		local player = Players:GetPlayerFromCharacter(character)
		
		if player then
			local entity = Entity:Get(player)
		end
	end
end)

Let’s retrieve our MultiStats component using the entity:GetComponent() method:

-- Touched event
script.Parent.Touched:Connect(function(part)
	if part.Parent and part.Parent:FindFirstChild("Humanoid") then
		local character = part.Parent
		local player = Players:GetPlayerFromCharacter(character)
		
		if player then
			local entity = Entity:Get(player)
			local component = entity:GetComponent("MultiStats")
		end
	end
end)

Now we can use our component the same as before!


Next Steps

What happens next is up to you! You can create your own components using the pre-packed component for reference, I’m curious to see what you will make!


Feedback

If you encounter any issues or errors, or you need further assistance or guides on this project, do let me know in the replies below, I’m open to help! Also, let me know what you will create using my module, or if you plan on using it in the future, thank you for using EntityManager!


Do you plan on using EntityManager in the near future?

  • Yes
  • No
  • Not sure
0 voters

Do you believe this module could help developers?

  • Yes
  • No
  • Not sure
0 voters

Should I continue this project?

  • Yes
  • No
  • Not sure
0 voters

Have you tried it? How do you rate it?

  • 1
  • 2
  • 3
  • 4
  • 5
  • Not sure
0 voters


Changelog

28/03/25 - Version v1.0.0-rc1* (release candidate, pre-release)

  • Created main modules and component support
  • Added component loaders
  • Created entity classes

01/04/25 - Version v1.0.0-rc2* (release candidate, pre-release)

  • First public release
  • Added a pre-packed version of my module MultiStats translated to a component
  • Added annotations & type checking support
  • Minor bug fixes

:warning: WARNING!

Upgrading to this release may require updating legacy code! Failure to update may cause your existing code to break or malfunction, please review the following migration guide to upgrade your code accordingly:

  • Take advantage of the new type annotations for a smoother experience.

01/04/25 - Version v1.0.0-rc2.1* (release candidate, pre-release)

  • Included MIT license inside the project
  • Created a read me script inside the main module

02/04/25 - Version v1.0.0 (stable release, supported)

  • Same as v1.0.0-rc2.1, now officially marked as stable
  • No major code changes, just confirming stability

26/05/25 - Version v2.0.0 (stable release, latest)

  • Added method to auto-generate next unique ID
  • Added distinct events for update handling (OnInitialized, OnDestroyed, OnComponentAdded, OnComponentRemoved, OnPropertyChanged)
  • Removed automatic entity initialization in favor of OnInitialized event
  • Added support for batch updating and filtering entities
  • Added manual loading components and isolated automatic loading components
  • Added GoodSignal by @stravant inside the module
  • Fixed duplicate component instantiation bug
  • Minor bug fixes

:warning: WARNING!

Upgrading to this release may require updating legacy code! Failure to update may cause your existing code to break or malfunction, please review the following migration guide to upgrade your code accordingly:

  • The automatic entity initialization was removed.
  • Use the new OnInitialized event to trigger setup logic instead of relying on auto-initialization or make sure to initialize entities manually after creating them.

  • The bug causing duplicate component loading is fixed.
  • Ensure your code does not manually load components multiple times without checks or your code will error to prevent data overwriting.

  • Use the new GetNextId method to generate unique IDs automatically instead of manual ID assignment.
  • Take advantage of the new batch updating and filtering features for entities to optimize performance.

  • Manual and automatic loading components are now separated.
  • Update your loading logic accordingly to use the correct loading method.

*a release candidate is a version of a resource that is nearly ready for release but may still have some minor bugs, it is a beta version with the potential to be a stable product, which is ready to release unless significant bugs emerge



This resource is licensed under the MIT License. You are free to use, modify, and distribute it, but please include the original copyright and license notice in any copy of the resource that you distribute.

MIT License

Copyright (c) 2025 iAmDany (@Danielhoteaale)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the “Software”), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.




entitymanager-banner-wide

A modular and reusable entity-component system

22 Likes

Is this an ECS or OOP composition?

ECS, however if you tweak some things you can probably use it as an OOP framework too.

1 Like

New release: version v2.0.0

This update includes important improvements and fixes to enhance your experience:

  • Added method to auto-generate next unique ID
  • Added distinct events for update handling (OnInitialized, OnDestroyed, OnComponentAdded, OnComponentRemoved, OnPropertyChanged)
  • Removed automatic entity initialization in favor of OnInitialized event
  • Added support for batch updating and filtering entities
  • Added manual loading components and isolated automatic loading components
  • Added GoodSignal by @stravant inside the module
  • Fixed duplicate component instantiation bug
  • Minor bug fixes

:warning: WARNING!

Upgrading to this release may require updating legacy code! Failure to update may cause your existing code to break or malfunction, please review the following migration guide to upgrade your code accordingly:

  • The automatic entity initialization was removed.
  • Use the new OnInitialized event to trigger setup logic instead of relying on auto-initialization or make sure to initialize entities manually after creating them.

  • The bug causing duplicate component loading is fixed.
  • Ensure your code does not manually load components multiple times without checks or your code will error to prevent data overwriting.

  • Use the new GetNextId method to generate unique IDs automatically instead of manual ID assignment.
  • Take advantage of the new batch updating and filtering features for entities to optimize performance.

  • Manual and automatic loading components are now separated.
  • Update your loading logic accordingly to use the correct loading method.
1 Like

Why should we chose this over Jecs or Matter for example? They’re pretty established. Can we get benchmarks?

1 Like

This project began as a personal asset, which I later decided to release publicly. Ultimately, the choice of which solution to use is up to the developer.

While my module isn’t a pure ECS implementation, it blends OOP principles with ECS concepts. I believe this hybrid approach is what sets it apart.

Regarding benchmarks against established modules like Jecs or Matter, I want to be transparent. I’m not expecting my module to outperform them, as those libraries are highly optimized and rigorously tested. That said, I’m open to running benchmarks and will aim to include performance comparisons in a future release.

1 Like

Right this makes sense if it is not trying to be something that is competing but just a humble option for developers to adapt to and learn as well as just to mess around with. Respect the humble reply and the nice explanation.

1 Like

This is cool and all, makes composition more centralized, but a few things:

You store your components in a hashmap which in luau is not contiguous memory

You store your entities in a hashmap too, but this doesn’t really matter if you don’t have queries :V

You can also do this with any other ecs module, the reason why it isn’t recommended is cuz it fragments columns making iteration and lookup slower than if you had a fixed number of components, but still pretty fast

I don’t see any exposed iteration logic in the module, atleast the marketplace one

Jecs has this too in the form of query listeners, but im pretty sure others like matter have it too, reason being you don’t really wanna create multiple events for a single entity when you have thousands of entities :V

I would say a typical ecs module like jecs is pretty easy to understand, reads the docs and examples!

Aside from that, a few nitpicks of mine about the resource itself:

  1. Why are you creating a new metatable for each entity? The metamethods aren’t even dupclosured because you’re using upvalues
    Also, firing a signal in a metamethod?
    Also also, properties is probably supposed to be a hashmap, and # doesn’t work on those, only arrays
    Lastly, __pairs isn’t a real metamethod
  2. Why do you have to call Initialize() for every new entity you make? Just setup components on construct
  3. Having __index and __newindex functions for protected properties? That’s gonna slow down index and setting by a ton…
  4. The types aren’t even inferred for each component, you have to define them yourself (which you don’t have to do with ecs libs like jecs, value attached to components type is stored in the type of the component, and you handle components on your own so the types are much better)

In all honesty, I’d have no issue with this if you didn’t make all the claims you did, but you did, so here we are :V

I’d just do composition manually with a module containing all my behaviors/use an actual ecs library, but eh feel free to use this if you want

2 Likes

These points weren’t mentioned in the summary of my module, instead, that section is meant to give a general overview of what an ECS is. Directly below, I explained how my module specifically addresses those aspects.

As I previously mentioned, this module wasn’t built with the goal of replacing other existing solutions. It’s an alternative, and developers are free to choose what best suits their needs.

There is a Filters module included in the Marketplace version. It provides a custom :Filter() method, as well as :WithComponent() filtering, and functions for mapping and looping.

Again, this module wasn’t designed to compete directly with libraries like Jecs or Matter. However, your point on performance is noted, and I may revisit this area to explore possible improvements to performance in the future.

That’s a valid point, likely an oversight on my part. I’ll address it in the next patch release.

The __pairs metamethod was introduced in Lua 5.2, but it may not be supported in Luau. I likely missed that distinction at the time.

This behavior was changed in the latest release. It now uses the OnInitialized event and defers initialization to avoid automatically loading defaults for every entity, especially in cases where it’s not needed.

This was done to prevent entity IDs from being unintentionally changed or overwritten. While it may impact performance slightly, the trade-off was made for data safety and I may change that in the future.

I initially aimed for type inference, but due to limitations in Roblox Studios’s editor linting, I couldn’t achieve that functionality. It’s possible other libraries have found workarounds that I haven’t yet come across.

I appreciate the feedback. As mentioned earlier, I didn’t design this to outperform other libraries, but rather to offer an alternative that mixes OOP and ECS concepts in one single solution.

Im just a bit confused as to why you list one of the main benefits of using an ecs (memory locality and memory efficiency) while also not providing that at all

Fair enough, just think the post is misleading

Ah my bad, but this isnt really ecs-like though, it has bad memory locality because youre using a hashmap and youre looping through every single entity to find ones which have a specific archetype (a method which ecs based on archetypes dont have to do)

I didnt say this was supposed to compete, im just saying an alternative other ecs libraries have comeup with. Multiple luau signals per entity is like pretty bad

You should really consider just prefixing your ids with __ like __id if you insist on using oop, __index and __newindex functions are multiple tens of times slower than if you didnt have them (and youre also firing a signal in __newindex :V)

The workaround is to actually make an ecs instead of a hybrid :V

I know, but the performance is worse than if you just did composition by yourself (which isnt too hard), you cant just keep saying its not supposed to compete as an excuse to not improve the resource

That’s because I have not implemented it yet, the same way some features which were released in the last version were not available until then. And I will work on covering all of those points over time.

That defeats the whole purpose of my module and turns it into a generic ECS solution. If that’s what you need for your systems, you’re free to use any other alternatives.

I already mentioned in my reply that I’m willing to target all performance issues you pointed out in a future patch to enhance performance. It’s not an excuse as long as I’m willing to address it.

If you haven’t implemented it yet why does the post imply that it is :thinking:

Fair enough ig

Alright man

1 Like

This is turning into a thread but I appreciate your feedback. I already addressed this exact issue but I could make some changes to the post in case it’s confusing.

1 Like

I dont see why would you use this over anything, can you please explain to me why should i use this and why would this be helpful to me while creating my project.

Edit:
Ok, now i see why i can use this, its pretty useful overall

1 Like

For your example you get the player after already receiving the player for touched, and also for playeradded. What’s the simplicity in doing this compared to just typing player.leaderstats.Coins.Value += 500 or however much you want. It’s 1 line of code vs like 5 unnecessary lines.