Sunday, October 25, 2020

Sound Randomizations and Asset Store Considerations

In my last post I discussed the sound effects that have been added when the character is running, walking, wading, swimming, and treading.  When animations are only a second or so in length, sounds can get very repetitive.  Using a Unity Store product to manage sounds does save time.  However, I couldn't figure out how to implement a certain feature I wanted.  Since the sounds were spawned from a script attached to a Mechanim state, they only randomized when state changed.  For example, when using WASD to maneuver the character in water, the sound randomization features of the product I purchased only performed the randomization when the state of the animation switched from say, swimming to idle or vice-versa.  They would not change until the player state was different.  I wrote the developer to ask if there was something to be done to fire the randomization event within the current state's animation. He politely responded that I would have to write my own behavior for that.  So, that is exactly what I did.

State Machine Sound Controller

Digging around, I found I could attach a script to the state if it inherited from StateMachineBehaviour.  This would enable me to attach my new script in place of the store asset's script which was controlling the sounds.  There are three events that are necessary to override in order to manage the randomizing of sounds mid-state.  Take a look at my new script.

 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
using UnityEngine;

public class STStateMachineSoundController : StateMachineBehaviour
    {        
        public AudioClip[] AudioClipArray;
        private AudioSource _audioSource;
        private bool _isVariety;
        private int _loopCount;

        private void Awake()
        {      
            //assign _audioSource
            _audioSource = STGameManager.Instance.PlayerObject.GetComponent<AudioSource>();
            //determine if variety will be used
            _isVariety = AudioClipArray.Length > 1;
            //initialize loopCount
            _loopCount = 0;
        }

        public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
            //This is a bit counter-intuitive to stop the sound OnStateEnter.
            //But this ensures the previous state's sound stops playing and doesn't overlap events.
            //This state's sound will get picked up on the first OnStateUpdate
            _loopCount = 0;
            _audioSource.Stop();
        }

        public override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {                        
            if (!_audioSource.isPlaying)
            {
                PlayClip();                
            }                        
            if (_isVariety)
            {
                //only change the current variety at the end of the current animation loop
                //get the integral portion of the normalizedTime
                int thisLoop = Mathf.FloorToInt(stateInfo.normalizedTime);
                float progress = stateInfo.normalizedTime;
                //get the decimal portion of normalizedTime;
                progress = Mathf.Repeat(progress, 1.0f);
                //at the end of the current state's loop
                //select a new random variety clip when the loop cycles
                if (_loopCount != thisLoop && progress >= .99f)
                {
                    _loopCount++;
                    PlayClip();
                }
            }
        }

        public override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {            
            //stop audio in case next state does not use audio
            _audioSource.Stop();
        }

        private void PlayClip()
        {
            if (_isVariety)
            {
                //randomly pick a new clip to play from the array
                int randomClip = Utils.MathHelpers.GetRandomInteger(0, AudioClipArray.Length - 1);
                _audioSource.clip = AudioClipArray[randomClip];
            }
            else
            {
                _audioSource.clip = AudioClipArray[0];
            }
            _audioSource.Play();
        }
    }

When dragging this script onto a Mechanim animation state, the AudioClipArray[] allows me to establish the number of sound variations desired for that particular state.  If there is only to be a single sound, I set the number to 1.  Regardless, all that is needed to make it work is to drag sound clips onto the AudioClipArray Elements in the inspector.  That's it!

The code is heavily commented, but still pretty easy to follow.  The _isVariety bool is used to randomly select from the array list if the list has more than one sound clip in it.  When any particular state is entered, I found it best to stop the audio source from playing an existing sound.  I introduced a loop counter to help ensure a complete cycle before changing to a different sound clip.  The Mathf.Repeat() method is very nice for getting the decimal portion of the state's elapsed time.  When the decimal portiono is >= .99f it means the animation loop is almost completed.  I only wanted the sound to change at the end of the loop. 

There are two additional things I might need to mention for any new developers.  First, the _audioSource assignment in the Awake() simply identifies the game object in my scene that has the  necessary AudioSource component attached.  And second, the Utils.MathHelpers.GetRandomInteger method is part of my own Utils class that contains many helper methods I have written to use in my game.  Simply replace that with something like the following to generate a random selection between 0 and the number of sounds in the AudioClipArray[].

1
2
Random r = new Random();
int rInt = r.Next(0, AudioClipArray.Length - 1);

I dragged this script onto all my movement Mechanim states, set the number of sound clips for each one, and dragged the appropriate clips onto each script's elements.  This allowed me to remove the store product's script altogether.  It's fast, easy to configure and use, and most importantly, I know what the code does.  

Asset Store Considerations

When purchasing Asset Store packages for use in my game, I keep the following considerations in mind.  First, using someone else's code means I am introducing unfamiliar code into my game.  I don't have time to read every line of code in every Asset Store purchase I use in my game.  I have to accept this as a risk.  There could be bugs in that code.  There could be added fluff (parts I will not be using) in the code that may bog down my game.  There could also be future updates to that code which may break my code after it is updated.  Of course, all developers must understand that every library we are "using" brings with it a certain level of trust we place in the developers who made them.  But I think the most important thing to consider is being willing to step back and write something ourselves if the need arises.  This is why learning C# is so important for Unity developers.  Not everything can be purchased from the Unity Store.  And while, yes, there are non-programming tools such as Bolt that can assist people in creating games, there is a whole lot of satisfaction in knowing you have the knowledge and skills to venture into the coding world yourself, if and when the need arises.  

I will hold off on a video until I have a little more to show you.  As you can see, I am in the very early stages of development of this game of mine.  I appreciate you following along.  I hope something from my experiences will help other developers get inspired.  Until next time...

Sunday, October 11, 2020

Simulating Water Depth With Animations and Sounds

The past few weeks of development work have been focused around simulating tile depth for watery tiles.  Since the game is meant to be a survival game, it seemed only fitting the player's character should be able to wade into and even swim in water.  For this, I needed to create a few systems for establishing water depth, character sprite masking to hide parts of the character that are submerged, animation and speed transitions, and all of the proper sounds to complete the sensory experience.

Tile Depth

Establishing a depth value for water tiles was the first step.  The map is generated from Perlin noise, with each tile being assigned a terrain type based on the weight of each type compared to the other terrain types and the desired order (fixed or random) based on the player's selections.  I added a new float property to my class that stores tile information.  Since all tiles have an X and Y position, I thought it proper to name the depth variable using the letter Z.

During the map creation process, all top tiles are initialized with a Z value of 0.  Once the entire map has been created from the Perlin noise values, and after all special tiles have been cluster-spawned onto their respective tile types, the tile array is passed to a method to establish tile depth for types brackish water, salt water, fresh water, swamp, and marsh.  The process of setting the Z depth requires obtaining information about every adjacent tile to the target tile.  Consider this small map of tiles.

Tile Depth

The green tiles are land and the blue tiles are water.  The technique I used was to make several passes through the tilemap array for each tile type that needed depth.  The first pass of each tile type only checks to see if any adjacent tiles do not match the target tile.  If any tile surrounding the target tile doesn't match, it means the target tile is next to land.  Consider the blue tiles marked with a 1.  They all have at least one adjacent tile that is land.  Each of these target tiles get a depth of 1.  In the next pass through the array, all of the tiles with a Z value of 0 that have at least one adjacent tile with a value of 1 get assigned a Z value of 2.  The next pass checks and sets for 3, then 4 and so on.  The last pass, or the pass with the highest value, checks for its own value and sets any remaining 0's to the same value.  This essentially spreads out the remaining tiles to be the deepest.

Sprite Mask Positioning

Next, I added a sprite mask (the orange box below) to my character prefab (his name is Boomer), setting all of the sprites that make up the character to only be visible outside of the mask.  When Boomer is on land, the mask sits beneath him, out of view.  But as he descends into liquid tiles, based on the Z value of the tile he is one, the sprite mask is adjusted up to cover his lower body.

Boomer and His Sprite Mask

In the player movement script, the Z value of the tile Boomer is on is checked.  The transform of the sprite mask is then adjusted based on that value.  Here is what it looks like when Boomer is in deep water, just before it is deep enough for him to start swimming.

Boomer Wading Out into a River

Animations and Sounds

Sometimes it is prudent and even necessary to reinvent the wheel.  But most of the time it isn't.  I have relied on several third-party assets from the Unity Store, and I can say that I am mostly pleased with what I have purchased and used so far.  Boomer is a scruffy kind of survivalist that I created using Character Creator 2D.  I really have no hesitation plugging for this great asset.  The developer is extremely interested in customer feedback and satisfaction.  He has helped me several times and even implemented suggestions I have made.  That said, the animations provided out-of-the-box have been great for my development, saving me precious time and effort.  My animation controller was very easy to setup with dozens of included animations for Boomer.  

The sound system I have been using is also from the Unity Store.  It is called Master Audio.  The product seems to be much more than I could possibly use in this project, and the learning curve is practically a U-Turn.  That said, it is a solid product and I do recommend it.  One thing I have utilized for this part of the project is a script that enables me to link a sound with an animation.  The Mechanim State Sounds component script from Master Audio can be added directly to an animation.  Once configured, I was able to play the appropriate sound effect for each of the animations: slow walk, walk, run, swim, idle swim.  I believe this step took me the longest to figure out.

With depth established for tiles, it was very easy to adjust the speed of the character's movement based on tile depth.  Wading out into deep water slows the character's movement, but once swimming the speed is increased.  Eventually, speed will also be adjusted by character skill, health, age, etc.

These few dev tasks took me much longer than I expected.  It was especially frustrating to try and sync animations with sounds.  When I finally got the bright idea to trim the sounds to be the same length of time as the animations, everything began to line up.  Overall, I am satisfied with the results enough to move on to other parts of the game.  

My wife watches me work on this and she made a comment to me the other day that game development looks like hard work.  As always, she is absolutely right.  Feel free to have a look at the video below where I demonstrate all of these features in game.



Until next time...