-
-
Save JackDraak/9530bf86685a62a83b3f2cc0fb132346 to your computer and use it in GitHub Desktop.
/// FishDrone by JackDraak | |
/// July 2018 | |
/// 'changelog' viewable on GitHub. | |
/// | |
using UnityEngine; | |
public class FishDrone : MonoBehaviour | |
{ | |
private Animator animator; | |
private float changeDelay, changeTime; | |
private float correctedSpeed, newSpeed, speed; | |
private float correctedTurnRate, turnRate; | |
private float roughScale, scaleFactor; | |
private float sleepTime = 0; | |
private int layerMask; // = 1 << 8; // Bit shift the index of the layer (8) to get a bit mask | |
private Quaternion startQuat; | |
private Rigidbody thisRigidbody; | |
private Vector3 dimensions = Vector3.zero; | |
private Vector3 fore, port, starbord; | |
private Vector3 startPos; | |
private const float ANIMATION_SCALING_LARGE = 0.4f; | |
private const float ANIMATION_SCALING_MED = 0.7f; | |
private const float ANIMATION_SCALING_SMALL = 1.7f; | |
private const float ANIMATION_SPEED_FACTOR = 1.8f; | |
private const float CHANGE_TIME_MAX = 10f; | |
private const float CHANGE_TIME_MIN = 4f; | |
private const float LERP_FACTOR_FOR_SPEED = 0.003f; | |
private const float RAYCAST_CORRECTION_FACTOR = 13.3f; | |
private const float RAYCAST_DRAWTIME = 3f; | |
private const float RAYCAST_DETECTION_ANGLE = 37.5f; | |
private const float RAYCAST_FRAME_GAP = 0.1f; | |
private const float RAYCAST_MAX_DISTANCE = 1.0f; | |
private const float RAYCAST_SLEEP_DELAY = 0.3f; | |
private const float SCALE_MAX = 1.6f; | |
private const float SCALE_MIN = 0.4f; | |
private const float SIZE_LARGE_BREAK = 3f; | |
private const float SIZE_MID_BREAK = 2f; | |
private const float SPEED_MAX = 1.3f; | |
private const float SPEED_MIN = 0.2f; | |
private const float TURNRATE_MAX = 10f; | |
private const float TURNRATE_MIN = 3f; | |
private void BeFishy() | |
{ | |
OrientView(); | |
PlanPath(); | |
Motivate(); | |
LerpSpeed(); | |
} | |
private bool FiftyFifty() // On average, return 'True' ~half the time, and 'False' ~half the time. | |
{ | |
if (Mathf.FloorToInt(Random.Range(0, 2)) == 1) return true; | |
else return false; | |
} | |
private void FixedUpdate() | |
{ | |
BeFishy(); | |
} | |
private void Init() | |
{ | |
// Set dynamic turnrate/direction. | |
turnRate = Random.Range(TURNRATE_MIN, TURNRATE_MAX); | |
if (FiftyFifty()) turnRate = -turnRate; | |
// Set dynamic starting orientation in Y dimension. | |
Vector3 thisRotation = Vector3.zero; | |
thisRotation.y = Random.Range(0f, 360f); | |
transform.Rotate(thisRotation, Space.World); | |
// Set dynamic scale. | |
Vector3 scale = Vector3.zero; | |
scale.x = Random.Range(SCALE_MIN, SCALE_MAX); | |
scale.y = Random.Range(SCALE_MIN, SCALE_MAX); | |
scale.z = Random.Range(SCALE_MIN, SCALE_MAX); | |
transform.localScale = scale; | |
// Set dynamic animation speed (~slower for larger fish). | |
roughScale = scale.x + scale.y + scale.z / 3.0f; // Average the scales of the 3 planes. | |
if (roughScale < SIZE_MID_BREAK) scaleFactor = ANIMATION_SCALING_SMALL; | |
else if (roughScale < SIZE_LARGE_BREAK) scaleFactor = ANIMATION_SCALING_MED; | |
else scaleFactor = ANIMATION_SCALING_LARGE; | |
SetSpeed(); | |
} | |
private void LerpSpeed() | |
{ | |
if (!(Mathf.Approximately(speed, newSpeed))) | |
{ | |
speed = Mathf.Lerp(speed, newSpeed, LERP_FACTOR_FOR_SPEED); | |
animator.SetFloat("stateSpeed", speed * scaleFactor * ANIMATION_SPEED_FACTOR); | |
} | |
} | |
private void Motivate() | |
{ | |
// Turn. | |
dimensions.y = Time.deltaTime * correctedTurnRate; | |
transform.Rotate(dimensions, Space.World); | |
// Propel. | |
transform.Translate(Vector3.forward * Time.fixedDeltaTime * correctedSpeed, Space.Self); | |
if (changeTime + changeDelay < Time.time) SetSpeed(); | |
} | |
private void OrientView() | |
{ | |
// Orient transform with direction of travel. | |
transform.rotation = Quaternion.LookRotation(transform.forward); // Not strictly required. | |
// Set up whiskers based on current position and facing. | |
fore = transform.forward; | |
port = Quaternion.Euler(0, -RAYCAST_DETECTION_ANGLE, 0) * transform.forward; | |
starbord = Quaternion.Euler(0, RAYCAST_DETECTION_ANGLE, 0) * transform.forward; | |
// To create a vector on 45 degrees... | |
//left45 = (transform.forward - transform.right).normalized; // 45* to the left of fore. | |
//right45 = (transform.forward + transform.right).normalized; // 45* to the right of fore. | |
// Enable these rays to visualize the wiskers in the scene view. | |
///Debug.DrawRay(transform.position, fore, Color.magenta, 0); | |
///Debug.DrawRay(transform.position, port, Color.cyan, 0); | |
///Debug.DrawRay(transform.position, starbord, Color.green, 0); | |
} | |
// TODO note that the 'background' rocks do not have colliders... this needs to be fixed. | |
private void PlanPath() | |
{ | |
// Sleep for a spell to minimize the costly raycasting calls. | |
if (Time.time > sleepTime) | |
{ | |
sleepTime = Time.time + RAYCAST_SLEEP_DELAY; | |
RaycastHit hitPort, hitStarbord; | |
// Look ahead. | |
if (Physics.Raycast(transform.position, fore, RAYCAST_MAX_DISTANCE, layerMask)) | |
{ | |
if (correctedSpeed == 0) correctedSpeed = speed; | |
Debug.DrawRay(transform.position, fore, Color.red, RAYCAST_DRAWTIME); | |
correctedSpeed = Mathf.Lerp(speed, speed * 0.2f, 1 / correctedSpeed); | |
sleepTime = Time.time + RAYCAST_FRAME_GAP; | |
} | |
else correctedSpeed = speed; | |
// Look left. | |
if (Physics.Raycast(transform.position, port, out hitPort, RAYCAST_MAX_DISTANCE, layerMask)) | |
{ | |
Debug.DrawRay(transform.position, port, Color.blue, RAYCAST_DRAWTIME); | |
sleepTime = Time.time + RAYCAST_FRAME_GAP; | |
} | |
// Look right. | |
if (Physics.Raycast(transform.position, starbord, out hitStarbord, RAYCAST_MAX_DISTANCE, layerMask)) | |
{ | |
Debug.DrawRay(transform.position, starbord, Color.yellow, RAYCAST_DRAWTIME); | |
sleepTime = Time.time + RAYCAST_FRAME_GAP; | |
} | |
// Turn more sharply when a neighbour is detected. | |
// TODO when there are neighbors on both sides, if course is toward closer target then invert course. | |
if (hitPort.distance > 0 || hitStarbord.distance > 0) | |
{ | |
correctedTurnRate = turnRate * RAYCAST_CORRECTION_FACTOR; | |
} | |
else correctedTurnRate = turnRate; | |
} | |
} | |
private void OnCollisionEnter(Collision collision) | |
{ | |
if (collision.gameObject.tag == "Player") speed++; | |
} | |
public void Reset() | |
{ | |
if (transform != null) | |
{ | |
transform.position = startPos; | |
transform.rotation = startQuat; | |
Init(); | |
} | |
} | |
private void SetSpeed() | |
{ | |
changeTime = Time.time; | |
changeDelay = Random.Range(CHANGE_TIME_MIN, CHANGE_TIME_MAX); | |
newSpeed = Random.Range(SPEED_MIN, SPEED_MAX); | |
} | |
private void Start() | |
{ | |
// Setup for collision-avoidance: layerMask | |
// Bit shift the index of the layer (8) to get a bit mask | |
layerMask = 1 << 8; // This would cast rays only against colliders in layer 8. | |
// But we want to collide against everything except layer 8. | |
layerMask = ~layerMask; // The ~ operator inverts the bitmask. | |
animator = GetComponent<Animator>(); | |
startPos = transform.position; | |
startQuat = transform.rotation; | |
Init(); | |
} | |
} |
As noted on Reddit, if I have a scene with ~50 or more fish drones, the frame-rate begins to plummet. A couple smart commenters suggest setting up the detection to sleep for X time or X number of frames when it doesn't detect any neighbors... a great suggestion.
i.e.:
if(Time.frameCount % 10 == 0){ AvoidCollissions() ; }
I think instead of using frame-count I'll use Time.time and let each of the 3 whiskers be independent (i.e. each one will have it's own timer, so that I can define a maximum sleep period, and 2/3 can continue to sleep when another is active.
Ugh, doing 3 timers was causing havoc because of the way I have it set up to use both left and right views before making a decision, so for now I've got a single timer that sleeps for 0.3 seconds if there are no neighbors detected. My current scene has 138 fish drones active, and is maintaining 50+ FPS. (Previously, 50 drones ran ~15-25 FPS).
Recent Updates: I've been working on the OrientView() and PlanPath() functions. The proper Google search request can really help get you places, that's when I stumbled onto the two tricks for casting rays at 45 degrees or arbitrary angles.
VIDEO DEMO: https://youtu.be/SCe_iFqrBa4