using System.Collections.Generic; using UnityEngine; [RequireComponent(typeof(Rigidbody2D))] [RequireComponent(typeof(Collider2D))] public class TrickController : MonoBehaviour { [SerializeField] private string treatTag = "Interactable"; [SerializeField] private float moveSpeed = 3.5f; [SerializeField] private float targetRefreshInterval = 0.4f; [SerializeField] private float consumeDistance = 0.35f; [SerializeField] private float wanderInterval = 1.2f; [SerializeField] private float consumeCooldown = 1.5f; [SerializeField] private TrickController trickPrefab; [SerializeField] private float avoidPlayerRadius = 1.2f; [SerializeField] private float avoidTrickRadius = 0.8f; [SerializeField] private float treatSeekWeight = 1f; [SerializeField] private float avoidPlayerWeight = 0.25f; [SerializeField] private float avoidTrickWeight = 0.2f; [SerializeField] private float wanderWeight = 0.4f; [SerializeField] private float jumpImpulse = 6f; [SerializeField] private float jumpCooldown = 1f; [SerializeField] private float jumpChancePerSecond = 0.5f; [SerializeField] private LayerMask groundMask = ~0; [SerializeField] private float groundCheckDistance = 0.05f; private Rigidbody2D body; private Transform target; private float nextTargetTime; private Vector2 wanderDirection; private float nextWanderTime; private bool isCarried; private float nextConsumeTime; private float desiredXVelocity; private float nextJumpTime; private void Awake() { body = GetComponent(); body.bodyType = RigidbodyType2D.Dynamic; body.gravityScale = 1f; body.freezeRotation = true; body.collisionDetectionMode = CollisionDetectionMode2D.Continuous; body.interpolation = RigidbodyInterpolation2D.Interpolate; } private void Update() { if (isCarried) { return; } if (Time.time >= nextTargetTime) { target = FindNearestTreat(); if (target == null) { // If we can't find treats while we're tagged Interactable, keep moving. wanderDirection = Random.insideUnitCircle.normalized; if (wanderDirection.sqrMagnitude < 0.01f) { wanderDirection = Vector2.right; } nextWanderTime = Time.time + wanderInterval; } nextTargetTime = Time.time + targetRefreshInterval; } if (Time.time >= nextWanderTime) { wanderDirection = Random.insideUnitCircle.normalized; if (wanderDirection.sqrMagnitude < 0.01f) { wanderDirection = Vector2.right; } nextWanderTime = Time.time + wanderInterval; } TryConsumeTreat(); } private void FixedUpdate() { if (isCarried) { return; } Vector2 desiredVelocity; var stopForTreat = false; var avoidPlayerDirection = GetAvoidPlayerDirection(); var avoidTrickDirection = GetAvoidTrickDirection(); var hasTarget = target != null; var canConsume = Time.time >= nextConsumeTime; var desiredX = 0f; if (hasTarget) { var direction = ((Vector2)target.position - (Vector2)transform.position).normalized; desiredX += direction.x * treatSeekWeight; stopForTreat = Vector2.Distance(transform.position, target.position) <= consumeDistance; } else { desiredX += wanderDirection.x * wanderWeight; } if (!hasTarget || !canConsume) { desiredX += avoidPlayerDirection * avoidPlayerWeight; } desiredX += avoidTrickDirection * avoidTrickWeight; if (Mathf.Abs(desiredX) < 0.01f) { desiredX = wanderDirection.x * 0.2f; } desiredVelocity = new Vector2(Mathf.Clamp(desiredX, -1f, 1f) * moveSpeed, 0f); var velocity = body.linearVelocity; desiredXVelocity = stopForTreat ? 0f : desiredVelocity.x; velocity.x = Mathf.Lerp(velocity.x, desiredXVelocity, 0.25f); body.linearVelocity = velocity; var isMoving = Mathf.Abs(desiredXVelocity) > 0.1f; TryJump(isMoving); } private Transform FindNearestTreat() { var treats = GameObject.FindGameObjectsWithTag(treatTag); if (treats == null || treats.Length == 0) { return null; } Transform best = null; var bestDistance = float.MaxValue; var origin = transform.position; for (var i = 0; i < treats.Length; i++) { var candidate = treats[i]; if (candidate == gameObject) { continue; } if (!candidate.activeInHierarchy) { continue; } if (!candidate.TryGetComponent(out var collider) || !collider.enabled) { continue; } var distance = Vector2.Distance(origin, candidate.transform.position); if (distance < bestDistance) { bestDistance = distance; best = candidate.transform; } } return best; } private void TryConsumeTreat() { if (isCarried) { return; } if (Time.time < nextConsumeTime) { return; } var hits = Physics2D.OverlapCircleAll(transform.position, consumeDistance); for (var i = 0; i < hits.Length; i++) { var hit = hits[i]; if (hit == null || hit.gameObject == gameObject || !hit.CompareTag(treatTag)) { continue; } var spawnPosition = hit.transform.position; Destroy(hit.gameObject); if (target == hit.transform) { target = null; } SpawnTrick(spawnPosition); nextConsumeTime = Time.time + consumeCooldown; break; } } private void SpawnTrick(Vector3 position) { if (trickPrefab != null) { Instantiate(trickPrefab, position, Quaternion.identity); return; } Instantiate(gameObject, position, Quaternion.identity); } private float GetAvoidPlayerDirection() { var players = FindObjectsByType(FindObjectsSortMode.None); if (players == null || players.Length == 0) { return 0f; } Transform closest = null; var closestDist = float.MaxValue; var origin = transform.position; for (var i = 0; i < players.Length; i++) { var candidate = players[i]; if (candidate == null || !candidate.gameObject.activeInHierarchy) { continue; } var dist = Vector2.Distance(origin, candidate.transform.position); if (dist < closestDist) { closestDist = dist; closest = candidate.transform; } } if (closest == null || closestDist > avoidPlayerRadius) { return 0f; } return Mathf.Sign(origin.x - closest.position.x); } private float GetAvoidTrickDirection() { var tricks = FindObjectsByType(FindObjectsSortMode.None); if (tricks == null || tricks.Length <= 1) { return 0f; } Transform closest = null; var closestDist = float.MaxValue; var origin = transform.position; for (var i = 0; i < tricks.Length; i++) { var candidate = tricks[i]; if (candidate == null || candidate == this || !candidate.gameObject.activeInHierarchy) { continue; } var dist = Vector2.Distance(origin, candidate.transform.position); if (dist < closestDist) { closestDist = dist; closest = candidate.transform; } } if (closest == null || closestDist > avoidTrickRadius) { return 0f; } return Mathf.Sign(origin.x - closest.position.x); } private void TryJump(bool isMoving) { if (!isMoving || Time.time < nextJumpTime) { return; } if (!IsGrounded()) { return; } var chance = jumpChancePerSecond * Time.fixedDeltaTime; if (Random.value > chance) { return; } var velocity = body.linearVelocity; velocity.y = 0f; body.linearVelocity = velocity; body.AddForce(Vector2.up * jumpImpulse, ForceMode2D.Impulse); nextJumpTime = Time.time + jumpCooldown; } private bool IsGrounded() { var collider = GetComponent(); if (collider == null) { return false; } var bounds = collider.bounds; var checkSize = new Vector2(bounds.size.x * 0.9f, groundCheckDistance * 2f); var checkCenter = new Vector2(bounds.center.x, bounds.min.y - groundCheckDistance); var hits = Physics2D.OverlapBoxAll(checkCenter, checkSize, 0f, groundMask); for (var i = 0; i < hits.Length; i++) { var hit = hits[i]; if (hit != null && hit != collider) { return true; } } return false; } public void SetCarried(bool carried) { isCarried = carried; if (carried) { target = null; var velocity = body.linearVelocity; velocity.x = 0f; body.linearVelocity = velocity; } } }