using System.Collections.Generic; using Unity.BossRoom.Gameplay.GameplayObjects; using Unity.Netcode; using UnityEngine; namespace Unity.BossRoom.Gameplay.Actions { public static class ActionUtils { //cache Physics Cast hits, to minimize allocs. static RaycastHit[] s_Hits = new RaycastHit[4]; // cache layer IDs (after first use). -1 is a sentinel value meaning "uninitialized" static int s_PCLayer = -1; static int s_NpcLayer = -1; static int s_EnvironmentLayer = -1; /// /// When doing line-of-sight checks we assume the characters' "eyes" are at this height above their transform /// static readonly Vector3 k_CharacterEyelineOffset = new Vector3(0, 1, 0); /// /// When teleporting to a destination, this is how far away from the destination spot to arrive /// const float k_CloseDistanceOffset = 1; /// /// When checking if a teleport-destination is "too close" to the starting spot, anything less than this is too close /// const float k_VeryCloseTeleportRange = k_CloseDistanceOffset + 1; /// /// Does a melee foe hit detect. /// /// true if the attacker is an NPC (and therefore should hit PCs). False for the reverse. /// The collider of the attacking GameObject. /// The range in meters to check for foes. /// Place an uninitialized RayCastHit[] ref in here. It will be set to the results array. /// /// This method does not alloc. It returns a maximum of 4 results. Consume the results immediately, as the array will be overwritten with /// the next similar query. /// /// Total number of foes encountered. public static int DetectMeleeFoe(bool isNPC, Collider attacker, float range, out RaycastHit[] results) { return DetectNearbyEntities(isNPC, !isNPC, attacker, range, out results); } /// /// Detects friends and/or foes near us. /// /// true if we should detect PCs /// true if we should detect NPCs /// The collider of the attacking GameObject. /// The range in meters to check. /// Place an uninitialized RayCastHit[] ref in here. It will be set to the results array. /// public static int DetectNearbyEntities(bool wantPcs, bool wantNpcs, Collider attacker, float range, out RaycastHit[] results) { //this simple detect just does a boxcast out from our position in the direction we're facing, out to the range of the attack. var myBounds = attacker.bounds; if (s_PCLayer == -1) s_PCLayer = LayerMask.NameToLayer("PCs"); if (s_NpcLayer == -1) s_NpcLayer = LayerMask.NameToLayer("NPCs"); int mask = 0; if (wantPcs) mask |= (1 << s_PCLayer); if (wantNpcs) mask |= (1 << s_NpcLayer); int numResults = Physics.BoxCastNonAlloc(attacker.transform.position, myBounds.extents, attacker.transform.forward, s_Hits, Quaternion.identity, range, mask); results = s_Hits; return numResults; } /// /// Does this NetId represent a valid target? Used by Target Action. The target needs to exist, be a /// NetworkCharacterState, and be alive. In the future, it will be any non-dead IDamageable. /// /// the NetId of the target to investigate /// true if this is a valid target public static bool IsValidTarget(ulong targetId) { //note that we DON'T check if you're an ally. It's perfectly valid to target friends, //because there are friendly skills, such as Heal. if (NetworkManager.Singleton.SpawnManager == null || !NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(targetId, out var targetChar)) { return false; } var targetable = targetChar.GetComponent(); return targetable != null && targetable.IsValidTarget; } /// /// Given the coordinates of two entities, checks to see if there is an obstacle between them. /// (Since character coordinates are beneath the feet of the visual avatar, we add a small amount of height to /// these coordinates to simulate their eye-line.) /// /// first character's position /// second character's position /// the point where an obstruction occurred (or if no obstruction, this is just character2Pos) /// true if no obstructions, false if there is a Ground-layer object in the way public static bool HasLineOfSight(Vector3 character1Pos, Vector3 character2Pos, out Vector3 missPos) { if (s_EnvironmentLayer == -1) { s_EnvironmentLayer = LayerMask.NameToLayer("Environment"); } int mask = 1 << s_EnvironmentLayer; character1Pos += k_CharacterEyelineOffset; character2Pos += k_CharacterEyelineOffset; var rayDirection = character2Pos - character1Pos; var distance = rayDirection.magnitude; var numHits = Physics.RaycastNonAlloc(new Ray(character1Pos, rayDirection), s_Hits, distance, mask); if (numHits == 0) { missPos = character2Pos; return true; } else { missPos = s_Hits[0].point; return false; } } /// /// Helper method that calculates the percent a charge-up action is charged, based on how long it has run, returning a value /// from 0-1. /// /// The time when we finished charging up, or 0 if we're still charging. /// How long the action has been running. /// when the action started. /// the total execution time of the action (usually not its duration). /// Percent charge-up, from 0 to 1. public static float GetPercentChargedUp(float stoppedChargingUpTime, float timeRunning, float timeStarted, float execTime) { float timeSpentChargingUp; if (stoppedChargingUpTime == 0) { timeSpentChargingUp = timeRunning; // we're still charging up, so all of our runtime has been charge-up time } else { timeSpentChargingUp = stoppedChargingUpTime - timeStarted; } return Mathf.Clamp01(timeSpentChargingUp / execTime); } /// /// Determines a spot very near a chosen location, so that we can teleport next to the target (rather /// than teleporting literally on top of the target). Can optionally perform a bunch of additional checks: /// - can do a line-of-sight check and stop at the first obstruction. /// - can make sure that the chosen spot is a meaningful distance away from the starting spot. /// - can make sure that the chosen spot is no further than a specified distance away. /// /// character's transform /// location we want to be next to /// true if we should be blocked by obstructions such as walls /// if we should fix up very short teleport destinations, the new location will be this far away (in meters). -1 = don't check for short teleports /// returned location will be no further away from characterTransform than this. -1 = no max distance /// new coordinates that are near the destination (or near the first obstruction) public static Vector3 GetDashDestination(Transform characterTransform, Vector3 targetSpot, bool stopAtObstructions, float distanceToUseIfVeryClose = -1, float maxDistance = -1) { Vector3 destinationSpot = targetSpot; if (distanceToUseIfVeryClose != -1) { // make sure our stopping point is a meaningful distance away! if (destinationSpot == Vector3.zero || Vector3.Distance(characterTransform.position, destinationSpot) <= k_VeryCloseTeleportRange) { // we don't have a meaningful stopping spot. Find a new one based on the character's current direction destinationSpot = characterTransform.position + characterTransform.forward * distanceToUseIfVeryClose; } } if (maxDistance != -1) { // make sure our stopping point isn't too far away! float distance = Vector3.Distance(characterTransform.position, destinationSpot); if (distance > maxDistance) { destinationSpot = Vector3.MoveTowards(destinationSpot, characterTransform.position, distance - maxDistance); } } if (stopAtObstructions) { // if we're going to hit an obstruction, stop at the obstruction if (!HasLineOfSight(characterTransform.position, destinationSpot, out Vector3 collidePos)) { destinationSpot = collidePos; } } // now get a spot "near" the end point destinationSpot = Vector3.MoveTowards(destinationSpot, characterTransform.position, k_CloseDistanceOffset); return destinationSpot; } } /// /// Small utility to better understand action start and stop conclusion /// public static class ActionConclusion { public const bool Stop = false; public const bool Continue = true; } /// /// Utility comparer to sort through RaycastHits by distance. /// public class RaycastHitComparer : IComparer { public int Compare(RaycastHit x, RaycastHit y) { return x.distance.CompareTo(y.distance); } } }