GitHub - andygeiss/ecs: Build your own Game-Engine based on the Entity Component System concept in Golang. (original) (raw)
ECS - Entity Component System
Build your own Game-Engine based on the Entity Component System concept in Golang.
Features
- Provide an easy-to-use framework to build a game engine from scratch.
- No dependencies to other modules or specific game libraries - Feel free to use what fits your needs.
- Minimum overhead - use only what is really needed.
Example engine
See engine-example for a basic implementation using raylib.
Walkthrough
Project layout
At first we create a basic project layout:
mkdir ecs-example cd ecs-example go mod init example mkdir components systems
Next we create a main.go with the following content:
package main
import ( "github.com/andygeiss/ecs" )
func main() { em := ecs.NewEntityManager() sm := ecs.NewSystemManager() de := ecs.NewDefaultEngine(em, sm) de.Setup() defer de.Teardown() de.Run() }
The execution of the program leads to an endless loop, as our engine is not yet able to react to user input.
The movement system
A system needs to implement the methods defined by the interfaceSystem. So we create a new file locally at systems/movement.go:
package systems
import ( "github.com/andygeiss/ecs" )
type movementSystem struct{}
func (a *movementSystem) Process(em ecs.EntityManager) (state int) { // This state simply tells the engine to stop after the first call. return ecs.StateEngineStop }
func (a *movementSystem) Setup() {}
func (a *movementSystem) Teardown() {}
func NewMovementSystem() ecs.System { return &movementSystem{} }
Now we can add the following lines to main.go:
sm := ecs.NewSystemManager() sm.Add(systems.NewMovementSystem()) // <-- de := ecs.NewDefaultEngine(em, sm)
If we start our program now, it returns immediately without looping forever.
The player entity
A game engine usually processes different types of components that represent information about the game world itself. A component only represents the data, and the systems are there to implement the behavior or game logic and change these components. Entities are simply a composition of components that provide a scalable data-oriented architecture.
A component needs to implement the methods defined by the interfaceComponent. Let's define our Player components by first creating a mask atcomponents/components.go:
package components
const ( MaskPosition = uint64(1 << 0) MaskVelocity = uint64(1 << 1) )
Then create a component for Position and Velocity by creating corresponding files such as components/position.go:
package components
type Position struct {
X float32 json:"x"
Y float32 json:"y"
}
func (a *Position) Mask() uint64 { return MaskPosition }
func (a *Position) WithX(x float32) *Position { a.X = x return a }
func (a *Position) WithY(y float32) *Position { a.Y = y return a }
func NewPosition() *Position { return &Position{} }
Now we can add the following lines to main.go:
em := ecs.NewEntityManager() em.Add(ecs.NewEntity("player", []ecs.Component{ // <-- components.NewPosition(). WithX(10). WithY(10), components.NewVelocity(). WithX(100). WithY(100), })) // -->
Extend the movement system
Our final step is to add behavior to our movement system:
func (a *movementSystem) Process(em ecs.EntityManager) (state int) { for _, e := range em.FilterByMask(components.MaskPosition | components.MaskVelocity) { position := e.Get(components.MaskPosition).(*components.Position) velocity := e.Get(components.MaskVelocity).(*components.Velocity) position.X += velocity.X * rl.GetFrameTime() position.Y += velocity.Y * rl.GetFrameTime() } return ecs.StateEngineStop }
The movement system now moves every entity which has a position and velocity component.
We can replace ecs.StateEngineStop with ecs.StateEngineContinue later if we add another system to handle user input.
A rendering system is also essential for a game, so you can use game libraries such as raylib orSDL. This system could look like this with raylib:
// ... func (a *renderingSystem) Setup() { rl.InitWindow(a.width, a.height, a.title) }
func (a *renderingSystem) Process(em core.EntityManager) (state int) { // First check if app should stop. if rl.WindowShouldClose() { return core.StateEngineStop } // Clear the screen if rl.IsWindowReady() { rl.BeginDrawing() rl.ClearBackground(rl.Black) rl.DrawFPS(10, 10) rl.EndDrawing() } return core.StateEngineContinue }
func (a *renderingSystem) Teardown() { rl.CloseWindow() }
