Ryle Radio 1.0.0
An open-source "radio" system for Unity, allowing tracks, tuning, broadcasters, and more!
Loading...
Searching...
No Matches
RadioObserver.cs
2using System;
3using System.Collections.Generic;
4using UnityEngine;
5
7{
8 /// <summary>
9 /// A component used to watch for specific happenings on a RadioOutput, e.g: a clip being a certain volume, a track starting, the tune being in a certain range\n
10 /// We don't inherit from RadioComponent here since we don't need to use a RadioData ref, but it's very similar
11 /// </summary>
12 /// <remarks>
13 /// One major thing to notice about this class is that each individual Observer component has specific tracks it's watching for- NOT each individual ObserverEvent.
14 /// This is due to limitations of MultiselectAttribute- it doesn't display in nested lists properly. This is theoretically something I can fix, but I'm not fantastic at custom editors so I'd prefer to accept this limitation for now.
15 /// With that being said, this means it *is* a bit cleaner to navigate multiple Observers in the scene- not all bad!
16 /// </remarks>
17
18 // This is partial as it's extended in ObserverEvent.cs
19 [AddComponentMenu("Ryle Radio/Radio Observer")]
20 public partial class RadioObserver : MonoBehaviour
21 {
22 /// <summary>
23 /// The event an observer is looking for.
24 ///
25 /// Except for Trigger events, we need a value(s) to check for a change with. E.g: checking if volume is above a threshold.
26 /// Trigger events wait for a certain thing to happen that doesn't need a value. E.g: checking if a track has just started playing
27 /// </summary>
28 public enum EventType
29 {
30 OutputVolume, ///< The volume of the track: tune power * broadcast power * insulation
31 Gain, ///< The gain of the track: this is currently defined exclusively in the track's gain variable
32 TunePower, ///< The tune power of the track: how close the RadioOutput.Tune value is to the range of the track
33 BroadcastPower, ///< The broadcast power of the track: how close to any active RadioBroadcasters the output's transform is
34 Insulation, ///< The insulation of the track: the higher the value the less insulation- the power of any RadioInsulator the output is inside of
35 TrackEnds, ///< The track ends, or loops- this is a Trigger event
36 TrackStarts, ///< The track starts, or loops (happens after TrackEnds)- this is a Trigger event
37 OutputTune, ///< The tune on the RadioOutput is changed
38
39 None, ///< Empty, mainly to temporarily disable an event without deleting it
40 }
41
42 /// <summary>
43 /// The method of comparisonType used for an event. For example, checking if the volume is greater than a number, or in a certain range
44 /// </summary>
45 public enum ComparisonType
46 {
47 Equal, /// The value is equal to a number <remarks>We're using floats for almost every EventType with a value, so this won't be used often</remarks>
48 GreaterThan, /// The value is greater than a number
49 GreaterThanOrEqual, /// The value is greater than or equal to a number
50 LessThan, /// The value is less than a number
51 LessThanOrEqual, /// The value is less than or equal to a number
52 BetweenInclusive, /// The value is between numbers x and y, including if it's equal to x or y
53 BetweenExclusive, /// The value between numbers x and y, but not equal to x or y
54 }
55
56 /// <summary>
57 /// The RadioOutput this observer is attached to
58 /// </summary>
59 [SerializeField] private RadioOutput output;
60
61 /// <summary>
62 /// The tracks that this observer is watching for events on. This is a flag int and is translated to a list of names in \ref AffectedTracks
63 /// </summary>
64 [SerializeField, Multiselect("TrackNames")]
65 private int affectedTracks;
66
67 /// <summary>
68 /// The events that this Observer responds uses, containing values/triggers to watch for and events to call
69 /// </summary>
70 [SerializeField] private List<ObserverEvent> events;
71
72 /// <summary>
73 /// A buffer for events to run on Update.
74 /// We cannot call \ref UnityEvents
75 /// in the audio thread, so we need a buffer here so we can run them in \ref Update instead.
76 ///
77 /// <b>See also: </b>\ref stayedEvents
78 /// </summary>
79 private List<Action> toDoOnUpdate = new();
80
81 /// <summary>
82 /// A tracker for which ObserverEvents have been called for this specific frame- prevents us from calling an OnStay event hundreds of times in a frame due to the audio thread being WAY faster
83 ///
84 /// <b>See also:</b> \ref toDoOnUpdate
85 /// </summary>
86 private List<ObserverEvent> stayedEvents = new();
87
88#if !SKIP_IN_DOXYGEN
89 // The names that the MultiselectAttribute on affectedTracks can use
90 private List<string> TrackNames => output != null ? output.Data.TrackNames : new() { "Output not assigned!" };
91
92#endif
93
94 /// <summary>
95 /// \ref affectedTracks as a list of names rather than a flag int. Created and cached in \ref AffectedTracks
96 /// </summary>
97 private string[] affectedTrackNames;
98
99 /// <summary>
100 /// The tracks selected on this Observer.
101 ///
102 /// This is an accessor for \ref affectedTrackNames and \ref affectedTracks - uses MultiselectAttribute.To to automatically convert the flag int to a string array
103 /// </summary>
104 public string[] AffectedTracks
105 {
106 get
107 {
108 if (affectedTrackNames == null) // the work is done for us in the MultiselectAttribute. thank you multiselectattribute i love you multiselectattribute
109 affectedTrackNames = MultiselectAttribute.To<string>(affectedTracks, TrackNames.ToArray());
110
111 return affectedTrackNames;
112 }
113 }
114
115
116 /// <summary>
117 /// Attaches the Observer to \ref output
118 /// </summary>
119 private void Awake()
120 {
121 // attach this observer to the output
122 output.Observers.Add(this);
123 }
124
125 /// <summary>
126 /// Detaches this Observer from \ref output
127 /// </summary>
128 private void OnDestroy()
129 {
130 output.Observers.Remove(this);
131 }
132
133 /// <summary>
134 /// Run the buffered events and reset for the next frame
135 /// </summary>
136 private void Update()
137 {
138 // time to cache the update queue
139 // we need to do this as toDoOnUpdate could be changed on a different thread while we proceed through it, causing errors
140 List<Action> locUpdate;
141
142 // lock the update queue to clone it uninterrupted
143 lock (toDoOnUpdate)
144 {
145 locUpdate = new(toDoOnUpdate);
146 }
147
148 // move through the update queue to call the associated events
149 foreach (Action a in locUpdate)
150 {
151 a();
152
153 // remove this event from the update queue
154 lock (toDoOnUpdate)
155 toDoOnUpdate.Remove(a);
156 }
157
158 // clear stay events from last frame so they can be called again this frame
159 stayedEvents.Clear();
160 }
161
162 /// <summary>
163 /// Assigns each event to a RadioTrackPlayer for one of our \ref AffectedTracks\. This is called when a new RadioTrackPlayer is created that's playing a Track in \ref AffectedTracks.
164 /// </summary>
165 /// <param name="_player">A RadioTrackPlayer playing one of our \ref AffectedTracks</param>
166 public void AssignEvents(RadioTrackPlayer _player)
167 {
168 foreach (ObserverEvent e in events)
169 {
170 // if the OnStay event for this ObserverEvent has already been called this frame, don't call it again
171 // if we didn't have this, it would be calling the OnStay events hundreds of times per frame
172 if (stayedEvents.Contains(e))
173 break;
174 else
175 stayedEvents.Add(e);
176
177 switch (e.eventType)
178 {
179 // the _player events are mostly very similar- they give us the player it's called on, and the value
180 // we don't do anything with the player info in this script, but it's there for custom behaviour
181 //
182 // each sample, we check if a copy of this event has been called yet. If it has, we don't add another one, as it would hugely slow down at runtime
183 case EventType.OutputVolume:
184 _player.OnVolume += (player, volume) => { ValueEvent(e, volume); };
185 break;
186
187 case EventType.Gain:
188 _player.OnGain -= (player, gain) => { ValueEvent(e, gain); };
189 break;
190
191 case EventType.TunePower:
192 _player.OnTunePower += (player, tunePower) => { ValueEvent(e, tunePower); };
193 break;
194
195 case EventType.BroadcastPower:
196 _player.OnBroadcastPower += (player, power) => { ValueEvent(e, power); };
197 break;
198
199 case EventType.Insulation:
200 _player.OnInsulation += (player, insulation) => { ValueEvent(e, insulation); };
201 break;
202
203 // this is a single event rather than a value- there is no value associated with the track starting or ending, it just starts or ends
204 case EventType.TrackStarts:
205 _player.OnPlay += (player) => { TriggerEvent(e); };
206 break;
207
208 case EventType.TrackEnds:
209 _player.OnEnd += (player) => { TriggerEvent(e); };
210 break;
211
212 // this event watches the tune of the output- it doesn't actually use a player here
213 case EventType.OutputTune:
214 output.OnTune += (tune) => { ValueEvent(e, tune); };
215 break;
216
217 // an empty event gets no behaviour - pretty sure gandhi said this at one point idk man
218 default:
219 case EventType.None:
220 break;
221 }
222 }
223
224 }
225
226 /// <summary>
227 /// A generic method letting an ObserverEvent tracking a value watch for its change.
228 ///
229 /// This is what's called every time a Track's observed value is changed. If the new value fulfills the given ObserverEvent, it'll be called. E.g: volume is in given range- call event
230 ///
231 /// <b>See also: \ref TriggerEvent()</b>
232 /// </summary>
233 /// <param name="_event">Contains the change we're watching for, and the event to call when the change happens</param>
234 /// <param name="_value">The observed value right now</param>
235 private void ValueEvent(ObserverEvent _event, float _value)
236 {
237 // if this event doesn't use a comparisonType, this method should not have been called- tell the user
238 if (!_event.NeedComparison)
239 {
240 Debug.LogWarning($"Attempting to use the FloatEvent method on trigger event {_event}! To fix, either modify the NeedComparison property in ObserverEvent, or change the method used in AssignEvents to TriggerEvent.");
241 return;
242 }
243
244 // evaluate the comparisonType
245 if (_event.EvaluateComparison(_value))
246 {
247 // if it passes,
248
249 // and this is the first time the event is called,
250 if (!_event.staying)
251 {
252 _event.staying = true;
253
254 // call the onTrigger event
255 lock (toDoOnUpdate)
256 {
257 toDoOnUpdate.Add(() => _event.onTrigger.Invoke(_value));
258 }
259 }
260 // and this is not the first time the event is called,
261 else
262 {
263 // call the onStay event
264 lock (toDoOnUpdate)
265 {
266 toDoOnUpdate.Add(() => _event.onStay.Invoke(_value));
267 }
268 }
269 }
270 else
271 {
272 // if it doesn't pass,
273
274 // and the event has just been called,
275 if (_event.staying)
276 {
277 _event.staying = false;
278
279 // call the onEnd event
280 lock (toDoOnUpdate)
281 {
282 toDoOnUpdate.Add(() => _event.onEnd.Invoke(_value));
283 }
284 }
285 }
286 }
287
288 /// <summary>
289 /// A generic method tracking if an ObserverEvent's trigger has been called. E.g: a track has just started playing, call event
290 ///
291 /// <b>See also: </b> \ref ValueEvent()
292 /// </summary>
293 /// <param name="_event">Contains the trigger we're watching for, and the event to call when it's triggered</param>
294 private void TriggerEvent(ObserverEvent _event)
295 {
296 // if the event is triggered, call the onTrigger event- the other events are not applicable
297 lock (toDoOnUpdate)
298 toDoOnUpdate.Add(() => _event.onTrigger.Invoke(1));
299 }
300 }
301
302}
A singular event inside a RadioObserver, tracking a value or trigger and invoking methods accordingly...
UnityEvent< float > onTrigger
Called when eventType is fulfilled for the first time. Only called once until onEnd is called.
UnityEvent< float > onStay
Called every frame while eventType is fulfilled, including the first. Cannot be called if eventType i...
bool NeedComparison
Does this event need a value or range to compare to?
EventType eventType
The type of event this is looking for.
bool EvaluateComparison(float _cValue)
If this event uses a comparison, check if its fulfilled.
bool staying
Tracks if this event has been called in this or the previous frame.
UnityEvent< float > onEnd
Called when eventType is no longer fulfilled. Only called once until the event is fulfilled again....
A component used to watch for specific happenings on a RadioOutput, e.g: a clip being a certain volum...
List< ObserverEvent > events
The events that this Observer responds uses, containing values/triggers to watch for and events to ca...
void TriggerEvent(ObserverEvent _event)
A generic method tracking if an ObserverEvent's trigger has been called. E.g: a track has just starte...
RadioOutput output
The RadioOutput this observer is attached to.
string[] affectedTrackNames
affectedTracks as a list of names rather than a flag int. Created and cached in AffectedTracks
int affectedTracks
The tracks that this observer is watching for events on. This is a flag int and is translated to a li...
void OnDestroy()
Detaches this Observer from output.
string[] AffectedTracks
The tracks selected on this Observer.
EventType
The event an observer is looking for.
@ TunePower
The tune power of the track: how close the RadioOutput.Tune value is to the range of the track.
@ Gain
The gain of the track: this is currently defined exclusively in the track's gain variable.
@ OutputVolume
The volume of the track: tune power * broadcast power * insulation.
@ None
Empty, mainly to temporarily disable an event without deleting it.
@ TrackEnds
The track ends, or loops- this is a Trigger event.
@ OutputTune
The tune on the RadioOutput is changed.
@ BroadcastPower
The broadcast power of the track: how close to any active RadioBroadcasters the output's transform is...
@ Insulation
The insulation of the track: the higher the value the less insulation- the power of any RadioInsulato...
@ TrackStarts
The track starts, or loops (happens after TrackEnds)- this is a Trigger event.
void AssignEvents(RadioTrackPlayer _player)
Assigns each event to a RadioTrackPlayer for one of our AffectedTracks. This is called when a new Rad...
void Awake()
Attaches the Observer to output.
void ValueEvent(ObserverEvent _event, float _value)
A generic method letting an ObserverEvent tracking a value watch for its change.
ComparisonType
The method of comparisonType used for an event. For example, checking if the volume is greater than a...
@ GreaterThanOrEqual
The value is greater than a number.
@ BetweenExclusive
The value is between numbers x and y, including if it's equal to x or y.
@ LessThanOrEqual
The value is less than a number.
@ LessThan
The value is greater than or equal to a number.
@ BetweenInclusive
The value is less than or equal to a number.
@ GreaterThan
The value is equal to a number.
void Update()
Run the buffered events and reset for the next frame.
List< Action > toDoOnUpdate
A buffer for events to run on Update. We cannot call UnityEvents in the audio thread,...
List< ObserverEvent > stayedEvents
A tracker for which ObserverEvents have been called for this specific frame- prevents us from calling...
The main scene component for a radio that plays it through an AudioSource.
A custom attribute that allows ints to display as a multiselect dropdown for a given collection,...
A class that plays a certain RadioTrack at runtime. It's created newly for each track on each RadioOu...
Components to be placed on scene objects, e.g: Outputs, Broadcasters, Observers.
Tracks to be used on a radio- includes base classes.
Definition RadioUtils.cs:20