Add additional benchmarks and optimize

Allocate memory less often.
This commit is contained in:
Trevor Slocum 2021-12-07 19:20:36 -08:00
parent 83c9c1fb0c
commit 62caf1bdd2
16 changed files with 201 additions and 96 deletions

View File

@ -7,7 +7,7 @@ import (
var componentMutex sync.Mutex
// ComponentID is a component identifier. Each Component is assigned a unique ID
// via NewComponentID, and implements a ComponentID method returning its ID.
// via World.NewComponentID, and implements a ComponentID method returning its ID.
type ComponentID int
// Component represents data for an entity, and how it interacts with the world.

View File

@ -2,6 +2,9 @@ package gohan
import "log"
// Context represents the current iteration of a System's matching entities. It
// provides methods for retrieving components for the currently matched Entity,
// and removing the currently matched Entity.
type Context struct {
Entity Entity
@ -12,7 +15,7 @@ type Context struct {
// Component gets a Component of the currently handled Entity.
func (ctx *Context) Component(componentID ComponentID) interface{} {
if debug {
if debug != 0 {
var found bool
for _, id := range ctx.c {
if id == componentID {
@ -21,15 +24,15 @@ func (ctx *Context) Component(componentID ComponentID) interface{} {
}
}
if !found {
log.Panicf("illegal component access: component %d is not queried by system %s", componentID, ctx.w.systemName(ctx.s))
log.Panicf("illegal component access: component %d is not queried by %s", componentID, ctx.w.systemName(ctx.s))
}
}
return ctx.w.Component(ctx.Entity, componentID)
}
// RemoveEntity removes the currently handled Entity's components, causing it
// to no longer be handled by any system. Because Gohan reuses removed EntityIDs,
// applications must also remove any internal references to the removed Entity.
func (ctx *Context) RemoveEntity() {
ctx.w.RemoveEntity(ctx.Entity)
// to no longer be handled by any system. Because Gohan reuses removed Entity
// IDs, applications must also remove any other references to the removed Entity.
func (ctx *Context) RemoveEntity() bool {
return ctx.w.RemoveEntity(ctx.Entity)
}

21
doc.go
View File

@ -27,6 +27,17 @@ should be public (start with an uppercase letter) and may have any number of
publicly accessible data fields. They should not have any logic (i.e. game code)
within them, as all logic should be implemented within a System.
Rather than accessing components via Context.Component, using helper functions
(such as the following) helps to reduce code verbosity.
func Position(ctx *gohan.Context) *PositionComponent {
c, ok := ctx.Component(PositionComponentID).(*PositionComponent)
if !ok {
return nil
}
return c
}
System Design Guidelines
Systems are located in a separate package, typically named system. They should
@ -35,5 +46,15 @@ named as follows: NewSystemNameHere(). Data should be stored within components
attached to one or more entities, rather than within the systems themselves.
References to components must not be maintained outside each Update and Draw
call, or else the application will encounter race conditions.
Environment Variables
Running an application with the environment variable GOHAN_DEBUG set to 1
enables verification of Systems' access to Components. This verification is
disabled by default for performance reasons. While verification is enabled,
if a System attempts to access a Component which is not included in the
System's Needs or Uses, the application will panic and print information about
the illegal Component access. Setting GOHAN_DEBUG to 1 will also enable
printing System registration events and statistics.
*/
package gohan

View File

@ -16,9 +16,9 @@ func (w *World) NewEntity() Entity {
entityMutex.Lock()
defer entityMutex.Unlock()
if len(w.availableEntityIDs) > 0 {
id := w.availableEntityIDs[0]
w.availableEntityIDs = w.availableEntityIDs[1:]
if len(w.availableEntities) > 0 {
id := w.availableEntities[0]
w.availableEntities = w.availableEntities[1:]
w.allEntities = append(w.allEntities, id)
return id
}
@ -32,17 +32,24 @@ func (w *World) NewEntity() Entity {
// RemoveEntity removes the provided Entity's components, causing it to no
// longer be handled by any system. Because Gohan reuses removed EntityIDs,
// applications must also remove any internal references to the removed Entity.
func (w *World) RemoveEntity(entity Entity) {
func (w *World) RemoveEntity(entity Entity) bool {
entityMutex.Lock()
defer entityMutex.Unlock()
for i, e := range w.allEntities {
if e == entity {
w.allEntities = append(w.allEntities[:i], w.allEntities[i+1:]...)
w.removedEntities = append(w.removedEntities, e)
return
// Remove components.
for i := range w.components[entity] {
w.components[entity][i] = nil
}
w.removedEntities = append(w.removedEntities, entity)
return true
}
}
return false
}
var numEntities int

View File

@ -0,0 +1,30 @@
project_name: twinstick
builds:
-
id: twinstick
flags:
- -tags=example
goos:
- js
- linux
- windows
goarch:
- amd64
- wasm
archives:
-
id: twinstick
builds:
- twinstick
replacements:
386: i386
format_overrides:
- goos: js
format: zip
- goos: windows
format: zip
# files:
# - src: '../../*.md'
checksum:
name_template: 'checksums.txt'

View File

@ -20,10 +20,6 @@ func NewDrawBulletsSystem() *DrawBulletsSystem {
}
}
func (s *DrawBulletsSystem) Name() string {
return "DrawBullets"
}
func (s *DrawBulletsSystem) Needs() []gohan.ComponentID {
return []gohan.ComponentID{
component.PositionComponentID,
@ -35,7 +31,7 @@ func (s *DrawBulletsSystem) Uses() []gohan.ComponentID {
return nil
}
func (s *DrawBulletsSystem) Update(ctx *gohan.Context) error {
func (s *DrawBulletsSystem) Update(_ *gohan.Context) error {
return gohan.ErrSystemWithoutUpdate
}

View File

@ -22,10 +22,6 @@ func NewDrawPlayerSystem(player gohan.Entity) *drawPlayerSystem {
}
}
func (s *drawPlayerSystem) Name() string {
return "DrawPlayer"
}
func (s *drawPlayerSystem) Needs() []gohan.ComponentID {
return []gohan.ComponentID{
component.PositionComponentID,

View File

@ -27,10 +27,6 @@ func NewFireInputSystem(player gohan.Entity) *fireInputSystem {
}
}
func (s *fireInputSystem) Name() string {
return "FireInput"
}
func (_ *fireInputSystem) Needs() []gohan.ComponentID {
return []gohan.ComponentID{
component.PositionComponentID,
@ -107,6 +103,6 @@ func (s *fireInputSystem) Update(ctx *gohan.Context) error {
return nil
}
func (_ *fireInputSystem) Draw(ctx *gohan.Context, _ *ebiten.Image) error {
func (_ *fireInputSystem) Draw(_ *gohan.Context, _ *ebiten.Image) error {
return gohan.ErrSystemWithoutDraw
}

View File

@ -19,10 +19,6 @@ func NewMovementInputSystem(player gohan.Entity) *movementInputSystem {
}
}
func (s *movementInputSystem) Name() string {
return "MovementInput"
}
func (s *movementInputSystem) Needs() []gohan.ComponentID {
return []gohan.ComponentID{
component.VelocityComponentID,
@ -63,6 +59,6 @@ func (s *movementInputSystem) Update(ctx *gohan.Context) error {
return nil
}
func (s *movementInputSystem) Draw(ctx *gohan.Context, _ *ebiten.Image) error {
func (s *movementInputSystem) Draw(_ *gohan.Context, _ *ebiten.Image) error {
return gohan.ErrSystemWithoutDraw
}

View File

@ -27,10 +27,6 @@ func NewProfileSystem(player gohan.Entity) *profileSystem {
}
}
func (s *profileSystem) Name() string {
return "Profile"
}
func (s *profileSystem) Needs() []gohan.ComponentID {
return []gohan.ComponentID{
component.WeaponComponentID,
@ -41,13 +37,12 @@ func (s *profileSystem) Uses() []gohan.ComponentID {
return nil
}
func (s *profileSystem) Update(ctx *gohan.Context) error {
func (s *profileSystem) Update(_ *gohan.Context) error {
if ebiten.IsKeyPressed(ebiten.KeyControl) && inpututil.IsKeyJustPressed(ebiten.KeyP) {
if s.cpuProfile == nil {
log.Println("CPU profiling started...")
runtime.SetCPUProfileRate(0)
runtime.SetCPUProfileRate(1000)
runtime.SetCPUProfileRate(10000)
homeDir, err := os.UserHomeDir()
if err != nil {
@ -75,6 +70,6 @@ func (s *profileSystem) Update(ctx *gohan.Context) error {
return nil
}
func (s *profileSystem) Draw(ctx *gohan.Context, _ *ebiten.Image) error {
func (s *profileSystem) Draw(_ *gohan.Context, _ *ebiten.Image) error {
return gohan.ErrSystemWithoutDraw
}

View File

@ -14,10 +14,6 @@ type MovementSystem struct {
Player gohan.Entity
}
func (s *MovementSystem) Name() string {
return "Movement"
}
func (_ *MovementSystem) Needs() []gohan.ComponentID {
return []gohan.ComponentID{
component.PositionComponentID,
@ -81,6 +77,6 @@ func (s *MovementSystem) Update(ctx *gohan.Context) error {
return nil
}
func (_ *MovementSystem) Draw(ctx *gohan.Context, _ *ebiten.Image) error {
func (_ *MovementSystem) Draw(_ *gohan.Context, _ *ebiten.Image) error {
return gohan.ErrSystemWithoutDraw
}

View File

@ -29,10 +29,6 @@ func NewPrintInfoSystem(player gohan.Entity) *printInfoSystem {
return p
}
func (s *printInfoSystem) Name() string {
return "PrintInfo"
}
func (s *printInfoSystem) Needs() []gohan.ComponentID {
return []gohan.ComponentID{
component.WeaponComponentID,
@ -43,11 +39,11 @@ func (s *printInfoSystem) Uses() []gohan.ComponentID {
return nil
}
func (s *printInfoSystem) Update(ctx *gohan.Context) error {
func (s *printInfoSystem) Update(_ *gohan.Context) error {
return gohan.ErrSystemWithoutUpdate
}
func (s *printInfoSystem) Draw(ctx *gohan.Context, screen *ebiten.Image) error {
func (s *printInfoSystem) Draw(_ *gohan.Context, screen *ebiten.Image) error {
w := world.World
s.img.Clear()

View File

@ -1,3 +1,6 @@
//go:build example
// +build example
package world
import "code.rocketnine.space/tslocum/gohan"

View File

@ -11,12 +11,8 @@ import (
// indicating that the system does not utilize one of the methods. System
// methods which return one of these special error values will not be called again.
//
// See ErrSystemWithoutUpdate and ErrSystemWithoutDraw.
// See the special error values ErrSystemWithoutUpdate and ErrSystemWithoutDraw.
type System interface {
// Name returns the name of the System. This is only used when printing
// debug and error messages.
Name() string
// Needs returns a list of Components (specified by ID) which are required
// for an Entity to be handled by the System. When the game is running,
// matching entities will be passed to the Update and Draw methods.
@ -28,10 +24,10 @@ type System interface {
// return Components which are not also returned by Needs.
Uses() []ComponentID
// Update is called once for each matching entity each time the game state is updated.
// Update is called once for each matching Entity each time the game state is updated.
Update(ctx *Context) error
// Draw is called once for each matching entity each time the game is drawn to the screen.
// Draw is called once for each matching Entity each time the game is drawn to the screen.
Draw(ctx *Context, screen *ebiten.Image) error
}

107
world.go
View File

@ -4,6 +4,7 @@ import (
"fmt"
"log"
"os"
"reflect"
"strconv"
"strings"
"sync"
@ -12,14 +13,22 @@ import (
"github.com/hajimehoshi/ebiten/v2"
)
var debug bool
var debug int
func init() {
debugEnv := os.Getenv("GOHAN_DEBUG")
debugEnv = strings.TrimSpace(debugEnv)
debugEnv = strings.ToLower(debugEnv)
debug = debugEnv == "1" || debugEnv == "t" || debugEnv == "y" || debugEnv == "on" || debugEnv == "yes" || debugEnv == "true"
i, err := strconv.Atoi(debugEnv)
if err == nil {
debug = i
return
}
if debugEnv == "t" || debugEnv == "y" || debugEnv == "on" || debugEnv == "yes" || debugEnv == "true" {
debug = 1
}
}
// World represents a collection of Entities, Components and Systems.
@ -28,16 +37,18 @@ type World struct {
maxComponentID ComponentID
components [][]interface{}
components [][]interface{} // components[Entity][ComponentID]Component
allEntities []Entity
modifiedEntities []Entity
removedEntities []Entity
// availableEntityIDs is the set of EntityIDs available because they were
handledModifiedEntities map[Entity]bool
// availableEntities is the set of EntityIDs available because they were
// removed from the game.
availableEntityIDs []Entity
availableEntities []Entity
systems []System
systemEntities [][]Entity // Slice of entities matching each system.
@ -62,9 +73,12 @@ type World struct {
sync.Mutex
}
// NewWorld returns a new World.
func NewWorld() *World {
w := &World{
cacheTime: time.Second,
handledModifiedEntities: make(map[Entity]bool),
}
w.ctx = &Context{
@ -91,8 +105,8 @@ ATTACH:
w.systemEntities[systemIndex] = append(w.systemEntities[systemIndex], entity)
if debug {
log.Printf("Attached entity %d to system %s.", entity, w.systemName(systemIndex))
if debug > 1 {
log.Printf("Attached entity %d to %s.", entity, w.systemName(systemIndex))
}
}
}
@ -123,12 +137,13 @@ func AddSystemAfter(system System, after ...System) {
attachEntitiesToSystem(system)
}
*/
func (w *World) systemName(i int) string {
name := w.systems[i].Name()
if name == "" {
name = strconv.Itoa(i)
t := reflect.TypeOf(w.systems[i])
for t.Kind() == reflect.Ptr {
return strings.Title(t.Elem().Name())
}
return name
return strings.Title(t.Name())
}
func (w *World) updateSystem(i int) (int, error) {
@ -144,17 +159,14 @@ func (w *World) updateSystem(i int) (int, error) {
w.systemReceivesUpdate[i] = false
return 0, nil
}
return 0, fmt.Errorf("failed to update system %s for entity %d: %+v", w.systemName(i), entity, err)
return 0, fmt.Errorf("failed to update %s for entity %d: %+v", w.systemName(i), entity, err)
}
updated++
}
return updated, nil
}
func (w *World) propagateEntityChanges() {
entityMutex.Lock()
defer entityMutex.Unlock()
func (w *World) _handleRemovedEntities() {
for _, entity := range w.removedEntities {
// Remove from attached systems.
REMOVED:
@ -166,17 +178,21 @@ func (w *World) propagateEntityChanges() {
}
}
}
// Remove components.
w.components[entity] = make([]interface{}, w.maxComponentID+1)
}
// Mark EntityIDs as available.
w.availableEntityIDs = append(w.availableEntityIDs, w.removedEntities...)
// Mark EntityID as available.
w.availableEntities = append(w.availableEntities, w.removedEntities...)
w.removedEntities = nil
w.removedEntities = w.removedEntities[:0]
}
func (w *World) _handleModifiedEntities() {
for _, entity := range w.modifiedEntities {
if w.handledModifiedEntities[entity] {
continue
}
w.handledModifiedEntities[entity] = true
for i := range w.systems {
systemEntityIndex := -1
for j, systemEntity := range w.systemEntities[i] {
@ -201,8 +217,8 @@ func (w *World) propagateEntityChanges() {
w.systemEntities[i] = append(w.systemEntities[i], entity)
if debug {
log.Printf("Attached entity %d to system %s.", entity, w.systemName(i))
if debug > 1 {
log.Printf("Attached entity %d to %s.", entity, w.systemName(i))
}
} else if systemEntityIndex != -1 {
// Detach from system.
@ -210,7 +226,20 @@ func (w *World) propagateEntityChanges() {
}
}
}
w.modifiedEntities = nil
for k := range w.handledModifiedEntities {
delete(w.handledModifiedEntities, k)
}
w.modifiedEntities = w.modifiedEntities[:0]
}
func (w *World) propagateEntityChanges() {
entityMutex.Lock()
defer entityMutex.Unlock()
w._handleRemovedEntities()
w._handleModifiedEntities()
}
// Update updates the game state.
@ -221,10 +250,10 @@ func (w *World) Update() error {
w.propagateEntityChanges()
var t time.Time
if debug {
if debug != 0 {
t = time.Now()
}
var systems int
var entitiesUpdated int
for i, registered := range w.systemReceivesUpdate {
if !registered {
@ -236,16 +265,15 @@ func (w *World) Update() error {
}
entitiesUpdated += updated
systems++
if debug {
log.Printf("System %s: updated %d entities.", w.systemName(i), updated)
if debug != 0 {
log.Printf("- %s: %d updated.", w.systemName(i), updated)
}
}
w.systemUpdatedEntities = entitiesUpdated
if debug {
log.Printf("Finished updating %d systems in %.2fms.", systems, float64(time.Since(t).Microseconds())/1000)
if debug != 0 {
log.Printf("Handled %d entity updates in %.2fms.", entitiesUpdated, float64(time.Since(t).Microseconds())/1000)
}
return nil
}
@ -274,7 +302,7 @@ func (w *World) drawSystem(i int, screen *ebiten.Image) (int, error) {
w.systemReceivesDraw[i] = false
return 0, nil
}
return 0, fmt.Errorf("failed to draw system %s for entity %d: %+v", w.systemName(i), entity, err)
return 0, fmt.Errorf("failed to draw %s for entity %d: %+v", w.systemName(i), entity, err)
}
drawn++
}
@ -288,6 +316,11 @@ func (w *World) Draw(screen *ebiten.Image) error {
w.propagateEntityChanges()
var t time.Time
if debug != 0 {
t = time.Now()
}
var entitiesDrawn int
for i, registered := range w.systemReceivesDraw {
if !registered {
@ -300,8 +333,16 @@ func (w *World) Draw(screen *ebiten.Image) error {
}
entitiesDrawn += drawn
if debug != 0 {
log.Printf("- %s: %d drawn.", w.systemName(i), drawn)
}
}
w.systemDrawnEntities = entitiesDrawn
if debug != 0 {
log.Printf("Handled %d entity draws in %.2fms.", entitiesDrawn, float64(time.Since(t).Microseconds())/1000)
}
return nil
}

View File

@ -12,10 +12,6 @@ type movementSystem struct {
velocityComponentID ComponentID
}
func (s *movementSystem) Name() string {
return "Movement"
}
func (s *movementSystem) Needs() []ComponentID {
return []ComponentID{
s.positionComponentID,
@ -44,12 +40,21 @@ func TestWorld(t *testing.T) {
w, e, positionComponentID, velocityComponentID := newTestWorld()
entities := make([]Entity, iterations)
position := w.Component(e, positionComponentID).(*positionComponent)
velocity := w.Component(e, velocityComponentID).(*velocityComponent)
expectedX, expectedY := position.X+(velocity.X*iterations), position.Y+(velocity.Y*iterations)
for i := 0; i < iterations; i++ {
entities[i] = w.NewEntity()
if i > 0 {
if !w.RemoveEntity(entities[i-1]) {
t.Errorf("failed to remove entity %d", entities[i-1])
}
}
err := w.Update()
if err != nil {
t.Fatal(err)
@ -63,7 +68,7 @@ func TestWorld(t *testing.T) {
}
}
func BenchmarkUpdateWorld(b *testing.B) {
func BenchmarkUpdateWorldInactive(b *testing.B) {
w, _, _, _ := newTestWorld()
b.StopTimer()
@ -79,6 +84,31 @@ func BenchmarkUpdateWorld(b *testing.B) {
}
}
func BenchmarkUpdateWorldActive(b *testing.B) {
w, _, _, _ := newTestWorld()
entities := make([]Entity, b.N)
b.StopTimer()
b.ResetTimer()
b.ReportAllocs()
b.StartTimer()
for i := 0; i < b.N; i++ {
entities[i] = w.NewEntity()
if i > 0 {
if !w.RemoveEntity(entities[i-1]) {
b.Errorf("failed to remove entity %d", entities[i-1])
}
}
err := w.Update()
if err != nil {
b.Fatal(err)
}
}
}
func newTestWorld() (w *World, e Entity, positionComponentID ComponentID, velocityComponentID ComponentID) {
w = NewWorld()
@ -114,3 +144,6 @@ func newTestWorld() (w *World, e Entity, positionComponentID ComponentID, veloci
func round(f float64) float64 {
return math.Round(f*10) / 10
}
// Note: Because drawing a System is functionally the same as updating a System,
// as only an extra argument is passed, there are no drawing tests or benchmarks.