366 lines
11 KiB
C#
366 lines
11 KiB
C#
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<AudioSource>();
|
|
animator = GetComponent<Animator>();
|
|
sharkSpriteRenderer = GetComponent<SpriteRenderer>();
|
|
|
|
// 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);
|
|
}
|
|
|
|
if (killed)
|
|
{
|
|
if (!isDead)
|
|
{
|
|
isDead = true;
|
|
|
|
if (animator != null)
|
|
{
|
|
// Keep death state stable even if Any State transitions exist for movement.
|
|
animator.SetBool("canMove", false);
|
|
animator.Play("SharkDeath", 0, 0.0f);
|
|
}
|
|
|
|
audioSource.PlayOneShot(deathSound);
|
|
player.score += sharkKillPoints;
|
|
StartCoroutine(DestroyShark());
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Do stuff only if the spawn animation has finished.
|
|
if (spawnFinished)
|
|
{
|
|
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 until the death state has looped twice, with a timeout fallback so this cannot hang forever.
|
|
float elapsed = 0.0f;
|
|
const float maxWaitSeconds = 5.0f;
|
|
|
|
while (elapsed < maxWaitSeconds)
|
|
{
|
|
AnimatorStateInfo stateInfo = animator.GetCurrentAnimatorStateInfo(0);
|
|
if (stateInfo.IsName("SharkDeath") && stateInfo.normalizedTime >= 2.0f)
|
|
{
|
|
break;
|
|
}
|
|
|
|
elapsed += Time.deltaTime;
|
|
yield return null;
|
|
}
|
|
|
|
Destroy(gameObject);
|
|
}
|
|
|
|
void OnCollisionEnter2D(Collision2D collision)
|
|
{
|
|
if (collision.gameObject.CompareTag("Wall"))
|
|
{
|
|
Physics2D.IgnoreCollision(collision.collider, GetComponent<Collider2D>());
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
|
|
public void KillShark()
|
|
{
|
|
if (killed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
killed = true;
|
|
|
|
if (animator != null)
|
|
{
|
|
// Stop movement-related Any State transitions, then force the death animation.
|
|
animator.SetBool("canMove", false);
|
|
animator.ResetTrigger("killed");
|
|
animator.Play("SharkDeath", 0, 0.0f);
|
|
}
|
|
}
|
|
}
|