diff --git a/examples/new-api/assets/assets.go b/examples/new-api/assets/assets.go index 63ef3b75..7a967025 100644 --- a/examples/new-api/assets/assets.go +++ b/examples/new-api/assets/assets.go @@ -68,6 +68,9 @@ var Audio = gomp.CreateAssetLibrary( 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) { 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/components/audio.go b/examples/new-api/components/audio.go index 079a7a08..93032d0a 100644 --- a/examples/new-api/components/audio.go +++ b/examples/new-api/components/audio.go @@ -20,8 +20,11 @@ import ( ) type SoundEffect struct { - Clip *rl.Sound + // 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 @@ -36,3 +39,16 @@ 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/ids.go b/examples/new-api/components/ids.go index 74ccd2ba..f6063132 100644 --- a/examples/new-api/components/ids.go +++ b/examples/new-api/components/ids.go @@ -30,6 +30,7 @@ const ( SpaceshipIntentComponentId AsteroidSceneManagerComponentId SoundEffectManagerComponentId + SpatialAudioManagerComponentId TextureRectComponentId TextureCircleComponentId ) diff --git a/examples/new-api/entities/satellite.go b/examples/new-api/entities/satellite.go index 6953ad32..41a03857 100644 --- a/examples/new-api/entities/satellite.go +++ b/examples/new-api/entities/satellite.go @@ -15,13 +15,15 @@ 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" + + rl "github.com/gen2brain/raylib-go/raylib" ) type CreateSatelliteManagers struct { @@ -34,6 +36,7 @@ type CreateSatelliteManagers struct { RigidBodies *stdcomponents.RigidBodyComponentManager Velocities *stdcomponents.VelocityComponentManager Renderables *stdcomponents.RenderableComponentManager + SoundEffects *components.SoundEffectsComponentManager } func CreateSatellite( @@ -91,5 +94,14 @@ func CreateSatellite( CameraMask: config.MainCameraLayer | config.MinimapCameraLayer, }) + props.SoundEffects.Create(entity, components.SoundEffect{ + Clip: assets.Audio.Get("satellite_main_sound.wav"), + IsLooping: true, + IsPlaying: false, + Volume: 0.05, + Pan: 0.5, + Pitch: 1.0, + }) + return entity } diff --git a/examples/new-api/entities/spaceship.go b/examples/new-api/entities/spaceship.go index d7c93fae..354ad5b4 100644 --- a/examples/new-api/entities/spaceship.go +++ b/examples/new-api/entities/spaceship.go @@ -123,7 +123,7 @@ func CreateSpaceShip( IsPlaying: false, IsLooping: true, Pitch: 1.0, - Volume: 1.0, + Volume: 0.0, Pan: 0.5, }) diff --git a/examples/new-api/game.go b/examples/new-api/game.go index 9596e2a5..878b7038 100644 --- a/examples/new-api/game.go +++ b/examples/new-api/game.go @@ -79,6 +79,7 @@ func (g *Game) Init(engine *core.Engine) { systems.AssetLib.Init() systems.Audio.Init() systems.SpatialAudio.Init() + systems.AudioSettings.Init() systems.RenderCameras.Init() systems.Culling.Init() systems.TexturePositionSmooth.Init() @@ -128,6 +129,7 @@ func (g *Game) Render(dt time.Duration) { systems.YSort.Run() systems.Audio.Run(dt) systems.SpatialAudio.Run(dt) + systems.AudioSettings.Run(dt) scene.Render(dt) @@ -160,6 +162,7 @@ func (g *Game) Destroy() { systems.AssetLib.Destroy() systems.Audio.Destroy() systems.SpatialAudio.Destroy() + systems.AudioSettings.Destroy() systems.RenderCameras.Destroy() systems.Culling.Destroy() systems.TexturePositionSmooth.Destroy() diff --git a/examples/new-api/instances/component-list.go b/examples/new-api/instances/component-list.go index e2a39fdd..daff7de5 100644 --- a/examples/new-api/instances/component-list.go +++ b/examples/new-api/instances/component-list.go @@ -63,6 +63,7 @@ type ComponentList struct { SpaceshipIntent components.SpaceshipIntentComponentManager AsteroidSceneManager components.AsteroidSceneManagerComponentManager SoundEffects components.SoundEffectsComponentManager + SpatialAudio components.SpatialAudioComponentManager TextureRect components.TextureRectComponentManager PrimitiveCircle components.PrimitiveCircleComponentManager RenderVisible stdcomponents.RenderVisibleComponentManager @@ -114,6 +115,7 @@ func NewComponentList() ComponentList { SpaceshipIntent: components.NewSpaceshipIntentComponentManager(), AsteroidSceneManager: components.NewAsteroidSceneManagerComponentManager(), SoundEffects: components.NewSoundEffectsComponentManager(), + SpatialAudio: components.NewSpatialAudioComponentManager(), TextureRect: components.NewTextureRectComponentManager(), PrimitiveCircle: components.NewTextureCircleComponentManager(), } diff --git a/examples/new-api/instances/system-list.go b/examples/new-api/instances/system-list.go index 07a827cc..39279b12 100644 --- a/examples/new-api/instances/system-list.go +++ b/examples/new-api/instances/system-list.go @@ -47,6 +47,7 @@ func NewSystemList() SystemList { Player: systems.NewPlayerSystem(), RenderBogdan: systems.NewRenderBogdanSystem(), Audio: systems.NewAudioSystem(), + AudioSettings: systems.NewAudioSettingsSystem(), SpatialAudio: systems.NewSpatialAudioSystem(), DampingSystem: systems.NewDampingSystem(), AssteroddSystem: systems.NewAssteroddSystem(), @@ -90,6 +91,7 @@ type SystemList struct { RenderBogdan systems.RenderBogdanSystem Player systems.PlayerSystem Audio systems.AudioSystem + AudioSettings systems.AudioSettingsSystem SpatialAudio systems.SpatialAudioSystem DampingSystem systems.DampingSystem AssteroddSystem systems.AssteroddSystem diff --git a/examples/new-api/systems/asterodd.go b/examples/new-api/systems/asterodd.go index ed649ff3..8bd96b93 100644 --- a/examples/new-api/systems/asterodd.go +++ b/examples/new-api/systems/asterodd.go @@ -98,6 +98,7 @@ func (s *AssteroddSystem) Init() { BoxColliders: s.BoxColliders, RigidBodies: s.RigidBodies, Renderables: s.Renderables, + SoundEffects: s.SoundEffects, }, 500, 500, 0) entities.CreateSpaceSpawner(entities.CreateSpaceSpawnerManagers{ diff --git a/examples/new-api/systems/audio.go b/examples/new-api/systems/audio.go index 70d67641..e9d97c21 100644 --- a/examples/new-api/systems/audio.go +++ b/examples/new-api/systems/audio.go @@ -16,9 +16,13 @@ package systems import ( "gomp/examples/new-api/components" + "gomp/pkg/core" "gomp/pkg/ecs" + "gomp/pkg/worker" "time" + "github.com/negrel/assert" + rl "github.com/gen2brain/raylib-go/raylib" ) @@ -27,59 +31,107 @@ func NewAudioSystem() AudioSystem { } type AudioSystem struct { - EntityManager *ecs.EntityManager - SoundEffects *components.SoundEffectsComponentManager + EntityManager *ecs.EntityManager + SoundEffects *components.SoundEffectsComponentManager + accSoundEffectsDelete [][]ecs.Entity + numWorkers int + Engine *core.Engine } func (s *AudioSystem) Init() { + s.numWorkers = s.Engine.Pool().NumWorkers() + s.accSoundEffectsDelete = make([][]ecs.Entity, s.numWorkers) rl.InitAudioDevice() } func (s *AudioSystem) Run(dt time.Duration) { - s.SoundEffects.EachEntity()(func(entity ecs.Entity) bool { + for a := range s.accSoundEffectsDelete { + s.accSoundEffectsDelete[a] = s.accSoundEffectsDelete[a][:0] + } + + s.SoundEffects.ProcessEntities(func(entity ecs.Entity, workerId worker.WorkerId) { soundEffect := s.SoundEffects.GetUnsafe(entity) + assert.NotNil(soundEffect) + clip := soundEffect.Clip - // check if clip is valid - if clip == nil || clip.FrameCount == 0 { - return true + // check if clip is loaded + if clip == nil || !rl.IsSoundValid(*clip) { + return } if !soundEffect.IsPlaying { if rl.IsSoundPlaying(*clip) { rl.StopSound(*clip) - return true + return } else { *clip = rl.LoadSoundAlias(*clip) - rl.SetSoundVolume(*clip, soundEffect.Volume) - rl.SetSoundPitch(*clip, soundEffect.Pitch) - rl.SetSoundPan(*clip, soundEffect.Pan) - rl.PlaySound(*clip) soundEffect.IsPlaying = true - return true + return } } - rl.SetSoundVolume(*clip, soundEffect.Volume) - rl.SetSoundPitch(*clip, soundEffect.Pitch) - rl.SetSoundPan(*clip, soundEffect.Pan) - // check if sound is over if !rl.IsSoundPlaying(*clip) && soundEffect.IsPlaying { if soundEffect.IsLooping { rl.PlaySound(*clip) } else { // sound is over, remove entity - s.EntityManager.Delete(entity) + s.accSoundEffectsDelete[workerId] = append(s.accSoundEffectsDelete[workerId], entity) + // rl.UnloadSoundAlias(*clip) // TODO: this doesn't work https://github.com/gen2brain/raylib-go/issues/494 } } - - return true }) + + for a := range s.accSoundEffectsDelete { + for _, entity := range s.accSoundEffectsDelete[a] { + s.EntityManager.Delete(entity) + } + } } func (s *AudioSystem) Destroy() { rl.CloseAudioDevice() } + +func NewAudioSettingsSystem() AudioSettingsSystem { + return AudioSettingsSystem{} +} + +type AudioSettingsSystem struct { + EntityManager *ecs.EntityManager + SoundEffects *components.SoundEffectsComponentManager + SpatialAudio *components.SpatialAudioComponentManager +} + +func (s *AudioSettingsSystem) Init() {} + +func (s *AudioSettingsSystem) Run(dt time.Duration) { + s.SoundEffects.ProcessEntities(func(entity ecs.Entity, workerId worker.WorkerId) { + soundEffect := s.SoundEffects.GetUnsafe(entity) + assert.NotNil(soundEffect) + + clip := soundEffect.Clip + + // check if clip is loaded + if clip == nil { + return + } + + spatialSettings := s.SpatialAudio.GetUnsafe(entity) + + if spatialSettings != nil { + rl.SetSoundVolume(*clip, spatialSettings.Volume*soundEffect.Volume) + rl.SetSoundPan(*clip, spatialSettings.Pan) + } else { + rl.SetSoundVolume(*clip, soundEffect.Volume) + rl.SetSoundPan(*clip, soundEffect.Pan) + } + + rl.SetSoundPitch(*clip, soundEffect.Pitch) + }) +} +func (s *AudioSettingsSystem) Destroy() { +} diff --git a/examples/new-api/systems/spaceship-intents.go b/examples/new-api/systems/spaceship-intents.go index fe8d22df..f13ac5ec 100644 --- a/examples/new-api/systems/spaceship-intents.go +++ b/examples/new-api/systems/spaceship-intents.go @@ -144,6 +144,16 @@ func (s *SpaceshipIntentsSystem) Run(dt time.Duration) { 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 diff --git a/examples/new-api/systems/spatial-audio.go b/examples/new-api/systems/spatial-audio.go index be65a4ea..469a7651 100644 --- a/examples/new-api/systems/spatial-audio.go +++ b/examples/new-api/systems/spatial-audio.go @@ -16,13 +16,20 @@ 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" - rl "github.com/gen2brain/raylib-go/raylib" + "github.com/negrel/assert" +) + +const ( + MinDistance = 10 ) func NewSpatialAudioSystem() SpatialAudioSystem { @@ -30,72 +37,133 @@ func NewSpatialAudioSystem() SpatialAudioSystem { } type SpatialAudioSystem struct { - EntityManager *ecs.EntityManager - SoundEffects *components.SoundEffectsComponentManager - Positions *stdcomponents.PositionComponentManager - Player *components.PlayerTagComponentManager + 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 player ecs.Entity = 0 + 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 + } - s.Player.EachEntity()(func(entity ecs.Entity) bool { - player = entity - return false + return true }) - if player == 0 { + if mainCamera == 0 { return } - playerPos := s.Positions.GetUnsafe(player) + mainCameraComponent := s.Cameras.GetUnsafe(mainCamera) + assert.NotNil(mainCameraComponent) - if playerPos == nil { - return + var mainCameraPosition vectors.Vec2 = vectors.Vec2{ + X: mainCameraComponent.Camera2D.Target.X, + Y: mainCameraComponent.Camera2D.Target.Y, } - s.SoundEffects.EachEntity()(func(entity ecs.Entity) bool { - soundEffect := s.SoundEffects.GetUnsafe(entity) - - clip := soundEffect.Clip + 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) + } + } + }) - if clip == nil { - return true + for a := range s.accSpatialAudioCreate { + for _, entity := range s.accSpatialAudioCreate[a] { + s.SpatialAudio.Create(entity, components.SpatialAudio{ + Volume: 0, + Pan: 0.5, + }) } + } - if !soundEffect.IsPlaying { - return true + 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, + ) + }) - if position == nil { - return true - } + for i := range s.accSpatialAudioCreate { + s.accSpatialAudioCreate[i] = s.accSpatialAudioCreate[i][:0] + } - pan := s.calculatePan(playerPos.XY, position.XY) - rl.SetSoundPan(*clip, pan) + for i := range s.accSpatialAudioDelete { + s.accSpatialAudioDelete[i] = s.accSpatialAudioDelete[i][:0] + } - return true - }) } func (s *SpatialAudioSystem) Destroy() { } -func (s *SpatialAudioSystem) calculatePan(listener vectors.Vec2, source vectors.Vec2) float32 { - dx := float64(source.X - listener.X) - dy := float64(source.Y - listener.Y) - distanceSq := dx*dx + dy*dy +func (s *SpatialAudioSystem) calculatePan(listener vectors.Vec2, source vectors.Vec2, maxDistance float32) float32 { + distance := listener.Distance(source) - // Если источник и слушатель в одной точке - if distanceSq < 1e-9 { // Используем квадрат расстояния для оптимизации + if distance < MinDistance { return 0.5 } - distance := math.Sqrt(distanceSq) - pan := 0.5 - (dx / (2 * distance)) + 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 +}