Back

TchopeTchope

TchopeTchope

3D arcade game where the player must avoid the carrots sliding across and the knifes smashing in a cutting board arena as they collect the most sushi pieces they can by touching them.

The goal

I wanted, as a challenge, to finish a 3D programming project under a 24h period. It was made in Unity.

The result is a very simple arcade game with 12 scripts that can be classified under 4 categories: the gameplay object scripts, the pool scripts, the audio scripts and the UI scripts.

Gameplay object scripts

Under this category, we have 5 scripts: the player, the point, the knife, the carrot, and the almighty offense leader, who throws the knife and carrot instances at the player, while the poor player tries to rescue the points.

Player

Let's start with the player script, as it is directly influenced by the player. It keeps track of the player lives and points. It also updates the little player maki object position according to the player inputs.

Player.cs
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
///<summary>
///    Class <c>Player</c> represents and manages a player character with help of a character controller.
///</summary>
[RequireComponent(typeof(CharacterController))]
public class Player : MonoBehaviour
{
    #region Attributes
    [SerializeField]
    [Tooltip("Player speed multiplier.")]
    private float _PlayerSpeed = 2.0f;
    [SerializeField]
    [Tooltip("Max highness of jump.")]
    private float _JumpHeight = 1.0f;
    [SerializeField]
    [Tooltip("Constant force towards -y axis.")]
    private float _Gravity = -9.81f;
    [SerializeField]
    [Tooltip("Move smooth damp speed.")]
    private float _SmoothMoveSpeed = 0.3f;
    [SerializeField]
    [Tooltip("Number of lives before the player dies.")]
    [Range(1,9)]
    private int _MaxNumOfLives = 3;
    [Header("HUD Listeners")]
    [SerializeField]
    [Tooltip("Score listener that updates UI.")]
    private Listener _ScoreListener;
    [SerializeField]
    [Tooltip("Lives listener that updates UI.")]
    private Listener _LivesListener;
    [Header("Game Over Screen")]
    [SerializeField]
    private GameObject _GameOverScreen;
    
    private int _NumberOfPlayerLives;
    public int NumberOfPlayerLives
    {
        get {return _NumberOfPlayerLives;}
    }
    private int _NumberOfMakis = 0;
    public int NumberOfMakis
    {
        get {return _NumberOfMakis;}
    }
    private CharacterController _Controller;
    private Vector3 _PlayerYVelocity;
    private bool _GroundedPlayer;
    private Vector2 _CurrentMoveVector;
    private Vector2 _TargetMoveVector;
    private Vector2 _SmoothMoveVelocity;
    private bool _JumpToDo = false;
    private bool _IsAlive = true;
    public bool IsAlive
    {
        get {return _IsAlive;}
    }
    private Vector3 _StartPos;
    #endregion
    #region UnityEvents
    void Awake()
    {
        _Controller = GetComponent<CharacterController>();
    }
    void Start()
    {
        _NumberOfPlayerLives = _MaxNumOfLives;
        _StartPos = new Vector3(gameObject.transform.position.x, gameObject.transform.position.y, gameObject.transform.position.z);
    }
    void Update()
    {
        // Checks for flat movement.
        _GroundedPlayer = IsGrounded();
        _CurrentMoveVector = Vector2.SmoothDamp(_CurrentMoveVector, _TargetMoveVector, ref _SmoothMoveVelocity, _SmoothMoveSpeed);
        var move = new Vector3(_CurrentMoveVector.x, 0, _CurrentMoveVector.y);
        _Controller.Move(move * (Time.deltaTime * _PlayerSpeed));
        // Checks for jumps.
        if (_GroundedPlayer && _JumpToDo)
        {
            _JumpToDo = false;
            _PlayerYVelocity.y += Mathf.Sqrt(_JumpHeight * -0.01f * _Gravity);
            AudioManager.Instance.PlayJumpSFX();
        }
        _PlayerYVelocity.y += _Gravity * Time.deltaTime;
        _Controller.Move(_PlayerYVelocity * Time.deltaTime);
    }
    #endregion
    #region Methods
    ///<summary>
    ///    Event receiver <c>OnMove</c> sets up the flat movement the way the player wants.
    ///</summary>
    ///<param name="context">
    ///    Callback context for the player movement.
    ///</param>
    public void OnMove(InputAction.CallbackContext context)
    {
        // Started context doubles the message so we don't need it.
        if (context.started)
        {
            return;
        }
        _TargetMoveVector = context.action.ReadValue<Vector2>();
    }
    
    ///<summary>
    ///    Event receiver <c>OnJump</c> is called when player wants to jump.
    ///</summary>
    ///<param name="context">
    ///    Callback context for the player jump.
    ///</param>
    public void OnJump(InputAction.CallbackContext context)
    {
        if (context.started)
        {
            _JumpToDo = true;
        }
    }
    ///<summary>
    ///    Method <c>OnGrabAMaki</c> will increment the maki counter.
    ///</summary>
    public void OnGrabAMaki()
    {
        _NumberOfMakis += 1;
        _ScoreListener.UpdateUI(NumberOfMakis);
    }
    ///<summary>
    ///    Method <c>OnLoseALife</c> is utils for decreasing life counter and flagging death.
    ///</summary>
    public void OnLoseALife()
    {
        _NumberOfPlayerLives -= 1;
        _LivesListener.UpdateUI(NumberOfPlayerLives);
        if (_NumberOfPlayerLives <= 0) Die();
    }
    ///<summary>
    ///    Method <c>ResetPlayer</c> is utils for restarting the game.
    ///</summary>
    public void ResetPlayer()
    {
        _NumberOfPlayerLives = _MaxNumOfLives;
        _LivesListener.UpdateUI(NumberOfPlayerLives);
        _NumberOfMakis = 0;
        _ScoreListener.UpdateUI(NumberOfMakis);
        _CurrentMoveVector = new Vector3(0, 0, 0);
        _TargetMoveVector = new Vector3(0, 0, 0);
        _PlayerYVelocity = new Vector3(0, 0, 0);
        gameObject.transform.position = new Vector3(_StartPos.x, _StartPos.y, _StartPos.z);
        gameObject.SetActive(true);
        _GameOverScreen.SetActive(false);
        _IsAlive = true;
    }
    ///<summary>
    ///    Method <c>Die</c> will stop the game and prompt the game over menu.
    ///</summary>
    void Die()
    {
        _IsAlive = false;
        _GameOverScreen.SetActive(true);
        gameObject.SetActive(false);
    }
    ///<summary>
    ///    Accessor <c>IsGrounded</c> to get groundedness state.
    ///</summary>
    private bool IsGrounded()
    {
        _GroundedPlayer = _Controller.isGrounded;
        // Player will not fall when player is grounded.
        if (_GroundedPlayer && _PlayerYVelocity.y < 0)
        {
            _PlayerYVelocity.y = 0f;
        }
        return _GroundedPlayer;
    }
    #endregion
}

Offense Leader

The offense leader has 2 modes that get triggered in alternance. The first mode is the knife rain, during which knifes will fall from the sky. The second mode is the carrot swipe, during which carrots slide ominously across the board. Both modes are telegraphed to the player using visual cues.

OffenseLeader.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
///<summary>
///    Class <c>OffenseLeader</c> represents the opponent side and manages the different knife and carrot attacks.
///</summary>
public class OffenseLeader : MonoBehaviour
{
    #region Attributes
    [SerializeField]
    [Tooltip("An array of all the carrots that can interfere with the board.")]
    private Carrot[] _Carrots;
    [SerializeField]
    [Tooltip("The Pool Manager that can be paused, responsible of all the knife rain.")]
    private PausableHintPoolManager _KnifeRain;
    [SerializeField]
    [Tooltip("The Pool Manager of the makis.")]
    private PoolManager _MakiPool;
    [SerializeField]
    [Tooltip("Player.")]
    protected Player _Player;
    [SerializeField] 
    [Tooltip("Knife rain duration.")]
    private float _KnifeDuration = 8f;
    [SerializeField]
    [Tooltip("Carrot Time duration.")]
    private float _CarrotDuration = 8f;
    [SerializeField]
    [Tooltip("Carrot in between delay.")]
    private float _CarrotDelay = 0.5f;
    [SerializeField]
    [Tooltip("Number of carrots to move.")]
    private int _CarrotMoveNumber = 5;
    private float _TimeElapsedSince = 0f;
    private bool _IsRainTime = true;
    #endregion
    #region UnityEvents
    void Update()
    {
        _TimeElapsedSince += Time.deltaTime;
        if (_IsRainTime && _TimeElapsedSince > _KnifeDuration && _Player.IsAlive)
        {
            _KnifeRain.IsPaused = true;
            _IsRainTime = false;
            LaunchCarrots();
            _TimeElapsedSince = 0;
        }
        else if (!_IsRainTime && _TimeElapsedSince > _CarrotDuration && _Player.IsAlive)
        {
            _IsRainTime = true;
            _KnifeRain.ResumeAfterPause();
            _TimeElapsedSince = 0;
        }
        if (!_Player.IsAlive)
        {
            _TimeElapsedSince = 0;
            _IsRainTime = true;
            _KnifeRain.IsPaused = true;
        }
    }
    #endregion
    ///<summary>
    ///    Method <c>LaunchCarrots</c> throws a bunch of carrots onto the player; they are slightly delayed.
    ///</summary>
    #region Methods
    private void LaunchCarrots()
    {
        for (int i = 0;i < _Carrots.Length;++i)
        {
            int randomIndex = Random.Range(0, _Carrots.Length -1);
            Carrot temp = _Carrots[randomIndex];
            _Carrots[randomIndex] = _Carrots[i];
            _Carrots[i] = temp;
        }
        StartCoroutine(LaunchCarrotsWithDelay());
    }
    ///<summary>
    ///    Method <c>LaunchCarrotsWithDelay</c> is the coroutine responsible of the delay between carrots.
    ///</summary>
    private IEnumerator LaunchCarrotsWithDelay()
    {
        for (int i = 0;i < System.Math.Min(_Carrots.Length, _CarrotMoveNumber);++i)
        {
            _Carrots[i].Move();
            yield return new WaitForSeconds(_CarrotDelay);
        }
    }
    ///<summary>
    ///    Method <c>ResetGame</c> is utils for reseting the whole game.
    ///</summary>
    public void ResetGame()
    {
        foreach (Carrot car in _Carrots)
        {
            car.ResetPosition();
        }
        _Player.ResetPlayer();
        _MakiPool.ResumeAfterPause();
    }
    #endregion
}

Knife

The knife is a pool object that gets recycled over and over again. It manages a little hint, a small red cross, indicating where it's about to fall.

Knife.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
///<summary>
///    Class <c>Knife</c> represents collision to remove life from player.
///</summary>
public class Knife : PoolObject
{
    #region Attributes
    [SerializeField]
    [Tooltip("Initial offset Y from where the knife will fall.")]
    private float _OffsetYInit = 0.3f;
    [SerializeField]
    [Tooltip("Final offset Y where the knife will stop.")]
    private float _OffsetYFinal = -0.3f;
    [SerializeField]
    [Tooltip("Falling duration.")]
    private float _Duration = 0.1f;
    [Header("Visual Cue Cross Values")]
    [SerializeField]
    [Tooltip("Cross time of apparition before knife hits.")]
    private float _CrossDuration = 0.3f;
    [SerializeField]
    [Tooltip("Cross initial alpha channel.")]
    private float _CrossAlphaInit = 15f;
    [SerializeField]
    [Tooltip("Cross final alpha channel.")]
    private float _CrossAlphaFinal = 255f;
    private GameObject _Cross;
    public GameObject Cross
    {
        set {_Cross = value;}
    }
    #endregion
    #region Methods
    ///<summary>
    ///    Event receiver for <c>OnCollisionEnter</c> with a player will decrement player counter lives.
    ///</summary>
    void OnCollisionEnter(Collision collision)
    {
        if (collision.gameObject.layer == _LayerMaskPlayer) 
        {
            collision.gameObject.GetComponent<Player>().OnLoseALife();
            if (_PoolID >= 0)
            {
                _PM.SetIsFree(_PoolID);
            }
            EraseCross();
            gameObject.SetActive(false);
            AudioManager.Instance.PlayHitSFX();
        }
    }
    ///<summary>
    ///    Method <c>Initiate</c> will set the knife in motion so it gives a bit of time for the player to react.
    ///</summary>
    public override void Initiate()
    {
        // Cross apparition
        _Cross.transform.position = gameObject.transform.position;
        LeanTween.value( gameObject, UpdateValueSpriteRendererCallback, _CrossAlphaInit, _CrossAlphaFinal, _CrossDuration).setEase(LeanTweenType.easeOutElastic);
        // Knife movement
        gameObject.transform.rotation = Quaternion.Euler(0, Random.Range (0, 360), 0);
        float finalPos = gameObject.transform.position.y + _OffsetYFinal;
        gameObject.transform.position = new Vector3(gameObject.transform.position.x, gameObject.transform.position.y + _OffsetYInit, gameObject.transform.position.z);
        LeanTween.moveY( gameObject, finalPos, _Duration).setEase( LeanTweenType.easeInQuad ).setDelay(_CrossDuration).setOnComplete(EraseCross);
    }
    ///<summary>
    ///    Method <c>UpdateValueSpriteRendererCallback</c> joyous little callback to set alpha.
    ///</summary>
    private void UpdateValueSpriteRendererCallback( float val )
    {
        SpriteRenderer sr = _Cross.GetComponent<SpriteRenderer>();
        if (sr != null)
        {
            Color tmp = sr.color;
            tmp.a = val;
            sr.color = tmp;
        }
    }
    ///<summary>
    ///    Method <c>EraseCross</c> make sure the cross won't be seen after the knife disappears.
    ///</summary>
    private void EraseCross()
    {
        SpriteRenderer sr = _Cross.GetComponent<SpriteRenderer>();
        if (sr != null)
        {
            Color tmp = sr.color;
            tmp.a = 0;
            sr.color = tmp;
        }
    }
    #endregion
}

Carrot

The carrots are permanently spawned during the game. They use tweening to animate their swipes.

Carrot.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
///<summary>
///    Class <c>Carrot</c> represents collision to remove life from player.
///</summary>
public class Carrot : MonoBehaviour
{
    #region Attributes
    private int _LayerMaskPlayer;
    [SerializeField]
    [Tooltip("Initial position in X/Z.")]
    private float _InitPos = 0.3f;
    [SerializeField]
    [Tooltip("Final position in X/Z.")]
    private float _FinalPos = 0.3f;
    [SerializeField] 
    [Tooltip("Movement duration.")]
    private float _Duration = 6f;
    [SerializeField]
    [Tooltip("Buffer before tt starts moving.")]
    private float _PreMoveDelay = 0.3f;
    [SerializeField]
    [Tooltip("Is Moving On X (true) or Z (false)")]
    private bool _MovingOnX = true;
    private bool _IsComingBack = false;
    private float _RotationAmount = 360;
    private float _DisableCollisionsFor = 2f;
    private bool _AreCollisionsEnabled = true;
    private Vector3 _StartPos;
    #endregion
    #region UnityEvents
    void Start()
    {
        _LayerMaskPlayer = LayerMask.NameToLayer("Player");
        _StartPos = new Vector3(gameObject.transform.position.x, gameObject.transform.position.y, gameObject.transform.position.z);
    }
    #endregion
    #region Methods
    ///<summary>
    ///    Event receiver for <c>OnTriggerEnter</c> with a player will decrement player counter lives.
    ///</summary>
    void OnTriggerEnter(Collider collision)
    {
        if (collision.gameObject.layer == _LayerMaskPlayer && _AreCollisionsEnabled) 
        {
            AudioManager.Instance.PlayHitSFX();
            collision.gameObject.GetComponent<Player>().OnLoseALife();
            _AreCollisionsEnabled = false;
            StartCoroutine(EnableCollisions());
        }
    }
    private IEnumerator EnableCollisions()
    {
        yield return new WaitForSeconds(_DisableCollisionsFor);
        _AreCollisionsEnabled = true;
    }
    ///<summary>
    ///    Method <c>Move</c> will set the carrot in rotation so it gives a bit of time for the player to react.
    ///</summary>
    public void Move()
    {
        float destination = _FinalPos;
        if (_IsComingBack)
        {
            destination = _InitPos;
        }
        Vector3 rotateAxis = Vector3.forward;
        if (_MovingOnX)
        {
            rotateAxis = Vector3.forward;
        }
        // Rotation
        LeanTween.rotateAroundLocal( gameObject, rotateAxis, _RotationAmount, _PreMoveDelay);
        // Carrot movement
        if (_MovingOnX)
        {
            LeanTween.moveX( gameObject, destination, _Duration).setEase( LeanTweenType.easeInQuad ).setDelay(_PreMoveDelay);
        }
        else
        {
            LeanTween.moveZ( gameObject, destination, _Duration).setEase( LeanTweenType.easeInQuad ).setDelay(_PreMoveDelay);
        }
        _IsComingBack = !_IsComingBack;
    }
    ///<summary>
    ///    Method <c>ResetPosition</c> allows to replace carrot when retry game.
    ///</summary>
    public void ResetPosition()
    {
        gameObject.transform.position = new Vector3(_StartPos.x, _StartPos.y, _StartPos.z);
    }
    #endregion
}

Point

This script is attached to the little maki objects that the player have to collect. It's only keeping track of the collision.

Point.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
///<summary>
///    Class <c>Point</c> represents collision to add point to player score.
///</summary>
public class Point : PoolObject
{
    #region Methods
    ///<summary>
    ///    Event receiver for <c>OnCollisionEnter</c> with a player will increment player counter and free the maki.
    ///</summary>
    void OnCollisionEnter(Collision collision)
    {
        if (collision.gameObject.layer == _LayerMaskPlayer) 
        {
            collision.gameObject.GetComponent<Player>().OnGrabAMaki();
            if (_PoolID >= 0)
            {
                _PM.SetIsFree(_PoolID);
            }
            AudioManager.Instance.PlayMakiSFX();
            gameObject.SetActive(false);
        }
    }
    #endregion
}

This concludes the gameplay object scripts. Given more time, there would have been a bit more tinkering around the movement and the player feedback. Notably, the player movement feels very slidy and the collisions with the knifes or carrots are only signified by a UI update and a sound.

Pool

Our second category regroups the pool-related scripts.

Pool Manager

This manager is in charge of all the object pools in the game. It instanciates the objects of each pool, and enables/disables them when requested.

PoolManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
///<summary>
///    Class <c>PoolManager</c> represents a pool of objects that are created and recycled during the game.
///</summary>
[RequireComponent(typeof(BoxCollider))]
public class PoolManager : MonoBehaviour
{
    #region Attributes
    protected List<GameObject> _Pool = new List<GameObject>();
    protected List<int> _FreePoolObjects = new List<int>();
    [SerializeField]
    [Tooltip("The number of objects that will be instantiated and recycled.")]
    protected int _NumberOfPoolObjects = 20;
    [SerializeField]
    [Tooltip("The type of object populating the pool.")]
    protected GameObject _ObjectToInstantiate;
    [SerializeField]
    [Tooltip("Minimal time (in seconds) before the object returns to the pool.")]
    protected float _SecondsBeforeReturn;
    [SerializeField]
    [Tooltip("Pace at which the PM tries to take an object out of the pool.")]
    protected float _SecondsBetweenApparition;
    [SerializeField]
    [Tooltip("Fuziness in seconds of the pacing.")]
    protected float _FuzinessInSeconds;
    [SerializeField]
    [Tooltip("Player.")]
    protected Player _Player;
    [SerializeField]
    [Tooltip("Box collider of the PM.")]
    protected BoxCollider _MyCollider;
    #endregion
    #region UnityEvents
    protected void Start()
    {
        if(_ObjectToInstantiate != null)
        {
            InstantiateObjects();
        }
        StartCoroutine(ApparitionLoop());
    }
    #endregion
    #region Methods
    ///<summary>
    ///    Method <c>InstantiateObjects</c> will create _NumberOfPoolObjects of _ObjectToInstantiate, set inactive and store in the _Pool.
    ///</summary>
    protected virtual void InstantiateObjects()
    {
        for (int i = 0; i < _NumberOfPoolObjects; i++)
        {
            GameObject myObj = Instantiate(_ObjectToInstantiate);
            PoolObject pt = myObj.GetComponent<PoolObject>();
            if (pt != null)
            {
                pt.PoolID = i;
                pt.PM = this;
            }
            myObj.SetActive(false);
            _Pool.Add(myObj);
            _FreePoolObjects.Add(i);
        }
    }
    ///<summary>
    ///    Method <c>ApparitionLoop</c> will make the objects appear as they are needed in the game.
    ///</summary>
    protected virtual IEnumerator ApparitionLoop()
    {
        while (_Player.IsAlive && _Pool.Count > 0)
        {
            yield return new WaitForSeconds(Random.Range(_SecondsBetweenApparition, _SecondsBetweenApparition + _FuzinessInSeconds));
            TrySpawnOneObject();
        }
    }
    ///<summary>
    ///    Method <c>TrySpawnOneObject</c> spawn one of the object in any is available in the pool.
    ///</summary>
    protected void TrySpawnOneObject()
    {
        if (_FreePoolObjects.Count > 0)
        {
            int objPoolID = _FreePoolObjects[0];
            _FreePoolObjects.RemoveAt(0);
            _Pool[objPoolID].transform.position = GetPositionInSpawnZone();
            _Pool[objPoolID].SetActive(true);
            PoolObject pt = _Pool[objPoolID].GetComponent<PoolObject>();
            if (pt != null)
            {
                pt.Initiate();
            }
            StartCoroutine(WaitBeforeSettingFree(_Pool[objPoolID]));
        }
    }
    ///<summary>
    ///    Method <c>GetPositionInSpawnZone</c> gets a starting position in the spawn zone.
    ///</summary>
    protected Vector3 GetPositionInSpawnZone()
    {
        return new Vector3(
            Random.Range(_MyCollider.bounds.min.x, _MyCollider.bounds.max.x),
            Random.Range(_MyCollider.bounds.min.y, _MyCollider.bounds.max.y),
            Random.Range(_MyCollider.bounds.min.z, _MyCollider.bounds.max.z));
    }
    ///<summary>
    ///    Method <c>WaitBeforeSettingFree</c> recycles the pool objects when it's been a while and they are still active.
    ///</summary>
    protected IEnumerator WaitBeforeSettingFree(GameObject obj)
    {
        yield return new WaitForSeconds(Random.Range(_SecondsBeforeReturn, _SecondsBeforeReturn + _FuzinessInSeconds));
        if (obj.activeSelf)
        {
            PoolObject pt = obj.GetComponent<PoolObject>();
            if (pt != null)
            {
                SetIsFree(pt.PoolID);
            }
            obj.SetActive(false);
        }
    }
    ///<summary>
    ///    Method <c>SetIsFree</c> recycles the pool objects when something happened before their inner timer ran out.
    ///</summary>
    public void SetIsFree(int objPoolID)
    {
        if (!_FreePoolObjects.Contains(objPoolID))
        {
          _FreePoolObjects.Add(objPoolID);
        }
    }
    ///<summary>
    ///    Method <c>ResumeAfterPause</c> restart the ApparitionLoop when the pause is over.
    ///</summary>
    public virtual void ResumeAfterPause()
    {
        StartCoroutine(ApparitionLoop());
    }
    #endregion
}

Pausable Hint Pool Manager

Along with the pool manager, I also made a variant that can handle pool objects with their hint, just like the knife has.

PausableHintPoolManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
///<summary>
///    Class <c>PausableHintPoolManager</c> represents a pool of objects that are created and recycled during the game with a little extra element.
///    There is a hint added to this Pool (extra object associated with each pool object) that needs to appear at a precise spot at the same time.
///</summary>
public class PausableHintPoolManager : PoolManager
{
    #region Attributes    
    public bool IsPaused = false;
    [Header("Hint -- Visual Cue")]
    protected List<GameObject> _HintPool = new List<GameObject>();
    [SerializeField]
    [Tooltip("The type of object populating the hint pool.")]
    protected GameObject _HintToInstantiate;
    #endregion
    #region Methods
    ///<summary>
    ///    Method <c>InstantiateObjects</c> will create _NumberOfPoolObjects of _ObjectToInstantiate, set inactive and store in the _Pool.
    ///</summary>
    protected override void InstantiateObjects()
    {
        for (int i = 0; i < _NumberOfPoolObjects; i++)
        {
            GameObject myObj = Instantiate(_ObjectToInstantiate);
            GameObject myHint = Instantiate(_HintToInstantiate);
            PoolObject pt = myObj.GetComponent<PoolObject>();
            if (pt != null)
            {
                pt.PoolID = i;
                pt.PM = this;
                Knife nife = myObj.GetComponent<Knife>();
                if (nife != null)
                {
                    nife.Cross = myHint;
                }
            }
            myObj.SetActive(false);
            _Pool.Add(myObj);
            _HintPool.Add(myHint);
            _FreePoolObjects.Add(i);
        }
    }
    ///<summary>
    ///    Method <c>ApparitionLoop</c> will make the objects appear as they are needed in the game.
    ///</summary>
    protected override IEnumerator ApparitionLoop()
    {
        while (_Player.IsAlive && _Pool.Count > 0 && !IsPaused)
        {
            yield return new WaitForSeconds(Random.Range(_SecondsBetweenApparition, _SecondsBetweenApparition + _FuzinessInSeconds));
            TrySpawnOneObject();
        }
    }
    ///<summary>
    ///    Method <c>ResumeAfterPause</c> restart the ApparitionLoop when the pause is over.
    ///</summary>
    public override void ResumeAfterPause()
    {
        if (IsPaused)
        {
            IsPaused = false;
            StartCoroutine(ApparitionLoop());
        }
    }
    #endregion
}

Pool Object

This is the base class for the objects that can be found in a pool. The knife and the point are such objects.

PoolObject.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
///<summary>
///    Class <c>PoolObject</c> represents a pool object that can be initiated and that is linked to a PoolManager.
///</summary>
public class PoolObject : MonoBehaviour
{
    #region Attributes
    protected int _LayerMaskPlayer;
    protected int _PoolID = -1;
    protected PoolManager _PM;
    public int PoolID
    {
        get {return _PoolID;}
        set {_PoolID = value;}
    }
    public PoolManager PM
    {
        set {_PM = value;}
    }
    #endregion
    #region UnityEvents
    void Start()
    {
        _LayerMaskPlayer = LayerMask.NameToLayer("Player");
    }
    #endregion
    #region Methods
    ///<summary>
    ///    Method <c>Initiate</c> will initiate whatever the pool object needs to do when set active.
    ///</summary>
    public virtual void Initiate()
    {
    }
    #endregion
}

Pools were not necessary here, given the size of the project. Nonetheless, I really like them, they usually make for a cleaner project. Also, I had a little extra time during my 24h and felt compelled to add that feature.

UI and Audio

I will combine the last 2 categories here, as they are considerably smaller and implement very simple material.

This file handles the few buttons that can be found on the main menu, as well as the background music and sfx associated to pressing a button.

MainMenu.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement; 
public class MainMenu : MonoBehaviour
{
    public void Play()
    {
        SceneManager.LoadScene(1);
        AudioManager.Instance.PlayButtonSFX();
        AudioManager.Instance.StartInGameMusic();
    }
    public void Options()
    {
        AudioManager.Instance.PlayButtonSFX();
    }
    public void BackToMain()
    {
        AudioManager.Instance.PlayButtonSFX();
    }
    public void Menu()
    {
        SceneManager.LoadScene(0);
        AudioManager.Instance.PlayButtonSFX();
        AudioManager.Instance.StartMenuMusic();
    }
    public void Quit()
    {
        AudioManager.Instance.PlayButtonSFX();
        Application.Quit();
        Debug.Log("quit");
    }
}

OptionsMenu

The options menu contains a few buttons/sliders to adjust the volumes.

OptionsMenu.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class OptionsMenu : MonoBehaviour
{
    public void UpdateMusicVolume(float volume)
    {
        AudioManager.Instance.MusicVolume(volume);
    }
    public void UpdateSFXVolume(float volume)
    {
        AudioManager.Instance.SFXVolume(volume);
    }
}

Listener

Basic UI helper to convert the data format to the display format.

Listener.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;
///<summary>
///    Class <c>Listener</c> represents a TMP update.
///</summary>
public class Listener : MonoBehaviour
{
    #region Attributes
    [SerializeField]
    [Tooltip("String stored in textmeshpro.")]
    private string _StringPrompt = " Score : ";
    [SerializeField]
    [Tooltip("Textmeshpro object.")]
    private TextMeshProUGUI _TMP;
    #endregion
    #region Methods
    ///<summary>
    ///    Method <c>UpdateUI</c> receives a int and updates its own text value.
    ///</summary>
    public void UpdateUI(int value)
    {
        _TMP.text = _StringPrompt + value.ToString();
    }
    #endregion
}

Audio Manager

This convenient singleton is used to trigger a sound across the game. It also plays the music and adjust the volume settings.

AudioManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
///<summary>
///    Singleton class <c>AudioManager</c> represents the audio source interface.
///</summary>
public class AudioManager : MonoBehaviour
{
    #region Attributes
    public static AudioManager Instance;
    public Sound[] Songs, Sounds;
    public AudioSource Source,SFXSource;
    #endregion
    #region UnityEvents
    private void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
            DontDestroyOnLoad(gameObject);
        }
        else
        {
            Destroy(gameObject);
        }
    }
    #endregion
    #region Methods
    ///<summary>
    ///    Method <c>StartMenuMusic</c> start a song during menu scene.
    ///</summary>
    public void StartMenuMusic()
    {
        PlayMusic("Theme");
    }
    ///<summary>
    ///    Method <c>StartInGameMusic</c> start a song during game scene.
    ///</summary>
    public void StartInGameMusic()
    {
        PlayMusic("Theme2");
    }
    ///<summary>
    ///    Method <c>PlayMusic</c> start a song by given name.
    ///</summary>
    public void PlayMusic(string name)
    {
        Sound s = Array.Find(Songs, x => x.name == name);
        if (s == null)
        {
            Debug.Log("Song not found!");
        }
        else
        {
            Source.clip = s.clip;
            Source.Play();
        }
    }
    public void PlayButtonSFX()
    {
        PlaySFX("Button");
    }
    public void PlayHitSFX()
    {
        PlaySFX("Hit");
    }
    public void PlayJumpSFX()
    {
        PlaySFX("Jump");
    }
    public void PlayMakiSFX()
    {
        PlaySFX("Maki");
    }
    ///<summary>
    ///    Method <c>PlaySFX</c> start a sfx by given name.
    ///</summary>
    public void PlaySFX(string name)
    {
        Sound s = Array.Find(Sounds, x => x.name == name);
        if (s == null)
        {
            Debug.Log("Sound not found!");
        }
        else
        {
            SFXSource.PlayOneShot(s.clip);
        }
    }
    ///<summary>
    ///    Method <c>MusicVolume</c> helps updating volume for music.
    ///</summary>
    public void MusicVolume(float volume)
    {
        Source.volume = volume;
    }
    ///<summary>
    ///    Method <c>SFXVolume</c> helps updating volume for sfx.
    ///</summary>
    public void SFXVolume(float volume)
    {
        SFXSource.volume = volume;
        PlayButtonSFX();
    }
    #endregion
}

Readme.md

Finally, here's a little extra: the readme.

Readme.md
# TCHOPETCHOPE
TCHOPETCHOPE is a 3D arcade game where you control a salmon sushi (wasd to move, space bar to jump) moving around a cutting board. The goal is to accumulate as many cucumber makis (touch them to get them) while avoiding the knifes falling from above or the carrots sliding across the cutting board. You have three lives to do so, use them well!
Use the mouse to get around the menu. Unity version 2021.3.16.f1. I used the package Input System for player input. 
## Credits:
Skybox by Fantasy Skybox FREE - Render Knight
Obstacle patterns by LeanTween - Dented Pixel
Environment assets by Free Low Poly Nature Forest - Pure Poly
Smack Boom font by  Bangkit Tri Setiadi (dafont)
## Music by Roa:
Nightfall by Roa https://soundcloud.com/roa_music1031
Creative Commons — Attribution 3.0 Unported — CC BY 3.0
Free Download / Stream: https://bit.ly/al-nightfall
Music promoted by Audio Library https://youtu.be/WAD2veumgYc
Sleepless Nights by Roa https://soundcloud.com/roa_music1031
Creative Commons — Attribution 3.0 Unported — CC BY 3.0
Free Download / Stream: https://bit.ly/al-sleepless-nights
Music promoted by Audio Library https://youtu.be/ybRUMGomV5E
## Main Assets
Carrots, cutting board, sushi, makis, knifes and buttons by me.