using UnityEngine; using System.Collections; public class Shark : MonoBehaviour { public AudioClip spawnSound; public AudioClip angrySound; public AudioClip tiredSound; public AudioClip deathSound; public Player player; public SpriteRenderer gameBoardSpriteRenderer; private Bounds? gameBoardBounds = null; private AudioSource audioSource; private Animator animator; private int angle = 0; private bool isAngry = false; private bool isTired = false; private bool isDead = false; private float angryTimer = 0f; private float angryDuration = 5f; private float angryProbability = 0.1f; private static int sharkKillPoints = 100; private bool spawnFinished = false; [SerializeField] private float normalMaxSpeed = 2.5f; [SerializeField] private float turnRateDegreesPerSecond = 180f; [SerializeField] private float speedChangeRate = 4f; [SerializeField] private float targetReachedDistance = 0.15f; [SerializeField] private float alignmentThresholdDegrees = 5f; private SpriteRenderer sharkSpriteRenderer; private Bounds? movementBounds = null; private Vector2 currentTarget; private bool hasTarget = false; private float currentSpeed = 0f; private float currentHeadingAngle = 0f; // Start is called once before the first execution of Update after the MonoBehaviour is created void Start() { audioSource = GetComponent(); animator = GetComponent(); sharkSpriteRenderer = GetComponent(); // Ensure movement transitions stay blocked until spawn animation finishes. animator.SetBool("canMove", false); bool spawnLeft = Random.Range(0, 2) == 0; angle = spawnLeft ? 90 : 270; currentHeadingAngle = angle; // Initialize angle before triggering spawn, so post-spawn movement starts in the correct direction. animator.SetInteger("angle", angle); // Clear stale trigger state (useful when object is reused) before setting the selected spawn trigger. animator.ResetTrigger("spawnLeft"); animator.ResetTrigger("spawnRight"); animator.SetTrigger(spawnLeft ? "spawnLeft" : "spawnRight"); audioSource.PlayOneShot(spawnSound); } // Update is called once per frame void FixedUpdate() { // Initialize gameBoardBounds. if (gameBoardSpriteRenderer != null && gameBoardBounds == null) { gameBoardBounds = gameBoardSpriteRenderer.bounds; } // Build movement bounds by shrinking the board by 2x the shark's size total (1x on each side). if (gameBoardBounds != null && sharkSpriteRenderer != null && movementBounds == null) { Bounds sharkBounds = sharkSpriteRenderer.bounds; Bounds boardBounds = gameBoardBounds.Value; float minX = boardBounds.min.x + sharkBounds.size.x; float maxX = boardBounds.max.x - sharkBounds.size.x; float minY = boardBounds.min.y + sharkBounds.size.y; float maxY = boardBounds.max.y - sharkBounds.size.y; // Fallback to board center if the shrunken region collapses. if (minX > maxX) { float centerX = boardBounds.center.x; minX = centerX; maxX = centerX; } if (minY > maxY) { float centerY = boardBounds.center.y; minY = centerY; maxY = centerY; } Vector3 min = new Vector3(minX, minY, boardBounds.min.z); Vector3 max = new Vector3(maxX, maxY, boardBounds.max.z); movementBounds = new Bounds((min + max) * 0.5f, max - min); } // Check if the spawn animation has finished. if ( (animator.GetCurrentAnimatorStateInfo(0).IsName("SharkSpawnRight") || animator.GetCurrentAnimatorStateInfo(0).IsName("SharkSpawnLeft")) && animator.GetCurrentAnimatorStateInfo(0).normalizedTime >= 1.0f ) { spawnFinished = true; animator.SetBool("canMove", true); } // Do stuff only if the spawn animation has finished. if (spawnFinished) { // TODO: Check if the shark was killed. if (isDead) { animator.SetTrigger("killed"); audioSource.PlayOneShot(deathSound); player.score += sharkKillPoints; StartCoroutine(DestroyShark()); } else { UpdateMovement(); animator.SetInteger("angle", angle); if (!isAngry && !isTired && Random.value < angryProbability) { isAngry = true; angryTimer = angryDuration; audioSource.PlayOneShot(angrySound); } if (isAngry && !isTired && angryTimer <= 0f) { isAngry = false; isTired = true; audioSource.PlayOneShot(tiredSound); } if (isAngry) { angryTimer -= Time.fixedDeltaTime; } } } } private void UpdateMovement() { if (movementBounds == null) { return; } Vector2 currentPosition = transform.position; if (!hasTarget) { currentTarget = ChooseRandomTarget(); hasTarget = true; } Vector2 toTarget = currentTarget - currentPosition; if (toTarget.sqrMagnitude <= targetReachedDistance * targetReachedDistance) { currentTarget = ChooseRandomTarget(); toTarget = currentTarget - currentPosition; } if (toTarget.sqrMagnitude > 0.0001f) { float desiredAngle = AngleFromDirection(toTarget.normalized); float angleDelta = Mathf.Abs(Mathf.DeltaAngle(currentHeadingAngle, desiredAngle)); currentHeadingAngle = Mathf.MoveTowardsAngle( currentHeadingAngle, desiredAngle, turnRateDegreesPerSecond * Time.fixedDeltaTime ); float dynamicMaxSpeed = isAngry && !isTired ? normalMaxSpeed * 2f : normalMaxSpeed; float turningSpeed = normalMaxSpeed * (2f / 3f); float desiredSpeed = angleDelta <= alignmentThresholdDegrees ? dynamicMaxSpeed : turningSpeed; currentSpeed = Mathf.MoveTowards( currentSpeed, desiredSpeed, speedChangeRate * Time.fixedDeltaTime ); Vector2 forward = DirectionFromAngle(currentHeadingAngle); Vector2 nextPosition = currentPosition + forward * (currentSpeed * Time.fixedDeltaTime); transform.position = new Vector3(nextPosition.x, nextPosition.y, transform.position.z); angle = QuantizeAngleToTen(currentHeadingAngle); } } private Vector2 ChooseRandomTarget() { Bounds bounds = movementBounds.Value; float targetX = Random.Range(bounds.min.x, bounds.max.x); float targetY = Random.Range(bounds.min.y, bounds.max.y); return new Vector2(targetX, targetY); } private static float AngleFromDirection(Vector2 direction) { float raw = Mathf.Atan2(-direction.x, -direction.y) * Mathf.Rad2Deg; return (raw + 360f) % 360f; } private static Vector2 DirectionFromAngle(float degrees) { float radians = degrees * Mathf.Deg2Rad; return new Vector2(-Mathf.Sin(radians), -Mathf.Cos(radians)); } private static int QuantizeAngleToTen(float degrees) { int quantized = Mathf.RoundToInt(degrees / 10f) * 10; quantized = ((quantized % 360) + 360) % 360; return quantized; } private IEnumerator DestroyShark() { // Wait two loops of the death animation before destroying the shark object. yield return new WaitUntil( () => animator.GetCurrentAnimatorStateInfo(0).IsName("SharkDeath") && animator.GetCurrentAnimatorStateInfo(0).normalizedTime >= 2.0f ); Destroy(gameObject); } }