diff --git a/.github/workflows/master_workflow.yml b/.github/workflows/master_workflow.yml index 5b9a2596..8b4a3a5e 100644 --- a/.github/workflows/master_workflow.yml +++ b/.github/workflows/master_workflow.yml @@ -12,7 +12,7 @@ jobs: fail-fast: false matrix: os: [ 'ubuntu-latest' ] - go-version: [ '1.23.5' ] + go-version: [ '1.24.0' ] uses: ./.github/workflows/go_ci.yml with: go-version: ${{ matrix.go-version }} @@ -21,4 +21,4 @@ jobs: security-analysis: uses: ./.github/workflows/gosec_security_check.yml with: - go-version: '1.23.5' \ No newline at end of file + go-version: '1.24.0' \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 131dd1eb..426f3dcf 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,7 +7,7 @@ "request": "launch", "mode": "debug", "buildFlags": "-tags 'assert'", - "program": "./cmd/raylib-ecs" + "program": "./examples/new-api" } ] } \ No newline at end of file diff --git a/Roboto-SemiBold.ttf b/Roboto-SemiBold.ttf new file mode 100644 index 00000000..3f348341 Binary files /dev/null and b/Roboto-SemiBold.ttf differ diff --git a/aos_soa_test.go b/aos_soa_test.go new file mode 100644 index 00000000..d23a9a87 --- /dev/null +++ b/aos_soa_test.go @@ -0,0 +1,181 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package gomp + +import ( + "math/rand" + "testing" +) + +type Float3 struct { + X, Y, Z float64 +} + +// Array of Structures (AoS) +type Unit struct { + Pos Float3 + Velocity Float3 + HP int + Offset [AdditionalDataPer64]int64 +} + +type UnitsAoS struct { + Units []Unit +} + +// Structure of Arrays (SoA) +type UnitsSoA struct { + Positions [][pageSize]Float3 + Velocities [][pageSize]Float3 + HPs [][pageSize]int + Offsets [][pageSize][AdditionalDataPer64]int64 +} + +func UpdatePosition(pos *Float3, velocity *Float3) { + pos.X += velocity.X + pos.Y += velocity.Y + pos.Z += velocity.Z +} + +func TakeDamage(hp *int, damage int) { + *hp = max(0, *hp-damage) +} + +func UpdateOffset(offset *[AdditionalDataPer64]int64) { + for i := range offset { + offset[i]++ + } +} + +func ShouldRender(pos *Float3) bool { + return pos.X > 10.0 && pos.Y < 5.0 && pos.Z > 0.0 +} + +func GenerateUnitsAoS(count int) UnitsAoS { + units := UnitsAoS{Units: make([]Unit, count)} + for i := 0; i < count; i++ { + units.Units[i] = Unit{ + Pos: Float3{ + X: rand.Float64()*40 - 20, + Y: rand.Float64()*40 - 20, + Z: rand.Float64()*40 - 20, + }, + Velocity: Float3{ + X: rand.Float64()*2 - 1, + Y: rand.Float64()*2 - 1, + Z: rand.Float64()*2 - 1, + }, + HP: rand.Intn(151) + 50, + } + } + return units +} + +func MakeUnitsSoA(aos UnitsAoS) UnitsSoA { + soa := UnitsSoA{ + Positions: make([][pageSize]Float3, len(aos.Units)/pageSize+1), + Velocities: make([][pageSize]Float3, len(aos.Units)/pageSize+1), + HPs: make([][pageSize]int, len(aos.Units)/pageSize+1), + Offsets: make([][pageSize][AdditionalDataPer64]int64, len(aos.Units)/pageSize+1), + } + + for i, unit := range aos.Units { + a, b := i>>pageShift, i%pageSize + soa.Positions[a][b] = unit.Pos + soa.Velocities[a][b] = unit.Velocity + soa.HPs[a][b] = unit.HP + soa.Offsets[a][b] = unit.Offset + } + return soa +} + +func PositionSystem(positions [][pageSize]Float3, velocities [][pageSize]Float3) { + for i := 0; i < len(positions); i++ { + for j := 0; j < len(positions[i]); j++ { + UpdatePosition(&positions[i][j], &velocities[i][j]) + } + } +} + +func TakeDamageSystem(hps [][pageSize]int) { + for i := 0; i < len(hps); i++ { + for j := 0; j < len(hps[i]); j++ { + TakeDamage(&hps[i][j], 33) + } + } +} + +func OffsetSystem(offsets [][pageSize][AdditionalDataPer64]int64) { + for i := 0; i < len(offsets); i++ { + for j := 0; j < len(offsets[i]); j++ { + UpdateOffset(&offsets[i][j]) + } + } +} + +func renderSystem(positions [][pageSize]Float3, unitsToRender []int) { + for i := 0; i < len(positions); i++ { + for j := 0; j < len(positions[i]); j++ { + if ShouldRender(&positions[i][j]) { + index := j + i*len(positions[i]) + unitsToRender = append(unitsToRender, index) + } + } + } +} + +// RuDimbo Team +func BenchmarkUpdateRuDimbo(b *testing.B) { + b.ReportAllocs() + + units := GenerateUnitsAoS(NumOfEntities) + unitsToRender := make([]int, 0, NumOfEntities) + + for b.Loop() { + for i := range units.Units { + unit := &units.Units[i] + UpdatePosition(&unit.Pos, &unit.Velocity) + if ShouldRender(&unit.Pos) { + unitsToRender = append(unitsToRender, i) + } + TakeDamage(&unit.HP, 33) + UpdateOffset(&unit.Offset) + } + unitsToRender = unitsToRender[:0] + } +} + +// Rodd Team +func BenchmarkUpdateRodd(b *testing.B) { + b.ReportAllocs() + soaUnits := MakeUnitsSoA(GenerateUnitsAoS(NumOfEntities)) + unitsToRender := make([]int, 0, NumOfEntities) + for b.Loop() { + positions := soaUnits.Positions + velocities := soaUnits.Velocities + hps := soaUnits.HPs + offsets := soaUnits.Offsets + PositionSystem(positions, velocities) + TakeDamageSystem(hps) + OffsetSystem(offsets) + renderSystem(positions, unitsToRender) + unitsToRender = unitsToRender[:0] + } +} + +const NumOfEntities = 10_000_000 +const AdditionalDataPer64 = 30 +const pageShift = 10 +const pageSize = 1 << pageShift diff --git a/pkg/ecs/asset-library.go b/asset-library.go similarity index 86% rename from pkg/ecs/asset-library.go rename to asset-library.go index 1fb66553..5465366d 100644 --- a/pkg/ecs/asset-library.go +++ b/asset-library.go @@ -4,11 +4,9 @@ Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package ecs +package gomp import ( - "fmt" - "github.com/negrel/assert" ) @@ -18,8 +16,8 @@ type AnyAssetLibrary interface { UnloadAll() } -func CreateAssetLibrary[T any](loader func(path string) T, unloader func(path string, asset *T)) *AssetLibrary[T] { - return &AssetLibrary[T]{ +func CreateAssetLibrary[T any](loader func(path string) T, unloader func(path string, asset *T)) AssetLibrary[T] { + return AssetLibrary[T]{ data: make(map[string]*T), loader: loader, unloader: unloader, @@ -47,7 +45,7 @@ func (r *AssetLibrary[T]) Get(path string) *T { func (r *AssetLibrary[T]) Load(path string) { _, ok := r.data[path] - assert.False(ok, fmt.Errorf("asset already loaded: %s", path)) + assert.False(ok, "asset already loaded") resource := r.loader(path) r.data[path] = &resource @@ -68,7 +66,7 @@ func (r *AssetLibrary[T]) LoadAll() { func (r *AssetLibrary[T]) Unload(path string) { value, ok := r.data[path] - assert.True(ok, fmt.Errorf("asset not loaded: %s", path)) + assert.True(ok, "asset not loaded") r.unloader(path, value) delete(r.data, path) } diff --git a/assets/test.ttf b/assets/test.ttf new file mode 100644 index 00000000..52672188 Binary files /dev/null and b/assets/test.ttf differ diff --git a/desktop-components.go b/desktop-components.go new file mode 100644 index 00000000..d1b1ba62 --- /dev/null +++ b/desktop-components.go @@ -0,0 +1,53 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package gomp + +import ( + "gomp/stdcomponents" +) + +func NewDesktopComponents() DesktopComponents { + return DesktopComponents{ + Position: stdcomponents.NewPositionComponentManager(), + Rotation: stdcomponents.NewRotationComponentManager(), + Scale: stdcomponents.NewScaleComponentManager(), + Velocity: stdcomponents.NewVelocityComponentManager(), + Flip: stdcomponents.NewFlipComponentManager(), + Sprite: stdcomponents.NewSpriteComponentManager(), + SpriteSheet: stdcomponents.NewSpriteSheetComponentManager(), + SpriteMatrix: stdcomponents.NewSpriteMatrixComponentManager(), + Tint: stdcomponents.NewTintComponentManager(), + AnimationPlayer: stdcomponents.NewAnimationPlayerComponentManager(), + AnimationState: stdcomponents.NewAnimationStateComponentManager(), + RlTexturePro: stdcomponents.NewRlTextureProComponentManager(), + Network: stdcomponents.NewNetworkComponentManager(), + } +} + +type DesktopComponents struct { + Position stdcomponents.PositionComponentManager + Rotation stdcomponents.RotationComponentManager + Scale stdcomponents.ScaleComponentManager + Velocity stdcomponents.VelocityComponentManager + Flip stdcomponents.FlipComponentManager + Sprite stdcomponents.SpriteComponentManager + SpriteSheet stdcomponents.SpriteSheetComponentManager + SpriteMatrix stdcomponents.SpriteMatrixComponentManager + Tint stdcomponents.TintComponentManager + AnimationPlayer stdcomponents.AnimationPlayerComponentManager + AnimationState stdcomponents.AnimationStateComponentManager + RlTexturePro stdcomponents.RLTextureProComponentManager + Network stdcomponents.NetworkComponentManager +} diff --git a/engine.go b/engine.go new file mode 100644 index 00000000..0c305240 --- /dev/null +++ b/engine.go @@ -0,0 +1,24 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +<- HromRu Donated 2500 RUB +<- HromRu Donated 5000 RUB + +Thank you for your support! +*/ + +package gomp + +import ( + "gomp/pkg/core" +) + +func NewEngine(game core.AnyGame) core.Engine { + return core.NewEngine(game) +} diff --git a/examples/ebiten-ecs-370k/camera-system.go b/examples/ebiten-ecs-370k/camera-system.go deleted file mode 100644 index 2370ab1f..00000000 --- a/examples/ebiten-ecs-370k/camera-system.go +++ /dev/null @@ -1,114 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ - -package main - -import ( - "fmt" - ecs2 "gomp/pkg/ecs" - "image" - "image/color" - "image/draw" - "strings" - - "github.com/hajimehoshi/ebiten/v2" - "github.com/hajimehoshi/ebiten/v2/ebitenutil" - "golang.org/x/text/language" - "golang.org/x/text/message" -) - -type cameraSystem struct { - transformComponent *ecs2.ComponentManager[transform] - healthComponent *ecs2.ComponentManager[health] - colorComponent *ecs2.ComponentManager[color.RGBA] - movementComponent *ecs2.ComponentManager[movement] - cameraComponent *ecs2.ComponentManager[camera] - destroyComponentType *ecs2.ComponentManager[empty] - - buffer *image.RGBA - debugInfo []string - p *message.Printer -} - -func (s *cameraSystem) Init(world *ecs2.World) { - s.transformComponent = transformComponentType.GetManager(world) - s.healthComponent = healthComponentType.GetManager(world) - s.colorComponent = colorComponentType.GetManager(world) - s.movementComponent = movementComponentType.GetManager(world) - s.cameraComponent = cameraComponentType.GetManager(world) - s.destroyComponentType = destroyComponentType.GetManager(world) - - s.p = message.NewPrinter(language.Russian) - - newcamera := world.CreateEntity("camera") - s.cameraComponent.Create(newcamera, camera{ - mainLayer: cameraLayer{ - image: ebiten.NewImage(width, height), - zoom: 1, - }, - debugLayer: cameraLayer{ - image: ebiten.NewImage(width, height), - zoom: 2, - }, - }) - - s.buffer = image.NewRGBA(image.Rect(0, 0, width, height)) - s.debugInfo = make([]string, 0) -} - -func (s *cameraSystem) Run(world *ecs2.World) { - _, dy := ebiten.Wheel() - - draw.Draw(s.buffer, s.buffer.Bounds(), &image.Uniform{color.Transparent}, image.Point{}, draw.Src) - - s.colorComponent.AllParallel(func(entity ecs2.Entity, color *color.RGBA) bool { - if color == nil { - return true - } - - transform := s.transformComponent.Get(entity) - if transform == nil { - return true - } - - s.buffer.SetRGBA(int(transform.x), int(transform.y), *color) - return true - }) - - var mainCamera *camera - s.cameraComponent.AllData(func(c *camera) bool { - mainCamera = c - return false - }) - - mainCamera.mainLayer.image.Clear() - mainCamera.debugLayer.image.Clear() - - mainCamera.mainLayer.zoom += float64(dy) - mainCamera.mainLayer.image.WritePixels(s.buffer.Pix) - - if mainCamera.mainLayer.zoom < 0.1 { - mainCamera.mainLayer.zoom = 0.1 - } else if mainCamera.mainLayer.zoom > 100 { - mainCamera.mainLayer.zoom = 100 - } - - s.debugInfo = append(s.debugInfo, fmt.Sprintf("TPS %0.2f", ebiten.ActualTPS())) - s.debugInfo = append(s.debugInfo, fmt.Sprintf("FPS %0.2f", ebiten.ActualFPS())) - s.debugInfo = append(s.debugInfo, fmt.Sprintf("Zoom %0.2f", mainCamera.mainLayer.zoom)) - s.debugInfo = append(s.debugInfo, s.p.Sprintf("Entity count %d", entityCount)) - s.debugInfo = append(s.debugInfo, s.p.Sprintf("Transforms count %d", s.transformComponent.Len())) - s.debugInfo = append(s.debugInfo, s.p.Sprintf("Healths count %d", s.healthComponent.Len())) - s.debugInfo = append(s.debugInfo, s.p.Sprintf("Colors count %d", s.colorComponent.Len())) - s.debugInfo = append(s.debugInfo, s.p.Sprintf("Movements count %d", s.movementComponent.Len())) - s.debugInfo = append(s.debugInfo, s.p.Sprintf("Cameras count %d", s.cameraComponent.Len())) - s.debugInfo = append(s.debugInfo, s.p.Sprintf("Destroys count %d", s.destroyComponentType.Len())) - s.debugInfo = append(s.debugInfo, s.p.Sprintf("Pprof %d", pprofEnabled)) - - ebitenutil.DebugPrint(mainCamera.debugLayer.image, strings.Join(s.debugInfo, "\n")) - s.debugInfo = s.debugInfo[:0] -} -func (s *cameraSystem) Destroy(world *ecs2.World) {} diff --git a/examples/ebiten-ecs-370k/color-system.go b/examples/ebiten-ecs-370k/color-system.go deleted file mode 100644 index d0a58d00..00000000 --- a/examples/ebiten-ecs-370k/color-system.go +++ /dev/null @@ -1,48 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ - -package main - -import ( - ecs2 "gomp/pkg/ecs" - "image/color" -) - -type colorSystem struct { - transformComponent *ecs2.ComponentManager[transform] - healthComponent *ecs2.ComponentManager[health] - colorComponent *ecs2.ComponentManager[color.RGBA] - movementComponent *ecs2.ComponentManager[movement] - - baseColor color.RGBA -} - -func (s *colorSystem) Init(world *ecs2.World) { - s.transformComponent = transformComponentType.GetManager(world) - s.healthComponent = healthComponentType.GetManager(world) - s.colorComponent = colorComponentType.GetManager(world) - s.movementComponent = movementComponentType.GetManager(world) - - s.baseColor = color.RGBA{25, 220, 200, 255} -} -func (s *colorSystem) Run(world *ecs2.World) { - s.colorComponent.AllParallel(func(ei ecs2.Entity, c *color.RGBA) bool { - health := s.healthComponent.Get(ei) - if health == nil { - return true - } - - hpPercentage := float32(health.hp) / float32(health.maxHp) - - c.R = uint8(hpPercentage * float32(s.baseColor.R)) - c.G = uint8(hpPercentage * float32(s.baseColor.G)) - c.B = uint8(hpPercentage * float32(s.baseColor.B)) - c.A = s.baseColor.A - - return true - }) -} -func (s *colorSystem) Destroy(world *ecs2.World) {} diff --git a/examples/ebiten-ecs-370k/components.go b/examples/ebiten-ecs-370k/components.go deleted file mode 100644 index 84d35674..00000000 --- a/examples/ebiten-ecs-370k/components.go +++ /dev/null @@ -1,62 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ -package main - -import ( - ecs2 "gomp/pkg/ecs" - "image/color" - - "github.com/hajimehoshi/ebiten/v2" -) - -type transform struct { - x, y int32 -} - -type health struct { - hp, maxHp int32 -} - -type movement struct { - goToX, goToY int32 -} - -type cameraLayer struct { - image *ebiten.Image - zoom float64 -} -type camera struct { - mainLayer cameraLayer - debugLayer cameraLayer -} - -type empty struct{} - -const ( - transformId ecs2.ComponentID = iota - healthId - colorId - movementId - cameraId - destroyId -) - -var transformComponentType = ecs2.CreateComponentService[transform](transformId) -var healthComponentType = ecs2.CreateComponentService[health](healthId) -var colorComponentType = ecs2.CreateComponentService[color.RGBA](colorId) -var movementComponentType = ecs2.CreateComponentService[movement](movementId) -var cameraComponentType = ecs2.CreateComponentService[camera](cameraId) -var destroyComponentType = ecs2.CreateComponentService[empty](destroyId) - -// spawn creature every tick with random hp and position -// each creature looses hp every tick -// each creature has color that depends on its own maxHP and current hp -// when hp == 0 creature dies - -// spawn system -// movement system -// hp system -// destroy system diff --git a/examples/ebiten-ecs-370k/destroy-system.go b/examples/ebiten-ecs-370k/destroy-system.go deleted file mode 100644 index a0c5e922..00000000 --- a/examples/ebiten-ecs-370k/destroy-system.go +++ /dev/null @@ -1,41 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ - -package main - -import ( - ecs2 "gomp/pkg/ecs" - "image/color" -) - -type destroySystem struct { - transformComponent *ecs2.ComponentManager[transform] - healthComponent *ecs2.ComponentManager[health] - colorComponent *ecs2.ComponentManager[color.RGBA] - movementComponent *ecs2.ComponentManager[movement] - destroyComponent *ecs2.ComponentManager[empty] - - n int -} - -func (s *destroySystem) Init(world *ecs2.World) { - s.transformComponent = transformComponentType.GetManager(world) - s.healthComponent = healthComponentType.GetManager(world) - s.colorComponent = colorComponentType.GetManager(world) - s.movementComponent = movementComponentType.GetManager(world) - s.destroyComponent = destroyComponentType.GetManager(world) - -} -func (s *destroySystem) Run(world *ecs2.World) { - s.n = 0 - s.destroyComponent.All(func(e ecs2.Entity, h *empty) bool { - world.DestroyEntity(e) - entityCount-- - - return true - }) -} -func (s *destroySystem) Destroy(world *ecs2.World) {} diff --git a/examples/ebiten-ecs-370k/game.go b/examples/ebiten-ecs-370k/game.go deleted file mode 100644 index ef2306fe..00000000 --- a/examples/ebiten-ecs-370k/game.go +++ /dev/null @@ -1,82 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ - -package main - -import ( - "github.com/hajimehoshi/ebiten/v2" - ecs2 "gomp/pkg/ecs" -) - -type game struct { - world *ecs2.World - cameraComponents *ecs2.ComponentManager[camera] - op *ebiten.DrawImageOptions -} - -func newGame() game { - world := ecs2.CreateWorld("1 mil pixel") - - world.RegisterComponentServices( - &destroyComponentType, - &cameraComponentType, - &transformComponentType, - &healthComponentType, - &colorComponentType, - &movementComponentType, - ) - - // world.RegisterSystems(). - // Sequential( - // new(spawnSystem), - // new(hpSystem), - // new(colorSystem), - // new(destroySystem), - // new(cameraSystem), - // ) - - newGame := game{ - world: &world, - cameraComponents: cameraComponentType.GetManager(&world), - op: new(ebiten.DrawImageOptions), - } - - return newGame -} - -func (g *game) Update() error { - err := g.world.FixedUpdate() - if err != nil { - return err - } - - return nil -} - -func (g *game) Draw(screen *ebiten.Image) { - var mainCamera *camera - - g.cameraComponents.AllData(func(c *camera) bool { - mainCamera = c - return false - }) - - if mainCamera == nil { - return - } - - g.op.GeoM.Reset() - g.op.GeoM.Scale(mainCamera.mainLayer.zoom, mainCamera.mainLayer.zoom) - screen.DrawImage(mainCamera.mainLayer.image, g.op) - - g.op.GeoM.Reset() - g.op.GeoM.Scale(mainCamera.debugLayer.zoom, mainCamera.debugLayer.zoom) - screen.DrawImage(mainCamera.debugLayer.image, g.op) -} - -func (g *game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) { - return outsideWidth, outsideHeight -} diff --git a/examples/ebiten-ecs-370k/hp-system.go b/examples/ebiten-ecs-370k/hp-system.go deleted file mode 100644 index 3004d1cf..00000000 --- a/examples/ebiten-ecs-370k/hp-system.go +++ /dev/null @@ -1,40 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ - -package main - -import ( - ecs2 "gomp/pkg/ecs" - "image/color" -) - -type hpSystem struct { - transformComponent *ecs2.ComponentManager[transform] - healthComponent *ecs2.ComponentManager[health] - colorComponent *ecs2.ComponentManager[color.RGBA] - movementComponent *ecs2.ComponentManager[movement] - destroyComponent *ecs2.ComponentManager[empty] -} - -func (s *hpSystem) Init(world *ecs2.World) { - s.transformComponent = transformComponentType.GetManager(world) - s.healthComponent = healthComponentType.GetManager(world) - s.colorComponent = colorComponentType.GetManager(world) - s.movementComponent = movementComponentType.GetManager(world) - s.destroyComponent = destroyComponentType.GetManager(world) -} -func (s *hpSystem) Run(world *ecs2.World) { - s.healthComponent.AllParallel(func(entity ecs2.Entity, h *health) bool { - h.hp-- - - if h.hp <= 0 { - s.destroyComponent.Create(entity, struct{}{}) - } - - return true - }) -} -func (s *hpSystem) Destroy(world *ecs2.World) {} diff --git a/examples/ebiten-ecs-370k/main.go b/examples/ebiten-ecs-370k/main.go deleted file mode 100644 index f7a90258..00000000 --- a/examples/ebiten-ecs-370k/main.go +++ /dev/null @@ -1,34 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ - -package main - -import ( - "flag" - - "github.com/hajimehoshi/ebiten/v2" -) - -const ( - width = 1000 - height = 1000 -) - -var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file") - -func main() { - flag.Parse() - - newGame := newGame() - - ebiten.SetRunnableOnUnfocused(true) - ebiten.SetWindowSize(width, height) - ebiten.SetWindowTitle("370k pixel ecs") - - if err := ebiten.RunGame(&newGame); err != nil { - panic(err) - } -} diff --git a/examples/ebiten-ecs-370k/spawn-system.go b/examples/ebiten-ecs-370k/spawn-system.go deleted file mode 100644 index 45c41597..00000000 --- a/examples/ebiten-ecs-370k/spawn-system.go +++ /dev/null @@ -1,105 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ - -package main - -import ( - "fmt" - ecs2 "gomp/pkg/ecs" - "image/color" - "log" - "math/rand" - "os" - "runtime/pprof" - - "github.com/hajimehoshi/ebiten/v2" - "github.com/hajimehoshi/ebiten/v2/inpututil" -) - -type spawnSystem struct { - transformComponent *ecs2.ComponentManager[transform] - healthComponent *ecs2.ComponentManager[health] - colorComponent *ecs2.ComponentManager[color.RGBA] - movementComponent *ecs2.ComponentManager[movement] -} - -const ( - minHpPercentage = 20 - minMaxHp = 500 - maxMaxHp = 2000 -) - -var entityCount = 0 -var pprofEnabled = false - -func (s *spawnSystem) Init(world *ecs2.World) { - s.transformComponent = transformComponentType.GetManager(world) - s.healthComponent = healthComponentType.GetManager(world) - s.colorComponent = colorComponentType.GetManager(world) - s.movementComponent = movementComponentType.GetManager(world) -} -func (s *spawnSystem) Run(world *ecs2.World) { - if ebiten.IsKeyPressed(ebiten.KeySpace) { - for range rand.Intn(1000) { - - newCreature := world.CreateEntity("Creature") - - t := transform{ - x: rand.Int31n(1000), - y: rand.Int31n(1000), - } - - s.transformComponent.Create(newCreature, t) - - maxHp := minMaxHp + rand.Int31n(maxMaxHp-minMaxHp) - hp := int32(float32(maxHp) * float32(minHpPercentage+rand.Int31n(100-minHpPercentage)) / 100) - - h := health{ - hp: hp, - maxHp: maxHp, - } - - s.healthComponent.Create(newCreature, h) - - c := color.RGBA{ - R: 0, - G: 0, - B: 0, - A: 0, - } - - s.colorComponent.Create(newCreature, c) - - m := movement{ - goToX: t.x, - goToY: t.y, - } - - s.movementComponent.Create(newCreature, m) - - entityCount++ - } - } - - if inpututil.IsKeyJustPressed(ebiten.KeyF9) { - if *cpuprofile != "" { - if pprofEnabled { - pprof.StopCPUProfile() - fmt.Println("CPU Profile Stopped") - } else { - f, err := os.Create(*cpuprofile) - if err != nil { - log.Fatal(err) - } - pprof.StartCPUProfile(f) - fmt.Println("CPU Profile Started") - } - - pprofEnabled = !pprofEnabled - } - } -} -func (s *spawnSystem) Destroy(world *ecs2.World) {} diff --git a/examples/ebiten-ecs/client.go b/examples/ebiten-ecs/client.go deleted file mode 100644 index 44aa51c8..00000000 --- a/examples/ebiten-ecs/client.go +++ /dev/null @@ -1,40 +0,0 @@ -package main - -import ( - "gomp/examples/ebiten-ecs/scenes" - systems2 "gomp/examples/ebiten-ecs/systems" - "gomp/pkg/gomp" - - "time" - - "github.com/hajimehoshi/ebiten/v2" -) - -const tickRate = time.Second / 60 - -func main() { - game := gomp.NewGame(tickRate) - game.Debug = false - - game.LoadScene(scenes.VillageScene) - - // TODO: move BodySystem inside gomp engine such as render system - game.RegisterSystems( - gomp.BodySystem, - systems2.IntentSystem, - systems2.HeroMoveSystem, - ) - - e := game.Ebiten() - - e.RegisterInputHandlers() - - ebiten.SetRunnableOnUnfocused(true) - ebiten.SetWindowSize(1280, 720) - ebiten.SetWindowTitle("Engine") - - err := ebiten.RunGame(e) - if err != nil { - panic(err) - } -} diff --git a/examples/ebiten-ecs/components/hero-intent-component.go b/examples/ebiten-ecs/components/hero-intent-component.go deleted file mode 100644 index 0bde2200..00000000 --- a/examples/ebiten-ecs/components/hero-intent-component.go +++ /dev/null @@ -1,21 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ - -package components - -import ( - "gomp/pkg/gomp" - - "github.com/yohamta/donburi/features/math" -) - -type HeroIntentData struct { - Move math.Vec2 - Jump bool - Fire bool -} - -var HeroIntentComponent = gomp.CreateComponent[HeroIntentData]() diff --git a/examples/ebiten-ecs/entities/Hero.go b/examples/ebiten-ecs/entities/Hero.go deleted file mode 100644 index 350ed9b8..00000000 --- a/examples/ebiten-ecs/entities/Hero.go +++ /dev/null @@ -1,32 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ - -package entities - -import ( - "gomp/examples/ebiten-ecs/components" - "gomp/examples/ebiten-ecs/resources" - "gomp/pkg/gomp" - - "github.com/jakecoffman/cp/v2" -) - -type HealthData struct { - Health int - MaxHealth int -} - -var HealthComponent = gomp.CreateComponent[HealthData]() -var ManaComponent = gomp.CreateComponent[uint16]() - -var Hero = gomp.CreateEntity( - gomp.SpriteComponent.New(resources.Sprites("big_demon_idle_anim_f0.png")), - gomp.BodyComponent.New(*cp.NewKinematicBody()), - - components.HeroIntentComponent.New(components.HeroIntentData{}), - HealthComponent.New(HealthData{Health: 100, MaxHealth: 100}), - ManaComponent.New(125), -) diff --git a/examples/ebiten-ecs/resources/resources.go b/examples/ebiten-ecs/resources/resources.go deleted file mode 100644 index ab4fe486..00000000 --- a/examples/ebiten-ecs/resources/resources.go +++ /dev/null @@ -1,17 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ - -package resources - -import ( - "embed" - "gomp/pkg/gomp" -) - -//go:embed **/*.png -var fs embed.FS - -var Sprites = gomp.CreateSpriteResource(fs, "sprites") diff --git a/examples/ebiten-ecs/resources/sprites/area.png b/examples/ebiten-ecs/resources/sprites/area.png deleted file mode 100644 index 98bb19c3..00000000 Binary files a/examples/ebiten-ecs/resources/sprites/area.png and /dev/null differ diff --git a/examples/ebiten-ecs/resources/sprites/big_demon_idle_anim_f0.png b/examples/ebiten-ecs/resources/sprites/big_demon_idle_anim_f0.png deleted file mode 100644 index a0184430..00000000 Binary files a/examples/ebiten-ecs/resources/sprites/big_demon_idle_anim_f0.png and /dev/null differ diff --git a/examples/ebiten-ecs/resources/sprites/big_demon_idle_anim_f1.png b/examples/ebiten-ecs/resources/sprites/big_demon_idle_anim_f1.png deleted file mode 100644 index 718ec7ad..00000000 Binary files a/examples/ebiten-ecs/resources/sprites/big_demon_idle_anim_f1.png and /dev/null differ diff --git a/examples/ebiten-ecs/resources/sprites/big_demon_idle_anim_f2.png b/examples/ebiten-ecs/resources/sprites/big_demon_idle_anim_f2.png deleted file mode 100644 index 77937d59..00000000 Binary files a/examples/ebiten-ecs/resources/sprites/big_demon_idle_anim_f2.png and /dev/null differ diff --git a/examples/ebiten-ecs/resources/sprites/big_demon_idle_anim_f3.png b/examples/ebiten-ecs/resources/sprites/big_demon_idle_anim_f3.png deleted file mode 100644 index d059bc42..00000000 Binary files a/examples/ebiten-ecs/resources/sprites/big_demon_idle_anim_f3.png and /dev/null differ diff --git a/examples/ebiten-ecs/resources/sprites/big_demon_run_anim_f0.png b/examples/ebiten-ecs/resources/sprites/big_demon_run_anim_f0.png deleted file mode 100644 index f42aeecf..00000000 Binary files a/examples/ebiten-ecs/resources/sprites/big_demon_run_anim_f0.png and /dev/null differ diff --git a/examples/ebiten-ecs/resources/sprites/big_demon_run_anim_f1.png b/examples/ebiten-ecs/resources/sprites/big_demon_run_anim_f1.png deleted file mode 100644 index d8f3c9c6..00000000 Binary files a/examples/ebiten-ecs/resources/sprites/big_demon_run_anim_f1.png and /dev/null differ diff --git a/examples/ebiten-ecs/resources/sprites/big_demon_run_anim_f2.png b/examples/ebiten-ecs/resources/sprites/big_demon_run_anim_f2.png deleted file mode 100644 index aa07f45a..00000000 Binary files a/examples/ebiten-ecs/resources/sprites/big_demon_run_anim_f2.png and /dev/null differ diff --git a/examples/ebiten-ecs/resources/sprites/big_demon_run_anim_f3.png b/examples/ebiten-ecs/resources/sprites/big_demon_run_anim_f3.png deleted file mode 100644 index 20a1f222..00000000 Binary files a/examples/ebiten-ecs/resources/sprites/big_demon_run_anim_f3.png and /dev/null differ diff --git a/examples/ebiten-ecs/resources/sprites/big_zombie_idle_anim_f0.png b/examples/ebiten-ecs/resources/sprites/big_zombie_idle_anim_f0.png deleted file mode 100644 index c9f6d2fe..00000000 Binary files a/examples/ebiten-ecs/resources/sprites/big_zombie_idle_anim_f0.png and /dev/null differ diff --git a/examples/ebiten-ecs/resources/sprites/big_zombie_idle_anim_f1.png b/examples/ebiten-ecs/resources/sprites/big_zombie_idle_anim_f1.png deleted file mode 100644 index 66db44c1..00000000 Binary files a/examples/ebiten-ecs/resources/sprites/big_zombie_idle_anim_f1.png and /dev/null differ diff --git a/examples/ebiten-ecs/resources/sprites/big_zombie_idle_anim_f2.png b/examples/ebiten-ecs/resources/sprites/big_zombie_idle_anim_f2.png deleted file mode 100644 index 0273dba3..00000000 Binary files a/examples/ebiten-ecs/resources/sprites/big_zombie_idle_anim_f2.png and /dev/null differ diff --git a/examples/ebiten-ecs/resources/sprites/big_zombie_idle_anim_f3.png b/examples/ebiten-ecs/resources/sprites/big_zombie_idle_anim_f3.png deleted file mode 100644 index 4e3178b7..00000000 Binary files a/examples/ebiten-ecs/resources/sprites/big_zombie_idle_anim_f3.png and /dev/null differ diff --git a/examples/ebiten-ecs/resources/sprites/big_zombie_run_anim_f0.png b/examples/ebiten-ecs/resources/sprites/big_zombie_run_anim_f0.png deleted file mode 100644 index ed1ac2cb..00000000 Binary files a/examples/ebiten-ecs/resources/sprites/big_zombie_run_anim_f0.png and /dev/null differ diff --git a/examples/ebiten-ecs/resources/sprites/big_zombie_run_anim_f1.png b/examples/ebiten-ecs/resources/sprites/big_zombie_run_anim_f1.png deleted file mode 100644 index b8916f13..00000000 Binary files a/examples/ebiten-ecs/resources/sprites/big_zombie_run_anim_f1.png and /dev/null differ diff --git a/examples/ebiten-ecs/resources/sprites/big_zombie_run_anim_f2.png b/examples/ebiten-ecs/resources/sprites/big_zombie_run_anim_f2.png deleted file mode 100644 index b861473b..00000000 Binary files a/examples/ebiten-ecs/resources/sprites/big_zombie_run_anim_f2.png and /dev/null differ diff --git a/examples/ebiten-ecs/resources/sprites/big_zombie_run_anim_f3.png b/examples/ebiten-ecs/resources/sprites/big_zombie_run_anim_f3.png deleted file mode 100644 index 0f54f07e..00000000 Binary files a/examples/ebiten-ecs/resources/sprites/big_zombie_run_anim_f3.png and /dev/null differ diff --git a/examples/ebiten-ecs/scenes/dungeon.go b/examples/ebiten-ecs/scenes/dungeon.go deleted file mode 100644 index f0b604e1..00000000 --- a/examples/ebiten-ecs/scenes/dungeon.go +++ /dev/null @@ -1,9 +0,0 @@ -package scenes - -import ( - "gomp/examples/ebiten-ecs/entities" - "gomp/pkg/gomp" -) - -var DungeonScene = gomp.CreateScene("DungeonScene"). - AddEntities(entities.Hero(1)) diff --git a/examples/ebiten-ecs/scenes/village.go b/examples/ebiten-ecs/scenes/village.go deleted file mode 100644 index b5638507..00000000 --- a/examples/ebiten-ecs/scenes/village.go +++ /dev/null @@ -1,11 +0,0 @@ -package scenes - -import ( - "gomp/examples/ebiten-ecs/entities" - "gomp/pkg/gomp" -) - -var VillageScene = gomp.CreateScene("Village"). - AddEntities( - entities.Hero(1), - ) diff --git a/examples/ebiten-ecs/server.go b/examples/ebiten-ecs/server.go deleted file mode 100644 index fc4367cd..00000000 --- a/examples/ebiten-ecs/server.go +++ /dev/null @@ -1,16 +0,0 @@ -package main - -import ( - "context" - "gomp/examples/ebiten-ecs/scenes" - "gomp/pkg/gomp" -) - -func main() { - e := gomp.NewGame(tickRate) - e.Debug = true - - e.LoadScene(scenes.VillageScene) - - e.Run(context.Background()) -} diff --git a/examples/ebiten-ecs/systems/hero-move-system.go b/examples/ebiten-ecs/systems/hero-move-system.go deleted file mode 100644 index 70a9870a..00000000 --- a/examples/ebiten-ecs/systems/hero-move-system.go +++ /dev/null @@ -1,41 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ - -package systems - -import ( - "gomp/examples/ebiten-ecs/components" - "gomp/pkg/gomp" - - "github.com/yohamta/donburi" -) - -var HeroMoveSystem = gomp.CreateSystem(new(heroMoveController)) - -type heroMoveController struct { - world donburi.World -} - -func (c *heroMoveController) Init(game *gomp.Game) { - c.world = game.World -} - -func (c *heroMoveController) Update(dt float64) { - components.HeroIntentComponent.Query.Each(c.world, func(e *donburi.Entry) { - body := gomp.BodyComponent.Query.Get(e) - if body == nil { - return - } - - intent := components.HeroIntentComponent.Query.Get(e) - - playerSpeed := 200.0 - newX := intent.Move.X * playerSpeed - newY := -intent.Move.Y * playerSpeed - - body.SetVelocity(newX, newY) - }) -} diff --git a/examples/ebiten-ecs/systems/intent-system.go b/examples/ebiten-ecs/systems/intent-system.go deleted file mode 100644 index 6b742876..00000000 --- a/examples/ebiten-ecs/systems/intent-system.go +++ /dev/null @@ -1,77 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ - -package systems - -import ( - "gomp/examples/ebiten-ecs/components" - "gomp/pkg/gomp" - "math" - - input "github.com/quasilyte/ebitengine-input" - "github.com/yohamta/donburi" -) - -var IntentSystem = gomp.CreateSystem(new(intentContoller)) - -const ( - heroActionMoveUp input.Action = iota - heroActionMoveDown - heroActionMoveLeft - heroActionMoveRight - heroActionJump - heroActionFire -) - -type intentContoller struct { - world donburi.World - heroInputHandler *input.Handler -} - -func (c *intentContoller) Init(game *gomp.Game) { - c.world = game.World - - c.heroInputHandler = gomp.InputSystem.NewHandler(0, input.Keymap{ - heroActionMoveUp: {input.KeyGamepadUp, input.KeyUp, input.KeyW}, - heroActionMoveDown: {input.KeyGamepadDown, input.KeyDown, input.KeyS}, - heroActionMoveLeft: {input.KeyGamepadLeft, input.KeyLeft, input.KeyA}, - heroActionMoveRight: {input.KeyGamepadRight, input.KeyRight, input.KeyD}, - heroActionJump: {input.KeyGamepadA, input.KeySpace}, - }) -} - -func (c *intentContoller) Update(dt float64) { - components.HeroIntentComponent.Query.Each(c.world, func(e *donburi.Entry) { - intent := components.HeroIntentComponent.Query.Get(e) - - if c.heroInputHandler.ActionIsPressed(heroActionMoveUp) { - intent.Move.Y = 1 - } else if c.heroInputHandler.ActionIsPressed(heroActionMoveDown) { - intent.Move.Y = -1 - } else { - intent.Move.Y = 0 - } - - if c.heroInputHandler.ActionIsPressed(heroActionMoveLeft) { - intent.Move.X = -1 - } else if c.heroInputHandler.ActionIsPressed(heroActionMoveRight) { - intent.Move.X = 1 - } else { - intent.Move.X = 0 - } - - // Normalize diagonal movement - sumVector := math.Sqrt(intent.Move.X*intent.Move.X + intent.Move.Y*intent.Move.Y) - if sumVector != 0 { - intent.Move.X = intent.Move.X / sumVector - intent.Move.Y = intent.Move.Y / sumVector - } - - intent.Jump = c.heroInputHandler.ActionIsJustPressed(heroActionJump) - - intent.Fire = c.heroInputHandler.ActionIsPressed(heroActionFire) - }) -} diff --git a/examples/new-api/assets/assets.go b/examples/new-api/assets/assets.go new file mode 100644 index 00000000..7a967025 --- /dev/null +++ b/examples/new-api/assets/assets.go @@ -0,0 +1,79 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +package assets + +import ( + "embed" + "gomp" + "image/png" + "log" + "strings" + + rl "github.com/gen2brain/raylib-go/raylib" + "github.com/negrel/assert" +) + +//go:embed *.png +//go:embed *.wav +var fs embed.FS + +var Textures = gomp.CreateAssetLibrary( + func(path string) rl.Texture2D { + assert.True(rl.IsWindowReady(), "Window is not initialized") + + file, err := fs.Open(path) + if err != nil { + log.Panic("Error opening file") + } + defer file.Close() + + img, err := png.Decode(file) + if err != nil { + log.Panic("Error decoding file") + } + + rlImg := rl.NewImageFromImage(img) + return rl.LoadTextureFromImage(rlImg) + }, + func(path string, asset *rl.Texture2D) { + assert.True(rl.IsWindowReady(), "Window is not initialized") + rl.UnloadTexture(*asset) + }, +) + +var Audio = gomp.CreateAssetLibrary( + func(path string) rl.Sound { + file, err := fs.ReadFile(path) + + if err != nil { + log.Panic("Error opening file") + } + + fileTypeIndex := strings.LastIndex(path, ".") + + fileType := ".wav" + + if fileTypeIndex != -1 { + fileType = path[fileTypeIndex:] + } + + wave := rl.LoadWaveFromMemory(fileType, file, int32(len(file))) + + sound := rl.LoadSoundFromWave(wave) + rl.UnloadWave(wave) + + assert.True(rl.IsSoundValid(sound), "Error loading sound") + + // Mute sound by default. Later it controlled by Aduio systems + rl.SetSoundVolume(sound, 0) + + return sound + }, + func(path string, asset *rl.Sound) { + rl.UnloadSound(*asset) + }, +) diff --git a/examples/new-api/assets/bullet.png b/examples/new-api/assets/bullet.png new file mode 100644 index 00000000..c0647535 Binary files /dev/null and b/examples/new-api/assets/bullet.png differ diff --git a/examples/new-api/assets/damage_sound.wav b/examples/new-api/assets/damage_sound.wav new file mode 100644 index 00000000..768352e7 Binary files /dev/null and b/examples/new-api/assets/damage_sound.wav differ diff --git a/examples/new-api/assets/fly_sound.wav b/examples/new-api/assets/fly_sound.wav new file mode 100644 index 00000000..6a76bf7e Binary files /dev/null and b/examples/new-api/assets/fly_sound.wav differ diff --git a/examples/new-api/assets/gun_sound.wav b/examples/new-api/assets/gun_sound.wav new file mode 100644 index 00000000..79cbb24c Binary files /dev/null and b/examples/new-api/assets/gun_sound.wav differ diff --git a/examples/new-api/assets/meteor_large.png b/examples/new-api/assets/meteor_large.png new file mode 100644 index 00000000..2b996c05 Binary files /dev/null and b/examples/new-api/assets/meteor_large.png differ diff --git a/examples/raylib-ecs/assets/milansheet.png b/examples/new-api/assets/milansheet.png similarity index 100% rename from examples/raylib-ecs/assets/milansheet.png rename to examples/new-api/assets/milansheet.png diff --git a/examples/new-api/assets/satellite_B.png b/examples/new-api/assets/satellite_B.png new file mode 100644 index 00000000..1d984041 Binary files /dev/null and b/examples/new-api/assets/satellite_B.png differ diff --git a/examples/new-api/assets/satellite_main_sound.wav b/examples/new-api/assets/satellite_main_sound.wav new file mode 100644 index 00000000..a9d37070 Binary files /dev/null and b/examples/new-api/assets/satellite_main_sound.wav differ diff --git a/examples/new-api/assets/ship_E.png b/examples/new-api/assets/ship_E.png new file mode 100644 index 00000000..fb8910a6 Binary files /dev/null and b/examples/new-api/assets/ship_E.png differ diff --git a/examples/raylib-ecs/assets/star.png b/examples/new-api/assets/star.png similarity index 100% rename from examples/raylib-ecs/assets/star.png rename to examples/new-api/assets/star.png diff --git a/examples/new-api/assets/wall.png b/examples/new-api/assets/wall.png new file mode 100644 index 00000000..052e1f6f Binary files /dev/null and b/examples/new-api/assets/wall.png differ diff --git a/examples/new-api/components/asteroid-scene-manager.go b/examples/new-api/components/asteroid-scene-manager.go new file mode 100644 index 00000000..7e15b789 --- /dev/null +++ b/examples/new-api/components/asteroid-scene-manager.go @@ -0,0 +1,28 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package components + +import "gomp/pkg/ecs" + +type AsteroidSceneManager struct { + PlayerScore int32 + PlayerHp int32 +} + +type AsteroidSceneManagerComponentManager = ecs.ComponentManager[AsteroidSceneManager] + +func NewAsteroidSceneManagerComponentManager() AsteroidSceneManagerComponentManager { + return ecs.NewComponentManager[AsteroidSceneManager](AsteroidSceneManagerComponentId) +} diff --git a/examples/raylib-ecs/systems/systems-business.go b/examples/new-api/components/asteroid-tag.go similarity index 54% rename from examples/raylib-ecs/systems/systems-business.go rename to examples/new-api/components/asteroid-tag.go index 176acf02..aefa29e5 100644 --- a/examples/raylib-ecs/systems/systems-business.go +++ b/examples/new-api/components/asteroid-tag.go @@ -12,13 +12,15 @@ none :) Thank you for your support! */ -package systems +package components import "gomp/pkg/ecs" -// Business logic systems +type AsteroidTag struct { +} -var PlayerService = ecs.CreateSystemService(&playerController{}) -var SpawnService = ecs.CreateSystemService(&spawnController{}) -var HpService = ecs.CreateSystemService(&hpController{}, &PlayerService) -var ColorService = ecs.CreateSystemService(&colorController{}, &HpService) +type AsteroidComponentManager = ecs.ComponentManager[AsteroidTag] + +func NewAsteroidTagComponentManager() AsteroidComponentManager { + return ecs.NewComponentManager[AsteroidTag](AsteroidTagComponentId) +} diff --git a/examples/new-api/components/audio.go b/examples/new-api/components/audio.go new file mode 100644 index 00000000..93032d0a --- /dev/null +++ b/examples/new-api/components/audio.go @@ -0,0 +1,54 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package components + +import ( + rl "github.com/gen2brain/raylib-go/raylib" + "gomp/pkg/ecs" +) + +type SoundEffect struct { + // audio clip from assets + Clip *rl.Sound + // internal flag, should be false by default + IsPlaying bool + // should sound be looped. Default is false. If not looped, entity will be destroyed + IsLooping bool + // base is 1.0 + Volume float32 + // base is 1.0 + Pitch float32 + // base is 0.5. 1.0 is left, 0.0 is right + Pan float32 +} + +type SoundEffectsComponentManager = ecs.ComponentManager[SoundEffect] + +func NewSoundEffectsComponentManager() SoundEffectsComponentManager { + return ecs.NewComponentManager[SoundEffect](SoundEffectManagerComponentId) +} + +type SpatialAudio struct { + // follows raylib rules. Base is 1.0 + Volume float32 + // follows raylib rules. Base is 0.5, 1.0 is left, 0.0 is right + Pan float32 +} + +type SpatialAudioComponentManager = ecs.ComponentManager[SpatialAudio] + +func NewSpatialAudioComponentManager() SpatialAudioComponentManager { + return ecs.NewComponentManager[SpatialAudio](SpatialAudioManagerComponentId) +} diff --git a/examples/new-api/components/bullet-tag.go b/examples/new-api/components/bullet-tag.go new file mode 100644 index 00000000..b29b3f0c --- /dev/null +++ b/examples/new-api/components/bullet-tag.go @@ -0,0 +1,26 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package components + +import "gomp/pkg/ecs" + +type BulletTag struct { +} + +type BulletTagComponentManager = ecs.ComponentManager[BulletTag] + +func NewBulletTagComponentManager() BulletTagComponentManager { + return ecs.NewComponentManager[BulletTag](BulletTagComponentId) +} diff --git a/examples/new-api/components/controller.go b/examples/new-api/components/controller.go new file mode 100644 index 00000000..ec245041 --- /dev/null +++ b/examples/new-api/components/controller.go @@ -0,0 +1,25 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package components + +import "gomp/pkg/ecs" + +type Controller struct{} + +type ControllerComponentManager = ecs.ComponentManager[Controller] + +func NewControllerComponentManager() ControllerComponentManager { + return ecs.NewComponentManager[Controller](ControllerComponentId) +} diff --git a/examples/new-api/components/hp.go b/examples/new-api/components/hp.go new file mode 100644 index 00000000..d5ed1e39 --- /dev/null +++ b/examples/new-api/components/hp.go @@ -0,0 +1,27 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package components + +import "gomp/pkg/ecs" + +type Hp struct { + Hp, MaxHp int32 +} + +type HpComponentManager = ecs.ComponentManager[Hp] + +func NewHealthComponentManager() HpComponentManager { + return ecs.NewComponentManager[Hp](HealthComponentId) +} diff --git a/examples/new-api/components/ids.go b/examples/new-api/components/ids.go new file mode 100644 index 00000000..f6063132 --- /dev/null +++ b/examples/new-api/components/ids.go @@ -0,0 +1,36 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package components + +import ( + "gomp/stdcomponents" +) + +const ( + HealthComponentId = iota + stdcomponents.StdLastComponentId + ControllerComponentId + PlayerTagComponentId + BulletTagComponentId + WallComponentId + SpaceSpawnerTagComponentId + AsteroidTagComponentId + WeaponComponentId + SpaceshipIntentComponentId + AsteroidSceneManagerComponentId + SoundEffectManagerComponentId + SpatialAudioManagerComponentId + TextureRectComponentId + TextureCircleComponentId +) diff --git a/examples/new-api/components/space-spawner.go b/examples/new-api/components/space-spawner.go new file mode 100644 index 00000000..30b0fd05 --- /dev/null +++ b/examples/new-api/components/space-spawner.go @@ -0,0 +1,31 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package components + +import ( + "gomp/pkg/ecs" + "time" +) + +type SpaceSpawnerTag struct { + Cooldown time.Duration + CooldownLeft time.Duration +} + +type SpaceSpawnerComponentManager = ecs.ComponentManager[SpaceSpawnerTag] + +func NewSpaceSpawnerTagComponentManager() SpaceSpawnerComponentManager { + return ecs.NewComponentManager[SpaceSpawnerTag](SpaceSpawnerTagComponentId) +} diff --git a/examples/new-api/components/spaceship-intent.go b/examples/new-api/components/spaceship-intent.go new file mode 100644 index 00000000..40a7a1ee --- /dev/null +++ b/examples/new-api/components/spaceship-intent.go @@ -0,0 +1,31 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package components + +import "gomp/pkg/ecs" + +type SpaceshipIntent struct { + MoveUp bool + MoveDown bool + RotateLeft bool + RotateRight bool + Fire bool +} + +type SpaceshipIntentComponentManager = ecs.ComponentManager[SpaceshipIntent] + +func NewSpaceshipIntentComponentManager() SpaceshipIntentComponentManager { + return ecs.NewComponentManager[SpaceshipIntent](SpaceshipIntentComponentId) +} diff --git a/examples/new-api/components/tags.go b/examples/new-api/components/tags.go new file mode 100644 index 00000000..f59165a3 --- /dev/null +++ b/examples/new-api/components/tags.go @@ -0,0 +1,26 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package components + +import "gomp/pkg/ecs" + +type PlayerTag struct { +} + +type PlayerTagComponentManager = ecs.ComponentManager[PlayerTag] + +func NewPlayerTagComponentManager() PlayerTagComponentManager { + return ecs.NewComponentManager[PlayerTag](PlayerTagComponentId) +} diff --git a/examples/new-api/components/texture-circle.go b/examples/new-api/components/texture-circle.go new file mode 100644 index 00000000..52a5d4c3 --- /dev/null +++ b/examples/new-api/components/texture-circle.go @@ -0,0 +1,36 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package components + +import ( + rl "github.com/gen2brain/raylib-go/raylib" + "gomp/pkg/ecs" + "image/color" +) + +type TextureCircle struct { + CenterX float32 + CenterY float32 + Radius float32 + Rotation float32 + Origin rl.Vector2 + Color color.RGBA +} + +type PrimitiveCircleComponentManager = ecs.ComponentManager[TextureCircle] + +func NewTextureCircleComponentManager() PrimitiveCircleComponentManager { + return ecs.NewComponentManager[TextureCircle](TextureCircleComponentId) +} diff --git a/examples/new-api/components/texture-rect.go b/examples/new-api/components/texture-rect.go new file mode 100644 index 00000000..66e45e7d --- /dev/null +++ b/examples/new-api/components/texture-rect.go @@ -0,0 +1,34 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package components + +import ( + rl "github.com/gen2brain/raylib-go/raylib" + "gomp/pkg/ecs" + "image/color" +) + +type TextureRect struct { + Dest rl.Rectangle + Origin rl.Vector2 + Rotation float32 + Color color.RGBA +} + +type TextureRectComponentManager = ecs.ComponentManager[TextureRect] + +func NewTextureRectComponentManager() TextureRectComponentManager { + return ecs.NewComponentManager[TextureRect](TextureRectComponentId) +} diff --git a/examples/new-api/components/wall-tag.go b/examples/new-api/components/wall-tag.go new file mode 100644 index 00000000..f06fd71a --- /dev/null +++ b/examples/new-api/components/wall-tag.go @@ -0,0 +1,26 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package components + +import "gomp/pkg/ecs" + +type Wall struct { +} + +type WallTagComponentManager = ecs.ComponentManager[Wall] + +func NewWallComponentManager() WallTagComponentManager { + return ecs.NewComponentManager[Wall](WallComponentId) +} diff --git a/examples/raylib-ecs/systems/network-receive.go b/examples/new-api/components/weapon.go similarity index 54% rename from examples/raylib-ecs/systems/network-receive.go rename to examples/new-api/components/weapon.go index 9dc3a609..19c0e947 100644 --- a/examples/raylib-ecs/systems/network-receive.go +++ b/examples/new-api/components/weapon.go @@ -12,15 +12,21 @@ none :) Thank you for your support! */ -package systems +package components import ( "gomp/pkg/ecs" + "time" ) -type networkReceiveController struct{} +type Weapon struct { + Damage int + Cooldown time.Duration + CooldownLeft time.Duration +} -func (s *networkReceiveController) Init(world *ecs.World) {} -func (s *networkReceiveController) Update(world *ecs.World) {} -func (s *networkReceiveController) FixedUpdate(world *ecs.World) {} -func (s *networkReceiveController) Destroy(world *ecs.World) {} +type WeaponComponentManager = ecs.ComponentManager[Weapon] + +func NewWeaponComponentManager() WeaponComponentManager { + return ecs.NewComponentManager[Weapon](WeaponComponentId) +} diff --git a/examples/new-api/config/config.go b/examples/new-api/config/config.go new file mode 100644 index 00000000..0113c4db --- /dev/null +++ b/examples/new-api/config/config.go @@ -0,0 +1,36 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package config + +import "gomp/stdcomponents" + +const ( + TickRate = 20 + FrameRate = 0 +) + +const ( + DefaultCollisionLayer stdcomponents.CollisionLayer = iota + PlayerCollisionLayer + BulletCollisionLayer + EnemyCollisionLayer + WallCollisionLayer +) + +const ( + MainCameraLayer stdcomponents.CameraLayer = 1 << iota + DebugLayer + MinimapCameraLayer +) diff --git a/examples/new-api/entities/asteroid.go b/examples/new-api/entities/asteroid.go new file mode 100644 index 00000000..dd732ad2 --- /dev/null +++ b/examples/new-api/entities/asteroid.go @@ -0,0 +1,114 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package entities + +import ( + rl "github.com/gen2brain/raylib-go/raylib" + "gomp/examples/new-api/assets" + "gomp/examples/new-api/components" + "gomp/examples/new-api/config" + "gomp/pkg/ecs" + "gomp/stdcomponents" + "gomp/vectors" + "image/color" + "math/rand" +) + +type CreateAsteroidManagers struct { + EntityManager *ecs.EntityManager + Positions *stdcomponents.PositionComponentManager + Rotations *stdcomponents.RotationComponentManager + Scales *stdcomponents.ScaleComponentManager + Velocities *stdcomponents.VelocityComponentManager + CircleColliders *stdcomponents.CircleColliderComponentManager + Sprites *stdcomponents.SpriteComponentManager + AsteroidTags *components.AsteroidComponentManager + Hp *components.HpComponentManager + RigidBodies *stdcomponents.RigidBodyComponentManager + Renderables *stdcomponents.RenderableComponentManager + YSorts *stdcomponents.YSortComponentManager +} + +func CreateAsteroid( + props CreateAsteroidManagers, + posX, posY float32, + angle float64, + scaleFactor float32, + velocityX, velocityY float32, +) ecs.Entity { + e := props.EntityManager.Create() + props.Positions.Create(e, stdcomponents.Position{ + XY: vectors.Vec2{ + X: posX, + Y: posY, + }, + }) + props.Rotations.Create(e, stdcomponents.Rotation{}.SetFromDegrees(angle)) + props.Scales.Create(e, stdcomponents.Scale{ + XY: vectors.Vec2{ + X: 1 * scaleFactor, + Y: 1 * scaleFactor, + }, + }) + props.Velocities.Create(e, stdcomponents.Velocity{ + X: velocityX, + Y: velocityY, + }) + props.CircleColliders.Create(e, stdcomponents.CircleCollider{ + Radius: 20, + Offset: vectors.Vec2{ + X: 0, + Y: 0, + }, + Layer: config.EnemyCollisionLayer, + Mask: 1< -0.1 { + velocity.X = 0 + } + if velocity.Y < 0.1 && velocity.Y > -0.1 { + velocity.Y = 0 + } + } + }) +} + +func (s *DampingSystem) Destroy() {} diff --git a/examples/new-api/systems/debug-info.go b/examples/new-api/systems/debug-info.go new file mode 100644 index 00000000..126ab105 --- /dev/null +++ b/examples/new-api/systems/debug-info.go @@ -0,0 +1,265 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +package systems + +import ( + rl "github.com/gen2brain/raylib-go/raylib" + "gomp/examples/new-api/components" + "gomp/examples/new-api/config" + "gomp/pkg/ecs" + "gomp/stdcomponents" + "image/color" + "math" + "time" +) + +func NewDebugInfoSystem() DebugInfoSystem { + return DebugInfoSystem{} +} + +type DebugInfoSystem struct { + EntityManager *ecs.EntityManager + Positions *stdcomponents.PositionComponentManager + Rotations *stdcomponents.RotationComponentManager + Scales *stdcomponents.ScaleComponentManager + ColliderSleepStateComponentManager *stdcomponents.ColliderSleepStateComponentManager + CircleColliders *stdcomponents.CircleColliderComponentManager + BoxColliders *stdcomponents.BoxColliderComponentManager + Cameras *stdcomponents.CameraComponentManager + RenderTexture2D *stdcomponents.FrameBuffer2DComponentManager + TextureRect *components.TextureRectComponentManager + Texture *stdcomponents.RLTextureProComponentManager + Renderable *stdcomponents.RenderableComponentManager + RenderOrders *stdcomponents.RenderOrderComponentManager + AABBs *stdcomponents.AABBComponentManager + Circle *components.PrimitiveCircleComponentManager + CollisionChunks *stdcomponents.CollisionChunkComponentManager + Tints *stdcomponents.TintComponentManager + BvhTrees *stdcomponents.BvhTreeComponentManager + + debug bool + children children + liveParents []ecs.Entity +} + +type children map[ecs.Entity]childType +type childType struct { + id ecs.Entity + isAlive bool +} + +func (s *DebugInfoSystem) Init() { + s.children = make(children, s.BoxColliders.Len()+s.CircleColliders.Len()) + s.liveParents = make([]ecs.Entity, 0, s.BoxColliders.Len()+s.CircleColliders.Len()) +} + +func (s *DebugInfoSystem) Run(dt time.Duration) bool { + if rl.IsKeyPressed(rl.KeyF6) { + if !s.debug { + s.BoxColliders.EachEntity(func(e ecs.Entity) bool { + col := s.BoxColliders.GetUnsafe(e) + scale := s.Scales.GetUnsafe(e) + position := s.Positions.GetUnsafe(e) + rotation := s.Rotations.GetUnsafe(e) + + x := position.XY.X + y := position.XY.Y + width := col.WH.X * scale.XY.X + height := col.WH.Y * scale.XY.Y + s.spawnRect(x, y, width, height, rl.Vector2{ + X: col.Offset.X * scale.XY.X, + Y: col.Offset.Y * scale.XY.Y, + }, float32(rotation.Degrees()), rl.DarkGreen, e) + return true + }) + s.CircleColliders.EachEntity(func(e ecs.Entity) bool { + col := s.CircleColliders.GetUnsafe(e) + scale := s.Scales.GetUnsafe(e) + pos := s.Positions.GetUnsafe(e) + + circleColor := rl.DarkGreen + isSleeping := s.ColliderSleepStateComponentManager.GetUnsafe(e) + if isSleeping != nil { + circleColor = rl.Blue + } + + posWithOffset := pos.XY.Add(col.Offset.Mul(scale.XY)) + s.spawnCircle(posWithOffset.X, posWithOffset.Y, col.Radius*scale.XY.X, circleColor, e) + return true + }) + s.debug = true + } else { + s.Destroy() + } + + } + + if s.debug { + s.liveParents = s.liveParents[:0] + + // TODO: Parallelize this with future batches feature + // Follow child to texture of parent box collider + s.BoxColliders.EachEntity(func(e ecs.Entity) bool { + parentAABB := s.AABBs.GetUnsafe(e) + parentPosition := s.Positions.GetUnsafe(e) + col := s.BoxColliders.GetUnsafe(e) + scale := s.Scales.GetUnsafe(e) + rotation := s.Rotations.GetUnsafe(e) + + s.liveParents = append(s.liveParents, e) + child, ok := s.children[e] + + if ok { + childAABB := s.AABBs.GetUnsafe(child.id) + childRect := s.TextureRect.GetUnsafe(child.id) + + if parentAABB != nil && childAABB != nil { + childAABB.Min = parentAABB.Min + childAABB.Max = parentAABB.Max + childRect.Dest.X = parentPosition.XY.X + childRect.Dest.Y = parentPosition.XY.Y + childRect.Dest.Width = col.WH.X * scale.XY.X + childRect.Dest.Height = col.WH.Y * scale.XY.Y + childRect.Origin.X = col.Offset.X * scale.XY.X + childRect.Origin.Y = col.Offset.Y * scale.XY.Y + childRect.Rotation = float32(rotation.Degrees()) + } + + } else { + //TODO: defer spawning to non parallel function + x := parentPosition.XY.X + y := parentPosition.XY.Y + width := col.WH.X * scale.XY.X + height := col.WH.Y * scale.XY.Y + s.spawnRect(x, y, width, height, rl.Vector2{ + X: col.Offset.X * scale.XY.X, + Y: col.Offset.Y * scale.XY.Y, + }, float32(rotation.Degrees()), rl.DarkGreen, e) + } + + return true + }) + // TODO: Parallelize this with future batches feature + // Follow child to texture of parent circle collider + s.CircleColliders.EachEntity(func(e ecs.Entity) bool { + parentAABB := s.AABBs.GetUnsafe(e) + pos := s.Positions.GetUnsafe(e) + col := s.CircleColliders.GetUnsafe(e) + scale := s.Scales.GetUnsafe(e) + + s.liveParents = append(s.liveParents, e) + child, ok := s.children[e] + if ok { + childAABB := s.AABBs.GetUnsafe(child.id) + childCircle := s.Circle.GetUnsafe(child.id) + circleColor := rl.DarkGreen + isSleeping := s.ColliderSleepStateComponentManager.GetUnsafe(e) + if isSleeping != nil { + circleColor = rl.Blue + } + if parentAABB != nil && childAABB != nil { + childAABB.Min = parentAABB.Min + childAABB.Max = parentAABB.Max + posWithOffset := pos.XY.Add(col.Offset.Mul(scale.XY)) + childCircle.CenterX = posWithOffset.X + childCircle.CenterY = posWithOffset.Y + childCircle.Radius = col.Radius * scale.XY.X + childCircle.Color = circleColor + } + + } else { + //TODO: defer spawning to non parallel function + s.spawnCircle(pos.XY.X, pos.XY.Y, col.Radius*scale.XY.X, rl.DarkGreen, e) + } + return true + }) + + //Remove children that are not alive anymore + for _, parent := range s.liveParents { + if child, ok := s.children[parent]; ok { + child.isAlive = true + s.children[parent] = child + } + } + for key, entity := range s.children { + if !entity.isAlive { + s.EntityManager.Delete(entity.id) + delete(s.children, key) + } else { + entity.isAlive = false + s.children[key] = entity + } + } + } + + return false +} + +func (s *DebugInfoSystem) spawnRect(x float32, y float32, width float32, height float32, origin rl.Vector2, rotation float32, tint color.RGBA, parent ecs.Entity) { + if _, ok := s.children[parent]; !ok { + childEntity := s.EntityManager.Create() + s.TextureRect.Create(childEntity, components.TextureRect{ + Dest: rl.Rectangle{ + X: x, + Y: y, + Width: width, + Height: height, + }, + Origin: origin, + Rotation: rotation, + Color: tint, + }) + s.AABBs.Create(childEntity, stdcomponents.AABB{}) + s.Texture.Create(childEntity, stdcomponents.RLTexturePro{}) + s.Renderable.Create(childEntity, stdcomponents.Renderable{ + CameraMask: config.MinimapCameraLayer | config.MainCameraLayer, + Type: stdcomponents.SpriteRenderableType, + }) + s.RenderOrders.Create(childEntity, stdcomponents.RenderOrder{ + CalculatedZ: math.MaxInt, + }) + s.children[parent] = childType{ + id: childEntity, + isAlive: false, + } + } +} + +func (s *DebugInfoSystem) spawnCircle(x float32, y float32, radius float32, circleColor color.RGBA, e ecs.Entity) { + if _, ok := s.children[e]; !ok { + childEntity := s.EntityManager.Create() + s.Circle.Create(childEntity, components.TextureCircle{ + CenterX: x, + CenterY: y, + Radius: radius, + Rotation: 0, + Origin: rl.Vector2{}, + Color: circleColor, + }) + s.AABBs.Create(childEntity, stdcomponents.AABB{}) + s.Texture.Create(childEntity, stdcomponents.RLTexturePro{}) + s.Renderable.Create(childEntity, stdcomponents.Renderable{ + CameraMask: config.MinimapCameraLayer | config.MainCameraLayer, + Type: stdcomponents.SpriteRenderableType, + }) + s.RenderOrders.Create(childEntity, stdcomponents.RenderOrder{ + CalculatedZ: math.MaxInt, + }) + s.children[e] = childType{ + id: childEntity, + isAlive: false, + } + } +} + +func (s *DebugInfoSystem) Destroy() { + for _, child := range s.children { + s.EntityManager.Delete(child.id) + } + clear(s.children) + s.debug = false +} diff --git a/examples/new-api/systems/hp.go b/examples/new-api/systems/hp.go new file mode 100644 index 00000000..b6907ccb --- /dev/null +++ b/examples/new-api/systems/hp.go @@ -0,0 +1,60 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package systems + +import ( + "gomp/examples/new-api/components" + "gomp/pkg/ecs" + "time" +) + +func NewHpSystem() HpSystem { + return HpSystem{} +} + +type HpSystem struct { + EntityManager *ecs.EntityManager + Hps *components.HpComponentManager + Asteroids *components.AsteroidComponentManager + Players *components.PlayerTagComponentManager + Hp *components.HpComponentManager + AsteroidSceneManager *components.AsteroidSceneManagerComponentManager +} + +func (s *HpSystem) Init() {} +func (s *HpSystem) Run(dt time.Duration) { + s.Hps.EachEntity(func(e ecs.Entity) bool { + hp := s.Hps.GetUnsafe(e) + + if hp.Hp <= 0 { + asteroid := s.Asteroids.GetUnsafe(e) + player := s.Players.GetUnsafe(e) + s.AsteroidSceneManager.EachComponent(func(a *components.AsteroidSceneManager) bool { + if asteroid != nil { + a.PlayerScore += hp.MaxHp + } + if player != nil { + playerHp := s.Hp.GetUnsafe(e) + a.PlayerHp = playerHp.Hp + } + return false + }) + + s.EntityManager.Delete(e) + } + return true + }) +} +func (s *HpSystem) Destroy() {} diff --git a/examples/new-api/systems/player.go b/examples/new-api/systems/player.go new file mode 100644 index 00000000..984c5688 --- /dev/null +++ b/examples/new-api/systems/player.go @@ -0,0 +1,116 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +package systems + +import ( + rl "github.com/gen2brain/raylib-go/raylib" + "gomp/examples/new-api/components" + "gomp/examples/new-api/config" + "gomp/examples/new-api/entities" + "gomp/examples/new-api/sprites" + "gomp/pkg/ecs" + "gomp/stdcomponents" + "math/rand" +) + +func NewPlayerSystem() PlayerSystem { + return PlayerSystem{} +} + +type PlayerSystem struct { + EntityManager *ecs.EntityManager + SpriteMatrixes *stdcomponents.SpriteMatrixComponentManager + Positions *stdcomponents.PositionComponentManager + Rotations *stdcomponents.RotationComponentManager + Scales *stdcomponents.ScaleComponentManager + Velocities *stdcomponents.VelocityComponentManager + AnimationPlayers *stdcomponents.AnimationPlayerComponentManager + AnimationStates *stdcomponents.AnimationStateComponentManager + Tints *stdcomponents.TintComponentManager + Flips *stdcomponents.FlipComponentManager + HP *components.HpComponentManager + Controllers *components.ControllerComponentManager + Renderables *stdcomponents.RenderableComponentManager + YSorts *stdcomponents.YSortComponentManager + RenderOrders *stdcomponents.RenderOrderComponentManager + BoxColliders *stdcomponents.BoxColliderComponentManager + GenericCollider *stdcomponents.GenericColliderComponentManager + Players *components.PlayerTagComponentManager +} + +func (s *PlayerSystem) Init() { + s.SpriteMatrixes.Create(sprites.PlayerSpriteSharedComponentId, sprites.PlayerSpriteMatrix) + + for range 16_000 { + npc := entities.CreatePlayer( + s.EntityManager, s.SpriteMatrixes, s.Positions, s.Rotations, s.Scales, + s.Velocities, s.AnimationPlayers, s.AnimationStates, s.Tints, s.Flips, s.Renderables, + s.YSorts, s.RenderOrders, s.BoxColliders, s.GenericCollider, + ) + + npc.Position.XY.X = 100 + rand.Float32()*700 + npc.Position.XY.Y = 100 + rand.Float32()*500 + npc.AnimationPlayer.Current = uint8(rand.Intn(7)) + } + + player := entities.CreatePlayer( + s.EntityManager, s.SpriteMatrixes, s.Positions, s.Rotations, s.Scales, + s.Velocities, s.AnimationPlayers, s.AnimationStates, s.Tints, s.Flips, s.Renderables, + s.YSorts, s.RenderOrders, s.BoxColliders, s.GenericCollider, + ) + player.Position.XY.X = 100 + player.Position.XY.Y = 100 + player.GenericCollider.Layer = config.PlayerCollisionLayer + player.GenericCollider.Mask = 1 << config.EnemyCollisionLayer + + s.Controllers.Create(player.Entity, components.Controller{}) + s.Players.Create(player.Entity, components.PlayerTag{}) + +} +func (s *PlayerSystem) Run() { + + var speed float32 = 300 + + s.Controllers.EachEntity(func(e ecs.Entity) bool { + velocity := s.Velocities.GetUnsafe(e) + flip := s.Flips.GetUnsafe(e) + animationState := s.AnimationStates.GetUnsafe(e) + + velocity.X = 0 + velocity.Y = 0 + + if rl.IsKeyDown(rl.KeySpace) { + *animationState = entities.PlayerStateJump + } else { + *animationState = entities.PlayerStateIdle + if rl.IsKeyDown(rl.KeyD) { + *animationState = entities.PlayerStateWalk + velocity.X = speed + flip.X = false + } + if rl.IsKeyDown(rl.KeyA) { + *animationState = entities.PlayerStateWalk + velocity.X = -speed + flip.X = true + } + if rl.IsKeyDown(rl.KeyW) { + *animationState = entities.PlayerStateWalk + velocity.Y = -speed + } + if rl.IsKeyDown(rl.KeyS) { + *animationState = entities.PlayerStateWalk + velocity.Y = speed + } + } + + if rl.IsKeyPressed(rl.KeyK) { + s.EntityManager.Delete(e) + } + return true + }) +} +func (s *PlayerSystem) Destroy() {} diff --git a/examples/new-api/systems/render-bogdan.go b/examples/new-api/systems/render-bogdan.go new file mode 100644 index 00000000..38cd1389 --- /dev/null +++ b/examples/new-api/systems/render-bogdan.go @@ -0,0 +1,268 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package systems + +import ( + "fmt" + rl "github.com/gen2brain/raylib-go/raylib" + "gomp/pkg/ecs" + "gomp/stdcomponents" + "math" + "slices" + "sync" + "time" +) + +func NewRenderBogdanSystem() RenderBogdanSystem { + return RenderBogdanSystem{} +} + +type RenderBogdanSystem struct { + EntityManager *ecs.EntityManager + RlTexturePros *stdcomponents.RLTextureProComponentManager + Positions *stdcomponents.PositionComponentManager + Rotations *stdcomponents.RotationComponentManager + Scales *stdcomponents.ScaleComponentManager + AnimationPlayers *stdcomponents.AnimationPlayerComponentManager + Tints *stdcomponents.TintComponentManager + Flips *stdcomponents.FlipComponentManager + Renderables *stdcomponents.RenderableComponentManager + AnimationStates *stdcomponents.AnimationStateComponentManager + SpriteMatrixes *stdcomponents.SpriteMatrixComponentManager + RenderOrders *stdcomponents.RenderOrderComponentManager + ColliderBoxes *stdcomponents.BoxColliderComponentManager + Collisions *stdcomponents.CollisionComponentManager + renderList []renderEntry + instanceData []stdcomponents.RLTexturePro + camera rl.Camera2D +} + +type renderEntry struct { + Entity ecs.Entity + TextureId int + ZIndex float32 +} + +func (s *RenderBogdanSystem) Init() { + s.camera = rl.Camera2D{ + Target: rl.NewVector2(0, 0), + Offset: rl.NewVector2(0, 0), + Rotation: 0, + Zoom: 1, + } +} +func (s *RenderBogdanSystem) Run(dt time.Duration) bool { + if rl.WindowShouldClose() { + return false + } + + s.prepareRender(dt) + + rl.BeginDrawing() + rl.ClearBackground(rl.Black) + // draw grid + const gridSize = 256 + for i := int32(1); i < 1024/gridSize; i++ { + rl.DrawLine(i*gridSize, 0, i*gridSize, 768, rl.Green) + } + for i := int32(1); i < 768/gridSize; i++ { + rl.DrawLine(0, i*gridSize, 1024, i*gridSize, rl.Green) + } + s.render() + s.ColliderBoxes.EachEntity(func(e ecs.Entity) bool { + box := s.ColliderBoxes.GetUnsafe(e) + pos := s.Positions.GetUnsafe(e) + + rl.DrawRectangleLines(int32(pos.XY.X), int32(pos.XY.Y), int32(box.WH.X), int32(box.WH.Y), rl.Red) + return true + }) + s.Collisions.EachEntity(func(entity ecs.Entity) bool { + pos := s.Positions.GetUnsafe(entity) + rl.DrawRectangle(int32(pos.XY.X), int32(pos.XY.X), 16, 16, rl.Red) + return true + }) + rl.DrawRectangle(0, 0, 200, 60, rl.DarkBrown) + rl.DrawFPS(10, 10) + rl.DrawText(fmt.Sprintf("%d entities", s.EntityManager.Size()), 10, 30, 20, rl.RayWhite) + rl.EndDrawing() + + return true +} + +func (s *RenderBogdanSystem) Destroy() {} + +func (s *RenderBogdanSystem) render() { + // Extract and sort entities + if cap(s.renderList) < s.Renderables.Len() { + s.renderList = append(s.renderList, make([]renderEntry, 0, s.Renderables.Len()-cap(s.renderList))...) + } + s.Renderables.EachEntity(func(e ecs.Entity) bool { + sprite := s.SpriteMatrixes.Get(e) + renderOrder := s.RenderOrders.GetUnsafe(e) + s.renderList = append(s.renderList, renderEntry{ + Entity: e, + TextureId: int(sprite.Texture.ID), + ZIndex: renderOrder.CalculatedZ, + }) + return true + }) + + slices.SortStableFunc(s.renderList, func(a, b renderEntry) int { + if a.TextureId == b.TextureId { + return int(math.Floor(float64(a.ZIndex - b.ZIndex))) + } + return int(a.TextureId - b.TextureId) + }) + + // Batch and render + var currentTex = -1 + var instanceData []stdcomponents.RLTexturePro = make([]stdcomponents.RLTexturePro, 0, 8192) + for i := range s.renderList { + entry := &s.renderList[i] + if entry.TextureId != currentTex || len(instanceData) >= 8192 { + if len(instanceData) > 0 { + s.submitBatch(currentTex, instanceData) + instanceData = instanceData[:0] + } + currentTex = entry.TextureId + } + instanceData = append(instanceData, s.getInstanceData(entry.Entity)) + } + s.submitBatch(currentTex, instanceData) // Submit last batch + s.renderList = s.renderList[:0] +} + +func (s *RenderBogdanSystem) getInstanceData(e ecs.Entity) stdcomponents.RLTexturePro { + return *s.RlTexturePros.GetUnsafe(e) +} + +func (s *RenderBogdanSystem) prepareRender(dt time.Duration) { + wg := new(sync.WaitGroup) + wg.Add(6) + s.prepareAnimations(wg) + go s.prepareFlips(wg) + go s.preparePositions(wg, dt) + go s.prepareRotations(wg) + go s.prepareScales(wg) + go s.prepareTints(wg) + wg.Wait() +} + +func (s *RenderBogdanSystem) prepareAnimations(wg *sync.WaitGroup) { + defer wg.Done() + s.RlTexturePros.EachEntity(func(entity ecs.Entity) bool { + texturePro := s.RlTexturePros.GetUnsafe(entity) + animation := s.AnimationPlayers.GetUnsafe(entity) + if animation == nil { + return true + } + frame := &texturePro.Frame + if animation.Vertical { + frame.Y += frame.Height * float32(animation.Current) + } else { + frame.X += frame.Width * float32(animation.Current) + } + return true + }) +} + +func (s *RenderBogdanSystem) prepareFlips(wg *sync.WaitGroup) { + defer wg.Done() + s.RlTexturePros.EachEntity(func(entity ecs.Entity) bool { + texturePro := s.RlTexturePros.GetUnsafe(entity) + mirrored := s.Flips.GetUnsafe(entity) + if mirrored == nil { + return true + } + if mirrored.X { + texturePro.Frame.Width *= -1 + } + if mirrored.Y { + texturePro.Frame.Height *= -1 + } + return true + }) +} + +func (s *RenderBogdanSystem) preparePositions(wg *sync.WaitGroup, dt time.Duration) { + defer wg.Done() + //dts := dt.Seconds() + s.RlTexturePros.EachEntity(func(entity ecs.Entity) bool { + texturePro := s.RlTexturePros.GetUnsafe(entity) + position := s.Positions.GetUnsafe(entity) + if position == nil { + return true + } + //decay := 16.0 // DECAY IS TICKRATE DEPENDENT + //texturePro.Dest.X = float32(s.expDecay(float64(texturePro.Dest.X), float64(position.X), decay, dts)) + //texturePro.Dest.Y = float32(s.expDecay(float64(texturePro.Dest.Y), float64(position.Y), decay, dts)) + texturePro.Dest.X = position.XY.X + texturePro.Dest.Y = position.XY.Y + + return true + }) +} + +func (s *RenderBogdanSystem) prepareRotations(wg *sync.WaitGroup) { + defer wg.Done() + s.RlTexturePros.EachEntity(func(entity ecs.Entity) bool { + texturePro := s.RlTexturePros.GetUnsafe(entity) + rotation := s.Rotations.GetUnsafe(entity) + if rotation == nil { + return true + } + texturePro.Rotation = float32(rotation.Angle) + return true + }) +} + +func (s *RenderBogdanSystem) prepareScales(wg *sync.WaitGroup) { + defer wg.Done() + s.RlTexturePros.EachEntity(func(entity ecs.Entity) bool { + texturePro := s.RlTexturePros.GetUnsafe(entity) + scale := s.Scales.GetUnsafe(entity) + if scale == nil { + return true + } + texturePro.Dest.Width *= scale.XY.X + texturePro.Dest.Height *= scale.XY.Y + return true + }) +} + +func (s *RenderBogdanSystem) prepareTints(wg *sync.WaitGroup) { + defer wg.Done() + s.RlTexturePros.EachEntity(func(entity ecs.Entity) bool { + tr := s.RlTexturePros.GetUnsafe(entity) + tint := s.Tints.GetUnsafe(entity) + if tint == nil { + return true + } + trTint := &tr.Tint + trTint.A = tint.A + trTint.R = tint.R + trTint.G = tint.G + trTint.B = tint.B + return true + }) +} + +func (s *RenderBogdanSystem) submitBatch(texID int, data []stdcomponents.RLTexturePro) { + rl.BeginMode2D(s.camera) + for i := range data { + rl.DrawTexturePro(*data[i].Texture, data[i].Frame, data[i].Dest, data[i].Origin, data[i].Rotation, data[i].Tint) + } + rl.EndMode2D() +} diff --git a/examples/new-api/systems/render-overlay.go b/examples/new-api/systems/render-overlay.go new file mode 100644 index 00000000..25e3ac1a --- /dev/null +++ b/examples/new-api/systems/render-overlay.go @@ -0,0 +1,480 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package systems + +import ( + "fmt" + rl "github.com/gen2brain/raylib-go/raylib" + "github.com/negrel/assert" + "gomp/examples/new-api/components" + "gomp/examples/new-api/config" + "gomp/pkg/ecs" + "gomp/stdcomponents" + "gomp/vectors" + "image/color" + "slices" + "time" +) + +const ( + fpsAvgSamples = 100 + fontSize = 20 + fpsGraphWidth = 160 + fpsGraphHeight = 60 + msGraphMaxValue = 33.33 // Max ms to show on graph (30 FPS) +) + +func NewRenderOverlaySystem() RenderOverlaySystem { + return RenderOverlaySystem{} +} + +type RenderOverlaySystem struct { + EntityManager *ecs.EntityManager + SceneManager *components.AsteroidSceneManagerComponentManager + Cameras *stdcomponents.CameraComponentManager + FrameBuffer2D *stdcomponents.FrameBuffer2DComponentManager + CollisionChunks *stdcomponents.CollisionChunkComponentManager + CollisionCells *stdcomponents.CollisionCellComponentManager + Tints *stdcomponents.TintComponentManager + BvhTrees *stdcomponents.BvhTreeComponentManager + Positions *stdcomponents.PositionComponentManager + AABBs *stdcomponents.AABBComponentManager + Collisions *stdcomponents.CollisionComponentManager + ColliderSleepStateComponentManager *stdcomponents.ColliderSleepStateComponentManager + Textures *stdcomponents.RLTextureProComponentManager + frameBuffer ecs.Entity + monitorWidth int + monitorHeight int + debugLvl int + debug bool + lastFPSTime time.Time + frameCount int + currentFPS int + fpsSamples []int + fpsSampleSum int + fpsSampleIdx int + avgFPS float64 + lowestFps int + lastFrameDuration time.Duration + msHistory []float64 // ms per frame, ring buffer + msHistoryIdx int +} + +func (s *RenderOverlaySystem) Init() { + s.monitorWidth = rl.GetScreenWidth() + s.monitorHeight = rl.GetScreenHeight() + + s.frameBuffer = s.EntityManager.Create() + s.FrameBuffer2D.Create(s.frameBuffer, stdcomponents.FrameBuffer2D{ + Frame: rl.Rectangle{X: 0, Y: 0, Width: float32(s.monitorWidth), Height: float32(s.monitorHeight)}, + Texture: rl.LoadRenderTexture(int32(s.monitorWidth), int32(s.monitorHeight)), + Layer: config.DebugLayer, + BlendMode: rl.BlendAlphaPremultiply, + Tint: rl.White, + Dst: rl.Rectangle{Width: float32(s.monitorWidth), Height: float32(s.monitorHeight)}, + }) + + // Initialize ms history buffer for graph + s.msHistory = make([]float64, fpsGraphWidth) +} + +func (s *RenderOverlaySystem) Run(dt time.Duration) bool { + if rl.IsKeyPressed(rl.KeyF6) { + if !s.debug { + s.debug = true + } else { + s.debug = false + } + } + if rl.IsKeyPressed(rl.KeyF7) { + s.debugLvl-- + if s.debugLvl < 0 { + s.debugLvl = 63 + } + } + if rl.IsKeyPressed(rl.KeyF8) { + s.debugLvl++ + if s.debugLvl > 63 { + s.debugLvl = 0 + } + } + + // FPS calculation (custom) + now := time.Now() + if s.lastFPSTime.IsZero() { + s.lastFPSTime = now + s.fpsSamples = make([]int, fpsAvgSamples) + s.msHistory = make([]float64, fpsGraphWidth) + } + s.frameCount++ + s.lastFrameDuration = dt + + { // Store current frame FPS in samples + frameFPS := 0 + if dt > 0 { + // Correct calculation: convert duration to frames per second + frameFPS = int(time.Second / dt) + } + s.fpsSampleSum -= s.fpsSamples[s.fpsSampleIdx] + s.fpsSamples[s.fpsSampleIdx] = frameFPS + s.fpsSampleSum += frameFPS + s.fpsSampleIdx = (s.fpsSampleIdx + 1) % len(s.fpsSamples) + + // Calculate average FPS over samples + s.avgFPS = float64(s.fpsSampleSum) / float64(len(s.fpsSamples)) + + // Calculate 1% FPS (lowest 1% frame in the sample window) + s.lowestFps = slices.Min(s.fpsSamples) + + // Update frame time history (ms) on every frame + // Use average of last two frames for smoother graph + var ms float64 + if s.lastFrameDuration > 0 { + ms = float64(s.lastFrameDuration.Microseconds()) / 1000.0 + s.msHistory[s.msHistoryIdx] = ms + s.msHistoryIdx = (s.msHistoryIdx + 1) % len(s.msHistory) + } + + if now.Sub(s.lastFPSTime) >= time.Second { + s.currentFPS = s.frameCount + s.frameCount = 0 + s.lastFPSTime = now + } + } + + s.Cameras.EachEntity(func(entity ecs.Entity) bool { + camera := s.Cameras.GetUnsafe(entity) + fb := s.FrameBuffer2D.GetUnsafe(entity) + switch fb.Layer { + case config.MainCameraLayer: + overlayFrame := s.FrameBuffer2D.GetUnsafe(s.frameBuffer) + rl.BeginTextureMode(overlayFrame.Texture) + rl.ClearBackground(rl.Blank) + + // Debug mode: BVH tree and dots + if s.debug { + rl.BeginMode2D(camera.Camera2D) + cameraRect := camera.Rect() + + s.CollisionCells.EachEntity(func(e ecs.Entity) bool { + cell := s.CollisionCells.GetUnsafe(e) + assert.NotNil(cell) + + if cell.Layer != stdcomponents.CollisionLayer(s.debugLvl) { + return true + } + + tint := s.Tints.GetUnsafe(e) + assert.NotNil(tint) + + position := s.Positions.GetUnsafe(e) + assert.NotNil(position) + + clr := color.RGBA{ + R: tint.R, + G: tint.G, + B: tint.B, + A: 255, + } + + // Simple AABB culling + if s.intersects(cameraRect, vectors.Rectangle{ + X: position.XY.X, + Y: position.XY.Y, + Width: cell.Size, + Height: cell.Size, + }) { + rl.DrawRectangleLines(int32(position.XY.X), int32(position.XY.Y), int32(cell.Size), int32(cell.Size), clr) + } + return true + }) + s.CollisionChunks.EachEntity(func(e ecs.Entity) bool { + chunk := s.CollisionChunks.GetUnsafe(e) + assert.NotNil(chunk) + + if chunk.Layer != stdcomponents.CollisionLayer(s.debugLvl) { + return true + } + + tint := s.Tints.GetUnsafe(e) + assert.NotNil(tint) + + position := s.Positions.GetUnsafe(e) + assert.NotNil(position) + + tree := s.BvhTrees.GetUnsafe(e) + assert.NotNil(tree) + + tree.AabbNodes.EachData(func(a *stdcomponents.AABB) bool { + // Simple AABB culling + if s.intersects(cameraRect, a.Rect()) { + rl.DrawRectangleRec(rl.Rectangle{ + X: a.Min.X, + Y: a.Min.Y, + Width: a.Max.X - a.Min.X, + Height: a.Max.Y - a.Min.Y, + }, *tint) + } + return true + }) + + clr := color.RGBA{ + R: tint.R, + G: tint.G, + B: tint.B, + A: 255, + } + + // Simple AABB culling + if s.intersects(cameraRect, vectors.Rectangle{ + X: position.XY.X, + Y: position.XY.Y, + Width: chunk.Size, + Height: chunk.Size, + }) { + rl.DrawRectangleLines(int32(position.XY.X), int32(position.XY.Y), int32(chunk.Size), int32(chunk.Size), clr) + } + return true + }) + s.AABBs.EachEntity(func(e ecs.Entity) bool { + aabb := s.AABBs.GetUnsafe(e) + clr := rl.Green + isSleeping := s.ColliderSleepStateComponentManager.GetUnsafe(e) + if isSleeping != nil { + clr = rl.Blue + } + if s.intersects(cameraRect, aabb.Rect()) { + rl.DrawRectangleLinesEx(rl.Rectangle{ + X: aabb.Min.X, + Y: aabb.Min.Y, + Width: aabb.Max.X - aabb.Min.X, + Height: aabb.Max.Y - aabb.Min.Y, + }, 1, clr) + } + return true + }) + s.Collisions.EachEntity(func(entity ecs.Entity) bool { + pos := s.Positions.GetUnsafe(entity) + rec := vectors.Rectangle{ + X: pos.XY.X - 8, + Y: pos.XY.Y - 8, + Width: 16, + Height: 16, + } + if s.intersects(cameraRect, rec) { + rl.DrawRectangleRec(rl.Rectangle(rec), rl.Red) + } + return true + }) + s.Textures.EachComponent(func(r *stdcomponents.RLTexturePro) bool { + rec := vectors.Rectangle{ + X: r.Dest.X - 2, + Y: r.Dest.Y - 2, + Width: 4, + Height: 4, + } + if s.intersects(cameraRect, rec) { + rl.DrawRectanglePro(rl.Rectangle(rec), rl.Vector2{}, r.Rotation, rl.Red) + } + return true + }) + + rl.EndMode2D() + } + + // Print stats + const x = 10 + const y = 10 + statsPanelWidth := float32(200 + fpsGraphWidth) + statsPanelHeight := float32(y + fontSize*8) + rl.DrawRectangleRec(rl.Rectangle{Height: statsPanelHeight, Width: statsPanelWidth}, rl.Black) + s.drawCustomFPS(x, y) + rl.DrawText(fmt.Sprintf("%d entities", s.EntityManager.Size()), x, y+fontSize*6, fontSize, rl.RayWhite) + rl.DrawText(fmt.Sprintf("%d debugLvl", s.debugLvl), x, y+fontSize*7, 20, rl.RayWhite) + // Game over + s.SceneManager.EachComponent(func(a *components.AsteroidSceneManager) bool { + rl.DrawText(fmt.Sprintf("Player HP: %d", a.PlayerHp), x, y+fontSize*4, 20, rl.RayWhite) + rl.DrawText(fmt.Sprintf("Score: %d", a.PlayerScore), x, y+fontSize*5, 20, rl.RayWhite) + if a.PlayerHp <= 0 { + text := "Game Over" + textSize := rl.MeasureTextEx(rl.GetFontDefault(), text, 96, 0) + x := (s.monitorWidth - int(textSize.X)) / 2 + y := (s.monitorHeight - int(textSize.Y)) / 2 + rl.DrawText(text, int32(x), int32(y), 96, rl.Red) + + } + return false + }) + rl.EndTextureMode() + case config.MinimapCameraLayer: + rl.BeginTextureMode(fb.Texture) + rl.DrawRectangleLines(2, 2, fb.Texture.Texture.Width-2, fb.Texture.Texture.Height-2, rl.Green) + rl.EndTextureMode() + default: + panic("not implemented") + } + return true + }) + + if rl.IsWindowResized() { + s.monitorWidth = rl.GetScreenWidth() + s.monitorHeight = rl.GetScreenHeight() + fb := s.FrameBuffer2D.GetUnsafe(s.frameBuffer) + rl.UnloadRenderTexture(fb.Texture) + fb.Texture = rl.LoadRenderTexture(int32(s.monitorWidth), int32(s.monitorHeight)) + fb.Frame = rl.Rectangle{X: 0, Y: 0, Width: float32(s.monitorWidth), Height: float32(s.monitorHeight)} + fb.Dst = rl.Rectangle{Width: float32(s.monitorWidth), Height: float32(s.monitorHeight)} + } + return true +} + +// Draws FPS stats: low, current frame, average, and current FPS +func (s *RenderOverlaySystem) drawCustomFPS(x, y int32) { + fps := int32(s.currentFPS) + + // Frame time in milliseconds + frameTimeMs := 0.0 + if s.lastFrameDuration > 0 { + frameTimeMs = float64(s.lastFrameDuration.Microseconds()) / 1000.0 + } + + avgFPS := int32(s.avgFPS) + + // Colors + fontColor := rl.Lime + if fps < 30 { + fontColor = rl.Red + } else if fps < 60 { + fontColor = rl.Yellow + } + + // Frame time color (lower is better) + frameTimeColor := rl.Lime + if frameTimeMs > 33.33 { // 30 FPS threshold (33.33ms) + frameTimeColor = rl.Red + } else if frameTimeMs > 16.67 { // 60 FPS threshold (16.67ms) + frameTimeColor = rl.Yellow + } + + // Draw all stats + rl.DrawText(fmt.Sprintf("FPS: %d", fps), x, y, fontSize, fontColor) + rl.DrawText(fmt.Sprintf("Frame: %.2f ms", frameTimeMs), x, y+fontSize, fontSize, frameTimeColor) + rl.DrawText(fmt.Sprintf("Avg %d: %d", fpsAvgSamples, avgFPS), x, y+fontSize*2, fontSize, fontColor) + rl.DrawText(fmt.Sprintf("Low: %d", s.lowestFps), x, y+fontSize*3, fontSize, fontColor) + + // Draw ms graph + s.drawMsGraph(x+180, y) +} + +// Draw a graph of historical frame times in milliseconds +func (s *RenderOverlaySystem) drawMsGraph(x, y int32) { + // Draw graph border + rl.DrawRectangleLinesEx(rl.Rectangle{ + X: float32(x), + Y: float32(y), + Width: float32(fpsGraphWidth), + Height: float32(fpsGraphHeight), + }, 1, rl.Gray) + + // Draw graph background + rl.DrawRectangle(x+1, y+1, fpsGraphWidth-2, fpsGraphHeight-2, rl.Black) + + // Draw horizontal reference lines (33.33ms, 16.67ms, 8.33ms) + // These correspond to 30 FPS, 60 FPS, and 120 FPS + refLines := []struct { + ms float32 + color rl.Color + label string + }{ + {33.33, rl.Red, "33.33 (30 FPS)"}, + {16.67, rl.Yellow, "16.67 (60 FPS)"}, + {8.33, rl.Green, "8.33 (120 FPS)"}, + } + for _, ref := range refLines { + refY := y + int32(float32(fpsGraphHeight)*(ref.ms/msGraphMaxValue)) + rl.DrawLineEx( + rl.NewVector2(float32(x), float32(refY)), + rl.NewVector2(float32(x+int32(fpsGraphWidth)), float32(refY)), + 1.0, + ref.color, + ) + rl.DrawText(ref.label, x+int32(fpsGraphWidth)+2, refY-8, 10, rl.Fade(ref.color, 0.8)) + } + + // Start from the oldest sample and move forward + startIdx := s.msHistoryIdx % len(s.msHistory) + + // Draw frame time data points and connect with lines + for i := 0; i < len(s.msHistory)-1; i++ { + // Calculate indices in a way that we're drawing from left to right, + // with the newest data on the right + idx := (startIdx + i) % len(s.msHistory) + nextIdx := (startIdx + i + 1) % len(s.msHistory) + + ms1 := float32(s.msHistory[idx]) + ms2 := float32(s.msHistory[nextIdx]) + + // Clamp values to max + if ms1 > msGraphMaxValue { + ms1 = msGraphMaxValue + } + if ms2 > msGraphMaxValue { + ms2 = msGraphMaxValue + } + + // Calculate positions (note: for ms, higher value = worse performance, so we scale directly) + x1 := x + int32(i) + y1 := y + int32(float32(fpsGraphHeight)*(ms1/msGraphMaxValue)) + x2 := x + int32(i+1) + y2 := y + int32(float32(fpsGraphHeight)*(ms2/msGraphMaxValue)) + + // Choose color based on frame time + lineColor := rl.Green + if ms2 > 16.67 { // 60 FPS threshold + lineColor = rl.Yellow + } + if ms2 > 33.33 { // 30 FPS threshold + lineColor = rl.Red + } + + // Skip drawing if either value is zero (not yet initialized) + if ms1 > 0 && ms2 > 0 { + rl.DrawLineEx( + rl.NewVector2(float32(x1), float32(y1)), + rl.NewVector2(float32(x2), float32(y2)), + 2.0, + lineColor, + ) + } + } + + // Draw a vertical line indicating the current position in the buffer + currentX := x + int32(len(s.msHistory)-1) + rl.DrawLineEx( + rl.NewVector2(float32(currentX), float32(y)), + rl.NewVector2(float32(currentX), float32(y+int32(fpsGraphHeight))), + 1.0, + rl.White, + ) +} + +func (s *RenderOverlaySystem) intersects(rect1, rect2 vectors.Rectangle) bool { + return rect1.X < rect2.X+rect2.Width && + rect1.X+rect1.Width > rect2.X && + rect1.Y < rect2.Y+rect2.Height && + rect1.Y+rect1.Height > rect2.Y +} + +func (s *RenderOverlaySystem) Destroy() {} diff --git a/examples/new-api/systems/space-spawner.go b/examples/new-api/systems/space-spawner.go new file mode 100644 index 00000000..36163858 --- /dev/null +++ b/examples/new-api/systems/space-spawner.go @@ -0,0 +1,83 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package systems + +import ( + "gomp/examples/new-api/components" + "gomp/examples/new-api/entities" + "gomp/pkg/ecs" + "gomp/stdcomponents" + "math/rand" + "time" +) + +func NewSpaceSpawnerSystem() SpaceSpawnerSystem { + return SpaceSpawnerSystem{} +} + +type SpaceSpawnerSystem struct { + EntityManager *ecs.EntityManager + Positions *stdcomponents.PositionComponentManager + SpaceSpawners *components.SpaceSpawnerComponentManager + Asteroids *components.AsteroidComponentManager + Hp *components.HpComponentManager + Sprites *stdcomponents.SpriteComponentManager + CircleColliders *stdcomponents.CircleColliderComponentManager + Velocities *stdcomponents.VelocityComponentManager + Rotations *stdcomponents.RotationComponentManager + Scales *stdcomponents.ScaleComponentManager + RigidBodies *stdcomponents.RigidBodyComponentManager + Renderables *stdcomponents.RenderableComponentManager + RenderOrders *stdcomponents.RenderOrderComponentManager + Textures *stdcomponents.RLTextureProComponentManager + YSorts *stdcomponents.YSortComponentManager +} + +func (s *SpaceSpawnerSystem) Init() {} +func (s *SpaceSpawnerSystem) Run(dt time.Duration) { + s.SpaceSpawners.EachEntity(func(e ecs.Entity) bool { + position := s.Positions.GetUnsafe(e) + velocity := s.Velocities.GetUnsafe(e) + + if position.XY.X > 5000 || position.XY.X < 0 { + velocity.X = -velocity.X + } + + spawner := s.SpaceSpawners.GetUnsafe(e) + if spawner.CooldownLeft > 0 { + spawner.CooldownLeft -= dt + return true + } + + pos := s.Positions.GetUnsafe(e) + entities.CreateAsteroid(entities.CreateAsteroidManagers{ + EntityManager: s.EntityManager, + Positions: s.Positions, + Rotations: s.Rotations, + Scales: s.Scales, + Velocities: s.Velocities, + CircleColliders: s.CircleColliders, + Sprites: s.Sprites, + AsteroidTags: s.Asteroids, + Hp: s.Hp, + RigidBodies: s.RigidBodies, + Renderables: s.Renderables, + YSorts: s.YSorts, + }, pos.XY.X, pos.XY.Y, 0, 1+rand.Float32()*2, 0, 50+rand.Float32()*100) + spawner.CooldownLeft = spawner.Cooldown + return true + }) +} +func (s *SpaceSpawnerSystem) Destroy() {} diff --git a/examples/new-api/systems/spaceship-intents.go b/examples/new-api/systems/spaceship-intents.go new file mode 100644 index 00000000..ee95d448 --- /dev/null +++ b/examples/new-api/systems/spaceship-intents.go @@ -0,0 +1,167 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package systems + +import ( + "gomp/examples/new-api/assets" + "gomp/examples/new-api/components" + "gomp/examples/new-api/entities" + "gomp/pkg/ecs" + "gomp/stdcomponents" + "gomp/vectors" + "math" + "time" +) + +func NewSpaceshipIntentsSystem() SpaceshipIntentsSystem { + return SpaceshipIntentsSystem{} +} + +type SpaceshipIntentsSystem struct { + EntityManager *ecs.EntityManager + SpaceshipIntents *components.SpaceshipIntentComponentManager + Positions *stdcomponents.PositionComponentManager + Velocities *stdcomponents.VelocityComponentManager + Rotations *stdcomponents.RotationComponentManager + Scales *stdcomponents.ScaleComponentManager + BoxColliders *stdcomponents.BoxColliderComponentManager + CircleColliders *stdcomponents.CircleColliderComponentManager + BulletTags *components.BulletTagComponentManager + Sprites *stdcomponents.SpriteComponentManager + Textures *stdcomponents.RLTextureProComponentManager + RigidBodies *stdcomponents.RigidBodyComponentManager + Weapons *components.WeaponComponentManager + Hps *components.HpComponentManager + SoundEffects *components.SoundEffectsComponentManager + TexturePositionSmooth *stdcomponents.TexturePositionSmoothComponentManager + Renderables *stdcomponents.RenderableComponentManager + RenderOrders *stdcomponents.RenderOrderComponentManager + moveSpeed float32 +} + +func (s *SpaceshipIntentsSystem) Init() {} +func (s *SpaceshipIntentsSystem) Run(dt time.Duration) { + var moveSpeedMax float32 = 300 + var moveSpeedMaxBackwards float32 = -200 + var rotateSpeed vectors.Radians = 3 + var speedIncrement float32 = 10 + + var bulletSpeed float32 = 300 + + dtSec := float32(dt.Seconds()) + + s.SpaceshipIntents.EachEntity(func(entity ecs.Entity) bool { + intent := s.SpaceshipIntents.GetUnsafe(entity) + vel := s.Velocities.GetUnsafe(entity) + rot := s.Rotations.GetUnsafe(entity) + pos := s.Positions.GetUnsafe(entity) + weapon := s.Weapons.GetUnsafe(entity) + hp := s.Hps.GetUnsafe(entity) + flySfx := s.SoundEffects.GetUnsafe(entity) + + if intent.RotateLeft { + rot.Angle -= rotateSpeed * vectors.Radians(dtSec) + } + if intent.RotateRight { + rot.Angle += rotateSpeed * vectors.Radians(dtSec) + } + if intent.MoveUp { + s.moveSpeed += speedIncrement + if s.moveSpeed > moveSpeedMax { + s.moveSpeed = moveSpeedMax + } + } + if intent.MoveDown { + s.moveSpeed -= speedIncrement + if s.moveSpeed < moveSpeedMaxBackwards { + s.moveSpeed = moveSpeedMaxBackwards + } + } + + if !intent.MoveUp && !intent.MoveDown { + if s.moveSpeed > 0 { + s.moveSpeed -= speedIncrement + } else if s.moveSpeed < 0 { + s.moveSpeed += speedIncrement + } + } + + absMoveSpeed := math.Abs(float64(s.moveSpeed)) + flySfx.Volume = float32(absMoveSpeed / float64(moveSpeedMax)) + + if (intent.MoveUp || intent.MoveDown || intent.RotateLeft || intent.RotateRight || s.moveSpeed != 0) && !flySfx.IsPlaying { + flySfx.IsPlaying = true + } else if !(intent.MoveUp || intent.MoveDown || intent.RotateLeft || intent.RotateRight || s.moveSpeed != 0) && flySfx.IsPlaying || hp.Hp == 0 { + flySfx.IsPlaying = false + } + + vel.Y = float32(math.Cos(rot.Angle+math.Pi)) * s.moveSpeed + vel.X = -float32(math.Sin(rot.Angle+math.Pi)) * s.moveSpeed + + if weapon.CooldownLeft <= 0 { + if intent.Fire { + var count int = 30 + for i := range count { + var angle = math.Pi*2/float64(count)*float64(i) + rot.Angle + + bulletVelocityY := vel.Y + float32(math.Cos(angle+math.Pi))*bulletSpeed + bulletVelocityX := vel.X - float32(math.Sin(angle+math.Pi))*bulletSpeed + entities.CreateBullet(entities.CreateBulletManagers{ + EntityManager: s.EntityManager, + Positions: s.Positions, + Rotations: s.Rotations, + Scales: s.Scales, + Velocities: s.Velocities, + CircleColliders: s.CircleColliders, + BoxColliders: s.BoxColliders, + RigidBodies: s.RigidBodies, + Sprites: s.Sprites, + BulletTags: s.BulletTags, + Hps: s.Hps, + Renderables: s.Renderables, + }, pos.XY.X, pos.XY.Y, angle, bulletVelocityX, bulletVelocityY) + } + weapon.CooldownLeft = weapon.Cooldown + + fireSoundEntity := s.EntityManager.Create() + + s.SoundEffects.Create(fireSoundEntity, components.SoundEffect{ + Clip: assets.Audio.Get("gun_sound.wav"), + IsPlaying: false, + IsLooping: false, + Pitch: 1.0, + Volume: 1.0, + Pan: 0.5, + }) + + s.Positions.Create( + fireSoundEntity, + stdcomponents.Position{ + XY: vectors.Vec2{ + X: pos.XY.X, + Y: pos.XY.Y, + }, + }, + ) + } + } else { + weapon.CooldownLeft -= dt + } + + return true + }) +} + +func (s *SpaceshipIntentsSystem) Destroy() {} diff --git a/examples/new-api/systems/spatial-audio.go b/examples/new-api/systems/spatial-audio.go new file mode 100644 index 00000000..b03baa15 --- /dev/null +++ b/examples/new-api/systems/spatial-audio.go @@ -0,0 +1,169 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package systems + +import ( + "gomp/examples/new-api/components" + "gomp/examples/new-api/config" + "gomp/pkg/core" + "gomp/pkg/ecs" + "gomp/pkg/worker" + "gomp/stdcomponents" + "gomp/vectors" + "math" + "time" + + "github.com/negrel/assert" +) + +const ( + MinDistance = 10 +) + +func NewSpatialAudioSystem() SpatialAudioSystem { + return SpatialAudioSystem{} +} + +type SpatialAudioSystem struct { + EntityManager *ecs.EntityManager + SoundEffects *components.SoundEffectsComponentManager + Positions *stdcomponents.PositionComponentManager + SpatialAudio *components.SpatialAudioComponentManager + Cameras *stdcomponents.CameraComponentManager + numWorkers int + accSpatialAudioCreate [][]ecs.Entity + accSpatialAudioDelete [][]ecs.Entity + Engine *core.Engine +} + +func (s *SpatialAudioSystem) Init() { + s.numWorkers = s.Engine.Pool().NumWorkers() + s.accSpatialAudioCreate = make([][]ecs.Entity, s.numWorkers) + s.accSpatialAudioDelete = make([][]ecs.Entity, s.numWorkers) +} + +func (s *SpatialAudioSystem) Run(dt time.Duration) { + var mainCamera ecs.Entity + + // TODO: Add listener component? Then we need position component on it... + s.Cameras.EachEntity(func(entity ecs.Entity) bool { + camera := s.Cameras.GetUnsafe(entity) + assert.NotNil(camera) + if camera.Layer == config.MainCameraLayer { + mainCamera = entity + return false + } + + return true + }) + + if mainCamera == 0 { + return + } + + mainCameraComponent := s.Cameras.GetUnsafe(mainCamera) + assert.NotNil(mainCameraComponent) + + var mainCameraPosition vectors.Vec2 = vectors.Vec2{ + X: mainCameraComponent.Camera2D.Target.X, + Y: mainCameraComponent.Camera2D.Target.Y, + } + + s.SoundEffects.ProcessEntities(func(entity ecs.Entity, workerId worker.WorkerId) { + position := s.Positions.Has(entity) + + if s.SpatialAudio.Has(entity) { + if !position { + s.accSpatialAudioDelete[workerId] = append(s.accSpatialAudioDelete[workerId], entity) + } + } else { + if position { + s.accSpatialAudioCreate[workerId] = append(s.accSpatialAudioCreate[workerId], entity) + } + } + }) + + for a := range s.accSpatialAudioCreate { + for _, entity := range s.accSpatialAudioCreate[a] { + s.SpatialAudio.Create(entity, components.SpatialAudio{ + Volume: 0, + Pan: 0.5, + }) + } + } + + for a := range s.accSpatialAudioDelete { + for _, entity := range s.accSpatialAudioDelete[a] { + s.SpatialAudio.Delete(entity) + } + } + + s.SpatialAudio.ProcessEntities(func(entity ecs.Entity, workerId worker.WorkerId) { + spatialAudio := s.SpatialAudio.GetUnsafe(entity) + assert.NotNil(spatialAudio) + + position := s.Positions.GetUnsafe(entity) + assert.NotNil(position) + + spatialAudio.Volume = s.calculateVolume( + mainCameraPosition, + position.XY, + mainCameraComponent.Camera2D.Offset.X*2, + ) + spatialAudio.Pan = s.calculatePan( + mainCameraPosition, + position.XY, + mainCameraComponent.Camera2D.Offset.X*2, + ) + }) + + for i := range s.accSpatialAudioCreate { + s.accSpatialAudioCreate[i] = s.accSpatialAudioCreate[i][:0] + } + + for i := range s.accSpatialAudioDelete { + s.accSpatialAudioDelete[i] = s.accSpatialAudioDelete[i][:0] + } + +} +func (s *SpatialAudioSystem) Destroy() { +} + +func (s *SpatialAudioSystem) calculatePan(listener vectors.Vec2, source vectors.Vec2, maxDistance float32) float32 { + distance := listener.Distance(source) + + if distance < MinDistance { + return 0.5 + } + + distanceX := float64(listener.X - source.X) + + pan := (1 + (distanceX / float64(maxDistance))) / 2 + + return float32(math.Max(0, math.Min(1, pan))) +} + +func (s *SpatialAudioSystem) calculateVolume(listener vectors.Vec2, source vectors.Vec2, maxDistance float32) float32 { + distance := float64(listener.Distance(source)) + + if distance < MinDistance { + return 1 + } + + spatialVolume := 1 - (distance / float64(maxDistance)) // TODO: add ability to configure volume hearing distance + volume := math.Max(0, math.Min(1, spatialVolume)) + + return float32(math.Sin((volume * math.Pi) / 2)) // TODO: add ability to configure volume falloff. Current is easeOutSine +} diff --git a/examples/new-api/systems/texture-circle.go b/examples/new-api/systems/texture-circle.go new file mode 100644 index 00000000..23c1c6f7 --- /dev/null +++ b/examples/new-api/systems/texture-circle.go @@ -0,0 +1,68 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +package systems + +import ( + rl "github.com/gen2brain/raylib-go/raylib" + "github.com/negrel/assert" + "gomp/examples/new-api/components" + "gomp/pkg/core" + "gomp/pkg/ecs" + "gomp/pkg/worker" + "gomp/stdcomponents" + "time" +) + +func NewTextureCircleSystem() TextureCircleSystem { + return TextureCircleSystem{} +} + +type TextureCircleSystem struct { + Circles *components.PrimitiveCircleComponentManager + Textures *stdcomponents.RLTextureProComponentManager + texture rl.RenderTexture2D + + Engine *core.Engine +} + +func (s *TextureCircleSystem) Init() { + const circleRadius = 128 + var texture = rl.LoadRenderTexture(circleRadius*2, circleRadius*2) + rl.BeginTextureMode(texture) + rl.DrawCircle(circleRadius, circleRadius, circleRadius, rl.White) + rl.EndTextureMode() + s.texture = texture +} + +func (s *TextureCircleSystem) Run(dt time.Duration) { + s.Circles.ProcessEntities(func(entity ecs.Entity, workerId worker.WorkerId) { + circle := s.Circles.GetUnsafe(entity) + assert.NotNil(circle, "circle is nil; entity: %d", entity) + texture := s.Textures.GetUnsafe(entity) + assert.NotNil(texture, "texture is nil; entity: %d", entity) + + texture.Texture = &s.texture.Texture + texture.Dest.X = circle.CenterX + texture.Dest.Y = circle.CenterY + texture.Dest.Width = circle.Radius * 2 + texture.Dest.Height = circle.Radius * 2 + texture.Frame = rl.Rectangle{ + X: 0, + Y: 0, + Width: float32(s.texture.Texture.Width), + Height: float32(s.texture.Texture.Height), + } + texture.Rotation = circle.Rotation + texture.Origin.X = circle.Origin.X + circle.Radius + texture.Origin.Y = circle.Origin.Y + circle.Radius + texture.Tint = circle.Color + }) +} + +func (s *TextureCircleSystem) Destroy() { + rl.UnloadRenderTexture(s.texture) +} diff --git a/examples/new-api/systems/texture-rect.go b/examples/new-api/systems/texture-rect.go new file mode 100644 index 00000000..b5a128d6 --- /dev/null +++ b/examples/new-api/systems/texture-rect.go @@ -0,0 +1,60 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +package systems + +import ( + rl "github.com/gen2brain/raylib-go/raylib" + "github.com/negrel/assert" + "gomp/examples/new-api/components" + "gomp/pkg/core" + "gomp/pkg/ecs" + "gomp/pkg/worker" + "gomp/stdcomponents" + "runtime" + "time" +) + +func NewTextureRectSystem() TextureRectSystem { + return TextureRectSystem{} +} + +type TextureRectSystem struct { + TextureRect *components.TextureRectComponentManager + Textures *stdcomponents.RLTextureProComponentManager + texture rl.RenderTexture2D + numWorkers int + Engine *core.Engine +} + +func (s *TextureRectSystem) Init() { + var texture = rl.LoadRenderTexture(1, 1) // :) + rl.BeginTextureMode(texture) + rl.ClearBackground(rl.White) + rl.EndTextureMode() + s.texture = texture + s.numWorkers = runtime.NumCPU() - 2 +} + +func (s *TextureRectSystem) Run(dt time.Duration) { + // Create shallow copy of texture to draw rectangles + s.TextureRect.ProcessEntities(func(entity ecs.Entity, workerId worker.WorkerId) { + rect := s.TextureRect.GetUnsafe(entity) + assert.NotNil(rect, "rect is nil; entity: %d", entity) + texture := s.Textures.GetUnsafe(entity) + assert.NotNil(texture, "texture is nil; entity: %d", entity) + + texture.Texture = &s.texture.Texture + texture.Dest = rect.Dest + texture.Rotation = rect.Rotation + texture.Origin = rect.Origin + texture.Tint = rect.Color + }) +} + +func (s *TextureRectSystem) Destroy() { + rl.UnloadRenderTexture(s.texture) +} diff --git a/examples/raylib-ecs/assets/assets.go b/examples/raylib-ecs/assets/assets.go deleted file mode 100644 index c7383c04..00000000 --- a/examples/raylib-ecs/assets/assets.go +++ /dev/null @@ -1,20 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ -package assets - -import ( - rl "github.com/gen2brain/raylib-go/raylib" - "gomp/pkg/ecs" -) - -var Textures = ecs.CreateAssetLibrary( - func(path string) rl.Texture2D { - return rl.LoadTexture(path) - }, - func(path string, asset *rl.Texture2D) { - rl.UnloadTexture(*asset) - }, -) diff --git a/examples/raylib-ecs/components/components.go b/examples/raylib-ecs/components/components.go deleted file mode 100644 index 97826fd8..00000000 --- a/examples/raylib-ecs/components/components.go +++ /dev/null @@ -1,42 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ - -package components - -import ( - "gomp/pkg/ecs" -) - -// ============ -// Business -// ============ - -var HealthService = ecs.CreateComponentService[Health](HEALTH_ID) - -// ============ -// Default -// ============ - -var PositionService = ecs.CreateComponentService[Position](POSITION_ID) -var RotationService = ecs.CreateComponentService[Rotation](ROTATION_ID) -var ScaleService = ecs.CreateComponentService[Scale](SCALE_ID) -var MirroredService = ecs.CreateComponentService[Mirrored](MIRRORED_ID) - -// Rendering - -var SpriteService = ecs.CreateComponentService[Sprite](SPRITE_ID) -var SpriteSheetService = ecs.CreateComponentService[SpriteSheet](SPRITE_SHEET_ID) -var SpriteMatrixService = ecs.CreateComponentService[SpriteMatrix](SPRITE_MATRIX_ID) -var TintService = ecs.CreateComponentService[Tint](TINT_ID) - -var AnimationPlayerService = ecs.CreateComponentService[AnimationPlayer](ANIMATION_ID) -var AnimationStateService = ecs.CreateComponentService[AnimationState](ANIMATION_STATE_ID) - -var TextureRenderService = ecs.CreateComponentService[TextureRender](TEXTURE_RENDER_ID) - -// Network - -var NetworkComponentService = ecs.CreateComponentService[Network](NETWORK_ID) diff --git a/examples/raylib-ecs/components/ids.go b/examples/raylib-ecs/components/ids.go deleted file mode 100644 index d671e938..00000000 --- a/examples/raylib-ecs/components/ids.go +++ /dev/null @@ -1,31 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ - -package components - -import ( - "gomp/pkg/ecs" -) - -const ( - INVALID_ID ecs.ComponentID = iota - COLOR_ID - POSITION_ID - ROTATION_ID - SCALE_ID - MIRRORED_ID - HEALTH_ID - DESTROY_ID - SPRITE_ID - SPRITE_SHEET_ID - SPRITE_MATRIX_ID - TEXTURE_RENDER_ID - ANIMATION_ID - ANIMATION_STATE_ID - TINT_ID - SPRITE_ANIMATOR_ID - NETWORK_ID -) diff --git a/examples/raylib-ecs/components/types.go b/examples/raylib-ecs/components/types.go deleted file mode 100644 index c161d96f..00000000 --- a/examples/raylib-ecs/components/types.go +++ /dev/null @@ -1,103 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. - -===-===-===-===-===-===-===-===-===-=== -Donations during this file development: --===-===-===-===-===-===-===-===-===-=== - -none :) - -Thank you for your support! -*/ - -package components - -import ( - rl "github.com/gen2brain/raylib-go/raylib" - "image/color" - "time" -) - -// Business - -type Health struct { - Hp, MaxHp int32 -} - -// Default - -type Position struct { - X, Y float32 -} -type Rotation struct { - Angle float32 -} -type Scale struct { - X, Y float32 -} -type Mirrored struct { - X, Y bool -} - -// Render - -type Sprite struct { - Texture *rl.Texture2D - Frame rl.Rectangle - Origin rl.Vector2 - Tint color.RGBA -} -type SpriteSheet struct { - Texture *rl.Texture2D - Frame rl.Rectangle - Origin rl.Vector2 - NumOfFrames int32 - FPS int32 - Vertical bool -} -type Tint = color.RGBA -type SpriteMatrixAnimation struct { - Name string - Frame rl.Rectangle - NumOfFrames uint8 - Vertical bool - Loop bool -} -type SpriteMatrix struct { - Texture *rl.Texture2D - Origin rl.Vector2 - FPS int32 - Animations []SpriteMatrixAnimation -} -type TextureRender struct { - Texture *rl.Texture2D - Frame rl.Rectangle - Origin rl.Vector2 - Tint color.RGBA - Dest rl.Rectangle - Rotation float32 -} -type AnimationPlayer struct { - First uint8 - Last uint8 - Current uint8 - Speed float32 - Loop bool - Vertical bool - ElapsedTime time.Duration - FrameDuration time.Duration - State AnimationState - IsInitialized bool -} -type AnimationState int - -// Network - -type NetworkId int32 -type Network struct { - Id NetworkId - PatchIn []byte - PatchOut []byte -} diff --git a/examples/raylib-ecs/entities/player.go b/examples/raylib-ecs/entities/player.go deleted file mode 100644 index 6e92ee91..00000000 --- a/examples/raylib-ecs/entities/player.go +++ /dev/null @@ -1,116 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ - -package entities - -import ( - rl "github.com/gen2brain/raylib-go/raylib" - "gomp/examples/raylib-ecs/assets" - "gomp/examples/raylib-ecs/components" - ecs2 "gomp/pkg/ecs" -) - -const ( - PlayerStateIdle components.AnimationState = iota - PlayerStateWalk - PlayerStateJump - PlayerStateFall - PlayerStateAttack - PlayerStateHurt - PlayerStateDie -) - -type Player struct { - ecs2.Entity - Position *components.Position - Rotation *components.Rotation - Scale *components.Scale - SpriteMatrix *components.SpriteMatrix - Tint *components.Tint - AnimationPlayer *components.AnimationPlayer - AnimationState *components.AnimationState - Mirrored *components.Mirrored -} - -var playerSpriteMatrix = components.SpriteMatrix{ - Texture: assets.Textures.Get("examples/raylib-ecs/assets/milansheet.png"), - Origin: rl.Vector2{X: 0.5, Y: 0.5}, - FPS: 12, - Animations: []components.SpriteMatrixAnimation{ - { - Name: "idle", - Frame: rl.Rectangle{X: 0, Y: 0, Width: 96, Height: 128}, - NumOfFrames: 1, - Vertical: false, - Loop: true, - }, - { - Name: "walk", - Frame: rl.Rectangle{X: 0, Y: 512, Width: 96, Height: 128}, - NumOfFrames: 8, - Vertical: false, - Loop: true, - }, - { - Name: "jump", - Frame: rl.Rectangle{X: 96, Y: 0, Width: 96, Height: 128}, - NumOfFrames: 1, - Vertical: false, - Loop: false, - }, - }, -} - -func CreatePlayer(world *ecs2.World) (player Player) { - // Getting managers - spriteMatrixes := components.SpriteMatrixService.GetManager(world) - positions := components.PositionService.GetManager(world) - rotations := components.RotationService.GetManager(world) - scales := components.ScaleService.GetManager(world) - animationPlayers := components.AnimationPlayerService.GetManager(world) - animationStates := components.AnimationStateService.GetManager(world) - tints := components.TintService.GetManager(world) - mirrored := components.MirroredService.GetManager(world) - - // Creating new player - - entity := world.CreateEntity("player") - player.Entity = entity - - // Adding position component - t := components.Position{} - player.Position = positions.Create(entity, t) - - // Adding rotation component - rotation := components.Rotation{} - player.Rotation = rotations.Create(entity, rotation) - - // Adding scale component - scale := components.Scale{ - X: 1, - Y: 1, - } - player.Scale = scales.Create(entity, scale) - - // Adding Tint component - tint := components.Tint{R: 255, G: 255, B: 255, A: 255} - player.Tint = tints.Create(entity, tint) - - // Adding sprite matrix component - player.SpriteMatrix = spriteMatrixes.Create(entity, playerSpriteMatrix) - - // Adding animation player component - animation := components.AnimationPlayer{} - player.AnimationPlayer = animationPlayers.Create(entity, animation) - - // Adding Animation state component - player.AnimationState = animationStates.Create(entity, PlayerStateWalk) - - // Adding Mirrored component - player.Mirrored = mirrored.Create(entity, components.Mirrored{}) - - return player -} diff --git a/examples/raylib-ecs/main.go b/examples/raylib-ecs/main.go deleted file mode 100644 index d89c0359..00000000 --- a/examples/raylib-ecs/main.go +++ /dev/null @@ -1,84 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. - -===-===-===-===-===-===-===-===-===-=== -Donations during this file development: --===-===-===-===-===-===-===-===-===-=== - -<- Монтажер сука Donated 50 RUB - -Thank you for your support! -*/ - -package main - -import ( - "gomp/examples/raylib-ecs/components" - "gomp/examples/raylib-ecs/systems" - "gomp/pkg/ecs" -) - -func main() { - world := ecs.CreateWorld("main-raylib") - defer world.Destroy() - - // world.ApplyPatch(patch) - - // components.TransformService.OnPatch(func(newstate components.Transform, oldstate components.Transform) components.Transform { - // return new - // }) - - world.RegisterComponentServices( - &components.PositionService, - &components.RotationService, - &components.ScaleService, - &components.MirroredService, - &components.HealthService, - &components.SpriteService, - &components.SpriteSheetService, - &components.SpriteMatrixService, - &components.TintService, - &components.AnimationPlayerService, - &components.AnimationStateService, - &components.TextureRenderService, - ) - - world.RegisterSystems(). - Sequential( // Initial systems: Main thread - &systems.InitService, - ). - Parallel( // Network receive systems - &systems.NetworkService, - &systems.NetworkReceiveService, - ). - Parallel( // Business logic systems - &systems.PlayerService, - &systems.HpService, - &systems.ColorService, - ). - Parallel( - // Network send systems - &systems.NetworkSendService, - // Prerender systems - &systems.AnimationSpriteMatrixService, - &systems.AnimationPlayerService, - &systems.TRSpriteService, - &systems.TRSpriteSheetService, - &systems.TRSpriteMatrixService, - &systems.TRAnimationService, - &systems.TRMirroredService, - &systems.TRPositionService, - &systems.TRRotationService, - &systems.TRScaleService, - &systems.TRTintService, - ). - Sequential( // Render systems: Main thread - &systems.RenderService, - &systems.DebugService, - &systems.AssetLibService, - ) - - world.Run(1) -} diff --git a/examples/raylib-ecs/systems/asset-library.go b/examples/raylib-ecs/systems/asset-library.go deleted file mode 100644 index c63d6c65..00000000 --- a/examples/raylib-ecs/systems/asset-library.go +++ /dev/null @@ -1,28 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ - -package systems - -import ( - ecs2 "gomp/pkg/ecs" -) - -type assetLibController struct { - assets []ecs2.AnyAssetLibrary -} - -func (s *assetLibController) Init(world *ecs2.World) {} -func (s *assetLibController) Update(world *ecs2.World) { - for _, asset := range s.assets { - asset.LoadAll() - } -} -func (s *assetLibController) FixedUpdate(world *ecs2.World) {} -func (s *assetLibController) Destroy(world *ecs2.World) { - for _, asset := range s.assets { - asset.UnloadAll() - } -} diff --git a/examples/raylib-ecs/systems/color.go b/examples/raylib-ecs/systems/color.go deleted file mode 100644 index 64b50561..00000000 --- a/examples/raylib-ecs/systems/color.go +++ /dev/null @@ -1,44 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ - -package systems - -import ( - "gomp/examples/raylib-ecs/components" - ecs2 "gomp/pkg/ecs" - "image/color" -) - -type colorController struct { - baseColor color.RGBA -} - -func (s *colorController) Init(world *ecs2.World) { - s.baseColor = color.RGBA{25, 220, 200, 255} -} -func (s *colorController) Update(world *ecs2.World) {} -func (s *colorController) FixedUpdate(world *ecs2.World) { - sprites := components.SpriteService.GetManager(world) - hps := components.HealthService.GetManager(world) - - sprites.AllParallel(func(entity ecs2.Entity, sprite *components.Sprite) bool { - hp := hps.Get(entity) - if hp == nil { - return true - } - - hpPercentage := float32(hp.Hp) / float32(hp.MaxHp) - - sprite.Tint = color.RGBA{ - uint8(hpPercentage * float32(s.baseColor.R)), - uint8(hpPercentage * float32(s.baseColor.G)), - uint8(hpPercentage * float32(s.baseColor.B)), - s.baseColor.A, - } - return true - }) -} -func (s *colorController) Destroy(world *ecs2.World) {} diff --git a/examples/raylib-ecs/systems/debug.go b/examples/raylib-ecs/systems/debug.go deleted file mode 100644 index f05b417f..00000000 --- a/examples/raylib-ecs/systems/debug.go +++ /dev/null @@ -1,42 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ - -package systems - -import ( - "fmt" - "gomp/pkg/ecs" - "log" - "os" - "runtime/pprof" - - rl "github.com/gen2brain/raylib-go/raylib" -) - -type debugController struct { - pprofEnabled bool -} - -func (s *debugController) Init(world *ecs.World) {} -func (s *debugController) Update(world *ecs.World) { - if rl.IsKeyPressed(rl.KeyF9) { - if s.pprofEnabled { - pprof.StopCPUProfile() - fmt.Println("CPU Profile Stopped") - } else { - f, err := os.Create("cpu.out") - if err != nil { - log.Fatal(err) - } - pprof.StartCPUProfile(f) - fmt.Println("CPU Profile Started") - } - - s.pprofEnabled = !s.pprofEnabled - } -} -func (s *debugController) FixedUpdate(world *ecs.World) {} -func (s *debugController) Destroy(world *ecs.World) {} diff --git a/examples/raylib-ecs/systems/hp.go b/examples/raylib-ecs/systems/hp.go deleted file mode 100644 index 2fe51b95..00000000 --- a/examples/raylib-ecs/systems/hp.go +++ /dev/null @@ -1,31 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ - -package systems - -import ( - "gomp/examples/raylib-ecs/components" - ecs2 "gomp/pkg/ecs" -) - -type hpController struct{} - -func (s *hpController) Init(world *ecs2.World) {} -func (s *hpController) Update(world *ecs2.World) {} -func (s *hpController) FixedUpdate(world *ecs2.World) { - healths := components.HealthService.GetManager(world) - - healths.All(func(entity ecs2.Entity, h *components.Health) bool { - h.Hp-- - - if h.Hp <= 0 { - world.DestroyEntity(entity) - } - - return true - }) -} -func (s *hpController) Destroy(world *ecs2.World) {} diff --git a/examples/raylib-ecs/systems/init.go b/examples/raylib-ecs/systems/init.go deleted file mode 100644 index d81ded04..00000000 --- a/examples/raylib-ecs/systems/init.go +++ /dev/null @@ -1,20 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ - -package systems - -import ( - "gomp/pkg/ecs" -) - -type initController struct { -} - -func (s *initController) Init(world *ecs.World) {} - -func (s *initController) Update(world *ecs.World) {} -func (s *initController) FixedUpdate(world *ecs.World) {} -func (s *initController) Destroy(world *ecs.World) {} diff --git a/examples/raylib-ecs/systems/player.go b/examples/raylib-ecs/systems/player.go deleted file mode 100644 index cd5f6658..00000000 --- a/examples/raylib-ecs/systems/player.go +++ /dev/null @@ -1,56 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ - -package systems - -import ( - rl "github.com/gen2brain/raylib-go/raylib" - "gomp/examples/raylib-ecs/components" - "gomp/examples/raylib-ecs/entities" - "gomp/pkg/ecs" -) - -type playerController struct { - player entities.Player -} - -func (s *playerController) Init(world *ecs.World) { - s.player = entities.CreatePlayer(world) - s.player.Position.X = 100 - s.player.Position.Y = 100 - -} -func (s *playerController) Update(world *ecs.World) { - animationStates := components.AnimationStateService.GetManager(world) - - animationState := animationStates.Get(s.player.Entity) - - if rl.IsKeyDown(rl.KeySpace) { - *animationState = entities.PlayerStateJump - } else { - *animationState = entities.PlayerStateIdle - if rl.IsKeyDown(rl.KeyD) { - *animationState = entities.PlayerStateWalk - s.player.Position.X++ - s.player.Mirrored.X = false - } - if rl.IsKeyDown(rl.KeyA) { - *animationState = entities.PlayerStateWalk - s.player.Position.X-- - s.player.Mirrored.X = true - } - if rl.IsKeyDown(rl.KeyW) { - *animationState = entities.PlayerStateWalk - s.player.Position.Y-- - } - if rl.IsKeyDown(rl.KeyS) { - *animationState = entities.PlayerStateWalk - s.player.Position.Y++ - } - } -} -func (s *playerController) FixedUpdate(world *ecs.World) {} -func (s *playerController) Destroy(world *ecs.World) {} diff --git a/examples/raylib-ecs/systems/render.go b/examples/raylib-ecs/systems/render.go deleted file mode 100644 index d44d2df3..00000000 --- a/examples/raylib-ecs/systems/render.go +++ /dev/null @@ -1,55 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ - -package systems - -import ( - "fmt" - rl "github.com/gen2brain/raylib-go/raylib" - "gomp/examples/raylib-ecs/components" - "gomp/pkg/ecs" -) - -type renderController struct { - windowWidth, windowHeight int32 -} - -func (s *renderController) Init(world *ecs.World) { - rl.InitWindow(s.windowWidth, s.windowHeight, "raylib [core] ebiten-ecs - basic window") - currentMonitorRefreshRate := int32(rl.GetMonitorRefreshRate(rl.GetCurrentMonitor())) - rl.SetTargetFPS(currentMonitorRefreshRate) -} -func (s *renderController) Update(world *ecs.World) { - spriteRenders := components.TextureRenderService.GetManager(world) - - if rl.WindowShouldClose() { - world.SetShouldDestroy(true) - return - } - - rl.BeginDrawing() - - rl.ClearBackground(rl.Black) - - spriteRenders.AllData(func(tr *components.TextureRender) bool { - texture := *tr.Texture - rl.DrawTexturePro(texture, tr.Frame, tr.Dest, tr.Origin, tr.Rotation, tr.Tint) - return true - }) - - // rl.DrawRectangle(0, 0, 120, 120, rl.DarkGray) - rl.DrawFPS(10, 10) - rl.DrawText(fmt.Sprintf("%d", world.Size()), 10, 30, 20, rl.Red) - rl.DrawText(fmt.Sprintf("%s", world.DtUpdate()), 10, 50, 20, rl.Red) - rl.DrawText(fmt.Sprintf("%s", world.DtFixedUpdate()), 10, 70, 20, rl.Red) - - rl.EndDrawing() -} - -func (s *renderController) FixedUpdate(world *ecs.World) {} -func (s *renderController) Destroy(world *ecs.World) { - rl.CloseWindow() -} diff --git a/examples/raylib-ecs/systems/spawn.go b/examples/raylib-ecs/systems/spawn.go deleted file mode 100644 index 304dc6c2..00000000 --- a/examples/raylib-ecs/systems/spawn.go +++ /dev/null @@ -1,89 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ - -package systems - -import ( - "gomp/examples/raylib-ecs/assets" - "gomp/examples/raylib-ecs/components" - "gomp/pkg/ecs" - "math/rand" - - rl "github.com/gen2brain/raylib-go/raylib" -) - -type spawnController struct { - pprofEnabled bool -} - -const ( - minHpPercentage = 20 - minMaxHp = 500 - maxMaxHp = 2000 -) - -func (s *spawnController) Init(world *ecs.World) {} -func (s *spawnController) Update(world *ecs.World) { - sprites := components.SpriteService.GetManager(world) - healths := components.HealthService.GetManager(world) - positions := components.PositionService.GetManager(world) - rotations := components.RotationService.GetManager(world) - scales := components.ScaleService.GetManager(world) - - if rl.IsKeyDown(rl.KeySpace) { - for range rand.Intn(10000) { - if world.Size() > 100_000_000 { - break - } - - newCreature := world.CreateEntity("Creature") - - // Adding position component - t := components.Position{ - X: float32(rand.Int31n(800)), - Y: float32(rand.Int31n(600)), - } - positions.Create(newCreature, t) - - // Adding rotation component - rotation := components.Rotation{ - Angle: float32(rand.Int31n(360)), - } - rotations.Create(newCreature, rotation) - - // Adding scale component - scale := components.Scale{ - X: 2, - Y: 2, - } - scales.Create(newCreature, scale) - - // Adding HP component - maxHp := minMaxHp + rand.Int31n(maxMaxHp-minMaxHp) - hp := int32(float32(maxHp) * float32(minHpPercentage+rand.Int31n(100-minHpPercentage)) / 100) - h := components.Health{ - Hp: hp, - MaxHp: maxHp, - } - healths.Create(newCreature, h) - - texture := assets.Textures.Get("assets/star.png") - - // Adding sprite component - c := components.Sprite{ - Origin: rl.Vector2{X: 0.5, Y: 0.5}, - Texture: texture, - Frame: rl.Rectangle{X: 0, Y: 0, Width: float32(texture.Width), Height: float32(texture.Height)}, - } - - sprites.Create(newCreature, c) - } - } -} -func (s *spawnController) FixedUpdate(world *ecs.World) { -} - -func (s *spawnController) Destroy(world *ecs.World) {} diff --git a/examples/raylib-ecs/systems/systems-engine.go b/examples/raylib-ecs/systems/systems-engine.go deleted file mode 100644 index d7f7c6c2..00000000 --- a/examples/raylib-ecs/systems/systems-engine.go +++ /dev/null @@ -1,55 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. - -===-===-===-===-===-===-===-===-===-=== -Donations during this file development: --===-===-===-===-===-===-===-===-===-=== - -none :) - -Thank you for your support! -*/ - -package systems - -import ( - "gomp/examples/raylib-ecs/assets" - "gomp/pkg/ecs" -) - -// Engine Initial Systems - -var InitService = ecs.CreateSystemService(&initController{}) -var DebugService = ecs.CreateSystemService(&debugController{}) - -// Engine Network systems - -var NetworkService = ecs.CreateSystemService(&networkController{}) -var NetworkSendService = ecs.CreateSystemService(&networkSendController{}) -var NetworkReceiveService = ecs.CreateSystemService(&networkReceiveController{}) - -// Engine Texture PreRender systems - -var AnimationSpriteMatrixService = ecs.CreateSystemService(&animationSpriteMatrixController{}) -var AnimationPlayerService = ecs.CreateSystemService(&animationPlayerController{}, &AnimationSpriteMatrixService) -var TRSpriteService = ecs.CreateSystemService(&trSpriteController{}, &AnimationPlayerService) -var TRSpriteSheetService = ecs.CreateSystemService(&trSpriteSheetController{}, &AnimationPlayerService) -var TRSpriteMatrixService = ecs.CreateSystemService(&trSpriteMatrixController{}, &AnimationPlayerService) -var TRAnimationService = ecs.CreateSystemService(&trAnimationController{}, &TRSpriteService, &TRSpriteSheetService, &TRSpriteMatrixService) -var TRMirroredService = ecs.CreateSystemService(&trMirroredController{}, &TRSpriteService, &TRSpriteSheetService, &TRSpriteMatrixService, &TRAnimationService) -var TRPositionService = ecs.CreateSystemService(&trPositionController{}, &TRSpriteService, &TRSpriteSheetService, &TRSpriteMatrixService) -var TRRotationService = ecs.CreateSystemService(&trRotationController{}, &TRSpriteService, &TRSpriteSheetService, &TRSpriteMatrixService) -var TRScaleService = ecs.CreateSystemService(&trScaleController{}, &TRSpriteService, &TRSpriteSheetService, &TRSpriteMatrixService) -var TRTintService = ecs.CreateSystemService(&trTintController{}, &TRSpriteService, &TRSpriteSheetService, &TRSpriteMatrixService) - -// Engine Render Systems - -var AssetLibService = ecs.CreateSystemService(&assetLibController{ - assets: []ecs.AnyAssetLibrary{assets.Textures}, -}) -var RenderService = ecs.CreateSystemService(&renderController{ - windowWidth: 1024, - windowHeight: 768, -}) diff --git a/examples/raylib-ecs/systems/texture-render-animation.go b/examples/raylib-ecs/systems/texture-render-animation.go deleted file mode 100644 index e3f25e3c..00000000 --- a/examples/raylib-ecs/systems/texture-render-animation.go +++ /dev/null @@ -1,45 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ - -package systems - -import ( - "gomp/examples/raylib-ecs/components" - ecs2 "gomp/pkg/ecs" -) - -// TextureRenderPosition is a system that sets Position of textureRender -type trAnimationController struct{} - -func (s *trAnimationController) Init(world *ecs2.World) {} -func (s *trAnimationController) FixedUpdate(world *ecs2.World) {} -func (s *trAnimationController) Update(world *ecs2.World) { - // Get component managers - animations := components.AnimationPlayerService.GetManager(world) - textureRenders := components.TextureRenderService.GetManager(world) - - // Update sprites and spriteRenders - textureRenders.AllParallel(func(entity ecs2.Entity, tr *components.TextureRender) bool { - if tr == nil { - return true - } - - animation := animations.Get(entity) - if animation == nil { - return true - } - - frame := &tr.Frame - if animation.Vertical { - frame.Y += frame.Height * float32(animation.Current) - } else { - frame.X += frame.Width * float32(animation.Current) - } - - return true - }) -} -func (s *trAnimationController) Destroy(world *ecs2.World) {} diff --git a/examples/raylib-ecs/systems/texture-render-mirrored.go b/examples/raylib-ecs/systems/texture-render-mirrored.go deleted file mode 100644 index fe2493a1..00000000 --- a/examples/raylib-ecs/systems/texture-render-mirrored.go +++ /dev/null @@ -1,45 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ - -package systems - -import ( - "gomp/examples/raylib-ecs/components" - ecs2 "gomp/pkg/ecs" -) - -// TextureRenderScale is a system that sets Scale of textureRender -type trMirroredController struct{} - -func (s *trMirroredController) Init(world *ecs2.World) {} -func (s *trMirroredController) FixedUpdate(world *ecs2.World) {} -func (s *trMirroredController) Update(world *ecs2.World) { - // Get component managers - mirroreds := components.MirroredService.GetManager(world) - textureRenders := components.TextureRenderService.GetManager(world) - - // Update sprites and spriteRenders - textureRenders.AllParallel(func(entity ecs2.Entity, tr *components.TextureRender) bool { - if tr == nil { - return true - } - - mirrored := mirroreds.Get(entity) - if mirrored == nil { - return true - } - - if mirrored.X { - tr.Frame.Width *= -1 - } - if mirrored.Y { - tr.Frame.Height *= -1 - } - - return true - }) -} -func (s *trMirroredController) Destroy(world *ecs2.World) {} diff --git a/examples/raylib-ecs/systems/texture-render-position.go b/examples/raylib-ecs/systems/texture-render-position.go deleted file mode 100644 index 18c3cfed..00000000 --- a/examples/raylib-ecs/systems/texture-render-position.go +++ /dev/null @@ -1,41 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ - -package systems - -import ( - "gomp/examples/raylib-ecs/components" - ecs2 "gomp/pkg/ecs" -) - -// TextureRenderPosition is a system that sets Position of textureRender -type trPositionController struct{} - -func (s *trPositionController) Init(world *ecs2.World) {} -func (s *trPositionController) FixedUpdate(world *ecs2.World) {} -func (s *trPositionController) Update(world *ecs2.World) { - // Get component managers - positions := components.PositionService.GetManager(world) - textureRenders := components.TextureRenderService.GetManager(world) - - // Update sprites and spriteRenders - textureRenders.AllParallel(func(entity ecs2.Entity, tr *components.TextureRender) bool { - if tr == nil { - return true - } - - position := positions.Get(entity) - if position == nil { - return true - } - - tr.Dest.X = position.X - tr.Dest.Y = position.Y - - return true - }) -} -func (s *trPositionController) Destroy(world *ecs2.World) {} diff --git a/examples/raylib-ecs/systems/texture-render-rotation.go b/examples/raylib-ecs/systems/texture-render-rotation.go deleted file mode 100644 index 24648b04..00000000 --- a/examples/raylib-ecs/systems/texture-render-rotation.go +++ /dev/null @@ -1,40 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ - -package systems - -import ( - "gomp/examples/raylib-ecs/components" - ecs2 "gomp/pkg/ecs" -) - -// TextureRenderRotation is a system that sets Rotation of textureRender -type trRotationController struct{} - -func (s *trRotationController) Init(world *ecs2.World) {} -func (s *trRotationController) FixedUpdate(world *ecs2.World) {} -func (s *trRotationController) Update(world *ecs2.World) { - // Get component managers - rotations := components.RotationService.GetManager(world) - textureRenders := components.TextureRenderService.GetManager(world) - - // Update sprites and spriteRenders - textureRenders.AllParallel(func(entity ecs2.Entity, tr *components.TextureRender) bool { - if tr == nil { - return true - } - - rotation := rotations.Get(entity) - if rotation == nil { - return true - } - - tr.Rotation = rotation.Angle - - return true - }) -} -func (s *trRotationController) Destroy(world *ecs2.World) {} diff --git a/examples/raylib-ecs/systems/texture-render-scale.go b/examples/raylib-ecs/systems/texture-render-scale.go deleted file mode 100644 index 08d6be38..00000000 --- a/examples/raylib-ecs/systems/texture-render-scale.go +++ /dev/null @@ -1,41 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ - -package systems - -import ( - "gomp/examples/raylib-ecs/components" - ecs2 "gomp/pkg/ecs" -) - -// TextureRenderScale is a system that sets Scale of textureRender -type trScaleController struct{} - -func (s *trScaleController) Init(world *ecs2.World) {} -func (s *trScaleController) FixedUpdate(world *ecs2.World) {} -func (s *trScaleController) Update(world *ecs2.World) { - // Get component managers - scales := components.ScaleService.GetManager(world) - textureRenders := components.TextureRenderService.GetManager(world) - - // Update sprites and spriteRenders - textureRenders.AllParallel(func(entity ecs2.Entity, tr *components.TextureRender) bool { - if tr == nil { - return true - } - - scale := scales.Get(entity) - if scale == nil { - return true - } - - tr.Dest.Width *= scale.X - tr.Dest.Height *= scale.Y - - return true - }) -} -func (s *trScaleController) Destroy(world *ecs2.World) {} diff --git a/examples/raylib-ecs/systems/texture-render-sprite.go b/examples/raylib-ecs/systems/texture-render-sprite.go deleted file mode 100644 index 4d0ec0c5..00000000 --- a/examples/raylib-ecs/systems/texture-render-sprite.go +++ /dev/null @@ -1,86 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. - -===-===-===-===-===-===-===-===-===-=== -Donations during this file deveopment: --===-===-===-===-===-===-===-===-===-=== - -<- Монтажер сука Donated 50 RUB - -Thank you for your support! -*/ - -package systems - -import ( - rl "github.com/gen2brain/raylib-go/raylib" - "gomp/examples/raylib-ecs/components" - ecs2 "gomp/pkg/ecs" -) - -// TextureRenderSprite is a system that prepares Sprite to be rendered -type trSpriteController struct{} - -func (s *trSpriteController) Init(world *ecs2.World) {} -func (s *trSpriteController) FixedUpdate(world *ecs2.World) {} -func (s *trSpriteController) Update(world *ecs2.World) { - // Get component managers - sprites := components.SpriteService.GetManager(world) - textureRenders := components.TextureRenderService.GetManager(world) - - // Update sprites and spriteRenders - sprites.AllParallel(func(entity ecs2.Entity, sprite *components.Sprite) bool { - if sprite == nil { - return true - } - - spriteFrame := sprite.Frame - spriteOrigin := sprite.Origin - spriteTint := sprite.Tint - - tr := textureRenders.Get(entity) - if tr == nil { - // Create new spriteRender - newRender := components.TextureRender{ - Texture: sprite.Texture, - Frame: sprite.Frame, - Origin: sprite.Origin, - Tint: sprite.Tint, - Dest: rl.NewRectangle( - 0, - 0, - sprite.Frame.Width, - sprite.Frame.Height, - ), - } - - textureRenders.Create(entity, newRender) - } else { - // Update spriteRender - // tr.Texture = sprite.Texture - trFrame := &tr.Frame - trFrame.X = spriteFrame.X - trFrame.Y = spriteFrame.Y - trFrame.Width = spriteFrame.Width - trFrame.Height = spriteFrame.Height - - trOrigin := &tr.Origin - trOrigin.X = spriteOrigin.X - trOrigin.Y = spriteOrigin.Y - - trTint := &tr.Tint - trTint.A = spriteTint.A - trTint.R = spriteTint.R - trTint.G = spriteTint.G - trTint.B = spriteTint.B - - trDest := &tr.Dest - trDest.Width = spriteFrame.Width - trDest.Height = spriteFrame.Height - } - return true - }) -} -func (s *trSpriteController) Destroy(world *ecs2.World) {} diff --git a/examples/raylib-ecs/systems/texture-render-spritematrix.go b/examples/raylib-ecs/systems/texture-render-spritematrix.go deleted file mode 100644 index 1a31b34b..00000000 --- a/examples/raylib-ecs/systems/texture-render-spritematrix.go +++ /dev/null @@ -1,66 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ - -package systems - -import ( - rl "github.com/gen2brain/raylib-go/raylib" - "gomp/examples/raylib-ecs/components" - ecs2 "gomp/pkg/ecs" -) - -// TextureRenderSprite is a system that prepares SpriteSheet to be rendered -type trSpriteMatrixController struct{} - -func (s *trSpriteMatrixController) Init(world *ecs2.World) {} -func (s *trSpriteMatrixController) FixedUpdate(world *ecs2.World) {} -func (s *trSpriteMatrixController) Update(world *ecs2.World) { - // Get component managers - spriteMatrixes := components.SpriteMatrixService.GetManager(world) - textureRenders := components.TextureRenderService.GetManager(world) - animationStates := components.AnimationStateService.GetManager(world) - - // Update sprites and spriteRenders - spriteMatrixes.AllParallel(func(entity ecs2.Entity, spriteMatrix *components.SpriteMatrix) bool { - if spriteMatrix == nil { - return true - } - - animationState := animationStates.Get(entity) - if animationState == nil { - return true - } - - currentAnimationFrame := spriteMatrix.Animations[*animationState].Frame - - tr := textureRenders.Get(entity) - if tr == nil { - // Create new spriteRender - newRender := components.TextureRender{ - Texture: spriteMatrix.Texture, - Origin: spriteMatrix.Origin, - Frame: currentAnimationFrame, - Dest: rl.Rectangle{ - Width: currentAnimationFrame.Width, - Height: currentAnimationFrame.Height, - }, - } - - textureRenders.Create(entity, newRender) - } else { - // Update spriteRender - tr.Texture = spriteMatrix.Texture - tr.Origin = spriteMatrix.Origin - tr.Dest = rl.Rectangle{ - Width: currentAnimationFrame.Width, - Height: currentAnimationFrame.Height, - } - tr.Frame = currentAnimationFrame - } - return true - }) -} -func (s *trSpriteMatrixController) Destroy(world *ecs2.World) {} diff --git a/examples/raylib-ecs/systems/texture-render-spritesheet.go b/examples/raylib-ecs/systems/texture-render-spritesheet.go deleted file mode 100644 index 897b3057..00000000 --- a/examples/raylib-ecs/systems/texture-render-spritesheet.go +++ /dev/null @@ -1,56 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ - -package systems - -import ( - rl "github.com/gen2brain/raylib-go/raylib" - "gomp/examples/raylib-ecs/components" - ecs2 "gomp/pkg/ecs" -) - -// TextureRenderSprite is a system that prepares SpriteSheet to be rendered -type trSpriteSheetController struct{} - -func (s *trSpriteSheetController) Init(world *ecs2.World) {} -func (s *trSpriteSheetController) FixedUpdate(world *ecs2.World) {} -func (s *trSpriteSheetController) Update(world *ecs2.World) { - // Get component managers - spriteSheets := components.SpriteSheetService.GetManager(world) - textureRenders := components.TextureRenderService.GetManager(world) - - // Update sprites and spriteRenders - spriteSheets.AllParallel(func(entity ecs2.Entity, spriteSheet *components.SpriteSheet) bool { - if spriteSheet == nil { - return true - } - - tr := textureRenders.Get(entity) - if tr == nil { - // Create new spriteRender - newRender := components.TextureRender{ - Texture: spriteSheet.Texture, - Frame: spriteSheet.Frame, - Origin: spriteSheet.Origin, - Dest: rl.NewRectangle( - 0, - 0, - spriteSheet.Frame.Width, - spriteSheet.Frame.Height, - ), - } - - textureRenders.Create(entity, newRender) - } else { - // Update spriteRender - tr.Texture = spriteSheet.Texture - tr.Frame = spriteSheet.Frame - tr.Origin = spriteSheet.Origin - } - return true - }) -} -func (s *trSpriteSheetController) Destroy(world *ecs2.World) {} diff --git a/examples/raylib-ecs/systems/texture-render-tint.go b/examples/raylib-ecs/systems/texture-render-tint.go deleted file mode 100644 index 48881870..00000000 --- a/examples/raylib-ecs/systems/texture-render-tint.go +++ /dev/null @@ -1,44 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ - -package systems - -import ( - "gomp/examples/raylib-ecs/components" - ecs2 "gomp/pkg/ecs" -) - -// TextureRenderScale is a system that sets Scale of textureRender -type trTintController struct{} - -func (s *trTintController) Init(world *ecs2.World) {} -func (s *trTintController) FixedUpdate(world *ecs2.World) {} -func (s *trTintController) Update(world *ecs2.World) { - // Get component managers - tints := components.TintService.GetManager(world) - textureRenders := components.TextureRenderService.GetManager(world) - - // Update sprites and spriteRenders - textureRenders.AllParallel(func(entity ecs2.Entity, tr *components.TextureRender) bool { - if tr == nil { - return true - } - - tint := tints.Get(entity) - if tint == nil { - return true - } - - trTint := &tr.Tint - trTint.A = tint.A - trTint.R = tint.R - trTint.G = tint.G - trTint.B = tint.B - - return true - }) -} -func (s *trTintController) Destroy(world *ecs2.World) {} diff --git a/go.mod b/go.mod index ac59a682..a9180438 100644 --- a/go.mod +++ b/go.mod @@ -1,61 +1,77 @@ module gomp -go 1.23.5 +go 1.24 replace github.com/ugorji/go v1.1.4 => github.com/ugorji/go v1.1.7 require ( + github.com/Zyko0/go-sdl3 v0.0.0-20250324113244-771f317184f7 + github.com/bytecodealliance/wasmtime-go/v32 v32.0.0 github.com/coder/websocket v1.8.12 - github.com/gen2brain/raylib-go/raylib v0.0.0-20250109172833-6dbba4f81a9b - github.com/hajimehoshi/ebiten/v2 v2.7.10 - github.com/jakecoffman/cp/v2 v2.0.2 - github.com/labstack/echo-contrib v0.17.1 + github.com/fasthttp/websocket v1.5.8 + github.com/felixge/fgprof v0.9.5 + github.com/gen2brain/raylib-go/raylib v0.0.0-20250215042252-db8e47f0e5c5 + github.com/gofiber/contrib/websocket v1.3.4 + github.com/gofiber/fiber/v2 v2.52.6 + github.com/hajimehoshi/ebiten/v2 v2.8.6 + github.com/hajimehoshi/go-steamworks v0.0.0-20241112125913-96b2a6baef69 + github.com/jakecoffman/cp/v2 v2.1.0 + github.com/jfreymuth/go-sdl3 v0.1.3-0.20250226211328-622f8250e21c + github.com/jupiterrider/purego-sdl3 v0.0.0-20250223121749-61a56748f345 + github.com/labstack/echo-contrib v0.17.2 github.com/labstack/gommon v0.4.2 github.com/negrel/assert v0.5.0 github.com/quasilyte/ebitengine-input v0.9.1 github.com/quic-go/quic-go v0.49.0 github.com/sevenNt/echo-pprof v0.1.1-0.20230131020615-4dd36891e14b github.com/stretchr/testify v1.10.0 - github.com/yohamta/donburi v1.15.4 - golang.org/x/time v0.5.0 + github.com/tetratelabs/wazero v1.9.0 + github.com/veandco/go-sdl2 v0.4.40 + github.com/yohamta/donburi v1.15.7 + golang.org/x/time v0.10.0 ) require ( + github.com/Zyko0/purego-gen v0.0.0-20250308152853-097c3ba1e28a // indirect + github.com/andybalholm/brotli v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/francoispqt/gojay v1.2.13 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect - github.com/golang-jwt/jwt v3.2.2+incompatible // indirect - github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect + github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/gorilla/context v1.1.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/onsi/ginkgo/v2 v2.9.5 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/quasilyte/gmath v0.0.0-20221217210116-fba37a2e15c7 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.62.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect go.uber.org/mock v0.5.0 // indirect golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 // indirect - golang.org/x/mod v0.18.0 // indirect - golang.org/x/tools v0.22.0 // indirect - gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect + golang.org/x/mod v0.21.0 // indirect + golang.org/x/tools v0.25.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) require ( - github.com/ebitengine/gomobile v0.0.0-20240518074828-e86332849895 // indirect + github.com/ebitengine/gomobile v0.0.0-20240911145611-4856209ac325 // indirect github.com/ebitengine/hideconsole v1.0.0 // indirect - github.com/ebitengine/purego v0.7.1 // indirect + github.com/ebitengine/purego v0.9.0-alpha.2.0.20250124174847-29f0104e3c2b // indirect github.com/gorilla/sessions v1.4.0 github.com/gorilla/websocket v1.5.3 github.com/jezek/xgb v1.1.1 // indirect - github.com/labstack/echo/v4 v4.12.0 + github.com/labstack/echo/v4 v4.13.3 github.com/mattn/go-isatty v0.0.20 // indirect - golang.org/x/crypto v0.26.0 // indirect - golang.org/x/net v0.28.0 // indirect - golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.23.0 // indirect - golang.org/x/text v0.17.0 - google.golang.org/protobuf v1.34.1 + golang.org/x/crypto v0.38.0 // indirect + golang.org/x/net v0.40.0 // indirect + golang.org/x/sync v0.14.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.25.0 + google.golang.org/protobuf v1.36.1 ) diff --git a/go.sum b/go.sum index f14a7756..05830514 100644 --- a/go.sum +++ b/go.sum @@ -8,13 +8,24 @@ dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1 dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Zyko0/go-sdl3 v0.0.0-20250324113244-771f317184f7 h1:jlrgDnYn7sUJqiKp5n6t2t9IwJt427DN6zoallnRJMU= +github.com/Zyko0/go-sdl3 v0.0.0-20250324113244-771f317184f7/go.mod h1:y9Xormjz/GWcAeGbl+Z2zOaKtKfeeZ+5x8UiojyFFdg= +github.com/Zyko0/purego-gen v0.0.0-20250308152853-097c3ba1e28a h1:N5RnQ3/z1/HjCivGNfRbID/3QreOQ6Dulr3FAOgULqc= +github.com/Zyko0/purego-gen v0.0.0-20250308152853-097c3ba1e28a/go.mod h1:yJn8avSZmgophSwHMUDhlEIzdkB9uUTEctszmQmjxDo= +github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= +github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/bytecodealliance/wasmtime-go/v32 v32.0.0 h1:rGZFaIExj4h5EwehU+rnALJSq+OOvDEcutiyH+qSAzo= +github.com/bytecodealliance/wasmtime-go/v32 v32.0.0/go.mod h1:JRtCAOIPwpAESq6DD3L11RFyTDYQnQf4UgsbOwbKvpU= +github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= +github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs= +github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= @@ -23,18 +34,22 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/ebitengine/gomobile v0.0.0-20240518074828-e86332849895 h1:48bCqKTuD7Z0UovDfvpCn7wZ0GUZ+yosIteNDthn3FU= -github.com/ebitengine/gomobile v0.0.0-20240518074828-e86332849895/go.mod h1:XZdLv05c5hOZm3fM2NlJ92FyEZjnslcMcNRrhxs8+8M= +github.com/ebitengine/gomobile v0.0.0-20240911145611-4856209ac325 h1:Gk1XUEttOk0/hb6Tq3WkmutWa0ZLhNn/6fc6XZpM7tM= +github.com/ebitengine/gomobile v0.0.0-20240911145611-4856209ac325/go.mod h1:ulhSQcbPioQrallSuIzF8l1NKQoD7xmMZc5NxzibUMY= github.com/ebitengine/hideconsole v1.0.0 h1:5J4U0kXF+pv/DhiXt5/lTz0eO5ogJ1iXb8Yj1yReDqE= github.com/ebitengine/hideconsole v1.0.0/go.mod h1:hTTBTvVYWKBuxPr7peweneWdkUwEuHuB3C1R/ielR1A= -github.com/ebitengine/purego v0.7.1 h1:6/55d26lG3o9VCZX8lping+bZcmShseiqlh2bnUDiPA= -github.com/ebitengine/purego v0.7.1/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= +github.com/ebitengine/purego v0.9.0-alpha.2.0.20250124174847-29f0104e3c2b h1:/KAOJuXR4cWaQIiA9xBMDSQJ1JXq5gZHdSK8prrtUqQ= +github.com/ebitengine/purego v0.9.0-alpha.2.0.20250124174847-29f0104e3c2b/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/fasthttp/websocket v1.5.8 h1:k5DpirKkftIF/w1R8ZzjSgARJrs54Je9YJK37DL/Ah8= +github.com/fasthttp/websocket v1.5.8/go.mod h1:d08g8WaT6nnyvg9uMm8K9zMYyDjfKyj3170AtPRuVU0= +github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY= +github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/gen2brain/raylib-go/raylib v0.0.0-20250109172833-6dbba4f81a9b h1:JJfspevP3YOXcSKVABizYOv++yMpTJIdPUtoDzF/RWw= -github.com/gen2brain/raylib-go/raylib v0.0.0-20250109172833-6dbba4f81a9b/go.mod h1:BaY76bZk7nw1/kVOSQObPY1v1iwVE1KHAGMfvI6oK1Q= +github.com/gen2brain/raylib-go/raylib v0.0.0-20250215042252-db8e47f0e5c5 h1:k8ZAxLgb/p5TvCi5VHFHM8JdnjwShNK4A0bLIwbktAU= +github.com/gen2brain/raylib-go/raylib v0.0.0-20250215042252-db8e47f0e5c5/go.mod h1:BaY76bZk7nw1/kVOSQObPY1v1iwVE1KHAGMfvI6oK1Q= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= @@ -42,9 +57,14 @@ github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= +github.com/gofiber/contrib/websocket v1.3.4 h1:tWeBdbJ8q0WFQXariLN4dBIbGH9KBU75s0s7YXplOSg= +github.com/gofiber/contrib/websocket v1.3.4/go.mod h1:kTFBPC6YENCnKfKx0BoOFjgXxdz7E85/STdkmZPEmPs= +github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI= +github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= -github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= @@ -63,8 +83,10 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 h1:y3N7Bm7Y9/CtpiVkw/ZWj6lSlDF3F74SfKwfTCer72Q= +github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= @@ -78,35 +100,47 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= -github.com/hajimehoshi/ebiten/v2 v2.7.10 h1:fsVukQdPDUlalSSpFkuszTy0cK2DL0fxFoSnTVdlmAM= -github.com/hajimehoshi/ebiten/v2 v2.7.10/go.mod h1:Ulbq5xDmdx47P24EJ+Mb31Zps7vQq+guieG9mghQUaA= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/jakecoffman/cp/v2 v2.0.2 h1:HN+youpOhd8xgWYw5amqiJFLoreAIB/uI/EEzZohLjA= -github.com/jakecoffman/cp/v2 v2.0.2/go.mod h1:Q0hFU7Kk6PMw4dwgFtvBC6O4KTm7ewiLuHrXtHMicyU= +github.com/hajimehoshi/ebiten/v2 v2.8.6 h1:Dkd/sYI0TYyZRCE7GVxV59XC+WCi2BbGAbIBjXeVC1U= +github.com/hajimehoshi/ebiten/v2 v2.8.6/go.mod h1:cCQ3np7rdmaJa1ZnvslraVlpxNb3wCjEnAP1LHNyXNA= +github.com/hajimehoshi/go-steamworks v0.0.0-20241112125913-96b2a6baef69 h1:R5DmT3Ffuccaf3U7DiLYpro2avyBz7D112v9eqm8NvE= +github.com/hajimehoshi/go-steamworks v0.0.0-20241112125913-96b2a6baef69/go.mod h1:xQbwn4VSK2CwjfAgpolFH8MYSu96NQZhiOuksu1vvdY= +github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= +github.com/jakecoffman/cp/v2 v2.1.0 h1:s0almZ7zDZs9JY35ciUgCoVKTMmdPkokF1dxHg226Wo= +github.com/jakecoffman/cp/v2 v2.1.0/go.mod h1:Q0hFU7Kk6PMw4dwgFtvBC6O4KTm7ewiLuHrXtHMicyU= github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4= github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= +github.com/jfreymuth/go-sdl3 v0.1.3-0.20250226211328-622f8250e21c h1:c7cKhwG6vsLRR2HHpAdhVX9KAD+wlB3WfF2cnZ3q4Es= +github.com/jfreymuth/go-sdl3 v0.1.3-0.20250226211328-622f8250e21c/go.mod h1:37mPz4b0UCp9QTMrMiKfhq2mDofqBW7ccklWSKH9YL0= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jupiterrider/purego-sdl3 v0.0.0-20250223121749-61a56748f345 h1:t3LCgVzMRS2q7fL4T9pgshIUQuqBQD+ERMKUJNgL+Qo= +github.com/jupiterrider/purego-sdl3 v0.0.0-20250223121749-61a56748f345/go.mod h1:bUv1BcdO0uvGAb0mT5PDSInh67TlbzXBBL4/vVwcEEs= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/labstack/echo-contrib v0.17.1 h1:7I/he7ylVKsDUieaGRZ9XxxTYOjfQwVzHzUYrNykfCU= -github.com/labstack/echo-contrib v0.17.1/go.mod h1:SnsCZtwHBAZm5uBSAtQtXQHI3wqEA73hvTn0bYMKnZA= -github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0= -github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM= +github.com/labstack/echo-contrib v0.17.2 h1:K1zivqmtcC70X9VdBFdLomjPDEVHlrcAObqmuFj1c6w= +github.com/labstack/echo-contrib v0.17.2/go.mod h1:NeDh3PX7j/u+jR4iuDt1zHmWZSCz9c/p9mxXcDpyS8E= +github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= +github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -122,6 +156,7 @@ github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3Ro github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= +github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -135,7 +170,12 @@ github.com/quasilyte/gmath v0.0.0-20221217210116-fba37a2e15c7 h1:mvIS9aGirkzuYmH github.com/quasilyte/gmath v0.0.0-20221217210116-fba37a2e15c7/go.mod h1:EbI+KMbALSVE2s0YFOQpR4uj66zBh9ter5P4CBMSuvA= github.com/quic-go/quic-go v0.49.0 h1:w5iJHXwHxs1QxyBv1EHKuC50GX5to8mJAxvtnttJp94= github.com/quic-go/quic-go v0.49.0/go.mod h1:s2wDnmCdooUQBmQfpUSTCYBl1/D4FcqbULMMkASvR6s= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511 h1:KanIMPX0QdEdB4R3CiimCAbxFrhB3j7h0/OvpYGVQa8= +github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sevenNt/echo-pprof v0.1.1-0.20230131020615-4dd36891e14b h1:IXGKwQZ6+llGbDFyTJvBXWGTkfrAqsbYwtVVm+Ax4WU= github.com/sevenNt/echo-pprof v0.1.1-0.20230131020615-4dd36891e14b/go.mod h1:ArUb+H7+Tew7UUjK6x2xiAqFrznLrANIfz9M6m66J0c= @@ -164,19 +204,30 @@ github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5k github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= +github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.62.0 h1:8dKRBX/y2rCzyc6903Zu1+3qN0H/d2MsxPPmVNamiH0= +github.com/valyala/fasthttp v1.62.0/go.mod h1:FCINgr4GKdKqV8Q0xv8b+UxPV+H/O5nNFo3D+r54Htg= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/veandco/go-sdl2 v0.4.40 h1:fZv6wC3zz1Xt167P09gazawnpa0KY5LM7JAvKpX9d/U= +github.com/veandco/go-sdl2 v0.4.40/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofep6SNiAjY= github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= -github.com/yohamta/donburi v1.15.4 h1:R3nWrYgnhyDBn3NjtEjHMfxBdtq2m1/IiEjgdDW9vqc= -github.com/yohamta/donburi v1.15.4/go.mod h1:FdjU9hpwAsAs1qRvqsSTJimPJ0dipvdnr9hMJXYc1Rk= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/yohamta/donburi v1.15.7 h1:so/vHf1L133d0SFVrCUzMMueh2ko39wRkrcpNLdzvz8= +github.com/yohamta/donburi v1.15.7/go.mod h1:FdjU9hpwAsAs1qRvqsSTJimPJ0dipvdnr9hMJXYc1Rk= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= @@ -185,18 +236,18 @@ golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+ golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 h1:LoYXNGAShUG3m/ehNk4iFctuhGX/+R1ZpfJ4/ia80JM= golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= -golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= -golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= +golang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw= +golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= -golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -205,8 +256,8 @@ golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -216,32 +267,31 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= -golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= +golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= -golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= +golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= @@ -258,8 +308,8 @@ google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmE google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= +google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/bench/aabb_test.go b/internal/bench/aabb_test.go new file mode 100644 index 00000000..6b7720a2 --- /dev/null +++ b/internal/bench/aabb_test.go @@ -0,0 +1,176 @@ +package bench + +import ( + "math" + "math/rand" + "testing" +) + +const ( + numCircles = 10000 + numAABBs = 10000 +) + +// Circle represents a circle with center (x, y) and radius r +type Circle struct { + X, Y, R float64 +} + +// AABB represents an axis-aligned bounding box +type AABB struct { + XMin, YMin, XMax, YMax float64 +} + +// circleCircleIntersect1 checks intersection using distance-based method +func circleCircleIntersect1(c1, c2 Circle) bool { + dx := c1.X - c2.X + dy := c1.Y - c2.Y + distance := math.Sqrt(dx*dx + dy*dy) + return distance < c1.R+c2.R +} + +// circleCircleIntersect2 checks intersection using squared distance +func circleCircleIntersect2(c1, c2 Circle) bool { + dx := c1.X - c2.X + dy := c1.Y - c2.Y + rSum := c1.R + c2.R + return dx*dx+dy*dy < rSum*rSum +} + +// circleAABBIntersect checks if circle intersects with AABB +func circleAABBIntersect(c Circle, a AABB) bool { + // Find closest point on AABB to circle center + closestX := math.Max(a.XMin, math.Min(c.X, a.XMax)) + closestY := math.Max(a.YMin, math.Min(c.Y, a.YMax)) + + // Calculate distance between closest point and circle center + dx := c.X - closestX + dy := c.Y - closestY + distance := math.Sqrt(dx*dx + dy*dy) + + return distance < c.R +} + +// aabbIntersect checks if two AABBs intersect +func aabbIntersect(a, b AABB) bool { + // Check if one box is to the left of the other + return a.XMax >= b.XMin && a.XMin <= b.XMax && a.YMax >= b.YMin && a.YMin <= b.YMax +} + +func aabbIntersect2(a, b AABB) bool { + // Check if one box is to the left of the other + if a.XMax < b.XMin || b.XMax < a.XMin { + return false + } + + return !(a.YMax < b.YMin || b.YMax < a.YMin) +} + +// generateRandomCircle generates a random circle +func generateRandomCircle() Circle { + return Circle{ + X: rand.Float64() * 100, + Y: rand.Float64() * 100, + R: rand.Float64()*10 + 5, + } +} + +// generateRandomAABB generates a random AABB +func generateRandomAABB() AABB { + x1 := rand.Float64() * 100 + y1 := rand.Float64() * 100 + w := rand.Float64()*20 + 10 + h := rand.Float64()*20 + 10 + return AABB{ + XMin: x1, + YMin: y1, + XMax: x1 + w, + YMax: y1 + h, + } +} + +func BenchmarkCircleCircleIntersect1(b *testing.B) { + rand.Seed(42) + circles := make([]Circle, numCircles) + for i := range circles { + circles[i] = generateRandomCircle() + } + + for b.Loop() { + lastIndex := len(circles) + for j := 0; j < lastIndex; j++ { + for k := j + 1; k < lastIndex; k++ { + circleCircleIntersect1(circles[j], circles[k]) + } + } + } +} + +func BenchmarkCircleCircleIntersect2(b *testing.B) { + rand.Seed(42) + circles := make([]Circle, numCircles) + for i := range circles { + circles[i] = generateRandomCircle() + } + + for b.Loop() { + lastIndex := len(circles) + for j := 0; j < lastIndex; j++ { + for k := j + 1; k < lastIndex; k++ { + circleCircleIntersect2(circles[j], circles[k]) + } + } + } +} + +// +//func BenchmarkCircleAABBIntersect(b *testing.B) { +// rand.Seed(42) +// circles := make([]Circle, numCircles) +// aabbs := make([]AABB, numAABBs) +// for i := range circles { +// circles[i] = generateRandomCircle() +// aabbs[i] = generateRandomAABB() +// } +// +// b.ResetTimer() +// for i := 0; i < b.N; i++ { +// for j := 0; j < len(aabbs); j++ { +// circleAABBIntersect(circles[j], aabbs[j]) +// } +// } +//} + +func BenchmarkAABBIntersect(b *testing.B) { + rand.Seed(42) + aabbs := make([]AABB, numAABBs) + for i := range aabbs { + aabbs[i] = generateRandomAABB() + } + + for b.Loop() { + lastIndex := len(aabbs) + for j := 0; j < lastIndex; j++ { + for k := j + 1; k < lastIndex; k++ { + aabbIntersect(aabbs[j], aabbs[k]) + } + } + } +} + +func BenchmarkAABBIntersect2(b *testing.B) { + rand.Seed(42) + aabbs := make([]AABB, numAABBs) + for i := range aabbs { + aabbs[i] = generateRandomAABB() + } + + for b.Loop() { + lastIndex := len(aabbs) + for j := 0; j < lastIndex; j++ { + for k := j + 1; k < lastIndex; k++ { + aabbIntersect2(aabbs[j], aabbs[k]) + } + } + } +} diff --git a/internal/cgo/cgo-hello.go b/internal/cgo/cgo-hello.go new file mode 100644 index 00000000..37b9bbe8 --- /dev/null +++ b/internal/cgo/cgo-hello.go @@ -0,0 +1,27 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package main + +/* +double add(double a, double b) { + return a + b; +} +*/ +import "C" +import "fmt" + +func main() { + fmt.Println(C.add(1, 2)) +} diff --git a/internal/ebiten/main.go b/internal/ebiten/main.go new file mode 100644 index 00000000..54e2a09d --- /dev/null +++ b/internal/ebiten/main.go @@ -0,0 +1,104 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package main + +import ( + "fmt" + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/ebitenutil" + "github.com/hajimehoshi/ebiten/v2/vector" + "image/color" + "log" + "time" +) + +const ( + screenWidth = 1280 + screenHeight = 720 + rectCounter = 1 << 18 + frameCount = 100 +) + +type Rect struct { + X float32 + Y float32 + W float32 + H float32 +} + +type Game struct { + rects []Rect + frameTimes []float64 + updatedAt time.Time + dt time.Duration + fps float64 + avgFps float64 +} + +func (g *Game) Update() error { + g.dt = time.Since(g.updatedAt) + g.updatedAt = time.Now() + g.fps = 1.0 / g.dt.Seconds() + + g.frameTimes = append(g.frameTimes, g.fps) + if len(g.frameTimes) > frameCount { + g.frameTimes = g.frameTimes[1:] + } + + var totalFps float64 + for _, frameTime := range g.frameTimes { + totalFps += frameTime + } + g.avgFps = totalFps / float64(len(g.frameTimes)) + + if ebiten.IsKeyPressed(ebiten.KeyEscape) { + return fmt.Errorf("quit") + } + + return nil +} + +func (g *Game) Draw(screen *ebiten.Image) { + screen.Fill(color.Black) + + for _, rect := range g.rects { + vector.DrawFilledRect(screen, rect.X, rect.Y, rect.W, rect.H, color.White, false) + } + + ebitenutil.DebugPrint(screen, fmt.Sprintf("FPS: %0.2f\nAvg FPS: %0.2f\nDT: %s", g.fps, g.avgFps, g.dt)) +} + +func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) { + return screenWidth, screenHeight +} + +func main() { + game := &Game{ + rects: make([]Rect, rectCounter), + frameTimes: make([]float64, 0, frameCount), + updatedAt: time.Now(), + } + + for i := range game.rects { + game.rects[i] = Rect{X: 300, Y: 300, W: 10, H: 10} + } + + ebiten.SetWindowSize(screenWidth, screenHeight) + ebiten.SetWindowTitle("Hello, World!") + + if err := ebiten.RunGame(game); err != nil && err.Error() != "quit" { + log.Fatal(err) + } +} diff --git a/internal/geometry/main.go b/internal/geometry/main.go new file mode 100644 index 00000000..969ca3ad --- /dev/null +++ b/internal/geometry/main.go @@ -0,0 +1,121 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package main + +import ( + "fmt" +) + +// Point представляет точку с координатами X и Y. +type Point struct { + X, Y float64 +} + +// Polygon представляет полигон как набор точек. +type Polygon []Point + +// Projection представляет проекцию полигона на ось. +type Projection struct { + Min, Max float64 +} + +// Main функция для проверки пересечения двух полигонов. +func PolygonsIntersect(p1, p2 Polygon) bool { + // Получаем все оси для проверки + axes := getAxes(p1) + axes = append(axes, getAxes(p2)...) + + // Проверяем каждую ось + for _, axis := range axes { + // Проецируем оба полигона на ось + proj1 := project(p1, axis) + proj2 := project(p2, axis) + + // Проверяем, перекрываются ли проекции + if !overlap(proj1, proj2) { + return false + } + } + + return true +} + +// getAxes возвращает все нормали к ребрам полигона. +func getAxes(p Polygon) []Point { + axes := make([]Point, 0, len(p)) + for i := 0; i < len(p); i++ { + p1 := p[i] + p2 := p[(i+1)%len(p)] // Следующая точка (замыкаем полигон) + + // Вектор ребра + edge := Point{p2.X - p1.X, p2.Y - p1.Y} + + // Нормаль к ребру (перпендикулярный вектор) + normal := Point{-edge.Y, edge.X} + axes = append(axes, normal) + } + return axes +} + +// project проецирует полигон на ось и возвращает проекцию. +func project(p Polygon, axis Point) Projection { + min := dot(p[0], axis) + max := min + + for _, point := range p { + proj := dot(point, axis) + if proj < min { + min = proj + } + if proj > max { + max = proj + } + } + + return Projection{Min: min, Max: max} +} + +// dot возвращает скалярное произведение двух точек. +func dot(p1, p2 Point) float64 { + return p1.X*p2.X + p1.Y*p2.Y +} + +// overlap проверяет, перекрываются ли две проекции. +func overlap(proj1, proj2 Projection) bool { + return proj1.Min <= proj2.Max && proj2.Min <= proj1.Max +} + +func main() { + // Пример двух полигонов (выпуклых) + polygon1 := Polygon{ + {0, 0}, + {4, 0}, + {4, 4}, + {0, 4}, + } + polygon2 := Polygon{ + {2, 2}, + {6, 2}, + {6, 6}, + {2, 6}, + } + + // Проверяем пересечение + if PolygonsIntersect(polygon1, polygon2) { + fmt.Println("Полигоны пересекаются.") + } else { + fmt.Println("Полигоны не пересекаются.") + } +} diff --git a/internal/raylib/main.go b/internal/raylib/main.go new file mode 100644 index 00000000..6588cbed --- /dev/null +++ b/internal/raylib/main.go @@ -0,0 +1,101 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package main + +import ( + "fmt" + "log" + "os" + "runtime" + "runtime/pprof" + "time" + + "github.com/gen2brain/raylib-go/raylib" +) + +const ( + screenWidth = 1280 + screenHeight = 720 + batchSize = 1 << 14 + rectCounter = 1 << 18 + framerate = 600 + frameCount = 100 // Number of frames to average +) + +func main() { + f, err := os.Create("cpu.out") + if err != nil { + log.Fatal(err) + } + defer f.Close() + + err = pprof.StartCPUProfile(f) + if err != nil { + log.Fatal(err) + } + defer pprof.StopCPUProfile() + + fmt.Println("CPU Profile Started") + defer fmt.Println("CPU Profile Stopped") + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + rl.InitWindow(screenWidth, screenHeight, "raylib example") + defer rl.CloseWindow() + + rl.SetTargetFPS(framerate) + + rects := make([]rl.Rectangle, rectCounter) + for i := range rects { + rects[i] = rl.Rectangle{X: 300, Y: 300, Width: 10, Height: 10} + } + + var dt time.Duration = time.Second + var fps, avgFps float64 + updatedAt := time.Now() + + frameTimes := make([]float64, 0, frameCount) + + for !rl.WindowShouldClose() { + fps = float64(time.Second) / float64(dt) + dt = time.Since(updatedAt) + updatedAt = time.Now() + + frameTimes = append(frameTimes, fps) + if len(frameTimes) > frameCount { + frameTimes = frameTimes[1:] + } + + var totalFps float64 + for _, frameTime := range frameTimes { + totalFps += frameTime + } + avgFps = totalFps / float64(len(frameTimes)) + + rl.BeginDrawing() + rl.ClearBackground(rl.Black) + + rl.DrawText(fmt.Sprintf("FPS %f", fps), 10, 10, 20, rl.White) + rl.DrawText(fmt.Sprintf("Avg FPS %f", avgFps), 10, 30, 20, rl.White) + rl.DrawText(fmt.Sprintf("dt %s", dt.String()), 10, 50, 20, rl.White) + + for _, rect := range rects { + rl.DrawRectangleRec(rect, rl.White) + } + + rl.EndDrawing() + } +} diff --git a/internal/sdl2/sdl-game.go b/internal/sdl2/sdl-game.go new file mode 100644 index 00000000..a9dd98df --- /dev/null +++ b/internal/sdl2/sdl-game.go @@ -0,0 +1,185 @@ +package main + +import ( + "fmt" + "github.com/veandco/go-sdl2/sdl" + "github.com/veandco/go-sdl2/ttf" + "log" + "os" + "runtime" + "runtime/pprof" + "time" +) + +const batchSize = 1 << 14 +const rectCounter = 1 << 18 +const framerate = 600 +const frameCount = 100 // Number of frames to average + +func main() { + f, err := os.Create("cpu.out") + if err != nil { + log.Fatal(err) + } + defer f.Close() + + err = pprof.StartCPUProfile(f) + if err != nil { + log.Fatal(err) + } + defer pprof.StopCPUProfile() + + fmt.Println("CPU Profile Started") + defer fmt.Println("CPU Profile Stopped") + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + ok := sdl.SetHint(sdl.HINT_RENDER_DRIVER, "gpu,software") + if !ok { + panic(sdl.GetError()) + } + must(ttf.Init()) + defer ttf.Quit() + must(sdl.Init(sdl.INIT_EVERYTHING)) + defer sdl.Quit() + + window, renderer, err := sdl.CreateWindowAndRenderer(1280, 720, sdl.WINDOW_RESIZABLE) + must(err) + defer window.Destroy() + defer renderer.Destroy() + + font, err := ttf.OpenFont("Roboto-SemiBold.ttf", 14) + must(err) + defer font.Close() + + var renderTicker *time.Ticker + if framerate > 0 { + renderTicker = time.NewTicker(time.Second / time.Duration(framerate)) + defer renderTicker.Stop() + } + + var dt time.Duration = time.Second + var fps, avgFps float64 + updatedAt := time.Now() + + rects := make([]sdl.FRect, rectCounter) + for i := range rects { + rects[i] = sdl.FRect{X: 300, Y: 300, W: 10, H: 10} + } + + frameTimes := make([]float64, 0, frameCount) + +Outer: + for { + if renderTicker != nil { + <-renderTicker.C + } + fps = time.Second.Seconds() / dt.Seconds() + dt = time.Since(updatedAt) + updatedAt = time.Now() + + frameTimes = append(frameTimes, fps) + if len(frameTimes) > frameCount { + frameTimes = frameTimes[1:] + } + + var totalFps float64 + for _, frameTime := range frameTimes { + totalFps += frameTime + } + avgFps = totalFps / float64(len(frameTimes)) + + for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() { + switch event.(type) { + case *sdl.QuitEvent: + break Outer + case *sdl.KeyboardEvent: + if event.(*sdl.KeyboardEvent).Keysym.Scancode == sdl.SCANCODE_ESCAPE { + break Outer + } + } + } + + must(renderer.SetDrawColor(0, 0, 0, 255)) + must(renderer.Clear()) + + must(renderer.SetDrawColor(255, 255, 255, 255)) + + for i := 0; i < len(rects); i += batchSize { + must(renderer.FillRectsF(rects[i : i+batchSize])) + } + + must(drawText(renderer, font, 10, 10, fmt.Sprintf("FPS %f", fps))) + must(drawText(renderer, font, 10, 30, fmt.Sprintf("Avg FPS %f", avgFps))) + must(drawText(renderer, font, 10, 50, fmt.Sprintf("dt %s", dt.String()))) + + renderer.Present() + } +} + +func must(err error) { + if err != nil { + println(err.Error()) + panic(err) + } +} + +// Cache all char textures on the fly +// TODO: preload all chars initially (maybe +1FPS) +var letterCache [256]*sdl.Texture + +func drawText(renderer *sdl.Renderer, font *ttf.Font, x, y int32, text string) error { + var totalWidth int32 + var maxHeight int32 + + for _, char := range text { + if char < 256 { + if texture := letterCache[char]; texture != nil { + _, _, w, h, err := texture.Query() + if err != nil { + return err + } + dst := sdl.Rect{X: x + totalWidth, Y: y, W: w, H: h} + if err := renderer.Copy(texture, nil, &dst); err != nil { + return err + } + totalWidth += w + if h > maxHeight { + maxHeight = h + } + continue + } + } + + surface, err := font.RenderUTF8Blended(string(char), sdl.Color{R: 255, G: 255, B: 255, A: 255}) + if err != nil { + return err + } + defer surface.Free() + + texture, err := renderer.CreateTextureFromSurface(surface) + if err != nil { + return err + } + if char < 256 { + letterCache[char] = texture + } + + _, _, w, h, err := texture.Query() + if err != nil { + return err + } + + dst := sdl.Rect{X: x + totalWidth, Y: y, W: w, H: h} + if err := renderer.Copy(texture, nil, &dst); err != nil { + return err + } + totalWidth += w + if h > maxHeight { + maxHeight = h + } + } + + return nil +} diff --git a/internal/sdl3-cgo/sdl3cgo-game.go b/internal/sdl3-cgo/sdl3cgo-game.go new file mode 100644 index 00000000..e8bce7e9 --- /dev/null +++ b/internal/sdl3-cgo/sdl3cgo-game.go @@ -0,0 +1,121 @@ +package main + +import "C" +import ( + "fmt" + "github.com/jfreymuth/go-sdl3/sdl" + "log" + "os" + "runtime" + "runtime/pprof" + "time" +) + +const batchSize = 1 << 14 +const rectCounter = 1 << 18 +const framerate = 600 +const frameCount = 100 // Number of frames to average + +func main() { + f, err := os.Create("cpu.out") + if err != nil { + log.Fatal(err) + } + defer f.Close() + + err = pprof.StartCPUProfile(f) + if err != nil { + log.Fatal(err) + } + defer pprof.StopCPUProfile() + + fmt.Println("CPU Profile Started") + defer fmt.Println("CPU Profile Stopped") + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + sdl.SetHint(sdl.HintRenderDriver, "gpu,software") + must(sdl.Init(sdl.InitVideo)) + defer sdl.Quit() + + w, r, e := sdl.CreateWindowAndRenderer("sld c-go", 1280, 720, sdl.WindowResizable) + must(e) + defer w.Destroy() + defer r.Destroy() + + n, err := r.Name() + fmt.Println(n, sdl.GetRenderDriver(0)) + + var renderTicker *time.Ticker + if framerate > 0 { + renderTicker = time.NewTicker(time.Second / time.Duration(framerate)) + defer renderTicker.Stop() + } + + var dt time.Duration = time.Second + var fps, avgFps float64 + updatedAt := time.Now() + + rects := make([]sdl.FRect, rectCounter) + for i := range rects { + rects[i] = sdl.FRect{X: 300, Y: 300, W: 10, H: 10} + } + + frameTimes := make([]float64, 0, frameCount) + +Outer: + for { + if renderTicker != nil { + <-renderTicker.C + } + fps = time.Second.Seconds() / dt.Seconds() + dt = time.Since(updatedAt) + updatedAt = time.Now() + + frameTimes = append(frameTimes, fps) + if len(frameTimes) > frameCount { + frameTimes = frameTimes[1:] + } + + var totalFps float64 + for _, frameTime := range frameTimes { + totalFps += frameTime + } + avgFps = totalFps / float64(len(frameTimes)) + + var event sdl.Event + for sdl.PollEvent(&event) { + switch event.Type() { + case sdl.EventQuit: + break Outer + case sdl.EventKeyDown: + if event.Keyboard().Scancode == sdl.ScancodeEscape { + break Outer + } + } + } + + must(r.SetDrawColor(0, 0, 0, 255)) + must(r.Clear()) + + must(r.SetDrawColor(255, 255, 255, 255)) + + for i := 0; i < len(rects); i += batchSize { + must(r.FillRects(rects[i : i+batchSize])) + } + + must(r.DebugText(10, 10, fmt.Sprintf("FPS %f", fps))) + must(r.DebugText(10, 20, fmt.Sprintf("Avg FPS %f", avgFps))) + must(r.DebugText(10, 30, fmt.Sprintf("dt %s", dt.String()))) + + must(r.Present()) + } +} + +func must(err error) { + if err != nil { + println(err.Error()) + panic(err) + } +} diff --git a/internal/sdl3-pure-zukko/sdl-zukko.go b/internal/sdl3-pure-zukko/sdl-zukko.go new file mode 100644 index 00000000..a86c26a7 --- /dev/null +++ b/internal/sdl3-pure-zukko/sdl-zukko.go @@ -0,0 +1,139 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package main + +import ( + "fmt" + "github.com/Zyko0/go-sdl3/bin/binsdl" + "github.com/Zyko0/go-sdl3/sdl" + "log" + "os" + "runtime" + "runtime/pprof" + "time" +) + +const batchSize = 1 << 14 +const rectCounter = 1 << 18 +const framerate = 600 +const frameCount = 100 // Number of frames to average + +func main() { + f, err := os.Create("cpu.out") + if err != nil { + log.Fatal(err) + } + defer f.Close() + + err = pprof.StartCPUProfile(f) + if err != nil { + log.Fatal(err) + } + defer pprof.StopCPUProfile() + + fmt.Println("CPU Profile Started") + defer fmt.Println("CPU Profile Stopped") + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + defer binsdl.Load().Unload() // sdl.LoadLibrary(sdl.Path()) + + sdl.SetHint(sdl.HINT_RENDER_DRIVER, "gpu,software") // <- gpu driver creates memory leak + if err := sdl.Init(sdl.INIT_VIDEO); err != nil { + panic(err.Error()) + } + defer sdl.Quit() + + //var w *sdl.Window + //var r *sdl.Renderer + w, r, err := sdl.CreateWindowAndRenderer("Hello, World!", 1280, 720, sdl.WINDOW_RESIZABLE) + if err != nil { + panic(err.Error()) + } + defer r.Destroy() + defer w.Destroy() + + var dt time.Duration = time.Second + var fps, avgFps float64 + updatedAt := time.Now() + + var renderTicker *time.Ticker + if framerate > 0 { + renderTicker = time.NewTicker(time.Second / time.Duration(framerate)) + defer renderTicker.Stop() + } + + rects := make([]sdl.FRect, rectCounter) + for i := range rects { + rects[i] = sdl.FRect{X: 300, Y: 300, W: 10, H: 10} + } + + fmt.Println(sdl.GetRenderDriver(0)) + frameTimes := make([]float64, 0, frameCount) + +Outer: + for { + if renderTicker != nil { + <-renderTicker.C + } + fps = time.Second.Seconds() / dt.Seconds() + dt = time.Since(updatedAt) + updatedAt = time.Now() + + frameTimes = append(frameTimes, fps) + if len(frameTimes) > frameCount { + frameTimes = frameTimes[1:] + } + + var totalFps float64 + for _, frameTime := range frameTimes { + totalFps += frameTime + } + avgFps = totalFps / float64(len(frameTimes)) + + var event sdl.Event + for sdl.PollEvent(&event) { + switch event.Type { + case sdl.EVENT_QUIT: + break Outer + case sdl.EVENT_KEY_DOWN: + if event.KeyboardEvent().Scancode == sdl.SCANCODE_ESCAPE { + break Outer + } + } + } + r.SetDrawColor(0, 0, 0, 255) + r.Clear() + + r.SetDrawColor(255, 255, 255, 255) + for i := 0; i < len(rects); i += batchSize { + r.RenderFillRects(rects[i : i+batchSize]) + } + + r.DebugText(10, 10, fmt.Sprintf("FPS %f", fps)) + r.DebugText(10, 20, fmt.Sprintf("Avg FPS %f", avgFps)) + r.DebugText(10, 30, fmt.Sprintf("dt %s", dt.String())) + + r.Present() + } +} + +func must(err error) { + if err != nil { + println(err.Error()) + panic(err) + } +} diff --git a/internal/sdl3-pure/sdl-game.go b/internal/sdl3-pure/sdl-game.go new file mode 100644 index 00000000..e89b1f46 --- /dev/null +++ b/internal/sdl3-pure/sdl-game.go @@ -0,0 +1,123 @@ +package main + +import ( + "fmt" + "github.com/jupiterrider/purego-sdl3/sdl" + "log" + "os" + "runtime" + "runtime/pprof" + "time" +) + +const batchSize = 1 << 14 +const rectCounter = 1 << 18 +const framerate = 600 +const frameCount = 100 // Number of frames to average + +func main() { + f, err := os.Create("cpu.out") + if err != nil { + log.Fatal(err) + } + defer f.Close() + + err = pprof.StartCPUProfile(f) + if err != nil { + log.Fatal(err) + } + defer pprof.StopCPUProfile() + + fmt.Println("CPU Profile Started") + defer fmt.Println("CPU Profile Stopped") + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + sdl.SetHint(sdl.HintRenderDriver, "gpu,software") + defer sdl.Quit() + if !sdl.Init(sdl.InitVideo) { + panic(sdl.GetError()) + } + + var w *sdl.Window + var r *sdl.Renderer + + if !sdl.CreateWindowAndRenderer("Hello, World!", 1280, 720, sdl.WindowResizable, &w, &r) { + panic(sdl.GetError()) + } + defer sdl.DestroyRenderer(r) + defer sdl.DestroyWindow(w) + + fmt.Println(sdl.GetRendererName(r), sdl.GetRenderDriver(0)) + + var dt time.Duration = time.Second + var fps, avgFps float64 + updatedAt := time.Now() + + var renderTicker *time.Ticker + if framerate > 0 { + renderTicker = time.NewTicker(time.Second / time.Duration(framerate)) + defer renderTicker.Stop() + } + + rects := make([]sdl.FRect, rectCounter) + for i := range rects { + rects[i] = sdl.FRect{X: 300, Y: 300, W: 10, H: 10} + } + + frameTimes := make([]float64, 0, frameCount) + +Outer: + for { + if renderTicker != nil { + <-renderTicker.C + } + fps = time.Second.Seconds() / dt.Seconds() + dt = time.Since(updatedAt) + updatedAt = time.Now() + + frameTimes = append(frameTimes, fps) + if len(frameTimes) > frameCount { + frameTimes = frameTimes[1:] + } + + var totalFps float64 + for _, frameTime := range frameTimes { + totalFps += frameTime + } + avgFps = totalFps / float64(len(frameTimes)) + + var event sdl.Event + for sdl.PollEvent(&event) { + switch event.Type() { + case sdl.EventQuit: + break Outer + case sdl.EventKeyDown: + if event.Key().Scancode == sdl.ScancodeEscape { + break Outer + } + } + } + sdl.SetRenderDrawColor(r, 0, 0, 0, 255) + sdl.RenderClear(r) + + sdl.SetRenderDrawColor(r, 255, 255, 255, 255) + for i := 0; i < len(rects); i += batchSize { + sdl.RenderFillRects(r, rects[i:i+batchSize]...) + } + + sdl.RenderDebugText(r, 10, 10, fmt.Sprintf("FPS %f", fps)) + sdl.RenderDebugText(r, 10, 20, fmt.Sprintf("Avg FPS %f", avgFps)) + sdl.RenderDebugText(r, 10, 30, fmt.Sprintf("dt %s", dt.String())) + + sdl.RenderPresent(r) + } +} + +func must(err error) { + if err != nil { + println(err.Error()) + panic(err) + } +} diff --git a/internal/tomb-mates-demo/main.go b/internal/tomb-mates-demo/main.go index 014cb1f7..f7c61e9b 100644 --- a/internal/tomb-mates-demo/main.go +++ b/internal/tomb-mates-demo/main.go @@ -172,7 +172,7 @@ package tomb_mates_demo // // } // g.Mx.Lock() -// components.Render.Each(g.World, func(e *ecs.Entry) { +// components.RenderAssterodd.Each(g.EntityManager, func(e *ecs.Entry) { // body := components.Transform.GetValue(e) // op.GeoM.Reset() @@ -180,7 +180,7 @@ package tomb_mates_demo // screen.DrawImage(dotBlue, op) // }) -// components.NetworkEntity.Each(g.World, func(e *ecs.Entry) { +// components.NetworkEntity.Each(g.EntityManager, func(e *ecs.Entry) { // ne := components.NetworkEntity.GetValue(e) // op.GeoM.Reset() diff --git a/internal/wasyan-runner/main.go b/internal/wasyan-runner/main.go new file mode 100644 index 00000000..d71e7b88 --- /dev/null +++ b/internal/wasyan-runner/main.go @@ -0,0 +1,86 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package main + +import ( + "fmt" + "github.com/bytecodealliance/wasmtime-go/v32" + "log" + "os" +) + +func main() { + engine := wasmtime.NewEngine() + defer engine.Close() + store := wasmtime.NewStore(engine) + defer store.Close() + + // Create a linker with WASI functions defined within it + linker := wasmtime.NewLinker(engine) + err := linker.DefineWasi() + check(err) + + wasiConfig := wasmtime.NewWasiConfig() + defer wasiConfig.Close() + + wasiConfig.InheritStderr() + wasiConfig.InheritStdout() + + store.SetWasi(wasiConfig) + + wasm, err := os.ReadFile("hello.wasm") + check(err) + + module, err := wasmtime.NewModule(engine, wasm) + check(err) + defer module.Close() + + instance, err := linker.Instantiate(store, module) + check(err) + + //get_game := wasmtime.WrapFunc(store, func(gameRef uint32) { + // + //}) + // + //set_game := wasmtime.WrapFunc(store, func(gameRef uint32) { + // + //}) + //instance, err := wasmtime.NewInstance(store, module, []wasmtime.AsExtern{get_game, set_game}) + + // Init + initFn := instance.GetFunc(store, "_start") + if initFn == nil { + initFn = instance.GetFunc(store, "_initialize") + if initFn == nil { + panic("Init function not found") + } + } + + _, err = initFn.Call(store) + check(err) + log.Println("Initialized") + + //call + gcd := instance.GetFunc(store, "add") + val, err := gcd.Call(store, 6, 27) + check(err) + fmt.Printf("add(6, 27) = %d\n", val.(int32)) +} + +func check(err error) { + if err != nil { + panic(err) + } +} diff --git a/internal/wasyan-server/main.go b/internal/wasyan-server/main.go new file mode 100644 index 00000000..6673b677 --- /dev/null +++ b/internal/wasyan-server/main.go @@ -0,0 +1,157 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +<- HromRU Donated 2 500 RUB +<- Еблан Donated 228 RUB + +Thank you for your support! +*/ + +package main + +import ( + "context" + "fmt" + "github.com/gofiber/fiber/v2" + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" + "gomp/internal/wasyan" + "gomp/vectors" + "log" + "os" + "strconv" + "time" + "unsafe" +) + +func main() { + var ctx = context.Background() + var nodeManager = NodeManager{} + var game = wasyan.Game{ + Position: vectors.Vec2{X: 1, Y: 2}, + Velocity: vectors.Vec2{X: 3, Y: 4}, + } + + nodeManager.runtime = wazero.NewRuntime(ctx) + defer nodeManager.runtime.Close(ctx) + + var sizeOfGame = int(unsafe.Sizeof(game)) + var getGameModule = Module{ + Fn: api.GoModuleFunc(func(ctx context.Context, mod api.Module, stack []uint64) { + gameBytes := unsafe.Slice((*byte)(unsafe.Pointer(&game)), sizeOfGame) + gameRef := api.DecodeU32(stack[0]) + mod.Memory().Write(gameRef, gameBytes) + }), + Params: []api.ValueType{api.ValueTypeI32}, + Results: []api.ValueType{}, + } + + var setGameModule = Module{ + Fn: api.GoModuleFunc(func(ctx context.Context, mod api.Module, stack []uint64) { + gameRef := api.DecodeU32(stack[0]) + r, _ := mod.Memory().Read(gameRef, uint32(unsafe.Sizeof(game))) + localGame := (*wasyan.Game)(unsafe.Pointer(unsafe.SliceData(r))) + game = *localGame + }), + Params: []api.ValueType{api.ValueTypeI32}, + Results: []api.ValueType{}, + } + + _, err := nodeManager.runtime.NewHostModuleBuilder("env"). + NewFunctionBuilder(). + WithGoModuleFunction(getGameModule.Fn, getGameModule.Params, getGameModule.Results). + Export("get_game"). + NewFunctionBuilder(). + WithGoModuleFunction(setGameModule.Fn, setGameModule.Params, setGameModule.Results). + Export("set_game"). + Instantiate(ctx) + if err != nil { + log.Panicln(err) + } + + wasi_snapshot_preview1.MustInstantiate(ctx, nodeManager.runtime) + + // Configure the module to initialize the reactor. + nodeManager.config = wazero.NewModuleConfig(). + WithStartFunctions("_start", "_initialize"). + WithStdout(os.Stdout). + WithStderr(os.Stderr) + + var app = fiber.New() + + //app.Use(recover.New()) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World!") + }) + + app.Get("/update/:file", func(c *fiber.Ctx) error { + file := c.Params("file") + err := nodeManager.LoadWasm(c.Context(), file) + if err != nil { + return c.SendString("Error loading module:\n" + err.Error()) + } + log.Println("Instance updated") + + return c.SendString("Instance updated") + }) + + app.Get("/internal/add", func(c *fiber.Ctx) error { + a, err := strconv.Atoi(c.Query("a")) + if err != nil { + return c.SendString("Error parsing a:" + err.Error()) + } + b, err := strconv.Atoi(c.Query("b")) + if err != nil { + return c.SendString("Error parsing b:" + err.Error()) + } + + start := time.Now() + wasyan.UpdateGame(&game, vectors.Vec2{X: float32(a), Y: float32(b)}) + duration := time.Since(start).String() + + return c.SendString("Result is " + fmt.Sprint(game) + "\n" + duration) + }) + + app.Get("/call/:name", func(c *fiber.Ctx) error { + name := c.Params("name") + a, err := strconv.Atoi(c.Query("a")) + if err != nil { + return c.SendString("Error parsing a:" + err.Error()) + } + b, err := strconv.Atoi(c.Query("b")) + if err != nil { + return c.SendString("Error parsing b:" + err.Error()) + } + + instance := &nodeManager.Instances + module := instance.Module + if module == nil { + return c.SendString("Module not found") + } + + fn := module.ExportedFunction(name) + if fn == nil { + return c.SendString("RPC not found") + } + + start := time.Now() + results, err := fn.Call(c.Context(), api.EncodeU32(uint32(a)), api.EncodeU32(uint32(b))) + duration := time.Since(start).String() + if err != nil { + log.Println(err) + return c.SendString("Error calling RPC\n" + err.Error()) + } + + return c.SendString("Result is " + fmt.Sprint(results) + "\n" + "Game is " + fmt.Sprint(game) + "\n" + duration) + }) + + log.Fatal(app.Listen(":3000")) +} diff --git a/internal/wasyan-server/modules.go b/internal/wasyan-server/modules.go new file mode 100644 index 00000000..de138299 --- /dev/null +++ b/internal/wasyan-server/modules.go @@ -0,0 +1,25 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package main + +import ( + "github.com/tetratelabs/wazero/api" +) + +type Module struct { + Fn api.GoModuleFunction + Params []api.ValueType + Results []api.ValueType +} diff --git a/internal/wasyan-server/node.go b/internal/wasyan-server/node.go new file mode 100644 index 00000000..9aa67f49 --- /dev/null +++ b/internal/wasyan-server/node.go @@ -0,0 +1,70 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package main + +import ( + "context" + "errors" + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/api" + "os" + "sync" + "sync/atomic" +) + +type NodeInstance struct { + Module api.Module +} + +type NodeManager struct { + Instances NodeInstance + LastUsedInstanceIndex atomic.Int32 + config wazero.ModuleConfig + runtime wazero.Runtime + mx sync.Mutex +} + +func (nm *NodeManager) LoadWasm(ctx context.Context, file string) error { + wasm, err := os.ReadFile(file) + if err != nil { + return errors.New("Error opening file:" + err.Error()) + } + + compiledWasm, err := nm.runtime.CompileModule(ctx, wasm) + if err != nil { + return errors.New("Error compiling module:" + err.Error()) + } + + if nm.Instances.Module != nil { + err := nm.Instances.Module.Close(ctx) + if err != nil { + return errors.New("Error closing module:" + err.Error()) + } + } + + mod, err := nm.runtime.InstantiateModule(ctx, compiledWasm, nm.config) + if err != nil { + return errors.New("Error creating instance:" + err.Error()) + } + + { + nm.mx.Lock() + defer nm.mx.Unlock() + nm.Instances.Module = mod + nm.LastUsedInstanceIndex.Store(0) + } + + return nil +} diff --git a/internal/wasyan-wasm/bindings.go b/internal/wasyan-wasm/bindings.go new file mode 100644 index 00000000..2abf1ec2 --- /dev/null +++ b/internal/wasyan-wasm/bindings.go @@ -0,0 +1,34 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package main + +import ( + "gomp/internal/wasyan" + "unsafe" +) + +//go:wasmimport env get_game +func get_game(gameRef uint32) + +//go:wasmimport env set_game +func set_game(gameRef uint32) + +func GetGame(game *wasyan.Game) { + get_game(uint32(uintptr(unsafe.Pointer(game)))) +} + +func SetGame(game *wasyan.Game) { + set_game(uint32(uintptr(unsafe.Pointer(game)))) +} diff --git a/internal/wasyan-wasm/main.go b/internal/wasyan-wasm/main.go new file mode 100644 index 00000000..7c5131e3 --- /dev/null +++ b/internal/wasyan-wasm/main.go @@ -0,0 +1,33 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package main + +import ( + "gomp/internal/wasyan" + "gomp/vectors" +) + +func main() {} + +//go:wasmexport add +func add(a, b uint32) uint32 { + //panic("lol") + var game wasyan.Game + GetGame(&game) + wasyan.UpdateGame(&game, vectors.Vec2{X: float32(a), Y: float32(b)}) + SetGame(&game) + + return a + b +} diff --git a/internal/wasyan-wasm/wasm_test.go b/internal/wasyan-wasm/wasm_test.go new file mode 100644 index 00000000..5d029df1 --- /dev/null +++ b/internal/wasyan-wasm/wasm_test.go @@ -0,0 +1,62 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package main + +import ( + "context" + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" + "os" + "testing" +) + +func TestWASM(t *testing.T) { + ctx := context.Background() + r := wazero.NewRuntime(ctx) + defer r.Close(ctx) + + // Instantiate WASI + wasi_snapshot_preview1.MustInstantiate(ctx, r) + + config := wazero.NewModuleConfig(). + WithStartFunctions("_start"). // Or "_initialize" based on the module type + //WithStartFunctions("_initialize"). // Or "_initialize" based on the module type + WithStdout(os.Stdout). + WithStderr(os.Stderr) + + // Load WASM module + wasm, err := os.ReadFile("hello.wasm") + if err != nil { + t.Fatal(err) + } + + compiledWasm, err := r.CompileModule(ctx, wasm) + if err != nil { + t.Fatal(err) + } + + mod, err := r.InstantiateModule(ctx, compiledWasm, config) + if err != nil { + t.Fatal(err) + } + + // Call the function + add := mod.ExportedFunction("add") + _, err = add.Call(ctx, api.EncodeU32(1), api.EncodeU32(2)) + if err != nil { + t.Fatal(err) + } +} diff --git a/internal/wasyan/shared.go b/internal/wasyan/shared.go new file mode 100644 index 00000000..5a5192ca --- /dev/null +++ b/internal/wasyan/shared.go @@ -0,0 +1,53 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package wasyan + +import ( + "gomp/vectors" + "math" + "sync" +) + +func Add(a, b int32) int32 { + var c float64 + c += float64(a) + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + for range 1_000_000 { + c += float64(b) + } + }() + wg.Wait() + + return int32(c) +} + +func UpdateGame(game *Game, newVelocity vectors.Vec2) { + game.Velocity = newVelocity + for i := 0; i < 1_000_000; i++ { + j := float32(i) + game.Velocity.X = float32(math.Sin(float64(j + game.Velocity.Y))) + game.Velocity.Y = float32(math.Cos(float64(j + game.Velocity.X))) + } + game.Position.X += game.Velocity.X + game.Position.Y += game.Velocity.Y +} + +type Game struct { + Position vectors.Vec2 + Velocity vectors.Vec2 +} diff --git a/pkg/bvh/tree.go b/pkg/bvh/tree.go new file mode 100644 index 00000000..67ad72b9 --- /dev/null +++ b/pkg/bvh/tree.go @@ -0,0 +1,332 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package bvh + +import ( + "cmp" + "github.com/negrel/assert" + "gomp/pkg/ecs" + "gomp/stdcomponents" + "gomp/vectors" + "math" + "math/bits" + "slices" +) + +type node struct { + childIndex int32 // if < 0 then points to leaf +} + +type leaf struct { + id ecs.Entity +} + +type component struct { + entity ecs.Entity + aabb stdcomponents.AABB + code uint64 +} + +func NewTree(layer stdcomponents.CollisionLayer) Tree { + return Tree{ + nodes: ecs.NewPagedArray[node](), + AabbNodes: ecs.NewPagedArray[stdcomponents.AABB](), + leaves: ecs.NewPagedArray[leaf](), + AabbLeaves: ecs.NewPagedArray[stdcomponents.AABB](), + codes: ecs.NewPagedArray[uint64](), + components: ecs.NewPagedArray[component](), + layer: layer, + } +} + +type Tree struct { + nodes ecs.PagedArray[node] + AabbNodes ecs.PagedArray[stdcomponents.AABB] + leaves ecs.PagedArray[leaf] + AabbLeaves ecs.PagedArray[stdcomponents.AABB] + codes ecs.PagedArray[uint64] + components ecs.PagedArray[component] + layer stdcomponents.CollisionLayer + + componentsSlice []component +} + +func (t *Tree) AddComponent(entity ecs.Entity, aabb stdcomponents.AABB) { + code := t.morton2D(&aabb) + t.components.Append(component{ + entity: entity, + aabb: aabb, + code: code, + }) +} + +func (t *Tree) Build() { + // Reset tree + t.nodes.Reset() + t.AabbNodes.Reset() + t.leaves.Reset() + t.AabbLeaves.Reset() + t.codes.Reset() + + // Extract and sort components by morton code + if cap(t.componentsSlice) < t.components.Len() { + t.componentsSlice = make([]component, 0, max(cap(t.componentsSlice)*2, t.components.Len())) + } + + t.componentsSlice = t.components.Raw(t.componentsSlice) + + slices.SortFunc(t.componentsSlice, func(a, b component) int { + return cmp.Compare(a.code, b.code) + }) + + // Add leaves + for i := range t.componentsSlice { + component := &t.componentsSlice[i] + t.leaves.Append(leaf{id: component.entity}) + t.AabbLeaves.Append(component.aabb) + t.codes.Append(component.code) + } + t.components.Reset() + + if t.leaves.Len() == 0 { + return + } + + // Add root node + t.nodes.Append(node{-1}) + t.AabbNodes.Append(stdcomponents.AABB{}) + + type buildTask struct { + parentIndex int + start int + end int + childrenCreated bool + } + + stack := [64]buildTask{ + {parentIndex: 0, start: 0, end: t.leaves.Len() - 1, childrenCreated: false}, + } + stackLen := 1 + + for stackLen > 0 { + stackLen-- + // Pop the last task + task := stack[stackLen] + + if !task.childrenCreated { + if task.start == task.end { + // Leaf node + t.nodes.Get(task.parentIndex).childIndex = -int32(task.start) + t.AabbNodes.Set(task.parentIndex, t.AabbLeaves.GetValue(task.start)) + continue + } + + split := t.findSplit(task.start, task.end) + + // Create left and right nodes + leftIndex := t.nodes.Len() + t.nodes.Append(node{-1}) + t.nodes.Append(node{-1}) + t.AabbNodes.Append(stdcomponents.AABB{}) + t.AabbNodes.Append(stdcomponents.AABB{}) + + // Set parent's childIndex to leftIndex + t.nodes.Get(task.parentIndex).childIndex = int32(leftIndex) + + // Push parent task back with childrenCreated=true + stack[stackLen] = buildTask{ + parentIndex: task.parentIndex, + start: task.start, + end: task.end, + childrenCreated: true, + } + stackLen++ + + // Push right child task (split+1 to end) + stack[stackLen] = buildTask{ + parentIndex: leftIndex + 1, + start: split + 1, + end: task.end, + childrenCreated: false, + } + stackLen++ + + // Push left child task (start to split) + stack[stackLen] = buildTask{ + parentIndex: leftIndex, + start: task.start, + end: split, + childrenCreated: false, + } + stackLen++ + } else { + // Merge children's AABBs into parent + leftChildIndex := int(t.nodes.Get(task.parentIndex).childIndex) + rightChildIndex := leftChildIndex + 1 + + leftAABB := t.AabbNodes.Get(leftChildIndex) + rightAABB := t.AabbNodes.Get(rightChildIndex) + + merged := t.mergeAABB(leftAABB, rightAABB) + t.AabbNodes.Set(task.parentIndex, merged) + } + } + t.components.Reset() +} + +func (t *Tree) Layer() stdcomponents.CollisionLayer { + return t.layer +} + +func (t *Tree) Query(aabb *stdcomponents.AABB, result []ecs.Entity) []ecs.Entity { + if t.nodes.Len() == 0 { // Handle empty tree + return result + } + + // Use stack-based traversal + const stackSize = 32 + stack := [stackSize]int32{0} + stackLen := 1 + + for stackLen > 0 { + stackLen-- + nodeIndex := int(stack[stackLen]) + a := t.AabbNodes.Get(nodeIndex) + b := aabb + + // Early exit if no AABB overlap + if !t.aabbOverlap(a, b) { + continue + } + + node := t.nodes.Get(nodeIndex) + if node.childIndex <= 0 { + // Is a leaf + index := -int(node.childIndex) + leafAabb := t.AabbLeaves.Get(index) + if t.aabbOverlap(leafAabb, aabb) { + result = append(result, t.leaves.Get(index).id) + } + continue + } + + // Push child indices (right and left) onto the stack. + stack[stackLen] = node.childIndex + 1 + stack[stackLen+1] = node.childIndex + stackLen += 2 + } + + return result +} + +// go:inline aabbOverlap checks if two AABB intersect +func (t *Tree) aabbOverlap(a, b *stdcomponents.AABB) bool { + return a.Max.X >= b.Min.X && a.Min.X <= b.Max.X && + a.Max.Y >= b.Min.Y && a.Min.Y <= b.Max.Y +} + +// findSplit finds the position where the highest bit changes +func (t *Tree) findSplit(start, end int) int { + // Identical Morton sortedMortonCodes => split the range in the middle. + first := t.codes.GetValue(start) + last := t.codes.GetValue(end) + + if first == last { + return (start + end) >> 1 + } + + // Calculate the number of highest bits that are the same + // for all objects, using the count-leading-zeros intrinsic. + commonPrefix := bits.LeadingZeros64(first ^ last) + + // Use binary search to find where the next bit differs. + // Specifically, we are looking for the highest object that + // shares more than commonPrefix bits with the first one. + split := start + step := end - start + + for { + step = (step + 1) >> 1 // exponential decrease + newSplit := split + step // proposed new position + + if newSplit < end { + splitCode := t.codes.GetValue(newSplit) + splitPrefix := bits.LeadingZeros64(first ^ splitCode) + if splitPrefix > commonPrefix { + split = newSplit + } + } + + if step <= 1 { + break + } + } + + return split +} + +// mergeAABB combines two AABB +func (t *Tree) mergeAABB(a, b *stdcomponents.AABB) stdcomponents.AABB { + return stdcomponents.AABB{ + Min: vectors.Vec2{ + X: min(a.Min.X, b.Min.X), + Y: min(a.Min.Y, b.Min.Y), + }, + Max: vectors.Vec2{ + X: max(a.Max.X, b.Max.X), + Y: max(a.Max.Y, b.Max.Y), + }, + } +} + +// Expands a 16-bit integer into 32 bits by inserting 1 zero after each bit +func (t *Tree) expandBits2D(v uint32) uint32 { + v = (v | (v << 8)) & 0x00FF00FF + v = (v | (v << 4)) & 0x0F0F0F0F + v = (v | (v << 2)) & 0x33333333 + v = (v | (v << 1)) & 0x55555555 + return v +} + +const mortonPrecision = (1 << 16) - 1 + +func (t *Tree) morton2D(aabb *stdcomponents.AABB) uint64 { + center := aabb.Center() + // Scale coordinates to 16-bit integers + //assert.True(center.X >= 0 && center.Y >= 0, "morton2D: center out of range") + + xx := uint64(float64(center.X) * mortonPrecision) + yy := uint64(float64(center.Y) * mortonPrecision) + + assert.True(xx < math.MaxUint64, "morton2D: x out of range") + assert.True(yy < math.MaxUint64, "morton2D: y out of range") + + // Spread the bits of x into the even positions + xx = (xx | (xx << 16)) & 0x0000FFFF0000FFFF + xx = (xx | (xx << 8)) & 0x00FF00FF00FF00FF + xx = (xx | (xx << 4)) & 0x0F0F0F0F0F0F0F0F + xx = (xx | (xx << 2)) & 0x3333333333333333 + xx = (xx | (xx << 1)) & 0x5555555555555555 + + // Spread the bits of y into the even positions and shift to odd positions + yy = (yy | (yy << 16)) & 0x0000FFFF0000FFFF + yy = (yy | (yy << 8)) & 0x00FF00FF00FF00FF + yy = (yy | (yy << 4)) & 0x0F0F0F0F0F0F0F0F + yy = (yy | (yy << 2)) & 0x3333333333333333 + yy = (yy | (yy << 1)) & 0x5555555555555555 + + // Combine x (even bits) and y (odd bits) + return xx | (yy << 1) +} diff --git a/pkg/collision/epa.go b/pkg/collision/epa.go new file mode 100644 index 00000000..7c964f80 --- /dev/null +++ b/pkg/collision/epa.go @@ -0,0 +1,91 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package gjk + +import ( + "gomp/stdcomponents" + "gomp/vectors" + "math" +) + +const ( + epaMaxTolerance = 0.01 // should not be very low because of circle colliders +) + +/* +EPA - Expanding Polytope Algorithm +Based on https://dyn4j.org/2010/05/epa-expanding-polytope-algorithm/#epa-alternatives +*/ +func (s *GJK) EPA( + a, b AnyCollider, + transformA, transformB stdcomponents.Transform2d, +) (vectors.Vec2, float32) { + polytope := s.simplex.toPolytope(make([]vectors.Vec2, 0, 6)) + + bestNormal := vectors.Vec2{} + bestDistance := float32(math.MaxFloat32) + bestTolerance := float32(math.MaxFloat32) + + for range maxIterations { + edge := s.findClosestEdge(polytope) + point := s.minkowskiSupport2d(a, b, transformA, transformB, edge.normal) + distance := point.Dot(edge.normal) + tolerance := distance - edge.distance + if tolerance < epaMaxTolerance { + return edge.normal, distance + } + if tolerance < bestTolerance { + bestTolerance = tolerance + bestNormal = edge.normal + bestDistance = distance + } + + polytope = append(polytope[:edge.index], append([]vectors.Vec2{point}, polytope[edge.index:]...)...) + } + + return bestNormal, bestDistance +} + +func (s *GJK) findClosestEdge(polytope []vectors.Vec2) closestEdge { + closest := closestEdge{ + distance: float32(math.MaxFloat32), + normal: vectors.Vec2{}, + index: -1, + } + + for i := 0; i < len(polytope); i++ { + j := (i + 1) % len(polytope) + a := polytope[i] + b := polytope[j] + + edge := b.Sub(a) + normal := edge.Perpendicular().Normalize() + distance := normal.Dot(a) + + if distance < closest.distance { + closest.distance = distance + closest.normal = normal + closest.index = j + } + } + + return closest +} + +type closestEdge struct { + distance float32 + normal vectors.Vec2 + index int +} diff --git a/pkg/collision/gjk.go b/pkg/collision/gjk.go new file mode 100644 index 00000000..7b71d50a --- /dev/null +++ b/pkg/collision/gjk.go @@ -0,0 +1,73 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +Thank you for your support! +*/ + +package gjk + +import ( + "gomp/stdcomponents" + "gomp/vectors" +) + +const ( + maxIterations = 64 +) + +func New() GJK { + return GJK{} +} + +type GJK struct { + simplex Simplex2d +} + +type AnyCollider interface { + GetSupport(direction vectors.Vec2, transform stdcomponents.Transform2d) vectors.Vec2 +} + +/* +CheckCollision - GJK, Distance, Closest Points +https://www.youtube.com/watch?v=Qupqu1xe7Io +https://dyn4j.org/2010/04/gjk-distance-closest-points/#gjk-distance +*/ +func (s *GJK) CheckCollision( + a, b AnyCollider, + transformA, transformB stdcomponents.Transform2d, +) bool { + direction := vectors.Vec2{X: 1, Y: 0} + + p := s.minkowskiSupport2d(a, b, transformA, transformB, direction) + s.simplex.add(p.ToVec3()) + direction = p.Neg() + + for range maxIterations { + p = s.minkowskiSupport2d(a, b, transformA, transformB, direction) + + if p.Dot(direction) < 0 { + return false + } + + s.simplex.add(p.ToVec3()) + + if s.simplex.do(&direction) { + return true + } + } + + panic("GJK infinite loop") +} + +func (s *GJK) minkowskiSupport2d(a, b AnyCollider, transformA, transformB stdcomponents.Transform2d, direction vectors.Vec2) vectors.Vec2 { + aSupport := a.GetSupport(direction, transformA) + bSupport := b.GetSupport(direction.Neg(), transformB) + support := aSupport.Sub(bSupport) + return support +} diff --git a/pkg/collision/simplex.go b/pkg/collision/simplex.go new file mode 100644 index 00000000..013d1221 --- /dev/null +++ b/pkg/collision/simplex.go @@ -0,0 +1,119 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +Thank you for your support! +*/ + +package gjk + +import "gomp/vectors" + +type Simplex2d struct { + a, b, c vectors.Vec3 + count int +} + +func (s *Simplex2d) do(direction *vectors.Vec2) bool { + ao := s.a.Neg() + + switch s.count { + case 2: // Line + ab := s.b.Sub(s.a) + if ab.Dot(ao) > 0 { + newDirection := ab.Cross(ao).Cross(ab) + direction.X = newDirection.X + direction.Y = newDirection.Y + } else { + direction.X = ao.X + direction.Y = ao.Y + s.count = 1 + } + case 3: // Triangle + ab := s.b.Sub(s.a) + ac := s.c.Sub(s.a) + abc := ab.Cross(ac) + + if abc.Cross(ac).Dot(ao) > 0 { + if ac.Dot(ao) > 0 { + newDirection := ac.Cross(ao).Cross(ac) + direction.X = newDirection.X + direction.Y = newDirection.Y + s.b = s.c + s.count = 2 + } else { + if ab.Dot(ao) > 0 { + newDirection := ab.Cross(ao).Cross(ab) + direction.X = newDirection.X + direction.Y = newDirection.Y + s.count = 2 + } else { + direction.X = ao.X + direction.Y = ao.Y + s.count = 1 + } + } + } else { + if ab.Cross(abc).Dot(ao) > 0 { + if ab.Dot(ao) > 0 { + newDirection := ab.Cross(ao).Cross(ab) + direction.X = newDirection.X + direction.Y = newDirection.Y + s.count = 2 + } else { + direction.X = ao.X + direction.Y = ao.Y + s.count = 1 + } + } else { + return true + // if abc.Dot(ao) > 0 { + // newDirection := abc + // direction.X = newDirection.X + // direction.Y = newDirection.Y + // } else { + // s.b, s.c = s.a, s.b + // newDirection := abc.Neg() + // direction.X = newDirection.X + // direction.Y = newDirection.Y + // } + } + } + default: + panic("Invalid simplex") + } + return false +} + +func (s *Simplex2d) add(p vectors.Vec3) { + switch s.count { + case 0: + s.a = p + case 1: + s.a, s.b = p, s.a + case 2: + s.a, s.b, s.c = p, s.a, s.b + default: + panic("Invalid simplex") + } + s.count++ +} + +func (s *Simplex2d) toPolytope(polytope []vectors.Vec2) []vectors.Vec2 { + switch s.count { + case 1: + polytope = append(polytope, s.a.ToVec2()) + case 2: + polytope = append(polytope, s.a.ToVec2(), s.b.ToVec2()) + case 3: + polytope = append(polytope, s.a.ToVec2(), s.b.ToVec2(), s.c.ToVec2()) + default: + panic("Invalid simplex") + } + return polytope +} diff --git a/pkg/core/engine.go b/pkg/core/engine.go new file mode 100644 index 00000000..277bc93b --- /dev/null +++ b/pkg/core/engine.go @@ -0,0 +1,93 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +<- тажефигня Donated 500 RUB + +Thank you for your support! +*/ + +package core + +import ( + "gomp/pkg/worker" + "log" + "runtime" + "time" +) + +const ( + MaxFrameSkips = 5 +) + +func NewEngine(game AnyGame) Engine { + numCpu := max(runtime.NumCPU()-1, 1) + engine := Engine{ + Game: game, + pool: worker.NewPool(numCpu), + } + return engine +} + +type Engine struct { + Game AnyGame + pool worker.Pool +} + +func (e *Engine) Run(tickrate uint, framerate uint) { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + fixedUpdDuration := time.Second / time.Duration(tickrate) + + var renderTicker *time.Ticker + if framerate > 0 { + renderTicker = time.NewTicker(time.Second / time.Duration(framerate)) + defer renderTicker.Stop() + } + + e.Game.Init(e) + defer e.Game.Destroy() + + var lastUpdateAt = time.Now() // TODO: REMOVE? + var nextFixedUpdateAt = time.Now() + var dt = time.Since(lastUpdateAt) + + e.pool.Start() + + for !e.Game.ShouldDestroy() { + if renderTicker != nil { + <-renderTicker.C + } + dt = time.Since(lastUpdateAt) + lastUpdateAt = time.Now() + + // Update + e.Game.Update(dt) + + // Fixed Update + loops := 0 + // TODO: Refactor to work without for loop + for nextFixedUpdateAt.Compare(time.Now()) == -1 && loops < MaxFrameSkips { + e.Game.FixedUpdate(fixedUpdDuration) + nextFixedUpdateAt = nextFixedUpdateAt.Add(fixedUpdDuration) + loops++ + } + if loops >= MaxFrameSkips { + nextFixedUpdateAt = time.Now() + log.Println("Too many updates detected") + } + + // RenderAssterodd + e.Game.Render(dt) + } +} + +func (e *Engine) Pool() *worker.Pool { + return &e.pool +} diff --git a/pkg/core/game.go b/pkg/core/game.go new file mode 100644 index 00000000..5930e721 --- /dev/null +++ b/pkg/core/game.go @@ -0,0 +1,28 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package core + +import ( + "time" +) + +type AnyGame interface { + Init(engine *Engine) + Update(dt time.Duration) + FixedUpdate(dt time.Duration) + Render(dt time.Duration) + Destroy() + ShouldDestroy() bool +} diff --git a/pkg/ecs/accumulator.go b/pkg/ecs/accumulator.go new file mode 100644 index 00000000..0fff717a --- /dev/null +++ b/pkg/ecs/accumulator.go @@ -0,0 +1,92 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package ecs + +import ( + "github.com/negrel/assert" + "gomp/pkg/worker" +) + +// NewAccumulator creates a new accumulator instance +func NewAccumulator[T any]( + initFn func() T, + resetFn func(T) T, + mergeFn func([]T) T, + processFn func(T), +) Accumulator[T] { + return Accumulator[T]{ + initFn: initFn, + resetFn: resetFn, + mergeFn: mergeFn, + processFn: processFn, + } +} + +// Accumulator handles multi-worker accumulation with flexible merge strategies +type Accumulator[T any] struct { + workerData []T // Per-worker storage + initFn func() T // Initializes worker-specific storage + resetFn func(T) T // Clears worker data while preserving allocations + mergeFn func([]T) T // Combines all worker data into final result + processFn func(T) // Processes worker data + isInitialized bool +} + +// Init initializes the accumulator +func (a *Accumulator[T]) Init(pool *worker.Pool) { + a.workerData = make([]T, pool.NumWorkers()) + for i := range a.workerData { + a.workerData[i] = a.initFn() + } + a.isInitialized = true +} + +// Update modifies worker-specific data in a thread-safe manner +func (a *Accumulator[T]) Update(workerID worker.WorkerId, update func(acc T) T) { + assert.True(a.isInitialized) + a.workerData[workerID] = update(a.workerData[workerID]) +} + +// Merge combines all worker data into final result +func (a *Accumulator[T]) Merge() T { + assert.True(a.isInitialized) + return a.mergeFn(a.workerData) +} + +// Process applies finalization function to all collected data +func (a *Accumulator[T]) Process() { + assert.True(a.isInitialized) + for i := range a.workerData { + a.processFn(a.workerData[i]) + } +} + +// Reset prepares the accumulator for new data collection +func (a *Accumulator[T]) Reset() { + assert.True(a.isInitialized) + for i := range a.workerData { + a.workerData[i] = a.resetFn(a.workerData[i]) + } +} + +// Destroy releases all resources associated with the accumulator +func (a *Accumulator[T]) Destroy() { + a.workerData = nil + a.initFn = nil + a.resetFn = nil + a.mergeFn = nil + a.processFn = nil + a.isInitialized = false +} diff --git a/pkg/ecs/bit-array.go b/pkg/ecs/bit-array.go deleted file mode 100644 index 2343d0b7..00000000 --- a/pkg/ecs/bit-array.go +++ /dev/null @@ -1,88 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ - -package ecs - -import ( - "math/bits" -) - -// type BitArray []uint64 - -// // New creates a new BitArray with the given number of bits. -// func NewBitArray(size uint64) BitArray { -// words := (size + 63) / 64 -// return make(BitArray, words) -// } - -// // ensureIndex ensures the given bit index is accessible, resizing the array if necessary. -// func (b *BitArray) ensureIndex(index uint64) { -// wordsNeeded := (index / 64) + 1 -// if uint64(len(*b)) < wordsNeeded { -// newData := make(BitArray, wordsNeeded) -// copy(newData, *b) -// *b = newData -// } -// } - -// // Resize adjusts the BitArray size to accommodate a specific number of bits. -// // Shrinks the underlying array if the new size is smaller. -// func (b *BitArray) Resize(newSize uint64) { -// newWords := (newSize + 63) / 64 -// currentWords := uint64(len(*b)) -// if newWords > currentWords { -// newData := make(BitArray, newWords) -// copy(newData, *b) -// *b = newData -// } else if newWords < currentWords { -// *b = (*b)[:newWords] -// } -// } -// // Size calculates the total number of bits in the BitArray. -// func (b BitArray) Size() uint64 { -// return uint64(len(b)) * 64 -// } - -// ComponentBitArray256 is a structure to manage an array of uint64 values as a bit array. -const bit_array_size = 256 / bits.UintSize - -type ComponentBitArray256 [bit_array_size]uint - -// Set sets the bit at the given index to 1. -func (b *ComponentBitArray256) Set(index ComponentID) { - b[index/bits.UintSize] |= 1 << (index % bits.UintSize) -} - -// Unset clears the bit at the given index (sets it to 0). -func (b *ComponentBitArray256) Unset(index ComponentID) { - b[index/bits.UintSize] &^= 1 << (index % bits.UintSize) -} - -// Toggle toggles the bit at the given index. -func (b *ComponentBitArray256) Toggle(index ComponentID) { - b[index/bits.UintSize] ^= 1 << (index % bits.UintSize) -} - -// IsSet checks if the bit at the given index is set (1). Automatically resizes if the index is out of bounds. -func (b *ComponentBitArray256) IsSet(index ComponentID) bool { - return (b[index/bits.UintSize] & (1 << (index % bits.UintSize))) != 0 -} - -func (b *ComponentBitArray256) AllSet(yield func(ComponentID) bool) { - var id ComponentID - var raisedBitsCount int - for i, v := range b { - raisedBitsCount = bits.OnesCount(v) - for range raisedBitsCount { - index := bits.Len(v) - 1 - v &^= 1 << index - id = ComponentID(i*bits.UintSize + index) - if !yield(id) { - return - } - } - } -} diff --git a/pkg/ecs/component-bit-table.go b/pkg/ecs/component-bit-table.go new file mode 100644 index 00000000..ef7ab94a --- /dev/null +++ b/pkg/ecs/component-bit-table.go @@ -0,0 +1,154 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package ecs + +import ( + "github.com/negrel/assert" + "math/bits" +) + +const ( + uintShift = 7 - 64/bits.UintSize +) + +func NewComponentBitTable(maxComponentsLen int) ComponentBitTable { + bitsetSize := ((maxComponentsLen - 1) / bits.UintSize) + 1 + return ComponentBitTable{ + bitsetsBook: make([][]uint, 0, initialBookSize), + entitiesBook: make([]*entityArray, 0, initialBookSize), + lookup: NewPagedMap[Entity, int](), + bitsetSize: bitsetSize, + pageSize: bitsetSize * pageSize, + } +} + +type ComponentBitTable struct { + bitsetsBook [][]uint + entitiesBook []*entityArray + lookup PagedMap[Entity, int] + length int + bitsetSize int + pageSize int +} + +type entityArray [pageSize]Entity + +func (b *ComponentBitTable) Create(entity Entity) { + assert.False(b.lookup.Has(entity), "entity already exists") + + b.extend() + bitsId := b.length + b.lookup.Set(entity, bitsId) + pageId, entityId := b.getPageIDAndEntityIndex(bitsId) + b.entitiesBook[pageId][entityId] = entity + b.length++ +} + +func (b *ComponentBitTable) Delete(entity Entity) { + bitsetIndex, ok := b.lookup.Get(entity) + assert.True(ok, "entity not found") + + // Get the index of the last entity + lastIndex := b.length - 1 + + // If this is not the last entity, swap with the last one + if bitsetIndex != lastIndex { + lastPageId, lastEntityId := b.getPageIDAndEntityIndex(lastIndex) + lastBitsetId := lastEntityId * b.bitsetSize + deletePageId, deleteEntityId := b.getPageIDAndEntityIndex(bitsetIndex) + deleteBitsetId := deleteEntityId * b.bitsetSize + + // Copy bitset from last entity to the deleted entity's position + for i := 0; i < b.bitsetSize; i++ { + b.bitsetsBook[deletePageId][deleteBitsetId+i] = b.bitsetsBook[lastPageId][lastBitsetId+i] + b.bitsetsBook[lastPageId][lastBitsetId+i] = 0 + } + + // Get the last entity and update its position in lookup + lastEntity := b.entitiesBook[lastPageId][lastEntityId] + b.entitiesBook[deletePageId][deleteEntityId] = lastEntity + b.lookup.Set(lastEntity, bitsetIndex) + } + + b.lookup.Delete(entity) + b.length-- +} + +// Set sets the bit at the given index to 1. +func (b *ComponentBitTable) Set(entity Entity, componentId ComponentId) { + bitsId, ok := b.lookup.Get(entity) + assert.True(ok, "entity not found") + + pageId, bitsetId := b.getPageIDAndBitsetIndex(bitsId) + offset := int(componentId) >> uintShift + b.bitsetsBook[pageId][bitsetId+offset] |= 1 << (componentId % bits.UintSize) +} + +// Unset clears the bit at the given index (sets it to 0). +func (b *ComponentBitTable) Unset(entity Entity, componentId ComponentId) { + bitsId, ok := b.lookup.Get(entity) + assert.True(ok, "entity not found") + + pageId, bitsetId := b.getPageIDAndBitsetIndex(bitsId) + offset := int(componentId) >> uintShift + b.bitsetsBook[pageId][bitsetId+offset] &= ^(1 << (componentId % bits.UintSize)) +} + +func (b *ComponentBitTable) Test(entity Entity, componentId ComponentId) bool { + bitsId, ok := b.lookup.Get(entity) + if !ok { + return false + } + pageId, bitsetId := b.getPageIDAndBitsetIndex(bitsId) + offset := int(componentId) >> uintShift + return (b.bitsetsBook[pageId][bitsetId+offset] & (1 << (componentId % bits.UintSize))) != 0 +} + +func (b *ComponentBitTable) AllSet(entity Entity, yield func(ComponentId) bool) { + bitsId, ok := b.lookup.Get(entity) + if !ok { + return + } + pageId, bitsetId := b.getPageIDAndBitsetIndex(bitsId) + bitset := b.bitsetsBook[pageId][bitsetId : bitsetId+b.bitsetSize] + for i, set := range bitset { + j := 0 + for set != 0 { + if set&1 == 1 { + if !yield(ComponentId(i*bits.UintSize + j)) { + return + } + } + set >>= 1 + j++ + } + } +} + +func (b *ComponentBitTable) extend() { + lastChunkId, lastEntityId := b.getPageIDAndEntityIndex(b.length) + if lastChunkId == len(b.bitsetsBook) && lastEntityId == 0 { + b.bitsetsBook = append(b.bitsetsBook, make([]uint, b.pageSize)) + b.entitiesBook = append(b.entitiesBook, &entityArray{}) + } +} + +func (b *ComponentBitTable) getPageIDAndBitsetIndex(index int) (int, int) { + return index >> pageSizeShift, (index & pageSizeMask) * b.bitsetSize +} + +func (b *ComponentBitTable) getPageIDAndEntityIndex(index int) (int, int) { + return index >> pageSizeShift, index & pageSizeMask +} diff --git a/pkg/ecs/component-bit-table_test.go b/pkg/ecs/component-bit-table_test.go new file mode 100644 index 00000000..a5eae592 --- /dev/null +++ b/pkg/ecs/component-bit-table_test.go @@ -0,0 +1,432 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package ecs + +import ( + "math/bits" + "testing" +) + +func TestNewComponentBitTable(t *testing.T) { + // Test with different max component sizes + tests := []struct { + name string + maxComponentsLen int + expectedBitsetSize int + }{ + {"Small", 10, 1}, + {"Medium", 64, 1}, + {"Large", 192, 3}, + {"Below", 172, 3}, + {"Above", 200, 4}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + table := NewComponentBitTable(tt.maxComponentsLen) + + if cap(table.bitsetsBook) != initialBookSize { + t.Errorf("Expected %d preallocated chunks, got %d", initialBookSize, cap(table.bitsetsBook)) + } + }) + } +} + +func TestComponentBitTable_Set(t *testing.T) { + table := NewComponentBitTable(100) + + // Set a bit for a new entity + entity1 := Entity(1) + table.Create(entity1) + table.Set(entity1, ComponentId(5)) + + // Verify bit was set + bitsId, ok := table.lookup.Get(entity1) + if !ok { + t.Fatalf("Entity %d not found in lookup", entity1) + } + + pageId, bitsetId := table.getPageIDAndBitsetIndex(bitsId) + offset := int(ComponentId(5) >> uintShift) + mask := uint(1 << (ComponentId(5) % bits.UintSize)) + + if (table.bitsetsBook[pageId][bitsetId+offset] & mask) == 0 { + t.Errorf("Expected bit to be set for entity %d, component %d", entity1, 5) + } + + // Set multiple bits for same entity + table.Set(entity1, ComponentId(10)) + table.Set(entity1, ComponentId(63)) + table.Set(entity1, ComponentId(64)) + + // Set bits for a different entity + entity2 := Entity(2) + table.Create(entity2) + table.Set(entity2, ComponentId(5)) +} + +func TestComponentBitTable_Unset(t *testing.T) { + table := NewComponentBitTable(100) + entity := Entity(1) + + table.Create(entity) + // Set and then unset + table.Set(entity, ComponentId(5)) + table.Set(entity, ComponentId(10)) + table.Unset(entity, ComponentId(5)) + + // Verify bit was unset + bitsId, _ := table.lookup.Get(entity) + pageId, bitsetId := table.getPageIDAndBitsetIndex(bitsId) + offset := int(ComponentId(5) >> uintShift) + mask := uint(1 << (ComponentId(5) % bits.UintSize)) + + if (table.bitsetsBook[pageId][bitsetId+offset] & mask) != 0 { + t.Errorf("Expected bit to be unset for entity %d, component %d", entity, 5) + } + + // Verify other bit is still set + offset = int(ComponentId(10) >> uintShift) + mask = uint(1 << (ComponentId(10) % bits.UintSize)) + + if (table.bitsetsBook[pageId][bitsetId+offset] & mask) == 0 { + t.Errorf("Expected bit to still be set for entity %d, component %d", entity, 10) + } +} + +func TestComponentBitTable_Test(t *testing.T) { + table := NewComponentBitTable(100) + + // Test for non-existent entity + if table.Test(Entity(999), ComponentId(5)) { + t.Error("Test should return false for non-existent entity") + } + + // Set up an entity with some components + entity := Entity(42) + table.Create(entity) + table.Set(entity, ComponentId(5)) + table.Set(entity, ComponentId(64)) + + // Test for set components + if !table.Test(entity, ComponentId(5)) { + t.Error("Test should return true for set component 5") + } + if !table.Test(entity, ComponentId(64)) { + t.Error("Test should return true for set component 64") + } + + // Test for unset component + if table.Test(entity, ComponentId(10)) { + t.Error("Test should return false for unset component 10") + } + + // Test after unsetting a component + table.Unset(entity, ComponentId(5)) + if table.Test(entity, ComponentId(5)) { + t.Error("Test should return false after component is unset") + } + + // Test components at boundaries + entity2 := Entity(43) + table.Create(entity2) + table.Set(entity2, ComponentId(0)) + table.Set(entity2, ComponentId(64)) // First bit in second uint + table.Set(entity2, ComponentId(65)) // Second bit in second uint + + if !table.Test(entity2, ComponentId(0)) { + t.Error("Test should return true for set component at uint boundary (0)") + } + if !table.Test(entity2, ComponentId(64)) { + t.Error("Test should return true for set component at uint boundary (64)") + } + if !table.Test(entity2, ComponentId(65)) { + t.Error("Test should return true for set component at uint boundary (65)") + } +} + +func TestComponentBitTable_AllSet(t *testing.T) { + table := NewComponentBitTable(200) + entity := Entity(1) + table.Create(entity) + + // Set several components + expectedComponents := []ComponentId{5, 10, 64, 128, 199} + for _, id := range expectedComponents { + table.Set(entity, id) + } + + // Use AllSet to collect components + var foundComponents []ComponentId + table.AllSet(entity, func(id ComponentId) bool { + foundComponents = append(foundComponents, id) + return true + }) + + // Verify all components were found + if len(foundComponents) != len(expectedComponents) { + t.Errorf("Expected %d components, found %d", len(expectedComponents), len(foundComponents)) + } + + // Check each component + componentMap := make(map[ComponentId]bool) + for _, id := range foundComponents { + componentMap[id] = true + } + + for _, id := range expectedComponents { + if !componentMap[id] { + t.Errorf("Component %d not found in AllSet results", id) + } + } + + // Test early termination + count := 0 + table.AllSet(entity, func(id ComponentId) bool { + count++ + return count < 3 // Stop after finding 2 components + }) + + if count != 3 { + t.Errorf("Early termination didn't work as expected. Count: %d", count) + } +} + +func TestComponentBitTable_extend(t *testing.T) { + // Create a table with a small chunk size for testing + table := NewComponentBitTable(bits.UintSize) + + // Create entities to fill the first chunk + for i := 0; i < pageSize; i++ { + e := Entity(i) + table.Create(e) + table.Set(e, ComponentId(i%bits.UintSize)) + } + if len(table.bitsetsBook) != 1 { + t.Errorf("Expected table to extend, got %d chunks", len(table.bitsetsBook)) + } + + // Create entity to force extension + table.Create(pageSize) + if len(table.bitsetsBook) <= 1 { + t.Errorf("Expected table to be extended, got %d chunks", len(table.bitsetsBook)) + } + + // Create more entities + for i := pageSize + 1; i < (pageSize*2)+1; i++ { + e := Entity(i) + table.Create(e) + table.Set(e, ComponentId(1)) + } + if len(table.bitsetsBook) <= 2 { + t.Errorf("Expected table to extend beyond 2 chunks, got %d chunks", len(table.bitsetsBook)) + } +} + +func TestComponentBitTable_EdgeCases(t *testing.T) { + table := NewComponentBitTable(100) + + // Test AllSet on non-existent entity + called := false + table.AllSet(Entity(999), func(id ComponentId) bool { + called = true + return true + }) + + if called { + t.Errorf("AllSet callback should not be called for non-existent entity") + } + + // Test setting across bit boundaries + entity := Entity(42) + table.Create(entity) + table.Set(entity, ComponentId(0)) // First bit in first uint + table.Set(entity, ComponentId(63)) // Last bit in first uint + table.Set(entity, ComponentId(64)) // First bit in second uint + + // Verify all bits + var found []ComponentId + table.AllSet(entity, func(id ComponentId) bool { + found = append(found, id) + return true + }) + + if len(found) != 3 || !contains(found, ComponentId(0)) || !contains(found, ComponentId(63)) || !contains(found, ComponentId(64)) { + t.Errorf("Expected components 0, 63 and 64, got %v", found) + } +} + +func TestComponentBitTable_Create(t *testing.T) { + table := NewComponentBitTable(100) + entity := Entity(42) + + // Create entity + table.Create(entity) + + // Verify entity is in lookup + _, ok := table.lookup.Get(entity) + if !ok { + t.Fatalf("Entity %d not found in lookup after Create", entity) + } + + // Check that entity ID is stored in entities book + bitsId, _ := table.lookup.Get(entity) + pageId, entityId := table.getPageIDAndEntityIndex(bitsId) + + if table.entitiesBook[pageId][entityId] != entity { + t.Errorf("Expected entity ID %d stored in entities book, got %d", + entity, table.entitiesBook[pageId][entityId]) + } +} + +func TestComponentBitTable_Delete(t *testing.T) { + table := NewComponentBitTable(100) + entity := Entity(42) + table.Create(entity) + + // Set multiple components + table.Set(entity, ComponentId(5)) + table.Set(entity, ComponentId(10)) + + // Ensure components are set + if !table.Test(entity, ComponentId(5)) || !table.Test(entity, ComponentId(10)) { + t.Fatalf("Expected components to be set for entity %d", entity) + } + + // Delete the entity + table.Delete(entity) + + // Verify entity is no longer in lookup + _, ok := table.lookup.Get(entity) + if ok { + t.Errorf("Entity %d should be removed from lookup after deletion", entity) + } + + // Test should return false for deleted entity + if table.Test(entity, ComponentId(5)) || table.Test(entity, ComponentId(10)) { + t.Errorf("Test should return false for deleted entity %d", entity) + } +} + +func TestComponentBitTable_DeleteWithSwap(t *testing.T) { + table := NewComponentBitTable(100) + + // Create two entities + entity1 := Entity(1) + entity2 := Entity(2) + table.Create(entity1) + table.Create(entity2) + + // Set different components for each + table.Set(entity1, ComponentId(5)) + table.Set(entity1, ComponentId(10)) + table.Set(entity2, ComponentId(15)) + table.Set(entity2, ComponentId(20)) + + // Get entity2's bits ID before deletion of entity1 + entity2BitsId, _ := table.lookup.Get(entity2) + + // Delete the first entity - should swap with entity2 + table.Delete(entity1) + + // Verify entity1 is gone + _, ok := table.lookup.Get(entity1) + if ok { + t.Errorf("Entity %d should be removed from lookup", entity1) + } + + // Verify entity2's data is still accessible + if !table.Test(entity2, ComponentId(15)) || !table.Test(entity2, ComponentId(20)) { + t.Errorf("Entity %d should still have its components after swap", entity2) + } + + // Entity2's lookup entry should now point to entity1's old position + newEntity2BitsId, ok := table.lookup.Get(entity2) + if !ok { + t.Fatalf("Entity %d not found after swap", entity2) + } + + // entity2 should have been moved to entity1's position + if newEntity2BitsId == entity2BitsId { + t.Errorf("Entity %d position should have changed after swap", entity2) + } + + // Ensure entity is stored in the entity book + pageId, entityId := table.getPageIDAndEntityIndex(newEntity2BitsId) + if table.entitiesBook[pageId][entityId] != entity2 { + t.Errorf("Entity ID %d not correctly stored after swap, got %d", + entity2, table.entitiesBook[pageId][entityId]) + } +} + +func TestComponentBitTable_MultipleOperations(t *testing.T) { + table := NewComponentBitTable(100) + + // Create, set, delete several entities in sequence + for i := 1; i <= 5; i++ { + entity := Entity(i) + table.Create(entity) + table.Set(entity, ComponentId(i)) + table.Set(entity, ComponentId(i+10)) + } + + // Delete entity 2 and 4 + table.Delete(Entity(2)) + table.Delete(Entity(4)) + + // Verify entities 1, 3, 5 still exist with correct components + for _, id := range []Entity{1, 3, 5} { + if !table.Test(id, ComponentId(int(id))) || !table.Test(id, ComponentId(int(id)+10)) { + t.Errorf("Entity %d should still have its components", id) + } + } + + // Verify entities 2 and 4 are gone + for _, id := range []Entity{2, 4} { + if _, ok := table.lookup.Get(id); ok { + t.Errorf("Entity %d should have been deleted", id) + } + } + + // Create new entities + for i := 6; i <= 7; i++ { + entity := Entity(i) + table.Create(entity) + table.Set(entity, ComponentId(i)) + } + + // Verify new entities have correct components + for i := 6; i <= 7; i++ { + entity := Entity(i) + if !table.Test(entity, ComponentId(i)) { + t.Errorf("Entity %d should have component %d set", entity, i) + } + } + + // The length should be 5 (entities 1, 3, 5, 6, 7) + if table.length != 5 { + t.Errorf("Expected length 5, got %d", table.length) + } +} + +// Helper function to check if slice contains a value +func contains(s []ComponentId, id ComponentId) bool { + for _, v := range s { + if v == id { + return true + } + } + return false +} diff --git a/pkg/ecs/component-bitset.go b/pkg/ecs/component-bitset.go new file mode 100644 index 00000000..8e3fcc6d --- /dev/null +++ b/pkg/ecs/component-bitset.go @@ -0,0 +1,144 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package ecs + +import ( + "github.com/negrel/assert" + "math/big" + "math/bits" +) + +const ComponentBitsetPreallocate = 1024 + +func NewComponentBitSet() ComponentBitSet { + return ComponentBitSet{ + bits: make([]BitSet, 0, ComponentBitsetPreallocate), + entities: make([]Entity, 0, ComponentBitsetPreallocate), + lookup: make(map[Entity]int, ComponentBitsetPreallocate), + } +} + +const ( + a = 1243123123 & 0 +) + +type BitSet = big.Int + +type ComponentBitSet struct { + bits []BitSet + entities []Entity + lookup map[Entity]int +} + +func (b *ComponentBitSet) Get(entity Entity) BitSet { + bitsId, ok := b.lookup[entity] + assert.True(ok, "entity not found") + return b.bits[bitsId] +} + +// Set sets the bit at the given index to 1. +func (b *ComponentBitSet) Set(entity Entity, componentId ComponentId) { + bitsId, ok := b.lookup[entity] + if !ok { + bitsId = len(b.bits) + b.lookup[entity] = bitsId + b.entities = append(b.entities, entity) + b.bits = append(b.bits, BitSet{}) + } + + bitSet := &b.bits[bitsId] + bitSet.SetBit(bitSet, int(componentId), 1) +} + +// Unset clears the bit at the given index (sets it to 0). +func (b *ComponentBitSet) Unset(entity Entity, componentId ComponentId) { + bitsId, ok := b.lookup[entity] + assert.True(ok, "entity not found") + bitSet := &b.bits[bitsId] + bitSet.SetBit(bitSet, int(componentId), 0) +} + +// Toggle toggles the bit at the given index. +func (b *ComponentBitSet) Toggle(entity Entity, componentId ComponentId) { + bitsId, ok := b.lookup[entity] + assert.True(ok, "entity not found") + bitSet := &b.bits[bitsId] + bitSet.SetBit(bitSet, int(componentId), 1-bitSet.Bit(int(componentId))) +} + +// IsSet checks if the bit at the given index is set (1). Automatically resizes if the index is out of bounds. +func (b *ComponentBitSet) IsSet(entity Entity, componentId ComponentId) bool { + bitsId, ok := b.lookup[entity] + assert.True(ok, "entity not found") + bitSet := &b.bits[bitsId] + + return bitSet.Bit(int(componentId)) == 1 +} + +func (b *ComponentBitSet) Delete(entity Entity) { + bitsId, ok := b.lookup[entity] + assert.True(ok, "entity not found") + + lastIndex := len(b.bits) - 1 + if bitsId < lastIndex { + // swap the dead element with the last one + b.bits[bitsId], b.bits[lastIndex] = b.bits[lastIndex], b.bits[bitsId] + b.entities[bitsId] = b.entities[lastIndex] + + // update lookup table + b.lookup[b.entities[bitsId]] = bitsId + } + + b.bits = b.bits[:lastIndex] + b.entities = b.entities[:lastIndex] + delete(b.lookup, entity) +} + +func (b *ComponentBitSet) FilterByMask(mask BitSet, yield func(entity Entity) bool) { + bitsLen := len(b.bits) + var cmpBitset BitSet + var zeroBitSet BitSet + + for i := range bitsLen { + bitSet := &b.bits[i] + if cmpBitset.And(&cmpBitset, &zeroBitSet).And(bitSet, &mask).Cmp(&mask) != 0 { + continue + } + + if !yield(b.entities[i]) { + return + } + } +} + +func (b *ComponentBitSet) AllSet(entity Entity, yield func(ComponentId) bool) { + bitsId, ok := b.lookup[entity] + assert.True(ok, "entity not found") + + bitSet := b.bits[bitsId].Bits() + var id ComponentId + + for i := range bitSet { + v := uint(bitSet[i]) + for v != 0 { + index := bits.TrailingZeros(v) + v &^= 1 << index + id = ComponentId(i*bits.UintSize + index) + if !yield(id) { + return + } + } + } +} diff --git a/pkg/ecs/component-bool-table.go b/pkg/ecs/component-bool-table.go new file mode 100644 index 00000000..1ade7594 --- /dev/null +++ b/pkg/ecs/component-bool-table.go @@ -0,0 +1,106 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package ecs + +import ( + "github.com/negrel/assert" +) + +//const ( +// pageSizeShift = 10 +// pageSize = 1 << pageSizeShift +// initialBookSize = 1 // Starting with a small initial book size +//) + +func NewComponentBoolTable(maxComponentsLen int) ComponentBoolTable { + return ComponentBoolTable{ + bools: make([][]bool, 0, initialBookSize), + lookup: make(map[Entity]int, initialBookSize), + bytesArraySize: maxComponentsLen, + } +} + +type ComponentBoolTable struct { + bools [][]bool + lookup map[Entity]int + length int + bytesArraySize int +} + +func (b *ComponentBoolTable) Create(entity Entity) { + boolsId, ok := b.lookup[entity] + if !ok { + b.extend() + boolsId = b.length + b.lookup[entity] = boolsId + b.length += b.bytesArraySize + } +} + +// Set sets the bit at the given index to 1. +func (b *ComponentBoolTable) Set(entity Entity, componentId ComponentId) { + boolsId, ok := b.lookup[entity] + if !ok { + b.extend() + boolsId = b.length + b.lookup[entity] = boolsId + b.length += b.bytesArraySize + } + chunkId := boolsId >> pageSizeShift + index := boolsId % pageSize + b.bools[chunkId][index+int(componentId)] = true +} + +// Unset clears the bit at the given index (sets it to 0). +func (b *ComponentBoolTable) Unset(entity Entity, componentId ComponentId) { + boolsId, ok := b.lookup[entity] + assert.True(ok, "entity not found") + chunkId := boolsId >> pageSizeShift + index := boolsId % pageSize + b.bools[chunkId][index+int(componentId)] = false +} + +func (b *ComponentBoolTable) Test(entity Entity, componentId ComponentId) bool { + boolsId, ok := b.lookup[entity] + if !ok { + return false + } + chunkId := boolsId >> pageSizeShift + index := boolsId % pageSize + return b.bools[chunkId][index+int(componentId)] +} + +func (b *ComponentBoolTable) extend() { + lastChunkId := b.length >> pageSizeShift + if lastChunkId == len(b.bools) && b.length%pageSize == 0 { + b.bools = append(b.bools, make([]bool, b.bytesArraySize*pageSize)) + } +} + +func (b *ComponentBoolTable) AllSet(entity Entity, yield func(ComponentId) bool) { + boolsId, ok := b.lookup[entity] + if !ok { + return + } + chunkId := boolsId >> pageSizeShift + index := boolsId % pageSize + for i := 0; i < b.bytesArraySize; i++ { + if b.bools[chunkId][index+i] { + if !yield(ComponentId(i)) { + return + } + } + } +} diff --git a/pkg/ecs/component-bool-table_test.go b/pkg/ecs/component-bool-table_test.go new file mode 100644 index 00000000..6fb5ed7a --- /dev/null +++ b/pkg/ecs/component-bool-table_test.go @@ -0,0 +1,62 @@ +package ecs + +import "testing" + +func TestComponentByteTable_SetAndTest(t *testing.T) { + // ...existing code... + table := NewComponentBoolTable(10) + entity := Entity(1) + table.Set(entity, ComponentId(3)) + if !table.Test(entity, ComponentId(3)) { + t.Errorf("Expected component 3 to be set for entity %d", entity) + } +} + +func TestComponentByteTable_Unset(t *testing.T) { + // ...existing code... + table := NewComponentBoolTable(10) + entity := Entity(2) + table.Set(entity, ComponentId(5)) + if !table.Test(entity, ComponentId(5)) { + t.Errorf("Expected component 5 to be set for entity %d", entity) + } + table.Unset(entity, ComponentId(5)) + if table.Test(entity, ComponentId(5)) { + t.Errorf("Expected component 5 to be unset for entity %d", entity) + } +} + +func TestComponentByteTable_AllSet(t *testing.T) { + // ...existing code... + table := NewComponentBoolTable(10) + entity := Entity(3) + components := []ComponentId{2, 4, 7} + for _, id := range components { + table.Set(entity, id) + } + + var got []ComponentId + table.AllSet(entity, func(id ComponentId) bool { + got = append(got, id) + return true + }) + + if len(got) != len(components) { + t.Errorf("Expected %d components, got %d", len(components), len(got)) + } +} + +func TestComponentByteTable_MultipleEntities(t *testing.T) { + // ...existing code... + table := NewComponentBoolTable(10) + for i := 1; i <= 5; i++ { + e := Entity(i) + table.Set(e, ComponentId(i)) + } + for i := 1; i <= 5; i++ { + e := Entity(i) + if !table.Test(e, ComponentId(i)) { + t.Errorf("Entity %d should have component %d set", e, i) + } + } +} diff --git a/pkg/ecs/component-manager-shared.go b/pkg/ecs/component-manager-shared.go new file mode 100644 index 00000000..1e7399c1 --- /dev/null +++ b/pkg/ecs/component-manager-shared.go @@ -0,0 +1,303 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +package ecs + +import ( + "gomp/pkg/worker" + "sync" + + "github.com/negrel/assert" +) + +type SharedComponentInstanceId uint16 + +var _ AnyComponentManagerPtr = &SharedComponentManager[any]{} + +func NewSharedComponentManager[T any](id ComponentId) SharedComponentManager[T] { + newManager := SharedComponentManager[T]{ + components: NewPagedArray[T](), + instances: NewPagedArray[SharedComponentInstanceId](), + instanceToComponent: NewPagedMap[SharedComponentInstanceId, int](), + entityToComponent: NewPagedMap[Entity, int](), + entities: NewPagedArray[Entity](), + references: NewPagedArray[SharedComponentInstanceId](), + lookup: NewPagedMap[Entity, int](), + + id: id, + isInitialized: true, + + TrackChanges: false, + createdEntities: NewPagedArray[Entity](), + patchedEntities: NewPagedArray[Entity](), + deletedEntities: NewPagedArray[Entity](), + } + + return newManager +} + +type SharedComponentManager[T any] struct { + mx sync.Mutex + components PagedArray[T] + instances PagedArray[SharedComponentInstanceId] + instanceToComponent PagedMap[SharedComponentInstanceId, int] // value is components array index + entityToComponent PagedMap[Entity, int] + + // for itter returning 2 values + entities PagedArray[Entity] + references PagedArray[SharedComponentInstanceId] + lookup PagedMap[Entity, int] + + entityManager *EntityManager + entityComponentBitTable *ComponentBitTable + pool *worker.Pool + + id ComponentId + isInitialized bool + + // Patch + + TrackChanges bool // Enable TrackChanges to track changes and add them to patch + createdEntities PagedArray[Entity] + patchedEntities PagedArray[Entity] + deletedEntities PagedArray[Entity] + + encoder func([]T) []byte + decoder func([]byte) []T +} + +func (c *SharedComponentManager[T]) registerWorkerPool(pool *worker.Pool) { + c.pool = pool +} + +func (c *SharedComponentManager[T]) PatchAdd(entity Entity) { + //TODO implement me + panic("implement me") +} + +func (c *SharedComponentManager[T]) PatchGet() ComponentPatch { + //TODO implement me + panic("implement me") +} + +func (c *SharedComponentManager[T]) PatchApply(patch ComponentPatch) { + //TODO implement me + panic("implement me") +} + +func (c *SharedComponentManager[T]) PatchReset() { + //TODO implement me + panic("implement me") +} + +func (c *SharedComponentManager[T]) Id() ComponentId { + return c.id +} + +func (c *SharedComponentManager[T]) registerEntityManager(entityManager *EntityManager) { + c.entityManager = entityManager + c.entityComponentBitTable = &entityManager.componentBitTable +} + +//===================================== +//===================================== +//===================================== + +// Create an instance of shared component +func (c *SharedComponentManager[T]) Create(instanceId SharedComponentInstanceId, value T) *T { + c.mx.Lock() + defer c.mx.Unlock() + + c.assertBegin() + defer c.assertEnd() + + componentIndex := c.components.Len() + component := c.components.Append(value) + c.instances.Append(instanceId) + c.instanceToComponent.Set(instanceId, componentIndex) + + return component +} + +func (c *SharedComponentManager[T]) Get(entity Entity) (component *T) { + assert.True(c.isInitialized, "SharedComponentManager should be created with SharedNewComponentManager()") + index, exists := c.entityToComponent.Get(entity) + if !exists { + return nil + } + component = c.components.Get(index) + return component +} + +func (c *SharedComponentManager[T]) GetComponentByInstance(instanceId SharedComponentInstanceId) (component *T) { + assert.True(c.isInitialized, "SharedComponentManager should be created with SharedNewComponentManager()") + index, exists := c.instanceToComponent.Get(instanceId) + if !exists { + return nil + } + component = c.components.Get(index) + return component +} + +func (c *SharedComponentManager[T]) GetInstanceByEntity(entity Entity) (SharedComponentInstanceId, bool) { + assert.True(c.isInitialized, "SharedComponentManager should be created with SharedNewComponentManager()") + index, exists := c.lookup.Get(entity) + if !exists { + return 0, false + } + instanceId := *c.instances.Get(index) + return instanceId, true +} + +func (c *SharedComponentManager[T]) Set(entity Entity, instanceId SharedComponentInstanceId) *T { + assert.True(c.isInitialized, "SharedComponentManager should be created with SharedNewComponentManager()") + index, exists := c.lookup.Get(entity) + if exists { + c.references.Set(index, instanceId) + } else { + newIndex := c.entities.Len() + c.entities.Append(entity) + c.references.Append(instanceId) + c.lookup.Set(entity, newIndex) + } + componentIndex, _ := c.instanceToComponent.Get(instanceId) + c.entityToComponent.Set(entity, componentIndex) + c.patchedEntities.Append(entity) + c.entityComponentBitTable.Set(entity, c.id) + return c.components.Get(componentIndex) +} + +func (c *SharedComponentManager[T]) Delete(entity Entity) { + c.mx.Lock() + defer c.mx.Unlock() + + c.assertBegin() + defer c.assertEnd() + + index, _ := c.lookup.Get(entity) + + lastIndex := c.references.Len() - 1 + if index < lastIndex { + // Swap the dead element with the last one + c.references.Swap(index, lastIndex) + newSwappedEntityId, _ := c.entities.Swap(index, lastIndex) + assert.True(newSwappedEntityId != nil) + + // Update the lookup table + c.lookup.Set(*newSwappedEntityId, index) + } + + // Shrink the container + c.references.SoftReduce() + c.entities.SoftReduce() + + c.lookup.Delete(entity) + c.entityToComponent.Delete(entity) + c.entityComponentBitTable.Unset(entity, c.id) + + c.deletedEntities.Append(entity) +} + +func (c *SharedComponentManager[T]) Has(entity Entity) bool { + _, ok := c.lookup.Get(entity) + return ok +} + +func (c *SharedComponentManager[T]) Len() int { + assert.True(c.isInitialized, "SharedComponentManager should be created with CreateComponentService()") + return c.entities.Len() +} + +func (c *SharedComponentManager[T]) Clean() { + // c.entityComponentBitSet.Clean() + //c.components.Clean() + // c.Entities.Clean() +} + +// ======================================================== +// Iterators +// ======================================================== + +func (c *SharedComponentManager[T]) EachComponent(yield func(*T) bool) { + c.assertBegin() + defer c.assertEnd() + c.components.EachData(yield) +} + +func (c *SharedComponentManager[T]) EachEntity(yield func(Entity) bool) { + c.assertBegin() + defer c.assertEnd() + c.entities.EachDataValue(yield) +} + +func (c *SharedComponentManager[T]) Each() func(yield func(Entity, *T) bool) { + c.assertBegin() + defer c.assertEnd() + return func(yield func(Entity, *T) bool) { + c.components.Each()(func(i int, d *T) bool { + entity := c.entities.Get(i) + entId := *entity + shouldContinue := yield(entId, d) + return shouldContinue + }) + } +} + +// ======================================================== +// Iterators Parallel +// ======================================================== + +func (c *SharedComponentManager[T]) ProcessComponents(handler func(*T, worker.WorkerId)) { + c.assertBegin() + defer c.assertEnd() + c.components.ProcessData(handler, c.pool) +} + +func (c *SharedComponentManager[T]) EachEntityParallel(handler func(Entity, worker.WorkerId)) { + c.assertBegin() + defer c.assertEnd() + c.entities.ProcessDataValue(handler, c.pool) +} + +func (c *SharedComponentManager[T]) EachParallel(numWorkers int) func(yield func(Entity, *T, int) bool) { + c.assertBegin() + defer c.assertEnd() + return func(yield func(Entity, *T, int) bool) { + c.components.EachParallel(numWorkers)(func(i int, t *T, workerId int) bool { + entity := c.entities.Get(i) + entId := *entity + shouldContinue := yield(entId, t, workerId) + return shouldContinue + }) + } +} + +// ======================================================== +// Patches +// ======================================================== + +func (c *SharedComponentManager[T]) IsTrackingChanges() bool { + return c.TrackChanges +} + +// ======================================================== +// Utils +// ======================================================== + +func (c *SharedComponentManager[T]) RawComponents(ptr []T) { + c.components.Raw(ptr) +} + +func (c *SharedComponentManager[T]) assertBegin() { + assert.True(c.isInitialized, "SharedComponentManager should be created with SharedNewComponentManager()") + assert.True(c.components.Len() == c.lookup.Len(), "Lookup Count must always be the same as the number of components!") + assert.True(c.entities.Len() == c.components.Len(), "Entity Count must always be the same as the number of components!") +} + +func (c *SharedComponentManager[T]) assertEnd() { + assert.True(c.components.Len() == c.lookup.Len(), "Lookup Count must always be the same as the number of components!") + assert.True(c.entities.Len() == c.components.Len(), "Entity Count must always be the same as the number of components!") +} diff --git a/pkg/ecs/component-manager.go b/pkg/ecs/component-manager.go index e63b1d7d..09785e38 100644 --- a/pkg/ecs/component-manager.go +++ b/pkg/ecs/component-manager.go @@ -2,77 +2,156 @@ This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +<- Hininn Donated 2 000 RUB +<- Сосисочник Паша Donated 77 RUB +<- mitwelve Donated 1 000 RUB + +Thank you for your support! */ package ecs import ( - "iter" - "math/big" - "sync" - "github.com/negrel/assert" + "gomp/pkg/worker" + "sync" ) -type ComponentManagerX[T any] struct { - mx *sync.Mutex - components *PagedArray[T] - entities *PagedArray[Entity] - lookup *PagedMap[Entity, int32] - ID ComponentID +// ================ +// Contracts +// ================ + +type ComponentId uint16 +type AnyComponentList interface{} +type AnyComponentListPtr interface{} + +type AnyComponentManagerPtr interface { + Id() ComponentId + Delete(Entity) + Clean() + Has(Entity) bool + PatchAdd(Entity) + PatchGet() ComponentPatch + PatchApply(patch ComponentPatch) + PatchReset() + IsTrackingChanges() bool + registerEntityManager(*EntityManager) + registerWorkerPool(*worker.Pool) +} + +// ================ +// Service +// ================ + +var _ AnyComponentManagerPtr = &ComponentManager[any]{} + +func NewComponentManager[T any](id ComponentId) ComponentManager[T] { + newManager := ComponentManager[T]{ + components: NewPagedArray[T](), + entities: NewPagedArray[Entity](), + lookup: NewPagedMap[Entity, int](), + + id: id, + isInitialized: true, + + TrackChanges: false, + createdEntities: NewPagedArray[Entity](), + patchedEntities: NewPagedArray[Entity](), + deletedEntities: NewPagedArray[Entity](), + } + + return newManager +} + +type ComponentManager[T any] struct { + components PagedArray[T] + entities PagedArray[Entity] + lookup PagedMap[Entity, int] + + entityManager *EntityManager + entityComponentBitTable *ComponentBitTable + + id ComponentId isInitialized bool + + pool *worker.Pool + + // Patch + + TrackChanges bool // Enable TrackChanges to track changes and add them to patch + createdEntities PagedArray[Entity] + patchedEntities PagedArray[Entity] + deletedEntities PagedArray[Entity] + + encoder func([]T) []byte + decoder func([]byte) []T + + mx sync.Mutex } -type ComponentSeviceX[T any] struct { - id ComponentID - managers map[*World]*ComponentManagerX[T] +// ComponentChanges with byte encoded Components +type ComponentChanges struct { + Len int + Components []byte + Entities []Entity } -func (m *ComponentSeviceX[T]) Instance(world *World) *ComponentManagerX[T] { - instance, ok := m.managers[world] - assert.True(ok) - return instance +// ComponentPatch with byte encoded Created, Patched and Deleted components +type ComponentPatch struct { + ID ComponentId + Created ComponentChanges + Patched ComponentChanges + Deleted ComponentChanges } -func CreateComponentServiceX[T any](id ComponentID) *ComponentSeviceX[T] { - return &ComponentSeviceX[T]{ - id: id, - managers: make(map[*World]*ComponentManagerX[T]), - } +func (c *ComponentManager[T]) Id() ComponentId { + return c.id } -func (c *ComponentManagerX[T]) registerComponentMask(mask *ComponentManagerX[big.Int]) { - // c.worldMask = mask +func (c *ComponentManager[T]) registerEntityManager(entityManager *EntityManager) { + c.entityManager = entityManager + c.entityComponentBitTable = &entityManager.componentBitTable } -func (c *ComponentManagerX[T]) getId() ComponentID { - return c.ID +func (c *ComponentManager[T]) registerWorkerPool(pool *worker.Pool) { + c.pool = pool } -func (c *ComponentManagerX[T]) Create(entity Entity, value T) (returnValue *T) { +//===================================== +//===================================== +//===================================== + +func (c *ComponentManager[T]) Create(entity Entity, value T) (component *T) { c.mx.Lock() defer c.mx.Unlock() - // ComponentManager must be initialized with CreateComponentManager() - assert.True(c.isInitialized) - - // Only one of component per enity allowed! - assert.False(c.Has(entity)) - - // Entity Count must always be the same as the number of components! - assert.True(c.entities.Len() == c.components.Len()) - assert.True(c.components.Len() == c.lookup.Len()) + assert.False(c.Has(entity), "Only one of component per entity allowed!") + c.assertBegin() + defer c.assertEnd() var index = c.components.Len() c.lookup.Set(entity, index) c.entities.Append(entity) - return c.components.Append(value) + component = c.components.Append(value) + + c.entityComponentBitTable.Set(entity, c.id) + + //c.createdEntities.Append(entity) + + return component } -func (c *ComponentManagerX[T]) Get(entity Entity) *T { - // ComponentManager must be initialized with CreateComponentManager() - assert.True(c.isInitialized) +/* +GetUnsafe - is not thread safe. DO NOT store the pointer to the value anywhere, because it might be changed anytime with Create or Delete operations. +*/ +func (c *ComponentManager[T]) GetUnsafe(entity Entity) (component *T) { + assert.True(c.isInitialized, "ComponentManager should be created with NewComponentManager()") index, ok := c.lookup.Get(entity) if !ok { @@ -82,21 +161,51 @@ func (c *ComponentManagerX[T]) Get(entity Entity) *T { return c.components.Get(index) } -func (c *ComponentManagerX[T]) Remove(entity Entity) { +func (c *ComponentManager[T]) Get(entity Entity) (component T, ok bool) { c.mx.Lock() defer c.mx.Unlock() - // ComponentManager must be initialized with CreateComponentManager() - assert.True(c.isInitialized) + assert.True(c.isInitialized, "ComponentManager should be created with NewComponentManager()") + + index, ok := c.lookup.Get(entity) + if !ok { + return component, false + } + + return c.components.GetValue(index), true +} + +func (c *ComponentManager[T]) Set(entity Entity, value T) *T { + c.mx.Lock() + defer c.mx.Unlock() + + assert.True(c.isInitialized, "ComponentManager should be created with NewComponentManager()") + + index, ok := c.lookup.Get(entity) + if !ok { + return nil + } + + component := c.components.Set(index, value) + + c.patchedEntities.Append(entity) + + return component +} + +func (c *ComponentManager[T]) Delete(entity Entity) { + c.mx.Lock() + defer c.mx.Unlock() - // ENTITY HAS NO COMPONENT! - assert.True(c.Has(entity)) + c.assertBegin() + defer c.assertEnd() - index, _ := c.lookup.Get(entity) + index, exists := c.lookup.Get(entity) + assert.True(exists, "Entity does not have component") lastIndex := c.components.Len() - 1 if index < lastIndex { - // Swap the the dead element with the last one + // Swap the dead element with the last one c.components.Swap(index, lastIndex) newSwappedEntityId, _ := c.entities.Swap(index, lastIndex) assert.True(newSwappedEntityId != nil) @@ -110,94 +219,209 @@ func (c *ComponentManagerX[T]) Remove(entity Entity) { c.entities.SoftReduce() c.lookup.Delete(entity) + c.entityComponentBitTable.Unset(entity, c.id) - // Entity Count must always be the same as the number of components! - assert.True(c.entities.Len() == c.components.Len()) - assert.True(c.components.Len() == c.lookup.Len()) + //c.deletedEntities.Append(entity) } -func (c *ComponentManagerX[T]) Has(entity Entity) bool { +func (c *ComponentManager[T]) Has(entity Entity) bool { _, ok := c.lookup.Get(entity) return ok } -func (c *ComponentManagerX[T]) All(yield func(Entity, *T) bool) { - // ComponentManager must be initialized with CreateComponentManager() - assert.True(c.isInitialized) - - // Entity Count must always be the same as the number of components! - assert.True(c.entities.Len() == c.components.Len()) - assert.True(c.components.Len() == c.lookup.Len()) - - nextData, stopData := iter.Pull(c.components.AllData) - defer stopData() - - nextEntity, stopEntity := iter.Pull(c.entities.AllData) - defer stopEntity() - - for { - data, ok := nextData() - if !ok { - break - } - entityId, ok := nextEntity() - if !ok { - break - } - assert.True(entityId != nil) - entId := *entityId - shouldContinue := yield(entId, data) - if !shouldContinue { - break - } +func (c *ComponentManager[T]) Len() int { + assert.True(c.isInitialized, "ComponentManager should be created with CreateComponentService()") + return c.components.Len() +} + +func (c *ComponentManager[T]) Clean() { + // c.entityComponentBitSet.Clean() + //c.components.Clean() + // c.Entities.Clean() +} + +// ======================================================== +// Iterators +// ======================================================== + +func (c *ComponentManager[T]) EachComponent(yield func(entity *T) bool) { + c.assertBegin() + defer c.assertEnd() + c.components.EachData(yield) +} + +func (c *ComponentManager[T]) EachEntity(yield func(entity Entity) bool) { + c.assertBegin() + defer c.assertEnd() + c.entities.EachDataValue(yield) +} + +func (c *ComponentManager[T]) Each() func(yield func(entity Entity, component *T) bool) { + c.assertBegin() + defer c.assertEnd() + return func(yield func(entity Entity, component *T) bool) { + c.components.Each()(func(i int, d *T) bool { + entity := c.entities.Get(i) + entId := *entity + shouldContinue := yield(entId, d) + return shouldContinue + }) } } -func (c *ComponentManagerX[T]) AllParallel(yield func(Entity, *T) bool) { - // ComponentManager must be initialized with CreateComponentManager() - assert.True(c.isInitialized) +// ======================================================== +// Iterators Parallel +// ======================================================== - // Entity Count must always be the same as the number of components! - assert.True(c.entities.Len() == c.components.Len()) - assert.True(c.components.Len() == c.lookup.Len()) +func (c *ComponentManager[T]) ProcessEntities(handler func(Entity, worker.WorkerId)) { + c.assertBegin() + defer c.assertEnd() + c.entities.ProcessDataValue(handler, c.pool) +} - c.components.AllParallel(func(i int32, t *T) bool { - entId := c.entities.Get(i) - assert.True(entId != nil) - return yield(*entId, t) - }) +func (c *ComponentManager[T]) ProcessComponents(handler func(*T, worker.WorkerId)) { + c.assertBegin() + defer c.assertEnd() + c.components.ProcessData(handler, c.pool) } -func (c *ComponentManagerX[T]) AllData(yield func(*T) bool) { - // ComponentManager must be initialized with CreateComponentManager() - assert.True(c.isInitialized) +func (c *ComponentManager[T]) EachParallel(numWorkers int) func(yield func(Entity, *T, int) bool) { + c.assertBegin() + defer c.assertEnd() + return func(yield func(Entity, *T, int) bool) { + c.components.EachParallel(numWorkers)(func(i int, t *T, workerId int) bool { + entity := c.entities.Get(i) + entId := *entity + shouldContinue := yield(entId, t, workerId) + return shouldContinue + }) + } +} - // Entity Count must always be the same as the number of components! - assert.True(c.entities.Len() == c.components.Len()) - assert.True(c.components.Len() == c.lookup.Len()) +func (c *ComponentManager[T]) EachEntityParallel(handler func(Entity, worker.WorkerId)) { + c.assertBegin() + defer c.assertEnd() + c.entities.EachDataValueParallel(handler, c.pool) +} - c.components.AllData(yield) +func (c *ComponentManager[T]) EachComponentParallel(handler func(*T, worker.WorkerId)) { + c.assertBegin() + defer c.assertEnd() + c.components.EachDataParallel(handler, c.pool) } -func (c *ComponentManagerX[T]) AllDataParallel(yield func(*T) bool) { - // ComponentManager must be initialized with CreateComponentManager() - assert.True(c.isInitialized) +// ======================================================== +// Patches +// ======================================================== - // Entity Count must always be the same as the number of components! - assert.True(c.entities.Len() == c.components.Len()) - assert.True(c.components.Len() == c.lookup.Len()) +func (c *ComponentManager[T]) PatchAdd(entity Entity) { + assert.True(c.TrackChanges) + c.patchedEntities.Append(entity) +} - c.components.AllDataParallel(yield) +func (c *ComponentManager[T]) PatchGet() ComponentPatch { + assert.True(c.TrackChanges) + patch := ComponentPatch{ + ID: c.id, + Created: c.getChangesBinary(&c.createdEntities), + Patched: c.getChangesBinary(&c.patchedEntities), + Deleted: c.getChangesBinary(&c.deletedEntities), + } + return patch } -func (c *ComponentManagerX[T]) Len() int32 { - // ComponentManager must be initialized with CreateComponentManager() - assert.True(c.isInitialized) +func (c *ComponentManager[T]) PatchApply(patch ComponentPatch) { + assert.True(c.TrackChanges) + assert.True(patch.ID == c.id) + assert.True(c.decoder != nil) - return c.components.Len() + var components []T + + created := patch.Created + components = c.decoder(created.Components) + for i := range created.Len { + c.Create(created.Entities[i], components[i]) + } + + patched := patch.Patched + components = c.decoder(patched.Components) + for i := range patched.Len { + c.Set(patched.Entities[i], components[i]) + } + + deleted := patch.Deleted + components = c.decoder(deleted.Components) + for i := range deleted.Len { + c.Delete(deleted.Entities[i]) + } +} + +func (c *ComponentManager[T]) PatchReset() { + assert.True(c.TrackChanges) + c.createdEntities.Reset() + c.patchedEntities.Reset() + c.deletedEntities.Reset() +} + +func (c *ComponentManager[T]) getChangesBinary(source *PagedArray[Entity]) ComponentChanges { + changesLen := source.Len() + + components := make([]T, 0, changesLen) + entities := make([]Entity, 0, changesLen) + + source.EachData(func(e *Entity) bool { + assert.True(e != nil) + entId := *e + assert.True(c.Has(entId)) + components = append(components, *c.GetUnsafe(entId)) + entities = append(entities, entId) + return true + }) + + assert.True(c.encoder != nil) + + componentsBinary := c.encoder(components) + + return ComponentChanges{ + Len: changesLen, + Components: componentsBinary, + Entities: entities, + } +} + +func (c *ComponentManager[T]) SetEncoder(function func(components []T) []byte) *ComponentManager[T] { + c.encoder = function + return c +} + +func (c *ComponentManager[T]) SetDecoder(function func(data []byte) []T) *ComponentManager[T] { + c.decoder = function + return c +} + +func (c *ComponentManager[T]) IsTrackingChanges() bool { + return c.TrackChanges +} + +// ======================================================== +// Utils +// ======================================================== + +func (c *ComponentManager[T]) RawComponents(ptr []T) []T { + return c.components.Raw(ptr) +} + +func (c *ComponentManager[T]) RawEntities(ptr []Entity) []Entity { + return c.entities.Raw(ptr) +} + +func (c *ComponentManager[T]) assertBegin() { + assert.True(c.isInitialized, "ComponentManager should be created with NewComponentManager()") + assert.True(c.components.Len() == c.lookup.Len(), "Lookup Count must always be the same as the number of components!") + assert.True(c.entities.Len() == c.components.Len(), "Entity Count must always be the same as the number of components!") } -func (c *ComponentManagerX[T]) Clean() { - // c.components.Clean() - // c.entities.Clean() +func (c *ComponentManager[T]) assertEnd() { + assert.True(c.components.Len() == c.lookup.Len(), "Lookup Count must always be the same as the number of components!") + assert.True(c.entities.Len() == c.components.Len(), "Entity Count must always be the same as the number of components!") } diff --git a/pkg/ecs/component-table_bench_test.go b/pkg/ecs/component-table_bench_test.go new file mode 100644 index 00000000..2265ad71 --- /dev/null +++ b/pkg/ecs/component-table_bench_test.go @@ -0,0 +1,122 @@ +package ecs + +import "testing" + +const testEntitiesLen Entity = 100_000 +const maxComponentsLen = 1024 + +func BenchmarkComponentBitTable_SetAndTest(b *testing.B) { + // using a fixed maximum components length + // create and extend the table + table := NewComponentBitTable(maxComponentsLen) + for i := Entity(0); i < testEntitiesLen; i++ { + comp := ComponentId(i % maxComponentsLen) + table.Create(i) + table.Set(i, comp) + if !table.Test(i, comp) { + b.Fatalf("BitTable: expected entity %d to have component %d set", i, comp) + } + } + for i := Entity(0); i < testEntitiesLen; i++ { + table.Delete(i) + } + + b.ReportAllocs() + for b.Loop() { + for i := Entity(0); i < testEntitiesLen; i++ { + comp := ComponentId(i % maxComponentsLen) + table.Create(i) + table.Set(i, comp) + if !table.Test(i, comp) { + b.Fatalf("BitTable: expected entity %d to have component %d set", i, comp) + } + } + b.StopTimer() + for i := Entity(0); i < testEntitiesLen; i++ { + table.Delete(i) + } + b.StartTimer() + } +} + +func BenchmarkComponentBitTable_SetTestDelete(b *testing.B) { + table := NewComponentBitTable(maxComponentsLen) + //for i := Entity(1); i < testEntitiesLen; i++ { + // table.Create(Entity(i)) + //} + // using a fixed maximum components length + b.ReportAllocs() + for b.Loop() { + b.StopTimer() + for i := Entity(0); i < testEntitiesLen; i++ { + table.Create(i) + } + b.StartTimer() + + for i := Entity(0); i < testEntitiesLen; i++ { + comp := ComponentId(i % maxComponentsLen) + table.Set(i, comp) + if !table.Test(i, comp) { + b.Fatalf("BitTable: expected entity %d to have component %d set", i, comp) + } + table.Delete(i) + } + } +} + +func BenchmarkComponentBitTable_Delete(b *testing.B) { + // using a fixed maximum components length + table := NewComponentBitTable(maxComponentsLen) + // Setup - create and set components for all entities + for i := Entity(0); i < testEntitiesLen; i++ { + entity := Entity(i) + table.Create(entity) + table.Set(entity, ComponentId(i%maxComponentsLen)) + } + + b.ReportAllocs() + b.ResetTimer() + for b.Loop() { + // Delete all entities + for i := Entity(0); i < testEntitiesLen; i++ { + entity := Entity(i) + table.Delete(entity) + } + + // Recreate for next iteration + for i := Entity(0); i < testEntitiesLen; i++ { + entity := Entity(i) + table.Create(entity) + table.Set(entity, ComponentId(i%maxComponentsLen)) + } + } +} + +func BenchmarkComponentBitTable_AllSet(b *testing.B) { + table := NewComponentBitTable(maxComponentsLen) + // Prepare entities with multiple components each + for i := Entity(0); i < testEntitiesLen; i++ { + entity := Entity(i) + table.Create(entity) + // Give each entity 100 components + for j := 0; j < 100; j++ { + table.Set(entity, ComponentId(j%maxComponentsLen)) + } + } + + b.ReportAllocs() + b.ResetTimer() + for b.Loop() { + for i := Entity(0); i < testEntitiesLen; i++ { + entity := Entity(i) + count := 0 + table.AllSet(entity, func(id ComponentId) bool { + count++ + return true + }) + if count != 100 { + b.Fatalf("Expected 3 components, got %d", count) + } + } + } +} diff --git a/pkg/ecs/component.go b/pkg/ecs/component.go deleted file mode 100644 index 33aba089..00000000 --- a/pkg/ecs/component.go +++ /dev/null @@ -1,390 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ - -package ecs - -import ( - "fmt" - "math/big" - "sync" - - "github.com/negrel/assert" -) - -// ================ -// Contracts -// ================ - -type AnyComponentServicePtr interface { - register(*World) AnyComponentManagerPtr - getId() ComponentID -} - -type AnyComponentManagerPtr interface { - registerComponentMask(mask *ComponentManager[big.Int]) - getId() ComponentID - Remove(Entity) - Clean() - Has(Entity) bool - PatchAdd(Entity) - PatchGet() ComponentPatch - PatchApply(patch ComponentPatch) - PatchReset() - IsTrackingChanges() bool -} - -// ================ -// Service -// ================ - -type ComponentService[T any] struct { - id ComponentID - managers map[*World]*ComponentManager[T] -} - -func (c *ComponentService[T]) GetManager(world *World) *ComponentManager[T] { - manager, ok := c.managers[world] - assert.True(ok, fmt.Sprintf("Component <%T> is not registered in <%s> world", c, world.Title)) - return manager -} - -func (c *ComponentService[T]) register(world *World) AnyComponentManagerPtr { - newManager := ComponentManager[T]{ - mx: new(sync.Mutex), - - components: NewPagedArray[T](), - entities: NewPagedArray[Entity](), - lookup: NewPagedMap[Entity, int32](), - - maskComponent: world.entityComponentMask, - id: c.id, - isInitialized: true, - - TrackChanges: false, - createdEntities: NewPagedArray[Entity](), - patchedEntities: NewPagedArray[Entity](), - deletedEntities: NewPagedArray[Entity](), - } - - c.managers[world] = &newManager - - return &newManager -} - -func (c *ComponentService[T]) getId() ComponentID { - return c.id -} - -// ================ -// Service -// ================ - -type ComponentManager[T any] struct { - mx *sync.Mutex - - components *PagedArray[T] - entities *PagedArray[Entity] - lookup *PagedMap[Entity, int32] - - maskComponent *SparseSet[ComponentBitArray256, Entity] - id ComponentID - isInitialized bool - - // Patch - - TrackChanges bool // Enable TrackChanges to track changes and add them to patch - createdEntities *PagedArray[Entity] - patchedEntities *PagedArray[Entity] - deletedEntities *PagedArray[Entity] - - encoder func([]T) []byte - decoder func([]byte) []T -} - -// ComponentChanges with byte encoded Components -type ComponentChanges struct { - Len int32 - Components []byte - Entities []Entity -} - -// ComponentPatch with byte encoded Created, Patched and Deleted components -type ComponentPatch struct { - ID ComponentID - Created ComponentChanges - Patched ComponentChanges - Deleted ComponentChanges -} - -func (c *ComponentManager[T]) getId() ComponentID { - return c.id -} - -func (c *ComponentManager[T]) registerComponentMask(*ComponentManager[big.Int]) { -} - -//===================================== -//===================================== -//===================================== - -func (c *ComponentManager[T]) Create(entity Entity, value T) (component *T) { - c.mx.Lock() - defer c.mx.Unlock() - - assert.True(c.isInitialized, "ComponentManager should be created with CreateComponentService()") - assert.False(c.Has(entity), "Only one of component per entity allowed!") - assert.True(c.components.Len() == c.lookup.Len(), "Lookup Count must always be the same as the number of components!") - assert.True(c.entities.Len() == c.components.Len(), "Entity Count must always be the same as the number of components!") - - var index = c.components.Len() - - c.lookup.Set(entity, index) - c.entities.Append(entity) - component = c.components.Append(value) - - mask := c.maskComponent.GetPtr(entity) - mask.Set(c.id) - - c.createdEntities.Append(entity) - - return component -} - -func (c *ComponentManager[T]) Get(entity Entity) (component *T) { - assert.True(c.isInitialized, "ComponentManager should be created with CreateComponentService()") - - index, ok := c.lookup.Get(entity) - if !ok { - return nil - } - - return c.components.Get(index) -} - -func (c *ComponentManager[T]) Set(entity Entity, value T) *T { - assert.True(c.isInitialized, "ComponentManager should be created with CreateComponentService()") - - index, ok := c.lookup.Get(entity) - if !ok { - return nil - } - - component := c.components.Set(index, value) - - c.patchedEntities.Append(entity) - - return component -} - -func (c *ComponentManager[T]) Remove(entity Entity) { - c.mx.Lock() - defer c.mx.Unlock() - - assert.True(c.isInitialized, "ComponentManager should be created with CreateComponentService()") - assert.True(c.components.Len() == c.lookup.Len(), "Lookup Count must always be the same as the number of components!") - assert.True(c.entities.Len() == c.components.Len(), "Entity Count must always be the same as the number of components!") - - index, _ := c.lookup.Get(entity) - - lastIndex := c.components.Len() - 1 - if index < lastIndex { - // Swap the dead element with the last one - c.components.Swap(index, lastIndex) - newSwappedEntityId, _ := c.entities.Swap(index, lastIndex) - assert.True(newSwappedEntityId != nil) - - // Update the lookup table - c.lookup.Set(*newSwappedEntityId, index) - } - - // Shrink the container - c.components.SoftReduce() - c.entities.SoftReduce() - - c.lookup.Delete(entity) - mask := c.maskComponent.GetPtr(entity) - mask.Unset(c.id) - - c.deletedEntities.Append(entity) - - assert.True(c.components.Len() == c.lookup.Len(), "Lookup Count must always be the same as the number of components!") - assert.True(c.entities.Len() == c.components.Len(), "Entity Count must always be the same as the number of components!") -} - -func (c *ComponentManager[T]) Has(entity Entity) bool { - _, ok := c.lookup.Get(entity) - return ok -} - -// Patches - -func (c *ComponentManager[T]) PatchAdd(entity Entity) { - assert.True(c.TrackChanges) - - c.patchedEntities.Append(entity) -} - -func (c *ComponentManager[T]) PatchGet() ComponentPatch { - assert.True(c.TrackChanges) - - patch := ComponentPatch{ - ID: c.id, - Created: c.getChangesBinary(c.createdEntities), - Patched: c.getChangesBinary(c.patchedEntities), - Deleted: c.getChangesBinary(c.deletedEntities), - } - - return patch -} - -func (c *ComponentManager[T]) PatchApply(patch ComponentPatch) { - assert.True(c.TrackChanges) - assert.True(patch.ID == c.id) - assert.True(c.decoder != nil) - - var components []T - - created := patch.Created - components = c.decoder(created.Components) - for i := range created.Len { - c.Create(created.Entities[i], components[i]) - } - - patched := patch.Patched - components = c.decoder(patched.Components) - for i := range patched.Len { - c.Set(patched.Entities[i], components[i]) - } - - deleted := patch.Deleted - components = c.decoder(deleted.Components) - for i := range deleted.Len { - c.Remove(deleted.Entities[i]) - } -} - -func (c *ComponentManager[T]) PatchReset() { - assert.True(c.TrackChanges) - - c.createdEntities.Reset() - c.patchedEntities.Reset() - c.deletedEntities.Reset() -} - -func (c *ComponentManager[T]) getChangesBinary(source *PagedArray[Entity]) ComponentChanges { - changesLen := source.Len() - - components := make([]T, 0, changesLen) - entities := make([]Entity, 0, changesLen) - - source.AllData(func(e *Entity) bool { - assert.True(e != nil) - entId := *e - assert.True(c.Has(entId)) - components = append(components, *c.Get(entId)) - entities = append(entities, entId) - return true - }) - - assert.True(c.encoder != nil) - - componentsBinary := c.encoder(components) - - return ComponentChanges{ - Len: changesLen, - Components: componentsBinary, - Entities: entities, - } -} - -func (c *ComponentManager[T]) SetEncoder(function func(components []T) []byte) *ComponentManager[T] { - c.encoder = function - return c -} - -func (c *ComponentManager[T]) SetDecoder(function func(data []byte) []T) *ComponentManager[T] { - c.decoder = function - return c -} - -func (c *ComponentManager[T]) IsTrackingChanges() bool { - return c.TrackChanges -} - -// Iterators - -func (c *ComponentManager[T]) All(yield func(Entity, *T) bool) { - assert.True(c.isInitialized, "ComponentManager should be created with CreateComponentService()") - - assert.True(c.components.Len() == c.lookup.Len(), "Lookup Count must always be the same as the number of components!") - assert.True(c.entities.Len() == c.components.Len(), "Entity Count must always be the same as the number of components!") - - c.components.All(func(i int32, d *T) bool { - assert.True(d != nil) - entity := c.entities.Get(i) - assert.True(entity != nil) - entId := *entity - shouldContinue := yield(entId, d) - return shouldContinue - }) - - assert.True(c.components.Len() == c.lookup.Len(), "Lookup Count must always be the same as the number of components!") - assert.True(c.entities.Len() == c.components.Len(), "Entity Count must always be the same as the number of components!") -} - -func (c *ComponentManager[T]) AllParallel(yield func(Entity, *T) bool) { - assert.True(c.isInitialized, "ComponentManager should be created with CreateComponentService()") - - assert.True(c.components.Len() == c.lookup.Len(), "Lookup Count must always be the same as the number of components!") - assert.True(c.entities.Len() == c.components.Len(), "Entity Count must always be the same as the number of components!") - - c.components.AllParallel(func(i int32, t *T) bool { - entity := c.entities.Get(i) - assert.True(entity != nil) - entId := *entity - shouldContinue := yield(entId, t) - return shouldContinue - }) - - assert.True(c.components.Len() == c.lookup.Len(), "Lookup Count must always be the same as the number of components!") - assert.True(c.entities.Len() == c.components.Len(), "Entity Count must always be the same as the number of components!") -} - -func (c *ComponentManager[T]) AllData(yield func(*T) bool) { - assert.True(c.isInitialized, "ComponentManager should be created with CreateComponentService()") - assert.True(c.components.Len() == c.lookup.Len(), "Lookup Count must always be the same as the number of components!") - assert.True(c.entities.Len() == c.components.Len(), "Entity Count must always be the same as the number of components!") - - c.components.AllData(yield) - - assert.True(c.components.Len() == c.lookup.Len(), "Lookup Count must always be the same as the number of components!") - assert.True(c.entities.Len() == c.components.Len(), "Entity Count must always be the same as the number of components!") -} - -func (c *ComponentManager[T]) AllDataParallel(yield func(*T) bool) { - assert.True(c.isInitialized, "ComponentManager should be created with CreateComponentService()") - assert.True(c.components.Len() == c.lookup.Len(), "Lookup Count must always be the same as the number of components!") - assert.True(c.entities.Len() == c.components.Len(), "Entity Count must always be the same as the number of components!") - - c.components.AllDataParallel(yield) - - assert.True(c.components.Len() == c.lookup.Len(), "Lookup Count must always be the same as the number of components!") - assert.True(c.entities.Len() == c.components.Len(), "Entity Count must always be the same as the number of components!") -} - -// Utils - -func (c *ComponentManager[T]) Len() int32 { - assert.True(c.isInitialized, "ComponentManager should be created with CreateComponentService()") - - return c.components.Len() -} - -func (c *ComponentManager[T]) Clean() { - c.maskComponent.Clean() - // c.components.Clean() - // c.entities.Clean() -} diff --git a/pkg/ecs/component_test.go b/pkg/ecs/component_test.go deleted file mode 100644 index 8b156a0c..00000000 --- a/pkg/ecs/component_test.go +++ /dev/null @@ -1,187 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ - -package ecs - -import ( - "image/color" - "math/rand" - "testing" -) - -type pixel struct { - x int32 - y int32 - hp int32 - color color.RGBA - breath bool -} - -var pixelComponentType = CreateComponentService[pixel](1) - -// Commonly used functions in both benchmarks. -func PrepareWorld(description string, system AnySystemServicePtr) *World { - world := CreateWorld(description) - - world.RegisterComponentServices( - &pixelComponentType, - ) - world.RegisterSystems(). - Parallel( - system, - ) - - return &world -} - -func InitPixelComponent(pixelComponent *ComponentManager[pixel], world *World) { - pixelComponent = pixelComponentType.GetManager(world) - determRand := rand.New(rand.NewSource(42)) - - for i := range 1000 { - for j := range 1000 { - newPixel := world.CreateEntity("Pixel") - - randomGreen := uint8(135 / (determRand.Intn(10) + 1)) - randomBlue := uint8(135 / (determRand.Intn(10) + 1)) - - randomColor := color.RGBA{ - G: randomGreen, - B: randomBlue, - A: 255, - } - pixelComponent.Create(newPixel, pixel{ - x: int32(j), - y: int32(i), - hp: 100, - color: randomColor, - }) - } - } -} - -type pixelSystem struct { - pixelComponent ComponentManager[pixel] -} - -func (s *pixelSystem) Init(world *World) { - InitPixelComponent(&s.pixelComponent, world) -} - -func (s *pixelSystem) Destroy(world *World) {} - -func (s *pixelSystem) Update(world *World) {} -func (s *pixelSystem) FixedUpdate(world *World) { - for pixel := range s.pixelComponent.AllData { - // Note: was not extracted to separate function to simulate - // real-world interaction between range loop and inner code. - color := &pixel.color - - if pixel.breath { - if color.G < 135 { - color.G++ - } else { - pixel.hp++ - } - if color.B < 135 { - color.B++ - } else { - pixel.hp++ - } - } else { - if color.G > 0 { - color.G-- - } else { - pixel.hp-- - } - if color.B > 0 { - color.B-- - } else { - pixel.hp-- - } - } - - if pixel.hp <= 0 { - pixel.breath = true - } else if pixel.hp >= 100 { - pixel.breath = false - } - } -} - -// Direct call iteration type -type pixelSystemDirectCall struct { - pixelComponent ComponentManager[pixel] -} - -func (s *pixelSystemDirectCall) Init(world *World) { - InitPixelComponent(&s.pixelComponent, world) -} - -func (s *pixelSystemDirectCall) Destroy(world *World) {} -func (s *pixelSystemDirectCall) Update(world *World) {} -func (s *pixelSystemDirectCall) FixedUpdate(world *World) { - s.pixelComponent.AllDataParallel(func(pixel *pixel) bool { - color := &pixel.color - - if pixel.breath { - if color.G < 135 { - color.G++ - } else { - pixel.hp++ - } - if color.B < 135 { - color.B++ - } else { - pixel.hp++ - } - } else { - if color.G > 0 { - color.G-- - } else { - pixel.hp-- - } - if color.B > 0 { - color.B-- - } else { - pixel.hp-- - } - } - - if pixel.hp <= 0 { - pixel.breath = true - } else if pixel.hp >= 100 { - pixel.breath = false - } - return true - }) -} - -// Note: amount of memory allocated changes between tests even with deterministic rand. -// Observed range 918063 B/op - 1108007 B/op -func BenchmarkRangeIteration(b *testing.B) { - pixelSys := CreateSystemService(new(pixelSystem)) - world := PrepareWorld("range iteration", &pixelSys) - - b.ReportAllocs() - b.ResetTimer() - for range b.N { - world.runSystemFunction(SystemFunctionFixedUpdate) - } -} - -// Note: amount of memory allocated changes between tests even with deterministic rand. -// Observed range 868437 B/op - 1047789 B/op -func BenchmarkDirectCallIteration(b *testing.B) { - pixelSys := CreateSystemService(new(pixelSystemDirectCall)) - world := PrepareWorld("direct call iteration", &pixelSys) - - b.ReportAllocs() - b.ResetTimer() - for range b.N { - world.runSystemFunction(SystemFunctionFixedUpdate) - } -} diff --git a/pkg/ecs/ecs.go b/pkg/ecs/ecs.go index 9a906718..3c30b148 100644 --- a/pkg/ecs/ecs.go +++ b/pkg/ecs/ecs.go @@ -5,50 +5,3 @@ with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package ecs - -import ( - "sync" - "time" -) - -type WorldID uint32 - -const ( - PREALLOC_DELETED_ENTITIES uint32 = 1 << 10 -) - -func CreateWorld(title string) World { - id := generateWorldID() - maskSet := NewSparseSet[ComponentBitArray256, Entity]() - - world := World{ - ID: id, - Title: title, - wg: new(sync.WaitGroup), - mx: new(sync.Mutex), - deletedEntityIDs: make([]Entity, 0, PREALLOC_DELETED_ENTITIES), - entityComponentMask: &maskSet, - lastUpdateAt: time.Now(), - lastFixedUpdateAt: time.Now(), - components: make(map[ComponentID]AnyComponentManagerPtr), - } - - return world -} - -func CreateComponentService[T any](id ComponentID) ComponentService[T] { - component := ComponentService[T]{ - id: id, - managers: make(map[*World]*ComponentManager[T]), - } - - return component -} - -func CreateSystemService[T AnySystemControllerPtr](controller T, dependsOn ...AnySystemServicePtr) SystemService[T] { - return SystemService[T]{ - initValue: controller, - dependsOn: dependsOn, - instances: make(map[*World]*SystemServiceInstance), - } -} diff --git a/pkg/ecs/entity-manager.go b/pkg/ecs/entity-manager.go new file mode 100644 index 00000000..35269fca --- /dev/null +++ b/pkg/ecs/entity-manager.go @@ -0,0 +1,176 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +<- MuTaToR Donated 250 RUB +<- Бодрящий член Donated 100 RUB +<- Plambirik Donated 5 000 RUB +<- Бодрящий член Donated 100 RUB +<- MuTaToR Donated 250 RUB +<- ksana_pro Donated 100 RUB +<- Skomich Donated 250 RUB +<- MuTaToR Donated 250 RUB +<- Бодрящий член Donated 100 RUB +<- мой код полная хуйня Donated 251 RUB +<- ksana_pro Donated 100 RUB +<- дубина Donated 250 RUB +<- WoWnik Donated 100 RUB +<- Vorobyan Donated 100 RUB +<- MuTaToR Donated 250 RUB +<- Мандовожка милана Donated 100 RUB +<- ksana_pro Donated 100 RUB +<- Зритель Donated 250 RUB +<- Ричард Donated 100 RUB +<- ksana_pro Donated 100 RUB +<- Ksana_pro Donated 100 RUB +<- Ksana_pro Donated 100 RUB +<- Ksana_pro Donated 100 RUB +<- Монтажер сука Donated 50 RUB + +Thank you for your support! +*/ + +package ecs + +import ( + "fmt" + "sync" + "sync/atomic" +) + +const ( + PREALLOC_DEFAULT uint32 = 1 << 10 +) + +func NewEntityManager() EntityManager { + entityManager := EntityManager{ + deletedEntityIDs: make([]Entity, 0, PREALLOC_DEFAULT), + components: make(map[ComponentId]AnyComponentManagerPtr), + } + + return entityManager +} + +type EntityManager struct { + lastId Entity + size uint32 + + groups map[string][]Entity + components map[ComponentId]AnyComponentManagerPtr + deletedEntityIDs []Entity + componentBitTable ComponentBitTable + mx sync.Mutex + + patch Patch +} + +type Patch []ComponentPatch + +func (e *EntityManager) Create() Entity { + e.mx.Lock() + defer e.mx.Unlock() + var newId = e.generateEntityID() + e.componentBitTable.Create(newId) + + e.size++ + + return newId +} + +func (e *EntityManager) Delete(entity Entity) { + e.mx.Lock() + defer e.mx.Unlock() + e.componentBitTable.AllSet(entity, func(id ComponentId) bool { + e.components[id].Delete(entity) + return true + }) + e.componentBitTable.Delete(entity) + + e.deletedEntityIDs = append(e.deletedEntityIDs, entity) + e.size-- +} + +func (e *EntityManager) Clean() { + for i := range e.components { + e.components[i].Clean() + } +} + +func (e *EntityManager) Size() uint32 { + return e.size +} + +func (e *EntityManager) LastId() Entity { + return e.lastId +} + +func (e *EntityManager) Destroy() { + e.Clean() +} + +func (e *EntityManager) PatchGet() Patch { + patch := e.patch + + for _, component := range e.components { + if !component.IsTrackingChanges() { + continue + } + + e.patch[component.Id()] = component.PatchGet() + } + + return patch +} + +func (e *EntityManager) PatchApply(patch Patch) { + for _, componentPatch := range patch { + component := e.components[componentPatch.ID] + if component == nil { + panic(fmt.Sprintf("Component %d does not exist", componentPatch.ID)) + } + + if !component.IsTrackingChanges() { + continue + } + + component.PatchApply(componentPatch) + } +} + +func (e *EntityManager) PatchReset() { + for i, component := range e.components { + if component == nil { + panic(fmt.Sprintf("Component %d does not exist", i)) + } + + if !component.IsTrackingChanges() { + continue + } + + component.PatchReset() + } +} + +func (e *EntityManager) init() { + e.componentBitTable = NewComponentBitTable(len(e.components)) + e.patch = make(Patch, len(e.components)) +} + +func (e *EntityManager) generateEntityID() (newId Entity) { + if len(e.deletedEntityIDs) == 0 { + newId = Entity(atomic.AddUint32((*entityType)(&e.lastId), 1)) + } else { + newId = e.deletedEntityIDs[len(e.deletedEntityIDs)-1] + e.deletedEntityIDs = e.deletedEntityIDs[:len(e.deletedEntityIDs)-1] + } + return newId +} + +func (e *EntityManager) registerComponent(c AnyComponentManagerPtr) { + e.components[c.Id()] = c +} diff --git a/pkg/ecs/entity.go b/pkg/ecs/entity.go new file mode 100644 index 00000000..6d6d83bb --- /dev/null +++ b/pkg/ecs/entity.go @@ -0,0 +1,46 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +<- Basserti Donated 250 RUB +<- ubiblasosnalim Donated 100 RUB + +Thank you for your support! +*/ + +package ecs + +import "github.com/negrel/assert" + +type EntityVersion uint + +type entityType = uint32 +type Entity entityType + +func (e *Entity) IsVersion(version EntityVersion) bool { + return e.GetVersion() == version +} + +func (e *Entity) SetVersion(version EntityVersion) { + assert.True(version <= MaxEntityGenerationId, "version is too high") + *e = Entity(entityType(*e) - entityType(e.GetVersion()<<(entityPower-generationPower)) | entityType(version)<<(entityPower-generationPower)) +} + +func (e *Entity) GetVersion() EntityVersion { + return EntityVersion(*e >> (entityPower - generationPower)) +} + +const ( + entityPower = 32 + generationPower = 2 + MaxEntityGenerationId EntityVersion = 1< 0 { + newLookup[g.keys[i]] = len(newCells) + newCells = append(newCells, cell) + newKeys = append(newKeys, g.keys[i]) + } + } + + // Rebuild entity lookup + g.lookupByEntity = make(map[Entity]int, len(g.lookupByEntity)) + for i, cell := range newCells { + for _, e := range cell.Entities { + g.lookupByEntity[e] = i + } + } + + g.cells = newCells + g.keys = newKeys + g.lookupByKey = newLookup +} + +func (g *SpatialGrid) BoundingBoxesIntersect(x1, y1, x2, y2 float64) bool { + // Fast AABB check using spatial grid cell size + dx := math.Abs(x1 - x2) + dy := math.Abs(y1 - y2) + return dx < g.cellSize*2 && dy < g.cellSize*2 +} + +func (g *SpatialGrid) GetActiveCells() []GridCell { + return g.cells +} diff --git a/pkg/ecs/int-log.go b/pkg/ecs/int-log.go index 929a8f82..42444808 100644 --- a/pkg/ecs/int-log.go +++ b/pkg/ecs/int-log.go @@ -8,6 +8,10 @@ package ecs import "math/bits" -func FastIntLog2(value int) int { - return bits.Len64(uint64(value)) - 1 +func FastIntLog2(value uint64) int { + return bits.Len64(value) - 1 +} + +func FastestIntLog2(value uint64) int { + return bits.LeadingZeros64(value) ^ 63 } diff --git a/pkg/ecs/int-log_test.go b/pkg/ecs/int-log_test.go index 62ebb2f2..5085b9be 100644 --- a/pkg/ecs/int-log_test.go +++ b/pkg/ecs/int-log_test.go @@ -8,28 +8,45 @@ package ecs import ( "math" + "math/bits" "testing" ) -func TesCalcIndex(t *testing.T) { - for i := 0; i <= 100_000_000; i++ { - value := i>>10 + 1 +func TestCalcIndex(t *testing.T) { + for i := uint64(0); i <= 100_000_000; i++ { + var value uint64 = i>>10 + 1 want := int(math.Log2(float64(value))) - have := FastIntLog2(value) + want2 := int(bits.LeadingZeros64(value) ^ 63) + have := bits.Len64(value) - 1 if want != have { t.Fatalf("i: %v, want: %v, got: %v", i, want, have) } + if want2 != have { + t.Fatalf("i: %v, want: %v, got: %v", i, want2, have) + } + } +} + +func BenchmarkFastestLog2(b *testing.B) { + var i uint64 = 1 + for b.Loop() { + _ = FastestIntLog2(i/10 + 1) + i++ } } -func BenchmarkFastLog2(t *testing.B) { - for range t.N { - _ = FastIntLog2(t.N/10 + 1) +func BenchmarkFastLog2(b *testing.B) { + var i uint64 = 1 + for b.Loop() { + _ = FastIntLog2(i/10 + 1) + i++ } } -func BenchmarkStdMathLog2(t *testing.B) { - for range t.N { - _ = int(math.Log2(float64(t.N/10 + 1))) +func BenchmarkStdMathLog2(b *testing.B) { + var i uint64 = 1 + for b.Loop() { + _ = math.Log2(float64(i/10 + 1)) + i++ } } diff --git a/pkg/ecs/lookup-bench_test.go b/pkg/ecs/lookup-bench_test.go new file mode 100644 index 00000000..fae2e727 --- /dev/null +++ b/pkg/ecs/lookup-bench_test.go @@ -0,0 +1,365 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package ecs + +import ( + "math/rand" + "sync" + "testing" +) + +const ( + keysNumberOfBases = 1 << 16 + keysOffsetRange = 2 << 16 +) + +// Generate a key that is close to others but spans a wide range +func generateKeys(numBases int) func() uint32 { + // Create an array of base keys + bases := make([]uint32, numBases) + for i := 0; i < numBases; i++ { + bases[i] = rand.Uint32() + } + + // Offset range (e.g., keys will be within ±1000 of a base) + offsetRange := uint32(keysOffsetRange) + + return func() uint32 { + // Randomly select a base key + base := bases[rand.Intn(numBases)] + // Generate a random offset within the range + offset := rand.Uint32() % offsetRange + // Randomly add or subtract the offset + if rand.Intn(2) == 0 { + return base + offset + } + return base - offset + } +} + +// Benchmark for default Go map Set with close keys (multiple bases) +func Benchmark_Map_Set(b *testing.B) { + b.ReportAllocs() + + m := make(map[uint32]int) + gen := generateKeys(keysNumberOfBases) // Use keysNumberOfBases base keys + keys := make([]uint32, b.N) + for i := 0; i < b.N; i++ { + keys[i] = gen() + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + m[keys[i]] = i + } +} + +// Benchmark for LookupMap Set with close keys (multiple bases) +func Benchmark_LookupMap_Set(b *testing.B) { + b.ReportAllocs() + + lm := &LookupMap[int]{} + gen := generateKeys(keysNumberOfBases) // Use keysNumberOfBases base keys + keys := make([]uint32, b.N) + for i := 0; i < b.N; i++ { + keys[i] = gen() + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + lm.Set(keys[i], i) + } +} + +// Benchmark for default Go map Get with close keys (multiple bases) +func Benchmark_Map_Get(b *testing.B) { + b.ReportAllocs() + + m := make(map[uint32]int) + gen := generateKeys(keysNumberOfBases) // Use keysNumberOfBases base keys + keys := make([]uint32, b.N) + for i := 0; i < b.N; i++ { + keys[i] = gen() + m[keys[i]] = i + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = m[keys[i]] + } +} + +// Benchmark for LookupMap Get with close keys (multiple bases) +func Benchmark_LookupMap_Get(b *testing.B) { + b.ReportAllocs() + + lm := &LookupMap[int]{} + gen := generateKeys(keysNumberOfBases) // Use keysNumberOfBases base keys + keys := make([]uint32, b.N) + for i := 0; i < b.N; i++ { + keys[i] = gen() + lm.Set(keys[i], i) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + lm.Get(keys[i]) + } +} + +// Benchmark for default Go map concurrent Set with close keys (multiple bases, with sync.Mutex) +func Benchmark_Map_Concurrent_Set(b *testing.B) { + b.ReportAllocs() + + m := make(map[uint32]int) + var mx sync.Mutex + gen := generateKeys(keysNumberOfBases) // Use keysNumberOfBases base keys + keys := make([]uint32, b.N) + for i := 0; i < b.N; i++ { + keys[i] = gen() + } + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + i := 0 + for pb.Next() { + mx.Lock() + m[keys[i]] = i + mx.Unlock() + i++ + } + }) +} + +// Benchmark for LookupMap concurrent Set with close keys (multiple bases) +func Benchmark_LookupMap_Concurrent_Set(b *testing.B) { + b.ReportAllocs() + + lm := &LookupMap[int]{} + gen := generateKeys(keysNumberOfBases) // Use keysNumberOfBases base keys + keys := make([]uint32, b.N) + for i := 0; i < b.N; i++ { + keys[i] = gen() + } + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + i := 0 + for pb.Next() { + lm.Set(keys[i], i) + i++ + } + }) +} + +// Benchmark for default Go map concurrent Get with close keys (multiple bases, with sync.RWMutex) +func Benchmark_Map_Concurrent_Get(b *testing.B) { + b.ReportAllocs() + + m := make(map[uint32]int) + var mx sync.RWMutex + gen := generateKeys(keysNumberOfBases) // Use keysNumberOfBases base keys + keys := make([]uint32, b.N) + for i := 0; i < b.N; i++ { + keys[i] = gen() + m[keys[i]] = i + } + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + i := 0 + for pb.Next() { + mx.RLock() + _ = m[keys[i]] + mx.RUnlock() + i++ + } + }) +} + +// Benchmark for LookupMap concurrent Get with close keys (multiple bases) +func Benchmark_LookupMap_Concurrent_Get(b *testing.B) { + b.ReportAllocs() + + lm := &LookupMap[int]{} + gen := generateKeys(keysNumberOfBases) // Use keysNumberOfBases base keys + keys := make([]uint32, b.N) + for i := 0; i < b.N; i++ { + keys[i] = gen() + lm.Set(keys[i], i) + } + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + i := 0 + for pb.Next() { + lm.Get(keys[i]) + i++ + } + }) +} + +func BenchmarkAll(b *testing.B) { + b.Run("Set", BenchmarkSet) + b.Run("Get", BenchmarkGet) + b.Run("Set_Concurrent", BenchmarkSetConcurrent) + b.Run("Get_Concurrent", BenchmarkGetConcurrent) +} + +func BenchmarkSet(b *testing.B) { + gen := generateKeys(keysNumberOfBases) // Use keysNumberOfBases base keys + b.Run("Map_Set", func(b *testing.B) { + keys := make([]uint32, b.N) + for i := 0; i < b.N; i++ { + keys[i] = gen() + } + b.ReportAllocs() + m := make(map[uint32]int) + b.ResetTimer() + for i := 0; i < len(keys); i++ { + m[keys[i]] = i + } + }) + b.Run("LookupMap_Set", func(b *testing.B) { + keys := make([]uint32, b.N) + for i := 0; i < b.N; i++ { + keys[i] = gen() + } + b.ReportAllocs() + lm := NewLookupMap[int]() + b.ResetTimer() + for i := 0; i < len(keys); i++ { + lm.Set(keys[i], i) + } + }) +} + +func BenchmarkGet(b *testing.B) { + gen := generateKeys(keysNumberOfBases) // Use keysNumberOfBases base keys + b.Run("Map_Get", func(b *testing.B) { + keys := make([]uint32, b.N) + for i := 0; i < b.N; i++ { + keys[i] = gen() + } + b.ReportAllocs() + m := make(map[uint32]int) + for i := 0; i < len(keys); i++ { + m[keys[i]] = i + } + b.ResetTimer() + for i := 0; i < len(keys); i++ { + _ = m[keys[i]] + } + }) + b.Run("LookupMap_Get", func(b *testing.B) { + keys := make([]uint32, b.N) + for i := 0; i < b.N; i++ { + keys[i] = gen() + } + b.ReportAllocs() + lm := NewLookupMap[int]() + for i := 0; i < len(keys); i++ { + lm.Set(keys[i], i) + } + b.ResetTimer() + for i := 0; i < len(keys); i++ { + lm.Get(keys[i]) + } + }) +} + +func BenchmarkSetConcurrent(b *testing.B) { + gen := generateKeys(keysNumberOfBases) // Use keysNumberOfBases base keys + b.Run("Map_Concurrent_Set", func(b *testing.B) { + keys := make([]uint32, b.N) + for i := 0; i < b.N; i++ { + keys[i] = gen() + } + b.ReportAllocs() + m := make(map[uint32]int) + var mx sync.Mutex + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + i := 0 + for pb.Next() { + mx.Lock() + m[keys[i]] = i + mx.Unlock() + i++ + } + }) + }) + b.Run("LookupMap_Concurrent_Set", func(b *testing.B) { + keys := make([]uint32, b.N) + for i := 0; i < b.N; i++ { + keys[i] = gen() + } + b.ReportAllocs() + lm := NewLookupMap[int]() + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + i := 0 + for pb.Next() { + lm.Set(keys[i], i) + i++ + } + }) + }) +} + +func BenchmarkGetConcurrent(b *testing.B) { + gen := generateKeys(keysNumberOfBases) // Use keysNumberOfBases base keys + b.Run("Map_Concurrent_Get", func(b *testing.B) { + keys := make([]uint32, b.N) + for i := 0; i < b.N; i++ { + keys[i] = gen() + } + b.ReportAllocs() + m := make(map[uint32]int) + var mx sync.RWMutex + for i := 0; i < len(keys); i++ { + m[keys[i]] = i + } + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + i := 0 + for pb.Next() { + mx.RLock() + _ = m[keys[i]] + mx.RUnlock() + i++ + } + }) + }) + b.Run("LookupMap_Concurrent_Get", func(b *testing.B) { + keys := make([]uint32, b.N) + for i := 0; i < b.N; i++ { + keys[i] = gen() + } + b.ReportAllocs() + lm := NewLookupMap[int]() + for i := 0; i < len(keys); i++ { + lm.Set(keys[i], i) + } + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + i := 0 + for pb.Next() { + lm.Get(keys[i]) + i++ + } + }) + }) +} diff --git a/pkg/ecs/lookup.go b/pkg/ecs/lookup.go new file mode 100644 index 00000000..aed313af --- /dev/null +++ b/pkg/ecs/lookup.go @@ -0,0 +1,83 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package ecs + +import "sync" + +const ( + IndexBits = 10 + IndexMask = 1<> 20 & IndexMask), int(key >> 10 & IndexMask), int(key & IndexMask) +} + +const ( + k = 1024*1024*1024*4 - 1 + i0 = k >> 30 & IndexMask + i1 = k >> 20 & IndexMask + i2 = k >> 10 & IndexMask + i3 = k & IndexMask +) diff --git a/pkg/ecs/lookup_test.go b/pkg/ecs/lookup_test.go new file mode 100644 index 00000000..9316bdca --- /dev/null +++ b/pkg/ecs/lookup_test.go @@ -0,0 +1,78 @@ +package ecs + +import ( + "sync" + "testing" +) + +func TestLookupMap(t *testing.T) { + lm := &LookupMap[int]{} + + // Test setting and getting a value + key := uint32(0x12345678) + value := 42 + lm.Set(key, value) + + // Test getting the value + ret, ok := lm.Get(key) + if !ok { + t.Errorf("Expected to find value for key %x, but got not found", key) + } + if ret != value { + t.Errorf("Expected value %d for key %x, but got %d", value, key, ret) + } + + // Test getting a non-existent key + nonExistentKey := uint32(0x87654321) + ret, ok = lm.Get(nonExistentKey) + if ok { + t.Errorf("Expected not to find value for key %x, but got found", nonExistentKey) + } + if ret != 0 { + t.Errorf("Expected zero value for non-existent key %x, but got %d", nonExistentKey, ret) + } + + // Test overwriting a value + newValue := 100 + lm.Set(key, newValue) + ret, ok = lm.Get(key) + if !ok { + t.Errorf("Expected to find value for key %x after overwrite, but got not found", key) + } + if ret != newValue { + t.Errorf("Expected value %d for key %x after overwrite, but got %d", newValue, key, ret) + } +} + +func TestLookupMapConcurrency(t *testing.T) { + lm := &LookupMap[int]{} + key := uint32(0x12345678) + value := 42 + + // Test concurrent writes + var wg sync.WaitGroup + for i := 0; i < 100; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + lm.Set(key, value+i) + }(i) + } + wg.Wait() + + // Test concurrent reads + for i := 0; i < 100; i++ { + wg.Add(1) + go func() { + defer wg.Done() + ret, ok := lm.Get(key) + if !ok { + t.Errorf("Expected to find value for key %x, but got not found", key) + } + if ret < value || ret >= value+100 { + t.Errorf("Expected value between %d and %d for key %x, but got %d", value, value+99, key, ret) + } + }() + } + wg.Wait() +} diff --git a/pkg/ecs/map.go b/pkg/ecs/map.go new file mode 100644 index 00000000..abcd89be --- /dev/null +++ b/pkg/ecs/map.go @@ -0,0 +1,34 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +package ecs + +type Map[K Entity | SharedComponentInstanceId, V any] struct { + book map[K]V +} + +func NewMap[K Entity | SharedComponentInstanceId, V any]() Map[K, V] { + return Map[K, V]{ + book: make(map[K]V, initialBookSize), + } +} + +func (m *Map[K, V]) Get(key K) (value V, ok bool) { + value, ok = m.book[key] + return value, ok +} + +func (m *Map[K, V]) Set(key K, value V) { + m.book[key] = value +} + +func (m *Map[K, V]) Delete(key K) { + delete(m.book, key) +} + +func (m *Map[K, V]) Len() int { + return len(m.book) +} diff --git a/pkg/ecs/paged-array.go b/pkg/ecs/paged-array.go index 11620ed2..dbc6fd0e 100644 --- a/pkg/ecs/paged-array.go +++ b/pkg/ecs/paged-array.go @@ -7,75 +7,165 @@ with this file, You can obtain one at http://mozilla.org/MPL/2.0/. package ecs import ( - "runtime" - "sync" - "github.com/negrel/assert" + "gomp/pkg/worker" + "sync" ) -type PagedArray[T any] struct { - book []ArrayPage[T] - currentPageIndex int32 - len int32 - parallelCount uint8 +/* +Go Slice tips and tricks - https://ueokande.github.io/go-slice-tricks/ +*/ + +func NewPagedArray[T any]() (a PagedArray[T]) { + a.book = make([]*ArrayPage[T], initialBookSize) + for i := 0; i < initialBookSize; i++ { + a.book[i] = &ArrayPage[T]{} + } + a.edTasks = make([]EachDataTask[T], initialBookSize) + a.edvTasks = make([]EachDataValueTask[T], initialBookSize) + a.edvpTasks = make([]EachDataValueParallelTask[T], pageSize) + a.edpTasks = make([]EachDataParallelTask[T], pageSize) + return a } -func NewPagedArray[T any]() (a *PagedArray[T]) { - a = new(PagedArray[T]) - a.book = make([]ArrayPage[T], 2, book_size) - a.parallelCount = uint8(runtime.NumCPU()) / 2 +type SlicePage[T any] struct { + len int + data []T +} - return a +type PagedArray[T any] struct { + book []*ArrayPage[T] + currentPageIndex int + len int + wg sync.WaitGroup + + // Cache + edvTasks []EachDataValueTask[T] + edTasks []EachDataTask[T] + edvpTasks []EachDataValueParallelTask[T] + edpTasks []EachDataParallelTask[T] } -func (a *PagedArray[T]) Len() int32 { +type ArrayPage[T any] struct { + len int + data [pageSize]T +} + +func (a *PagedArray[T]) Len() int { return a.len } -func (a *PagedArray[T]) Get(index int32) *T { +func (a *PagedArray[T]) Get(index int) *T { assert.True(index >= 0, "index out of range") assert.True(index < a.len, "index out of range") pageId, index := a.getPageIdAndIndex(index) - page := &a.book[pageId] + page := a.book[pageId] return &(page.data[index]) } -func (a *PagedArray[T]) Set(index int32, value T) *T { +func (a *PagedArray[T]) GetValue(index int) T { + assert.True(index >= 0, "index out of range") + assert.True(index < a.len, "index out of range") + + pageId, index := a.getPageIdAndIndex(index) + page := a.book[pageId] + + return page.data[index] +} + +func (a *PagedArray[T]) Set(index int, value T) *T { assert.True(index >= 0, "index out of range") assert.True(index < a.len, "index out of range") pageId, index := a.getPageIdAndIndex(index) - page := &a.book[pageId] + page := a.book[pageId] page.data[index] = value return &page.data[index] } +func (a *PagedArray[T]) extend() { + oldLen := len(a.book) + newLen := oldLen * 2 + newBooks := make([]*ArrayPage[T], newLen) + copy(newBooks, a.book) + for i := oldLen; i < newLen; i++ { + newBooks[i] = &ArrayPage[T]{} + } + a.book = newBooks + + newEdvTasks := make([]EachDataValueTask[T], newLen) + copy(newEdvTasks, a.edvTasks) + a.edvTasks = newEdvTasks + + newEdTasks := make([]EachDataTask[T], newLen) + copy(newEdTasks, a.edTasks) + a.edTasks = newEdTasks + + newEdvpTasks := make([]EachDataValueParallelTask[T], pageSize) + copy(newEdvpTasks, a.edvpTasks) + a.edvpTasks = newEdvpTasks + + newEdpTasks := make([]EachDataParallelTask[T], pageSize) + copy(newEdpTasks, a.edpTasks) + a.edpTasks = newEdpTasks +} + func (a *PagedArray[T]) Append(value T) *T { - if a.currentPageIndex >= int32(len(a.book)) { - newBooks := make([]ArrayPage[T], len(a.book)*2) - a.book = append(a.book, newBooks...) + if a.currentPageIndex >= len(a.book) { + a.extend() } - page := &a.book[a.currentPageIndex] + page := a.book[a.currentPageIndex] + if page.len == pageSize { + a.currentPageIndex++ + if a.currentPageIndex >= len(a.book) { + a.extend() + } + page = a.book[a.currentPageIndex] + } page.data[page.len] = value result := &page.data[page.len] page.len++ - if page.len == page_size { - a.currentPageIndex++ - } a.len++ return result } +func (a *PagedArray[T]) AppendMany(values ...T) *T { + var result *T + for i := range values { + value := values[i] + if a.currentPageIndex >= len(a.book) { + a.extend() + } + + page := a.book[a.currentPageIndex] + + if page.len == pageSize { + a.currentPageIndex++ + if a.currentPageIndex >= len(a.book) { + a.extend() + } + page = a.book[a.currentPageIndex] + } + page.data[page.len] = value + result = &page.data[page.len] + page.len++ + a.len++ + } + + return result +} + +// SoftReduce - removes the last element of the array func (a *PagedArray[T]) SoftReduce() { assert.True(a.len > 0, "Len is already 0") - page := &a.book[a.currentPageIndex] + page := a.book[a.currentPageIndex] assert.True(page.len > 0, "Len is already 0") page.len-- @@ -86,9 +176,10 @@ func (a *PagedArray[T]) SoftReduce() { } } +// Reset - resets the array to its initial state func (a *PagedArray[T]) Reset() { - for range a.currentPageIndex { - page := &a.book[a.currentPageIndex] + for i := 0; i <= a.currentPageIndex; i++ { + page := a.book[i] page.len = 0 } @@ -96,7 +187,7 @@ func (a *PagedArray[T]) Reset() { a.len = 0 } -func (a *PagedArray[T]) Copy(fromIndex, toIndex int32) { +func (a *PagedArray[T]) Copy(fromIndex, toIndex int) { assert.True(fromIndex >= 0, "index out of range") assert.True(fromIndex < a.len, "index out of range") from := a.Get(fromIndex) @@ -108,7 +199,7 @@ func (a *PagedArray[T]) Copy(fromIndex, toIndex int32) { *to = *from } -func (a *PagedArray[T]) Swap(i, j int32) (newI, NewJ *T) { +func (a *PagedArray[T]) Swap(i, j int) (newI, NewJ *T) { assert.True(i >= 0, "index out of range") assert.True(i < a.len, "index out of range") x := a.Get(i) @@ -128,140 +219,238 @@ func (a *PagedArray[T]) Last() *T { return a.Get(index) } -func (a *PagedArray[T]) getPageIdAndIndex(index int32) (int32, int32) { - pageId := index >> page_size_shift - assert.True(pageId < int32(len(a.book)), "index out of range") +func (a *PagedArray[T]) Raw(result []T) []T { + totalLen := a.len + // Ensure the result has enough capacity and set the correct length. + if cap(result) < totalLen { + result = make([]T, totalLen) + } else { + result = result[:totalLen] + } - index %= page_size - assert.True(index < page_size, "index out of range") + pos := 0 + for i := 0; i <= a.currentPageIndex; i++ { + page := a.book[i] + n := copy(result[pos:], page.data[:page.len]) + pos += n + } - return pageId, index + return result } -func (a *PagedArray[T]) All(yield func(int32, *T) bool) { - var page *ArrayPage[T] - var index_offset int32 +func (a *PagedArray[T]) getPageIdAndIndex(index int) (int, int) { + return index >> pageSizeShift, index & pageSizeMask +} - book := a.book +// ========================= +// Iterators +// ========================= - if a.len == 0 { - return - } +func (a *PagedArray[T]) Each() func(yield func(int, *T) bool) { + return func(yield func(int, *T) bool) { + var page *ArrayPage[T] + var indexOffset int - for i := a.currentPageIndex; i >= 0; i-- { - page = &book[i] - index_offset = i << page_size_shift + book := a.book - for j := page.len - 1; j >= 0; j-- { - if !yield(index_offset+j, &page.data[j]) { - return + if a.len == 0 { + return + } + + for i := a.currentPageIndex; i >= 0; i-- { + page = book[i] + indexOffset = i << pageSizeShift + + for j := page.len - 1; j >= 0; j-- { + if !yield(indexOffset+j, &page.data[j]) { + return + } } } } } -func (a *PagedArray[T]) AllParallel(yield func(int32, *T) bool) { - var page *ArrayPage[T] - var data *[page_size]T - var index_offset int32 - - book := a.book - wg := new(sync.WaitGroup) - gorutineBudget := a.parallelCount - - runner := func(data *[page_size]T, offset int32, startIndex int32, wg *sync.WaitGroup) { - defer wg.Done() - for j := startIndex; j >= 0; j-- { - if !yield(offset+j, &(data[j])) { - return +func (a *PagedArray[T]) EachParallel(numWorkers int) func(yield func(int, *T, int) bool) { + return func(yield func(int, *T, int) bool) { + assert.True(numWorkers > 0) + var chunkSize = a.len / numWorkers + var wg sync.WaitGroup + + wg.Add(numWorkers) + for workedId := 0; workedId < numWorkers; workedId++ { + startIndex := workedId * chunkSize + endIndex := startIndex + chunkSize - 1 + if workedId == numWorkers-1 { // have to set endIndex to entities length, if last worker + endIndex = a.len } + go func(start int, end int) { + defer wg.Done() + r := end - start + for i := range r { + if !yield(i, a.Get(i+startIndex), workedId) { + return + } + } + }(startIndex, endIndex) } + wg.Wait() } +} + +func (a *PagedArray[T]) EachData(yield func(*T) bool) { + var page *ArrayPage[T] + var book = a.book if a.len == 0 { return } - wg.Add(int(a.currentPageIndex) + 1) for i := a.currentPageIndex; i >= 0; i-- { - page = &book[i] - data = &page.data - index_offset = int32(i) << page_size_shift - - if gorutineBudget > 0 { - go runner(data, index_offset, page.len-1, wg) - gorutineBudget-- - continue - } + page = book[i] - runner(data, index_offset, page.len-1, wg) + for j := page.len - 1; j >= 0; j-- { + if !yield(&page.data[j]) { + return + } + } } - - wg.Wait() } -func (a *PagedArray[T]) AllData(yield func(*T) bool) { +func (a *PagedArray[T]) EachDataValue(yield func(T) bool) { var page *ArrayPage[T] - - book := a.book + var book = a.book if a.len == 0 { return } for i := a.currentPageIndex; i >= 0; i-- { - page = &book[i] + page = book[i] for j := page.len - 1; j >= 0; j-- { - if !yield(&page.data[j]) { + if !yield(page.data[j]) { return } } } } -func (a *PagedArray[T]) AllDataParallel(yield func(*T) bool) { - var page *ArrayPage[T] - var data *[page_size]T - - book := a.book - wg := new(sync.WaitGroup) - gorutineBudget := a.parallelCount - runner := func(data *[page_size]T, startIndex int32, wg *sync.WaitGroup) { - defer wg.Done() - for j := startIndex; j >= 0; j-- { - if !yield(&(data[j])) { - return - } - } +func (a *PagedArray[T]) ProcessDataValue(handler func(T, worker.WorkerId), pool *worker.Pool) { + assert.NotNil(handler) + assert.NotNil(pool) + pool.GroupAdd(a.currentPageIndex + 1) + for i := a.currentPageIndex; i >= 0; i-- { + j := a.currentPageIndex - i + a.edvTasks[j].page = a.book[i] + a.edvTasks[j].f = handler + pool.ProcessGroupTask(&a.edvTasks[j]) } + pool.GroupWait() +} - if a.len == 0 { - return +func (a *PagedArray[T]) ProcessData(handler func(*T, worker.WorkerId), pool *worker.Pool) { + assert.NotNil(handler) + assert.NotNil(pool) + pool.GroupAdd(a.currentPageIndex + 1) + for i := a.currentPageIndex; i >= 0; i-- { + j := a.currentPageIndex - i + a.edTasks[j].page = a.book[i] + a.edTasks[j].f = handler + pool.ProcessGroupTask(&a.edTasks[j]) } + pool.GroupWait() +} + +func (a *PagedArray[T]) EachDataValueParallel(handler func(T, worker.WorkerId), pool *worker.Pool) { + assert.NotNil(handler) + assert.NotNil(pool) - wg.Add(int(a.currentPageIndex) + 1) + var page *ArrayPage[T] + var book = a.book + + pool.GroupAdd(a.len) for i := a.currentPageIndex; i >= 0; i-- { - page = &book[i] - data = &page.data + page = book[i] + n := (a.currentPageIndex - i) * pageSize + for j := page.len - 1; j >= 0; j-- { + m := n + (page.len - 1 - j) + a.edvpTasks[m].page = page + a.edvpTasks[m].index = m + a.edvpTasks[m].f = handler + pool.ProcessGroupTask(&a.edvpTasks[m]) + } + } + pool.GroupWait() +} + +func (a *PagedArray[T]) EachDataParallel(handler func(*T, worker.WorkerId), pool *worker.Pool) { + assert.NotNil(handler) + assert.NotNil(pool) - if gorutineBudget > 0 { - go runner(data, page.len-1, wg) - gorutineBudget-- - continue + var page *ArrayPage[T] + var book = a.book + + pool.GroupAdd(a.len) + for i := a.currentPageIndex; i >= 0; i-- { + page = book[i] + n := (a.currentPageIndex - i) * pageSize + for j := page.len - 1; j >= 0; j-- { + m := n + (page.len - 1 - j) + a.edpTasks[m].page = page + a.edpTasks[m].index = m + a.edpTasks[m].f = handler + pool.ProcessGroupTask(&a.edpTasks[m]) } + } + pool.GroupWait() +} + +// ========================= +// TASKS +// ========================= - runner(data, page.len-1, wg) +type EachDataValueTask[T any] struct { + f func(T, worker.WorkerId) + page *ArrayPage[T] +} + +func (t *EachDataValueTask[T]) Run(workerId worker.WorkerId) error { + for i := 0; i < t.page.len; i++ { + t.f(t.page.data[i], workerId) } - wg.Wait() + return nil } -type SlicePage[T any] struct { - len int32 - data []T +type EachDataTask[T any] struct { + f func(*T, worker.WorkerId) + page *ArrayPage[T] } -type ArrayPage[T any] struct { - len int32 - data [page_size]T +func (t *EachDataTask[T]) Run(workerId worker.WorkerId) error { + for i := 0; i < t.page.len; i++ { + t.f(&t.page.data[i], workerId) + } + return nil +} + +type EachDataValueParallelTask[T any] struct { + f func(T, worker.WorkerId) + page *ArrayPage[T] + index int +} + +func (t *EachDataValueParallelTask[T]) Run(workerId worker.WorkerId) error { + t.f(t.page.data[t.index], workerId) + return nil +} + +type EachDataParallelTask[T any] struct { + f func(*T, worker.WorkerId) + page *ArrayPage[T] + index int +} + +func (t *EachDataParallelTask[T]) Run(workerId worker.WorkerId) error { + t.f(&t.page.data[t.index], workerId) + return nil } diff --git a/pkg/ecs/paged-map.go b/pkg/ecs/paged-map.go index 4da7977d..fa4b4445 100644 --- a/pkg/ecs/paged-map.go +++ b/pkg/ecs/paged-map.go @@ -6,87 +6,105 @@ with this file, You can obtain one at http://mozilla.org/MPL/2.0/. package ecs -import ( - "github.com/negrel/assert" -) - const ( - page_size_shift int32 = 10 - page_size int32 = 1 << page_size_shift - book_size int32 = 1 << 10 + pageSizeShift = 10 + pageSize = 1 << pageSizeShift + pageSizeMask = pageSize - 1 + initialBookSize = 32 // Starting with a small initial book size ) -type MapPage[K Entity, V any] map[K]V -type PagedMap[K Entity, V any] struct { - len int32 +type PagedMap[K Entity | SharedComponentInstanceId, V any] struct { + len int book []SlicePage[MapValue[V]] } + type MapValue[V any] struct { value V ok bool } -func NewPagedMap[K Entity, V any]() *PagedMap[K, V] { - return &PagedMap[K, V]{ - book: make([]SlicePage[MapValue[V]], book_size), +func NewPagedMap[K Entity | SharedComponentInstanceId, V any]() PagedMap[K, V] { + return PagedMap[K, V]{ + book: make([]SlicePage[MapValue[V]], 0, initialBookSize), } } func (m *PagedMap[K, V]) Get(key K) (value V, ok bool) { - page_id, index := m.getPageIdAndIndex(key) - if page_id >= cap(m.book) { + pageID, index := m.getPageIDAndIndex(key) + if pageID >= len(m.book) { return value, false } - page := m.book[page_id] + page := &m.book[pageID] if page.data == nil { return value, false } - if index >= cap(page.data) { - return value, false - } - d := page.data[index] + d := &page.data[index] return d.value, d.ok } func (m *PagedMap[K, V]) Set(key K, value V) { - page_id, index := m.getPageIdAndIndex(key) - if page_id >= cap(m.book) { - // extend the pages slice - new_pages := make([]SlicePage[MapValue[V]], cap(m.book)*2) - m.book = append(m.book, new_pages...) - m.Set(key, value) - return + pageID, index := m.getPageIDAndIndex(key) + if pageID >= len(m.book) { + m.expandBook(pageID + 1) } - page := m.book[page_id] + page := &m.book[pageID] if page.data == nil { - page.data = make([]MapValue[V], page_size) - m.book[page_id] = page + page.data = make([]MapValue[V], pageSize) } - d := &page.data[index] - if !d.ok { + entry := &page.data[index] + if !entry.ok { m.len++ - d.ok = true + entry.ok = true } - d.value = value + entry.value = value } func (m *PagedMap[K, V]) Delete(key K) { - page_id, index := m.getPageIdAndIndex(key) - // Do not attempt to delete a value that does not exist - assert.True(page_id < cap(m.book)) - page := &m.book[page_id] - // Do not attempt to delete a value that does not exist - assert.True(page != nil) - page.data[index].ok = false - m.len-- + pageID, index := m.getPageIDAndIndex(key) + if pageID >= len(m.book) { + return + } + page := &m.book[pageID] + if page.data == nil { + return + } + entry := &page.data[index] + if entry.ok { + entry.ok = false + m.len-- + } +} + +func (m *PagedMap[K, V]) Has(key K) bool { + pageID, index := m.getPageIDAndIndex(key) + if pageID >= len(m.book) { + return false + } + page := &m.book[pageID] + if page.data == nil { + return false + } + return page.data[index].ok +} + +func (m *PagedMap[K, V]) getPageIDAndIndex(key K) (pageID int, index int) { + return int(key) >> pageSizeShift, int(key) & pageSizeMask } -func (m *PagedMap[K, V]) getPageIdAndIndex(key K) (page_id int, index int) { - page_id = int(key) >> page_size_shift - index = int(int32(key) % page_size) - return +func (m *PagedMap[K, V]) expandBook(minLen int) { + if minLen <= cap(m.book) { + m.book = m.book[:minLen] + return + } + newCap := minLen + if newCap < 2*cap(m.book) { + newCap = 2 * cap(m.book) + } + newBook := make([]SlicePage[MapValue[V]], minLen, newCap) + copy(newBook, m.book) + m.book = newBook } -func (m *PagedMap[K, V]) Len() int32 { +func (m *PagedMap[K, V]) Len() int { return m.len } diff --git a/pkg/ecs/slice.go b/pkg/ecs/slice.go new file mode 100644 index 00000000..5cfe0117 --- /dev/null +++ b/pkg/ecs/slice.go @@ -0,0 +1,231 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +<- Саратов Рай Donated 1 000 RUB + +Thank you for your support! +*/ + +package ecs + +import ( + "sync" + + "github.com/negrel/assert" +) + +// Slice tricks https://ueokande.github.io/go-slice-tricks/ + +type Slice[T any] struct { + data []T +} + +func NewSlice[T any](size int) (a Slice[T]) { + a.data = make([]T, 0, size) + return a +} + +func (a *Slice[T]) Len() int { + return len(a.data) +} + +func (a *Slice[T]) Get(index int) *T { + assert.True(index >= 0, "index out of range") + assert.True(index < len(a.data), "index out of range") + + return &a.data[index] +} + +func (a *Slice[T]) GetValue(index int) T { + assert.True(index >= 0, "index out of range") + assert.True(index < len(a.data), "index out of range") + + return a.data[index] +} + +func (a *Slice[T]) Set(index int, value T) *T { + assert.True(index >= 0, "index out of range") + assert.True(index < len(a.data), "index out of range") + + a.data[index] = value + + return &a.data[index] +} + +func (a *Slice[T]) Append(values ...T) []T { + a.data = append(a.data, values...) + disCap := 1 << (FastIntLog2(uint64(len(a.data))) + 1) + if cap(a.data) > disCap { + a.data = a.data[:len(a.data):disCap] + } + return a.data +} + +func (a *Slice[T]) SoftReduce() { + assert.True(len(a.data) > 0, "Len is already 0") + a.data = a.data[:len(a.data)-1] +} + +func (a *Slice[T]) Reset() { + a.data = a.data[:0] +} + +func (a *Slice[T]) Copy(fromIndex, toIndex int) { + assert.True(fromIndex >= 0, "index out of range") + assert.True(fromIndex < len(a.data), "index out of range") + from := a.Get(fromIndex) + + assert.True(toIndex >= 0, "index out of range") + assert.True(toIndex < len(a.data), "index out of range") + to := a.Get(toIndex) + + *to = *from +} + +func (a *Slice[T]) Swap(i, j int) (newI, NewJ *T) { + assert.True(i >= 0, "index out of range") + assert.True(i < len(a.data), "index out of range") + x := a.Get(i) + + assert.True(j >= 0, "index out of range") + assert.True(j < len(a.data), "index out of range") + y := a.Get(j) + + *x, *y = *y, *x + return x, y +} + +func (a *Slice[T]) Last() *T { + index := len(a.data) - 1 + assert.True(index >= 0, "index out of range") + + return a.Get(index) +} + +func (a *Slice[T]) Raw() []T { + return a.data +} + +func (a *Slice[T]) getPageIdAndIndex(index int) (int, int) { + pageId := index >> pageSizeShift + assert.True(pageId < len(a.data), "index out of range") + + index %= pageSize + assert.True(index < pageSize, "index out of range") + + return pageId, index +} + +func (a *Slice[T]) Each() func(yield func(int, *T) bool) { + return func(yield func(int, *T) bool) { + for j := len(a.data) - 1; j >= 0; j-- { + if !yield(j, &a.data[j]) { + return + } + } + } +} + +func (a *Slice[T]) EachData() func(yield func(*T) bool) { + return func(yield func(*T) bool) { + for j := len(a.data) - 1; j >= 0; j-- { + if !yield(&a.data[j]) { + return + } + } + } +} + +func (a *Slice[T]) EachDataValue() func(yield func(T) bool) { + return func(yield func(T) bool) { + for j := len(a.data) - 1; j >= 0; j-- { + if !yield(a.data[j]) { + return + } + } + } +} + +func (a *Slice[T]) EachParallel(numWorkers int) func(yield func(int, *T, int) bool) { + return func(yield func(int, *T, int) bool) { + assert.True(numWorkers > 0) + var chunkSize = len(a.data) / numWorkers + var wg sync.WaitGroup + + wg.Add(numWorkers) + for workedId := 0; workedId < numWorkers; workedId++ { + startIndex := workedId * chunkSize + endIndex := startIndex + chunkSize - 1 + if workedId == numWorkers-1 { // have to set endIndex to entities length, if last worker + endIndex = len(a.data) + } + go func(start int, end int) { + defer wg.Done() + for i := range a.data[start:end] { + if !yield(i, &a.data[i+startIndex], workedId) { + return + } + } + }(startIndex, endIndex) + } + wg.Wait() + } +} + +func (a *Slice[T]) EachDataValueParallel(numWorkers int) func(yield func(T, int) bool) { + return func(yield func(T, int) bool) { + assert.True(numWorkers > 0) + var chunkSize = len(a.data) / numWorkers + var wg sync.WaitGroup + + wg.Add(numWorkers) + for workedId := 0; workedId < numWorkers; workedId++ { + startIndex := workedId * chunkSize + endIndex := startIndex + chunkSize - 1 + if workedId == numWorkers-1 { // have to set endIndex to entities length, if last worker + endIndex = len(a.data) + } + go func(start int, end int) { + defer wg.Done() + for i := range a.data[start:end] { + if !yield(a.data[i+startIndex], workedId) { + return + } + } + }(startIndex, endIndex) + } + wg.Wait() + } +} + +func (a *Slice[T]) EachDataParallel(numWorkers int) func(yield func(*T, int) bool) { + return func(yield func(*T, int) bool) { + assert.True(numWorkers > 0) + var chunkSize = len(a.data) / numWorkers + var wg sync.WaitGroup + + wg.Add(numWorkers) + for workedId := 0; workedId < numWorkers; workedId++ { + startIndex := workedId * chunkSize + endIndex := startIndex + chunkSize - 1 + if workedId == numWorkers-1 { // have to set endIndex to entities length, if last worker + endIndex = len(a.data) + } + go func(start int, end int) { + defer wg.Done() + for i := range a.data[start:end] { + if !yield(&a.data[i+startIndex], workedId) { + return + } + } + }(startIndex, endIndex) + } + wg.Wait() + } +} diff --git a/pkg/ecs/sparse-set.go b/pkg/ecs/sparse-set.go index b6bdb40a..e2dea609 100644 --- a/pkg/ecs/sparse-set.go +++ b/pkg/ecs/sparse-set.go @@ -6,14 +6,14 @@ with this file, You can obtain one at http://mozilla.org/MPL/2.0/. package ecs -type SparseSet[TData any, TKey Entity | ComponentID | WorldID | int] struct { +type SparseSet[TData any, TKey Entity | ComponentId | int] struct { // TODO: refactor map to a slice with using of a deletedSparseElements slice sparse *ChunkMap[int] denseData *ChunkArray[TData] denseIndex *ChunkArray[TKey] } -func NewSparseSet[TData any, TKey Entity | ComponentID | WorldID | int]() SparseSet[TData, TKey] { +func NewSparseSet[TData any, TKey Entity | ComponentId | int]() SparseSet[TData, TKey] { set := SparseSet[TData, TKey]{} set.sparse = NewChunkMap[int](5, 10) set.denseData = NewChunkArray[TData](5, 10) diff --git a/pkg/ecs/system-builder.go b/pkg/ecs/system-builder.go deleted file mode 100644 index 91c66d2c..00000000 --- a/pkg/ecs/system-builder.go +++ /dev/null @@ -1,35 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ - -package ecs - -type SystemBuilder struct { - world *World - systems *[][]*SystemServiceInstance -} - -func (b *SystemBuilder) Sequential(systems ...AnySystemServicePtr) *SystemBuilder { - for i := range systems { - instance := systems[i].register(b.world) - *b.systems = append(*b.systems, []*SystemServiceInstance{instance}) - instance.controller.Init(b.world) - } - - return b -} - -func (b *SystemBuilder) Parallel(systems ...AnySystemServicePtr) *SystemBuilder { - systemInstances := []*SystemServiceInstance{} - - for i := range systems { - instance := systems[i].register(b.world) - systemInstances = append(systemInstances, instance) - go instance.Run() - } - - *b.systems = append(*b.systems, systemInstances) - return b -} diff --git a/pkg/ecs/system.go b/pkg/ecs/system.go index 3c305dd5..b557eea2 100644 --- a/pkg/ecs/system.go +++ b/pkg/ecs/system.go @@ -6,142 +6,18 @@ with this file, You can obtain one at http://mozilla.org/MPL/2.0/. package ecs -import "sync" - -type AnySystemPtr[W any] interface { +type AnySystemPtrDeprecated[W any] interface { Init(*W) Run(*W) Destroy(*W) } -type SystemFunctionMethod int - -const ( - systemFunctionInit SystemFunctionMethod = iota - systemFunctionUpdate - SystemFunctionFixedUpdate - systemFunctionDestroy -) - -type AnySystemControllerPtr interface { - Init(*World) - Update(*World) - FixedUpdate(*World) - Destroy(*World) -} - -type AnySystemServicePtr interface { - register(*World) *SystemServiceInstance - registerDependencyFor(*World, AnySystemServicePtr) - getInstance(*World) *SystemServiceInstance -} - -type SystemServiceInstance struct { - dependsOnNum int - controller AnySystemControllerPtr - dependencyFor []*SystemServiceInstance - dependencyWg *sync.WaitGroup - world *World - initChan chan struct{} - updateChan chan struct{} - fixedUpdateChan chan struct{} - destroyChan chan struct{} -} - -func (s *SystemServiceInstance) PrepareWg() { - s.dependencyWg.Add(s.dependsOnNum) -} - -func (s *SystemServiceInstance) WaitForDeps() { - s.dependencyWg.Wait() -} - -func (s *SystemServiceInstance) SendDone() { - for i := range s.dependencyFor { - s.dependencyFor[i].dependencyWg.Done() - } -} - -func (s *SystemServiceInstance) Add(delta int) { - s.dependencyWg.Add(delta) -} - -func (s *SystemServiceInstance) Run() { - controller := s.controller - - controller.Init(s.world) - defer controller.Destroy(s.world) - - shoudDestroy := false - for !shoudDestroy { - select { - case <-s.initChan: - s.WaitForDeps() - controller.Init(s.world) - s.SendDone() - case <-s.updateChan: - s.WaitForDeps() - controller.Update(s.world) - s.SendDone() - case <-s.fixedUpdateChan: - s.WaitForDeps() - controller.FixedUpdate(s.world) - s.SendDone() - case <-s.destroyChan: - s.WaitForDeps() - shoudDestroy = true - s.SendDone() - } - s.world.wg.Done() - } -} - -func (s *SystemServiceInstance) asyncInit() { - s.initChan <- struct{}{} -} - -func (s *SystemServiceInstance) asyncUpdate() { - s.updateChan <- struct{}{} -} - -func (s *SystemServiceInstance) asyncFixedUpdate() { - s.fixedUpdateChan <- struct{}{} -} - -func (s *SystemServiceInstance) asyncDestroy() { - s.destroyChan <- struct{}{} +type AnySystemPtr interface { + Init(manager *EntityManager) + Update(manager *EntityManager) + FixedUpdate(manager *EntityManager) + Destroy(manager *EntityManager) } -type SystemService[T AnySystemControllerPtr] struct { - initValue T - dependsOn []AnySystemServicePtr - instances map[*World]*SystemServiceInstance -} - -func (s *SystemService[T]) register(world *World) *SystemServiceInstance { - s.instances[world] = &SystemServiceInstance{ - controller: s.initValue, - dependsOnNum: len(s.dependsOn), - dependencyWg: new(sync.WaitGroup), - world: world, - initChan: make(chan struct{}), - updateChan: make(chan struct{}), - fixedUpdateChan: make(chan struct{}), - destroyChan: make(chan struct{}), - } - - for i := range s.dependsOn { - s.dependsOn[i].registerDependencyFor(world, s) - } - - return s.instances[world] -} - -func (s *SystemService[T]) registerDependencyFor(world *World, dep AnySystemServicePtr) { - instance := s.instances[world] - instance.dependencyFor = append(instance.dependencyFor, dep.getInstance(world)) -} - -func (s *SystemService[T]) getInstance(world *World) *SystemServiceInstance { - return s.instances[world] -} +type AnySystemListPtr interface{} +type AnySystemList interface{} diff --git a/pkg/ecs/types.go b/pkg/ecs/types.go deleted file mode 100644 index 2e1a3418..00000000 --- a/pkg/ecs/types.go +++ /dev/null @@ -1,39 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ - -package ecs - -import "github.com/negrel/assert" - -type Mask uint64 -type entityType = uint32 -type ComponentID uint8 - -type EntityVersion uint - -type Entity uint32 - -func (e Entity) IsVersion(version EntityVersion) bool { - return e.GetVersion() == version -} - -func (e *Entity) SetVersion(version EntityVersion) { - assert.True(version <= MaxEntityVersionId, "version is too high") - *e = Entity(entityType(*e) - entityType(e.GetVersion()<<(entityPower-versionPower)) | entityType(version)<<(entityPower-versionPower)) -} - -func (e Entity) GetVersion() EntityVersion { - return EntityVersion(e >> (entityPower - versionPower)) -} - -const ( - entityPower = 32 - versionPower = 2 - MaxEntityVersionId EntityVersion = 1< dtTreshold { - log.Println("WARNING: Game tick rate is too high", dt, dtTreshold) - return - } - - lenSys := len(g.systems) - for i := 0; i < lenSys; i++ { - g.systems[i].Update(dt) - } - - g.wg.Add(len(g.LoadedScenes)) - for i := range g.LoadedScenes { - go updateSystemsAsync(g.LoadedScenes[i], dt, g.wg) - } - g.wg.Wait() -} - -func (g *Game) LoadScene(scene Scene) *Scene { - g.mx.Lock() - defer g.mx.Unlock() - - if g.Debug { - log.Println("Loading scene:", scene.Name) - defer log.Println("Scene loaded:", scene.Name) - } - - //check if scene already exists - suffix := 1 - - for { - prefixedName := fmt.Sprintf("%s_%d", scene.Name, suffix) - - if _, ok := g.LoadedScenes[prefixedName]; ok { - suffix++ - continue - } - - scene.Name = prefixedName - break - } - - c := scene.SceneComponent.New(SceneData{Name: scene.Name}) - entitiesLen := len(scene.Entities) - for i := 0; i < entitiesLen; i++ { - scene.Entities[i](g.World, c) - } - - g.LoadedScenes[scene.Name] = &scene - return &scene -} - -func (g *Game) UnloadScene(scene *Scene) { - g.mx.Lock() - defer g.mx.Unlock() - - if scene == nil { - panic("Trying to unload nil scene") - } - - if g.Debug { - log.Println("Unloading scene: ", scene.Name) - defer log.Println("Scene unloaded: ", scene.Name) - } - - // check if scene exists - if _, ok := g.LoadedScenes[scene.Name]; !ok { - return - } - - scene.SceneComponent.Query.Each(g.World, func(e *donburi.Entry) { - g.World.Remove(e.Entity()) - }) - - delete(g.LoadedScenes, scene.Name) -} - -func (g *Game) UnloadAllScenes() { - for i := range g.LoadedScenes { - g.UnloadScene(g.LoadedScenes[i]) - } -} - -func (g *Game) RegisterSystems(systems ...System) { - g.mx.Lock() - defer g.mx.Unlock() - - for i := range systems { - g.systems = append(g.systems, systems[i]) - g.systems[i].Init(g) - } -} - -func updateSystemsAsync(scene *Scene, dt float64, wg *sync.WaitGroup) { - defer wg.Done() - lenSys := len(scene.Systems) - for i := 0; i < lenSys; i++ { - scene.Systems[i].Update(dt) - } -} diff --git a/pkg/gomp/gomp.go b/pkg/gomp/gomp.go deleted file mode 100644 index b85c68a3..00000000 --- a/pkg/gomp/gomp.go +++ /dev/null @@ -1,92 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ - -package gomp - -import ( - "fmt" - "reflect" - "time" - - "github.com/yohamta/donburi" -) - -func NewGame(tickRate time.Duration) *Game { - game := new(Game) - - game.Init(tickRate) - - return game -} - -func CreateEntity(components ...Component) func(amount int) []Entity { - return func(amount int) []Entity { - if amount <= 0 { - panic(fmt.Sprint("Adding Entity to scene with (", amount, ") amount failed. Amount must be greater than 0.")) - } - - entArr := make([]Entity, amount) - ent := func(world donburi.World, extra ...Component) { - components := append(components, extra...) - cmpnnts := make([]IComponent, len(components)) - for i, c := range components { - cmpnnts[i] = c.ComponentType - } - - entity := world.Create(cmpnnts...) - entry := world.Entry(entity) - for _, c := range components { - c.Set(entry) - } - } - - for i := 0; i < amount; i++ { - entArr[i] = ent - } - - return entArr - } -} - -type ComponentFactory[T any] struct { - Query *donburi.ComponentType[T] -} - -func CreateComponent[T any]() ComponentFactory[T] { - typeFor := reflect.TypeFor[T]() - - if typeFor.Kind() == reflect.Interface { - panic(fmt.Sprint("CreateComponent[", typeFor.String(), "] failed. Type must not be an interface.")) - } - - return ComponentFactory[T]{Query: donburi.NewComponentType[T]()} -} - -func (cf ComponentFactory[T]) New(data T) Component { - return Component{ - ComponentType: cf.Query, - Set: func(entity *donburi.Entry) { - cf.Query.SetValue(entity, data) - }, - } -} - -// func CreateComponent[T any](initData T) *donburi.ComponentType[T] { -// return donburi.NewComponentType[T](initData) -// } - -var systemId uint16 = 0 - -func CreateSystem(controller SystemController) System { - sys := System{ - ID: systemId, - Controller: controller, - } - - systemId++ - - return sys -} diff --git a/pkg/gomp/network-player.go b/pkg/gomp/network-player.go deleted file mode 100644 index 94ce7d42..00000000 --- a/pkg/gomp/network-player.go +++ /dev/null @@ -1,15 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ - -package gomp - -type NetworkData struct { - PlayerID int -} - -var NetworkComponent = CreateComponent[NetworkData]() - -var Player = CreateEntity() diff --git a/pkg/gomp/network-system.go b/pkg/gomp/network-system.go deleted file mode 100644 index 4e0eb15c..00000000 --- a/pkg/gomp/network-system.go +++ /dev/null @@ -1,74 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ - -package gomp - -import ( - "github.com/yohamta/donburi" -) - -var NetworkSystem = CreateSystem(new(networkController)) - -type networkController struct { - world donburi.World - addPlayers chan int - removePlayers chan int - newEvent chan int - sendEvent chan int -} - -func (c *networkController) Init(game *Game) { - c.world = game.World -} - -func (c *networkController) Update(dt float64) { - for i := 0; i < len(c.addPlayers); i++ { - playerId := <-c.addPlayers - - network := NetworkComponent.New(NetworkData{ - PlayerID: playerId, - }) - - player := CreateEntity(network)(1) - playerLen := len(player) - for i := 0; i < playerLen; i++ { - player[i](c.world) - } - - if i >= 9 { - break - } - } - - for i := 0; i < len(c.removePlayers); i++ { - playerId := <-c.removePlayers - - NetworkComponent.Query.Each(c.world, func(e *donburi.Entry) { - network := NetworkComponent.Query.Get(e) - - if network.PlayerID != playerId { - return - } - - ent := e.Entity() - c.world.Remove(ent) - }) - - if i >= 9 { - break - } - } - - for i := 0; i < len(c.newEvent); i++ { - // event := <-c.newEvent - - } - - for i := 0; i < len(c.sendEvent); i++ { - // event := <-c.sendEvent - - } -} diff --git a/pkg/gomp/render-system-ebiten.go b/pkg/gomp/render-system-ebiten.go deleted file mode 100644 index a0a7f2cf..00000000 --- a/pkg/gomp/render-system-ebiten.go +++ /dev/null @@ -1,34 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ - -package gomp - -import ( - "github.com/hajimehoshi/ebiten/v2" - "github.com/yohamta/donburi" -) - -var EbitenRenderSystem = CreateSystem(new(ebitenRenderSystemController)) - -// ebitenRenderSystemController is a system that updates the physics of a game -type ebitenRenderSystemController struct { - world donburi.World -} - -func (c *ebitenRenderSystemController) Init(game *Game) { - c.world = game.World -} - -func (c *ebitenRenderSystemController) Update(dt float64) { - SpriteComponent.Query.Each(c.world, func(e *donburi.Entry) { - sprite := SpriteComponent.Query.Get(e) - if !e.HasComponent(RenderComponent.Query) { - donburi.Add(e, RenderComponent.Query, &RenderData{ - Sprite: ebiten.NewImageFromImage(sprite.Image), - }) - } - }) -} diff --git a/pkg/gomp/resources.go b/pkg/gomp/resources.go deleted file mode 100644 index 4bd87d85..00000000 --- a/pkg/gomp/resources.go +++ /dev/null @@ -1,99 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. - -<- Монтажер сука Donated 50 RUB -*/ - -package gomp - -import ( - "embed" - "fmt" - "image/png" - "io/fs" - "log" - "os" -) - -func walkDir(fs fs.ReadDirFS, prefix string, fn func(path string, info os.FileInfo, err error) error) error { - dirEntries, err := fs.ReadDir(prefix) - if err != nil { - return err - } - - for _, entry := range dirEntries { - info, err := entry.Info() - if err != nil { - return err - } - - err = fn(entry.Name(), info, nil) - if err != nil { - return err - } - } - - return nil -} - -func CreateSpriteResource(fs embed.FS, prefix string) func(filename string) SpriteData { - sprites := make(map[string]SpriteData) - - err := walkDir(fs, prefix, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - - if info.IsDir() { - return nil - } - - filename := prefix + "/" + info.Name() - file, err := fs.Open(filename) - if err != nil { - log.Println("Error opening file") - return err - } - defer file.Close() - - img, err := png.Decode(file) - if err != nil { - log.Println("Error decoding file") - return err - } - - fileCfg, err := fs.Open(filename) - if err != nil { - log.Println("Error decoding file") - return err - } - defer fileCfg.Close() - - cfg, err := png.DecodeConfig(fileCfg) - if err != nil { - log.Println("Error decoding cfg file") - return err - } - - sprites[info.Name()] = SpriteData{ - Image: img, - Config: cfg, - } - - return nil - }) - - if err != nil { - panic(err) - } - - return func(filename string) SpriteData { - if sprite, ok := sprites[filename]; ok { - return sprite - } - - panic(fmt.Sprint("File <", filename, "> does not exist in <", prefix, "> resource.")) - } -} diff --git a/pkg/gomp/scene-system.go b/pkg/gomp/scene-system.go deleted file mode 100644 index b4f180e2..00000000 --- a/pkg/gomp/scene-system.go +++ /dev/null @@ -1,16 +0,0 @@ -package gomp - -// func SceneSystem() ecs.System { -// return gomp.CreateSystem(new(sceneSystemController)) -// } - -type sceneSystemController struct { -} - -func (s *sceneSystemController) Init(game *Game) { - -} - -func (s *sceneSystemController) Update(dt float64) { - -} diff --git a/pkg/gomp/scene.go b/pkg/gomp/scene.go deleted file mode 100644 index 67822271..00000000 --- a/pkg/gomp/scene.go +++ /dev/null @@ -1,45 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ - -package gomp - -type Scene struct { - Name string - - Systems []*System - Entities []Entity - SceneComponent ComponentFactory[SceneData] -} - -type SceneData struct { - Name string -} - -type sceneFactoryEntities struct { - scene *Scene -} - -func (f sceneFactoryEntities) AddEntities(ent ...[]Entity) Scene { - for i := 0; i <= len(ent); i++ { - f.scene.Entities = ent[0] - } - - return *f.scene -} - -func CreateScene(name string) sceneFactoryEntities { - var SceneComponent = CreateComponent[SceneData]() - scene := new(Scene) - scene.SceneComponent = SceneComponent - - scene.Name = name - - factory := sceneFactoryEntities{ - scene: scene, - } - - return factory -} diff --git a/pkg/gomp/server.go b/pkg/gomp/server.go deleted file mode 100644 index 6c8cb964..00000000 --- a/pkg/gomp/server.go +++ /dev/null @@ -1,203 +0,0 @@ -/* -This Source Code Form is subject to the terms of the Mozilla -Public License, v. 2.0. If a copy of the MPL was not distributed -with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ - -package gomp - -import ( - "fmt" - "os" - "runtime" - "strings" - "sync" - "time" - - "net/http" - _ "net/http/pprof" - - "github.com/gorilla/sessions" - "github.com/gorilla/websocket" - "github.com/labstack/echo-contrib/session" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" - "github.com/labstack/gommon/log" - echopprof "github.com/sevenNt/echo-pprof" - "golang.org/x/time/rate" -) - -func (game *Game) RunServer() { - runtime.SetCPUProfileRate(1000) - - e := echo.New() - // e.Use(middleware.Logger()) - e.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{ - StackSize: 1 << 10, // 1 KB - LogLevel: log.ERROR, - DisableStackAll: true, - DisablePrintStack: true, - })) - - e.Use(middleware.BodyLimit("2M")) - e.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(rate.Limit(60)))) - e.Use(session.Middleware(sessions.NewCookieStore([]byte(getEnv("AUTH_SECRET", "jdkljskldjslk"))))) - e.Use(middleware.GzipWithConfig(middleware.GzipConfig{ - Level: 5, - Skipper: func(c echo.Context) bool { - return strings.Contains(c.Path(), "ws") // Change "metrics" for your own path - }, - })) - - e.GET("/ws", wsHandler(game)) - - echopprof.Wrap(e) - - go e.Start(":27015") -} - -var upgrader = websocket.Upgrader{ - ReadBufferSize: 1024, - WriteBufferSize: 1024, - CheckOrigin: func(r *http.Request) bool { - return true - }, -} - -func wsHandler(game *Game) echo.HandlerFunc { - return func(c echo.Context) error { - if len(game.Network.Host.clients) >= game.Network.Host.MaxClients { - return c.String(http.StatusBadRequest, "Too many players") - } - - conn, err := upgrader.Upgrade(c.Response().Writer, c.Request(), nil) - if err != nil { - return err - } - - return handleWsConnection(c, conn, game) - } -} - -type wsClient struct { - id NetworkPlayerId - conn *websocket.Conn - send chan []byte - receiver chan []byte -} - -func (c *wsClient) Send(msg []byte) { - c.send <- msg -} - -func (c *wsClient) Close() error { - return c.conn.Close() -} - -func handleWsConnection(ctx echo.Context, conn *websocket.Conn, game *Game) error { - var id NetworkPlayerId = 1 - - client := wsClient{} - - client.id = id - client.conn = conn - client.send = make(chan []byte, 512) - - receiver := game.Network.Host.AddClient(id, &client) - defer game.Network.Host.RemoveClient(id) - - client.receiver = receiver - - var wg sync.WaitGroup - wg.Add(2) - - go client.readPump(&wg) - go client.writePump(&wg) - - wg.Wait() - - return conn.Close() -} - -const ( - // Time allowed to write a message to the peer. - writeWait = 10 * time.Second - - // Time allowed to read the next pong message from the peer. - pongWait = 60 * time.Second - - // Send pings to peer with this period. Must be less than pongWait. - pingPeriod = (pongWait * 9) / 10 - - // Maximum message size allowed from peer. - maxMessageSize = 512 -) - -// readPump pumps messages from the websocket connection to the hub. -// -// The application runs readPump in a per-connection goroutine. The application -// ensures that there is at most one reader on a connection by executing all -// reads from this goroutine. -func (c *wsClient) readPump(wg *sync.WaitGroup) { - defer func() { - wg.Done() - }() - - c.conn.SetReadLimit(maxMessageSize) - c.conn.SetReadDeadline(time.Now().Add(pongWait)) - c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil }) - - for { - _, message, err := c.conn.ReadMessage() - if err != nil { - if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { - fmt.Printf("error: %v", err) - } - return - } - - c.receiver <- message - } -} - -// writePump pumps messages from the hub to the websocket connection. -// -// A goroutine running writePump is started for each connection. The -// application ensures that there is at most one writer to a connection by -// executing all writes from this goroutine. -func (c *wsClient) writePump(wg *sync.WaitGroup) { - defer wg.Done() - - pingTicker := time.NewTicker(pingPeriod) - defer pingTicker.Stop() - - for { - select { - case message, ok := <-c.send: - c.conn.SetWriteDeadline(time.Now().Add(writeWait)) - if !ok { - // The hub closed the channel. - c.conn.WriteMessage(websocket.CloseMessage, []byte{}) - return - } - - err := c.conn.WriteMessage(websocket.BinaryMessage, message) - if err != nil { - return - } - - case <-pingTicker.C: - c.conn.SetWriteDeadline(time.Now().Add(writeWait)) - if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil { - return - } - } - } -} - -func getEnv(key, fallback string) string { - if value, ok := os.LookupEnv(key); ok { - return value - } - return fallback -} diff --git a/pkg/gtime/time.go b/pkg/gtime/time.go new file mode 100644 index 00000000..976d38db --- /dev/null +++ b/pkg/gtime/time.go @@ -0,0 +1,25 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package gtime + +/* +#include "timestamp.h" +*/ +import "C" + +// GetTimestampC возвращает текущий UNIX timestamp +func GetTimestampC() int64 { + return int64(C.get_system_timestamp()) +} diff --git a/pkg/gtime/time_bench_test.go b/pkg/gtime/time_bench_test.go new file mode 100644 index 00000000..73a3bcac --- /dev/null +++ b/pkg/gtime/time_bench_test.go @@ -0,0 +1,91 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package gtime + +import ( + "gomp/pkg/timeasm" + "testing" + "time" +) + +const loadsize = 0 + +func workLoad(a int64) int64 { + var result int64 = 0 + for range a { + result += a * a + } + return result +} + +func BenchmarkAsm2(b *testing.B) { + b.ReportAllocs() + + for b.Loop() { + n := timeasm.Now() + _ = workLoad(loadsize) + _ = timeasm.Now() - n + } +} + +func BenchmarkAsm(b *testing.B) { + b.ReportAllocs() + + for b.Loop() { + n := timeasm.Cputicks() + _ = workLoad(loadsize) + _ = timeasm.Cputicks() - n + } +} + +func BenchmarkC(b *testing.B) { + b.ReportAllocs() + + for b.Loop() { + n := GetTimestampC() + _ = workLoad(loadsize) + _ = GetTimestampC() - n + } +} + +func BenchmarkTimeNow(b *testing.B) { + b.ReportAllocs() + + for b.Loop() { + n := time.Now() + _ = workLoad(loadsize) + _ = time.Since(n) + } +} + +func BenchmarkTimeNowUnix(b *testing.B) { + b.ReportAllocs() + + for b.Loop() { + n := time.Now().Unix() + _ = workLoad(loadsize) + _ = time.Now().Unix() - n + } +} + +// Опционально: сравнение с наносекундной точностью +func BenchmarkTimeNowUnixNano(b *testing.B) { + b.ReportAllocs() + for b.Loop() { + n := time.Now().UnixNano() + _ = workLoad(loadsize) + _ = time.Now().UnixNano() - n + } +} diff --git a/pkg/gtime/timestamp.c b/pkg/gtime/timestamp.c new file mode 100644 index 00000000..db33fc36 --- /dev/null +++ b/pkg/gtime/timestamp.c @@ -0,0 +1,5 @@ +#include "timestamp.h" + +long long get_system_timestamp() { + return (long long)time(NULL); +} \ No newline at end of file diff --git a/pkg/gtime/timestamp.h b/pkg/gtime/timestamp.h new file mode 100644 index 00000000..9d135ed1 --- /dev/null +++ b/pkg/gtime/timestamp.h @@ -0,0 +1,8 @@ +#ifndef TIMESTAMP_H +#define TIMESTAMP_H + +#include + +long long get_system_timestamp(); + +#endif \ No newline at end of file diff --git a/pkg/legacy/network-system.go b/pkg/legacy/network-system.go new file mode 100644 index 00000000..a4b3fe54 --- /dev/null +++ b/pkg/legacy/network-system.go @@ -0,0 +1,74 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +package legacy + +//import ( +// "github.com/yohamta/donburi" +//) +// +//var NetworkSystem = CreateSystem(new(networkController)) +// +//type networkController struct { +// world donburi.World +// addPlayers chan int +// removePlayers chan int +// newEvent chan int +// sendEvent chan int +//} +// +//func (c *networkController) Init(game *Game) { +// c.world = game.World +//} +// +//func (c *networkController) Update(dt float64) { +// for i := 0; i < len(c.addPlayers); i++ { +// playerId := <-c.addPlayers +// +// network := NetworkComponent.New(NetworkData{ +// PlayerID: playerId, +// }) +// +// player := CreateEntity(network)(1) +// playerLen := len(player) +// for i := 0; i < playerLen; i++ { +// player[i](c.world) +// } +// +// if i >= 9 { +// break +// } +// } +// +// for i := 0; i < len(c.removePlayers); i++ { +// playerId := <-c.removePlayers +// +// NetworkComponent.Query.Each(c.world, func(e *donburi.Entry) { +// network := NetworkComponent.Query.Get(e) +// +// if network.PlayerID != playerId { +// return +// } +// +// ent := e.Entity() +// c.world.Remove(ent) +// }) +// +// if i >= 9 { +// break +// } +// } +// +// for i := 0; i < len(c.newEvent); i++ { +// // event := <-c.newEvent +// +// } +// +// for i := 0; i < len(c.sendEvent); i++ { +// // event := <-c.sendEvent +// +// } +//} diff --git a/pkg/gomp/network.go b/pkg/legacy/network.go similarity index 99% rename from pkg/gomp/network.go rename to pkg/legacy/network.go index ee7c16db..5f994f86 100644 --- a/pkg/gomp/network.go +++ b/pkg/legacy/network.go @@ -4,7 +4,7 @@ Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package gomp +package legacy type NetworkMode int diff --git a/pkg/legacy/resources.go b/pkg/legacy/resources.go new file mode 100644 index 00000000..157853f0 --- /dev/null +++ b/pkg/legacy/resources.go @@ -0,0 +1,99 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +<- Монтажер сука Donated 50 RUB +*/ + +package legacy + +//import ( +// "embed" +// "fmt" +// "image/png" +// "io/fs" +// "log" +// "os" +//) +// +//func walkDir(fs fs.ReadDirFS, prefix string, fn func(path string, info os.FileInfo, err error) error) error { +// dirEntries, err := fs.ReadDir(prefix) +// if err != nil { +// return err +// } +// +// for _, entry := range dirEntries { +// info, err := entry.Info() +// if err != nil { +// return err +// } +// +// err = fn(entry.Name(), info, nil) +// if err != nil { +// return err +// } +// } +// +// return nil +//} +// +//func CreateSpriteResource(fs embed.FS, prefix string) func(filename string) SpriteData { +// sprites := make(map[string]SpriteData) +// +// err := walkDir(fs, prefix, func(path string, info os.FileInfo, err error) error { +// if err != nil { +// return err +// } +// +// if info.IsDir() { +// return nil +// } +// +// filename := prefix + "/" + info.Name() +// file, err := fs.Open(filename) +// if err != nil { +// log.Println("Error opening file") +// return err +// } +// defer file.Close() +// +// img, err := png.Decode(file) +// if err != nil { +// log.Println("Error decoding file") +// return err +// } +// +// fileCfg, err := fs.Open(filename) +// if err != nil { +// log.Println("Error decoding file") +// return err +// } +// defer fileCfg.Close() +// +// cfg, err := png.DecodeConfig(fileCfg) +// if err != nil { +// log.Println("Error decoding cfg file") +// return err +// } +// +// sprites[info.Name()] = SpriteData{ +// Image: img, +// Config: cfg, +// } +// +// return nil +// }) +// +// if err != nil { +// panic(err) +// } +// +// return func(filename string) SpriteData { +// if sprite, ok := sprites[filename]; ok { +// return sprite +// } +// +// panic(fmt.Sprint("File <", filename, "> does not exist in <", prefix, "> resource.")) +// } +//} diff --git a/pkg/legacy/server.go b/pkg/legacy/server.go new file mode 100644 index 00000000..45dbce4d --- /dev/null +++ b/pkg/legacy/server.go @@ -0,0 +1,203 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +package legacy + +//import ( +// "fmt" +// "os" +// "runtime" +// "strings" +// "sync" +// "time" +// +// "net/http" +// _ "net/http/pprof" +// +// "github.com/gorilla/sessions" +// "github.com/gorilla/websocket" +// "github.com/labstack/echo-contrib/session" +// "github.com/labstack/echo/v4" +// "github.com/labstack/echo/v4/middleware" +// "github.com/labstack/gommon/log" +// echopprof "github.com/sevenNt/echo-pprof" +// "golang.org/x/time/rate" +//) +// +//func (game *Game) RunServer() { +// runtime.SetCPUProfileRate(1000) +// +// e := echo.New() +// // e.Use(middleware.Logger()) +// e.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{ +// StackSize: 1 << 10, // 1 KB +// LogLevel: log.ERROR, +// DisableStackAll: true, +// DisablePrintStack: true, +// })) +// +// e.Use(middleware.BodyLimit("2M")) +// e.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(rate.Limit(60)))) +// e.Use(session.Middleware(sessions.NewCookieStore([]byte(getEnv("AUTH_SECRET", "jdkljskldjslk"))))) +// e.Use(middleware.GzipWithConfig(middleware.GzipConfig{ +// Level: 5, +// Skipper: func(c echo.Context) bool { +// return strings.Contains(c.Path(), "ws") // Change "metrics" for your own path +// }, +// })) +// +// e.GET("/ws", wsHandler(game)) +// +// echopprof.Wrap(e) +// +// go e.Start(":27015") +//} +// +//var upgrader = websocket.Upgrader{ +// ReadBufferSize: 1024, +// WriteBufferSize: 1024, +// CheckOrigin: func(r *http.Request) bool { +// return true +// }, +//} +// +//func wsHandler(game *Game) echo.HandlerFunc { +// return func(c echo.Context) error { +// if len(game.Network.Host.clients) >= game.Network.Host.MaxClients { +// return c.String(http.StatusBadRequest, "Too many players") +// } +// +// conn, err := upgrader.Upgrade(c.Response().Writer, c.Request(), nil) +// if err != nil { +// return err +// } +// +// return handleWsConnection(c, conn, game) +// } +//} +// +//type wsClient struct { +// id NetworkPlayerId +// conn *websocket.Conn +// send chan []byte +// receiver chan []byte +//} +// +//func (c *wsClient) Send(msg []byte) { +// c.send <- msg +//} +// +//func (c *wsClient) Close() error { +// return c.conn.Close() +//} +// +//func handleWsConnection(ctx echo.Context, conn *websocket.Conn, game *Game) error { +// var id NetworkPlayerId = 1 +// +// client := wsClient{} +// +// client.id = id +// client.conn = conn +// client.send = make(chan []byte, 512) +// +// receiver := game.Network.Host.AddClient(id, &client) +// defer game.Network.Host.RemoveClient(id) +// +// client.receiver = receiver +// +// var wg sync.WaitGroup +// wg.Add(2) +// +// go client.readPump(&wg) +// go client.writePump(&wg) +// +// wg.Wait() +// +// return conn.Close() +//} +// +//const ( +// // Time allowed to write a message to the peer. +// writeWait = 10 * time.Second +// +// // Time allowed to read the next pong message from the peer. +// pongWait = 60 * time.Second +// +// // Send pings to peer with this period. Must be less than pongWait. +// pingPeriod = (pongWait * 9) / 10 +// +// // Maximum message size allowed from peer. +// maxMessageSize = 512 +//) +// +//// readPump pumps messages from the websocket connection to the hub. +//// +//// The application runs readPump in a per-connection goroutine. The application +//// ensures that there is at most one reader on a connection by executing all +//// reads from this goroutine. +//func (c *wsClient) readPump(wg *sync.WaitGroup) { +// defer func() { +// wg.Done() +// }() +// +// c.conn.SetReadLimit(maxMessageSize) +// c.conn.SetReadDeadline(time.Now().Add(pongWait)) +// c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil }) +// +// for { +// _, message, err := c.conn.ReadMessage() +// if err != nil { +// if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { +// fmt.Printf("error: %v", err) +// } +// return +// } +// +// c.receiver <- message +// } +//} +// +//// writePump pumps messages from the hub to the websocket connection. +//// +//// A goroutine running writePump is started for each connection. The +//// application ensures that there is at most one writer to a connection by +//// executing all writes from this goroutine. +//func (c *wsClient) writePump(wg *sync.WaitGroup) { +// defer wg.Done() +// +// pingTicker := time.NewTicker(pingPeriod) +// defer pingTicker.Stop() +// +// for { +// select { +// case message, ok := <-c.send: +// c.conn.SetWriteDeadline(time.Now().Add(writeWait)) +// if !ok { +// // The hub closed the channel. +// c.conn.WriteMessage(websocket.CloseMessage, []byte{}) +// return +// } +// +// err := c.conn.WriteMessage(websocket.BinaryMessage, message) +// if err != nil { +// return +// } +// +// case <-pingTicker.C: +// c.conn.SetWriteDeadline(time.Now().Add(writeWait)) +// if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil { +// return +// } +// } +// } +//} +// +//func getEnv(key, fallback string) string { +// if value, ok := os.LookupEnv(key); ok { +// return value +// } +// return fallback +//} diff --git a/pkg/matrix/matrix.go b/pkg/matrix/matrix.go new file mode 100644 index 00000000..c3c16566 --- /dev/null +++ b/pkg/matrix/matrix.go @@ -0,0 +1,36 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package matrix + +type V4 [4]float32 +type M4 [16]float32 + +//go:nosplit +func multiplyasm(data []V4, m M4) + +func M4MultiplyV4(m M4, v V4) V4 { + return V4{ + v[0]*m[0] + v[1]*m[4] + v[2]*m[8] + v[3]*m[12], + v[0]*m[1] + v[1]*m[5] + v[2]*m[9] + v[3]*m[13], + v[0]*m[2] + v[1]*m[6] + v[2]*m[10] + v[3]*m[14], + v[0]*m[3] + v[1]*m[7] + v[2]*m[11] + v[3]*m[15], + } +} + +func multiply(data []V4, m M4) { + for i, v := range data { + data[i] = M4MultiplyV4(m, v) + } +} diff --git a/pkg/matrix/matrix_amd64.s b/pkg/matrix/matrix_amd64.s new file mode 100644 index 00000000..308294dd --- /dev/null +++ b/pkg/matrix/matrix_amd64.s @@ -0,0 +1,83 @@ +// func multiplyasm(data []V4, m M4) +// +// memory layout of the stack relative to FP +// +0 data slice ptr +// +8 data slice len +// +16 data slice cap +// +24 m[0] | m[1] +// +32 m[2] | m[3] +// +40 m[4] | m[5] +// +48 m[6] | m[7] +// +56 m[8] | m[9] +// +64 m[10] | m[11] +// +72 m[12] | m[13] +// +80 m[14] | m[15] + +#include "textflag.h" + +TEXT ·multiplyasm(SB),NOSPLIT,$0 + // data ptr + MOVQ data+0(FP), CX + // data len + MOVQ data+8(FP), SI + // index into data + MOVQ $0, AX + // return early if zero length + CMPQ AX, SI + JE END + // load the matrix into 128-bit wide xmm registers + // load [m[0], m[1], m[2], m[3]] into xmm0 + MOVUPS m+24(FP), X0 + // load [m[4], m[5], m[6], m[7]] into xmm1 + MOVUPS m+40(FP), X1 + // load [m[8], m[9], m[10], m[11]] into xmm2 + MOVUPS m+56(FP), X2 + // load [m[12], m[13], m[14], m[15]] into xmm3 + MOVUPS m+72(FP), X3 +LOOP: + // load each component of the vector into xmm registers + // load data[i][0] (x) into xmm4 + MOVSS 0(CX), X4 + // load data[i][1] (y) into xmm5 + MOVSS 4(CX), X5 + // load data[i][2] (z) into xmm6 + MOVSS 8(CX), X6 + // load data[i][3] (w) into xmm7 + MOVSS 12(CX), X7 + // copy each component of the matrix across each register + // [0, 0, 0, x] => [x, x, x, x] + SHUFPS $0, X4, X4 + // [0, 0, 0, y] => [y, y, y, y] + SHUFPS $0, X5, X5 + // [0, 0, 0, z] => [z, z, z, z] + SHUFPS $0, X6, X6 + // [0, 0, 0, w] => [w, w, w, w] + SHUFPS $0, X7, X7 + // xmm4 = [m[0], m[1], m[2], m[3]] .* [x, x, x, x] + MULPS X0, X4 + // xmm5 = [m[4], m[5], m[6], m[7]] .* [y, y, y, y] + MULPS X1, X5 + // xmm6 = [m[8], m[9], m[10], m[11]] .* [z, z, z, z] + MULPS X2, X6 + // xmm7 = [m[12], m[13], m[14], m[15]] .* [w, w, w, w] + MULPS X3, X7 + // xmm4 = xmm4 + xmm5 + ADDPS X5, X4 + // xmm4 = xmm4 + xmm6 + ADDPS X6, X4 + // xmm4 = xmm4 + xmm7 + ADDPS X7, X4 + // data[i] = xmm4 + MOVNTPS X4, 0(CX) + // data++ + ADDQ $16, CX + // i++ + INCQ AX + // if i >= len(data) break + CMPQ AX, SI + JLT LOOP +END: + // since we use a non-temporal write (MOVNTPS) + // make sure all writes are visible before we leave the function + SFENCE + RET diff --git a/pkg/matrix/matrix_bench_test.go b/pkg/matrix/matrix_bench_test.go new file mode 100644 index 00000000..e89acc4b --- /dev/null +++ b/pkg/matrix/matrix_bench_test.go @@ -0,0 +1,62 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package matrix + +import ( + "testing" +) + +// createSampleM4 generates a sample 4x4 identity matrix. +func createSampleM4() M4 { + return M4{ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1, + } +} + +// createSampleData generates a slice of sample V4 vectors. +func createSampleData(size int) []V4 { + data := make([]V4, size) + for i := range data { + data[i] = V4{float32(i), float32(i + 1), float32(i + 2), float32(i + 3)} + } + return data +} + +// BenchmarkMultiply benchmarks the multiply function with a 1000-element slice. +func BenchmarkMultiply(b *testing.B) { + m := createSampleM4() + backupData := createSampleData(1000) // Sample data with 1000 vectors + data := make([]V4, len(backupData)) // Preallocate to avoid allocations in loop + b.ResetTimer() + for i := 0; i < b.N; i++ { + copy(data, backupData) // Reset data to original state + multiply(data, m) + } +} + +// BenchmarkMultiplyAsm benchmarks the multiply function with a 1000-element slice. +func BenchmarkMultiplyAsm(b *testing.B) { + m := createSampleM4() + backupData := createSampleData(1000) // Sample data with 1000 vectors + data := make([]V4, len(backupData)) // Preallocate to avoid allocations in loop + b.ResetTimer() + for i := 0; i < b.N; i++ { + copy(data, backupData) // Reset data to original state + multiplyasm(data, m) + } +} diff --git a/pkg/gomp/render-component-ebiten.go b/pkg/timeasm/main.go similarity index 53% rename from pkg/gomp/render-component-ebiten.go rename to pkg/timeasm/main.go index a595660e..b2900b84 100644 --- a/pkg/gomp/render-component-ebiten.go +++ b/pkg/timeasm/main.go @@ -1,17 +1,15 @@ -package gomp - /* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ -import ( - "github.com/hajimehoshi/ebiten/v2" -) +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== -type RenderData struct { - Sprite *ebiten.Image -} +none :) + +Thank you for your support! +*/ -var RenderComponent = CreateComponent[RenderData]() +package timeasm diff --git a/pkg/timeasm/timestamp.go b/pkg/timeasm/timestamp.go new file mode 100644 index 00000000..b95c893c --- /dev/null +++ b/pkg/timeasm/timestamp.go @@ -0,0 +1,63 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package timeasm + +import ( + "math/bits" + "time" + _ "unsafe" // Для go:linkname +) + +var ( + tscFrequency uint64 + baseTimeUnix int64 + tscFrequencyShift int +) + +//func init() { +// calibrateTSC() +//} + +func calibrateTSC() { + baseTime := time.Now() + baseTimeUnix = baseTime.UnixNano() + ticksStart := Cputicks() + time.Sleep(100 * time.Millisecond) + endTime := time.Now() + ticksEnd := Cputicks() + + tscFrequency = (ticksEnd - ticksStart) * 10 / uint64(endTime.Sub(baseTime).Nanoseconds()) + tscFrequencyShift = bits.Len64(tscFrequency) - 1 +} + +func GetTimestamp() uint64 { + return Cputicks() +} + +var baseNano int64 = 0 +var baseTsc uint64 +var scale float64 + +func Now() int64 { + if baseNano == 0 { + baseNano = time.Now().UnixNano() + baseTsc = Cputicks() + time.Sleep(1e7) + rn := time.Now().UnixNano() + ct := Cputicks() + scale = float64(rn-baseNano) / float64(ct-baseTsc) + } + return baseNano + int64(float64(Cputicks()-baseTsc)*scale) +} diff --git a/pkg/timeasm/timestamp_amd64.s b/pkg/timeasm/timestamp_amd64.s new file mode 100644 index 00000000..99f68a88 --- /dev/null +++ b/pkg/timeasm/timestamp_amd64.s @@ -0,0 +1,7 @@ +// func Cputicks(void) (n uint64) +TEXT ·Cputicks(SB),7,$0 + RDTSC + SHLQ $32, DX + ADDQ DX, AX + MOVQ AX, n+0(FP) + RET diff --git a/pkg/timeasm/timestamp_asm.go b/pkg/timeasm/timestamp_asm.go new file mode 100644 index 00000000..56404cca --- /dev/null +++ b/pkg/timeasm/timestamp_asm.go @@ -0,0 +1,18 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +// timestamp_asm.go +package timeasm + +func Cputicks() (t uint64) diff --git a/pkg/worker/pool.go b/pkg/worker/pool.go new file mode 100644 index 00000000..3bdf8969 --- /dev/null +++ b/pkg/worker/pool.go @@ -0,0 +1,85 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +<- Еблан Donated 228 RUB +<- VsehVertela Donated 500 RUB +<- Linkwayz Donated 500 RUB +<- thespacetime Donated 10 EUR +<- Linkwayz Donated 1500 RUB +<- mitwelve Donated 100 RUB +<- tema881 Donated 100 RUB + +Thank you for your support! +*/ + +package worker + +import ( + "context" + "sync" +) + +func NewPool(n int) Pool { + return Pool{ + workers: make([]Worker, n), + taskChan: make(chan AnyTask, n*4), + } +} + +type Pool struct { + workers []Worker + wg sync.WaitGroup + ctx context.Context + ctxCancel context.CancelFunc + + // Cache + taskChan chan AnyTask + groupTaskWg *sync.WaitGroup +} + +func (p *Pool) Start() { + p.ctx, p.ctxCancel = context.WithCancel(context.Background()) + p.groupTaskWg = new(sync.WaitGroup) + p.wg = sync.WaitGroup{} + for i := range p.workers { + p.workers[i] = NewWorker(p.ctx, WorkerId(i), p.taskChan, p.groupTaskWg) + p.workers[i].Start(&p.wg) + } +} + +func (p *Pool) AddWorker() { + p.workers = append(p.workers, NewWorker(p.ctx, WorkerId(len(p.workers)), p.taskChan, p.groupTaskWg)) + p.workers[len(p.workers)-1].Start(&p.wg) +} + +func (p *Pool) RemoveWorker() { + p.workers[len(p.workers)-1].Stop() + p.workers = p.workers[:len(p.workers)-1] +} + +func (p *Pool) Stop() { + p.ctxCancel() + p.wg.Wait() +} + +func (p *Pool) GroupAdd(n int) { + p.groupTaskWg.Add(n) +} + +func (p *Pool) ProcessGroupTask(tasks AnyTask) { + p.taskChan <- tasks +} + +func (p *Pool) GroupWait() { + p.groupTaskWg.Wait() +} + +func (p *Pool) NumWorkers() int { + return len(p.workers) +} diff --git a/pkg/worker/task.go b/pkg/worker/task.go new file mode 100644 index 00000000..8680ca54 --- /dev/null +++ b/pkg/worker/task.go @@ -0,0 +1,28 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package worker + +type AnyTask interface { + Run(workerId WorkerId) error +} + +type TaskError struct { + Err error + Id WorkerId +} + +func (e TaskError) Error() string { + return e.Err.Error() +} diff --git a/pkg/worker/worker.go b/pkg/worker/worker.go new file mode 100644 index 00000000..01459ab0 --- /dev/null +++ b/pkg/worker/worker.go @@ -0,0 +1,67 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package worker + +import ( + "context" + "sync" +) + +type WorkerId int + +func NewWorker(ctx context.Context, id WorkerId, taskChan <-chan AnyTask, taskWg *sync.WaitGroup) Worker { + return Worker{ + id: id, + ctx: ctx, + taskWg: taskWg, + taskChan: taskChan, + } +} + +type Worker struct { + id WorkerId + ctx context.Context + taskChan <-chan AnyTask + taskWg *sync.WaitGroup + wg sync.WaitGroup +} + +func (w *Worker) Start(poolWg *sync.WaitGroup) { + poolWg.Add(1) + go w.run(poolWg) +} + +func (w *Worker) Stop() { +} + +func (w *Worker) run(poolWg *sync.WaitGroup) { + defer poolWg.Done() + for { + select { + case <-w.ctx.Done(): + return + case task := <-w.taskChan: + err := task.Run(w.id) + w.taskWg.Done() + if err != nil { + panic("not implemented") + } + } + } +} + +func (w *Worker) Id() WorkerId { + return w.id +} diff --git a/race_test.go b/race_test.go new file mode 100644 index 00000000..4c1365da --- /dev/null +++ b/race_test.go @@ -0,0 +1,77 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package gomp + +import ( + "sync" + "testing" +) + +func TestRaceCondition(t *testing.T) { + var ( + numGoroutines = 10000 // Количество горутин + sliceSize = 10000 // Размер слайса (меньше numGoroutines) + ) + slice := make([]float32, sliceSize) + + //var wg sync.WaitGroup + //wg.Add(numGoroutines) + + for i := 0; i < numGoroutines; i++ { + go func(id int) { + //defer wg.Done() + index := id % sliceSize // Индекс может повторяться + slice[index] = float32(id) + }(i) + } + + //wg.Wait() + for i := 0; i < sliceSize; i++ { + if slice[i] != float32(i) { + t.Errorf("slice[%d] = %f, want %f", i, slice[i], float32(i)) + } + } + t.Log(slice) +} + +// СТЕНА Опыта MaxHero90@twitch - 7 лет опыта работы гофером без использования waitGroup научили его наконец ценить эту волшебную атомик структуру богов. +func TestRaceConditionMaxHero90(t *testing.T) { + var ( + numGoroutines = 2 // Количество горутин + sliceSize = 100000 // Размер слайса (меньше numGoroutines) + ) + slice := make([]float32, sliceSize) + slicePtr := &slice + + var wg sync.WaitGroup + wg.Add(numGoroutines) + + for i := 0; i < numGoroutines; i++ { + go func(id int, s *[]float32) { + defer wg.Done() + for j := id; j < len(*s); j += 2 { + (*s)[j] = float32(id) + } + }(i, slicePtr) + } + + wg.Wait() // this do not cause a race condition + // time.Sleep(time.Second * 3) - this cause a race condition + for i := 0; i < sliceSize; i++ { + if (*slicePtr)[i] != float32(i%2) { + t.Errorf("slice[%d] = %f, want %f", i, (*slicePtr)[i], float32(i)) + } + } +} diff --git a/examples/raylib-ecs/systems/example.go b/scene.go similarity index 57% rename from examples/raylib-ecs/systems/example.go rename to scene.go index 90c3b50f..ffde367a 100644 --- a/examples/raylib-ecs/systems/example.go +++ b/scene.go @@ -12,15 +12,22 @@ none :) Thank you for your support! */ -package systems +package gomp import ( "gomp/pkg/ecs" + "time" ) -type exampleController struct{} +type SceneId uint16 -func (s *exampleController) Init(world *ecs.World) {} -func (s *exampleController) Update(world *ecs.World) {} -func (s *exampleController) FixedUpdate(world *ecs.World) {} -func (s *exampleController) Destroy(world *ecs.World) {} +type AnyScene interface { + Init(world ecs.AnyWorld) + Update(dt time.Duration) SceneId + FixedUpdate(dt time.Duration) + Render(dt time.Duration) + Destroy() + OnEnter() + OnExit() + Id() SceneId +} diff --git a/stdcomponents/aabb.go b/stdcomponents/aabb.go new file mode 100644 index 00000000..fd3df068 --- /dev/null +++ b/stdcomponents/aabb.go @@ -0,0 +1,44 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package stdcomponents + +import ( + "gomp/pkg/ecs" + "gomp/vectors" +) + +type AABB struct { + Min vectors.Vec2 + Max vectors.Vec2 +} + +func (a AABB) Center() vectors.Vec2 { + return a.Min.Add(a.Max).Scale(0.5) +} + +func (a AABB) Rect() vectors.Rectangle { + return vectors.Rectangle{ + X: a.Min.X, + Y: a.Min.Y, + Width: a.Max.X - a.Min.X, + Height: a.Max.Y - a.Min.Y, + } +} + +type AABBComponentManager = ecs.ComponentManager[AABB] + +func NewAABBComponentManager() AABBComponentManager { + return ecs.NewComponentManager[AABB](AABBComponentId) +} diff --git a/stdcomponents/animation-player.go b/stdcomponents/animation-player.go new file mode 100644 index 00000000..420f92d7 --- /dev/null +++ b/stdcomponents/animation-player.go @@ -0,0 +1,39 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package stdcomponents + +import ( + "gomp/pkg/ecs" + "time" +) + +type AnimationPlayer struct { + First uint8 + Last uint8 + Current uint8 + Speed float32 + Loop bool + Vertical bool + ElapsedTime time.Duration + FrameDuration time.Duration + State AnimationState + IsInitialized bool +} + +type AnimationPlayerComponentManager = ecs.ComponentManager[AnimationPlayer] + +func NewAnimationPlayerComponentManager() AnimationPlayerComponentManager { + return ecs.NewComponentManager[AnimationPlayer](AnimationPlayerComponentId) +} diff --git a/stdcomponents/animation-state.go b/stdcomponents/animation-state.go new file mode 100644 index 00000000..e88f2b9b --- /dev/null +++ b/stdcomponents/animation-state.go @@ -0,0 +1,27 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package stdcomponents + +import ( + "gomp/pkg/ecs" +) + +type AnimationState int32 + +type AnimationStateComponentManager = ecs.ComponentManager[AnimationState] + +func NewAnimationStateComponentManager() AnimationStateComponentManager { + return ecs.NewComponentManager[AnimationState](AnimationStateComponentId) +} diff --git a/stdcomponents/bvh-tree.go b/stdcomponents/bvh-tree.go new file mode 100644 index 00000000..dd14c993 --- /dev/null +++ b/stdcomponents/bvh-tree.go @@ -0,0 +1,325 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package stdcomponents + +import ( + "github.com/negrel/assert" + "gomp/pkg/ecs" + "gomp/vectors" + "math" + "math/bits" + "slices" +) + +const mortonPrecision = (1 << 16) - 1 + +type BvhNode struct { + ChildIndex int32 // if < 0 then points to BvhLeaf +} + +type BvhLeaf struct { + Id ecs.Entity +} + +type BvhComponent struct { + Entity ecs.Entity + Aabb AABB + Code uint64 +} + +type BvhTree struct { + Nodes ecs.PagedArray[BvhNode] + AabbNodes ecs.PagedArray[AABB] + Leaves ecs.PagedArray[BvhLeaf] + AabbLeaves ecs.PagedArray[AABB] + Codes ecs.PagedArray[uint64] + Components ecs.PagedArray[BvhComponent] + + sorted []BvhComponent +} + +func (t *BvhTree) Init() { + t.Nodes = ecs.NewPagedArray[BvhNode]() + t.AabbNodes = ecs.NewPagedArray[AABB]() + t.Leaves = ecs.NewPagedArray[BvhLeaf]() + t.AabbLeaves = ecs.NewPagedArray[AABB]() + t.Codes = ecs.NewPagedArray[uint64]() + t.Components = ecs.NewPagedArray[BvhComponent]() +} + +func (t *BvhTree) AddComponent(entity ecs.Entity, aabb AABB) { + code := t.morton2D(&aabb) + t.Components.Append(BvhComponent{ + Entity: entity, + Aabb: aabb, + Code: code, + }) +} + +func (t *BvhTree) Query(aabb AABB, result []ecs.Entity) []ecs.Entity { + if t.Nodes.Len() == 0 { // Handle empty tree + return result + } + + // Use stack-based traversal + const stackSize = 32 + stack := [stackSize]int32{0} + stackLen := 1 + + for stackLen > 0 { + stackLen-- + nodeIndex := int(stack[stackLen]) + a := t.AabbNodes.Get(nodeIndex) + + // Early exit if no AABB overlap + if !t.aabbOverlap(*a, aabb) { + continue + } + + node := t.Nodes.Get(nodeIndex) + if node.ChildIndex <= 0 { + // Is a leaf + index := -int(node.ChildIndex) + leafAabb := t.AabbLeaves.Get(index) + if t.aabbOverlap(*leafAabb, aabb) { + result = append(result, t.Leaves.Get(index).Id) + } + continue + } + + // Push child indices (right and left) onto the stack. + stack[stackLen] = node.ChildIndex + 1 + stack[stackLen+1] = node.ChildIndex + stackLen += 2 + } + + return result +} + +// go:inline aabbOverlap checks if two AABB intersect +func (t *BvhTree) aabbOverlap(a, b AABB) bool { + return a.Max.X >= b.Min.X && a.Min.X <= b.Max.X && + a.Max.Y >= b.Min.Y && a.Min.Y <= b.Max.Y +} + +// Expands a 16-bit integer into 32 bits by inserting 1 zero after each bit +func (t *BvhTree) expandBits2D(v uint32) uint32 { + v = (v | (v << 8)) & 0x00FF00FF + v = (v | (v << 4)) & 0x0F0F0F0F + v = (v | (v << 2)) & 0x33333333 + v = (v | (v << 1)) & 0x55555555 + return v +} + +func (t *BvhTree) morton2D(aabb *AABB) uint64 { + center := aabb.Center() + // Scale coordinates to 16-bit integers + //assert.True(center.X >= 0 && center.Y >= 0, "morton2D: center out of range") + + xx := uint64(float64(center.X) * mortonPrecision) + yy := uint64(float64(center.Y) * mortonPrecision) + + assert.True(xx < math.MaxUint64, "morton2D: x out of range") + assert.True(yy < math.MaxUint64, "morton2D: y out of range") + + // Spread the bits of x into the even positions + xx = (xx | (xx << 16)) & 0x0000FFFF0000FFFF + xx = (xx | (xx << 8)) & 0x00FF00FF00FF00FF + xx = (xx | (xx << 4)) & 0x0F0F0F0F0F0F0F0F + xx = (xx | (xx << 2)) & 0x3333333333333333 + xx = (xx | (xx << 1)) & 0x5555555555555555 + + // Spread the bits of y into the even positions and shift to odd positions + yy = (yy | (yy << 16)) & 0x0000FFFF0000FFFF + yy = (yy | (yy << 8)) & 0x00FF00FF00FF00FF + yy = (yy | (yy << 4)) & 0x0F0F0F0F0F0F0F0F + yy = (yy | (yy << 2)) & 0x3333333333333333 + yy = (yy | (yy << 1)) & 0x5555555555555555 + + // Combine x (even bits) and y (odd bits) + return xx | (yy << 1) +} + +func (t *BvhTree) Build() { + // Reset tree + t.Nodes.Reset() + t.AabbNodes.Reset() + t.Leaves.Reset() + t.AabbLeaves.Reset() + t.Codes.Reset() + + if cap(t.sorted) < t.Components.Len() { + t.sorted = make([]BvhComponent, 0, t.Components.Len()) + } + + t.sorted = t.Components.Raw(t.sorted) + + slices.SortFunc(t.sorted, func(a, b BvhComponent) int { + return int(a.Code - b.Code) + }) + + // Add leaves + for i := range t.sorted { + component := t.sorted[i] + t.Leaves.Append(BvhLeaf{Id: component.Entity}) + t.AabbLeaves.Append(component.Aabb) + t.Codes.Append(component.Code) + } + t.Components.Reset() + t.sorted = t.sorted[:0] + + if t.Leaves.Len() == 0 { + return + } + + // Add root node + t.Nodes.Append(BvhNode{-1}) + t.AabbNodes.Append(AABB{}) + + type buildTask struct { + parentIndex int + start int + end int + childrenCreated bool + } + + stack := [64]buildTask{ + {parentIndex: 0, start: 0, end: t.Leaves.Len() - 1, childrenCreated: false}, + } + stackLen := 1 + + for stackLen > 0 { + stackLen-- + // Pop the last task + task := stack[stackLen] + + if !task.childrenCreated { + if task.start == task.end { + // Leaf node + t.Nodes.Get(task.parentIndex).ChildIndex = -int32(task.start) + t.AabbNodes.Set(task.parentIndex, t.AabbLeaves.GetValue(task.start)) + continue + } + + split := t.findSplit(task.start, task.end) + + // Create left and right nodes + leftIndex := t.Nodes.Len() + t.Nodes.Append(BvhNode{-1}) + t.Nodes.Append(BvhNode{-1}) + t.AabbNodes.Append(AABB{}) + t.AabbNodes.Append(AABB{}) + + // Set parent's childIndex to leftIndex + t.Nodes.Get(task.parentIndex).ChildIndex = int32(leftIndex) + + // Push parent task back with childrenCreated=true + stack[stackLen] = buildTask{ + parentIndex: task.parentIndex, + start: task.start, + end: task.end, + childrenCreated: true, + } + stackLen++ + + // Push right child task (split+1 to end) + stack[stackLen] = buildTask{ + parentIndex: leftIndex + 1, + start: split + 1, + end: task.end, + childrenCreated: false, + } + stackLen++ + + // Push left child task (start to split) + stack[stackLen] = buildTask{ + parentIndex: leftIndex, + start: task.start, + end: split, + childrenCreated: false, + } + stackLen++ + } else { + // Merge children's AABBs into parent + leftChildIndex := int(t.Nodes.Get(task.parentIndex).ChildIndex) + rightChildIndex := leftChildIndex + 1 + + leftAABB := t.AabbNodes.Get(leftChildIndex) + rightAABB := t.AabbNodes.Get(rightChildIndex) + + merged := t.mergeAABB(leftAABB, rightAABB) + t.AabbNodes.Set(task.parentIndex, merged) + } + } + t.Components.Reset() +} + +func (t *BvhTree) findSplit(start, end int) int { + // Identical Morton sortedMortonCodes => split the range in the middle. + first := t.Codes.GetValue(start) + last := t.Codes.GetValue(end) + + if first == last { + return (start + end) >> 1 + } + + // Calculate the number of highest bits that are the same + // for all objects, using the count-leading-zeros intrinsic. + commonPrefix := bits.LeadingZeros64(first ^ last) + + // Use binary search to find where the next bit differs. + // Specifically, we are looking for the highest object that + // shares more than commonPrefix bits with the first one. + split := start + step := end - start + + for { + step = (step + 1) >> 1 // exponential decrease + newSplit := split + step // proposed new position + + if newSplit < end { + splitCode := t.Codes.GetValue(newSplit) + splitPrefix := bits.LeadingZeros64(first ^ splitCode) + if splitPrefix > commonPrefix { + split = newSplit + } + } + + if step <= 1 { + break + } + } + + return split +} + +func (t *BvhTree) mergeAABB(a, b *AABB) AABB { + return AABB{ + Min: vectors.Vec2{ + X: min(a.Min.X, b.Min.X), + Y: min(a.Min.Y, b.Min.Y), + }, + Max: vectors.Vec2{ + X: max(a.Max.X, b.Max.X), + Y: max(a.Max.Y, b.Max.Y), + }, + } +} + +type BvhTreeComponentManager = ecs.ComponentManager[BvhTree] + +func NewBvhTreeComponentManager() BvhTreeComponentManager { + return ecs.NewComponentManager[BvhTree](BvhTreeComponentId) +} diff --git a/stdcomponents/camera.go b/stdcomponents/camera.go new file mode 100644 index 00000000..8bcfbe8d --- /dev/null +++ b/stdcomponents/camera.go @@ -0,0 +1,123 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package stdcomponents + +import ( + rl "github.com/gen2brain/raylib-go/raylib" + "gomp/pkg/ecs" + "gomp/vectors" + "image/color" + "math" +) + +const ( + Culling2DFullscreenBB uint8 = iota + Culling2DNone // TODO: set default? + // Culling2DDistance + // Culling2DRectangle + // Culling2DOcclusion difficult to implement + // Culling2DFrustum huh +) + +type CameraLayer uint64 + +// Camera component, defines a camera component +// Camera2D is a struct that defines a 2D camera +// Dst defines the camera's destination on the renderer screen +// Layer defines the camera's layer, default is 0 - disabled +// Order defines the camera's order, ascending order +type Camera struct { + rl.Camera2D + Dst vectors.Rectangle // TODO: remove? + Layer CameraLayer + Culling uint8 + Order int + BlendMode rl.BlendMode + BGColor color.RGBA + Tint color.RGBA +} + +func (c *Camera) Rect() vectors.Rectangle { + // Calculate the non-rotated top-left corner of the view rectangle + x := c.Target.X - (c.Offset.X / c.Zoom) + y := c.Target.Y - (c.Offset.Y / c.Zoom) + width := c.Offset.X * 2 / c.Zoom + height := c.Offset.Y * 2 / c.Zoom + + // When rotation is zero, we can return directly. + if c.Rotation == 0 { + return vectors.Rectangle{ + X: x, + Y: y, + Width: width, + Height: height, + } + } + + // Define the four corners of the non-rotated rectangle + topLeft := vectors.Vec2{X: x, Y: y} + topRight := vectors.Vec2{X: x + width, Y: y} + bottomRight := vectors.Vec2{X: x + width, Y: y + height} + bottomLeft := vectors.Vec2{X: x, Y: y + height} + + // Rotate each corner around the camera.Target using the camera rotation + topLeft = rotatePoint(topLeft, vectors.Vec2(c.Target), float64(c.Rotation)) + topRight = rotatePoint(topRight, vectors.Vec2(c.Target), float64(c.Rotation)) + bottomRight = rotatePoint(bottomRight, vectors.Vec2(c.Target), float64(c.Rotation)) + bottomLeft = rotatePoint(bottomLeft, vectors.Vec2(c.Target), float64(c.Rotation)) + + // Determine the axis-aligned bounding box that contains all rotated points + // TODO: fast math 32bit + minX := math.Min(math.Min(float64(topLeft.X), float64(topRight.X)), math.Min(float64(bottomRight.X), float64(bottomLeft.X))) + maxX := math.Max(math.Max(float64(topLeft.X), float64(topRight.X)), math.Max(float64(bottomRight.X), float64(bottomLeft.X))) + minY := math.Min(math.Min(float64(topLeft.Y), float64(topRight.Y)), math.Min(float64(bottomRight.Y), float64(bottomLeft.Y))) + maxY := math.Max(math.Max(float64(topLeft.Y), float64(topRight.Y)), math.Max(float64(bottomRight.Y), float64(bottomLeft.Y))) + + return vectors.Rectangle{ + X: float32(minX), + Y: float32(minY), + Width: float32(maxX - minX), + Height: float32(maxY - minY), + } +} + +// rotatePoint rotates point p around pivot by angle degrees. +func rotatePoint(p, pivot vectors.Vec2, angle float64) vectors.Vec2 { + // Convert angle from degrees to radians. + // TODO: fast math 32bit + theta := angle * (math.Pi / 180) + sinTheta := float32(math.Sin(theta)) + cosTheta := float32(math.Cos(theta)) + + // Translate point to origin + dx := p.X - pivot.X + dy := p.Y - pivot.Y + + // Apply rotation matrix + rotatedX := dx*cosTheta - dy*sinTheta + rotatedY := dx*sinTheta + dy*cosTheta + + // Translate point back + return vectors.Vec2{ + X: rotatedX + pivot.X, + Y: rotatedY + pivot.Y, + } +} + +type CameraComponentManager = ecs.ComponentManager[Camera] + +func NewCameraComponentManager() CameraComponentManager { + return ecs.NewComponentManager[Camera](CameraComponentId) +} diff --git a/stdcomponents/colliders.go b/stdcomponents/colliders.go new file mode 100644 index 00000000..79127401 --- /dev/null +++ b/stdcomponents/colliders.go @@ -0,0 +1,190 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package stdcomponents + +import ( + "gomp/pkg/ecs" + "gomp/vectors" + "math" +) + +type ColliderShape uint8 + +const ( + InvalidColliderShape ColliderShape = iota + BoxColliderShape + CircleColliderShape + PolygonColliderShape +) + +type CollisionMask uint64 + +func (m CollisionMask) HasLayer(layer CollisionLayer) bool { + return m&(1<> 31 + ySign := math.Float32bits(localDir.Y) >> 31 + localSupport := vectors.Vec2{ + X: c.WH.X * (1 - float32(xSign)), // 0 if negative, WH.X otherwise + Y: c.WH.Y * (1 - float32(ySign)), + } + + // Apply offset and rotate to world space + vertex := localSupport.Sub(c.Offset) + rotated := vectors.Vec2{ + X: vertex.X*cos - vertex.Y*sin, + Y: vertex.X*sin + vertex.Y*cos, + } + + return rotated.Mul(transform.Scale).Add(transform.Position) +} + +type BoxColliderComponentManager = ecs.ComponentManager[BoxCollider] + +func NewBoxColliderComponentManager() BoxColliderComponentManager { + return ecs.NewComponentManager[BoxCollider](ColliderBoxComponentId) +} + +// =========================== +// Circle Collider +// =========================== + +type CircleCollider struct { + Radius float32 + Layer CollisionLayer + Mask CollisionMask + Offset vectors.Vec2 + AllowSleep bool +} + +func (c *CircleCollider) GetSupport(direction vectors.Vec2, transform Transform2d) vectors.Vec2 { + var radiusWithOffset vectors.Vec2 + // Handle zero direction to avoid division by zero + if direction.X == 0 && direction.Y == 0 { + // Fallback to a default direction (e.g., right) + defaultDir := vectors.Vec2{X: c.Radius, Y: 0} + radiusWithOffset = defaultDir.Sub(c.Offset).Mul(transform.Scale) + } else { + // Compute scaled direction without trigonometry + mag := float32(math.Hypot(float64(direction.X), float64(direction.Y))) + invMag := c.Radius / mag + scaledDir := vectors.Vec2{ + X: direction.X * invMag, + Y: direction.Y * invMag, + } + // Apply offset, scale, and translation + radiusWithOffset = scaledDir.Sub(c.Offset).Mul(transform.Scale) + } + + return transform.Position.Add(radiusWithOffset) +} + +type CircleColliderComponentManager = ecs.ComponentManager[CircleCollider] + +func NewCircleColliderComponentManager() CircleColliderComponentManager { + return ecs.NewComponentManager[CircleCollider](ColliderCircleComponentId) +} + +// =========================== +// Polygon Collider +// =========================== + +type PolygonCollider struct { + Vertices []vectors.Vec2 + Layer CollisionLayer + Mask CollisionMask + Offset vectors.Vec2 + AllowSleep bool +} + +func (c *PolygonCollider) GetSupport(direction vectors.Vec2, transform Transform2d) vectors.Vec2 { + maxDot := math.Inf(-1) + var maxVertex vectors.Vec2 + + for _, v := range c.Vertices { + scaled := v.Mul(transform.Scale) + rotated := scaled.Rotate(transform.Rotation) + worldVertex := transform.Position.Add(rotated) + dot := float64(worldVertex.Dot(direction)) + if dot > maxDot { + maxDot = dot + maxVertex = worldVertex + } + } + return maxVertex +} + +type PolygonColliderComponentManager = ecs.ComponentManager[PolygonCollider] + +func NewPolygonColliderComponentManager() PolygonColliderComponentManager { + return ecs.NewComponentManager[PolygonCollider](PolygonColliderComponentId) +} + +// =========================== +// Generic Collider +// =========================== + +type GenericCollider struct { + Shape ColliderShape + Layer CollisionLayer + Mask CollisionMask + Offset vectors.Vec2 + AllowSleep bool +} + +type GenericColliderComponentManager = ecs.ComponentManager[GenericCollider] + +func NewGenericColliderComponentManager() GenericColliderComponentManager { + return ecs.NewComponentManager[GenericCollider](GenericColliderComponentId) +} + +type ColliderSleepState struct{} + +type ColliderSleepStateComponentManager = ecs.ComponentManager[ColliderSleepState] + +func NewColliderSleepStateComponentManager() ColliderSleepStateComponentManager { + return ecs.NewComponentManager[ColliderSleepState](ColliderSleepStateComponentId) +} diff --git a/stdcomponents/collision-cell.go b/stdcomponents/collision-cell.go new file mode 100644 index 00000000..95f2c334 --- /dev/null +++ b/stdcomponents/collision-cell.go @@ -0,0 +1,111 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package stdcomponents + +import ( + "gomp/pkg/ecs" + "gomp/pkg/worker" + "sync" + + "github.com/negrel/assert" +) + +const ( + MembersPerCellSqrt = 4 + membersPerCell = MembersPerCellSqrt * MembersPerCellSqrt +) + +func NewMemberListPool(workerPool *worker.Pool) MemberListPool { + return MemberListPool{ + pool: sync.Pool{ + New: func() any { + list := &MemberList{ + Members: make([]ecs.Entity, 0, membersPerCell), + Lookup: ecs.NewGenMap[ecs.Entity, int](membersPerCell), + InputAcc: make([][]ecs.Entity, workerPool.NumWorkers()), + } + + for i := range list.InputAcc { + list.InputAcc[i] = make([]ecs.Entity, 0, membersPerCell) + } + + return list + }, + }, + } +} + +type MemberListPool struct { + pool sync.Pool +} + +func (p *MemberListPool) Get() *MemberList { + return p.pool.Get().(*MemberList) +} + +func (p *MemberListPool) Put(ml *MemberList) { + p.pool.Put(ml) +} + +type MemberList struct { + Members []ecs.Entity + Lookup ecs.GenMap[ecs.Entity, int] + InputAcc [][]ecs.Entity +} + +func (ml *MemberList) Add(member ecs.Entity) { + ml.Members = append(ml.Members, member) + ml.Lookup.Set(member, len(ml.Members)-1) +} + +func (ml *MemberList) Delete(member ecs.Entity) { + index, ok := ml.Lookup.Get(member) + assert.True(ok) + lastIndex := len(ml.Members) - 1 + if index < lastIndex { + // Swap the dead element with the last one + ml.Members[index], ml.Members[lastIndex] = ml.Members[lastIndex], ml.Members[index] + // Update Lookup table + ml.Lookup.Set(ml.Members[index], index) + } + ml.Members = ml.Members[:lastIndex] + ml.Lookup.Delete(member) +} + +func (ml *MemberList) Reset() { + ml.Lookup.Reset() + ml.Members = ml.Members[:0:membersPerCell] + for i := range ml.InputAcc { + ml.InputAcc[i] = ml.InputAcc[i][:0:membersPerCell] + } +} + +func (ml *MemberList) Has(member ecs.Entity) bool { + return ml.Lookup.Has(member) +} + +type CollisionCell struct { + Index SpatialCellIndex + Layer CollisionLayer + Grid ecs.Entity + Size float32 + Members *MemberList +} + +type CollisionCellComponentManager = ecs.ComponentManager[CollisionCell] + +func NewCollisionCellComponentManager() CollisionCellComponentManager { + return ecs.NewComponentManager[CollisionCell](CollisionCellComponentId) +} diff --git a/stdcomponents/collision-chunk.go b/stdcomponents/collision-chunk.go new file mode 100644 index 00000000..6ae15b97 --- /dev/null +++ b/stdcomponents/collision-chunk.go @@ -0,0 +1,30 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package stdcomponents + +import ( + "gomp/pkg/ecs" +) + +type CollisionChunk struct { + Size float32 + Layer CollisionLayer +} + +type CollisionChunkComponentManager = ecs.ComponentManager[CollisionChunk] + +func NewCollisionChunkComponentManager() CollisionChunkComponentManager { + return ecs.NewComponentManager[CollisionChunk](CollisionChunkComponentId) +} diff --git a/stdcomponents/collision-grid-member.go b/stdcomponents/collision-grid-member.go new file mode 100644 index 00000000..9f808150 --- /dev/null +++ b/stdcomponents/collision-grid-member.go @@ -0,0 +1,27 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package stdcomponents + +import "gomp/pkg/ecs" + +type CollisionGridMember struct { + Grid ecs.Entity +} + +type CollisionGridMemberComponentManager = ecs.ComponentManager[CollisionGridMember] + +func NewCollisionGridMemberComponentManager() CollisionGridMemberComponentManager { + return ecs.NewComponentManager[CollisionGridMember](CollisionGridMemberComponentId) +} diff --git a/stdcomponents/collision-grid.go b/stdcomponents/collision-grid.go new file mode 100644 index 00000000..563f4a5e --- /dev/null +++ b/stdcomponents/collision-grid.go @@ -0,0 +1,88 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +<- tema881 Donated 100 RUB + +Thank you for your support! +*/ + +package stdcomponents + +import ( + "gomp/pkg/ecs" + "gomp/vectors" +) + +func NewCollisionGrid(collisionLayer CollisionLayer, cellSize float32) CollisionGrid { + g := CollisionGrid{ + Layer: collisionLayer, + CellSize: cellSize, + CellMap: ecs.NewGenMap[SpatialCellIndex, ecs.Entity](1024), + CreateCellsAccumulator: nil, + } + + return g +} + +// CollisionGrid is a grid of cells that can be used for collision detection +type CollisionGrid struct { + Layer CollisionLayer // Layer of the grid + CellSize float32 // Size of a cell + + CreateCellsAccumulator []ecs.GenMap[SpatialCellIndex, struct{}] + CellMap ecs.GenMap[SpatialCellIndex, ecs.Entity] + // TODO: add chunks with fixed number of cells (32 x 32) +} + +type SpatialCellIndex struct { + X, Y int +} + +func (i SpatialCellIndex) ToVec2() vectors.Vec2 { + return vectors.Vec2{X: float32(i.X), Y: float32(i.Y)} +} + +// Query returns the EntityIds of Cells that intersect the AABB +func (g *CollisionGrid) Query(bb AABB, result *ecs.PagedArray[ecs.Entity]) { + // get spatial index of aabb + minSpatialCellIndex := g.GetCellIndex(bb.Min) + maxSpatialCellIndex := g.GetCellIndex(bb.Max) + // make a list of all spatial indexes that intersect the aabb + // get cells that intersect the aabb by spatial indexes + for i := minSpatialCellIndex.X; i <= maxSpatialCellIndex.X; i++ { + for j := minSpatialCellIndex.Y; j <= maxSpatialCellIndex.Y; j++ { + spatialIndex := SpatialCellIndex{X: i, Y: j} + cellEntity, exists := g.CellMap.Get(spatialIndex) + if !exists { + continue + } + result.Append(cellEntity) + } + } +} + +func (g *CollisionGrid) GetCellIndex(position vectors.Vec2) SpatialCellIndex { + return SpatialCellIndex{ + X: int(position.X / g.CellSize), + Y: int(position.Y / g.CellSize), + } +} + +func (g *CollisionGrid) CalculateSpatialHash(bb AABB) SpatialHash { + return SpatialHash{ + Min: g.GetCellIndex(bb.Min), + Max: g.GetCellIndex(bb.Max), + } +} + +type CollisionGridComponentManager = ecs.ComponentManager[CollisionGrid] + +func NewCollisionGridComponentManager() CollisionGridComponentManager { + return ecs.NewComponentManager[CollisionGrid](CollisionGridComponentId) +} diff --git a/stdcomponents/collision.go b/stdcomponents/collision.go new file mode 100644 index 00000000..438f756c --- /dev/null +++ b/stdcomponents/collision.go @@ -0,0 +1,43 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package stdcomponents + +import ( + "gomp/pkg/ecs" + "gomp/vectors" +) + +type CollisionState uint8 + +const ( + CollisionStateNone CollisionState = iota + CollisionStateEnter + CollisionStateStay + CollisionStateExit +) + +// Collision Marks a proxy entity as representing a collision pair between E1 and E2 +type Collision struct { + E1, E2 ecs.Entity + State CollisionState + Normal vectors.Vec2 // Collision normal (direction) + Depth float32 // Penetration depth +} + +type CollisionComponentManager = ecs.ComponentManager[Collision] + +func NewCollisionComponentManager() CollisionComponentManager { + return ecs.NewComponentManager[Collision](CollisionComponentId) +} diff --git a/stdcomponents/flip.go b/stdcomponents/flip.go new file mode 100644 index 00000000..32775772 --- /dev/null +++ b/stdcomponents/flip.go @@ -0,0 +1,27 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package stdcomponents + +import "gomp/pkg/ecs" + +type Flip struct { + X, Y bool +} + +type FlipComponentManager = ecs.ComponentManager[Flip] + +func NewFlipComponentManager() FlipComponentManager { + return ecs.NewComponentManager[Flip](FlipComponentId) +} diff --git a/stdcomponents/frame-buffer-2d.go b/stdcomponents/frame-buffer-2d.go new file mode 100644 index 00000000..a7c053c1 --- /dev/null +++ b/stdcomponents/frame-buffer-2d.go @@ -0,0 +1,48 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package stdcomponents + +import ( + rl "github.com/gen2brain/raylib-go/raylib" + "gomp/pkg/ecs" + "gomp/vectors" + "image/color" +) + +type FrameBuffer2D struct { + Position rl.Vector2 + Frame rl.Rectangle + Texture rl.RenderTexture2D + Layer CameraLayer + BlendMode rl.BlendMode + Rotation float32 + Tint color.RGBA + Dst rl.Rectangle +} + +func (d FrameBuffer2D) FrameRect() vectors.Rectangle { + return vectors.Rectangle{ + X: d.Frame.X, + Y: d.Frame.Y, + Width: d.Frame.Width, + Height: d.Frame.Height, + } +} + +type FrameBuffer2DComponentManager = ecs.ComponentManager[FrameBuffer2D] + +func NewFrameBuffer2DComponentManager() FrameBuffer2DComponentManager { + return ecs.NewComponentManager[FrameBuffer2D](FrameBuffer2DComponentId) +} diff --git a/stdcomponents/ids.go b/stdcomponents/ids.go new file mode 100644 index 00000000..8184c516 --- /dev/null +++ b/stdcomponents/ids.go @@ -0,0 +1,54 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +package stdcomponents + +import ( + "gomp/pkg/ecs" +) + +// StdLastComponentId MUST always be the last +const ( + InvalidComponentId ecs.ComponentId = iota + TransformComponentId + PositionComponentId + RotationComponentId + ScaleComponentId + FlipComponentId + VelocityComponentId + SpriteComponentId + SpriteSheetComponentId + SpriteMatrixComponentId + RLTextureProComponentId + AnimationPlayerComponentId + AnimationStateComponentId + TintComponentId + NetworkComponentId + RenderableComponentId + YSortComponentId + RenderOrderComponentId + GenericColliderComponentId + ColliderBoxComponentId + ColliderCircleComponentId + ColliderSleepStateComponentId + PolygonColliderComponentId + CollisionComponentId + SpatialHashComponentId + AABBComponentId + RigidBodyComponentId + BvhTreeComponentId + CollisionGridComponentId + CollisionCellComponentId + CollisionChunkComponentId + CollisionGridMemberComponentId + s + FrameBuffer2DComponentId + CameraComponentId + TexturePositionSmoothComponentId + RenderVisibleComponentId + + StdLastComponentId // StdLastComponentId MUST always be the last +) diff --git a/stdcomponents/network.go b/stdcomponents/network.go new file mode 100644 index 00000000..558db91b --- /dev/null +++ b/stdcomponents/network.go @@ -0,0 +1,30 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package stdcomponents + +import "gomp/pkg/ecs" + +type NetworkId int32 +type Network struct { + Id NetworkId + PatchIn []byte + PatchOut []byte +} + +type NetworkComponentManager = ecs.ComponentManager[Network] + +func NewNetworkComponentManager() NetworkComponentManager { + return ecs.NewComponentManager[Network](NetworkComponentId) +} diff --git a/stdcomponents/position-smooth.go b/stdcomponents/position-smooth.go new file mode 100644 index 00000000..5bcd7614 --- /dev/null +++ b/stdcomponents/position-smooth.go @@ -0,0 +1,35 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package stdcomponents + +import ( + "gomp/pkg/ecs" +) + +const ( + TexturePositionSmoothOff TexturePositionSmooth = iota + TexturePositionSmoothLerp + TexturePositionSmoothExpDecay +) + +// TexturePositionSmooth is the component tag for stdsystems.TexturePositionSmoothSystem +// TODO: refactor or make stable realization +type TexturePositionSmooth uint8 + +type TexturePositionSmoothComponentManager = ecs.ComponentManager[TexturePositionSmooth] + +func NewTexturePositionSmoothComponentManager() TexturePositionSmoothComponentManager { + return ecs.NewComponentManager[TexturePositionSmooth](TexturePositionSmoothComponentId) +} diff --git a/stdcomponents/position.go b/stdcomponents/position.go new file mode 100644 index 00000000..d1bbed5b --- /dev/null +++ b/stdcomponents/position.go @@ -0,0 +1,30 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package stdcomponents + +import ( + "gomp/pkg/ecs" + "gomp/vectors" +) + +type Position struct { + XY vectors.Vec2 +} + +type PositionComponentManager = ecs.ComponentManager[Position] + +func NewPositionComponentManager() PositionComponentManager { + return ecs.NewComponentManager[Position](PositionComponentId) +} diff --git a/stdcomponents/render-order.go b/stdcomponents/render-order.go new file mode 100644 index 00000000..68f0f638 --- /dev/null +++ b/stdcomponents/render-order.go @@ -0,0 +1,27 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package stdcomponents + +import "gomp/pkg/ecs" + +type RenderOrder struct { + CalculatedZ float32 +} + +type RenderOrderComponentManager = ecs.ComponentManager[RenderOrder] + +func NewRenderOrderComponentManager() RenderOrderComponentManager { + return ecs.NewComponentManager[RenderOrder](RenderOrderComponentId) +} diff --git a/stdcomponents/render-visible.go b/stdcomponents/render-visible.go new file mode 100644 index 00000000..7ab8b444 --- /dev/null +++ b/stdcomponents/render-visible.go @@ -0,0 +1,25 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package stdcomponents + +import "gomp/pkg/ecs" + +type RenderVisible struct{} + +type RenderVisibleComponentManager = ecs.ComponentManager[RenderVisible] + +func NewRenderVisibleComponentManager() RenderVisibleComponentManager { + return ecs.NewComponentManager[RenderVisible](RenderVisibleComponentId) +} diff --git a/stdcomponents/renderable.go b/stdcomponents/renderable.go new file mode 100644 index 00000000..0d60fb1e --- /dev/null +++ b/stdcomponents/renderable.go @@ -0,0 +1,37 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package stdcomponents + +import "gomp/pkg/ecs" + +const ( + InvalidRenderableType RenderableType = iota + SpriteRenderableType + SpriteMatrixRenderableType +) + +type Renderable struct { + CameraMask CameraLayer + Type RenderableType + Observed bool +} + +type RenderableType uint8 + +type RenderableComponentManager = ecs.ComponentManager[Renderable] + +func NewRenderableComponentManager() RenderableComponentManager { + return ecs.NewComponentManager[Renderable](RenderableComponentId) +} diff --git a/stdcomponents/rigidbody.go b/stdcomponents/rigidbody.go new file mode 100644 index 00000000..84ff8423 --- /dev/null +++ b/stdcomponents/rigidbody.go @@ -0,0 +1,28 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package stdcomponents + +import "gomp/pkg/ecs" + +type RigidBody struct { + IsStatic bool + Mass float32 +} + +type RigidBodyComponentManager = ecs.ComponentManager[RigidBody] + +func NewRigidBodyComponentManager() RigidBodyComponentManager { + return ecs.NewComponentManager[RigidBody](RigidBodyComponentId) +} diff --git a/stdcomponents/rl-texture-pro.go b/stdcomponents/rl-texture-pro.go new file mode 100644 index 00000000..b6050790 --- /dev/null +++ b/stdcomponents/rl-texture-pro.go @@ -0,0 +1,46 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package stdcomponents + +import ( + rl "github.com/gen2brain/raylib-go/raylib" + "gomp/pkg/ecs" + "gomp/vectors" + "image/color" +) + +type RLTexturePro struct { + Texture *rl.Texture2D + Frame rl.Rectangle + Origin rl.Vector2 + Tint color.RGBA + Dest rl.Rectangle + Rotation float32 +} + +func (t *RLTexturePro) Rect() vectors.Rectangle { + return vectors.Rectangle{ + X: t.Dest.X - t.Origin.X, + Y: t.Dest.Y - t.Origin.Y, + Width: t.Dest.Width, + Height: t.Dest.Height, + } +} + +type RLTextureProComponentManager = ecs.ComponentManager[RLTexturePro] + +func NewRlTextureProComponentManager() RLTextureProComponentManager { + return ecs.NewComponentManager[RLTexturePro](RLTextureProComponentId) +} diff --git a/stdcomponents/rotation.go b/stdcomponents/rotation.go new file mode 100644 index 00000000..d8856886 --- /dev/null +++ b/stdcomponents/rotation.go @@ -0,0 +1,40 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package stdcomponents + +import ( + "gomp/pkg/ecs" + "gomp/vectors" + "math" +) + +type Rotation struct { + Angle vectors.Radians +} + +func (r Rotation) SetFromDegrees(deg float64) Rotation { + r.Angle = deg * math.Pi / 180 + return r +} + +func (r Rotation) Degrees() float64 { + return r.Angle * 180 / math.Pi +} + +type RotationComponentManager = ecs.ComponentManager[Rotation] + +func NewRotationComponentManager() RotationComponentManager { + return ecs.NewComponentManager[Rotation](RotationComponentId) +} diff --git a/stdcomponents/scale.go b/stdcomponents/scale.go new file mode 100644 index 00000000..9d2925bc --- /dev/null +++ b/stdcomponents/scale.go @@ -0,0 +1,30 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package stdcomponents + +import ( + "gomp/pkg/ecs" + "gomp/vectors" +) + +type Scale struct { + XY vectors.Vec2 +} + +type ScaleComponentManager = ecs.ComponentManager[Scale] + +func NewScaleComponentManager() ScaleComponentManager { + return ecs.NewComponentManager[Scale](ScaleComponentId) +} diff --git a/stdcomponents/spatial-index.go b/stdcomponents/spatial-index.go new file mode 100644 index 00000000..601e4b27 --- /dev/null +++ b/stdcomponents/spatial-index.go @@ -0,0 +1,30 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package stdcomponents + +import ( + "gomp/pkg/ecs" +) + +type SpatialHash struct { + Min SpatialCellIndex + Max SpatialCellIndex +} + +type SpatialHashComponentManager = ecs.ComponentManager[SpatialHash] + +func NewSpatialHashComponentManager() SpatialHashComponentManager { + return ecs.NewComponentManager[SpatialHash](SpatialHashComponentId) +} diff --git a/stdcomponents/sprite-matrix.go b/stdcomponents/sprite-matrix.go new file mode 100644 index 00000000..e2afa6a3 --- /dev/null +++ b/stdcomponents/sprite-matrix.go @@ -0,0 +1,44 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package stdcomponents + +import ( + rl "github.com/gen2brain/raylib-go/raylib" + "gomp/pkg/ecs" + "gomp/vectors" +) + +type SpriteMatrixAnimation struct { + Name string + Frame rl.Rectangle + NumOfFrames uint8 + Vertical bool + Loop bool +} + +type SpriteMatrix struct { + Texture *rl.Texture2D + Origin rl.Vector2 + Dest rl.Rectangle + FPS int32 + Animations []SpriteMatrixAnimation + Rotation vectors.Radians +} + +type SpriteMatrixComponentManager = ecs.SharedComponentManager[SpriteMatrix] + +func NewSpriteMatrixComponentManager() SpriteMatrixComponentManager { + return ecs.NewSharedComponentManager[SpriteMatrix](SpriteMatrixComponentId) +} diff --git a/stdcomponents/sprite-sheet.go b/stdcomponents/sprite-sheet.go new file mode 100644 index 00000000..3017e6f5 --- /dev/null +++ b/stdcomponents/sprite-sheet.go @@ -0,0 +1,35 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package stdcomponents + +import ( + rl "github.com/gen2brain/raylib-go/raylib" + "gomp/pkg/ecs" +) + +type SpriteSheet struct { + Texture *rl.Texture2D + Frame rl.Rectangle + Origin rl.Vector2 + NumOfFrames int32 + FPS int32 + Vertical bool +} + +type SpriteSheetComponentManager = ecs.ComponentManager[SpriteSheet] + +func NewSpriteSheetComponentManager() SpriteSheetComponentManager { + return ecs.NewComponentManager[SpriteSheet](SpriteSheetComponentId) +} diff --git a/stdcomponents/sprite.go b/stdcomponents/sprite.go new file mode 100644 index 00000000..80e9ead4 --- /dev/null +++ b/stdcomponents/sprite.go @@ -0,0 +1,34 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package stdcomponents + +import ( + rl "github.com/gen2brain/raylib-go/raylib" + "gomp/pkg/ecs" + "image/color" +) + +type Sprite struct { + Texture *rl.Texture2D + Frame rl.Rectangle + Origin rl.Vector2 + Tint color.RGBA +} + +type SpriteComponentManager = ecs.ComponentManager[Sprite] + +func NewSpriteComponentManager() SpriteComponentManager { + return ecs.NewComponentManager[Sprite](SpriteComponentId) +} diff --git a/stdcomponents/tint.go b/stdcomponents/tint.go new file mode 100644 index 00000000..9b26769b --- /dev/null +++ b/stdcomponents/tint.go @@ -0,0 +1,28 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package stdcomponents + +import ( + "gomp/pkg/ecs" + "image/color" +) + +type Tint = color.RGBA // TODO: remove type alias + +type TintComponentManager = ecs.ComponentManager[Tint] + +func NewTintComponentManager() TintComponentManager { + return ecs.NewComponentManager[Tint](TintComponentId) +} diff --git a/stdcomponents/transform.go b/stdcomponents/transform.go new file mode 100644 index 00000000..7dd3e18f --- /dev/null +++ b/stdcomponents/transform.go @@ -0,0 +1,32 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package stdcomponents + +import ( + "gomp/pkg/ecs" + "gomp/vectors" +) + +type Transform2d struct { + Position vectors.Vec2 + Rotation vectors.Radians + Scale vectors.Vec2 +} + +type TransformComponentManager = ecs.ComponentManager[Transform2d] + +func NewTransformComponentManager() TransformComponentManager { + return ecs.NewComponentManager[Transform2d](TransformComponentId) +} diff --git a/stdcomponents/velocity.go b/stdcomponents/velocity.go new file mode 100644 index 00000000..24098f75 --- /dev/null +++ b/stdcomponents/velocity.go @@ -0,0 +1,39 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package stdcomponents + +import ( + "gomp/pkg/ecs" + "gomp/vectors" +) + +type Velocity struct { + X, Y float32 +} + +func (v Velocity) Vec2() vectors.Vec2 { + return vectors.Vec2{X: v.X, Y: v.Y} +} + +func (v *Velocity) SetVec2(velocity vectors.Vec2) { + v.X = velocity.X + v.Y = velocity.Y +} + +type VelocityComponentManager = ecs.ComponentManager[Velocity] + +func NewVelocityComponentManager() VelocityComponentManager { + return ecs.NewComponentManager[Velocity](VelocityComponentId) +} diff --git a/stdcomponents/ysort.go b/stdcomponents/ysort.go new file mode 100644 index 00000000..1eef5b57 --- /dev/null +++ b/stdcomponents/ysort.go @@ -0,0 +1,26 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package stdcomponents + +import "gomp/pkg/ecs" + +type YSort struct { +} + +type YSortComponentManager = ecs.ComponentManager[YSort] + +func NewYSortComponentManager() YSortComponentManager { + return ecs.NewComponentManager[YSort](YSortComponentId) +} diff --git a/stdentities/collision-grid.go b/stdentities/collision-grid.go new file mode 100644 index 00000000..76af2170 --- /dev/null +++ b/stdentities/collision-grid.go @@ -0,0 +1,37 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package stdentities + +import ( + "gomp/pkg/ecs" + "gomp/stdcomponents" +) + +type CreateCollisionGridManagers struct { + EntityManager *ecs.EntityManager + Grid *stdcomponents.CollisionGridComponentManager +} + +func CreateCollisionGrid( + props *CreateCollisionGridManagers, + layer stdcomponents.CollisionLayer, + cellSize float32, +) ecs.Entity { + e := props.EntityManager.Create() + + props.Grid.Create(e, stdcomponents.NewCollisionGrid(layer, cellSize)) + + return e +} diff --git a/examples/raylib-ecs/systems/animation-player.go b/stdsystems/animation-player.go similarity index 55% rename from examples/raylib-ecs/systems/animation-player.go rename to stdsystems/animation-player.go index 021906b5..8db224ae 100644 --- a/examples/raylib-ecs/systems/animation-player.go +++ b/stdsystems/animation-player.go @@ -4,27 +4,35 @@ Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package systems +package stdsystems import ( - "fmt" - "gomp/examples/raylib-ecs/components" "gomp/pkg/ecs" + "gomp/pkg/worker" + "gomp/stdcomponents" "time" "github.com/negrel/assert" ) -type animationPlayerController struct{} +func NewAnimationPlayerSystem() AnimationPlayerSystem { + return AnimationPlayerSystem{} +} -func (s *animationPlayerController) Init(world *ecs.World) {} -func (s *animationPlayerController) Update(world *ecs.World) { - animationPlayers := components.AnimationPlayerService.GetManager(world) +type AnimationPlayerSystem struct { + AnimationPlayers *ecs.ComponentManager[stdcomponents.AnimationPlayer] + lastRunAt time.Time +} - animationPlayers.AllDataParallel(func(animation *components.AnimationPlayer) bool { - animation.ElapsedTime += time.Duration(float32(world.DtUpdate().Microseconds())*animation.Speed) * time.Microsecond +func (s *AnimationPlayerSystem) Init() { + s.lastRunAt = time.Now() +} +func (s *AnimationPlayerSystem) Run() { + dt := time.Since(s.lastRunAt) + s.AnimationPlayers.ProcessComponents(func(animation *stdcomponents.AnimationPlayer, workerId worker.WorkerId) { + animation.ElapsedTime += time.Duration(float32(dt.Microseconds())*animation.Speed) * time.Microsecond - assert.True(animation.FrameDuration > 0, fmt.Errorf("frame duration must be greater than 0 (got %v)", animation.FrameDuration)) + assert.True(animation.FrameDuration > 0, "frame duration must be greater than 0") // Check if animation is playing backwards if animation.Speed < 0 { @@ -55,8 +63,7 @@ func (s *animationPlayerController) Update(world *ecs.World) { } } - return true }) + s.lastRunAt = time.Now() } -func (s *animationPlayerController) FixedUpdate(world *ecs.World) {} -func (s *animationPlayerController) Destroy(world *ecs.World) {} +func (s *AnimationPlayerSystem) Destroy() {} diff --git a/examples/raylib-ecs/systems/animation-spritematrix.go b/stdsystems/animation-spritematrix.go similarity index 50% rename from examples/raylib-ecs/systems/animation-spritematrix.go rename to stdsystems/animation-spritematrix.go index 00019b10..62bd10c6 100644 --- a/examples/raylib-ecs/systems/animation-spritematrix.go +++ b/stdsystems/animation-spritematrix.go @@ -4,29 +4,35 @@ Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package systems +package stdsystems import ( - "gomp/examples/raylib-ecs/components" - ecs2 "gomp/pkg/ecs" + "gomp/pkg/ecs" + "gomp/stdcomponents" "time" ) -type animationSpriteMatrixController struct{} +func NewAnimationSpriteMatrixSystem() AnimationSpriteMatrixSystem { + return AnimationSpriteMatrixSystem{} +} -func (s *animationSpriteMatrixController) Init(world *ecs2.World) {} -func (s *animationSpriteMatrixController) Update(world *ecs2.World) { - animationPlayers := components.AnimationPlayerService.GetManager(world) - animationStates := components.AnimationStateService.GetManager(world) - spriteMatrixes := components.SpriteMatrixService.GetManager(world) +type AnimationSpriteMatrixSystem struct { + World *ecs.EntityManager + AnimationPlayers *stdcomponents.AnimationPlayerComponentManager + AnimationStates *stdcomponents.AnimationStateComponentManager + SpriteMatrixes *stdcomponents.SpriteMatrixComponentManager +} - animationPlayers.AllParallel(func(e ecs2.Entity, animationPlayer *components.AnimationPlayer) bool { - spriteMatrix := spriteMatrixes.Get(e) +func (s *AnimationSpriteMatrixSystem) Init() {} +func (s *AnimationSpriteMatrixSystem) Run() { + s.AnimationPlayers.EachEntity(func(e ecs.Entity) bool { + animationPlayer := s.AnimationPlayers.GetUnsafe(e) + spriteMatrix := s.SpriteMatrixes.Get(e) if spriteMatrix == nil { return true } - animationStatePtr := animationStates.Get(e) + animationStatePtr := s.AnimationStates.GetUnsafe(e) if animationStatePtr == nil { return true } @@ -38,8 +44,6 @@ func (s *animationSpriteMatrixController) Update(world *ecs2.World) { currentAnimation := spriteMatrix.Animations[animationState] - animationPlayer.First = 0 - animationPlayer.Current = 0 animationPlayer.Last = currentAnimation.NumOfFrames - 1 animationPlayer.Loop = currentAnimation.Loop animationPlayer.Vertical = currentAnimation.Vertical @@ -51,5 +55,4 @@ func (s *animationSpriteMatrixController) Update(world *ecs2.World) { return true }) } -func (s *animationSpriteMatrixController) FixedUpdate(world *ecs2.World) {} -func (s *animationSpriteMatrixController) Destroy(world *ecs2.World) {} +func (s *AnimationSpriteMatrixSystem) Destroy() {} diff --git a/stdsystems/asset-library.go b/stdsystems/asset-library.go new file mode 100644 index 00000000..b640b233 --- /dev/null +++ b/stdsystems/asset-library.go @@ -0,0 +1,33 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +package stdsystems + +import ( + "gomp" +) + +func NewAssetLibSystem(assets []gomp.AnyAssetLibrary) AssetLibSystem { + return AssetLibSystem{ + assets: assets, + } +} + +type AssetLibSystem struct { + assets []gomp.AnyAssetLibrary +} + +func (s *AssetLibSystem) Init() {} +func (s *AssetLibSystem) Run() { + for _, asset := range s.assets { + asset.LoadAll() + } +} +func (s *AssetLibSystem) Destroy() { + for _, asset := range s.assets { + asset.UnloadAll() + } +} diff --git a/stdsystems/bvh-tree.go b/stdsystems/bvh-tree.go new file mode 100644 index 00000000..4ab54b28 --- /dev/null +++ b/stdsystems/bvh-tree.go @@ -0,0 +1,195 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package stdsystems + +import ( + "cmp" + "gomp/pkg/ecs" + "gomp/stdcomponents" + "gomp/vectors" + "math/bits" + "slices" + "time" +) + +func NewBvhTreeSystem() BvhTreeSystem { + return BvhTreeSystem{} +} + +type BvhTreeSystem struct { + EntityManager *ecs.EntityManager +} + +func (s *BvhTreeSystem) Init() {} +func (s *BvhTreeSystem) Run(dt time.Duration) {} +func (s *BvhTreeSystem) Destroy() {} + +func (s *BvhTreeSystem) build(t *stdcomponents.BvhTree) { + // Reset tree + t.Nodes.Reset() + t.AabbNodes.Reset() + t.Leaves.Reset() + t.AabbLeaves.Reset() + t.Codes.Reset() + + var sorted []stdcomponents.BvhComponent + sorted = t.Components.Raw(sorted) + + slices.SortFunc(sorted, func(a, b stdcomponents.BvhComponent) int { + return cmp.Compare(a.Code, b.Code) + }) + + // Add leaves + for i := range sorted { + component := sorted[i] + t.Leaves.Append(stdcomponents.BvhLeaf{Id: component.Entity}) + t.AabbLeaves.Append(component.Aabb) + t.Codes.Append(component.Code) + } + t.Components.Reset() + + if t.Leaves.Len() == 0 { + return + } + + // Add root node + t.Nodes.Append(stdcomponents.BvhNode{-1}) + t.AabbNodes.Append(stdcomponents.AABB{}) + + type buildTask struct { + parentIndex int + start int + end int + childrenCreated bool + } + + stack := []buildTask{ + {parentIndex: 0, start: 0, end: t.Leaves.Len() - 1, childrenCreated: false}, + } + + for len(stack) > 0 { + // Pop the last task + task := stack[len(stack)-1] + stack = stack[:len(stack)-1] + + if !task.childrenCreated { + if task.start == task.end { + // Leaf node + t.Nodes.Get(task.parentIndex).ChildIndex = -int32(task.start) + t.AabbNodes.Set(task.parentIndex, t.AabbLeaves.GetValue(task.start)) + continue + } + + split := s.findSplit(t, task.start, task.end) + + // Create left and right nodes + leftIndex := t.Nodes.Len() + t.Nodes.Append(stdcomponents.BvhNode{-1}) + t.Nodes.Append(stdcomponents.BvhNode{-1}) + t.AabbNodes.Append(stdcomponents.AABB{}) + t.AabbNodes.Append(stdcomponents.AABB{}) + + // Set parent's childIndex to leftIndex + t.Nodes.Get(task.parentIndex).ChildIndex = int32(leftIndex) + + // Push parent task back with childrenCreated=true + stack = append(stack, buildTask{ + parentIndex: task.parentIndex, + start: task.start, + end: task.end, + childrenCreated: true, + }) + + // Push right child task (split+1 to end) + stack = append(stack, buildTask{ + parentIndex: leftIndex + 1, + start: split + 1, + end: task.end, + childrenCreated: false, + }) + + // Push left child task (start to split) + stack = append(stack, buildTask{ + parentIndex: leftIndex, + start: task.start, + end: split, + childrenCreated: false, + }) + } else { + // Merge children's AABBs into parent + leftChildIndex := int(t.Nodes.Get(task.parentIndex).ChildIndex) + rightChildIndex := leftChildIndex + 1 + + leftAABB := t.AabbNodes.Get(leftChildIndex) + rightAABB := t.AabbNodes.Get(rightChildIndex) + + merged := s.mergeAABB(leftAABB, rightAABB) + t.AabbNodes.Set(task.parentIndex, merged) + } + } + t.Components.Reset() +} + +func (s *BvhTreeSystem) findSplit(t *stdcomponents.BvhTree, start, end int) int { + // Identical Morton sortedMortonCodes => split the range in the middle. + first := t.Codes.GetValue(start) + last := t.Codes.GetValue(end) + + if first == last { + return (start + end) >> 1 + } + + // Calculate the number of highest bits that are the same + // for all objects, using the count-leading-zeros intrinsic. + commonPrefix := bits.LeadingZeros64(first ^ last) + + // Use binary search to find where the next bit differs. + // Specifically, we are looking for the highest object that + // shares more than commonPrefix bits with the first one. + split := start + step := end - start + + for { + step = (step + 1) >> 1 // exponential decrease + newSplit := split + step // proposed new position + + if newSplit < end { + splitCode := t.Codes.GetValue(newSplit) + splitPrefix := bits.LeadingZeros64(first ^ splitCode) + if splitPrefix > commonPrefix { + split = newSplit + } + } + + if step <= 1 { + break + } + } + + return split +} + +func (s *BvhTreeSystem) mergeAABB(a, b *stdcomponents.AABB) stdcomponents.AABB { + return stdcomponents.AABB{ + Min: vectors.Vec2{ + X: min(a.Min.X, b.Min.X), + Y: min(a.Min.Y, b.Min.Y), + }, + Max: vectors.Vec2{ + X: max(a.Max.X, b.Max.X), + Y: max(a.Max.Y, b.Max.Y), + }, + } +} diff --git a/stdsystems/collider.go b/stdsystems/collider.go new file mode 100644 index 00000000..365c3771 --- /dev/null +++ b/stdsystems/collider.go @@ -0,0 +1,217 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +<- rpecb Donated 500 RUB + +Thank you for your support! +*/ + +package stdsystems + +import ( + "github.com/negrel/assert" + "gomp/pkg/core" + "gomp/pkg/ecs" + "gomp/pkg/worker" + "gomp/stdcomponents" + "gomp/vectors" + "time" +) + +func NewColliderSystem() ColliderSystem { + return ColliderSystem{} +} + +type ColliderSystem struct { + EntityManager *ecs.EntityManager + Positions *stdcomponents.PositionComponentManager + Scales *stdcomponents.ScaleComponentManager + Rotations *stdcomponents.RotationComponentManager + Velocities *stdcomponents.VelocityComponentManager + GenericColliders *stdcomponents.GenericColliderComponentManager + BoxColliders *stdcomponents.BoxColliderComponentManager + CircleColliders *stdcomponents.CircleColliderComponentManager + ColliderSleepStateComponentManager *stdcomponents.ColliderSleepStateComponentManager + AABB *stdcomponents.AABBComponentManager + + numWorkers int + accAABB [][]ecs.Entity + accGenericColliders [][]ecs.Entity + accColliderSleepCreate [][]ecs.Entity + accColliderSleepDelete [][]ecs.Entity + Engine *core.Engine +} + +func (s *ColliderSystem) Init() { + s.numWorkers = s.Engine.Pool().NumWorkers() + s.accAABB = make([][]ecs.Entity, s.numWorkers) + s.accGenericColliders = make([][]ecs.Entity, s.numWorkers) + + s.accColliderSleepCreate = make([][]ecs.Entity, s.numWorkers) + s.accColliderSleepDelete = make([][]ecs.Entity, s.numWorkers) +} +func (s *ColliderSystem) Run(dt time.Duration) { + for i := range s.accAABB { + s.accAABB[i] = s.accAABB[i][:0] + } + for i := range s.accGenericColliders { + s.accGenericColliders[i] = s.accGenericColliders[i][:0] + } + for i := range s.accColliderSleepCreate { + s.accColliderSleepCreate[i] = s.accColliderSleepCreate[i][:0] + } + for i := range s.accColliderSleepDelete { + s.accColliderSleepDelete[i] = s.accColliderSleepDelete[i][:0] + } + s.BoxColliders.ProcessEntities(func(entity ecs.Entity, workerId worker.WorkerId) { + if !s.GenericColliders.Has(entity) { + s.accGenericColliders[workerId] = append(s.accGenericColliders[workerId], entity) + } + if !s.AABB.Has(entity) { + s.accAABB[workerId] = append(s.accAABB[workerId], entity) + } + }) + s.CircleColliders.ProcessEntities(func(entity ecs.Entity, workerId worker.WorkerId) { + if !s.GenericColliders.Has(entity) { + s.accGenericColliders[workerId] = append(s.accGenericColliders[workerId], entity) + } + if !s.AABB.Has(entity) { + s.accAABB[workerId] = append(s.accAABB[workerId], entity) + } + }) + for i := range s.accAABB { + a := s.accAABB[i] + for _, entity := range a { + s.AABB.Create(entity, stdcomponents.AABB{}) + } + } + for i := range s.accGenericColliders { + a := s.accGenericColliders[i] + for _, entity := range a { + s.GenericColliders.Create(entity, stdcomponents.GenericCollider{}) + } + } + + s.BoxColliders.ProcessEntities(func(entity ecs.Entity, workerId worker.WorkerId) { + boxCollider := s.BoxColliders.GetUnsafe(entity) + + genCollider := s.GenericColliders.GetUnsafe(entity) + + genCollider.Layer = boxCollider.Layer + genCollider.Mask = boxCollider.Mask + genCollider.Offset.X = boxCollider.Offset.X + genCollider.Offset.Y = boxCollider.Offset.Y + genCollider.Shape = stdcomponents.BoxColliderShape + genCollider.AllowSleep = boxCollider.AllowSleep + }) + + s.BoxColliders.ProcessEntities(func(entity ecs.Entity, workerId worker.WorkerId) { + boxCollider := s.BoxColliders.GetUnsafe(entity) + assert.NotNil(boxCollider) + + position := s.Positions.GetUnsafe(entity) + assert.NotNil(position) + + scale := s.Scales.GetUnsafe(entity) + assert.NotNil(scale) + + rotation := s.Rotations.GetUnsafe(entity) + assert.NotNil(rotation) + + aabb := s.AABB.GetUnsafe(entity) + assert.NotNil(aabb) + + a := boxCollider.WH + b := vectors.Vec2{X: 0, Y: boxCollider.WH.Y} + c := vectors.Vec2{X: 0, Y: 0} + d := vectors.Vec2{X: boxCollider.WH.X, Y: 0} + + c = c.Sub(boxCollider.Offset).Rotate(rotation.Angle) + a = a.Sub(boxCollider.Offset).Rotate(rotation.Angle) + b = b.Sub(boxCollider.Offset).Rotate(rotation.Angle) + d = d.Sub(boxCollider.Offset).Rotate(rotation.Angle) + + aabb.Min = vectors.Vec2{X: min(b.X, c.X, a.X, d.X), Y: min(b.Y, c.Y, a.Y, d.Y)}.Mul(scale.XY) + aabb.Max = vectors.Vec2{X: max(b.X, c.X, a.X, d.X), Y: max(b.Y, c.Y, a.Y, d.Y)}.Mul(scale.XY) + + aabb.Min = position.XY.Add(aabb.Min) + aabb.Max = position.XY.Add(aabb.Max) + }) + + s.CircleColliders.ProcessEntities(func(entity ecs.Entity, workerId worker.WorkerId) { + circleCollider := s.CircleColliders.GetUnsafe(entity) + assert.NotNil(circleCollider) + + genCollider := s.GenericColliders.GetUnsafe(entity) + assert.NotNil(genCollider) + + genCollider.Layer = circleCollider.Layer + genCollider.Mask = circleCollider.Mask + genCollider.Offset.X = circleCollider.Offset.X + genCollider.Offset.Y = circleCollider.Offset.Y + genCollider.Shape = stdcomponents.CircleColliderShape + genCollider.AllowSleep = circleCollider.AllowSleep + }) + + s.CircleColliders.ProcessEntities(func(entity ecs.Entity, workerId worker.WorkerId) { + circleCollider := s.CircleColliders.GetUnsafe(entity) + assert.NotNil(circleCollider) + + position := s.Positions.GetUnsafe(entity) + assert.NotNil(position) + + scale := s.Scales.GetUnsafe(entity) + assert.NotNil(scale) + + aabb := s.AABB.GetUnsafe(entity) + assert.NotNil(aabb) + + offset := circleCollider.Offset.Mul(scale.XY) + scaledRadius := scale.XY.Scale(circleCollider.Radius) + aabb.Min = position.XY.Add(offset).Sub(scaledRadius) + aabb.Max = position.XY.Add(offset).Add(scaledRadius) + }) + + s.GenericColliders.ProcessEntities(func(entity ecs.Entity, workerId worker.WorkerId) { + genCollider := s.GenericColliders.GetUnsafe(entity) + if genCollider.AllowSleep { + shouldSleep := true + velocity := s.Velocities.GetUnsafe(entity) + if velocity != nil { + if velocity.Vec2().LengthSquared() != 0 { + shouldSleep = false + } + } + isSleeping := s.ColliderSleepStateComponentManager.GetUnsafe(entity) + if shouldSleep { + if isSleeping == nil { + s.accColliderSleepCreate[workerId] = append(s.accColliderSleepCreate[workerId], entity) + } + } else { + if isSleeping != nil { + s.accColliderSleepDelete[workerId] = append(s.accColliderSleepDelete[workerId], entity) + } + } + } + }) + for i := range s.accColliderSleepCreate { + a := s.accColliderSleepCreate[i] + for _, entity := range a { + s.ColliderSleepStateComponentManager.Create(entity, stdcomponents.ColliderSleepState{}) + } + } + for i := range s.accColliderSleepDelete { + a := s.accColliderSleepDelete[i] + for _, entity := range a { + s.ColliderSleepStateComponentManager.Delete(entity) + } + } + +} +func (s *ColliderSystem) Destroy() {} diff --git a/stdsystems/collision-detection-bvh.go b/stdsystems/collision-detection-bvh.go new file mode 100644 index 00000000..dad9f7d2 --- /dev/null +++ b/stdsystems/collision-detection-bvh.go @@ -0,0 +1,283 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package stdsystems + +import ( + "gomp/pkg/bvh" + gjk "gomp/pkg/collision" + "gomp/pkg/core" + "gomp/pkg/ecs" + "gomp/pkg/worker" + "gomp/stdcomponents" + "gomp/vectors" + "sync" + "time" +) + +const debugTree = false + +func NewCollisionDetectionBVHSystem() CollisionDetectionBVHSystem { + return CollisionDetectionBVHSystem{ + activeCollisions: make(map[CollisionPair]ecs.Entity), + trees: make([]bvh.Tree, 0, 8), + treesLookup: make(map[stdcomponents.CollisionLayer]int, 8), + } +} + +type CollisionDetectionBVHSystem struct { + EntityManager *ecs.EntityManager + Positions *stdcomponents.PositionComponentManager + Rotations *stdcomponents.RotationComponentManager + Scales *stdcomponents.ScaleComponentManager + GenericCollider *stdcomponents.GenericColliderComponentManager + BoxColliders *stdcomponents.BoxColliderComponentManager + CircleColliders *stdcomponents.CircleColliderComponentManager + PolygonColliders *stdcomponents.PolygonColliderComponentManager + Collisions *stdcomponents.CollisionComponentManager + SpatialIndex *stdcomponents.SpatialHashComponentManager + AABB *stdcomponents.AABBComponentManager + ColliderSleepStateComponentManager *stdcomponents.ColliderSleepStateComponentManager + BvhTreeComponentManager *stdcomponents.BvhTreeComponentManager + + trees []bvh.Tree + treesLookup map[stdcomponents.CollisionLayer]int + + collisionEvents []ecs.PagedArray[CollisionEvent] + + activeCollisions map[CollisionPair]ecs.Entity // Maps collision pairs to proxy entities + currentCollisions map[CollisionPair]struct{} + numWorkers int + Engine *core.Engine +} + +func (s *CollisionDetectionBVHSystem) Init() { + s.numWorkers = s.Engine.Pool().NumWorkers() + s.collisionEvents = make([]ecs.PagedArray[CollisionEvent], s.numWorkers) + for i := range s.numWorkers { + s.collisionEvents[i] = ecs.NewPagedArray[CollisionEvent]() + } +} + +func (s *CollisionDetectionBVHSystem) Run(dt time.Duration) { + s.currentCollisions = make(map[CollisionPair]struct{}) + defer s.processExitStates() + + if s.GenericCollider.Len() == 0 { + return + } + + // Fill trees + s.GenericCollider.EachEntity(func(entity ecs.Entity) bool { + aabb := s.AABB.GetUnsafe(entity) + layer := s.GenericCollider.GetUnsafe(entity).Layer + + treeId, exists := s.treesLookup[layer] + if !exists { + treeId = len(s.trees) + s.trees = append(s.trees, bvh.NewTree(layer)) + s.treesLookup[layer] = treeId + } + + s.trees[treeId].AddComponent(entity, *aabb) + + return true + }) + + // Build trees + wg := new(sync.WaitGroup) + wg.Add(len(s.trees)) + for i := range s.trees { + go func(i int, w *sync.WaitGroup) { + defer w.Done() + s.trees[i].Build() + }(i, wg) + } + wg.Wait() + + s.findEntityCollisions() + s.registerCollisionEvents() +} + +func (s *CollisionDetectionBVHSystem) Destroy() {} + +func (s *CollisionDetectionBVHSystem) findEntityCollisions() { + s.GenericCollider.ProcessEntities(func(entity ecs.Entity, workerId worker.WorkerId) { + potentialEntities := s.broadPhase(entity, make([]ecs.Entity, 0, 64)) + if len(potentialEntities) == 0 { + return + } + + s.narrowPhase(entity, potentialEntities, workerId) + }) +} + +func (s *CollisionDetectionBVHSystem) registerCollisionEvents() { + for i := range s.collisionEvents { + events := &s.collisionEvents[i] + events.EachData(func(event *CollisionEvent) bool { + pair := CollisionPair{event.entityA, event.entityB} + s.currentCollisions[pair] = struct{}{} + displacement := event.normal.Scale(event.depth) + pos := event.position.Add(displacement) + + if _, exists := s.activeCollisions[pair]; !exists { + proxy := s.EntityManager.Create() + s.Collisions.Create(proxy, stdcomponents.Collision{ + E1: pair.E1, + E2: pair.E2, + State: stdcomponents.CollisionStateEnter, + Normal: event.normal, + Depth: event.depth, + }) + + s.Positions.Create(proxy, stdcomponents.Position{ + XY: vectors.Vec2{ + X: pos.X, Y: pos.Y, + }}) + s.activeCollisions[pair] = proxy + } else { + proxy := s.activeCollisions[pair] + collision := s.Collisions.GetUnsafe(proxy) + position := s.Positions.GetUnsafe(proxy) + collision.State = stdcomponents.CollisionStateStay + collision.Depth = event.depth + collision.Normal = event.normal + position.XY.X = pos.X + position.XY.Y = pos.Y + } + return true + }) + events.Reset() + } +} + +func (s *CollisionDetectionBVHSystem) broadPhase(entityA ecs.Entity, result []ecs.Entity) []ecs.Entity { + colliderA := s.GenericCollider.GetUnsafe(entityA) + if colliderA.AllowSleep { + isSleeping := s.ColliderSleepStateComponentManager.GetUnsafe(entityA) + if isSleeping != nil { + return result + } + } + + aabb := s.AABB.GetUnsafe(entityA) + + // Iterate through all trees + for treeIndex := range s.trees { + tree := &s.trees[treeIndex] + layer := tree.Layer() + + // Check if mask includes this layer + if !colliderA.Mask.HasLayer(layer) { + continue + } + + // Traverse this BVH tree for potential collisions + result = tree.Query(aabb, result) + } + return result +} + +func (s *CollisionDetectionBVHSystem) narrowPhase(entityA ecs.Entity, potentialEntities []ecs.Entity, workerId worker.WorkerId) { + for _, entityB := range potentialEntities { + if entityA == entityB { + continue + } + + colliderA := s.GenericCollider.GetUnsafe(entityA) + colliderB := s.GenericCollider.GetUnsafe(entityB) + posA := s.Positions.GetUnsafe(entityA) + posB := s.Positions.GetUnsafe(entityB) + scaleA := s.Scales.GetUnsafe(entityA) + scaleB := s.Scales.GetUnsafe(entityB) + rotA := s.Rotations.GetUnsafe(entityA) + rotB := s.Rotations.GetUnsafe(entityB) + transformA := stdcomponents.Transform2d{ + Position: posA.XY, + Rotation: rotA.Angle, + Scale: scaleA.XY, + } + transformB := stdcomponents.Transform2d{ + Position: posB.XY, + Rotation: rotB.Angle, + Scale: scaleB.XY, + } + + circleA := s.CircleColliders.GetUnsafe(entityA) + circleB := s.CircleColliders.GetUnsafe(entityB) + if circleA != nil && circleB != nil { + radiusA := circleA.Radius * scaleA.XY.X + radiusB := circleB.Radius * scaleB.XY.X + if transformA.Position.Distance(transformB.Position) < radiusA+radiusB { + s.collisionEvents[workerId].Append(CollisionEvent{ + entityA: entityA, + entityB: entityB, + position: transformA.Position, + normal: transformB.Position.Sub(transformA.Position).Normalize(), + depth: radiusA + radiusB - transformB.Position.Distance(transformA.Position), + }) + } + continue + } + + // GJK strategy + colA := s.getGjkCollider(colliderA, entityA) + colB := s.getGjkCollider(colliderB, entityB) + // First detect collision using GJK + test := gjk.New() + if !test.CheckCollision(colA, colB, transformA, transformB) { + continue + } + + // If collision detected, get penetration details using EPA + normal, depth := test.EPA(colA, colB, transformA, transformB) + position := posA.XY.Add(posB.XY.Sub(posA.XY)) + s.collisionEvents[workerId].Append(CollisionEvent{ + entityA: entityA, + entityB: entityB, + position: position, + normal: normal, + depth: depth, + }) + } +} + +func (s *CollisionDetectionBVHSystem) processExitStates() { + for pair, proxy := range s.activeCollisions { + if _, exists := s.currentCollisions[pair]; !exists { + collision := s.Collisions.GetUnsafe(proxy) + if collision.State == stdcomponents.CollisionStateExit { + delete(s.activeCollisions, pair) + s.EntityManager.Delete(proxy) + } else { + collision.State = stdcomponents.CollisionStateExit + } + } + } +} + +func (s *CollisionDetectionBVHSystem) getGjkCollider(collider *stdcomponents.GenericCollider, entity ecs.Entity) gjk.AnyCollider { + switch collider.Shape { + case stdcomponents.BoxColliderShape: + return s.BoxColliders.GetUnsafe(entity) + case stdcomponents.CircleColliderShape: + return s.CircleColliders.GetUnsafe(entity) + case stdcomponents.PolygonColliderShape: + return s.PolygonColliders.GetUnsafe(entity) + default: + panic("unsupported collider shape") + } + return nil +} diff --git a/stdsystems/collision-detection-grid.go b/stdsystems/collision-detection-grid.go new file mode 100644 index 00000000..58d26d36 --- /dev/null +++ b/stdsystems/collision-detection-grid.go @@ -0,0 +1,304 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +<- SoundOfTheWind Donated 100 RUB +<- Anonymous Donated 500 RUB + +Thank you for your support! +*/ + +package stdsystems + +import ( + "gomp/pkg/ecs" + "gomp/stdcomponents" + "gomp/vectors" + "time" +) + +func NewCollisionDetectionGridSystem() CollisionDetectionGridSystem { + return CollisionDetectionGridSystem{ + cellSizeX: 192, + cellSizeY: 192, + spatialBuckets: make(map[stdcomponents.SpatialHash][]ecs.Entity, 32), + } +} + +type CollisionDetectionGridSystem struct { + EntityManager *ecs.EntityManager + Positions *stdcomponents.PositionComponentManager + Scales *stdcomponents.ScaleComponentManager + GenericCollider *stdcomponents.GenericColliderComponentManager + BoxColliders *stdcomponents.BoxColliderComponentManager + Collisions *stdcomponents.CollisionComponentManager + SpatialIndex *stdcomponents.SpatialHashComponentManager + + cellSizeX int + cellSizeY int + + // Cache + activeCollisions map[CollisionPair]ecs.Entity // Maps collision pairs to proxy entities + currentCollisions map[CollisionPair]struct{} + spatialBuckets map[stdcomponents.SpatialHash][]ecs.Entity + entityToCell map[ecs.Entity]stdcomponents.SpatialHash + aabbs map[ecs.Entity]aabb +} + +type aabb struct { + Left, Right, Top, Bottom float32 +} + +type CollisionPair struct { + E1, E2 ecs.Entity +} + +func (c CollisionPair) Normalize() CollisionPair { + if c.E1 > c.E2 { + return CollisionPair{c.E2, c.E1} + } + return c +} + +func (s *CollisionDetectionGridSystem) Init() { + s.activeCollisions = make(map[CollisionPair]ecs.Entity) +} + +type CollisionEvent struct { + entityA, entityB ecs.Entity + position vectors.Vec2 + normal vectors.Vec2 + depth float32 +} + +func (s *CollisionDetectionGridSystem) Run(dt time.Duration) { + //if len(s.entityToCell) < s.GenericCollider.Len() { + // s.entityToCell = make(map[ecs.Entity]stdcomponents.SpatialHash, s.GenericCollider.Len()) + //} + //// Reuse spatialBuckets to reduce allocations + //for k := range s.spatialBuckets { + // delete(s.spatialBuckets, k) + //} + //s.currentCollisions = make(map[CollisionPair]struct{}) + // + //// Build spatial buckets and entity-to-cell map + //s.GenericCollider.EachEntity()(func(entity ecs.Entity) bool { + // position := s.Positions.GetUnsafe(entity) + // scale := s.Scales.GetUnsafe(entity) + // + // collider := s.GenericCollider.GetUnsafe(entity) + // cellX := int(position.XY.X-(collider.Offset.X*scale.XY.X)) / s.cellSizeX + // cellY := int(position.XY.Y-(collider.Offset.Y*scale.XY.Y)) / s.cellSizeY + // cell := stdcomponents.SpatialHash{X: cellX, Y: cellY} + // s.entityToCell[entity] = cell + // s.spatialBuckets[cell] = append(s.spatialBuckets[cell], entity) + // return true + //}) + // + //// Precompute AABBs for box colliders + //s.aabbs = make(map[ecs.Entity]aabb, s.BoxColliders.Len()) + //s.BoxColliders.EachEntity()(func(entity ecs.Entity) bool { + // position := s.Positions.GetUnsafe(entity) + // collider := s.BoxColliders.GetUnsafe(entity) + // scale := s.Scales.GetUnsafe(entity) + // newAABB := aabb{ + // Left: position.XY.X - (collider.Offset.X * scale.XY.X), + // Right: position.XY.X + (collider.WH.X-collider.Offset.X)*scale.XY.X, + // Top: position.XY.Y - (collider.Offset.Y * scale.XY.Y), + // Bottom: position.XY.Y + (collider.WH.Y-collider.Offset.Y)*scale.XY.Y, + // } + // s.aabbs[entity] = newAABB + // return true + //}) + // + //// Create collision channel + //collisionChan := make(chan CollisionEvent, 4096) + //doneChan := make(chan struct{}) + // + //// Start result collector + //go func() { + // for event := range collisionChan { + // pair := CollisionPair{event.entityA, event.entityB}.Normalize() + // s.currentCollisions[pair] = struct{}{} + // + // if _, exists := s.activeCollisions[pair]; !exists { + // proxy := s.EntityManager.Create() + // s.Collisions.Create(proxy, stdcomponents.Collision{E1: pair.E1, E2: pair.E2, State: stdcomponents.CollisionStateEnter}) + // s.Positions.Create(proxy, stdcomponents.Position{ + // XY: vectors.Vec2{ + // X: event.position.X, + // Y: event.position.Y, + // }, + // }) + // s.activeCollisions[pair] = proxy + // } else { + // proxy := s.activeCollisions[pair] + // s.Collisions.GetUnsafe(proxy).State = stdcomponents.CollisionStateStay + // s.Positions.GetUnsafe(proxy).XY.X = event.position.X + // s.Positions.GetUnsafe(proxy).XY.Y = event.position.Y + // } + // } + // close(doneChan) + //}() + // + //entities := s.GenericCollider.RawEntities(make([]ecs.Entity, 0, s.GenericCollider.Len())) + // + //// Worker pool setup + //var wg sync.WaitGroup + //maxNumWorkers := runtime.NumCPU() - 2 + //entitiesLength := len(entities) + //// get minimum 1 worker for small amount of entities, and maximum maxNumWorkers for a lot entities + //numWorkers := max(min(entitiesLength/32, maxNumWorkers), 1) + //chunkSize := entitiesLength / numWorkers + // + //wg.Add(numWorkers) + // + //for i := 0; i < numWorkers; i++ { + // startIndex := i * chunkSize + // endIndex := startIndex + chunkSize - 1 + // if i == numWorkers-1 { // have to set endIndex to entites lenght, if last worker + // endIndex = entitiesLength + // } + // + // go func(start int, end int) { + // defer wg.Done() + // + // for _, entityA := range entities[start:end] { + // collider := s.GenericCollider.GetUnsafe(entityA) + // + // switch collider.Shape { + // case stdcomponents.BoxColliderShape: + // s.boxToXCollision(entityA, collisionChan) + // case stdcomponents.CircleColliderShape: + // s.circleToXCollision(entityA) + // default: + // panic("Unknown collider shape") + // } + // } + // }(startIndex, endIndex) + //} + // + //// Wait for workers and close collision channel + //wg.Wait() + //close(collisionChan) + //<-doneChan // Wait for result collector + // + ////s.GenericCollider.EachEntity(func(entity ecs.Entity) bool { + //// collider := s.GenericCollider.GetUnsafe(entity) + //// + //// switch collider.Shape { + //// case stdcomponents.BoxColliderShape: + //// s.boxToXCollision(entity) + //// case stdcomponents.CircleColliderShape: + //// s.circleToXCollision(entity) + //// default: + //// panic("Unknown collider shape") + //// } + //// return true + ////}) + // + //s.processExitStates() +} +func (s *CollisionDetectionGridSystem) Destroy() {} + +func (s *CollisionDetectionGridSystem) registerCollision(entityA, entityB ecs.Entity, posX, posY float32) { + pair := CollisionPair{entityA, entityB}.Normalize() + + s.currentCollisions[pair] = struct{}{} + + // Create proxy entity for new collisions + if _, exists := s.activeCollisions[pair]; !exists { + proxy := s.EntityManager.Create() + s.Collisions.Create(proxy, stdcomponents.Collision{E1: pair.E1, E2: pair.E2, State: stdcomponents.CollisionStateEnter}) + s.Positions.Create(proxy, stdcomponents.Position{XY: vectors.Vec2{X: posX, Y: posY}}) + s.activeCollisions[pair] = proxy + } else { + s.Collisions.GetUnsafe(s.activeCollisions[pair]).State = stdcomponents.CollisionStateStay + s.Positions.GetUnsafe(s.activeCollisions[pair]).XY.X = posX + s.Positions.GetUnsafe(s.activeCollisions[pair]).XY.Y = posY + } +} + +func (s *CollisionDetectionGridSystem) processExitStates() { + for pair, proxy := range s.activeCollisions { + if _, exists := s.currentCollisions[pair]; !exists { + collision := s.Collisions.GetUnsafe(proxy) + if collision.State == stdcomponents.CollisionStateExit { + delete(s.activeCollisions, pair) + s.EntityManager.Delete(proxy) + } else { + collision.State = stdcomponents.CollisionStateExit + } + } + } +} + +func (s *CollisionDetectionGridSystem) boxToXCollision(entityA ecs.Entity, collisionChan chan<- CollisionEvent) { + //position1 := s.Positions.GetUnsafe(entityA) + //spatialIndex1 := s.entityToCell[entityA] + //genericCollider1 := s.GenericCollider.GetUnsafe(entityA) + // + //var nearByIndexes = [9]stdcomponents.SpatialHash{ + // {spatialIndex1.X, spatialIndex1.Y}, + // {spatialIndex1.X - 1, spatialIndex1.Y}, + // {spatialIndex1.X + 1, spatialIndex1.Y}, + // {spatialIndex1.X, spatialIndex1.Y - 1}, + // {spatialIndex1.X, spatialIndex1.Y + 1}, + // {spatialIndex1.X - 1, spatialIndex1.Y - 1}, + // {spatialIndex1.X + 1, spatialIndex1.Y + 1}, + // {spatialIndex1.X - 1, spatialIndex1.Y + 1}, + // {spatialIndex1.X + 1, spatialIndex1.Y - 1}, + //} + // + //for _, spatialIndex := range nearByIndexes { + // bucket := s.spatialBuckets[spatialIndex] + // for _, entityB := range bucket { + // if entityA >= entityB { + // continue // Skip duplicate checks + // } + // + // // Broad Phase + // genericCollider2 := s.GenericCollider.GetUnsafe(entityB) + // if genericCollider1.Mask&(1< aabbB.Right { + // continue + // } + // // Then Y-axis + // if aabbA.Bottom < aabbB.Top || aabbA.Top > aabbB.Bottom { + // continue + // } + // + // posX := (position1.XY.X + position2.XY.X) / 2 + // posY := (position1.XY.Y + position2.XY.Y) / 2 + // collisionChan <- CollisionEvent{entityA, entityB, vectors.Vec2{posX, posY}, vectors.Vec2{posX, posY}, 0} + // case stdcomponents.CircleColliderShape: + // panic("Circle-Box collision not implemented") + // default: + // panic("Unknown collision shape") + // } + // } + //} +} + +func (s *CollisionDetectionGridSystem) circleToXCollision(entityA ecs.Entity) { + panic("Circle-X collision not implemented") +} diff --git a/stdsystems/collision-detection.go b/stdsystems/collision-detection.go new file mode 100644 index 00000000..1b8a7040 --- /dev/null +++ b/stdsystems/collision-detection.go @@ -0,0 +1,327 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +<- Фрея Donated 2 000 RUB + +Thank you for your support! +*/ + +package stdsystems + +import ( + "github.com/negrel/assert" + gjk "gomp/pkg/collision" + "gomp/pkg/core" + "gomp/pkg/ecs" + "gomp/pkg/worker" + "gomp/stdcomponents" + "gomp/vectors" + "math" + "math/bits" + "time" +) + +func NewCollisionDetectionSystem() CollisionDetectionSystem { + return CollisionDetectionSystem{} +} + +type CollisionDetectionSystem struct { + EntityManager *ecs.EntityManager + Positions *stdcomponents.PositionComponentManager + Rotations *stdcomponents.RotationComponentManager + Scales *stdcomponents.ScaleComponentManager + GenericCollider *stdcomponents.GenericColliderComponentManager + BoxColliders *stdcomponents.BoxColliderComponentManager + CircleColliders *stdcomponents.CircleColliderComponentManager + PolygonColliders *stdcomponents.PolygonColliderComponentManager + Collisions *stdcomponents.CollisionComponentManager + SpatialIndex *stdcomponents.SpatialHashComponentManager + AABB *stdcomponents.AABBComponentManager + ColliderSleepStateComponentManager *stdcomponents.ColliderSleepStateComponentManager + BvhTreeComponentManager *stdcomponents.BvhTreeComponentManager + CollisionGridComponentManager *stdcomponents.CollisionGridComponentManager + CollisionChunkComponentManager *stdcomponents.CollisionChunkComponentManager + CollisionCellComponentManager *stdcomponents.CollisionCellComponentManager + Engine *core.Engine + + collisionEventAcc []ecs.PagedArray[CollisionEvent] + activeCollisions map[CollisionPair]ecs.Entity // Maps collision pairs to proxy entities + currentCollisions map[CollisionPair]struct{} + gridLookup map[stdcomponents.CollisionLayer]*stdcomponents.CollisionGrid + potentialEntities []ecs.PagedArray[ecs.Entity] +} + +func (s *CollisionDetectionSystem) Init() { + s.gridLookup = make(map[stdcomponents.CollisionLayer]*stdcomponents.CollisionGrid) + s.collisionEventAcc = make([]ecs.PagedArray[CollisionEvent], s.Engine.Pool().NumWorkers()) + s.potentialEntities = make([]ecs.PagedArray[ecs.Entity], s.Engine.Pool().NumWorkers()) + for i := 0; i < s.Engine.Pool().NumWorkers(); i++ { + s.collisionEventAcc[i] = ecs.NewPagedArray[CollisionEvent]() + s.potentialEntities[i] = ecs.NewPagedArray[ecs.Entity]() + } + s.activeCollisions = make(map[CollisionPair]ecs.Entity) +} + +func (s *CollisionDetectionSystem) Run(dt time.Duration) { + s.CollisionGridComponentManager.EachComponent(func(grid *stdcomponents.CollisionGrid) bool { + s.gridLookup[grid.Layer] = grid + return true + }) + + s.currentCollisions = make(map[CollisionPair]struct{}) + + if s.GenericCollider.Len() == 0 { + return + } + + s.GenericCollider.ProcessEntities(func(entity ecs.Entity, workerId worker.WorkerId) { + potentialEntities := &s.potentialEntities[workerId] + s.broadPhase(entity, potentialEntities) + if potentialEntities.Len() == 0 { + return + } + s.narrowPhase(entity, potentialEntities, workerId) + potentialEntities.Reset() + }) + + s.registerCollisionEvents() + s.processExitStates() + + for i := 0; i < len(s.collisionEventAcc); i++ { + s.collisionEventAcc[i].Reset() + } +} + +func (s *CollisionDetectionSystem) Destroy() { + s.collisionEventAcc = nil + s.activeCollisions = nil + s.currentCollisions = nil + s.gridLookup = nil +} + +func (s *CollisionDetectionSystem) broadPhase(entityA ecs.Entity, potentialEntities *ecs.PagedArray[ecs.Entity]) { + colliderA := s.GenericCollider.GetUnsafe(entityA) + + // Early exit for sleeping colliders (moved aabb access after sleep check) + if colliderA.AllowSleep && s.ColliderSleepStateComponentManager.Has(entityA) { + return + } + + aabbPtr := s.AABB.GetUnsafe(entityA) + assert.NotNil(aabbPtr) + bb := *aabbPtr + + // Direct layer bitmask iteration + mask := colliderA.Mask + + // Iterate only set bits in mask + for mask != 0 { + // Get the least significant raised bit position + layer := stdcomponents.CollisionLayer(bits.TrailingZeros32(uint32(mask))) + mask ^= 1 << layer // Clear the processed bit + + // Direct grid access + grid := s.gridLookup[layer] + assert.NotNil(grid) + + // Query grid + minSpatialCellIndex := grid.GetCellIndex(bb.Min) + maxSpatialCellIndex := grid.GetCellIndex(bb.Max) + // make a list of all spatial indexes that intersect the aabb + // get cells that intersect the aabb by spatial indexes + for i := minSpatialCellIndex.X; i <= maxSpatialCellIndex.X; i++ { + for j := minSpatialCellIndex.Y; j <= maxSpatialCellIndex.Y; j++ { + cellEntity, exists := grid.CellMap.Get(stdcomponents.SpatialCellIndex{X: i, Y: j}) + if !exists { + continue + } + + cell := s.CollisionCellComponentManager.GetUnsafe(cellEntity) + assert.NotNil(cell) + + members := cell.Members.Members + + for _, entityB := range members { + if entityB != entityA { + potentialEntities.Append(entityB) + } + } + } + } + } +} + +func (s *CollisionDetectionSystem) narrowPhase(entityA ecs.Entity, potentialEntities *ecs.PagedArray[ecs.Entity], workerId worker.WorkerId) { + posA := s.Positions.GetUnsafe(entityA) + assert.NotNil(posA) + + colliderA := s.GenericCollider.GetUnsafe(entityA) + assert.NotNil(colliderA) + + scaleA := s.Scales.GetUnsafe(entityA) + assert.NotNil(scaleA) + + rotA := s.Rotations.GetUnsafe(entityA) + assert.NotNil(rotA) + + aabbA := s.AABB.GetUnsafe(entityA) + assert.NotNil(aabbA) + + colA := s.getGjkCollider(colliderA, entityA) + + circleA := s.CircleColliders.GetUnsafe(entityA) + // Cache circleA properties if exists + var radiusA float32 + if circleA != nil { + radiusA = circleA.Radius * scaleA.XY.X + } + + transformA := stdcomponents.Transform2d{ + Position: posA.XY, + Rotation: rotA.Angle, + Scale: scaleA.XY, + } + + for entityB := range potentialEntities.EachDataValue { + aabbB := s.AABB.GetUnsafe(entityB) + assert.NotNil(aabbB) + + if !(aabbA.Max.X >= aabbB.Min.X && aabbA.Min.X <= aabbB.Max.X && + aabbA.Max.Y >= aabbB.Min.Y && aabbA.Min.Y <= aabbB.Max.Y) { + continue + } + + positionB := s.Positions.GetUnsafe(entityB) + assert.NotNil(positionB) + colliderB := s.GenericCollider.GetUnsafe(entityB) + assert.NotNil(colliderB) + scaleB := s.Scales.GetUnsafe(entityB) + assert.NotNil(scaleB) + rotationB := s.Rotations.GetUnsafe(entityB) + assert.NotNil(rotationB) + circleB := s.CircleColliders.GetUnsafe(entityB) + + // 1. FAST PATH: Circle-circle collision + if circleA != nil && circleB != nil { + posB := positionB.XY + radiusB := circleB.Radius * scaleB.XY.X + + // Vector math with early exit + dx := posB.X - transformA.Position.X + dy := posB.Y - transformA.Position.Y + sqDist := dx*dx + dy*dy + sumRadii := radiusA + radiusB + + if sqDist < sumRadii*sumRadii { + dist := float32(math.Sqrt(float64(sqDist))) + assert.NotZero(dist) + event := CollisionEvent{ + entityA: entityA, + entityB: entityB, + position: posA.XY, + normal: vectors.Vec2{ + X: dx / dist, + Y: dy / dist, + }, + depth: sumRadii - dist, + } + s.collisionEventAcc[workerId].Append(event) + } + continue + } + + // 2. GJK/EPA PATH + test := gjk.New() + transformB := stdcomponents.Transform2d{ + Position: positionB.XY, + Rotation: rotationB.Angle, + Scale: scaleB.XY, + } + colB := s.getGjkCollider(colliderB, entityB) + // Detect collision using GJK + if test.CheckCollision(colA, colB, transformA, transformB) { + // If collision detected, get penetration details using EPA + normal, depth := test.EPA(colA, colB, transformA, transformB) + position := posA.XY.Add(positionB.XY.Sub(posA.XY)) + s.collisionEventAcc[workerId].Append(CollisionEvent{ + entityA: entityA, + entityB: entityB, + position: position, + normal: normal, + depth: depth, + }) + } + } +} +func (s *CollisionDetectionSystem) registerCollisionEvents() { + for i := range s.collisionEventAcc { + events := &s.collisionEventAcc[i] + events.EachData(func(event *CollisionEvent) bool { + pair := CollisionPair{event.entityA, event.entityB} + s.currentCollisions[pair] = struct{}{} + displacement := event.normal.Scale(event.depth) + pos := event.position.Add(displacement) + + if _, exists := s.activeCollisions[pair]; !exists { + proxy := s.EntityManager.Create() + s.Collisions.Create(proxy, stdcomponents.Collision{ + E1: pair.E1, + E2: pair.E2, + State: stdcomponents.CollisionStateEnter, + Normal: event.normal, + Depth: event.depth, + }) + + s.Positions.Create(proxy, stdcomponents.Position{ + XY: vectors.Vec2{ + X: pos.X, Y: pos.Y, + }}) + s.activeCollisions[pair] = proxy + } else { + proxy := s.activeCollisions[pair] + collision := s.Collisions.GetUnsafe(proxy) + position := s.Positions.GetUnsafe(proxy) + collision.State = stdcomponents.CollisionStateStay + collision.Depth = event.depth + collision.Normal = event.normal + position.XY.X = pos.X + position.XY.Y = pos.Y + } + return true + }) + events.Reset() + } +} + +func (s *CollisionDetectionSystem) processExitStates() { + for pair, proxy := range s.activeCollisions { + if _, exists := s.currentCollisions[pair]; !exists { + collision := s.Collisions.GetUnsafe(proxy) + if collision.State == stdcomponents.CollisionStateExit { + delete(s.activeCollisions, pair) + s.EntityManager.Delete(proxy) + } else { + collision.State = stdcomponents.CollisionStateExit + } + } + } +} +func (s *CollisionDetectionSystem) getGjkCollider(collider *stdcomponents.GenericCollider, entity ecs.Entity) gjk.AnyCollider { + switch collider.Shape { + case stdcomponents.BoxColliderShape: + return s.BoxColliders.GetUnsafe(entity) + case stdcomponents.CircleColliderShape: + return s.CircleColliders.GetUnsafe(entity) + case stdcomponents.PolygonColliderShape: + return s.PolygonColliders.GetUnsafe(entity) + default: + panic("unsupported collider shape") + } + return nil +} diff --git a/stdsystems/collision-reslution.go b/stdsystems/collision-reslution.go new file mode 100644 index 00000000..a5f9b3c2 --- /dev/null +++ b/stdsystems/collision-reslution.go @@ -0,0 +1,102 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package stdsystems + +import ( + "github.com/negrel/assert" + "gomp/pkg/ecs" + "gomp/pkg/worker" + "gomp/stdcomponents" + "gomp/vectors" + "time" +) + +func NewCollisionResolutionSystem() CollisionResolutionSystem { + return CollisionResolutionSystem{} +} + +type CollisionResolutionSystem struct { + EntityManager *ecs.EntityManager + Collisions *stdcomponents.CollisionComponentManager + Positions *stdcomponents.PositionComponentManager + RigidBodies *stdcomponents.RigidBodyComponentManager + Velocities *stdcomponents.VelocityComponentManager +} + +func (s *CollisionResolutionSystem) Init() {} +func (s *CollisionResolutionSystem) Run(dt time.Duration) { + s.Collisions.ProcessComponents(func(collision *stdcomponents.Collision, _ worker.WorkerId) { + if collision.State == stdcomponents.CollisionStateEnter || collision.State == stdcomponents.CollisionStateStay { + // Resolve penetration + var displacement vectors.Vec2 + + // Move entities apart + rigidbody1 := s.RigidBodies.GetUnsafe(collision.E1) + rigidbody2 := s.RigidBodies.GetUnsafe(collision.E2) + + if rigidbody1 == nil || rigidbody2 == nil { + return + } + + if !rigidbody1.IsStatic && !rigidbody2.IsStatic { + // both objects are dynamic + displacement = collision.Normal.Scale(collision.Depth * 0.5) + } else { + // one of the objects is static + displacement = collision.Normal.Scale(collision.Depth) + } + + if !rigidbody1.IsStatic { + p1 := s.Positions.GetUnsafe(collision.E1) + p1d := p1.XY.Sub(displacement) + p1.XY.X, p1.XY.Y = p1d.X, p1d.Y + } + + if !rigidbody2.IsStatic { + p2 := s.Positions.GetUnsafe(collision.E2) + p2d := p2.XY.Add(displacement) + p2.XY.X, p2.XY.Y = p2d.X, p2d.Y + } + + // Apply impulse resolution + velocity1 := s.Velocities.GetUnsafe(collision.E1) + assert.NotNil(velocity1) + velocity2 := s.Velocities.GetUnsafe(collision.E2) + assert.NotNil(velocity2) + + relativeVelocity := velocity2.Vec2().Sub(velocity1.Vec2()) + velocityAlongNormal := relativeVelocity.Dot(collision.Normal) + + if velocityAlongNormal > 0 { + return + } + + e := float32(1.0) // Coefficient of restitution (elasticity) + j := -(1 + e) * velocityAlongNormal + j /= 1/rigidbody1.Mass + 1/rigidbody2.Mass + + impulse := collision.Normal.Scale(j) + + if !rigidbody1.IsStatic { + velocity1.SetVec2(velocity1.Vec2().Sub(impulse.Scale(1 / rigidbody1.Mass))) + } + + if !rigidbody2.IsStatic { + velocity2.SetVec2(velocity2.Vec2().Add(impulse.Scale(1 / rigidbody2.Mass))) + } + } + }) +} +func (s *CollisionResolutionSystem) Destroy() {} diff --git a/stdsystems/collision-setup.go b/stdsystems/collision-setup.go new file mode 100644 index 00000000..ed3b9dd9 --- /dev/null +++ b/stdsystems/collision-setup.go @@ -0,0 +1,332 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +<- SeniorOverflow Donated 500 RUB +<- SeniorOverflow Donated 1 000 RUB +<- Монтажёр сука Donated 100 RUB +<- Монтажёр сука Donated 100 RUB + +Thank you for your support! +*/ + +package stdsystems + +import ( + "gomp/pkg/core" + "gomp/pkg/ecs" + "gomp/pkg/worker" + "gomp/stdcomponents" + "image/color" + "math/rand" + "time" + + "github.com/negrel/assert" +) + +const ( + collidersPerCell = 8 +) + +func NewCollisionSetupSystem() CollisionSetupSystem { + return CollisionSetupSystem{} +} + +type CollisionSetupSystem struct { + EntityManager *ecs.EntityManager + Positions *stdcomponents.PositionComponentManager + Rotations *stdcomponents.RotationComponentManager + Scales *stdcomponents.ScaleComponentManager + Tints *stdcomponents.TintComponentManager + GenericCollider *stdcomponents.GenericColliderComponentManager + BoxColliders *stdcomponents.BoxColliderComponentManager + CircleColliders *stdcomponents.CircleColliderComponentManager + PolygonColliders *stdcomponents.PolygonColliderComponentManager + Collisions *stdcomponents.CollisionComponentManager + SpatialHash *stdcomponents.SpatialHashComponentManager + AABB *stdcomponents.AABBComponentManager + ColliderSleepStateComponentManager *stdcomponents.ColliderSleepStateComponentManager + BvhTreeComponentManager *stdcomponents.BvhTreeComponentManager + CollisionGridComponentManager *stdcomponents.CollisionGridComponentManager + CollisionChunkComponentManager *stdcomponents.CollisionChunkComponentManager + CollisionCellComponentManager *stdcomponents.CollisionCellComponentManager + CollisionGridMemberComponentManager *stdcomponents.CollisionGridMemberComponentManager + + gridLookup map[stdcomponents.CollisionLayer]ecs.Entity + Engine *core.Engine + + clearCellAccumulator []ecs.PagedArray[ecs.Entity] + + memberListPool stdcomponents.MemberListPool +} + +func (s *CollisionSetupSystem) Init() { + s.memberListPool = stdcomponents.NewMemberListPool(s.Engine.Pool()) + s.clearCellAccumulator = make([]ecs.PagedArray[ecs.Entity], s.Engine.Pool().NumWorkers()) + for i := range s.Engine.Pool().NumWorkers() { + s.clearCellAccumulator[i] = ecs.NewPagedArray[ecs.Entity]() + } + s.gridLookup = make(map[stdcomponents.CollisionLayer]ecs.Entity, 64) +} + +func (s *CollisionSetupSystem) Run(dt time.Duration) { + s.setup() +} + +func (s *CollisionSetupSystem) Destroy() { + s.gridLookup = nil +} +func (s *CollisionSetupSystem) setup() { + // Prepare grids + s.CollisionGridComponentManager.EachEntity(func(entity ecs.Entity) bool { + grid := s.CollisionGridComponentManager.GetUnsafe(entity) + assert.NotNil(grid) + + s.gridLookup[grid.Layer] = entity + + if grid.CreateCellsAccumulator == nil { + grid.CreateCellsAccumulator = make([]ecs.GenMap[stdcomponents.SpatialCellIndex, struct{}], s.Engine.Pool().NumWorkers()) + for i := range grid.CreateCellsAccumulator { + grid.CreateCellsAccumulator[i] = ecs.NewGenMap[stdcomponents.SpatialCellIndex, struct{}](1024) + } + } + + return true + }) + + // Create grid member components + var gridMemberAccumulator = make([][]ecs.Entity, s.Engine.Pool().NumWorkers()) + s.GenericCollider.ProcessEntities(func(entity ecs.Entity, id worker.WorkerId) { + if s.CollisionGridMemberComponentManager.Has(entity) { + return + } + gridMemberAccumulator[id] = append(gridMemberAccumulator[id], entity) + }) + for i := range gridMemberAccumulator { + acc := gridMemberAccumulator[i] + for j := range acc { + entity := acc[j] + s.CollisionGridMemberComponentManager.Create(entity, stdcomponents.CollisionGridMember{}) + } + } + + // Update grid member component + s.GenericCollider.ProcessEntities(func(entity ecs.Entity, id worker.WorkerId) { + collider := s.GenericCollider.GetUnsafe(entity) + assert.NotNil(collider) + + gridMember := s.CollisionGridMemberComponentManager.GetUnsafe(entity) + assert.NotNil(gridMember) + + gridEntity, exists := s.gridLookup[collider.Layer] + assert.True(exists) + + gridMember.Grid = gridEntity + }) + + // Create spatial hash components + var spatialHashAccumulator = make([][]ecs.Entity, s.Engine.Pool().NumWorkers()) + s.GenericCollider.ProcessEntities(func(entity ecs.Entity, id worker.WorkerId) { + if s.SpatialHash.Has(entity) { + return + } + spatialHashAccumulator[id] = append(spatialHashAccumulator[id], entity) + }) + for i := range spatialHashAccumulator { + acc := spatialHashAccumulator[i] + for j := range acc { + entity := acc[j] + s.SpatialHash.Create(entity, stdcomponents.SpatialHash{}) + } + spatialHashAccumulator[i] = spatialHashAccumulator[i][:0] + } + + // Calculate spatial hash for each collider + s.CollisionGridMemberComponentManager.ProcessEntities(func(entity ecs.Entity, id worker.WorkerId) { + gridMember := s.CollisionGridMemberComponentManager.GetUnsafe(entity) + assert.NotNil(gridMember) + + grid := s.CollisionGridComponentManager.GetUnsafe(gridMember.Grid) + assert.NotNil(grid) + + spatialHash := s.SpatialHash.GetUnsafe(entity) + assert.NotNil(spatialHash) + + bb := s.AABB.GetUnsafe(entity) + assert.NotNil(bb) + + newSpatialHash := grid.CalculateSpatialHash(*bb) + spatialHash.Min = newSpatialHash.Min + spatialHash.Max = newSpatialHash.Max + }) + + // Accumulate spatial hash that need to be created + s.SpatialHash.ProcessEntities(func(entity ecs.Entity, workerId worker.WorkerId) { + spatialHash := s.SpatialHash.GetUnsafe(entity) + assert.NotNil(spatialHash) + + gridMember := s.CollisionGridMemberComponentManager.GetUnsafe(entity) + assert.NotNil(gridMember) + + grid := s.CollisionGridComponentManager.GetUnsafe(gridMember.Grid) + assert.NotNil(grid) + + for i := spatialHash.Min.X; i <= spatialHash.Max.X; i++ { + for j := spatialHash.Min.Y; j <= spatialHash.Max.Y; j++ { + index := stdcomponents.SpatialCellIndex{X: i, Y: j} + if grid.CellMap.Has(index) { + continue + } + grid.CreateCellsAccumulator[workerId].Set(index, struct{}{}) + } + } + }) + + // Create requested cells + s.CollisionGridComponentManager.EachEntityParallel(func(gridEntity ecs.Entity, workerId worker.WorkerId) { + grid := s.CollisionGridComponentManager.GetUnsafe(gridEntity) + assert.NotNil(grid) + for i := range grid.CreateCellsAccumulator { + set := &grid.CreateCellsAccumulator[i] + set.Clear() + for cellIndex := range set.Each() { + if grid.CellMap.Has(cellIndex) { + continue + } + cellEntity := s.EntityManager.Create() + s.CollisionCellComponentManager.Create(cellEntity, stdcomponents.CollisionCell{ + Index: cellIndex, + Layer: grid.Layer, + Grid: gridEntity, + Size: grid.CellSize, + }) + s.Positions.Create(cellEntity, stdcomponents.Position{ + XY: cellIndex.ToVec2().Scale(grid.CellSize), + }) + const colorbase uint8 = 120 + s.Tints.Create(cellEntity, color.RGBA{ + R: colorbase + uint8(rand.Intn(int(255-colorbase))), + G: colorbase + uint8(rand.Intn(int(255-colorbase))), + B: colorbase + uint8(rand.Intn(int(255-colorbase))), + A: 70, + }) + grid.CellMap.Set(cellIndex, cellEntity) + } + grid.CreateCellsAccumulator[i].Reset() + } + }) + s.CollisionCellComponentManager.ProcessComponents(func(cell *stdcomponents.CollisionCell, id worker.WorkerId) { + if cell.Members != nil { + return + } + cell.Members = s.memberListPool.Get() + }) + + // Remove entities from cells + s.CollisionCellComponentManager.ProcessComponents(func(cell *stdcomponents.CollisionCell, id worker.WorkerId) { + memberList := cell.Members + assert.NotNil(memberList) + for i := len(memberList.Members) - 1; i >= 0; i-- { + member := memberList.Members[i] + spatialHash := s.SpatialHash.GetUnsafe(member) + assert.NotNil(spatialHash) + + // Check if entity is in cell + if cell.Index.X >= spatialHash.Min.X && cell.Index.X <= spatialHash.Max.X { + if cell.Index.Y >= spatialHash.Min.Y && cell.Index.Y <= spatialHash.Max.Y { + continue + } + } + + // Entity should be removed from cell + memberList.Members = append(memberList.Members[:i], memberList.Members[i+1:]...) + memberList.Lookup.Delete(member) + } + memberList.Lookup.Clear() + }) + + // Distribute entities in grid cells + s.SpatialHash.ProcessEntities(func(entity ecs.Entity, workerId worker.WorkerId) { + spatialHash := s.SpatialHash.GetUnsafe(entity) + assert.NotNil(spatialHash) + + gridMember := s.CollisionGridMemberComponentManager.GetUnsafe(entity) + assert.NotNil(gridMember) + + grid := s.CollisionGridComponentManager.GetUnsafe(gridMember.Grid) + assert.NotNil(grid) + + for i := spatialHash.Min.X; i <= spatialHash.Max.X; i++ { + for j := spatialHash.Min.Y; j <= spatialHash.Max.Y; j++ { + cellEntity, exists := grid.CellMap.Get(stdcomponents.SpatialCellIndex{X: i, Y: j}) + assert.True(exists) + + cell := s.CollisionCellComponentManager.GetUnsafe(cellEntity) + assert.NotNil(cell) + members := cell.Members + assert.NotNil(members) + + if members.Has(entity) { + continue + } + + members.InputAcc[workerId] = append(members.InputAcc[workerId], entity) + } + } + }) + + //Build grid cells + s.CollisionCellComponentManager.ProcessComponents(func(cell *stdcomponents.CollisionCell, workerId worker.WorkerId) { + members := cell.Members + for i := range members.InputAcc { + acc := members.InputAcc[i] + for j := range acc { + members.Members = append(members.Members, acc[j]) + members.Lookup.Set(acc[j], len(members.Members)-1) + } + members.InputAcc[i] = acc[:0] + } + }) + + //Remove empty cells + s.CollisionCellComponentManager.ProcessEntities(func(cellEntity ecs.Entity, workerId worker.WorkerId) { + cell := s.CollisionCellComponentManager.GetUnsafe(cellEntity) + assert.NotNil(cell) + + if len(cell.Members.Members) != 0 { + return + } + cell.Members.Reset() + s.memberListPool.Put(cell.Members) + cell.Members = nil + s.clearCellAccumulator[workerId].Append(cellEntity) + }) + for i := range s.clearCellAccumulator { + v := &s.clearCellAccumulator[i] + v.EachDataValue(func(cellEntity ecs.Entity) bool { + cell := s.CollisionCellComponentManager.GetUnsafe(cellEntity) + assert.NotNil(cell) + + grid := s.CollisionGridComponentManager.GetUnsafe(cell.Grid) + assert.NotNil(grid) + + grid.CellMap.Delete(cell.Index) + return true + }) + v.EachDataValue(func(cellEntity ecs.Entity) bool { + s.EntityManager.Delete(cellEntity) + return true + }) + v.Reset() + } + s.CollisionGridComponentManager.EachComponentParallel(func(grid *stdcomponents.CollisionGrid, workerId worker.WorkerId) { + grid.CellMap.Clear() + }) +} +func (s *CollisionSetupSystem) detection() { +} diff --git a/stdsystems/culling.go b/stdsystems/culling.go new file mode 100644 index 00000000..98993ffc --- /dev/null +++ b/stdsystems/culling.go @@ -0,0 +1,108 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +package stdsystems + +import ( + "github.com/negrel/assert" + "gomp/pkg/core" + "gomp/pkg/ecs" + "gomp/pkg/worker" + "gomp/stdcomponents" + "gomp/vectors" + "time" +) + +func NewCullingSystem() CullingSystem { + return CullingSystem{} +} + +type CullingSystem struct { + Renderables *stdcomponents.RenderableComponentManager + RenderVisible *stdcomponents.RenderVisibleComponentManager + RenderOrders *stdcomponents.RenderOrderComponentManager + Textures *stdcomponents.RLTextureProComponentManager + AABBs *stdcomponents.AABBComponentManager + Cameras *stdcomponents.CameraComponentManager + RenderTexture2D *stdcomponents.FrameBuffer2DComponentManager + numWorkers int + accRenderVisibleCreate [][]ecs.Entity + accRenderVisibleDelete [][]ecs.Entity + Engine *core.Engine +} + +func (s *CullingSystem) Init() { + s.numWorkers = s.Engine.Pool().NumWorkers() + s.accRenderVisibleCreate = make([][]ecs.Entity, s.numWorkers) + s.accRenderVisibleDelete = make([][]ecs.Entity, s.numWorkers) +} + +func (s *CullingSystem) Run(dt time.Duration) { + for i := range s.accRenderVisibleCreate { + s.accRenderVisibleCreate[i] = s.accRenderVisibleCreate[i][:0] + } + for i := range s.accRenderVisibleDelete { + s.accRenderVisibleDelete[i] = s.accRenderVisibleDelete[i][:0] + } + + s.Renderables.ProcessComponents(func(r *stdcomponents.Renderable, workerId worker.WorkerId) { + r.Observed = false + }) + + s.Cameras.EachEntity(func(entity ecs.Entity) bool { + camera := s.Cameras.GetUnsafe(entity) + cameraRect := camera.Rect() + + s.Renderables.ProcessEntities(func(entity ecs.Entity, workerId worker.WorkerId) { + renderable := s.Renderables.GetUnsafe(entity) + assert.NotNil(renderable) + + texture := s.Textures.GetUnsafe(entity) + assert.NotNil(texture) + + textureRect := texture.Rect() + + if s.intersects(cameraRect, textureRect) { + renderable.Observed = true + } + }) + return true + }) + + s.Renderables.ProcessEntities(func(entity ecs.Entity, workerId worker.WorkerId) { + renderable := s.Renderables.GetUnsafe(entity) + assert.NotNil(renderable) + if !s.RenderVisible.Has(entity) { + if renderable.Observed { + s.accRenderVisibleCreate[workerId] = append(s.accRenderVisibleCreate[workerId], entity) + } + } else { + if !renderable.Observed { + s.accRenderVisibleDelete[workerId] = append(s.accRenderVisibleDelete[workerId], entity) + } + } + }) + for a := range s.accRenderVisibleCreate { + for _, entity := range s.accRenderVisibleCreate[a] { + s.RenderVisible.Create(entity, stdcomponents.RenderVisible{}) + } + } + for a := range s.accRenderVisibleDelete { + for _, entity := range s.accRenderVisibleDelete[a] { + s.RenderVisible.Delete(entity) + } + } +} + +func (_ *CullingSystem) intersects(rect1, rect2 vectors.Rectangle) bool { + return rect1.X < rect2.X+rect2.Width && + rect1.X+rect1.Width > rect2.X && + rect1.Y < rect2.Y+rect2.Height && + rect1.Y+rect1.Height > rect2.Y +} + +func (s *CullingSystem) Destroy() { +} diff --git a/stdsystems/debug.go b/stdsystems/debug.go new file mode 100644 index 00000000..22ed7b6d --- /dev/null +++ b/stdsystems/debug.go @@ -0,0 +1,103 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +package stdsystems + +import ( + "log" + "net/http" + _ "net/http/pprof" + "os" + "runtime/pprof" + "runtime/trace" + + "github.com/felixge/fgprof" + rl "github.com/gen2brain/raylib-go/raylib" +) + +const gpprof = false + +func NewDebugSystem() DebugSystem { + return DebugSystem{} +} + +type DebugSystem struct { + pprofEnabled bool + traceEnabled bool + traceCounter int +} + +func (s *DebugSystem) Init() { + if gpprof { + http.DefaultServeMux.Handle("/debug/fgprof", fgprof.Handler()) + go func() { + log.Println(http.ListenAndServe(":6060", nil)) + }() + } + +} +func (s *DebugSystem) Run() { + if rl.IsKeyPressed(rl.KeyF9) { + if s.pprofEnabled { + pprof.StopCPUProfile() + log.Println("CPU Profile Stopped") + + // Create a memory profile file + memProfileFile, err := os.Create("mem.out") + if err != nil { + panic(err) + } + defer memProfileFile.Close() + + // Write memory profile to file + if err := pprof.WriteHeapProfile(memProfileFile); err != nil { + panic(err) + } + log.Println("Memory profile written to mem.prof") + } else { + f, err := os.Create("cpu.out") + if err != nil { + log.Fatal(err) + } + + err = pprof.StartCPUProfile(f) + if err != nil { + log.Fatal(err) + } + log.Println("CPU Profile Started") + } + + s.pprofEnabled = !s.pprofEnabled + } + + if rl.IsKeyPressed(rl.KeyF10) { + if !s.traceEnabled { + f, err := os.Create("trace.out") + if err != nil { + log.Fatal(err) + } + + err = trace.Start(f) + if err != nil { + log.Fatal(err) + } + log.Println("Trace Profile Started") + + s.traceEnabled = true + } + } + + if s.traceEnabled { + s.traceCounter++ + if s.traceCounter == 50 { + trace.Stop() + s.traceEnabled = false + s.traceCounter = 0 + log.Println("Trace Profile Stopped") + } + } +} +func (s *DebugSystem) Destroy() {} diff --git a/stdsystems/network-receive.go b/stdsystems/network-receive.go new file mode 100644 index 00000000..33e6eab3 --- /dev/null +++ b/stdsystems/network-receive.go @@ -0,0 +1,27 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package stdsystems + +import "time" + +func NewNetworkReceiveSystem() NetworkReceiveSystem { + return NetworkReceiveSystem{} +} + +type NetworkReceiveSystem struct{} + +func (s *NetworkReceiveSystem) Init() {} +func (s *NetworkReceiveSystem) Run(dt time.Duration) {} +func (s *NetworkReceiveSystem) Destroy() {} diff --git a/examples/raylib-ecs/systems/network-send.go b/stdsystems/network-send.go similarity index 55% rename from examples/raylib-ecs/systems/network-send.go rename to stdsystems/network-send.go index 4ae622ca..66b7635b 100644 --- a/examples/raylib-ecs/systems/network-send.go +++ b/stdsystems/network-send.go @@ -12,36 +12,42 @@ none :) Thank you for your support! */ -package systems +package stdsystems import ( "fmt" - "gomp/examples/raylib-ecs/components" "gomp/network" "gomp/pkg/ecs" + "gomp/stdcomponents" + "time" ) -type networkSendController struct{} +func NewNetworkSendSystem() NetworkSendSystem { + return NetworkSendSystem{} +} -func (s *networkSendController) Init(world *ecs.World) { - positions := components.PositionService.GetManager(world) - rotations := components.RotationService.GetManager(world) - mirroreds := components.MirroredService.GetManager(world) +type NetworkSendSystem struct { + World *ecs.EntityManager + Positions *stdcomponents.PositionComponentManager + Rotations *stdcomponents.RotationComponentManager + Mirroreds *stdcomponents.FlipComponentManager +} - positions.TrackChanges = true - rotations.TrackChanges = true - mirroreds.TrackChanges = true +func (s *NetworkSendSystem) Init() { + s.Positions.TrackChanges = true + s.Rotations.TrackChanges = true + s.Mirroreds.TrackChanges = true - positions.SetEncoder(func(components []components.Position) []byte { + s.Positions.SetEncoder(func(components []stdcomponents.Position) []byte { data := make([]byte, 0) for _, component := range components { - binary := fmt.Sprintf("%b", component.X) + binary := fmt.Sprintf("%b", component.XY.X) data = append(data, []byte(binary)...) } return data }) - rotations.SetEncoder(func(components []components.Rotation) []byte { + s.Rotations.SetEncoder(func(components []stdcomponents.Rotation) []byte { data := make([]byte, 0) for _, component := range components { binary := fmt.Sprintf("%b", component.Angle) @@ -50,7 +56,7 @@ func (s *networkSendController) Init(world *ecs.World) { return data }) - mirroreds.SetEncoder(func(components []components.Mirrored) []byte { + s.Mirroreds.SetEncoder(func(components []stdcomponents.Flip) []byte { data := make([]byte, 0) for _, component := range components { binary := fmt.Sprintf("%b", component.X) @@ -60,8 +66,7 @@ func (s *networkSendController) Init(world *ecs.World) { return data }) } -func (s *networkSendController) Update(world *ecs.World) {} -func (s *networkSendController) FixedUpdate(world *ecs.World) { +func (s *NetworkSendSystem) Run(dt time.Duration) { //patch := world.PatchGet() //world.PatchReset() //log.Printf("%v", patch) @@ -69,4 +74,4 @@ func (s *networkSendController) FixedUpdate(world *ecs.World) { network.Quic.Send([]byte("patch"), 0) } } -func (s *networkSendController) Destroy(world *ecs.World) {} +func (s *NetworkSendSystem) Destroy() {} diff --git a/examples/raylib-ecs/systems/network.go b/stdsystems/network.go similarity index 69% rename from examples/raylib-ecs/systems/network.go rename to stdsystems/network.go index d50bb72e..4157556e 100644 --- a/examples/raylib-ecs/systems/network.go +++ b/stdsystems/network.go @@ -12,12 +12,12 @@ none :) Thank you for your support! */ -package systems +package stdsystems import ( rl "github.com/gen2brain/raylib-go/raylib" "gomp/network" - "gomp/pkg/ecs" + "time" ) type NetworkMode int @@ -28,12 +28,16 @@ const ( Client ) -type networkController struct { +func NewNetworkSystem() NetworkSystem { + return NetworkSystem{} } -func (s *networkController) Init(world *ecs.World) { +type NetworkSystem struct { } -func (s *networkController) Update(world *ecs.World) { + +func (s *NetworkSystem) Init() { +} +func (s *NetworkSystem) Run(dt time.Duration) { if rl.IsKeyPressed(rl.KeyP) { network.Quic.Host("127.0.0.1:27015") } @@ -42,5 +46,4 @@ func (s *networkController) Update(world *ecs.World) { network.Quic.Connect("127.0.0.1:27015") } } -func (s *networkController) FixedUpdate(world *ecs.World) {} -func (s *networkController) Destroy(world *ecs.World) {} +func (s *NetworkSystem) Destroy() {} diff --git a/stdsystems/os-handler-system.go b/stdsystems/os-handler-system.go new file mode 100644 index 00000000..eecb8fdc --- /dev/null +++ b/stdsystems/os-handler-system.go @@ -0,0 +1,38 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +package stdsystems + +import ( + rl "github.com/gen2brain/raylib-go/raylib" + "time" +) + +func NewOSHandlerSystem() OSHandlerSystem { + return OSHandlerSystem{} +} + +type OSHandlerSystem struct{} + +func (s *OSHandlerSystem) Init() { + // TODO: pass parameters, resize or reinit. + rl.SetConfigFlags(rl.FlagWindowResizable) + rl.InitWindow(1280, 720, "GOMP") +} + +func (s *OSHandlerSystem) Run(dt time.Duration) bool { + if rl.IsKeyPressed(rl.KeyEscape) { + return true + } + if rl.WindowShouldClose() { + return true + } + return false +} + +func (s *OSHandlerSystem) Destroy() { + rl.CloseWindow() +} diff --git a/stdsystems/render-2d-cameras.go b/stdsystems/render-2d-cameras.go new file mode 100644 index 00000000..ce17819f --- /dev/null +++ b/stdsystems/render-2d-cameras.go @@ -0,0 +1,290 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +package stdsystems + +import ( + "cmp" + rl "github.com/gen2brain/raylib-go/raylib" + "github.com/negrel/assert" + "gomp/pkg/core" + "gomp/pkg/ecs" + "gomp/pkg/worker" + "gomp/stdcomponents" + "math" + "runtime" + "slices" + "time" +) + +func NewRender2DCamerasSystem() Render2DCamerasSystem { + return Render2DCamerasSystem{} +} + +type Render2DCamerasSystem struct { + Renderables *stdcomponents.RenderableComponentManager + RenderVisibles *stdcomponents.RenderVisibleComponentManager + RenderOrders *stdcomponents.RenderOrderComponentManager + Textures *stdcomponents.RLTextureProComponentManager + Tints *stdcomponents.TintComponentManager + AABBs *stdcomponents.AABBComponentManager + Cameras *stdcomponents.CameraComponentManager + RenderTexture2D *stdcomponents.FrameBuffer2DComponentManager + AnimationPlayers *stdcomponents.AnimationPlayerComponentManager + Flips *stdcomponents.FlipComponentManager + Positions *stdcomponents.PositionComponentManager + Scales *stdcomponents.ScaleComponentManager + Rotations *stdcomponents.RotationComponentManager + renderObjects []renderObject + renderObjectsSorted []renderObjectSorted + numWorkers int + + Engine *core.Engine +} + +type renderObject struct { + texture stdcomponents.RLTexturePro + mask stdcomponents.CameraLayer + order float32 +} + +type renderObjectSorted struct { + entity ecs.Entity + order float32 +} + +func (s *Render2DCamerasSystem) Init() { + s.renderObjects = make([]renderObject, 0, s.RenderVisibles.Len()) + s.numWorkers = runtime.NumCPU() - 2 +} + +func (s *Render2DCamerasSystem) Run(dt time.Duration) { + s.prepareRender(dt) + + // Collect and sort render objects + if cap(s.renderObjects) < s.RenderVisibles.Len() { + s.renderObjects = make([]renderObject, 0, max(s.RenderVisibles.Len(), cap(s.renderObjects)*2)) + } + + if cap(s.renderObjectsSorted) < s.RenderVisibles.Len() { + s.renderObjectsSorted = make([]renderObjectSorted, 0, max(s.RenderVisibles.Len(), cap(s.renderObjects)*2)) + } + + s.RenderVisibles.EachEntity(func(entity ecs.Entity) bool { + o := s.RenderOrders.GetUnsafe(entity) + assert.NotNil(o) + + s.renderObjectsSorted = append(s.renderObjectsSorted, renderObjectSorted{ + entity: entity, + order: o.CalculatedZ, + }) + + return true + }) + + slices.SortFunc(s.renderObjectsSorted, func(a, b renderObjectSorted) int { + return cmp.Compare(a.order, b.order) + }) + + for i := range s.renderObjectsSorted { + obj := &s.renderObjectsSorted[i] + + t := s.Textures.GetUnsafe(obj.entity) + assert.NotNil(t) + + //TODO: rework this with future new assets manager + if t.Texture == nil { + continue + } + + r := s.Renderables.GetUnsafe(obj.entity) + assert.NotNil(r) + + s.renderObjects = append(s.renderObjects, renderObject{ + texture: *t, + mask: r.CameraMask, + order: obj.order, + }) + } + + s.Cameras.EachEntity(func(cameraEntity ecs.Entity) bool { + camera := s.Cameras.GetUnsafe(cameraEntity) + assert.NotNil(camera) + renderTexture := s.RenderTexture2D.GetUnsafe(cameraEntity) + assert.NotNil(renderTexture) + + // Draw render objects + rl.BeginTextureMode(renderTexture.Texture) + rl.BeginMode2D(camera.Camera2D) + rl.ClearBackground(camera.BGColor) + + for i := range s.renderObjects { + obj := &s.renderObjects[i] + if camera.Layer&obj.mask == 0 { + continue + } + rl.DrawTexturePro(*obj.texture.Texture, obj.texture.Frame, obj.texture.Dest, obj.texture.Origin, obj.texture.Rotation, obj.texture.Tint) + } + + rl.EndMode2D() + rl.EndTextureMode() + + return true + }) + + s.renderObjects = s.renderObjects[:0] + s.renderObjectsSorted = s.renderObjectsSorted[:0] +} + +func (s *Render2DCamerasSystem) Destroy() { +} + +func (s *Render2DCamerasSystem) prepareRender(dt time.Duration) { + s.Textures.ProcessEntities(func(entity ecs.Entity, workerId worker.WorkerId) { + texturePro := s.Textures.GetUnsafe(entity) + assert.NotNil(texturePro) + + animation := s.AnimationPlayers.GetUnsafe(entity) + if animation != nil { + frame := &texturePro.Frame + if animation.Vertical { + frame.Y += frame.Height * float32(animation.Current) + } else { + frame.X += frame.Width * float32(animation.Current) + } + } + + flipped := s.Flips.GetUnsafe(entity) + if flipped != nil { + if flipped.X { + texturePro.Frame.Width *= -1 + } + if flipped.Y { + texturePro.Frame.Height *= -1 + } + } + + position := s.Positions.GetUnsafe(entity) + if position != nil { + //decay := 40.0 // DECAY IS TICKRATE DEPENDENT + //x := float32(s.expDecay(float64(texturePro.Dest.X), float64(position.XY.X), decay, dts)) + //y := float32(s.expDecay(float64(texturePro.Dest.Y), float64(position.XY.Y), decay, dts)) + texturePro.Dest.X = position.XY.X + texturePro.Dest.Y = position.XY.Y + } + + rotation := s.Rotations.GetUnsafe(entity) + if rotation != nil { + texturePro.Rotation = float32(rotation.Degrees()) + } + + scale := s.Scales.GetUnsafe(entity) + if scale != nil { + texturePro.Dest.Width *= scale.XY.X + texturePro.Dest.Height *= scale.XY.Y + } + + tint := s.Tints.GetUnsafe(entity) + if tint != nil { + trTint := &texturePro.Tint + trTint.A = tint.A + trTint.R = tint.R + trTint.G = tint.G + trTint.B = tint.B + } + }) +} + +func (s *Render2DCamerasSystem) prepareAnimations() { + // TODO revert loop to process animations but not a textures? + s.Textures.ProcessEntities(func(entity ecs.Entity, workerId worker.WorkerId) { + texturePro := s.Textures.GetUnsafe(entity) + animation := s.AnimationPlayers.GetUnsafe(entity) + if animation == nil { + return + } + frame := &texturePro.Frame + if animation.Vertical { + frame.Y += frame.Height * float32(animation.Current) + } else { + frame.X += frame.Width * float32(animation.Current) + } + }) +} + +func (s *Render2DCamerasSystem) prepareFlips() { + s.Textures.ProcessEntities(func(entity ecs.Entity, workerId worker.WorkerId) { + texturePro := s.Textures.GetUnsafe(entity) + flipped := s.Flips.GetUnsafe(entity) + if flipped == nil { + return + } + if flipped.X { + texturePro.Frame.Width *= -1 + } + if flipped.Y { + texturePro.Frame.Height *= -1 + } + }) +} + +func (s *Render2DCamerasSystem) preparePositions(dt time.Duration) { + //dts := dt.Seconds() + s.Textures.ProcessEntities(func(entity ecs.Entity, workerId worker.WorkerId) { + texturePro := s.Textures.GetUnsafe(entity) + position := s.Positions.GetUnsafe(entity) + if position == nil { + return + } + //decay := 40.0 // DECAY IS TICKRATE DEPENDENT + //x := float32(s.expDecay(float64(texturePro.Dest.X), float64(position.XY.X), decay, dts)) + //y := float32(s.expDecay(float64(texturePro.Dest.Y), float64(position.XY.Y), decay, dts)) + texturePro.Dest.X = position.XY.X + texturePro.Dest.Y = position.XY.Y + }) +} + +func (s *Render2DCamerasSystem) prepareRotations() { + s.Textures.ProcessEntities(func(entity ecs.Entity, workerId worker.WorkerId) { + texturePro := s.Textures.GetUnsafe(entity) + rotation := s.Rotations.GetUnsafe(entity) + if rotation == nil { + return + } + texturePro.Rotation = float32(rotation.Degrees()) + }) +} + +func (s *Render2DCamerasSystem) prepareScales() { + s.Textures.ProcessEntities(func(entity ecs.Entity, workerId worker.WorkerId) { + texturePro := s.Textures.GetUnsafe(entity) + scale := s.Scales.GetUnsafe(entity) + if scale == nil { + return + } + texturePro.Dest.Width *= scale.XY.X + texturePro.Dest.Height *= scale.XY.Y + }) +} + +func (s *Render2DCamerasSystem) prepareTints() { + s.Textures.ProcessEntities(func(entity ecs.Entity, workerId worker.WorkerId) { + tr := s.Textures.GetUnsafe(entity) + tint := s.Tints.GetUnsafe(entity) + if tint == nil { + return + } + trTint := &tr.Tint + trTint.A = tint.A + trTint.R = tint.R + trTint.G = tint.G + trTint.B = tint.B + }) +} + +func (s *Render2DCamerasSystem) expDecay(a, b, decay, dt float64) float64 { + return b + (a-b)*(math.Exp(-decay*dt)) +} diff --git a/stdsystems/render.go b/stdsystems/render.go new file mode 100644 index 00000000..9d11fea0 --- /dev/null +++ b/stdsystems/render.go @@ -0,0 +1,88 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +<- HromRu Donated 1 500 RUB +<- MuTaToR Donated 500 RUB + +Thank you for your support! +*/ + +package stdsystems + +import ( + rl "github.com/gen2brain/raylib-go/raylib" + "gomp/pkg/ecs" + "gomp/stdcomponents" + "slices" + "time" +) + +func NewRenderSystem() RenderSystem { + return RenderSystem{} +} + +type RenderSystem struct { + EntityManager *ecs.EntityManager + FrameBuffer2D *stdcomponents.FrameBuffer2DComponentManager + + renderTextures []rl.RenderTexture2D + frames []stdcomponents.FrameBuffer2D +} + +func (s *RenderSystem) Init() { + s.renderTextures = make([]rl.RenderTexture2D, 0, s.FrameBuffer2D.Len()) + s.frames = make([]stdcomponents.FrameBuffer2D, 0, s.FrameBuffer2D.Len()) +} + +func (s *RenderSystem) Run(dt time.Duration) bool { + s.FrameBuffer2D.EachComponent(func(c *stdcomponents.FrameBuffer2D) bool { + s.frames = append(s.frames, *c) + return true + }) + slices.SortFunc(s.frames, func(a, b stdcomponents.FrameBuffer2D) int { + return int(a.Layer - b.Layer) + }) + + rl.BeginDrawing() + + for _, frame := range s.frames { + rl.BeginBlendMode(frame.BlendMode) + rl.DrawTexturePro(frame.Texture.Texture, + rl.Rectangle{ + X: 0, + Y: 0, + Width: float32(frame.Texture.Texture.Width), + Height: -float32(frame.Texture.Texture.Height), + }, + frame.Dst, + rl.Vector2{}, + frame.Rotation, + frame.Tint, + ) + rl.EndBlendMode() + } + + rl.EndDrawing() + + s.frames = s.frames[:0] + return false +} + +func (s *RenderSystem) Destroy() { +} + +type RenderInjector struct { + EntityManager *ecs.EntityManager + FrameBuffer2D *stdcomponents.FrameBuffer2DComponentManager +} + +func (s *RenderSystem) InjectWorld(injector *RenderInjector) { + s.EntityManager = injector.EntityManager + s.FrameBuffer2D = injector.FrameBuffer2D +} diff --git a/stdsystems/sprite-matrix.go b/stdsystems/sprite-matrix.go new file mode 100644 index 00000000..537b19fa --- /dev/null +++ b/stdsystems/sprite-matrix.go @@ -0,0 +1,52 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +package stdsystems + +import ( + rl "github.com/gen2brain/raylib-go/raylib" + "gomp/pkg/ecs" + "gomp/stdcomponents" +) + +func NewSpriteMatrixSystem() SpriteMatrixSystem { + return SpriteMatrixSystem{} +} + +// SpriteMatrixSystem is a system that prepares SpriteSheet to be rendered +type SpriteMatrixSystem struct { + SpriteMatrixes *stdcomponents.SpriteMatrixComponentManager + RLTexturePros *stdcomponents.RLTextureProComponentManager + AnimationStates *stdcomponents.AnimationStateComponentManager + Positions *stdcomponents.PositionComponentManager +} + +func (s *SpriteMatrixSystem) Init() {} +func (s *SpriteMatrixSystem) Run() { + s.SpriteMatrixes.EachEntity(func(entity ecs.Entity) bool { + spriteMatrix := s.SpriteMatrixes.Get(entity) // + position := s.Positions.GetUnsafe(entity) + animationState := s.AnimationStates.GetUnsafe(entity) // + + frame := spriteMatrix.Animations[*animationState].Frame + + tr := s.RLTexturePros.GetUnsafe(entity) + if tr == nil { + s.RLTexturePros.Create(entity, stdcomponents.RLTexturePro{ + Texture: spriteMatrix.Texture, // + Frame: frame, // + Origin: spriteMatrix.Origin, + Dest: rl.Rectangle{X: position.XY.X, Y: position.XY.Y, Width: frame.Width, Height: frame.Height}, // + }) + } else { + // Run spriteRender + + tr.Frame = frame + } + return true + }) +} +func (s *SpriteMatrixSystem) Destroy() {} diff --git a/stdsystems/sprite-sheet.go b/stdsystems/sprite-sheet.go new file mode 100644 index 00000000..9bb45b8d --- /dev/null +++ b/stdsystems/sprite-sheet.go @@ -0,0 +1,55 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +package stdsystems + +import ( + "gomp/stdcomponents" +) + +func NewTextureRenderSpriteSheetSystem() TextureRenderSpriteSheetSystem { + return TextureRenderSpriteSheetSystem{} +} + +// TextureRenderSpriteSheetSystem is a system that prepares SpriteSheet to be rendered +type TextureRenderSpriteSheetSystem struct { + SpriteSheets *stdcomponents.SpriteSheetComponentManager + TextureRenders *stdcomponents.RLTextureProComponentManager +} + +func (s *TextureRenderSpriteSheetSystem) Init() {} +func (s *TextureRenderSpriteSheetSystem) Run() { + //s.SpriteSheets.EachParallel(func(entity ecs.Entity, spriteSheet *stdcomponents.SpriteSheet) bool { + // if spriteSheet == nil { + // return true + // } + // + // tr := s.RlTexturePros.GetUnsafe(entity) + // if tr == nil { + // // Create new spriteRender + // newRender := stdcomponents.RLTexturePro{ + // TextureId: spriteSheet.TextureId, + // Frame: spriteSheet.Frame, + // Origin: spriteSheet.Origin, + // Dest: rl.NewRectangle( + // 0, + // 0, + // spriteSheet.Frame.Width, + // spriteSheet.Frame.Height, + // ), + // } + // + // s.RlTexturePros.Create(entity, newRender) + // } else { + // // Run spriteRender + // tr.TextureId = spriteSheet.TextureId + // tr.Frame = spriteSheet.Frame + // tr.Origin = spriteSheet.Origin + // } + // return true + //}) +} +func (s *TextureRenderSpriteSheetSystem) Destroy() {} diff --git a/stdsystems/sprite.go b/stdsystems/sprite.go new file mode 100644 index 00000000..8952dd76 --- /dev/null +++ b/stdsystems/sprite.go @@ -0,0 +1,105 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +<- Монтажер сука Donated 50 RUB + +Thank you for your support! +*/ + +package stdsystems + +import ( + rl "github.com/gen2brain/raylib-go/raylib" + "github.com/negrel/assert" + "gomp/pkg/core" + "gomp/pkg/ecs" + "gomp/pkg/worker" + "gomp/stdcomponents" +) + +func NewSpriteSystem() SpriteSystem { + return SpriteSystem{} +} + +// SpriteSystem is a system that prepares Sprite to be rendered +type SpriteSystem struct { + Positions *stdcomponents.PositionComponentManager + Scales *stdcomponents.ScaleComponentManager + Sprites *stdcomponents.SpriteComponentManager + RLTexturePros *stdcomponents.RLTextureProComponentManager + RenderOrder *stdcomponents.RenderOrderComponentManager + + numWorkers int + accRenderOrder [][]ecs.Entity + accRLTexturePros [][]ecs.Entity + Engine *core.Engine +} + +func (s *SpriteSystem) Init() { + s.numWorkers = s.Engine.Pool().NumWorkers() + s.accRenderOrder = make([][]ecs.Entity, s.numWorkers) + s.accRLTexturePros = make([][]ecs.Entity, s.numWorkers) +} +func (s *SpriteSystem) Run() { + for i := range s.accRenderOrder { + s.accRenderOrder[i] = s.accRenderOrder[i][:0] + } + for i := range s.accRLTexturePros { + s.accRLTexturePros[i] = s.accRLTexturePros[i][:0] + } + s.Sprites.ProcessEntities(func(entity ecs.Entity, workerId worker.WorkerId) { + if !s.RenderOrder.Has(entity) { + s.accRenderOrder[workerId] = append(s.accRenderOrder[workerId], entity) + } + if !s.RLTexturePros.Has(entity) { + s.accRLTexturePros[workerId] = append(s.accRLTexturePros[workerId], entity) + } + }) + for a := range s.accRenderOrder { + for _, entity := range s.accRenderOrder[a] { + s.RenderOrder.Create(entity, stdcomponents.RenderOrder{}) + } + } + for a := range s.accRLTexturePros { + for _, entity := range s.accRLTexturePros[a] { + s.RLTexturePros.Create(entity, stdcomponents.RLTexturePro{}) + } + } + + s.Sprites.ProcessEntities(s.updateTextureRender) +} +func (s *SpriteSystem) Destroy() {} +func (s *SpriteSystem) updateTextureRender(entity ecs.Entity, workerId worker.WorkerId) { + sprite := s.Sprites.GetUnsafe(entity) + assert.NotNil(sprite) + + position := s.Positions.GetUnsafe(entity) + assert.NotNil(position) + + scale := s.Scales.GetUnsafe(entity) + assert.NotNil(scale) + + renderOrder := s.RenderOrder.GetUnsafe(entity) + assert.NotNil(renderOrder) + + tr := s.RLTexturePros.GetUnsafe(entity) + assert.NotNil(tr) + + tr.Texture = sprite.Texture + tr.Frame = sprite.Frame + tr.Origin = rl.Vector2{ + X: sprite.Origin.X * scale.XY.X, + Y: sprite.Origin.Y * scale.XY.Y, + } + tr.Dest.X = position.XY.X + tr.Dest.Y = position.XY.Y + tr.Dest.Width = sprite.Frame.Width + tr.Dest.Height = sprite.Frame.Height + tr.Tint = sprite.Tint +} diff --git a/stdsystems/texture-position-smooth.go b/stdsystems/texture-position-smooth.go new file mode 100644 index 00000000..461d0117 --- /dev/null +++ b/stdsystems/texture-position-smooth.go @@ -0,0 +1,96 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +package stdsystems + +import ( + rl "github.com/gen2brain/raylib-go/raylib" + "github.com/negrel/assert" + "gomp/pkg/core" + "gomp/pkg/ecs" + "gomp/pkg/worker" + "gomp/stdcomponents" + "gomp/vectors" + "math" + "runtime" + "time" +) + +func NewTexturePositionSmoothSystem() TexturePositionSmoothSystem { + return TexturePositionSmoothSystem{} +} + +type TexturePositionSmoothSystem struct { + TexturePositionSmooth *stdcomponents.TexturePositionSmoothComponentManager + Position *stdcomponents.PositionComponentManager + RLTexture *stdcomponents.RLTextureProComponentManager + numWorkers int + Engine *core.Engine +} + +func (s *TexturePositionSmoothSystem) Init() { + s.numWorkers = runtime.NumCPU() - 2 +} + +func (s *TexturePositionSmoothSystem) Run(dt time.Duration) { + //DEBUG Temporary, TODO: remove + if rl.IsKeyPressed(rl.KeyI) { + s.TexturePositionSmooth.ProcessComponents(func(t *stdcomponents.TexturePositionSmooth, workerId worker.WorkerId) { + *t = stdcomponents.TexturePositionSmoothOff + }) + } + if rl.IsKeyPressed(rl.KeyO) { + s.TexturePositionSmooth.ProcessComponents(func(t *stdcomponents.TexturePositionSmooth, workerId worker.WorkerId) { + *t = stdcomponents.TexturePositionSmoothLerp + }) + } + if rl.IsKeyPressed(rl.KeyP) { + s.TexturePositionSmooth.ProcessComponents(func(t *stdcomponents.TexturePositionSmooth, workerId worker.WorkerId) { + *t = stdcomponents.TexturePositionSmoothExpDecay + }) + } + //END DEBUG + + s.TexturePositionSmooth.ProcessEntities(func(entity ecs.Entity, workerId worker.WorkerId) { + position := s.Position.GetUnsafe(entity) + texture := s.RLTexture.GetUnsafe(entity) + smooth := s.TexturePositionSmooth.GetUnsafe(entity) + if texture == nil { + return + } + assert.Nil(position, "position is nil") + + switch *smooth { + case stdcomponents.TexturePositionSmoothLerp: + dest := vectors.Vec2{X: texture.Dest.X, Y: texture.Dest.Y} + xy := s.Lerp2D(dest, position.XY, dt) + texture.Dest.X = xy.X + texture.Dest.Y = xy.Y + case stdcomponents.TexturePositionSmoothExpDecay: + dest := vectors.Vec2{X: texture.Dest.X, Y: texture.Dest.Y} + xy := s.ExpDecay2D(dest, position.XY, 10, float64(dt)) + texture.Dest.X = xy.X + texture.Dest.Y = xy.Y + default: + panic("not implemented") + } + }) +} + +func (s *TexturePositionSmoothSystem) Destroy() {} + +func (_ *TexturePositionSmoothSystem) Lerp2D(a, b vectors.Vec2, dt time.Duration) vectors.Vec2 { + return a.Add(b.Sub(a).Scale(float32(dt))) +} + +// ExpDecay2D applies an exponential decay to the position vector. +// TODO: float32 math package +func (_ *TexturePositionSmoothSystem) ExpDecay2D(a, b vectors.Vec2, decay, dt float64) vectors.Vec2 { + return vectors.Vec2{ + X: b.X + (a.X-b.X)*(float32(math.Exp(-decay*dt))), + Y: b.Y + (a.Y-b.Y)*(float32(math.Exp(-decay*dt))), + } +} diff --git a/stdsystems/velocity.go b/stdsystems/velocity.go new file mode 100644 index 00000000..fe3c64ec --- /dev/null +++ b/stdsystems/velocity.go @@ -0,0 +1,100 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +package stdsystems + +import ( + "github.com/negrel/assert" + "gomp/pkg/core" + "gomp/pkg/ecs" + "gomp/pkg/worker" + "gomp/stdcomponents" + "math" + "runtime" + "time" +) + +func NewVelocitySystem() VelocitySystem { + return VelocitySystem{} +} + +type VelocitySystem struct { + Velocities *stdcomponents.VelocityComponentManager + Positions *stdcomponents.PositionComponentManager + RigidBodies *stdcomponents.RigidBodyComponentManager + + numWorkers int + Engine *core.Engine +} + +func (s *VelocitySystem) Init() { + s.numWorkers = runtime.NumCPU() - 2 +} + +func (s *VelocitySystem) Run(dt time.Duration) { + dtSec := float32(dt.Seconds()) + + s.Velocities.ProcessEntities(func(e ecs.Entity, workerId worker.WorkerId) { + velocity := s.Velocities.GetUnsafe(e) + assert.True(s.isVelocityValid(velocity)) + + position := s.Positions.GetUnsafe(e) + assert.True(s.isPositionValid(position)) + + position.XY.X += velocity.X * dtSec + position.XY.Y += velocity.Y * dtSec + }) +} + +func (s *VelocitySystem) Destroy() {} + +func (s *VelocitySystem) isVelocityValid(velocity *stdcomponents.Velocity) bool { + if velocity == nil { + return false + } + + // Convert to float64 + x := float64(velocity.X) + y := float64(velocity.Y) + + if math.IsInf(x, 1) || math.IsInf(x, -1) { + return false + } + + if math.IsInf(y, 1) || math.IsInf(y, -1) { + return false + } + + if math.IsNaN(x) || math.IsNaN(y) { + return false + } + + return true +} + +func (s *VelocitySystem) isPositionValid(position *stdcomponents.Position) bool { + if position == nil { + return false + } + + // Convert to float64 + x := float64(position.XY.X) + y := float64(position.XY.Y) + + if math.IsInf(x, 1) || math.IsInf(x, -1) { + return false + } + + if math.IsInf(y, 1) || math.IsInf(y, -1) { + return false + } + + if math.IsNaN(x) || math.IsNaN(y) { + return false + } + + return true +} diff --git a/stdsystems/ysort.go b/stdsystems/ysort.go new file mode 100644 index 00000000..b42f7c5b --- /dev/null +++ b/stdsystems/ysort.go @@ -0,0 +1,50 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package stdsystems + +import ( + "gomp/pkg/ecs" + "gomp/pkg/worker" + "gomp/stdcomponents" +) + +const ySortOffsetScale float32 = 0.001 + +func NewYSortSystem() YSortSystem { + return YSortSystem{} +} + +type YSortSystem struct { + EntityManager *ecs.EntityManager + YSorts *stdcomponents.YSortComponentManager + Positions *stdcomponents.PositionComponentManager + RenderOrders *stdcomponents.RenderOrderComponentManager +} + +func (s *YSortSystem) Init() {} +func (s *YSortSystem) Run() { + s.YSorts.ProcessEntities(func(entity ecs.Entity, _ worker.WorkerId) { + pos := s.Positions.GetUnsafe(entity) + renderOrder := s.RenderOrders.GetUnsafe(entity) + + // Calculate depth based on Y position + yDepth := pos.XY.Y * ySortOffsetScale + + // Preserve original Z layer but add Y-based offset + //renderOrder.CalculatedZ = float32(int(pos.Z)) + yDepth + renderOrder.CalculatedZ = yDepth + }) +} +func (s *YSortSystem) Destroy() {} diff --git a/steam_appid.txt b/steam_appid.txt new file mode 100644 index 00000000..7ad80225 --- /dev/null +++ b/steam_appid.txt @@ -0,0 +1 @@ +480 \ No newline at end of file diff --git a/taskfile.yml b/taskfile.yml index ceed8639..ffe20f96 100644 --- a/taskfile.yml +++ b/taskfile.yml @@ -2,8 +2,16 @@ version: "3" tasks: dev: + env: + CGO_ENABLED: 1 cmds: - - air + - go run ./examples/new-api + + dev-assert: + env: + CGO_ENABLED: 1 + cmds: + - go run ./examples/new-api -tags assert server: cmds: @@ -30,10 +38,89 @@ tasks: cmds: - protoc --go_out=. internal/**/*.proto - pprof: + gpprof-cpu: cmds: - - go tool pprof -http=":8000" 'http://localhost:3000/debug/pprof/profile?seconds=10' + - go tool pprof --http=:6061 http://localhost:6060/debug/fgprof?seconds=20 pprof-cpu: cmds: - go tool pprof -http=":8000" ./cpu.out + + pprof-mem: + cmds: + - go tool pprof -http=":8001" ./mem.out + + pprof-trace: + cmds: + - go tool trace -http='127.0.0.1:8002' ./trace.out + + build-sdl: + env: + CGO_ENABLED: 1 + cmds: + - go build -o ./.dist/sdl3-cgo-game.exe internal/sdl3-cgo/sdl3cgo-game.go + - go build -o ./.dist/sdl3-purego-game.exe internal/sdl3-pure/sdl-game.go + - go build -o ./.dist/sdl3-purego-zukko-game.exe internal/sdl3-pure-zukko/sdl-zukko.go + + build-win64: + env: + CGO_ENABLED: 1 + cmds: + - go build -ldflags="-s -w" -o ./.dist/game-win64.exe -tags opengl43 ./examples/new-api + + build-mac: + - task: build-darwin-amd64 + - task: build-darwin-arm64 + - task: build-darwin-universal + - task: sign-darwin-universal + + build-darwin-amd64: + env: + CGO_ENABLED: 1 + cmds: + - env CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build -o ./.dist/game-darwin-amd64 -tags opengl43 examples/new-api/game.go + - chmod +x ./.dist/game-darwin-amd64 + + build-darwin-arm64: + env: + CGO_ENABLED: 1 + cmds: + - env CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 go build -o ./.dist/game-darwin-arm64 -tags opengl43 examples/new-api/game.go + - chmod +x ./.dist/game-darwin-arm64 + + build-darwin-universal: + cmds: + - lipo -create -output ./.dist/game-universal ./.dist/game-darwin-amd64 ./.dist/game-darwin-arm64 + - chmod +x ./.dist/game-universal + + sign-darwin-universal: + cmds: + - mkdir -p ./.dist/game.app/Contents/MacOS + - mv ./.dist/game-universal ./.dist/game.app/Contents/MacOS/game + - chmod +x ./.dist/game.app + - codesign --timestamp --options=runtime --deep -fs milanrodd-cert -v ./.dist/game.app + - xattr -cr ./.dist/game.app + - /usr/bin/ditto -c -k --keepParent ./.dist/game.app ./.dist/game.zip + + build-wasm: + env: + GOOS: wasip1 + GOARCH: wasm + watch: true + sources: + - 'internal/wasyan**/*.go' + cmds: + - go build -buildmode=c-shared -o ./hello.wasm ./internal/wasyan-wasm + - curl http://127.0.0.1:3000/update/hello.wasm + + + build-tiny-wasm: + env: + GOOS: wasip1 + GOARCH: wasm + watch: true + sources: + - 'internal/wasyan-wasm/*.go' + cmds: + - tinygo build -buildmode=c-shared -o ./hello.wasm -target=wasi ./internal/wasyan-wasm + - curl http://127.0.0.1:3000/update/hello.wasm diff --git a/pkg/gomp/basic-components.client.go b/vectors/radian.go similarity index 50% rename from pkg/gomp/basic-components.client.go rename to vectors/radian.go index fab4abad..b00567da 100644 --- a/pkg/gomp/basic-components.client.go +++ b/vectors/radian.go @@ -1,14 +1,17 @@ -//go:build !server -// +build !server - /* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -*/ -package gomp +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ -import input "github.com/quasilyte/ebitengine-input" +package vectors -var InputComponent = CreateComponent[input.Handler]() +type Radians = float64 diff --git a/vectors/rectangle.go b/vectors/rectangle.go new file mode 100644 index 00000000..983089f9 --- /dev/null +++ b/vectors/rectangle.go @@ -0,0 +1,19 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package vectors + +type Rectangle struct { + X, Y, Width, Height float32 +} diff --git a/vectors/vector2.go b/vectors/vector2.go new file mode 100644 index 00000000..d3a0f09f --- /dev/null +++ b/vectors/vector2.go @@ -0,0 +1,106 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package vectors + +import ( + "math" +) + +type Vec2 struct { + X, Y float32 +} + +func (v Vec2) Add(other Vec2) Vec2 { + return Vec2{v.X + other.X, v.Y + other.Y} +} + +func (v Vec2) Sub(other Vec2) Vec2 { + return Vec2{v.X - other.X, v.Y - other.Y} +} + +func (v Vec2) Mul(other Vec2) Vec2 { + return Vec2{v.X * other.X, v.Y * other.Y} +} + +func (v Vec2) Div(other Vec2) Vec2 { + return Vec2{v.X / other.X, v.Y / other.Y} +} + +func (v Vec2) AddScalar(scalar float32) Vec2 { + return Vec2{v.X + scalar, v.Y + scalar} +} + +func (v Vec2) SubScalar(scalar float32) Vec2 { + return Vec2{v.X - scalar, v.Y - scalar} +} + +func (v Vec2) Scale(scalar float32) Vec2 { + return Vec2{v.X * scalar, v.Y * scalar} +} + +func (v Vec2) Angle() Radians { + return math.Atan2(float64(v.Y), float64(v.X)) +} + +func (v Vec2) Length() float32 { + return float32(math.Sqrt(float64(v.X*v.X + v.Y*v.Y))) +} + +func (v Vec2) Distance(other Vec2) float32 { + return v.Sub(other).Length() +} + +func (v Vec2) Normalize() Vec2 { + return v.Scale(1 / v.Length()) +} + +func (v Vec2) Rotate(angle Radians) Vec2 { + return Vec2{ + v.X*float32(math.Cos(angle)) - v.Y*float32(math.Sin(angle)), + v.X*float32(math.Sin(angle)) + v.Y*float32(math.Cos(angle)), + } +} + +func (v Vec2) LengthSquared() float32 { + l := v.Length() + return l * l +} + +func (v Vec2) Neg() Vec2 { + return Vec2{-v.X, -v.Y} +} + +// Perpendicular - clockwise +func (v Vec2) Perpendicular() Vec2 { + return Vec2{ + X: v.Y, + Y: -v.X, + } +} + +func (v Vec2) Normal() Vec2 { + return Vec2{ + X: v.Y, + Y: -v.X, + } +} + +func (v Vec2) Dot(other Vec2) float32 { + return v.X*other.X + v.Y*other.Y +} + +func (v Vec2) ToVec3() Vec3 { + return Vec3{v.X, v.Y, 0} +} diff --git a/vectors/vector3.go b/vectors/vector3.go new file mode 100644 index 00000000..bb3d14e6 --- /dev/null +++ b/vectors/vector3.go @@ -0,0 +1,99 @@ +/* +This Source Code Form is subject to the terms of the Mozilla +Public License, v. 2.0. If a copy of the MPL was not distributed +with this file, You can obtain one at http://mozilla.org/MPL/2.0/. + +===-===-===-===-===-===-===-===-===-=== +Donations during this file development: +-===-===-===-===-===-===-===-===-===-=== + +none :) + +Thank you for your support! +*/ + +package vectors + +import ( + "math" +) + +type Vec3 struct { + X, Y, Z float32 +} + +func (v Vec3) Add(other Vec3) Vec3 { + return Vec3{v.X + other.X, v.Y + other.Y, v.Z + other.Z} +} + +func (v Vec3) Sub(other Vec3) Vec3 { + return Vec3{v.X - other.X, v.Y - other.Y, v.Z - other.Z} +} + +func (v Vec3) Mul(other Vec3) Vec3 { + return Vec3{v.X * other.X, v.Y * other.Y, v.Z * other.Z} +} + +func (a Vec3) Cross(b Vec3) Vec3 { + return Vec3{ + a.Y*b.Z - b.Y*a.Z, + a.Z*b.X - b.Z*a.X, + a.X*b.Y - b.X*a.Y, + } +} + +func (v Vec3) Div(other Vec3) Vec3 { + return Vec3{v.X / other.X, v.Y / other.Y, v.Z / other.Z} +} + +func (v Vec3) AddScalar(scalar float32) Vec3 { + return Vec3{v.X + scalar, v.Y + scalar, v.Z + scalar} +} + +func (v Vec3) SubScalar(scalar float32) Vec3 { + return Vec3{v.X - scalar, v.Y - scalar, v.Z - scalar} +} + +func (v Vec3) Scale(scalar float32) Vec3 { + return Vec3{v.X * scalar, v.Y * scalar, v.Z * scalar} +} + +func (v Vec3) Length() float32 { + return float32(math.Sqrt(float64(v.X*v.X + v.Y*v.Y + v.Z*v.Z))) +} + +func (v Vec3) Normalize() Vec3 { + return v.Scale(1 / v.Length()) +} + +func (v Vec3) Rotate(angle Radians) Vec3 { + return Vec3{ + v.X*float32(math.Cos(angle)) - v.Y*float32(math.Sin(angle)), + v.X*float32(math.Sin(angle)) + v.Y*float32(math.Cos(angle)), + v.Z, + } +} + +func (v Vec3) LengthSquared() float32 { + l := v.Length() + return l * l +} + +func (v Vec3) Neg() Vec3 { + return Vec3{-v.X, -v.Y, -v.Z} +} + +func (v Vec3) Perpendicular() Vec3 { + return Vec3{ + X: -v.Y, + Y: v.X, + } +} + +func (v Vec3) Dot(other Vec3) float32 { + return v.X*other.X + v.Y*other.Y + v.Z*other.Z +} + +func (v Vec3) ToVec2() Vec2 { + return Vec2{v.X, v.Y} +}