Files
Barrack-Unity/Assets/Scripts/Game/Shark.cs
T

318 lines
9.6 KiB
C#
Raw Normal View History

2026-08-10 21:35:53 -04:00
using UnityEngine;
2026-08-10 22:40:00 -04:00
using System.Collections;
2026-08-10 21:35:53 -04:00
public class Shark : MonoBehaviour
{
2026-08-10 22:40:00 -04:00
public AudioClip spawnSound;
public AudioClip angrySound;
public AudioClip tiredSound;
public AudioClip deathSound;
public AudioClip hitSound;
2026-08-10 22:40:00 -04:00
public Player player;
public SpriteRenderer gameBoardSpriteRenderer;
2026-08-10 23:04:48 -04:00
private Bounds? gameBoardBounds = null;
2026-08-10 22:40:00 -04:00
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;
2026-08-11 21:47:03 -04:00
[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;
2026-08-11 21:55:16 -04:00
[SerializeField]
private float targetTimeoutSeconds = 2f;
2026-08-11 21:47:03 -04:00
private SpriteRenderer sharkSpriteRenderer;
private Bounds? movementBounds = null;
private Vector2 currentTarget;
private bool hasTarget = false;
private float currentSpeed = 0f;
private float currentHeadingAngle = 0f;
2026-08-11 21:55:16 -04:00
private float currentTargetTimer = 0f;
2026-08-10 21:35:53 -04:00
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
2026-08-10 22:40:00 -04:00
audioSource = GetComponent<AudioSource>();
animator = GetComponent<Animator>();
2026-08-11 21:47:03 -04:00
sharkSpriteRenderer = GetComponent<SpriteRenderer>();
// Ensure movement transitions stay blocked until spawn animation finishes.
animator.SetBool("canMove", false);
2026-08-10 22:40:00 -04:00
bool spawnLeft = Random.Range(0, 2) == 0;
angle = spawnLeft ? 90 : 270;
2026-08-11 21:47:03 -04:00
currentHeadingAngle = angle;
// Initialize angle before triggering spawn, so post-spawn movement starts in the correct direction.
animator.SetInteger("angle", angle);
2026-08-10 22:40:00 -04:00
2026-08-11 21:47:03 -04:00
// 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");
2026-08-10 22:40:00 -04:00
audioSource.PlayOneShot(spawnSound);
2026-08-10 21:35:53 -04:00
}
// Update is called once per frame
2026-08-10 22:40:00 -04:00
void FixedUpdate()
{
2026-08-11 21:55:16 -04:00
// Foce reset the rotation.
transform.rotation = Quaternion.identity;
2026-08-11 21:47:03 -04:00
// Initialize gameBoardBounds.
2026-08-10 23:04:48 -04:00
if (gameBoardSpriteRenderer != null && gameBoardBounds == null)
{
gameBoardBounds = gameBoardSpriteRenderer.bounds;
}
2026-08-11 21:47:03 -04:00
// 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);
2026-08-11 21:47:03 -04:00
// 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.
2026-08-10 22:40:00 -04:00
if (
(animator.GetCurrentAnimatorStateInfo(0).IsName("SharkSpawnRight") ||
animator.GetCurrentAnimatorStateInfo(0).IsName("SharkSpawnLeft")) &&
animator.GetCurrentAnimatorStateInfo(0).normalizedTime >= 1.0f
)
{
spawnFinished = true;
2026-08-11 21:47:03 -04:00
animator.SetBool("canMove", true);
}
// Do stuff only if the spawn animation has finished.
if (spawnFinished)
2026-08-10 22:40:00 -04:00
{
// TODO: Check if the shark was killed.
if (isDead)
{
animator.SetTrigger("killed");
audioSource.PlayOneShot(deathSound);
player.score += sharkKillPoints;
StartCoroutine(DestroyShark());
}
else
{
2026-08-11 21:47:03 -04:00
UpdateMovement();
animator.SetInteger("angle", angle);
2026-08-10 22:40:00 -04:00
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;
}
}
}
}
2026-08-11 21:47:03 -04:00
private void UpdateMovement()
{
if (movementBounds == null)
{
return;
}
Vector2 currentPosition = transform.position;
if (!hasTarget)
{
currentTarget = ChooseRandomTarget();
hasTarget = true;
2026-08-11 21:55:16 -04:00
currentTargetTimer = 0f;
2026-08-11 21:47:03 -04:00
}
2026-08-11 21:55:16 -04:00
currentTargetTimer += Time.fixedDeltaTime;
2026-08-11 21:47:03 -04:00
Vector2 toTarget = currentTarget - currentPosition;
if (toTarget.sqrMagnitude <= targetReachedDistance * targetReachedDistance)
{
currentTarget = ChooseRandomTarget();
2026-08-11 21:55:16 -04:00
currentTargetTimer = 0f;
toTarget = currentTarget - currentPosition;
}
else if (currentTargetTimer >= targetTimeoutSeconds)
{
currentTarget = ChooseRandomTarget();
currentTargetTimer = 0f;
2026-08-11 21:47:03 -04:00
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;
}
2026-08-10 22:40:00 -04:00
private IEnumerator DestroyShark()
2026-08-10 21:35:53 -04:00
{
2026-08-10 22:40:00 -04:00
// 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);
2026-08-10 21:35:53 -04:00
}
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<Player>().OnBarHit();
}
}
}
2026-08-10 21:35:53 -04:00
}