Category Archives: Appendices

Appendix: Custom Spawning

In the 3rd workshop, we spawned two types of enemies but ended up using the same values for health, speed, and size for them. That greatly limited what could be done gameplay wise, in this appendix we will revisit the spawning code and take a deeper dive into what can be done.

Spawner.cs

using UnityEngine;
using System.Collections;

public class Spawner : MonoBehaviour
{
    // Local Variables
    [System.Serializable]
    public class EnemyType
    {
        public string m_strFriendlyName = "None";
        public string m_strEnemyType = "None";
        public int m_nEnemyHealth = 5;
        public float m_fEnemySpeed = 0.1f;
        public float m_fEnemyScale = 2.0f;
    }

    public float m_fSpawnRadius = 20.0f;
    public float m_fSpawnDelay = 3.0f;
    public EnemyType[] m_aEnemyTypes;
    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
            int nEnemyType = Random.Range(0, m_aEnemyTypes.Length);
            Enemy pEnemy = (Enemy)pNewObject.AddComponent(m_aEnemyTypes[nEnemyType].m_strEnemyType);
            pEnemy.Init(m_aEnemyTypes[nEnemyType].m_nEnemyHealth, m_aEnemyTypes[nEnemyType].m_fEnemySpeed, m_aEnemyTypes[nEnemyType].m_fEnemyScale);
        }
    }
}

Two areas have changed here, looking at the top first, you will see that the variables that were shared by the enemies have been grouped together into a new class ‘EnemyType’. This is a subclass, that exists within the ‘Spawner’ class, it is not a MonoBehavior like all of the previous classes up until this point. It is being used basically to group a collection of data elements. Subclasses are often interchanged with the ‘struct‘ type depending on the situation and code language, however in our case here using subclasses are easier with the Unity object inspector and will be our default choice.

     // Local Variables
    [System.Serializable]
    public class EnemyType
    {
        public string m_strFriendlyName = "None";
        public string m_strEnemyType = "None";
        public int m_nEnemyHealth = 5;
        public float m_fEnemySpeed = 0.1f;
        public float m_fEnemyScale = 2.0f;
    }

    public float m_fSpawnRadius = 20.0f;
    public float m_fSpawnDelay = 3.0f;
    public EnemyType[] m_aEnemyTypes;
    float m_fAccumulatedSpawnTime = 0.0f;

This class is prefixed with the label ‘[System.Serializable]’, ‘Serialization‘ is a fancy way of saying the code can be saved. A new variable was added the ‘m_aEnemyTypes’ which is an array of enemies for the spawner to pick from.

             // Set the behaviour on this new enemy
            int nEnemyType = Random.Range(0, m_aEnemyTypes.Length);
            Enemy pEnemy = (Enemy)pNewObject.AddComponent(m_aEnemyTypes[nEnemyType].m_strEnemyType);
            pEnemy.Init(m_aEnemyTypes[nEnemyType].m_nEnemyHealth, m_aEnemyTypes[nEnemyType].m_fEnemySpeed, m_aEnemyTypes[nEnemyType].m_fEnemyScale);

Then at the bottom of the code a random element from the array is picked and all the data from that element is used to craft the enemy. Notice that the ‘Random.Range’ function works different with integer values than with floats, by being ‘exclusive’ with the max value which means it won’t be selected random. This is very handy with arrays because the length is always one higher than the indexing would allow.

Take a look at it in the editor to see the difference:

Super Space Pigeon is beyond compare

Here we have created our two enemy types from before, the Space Saw and the Space Pigeon, however a third type was setup ‘Super Space Pigeon’ that reuses the normal Space Pigeon behavior but has different configuration data. Give it a try right here.

Appendix: Keyboard Turning

In the 3rd workshop we took the assumption that the player has a dual stick controller for granted. In a real situation, that may not be the case. A small adaptation to the code needs to be made to support multiple input configurations.

PlayerMovement.cs

     // Update is called once per frame
    void Update ()
    {
        // We are going to read the input every frame
        Vector3 vNewInput = new Vector3();
        if(Input.GetJoystickNames().Length > 0)
        {
            vNewInput.Set(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"), 0.0f);
        }
        // Use keyboard check if no joystick is connected
        else
        {
            // First the 'y' input
            if(Input.GetKey(KeyCode.Q) || Input.GetKey(KeyCode.W) || Input.GetKey(KeyCode.E))
                vNewInput.y = 1.0f;
            else if(Input.GetKey(KeyCode.Z) || Input.GetKey(KeyCode.X) || Input.GetKey(KeyCode.C))
                vNewInput.y = -1.0f;
            // Then the 'x' input
            if(Input.GetKey(KeyCode.Q) || Input.GetKey(KeyCode.A) || Input.GetKey(KeyCode.Z))
                vNewInput.x = -1.0f;
            else if(Input.GetKey(KeyCode.E) || Input.GetKey(KeyCode.D) || Input.GetKey(KeyCode.C))
                vNewInput.x = 1.0f;
        }
        ...
    }

PlayerAiming.cs

    // Update is called once per frame
    void Update ()
    {
        // We are going to read the input every frame
        Vector3 vNewInput = new Vector3();
        if(Input.GetJoystickNames().Length > 0)
        {
            vNewInput.Set(Input.GetAxis("Horizontal_R"), Input.GetAxis("Vertical_R"), 0.0f);
        }
        // Use keyboard check if no joystick is connected
        else
        {
            // First the 'y' input
            if(Input.GetKey(KeyCode.Keypad7) || Input.GetKey(KeyCode.Keypad8) || Input.GetKey(KeyCode.Keypad9))
                vNewInput.y = -1.0f;
            else if(Input.GetKey(KeyCode.Keypad1) || Input.GetKey(KeyCode.Keypad2) || Input.GetKey(KeyCode.Keypad3))
                vNewInput.y = 1.0f;
            // Then the 'x' input
            if(Input.GetKey(KeyCode.Keypad7) || Input.GetKey(KeyCode.Keypad4) || Input.GetKey(KeyCode.Keypad1))
                vNewInput.x = -1.0f;
            else if(Input.GetKey(KeyCode.Keypad9) || Input.GetKey(KeyCode.Keypad6) || Input.GetKey(KeyCode.Keypad3))
                vNewInput.x = 1.0f;    
        }

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

PlayerShooting.cs

     // Update is called once per frame
    void Update ()
    {
        // We are going to read the input every frame
        if(Input.GetJoystickNames().Length > 0)
        {
            // Check if our aim stick is in use
            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;
            }
        }
        else
        {
            // Check if any of our aim keys are in use
            bool bShoot = Input.GetKey(KeyCode.Keypad1) || Input.GetKey(KeyCode.Keypad2) || Input.GetKey(KeyCode.Keypad3) ||
                          Input.GetKey(KeyCode.Keypad4) || Input.GetKey(KeyCode.Keypad6) ||
                          Input.GetKey(KeyCode.Keypad7) || Input.GetKey(KeyCode.Keypad8) || Input.GetKey(KeyCode.Keypad9);

            if(!bShoot)
            {
                // Reset the shooting timing if we stop shooting
                m_fAccumulatedFireTime = 0.0f;
                return;
            }
        }
        ...
    }

All three of files were modified to include the following:

 if(Input.GetJoystickNames().Length > 0)

This call to the input system will return a list with the names of the joysticks attached to the system, if at anytime the list is empty it will instead read the keyboard status directly and map the left nine letters and they number pad to be moving and shooting.

Swap the files link here for the ones in workshop 3 to try it out. A demo of this working, and it will switch modes while running if you add or remove joysticks.

Alternatively, you can also bind the numpad keys in project’s input manager like this:

The key to good input

The [ ] square brackets are how you specify keys on the number pad, this will also work. As always, there are multiple ways to solve any problem when making games.