Fast Entity Component System in Golang

ECS

Fast Entity Component System in Golang

This module is the ECS part of the game engine i'm writing in Go.

Features:

  • as fast as packages with automatic code generation, but no setup and regeneration required for every change
  • bitmask for component mapping
  • sparse arrays for fast memory mapping
  • memory allocated in chunks instead of dynamic arrays, ensuring a single memory address for the lifetime of the component instance
  • filters instead of services, automatically updated after adding or removing components to the world
  • the code is commented and the documentation can be generated with godoc
  • 100% test coverage

Installation

go get github.com/marioolofo/go-gameengine-ecs

This project was made with Go 1.17, but it may work with older versions too

Example

See the examples folder.

From simple.go:

package main

import (
	"github.com/marioolofo/go/gameengine/ecs"
)

// Component IDs
const (
	TransformID ecs.ID = iota
	PhysicsID
)

type Vec2D struct {
	x, y float32
}

type TransformComponent struct {
	position Vec2D
	rotation float32
}

type PhysicsComponent struct {
	linearAccel, velocity Vec2D
	angularAccel, torque  float32
}

func main() {
	// initial configuration to create the world, new components can be
	// added latter with world.RegisterComponents()
	config := []ecs.ComponentConfig{
		{ID: TransformID, Component: TransformComponent{}},
		{ID: PhysicsID, Component: PhysicsComponent{}},
	}

	// NewWorld allocates a world and register the components
	world := ecs.NewWorld(config...)

	// World.NewEntity will add a new entity to this world
	entity := world.NewEntity()
	// World.Assign adds a list of components to the entity
	// If the entity already have the component, the Assign is ignored
	world.Assign(entity, PhysicsID, TransformID)

	// Any component registered on this entity can be retrieved using World.Component()
	// It's safe to keep this reference until the entity or the component is removed
	phys := (*PhysicsComponent)(world.Component(entity, PhysicsID))
	phys.linearAccel = Vec2D{x: 2, y: 1.5}

	// World.NewFilter creates a cache of entities that have the required components
	//
	// This solution is better than using Systems to update the entities because it's possible to
	// iterate over the filters at variable rate inside your own update function, for example,
	// the script for AI don't need to update at same frequency as physics and animations
	//
	// This filter will be automatically updated when entities or components are added/removed to the world
	filter := world.NewFilter(TransformID, PhysicsID)

	dt := float32(1.0 / 60.0)

	// filter.Entities() returns the updated list of entities that have the required components
	for _, entity := range filter.Entities() {
		// get the components for the entity
		phys := (*PhysicsComponent)(world.Component(entity, PhysicsID))
		tr := (*TransformComponent)(world.Component(entity, TransformID))

		phys.velocity.x += phys.linearAccel.x * dt
		phys.velocity.y += phys.linearAccel.y * dt

		tr.position.x += phys.velocity.x * dt
		tr.position.y += phys.velocity.y * dt

		phys.velocity.x *= 0.99
		phys.velocity.y *= 0.99
	}

	// When a filter is no longer needed, just call World.RemFilter() to remove it from the world
	// This is needed as the filters are updated when the world changes
	world.RemFilter(filter)
}

Benchmarks

The benchmark folder contains the implementations of a simple test case for performance comparison for this package as GameEngineECS, Entitas, Ento, Gecs and LecsGO and below are the results running on my machine:

Just creation and 4 components addition to the world:

goos: linux
goarch: amd64
pkg: github.com/marioolofo/go/gameengine/ecs/benchmark
cpu: Intel(R) Core(TM) i5-8300H CPU @ 2.30GHz
BenchmarkEntitas-8              1000000000               0.0000043 ns/op
BenchmarkEnto-8                 1000000000               0.0000147 ns/op
BenchmarkGecs-8                 1000000000               0.0000694 ns/op
BenchmarkLecsGO-8               1000000000               0.0000146 ns/op
BenchmarkGameEngineECS-8        1000000000               0.0000092 ns/op
PASS
ok      github.com/marioolofo/go/gameengine/ecs/benchmark       0.008s

Iteration time for 100 entities:

1000 iterations:

goos: linux
goarch: amd64
pkg: github.com/marioolofo/go/gameengine/ecs/benchmark
cpu: Intel(R) Core(TM) i5-8300H CPU @ 2.30GHz
BenchmarkEntitas-8              1000000000               0.003533 ns/op
BenchmarkEnto-8                 1000000000               0.02393 ns/op
BenchmarkGecs-8                 1000000000               0.01464 ns/op
BenchmarkLecsGO-8               1000000000               0.0006838 ns/op
BenchmarkGameEngineECS-8        1000000000               0.002037 ns/op
PASS
ok      github.com/marioolofo/go/gameengine/ecs/benchmark       0.308s

10000 iterations:

goos: linux
goarch: amd64
pkg: github.com/marioolofo/go/gameengine/ecs/benchmark
cpu: Intel(R) Core(TM) i5-8300H CPU @ 2.30GHz
BenchmarkEntitas-8              1000000000               0.02744 ns/op
BenchmarkEnto-8                 1000000000               0.2404 ns/op
BenchmarkGecs-8                 1000000000               0.1638 ns/op
BenchmarkLecsGO-8               1000000000               0.02645 ns/op
BenchmarkGameEngineECS-8        1000000000               0.01831 ns/op
PASS
ok      github.com/marioolofo/go/gameengine/ecs/benchmark       5.679s

100000 iterations:

goos: linux
goarch: amd64
pkg: github.com/marioolofo/go/gameengine/ecs/benchmark
cpu: Intel(R) Core(TM) i5-8300H CPU @ 2.30GHz
BenchmarkEntitas-8              1000000000           0.2639 ns/op
BenchmarkEnto-8                         1        2380309375 ns/op
BenchmarkGecs-8                         1        1749332537 ns/op
BenchmarkLecsGO-8                       1        1366320329 ns/op
BenchmarkGameEngineECS-8        1000000000           0.1825 ns/op
PASS
ok      github.com/marioolofo/go/gameengine/ecs/benchmark       11.826s

100 iterations for x entities allocated:

10000 entities:

goos: linux
goarch: amd64
pkg: github.com/marioolofo/go/gameengine/ecs/benchmark
cpu: Intel(R) Core(TM) i5-8300H CPU @ 2.30GHz
BenchmarkEntitas-8              1000000000               0.1011 ns/op
BenchmarkEnto-8                 1000000000               0.2553 ns/op
BenchmarkGecs-8                 1000000000               0.1471 ns/op
BenchmarkLecsGO-8               1000000000               0.01919 ns/op
BenchmarkGameEngineECS-8        1000000000               0.02215 ns/op
PASS
ok      github.com/marioolofo/go/gameengine/ecs/benchmark       6.777s

50000 entities:

goos: linux
goarch: amd64
pkg: github.com/marioolofo/go/gameengine/ecs/benchmark
cpu: Intel(R) Core(TM) i5-8300H CPU @ 2.30GHz
BenchmarkEntitas-8                     1        1064971463 ns/op
BenchmarkEnto-8                        1        1303889925 ns/op
BenchmarkGecs-8                 1000000000               0.7330 ns/op
BenchmarkLecsGO-8               1000000000               0.1251 ns/op
BenchmarkGameEngineECS-8        1000000000               0.1117 ns/op
PASS
ok      github.com/marioolofo/go/gameengine/ecs/benchmark       37.989s

100000 iterations:

goos: linux
goarch: amd64
pkg: github.com/marioolofo/go/gameengine/ecs/benchmark
cpu: Intel(R) Core(TM) i5-8300H CPU @ 2.30GHz
BenchmarkEntitas-8                     1        2276978192 ns/op
BenchmarkEnto-8                        1        2609283178 ns/op
BenchmarkGecs-8                        1        1448020979 ns/op
BenchmarkLecsGO-8               1000000000               0.2732 ns/op
BenchmarkGameEngineECS-8        1000000000               0.2230 ns/op
PASS
ok      github.com/marioolofo/go/gameengine/ecs/benchmark       13.763s

License

This project is distributed under the MIT licence.

MIT License

Copyright (c) 2022 Mario Olofo 
   
    

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.


   
Comments
  • adding and removing while iterating a filter

    adding and removing while iterating a filter

    Hello, I really like your ecs. I have a little game engine I wrote on top of raylib in c++ that I'm porting to Go - I've used Go for a total of 3 days at this point so it is a great learning exercise.

    You left a note in world.go about using world.Lock and world.Unlock if you need to add or remove entities or components while iterating a filter. At least with the way my engine is designed, that is necessary, particularly for spawning new entities from a spawner type entity (ie, particle system, gun, etc). In any case, just wondering if that was something you planned to add or if you decided it wasn't necessary since there seems to be no locking mechanism at the moment.

    Right now I'm adding entities during a filter iteration but I'm deferring removing any entities by collecting the ids of "dead" entities and removing them after the filter iteration. However, if I don't recreate a new filter each frame and remove it at the end of the frame, this will crash after entities have been removed when iterating the next filter, so something seems to go wrong with the updateFilters mechanism but I'm not sure what. This workaround is ok but probably defeats some efficiency I'd assume? Not sure if I'm doing something wrong.

  • Entity ID starts at 0

    Entity ID starts at 0

    First, I'd like to say I love this project and you've done an awesome job here.

    One thing that I noticed is that NewEntity(...) provides 0x00 as the first entity. This is fine but is a bit of a hassle if you'd like to know if a variable containing an entity ID is unassigned just by looking at its value. Databases usually start the first row/index at 0x01 to get around this issue.

    Where this comes up is when using entity id's in components; and pointers are not supported in components by this library.

    For example:

    type Transform struct {
    	Parent ecs.Entity
    	
    	Pos, Size, Scale, RotPoint, Rot m32.Vec3
    	Visible                         bool
    	M                               m32.Mat4
    }
    

    If we attach that as a component, then we don't really know if Parent is a real entity or if it is the initial value.

  • Question: any limit for component type or query

    Question: any limit for component type or query

    Hi, i've been using this ecs lib for a while. its fun and the api also clear and easy to use, but i have a question, do go-gameengine-ecs have any limitation? like maximum number of uniq component allowed etc?

  • Memory leak

    Memory leak

    Hello, I have been using the engine and now after few seconds of game I'm experiencing an state lost, and after few more seconds I'm receiving an panic:

    unexpected fault address 0x10007
    fatal error: fault
    [signal 0xc0000005 code=0x0 addr=0x10007 pc=0x7ff63f9618ac]
    

    I do believe it is a memory management problems due the default alignment I'm using so I've got printed my component's size and align during the creation of memory pool.

    name: Body / size: 576 / align: 8
    name: Caster / size: 32 / align: 8
    name: Damage / size: 24 / align: 8
    name: Damageable / size: 24 / align: 8
    name: ElementalChanger / size: 16 / align: 8
    name: Movable / size: 16 / align: 4
    name: Projectile / size: 24 / align: 8
    name: StateMachine / size: 608 / align: 8
    name: Storage / size: 24 / align: 8
    name: CameraFocus / size: 1 / align: 1
    name: Chest / size: 1 / align: 1
    name: OpenChest / size: 1 / align: 1
    name: ControllerEffected / size: 1 / align: 1
    name: DebugSpotlight / size: 1 / align: 1
    name: Player / size: 1 / align: 1
    name: Wall / size: 1 / align: 1
    

    I'm not sure how I'm supposed to fix this, I thought I could increase the alignment to a number power of 2 to improve performance but since the alignment is a multiple of size I would expect not have a problem. Please let me know if anyone experienced this or if I have understood this alignment wrong.

    Thanks

  • Benchmarks against EngoEngine's ecs

    Benchmarks against EngoEngine's ecs

    Hi. Thanks for your work. I was looking at this library and saw the benchmarks. I was wondering how they would compare to Github's most popular go ecs, which seems to be the one of the EngoEngine: https://github.com/EngoEngine/ecs

  • Does not compile with go1.17.7

    Does not compile with go1.17.7

    Hello. I want to say I like this project, and I've learned a lot about ECS while reading through it.

    I see the readme says it was made with go version 1.17, though I believe this may be a mistake, or possibly out of date. When pulling the repo and running go test I get the following

    [alexander@dola go-gameengine-ecs]$ go version
    go version go1.17.7 linux/amd64
    [alexander@dola go-gameengine-ecs]$ go test
    # github.com/marioolofo/go-gameengine-ecs [github.com/marioolofo/go-gameengine-ecs.test]
    ./mempool.go:120:31: reflect.ValueOf(value).UnsafePointer undefined (type reflect.Value has no field or method UnsafePointer)
    ./bitmask_test.go:90:35: undefined: testing.F
    FAIL    github.com/marioolofo/go-gameengine-ecs [build failed]
    

    I looked at the reflect and testing packages, and I believe that testing.F and Value.UnsafePointer arrive in go1.18beta1 and later. Those 2 locations in the print out seem to be the only places that use 1.18beta1 features. Thanks for your time

Ecsgo - Cache friendly, Multi threading Entity Component System in Go (with Generic)

ECSGo ECSGo is an Entity Component System(ECS) in Go. This is made with Generic

Oct 19, 2022
The Webhooks Listener-Plugin library consists of two component libraries written in GoLang

The Webhooks Listener-Plugin library consists of two component libraries written in GoLang: WebHook Listener Libraries and Plugin (Event Consumer) Libraries.

Feb 3, 2022
A toaster component for hogosuru framework
A toaster component for hogosuru framework

Toaster component for hogosuru Toaster implementation for hogosuru How to use? Create a hogosurutoaster.Toaster or attach it to a hogosuru container a

Mar 24, 2022
A boilerplate showing how to create a Pulumi component provider written in Go

xyz Pulumi Component Provider (Go) This repo is a boilerplate showing how to create a Pulumi component provider written in Go. You can search-replace

Mar 4, 2022
Packer Plugin Vagrant - The Vagrant multi-component plugin can be used with HashiCorp Packer to create custom images

Packer Plugin Vagrant - The Vagrant multi-component plugin can be used with HashiCorp Packer to create custom images

Jul 13, 2022
A golang application to mock the billing system

mock-billing-cli A golang application to mock the billing system in super markets Features View all items & items with filter Refill items with admin

Jan 13, 2022
Vulture - A Unix Operating System Built Using Golang

vulture A Unix Operating System Built Using Golang Requirements: macOS: make sur

Dec 30, 2022
Antch, a fast, powerful and extensible web crawling & scraping framework for Go

Antch Antch, inspired by Scrapy. If you're familiar with scrapy, you can quickly get started. Antch is a fast, powerful and extensible web crawling &

Jan 6, 2023
Fast conversions across various Go types with a simple API.

Go Package: conv Get: go get -u github.com/cstockton/go-conv Example: // Basic types if got, err := conv.Bool(`TRUE`); err == nil { fmt.Printf("conv.

Nov 29, 2022
Fast and secure initramfs generator
Fast and secure initramfs generator

Booster - fast and secure initramfs generator Initramfs is a specially crafted small root filesystem that mounted at the early stages of Linux OS boot

Dec 28, 2022
Stargather is fast GitHub repository stargazers information gathering tool

Stargather is fast GitHub repository stargazers information gathering tool that can scrapes: Organization, Location, Email, Twitter, Follow

Dec 12, 2022
The package manager for macOS you didn’t know you missed. Simple, functional, and fast.
The package manager for macOS you didn’t know you missed. Simple, functional, and fast.

Stew The package manager for macOS you didn’t know you missed. Built with simplicity, functionality, and most importantly, speed in mind. Installation

Mar 30, 2022
a really fast difficulty and pp calculator for osu!mania

gonia | mania star + pp calculator a very fast and accurate star + pp calculator for mania. gonia has low memory usage and very fast calculation times

Mar 10, 2022
Count Dracula is a fast metrics server that counts entries while automatically expiring old ones

In-Memory Expirable Key Counter This is a fast metrics server, ideal for tracking throttling. Put values to the server, and then count them. Values ex

Jun 17, 2022
Executor - Fast exec task with go and less mem ops

executor fast exec task with go and less mem ops Why we need executor? Go with g

Dec 19, 2022
A fast and easy-to-use gutenberg book downloader

Gutenberg Downloader A brief description of what this project does and who it's for Usage download books Download all english books as epubs with imag

Jan 11, 2022
:chart_with_upwards_trend: Monitors Go MemStats + System stats such as Memory, Swap and CPU and sends via UDP anywhere you want for logging etc...

Package stats Package stats allows for gathering of statistics regarding your Go application and system it is running on and sent them via UDP to a se

Nov 10, 2022
Real-time Charging System for Telecom & ISP environments

Real-time Online/Offline Charging System (OCS) for Telecom & ISP environments Features Real-time Online/Offline Charging System (OCS). Account Balance

Dec 31, 2022
Cross-platform file system notifications for Go.

File system notifications for Go fsnotify utilizes golang.org/x/sys rather than syscall from the standard library. Ensure you have the latest version

Dec 30, 2022