Skip to content

Instantly share code, notes, and snippets.

@polygonalcube
Last active January 17, 2026 21:02
Show Gist options
  • Select an option

  • Save polygonalcube/eb8e893689cdf0506bc228438345ac49 to your computer and use it in GitHub Desktop.

Select an option

Save polygonalcube/eb8e893689cdf0506bc228438345ac49 to your computer and use it in GitHub Desktop.
Junior Project: Lunch Break (Unity, C#)

Lunch Break is a first-person shooter where office supplies are weapons. As Programming Lead, my job was to enforce consistency & maintanability amongst the 30+ scripts shared by 6 programmers. I wrote 11 scripts (shown here) for this Unity project.

I also created the player controller & shooting functionality. The repo isn't available, as this project was made with Perforce.

using UnityEngine;
public class AmmoComponent : MonoBehaviour
{
/// <summary>
/// A component that manages an ammo variable.
///
/// The AmmoComponent will usually be seen on ranged weapons.
/// /// - Joshua
/// </summary>
/// <remarks>
/// Changes to make:
/// - Make "UseAmmo()" the only means of mutating "ammoLeft", to boost reasonability
/// - - If reload functionality is desired, a "Reload()" function can also mutate "ammoLeft"
///
/// - Joshua
/// </remarks>
private int ammoLeft = 10;
public int GetAmmoLeft()
{
return ammoLeft;
}
public bool IsEmpty()
{
return ammoLeft <= 0;
}
public int UseAmmo(int amountOfAmmoToUse = 1)
{
if (!IsEmpty())
{
ammoLeft -= amountOfAmmoToUse;
}
return ammoLeft;
}
}
using UnityEngine;
[RequireComponent(typeof(Collider))]
public class HitComponent : MonoBehaviour
{
/// <summary>
/// A component that acts as a hitbox.
///
/// The HitComponent handles interaction with objects that possess HPComponents. If an interaction takes place, the
/// current health of the HPComponent is decremented by a specified amount.
/// /// - Joshua
/// </summary>
public bool isActive = true;
public int hitStrength = 1;
private void OnTriggerEnter(Collider otherCollider)
{
if (isActive && otherCollider.TryGetComponent<HPComponent>(out HPComponent hpComponent))
{
hpComponent.ChangeHealth(-hitStrength);
}
}
}
using System;
using UnityEngine;
using UnityEngine.Events;
public class HPComponent : MonoBehaviour
{
/// <summary>
/// A component that manages the vitality of the object that possess it.
///
/// The HPComponent acts as the health of the object. If health reaches zero, an appropriate & specified action
/// takes place.
/// /// - Joshua
/// </summary>
/// <remarks>
/// Changes to make:
/// - Remove the EnemyAi field
/// - - As it stands, the component references & mutates the user, which is fundamentally incorrect, and makes both
/// - - parties tougher to reason about & debug
/// - Make "health" read only to improve reasonability
///
/// - Joshua
/// </remarks>
[Range(0, 200)]
public int startingHealth = 100;
[Range(0, 200)]
public int maxHealth = 100;
private int health;
enum BehaviorsOnDeath {Deactivate, Delete, Nothing}
[SerializeField] BehaviorsOnDeath behaviorOnDeath = BehaviorsOnDeath.Delete;
public event EventHandler OnDeath;
private void Start()
{
health = startingHealth;
}
public int GetHealth()
{
return health;
}
public int ChangeHealth(int changeInHealth)
{
health += changeInHealth;
if (IsDead())
{
Die();
}
if (health > maxHealth)
{
health = maxHealth;
}
return health;
}
public bool IsAlive()
{
return health > 0;
}
public bool IsDead()
{
return health <= 0;
}
void Die()
{
if (behaviorOnDeath == BehaviorsOnDeath.Deactivate)
{
gameObject.SetActive(false);
}
else if (behaviorOnDeath == BehaviorsOnDeath.Delete)
{
Destroy(gameObject);
}
}
}
using UnityEngine;
public class HurtComponent : MonoBehaviour
{
/// <remarks>
/// To see use after Build 1.
/// </remarks>
private HPComponent healthManager;
public LayerMask dangerousLayers;
public bool isActive = true;
private void Awake()
{
healthManager = GetComponent<HPComponent>();
}
void OnTriggerEnter(Collider otherCollider)
{
if (isActive && dangerousLayers.Contains(otherCollider) &&
otherCollider.gameObject.TryGetComponent<HitComponent>(out HitComponent hitbox))
{
healthManager?.ChangeHealth(hitbox.hitStrength);
}
}
}
using UnityEngine;
//[RequireComponent(typeof(CharacterController))]
public class MoveComponent : MonoBehaviour
{
/// <summary>
/// A component to move entities in a sans-physical manner.
///
/// If a character controller is present on this component's GameObject, movement will be driven by that, else
/// standard transform manipulation is utilized.
///
/// - Joshua
/// </summary>
private CharacterController characterController;
public bool alwaysMoving = false;
public bool affectedByGravity = true;
public float jumpHeight = 2f;
public float jumpGravity = -1f;
public float fallGravity = -2f;
public float acceleration = 1f;
public float deceleration = 1f;
private float currentSpeedX, currentSpeedY, currentSpeedZ;
public Vector3 maximumSpeed = new(10f, 999f, 10f);
public Vector3 moveDirection = new Vector3(0f, 0f, 0f);
void Awake()
{
characterController = GetComponent<CharacterController>();
}
void Update()
{
if (alwaysMoving)
{
Move();
}
}
public void Accelerate(ref float speedVar, float direction)
{
speedVar += (acceleration * 100f) * direction;
}
public void Decelerate(ref float speedVar)
{
speedVar += (deceleration * 100f) * Mathf.Sign(-speedVar);
if (Mathf.Abs(speedVar) <= (deceleration * 100f))
{
speedVar = 0f;
}
}
public void Cap(ref float speedVar, float speedCap, float direction)
{
bool maxSpeedExceeded = Mathf.Abs(speedVar) > speedCap;
bool maxSpeedWithDirectionalLimiterExceeded = (Mathf.Abs(speedVar) > speedCap * Mathf.Abs(direction)) &&
(direction != 0f);
if (maxSpeedWithDirectionalLimiterExceeded)
{
speedVar = speedCap * direction;
}
else if (maxSpeedExceeded)
{
speedVar = speedCap * Mathf.Sign(direction);
}
}
public void Jump()
{
currentSpeedY = Mathf.Abs(Mathf.Sqrt((jumpHeight * 100f) * -2f * jumpGravity));
}
public void Move(/*Vector3 moveDirection = moveDirection, */bool isGrounded = false)
{
if (Mathf.Abs(moveDirection.x) != 0f)
{
Accelerate(ref currentSpeedX, moveDirection.x);
}
else
{
Decelerate(ref currentSpeedX);
}
if (!affectedByGravity)
{
if (Mathf.Abs(moveDirection.y) != 0f)
{
Accelerate(ref currentSpeedY, moveDirection.y);
}
else
{
Decelerate(ref currentSpeedY);
}
}
if (Mathf.Abs(moveDirection.z) != 0f)
{
Accelerate(ref currentSpeedZ, moveDirection.z);
}
else
{
Decelerate(ref currentSpeedZ);
}
if (affectedByGravity)
{
if (currentSpeedY > 0)
{
currentSpeedY += (jumpGravity * 100f) * Time.deltaTime;
}
else
{
currentSpeedY += (fallGravity * 100f) * Time.deltaTime;
}
}
if (isGrounded)
{
currentSpeedY = 0f;
}
if (moveDirection.y >= 1f && affectedByGravity && isGrounded)
{
Jump();
}
Cap(ref currentSpeedX, maximumSpeed.x, moveDirection.x);
if (affectedByGravity)
{
Cap(ref currentSpeedY, maximumSpeed.y, -1f);
}
else
{
Cap(ref currentSpeedY, maximumSpeed.y, moveDirection.y);
}
Cap(ref currentSpeedZ, maximumSpeed.z, moveDirection.z);
if (characterController != null)
{
characterController.Move(new Vector3(currentSpeedX, currentSpeedY, currentSpeedZ) * Time.deltaTime);
}
else
{
transform.position += new Vector3(currentSpeedX, currentSpeedY, currentSpeedZ) * Time.deltaTime;
}
}
}
using FMOD.Studio;
using System;
using System.Collections;
using TMPro;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.SceneManagement;
[RequireComponent(typeof(HPComponent))]
[RequireComponent(typeof(MoveComponent))]
[RequireComponent(typeof(CharacterController))]
public class PlayerBase : MonoBehaviour
{
/// <summary>
/// The "main" player character script. The player character "brain".
///
/// - Joshua
/// </summary>
/// <remarks>
/// The idea is that this script should house the logic & behavior that other player scripts shouldn't care about.
///
/// Sliding movement has been moved to here. Once the player has a state machine, sliding can be given its own code
/// block or separate script, but for Build 1, this should do.
///
/// - Joshua
/// </remarks>
[Header("Components")]
private HPComponent healthManager;
public MoveComponent mover;
public MoveComponent moverSlide;
private Sliding playerSliding;
[Header("Input")]
public InputAction move;
public InputAction dash;
public InputAction jump;
public InputAction look;
public InputAction pickUp;
public InputAction shoot;
[HideInInspector] public Vector2 movementDirection = Vector2.zero;
[HideInInspector] public Vector2 lookDirection = Vector2.zero;
public Transform cameraTransform;
[HideInInspector] public CharacterController characterController;
public Transform groundCheckTransform;
public float groundCheckRadius = 0.5f;
public LayerMask groundLayer;
[Header("Weapon Display")]
[HideInInspector] public AmmoComponent weaponAmmoComponent;
[HideInInspector] public TextMeshProUGUI ammoText;
[HideInInspector] public TextMeshProUGUI healthText;
[Header("Audio")]
private EventInstance playerFootsteps;
void OnEnable()
{
move.Enable();
dash.Enable();
jump.Enable();
look.Enable();
pickUp.Enable();
shoot.Enable();
}
void OnDisable()
{
move.Disable();
dash.Disable();
jump.Disable();
look.Disable();
pickUp.Disable();
shoot.Disable();
}
private void Awake()
{
healthManager = GetComponent<HPComponent>();
characterController = GetComponent<CharacterController>();
if (cameraTransform == null)
{
cameraTransform = GameObject.FindGameObjectWithTag("MainCamera").transform;
}
playerSliding = GetComponent<Sliding>();
}
void Start()
{
healthManager.OnDeath += ResetSceneIfDead;
//playerFootsteps = AudioManager.instance.CreateInsance(FMODEvents.instance.playerFootsteps);
}
void Update()
{
ReceiveInput();
Movement();
DisplayStats();
}
void FixedUpdate()
{
UpdateSound();
}
public bool IsGrounded()
{
return Physics.CheckSphere(groundCheckTransform.position, groundCheckRadius, groundLayer);
}
void ReceiveInput()
{
lookDirection = look.ReadValue<Vector2>();
movementDirection = move.ReadValue<Vector2>();
}
void Movement()
{
Vector3 movementInput = Vector3.zero;
float inputX = movementDirection.x;
float inputZ = movementDirection.y;
print("inputX: " + inputX + " inputZ: " + inputZ);
movementInput = cameraTransform.right * inputX + transform.forward * inputZ;
if (jump.triggered && IsGrounded())
{
movementInput = new(movementInput.x, 1f, movementInput.z);
}
mover.moveDirection = movementInput;
mover.Move(IsGrounded());
if (playerSliding.isSliding)
{
moverSlide.Move();
}
}
void DisplayStats()
{
if (ammoText == null)
{
ammoText = GameObject.Find("Ammo Text").GetComponent<TextMeshProUGUI>();
}
if (weaponAmmoComponent != null && ammoText != null)
{
ammoText.text = "Ammo: " + weaponAmmoComponent.GetAmmoLeft();
}
else if (ammoText != null)
{
ammoText.text = "Ammo: 0";
}
if (healthText == null)
{
healthText = GameObject.Find("Health Text").GetComponent<TextMeshProUGUI>();
}
if (healthManager != null && healthText != null)
{
healthText.text = "Health: " + healthManager.GetHealth();
}
else if (healthText != null)
{
healthText.text = "Health: 0";
}
}
void ResetSceneIfDead(object sender, EventArgs args)
{
GameManager.gameManager.ReloadScene();
}
private void UpdateSound()
{
//if the player is grounded and not sliding and moving play footsteps
if (IsGrounded() && !playerSliding.isSliding && movementDirection != Vector2.zero)
{
PLAYBACK_STATE playbackState;
playerFootsteps.getPlaybackState(out playbackState);
if (playbackState.Equals(PLAYBACK_STATE.STOPPED))
{
playerFootsteps.start();
}
}
else
{
playerFootsteps.stop(STOP_MODE.ALLOWFADEOUT);
}
}
private void OnDrawGizmos()
{
Gizmos.DrawWireSphere(groundCheckTransform.position, groundCheckRadius);
}
}
using UnityEngine;
public interface RangedWeapon
{
/// <summary>
/// An interface that denotes ranged weapons, & defines common methods for weapons.
///
/// - Joshua
/// </summary>
public void Drop();
public void PickUp(GameObject parent);
public void Throw(Vector3 throwDirection);
public void Shoot();
public void PrepareToShoot();
public void PrepareToStop();
}
using UnityEngine;
[RequireComponent(typeof(Rigidbody))]
public class RigidbodyHelperComponent : MonoBehaviour
{
/// <summary>
/// Helper methods for objects with Rigidbodies.
///
/// - Joshua
/// </summary>
/// <remarks>
/// Changes to make:
/// - Rename to "PhysicsComponent"
///
/// - Joshua
/// </remarks>
public void ActivateRigidbody()
{
GetComponent<Rigidbody>().detectCollisions = true;
GetComponent<Rigidbody>().isKinematic = false;
}
public void DectivateRigidbody()
{
GetComponent<Rigidbody>().detectCollisions = false;
GetComponent<Rigidbody>().isKinematic = true;
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(SpawnerComponent))]
public class ShooterComponent : MonoBehaviour
{
[SerializeField] private SpawnerComponent spawnerComponent;
private float shotDelay;
public float shotDelaySet;
public float projectileSpeed;
public Transform target;
void Start()
{
shotDelay = 0f;
}
void Update()
{
shotDelay -= Time.deltaTime;
}
public GameObject Shoot()
{
if (shotDelay > 0f)
{
return null;
}
shotDelay = shotDelaySet;
GameObject newProjectile = spawnerComponent.Spawn(transform.position);
if (newProjectile == null)
{
return null;
}
if (newProjectile.TryGetComponent<MoveComponent>(out MoveComponent mover))
{
mover.alwaysMoving = true;
mover.moveDirection = transform.forward;
}
return newProjectile;
}
}
using System.Collections;
using UnityEngine;
public class SpawnerComponent : MonoBehaviour
{
[SerializeField] private GameObject spawnee;
[SerializeField] private LayerMask spawneeLayer;
[SerializeField] private float destroyAfterSeconds = 0f;
public GameObject Spawn(Vector3 spawnPosition, GameObject parent = null)
{
//Spawns spawnee at spawnPosition, returns the gameObject Instance
GameObject newObject = Instantiate(spawnee, spawnPosition, Quaternion.identity);
if (parent != null)
{
newObject.transform.SetParent(parent.transform);
}
if (destroyAfterSeconds > 0f)
{
StartCoroutine(Despawn(newObject));
}
return newObject;
}
IEnumerator Despawn(GameObject despawnee)
{
yield return new WaitForSeconds(destroyAfterSeconds);
Destroy(despawnee);
}
}
using UnityEngine;
public static class UnityExtensions
{
public static bool Contains(this LayerMask layerMask, int layer)
{
return (layerMask.value & (1 << layer)) != 0;
}
public static bool Contains(this LayerMask layerMask, GameObject gameObject)
{
return (layerMask.value & (1 << gameObject.layer)) != 0;
}
public static bool Contains(this LayerMask layerMask, Collider collider)
{
return (layerMask.value & (1 << collider.gameObject.layer)) != 0;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment