Summary

In Jawbreakers, some great catastrophe caused every single adult to disappear. The children, of course, realized that this meant no more school, bedtimes or limits on candy, and proceeded to enjoy themselves every day. Jawbreakers is about a group of children that decided to move into Candy Land, and abandoned theme park. There they like to play a game to see who can collect the most candy in the allotted time, using wits, toy weapons, and a special map that lets them control the old machines, including bridges, carousels and a train ride.

Platform: PC
Engine: Unity
Language: C#
Development Time: 7 weeks
Team Size: 3 Designers & 6 Artists

Role

In this project, I was focused on scripting player mechanics, such as moving and attacking as well as various interactable objects in the game. I implemented the particle effects and did the sound- design and implementation as well as the music.

Slap

Each character has a stick that they can use to slap the other players. When a player is hit with a slap, they drop a candy. Each character has a unique particle effect that spawns when the stick is swung.

The stick has a capsule collider attached to it and when the capsule collides with another player, a force is applied to the other player in the attacker’s forward direction. To make the slap feel tight and fair, I used animation events to enable and disable the capsule collider.

Show Slap Code
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SlapScript : MonoBehaviour 
{
  public float slapCooldown;
  public float chargeTime;
  [Tooltip("Around 2000 and up")]
  public float slapForce;
  public PlayerAudioScript playerAudioScriptRef;
  public GameObject swingPlacement;
  public GameObject hitFX;
  public GameObject swingFX;
  public PlayerInput playerInputRef;
  Animator playerAnimator;
  GameObject player;
  CapsuleCollider colliderRef;
  MeshRenderer meshRendererRef;
  bool animationFinished = false;
  bool canSlap = true;
  void Start () 
  {
    playerInputRef = gameObject.transform.root.GetComponent ();
    player = gameObject.transform.root.Find("Player").gameObject;
    colliderRef = gameObject.transform.GetComponent ();
    meshRendererRef = gameObject.transform.GetComponent ();
    playerAnimator = gameObject.transform.root.Find ("Player").GetComponent ();
  }
    
  void Update () 
  {
    
        if (playerInputRef.joystickNum != 0 &&  !playerInputRef.pause)
        {
            if (Input.GetButtonDown("X_Button" + playerInputRef.joystickNum) && !LevelBrain.levelBrainInstance.pauseInputForAll)
            {
                if (playerAnimator != null && canSlap)
                {
                    canSlap = false;
          playerAnimator.SetTrigger ("Hit");
          playerAudioScriptRef.PlaySlapSwingSound ();
          //Spawn particle FX
          GameObject instance = Instantiate (swingFX, swingPlacement.transform.position, swingPlacement.transform.rotation);
          instance.transform.parent = swingPlacement.transform;
                }
            }
        }
  }
  void OnTriggerEnter(Collider other)
  {
    if (other.gameObject.CompareTag("Player")){
      //Add force to the other player's Rigidbod
      Rigidbody otherRB = other.gameObject.transform.GetComponent ();
      otherRB.AddForce (player.transform.forward * slapForce);
      //Drop a candy from the other player's backpack
      BackpackScript otherBackpack = other.transform.root.GetComponentInChildren ();
      otherBackpack.DropWhenSlapped();
      //Add a particle FX and play sound
      Instantiate(hitFX, other.transform.position, transform.rotation);
      playerAudioScriptRef.PlaySlapHitSound ();
            PlayerStats.playerStatsInstance.PlayerHitOtherPlayer(playerInputRef.joystickNum);  
    }
    if (other.gameObject.tag == "Pinata")
    {
      other.gameObject.GetComponent().SpitOutCandy(gameObject);
    }
    if (other.gameObject.tag == "CandyChest") 
    {                
            other.gameObject.GetComponent().DropCandy(playerInputRef.joystickNum);      
    }
  }
  // This method is triggered from an event in the animation
  public void Slap()
  {
    colliderRef.enabled = true;
  }
  // This method is triggered from an event in the animation
  public void SetAnimationFinished()
  {
    colliderRef.enabled = false;
    animationFinished = false;
    canSlap = true;
    }
  void OnEnable()
  {
    SetAnimationFinished ();
  }
}

Candy Spawner

I made a candy spawner so that the level designer easily could choose where candy should be spawned in the level. The spawner is a box that can be scaled, so the spawning area can be as big or small as you want and the spawn time and amount can be set in the prefab. Before a candy spawns, a random location within the box is chosen. From that location, a raycast checks if the location is a valid spawn point (if it’s on the ground). If not, another raycast is spawned. When the raycast hits a valid location, the candy is spawned.

Show Candy Spawner Code
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SpawnerWithBounds : MonoBehaviour 
{

  Collider boxCollider;
  Vector3 max;
  Vector3 min;
  Vector3 spawnLocation;
  Vector3 hitPoint;
  Vector3 rayDirection;
  List<GameObject> candyList = new List<GameObject> ();

  public GameObject candy;
  // How often candy should spawn
  public float spawnTimer;
  // The spawn limit
  public int maxSpawnedObjects;

  void Start () 
  {
    boxCollider = GetComponent<BoxCollider> ();
    max = boxCollider.bounds.max;
    min = boxCollider.bounds.min;
    rayDirection = new Vector3 (0, -1, 0);
    CheckSpawn ();
  }

  // Check if the raycast from the radom location is a valid spawn point. If now, do another raycast.
  void CheckSpawn()
  {

    spawnLocation = new Vector3 (Random.Range (min.x, max.x), max.y , Random.Range (min.z, max.z));
    RaycastHit hit;
    Physics.Raycast (spawnLocation, (rayDirection) * 20, out hit);
    Debug.DrawRay (spawnLocation, (rayDirection) * 20);

    if (hit.transform.gameObject.layer == 8 && hit.point.y < 0) 
    {
      hitPoint = hit.point;
      SpawnCandy ();
    } else {

      CheckSpawn ();
    }
  }
  void SpawnCandy()
  {
    // Spawn a candy if the max limit hasn't been reached and add it to the candyList
    if (candyList.Count < maxSpawnedObjects) 
    {
      GameObject spawnedCandy = Instantiate (candy, hitPoint, Quaternion.identity);
      candyList.Add (spawnedCandy);
    }

    // Check if any candy from the list has been picked up or removed from the game. If it has, remove it from the list.
    for (int i = 0; i < candyList.Count; i++) 
    
    {
      if (candyList[i] == null || candyList[i].gameObject.GetComponent<ResourcePlacementScript> ().pickedUp) {
        candyList.Remove (candyList[i]);
      }
    }

    Invoke ("CheckSpawn", spawnTimer);
  }
}

Merry-Go-Round

The Merry-Go-Round is placed in the center of the level. It’s spinning slowly, and players can walk on and pick up candy. However, players can activate it with their maps fast for a short period of time. When the Merry-Go-Round is sped up, a force is pushing both candy and players towards the edge. So, the players should be careful not to fall in the water!

Show Merry-Go-Round Code
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MerryGoRound : MonoBehaviour 
{
  public float speed;
  public float speedMultiplier;
  public float speedTime;
  public float power;
  public GameObject[] sparkParticles;
  public MerrySoundScript merrySoundScriptRef;

  float actualSpeed;
  float actualPower;	
  List<GameObject> playerList = new List<GameObject>();

    void Start () 
  {
    actualSpeed = speed;
    actualPower = 0;
    }

  void Update () 
  {
    transform.Rotate(new Vector3(0, actualSpeed, 0) * Time.deltaTime);
  }

  void OnTriggerEnter(Collider other)
  {
    if(other.CompareTag("Player") || other.CompareTag("Resource"))
    {
      // Making the object stick to the Merry-Go-Round
      other.transform.parent = transform;
      AddObjectToList (other.gameObject);
    }
  }

  void OnTriggerExit(Collider other)
  {
    if (other.CompareTag("Player") || other.CompareTag("Resource")) 
    {
      // Un-stick the object from the Merry-Go-Round
      other.transform.parent = null;
      RemoveObjectFromList (other.gameObject);
    } 
  }

  public void AddObjectToList(GameObject player)
  {
    playerList.Add(player);

  }

  public void RemoveObjectFromList(GameObject player)
  {
    playerList.Remove(player);

  }
    
  // This method is triggered when the player clicks on the Merry-Go-Round from the map
  public void PressedOnObject()
  {
    Invoke("SlowDown", speedTime);
    actualSpeed = (actualSpeed * -1) * speedMultiplier;
    actualPower = power;
    merrySoundScriptRef.spedUp = true;

    // Activate particleFX
    foreach (GameObject spark in sparkParticles) 
    {
      spark.SetActive (true);
    }
  }
    
  void FixedUpdate()
  {

    foreach (GameObject objectInList in playerList) 
    {
      if (objectInList == null) 
      {
        Destroy (objectInList);
        continue;
      }

      if (actualPower > 0) 
      {
        Vector3 pushDirection = (objectInList.transform.position - transform.position);
        float distanceToMiddle = (objectInList.transform.position - transform.position).magnitude;
        Rigidbody playerRB = objectInList.GetComponent<Rigidbody> ();
        Vector3 pushDirectionNoY = new Vector3 (pushDirection.x, 0, pushDirection.z);

        if (objectInList.tag == "Player") 
        {
          // Pushes the player to the edge of the Merry-Go-Round when it's sped up
          playerRB.AddForce (pushDirectionNoY.normalized * actualPower);

        } else {
          // Add less force to the candy
          playerRB.AddForce (pushDirectionNoY.normalized * (actualPower / 4));
        }
      }
    }
  }
    
  void SlowDown()
  {
    if(actualSpeed > 0)
    {
      actualSpeed = speed;
      actualPower = 0;
      foreach (GameObject spark in sparkParticles) 
      {
        spark.SetActive (false);
        merrySoundScriptRef.spedUp = false;
      }
    }
    else 
    {
      actualSpeed = speed * -1;
      actualPower = 0;
      foreach (GameObject spark in sparkParticles) 
      {
        spark.SetActive (false);
        merrySoundScriptRef.spedUp = false;
      }
    }
  }
}

Since the game is a four player split screen, I couldn’t implement sounds with specialization.  Although it is possible to have multiple Audio Listeners, I found it better to only use one, and to play every sound effect as 2D. This way I had more control of what was played and when. The sound from the Merry-Go-Round is therefore played whenever a player is inside a sphere collider placed on the Merry-Go-Round. And when it’s sped up, the sound effect is pitched up.

Show Merry-Sound Code
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MerrySoundScript : MonoBehaviour {

  List<GameObject> playerList = new List<GameObject>();
  public AudioSource merrySound;
  [HideInInspector]public bool spedUp;

  bool doOnceFadeIn;
  bool doOnceFadeOut;
  bool doOncePitchUp;
  bool doOncePitchDown;

  public float targetPitch;
  float currentVolume;
  float currentPitch;
  float t;
  float p;

  void OnTriggerEnter(Collider other)
  {
    if (other.tag == "Player") 
    {
      playerList.Add (other.gameObject);
    }	
  }

  void OnTriggerExit(Collider other)
  {
    if (other.tag == "Player") 
    {
      playerList.Remove (other.gameObject);
    }		
  }

  void Update () 
  {
    if (playerList.Count >= 1) 
    {
      FadeInMerrySound ();
    } else {
      FadeOutMerrySound ();
    }
    if (spedUp) 
    {
      PitchUp ();
    } else {
      PitchDown ();
    }

    for(int i = 0; i < playerList.Count; ++i)
    {
      if (playerList[i].activeSelf == false)
      {
        playerList.Remove(playerList[i]);
      }
    }
  }
    
  void FadeInMerrySound()
  {
    if (!doOnceFadeIn) 
    {
      doOnceFadeIn = true;
      currentVolume = merrySound.volume;
      t = 0;
    }
    t += 1 * Time.deltaTime;
    merrySound.volume = Mathf.Lerp (currentVolume, 1, t);
    doOnceFadeOut = false;
  }

  void FadeOutMerrySound()
  {
    if (!doOnceFadeOut) 
    {
      doOnceFadeOut = true;
      currentVolume = merrySound.volume;
      t = 0;
    }
    t += 1 * Time.deltaTime;
    merrySound.volume = Mathf.Lerp (currentVolume, 0, t);
    doOnceFadeIn = false;
  }

  void PitchUp()
  {
    if (!doOncePitchUp) 
    {
      doOncePitchUp = true;
      currentPitch = merrySound.pitch;
      p = 0;
    }
    p += 1 * Time.deltaTime;
    merrySound.pitch = Mathf.Lerp (currentPitch, targetPitch, p);
    doOncePitchDown = false;
  }

  void PitchDown()
  {
    if (!doOncePitchDown) 
    {
      doOncePitchDown = true;
      currentPitch = merrySound.pitch;
      p = 0;
    }
    p += 1 * Time.deltaTime;
    merrySound.pitch = Mathf.Lerp (currentPitch, 1, p);
    doOncePitchUp = false;
  }
}

Music

To capture the feeling of the characters being “bad kids” running around and doing what they want, I went with a rock/punk theme. I kept it pretty soft to not take over too much.

Screenshots