Skip to content

Commit

Permalink
Update docs, add code coverage. Add Basic example
Browse files Browse the repository at this point in the history
  • Loading branch information
unitoftime committed Jan 3, 2025
1 parent ec18695 commit 260a9cd
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 6 deletions.
13 changes: 10 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ jobs:
- name: make All
run: make all

# Maybe one day
# - name: Upload coverage to Codecov
# uses: codecov/codecov-action@v2
- name: Update coverage report
uses: ncruces/go-coverage-report@v0
with:
report: true
chart: true
amend: true
if: |
matrix.os == 'ubuntu-latest' &&
github.event_name == 'push'
continue-on-error: true
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
[![Go Reference](https://pkg.go.dev/badge/github.com/unitoftime/ecs.svg)](https://pkg.go.dev/github.com/unitoftime/ecs)
[![Build](https://github.com/unitoftime/ecs/actions/workflows/build.yml/badge.svg)](https://github.com/unitoftime/ecs/actions/workflows/build.yml)
[![Go Coverage](https://github.com/unitoftime/ecs/wiki/coverage.svg)](https://raw.githack.com/wiki/unitoftime/ecs/coverage.html)


This is an ecs library I wrote for doing game development in Go. I'm actively using it and its pretty stable, but I do find bugs every once in a while. I might vary the APIs in the future if native iterators are added.

Expand All @@ -13,6 +15,11 @@ Conceptually you can imagine an ECS as one big table, where an `Id` column assoc

We use an archetype-based storage mechanism. Which simply means we have a specific table for a specific component layout. This means that if you add or remove components it can be somewhat expensive, because we have to copy the entire entity to the new table.

## Basic Full Example
You can find a fairly comprehensive example here:
- [Basic Example](https://github.com/unitoftime/ecs/tree/master/examples/basic)


### Basic Usage
Import the library: `import "github.com/unitoftime/ecs"`

Expand Down Expand Up @@ -82,9 +89,13 @@ query := ecs.Query2[Position, Velocity](world, ecs.Optional(Velocity))

Commands will eventually replace `ecs.Write(...)` once I figure out how their usage will work. Commands essentially buffer some work on the ECS so that the work can be executed later on. You can use them in loop safe ways by calling `Execute()` after your loop has completed. Right now they work like this:
```
cmd := ecs.NewCommand(world)
WriteCmd(cmd, id, Position{1,1,1})
WriteCmd(cmd, id, Velocity{1,1,1})
world := NewWorld()
cmd := NewCommandQueue(world)
cmd.SpawnEmpty().
Insert(ecs.C(Position{1, 2, 3})).
Insert(ecs.C(Velocity{1, 2, 3}))
cmd.Execute()
```

Expand Down
110 changes: 110 additions & 0 deletions examples/basic/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package main

import (
"fmt"
"time"

"github.com/unitoftime/ecs"
)

// This example illustrates the primary use cases for the ecs

type Name string

type Position struct {
X, Y, Z float64
}

type Velocity struct {
X, Y, Z float64
}

func main() {
// Create a New World
world := ecs.NewWorld()

// You can manually spawn entities like this
{
cmd := ecs.NewCommandQueue(world)

// Add entities
cmd.SpawnEmpty().
Insert(ecs.C(Position{1, 2, 3})).
Insert(ecs.C(Velocity{1, 2, 3})).
Insert(ecs.C(Name("My First Entity")))
cmd.Execute()
}

scheduler := ecs.NewScheduler()

// Append physics systems, these run on a fixed time step, so dt will always be constant
scheduler.AppendPhysics(
// Comment out if you want to spawn a new entity every frame
// ecs.NewSystem1(world, SpawnSystem),

// Option A: Create a function that returns a system
MoveSystemOption_A(world),

// Option B: Use the dynamic injection to create a system for you
ecs.NewSystem1(world, MoveSystemOption_B),

ecs.NewSystem1(world, PrintSystem),
)

// Also, add render systems if you want, These run as fast as possible
// scheduler.AppendRender()

// This will block until the scheduler exits `scheudler.SetQuit(true)`
scheduler.Run()
}

// Note: This system wasn't added to the scheduler, so that I wouldn't constantly spawn entities in the physics loop
// But, you can rely on commands to get injected for you, just like a query.
func SpawnSystem(dt time.Duration, commands *ecs.CommandQueue) {
// TODO: I'd like to rewrite this to be internally managed, but for now you must manually call Execute()
defer commands.Execute()

cmd := commands.SpawnEmpty()

name := Name(fmt.Sprintf("My Entity %d", cmd.Id()))
cmd.Insert(ecs.C(Position{1, 2, 3})).
Insert(ecs.C(Velocity{1, 2, 3})).
Insert(ecs.C(name))
}

// Option A: Define and return a system based on a closure
// - Provides a bit more flexibility if you need to establish variables ahead of the system
func MoveSystemOption_A(world *ecs.World) ecs.System {
query := ecs.Query2[Position, Velocity](world)

return ecs.NewSystem(func(dt time.Duration) {
query.MapId(func(id ecs.Id, pos *Position, vel *Velocity) {
sec := dt.Seconds()

pos.X += vel.X * sec
pos.Y += vel.Y * sec
pos.Z += vel.Z * sec
})
})
}

// Option 2: Define a system and have all the queries created and injected for you
// - Can be used for simpler systems that dont need to track much system-internal state
// - Use the `ecs.NewSystemN(world, systemFunction)` syntax (Where N represents the number of required resources)
func MoveSystemOption_B(dt time.Duration, query *ecs.View2[Position, Velocity]) {
query.MapId(func(id ecs.Id, pos *Position, vel *Velocity) {
sec := dt.Seconds()

pos.X += vel.X * sec
pos.Y += vel.Y * sec
pos.Z += vel.Z * sec
})
}

// A system that prints all entity names and their positions
func PrintSystem(dt time.Duration, query *ecs.View2[Name, Position]) {
query.MapId(func(id ecs.Id, name *Name, pos *Position) {
fmt.Printf("%s: %v\n", *name, pos)
})
}

0 comments on commit 260a9cd

Please sign in to comment.