Unity3D Workshop – 02 – An Animated Discussion

Welcome to the second game development workshop, where you will make a game in under two hours using the Unity3D Engine (version 4.0.1 at time of writing). The game project for this time will be a ‘side scroller’, or more exactly an ‘infinite runner’. You may be familiar with different types of side scrolling games:

Super Mario Brothers – Gradius – Bit.Trip Runner

There is a vast array of games in this category with variance on player movement, direction of scroll, forced scrolling, and many more.  For this project, it will be a one direction forced scrolling setup with a player who can jump or roll to avoid obstacles.

All of the assets and scripts for this project can be downloaded as one package here.

The player character’s run cycle is the first type of animation you will implement. Since this game is using a 2D game style, the first step is going to be creating what is known as a ‘sprite sheet‘. A ‘sprite sheet’ is a collection of images that are use to represent objects in a game at different poses and animation, think of it as a what you would get if you laid the pages of a flip book out end to end. Here is the place holder sprite sheet for today’s project:

Running Man with a head band.

This particular sprite sheet has four frames of animation to represent the character running. It may be hard to tell from seeing them all displayed together like that, each frame is a 128×128 pixel square placed left to right. This is important for how the code that drives the animation works, but the details for that will come later. For the time being if you are making your own art, make sure to keep each frame the same dimensions.

( Note: This sprite sheet could be reduced to 3 frames if we were concerned about optimizations at this point. )

To get started you need to setup a new project in Unity:

  • Create a cube: Object Hierarcy → Create → Cube → Rename to ‘Player’
    Position the cube in view of the camera
  • Import a texture: Drag into Unity → Drag onto ‘Player’ cube → Set to ‘Transparent/Diffuse’
  • Set the lighting: Edit → Render Settings → Ambient Light → White

All of these steps were done in the first workshop, go back to it as needed.. There is one new step at this point, in the inspector panel for the ‘Player’ cube, set the value for the tiling of the x-axis to 0.25. This will only use part of the texture at a time on the cube, and 0.25 exactly the size of one frame when measured horizontally. If the frame sizes on the sprite sheet were not uniform, the math to animate the character would be more complex. If everything is going according to plan, you should have something that looks a pretty close to this:

Step 1: The first step

The actual animation is going to be done in a script, create a new C# script named ‘SpriteAnimation’ (or import and rename ‘SpriteAnimation_01’) and open it MonoDevelop. The first pass on the animation setup only requires a few lines of code:

using UnityEngine;
using System.Collections;

public class SpriteAnimation : MonoBehaviour
{
    // Local Variables
    float m_fFrameTime = 0.0f;
    float m_nCurrentFrame = 0;
    Vector2 m_vTextureOffset = new Vector2();

    // Use this for initialization
    void Start ()
    {

    }

    // Update is called once per frame
    void Update()
    {
        // Keep track of how much time has passed in the current frame
        m_fFrameTime += Time.deltaTime;
        // If we have shown the current frame for more than 0.2 seconds
        if(m_fFrameTime > 0.2f)
        {
            // Cut off the frame time
            m_fFrameTime -= 0.2f;
            // Increment the frame number
            m_nCurrentFrame = (m_nCurrentFrame + 1) % 4;
            // Set the texture position
            m_vTextureOffset.x = 0.25f * m_nCurrentFrame;
            renderer.material.SetTextureOffset("_MainTex", m_vTextureOffset);
        }
    }
}

Much of this you have seen before, as a refresher here it is broken down in detail:

    // Local Variables
    float m_fFrameTime = 0.0f;
    float m_nCurrentFrame = 0;
    Vector2 m_vTextureOffset = new Vector2();

These are a few member variables are helpful to have handy, they keep track of what frame the animation is currently on, the time that is passing between frames, and what part of the texture is showing. This is a good opportunity to talk a little bit about variable types. Every programming language have it’s own variations on how it handles variables, we are using C# which is a ‘typed‘ language for variables. That means you have to be specific on the type of a variable when creating one.

Fortunately, there are a pretty common set of built-in types for most programming languages:

  • int: integer a whole number
  • float: a number with a decimal point
  • char: a single character
  • string: a string (set) of characters
  • bool: a true or false ‘boolean’

A full list of C# built-in types can be found here: http://msdn.microsoft.com/en-us/library/cs7y5x0x(v=vs.90).aspx

        // Keep track of how much time has passed in the current frame
        m_fFrameTime += Time.deltaTime;
        // If we have shown the current frame for more than 0.2 seconds
        if(m_fFrameTime > 0.2f)
        {
            // Cut off the frame time
            m_fFrameTime -= 0.2f;
            // Increment the frame number
            m_nCurrentFrame = (m_nCurrentFrame + 1) % 4;
            …

The update loop starts by keeping track of how much time has passed ‘deltaTime’ since the last update call. There are a few options on how to do the timing for games, this project is going to use game time to do the animation timing. As with the ‘Input’ global variable, you can also access ‘Time’ information globally.  By using time to determine the frame rate you will keep the animations looking consistent even if the frame rate of the game fluctuates.

A quick note about a lot of the numbers being used here. They are what programmers often refer to as ‘Magic Numbers’, which are in most cases evil, bad, and shouldn’t be used. They are called ‘magic’ because the programmer picked numbers to work instead of using variables and assumes they will always work ‘magically’ for all situations. By choosing 0.2f as the magic number it causes the sprite to change frames 5 frames per second.

// Set the texture position
 m_vTextureOffset.x = 0.25f * m_nCurrentFrame;
 renderer.material.SetTextureOffset("_MainTex", m_vTextureOffset);

To make the animation cycle the offset into the texture is shifted over based on the current frame. If you had variations in the size of the animation frames, you would have to have a list offsets, with a fix sizes simple math can be used.

At this point in the project when you try running it, it should look a lot like this.

As cool as that may look, out of context it feels a little rough just running in the back of the screen without any context. Adding a background should help a lot with that, you may recognize this background from the last project:

It’s still full of stars.

It’s still a good background for this project so no reason not to use it, put it into the project the same way that was done in the first workshop.

  • Create a plane: Object Hierarcy → Create → Plane → Rename to ‘Background’
  • Set the Camera to Orthographic size 10
  • Import a texture: Drag into Unity → Drag onto ‘Background’ plane
  • Rotate and Scale the plane to fill the camera

Double check your editor looks something like this:

Step 2: The background is back.

You now would be faced with a choice on how to animate this background, either by adding more options to the ‘SpriteAnimation’ script or by creating a new script all together. There is a trade off whenever you make this decision.  Some it lands on the side of the platform you are working on, there may be cases where you are limited to size or quantity of files.  Another factor is human readability, is it easier sort through one 10,000 line file or 10 files that are 1,000 lines each.  For these workshop we will lean heavliy to the use of multiple files keeping every distinct aspect separate as much as possible.

Create a new script, name it ‘TextureAnimation’ (or import it) and drop an instance of it onto the background. Switch over to the MonoDevelop editor. The texture animation done here at a fixed rate, it is similar to the ‘SpriteAnimation’ but it does not use a frame count, instead it simply moves the texture offset based on the update time.

using UnityEngine;
using System.Collections;

public class TextureAnimation: MonoBehaviour
{
    // Local Variables
    public float m_fScrollSpeed = 0.2f;
    Vector2 m_vTextureOffset = new Vector2();

    // Use this for initialization
    void Start () {

    }

    // Update is called once per frame
    void Update ()
    {
        // Scroll the texture on the x-offset but keep the value under 1.0
        m_vTextureOffset.x = (m_vTextureOffset.x + Time.deltaTime * m_fScrollSpeed) % 1.0f;
        // Apply it to the material
        renderer.material.SetTextureOffset("_MainTex", m_vTextureOffset);
    }
}

There is one symbol here that has been used a few times, and that is the ‘%’ or percent sign. In most programming languages this does not represent a percentage instead it stands for ‘mod’ or ‘modular‘, notice how the percent sign looks like it has the ‘/’ symbol for division in it? That’s intentional, here division is perform as normal but instead of normal division, only the remainder is kept. You have probably done this math before such as converting military time into twelve hour time, 18:00 hours is 6 p.m., not 1.5 a.m.s

With this script on your background, running the project should look like this.

The good news is that your runner looks like it is really moving through the world, the bad news is most games lie to their players.  A lot of what goes into make games is ‘smoke and mirrors’ you maybe surprised how much of this there is as you progress through the workshops.

There is one more bit of animation to add often referred to as ‘Tweening‘, or the moving, rotating, modifying of an object over time. To show this technique setup a new box object and name it ‘cloud’ below is the place holder art, don’t forget the transparency setting.

Silver Lining Included

After adding that your project should be close to this:

Step 3: A dark and cloudy night.

Again, you are faced with the option of modifying existing code or adding a new script, as before this workshop is sticking with small scrips with narrow scope. The name for the new script you need to create (or import) is ‘TweenAnimation’

using UnityEngine;
using System.Collections;

public class TweenAnimation : MonoBehaviour
{
    // Local Variables
    public float m_fXSpeed = -10.0f;
    public bool m_bLoop = true;
    public float m_fRotateSpeed = 0.0f;

    // Use this for initialization
    void Start () {

    }

    // Update is called once per frame
    void Update () 
    {
        // Move and rotate the object every update
        transform.Translate(m_fXSpeed * Time.deltaTime, 0.0f, 0.0f, Space.World);
        transform.Rotate(Vector3.forward, m_fRotateSpeed * Time.deltaTime);

        // If we are past the end of the screen
        if(transform.position.x < -20.0f)
        {
            transform.position = new Vector3(20.0f, transform.position.y, transform.position.z);
            gameObject.BroadcastMessage("TweenLoop");
        }
    }

    // Do nothing, this function is here to hide the warning
    //  on objects that don't have a use for 'TweenLoop' broadcast
    void TweenLoop() { }
}

With this code, you have handled three of the most common forms of 2D animation. Frame based animation on the player character, an animated texture on the background, and tweening animation on the cloud. The best part is that you are not limited to using just one, by combining scripts and settings you have a lot of flexibility to explore later.

Your project should look something like this.

Adding the extra element of the cloud moving at a speed slightly faster than the background creates a layering effect which enhances the appearance of the person running. This effect is known as ‘parallax‘ the optical illusion that objects closer to the viewer appear to pass faster than objects off in the distance.

This is something seen in everyday life for example riding in a car, if you fix your eyes on a tall building far in the distance it will stay in your view much longer than a street sign that is only in sight for fraction of a second. This is what creates the sense of speed when playing a racing game or a 2D side scroller.

All that animation looks good, but at this point, you have probably noticed that there is no gameplay yet in this project, fortunately now is a great time to add some. As with the last workshop, most gameplay requires some input from the player, meaning you should start by creating (or import) a new script ‘CharacterControl’ and load it up into MonoDevelop.

As the name would lead you to believe an infinite runner is just that, a game where the player never stops running. The player control this time will be jumping and rolling which will be used avoid obstacles that will be added in just a bit. The new script file will look very close to what was used in the previous project for player input.

[RequireComponent (typeof (Rigidbody))]
public class CharacterControl : MonoBehaviour
{
    public enum enPlayerState
    {
        enRun,
        enJump,
        enRoll
    }
    enPlayerState m_enPlayerState = enPlayerState.enRun;
    Vector3 m_vRollScale = new Vector3(1.0f, 1.0f, 1.0f);
    public float m_fJumpForce = 5.0f;

    // Use this for initialization
    void Start ()
    {
        rigidbody.useGravity = false;
    }

    // Update is called once per frame
    void Update ()
    {
        // The update is dependant on the state the player is
        switch(m_enPlayerState)
        {
            // What to do if the player is in jump state
            case enPlayerState.enJump:
            {
                // Anything above 2.0f is considered mid jump
                if(rigidbody.position.y < 2.0f)
                {
                    // Reset the rigid body
                    rigidbody.useGravity = false;
                    rigidbody.position = new Vector3(transform.position.x, 2.0f, transform.position.z);
                    rigidbody.velocity = new Vector3(0.0f, 0.0f, 0.0f);

                    // Reset the player state
                    m_enPlayerState = enPlayerState.enRun;
                    gameObject.BroadcastMessage("OnStateChange", (int)m_enPlayerState);
                }
            }
            break;

            // What to do if the player is in roll state
            case enPlayerState.enRoll:
            {
                // Roll lasts until the player lets go of the button / stick
                if(!Input.GetButton("Fire2") && (Input.GetAxis("Vertical") > -0.5f))
                {
                    // Reset the position and scale from rolling
                    transform.localScale += m_vRollScale;
                    rigidbody.position = new Vector3(transform.position.x, 2.0f, transform.position.z);

                    // Reset the player state
                    m_enPlayerState = enPlayerState.enRun;
                    gameObject.BroadcastMessage("OnStateChange", (int)m_enPlayerState);
                }
            }
            break;

            // What to do in all other cases
            default:
            {
                // Check jump first, it takes priority over roll
                if(Input.GetButton("Fire1") || (Input.GetAxis("Vertical") > 0.5f))
                {
                    m_enPlayerState = enPlayerState.enJump;
                    rigidbody.AddForce(0.0f, m_fJumpForce, 0.0f, ForceMode.Impulse);
                    rigidbody.useGravity = true;
                    // Broadcast the change to our animation state
                    gameObject.BroadcastMessage("OnStateChange", (int)m_enPlayerState);

                }
                // No jump?  Check roll then
                else if(Input.GetButton("Fire2") || (Input.GetAxis("Vertical") < -0.5f))
                {
                    m_enPlayerState = enPlayerState.enRoll;
                    transform.localScale -= m_vRollScale;  // Scale the player down
                    rigidbody.position = new Vector3(transform.position.x, 1.5f, transform.position.z);
                    // Broadcast the change to our animation state
                    gameObject.BroadcastMessage("OnStateChange", (int)m_enPlayerState);
                }
            }
            break;
        }
    }
}

This is by far the largest script used for a project, most of it will look very similar but there are some new elements, such as:

[RequireComponent (typeof (Rigidbody))]

This part at the very top is a lot more powerful that what it may seem. Up until now, if an object needed a component it was either created in a script or manually placed in the editor. This line here will cause the script to only work on an object that has a ‘Rigidbody’ component, it will create one automatically when the script is placed on the player object. As the projects get more complicated you’ll see this used a lot more.

    public enum enPlayerState
    {
        enRun,
        enJump,
        enRoll
    }
    enPlayerState m_enPlayerState = enPlayerState.enRun;

This is a new type of variable, the ‘enum’ or enumeration, a way to make code easier for humans to read and understand. By default the items here are given numbers in order they are listed starting with zero, so enRoll would equal 2. In large code bases it is much easier to search for ‘enRoll’ instead of the number ‘2’ and get usable results, also if the order changes later all uses of the enum values are still valid and do not need to be updated.

    // Update is called once per frame
    void Update ()
    {
        // The update is dependant on the state the player is
        switch(m_enPlayerState)
        {
            // What to do if the player is in jump state
            case enPlayerState.enJump:
                ...
            break;

            // What to do if the player is in roll state
            case enPlayerState.enRoll:
                ...
            break;

            // What to do in all other cases
            default:
                ...
            break;
        }
    }

The ‘switch‘ statement is a new way to control the flow of code, in some ways it is very close to how you have been using the ‘if’ and ‘if -> else’. Switch statements work by giving it a variable at the start of the switch statement and then using multiple ‘case’ conditionals to check and run the first one that matches. The code in ‘Default’ is called if no match is found amongst the case conditionals. The ‘break’ keyword here ends the switch statement and the flow of code continues after the end of the switch statement. If there is no ‘break’ statement the code will continue into the next ‘case’ until it reaches a ‘break’ if any.

Note: C# does not support ‘fallthrough’ in switch statements which makes them less useful than in other languages such as ‘JavaScript’ one of the other major languages for Unity. Because of this, the use of ‘switch’ statements in this workshop ‘switch’ and ‘if->else’ is a style and readability choice.

With that covered, it is time to look at what is actually happening here. You are now dipping your toe into creating a ‘state machine’. A ‘State Machine‘ is set of rules built for how objects, such as in this case the player, can interact in the world. Some state machines are trivial, such as a light switch being on or off, others are much more complex. In our use, the player state machine is very simple:

You are in the ‘reading’ state

If the player is in the running state they can exit the state by pressing the ‘jump’ button or holding the ‘roll’ button. However, the state machine will not allow a jumping player to roll or vice versa. You can see this in the ‘default’ case in the switch statement.

                // Check jump first, it takes priority over roll
                if(Input.GetButton("Fire1") || (Input.GetAxis("Vertical") > 0.5f))
                {
                    m_enPlayerState = enPlayerState.enJump;
                    rigidbody.AddForce(0.0f, m_fJumpForce, 0.0f, ForceMode.Impulse);
                    rigidbody.useGravity = true;
                    // Broadcast the change to our animation state
                    gameObject.BroadcastMessage("OnStateChange", (int)m_enPlayerState);

                }
                // No jump?  Check roll then
                else if(Input.GetButton("Fire2") || (Input.GetAxis("Vertical") < -0.5f))
                {
                    m_enPlayerState = enPlayerState.enRoll;
                    transform.localScale -= m_vRollScale;  // Scale the player down
                    rigidbody.position = new Vector3(transform.position.x, 1.5f, transform.position.z);
                    // Broadcast the change to our animation state
                    gameObject.BroadcastMessage("OnStateChange", (int)m_enPlayerState);
                }

The code will only reach this if the player isn’t in the jump or roll state, and then here it checks the input to determine if there will be a state change or not. Notice how the ordering of the if statements sets the priority of actions this is something to be aware of when making more complex state machines.

Inside of each of the conditionals, a few changes are made to the character to reflect the state change. In the jump, that rigidbody that the script will add is given an upwards force as an ‘impulse’ or shove upwards, and gravity is turn on to give the jump a nice parabolic arc. For the roll, the player is scaled down slightly to avoid potentially low obstacles, since the position of the player in this case is taken from center, the position is also offset slightly.

However, if the player was instead already in one of these states, the code flow would instead check to see if the player meet the conditions to return to the running state:

            // What to do if the player is in jump state
            case enPlayerState.enJump:
            {
                // Anything above 2.0f is considered mid jump
                if(rigidbody.position.y < 2.0f)
                {
                    ...
                }
            }
            break;

or

            // What to do if the player is in roll state
            case enPlayerState.enRoll:
            {
                // Roll lasts until the player lets go of the button / stick
                if(!Input.GetButton("Fire2") && (Input.GetAxis("Vertical") > -0.5f))
                {
                    ...
                }
            }
            break;

When the conditions are met that causes the state machine to return to ‘enRunning’, the changes made to enter the states such as scale or gravity are returned to normal. Through out all of this, whenever there was a state change, the code would broadcast it to the object:

gameObject.BroadcastMessage("OnStateChange", (int)m_enPlayerState);

Earlier we mentioned that C# is a typed language, and you need to be exact when using variables. Now, until the inevitable AI uprising, we can force a computer to believe us when it comes to what something is. This is done by ‘casting’ here the ‘m_enPlayerState’, which is an enum not an int, is be forced into the role of an int by the casting caused by the ‘(int)’ in front of it. Now to make something that listens for this message, to do that, open up the SpriteAnimation script file. (or import SpriteAnimation_02.cs as ‘SpriteAnimation’)

using UnityEngine;
using System.Collections;

public class SpriteAnimation : MonoBehaviour
{
    // Local Variables
    float m_fFrameTime = 0.0f;
    float m_nCurrentFrame = 0;
    Vector2 m_vTextureOffset = new Vector2();
    float[] m_afAnimStates = new float[] { 0.5f, 0.0f, 0.0f };

    // Use this for initialization
    void Start ()
    {
        // Set the tiling value
        renderer.material.SetTextureScale("_MainTex", new Vector2(0.25f, 0.5f));
        // Set the initial state
        OnStateChange(0);  
    }

    // Update is called once per frame
    void Update()
    {
        // Keep track of how much time has passed in the current frame
        m_fFrameTime += Time.deltaTime;
        // If we have shown the current frame for more than 0.2 seconds
        if(m_fFrameTime > 0.2f)
        {
            // Cut off the frame time
            m_fFrameTime -= 0.2f;
            // Increment the frame number
            m_nCurrentFrame = (m_nCurrentFrame + 1) % 4;
            // Set the texture position
            m_vTextureOffset.x = 0.25f * m_nCurrentFrame;
            renderer.material.SetTextureOffset("_MainTex", m_vTextureOffset);
        }
    }

    void OnStateChange(int nStateIndex)
    {
        m_vTextureOffset.y = m_afAnimStates[nStateIndex];
        renderer.material.SetTextureOffset("_MainTex", m_vTextureOffset);
    }
}

Just a few small changes here, first a new local variable.

  float[] m_afAnimStates = new float[] { 0.5f, 0.0f, 0.0f };

This is a member variable that is an array of floats ‘m_af’ notation. An ‘array‘ is an order collection of similar elements, like a row of mail boxes. All the mail boxes are in order by number and hold similar objects, however unlike city mailboxes, arrays don’t typically have gaps in the numbering. In our usage here, the array is initialized like this:

m_afAnimStates[0] = 0.5f;
m_afAnimStates[1] = 0.0f;
m_afAnimStates[2] = 0.0f;

Computers almost always start counting with zero, so this array has a length of three elements, the index for the last one is 2 (length – 1). This array is used lower in the ‘OnStateChange’ listener function.

    void OnStateChange(int nStateIndex)
    {
        m_vTextureOffset.y = m_afAnimStates[nStateIndex];
        renderer.material.SetTextureOffset("_MainTex", m_vTextureOffset);
    }

A small amount of code that does some interesting work. First it receives the player state from the CharacterControl script as an integer, it then uses that value to pick that right animation offset for that state and applies it to the material. Another way to look at it would be:

m_afAnimStates[enRun] = 0.5f;
m_afAnimStates[enJump] = 0.0f;
m_afAnimStates[enRoll] = 0.0f;

Why are two of the states using the same value? Simply because the new texture for the workshop doesn’t include a separate set of images for the roll and jump state. Something that could be added later by those artistically inclined.

Speaking of new textures, it’s time to switch back to the Unity editor, to import this new texture and apply it along with the character control script to the player object.

On a roll now

This time you do not have to adjust the tiling, that is now being done in the start function of the SpriteAnimation function, but it might look a bit odd in the editor. If everything is going well your editor should look something like this:

Step 4: Ready to roll

Give the project a quick test and try out jumping and sliding. Now you may have noticed that since the object now has a rigidbody it is colliding with the cloud or background. Worth a laugh when that happens, but not really useful for the project, uncheck the ‘Box Collider’ the cloud and background, and you should have a project that plays like this.

For the last few steps of this project, it is time to something for the player to dodge. For this ‘enemy’ the place holder art to use is:

A sharp foe

Create a cube with this texture in the world and then drop the ‘TweenAnimation’ script file on it, then switch over to the MonoDevelop to take a look at some unused parts from earlier.

        // Move and rotate the object every update
        transform.Translate(m_fXSpeed * Time.deltaTime, 0.0f, 0.0f, Space.World);
        transform.Rotate(Vector3.forward, m_fRotateSpeed * Time.deltaTime);

Adding a rotation option to the tween really adds a lot to the appears of this spike ball. Notice here that this is a new way to use the ‘Translate’ function. Using the ‘Space.World’ key word, it has the object ignore it’s own rotation which is changing constantly as the object rotates, and instead moves in a fixed direction. A bit like wheels on a car, their rotation is changing but the movement of the car is independent of the that rotation.

Now create a new script (or import), called ‘Enemy’ and fill it in with the following:

using UnityEngine;
using System.Collections;

[RequireComponent (typeof (AudioSource))]
[RequireComponent (typeof (Rigidbody))]
public class Enemy : MonoBehaviour
{
    AudioSource m_pAudioSource;

    // Use this for initialization
    void Start ()
    {
        rigidbody.useGravity = false;
        m_pAudioSource = (AudioSource)gameObject.GetComponent("AudioSource");
    }

    // Update is called once per frame
    void Update () {

    }

    // Receiver for TweenLoop broadcast
    void TweenLoop()
    {
        // Pick a random position to make the player need to roll or jump to avoid this object
        transform.position = new Vector3(transform.position.x, Random.Range(2.0f, 10.0f), 0.0f);
    }

    // Action To Perform when a rigid body is touching this object
    void OnTriggerEnter(Collider pOther)
    {
        // Play the 'Buzz' sound on collision
        if(!m_pAudioSource.isPlaying)
            m_pAudioSource.Play();
    }
}

There is a new component here the ‘AudioSource’ which will be looked at in detail later, but for now a quick peek at the code. You will see the ‘TweenLoop’ function:

    // Receiver for TweenLoop broadcast
    void TweenLoop()
    {
        // Pick a random position to make the player need to roll or jump to avoid this object
        transform.position = new Vector3(transform.position.x, Random.Range(2.0f, 10.0f), 0.0f);
    }

Every time the object loops around, it’s position is randomly picked for the next pass to cause the player need to dodge out of the way. Additionally there is the ‘OnTriggerEnter’ function that you should recognize from the previous workshop from the ‘Projectile’

    // Action To Perform when a rigid body is touching this object
    void OnTriggerEnter(Collider pOther)
    {
        // Play the 'Buzz' sound on collision
        if(!m_pAudioSource.isPlaying)
            m_pAudioSource.Play();
    }

Only instead of causing damage, it simply plays a sound effect. The place holder sound effect was created from one of many free tools, in this cause ‘Audacity’ was used to make this simple recording.

Bzzzz

Grab that sound effect, and place it into the project the same way you would for a texture, you should create a ‘Sounds’ folder first to keep everything organized. Then drop the ‘Enemy’ script you just created onto the ‘Enemy’ cube you added earlier and take a look at the component inspector. Pick the sound effect and uncheck ‘Play on awake’ inside of the ‘Audio Source’ component.

Step 5: Sounds like a plan

All that is left now is to run the project and try it out. If everything is working well it should play just like this.

That’s where work shop 2 will end, take any remaining time to try out your own customizations. Consider adding techniques from the first work shop such as being able to shoot the enemy or using the sin wave to make it harder to dodge. In the next work shop we will be taking the first steps into some AI, VFX, and UI by making a dual stick shooter.