Unity3D Workshop – 03 – Dualing sticks

Welcome to the third in our series of unity workshops, in these workshops we will take a closer look at game development by taking a game concept and making it inside of the Unity3D engine (version 4.1.1 at the time of writing). For this workshop, we will be investigating the ‘Dual Stick Shooter’ while this game style has been around for a very long time it had a dip in popularity between the arcades and the home console until the release of analog sticks. You may recognize games such as ‘Robotron‘ ‘Smash TV‘ or ‘Geometry Wars‘ that show the evolution of the game design structure.

Robotron: 2084 (1982), Smash TV (1990), Geometry Wars (2007)

Note: For the purpose of this workshop, the assumption will be made that you are using a controller while making this game, if not there will be a appendix afterwards for making a dual-stick shooter work on a keyboard alone.

The asset package can be downloaded: here.

Our first task for this project is going to be making the player object, we are still using basic cubes and textures as with the previous two projects, our player in this project will be represented by the ‘Starship Macaroni’

The Cheesiest

One adjustment we are going to make for this is instead of the default box collider, a sphere collider will be used. By swamping out the collider that is being the simulation for the hit box of the player can be better customized. Multiple collision shapes could be used to get the accuracy even tighter at the trade off of performance.

Step 01: One small step

Now to add the code to make the player move around, this is very similar to what was used on the tank in the first project, but since this is a space ship the ability to move in any direction is going to take a bit more code. Create or import ‘PlayerMovement’ script into the project.

using UnityEngine;
using System.Collections;

[RequireComponent (typeof (Rigidbody))]
public class PlayerMovement : MonoBehaviour
{
    // Local Variables
    Vector3 m_vMinBounds = new Vector3(-14.0f, 2.0f, 0.0f);
    Vector3 m_vMaxBounds = new Vector3(14.0f, 18.0f, 0.0f);
    public float m_fPlayerSpeed = 0.1f;

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

    // Update is called once per frame
    void Update ()
    {
        // We are going to read the input every frame
        Vector3 vNewInput = new Vector3(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"), 0.0f);

        // Only do any work if meaningful
        if(vNewInput.sqrMagnitude < 0.1f)
            return;

        // Turn the input in a normalized, or 'Unit' vector
        vNewInput.Normalize();
        // Add our input to our position
        transform.position += vNewInput * m_fPlayerSpeed;

        // Limit the position to our play area
        Vector3 vNewPosition = transform.position;
        vNewPosition = Vector3.Max(m_vMinBounds, vNewPosition);
        vNewPosition = Vector3.Min(m_vMaxBounds, vNewPosition);
        transform.position = vNewPosition;

        // Set our rotation to represent the input
        float fHeading = Vector3.Dot(Vector3.right, vNewInput);
        Vector3 vNewRotation = transform.rotation.eulerAngles;
        vNewRotation.z = fHeading * 90.0f;

        // Adjust our rotation if we're on the bottom half of the input circle
        if(vNewInput.y < 0.0f) // Input is inverted
        {
            vNewRotation.z = 180.0f - vNewRotation.z;    
        }

        // Apply the transform to the object        
        transform.rotation = Quaternion.Euler(vNewRotation);
    }

    // React to taking damage
    public virtual void TakeDamage() { }

}

Things that you should recognize:

  • ‘RequireComponent’ creating a rigid body for this object
  • The ‘public’ keyword making a value editable in the editor
  • The ‘start’ function initializing settings on components
  • The ‘update’ function happening every frame
  • The ‘max’ and ‘min’ functions keeping the player in a play area

Things you may not recognize should be more interesting.

        // Only do any work if meaningful
        if(vNewInput.sqrMagnitude < 0.1f)
            return;

Here the ‘sqrMagnitude’ is being checked to exit the update function early. Analog sticks have something called the ‘DeadZone’ this is the part of the stick that is unreliably inaccurate, and often increases as a the controller wares with use. When in the ‘DeadZone’ the input will be reported as zero instead, since the following code won’t work in that case this check will exit the function instead of trying to run good code on garbage data.

Unlike the first project where the tank always faced the same direction, our ship needs to rotate to respond to the input. To do this we do a bit of math, first a ‘Dot’ product, a dot product is the project of two vectors onto each other. Think of how your shadow changes in the sun, as the sun moves your shadow is the representation of the ‘Dot’ product between your height and the sun’s position in the sky.

One special thing about the Dot product is that if you use vectors of the same length, often Unit vectors that have a length of one, the dot product is the angle between them. We are using that fact here, where we take the Dot product of the engine constant ‘Vector3.right’ (1, 0, 0) and the player’s input which we ‘Normalized’ which turned it into a unit vector.

Before trying this code out make sure to do a small checklist to get the new project to line up with some of the defaults of the previous workshops.

  • Set the ‘Ambient Light’ to white
  • Make sure the ship is ‘Scaled’ to size 2, and rotated 180deg on the y-axis
  • Make sure the camera is in position at (0, 10, -10) and the ship is at (0, 10, 0)
  • Make sure the camera is ‘Orthographic’ and ‘Size’ is 10

Additionally, while in the camera settings, change the background to a nice color that best represents space. If everything is going according to play, your project should look like this.

You may have remembered from the start of this project that this is a ‘Dual’ stick shooter, so we are only half done on that part, adding that feature is the next step. Go to ‘Edit -> Project Settings -> Input’ and change ‘Fire1’ and ‘Fire2’ into our right stick input. You can add new input by adjusting the ‘Size’ setting at the top but since no buttons are being used for this project replacing existing buttons is fine.

Step 02: Taking Control

Notice that the ‘DeadZone’ is set to 0.19 that makes the inner most 19% of the stick read as zero to the scripts. With this new input source defined, something needs to listen to it, and since this is a ‘shooter’ it’s time to add a turret to our spaceship.

[IMG][IMG]

Here we have images for our turret base and the barrel to add our our assets. Create a pair of cubes as normal, each with one of these elements. However, on each of these, delete the ‘Box Collider’, they do not need any collision detection.

In the previous workshops we have talked about using multiple scripts to composite functionality and behaviors together on a single game object. To expand on this, now we will composite multiple game objects together to make a much more complex object. The first step is to stack up all our objects:

  • Set the ship to: (0, 10, 0)
  • Set the base to: (0, 10, -1)
  • Set the turret to: (0, 10, -2)

Step 03: Ship Construction, some assembly required.

You can use the camera settings as indicated by the arrow to adjust your viewport to be axis aligned, this will make the position of the elements much easier. Now while you could use complex math to get the scaling and positioning right, art is pretty subjective so take a few moments to scale and position the rotating base and the turret on the ship as you see fit. Leave the ‘z’ position alone unless you want to change the layering.

You can use the following numbers if you’d prefer to just jump ahead to the next step.

Step 04: The finished product.

This looks pretty good, but they are not associated with the player object at all, to do that, simply drag the ‘TurretBarrel’ onto the ‘TurretBase’ in the object Hierarchy, and then the ‘TurretBase’ onto the ‘Player’ object, and you will then have created your first composite object in Unity3D.

Step 05: The object tree

Running the project now, you will see the ‘Child’ object of the ship will move with ‘Parent’ object, the player. To make it rotate however, it’s time to add another script. Create or import ‘PlayerAiming’ script into the project.

using UnityEngine;
using System.Collections;

public class PlayerAiming : MonoBehaviour
{
    // Update is called once per frame
    void Update ()
    {
        // We are going to read the input every frame
        Vector3 vNewInput = new Vector3(Input.GetAxis("Horizontal_R"), Input.GetAxis("Vertical_R"), 0.0f);
        // Only do work if meaningful
        if(vNewInput.sqrMagnitude < 0.1f)
        {
            return;
        }

        // Set our rotation to represent the input
        vNewInput.Normalize();
        float fHeading = Vector3.Dot(Vector3.right, vNewInput);
        Vector3 vNewRotation = transform.rotation.eulerAngles;
        vNewRotation.z = fHeading * 90.0f;

        // Adjust our rotation if we're on the bottom half of the input circle
        if(vNewInput.y > 0.0f)
        {
            vNewRotation.z = 180.0f - vNewRotation.z;    
        }

        // Apply the transform to the object        
        transform.rotation = Quaternion.Euler(vNewRotation);
    }
}

This script is very similar to the player movement, it does use the new ‘Horizontal_R’ and ‘Vertical_R’ inputs. If you place the script onto the ‘TurretBase’ object and run the project you should have a nice looking turret and a slick space ship flying around, just like this.

There are a few terms that have been used a few times now, ‘Quaternion‘ and ‘Euler‘ these are both mathematical ways of representing angles. There are advantages and disadvantages to using them, which leads to another programming war to which is the right one to use. Because the inspector in the editor uses ‘Euler’ angles, these workshops will stick with using them over quaternions to keep from switching form constantly.

All that math aside, while we have the ‘dual sticks’ working, that is only a third of what we need to make a ‘dual stick shooter’. Our shooting makes noise, so place the ‘Pew.mp3‘ sound effect into a new set of folders ‘Resources’ then ‘Sounds’, this location is important later so make sure to place it in that structure. Then create or import both the ‘PlayerShooting’ and ‘Projectile’ scripts.

using UnityEngine;
using System.Collections;

public class PlayerShooting : MonoBehaviour
{
    public float m_fRateOfFire = 0.2f;
    public float m_fProjectileSpeed = 0.3f;
    float m_fAccumulatedFireTime = 0.0f;
    AudioSource m_pFireSound;

    // Use this for initialization
    void Start ()
    {
        m_pFireSound = (AudioSource)gameObject.AddComponent("AudioSource");
        m_pFireSound.clip = (AudioClip)Resources.Load("Sounds/Pew") as AudioClip;
    }

    // Update is called once per frame
    void Update ()
    {
        // We are going to read the input every frame
        Vector3 vNewInput = new Vector3(Input.GetAxis("Horizontal_R"), Input.GetAxis("Vertical_R"), 0.0f);
        if(vNewInput.sqrMagnitude < 0.1f)
        {
            // Reset the shooting timing if we stop shooting
            m_fAccumulatedFireTime = 0.0f;
            return;
        }

        // Keep track of extra time of shots
        m_fAccumulatedFireTime += Time.deltaTime;

        // Broadcast our shooting
        int nNumShotsFired = (int)(m_fAccumulatedFireTime / m_fRateOfFire);
        for(int nShotIter = 0; nShotIter < nNumShotsFired; ++nShotIter)
        {
            // Create a new shot
            GameObject pNewObject = GameObject.CreatePrimitive(PrimitiveType.Sphere);
            pNewObject.name = " Player_Projectile";
            pNewObject.renderer.material.color = Color.red;
            // Set it's position and rotation to match this object's in world space
            //  and put it on 0.0f 'z-axis' to make sure it'll hit targets
            pNewObject.transform.rotation = transform.rotation;
            Vector3 vTempPos = new Vector3(transform.position.x, transform.position.y, 0.0f);
            pNewObject.transform.position = vTempPos;
            // Scale it to a nice 'magic number' size
            pNewObject.transform.localScale = new Vector3(0.25f, 0.25f, 0.25f);

            // Add the projectile behavior to the object
            Projectile pNewProjectile = (Projectile)pNewObject.AddComponent("Projectile");
            pNewProjectile.Init(m_fProjectileSpeed);
        }

        if(nNumShotsFired > 0)
        {
            m_pFireSound.Play();    
        }

        m_fAccumulatedFireTime %= m_fRateOfFire;
    }
}
    // Use this for initialization
    void Start ()
    {
        m_pFireSound = (AudioSource)gameObject.AddComponent("AudioSource");
        m_pFireSound.clip = (AudioClip)Resources.Load("Sounds/Pew") as AudioClip;
    }

This is what makes the position of the file very important. Last time a sound was used it was picked in the editor, that method is still valid in this case, learning about this now is foreshadowing for later. What the ‘Resources.Load’ allows us to do is dig into the file structure and fine a resource to use. Most systems are very picky about spelling and capitalization, or ‘case sensitive’, which means if the sound isn’t working check to make sure the files are in the right place.

using UnityEngine;

On many occasions the scripts have used ‘built in’, ‘predefined’, or ‘common’ values and functions. These are generally provided by the libraries included by the ‘using’ keyword at the top of the file. As these projects get more complex the frequency of using additional libraries will also increase.

To add this script and make projectiles appear in the perfect spot, switch over to the editor and create an empty game object from the ‘Game Object’ pull down mention or use the ‘ctrl+shift+N’ shortcut key. Place this object in the hierarchy on the end of the barrel and position right at the end, then place the ‘PlayerShooting’ script onto it.

Step 06: Just the tip.

Switch over now to the ‘MonoDevelop’ editor and the ‘Projectile’ script.

 using UnityEngine;
using System.Collections;

[RequireComponent (typeof (Rigidbody))]
public class Projectile : MonoBehaviour
{
    // Local variables
    float m_fProjectileSpeed = 0.5f;
    float m_fProjectileLifeTime = 2.0f;

    // The constructor for this class
    public void Init(float fProjectileSpeed)
    {
        // How fast are we going to move?
        m_fProjectileSpeed = fProjectileSpeed;
    }

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

    // Update is called once per frame
    void Update ()
    {
        // Our project moves on it's own without needing player input
        transform.Translate(0.0f, m_fProjectileSpeed, 0.0f);

        m_fProjectileLifeTime -= Time.deltaTime;
        if(m_fProjectileLifeTime < 0.0f)
        {
            // Destroy this projectile after it's life time runs out
            DestroyObject(gameObject);
        }
    }

    // Collision handling
    void OnTriggerEnter(Collider pOther)
    {
        // We don't hit players
        if(pOther.gameObject.GetComponent<PlayerMovement>() != null)
            return;

//        // Tell enemies they were hit                            // Lines to be uncommented after adding the enemy
//        if(pOther.gameObject.GetComponent<Enemy>() != null)        // Lines to be uncommented after adding the enemy
//        {                                                        // Lines to be uncommented after adding the enemy
//            // Tell whatever we hit to take damage                // Lines to be uncommented after adding the enemy
//            pOther.gameObject.BroadcastMessage("TakeDamage");    // Lines to be uncommented after adding the enemy
//        }                                                        // Lines to be uncommented after adding the enemy

        // Destroy this projectile after collision
        DestroyObject(gameObject);
    }
}

No surprises here in this file, but a few minor things to take note of. First, there is a block of code commented out at the bottom, this section won’t work until later so for now we leave it inert as comments.  Secondly, these projectiles have a limited life span and will self delete after 2 seconds if they don’t hit anything. Speaking of collisions, in the ‘OnTriggerEnter’ function we are using:

         // We don't hit players
        if(pOther.gameObject.GetComponent<PlayerMovement>() != null)
            return;

Instead of the previous version which took the component to test for as a string name. Put everything together and the end result should be pretty close to this. The player is pretty much complete at this point, however without anything to shoot the end result falls flat, time to add some things to shoot.

For this workshop we are using two different enemy types, so add in some familiar faces and make materials for their textures, and also grab their associated sounds ‘SpacePigeon‘ and ‘SpaceSaw‘:

 using UnityEngine;
using System.Collections;

[RequireComponent (typeof (AudioSource))]
[RequireComponent (typeof (Rigidbody))]
public abstract class Enemy : MonoBehaviour
{
    // Local variables
    protected int m_nHitPoints = 5;
    protected float m_fMoveSpeed = 0.08f;
    protected AudioSource m_pAudioSource;

    // Use this for initialization
    public virtual void Start()
    {
        m_pAudioSource = (AudioSource)gameObject.GetComponent("AudioSource");
        m_pAudioSource.playOnAwake = false;

        rigidbody.useGravity = false;

        collider.isTrigger = true;
    }

    // Update is called once per frame
    public abstract void Update();

    // The constructor for this class
    public virtual void Init(int nEnemyHealth, float fMoveSpeed, float fScale)
    {
        // How much health do we have
        m_nHitPoints = nEnemyHealth;

        // How fast are we going to move?
        m_fMoveSpeed = fMoveSpeed;

        // How big is this enemy?
        transform.localScale = new Vector3(fScale, fScale, fScale);
    }

    // React to taking damage
    public virtual void TakeDamage()
    {
        // Take a hit, return if still alive
        if(--m_nHitPoints > 0)
            return;

        // Otherwise this enemy is dead
        DestroyObject(gameObject);
    }
}

The code here has all been done before, with just one exception of a new keyword: ‘virtual’, ‘protected’, and ‘abstract’.

Early in this workshop we created a complex object by combining simpler pieces together, using data and the editor, now the process will be repeated but in a coding process known as ‘polymorphism‘ or “Many Forms”. The basic idea is that many complex objects are made from a similar base object, working with base objects is often easier and more flexible.

The study of birds is also known as “ornithology’

That is a simplified example if we were building a ‘polymorphism’ inheritance for a ‘Bird’ type in our game. The ‘basic class’ of bird defines the common features. They have legs, wings, eyes, feathers for example it would also have some basic functions.

    public virtual int GetNumWings() { return m_nNumWings; }
    public virtual bool CanFly() { return m_bCanFly; }
    public virtual enFoodType GetDesiredFood() { return m_enFoodType; }
    public abstract void MoveTo(Vector3 vLocation);

When asking a base bird if it can fly, the result is based on what ‘derived class’ it actually is. Notice that the ‘MoveTo’ function does nothing, this is because it is an ‘abstract’ function which means any derived class must implement a version of this function. In this case, all birds have to define how they will move to a location. For example:

public class Pigeon : Bird
{
    public override void MoveTo(Vector3 vLocation)
    {
        FlyTo(vLocation);
    }
}

public class Penguin : Bird
{
    public override void MoveTo(Vector3 vLocation)
    {
        SwimTo(vLocation);
    }
}

As you can see here, when asked to ‘MoveTo’ a location, the Pigeon which is a Bird type will call it’s ‘override’ function to decided to ‘FlyTo’ that location, while a Penguin would instead use the ‘SwimTo’ route. This will all become clearer as we add our enemy types. First a deadly space saw, create or import the ‘EnemySaw’ script.

using UnityEngine;
using System.Collections;

public class EnemySaw : Enemy
{
    // Local Variables
    Vector3 m_vDirection;
    Vector3 m_vMinBounds = new Vector3(-14.0f, 2.0f, 0.0f);
    Vector3 m_vMaxBounds = new Vector3(14.0f, 18.0f, 0.0f);

    // Use this for initialization
    public override void Start()
    {
        // Call the base class
        base.Start();

        // What do we look like?
        renderer.material = (Material)Resources.Load("Materials/SpaceSaw") as Material;
        gameObject.name = " EnemySaw";

        // Play our sound
        m_pAudioSource.clip = (AudioClip)Resources.Load("Sounds/SpaceSaw") as AudioClip;
        m_pAudioSource.loop = true;
        m_pAudioSource.Play();
    }

    // The constructor for this class
    public override void Init(int nEnemyHealth, float fMoveSpeed, float fScale)
    {
        // Call the base class
        base.Init(nEnemyHealth, fMoveSpeed, fScale);

        // Do this class's init
        // First find our direction towards the target
        GameObject pTarget = GameObject.Find("Player");
        m_vDirection = pTarget.transform.position - transform.position;
        m_vDirection.Normalize();
    }

    // Update is called once per frame
    public override void Update()
    {
        // Move towards the player, try to crash into them
        transform.Translate(m_fMoveSpeed * m_vDirection, Space.World);
        // This saw is always spinning
        transform.RotateAround(Vector3.forward, 30.0f);

        // Adjust our direction
        if((transform.position.x < m_vMinBounds.x && m_vDirection.x < 0) ||
           (transform.position.x > m_vMaxBounds.x && m_vDirection.x > 0))
        {
            m_vDirection.x *= -1;
        }
        if((transform.position.y < m_vMinBounds.y && m_vDirection.y < 0) ||
           (transform.position.y > m_vMaxBounds.y && m_vDirection.y > 0))
        {
            m_vDirection.y *= -1;
        }
    }

    // Collision reaction
    void OnTriggerEnter(Collider pOther)
    {
        // We only hit other players
        if(pOther.gameObject.GetComponent<PlayerMovement>() == null)
            return;

        // Do Damage then explode!
        pOther.BroadcastMessage("TakeDamage");
        Destroy(gameObject);
    }
}

A pretty hefty bit of code to go over, so starting at the top:

public class EnemySaw : Enemy

Our ‘EnemySaw’ is a derived class of ‘Enemy’ which means we have access to what the base class has to offer and can be treated as that class.

    // Use this for initialization
    public override void Start

Here you are using the ‘override’ keyword to tell the engine that this version is more important than the one in the base class and should be called instead of the version in the base class.

        // Call the base class
        base.Start();

This line is where a lot of the polymorphism cleverness shows, while we just said the ‘override’ version is more important than the base class version, we can still run the base class code by using the ‘base’ label.

         // Play our sound
        m_pAudioSource.clip = (AudioClip)Resources.Load("Sounds/SpaceSaw") as AudioClip;
        m_pAudioSource.loop = true;
        m_pAudioSource.Play();

At first you might not notice what is interesting about this part, until you see that ‘m_pAudioSource’ is not local variable we declared in this class. Remember the ‘protected’ keyword from before in the ‘Enemy’ class, by using that derive classes such as ‘EnemySaw’ can then access the variable in it’s base class. For a quick refresher:

  • private (or blank) – Can only be accessed by the class that defined the variable
  • protected – Can only be accessed by the class that defined it and any derived classes
  • public – Can be accessed by anything
        // First find our direction towards the target
        GameObject pTarget = GameObject.Find("Player");

The last bit of new concepts in this file, here we are finding a game object by the name given to it in the editor. After finding that we set a direction towards the player, and on the update call we move on that trajectory, and bounce off the bounds of the play area.

using UnityEngine;
using System.Collections;

public class EnemyPigeon : Enemy
{
    // Local Variables
    GameObject m_pTarget;

    // Use this for initialization
    public override void Start()
    {
        // Call the base class
        base.Start();

        // What do we look like?
        renderer.material = (Material)Resources.Load("Materials/SpacePigeon") as Material;
        gameObject.name = " EnemyPigeon";

        // Play our sound
        m_pAudioSource.clip = (AudioClip)Resources.Load("Sounds/SpacePigeon") as AudioClip;
        m_pAudioSource.Play();
    }

    // The constructor for this class
    public override void Init(int nEnemyHealth, float fMoveSpeed, float fScale)
    {
        // Call the base class
        base.Init(nEnemyHealth, fMoveSpeed, fScale);

        // Do this class's init
        m_pTarget = GameObject.Find("Player");
    }

    // Update is called once per frame
    public override void Update()
    {
        // Make sure we have a target
        if(m_pTarget == null)
        {
            DestroyObject(gameObject);
            return;
        }

        // First find our direction towards the target
        Vector3 vDirToTarget = m_pTarget.transform.position - transform.position;
        vDirToTarget.Normalize();

        // Move towards the player, try to crash into them
        transform.Translate(m_fMoveSpeed * vDirToTarget, Space.World);

        // Rotate to face our target
        float fHeading = Vector3.Dot(Vector3.right, vDirToTarget);
        Vector3 vNewRotation = transform.rotation.eulerAngles;
        vNewRotation.z = fHeading * 90.0f;

        // Adjust our rotation if we're on the bottom half of the input circle
        if(vDirToTarget.y > 0.0f)
        {
            vNewRotation.z = 180.0f - vNewRotation.z;    
        }

        // Apply the transform to the object        
        transform.rotation = Quaternion.Euler(vNewRotation);
    }

    // Collision reaction
    void OnTriggerEnter(Collider pOther)
    {
        // We only hit other players
        if(pOther.gameObject.GetComponent<PlayerMovement>() == null)
            return;

        // Do Damage then explode!
        pOther.BroadcastMessage("TakeDamage");
        Destroy(gameObject);
    }
}

Above is the class for the space pigeon, very similar to the space saw, however on it’s update function it will keep adjusting it’s velocity and rotation to head strait towards the player. Both the saw and the pigeon are of the ‘enemy’ type, but they behave differently.

It is true that with careful composition the same setup could have been created by using a common code for the appearance of an ‘enemy’ and separate code for the unique behaviors, it comes down to a lot of personal preference and organizational tastes, you’ll come across both frequently and it’s good to learn multiple ways to solve problems when making games.

Now that both enemy types are defined, there is just one part left to do. In the previous projects there was only one enemy that was reused over and over. That’s not enough to convey the terrible secrets of space that the player has to deal with. By creating a spawned for the non stop stream of enemies we can achieve that feeling.

Create an empty game object, placed at (0, 10, 0) and name it ‘Spawner’ in the object hierarchy, then create or import the ‘Spawner’ script.

 using UnityEngine;
using System.Collections;

public class Spawner : MonoBehaviour
{
    // Local Variables
    public int m_nEnemyHealth = 5;
    public float m_fEnemySpeed = 0.1f;
    public float m_fSpawnRadius = 20.0f;
    public float m_fEnemyScale = 2.0f;
    public float m_fSpawnDelay = 3.0f;
    float m_fAccumulatedSpawnTime = 0.0f;

    // Use this for initialization
    void Start ()
    {

    }

    // Update is called once per frame
    void Update ()
    {
        // Keep track of how much time has passed
        m_fAccumulatedSpawnTime += Time.deltaTime;
        while(m_fAccumulatedSpawnTime > m_fSpawnDelay)
        {
            // Decrement the time
            m_fAccumulatedSpawnTime -= m_fSpawnDelay;

            // Create a new enemy
            GameObject pNewObject = GameObject.CreatePrimitive(PrimitiveType.Cube);

            // Match the spawner's rotation and position
            pNewObject.transform.rotation = transform.rotation;
            pNewObject.transform.position = transform.position;
            // Rotate the new object randomly
            pNewObject.transform.RotateAround(Vector3.forward, Random.Range(0.0f, 360.0f));
            // Then push the object back the spawn offset distance
            pNewObject.transform.Translate(m_fSpawnRadius, 0.0f, 0.0f);

            // Set the behaviour on this new enemy
            Enemy pEnemy;
            if(Random.value > 0.5f)
            {
                pEnemy = (Enemy)pNewObject.AddComponent("EnemyPigeon");
            }
            else
            {
                pEnemy = (Enemy)pNewObject.AddComponent("EnemySaw");
            }
            pEnemy.Init(m_nEnemyHealth, m_fEnemySpeed, m_fEnemyScale);
        }
    }
}

This is the spawner script, only a few details to go over. There is another keyword here, ‘while’ a while loop is control for code flow. It does something while the condition in the parentheses are true, they also come in the ‘do-while’ flavor where the action is performed before checking the conditional. Inside of this while loop, after adjusting the accumulated time, it creates a new object and then pushes it out from it’s position in a random direction, then picks a enemy type to assign it.

Add this script to the ‘Spawner’ object, and make sure to uncomment the lines at the bottom of the ‘Projectile’ script (short-cut ‘ctrl+alt+c’) and your project should look like this. At this point it’s time for you to experiment with what you have learned, mix and match techniques from all the workshops and come up with your own end creations.

Unlike the previous workshops this one will have some additional appendices on ‘Keyboard Control’ and ‘Advanced Spawning’ that you are invited to look at.