using UnityEngine; using System.Collections; public class Shark : MonoBehaviour { public AudioClip spawnSound; public AudioClip angrySound; public AudioClip tiredSound; public AudioClip deathSound; public AudioClip hitSound; 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; [SerializeField] private float targetTimeoutSeconds = 2f; private SpriteRenderer sharkSpriteRenderer; private Bounds? movementBounds = null; private Vector2 currentTarget; private bool hasTarget = false; private float currentSpeed = 0f; private float currentHeadingAngle = 0f; private float currentTargetTimer = 0f; private bool killed = false; // 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() { // Foce reset the rotation. transform.rotation = Quaternion.identity; // 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 + (2.0f * sharkBounds.size.x); float maxX = boardBounds.max.x - (2.0f * sharkBounds.size.x); float minY = boardBounds.min.y + (2.0f * sharkBounds.size.y); float maxY = boardBounds.max.y - (2.0f * 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) { if (killed && !isDead) { isDead = true; 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; currentTargetTimer = 0f; } currentTargetTimer += Time.fixedDeltaTime; Vector2 toTarget = currentTarget - currentPosition; if (toTarget.sqrMagnitude <= targetReachedDistance * targetReachedDistance) { currentTarget = ChooseRandomTarget(); currentTargetTimer = 0f; toTarget = currentTarget - currentPosition; } else if (currentTargetTimer >= targetTimeoutSeconds) { currentTarget = ChooseRandomTarget(); currentTargetTimer = 0f; 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); } void OnCollisionEnter2D(Collision2D collision) { if (collision.gameObject.CompareTag("Wall")) { Physics2D.IgnoreCollision(collision.collider, GetComponent()); } } void OnTriggerEnter2D(Collider2D other) { if(other.gameObject.CompareTag("Bar")) { if (!audioSource.isPlaying) { audioSource.PlayOneShot(hitSound); } // Destroy all bar segments when the ball hits a bar. var barSegments = GameObject.FindGameObjectsWithTag("Bar"); foreach (var barSegment in barSegments) { Destroy(barSegment); } // Notify the player that the bar has been hit. var player = GameObject.FindGameObjectWithTag("Player"); if (player != null) { player.GetComponent().OnBarHit(); } } } public void KillShark() { killed = true; } }