281 lines
16 KiB
C#
281 lines
16 KiB
C#
using UnityEngine;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
|
|
namespace BehaviorDesigner.Runtime.Tasks.Movement
|
|
{
|
|
public static class MovementUtility
|
|
{
|
|
private static Dictionary<GameObject, Dictionary<Type, Component>> gameObjectComponentMap = new Dictionary<GameObject, Dictionary<Type, Component>>();
|
|
private static Dictionary<GameObject, Dictionary<Type, Component[]>> gameObjectComponentsMap = new Dictionary<GameObject, Dictionary<Type, Component[]>>();
|
|
|
|
// Cast a sphere with the desired distance. Check each collider hit to see if it is within the field of view. Set objectFound
|
|
// to the object that is most directly in front of the agent
|
|
public static GameObject WithinSight(Transform transform, Vector3 positionOffset, float fieldOfViewAngle, float viewDistance, LayerMask objectLayerMask, Vector3 targetOffset, LayerMask ignoreLayerMask, bool useTargetBone, HumanBodyBones targetBone)
|
|
{
|
|
GameObject objectFound = null;
|
|
var hitColliders = Physics.OverlapSphere(transform.position, viewDistance, objectLayerMask);
|
|
if (hitColliders != null) {
|
|
float minAngle = Mathf.Infinity;
|
|
for (int i = 0; i < hitColliders.Length; ++i) {
|
|
float angle;
|
|
GameObject obj;
|
|
// Call the WithinSight function to determine if this specific object is within sight
|
|
if ((obj = WithinSight(transform, positionOffset, fieldOfViewAngle, viewDistance, hitColliders[i].gameObject, targetOffset, false, 0, out angle, ignoreLayerMask, useTargetBone, targetBone)) != null) {
|
|
// This object is within sight. Set it to the objectFound GameObject if the angle is less than any of the other objects
|
|
if (angle < minAngle) {
|
|
minAngle = angle;
|
|
objectFound = obj;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return objectFound;
|
|
}
|
|
|
|
// Cast a circle with the desired distance. Check each collider hit to see if it is within the field of view. Set objectFound
|
|
// to the object that is most directly in front of the agent
|
|
public static GameObject WithinSight2D(Transform transform, Vector3 positionOffset, float fieldOfViewAngle, float viewDistance, LayerMask objectLayerMask, Vector3 targetOffset, float angleOffset2D, LayerMask ignoreLayerMask)
|
|
{
|
|
GameObject objectFound = null;
|
|
var hitColliders = Physics2D.OverlapCircleAll(transform.position, viewDistance, objectLayerMask);
|
|
if (hitColliders != null) {
|
|
float minAngle = Mathf.Infinity;
|
|
for (int i = 0; i < hitColliders.Length; ++i) {
|
|
float angle;
|
|
GameObject obj;
|
|
// Call the 2D WithinSight function to determine if this specific object is within sight
|
|
if ((obj = WithinSight(transform, positionOffset, fieldOfViewAngle, viewDistance, hitColliders[i].gameObject, targetOffset, true, angleOffset2D, out angle, ignoreLayerMask, false, HumanBodyBones.Hips)) != null) {
|
|
// This object is within sight. Set it to the objectFound GameObject if the angle is less than any of the other objects
|
|
if (angle < minAngle) {
|
|
minAngle = angle;
|
|
objectFound = obj;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return objectFound;
|
|
}
|
|
|
|
// Public helper function that will automatically create an angle variable that is not used. This function is useful if the calling object doesn't
|
|
// care about the angle between transform and targetObject
|
|
public static GameObject WithinSight(Transform transform, Vector3 positionOffset, float fieldOfViewAngle, float viewDistance, GameObject targetObject, Vector3 targetOffset, LayerMask ignoreLayerMask, bool useTargetBone, HumanBodyBones targetBone)
|
|
{
|
|
float angle;
|
|
return WithinSight(transform, positionOffset, fieldOfViewAngle, viewDistance, targetObject, targetOffset, false, 0, out angle, ignoreLayerMask, useTargetBone, targetBone);
|
|
}
|
|
|
|
// Public helper function that will automatically create an angle variable that is not used. This function is useful if the calling object doesn't
|
|
// care about the angle between transform and targetObject
|
|
public static GameObject WithinSight2D(Transform transform, Vector3 positionOffset, float fieldOfViewAngle, float viewDistance, GameObject targetObject, Vector3 targetOffset, float angleOffset2D, LayerMask ignoreLayerMask, bool useTargetBone, HumanBodyBones targetBone)
|
|
{
|
|
float angle;
|
|
return WithinSight(transform, positionOffset, fieldOfViewAngle, viewDistance, targetObject, targetOffset, true, angleOffset2D, out angle, ignoreLayerMask, useTargetBone, targetBone);
|
|
}
|
|
|
|
// Determines if the targetObject is within sight of the transform. It will set the angle regardless of whether or not the object is within sight
|
|
public static GameObject WithinSight(Transform transform, Vector3 positionOffset, float fieldOfViewAngle, float viewDistance, GameObject targetObject, Vector3 targetOffset, bool usePhysics2D, float angleOffset2D, out float angle, int ignoreLayerMask, bool useTargetBone, HumanBodyBones targetBone)
|
|
{
|
|
if (targetObject == null) {
|
|
angle = 0;
|
|
return null;
|
|
}
|
|
if (useTargetBone) {
|
|
Animator animator;
|
|
if ((animator = GetComponentForType<Animator>(targetObject)) != null) {
|
|
var bone = animator.GetBoneTransform(targetBone);
|
|
if (bone != null) {
|
|
targetObject = bone.gameObject;
|
|
}
|
|
}
|
|
}
|
|
// The target object needs to be within the field of view of the current object
|
|
var direction = targetObject.transform.position - transform.TransformPoint(positionOffset);
|
|
if (usePhysics2D) {
|
|
var eulerAngles = transform.eulerAngles;
|
|
eulerAngles.z -= angleOffset2D;
|
|
angle = Vector3.Angle(direction, Quaternion.Euler(eulerAngles) * Vector3.up);
|
|
direction.z = 0;
|
|
} else {
|
|
angle = Vector3.Angle(direction, transform.forward);
|
|
direction.y = 0;
|
|
}
|
|
if (direction.magnitude < viewDistance && angle < fieldOfViewAngle * 0.5f) {
|
|
// The hit agent needs to be within view of the current agent
|
|
if (LineOfSight(transform, positionOffset, targetObject, targetOffset, usePhysics2D, ignoreLayerMask) != null) {
|
|
return targetObject; // return the target object meaning it is within sight
|
|
} else if (GetComponentForType<Collider>(targetObject) == null && GetComponentForType<Collider2D>(targetObject) == null) {
|
|
// If the linecast doesn't hit anything then that the target object doesn't have a collider and there is nothing in the way
|
|
if (targetObject.gameObject.activeSelf)
|
|
return targetObject;
|
|
}
|
|
}
|
|
// return null if the target object is not within sight
|
|
return null;
|
|
}
|
|
|
|
public static GameObject LineOfSight(Transform transform, Vector3 positionOffset, GameObject targetObject, Vector3 targetOffset, bool usePhysics2D, int ignoreLayerMask)
|
|
{
|
|
if (usePhysics2D) {
|
|
RaycastHit2D hit;
|
|
if ((hit = Physics2D.Linecast(transform.TransformPoint(positionOffset), targetObject.transform.TransformPoint(targetOffset), ~ignoreLayerMask))) {
|
|
if (hit.transform.IsChildOf(targetObject.transform) || targetObject.transform.IsChildOf(hit.transform)) {
|
|
return targetObject; // return the target object meaning it is within sight
|
|
}
|
|
}
|
|
} else {
|
|
RaycastHit hit;
|
|
if (Physics.Linecast(transform.TransformPoint(positionOffset), targetObject.transform.TransformPoint(targetOffset), out hit, ~ignoreLayerMask)) {
|
|
if (hit.transform.IsChildOf(targetObject.transform) || targetObject.transform.IsChildOf(hit.transform)) {
|
|
return targetObject; // return the target object meaning it is within sight
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Cast a sphere with the desired radius. Check each object's audio source to see if audio is playing. If audio is playing
|
|
// and its audibility is greater than the audibility threshold then return the object heard
|
|
public static GameObject WithinHearingRange(Transform transform, Vector3 positionOffset, float audibilityThreshold, float hearingRadius, LayerMask objectLayerMask)
|
|
{
|
|
GameObject objectHeard = null;
|
|
var hitColliders = Physics.OverlapSphere(transform.TransformPoint(positionOffset), hearingRadius, objectLayerMask);
|
|
if (hitColliders != null) {
|
|
float maxAudibility = 0;
|
|
for (int i = 0; i < hitColliders.Length; ++i) {
|
|
float audibility = 0;
|
|
GameObject obj;
|
|
// Call the WithinHearingRange function to determine if this specific object is within hearing range
|
|
if ((obj = WithinHearingRange(transform, positionOffset, audibilityThreshold, hitColliders[i].gameObject, ref audibility)) != null) {
|
|
// This object is within hearing range. Set it to the objectHeard GameObject if the audibility is less than any of the other objects
|
|
if (audibility > maxAudibility) {
|
|
maxAudibility = audibility;
|
|
objectHeard = obj;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return objectHeard;
|
|
}
|
|
|
|
// Cast a circle with the desired radius. Check each object's audio source to see if audio is playing. If audio is playing
|
|
// and its audibility is greater than the audibility threshold then return the object heard
|
|
public static GameObject WithinHearingRange2D(Transform transform, Vector3 positionOffset, float audibilityThreshold, float hearingRadius, LayerMask objectLayerMask)
|
|
{
|
|
GameObject objectHeard = null;
|
|
var hitColliders = Physics2D.OverlapCircleAll(transform.TransformPoint(positionOffset), hearingRadius, objectLayerMask);
|
|
if (hitColliders != null) {
|
|
float maxAudibility = 0;
|
|
for (int i = 0; i < hitColliders.Length; ++i) {
|
|
float audibility = 0;
|
|
GameObject obj;
|
|
// Call the WithinHearingRange function to determine if this specific object is within hearing range
|
|
if ((obj = WithinHearingRange(transform, positionOffset, audibilityThreshold, hitColliders[i].gameObject, ref audibility)) != null) {
|
|
// This object is within hearing range. Set it to the objectHeard GameObject if the audibility is less than any of the other objects
|
|
if (audibility > maxAudibility) {
|
|
maxAudibility = audibility;
|
|
objectHeard = obj;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return objectHeard;
|
|
}
|
|
|
|
// Public helper function that will automatically create an audibility variable that is not used. This function is useful if the calling call doesn't
|
|
// care about the audibility value
|
|
public static GameObject WithinHearingRange(Transform transform, Vector3 positionOffset, float audibilityThreshold, GameObject targetObject)
|
|
{
|
|
float audibility = 0;
|
|
return WithinHearingRange(transform, positionOffset, audibilityThreshold, targetObject, ref audibility);
|
|
}
|
|
|
|
public static GameObject WithinHearingRange(Transform transform, Vector3 positionOffset, float audibilityThreshold, GameObject targetObject, ref float audibility)
|
|
{
|
|
AudioSource[] colliderAudioSource;
|
|
// Check to see if the hit agent has an audio source and that audio source is playing
|
|
if ((colliderAudioSource = GetComponentsForType<AudioSource>(targetObject)) != null) {
|
|
for (int i = 0; i < colliderAudioSource.Length; ++i) {
|
|
if (colliderAudioSource[i].isPlaying) {
|
|
var distance = Vector3.Distance(transform.position, targetObject.transform.position);
|
|
if (colliderAudioSource[i].rolloffMode == AudioRolloffMode.Logarithmic) {
|
|
audibility = colliderAudioSource[i].volume / Mathf.Max(colliderAudioSource[i].minDistance, distance - colliderAudioSource[i].minDistance);
|
|
} else { // linear
|
|
audibility = colliderAudioSource[i].volume * Mathf.Clamp01((distance - colliderAudioSource[i].minDistance) / (colliderAudioSource[i].maxDistance - colliderAudioSource[i].minDistance));
|
|
}
|
|
if (audibility > audibilityThreshold) {
|
|
return targetObject;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Draws the line of sight representation
|
|
public static void DrawLineOfSight(Transform transform, Vector3 positionOffset, float fieldOfViewAngle, float angleOffset, float viewDistance, bool usePhysics2D)
|
|
{
|
|
#if UNITY_EDITOR
|
|
var oldColor = UnityEditor.Handles.color;
|
|
var color = Color.yellow;
|
|
color.a = 0.1f;
|
|
UnityEditor.Handles.color = color;
|
|
|
|
var halfFOV = fieldOfViewAngle * 0.5f + angleOffset;
|
|
var beginDirection = Quaternion.AngleAxis(-halfFOV, (usePhysics2D ? Vector3.forward : Vector3.up)) * (usePhysics2D ? transform.up : transform.forward);
|
|
UnityEditor.Handles.DrawSolidArc(transform.TransformPoint(positionOffset), (usePhysics2D ? transform.forward : transform.up), beginDirection, fieldOfViewAngle, viewDistance);
|
|
|
|
UnityEditor.Handles.color = oldColor;
|
|
#endif
|
|
}
|
|
|
|
public static T GetComponentForType<T>(GameObject target) where T : Component
|
|
{
|
|
Dictionary<Type, Component> typeComponentMap;
|
|
Component targetComponent;
|
|
// Return the cached component if it exists.
|
|
if (gameObjectComponentMap.TryGetValue(target, out typeComponentMap)) {
|
|
if (typeComponentMap.TryGetValue(typeof(T), out targetComponent)) {
|
|
return targetComponent as T;
|
|
}
|
|
} else {
|
|
// The cached component doesn't exist for the specified type.
|
|
typeComponentMap = new Dictionary<Type, Component>();
|
|
gameObjectComponentMap.Add(target, typeComponentMap);
|
|
}
|
|
|
|
// Find the component reference and cache the results.
|
|
targetComponent = target.GetComponent<T>();
|
|
typeComponentMap.Add(typeof(T), targetComponent);
|
|
return targetComponent as T;
|
|
}
|
|
|
|
public static T[] GetComponentsForType<T>(GameObject target) where T : Component
|
|
{
|
|
Dictionary<Type, Component[]> typeComponentsMap;
|
|
Component[] targetComponents;
|
|
// Return the cached component if it exists.
|
|
if (gameObjectComponentsMap.TryGetValue(target, out typeComponentsMap)) {
|
|
if (typeComponentsMap.TryGetValue(typeof(T), out targetComponents)) {
|
|
return targetComponents as T[];
|
|
}
|
|
} else {
|
|
// The cached components doesn't exist for the specified type.
|
|
typeComponentsMap = new Dictionary<Type, Component[]>();
|
|
gameObjectComponentsMap.Add(target, typeComponentsMap);
|
|
}
|
|
|
|
// Find the component reference and cache the results.
|
|
targetComponents = target.GetComponents<T>();
|
|
typeComponentsMap.Add(typeof(T), targetComponents);
|
|
return targetComponents as T[];
|
|
}
|
|
|
|
// Clears the static references.
|
|
public static void ClearCache()
|
|
{
|
|
gameObjectComponentMap.Clear();
|
|
gameObjectComponentsMap.Clear();
|
|
}
|
|
}
|
|
} |