using System; using System.Collections; using System.Collections.Generic; using Unity.MLAgents; using Unity.MLAgents.Sensors; using UnityEngine; /// /// A hummingbird Machine Learning Agent /// public class HummingbirdAgent : Agent { [Tooltip("Force to apply when moving")] public float moveForce = 2f; [Tooltip("Speed to pitch up or down")] public float pitchSpeed = 100f; [Tooltip("Speed to rotate around the up axis")] public float yawSpeed = 100f; [Tooltip("Transform at the tip of the beak")] public Transform beakTip; [Tooltip("The agent's camera")] public Camera agentCamera; [Tooltip("Whether this is training mode or gameplay mode")] public bool trainingMode; // The rigidbody of the agent new private Rigidbody rigidbody; // The flower area that the agent is in private FlowerArea flowerArea; // The nearest flower to the agent private Flower nearestFlower; // Allows for smoother pitch changes private float smoothPitchChange = 0f; // Allows for smoother yaw changes private float smoothYawChange = 0f; // Maximum angle that the bird can pitch up or down private const float MaxPitchAngle = 80f; // Maximum distance from the beak tip to accept nectar collision private const float BeakTipRadius = 0.008f; // Whether the agent is frozen (intentionally not flying) private bool frozen = false; /// /// The amount of nectar the agent has obtained this episode /// public float NectarObtained { get; private set; } /// /// Initialize the agent /// public override void Initialize() { rigidbody = GetComponent(); flowerArea = GetComponentInParent(); // If not training mode, no max step, play forever if (!trainingMode) MaxStep = 0; } /// /// Reset the agent when an episode begins /// public override void OnEpisodeBegin() { if (trainingMode) { // Only reset flowers in training when there is one agent per area flowerArea.ResetFlowers(); } // Reset nectar obtained NectarObtained = 0f; // Zero out velocities so that movement stops before a new episode begins rigidbody.velocity = Vector3.zero; rigidbody.angularVelocity = Vector3.zero; // Default to spawning in front of a flower bool inFrontOfFlower = true; if (trainingMode) { // Spawn in front of flower 50% of the time during training inFrontOfFlower = UnityEngine.Random.value > .5f; } // Move the agent to a new random position MoveToSafeRandomPosition(inFrontOfFlower); // Recalculate the nearest flower now that the agent has moved UpdateNearestFlower(); } /// /// Called when and action is received from either the player input or the neural network /// /// vectorAction[i] represents: /// Index 0: move vector x (+1 = right, -1 = left) /// Index 1: move vector y (+1 = up, -1 = down) /// Index 2: move vector z (+1 = forward, -1 = backward) /// Index 3: pitch angle (+1 = pitch up, -1 = pitch down) /// Index 4: yaw angle (+1 = turn right, -1 = turn left) /// /// The actions to take public override void OnActionReceived(float[] vectorAction) { // Don't take actions if frozen if (frozen) return; // Calculate movement vector Vector3 move = new Vector3(vectorAction[0], vectorAction[1], vectorAction[2]); // Add force in the direction of the move vector rigidbody.AddForce(move * moveForce); // Get the current rotation Vector3 rotationVector = transform.rotation.eulerAngles; // Calculate pitch and yaw rotation float pitchChange = vectorAction[3]; float yawChange = vectorAction[4]; // Calculate smooth rotation changes smoothPitchChange = Mathf.MoveTowards(smoothPitchChange, pitchChange, 2f * Time.fixedDeltaTime); smoothYawChange = Mathf.MoveTowards(smoothYawChange, yawChange, 2f * Time.fixedDeltaTime); // Calculate new pitch and yaw based on smoothed values // Clamp pitch to avoid flipping upside down float pitch = rotationVector.x + smoothPitchChange * Time.fixedDeltaTime * pitchSpeed; if (pitch > 180f) pitch -= 360f; pitch = Mathf.Clamp(pitch, -MaxPitchAngle, MaxPitchAngle); float yaw = rotationVector.y + smoothYawChange * Time.fixedDeltaTime * yawSpeed; // Apply the new rotation transform.rotation = Quaternion.Euler(pitch, yaw, 0f); } /// /// Collect vector observations from the environment /// /// The vector sensor public override void CollectObservations(VectorSensor sensor) { // If nearestFlower is null, observe an empty array and return early if (nearestFlower == null) { sensor.AddObservation(new float[10]); return; } // Observe the agent's local rotation (4 observations) sensor.AddObservation(transform.localRotation.normalized); // Get a vector from the beak tip to the nearest flower Vector3 toFlower = nearestFlower.FlowerCenterPosition - beakTip.position; // Observe a normalized vector pointing to the nearest flower (3 observations) sensor.AddObservation(toFlower.normalized); // Observe a dot product that indicates whether the beak tip is in front of the flower (1 observation) // (+1 means that the beak tip is directly in front of the flower, -1 means directly behind) sensor.AddObservation(Vector3.Dot(toFlower.normalized, -nearestFlower.FlowerUpVector.normalized)); // Observe a dot product that indicates whether the beak is pointing toward the flower (1 observation) // (+1 means that the beak is pointing directly at the flower, -1 means directly away) sensor.AddObservation(Vector3.Dot(beakTip.forward.normalized, -nearestFlower.FlowerUpVector.normalized)); // Observe the relative distance from the beak tip to the flower (1 observation) sensor.AddObservation(toFlower.magnitude / FlowerArea.AreaDiameter); // 10 total observations } /// /// When Behavior Type is set to "Heuristic Only" on the agent's Behavior Parameters, /// this function will be called. Its return values will be fed into /// instead of using the neural network /// /// And output action array public override void Heuristic(float[] actionsOut) { // Create placeholders for all movement/turning Vector3 forward = Vector3.zero; Vector3 left = Vector3.zero; Vector3 up = Vector3.zero; float pitch = 0f; float yaw = 0f; // Convert keyboard inputs to movement and turning // All values should be between -1 and +1 // Forward/backward if (Input.GetKey(KeyCode.W)) forward = transform.forward; else if (Input.GetKey(KeyCode.S)) forward = -transform.forward; // Left/right if (Input.GetKey(KeyCode.A)) left = -transform.right; else if (Input.GetKey(KeyCode.D)) left = transform.right; // Up/down if (Input.GetKey(KeyCode.E)) up = transform.up; else if (Input.GetKey(KeyCode.C)) up = -transform.up; // Pitch up/down if (Input.GetKey(KeyCode.UpArrow)) pitch = 1f; else if (Input.GetKey(KeyCode.DownArrow)) pitch = -1f; // Turn left/right if (Input.GetKey(KeyCode.LeftArrow)) yaw = -1f; else if (Input.GetKey(KeyCode.RightArrow)) yaw = 1f; // Combine the movement vectors and normalize Vector3 combined = (forward + left + up).normalized; // Add the 3 movement values, pitch, and yaw to the actionsOut array actionsOut[0] = combined.x; actionsOut[1] = combined.y; actionsOut[2] = combined.z; actionsOut[3] = pitch; actionsOut[4] = yaw; } /// /// Prevent the agent from moving and taking actions /// public void FreezeAgent() { Debug.Assert(trainingMode == false, "Freeze/Unfreeze not supported in training"); frozen = true; rigidbody.Sleep(); } /// /// Resume agent movement and actions /// public void UnfreezeAgent() { Debug.Assert(trainingMode == false, "Freeze/Unfreeze not supported in training"); frozen = false; rigidbody.WakeUp(); } /// /// Move the agent to a safe random position (i.e. does not collide with anything) /// If in front of flower, also point the beak at the flower /// /// Whether to choose a spot in front of a flower private void MoveToSafeRandomPosition(bool inFrontOfFlower) { bool safePositionFound = false; int attemptsRemaining = 100; // Prevent an infinite loop Vector3 potentialPosition = Vector3.zero; Quaternion potentialRotation = new Quaternion(); // Loop until a safe position is found or we run out of attempts while (!safePositionFound && attemptsRemaining > 0) { attemptsRemaining--; if (inFrontOfFlower) { // Pick a random flower Flower randomFlower = flowerArea.Flowers[UnityEngine.Random.Range(0, flowerArea.Flowers.Count)]; // Position 10 to 20 cm in front of the flower float distanceFromFlower = UnityEngine.Random.Range(.1f, .2f); potentialPosition = randomFlower.transform.position + randomFlower.FlowerUpVector * distanceFromFlower; // Point beak at flower (bird's head is center of transform) Vector3 toFlower = randomFlower.FlowerCenterPosition - potentialPosition; potentialRotation = Quaternion.LookRotation(toFlower, Vector3.up); } else { // Pick a random height from the ground float height = UnityEngine.Random.Range(1.2f, 2.5f); // Pick a random radius from the center of the area float radius = UnityEngine.Random.Range(2f, 7f); // Pick a random direction rotated around the y axis Quaternion direction = Quaternion.Euler(0f, UnityEngine.Random.Range(-180f, 180f), 0f); // Combine height, radius, and direction to pick a potential position potentialPosition = flowerArea.transform.position + Vector3.up * height + direction * Vector3.forward * radius; // Choose and set random starting pitch and yaw float pitch = UnityEngine.Random.Range(-60f, 60f); float yaw = UnityEngine.Random.Range(-180f, 180f); potentialRotation = Quaternion.Euler(pitch, yaw, 0f); } // Check to see if the agent will collide with anything Collider[] colliders = Physics.OverlapSphere(potentialPosition, 0.05f); // Safe position has been found if no colliders are overlapped safePositionFound = colliders.Length == 0; } Debug.Assert(safePositionFound, "Could not find a safe position to spawn"); // Set the position and rotation transform.position = potentialPosition; transform.rotation = potentialRotation; } /// /// Update the nearest flower to the agent /// private void UpdateNearestFlower() { foreach (Flower flower in flowerArea.Flowers) { if (nearestFlower == null && flower.HasNectar) { // No current nearest flower and this flower has nectar, so set to this flower nearestFlower = flower; } else if (flower.HasNectar) { // Calculate distance to this flower and distance to the current nearest flower float distanceToFlower = Vector3.Distance(flower.transform.position, beakTip.position); float distanceToCurrentNearestFlower = Vector3.Distance(nearestFlower.transform.position, beakTip.position); // If current nearest flower is empty OR this flower is closer, update the nearest flower if (!nearestFlower.HasNectar || distanceToFlower < distanceToCurrentNearestFlower) { nearestFlower = flower; } } } } /// /// Called when the agent's collider enters a trigger collider /// /// The trigger collider private void OnTriggerEnter(Collider other) { TriggerEnterOrStay(other); } /// /// Called when the agent's collider stays in a trigger collider /// /// The trigger collider private void OnTriggerStay(Collider other) { TriggerEnterOrStay(other); } /// /// Handles when the agen'ts collider enters or stays in a trigger collider /// /// The trigger collider private void TriggerEnterOrStay(Collider collider) { // Check if agent is colliding with nectar if (collider.CompareTag("nectar")) { Vector3 closestPointToBeakTip = collider.ClosestPoint(beakTip.position); // Check if the closest collision point is close to the beak tip // Note: a collision with anything but the beak tip should not count if (Vector3.Distance(beakTip.position, closestPointToBeakTip) < BeakTipRadius) { // Look up the flower for this nectar collider Flower flower = flowerArea.GetFlowerFromNectar(collider); // Attempt to take .01 nectar // Note: this is per fixed timestep, meaning it happens every .02 seconds, or 50x per second float nectarReceived = flower.Feed(.01f); // Keep track of nectar obtained NectarObtained += nectarReceived; if (trainingMode) { // Calculate reward for getting nectar float bonus = .02f * Mathf.Clamp01(Vector3.Dot(transform.forward.normalized, -nearestFlower.FlowerUpVector.normalized)); AddReward(.01f + bonus); } // If flower is empty, update the nearest flower if (!flower.HasNectar) { UpdateNearestFlower(); } } } } /// /// Called when the agent collides with something solid /// /// The collision info private void OnCollisionEnter(Collision collision) { if (trainingMode && collision.collider.CompareTag("boundary")) { // Collided with the area boundary, give a negative reward AddReward(-.5f); } } /// /// Called every frame /// private void Update() { // Draw a line from the beak tip to the nearest flower if (nearestFlower != null) Debug.DrawLine(beakTip.position, nearestFlower.FlowerCenterPosition, Color.green); } /// /// Called every .02 seconds /// private void FixedUpdate() { // Avoids scenario where nearest flower nectar is stolen by opponent and not updated if (nearestFlower != null && !nearestFlower.HasNectar) UpdateNearestFlower(); } }