Sun's Out Guns Out

Unity

Project Overview

  • Project Type: Unity 3D Game
  • Team Size: 5
  • Role: Gameplay Programmer, VFX & In-Game UI Development
  • Software: Unity, VS Code & Photoshop
  • Languages: C#
  • Download link: Play Game
  • Programming Reel: Watch

Project Brief

This is a third person "shooter" game where you play as a bodybuilder defending Venice Beach from an alien invasion. The player uses correctly timed poses and flexes to deflect enemy projectiles and eliminate threat.


Responsibilities/Achievements

  • - Program every player mechanic in the game from basic movement, sliding, throwing tanning oil grenades and other objects scattered around the level
  • - Develop the bullet deflection system
  • - Implement camera controls and effects
  • - Develop a Game Event system based on Scriptable Objects to decouple game modules
  • - Create a throwable objects system allowing a better game design
  • - Implement in-game Heads-up Display and Pause UI
  • - Create game's particle effects
  • - Implement player character animation state machine using motion captura animation data

Code Samples


/*
* Carlos Adan Cortes De la Fuente
* All rights reserved. Copyright (c)
* Email: dev@carlosadan.com
*/
												
[RequireComponent(typeof(CapsuleCollider))]
public abstract class BulletDeflector : MonoBehaviour {
	
	[SerializeField]
	private PlayerState playerState;
	[SerializeField]
	private BooleanVariable disableInput;
	[SerializeField]
	private FloatVariable stamina;
	[SerializeField]
	public EnumVariable bulletTypeToDeflect;
	[SerializeField]
	private FloatVariable mainTrigger;
	[SerializeField]
	private FloatVariable otherTrigger;
	[SerializeField]
	private BooleanVariable active;
	[SerializeField]
	private BooleanVariable otherTriggerActiveState;
	[SerializeField]
	private DeflectorProperties deflectorProperties;
	[SerializeField]
	private GameEvent OnPerfectDeflection;
	[SerializeField]
	private GameEvent OnPoseStarted;
	[SerializeField]
	private GameEvent addScore;
	[SerializeField]
	private float minScoreAddition = 100f;
	[SerializeField]
	private float maxScoreAddition = 250f;


	private Camera mainCamera;
	private CapsuleCollider coll;
	private float maxRadius;
	private bool mainTriggerPreviousState = false;
	private bool otherTriggerPreviousState = false;
	private float startDeflectionTimestamp = 0f;

	private void Awake()
	{
		mainCamera = Camera.main;
		coll = GetComponent<CapsuleCollider>();
		maxRadius = coll.radius;
		playerState.airDeflect = false;
	}

	private void FixedUpdate() {
		if (disableInput.Value || playerState.isPlayerDead) { active.Value = false; return; };
		
		active.Value = 	playerState.sliding == false && 
						playerState.throwingObject == false && 
						playerState.throwingGrenade == false && 
						playerState.meleeing == false && 
						stamina.Value > 0f && 
						((mainTrigger.Value != 0 && otherTrigger.Value == 0) || (mainTrigger.Value != 0 && otherTrigger.Value != 0 && mainTriggerPreviousState)) && 
						otherTriggerActiveState.Value == false && otherTriggerPreviousState == false;
						
		// First frame of deflection
		if (active.Value && mainTriggerPreviousState == false) {
			OnPoseStarted.Raise(this);
			startDeflectionTimestamp = Time.time;
			if (playerState.jumping || playerState.grounded == false) playerState.airDeflect = true;
		}

		// The frame the player let go off the trigger
		if (active.Value == false && mainTriggerPreviousState == true) {
			playerState.airDeflect = false;
		}	

		coll.enabled = active.Value;
		playerState.deflecting = active.Value || otherTriggerActiveState.Value;
		mainTriggerPreviousState = active.Value;
		otherTriggerPreviousState = otherTriggerActiveState.Value;
	}

	private void EvaluateDeflection(Bullet bullet)
	{
		if (Time.time - deflectorProperties.fixedTimeWindow > startDeflectionTimestamp) {
			bullet.DeflectRandomly();
			return;
		}
		
		float distance = Vector3.Magnitude(bullet.transform.position - transform.position);
		if (distance <= deflectorProperties.maxDistanceToPerfectDeflection) {
			distance = Mathf.Clamp(distance, deflectorProperties.minDistanceToPerfectDeflection, deflectorProperties.maxDistanceToPerfectDeflection);
			float multiplier = 1f -distance.Normalize(deflectorProperties.minDistanceToPerfectDeflection, deflectorProperties.maxDistanceToPerfectDeflection);
			bullet.DeflectPerfeclty(multiplier);
			OnPerfectDeflection.Raise(multiplier);
			addScore.Raise(multiplier.Denormalize(minScoreAddition, maxScoreAddition));
		} else {
			bullet.DeflectRandomly();
		}
	}

	private void OnTriggerEnter(Collider other) {
		Bullet bullet = other.gameObject.GetComponent<Bullet>();
		if (bullet != null && bullet.bulletType == bulletTypeToDeflect && bullet.bouncedByPlayer == false) {
			EvaluateDeflection(bullet);
		}
	}
}												
										

/*
* Carlos Adan Cortes De la Fuente
* All rights reserved. Copyright (c)
* Email: dev@carlosadan.com
*/

[RequireComponent(typeof(LineRenderer))]
public class LaunchArch : MonoBehaviour
{

	[SerializeField]
	private GameObject explosionIndicator;
	[SerializeField]
	private BooleanVariable showArch;
	[SerializeField]
	private FloatVariable throwingForce;
	[SerializeField]
	private LayerMask layerMask = -1;

	private GameObject indicatorInstance = null;
	public int maxPositionCount = 1024;

	private LineRenderer lr;

	private void Awake()
	{
		lr = GetComponent<LineRenderer>();
		indicatorInstance = Instantiate(explosionIndicator, transform.position, Quaternion.identity);
		indicatorInstance.SetActive(false);
	}

	private void Update()
	{
		if (showArch.Value == false)
		{
			lr.positionCount = 0;
			if (indicatorInstance.activeInHierarchy) indicatorInstance.SetActive(false);
		}
		else
		{
			DrawArch();
		}
	}

	private void DrawArch()
	{
		// Initialize Values
		float velocity = throwingForce.Value;
		Vector3 currentVelocity = transform.forward * velocity;
		Vector3 currentPosition = transform.position;

		lr.positionCount = maxPositionCount;

		int index = 0;
		bool keepDrawingArch = true;

		while (index < maxPositionCount || keepDrawingArch == false)
		{
			// Draw the current line renderer position
			lr.SetPosition(index, currentPosition);

			// Calculate the next position
			float time = Time.deltaTime;
			Vector3 nextVelocity = currentVelocity + Physics.gravity * time;
			Vector3 nextPosition = currentPosition + nextVelocity * time;

			// Check if it hits something and start
			RaycastHit hit;
			Vector3 nextPositionDirection = nextPosition - currentPosition;
			if (Physics.Raycast(currentPosition, nextPositionDirection, out hit, nextPositionDirection.magnitude, layerMask))
			{
				lr.positionCount = index + 2;
				lr.SetPosition(index + 1, hit.point);
				if (indicatorInstance != null) 
				{
					indicatorInstance.SetActive(true);
					indicatorInstance.transform.forward = hit.normal;
					indicatorInstance.transform.position = hit.point - transform.forward * 0.25f;
				};
				break;
			}
			else
			{
				indicatorInstance.SetActive(false);
			}
			currentPosition = nextPosition;
			currentVelocity = nextVelocity;
			index++;
		}
	}
}
										

/*
* Carlos Adan Cortes De la Fuente
* All rights reserved. Copyright (c)
* Email: dev@carlosadan.com
*/

[System.Serializable]
public class Response : UnityEvent<object> {}

public class GameEventListener : MonoBehaviour {

	public GameEvent gameEvent;
	public Response response;

	private void OnEnable() {
		gameEvent.RegisterListener(this);
	}
	
	private void OnDisable() {
		gameEvent.UnregisterListener(this);
	}

	public void OnEventRaised(object data) {
		response.Invoke(data);
	}
	
}

[CreateAssetMenu(fileName="NewGameEvent", menuName="SunsOutGunsOut/GameEvent")]
public class GameEvent : ScriptableObject {

	private List<GameEventListener> listeners = new List<GameEventListener>();

	public void Raise(object data) {
		// Go backwards in case we delete one listener
		for (int i = listeners.Count -1; i >= 0; i--) {
			listeners[i].OnEventRaised(data);
		}
	}

	public void RegisterListener(GameEventListener listener) {
		listeners.Add(listener);
	}
	
	public void UnregisterListener(GameEventListener listener) {
		listeners.Remove(listener);
	}
}
										

/*
* Carlos Adan Cortes De la Fuente
* All rights reserved. Copyright (c)
* Email: dev@carlosadan.com
*/
public class BulletIndicatorManager : MonoBehaviour {

	[SerializeField]
	private DeflectorProperties deflectorProperties;
	[SerializeField]
	private BulletIndicatorProperties indicatorProperties;
	[SerializeField]
	private TransformVariable player;
	[SerializeField]
	private BulletTracker blueBulletTracker;
	[SerializeField]
	private BulletTracker redBulletTracker;
	[SerializeField]
	private EnumVariable blueBulletType;
	[SerializeField]
	private EnumVariable redBulletType;
	
	[SerializeField]
	private PoolableObject redIndicatorPrefab;
	[SerializeField]
	private PoolableObject blueIndicatorPrefab;
	[SerializeField]
	private int poolSize = 5;

	private void Awake() {
		blueBulletTracker.bullets = new List<Bullet>();
		redBulletTracker.bullets = new List<Bullet>();
	}

	private void Start() {
		PoolManager.CreatePool(redIndicatorPrefab, poolSize);
		PoolManager.CreatePool(blueIndicatorPrefab, poolSize);
	}

	private void Update() {
		int blueBulletsCount = blueBulletTracker.bullets.Count;
		int redBulletsCount = redBulletTracker.bullets.Count;
		
		if (blueBulletsCount == 0 && redBulletsCount == 0) return;

		if (blueBulletsCount > 0) {
			foreach (Bullet bullet in blueBulletTracker.bullets) {
				if (bullet != null) {
					DetermineVisibility(bullet, blueIndicatorPrefab);
				}
			}
		}

		if (redBulletsCount > 0) {
			foreach (Bullet bullet in redBulletTracker.bullets) {
				DetermineVisibility(bullet, redIndicatorPrefab);
			}
		}
	}

	private void DetermineVisibility(Bullet bullet, PoolableObject pObject) {
		Vector3 direction = bullet.transform.position - player.Value.transform.position;
		direction.y = 0;

		if (direction.magnitude <= indicatorProperties.minDistanceRange) {
			var clone = PoolManager.GetObjectFromPool(pObject);
			BulletIndicator indicator = clone.GetComponent<BulletIndicator>();
			indicator.transform.position = transform.position;
			indicator.transform.forward = transform.up;
			indicator.SetScale(direction.magnitude);
			indicator.gameObject.SetActive(true);
		}
	}

	private void OnDrawGizmos()
	{
		Gizmos.color = Color.white;
        Gizmos.DrawLine(transform.position, transform.position + transform.forward * deflectorProperties.maxDistanceToPerfectDeflection);
		Gizmos.color = Color.red;
        Gizmos.DrawLine(transform.position, transform.position + transform.forward * deflectorProperties.minDistanceToPerfectDeflection);
	}
}