BASeBlock – Music Manipulation Lessons
As with most games, BASeBlock has music. Originally, I implemented a naive approach to have “Multiple music” playing; a simple stack. The multiple music idea is sort of like
how games might change the music to a boss music when a boss appears, and change it back when they die, or how a certain powerup might change the music while you have it.
This implementation sat for a while. it used a stack based approach- the Sound manager had PushMusic and PopMusic methods.
However, several critical flaws in this approach became clear after I added an invulnerability powerup. Everything seemed to work fine, (get the powerup, you’re invulnerable and while you are there is different music) however, the problem became clear when I, while still invulnerable, released a boss. The boss music would start playing; however, while the boss was alive, the invincible power would run out; it would “PopMusic” which would revert the music from the boss music to the starman music, and then only when the boss died would it go back to normal. This is obviously not intended. The ideal case would be:
- Player starts level. Level Music is playing.
- Player gets invulnerability powerup; invincible powerup music plays.
- While invincible, the player, or something causes a boss to spawn.
- when the boss is spawned, the invulnerable music can continue until the power runs out; at which point it plays the boss music, or, the boss music can replace the invincible music. The former is probably overall a better idea.
- Either way: the music must fit. Invincible music should only play while the player has said powerup; and boss music should only play when there is a boss.
Obviously, my approach failed miserably; it worked fine, but I had only had a single “active” piece of music at a time; how do you manage multiples?
After some thought, I considered the idea of “reference counting” or keeping track of how many times a given piece of music was requested to play. a boss spawning would increment the boss music by one, a second one with the same music would make it two; each time this happens, the sound manager could re-evaluate which piece of music to play based on finding the maximum reference count.
With this idea, I rearchitected some of the code within the SoundManager. The SoundManager (technically cNewSoundManager, since it was a rewrite of a strongly coupled version I had before) is essentially a class that, well, manages sound and music. I have a interface class that allows for different actual implementations of the details of playing sound (“Driver” classes, of you will) The Manager class itself merely deals with the details based on that basic functionality, which exposes a few critical events, such as music stopping and whatnot. The original “PushMusic” and “PopMusic” stack based approach used a small data class, shown here:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
private class ActiveMusicData { public string Name; public iActiveSoundObject ActiveSound{get;set;} public iSoundSourceObject Source { get; set; } public ActiveMusicData(String pName,iActiveSoundObject pActiveSound, iSoundSourceObject pSource) { ActiveSound = pActiveSound; Source = pSource; Name = pName; } } |
A minor explanation may be necessary; iActiveSoundObject is an interface class that is implemented by the “driver”; same for iSoundSourceObject; the details of how they work isn’t important, just that their interface methods do what the interface definition says. A Active Sound object is something that is “active” usually, this means it is playing, but it could also be paused. A Sound Source object can be used to “spawn” Active Sound Objects; in order to actually play music or sound, a iActiveSoundSource object is required. Rather than discard this class I extended from it. Arguably, I could have simply changed the actual class itself but that could always be done later:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
private class TemporaryMusicData:ActiveMusicData,IComparable<TemporaryMusicData> { public TemporaryMusicData(String pName, iActiveSoundObject pActiveSound, iSoundSourceObject pSource):base(pName,pActiveSound,pSource) { } public int Occurences; //reference count; we add one to this when a "temporary" music is played. and subtract one when it is "stopped". //we play the music with the highest "reference count"; items are removed when their "reference count" is zero. public int CompareTo(TemporaryMusicData other) { return Occurences.CompareTo(other.Occurences); } } |
Again, another private class. The Implementation of IComparable
Dictionary<String,TemporaryMusicData> to be precise; This indexes the TemporaryMusicData instances by Name (Key); the Name/Key is used by the sound Manager to index Sound sources, so getting the appropriate source is easy given a name, and it’s guaranteed to be unique since the listing is taken from the file system itself, and the loading routine has other considerations to prevent duplicate entries (and error handling for duplicate key Exceptions if they do occur). The Occurences field is basically the entire purpose here; when “Temporary” music is told to play, it merely increments the field for the appropriate entry at the Named Index; then both the Stop and Play routines will call another routine that Ensures that the item with the maximum occurences is playing. The implementation for the relevant routines:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 |
private String OriginalSoundName; //allocated when TemporaryMusicData is empty when music is pushed. private iSoundSourceObject OriginalSoundObject = null; //private SortedList<TemporaryMusicData, TemporaryMusicData> TempMusicData = new SortedList<TemporaryMusicData, TemporaryMusicData>(); private Dictionary<String, TemporaryMusicData> TempMusicData = new Dictionary<string, TemporaryMusicData>(); /// <summary> /// stops music played with the PlayTemporaryMusic function. /// </summary> /// <param name="MusicName"></param> public iActiveSoundObject StopTemporaryMusic(String MusicName) { MusicName = MusicName.ToUpper(); if (!TempMusicData.Any()) return mPlayingMusic; //no elements in Temporary Music Dictionary, so nothing to stop. TemporaryMusicData incrementData = getTemporaryMusicData(MusicName); incrementData.Occurences--; //add one to occurences. if (incrementData.Occurences == 0) TempMusicData.Remove(MusicName); return PlayMax(1.0f, true); } /// <summary> /// Plays "temporary" music; for example the music from a power up. /// </summary> /// <param name="MusicName"></param> public iActiveSoundObject PlayTemporaryMusic(String MusicName,float volume, bool loop) { MusicName = MusicName.ToUpper(); //the idea is simple, we want to allow for the following: //Player starts game. Music playing is standard level music or something. //player get's a power up, which has it's own music. this powerup is timed. //player causes something else to have different music; for example, they could spawn a boss or something. //when the powerup runs out, the music will revert to the normal game music; and when the boss is killed, it will go back to the //music that was playing from the powerup. //This attempts to mitigate this behaviour. //step one: if the list is empty... if (!TempMusicData.Any()) { //empty list. Initialize OriginalSoundName... OriginalSoundObject = mPlayingMusicSource; OriginalSoundName = scurrentPlayingMusic; } //step two: is there an element in the sorted list for MusicName? TemporaryMusicData incrementData = getTemporaryMusicData(MusicName); incrementData.Occurences++; //add one to occurences. return PlayMax(volume, loop); } private TemporaryMusicData getTemporaryMusicData(string MusicName) { MusicName=MusicName.ToUpper(); TemporaryMusicData incrementData; if (TempMusicData.ContainsKey(MusicName)) { incrementData = TempMusicData[MusicName]; } else { incrementData = new TemporaryMusicData(MusicName, null, mSoundSources[MusicName]); TempMusicData.Add(MusicName, incrementData); } return incrementData; } private iActiveSoundObject PlayMax(float volume, bool loop) { //now, find the one with the maximum occurences. if (!TempMusicData.Any()) { mPlayingMusic.Stop(); //play default. scurrentPlayingMusic = OriginalSoundName; mPlayingMusic = OriginalSoundObject.Play(loop,volume); return mPlayingMusic; } int currentmax = int.MinValue; TemporaryMusicData tmduse = null; foreach (var iterate in TempMusicData) { if (iterate.Value.Occurences > currentmax) { currentmax = iterate.Value.Occurences; tmduse = iterate.Value; } } if (scurrentPlayingMusic != tmduse.Name) { mPlayingMusic.Stop(); scurrentPlayingMusic = tmduse.Name; mPlayingMusic = tmduse.Source.Play(loop, volume); return mPlayingMusic; } return mPlayingMusic; } |
So far, this has worked well.
However, more recently I found that I also need the same sort of “reference count” management for other things related to powerups, such as the “DrawAttributes” of various objects. But it would be foolish to clutter up that code with this sort of thing. Surely there is some way that I can add the feature with little to no changes to existing code? Turns out, that leveraging a few C# features, this is relatively easily accomplished.
Consider the Nullable<T> class. Any struct or value type can be made “Nullable” using it; there is even a shortcut in the language syntax for type definitions to use it, by appending a question mark, (Nullable
Enter ReferenceCounted<T>
ReferenceCounted<T> is the name of the class that I created (or, as I write this, am creating) for this purpose. My original idea was to use implicit cast operators to make it a simple type change; assignments to the object of the “old type” (type T) would “automatically” be added to the reference list; going the other way, the ReferenceCounted<T> Type would be implicitly cast to T by way of taking the T value it currently has with the highest reference Count. This hit a snag, however; the second cast, thankfully, would work fine, but the first would not have the proper information; the cast operator is a static routine and wouldn’t have access to the ReferenceCounted<T> Object that is being assigned.
somewhat miffed but not surprised (it would be silly to provide for overloading of the assignment operator, but in this case I wish there was an exception), I didn’t give up; I just thought about it a little. And it hit me- I don’t need to overload the assignment operator to overload assignment; I could overload the addition operator and implement the “assignment” code there; this is what the Event classes do for event hooking; and I could use -= to remove “references”. Arguably, this would take more code and wouldn’t be quite as clean as I was hoping, but for the most part the actual reference counting logic would be out of the way, handled mostly by the implicit cast to T.
After some effort… it was made. Here is the source code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 |
using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing.Imaging; using System.Linq; using System.Text; namespace System.Collections.Generic { /// <summary> /// Implements a reference counted list of T, allowing for implicit conversion to type T. /// A "Reference counted list" keeps track of both the items in the list as well as a reference count for each. /// When an item is added, it's reference count is incremented. If the item already exists, the existing item has it's reference count incremented. /// Equality is determined by default using the Equals Method, but a predicate can be assigned. /// </summary> /// <typeparam name="T"></typeparam> class ReferenceCounted<T> { private T lastitem = default(T); private Dictionary<T, int> ReferenceDictionary= new Dictionary<T, int>(); /// <summary> /// Returns the Dictionary holding the Elements. /// </summary> /// <returns></returns> public Dictionary<T, int> getReferenceDictionary() { return ReferenceDictionary; } /// <summary> /// creates a new ReferenceCounted List Object. /// </summary> public ReferenceCounted() { } /// <summary> /// creates a new ReferenceCounted List Object, with the given IEqualityComparer as the comparison predicate. /// </summary> /// <param name="compareobject"></param> public ReferenceCounted(IEqualityComparer<T> compareobject) { //initialize Dictionary with given IEqualityComparer interface. ReferenceDictionary = new Dictionary<T, int>(compareobject); } /// <summary> /// adds the given element to this list; which will either add an nonexistent item to the list and set it's reference count to 1 or /// increment the value of the existing element for the given value, as determined by any IEqualityComparer predicate given in the constructor. /// </summary> /// <param name="value">Value to add or increment in the list.</param> public void AddElement(T value) { //adds an element to this ReferenceCounted object. //remove the previous item... or dereference it, rather. if (!ReferenceDictionary.Comparer.Equals(lastitem, default(T))) { RemoveElement(lastitem); lastitem = default(T); } //first, does the given item exist in our dictionary already? if (!ReferenceDictionary.ContainsKey(value)) { //if not, we add it. Add it with a value 0, since it will be incremented after this if. ReferenceDictionary.Add(value, 0); } //due to the above condition we know the element exists; increment it's reference count. ReferenceDictionary[value]++; _Dirty = true; } /// <summary> /// "removes" the given element from this list. This is accomplished by decrementing the appropriate keyed item in the dictionary. /// If the element is not in this dictionary, this method has no effect. /// </summary> /// <param name="value"></param> public void RemoveElement(T value) { if (!ReferenceDictionary.ContainsKey(value)) return; //nothing to do if no item. ReferenceDictionary[value]--; //if reference count is zero: remove it. if (ReferenceDictionary[value] == 0) ReferenceDictionary.Remove(value); _Dirty = true; } T _CurrentMaximum=default(T); bool _Dirty = true; //whether the max is out of date. /// <summary> /// retrieves the item in this list with the maximum reference count. /// If multiple items have the maximum, only the first one encountered will be returned. /// This method also caches the result; calls only calculate a new maximum if necessary. (if the list was changed since the last one was cached). /// /// </summary> /// <returns></returns> public T getMaxReferenced() { //find the item with the maximum value and return it's key. if (!_Dirty) return _CurrentMaximum; //otherwise, we're "dirty" and need to re-find the maximum again. int maxfound_int = int.MinValue; T maxfound_T = default(T); foreach (var iterate in ReferenceDictionary) { if (iterate.Value > maxfound_int) { maxfound_int = iterate.Value; maxfound_T = iterate.Key; } } _CurrentMaximum = maxfound_T; _Dirty = false; return _CurrentMaximum; } //Operator+; designed to be used like events- rather than code using = to "assign" a ReferenceCounted<T> value, //it will use += public static ReferenceCounted<T> operator+(ReferenceCounted<T> firstvalue, T secondvalue) { firstvalue.AddElement(secondvalue); return firstvalue; //return the ReferenceCounted<T> object. } /// <summary> /// Operator that calls RemoveElement. The result will be the ReferenceCounted object itself. /// </summary> /// <param name="firstvalue"></param> /// <param name="secondvalue"></param> /// <returns></returns> public static ReferenceCounted<T> operator -(ReferenceCounted<T> firstvalue, T secondvalue) { firstvalue.RemoveElement(secondvalue); return firstvalue; } /// <summary> /// implicitly converts this list to the type of it's component. /// </summary> /// <param name="value"></param> /// <returns></returns> public static implicit operator T(ReferenceCounted<T> value) { return value.getMaxReferenced(); } } } |
Aftermath
Pleased I had created a nice implementation, I set about creating the Comparison routine. Unfortunately, to my horror and surprise, the class which I wanted to use in conjunction with this class in one instance, ImageAttributes, had no way of getting it’s ColorMatrix. This presented an issue since I didn’t want added ColorMatrix values to mess about with the image, and the results could be less than extraordinary unless I cached each ImageAttributes.
And that was the entire purpose. However I decided to consider how else to acheive my goal; the goal here was to prevent powerups from changing the state of GameObject’s appearance in a manner that prevented them from undoing it. So, for example, powerups might have a limited duration, and the results from a overlap of two powerups could result in a confusing ending state for the object. The idea was to replace the GameObject class’s “DrawAttributes” field with a ReferenceCounted
So how do I address this?
I considered possibilities, and the problem, a bit more thoughtfully. Evidently, the ReferenceCounted<T> class would be very useful for it, but what would I use it for.
I’ve decided- though not yet attempted to implement- that I would use the ReferenceCounted class to keep track of The powerups themselves rather than a few fields of the gameobject. Since the powerup classes are what would result in the unwanted behaviour, it makes sense. So how does it work? Well, the framework basically allows a GameCharacter to have a list of GameCharacterAbilities; the GameCharacter calls the draw function of each when it draws, and it calls a frame function when it’s own frame function is called. My idea is to change that to a ReferenceCounted<GameCharacterAbility>. The code could then be changed to only call PerformFrame and Draw for the one with the highest reference count, or something similar.
The other possibility is to change the GameObject’s DrawAttributes field to a read-only property that is created “on the fly” from another new ColorMatrix field; the ColorMatrix item could be a ReferenceCounted object and therefore the use of that object in the property would use the implicit conversion operator. I’m trying to avoid this, even though I cannot foresee a circumstance where the ImageAttributes class provides something that I can’t do with a ColorMatrix (oh it does, but nothing I know how to do) I prefer to keep all my roads open, so to speak. If there was a way to compare the innards of the ImageAttributes, I could just change the DrawAttributes field to a ReferenceCounted object and make a new comparer, but it’s unfortunately not that simple.
Have something to say about this post? Comment!