From 3b105865902bec559b56ee6222e9061a5ebabf19 Mon Sep 17 00:00:00 2001 From: walker Date: Fri, 4 Aug 2023 11:02:08 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 33 +++++++ common_component.go | 9 ++ component.go | 47 ++++++++++ ecs_test.go | 44 +++++++++ entity.go | 9 ++ entry.go | 7 ++ events.go | 11 +++ go.mod | 5 + go.sum | 6 ++ query.go | 33 +++++++ system.go | 5 + world.go | 219 ++++++++++++++++++++++++++++++++++++++++++++ 12 files changed, 428 insertions(+) create mode 100644 .gitignore create mode 100644 common_component.go create mode 100644 component.go create mode 100644 ecs_test.go create mode 100644 entity.go create mode 100644 entry.go create mode 100644 events.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 query.go create mode 100644 system.go create mode 100644 world.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..553e134 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +.DS_Store +.thumbs.db +node_modules + +# Quasar core related directories +.quasar +/dist + +# Cordova related directories and files +/src-cordova/node_modules +/src-cordova/platforms +/src-cordova/plugins +/src-cordova/www + +# Capacitor related directories and files +/src-capacitor/www +/src-capacitor/node_modules + +# BEX related directories and files +/src-bex/www +/src-bex/js/core + +# Log files +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Editor directories and files +.idea +*.suo +*.ntvs* +*.njsproj +*.sln diff --git a/common_component.go b/common_component.go new file mode 100644 index 0000000..9fa0d4b --- /dev/null +++ b/common_component.go @@ -0,0 +1,9 @@ +package ecs + +type Id string + +var IdComp = NewComponentType[Id]() + +// func IdComp() *ComponentType[Id] { +// return NewComponentType[Id]() +// } diff --git a/component.go b/component.go new file mode 100644 index 0000000..fe302a4 --- /dev/null +++ b/component.go @@ -0,0 +1,47 @@ +package ecs + +import ( + "github.com/yohamta/donburi" +) + +type ComponentType[T any] struct { + *donburi.ComponentType[T] +} + +// Get returns component data from the entry. +func (c *ComponentType[T]) Get(entry *Entry) *T { + return c.ComponentType.Get(entry.Entry) +} + +// Set sets component data to the entry. +func (c *ComponentType[T]) Set(entry *Entry, component *T) { + c.ComponentType.Set(entry.Entry, component) +} + +// // Each iterates over the entities that have the component. +// func (c *ComponentType[T]) Each(w World, callback func(*Entry)) { +// c.ComponentType.Each(w.(*world).world, func(entry *donburi.Entry) { +// callback(&Entry{Entry: entry}) +// }) +// } + +// // First returns the first entity that has the component. +// func (c *ComponentType[T]) First(w World) (*Entry, bool) { +// entry, ok := c.ComponentType.First(w.(*world).world) +// return &Entry{entry}, ok +// } + +// // MustFirst returns the first entity that has the component or panics. +// func (c *ComponentType[T]) MustFirst(w World) *Entry { +// e, ok := c.First(w) +// if !ok { +// panic(fmt.Sprintf("no entity has the component %s", c.ComponentType.Name())) +// } + +// return e +// } + +// SetValue sets the value of the component. +func (c *ComponentType[T]) SetValue(entry *Entry, value T) { + c.ComponentType.SetValue(entry.Entry, value) +} diff --git a/ecs_test.go b/ecs_test.go new file mode 100644 index 0000000..3d83358 --- /dev/null +++ b/ecs_test.go @@ -0,0 +1,44 @@ +package ecs_test + +import ( + "log" + "testing" + "time" + + "joylink.club/ecs" +) + +type Sys1 struct { +} + +type Sys2 struct { +} + +func (s *Sys1) Update(w ecs.World) { + log.Println("系统1运行") +} + +func (s *Sys2) Update(w ecs.World) { + log.Println("系统2运行") +} + +func BaseTest(t *testing.T) { + log.Println("Base Test") + w := ecs.NewWorld(500) + w.AddSystem(&Sys1{}, &Sys2{}) + w.StartUp() + + time.Sleep(3 * time.Second) + // w.SetSpeed(2) + + // time.Sleep(3 * time.Second) + // w.Pause() + + // time.Sleep(2 * time.Second) + // w.Resume() + + // time.Sleep(2 * time.Second) + // w.Close() + + // time.Sleep(3 * time.Second) +} diff --git a/entity.go b/entity.go new file mode 100644 index 0000000..352feca --- /dev/null +++ b/entity.go @@ -0,0 +1,9 @@ +package ecs + +import "github.com/yohamta/donburi" + +type ( + Entity struct { + donburi.Entity + } +) diff --git a/entry.go b/entry.go new file mode 100644 index 0000000..d66c848 --- /dev/null +++ b/entry.go @@ -0,0 +1,7 @@ +package ecs + +import "github.com/yohamta/donburi" + +type Entry struct { + *donburi.Entry +} diff --git a/events.go b/events.go new file mode 100644 index 0000000..e70b54e --- /dev/null +++ b/events.go @@ -0,0 +1,11 @@ +package ecs + +type ( + EventType[T any] struct { + eventName string + componentType *ComponentType[T] + w World + } + + Subscriber[T any] func(w *World, event T) +) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b58e068 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module joylink.club/ecs + +go 1.20 + +require github.com/yohamta/donburi v1.3.8 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..9ac4ec0 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/yohamta/donburi v1.3.8 h1:ca4NuhzJ8Jeb6GAEf6ecksa+l8JWaAnr0WLqG20TimU= +github.com/yohamta/donburi v1.3.8/go.mod h1:5QkyraUjkzbMVTD2b8jaPFy1Uwjm/zdFN1c1lZGaezg= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= diff --git a/query.go b/query.go new file mode 100644 index 0000000..3ec778a --- /dev/null +++ b/query.go @@ -0,0 +1,33 @@ +package ecs + +import ( + "github.com/yohamta/donburi" + "github.com/yohamta/donburi/filter" +) + +type Query struct { + *donburi.Query +} + +// NewQuery creates a new query. +// It receives arbitrary filters that are used to filter entities. +func NewQuery(filter filter.LayoutFilter) *Query { + return &Query{ + donburi.NewQuery(filter), + } +} + +func (q *Query) Each(w World, callback func(*Entry)) { + q.Query.Each(w.(*world).world, func(entry *donburi.Entry) { + callback(&Entry{entry}) + }) +} + +func (q *Query) Count(w World) int { + return q.Query.Count(w.(*world).world) +} + +func (q *Query) First(w World) (entry *Entry, ok bool) { + e, ok := q.Query.First(w.(*world).world) + return &Entry{e}, ok +} diff --git a/system.go b/system.go new file mode 100644 index 0000000..0890e2f --- /dev/null +++ b/system.go @@ -0,0 +1,5 @@ +package ecs + +type ISystem interface { + Update(w World) +} diff --git a/world.go b/world.go new file mode 100644 index 0000000..4110b07 --- /dev/null +++ b/world.go @@ -0,0 +1,219 @@ +package ecs + +import ( + "fmt" + "log" + "reflect" + "time" + + "github.com/yohamta/donburi" + "github.com/yohamta/donburi/features/events" +) + +type WorldState int + +type WorldId int + +const ( + Init WorldState = iota + Running + Pause + Error + Closed +) + +type World interface { + + // Id returns the unique identifier for the world. + Id() WorldId + // Create creates a new entity with the specified components. + Create(components ...donburi.IComponentType) Entity + // CreateMany creates a new entity with the specified components. + CreateMany(n int, components ...donburi.IComponentType) []Entity + // Entry returns an entry for the specified entity. + Entry(entity Entity) *Entry + // Remove removes the specified entity. + Remove(entity Entity) + // Valid returns true if the specified entity is valid. + Valid(e Entity) bool + // Len returns the number of entities in the world. + Len() int + + StartUp() + Pause() + Resume() + SetSpeed(speed float64) error + AddSystem(sys ...ISystem) + Close() +} + +type world struct { + world donburi.World + systems []ISystem + state WorldState + tick int + speed float64 + + quit chan struct{} +} + +func NewComponentType[T any](opts ...interface{}) *ComponentType[T] { + ct := donburi.NewComponentType[T](opts...) + return &ComponentType[T]{ct} +} + +func NewWorld(tick int) World { + return &world{ + world: donburi.NewWorld(), + systems: make([]ISystem, 0), + state: Init, + tick: tick, + speed: 1, + quit: make(chan struct{}), + } +} + +func (w *world) Id() WorldId { + return WorldId(w.world.Id()) +} + +func (w *world) Create(components ...donburi.IComponentType) Entity { + len := len(components) + var icts []donburi.IComponentType = make([]donburi.IComponentType, len) + for i := 0; i < len; i++ { + icts[i] = components[i].(ComponentType[any]).ComponentType + } + entity := w.world.Create(icts...) + return Entity{entity} +} + +func (w *world) CreateMany(n int, components ...donburi.IComponentType) []Entity { + entitys := w.world.CreateMany(n, components...) + ets := make([]Entity, len(entitys)) + for i, e := range entitys { + ets[i] = Entity{e} + } + return ets +} + +func (w *world) Entry(entity Entity) *Entry { + return &Entry{w.world.Entry(entity.Entity)} +} + +func (w *world) Remove(entity Entity) { + w.world.Remove(entity.Entity) +} + +func (w *world) Valid(e Entity) bool { + return w.world.Valid(e.Entity) +} + +func (w *world) Len() int { + return w.world.Len() +} + +// 添加系统 +func (w *world) AddSystem(sys ...ISystem) { + w.systems = append(w.systems, sys...) +} + +// 执行所有事件处理 +func (w *world) ProcessAllEvents() { + events.ProcessAllEvents(w.world) +} + +// 暂停世界 +func (w *world) Pause() { + if w.state == Running { + w.state = Pause + } +} + +// 恢复世界运行 +func (w *world) Resume() { + if w.state == Pause { + w.state = Running + } +} + +const ( + SpeedMin = 0.1 + SpeedMax = 10 +) + +// 设置世界运行速度 +func (w *world) SetSpeed(speed float64) error { + if speed < SpeedMin || speed > SpeedMax { + return fmt.Errorf("速度必须在[%f, %d]之间", SpeedMin, SpeedMax) + } + w.speed = speed + return nil +} + +// 启动世界,世界逻辑开始执行且世界为运行状态 +func (w *world) StartUp() { + if w.state == Init { // 避免重复运行 + w.state = Running + go w.run() + } +} + +// 关闭世界 +func (w *world) Close() { + w.quit <- struct{}{} +} + +func (w *world) run() { + for { + select { + case <-w.quit: // 退出信号 + log.Println("仿真退出,id:", w.world.Id()) + w.state = Closed + default: + } + if w.state == Error { + log.Println("世界错误,关闭世界,id:", w.world.Id()) + return + } + if w.state == Closed { + log.Println("世界正常关闭,id:", w.world.Id()) + return + } + if w.state == Pause { // 暂停不更新 + log.Println("仿真暂停中,id:", w.world.Id()) + sleep := int64(float64(w.tick) / (w.speed)) + time.Sleep(time.Duration(sleep) * time.Millisecond) + continue + } + start := time.Now() + // fmt.Println("仿真更新,id:", info.id) + for _, sys := range w.systems { + sys.Update(w) + } + + // 处理所有事件 + // w.ProcessAllEvents() + + // 执行逻辑花费时间,单位ms + ot := time.Duration(time.Now().Nanosecond() - start.Nanosecond()).Milliseconds() + // 根据间隔和速度计算休眠时间 + sleep := int64(float64(w.tick)/(w.speed)) - ot + if sleep > 0 { + time.Sleep(time.Duration(sleep) * time.Millisecond) + } else { + log.Println("仿真无休眠,id:", w.world.Id()) + } + } +} + +func NewEventType[T any](w World) *EventType[T] { + // events.NewEventType() + ct := NewComponentType[T]() + var et T + name := reflect.TypeOf(et).Name() + return &EventType[T]{ + name, + ct, + w, + } +}