Digital Arts Foundation—Final Project: Week 9 (Production)

In week 9, the final week of production, I aimed to implement the final set of mechanics that’d make something that could be realistically considered a game: shooting and health/damage for both player and enemy. I decided to make an attack script that would contain the partitioned code for both player and enemy attacks. The damage would meanwhile reside in the player and enemy scripts at the bottom for now.

So to start off with, I took the code from this page of the Unity Answers forum:

 void Fire() 
 {
 Rigidbody bulletClone = (Rigidbody) Instantiate(bullet, transform.position, transform.rotation);
 bulletClone.velocity = transform.forward * bulletSpeed;
 // You can also acccess other components / scripts of the clone
 //rocketClone.GetComponent<MyRocketScript>().DoSomething();
 }
 
 
 // Use this for initialization
 void Start () {
 
 }
 
 // Update is called once per frame
 void Update () {
     if (Input.GetButtonDown("Fire1"))
         Fire(); 
 
 }

Figure 1: Simple Bullet Script (Unity Answers Forums 2013)

It provided the basic mechanics for shooting. The two notable changes I had to do to get it working was adapt to 2D (RigidBody2D) and apply a rotation (transform.forward becoming Quaternion.Euler(0,0,90) * transform.right). Since I had zero experience in geometric-based scripting, it was really a case of trial and error to find the correct code. I understand the “right” part refers to the x-axis though, with “up” and “forward” referring to the y and z axes respectively.

I started writing the enemy shooting in a separate function, before deciding to combine them into one function that would mostly include the differences within the arguments. Although this approach definitely helped my understanding in the end, it perhaps made it harder to debug, since I had to understand the differences between the enemy and player code and make sure both work.

After two challenging days of tackling the problem, I eventually made the enemy fire from eight different points in the direction of where the player was. It was challenging to constrain the angles and make sure the co-domain (set of destination) values were not defined by looking 180 degrees either way, and instead a full 360 degrees.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class AttackScript : MonoBehaviour
{
    public Rigidbody2D bullet;
    private PlayerScript playerScript;
    private EnemyScript enemyScript;
    public float waitTime = 0;
    private int waitCount = 0, waitPoint = 3;

    void Start()
    {
        playerScript = FindObjectOfType<PlayerScript>();
        enemyScript = FindObjectOfType<EnemyScript>();
        waitTime = Random.Range(0f, 2f);
    }

    // Update is called once per frame.
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space) && gameObject.CompareTag("Player"))
        {
            Fire("Player", playerScript.runSpeed * 5, Vector2.zero, Quaternion.identity, 0.3f, 1);
        }
        if (gameObject.CompareTag("Enemy") && bullet != null)
        {
            // Assigns distance, scale and direction variables.

            float distance = Vector2.Distance(transform.position, playerScript.transform.position);
            float scale = transform.localScale.x;
            Vector2 direction = (playerScript.transform.position) - this.transform.position;

            if (distance < scale*6)
            {

                // Increments the enemy wait time.

                waitTime += Time.deltaTime;
                waitCount = Mathf.RoundToInt(waitTime);

                // Constrains the angle to pi/4 radians (i.e. to the 8 vertices of an octagon) and positions/rotates the bullets accordingly.

                float angle = Mathf.Atan2(direction.y, direction.x);
                float constrainedAngle = Mathf.RoundToInt(angle / (Mathf.PI / 4f)) * (Mathf.PI / 4f);
                Vector3 position = new Vector3(Mathf.Cos(constrainedAngle), Mathf.Sin(constrainedAngle), 0);
                Quaternion rotation = Quaternion.LookRotation(Vector3.forward, position);

                Fire("Enemy", enemyScript.runSpeed * 5, position, rotation, 0.5f, scale);
            }
        }
    }

    void Fire(string side, float bulletSpeed, Vector3 positionModifier, Quaternion rotationModifier, float expiry, float scale)
    {
        if (playerScript.activeColor != playerScript.colorWhite && (side != "Enemy" || waitCount >= waitPoint))
        {

            // Launch bullet.

            Rigidbody2D bulletClone = Instantiate(bullet, transform.position + positionModifier, rotationModifier * Quaternion.Euler(0, 0, 90) * transform.rotation);
            bulletClone.velocity = rotationModifier * Quaternion.Euler(0,0,90) * transform.right * bulletSpeed;

            // Destroy the bullet after a fixed amount of time.

            Destroy(bulletClone.gameObject, expiry*Mathf.Sqrt(scale));

            // Reset enemy wait.

            waitTime = Random.Range(0f, 2f);
            waitCount = 0;
        }
        else if (side == "Player")
        {
            StartCoroutine(playerScript.Flicker(playerScript.gameObject));
        }
    }
}

Figure 2: My attack script.

In my finalized script, you can additionally see RNG determining when the enemy fires, as well as bullet speed and scale modifiers.

Figure 3: Demonstration of the firing of bullets in my Unity project.

It’s worth pointing out that I modified my hexagon enemy shape into an octagon, so it would fire in the 8 (primary and secondary) compass directions. A standard hexagon by comparison could not fire from both the side and the top/bottom. It was around this time, that I also made the level space larger, to more easily navigate around the enemies as a player. I also made the enemy bullets appear darker.

The complementing part to the attacks is the damage dealt. To take multiple hits without dying, there also needs to be health points. This part was more of a case of just figuring out the precise logic I wanted rather than looking up Unity functions.

    if (obj.CompareTag("Bullet") && obj.layer == LayerMask.NameToLayer("Player Bullet"))
    {
        deathCounter++;

        if (obj != null && gameObject != null)
        {
            StartCoroutine(playerScript.Flicker(gameObject));
        }

        if (deathCounter == deathPoint)
        {
            if (obj != null && gameObject != null)
            {
                Destroy(gameObject);
            }
        }

Figure 4: Enemy Damage Code

For the enemy damage, the enemy would simply flicker if it got damaged but was still alive, and if the counter reached its “deathPoint”, basically its amount of health, the enemy game object would be destroyed. I also added the following code:

        if (deathCounter > 20)
        {
            transform.localScale = new Vector3(2, 2, 1);
            transform.position = Vector2.MoveTowards(this.transform.position, Player.transform.position, -runSpeed * Time.deltaTime);
        }

Figure 5: Enemy Shrink and Retreat Code

Essentially big enemies have bigger health, if they get hit 20 times, it shrinks and then moves away from the player instead of towards the player.

The player damage was a bit trickier to implement. I wanted the player to change to the white mode if it had 1 health left, which additionally complicated things.

void PlayerDamage(GameObject obj)
{
    if ((obj.CompareTag("Bullet") && obj.layer == LayerMask.NameToLayer("Enemy Bullet")) || obj.CompareTag("Enemy") && !pauseDamage)
    {
        deathCounter++;
        if (obj != null)
        {
            switch (deathCounter)
            {
                case 1:
                    FlickerOrange();
                    break;
                case 2:
                    FlickerOrange();
                    ChangeColor();
                    break;
                case 3:
                    // Game over event.
                    gameObject.SetActive(false);
                    FindObjectOfType<GameManager>().GameOver();
                    break;
            }
        }
    }
}

void FlickerOrange()
{
    pauseDamage = true;
    InvokeRepeating("SwitchOrange", 0.2f, 0.2f);
    Invoke("CancelSwitchOrange", 2f);
}

void SwitchOrange()
{
    SpriteRenderer render = GetComponent<SpriteRenderer>();
    Color orange = new Color(1f, 0.5f, 0f);
    if (render.color == activeColor)
    {
        render.color = orange;
    }
    else
    {
        render.color = activeColor;
    }
}

void CancelSwitchOrange()
{
    SpriteRenderer render = GetComponent<SpriteRenderer>();
    CancelInvoke("SwitchOrange");
    render.color = activeColor;
    pauseDamage = false;
}

Figure 6: Player Damage Code.

Since I wanted different behaviour on each hit, with simple instructions, it was a good time to use a switch statement. On the first hit, I just wanted it the player to flicker orange; on the second hit, I wanted it to flicker orange and revert to the white mode; on the final hit, I wanted game over to trigger.

I also implemented a temporary pausing of damage after getting hit. Getting the pausing and flickering to work was quite difficult, but eventually I found that the InvokeRepeating function did what I wanted. It would allow me to call the flickering/pausing function in a time intervals. I could then CancelInvoke when I was done.

Finally, I made Health consumables which would restore health upon the player colliding with them.

Figure 7: Player Damage demonstration in my Unity project.

Additionally I cleaned up the Game Manager script, by making repeated loops that deactivate objects into one function when the Game Over function is called.

void Deactivate(string tagName)
{
    GameObject[] gameObjects = tagName != "Others" ? GameObject.FindGameObjectsWithTag(tagName) : findOthers;
    {
        foreach (GameObject i in gameObjects)
        {
            if (i != null)
            {
                i.SetActive(false);
            }
        }
    }
}

Figure 8: Custom Deactivation Method

The function takes a tag name argument, unless it’s labelled “Others”. “Others” is an array of single game objects to loop through that’s identified earlier in the code. Otherwise, it just loops through the game objects with the tag, and deactivates them.

That now concludes the production. It was a long and hard effort, but I’m fairly satisified with the results.

Figure 9: Production Summary Video

Next week I’ll go into Post-Production, looking at getting feedback from users to determine my course of action.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s