Metaverse Music
Overview
The Music scene allows the player to test his DJ skills with pads allowing him to trigger sounds and music, as well as to control light shows.
This demonstrates how to synchronize an audio track or a light through the network.
The scene includes several DJ pads. Some controls music and another one controls the lights.
Note that in desktop mode, the zoom icon at the bottom of each pad allows to display it in fullscreen in order to better control the UI with the mouse.
Music pads
Each music pad is composed of one or more buttons. To each button corresponds an AudioSource and a sound. The sound can be set as a loop.
A music pad contains a slider to change the volume.
A DJ pad behavior is managed by 3 classes:
DJPadVolumeSlider
: manages the volume of the padDJPadTouch
: manages the sound buttonsDJPadManager
: manages the overall function of the pad
DJPadVolumeSlider
When the player changes the volume by touching the slider, the DJPadVolumeSlider
calls the DJPadManager ChangeVolume
methods.
C#
void RequestVolumeChange(float volume)
{
padManager.ChangeVolume(volume);
}
public async void ChangeVolume(float volume)
{
// We use an attribute, so if another volume is requested while taking the authority, the last volume request is the one executed
lastVolumeRequest = volume;
if (!Object.HasStateAuthority)
{
await Object.WaitForStateAuthority();
}
MasterVolume = lastVolumeRequest;
}
The networked variable MasterVolume
in DJPadManager
is then synchronized over the network.
A ChangeDetector
is used to detect MasterVolume
variable modification in Render()
loop.
C#
[Networked]
public float MasterVolume { get; set; } = 1;
ChangeDetector changeDetector;
public override void Render()
{
base.Render();
foreach (var changedVar in changeDetector.DetectChanges(this))
{
if (changedVar == nameof(MasterVolume))
{
OnMasterVolumeChanged();
}
}
}
void OnMasterVolumeChanged()
{
if(volumeManager != null) volumeManager.OnVolumeChanged(this, MasterVolume);
}
So that everyone can update the local volume.
C#
public void OnVolumeChanged(DJPadManager bPMClipsPlayer, float volume)
{
ChangeSliderValue(volume);
}
DJPadTouch
Each button references an index, so that the DJPadManager
knows which button controls each audio source, and knows which buttons should be notified when an audio source state changed.
So when the user touchs a button, it calls the UpdatePadStatus()
method of DJPadTouch
.
Then, UpdatePadStatus()
notifies the DJPadManager
of the new status.
C#
public void UpdatePadStatus()
{
if (audioSource.clip)
{
isPlaying = !isPlaying;
padManager.ChangeAudioSourceState(this, isPlaying);
}
}
Also, when a button has been touched, the DJPadManager
calls the OnAudioSourceStatusChanged
method of the DJPadTouch
in order to ask it to update the button colors according to the new status.
DJPadManager
The status of each button is synchronized thanks to a network dictionnary called PadsStatus
.
C#
[Networked]
Capacity(50)]
public NetworkDictionary<int, NetworkBool> PadsStatus { get; }
At start, the DJPadManager
saves all buttons in a audioSourceManagers
dictionnary (all DJPadTouch
implements the IAudioSourceManager
interface).
C#
foreach (var manager in GetComponentsInChildren<IAudioSourceManager>(true))
{
audioSourceManagers[manager.AudioSourceIndex] = manager;
}
When the DJPadManager
receives a new button status with ChangeAudioSourceState()
, it requests for the StateAuthority if it doesn't have it yet and then update the networked dictionnary so that all remote users will received the update.
C#
public async void ChangeAudioSourceState(IAudioSourceManager audioSourceManager, bool isPlaying)
{
if (!Object.HasStateAuthority)
{
await Object.WaitForStateAuthority();
}
PadsStatus.Set(audioSourceManager.AudioSourceIndex, isPlaying);
}
The RefreshPads()
, SyncAudioSource()
& OnAudioStatusChanged()
methods are in charge to :
- update the audiosource of each button (
DJPadTouch
) according to the volume slider value - update the pad networked dictionnary if a button status has changed (an audio clip has finished for example)
- synchronized the audioSource (play/stop) of each button according to the networked dictionnary
- inform a button that its status changed in order to update the button colors
Instead of doing these updates as soon as the networked dictionary is changed, they are performed regulary based on the predefined BPM parameter
(method AudioLoop()
).
Light pad
The ligh pad controls 4 lights.
For each light, the pad allows the user to :
- switch on/off the light
- switch on/off the movement of the light
- change light intensity
The light pad architecture is very similar to the music pad.
The lighting is managed by the following classes:
LightPadManager
: manages the overall function of the padLightPadTouch
: manages the light buttonsLightSystem
: manages the light stateLightIntensitySlider
: manages the light intensityLightDirectionControler
: manages the light rotator
LightIntensitySlider
The LightIntensitySlider
is very similar to the DJPadVolumeSlider
.
When the player changes the intensity by touching the slider, the RequestIntensityChange
calls the LightPadManager ChangeIntensity()
method.
In the other way, the OnLightStatusChanged
method is called by the LightPadManager
in order to update the slider postion if a remote player changed the intensity.
LightPadTouch
LightPadTouch
manages light buttons.
As defined in the LightPadManager
, there are 3 kinds of light buttons :
C#
public enum LightManagerType
{
OnOff, // switch on/off the light
Movement, // switch on/off the movement
Intensity // slider to change the intensity
}
Each button references a light index, so that the LightPadManager
knows which button controls each light, and knows which buttons should be notified when a light state changed.
UpdatePadStatus()
is called when the player touch a button. It informes the PadManager
that the state must be changed.
OnLightStatusChanged()
is called by the PadManager
when the status changed so the button UI must be updated.
LightSystem
The LightSystem
class inherits from the abstract class EffectSystem
.
LightSystem
is in charge to change the light state according to the parameters received in ChangeState
method :
LightDirectionControler
LightDirectionControler
class controls the rotator script which rotates the light game object.
LightDirectionControler
is enabled/disabled by the LightSystem.
LightPadManager
LightPadManager
references all light objects (LightInfo
).
Each LightInfo
has an index and an effect system that can modify the light parameters.
C#
public struct LightInfo
{
public int lightIndex;
public EffectSystem effectSystem;
}
At start, LightPadManager
registers all light buttons into a dictionnary.
Also, there are 3 networked dictionnaries to save the various parameters of the lights (light on/off, movement on/off, light intensity)
C#
[Networked]
[Capacity(MAX_LIGHTS)]
public NetworkDictionary<int, NetworkBool> LightStatus { get; }
[Networked]
[Capacity(MAX_LIGHTS)]
public NetworkDictionary<int, NetworkBool> LightMovementStatus { get; }
[Networked]
[Capacity(MAX_LIGHTS)]
public NetworkDictionary<int, float> LightIntensities { get; }
ChangeDetector changeDetector;
When the local player push a button, the LightPadManager
is informed by the associated method (ChangeLightState()/ChangeMovementState()/ChangeIntensity()
) .
Then the associated networked dictionnary is updated.
For example, when a light is switch on/off, LightPadManager
requests for the StateAuthority
if it doesn't have it yet and then update the networked dictionnary so that all remote users will received the update.
C#
public async void ChangeLightState(LightPadTouch lightPadTouch, bool isLightOn)
{
if (!Object.HasStateAuthority)
{
await Object.WaitForStateAuthority();
}
// update network status
LightStatus.Set(lightPadTouch.LightIndex, isLightOn);
// movement must be stopped if the light is off
if (!isLightOn)
LightMovementStatus.Set(lightPadTouch.LightIndex, false);
}
As soon as a dictionnary is updated, the Refresh()
methods is called on local & remote players thanks to the ChangeDetector
.
C#
public override void Render()
{
base.Render();
foreach (var changedVar in changeDetector.DetectChanges(this))
{
if (changedVar == nameof(LightStatus))
{
Refresh();
}
if (changedVar == nameof(LightMovementStatus))
{
Refresh();
}
if (changedVar == nameof(LightIntensities))
{
Refresh();
}
}
}
Refresh()
is in charge to update all lights state and associated buttons using the UpdateLightsAndButtons()
method
When a player joins, its lights are updated with the networked dictionnaries.
Back to top