You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

237 lines
11 KiB
C#

3 weeks ago
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;
/// <summary>
/// When doing line-of-sight checks we assume the characters' "eyes" are at this height above their transform
/// </summary>
static readonly Vector3 k_CharacterEyelineOffset = new Vector3(0, 1, 0);
/// <summary>
/// When teleporting to a destination, this is how far away from the destination spot to arrive
/// </summary>
const float k_CloseDistanceOffset = 1;
/// <summary>
/// When checking if a teleport-destination is "too close" to the starting spot, anything less than this is too close
/// </summary>
const float k_VeryCloseTeleportRange = k_CloseDistanceOffset + 1;
/// <summary>
/// Does a melee foe hit detect.
/// </summary>
/// <param name="isNPC">true if the attacker is an NPC (and therefore should hit PCs). False for the reverse.</param>
/// <param name="attacker">The collider of the attacking GameObject.</param>
/// <param name="range">The range in meters to check for foes.</param>
/// <param name="results">Place an uninitialized RayCastHit[] ref in here. It will be set to the results array. </param>
/// <remarks>
/// 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.
/// </remarks>
/// <returns>Total number of foes encountered. </returns>
public static int DetectMeleeFoe(bool isNPC, Collider attacker, float range, out RaycastHit[] results)
{
return DetectNearbyEntities(isNPC, !isNPC, attacker, range, out results);
}
/// <summary>
/// Detects friends and/or foes near us.
/// </summary>
/// <param name="wantPcs">true if we should detect PCs</param>
/// <param name="wantNpcs">true if we should detect NPCs</param>
/// <param name="attacker">The collider of the attacking GameObject.</param>
/// <param name="range">The range in meters to check.</param>
/// <param name="results">Place an uninitialized RayCastHit[] ref in here. It will be set to the results array. </param>
/// <returns></returns>
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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="targetId">the NetId of the target to investigate</param>
/// <returns>true if this is a valid target</returns>
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<ITargetable>();
return targetable != null && targetable.IsValidTarget;
}
/// <summary>
/// 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.)
/// </summary>
/// <param name="character1Pos">first character's position</param>
/// <param name="character2Pos">second character's position</param>
/// <param name="missPos">the point where an obstruction occurred (or if no obstruction, this is just character2Pos)</param>
/// <returns>true if no obstructions, false if there is a Ground-layer object in the way</returns>
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;
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="stoppedChargingUpTime">The time when we finished charging up, or 0 if we're still charging.</param>
/// <param name="timeRunning">How long the action has been running. </param>
/// <param name="timeStarted">when the action started. </param>
/// <param name="execTime">the total execution time of the action (usually not its duration). </param>
/// <returns>Percent charge-up, from 0 to 1. </returns>
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);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="characterTransform">character's transform</param>
/// <param name="targetSpot">location we want to be next to</param>
/// <param name="stopAtObstructions">true if we should be blocked by obstructions such as walls</param>
/// <param name="distanceToUseIfVeryClose">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</param>
/// <param name="maxDistance">returned location will be no further away from characterTransform than this. -1 = no max distance</param>
/// <returns>new coordinates that are near the destination (or near the first obstruction)</returns>
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;
}
}
/// <summary>
/// Small utility to better understand action start and stop conclusion
/// </summary>
public static class ActionConclusion
{
public const bool Stop = false;
public const bool Continue = true;
}
/// <summary>
/// Utility comparer to sort through RaycastHits by distance.
/// </summary>
public class RaycastHitComparer : IComparer<RaycastHit>
{
public int Compare(RaycastHit x, RaycastHit y)
{
return x.distance.CompareTo(y.distance);
}
}
}