/************************************** Copyright Unluck Software www.chemicalbliss.com ***************************************/ using UnityEngine; public class FlockChild : MonoBehaviour { [HideInInspector] public FlockController _spawner; // Reference to the flock controller that spawned this bird [HideInInspector] public Vector3 _wayPoint; // Waypoint used to steer towards public float _speed; // Current speed of bird [HideInInspector] public bool _dived = true; // Has recently performed a dive movement [HideInInspector] public float _stuckCounter; // Prevents looping around a waypoint by increasing minimum distance to waypoint [HideInInspector] public float _damping; // Damping used for steering (steer speed) [HideInInspector] public bool _soar = true; // Indicates if this is soaring [HideInInspector] public bool _landing; // Indicates if bird is landing or sitting idle [HideInInspector] public float _targetSpeed; // Max bird speed [HideInInspector] public bool _move = true; // Indicates if bird can fly public GameObject _model; // Reference to bird model public Transform _modelT; // Cached model transform public Transform _thisT; // Cached this transform // --- Animator replacement for Legacy Animation --- private Animator _anim; // Animator on _model // We assume the spawner provides these state names (strings): // _spawner._flapAnimation, _spawner._soarAnimation, _spawner._idleAnimation [HideInInspector] public float _avoidValue; // Randomized to reduce uniform avoidance [HideInInspector] public float _avoidDistance; // Distance from obstacle before starting to avoid float _soarTimer; bool _instantiated; static int _updateNextSeed = 0; int _updateSeed = -1; [HideInInspector] public bool _avoid = true; public Vector3 _landingPosOffset; public void Start() { FindRequiredComponents(); Wander(0.0f); SetRandomScale(); _thisT.position = findWaypoint(); RandomizeStartAnimationFrame(); InitAvoidanceValues(); _speed = _spawner._minSpeed; _spawner._activeChildren++; _instantiated = true; if (_spawner._updateDivisor > 1) { int _updateSeedCap = _spawner._updateDivisor - 1; _updateNextSeed++; this._updateSeed = _updateNextSeed; _updateNextSeed = _updateNextSeed % _updateSeedCap; } } public void Update() { // Skip frames (performance batching from original) if (_spawner._updateDivisor <= 1 || _spawner._updateCounter == _updateSeed) { SoarTimeLimit(); CheckForDistanceToWaypoint(); RotationBasedOnWaypointOrAvoidance(); LimitRotationOfModel(); } } public void OnDisable() { CancelInvoke(); _spawner._activeChildren--; } public void OnEnable() { if (_instantiated) { _spawner._activeChildren++; if (_landing) { PlayStateSafe(_spawner._idleAnimation, 0.1f); } else { PlayStateSafe(_spawner._flapAnimation, 0.1f); } } } public void FindRequiredComponents() { if (_thisT == null) _thisT = transform; if (_model == null) { var modelTF = _thisT.Find("Model"); if (modelTF != null) _model = modelTF.gameObject; else _model = gameObject; // fallback if hierarchy differs } if (_modelT == null) _modelT = _model.transform; // Animator (replace Legacy Animation) _anim = _model.GetComponent(); if (_anim == null) { // Add a gentle fallback so script doesn't null-ref _anim = _model.AddComponent(); // NOTE: You still need to assign a RuntimeAnimatorController to this Animator in the prefab/inspector } } public void RandomizeStartAnimationFrame() { // Randomize normalized time (0..1) for current/idle state string state = string.IsNullOrEmpty(_spawner._flapAnimation) ? _spawner._idleAnimation : _spawner._flapAnimation; if (!string.IsNullOrEmpty(state)) { float n = Random.value; // normalized _anim.Play(state, 0, n); } } public void InitAvoidanceValues() { _avoidValue = Random.Range(.3f, .1f); if (_spawner._birdAvoidDistanceMax != _spawner._birdAvoidDistanceMin) { _avoidDistance = Random.Range(_spawner._birdAvoidDistanceMax, _spawner._birdAvoidDistanceMin); return; } _avoidDistance = _spawner._birdAvoidDistanceMin; } public void SetRandomScale() { float sc = Random.Range(_spawner._minScale, _spawner._maxScale); _thisT.localScale = new Vector3(sc, sc, sc); } // Soar Timeout - Limits how long a bird can soar public void SoarTimeLimit() { if (this._soar && _spawner._soarMaxTime > 0) { if (_soarTimer > _spawner._soarMaxTime) { this.Flap(); _soarTimer = 0.0f; } else { _soarTimer += _spawner._newDelta; } } } public void CheckForDistanceToWaypoint() { if (!_landing && (_thisT.position - _wayPoint).magnitude < _spawner._waypointDistance + _stuckCounter) { Wander(0.0f); _stuckCounter = 0.0f; } else if (!_landing) { _stuckCounter += _spawner._newDelta; } else { _stuckCounter = 0.0f; } } public void RotationBasedOnWaypointOrAvoidance() { Vector3 lookit = _wayPoint - _thisT.position; if (_targetSpeed > -1 && lookit != Vector3.zero) { Quaternion rotation = Quaternion.LookRotation(lookit); _thisT.rotation = Quaternion.Slerp(_thisT.rotation, rotation, _spawner._newDelta * _damping); } if (_spawner._childTriggerPos) { if ((_thisT.position - _spawner._posBuffer).magnitude < 1) { _spawner.SetFlockRandomPosition(); } } _speed = Mathf.Lerp(_speed, _targetSpeed, _spawner._newDelta * 2.5f); // Position forward based on object rotation if (_move) { _thisT.position += _thisT.forward * _speed * _spawner._newDelta; if (_avoid && _spawner._birdAvoid) Avoidance(); } } public bool Avoidance() { RaycastHit hit = new RaycastHit(); Vector3 fwd = _modelT.forward; bool r = false; Quaternion rot = Quaternion.identity; Vector3 rotE = Vector3.zero; Vector3 pos = Vector3.zero; pos = _thisT.position; rot = _thisT.rotation; rotE = _thisT.rotation.eulerAngles; if (Physics.Raycast(_thisT.position, fwd + (_modelT.right * _avoidValue), out hit, _avoidDistance, _spawner._avoidanceMask)) { rotE.y -= _spawner._birdAvoidHorizontalForce * _spawner._newDelta * _damping; rot.eulerAngles = rotE; _thisT.rotation = rot; r = true; } else if (Physics.Raycast(_thisT.position, fwd + (_modelT.right * -_avoidValue), out hit, _avoidDistance, _spawner._avoidanceMask)) { rotE.y += _spawner._birdAvoidHorizontalForce * _spawner._newDelta * _damping; rot.eulerAngles = rotE; _thisT.rotation = rot; r = true; } if (_spawner._birdAvoidDown && !this._landing && Physics.Raycast(_thisT.position, -Vector3.up, out hit, _avoidDistance, _spawner._avoidanceMask)) { rotE.x -= _spawner._birdAvoidVerticalForce * _spawner._newDelta * _damping; rot.eulerAngles = rotE; _thisT.rotation = rot; pos.y += _spawner._birdAvoidVerticalForce * _spawner._newDelta * .01f; _thisT.position = pos; r = true; } else if (_spawner._birdAvoidUp && !this._landing && Physics.Raycast(_thisT.position, Vector3.up, out hit, _avoidDistance, _spawner._avoidanceMask)) { rotE.x += _spawner._birdAvoidVerticalForce * _spawner._newDelta * _damping; rot.eulerAngles = rotE; _thisT.rotation = rot; pos.y -= _spawner._birdAvoidVerticalForce * _spawner._newDelta * .01f; _thisT.position = pos; r = true; } return r; } public void LimitRotationOfModel() { // Keep model's local X tilt limited; original behavior retained Quaternion rot = Quaternion.identity; Vector3 rotE = Vector3.zero; rot = _modelT.localRotation; rotE = rot.eulerAngles; if (((_soar && _spawner._flatSoar) || (_spawner._flatFly && !_soar)) && _wayPoint.y > _thisT.position.y || _landing) { rotE.x = Mathf.LerpAngle(_modelT.localEulerAngles.x, -_thisT.localEulerAngles.x, _spawner._newDelta * 1.75f); rot.eulerAngles = rotE; _modelT.localRotation = rot; } else { rotE.x = Mathf.LerpAngle(_modelT.localEulerAngles.x, 0.0f, _spawner._newDelta * 1.75f); rot.eulerAngles = rotE; _modelT.localRotation = rot; } } public void Wander(float delay) { if (!_landing) { _damping = Random.Range(_spawner._minDamping, _spawner._maxDamping); _targetSpeed = Random.Range(_spawner._minSpeed, _spawner._maxSpeed); Invoke("SetRandomMode", delay); } } public void SetRandomMode() { CancelInvoke("SetRandomMode"); if (!_dived && Random.value < _spawner._soarFrequency) { Soar(); } else if (!_dived && Random.value < _spawner._diveFrequency) { Dive(); } else { Flap(); } } public void Flap() { if (_move) { PlayStateSafe(_spawner._flapAnimation, 0.5f); _soar = false; animationSpeed(); _wayPoint = findWaypoint(); _dived = false; } } public Vector3 findWaypoint() { Vector3 t = Vector3.zero; t.x = Random.Range(-_spawner._spawnSphere, _spawner._spawnSphere) + _spawner._posBuffer.x; t.z = Random.Range(-_spawner._spawnSphereDepth, _spawner._spawnSphereDepth) + _spawner._posBuffer.z; t.y = Random.Range(-_spawner._spawnSphereHeight, _spawner._spawnSphereHeight) + _spawner._posBuffer.y; return t; } public void Soar() { if (_move) { PlayStateSafe(_spawner._soarAnimation, 1.5f); _wayPoint = findWaypoint(); _soar = true; } } public void Dive() { // Use soar animation (as original) then offset waypoint down if (!string.IsNullOrEmpty(_spawner._soarAnimation)) { PlayStateSafe(_spawner._soarAnimation, 1.5f); } // Original per-clip speed tweak removed (Animator replacement) _wayPoint = findWaypoint(); _wayPoint.y -= _spawner._diveValue; _dived = true; } public void animationSpeed() { // Global animator speed to imitate original random clip speeds if (!_dived && !_landing) { _anim.speed = Random.Range(_spawner._minAnimationSpeed, _spawner._maxAnimationSpeed); } else { _anim.speed = _spawner._maxAnimationSpeed; } } // --- Helper to safely crossfade/enter a state by name on Animator --- void PlayStateSafe(string stateName, float crossfadeDuration) { if (_anim == null || string.IsNullOrEmpty(stateName)) return; // CrossFade expects a hashed state; but using name is fine (Unity hashes internally) if (crossfadeDuration > 0f) _anim.CrossFadeInFixedTime(stateName, crossfadeDuration); else _anim.Play(stateName, 0, 0f); } }