From 9e6d3fbbc0eb57744df8c6c86aee15221f4b8eea Mon Sep 17 00:00:00 2001 From: ICOM725 <113233781+ICOM725@users.noreply.github.com> Date: Mon, 7 Sep 2026 20:11:00 +0800 Subject: [PATCH] Support the z coordinate when following spatial audio sources --- src/sound/typedefs/SpatialSoundConfig.js | 2 +- src/sound/webaudio/WebAudioSound.js | 7 +- tests/sound/webaudio/WebAudioSound.test.js | 77 ++++++++++++++++++++++ 3 files changed, 84 insertions(+), 2 deletions(-) diff --git a/src/sound/typedefs/SpatialSoundConfig.js b/src/sound/typedefs/SpatialSoundConfig.js index e8e7957dab..a23f823606 100644 --- a/src/sound/typedefs/SpatialSoundConfig.js +++ b/src/sound/typedefs/SpatialSoundConfig.js @@ -20,5 +20,5 @@ * @property {number} [coneInnerAngle=360] - The angle, in degrees, of a cone inside of which there will be no volume reduction. * @property {number} [coneOuterAngle=0] - The angle, in degrees, of a cone outside of which the volume will be reduced by a constant value, defined by the `coneOuterGain` property. * @property {number} [coneOuterGain=0] - The amount of volume reduction outside the cone defined by the `coneOuterAngle` attribute. Its default value is 0, meaning that no sound can be heard. A value between 0 and 1. - * @property {Phaser.Types.Math.Vector2Like} [follow] - Set this Sound object to automatically track the x/y position of this object. Can be a Phaser Game Object, Vec2 or anything that exposes public x/y properties. + * @property {Phaser.Types.Math.Vector3Like} [follow] - Set this Sound object to automatically track the x/y/z position of this object. Can be a Phaser Game Object, Vec2, Vec3 or anything that exposes public position properties. Coordinates not provided by the object are left unchanged. */ diff --git a/src/sound/webaudio/WebAudioSound.js b/src/sound/webaudio/WebAudioSound.js index de7e6d9cc4..c8b9b62ed3 100644 --- a/src/sound/webaudio/WebAudioSound.js +++ b/src/sound/webaudio/WebAudioSound.js @@ -123,7 +123,7 @@ var WebAudioSound = new Class({ * Game Object, this retains a reference to it. * * @name Phaser.Sound.WebAudioSound#spatialSource - * @type {?Phaser.Types.Math.Vector2Like} + * @type {?Phaser.Types.Math.Vector3Like} * @since 3.60.0 */ this.spatialSource = null; @@ -626,6 +626,7 @@ var WebAudioSound = new Class({ { var x = GetFastValue(this.spatialSource, 'x', null); var y = GetFastValue(this.spatialSource, 'y', null); + var z = GetFastValue(this.spatialSource, 'z', null); if (x && x !== this._spatialx) { @@ -635,6 +636,10 @@ var WebAudioSound = new Class({ { this._spatialy = this.spatialNode.positionY.value = y; } + if (z !== null && z !== this._spatialz) + { + this._spatialz = this.spatialNode.positionZ.value = z; + } } if (this.hasEnded) diff --git a/tests/sound/webaudio/WebAudioSound.test.js b/tests/sound/webaudio/WebAudioSound.test.js index 071911ee73..b0340915bd 100644 --- a/tests/sound/webaudio/WebAudioSound.test.js +++ b/tests/sound/webaudio/WebAudioSound.test.js @@ -575,6 +575,83 @@ describe('WebAudioSound', function () describe('update', function () { + it('should follow changes to all three spatial coordinates', function () + { + var target = { x: 10, y: 20, z: 30 }; + + sound.play({ source: { follow: target } }); + sound.update(); + + expect(sound.spatialNode.positionX.value).toBe(10); + expect(sound.spatialNode.positionY.value).toBe(20); + expect(sound.spatialNode.positionZ.value).toBe(30); + + target.z = -15; + sound.update(); + + expect(sound.spatialNode.positionZ.value).toBe(-15); + }); + + it('should follow a z coordinate of zero', function () + { + sound.spatialNode.positionZ.value = 30; + sound.play({ source: { follow: { x: 10, y: 20, z: 0 } } }); + sound.update(); + + expect(sound.spatialNode.positionZ.value).toBe(0); + }); + + it('should preserve the z position when following a two-dimensional object', function () + { + sound.spatialNode.positionZ.value = 30; + sound.play({ source: { follow: { x: 10, y: 20 } } }); + sound.update(); + + expect(sound.spatialNode.positionX.value).toBe(10); + expect(sound.spatialNode.positionY.value).toBe(20); + expect(sound.spatialNode.positionZ.value).toBe(30); + }); + + it('should preserve the z position when the target z is undefined', function () + { + sound.spatialNode.positionZ.value = 30; + sound.play({ source: { follow: { x: 10, y: 20, z: undefined } } }); + sound.update(); + + expect(sound.spatialNode.positionZ.value).toBe(30); + }); + + it('should not update the spatial position while paused', function () + { + sound.spatialNode.positionZ.value = 30; + sound.play({ source: { follow: { z: 10 } } }); + sound.pause(); + sound.update(); + + expect(sound.spatialNode.positionZ.value).toBe(30); + }); + + it('should only write the z position when it changes', function () + { + var positionZ = 0; + var setPositionZ = vi.fn(function (value) + { + positionZ = value; + }); + + Object.defineProperty(sound.spatialNode.positionZ, 'value', { + get: function () { return positionZ; }, + set: setPositionZ + }); + + sound.play({ source: { follow: { z: 10 } } }); + sound.update(); + sound.update(); + + expect(setPositionZ).toHaveBeenCalledTimes(1); + expect(setPositionZ).toHaveBeenCalledWith(10); + }); + it('should emit COMPLETE and reset state when hasEnded is true', function () { var completeFired = false;