using System;
+using System.Collections;
+using JetBrains.Annotations;
using UnityEngine;
+using UnityEngine.SceneManagement;
+using UnityEngine.UI;
namespace Controllers
{
public class GameController : MonoBehaviour
{
private State state = State.JumpBall; // A basketball game always starts with a jump ball.
+ internal Animator BallAnimation;
public Player player;
public Player enemy;
+ public bool freezeMotion;
+
+ private float startTime;
+ public static float timeLimit;
+
/// <summary>
/// The single ball for the game.
/// </summary>
[SerializeField] public Ball ball;
+ [SerializeField] private float dribbleHeight;
+ [SerializeField] private float dribbleSpeed;
+ private Vector3 ballTarget;
+
+ [SerializeField] public SpriteRenderer[] trenchCoatSegments;
+
+ [Header("Spawn Points")]
+ [SerializeField] private SpawnPoints PlayerSpawnPoints;
+ [SerializeField] private SpawnPoints EnemySpawnPoints;
+
+ [Header("Hoops")]
[SerializeField] public Hoop PlayerHoop;
[SerializeField] public Hoop EnemyHoop;
+ [Header("SFX")]
[SerializeField] public AudioSource dribbleSound;
+ [SerializeField] public AudioSource airhornSound;
+
+ [Header("UI")]
+ [SerializeField] private Text playerScoreText;
+ [SerializeField] private Text enemyScoreText;
+ [SerializeField] private Text timerText;
+
+ [SerializeField] private GameObject resultOverlay;
+ [SerializeField] private Text resultText;
+ [SerializeField] private GameObject actionsUI;
private void Awake()
{
ball.controller = this;
PlayerHoop.controller = this;
EnemyHoop.controller = this;
+ BallAnimation = ball.GetComponentInChildren<Animator>();
}
-
+
+ private void Start()
+ {
+ startTime = Time.time - 2f;
+ freezeMotion = true;
+ StartCoroutine(FadeInCoat());
+ }
+
+ private IEnumerator FadeInCoat()
+ {
+ foreach (var trenchCoat in trenchCoatSegments)
+ trenchCoat.material.color = new Color(1, 1, 1, 0);
+
+ yield return new WaitForSeconds(1f);
+
+ for (float t = 0; t < 1f; t += Time.deltaTime / 1)
+ {
+ foreach (var trenchCoat in trenchCoatSegments)
+ trenchCoat.material.color = new Color(1, 1, 1, Mathf.Lerp(0, 1, t));
+ yield return null;
+ }
+
+ yield return new WaitForSeconds(1f);
+
+ freezeMotion = false;
+ startTime = Time.time;
+ ball.Rigidbody.velocity = new Vector2(0, 10f);
+ }
+
+ private void Update()
+ {
+ UpdateUI();
+ }
+
+ private void FixedUpdate()
+ {
+ if (player.HasBall || enemy.HasBall)
+ {
+ ball.transform.position = ballTarget - new Vector3(0, (Mathf.Sin(Time.time * dribbleSpeed) + 1f) * (ballTarget.y - 0.75f) * dribbleHeight, 0);
+ }
+ }
+
+ private bool gameover;
+ private void UpdateUI()
+ {
+ playerScoreText.text = $"{player.score}";
+ enemyScoreText.text = $"{enemy.score}";
+
+ var remainingRaw = timeLimit - (Time.time - startTime);
+ var remaining = TimeSpan.FromSeconds(Mathf.Clamp(remainingRaw, 0, float.MaxValue));
+ timerText.text = $"{remaining.Minutes:00}:{remaining.Seconds:00}";
+
+ if (remainingRaw <= 0 && !gameover)
+ {
+ airhornSound.Play();
+ var outcome = player.score == enemy.score ? "TIE GAME" : player.score < enemy.score ? "AWAY TEAM WINS" : "HOME TEAM WINS";
+ actionsUI.SetActive(true);
+ ShowModal($"{outcome}\n{player.score}-{enemy.score}");
+
+ freezeMotion = true;
+ gameover = true;
+ }
+ }
+
public struct Player
{
/// <summary>
{
score += 2;
}
+
+ // They made a shot! Now respawn the players and give possession to the opposite player.
+ controller.Respawn(isEnemy ? Possession.Player : Possession.Enemy, $"{controller.player.score}-{controller.enemy.score}");
}
private State dribble => isEnemy ? State.EnemyDribble : State.PlayerDribble;
public void Move(Vector2 handPosition)
{
if (controller.state == (isEnemy ? State.EnemyDribble : State.PlayerDribble)) // Make sure they're dribbling.
- controller.ball.transform.position = handPosition; // TODO: Make this perform a dribbling motion, otherwise it looks like they're travelling.
+ controller.ballTarget = handPosition;
}
/// <summary>
if (Vector2.Distance(controller.ball.transform.position, handPosition) >= 1f) return false;
controller.state = dribble;
+ controller.BallAnimation.enabled = false;
controller.dribbleSound.Play();
Move(handPosition);
+
+ controller.ball.Rigidbody.bodyType = RigidbodyType2D.Kinematic;
+
return true;
}
/// Shoot the ball if possible.
/// </summary>
/// <param name="playerTransform"></param>
+ /// <param name="time"></param>
/// <returns>Whether or not the ball was shot</returns>
- public bool Shoot(Transform playerTransform)
+ public bool Shoot(Transform playerTransform, float time)
{
if (controller.state != dribble) return false; // We must be dribbling the ball to shoot it.
+ controller.BallAnimation.enabled = true;
controller.dribbleSound.Stop();
controller.state = shoot;
- controller.ball.Shoot(hoop.transform.position);
+ controller.ball.Rigidbody.bodyType = RigidbodyType2D.Dynamic;
+ controller.ball.Shoot(hoop, time);
lastShotPosition = playerTransform.position;
return true;
}
+
+ public void Foul(string reason)
+ {
+ // Give the other player the ball on a foul.
+ controller.Respawn(isEnemy ? Possession.Player : Possession.Enemy, reason);
+ }
}
internal void BallDropped()
{
+ BallAnimation.enabled = true;
dribbleSound.Stop();
+ ball.Rigidbody.bodyType = RigidbodyType2D.Dynamic;
state = State.Idle;
}
+ private void Respawn(Possession possession, string message)
+ {
+ BallDropped();
+
+ PlayerSpawnPoints.body.transform.position = new Vector3(PlayerSpawnPoints.character.position.x, PlayerSpawnPoints.character.position.y, PlayerSpawnPoints.body.transform.position.y);
+ PlayerSpawnPoints.body.GetComponent<Rigidbody2D>().velocity = Vector2.zero;
+
+ if (PlayerSpawnPoints.secondBody is { })
+ {
+ if (PlayerSpawnPoints.secondCharacter is { })
+ PlayerSpawnPoints.secondBody.transform.position = new Vector3(PlayerSpawnPoints.secondCharacter.position.x, PlayerSpawnPoints.secondCharacter.position.y, PlayerSpawnPoints.body.transform.position.y);
+ PlayerSpawnPoints.secondBody.GetComponent<Rigidbody2D>().velocity = Vector2.zero;
+ }
+
+ PlayerSpawnPoints.body.transform.localRotation = Quaternion.identity;
+ EnemySpawnPoints.body.transform.position = new Vector3(EnemySpawnPoints.character.position.x, EnemySpawnPoints.character.position.y, EnemySpawnPoints.body.transform.position.y);
+ ball.transform.position = possession switch
+ {
+ Possession.Player => new Vector3(PlayerSpawnPoints.ball.position.x, PlayerSpawnPoints.ball.position.y, ball.transform.position.y),
+ Possession.Enemy => new Vector3(EnemySpawnPoints.ball.position.x, EnemySpawnPoints.ball.position.y, ball.transform.position.y),
+ _ => ball.transform.position
+ };
+
+ // Set a cooldown so they can stop trying to wrangle the player while it respawns.
+ StartCoroutine(RespawnCooldown(possession, message));
+ }
+
+ private IEnumerator RespawnCooldown(Possession possession, string message)
+ {
+ // Show the new score.
+ var possessionText = possession == Possession.Player ? "HOME" : "AWAY";
+ ShowModal($"{message}\n{possessionText}'S POSSESSION");
+
+ freezeMotion = true;
+
+ yield return new WaitForSeconds(1f);
+
+ HideModal();
+
+ freezeMotion = false;
+ }
+
+ private void ShowModal(string text)
+ {
+ if (gameover) return;
+ resultOverlay.SetActive(true);
+ resultText.text = text;
+ }
+
+ private void HideModal()
+ {
+ if (gameover) return;
+ resultOverlay.SetActive(false);
+ }
+
+ public void Restart() => SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
+
+ public void MainMenu() => SceneManager.LoadScene("Menu");
+
internal enum State
{
Idle,
EnemyDribble,
EnemyShoot,
}
+
+ [Serializable]
+ private struct SpawnPoints
+ {
+ [SerializeField] internal Transform body;
+ [SerializeField] [CanBeNull] internal Transform secondBody;
+ [SerializeField] internal Transform ball;
+ [SerializeField] internal Transform character;
+ [SerializeField] [CanBeNull] internal Transform secondCharacter;
+ }
+
+ private enum Possession
+ {
+ Player,
+ Enemy
+ }
}
internal static class GameControllerStateExtensions