using System; using System.Collections; using JetBrains.Annotations; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.UI; namespace Controllers { /// /// The controller for the basketball-game logic. /// 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; /// /// The single ball for the game. /// [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() { player = new Player { isEnemy = false, controller = this }; enemy = new Player { isEnemy = true, controller = this }; ball.controller = this; PlayerHoop.controller = this; EnemyHoop.controller = this; BallAnimation = ball.GetComponentInChildren(); } 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 { /// /// Whether this player is the AI-enemy. /// internal bool isEnemy; /// /// A back-reference to the containing GameController. /// internal GameController controller; internal int score; private Vector2 lastShotPosition; public void Score(Vector2 Rim) { if (Vector2.Distance(lastShotPosition, Rim) >= 10) { score += 3; } else { 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; private State shoot => isEnemy ? State.EnemyShoot : State.PlayerShoot; private Hoop hoop => isEnemy ? controller.EnemyHoop : controller.PlayerHoop; public bool HasBall => controller.state == dribble; /// /// When dribbling, move the ball with the player. /// /// The position of the hand dribbling the ball. public void Move(Vector2 handPosition) { if (controller.state == (isEnemy ? State.EnemyDribble : State.PlayerDribble)) // Make sure they're dribbling. controller.ballTarget = handPosition; } /// /// Grab the ball if possible given the current game state. /// /// The position of the hand to attempt grabbing from. /// Whether or not the ball was able to be picked up. public bool GrabBall(Vector2 handPosition) { // Don't allow the ball to be picked up if someone shot it. Also don't try picking it up if we're already holding it. if (controller.state == shoot || controller.state == dribble) return false; // Make sure its within their grab area. 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. /// /// /// /// Whether or not the ball was shot 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.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().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().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, JumpBall, PlayerDribble, PlayerShoot, 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 { internal static bool IsShot(this GameController.State state) { return state == GameController.State.EnemyShoot || state == GameController.State.PlayerShoot; } internal static bool IsDribble(this GameController.State state) { return state == GameController.State.EnemyDribble || state == GameController.State.PlayerDribble; } } }