Init
This commit is contained in:
477
Assets/ThirdParty/DestroyIt/Scripts/Runtime/Managers/DestructionManager.cs
vendored
Normal file
477
Assets/ThirdParty/DestroyIt/Scripts/Runtime/Managers/DestructionManager.cs
vendored
Normal file
@@ -0,0 +1,477 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using UnityEngine;
|
||||
|
||||
namespace DestroyIt
|
||||
{
|
||||
/// <summary>
|
||||
/// Destruction Manager (Singleton) - manages all destructible objects.
|
||||
/// Put this script on an empty game object in your scene.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public class DestructionManager : MonoBehaviour
|
||||
{
|
||||
[Tooltip("If true, Destructible scripts will be deactivated on start, and will activate any time they are inside a trigger collider with the ActivateDestructibles script on it.")]
|
||||
[HideInInspector] public bool autoDeactivateDestructibles;
|
||||
[Tooltip("If true, Destructible terrain object scripts will be deactivated on start, and will activate any time they are inside a trigger collider with the ActivateDestructibles script on it.")]
|
||||
[HideInInspector] public bool autoDeactivateDestructibleTerrainObjects = true;
|
||||
[Tooltip("If true, Destructible terrain tree scripts will not be activated by ActivateDestructibles scripts. Recommended to leave this true for performance, unless you need to move trees during the game or use progressive damage textures on them.")]
|
||||
[HideInInspector] public bool destructibleTreesStayDeactivated = true;
|
||||
[Tooltip("The time in seconds to automatically deactivate Destructible scripts when they are outside an ActivateDestructibles trigger area.")]
|
||||
[HideInInspector] public float deactivateAfter = 2f;
|
||||
|
||||
[Tooltip("If true, Destructible objects can be damaged and destroyed. Turn this off if you want to globally deactivate Destructible objects taking damage.")]
|
||||
public bool allowDamage = true;
|
||||
[Tooltip("Maximum allowed persistent debris pieces in the scene.")]
|
||||
public int maxPersistentDebris = 400;
|
||||
[Tooltip("Maximum allowed destroyed prefabs within [withinSeconds] seconds. When this limit is reached, a particle effect will be used instead.")]
|
||||
public int destroyedPrefabLimit = 15;
|
||||
[Tooltip("Number of seconds within which no more than [destroyedPrefabLimit] destructions will be instantiated.")]
|
||||
public int withinSeconds = 4;
|
||||
[Tooltip("The default particle effect to use when an object is destroyed.")]
|
||||
public ParticleSystem defaultParticle;
|
||||
[Tooltip("If true, persistent debris is allowed to be culled even if the camera is currently rendering it.")]
|
||||
public bool removeVisibleDebris = true;
|
||||
[Tooltip("The time (in seconds) this script processes updates.")]
|
||||
public float updateFrequency = .5f;
|
||||
|
||||
[HideInInspector]
|
||||
public bool useCameraDistanceLimit = true; // If true, things beyond the specified distance from the main camera will be destroyed in a more limiting (ie, higher performance) way.
|
||||
[HideInInspector]
|
||||
public int cameraDistanceLimit = 100; // Specified game units (usually meters) from camera, where destruction limiting will occur.
|
||||
[HideInInspector]
|
||||
public int debrisLayer = -1;
|
||||
[HideInInspector]
|
||||
public Collider[] overlapColliders; // These are the colliders overlapped by an Overlap Sphere (used for determining affected objects in a blast radius without allocating GC).
|
||||
|
||||
// Private Variables
|
||||
private float _nextUpdate;
|
||||
private List<Destructible> _destroyedObjects;
|
||||
private List<Debris> _debrisPieces;
|
||||
private List<Texture2D> _detailMasks;
|
||||
|
||||
// Events
|
||||
public event Action DestroyedPrefabCounterChangedEvent;
|
||||
public event Action ActiveDebrisCounterChangedEvent;
|
||||
|
||||
// Properties
|
||||
public List<float> DestroyedPrefabCounter { get; private set; }
|
||||
|
||||
public bool IsDestroyedPrefabLimitReached => DestroyedPrefabCounter.Count >= destroyedPrefabLimit;
|
||||
|
||||
public int ActiveDebrisCount
|
||||
{
|
||||
get
|
||||
{
|
||||
int count = 0;
|
||||
foreach (Debris debris in _debrisPieces)
|
||||
{
|
||||
if (debris.IsActive)
|
||||
count ++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
// Hide the default constructor (use DestructionManager.Instance instead).
|
||||
private DestructionManager() { }
|
||||
|
||||
// Private reference only this class can access
|
||||
private static DestructionManager _instance;
|
||||
|
||||
// This is the public reference that other classes will use
|
||||
public static DestructionManager Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
// If _instance hasn't been set yet, we grab it from the scene.
|
||||
// This will only happen the first time this reference is used.
|
||||
if (_instance == null)
|
||||
_instance = FindObjectOfType<DestructionManager>();
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// Initialize variables
|
||||
DestroyedPrefabCounter = new List<float>();
|
||||
overlapColliders = new Collider[100];
|
||||
_detailMasks = Resources.LoadAll<Texture2D>("ProgressiveDamage").ToList();
|
||||
debrisLayer = LayerMask.NameToLayer("DestroyItDebris");
|
||||
_debrisPieces = new List<Debris>();
|
||||
_destroyedObjects = new List<Destructible>();
|
||||
_nextUpdate = Time.time + updateFrequency;
|
||||
|
||||
// If the default particle hasn't been assigned, try to get it from the Resources folder.
|
||||
if (defaultParticle == null)
|
||||
defaultParticle = Resources.Load<ParticleSystem>("Default_Particles/DefaultLargeParticle");
|
||||
|
||||
// Checks
|
||||
Check.IsDefaultParticleAssigned();
|
||||
if (Check.LayerExists("DestroyItDebris", false) == false)
|
||||
Debug.LogWarning("DestroyItDebris layer not found. Add a layer named 'DestroyItDebris' to your project if you want debris to ignore other debris when using Cling Points.");
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (Time.time < _nextUpdate) return;
|
||||
|
||||
// Manage Destroyed Prefab counter
|
||||
DestroyedPrefabCounter.Update(withinSeconds);
|
||||
|
||||
// Manage Debris Queue
|
||||
if (_debrisPieces.Count > 0)
|
||||
{
|
||||
// Cleanup references to debris no longer in the game
|
||||
int itemsRemoved = _debrisPieces.RemoveAll(x => x == null || !x.IsActive);
|
||||
if (itemsRemoved > 0)
|
||||
FireActiveDebrisCounterChangedEvent();
|
||||
//TODO: Debris is getting removed from the list, but not destroyed from the game. Debris parent objects should probably check their children periodically for enabled meshes.
|
||||
|
||||
// Disable debris until the Max Debris limit is satisfied.
|
||||
if (ActiveDebrisCount > maxPersistentDebris)
|
||||
{
|
||||
int overBy = ActiveDebrisCount - maxPersistentDebris;
|
||||
|
||||
foreach (Debris debris in _debrisPieces)
|
||||
{
|
||||
if (overBy <= 0) break;
|
||||
if (!debris.IsActive) continue;
|
||||
if (!removeVisibleDebris)
|
||||
{
|
||||
if (debris.Rigidbody.GetComponent<Renderer>() == null) continue;
|
||||
if (debris.Rigidbody.GetComponent<Renderer>().isVisible) continue;
|
||||
}
|
||||
// Disable the debris.
|
||||
debris.Disable();
|
||||
overBy -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Manage Destroyed Objects list (ie, we're spacing out the Destroy() calls for performance)
|
||||
if (_destroyedObjects.Count > 0)
|
||||
{
|
||||
// Destroy a maximum of 5 gameobjects per update, to space it out a little.
|
||||
int nbrObjects = _destroyedObjects.Count > 5 ? 5 : _destroyedObjects.Count;
|
||||
for (int i=0; i<nbrObjects; i++)
|
||||
{
|
||||
// Destroy the gameobject and remove it from the list.
|
||||
if (_destroyedObjects[i] != null && _destroyedObjects[i].gameObject != null)
|
||||
Destroy(_destroyedObjects[i].gameObject);
|
||||
}
|
||||
_destroyedObjects.RemoveRange(0, nbrObjects);
|
||||
}
|
||||
|
||||
_nextUpdate = Time.time + updateFrequency; // reset the next update time.
|
||||
}
|
||||
|
||||
/// <summary>Swaps the current destructible object with a new one and applies the correct materials to the new object.</summary>
|
||||
public void ProcessDestruction<T>(Destructible oldObj, GameObject destroyedPrefab, T damageInfo)
|
||||
{
|
||||
if (oldObj == null) return;
|
||||
if (!oldObj.canBeDestroyed) return;
|
||||
|
||||
oldObj.FireDestroyedEvent();
|
||||
|
||||
// Check for any audio clips we may need to play
|
||||
if (oldObj.destroyedSound != null)
|
||||
AudioSource.PlayClipAtPoint(oldObj.destroyedSound, oldObj.transform.position);
|
||||
|
||||
// Look for any debris objects clinging to the old object and un-parent them before destroying the old object.
|
||||
oldObj.ReleaseClingingDebris();
|
||||
|
||||
// Remove any Joints from the destroyed object
|
||||
//TODO: Add option for transerring joints to the destroyed prefab
|
||||
Joint[] joints = oldObj.GetComponentsInChildren<Joint>();
|
||||
foreach (Joint jnt in joints)
|
||||
Destroy(jnt);
|
||||
|
||||
// Unparent DamageEffects and turn off all particle emissions
|
||||
if (oldObj.damageEffects != null)
|
||||
{
|
||||
foreach (var damageEffect in oldObj.damageEffects)
|
||||
{
|
||||
if (!damageEffect.UnparentOnDestroy || damageEffect.GameObject == null) continue;
|
||||
damageEffect.GameObject.transform.SetParent(null, true);
|
||||
if (!damageEffect.StopEmittingOnDestroy || damageEffect.ParticleSystems == null || damageEffect.ParticleSystems.Length <= 0) continue;
|
||||
foreach (var particle in damageEffect.ParticleSystems)
|
||||
{
|
||||
var emission = particle.emission;
|
||||
emission.enabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Is this destructible object a stand-in for a Terrain Tree?
|
||||
if (oldObj.gameObject.HasTagInParent(Tag.TerrainTree))
|
||||
TreeManager.Instance.DestroyTreeAt(oldObj.transform.position);
|
||||
|
||||
// Should this object sink into the ground instead of being destroyed?
|
||||
if (oldObj.sinkWhenDestroyed)
|
||||
{
|
||||
DestructibleHelper.SinkAndDestroy(oldObj);
|
||||
return; // Exit immediately, don't do any more destruction processing.
|
||||
}
|
||||
|
||||
// Play a particle effect and exit.
|
||||
if (destroyedPrefab == null || IsDestroyedPrefabLimitReached)
|
||||
{
|
||||
DestroyWithParticleEffect(oldObj, oldObj.fallbackParticle, damageInfo);
|
||||
return;
|
||||
}
|
||||
|
||||
if (useCameraDistanceLimit)
|
||||
{
|
||||
// Find the distance between the camera and the destroyed object
|
||||
float distance = Vector3.Distance(oldObj.transform.position, Camera.main.transform.position);
|
||||
if (distance > cameraDistanceLimit)
|
||||
{
|
||||
DestroyWithParticleEffect(oldObj, oldObj.fallbackParticle, damageInfo);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If we've passed the checks above, we are creating debris.
|
||||
DestroyedPrefabCounter.Add(Time.time);
|
||||
FireDestroyedPrefabCounterChangedEvent();
|
||||
|
||||
// Unparent any specified child objects before destroying
|
||||
UnparentSpecifiedChildren(oldObj);
|
||||
|
||||
// Put the destroyed object in the Debris layer to keep new debris from clinging to it
|
||||
if (debrisLayer != -1)
|
||||
oldObj.gameObject.layer = debrisLayer;
|
||||
|
||||
// Try to get the destroyed prefab from the object pool
|
||||
GameObject newObj = ObjectPool.Instance.Spawn(destroyedPrefab, oldObj.PositionFixedUpdate, oldObj.RotationFixedUpdate, oldObj.GetInstanceID());
|
||||
InstantiateDebris(newObj, oldObj, damageInfo);
|
||||
|
||||
oldObj.gameObject.SetActive(false);
|
||||
_destroyedObjects.Add(oldObj);
|
||||
}
|
||||
|
||||
private void DestroyWithParticleEffect<T>(Destructible oldObj, ParticleSystem customParticle, T damageInfo)
|
||||
{
|
||||
if (oldObj.useFallbackParticle)
|
||||
{
|
||||
// Use the DestructibleGroup instance ID if it exists, otherwise use the Destructible object's parent's instance ID.
|
||||
GameObject parentObj = oldObj.gameObject.GetHighestParentWithTag(Tag.DestructibleGroup) ?? oldObj.gameObject;
|
||||
int instanceId = parentObj.GetInstanceID();
|
||||
|
||||
// Use the mesh center point as the starting position for the particle effect.
|
||||
var position = oldObj.MeshCenterPoint;
|
||||
|
||||
// If a particle spawn point has been specified, use that instead.
|
||||
if (oldObj.centerPointOverride != Vector3.zero)
|
||||
position = oldObj.centerPointOverride;
|
||||
|
||||
// Convert the particle spawn point position to world coordinates.
|
||||
position = oldObj.transform.TransformPoint(position);
|
||||
|
||||
// If no specific fallback particle effect is defined, use the default particle effect assigned in DestructionManager.
|
||||
ParticleManager.Instance.PlayEffect(customParticle ?? defaultParticle, oldObj, position, oldObj.transform.rotation, instanceId);
|
||||
}
|
||||
|
||||
UnparentSpecifiedChildren(oldObj);
|
||||
oldObj.gameObject.SetActive(false);
|
||||
_destroyedObjects.Add(oldObj);
|
||||
|
||||
// Reapply impact force to impact object so it punches through the destroyed object along its original path.
|
||||
// If you turn this off, impact objects will be deflected even though the impacted object was destroyed.
|
||||
if (damageInfo.GetType() == typeof(ImpactDamage))
|
||||
DestructibleHelper.ReapplyImpactForce(damageInfo as ImpactDamage, oldObj.VelocityReduction);
|
||||
}
|
||||
|
||||
private static void UnparentSpecifiedChildren(Destructible obj)
|
||||
{
|
||||
if (obj.unparentOnDestroy == null) return;
|
||||
|
||||
foreach (GameObject child in obj.unparentOnDestroy)
|
||||
{
|
||||
if (child == null)
|
||||
continue;
|
||||
|
||||
// Unparent the child object from the destructible object.
|
||||
child.transform.parent = null;
|
||||
|
||||
// Initialize any DelayedRigidbody scripts on the object.
|
||||
DelayedRigidbody[] delayedRigidbodies = child.GetComponentsInChildren<DelayedRigidbody>();
|
||||
foreach (DelayedRigidbody dr in delayedRigidbodies)
|
||||
dr.Initialize();
|
||||
|
||||
// Check whether we should turn off Kinematic on child objects, so they will fall freely.
|
||||
if (obj.disableKinematicOnUparentedChildren)
|
||||
{
|
||||
Rigidbody[] rigidbodies = child.GetComponentsInChildren<Rigidbody>();
|
||||
foreach (Rigidbody rbody in rigidbodies)
|
||||
rbody.isKinematic = false;
|
||||
}
|
||||
|
||||
// Turn off any animations
|
||||
Animation[] animations = child.GetComponentsInChildren<Animation>();
|
||||
foreach (Animation anim in animations)
|
||||
anim.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void InstantiateDebris<T>(GameObject newObj, Destructible oldObj, T damageInfo)
|
||||
{
|
||||
// Apply new materials derived from previous object's materials
|
||||
if (!oldObj.autoPoolDestroyedPrefab) // if the old object was autopooled, the destroyed object will come from the pool already having the right materials on it.
|
||||
DestructibleHelper.TransferMaterials(oldObj, newObj);
|
||||
|
||||
// For SpeedTree terrain trees, turn off the Hue Variation on the shader so it's locked in and the tree can fall without changing hue.
|
||||
if (oldObj.isTerrainTree)
|
||||
newObj.gameObject.LockHueVariation();
|
||||
|
||||
// Re-scale destroyed version if original destructible object has been scaled. (Scaling rigidbodies in general is bad, but this is put here for convenience.)
|
||||
if (oldObj.transform.lossyScale != new Vector3(1f, 1f, 1f)) // if destructible object has been scaled in the scene
|
||||
newObj.transform.localScale = oldObj.transform.lossyScale;
|
||||
|
||||
if (oldObj.destroyedPrefabParent != null)
|
||||
newObj.transform.parent = oldObj.destroyedPrefabParent.transform;
|
||||
|
||||
if (oldObj.isDebrisChipAway)
|
||||
{
|
||||
// If we are doing chip-away debris, attach the ChipAwayDebris script to each piece of debris and exit.
|
||||
Collider[] debrisColliders = newObj.GetComponentsInChildren<Collider>();
|
||||
foreach (Collider coll in debrisColliders)
|
||||
{
|
||||
if (coll.gameObject.GetComponent<ChipAwayDebris>() != null) continue;
|
||||
|
||||
// If there is already an attached non-kinematic rigidbody on the debris piece, remove it.
|
||||
if (coll.attachedRigidbody != null && !coll.attachedRigidbody.isKinematic)
|
||||
coll.attachedRigidbody.gameObject.RemoveComponent<Rigidbody>();
|
||||
|
||||
ChipAwayDebris chipAwayDebris = coll.gameObject.AddComponent<ChipAwayDebris>();
|
||||
chipAwayDebris.debrisMass = oldObj.chipAwayDebrisMass;
|
||||
chipAwayDebris.debrisDrag = oldObj.chipAwayDebrisDrag;
|
||||
chipAwayDebris.debrisAngularDrag = oldObj.chipAwayDebrisAngularDrag;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
oldObj.ReparentChildren(newObj); // assign the appropriate parent objects for Children to Reparent
|
||||
|
||||
// Attempt to get the debris rigidbodies from the PooledRigidbody property on the destroyed object first.
|
||||
Rigidbody[] debrisRigidbodies = oldObj.PooledRigidbodies;
|
||||
GameObject[] debrisRigidbodyGos = oldObj.PooledRigidbodyGos;
|
||||
|
||||
// If the debris rigidbodies weren't pooled on the destroyed object, try to retrieve them directly from the newly-spawned-in destroyed object instead.
|
||||
if (debrisRigidbodies == null || debrisRigidbodies.Length == 0)
|
||||
{
|
||||
debrisRigidbodies = newObj.GetComponentsInChildren<Rigidbody>();
|
||||
debrisRigidbodyGos = new GameObject[debrisRigidbodies.Length];
|
||||
for (int i = 0; i < debrisRigidbodies.Length; i++)
|
||||
debrisRigidbodyGos[i] = debrisRigidbodies[i].gameObject;
|
||||
}
|
||||
|
||||
// If we found rigidbodies on the destroyed object, assign them to the appropriate layer/tag/queue, and transfer velocity to them.
|
||||
if (debrisRigidbodies.Length > 0)
|
||||
{
|
||||
for (int i = 0; i < debrisRigidbodies.Length; i++)
|
||||
{
|
||||
// Assign each piece of debris to the Debris layer if it exists.
|
||||
if (debrisLayer != -1)
|
||||
debrisRigidbodies[i].gameObject.layer = debrisLayer;
|
||||
|
||||
// Reparent any debris tagged for reparenting.
|
||||
if (oldObj.debrisToReParentByName != null && oldObj.debrisToReParentByName.Count > 0 && oldObj.transform.parent != null && (oldObj.debrisToReParentByName.Contains("ALL DEBRIS") || oldObj.debrisToReParentByName.Contains(debrisRigidbodies[i].name)))
|
||||
{
|
||||
debrisRigidbodies[i].gameObject.transform.parent = oldObj.transform.parent;
|
||||
debrisRigidbodies[i].isKinematic = oldObj.debrisToReParentIsKinematic;
|
||||
}
|
||||
|
||||
// Add leftover velocity and angular velocity from destroyed object
|
||||
if (!debrisRigidbodies[i].isKinematic)
|
||||
{
|
||||
debrisRigidbodies[i].velocity = oldObj.VelocityFixedUpdate;
|
||||
debrisRigidbodies[i].angularVelocity = oldObj.AngularVelocityFixedUpdate;
|
||||
}
|
||||
|
||||
// Add debris to the debris queue.
|
||||
Debris debris = new Debris {Rigidbody = debrisRigidbodies[i], GameObject = debrisRigidbodyGos[i]};
|
||||
_debrisPieces.Add(debris);
|
||||
FireActiveDebrisCounterChangedEvent();
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to make some of the debris cling to adjacent rigidbodies
|
||||
if (oldObj.CheckForClingingDebris)
|
||||
newObj.MakeDebrisCling();
|
||||
|
||||
// Reapply impact force to impact object so it punches through the destroyed object along its original path.
|
||||
// If you turn this off, impact objects will be deflected even though the impacted object was destroyed.
|
||||
if (damageInfo.GetType() == typeof(ImpactDamage))
|
||||
DestructibleHelper.ReapplyImpactForce(damageInfo as ImpactDamage, oldObj.VelocityReduction);
|
||||
|
||||
if (damageInfo.GetType() == typeof(ExplosiveDamage) || damageInfo.GetType() == typeof(ImpactDamage))
|
||||
ExplosionHelper.ApplyForcesToDebris(newObj, 1f, damageInfo);
|
||||
}
|
||||
|
||||
public void SetProgressiveDamageTexture(Renderer rend, Material sourceMat, DamageLevel damageLevel)
|
||||
{
|
||||
if (sourceMat == null) return;
|
||||
if (!sourceMat.HasProperty("_DetailMask")) return;
|
||||
Texture sourceDetailMask = sourceMat.GetTexture("_DetailMask");
|
||||
if (sourceDetailMask == null) return;
|
||||
if (_detailMasks == null || _detailMasks.Count == 0) return;
|
||||
|
||||
string sourceDetailMaskName = Regex.Replace(sourceDetailMask.name, "_D[0-9]*$", "");
|
||||
Texture newDetailMask = null;
|
||||
foreach (Texture2D detailMask in _detailMasks)
|
||||
{
|
||||
if (detailMask.name == $"{sourceDetailMaskName}_D{damageLevel.visibleDamageLevel}")
|
||||
{
|
||||
newDetailMask = detailMask;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (newDetailMask == null) return;
|
||||
|
||||
MaterialPropertyBlock propBlock = new MaterialPropertyBlock();
|
||||
rend.GetPropertyBlock(propBlock);
|
||||
propBlock.SetTexture("_DetailMask", newDetailMask);
|
||||
rend.SetPropertyBlock(propBlock);
|
||||
}
|
||||
|
||||
public Texture2D GetDetailMask(Material sourceMat, DamageLevel damageLevel)
|
||||
{
|
||||
|
||||
if (sourceMat == null) return null;
|
||||
if (!sourceMat.HasProperty("_DetailMask")) return null;
|
||||
Texture sourceDetailMask = sourceMat.GetTexture("_DetailMask");
|
||||
if (sourceDetailMask == null) return null;
|
||||
if (_detailMasks == null || _detailMasks.Count == 0) return null;
|
||||
|
||||
string sourceDetailMaskName = Regex.Replace(sourceDetailMask.name, "_D[0-9]*$", "");
|
||||
|
||||
foreach (Texture2D detailMask in _detailMasks)
|
||||
{
|
||||
if (detailMask.name == $"{sourceDetailMaskName}_D{damageLevel.visibleDamageLevel - 1}")
|
||||
return detailMask;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Fires when the Destroyed Prefab counter changes.</summary>
|
||||
public void FireDestroyedPrefabCounterChangedEvent()
|
||||
{
|
||||
if (DestroyedPrefabCounterChangedEvent != null) // first, make sure there is at least one listener.
|
||||
DestroyedPrefabCounterChangedEvent(); // if so, trigger the event.
|
||||
}
|
||||
|
||||
/// <summary>Fires when the Active Debris count changes.</summary>
|
||||
public void FireActiveDebrisCounterChangedEvent()
|
||||
{
|
||||
if (ActiveDebrisCounterChangedEvent != null) // first, make sure there is at least one listener.
|
||||
ActiveDebrisCounterChangedEvent(); // if so, trigger the event.
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/ThirdParty/DestroyIt/Scripts/Runtime/Managers/DestructionManager.cs.meta
vendored
Normal file
11
Assets/ThirdParty/DestroyIt/Scripts/Runtime/Managers/DestructionManager.cs.meta
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 24262ec55dbf62e43ab60bbe9e272d59
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
286
Assets/ThirdParty/DestroyIt/Scripts/Runtime/Managers/ObjectPool.cs
vendored
Normal file
286
Assets/ThirdParty/DestroyIt/Scripts/Runtime/Managers/ObjectPool.cs
vendored
Normal file
@@ -0,0 +1,286 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
// ReSharper disable InconsistentNaming
|
||||
// ReSharper disable CommentTypo
|
||||
// ReSharper disable ForCanBeConvertedToForeach
|
||||
|
||||
namespace DestroyIt
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a pool of objects to pull from, instead of using expensive Instantiate/Destroy calls.
|
||||
/// This class is a Singleton, meaning it is called using ObjectPool.Instance.SomeFunction().
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public class ObjectPool : MonoBehaviour
|
||||
{
|
||||
// Hide the default constructor (use ObjectPool.Instance instead).
|
||||
private ObjectPool() { }
|
||||
|
||||
public List<PoolEntry> prefabsToPool;
|
||||
public bool suppressWarnings;
|
||||
|
||||
private GameObject[][] Pool;
|
||||
private Dictionary<int, GameObject> autoPooledObjects; // Destroyed Prefabs which are auto-added to the pool on start.
|
||||
private GameObject container;
|
||||
|
||||
// Here is a private reference only this class can access
|
||||
private static ObjectPool _instance;
|
||||
private bool isInitialized;
|
||||
|
||||
// This is the public reference that other classes will use
|
||||
public static ObjectPool Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_instance == null)
|
||||
CreateInstance();
|
||||
|
||||
if (!_instance.isInitialized)
|
||||
_instance.Start();
|
||||
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
private static void CreateInstance()
|
||||
{
|
||||
ObjectPool[] objectPools = FindObjectsOfType<ObjectPool>();
|
||||
if (objectPools.Length > 1)
|
||||
Debug.LogError("Multiple ObjectPool scripts found in scene. There can be only one.");
|
||||
if (objectPools.Length == 0)
|
||||
Debug.LogError("ObjectPool script not found in scene. This is required for DestroyIt to work properly.");
|
||||
|
||||
_instance = objectPools[0];
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
if (isInitialized) return;
|
||||
if (prefabsToPool == null) return;
|
||||
|
||||
// Check if the object pool container already exists. If so, use it.
|
||||
GameObject existingContainer = GameObject.Find("DestroyIt_ObjectPool");
|
||||
container = existingContainer != null ? existingContainer : new GameObject("DestroyIt_ObjectPool");
|
||||
container.SetActive(false);
|
||||
|
||||
autoPooledObjects = new Dictionary<int, GameObject>();
|
||||
|
||||
// Instantiate game objects from the PrefabsToPool list and add them to the Pool.
|
||||
Pool = new GameObject[prefabsToPool.Count][];
|
||||
for (int i = 0; i < prefabsToPool.Count; i++)
|
||||
{
|
||||
PoolEntry poolEntry = prefabsToPool[i];
|
||||
Pool[i] = new GameObject[poolEntry.Count];
|
||||
for (int n=0; n<poolEntry.Count; n++)
|
||||
{
|
||||
if (poolEntry.Prefab == null) continue;
|
||||
var newObj = Instantiate(poolEntry.Prefab);
|
||||
newObj.name = poolEntry.Prefab.name;
|
||||
PoolObject(newObj);
|
||||
}
|
||||
}
|
||||
isInitialized = true;
|
||||
CreateInstance();
|
||||
}
|
||||
|
||||
public void AddDestructibleObjectToPool(Destructible destObj)
|
||||
{
|
||||
if (autoPooledObjects.ContainsKey(destObj.GetInstanceID())) return;
|
||||
|
||||
if (destObj.destroyedPrefab != null && destObj.autoPoolDestroyedPrefab)
|
||||
{
|
||||
var newObj = Instantiate(destObj.destroyedPrefab, container.transform);
|
||||
|
||||
// If the destroyed prefab is also Destructible, add its destroyed prefab to the pool as well. (Recursive)
|
||||
Destructible[] destObjectsInObject = newObj.GetComponentsInChildren<Destructible>();
|
||||
for (int i = 0; i < destObjectsInObject.Length; i++)
|
||||
AddDestructibleObjectToPool(destObjectsInObject[i]);
|
||||
|
||||
newObj.transform.parent = container.transform;
|
||||
newObj.name = destObj.destroyedPrefab.name;
|
||||
newObj.AddTag(Tag.Pooled);
|
||||
DestructibleHelper.TransferMaterials(destObj, newObj);
|
||||
|
||||
// See if we will need to check for clinging debris. (Optimization)
|
||||
ClingPoint[] clingPoints = newObj.GetComponentsInChildren<ClingPoint>();
|
||||
if (clingPoints.Length == 0)
|
||||
destObj.CheckForClingingDebris = false;
|
||||
|
||||
// Add references to Rigidbodies and Rigidbody GameObjects to Destructible objects for better performance. (Optimization)
|
||||
destObj.PooledRigidbodies = newObj.GetComponentsInChildren<Rigidbody>();
|
||||
destObj.PooledRigidbodyGos = new GameObject[destObj.PooledRigidbodies.Length];
|
||||
for (int i = 0; i < destObj.PooledRigidbodies.Length; i++)
|
||||
destObj.PooledRigidbodyGos[i] = destObj.PooledRigidbodies[i].gameObject;
|
||||
|
||||
newObj.SetActive(false);
|
||||
autoPooledObjects.Add(destObj.GetInstanceID(), newObj);
|
||||
}
|
||||
}
|
||||
|
||||
// Spawn a game object from the original prefab, not from the pool. Used for resetting destroyed objects back to their original state.
|
||||
public GameObject SpawnFromOriginal(string prefabName)
|
||||
{
|
||||
foreach (PoolEntry entry in prefabsToPool)
|
||||
{
|
||||
if (entry.Prefab != null && entry.Prefab.name == prefabName)
|
||||
{
|
||||
GameObject obj = Instantiate(entry.Prefab);
|
||||
obj.name = prefabName;
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static GameObject InstantiateObject(GameObject prefab, Vector3 position, Quaternion rotation, Transform parent)
|
||||
{
|
||||
GameObject obj = Instantiate(prefab, position, rotation);
|
||||
if (obj == null) return null;
|
||||
|
||||
obj.transform.parent = parent;
|
||||
|
||||
if (parent != null) // If a parent transform was specified, set the position local to the parent.
|
||||
obj.transform.localPosition = position;
|
||||
else // Otherwise, set the position in relation to world space.
|
||||
obj.transform.position = position;
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
/// <summary>Spawns an object from the object pool, setting the object's parent to what you pass in.</summary>
|
||||
public GameObject Spawn(GameObject originalPrefab, Vector3 position, Quaternion rotation, Transform parent, int autoPoolID = 0)
|
||||
{
|
||||
// If an AutoPoolID was passed in, try to find it in the AutoPool dictionary.
|
||||
if (autoPooledObjects != null && autoPoolID != 0 && autoPooledObjects.ContainsKey(autoPoolID))
|
||||
{
|
||||
GameObject pooledObj = autoPooledObjects[autoPoolID];
|
||||
if (pooledObj != null)
|
||||
{
|
||||
pooledObj.transform.parent = parent;
|
||||
|
||||
if (parent != null) // If a parent transform was specified, set the position local to the parent.
|
||||
{
|
||||
pooledObj.transform.localPosition = position;
|
||||
pooledObj.transform.localRotation = rotation;
|
||||
}
|
||||
else // Otherwise, set the position in relation to world space.
|
||||
{
|
||||
pooledObj.transform.position = position;
|
||||
pooledObj.transform.rotation = rotation;
|
||||
}
|
||||
pooledObj.SetActive(true);
|
||||
return pooledObj;
|
||||
}
|
||||
}
|
||||
string origPrefabName = originalPrefab.name;
|
||||
|
||||
for (int i = 0; i < prefabsToPool.Count; i++)
|
||||
{
|
||||
GameObject prefab = prefabsToPool[i].Prefab;
|
||||
|
||||
if (prefab == null) continue;
|
||||
if (prefab.name != origPrefabName) continue;
|
||||
|
||||
if (Pool != null && Pool[i].Length > 0)
|
||||
{
|
||||
// Find the first available object to spawn from the pool.
|
||||
for (int j = 0; j < Pool[i].Length; j++)
|
||||
{
|
||||
if (Pool[i][j] != null)
|
||||
{
|
||||
GameObject pooledObj = Pool[i][j];
|
||||
Pool[i][j] = null;
|
||||
pooledObj.transform.parent = parent;
|
||||
|
||||
if (parent != null) // If a parent transform was specified, set the position local to the parent.
|
||||
pooledObj.transform.localPosition = position;
|
||||
else // Otherwise, set the position in relation to world space.
|
||||
pooledObj.transform.position = position;
|
||||
|
||||
pooledObj.transform.rotation = rotation;
|
||||
pooledObj.SetActive(true);
|
||||
return pooledObj;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Pool == null)
|
||||
{
|
||||
GameObject pooledObj = InstantiateObject(prefabsToPool[i].Prefab, position, rotation, parent);
|
||||
Debug.LogWarning("[" + origPrefabName + " was instantiated instead of spawned from pool. Reason: Pool is null.");
|
||||
return pooledObj;
|
||||
}
|
||||
|
||||
if (!prefabsToPool[i].OnlyPooled)
|
||||
{
|
||||
GameObject pooledObj = InstantiateObject(prefabsToPool[i].Prefab, position, rotation, parent);
|
||||
pooledObj.name = prefabsToPool[i].Prefab.name;
|
||||
pooledObj.AddTag(Tag.Pooled);
|
||||
if (!suppressWarnings)
|
||||
Debug.LogWarning("[" + origPrefabName + " was instantiated instead of spawned from pool. Reason: No objects remaining in the pool (size: " + Pool[i].Length + "). Consider increasing the pool size.");
|
||||
return pooledObj;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (!suppressWarnings)
|
||||
Debug.LogWarning("[" + origPrefabName + "] was instantiated instead of spawned from pool. Reason: Prefab not found in pool.");
|
||||
return InstantiateObject(originalPrefab, position, rotation, parent);
|
||||
}
|
||||
|
||||
/// <summary>Spawns an object from the object pool. The object will not be a child of any other object.</summary>
|
||||
public GameObject Spawn(GameObject originalPrefab, Vector3 position, Quaternion rotation, int autoPoolID = 0)
|
||||
{
|
||||
return Spawn(originalPrefab, position, rotation, null, autoPoolID);
|
||||
}
|
||||
|
||||
/// <summary>Put object back in the pool. You can force children to be reenabled, if desired.</summary>
|
||||
public void PoolObject(GameObject obj, bool reenableChildren = false)
|
||||
{
|
||||
for (int i = 0; i < prefabsToPool.Count; i++)
|
||||
{
|
||||
if (prefabsToPool[i].Prefab == null) continue;
|
||||
if (prefabsToPool[i].Prefab.name != obj.name) continue;
|
||||
|
||||
// Object was found. Deactivate it, stop/clear particle effects, and put it in the pool.
|
||||
obj.transform.parent = container.transform;
|
||||
ParticleSystem[] particleSystems = obj.GetComponentsInChildren<ParticleSystem>();
|
||||
for (int j = 0; j < particleSystems.Length; j++)
|
||||
{
|
||||
particleSystems[j].Stop();
|
||||
particleSystems[j].Clear();
|
||||
var emission = particleSystems[j].emission;
|
||||
emission.enabled = true;
|
||||
}
|
||||
if (reenableChildren)
|
||||
{
|
||||
Transform[] trans = obj.GetComponentsInChildren<Transform>(true);
|
||||
for (int j = 0; j < trans.Length; j++)
|
||||
trans[j].gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
obj.AddTag(Tag.Pooled);
|
||||
obj.SetActive(false);
|
||||
|
||||
// Try to find an empty spot for the object to be placed in.
|
||||
for (int j=0; j<Pool[i].Length; j++)
|
||||
{
|
||||
if (Pool[i][j] == null)
|
||||
{
|
||||
Pool[i][j] = obj;
|
||||
return;
|
||||
}
|
||||
}
|
||||
Destroy(obj);
|
||||
if (!suppressWarnings)
|
||||
Debug.LogWarning("[" + obj.name + "] was destroyed instead of pooled. Reason: The pool size for this prefab was too small (" + Pool[i].Length + "). Consider increasing the pool size.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Destroy(obj);
|
||||
if (!suppressWarnings)
|
||||
Debug.LogWarning("[" + obj.name + "] was destroyed instead of pooled. Reason: Prefab not found in pool.");
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/ThirdParty/DestroyIt/Scripts/Runtime/Managers/ObjectPool.cs.meta
vendored
Normal file
11
Assets/ThirdParty/DestroyIt/Scripts/Runtime/Managers/ObjectPool.cs.meta
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1f808b6d5af61ca4ab4d3ab5ac519f89
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
163
Assets/ThirdParty/DestroyIt/Scripts/Runtime/Managers/ParticleManager.cs
vendored
Normal file
163
Assets/ThirdParty/DestroyIt/Scripts/Runtime/Managers/ParticleManager.cs
vendored
Normal file
@@ -0,0 +1,163 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace DestroyIt
|
||||
{
|
||||
/// <summary>
|
||||
/// Particle Manager (Singleton) - manages the playing of particle effects and handles performance throttling.
|
||||
/// Call the PlayEffect() method, and this script decides whether to play the effect based on how many are currently active.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public class ParticleManager : MonoBehaviour
|
||||
{
|
||||
public int maxDestroyedParticles = 20; // max particles to allow during [withinSeconds].
|
||||
public int maxPerDestructible = 5; // max particles to allow for a single Destructible object or DestructibleGroup.
|
||||
public float withinSeconds = 4f; // remove particles from the managed list after this many seconds.
|
||||
public float updateFrequency = .5f; // The time (in seconds) this script updates its counters
|
||||
|
||||
public static ParticleManager Instance { get; private set; }
|
||||
public ActiveParticle[] ActiveParticles
|
||||
{
|
||||
get => _activeParticles;
|
||||
private set => _activeParticles = value;
|
||||
}
|
||||
public bool IsMaxActiveParticles => ActiveParticles.Length >= maxDestroyedParticles;
|
||||
|
||||
private float _nextUpdate;
|
||||
private ActiveParticle[] _activeParticles;
|
||||
private ParticleManager() { } // hide constructor
|
||||
|
||||
// Events
|
||||
public event Action ActiveParticlesCounterChangedEvent;
|
||||
|
||||
public void Awake()
|
||||
{
|
||||
ActiveParticles = new ActiveParticle[0];
|
||||
Instance = this;
|
||||
_nextUpdate = Time.time + updateFrequency;
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
if (!(Time.time > _nextUpdate)) return;
|
||||
if (_activeParticles.Length == 0) return;
|
||||
|
||||
int removeIndicesCounter = 0;
|
||||
int[] removeIndices = new int[0];
|
||||
bool isChanged = false;
|
||||
for (int i = 0; i < ActiveParticles.Length;i++ )
|
||||
{
|
||||
if (Time.time >= ActiveParticles[i].InstantiatedTime + withinSeconds)
|
||||
{
|
||||
isChanged = true;
|
||||
removeIndicesCounter++;
|
||||
Array.Resize(ref removeIndices, removeIndicesCounter);
|
||||
removeIndices[removeIndicesCounter - 1] = i;
|
||||
}
|
||||
}
|
||||
_activeParticles = _activeParticles.RemoveAllAt(removeIndices);
|
||||
if (isChanged)
|
||||
FireActiveParticlesCounterChangedEvent();
|
||||
|
||||
// Reset the nextUpdate counter.
|
||||
_nextUpdate = Time.time + updateFrequency;
|
||||
}
|
||||
|
||||
/// <summary>Plays a particle effect and adjusts its texture to have maximum damage level progressive damage (if specified).</summary>
|
||||
public void PlayEffect(ParticleSystem particle, Destructible destObj, Vector3 pos, Quaternion rot, int parentId)
|
||||
{
|
||||
if (particle == null)
|
||||
particle = DestructionManager.Instance.defaultParticle;
|
||||
|
||||
// Check if we're at the maximum active particle limit. If so, ignore the request to play the particle effect.
|
||||
if (IsMaxActiveParticles) return;
|
||||
|
||||
// Check if we've reached the max particle limit per destructible object for this object already.
|
||||
int parentParticleCount = ActiveParticles.Count(x => x.ParentId == parentId);
|
||||
if (parentParticleCount > maxPerDestructible) return;
|
||||
|
||||
// Instantiate and add to the ActiveParticles counter
|
||||
GameObject spawn = ObjectPool.Instance.Spawn(particle.gameObject, pos, rot);
|
||||
if (spawn == null || spawn.GetComponent<ParticleSystem>() == null) return;
|
||||
ActiveParticle aParticle = new ActiveParticle { GameObject = spawn, InstantiatedTime = Time.time, ParentId = parentId };
|
||||
Array.Resize(ref _activeParticles, _activeParticles.Length + 1);
|
||||
ActiveParticles[_activeParticles.Length - 1] = aParticle;
|
||||
FireActiveParticlesCounterChangedEvent();
|
||||
|
||||
// If a particle scale override has been specified...
|
||||
if (destObj.fallbackParticleScale != Vector3.one)
|
||||
{
|
||||
var particleSystems = spawn.GetComponentsInChildren<ParticleSystem>();
|
||||
foreach (ParticleSystem ps in particleSystems)
|
||||
{
|
||||
var main = ps.main;
|
||||
main.scalingMode = ParticleSystemScalingMode.Hierarchy;
|
||||
}
|
||||
|
||||
spawn.transform.localScale = destObj.fallbackParticleScale;
|
||||
|
||||
// If the particle effect is being put back into the object pool after use, set the PoolAfter script to reset this object back to its prefab,
|
||||
// so it won't have the scale overrides active when it plays again.
|
||||
var poolAfter = spawn.GetComponent<PoolAfter>();
|
||||
if (poolAfter != null)
|
||||
poolAfter.resetToPrefab = true;
|
||||
}
|
||||
|
||||
// Parent the particle effect under the fallback particle parent, if specified.
|
||||
if (destObj.fallbackParticleParent != null)
|
||||
spawn.transform.SetParent(destObj.fallbackParticleParent);
|
||||
|
||||
// Particle Effect Material Replacement
|
||||
if (destObj.fallbackParticleMatOption == 1) return; // No material replacement was selected, so just exit.
|
||||
|
||||
// Get the particle system renderers so we can replace the materials on them.
|
||||
if (spawn.GetComponent<ParticleSystem>() == null) return;
|
||||
ParticleSystemRenderer[] particleRenderers = spawn.GetComponentsInChildren<ParticleSystemRenderer>();
|
||||
|
||||
// Replace particle materials with the one (index 0) from the destroyed object.
|
||||
if (destObj.fallbackParticleMatOption == 0)
|
||||
{
|
||||
foreach (ParticleSystemRenderer particleRenderer in particleRenderers)
|
||||
{
|
||||
if (particleRenderer.renderMode != ParticleSystemRenderMode.Mesh) continue;
|
||||
|
||||
particleRenderer.material = destObj.GetDestroyedParticleEffectMaterial();
|
||||
|
||||
if (particleRenderer.sharedMaterial.IsProgressiveDamageCapable())
|
||||
{
|
||||
Texture2D detailMask = DestructionManager.Instance.GetDetailMask(particleRenderer.sharedMaterial, destObj.damageLevels[destObj.damageLevels.Count - 1]);
|
||||
particleRenderer.material.SetTexture("_DetailMask", detailMask);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Replace particle materials with custom ones specified on the Destructible object.
|
||||
if (destObj.fallbackParticleMatOption == 2)
|
||||
{
|
||||
foreach (ParticleSystemRenderer particleRenderer in particleRenderers)
|
||||
{
|
||||
if (particleRenderer.renderMode != ParticleSystemRenderMode.Mesh) continue;
|
||||
|
||||
// First, see if we need to replace the material with one defined on the Destructible script.
|
||||
MaterialMapping matMap = destObj.replaceParticleMats.Find(x => x.SourceMaterial == particleRenderer.sharedMaterial);
|
||||
Material newMat = matMap == null ? particleRenderer.sharedMaterial : matMap.ReplacementMaterial;
|
||||
|
||||
particleRenderer.material = newMat ?? destObj.GetDestroyedParticleEffectMaterial();
|
||||
if (particleRenderer.sharedMaterial.IsProgressiveDamageCapable())
|
||||
{
|
||||
Texture2D detailMask = DestructionManager.Instance.GetDetailMask(particleRenderer.sharedMaterial, destObj.damageLevels[destObj.damageLevels.Count - 1]);
|
||||
particleRenderer.material.SetTexture("_DetailMask", detailMask);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Fires when the number of Active Particles changes.</summary>
|
||||
public void FireActiveParticlesCounterChangedEvent()
|
||||
{
|
||||
if (ActiveParticlesCounterChangedEvent != null) // first, make sure there is at least one listener.
|
||||
ActiveParticlesCounterChangedEvent(); // if so, trigger the event.
|
||||
}
|
||||
}
|
||||
}
|
||||
8
Assets/ThirdParty/DestroyIt/Scripts/Runtime/Managers/ParticleManager.cs.meta
vendored
Normal file
8
Assets/ThirdParty/DestroyIt/Scripts/Runtime/Managers/ParticleManager.cs.meta
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 787a46710c5c1f2418efc062638d288a
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
382
Assets/ThirdParty/DestroyIt/Scripts/Runtime/Managers/TreeManager.cs
vendored
Normal file
382
Assets/ThirdParty/DestroyIt/Scripts/Runtime/Managers/TreeManager.cs
vendored
Normal file
@@ -0,0 +1,382 @@
|
||||
using System;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using UnityEngine.SceneManagement;
|
||||
using Debug = UnityEngine.Debug;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
// ReSharper disable SuggestVarOrType_SimpleTypes
|
||||
// ReSharper disable ForCanBeConvertedToForeach
|
||||
// ReSharper disable SuggestVarOrType_BuiltInTypes
|
||||
// ReSharper disable SuggestVarOrType_Elsewhere
|
||||
// ReSharper disable InconsistentNaming
|
||||
// ReSharper disable LoopCanBeConvertedToQuery
|
||||
// ReSharper disable CommentTypo
|
||||
// ReSharper disable StringLiteralTypo
|
||||
|
||||
namespace DestroyIt
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Manages destructible terrain trees.
|
||||
/// Attach this script to a gameobject (ie, _TreeManager) in your scene.
|
||||
/// On your Terrain, turn off Enable Tree Colliders. Tree colliders will be attached by this script at runtime.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(TerrainPreserver))]
|
||||
[DisallowMultipleComponent]
|
||||
public class TreeManager : MonoBehaviour
|
||||
{
|
||||
[Tooltip("The terrain managed by this script. Leave empty to manage the current active terrain.")]
|
||||
public Terrain terrain;
|
||||
|
||||
[Tooltip("Backs up the active terrain in the editor when you play the scene. This way if a crash occurs, you'll be able to restore from the backup and won't lose your placed trees, since the TreeManager replaces terrain trees with destructible stand-ins at runtime.")]
|
||||
public bool backupTerrain = true;
|
||||
|
||||
// NOTE: This folder and all its contents will be deleted each time Destructible Trees are updated!
|
||||
[Tooltip("The folder where the stripped-down destructible terrain tree prototype prefabs are stored.\n\nYou can change this if you want to store your tree stand-in resources somewhere else.")]
|
||||
public string pathToStandIns = "Assets/DestroyIt/Resources/TreeStandIns/";
|
||||
|
||||
[Tooltip("These are stripped-down tree prototype objects, containing only colliders and other essential components to make them destructible.\n\nYou don't need to change these - they are automatically generated when the Update Destructible Trees button is clicked.")]
|
||||
public List<DestructibleTree> destructibleTrees;
|
||||
|
||||
[HideInInspector]
|
||||
public List<TreeReset> treesToReset;
|
||||
|
||||
// Hide the default constructor (use TreeManager.Instance to access this class)
|
||||
private TreeManager() { }
|
||||
|
||||
private static TreeManager _instance;
|
||||
private List<TreeInstance> currentTreeInstances; // NOTE: It's important to keep this a List, don't convert to Array
|
||||
private TreeInstance[] originalTreeInstances;
|
||||
private bool isTerrainDataDirty; // Determines if the terrainData has a backup that hasn't been resolved (possibly from a Unity crash). Don't try to make changes to a dirty TerrainData - give the user an option to fix it first.
|
||||
|
||||
// Public Instance reference other classes will use
|
||||
public static TreeManager Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
// If _instance hasn't been set yet, we grab it from the scene.
|
||||
// This will only happen the first time this reference is used.
|
||||
if (_instance == null)
|
||||
_instance = FindObjectOfType<TreeManager>();
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
SceneManager.activeSceneChanged -= OnActiveSceneChanged;
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
// Exit immediately if there are no Destructible trees. In that case, there is nothing to manage.
|
||||
if (destructibleTrees == null || destructibleTrees.Count == 0) return;
|
||||
|
||||
// Get terrain to manage
|
||||
if (terrain == null)
|
||||
terrain = Terrain.activeTerrain;
|
||||
|
||||
if (terrain == null || terrain.terrainData == null)
|
||||
{
|
||||
Debug.LogWarning("No terrain to manage destructible trees on.");
|
||||
return;
|
||||
}
|
||||
|
||||
TreePrototype[] treePrototypes = terrain.terrainData.treePrototypes;
|
||||
TreeInstance[] treeInstances = terrain.terrainData.treeInstances;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
// Check if there is already a backup of the terrainData and exit if so.
|
||||
string terrainDataPath = AssetDatabase.GetAssetPath(terrain.terrainData);
|
||||
string terrainDataBkpPath = terrainDataPath.Replace(".asset", "") + "_bkp.asset";
|
||||
TerrainData terrainDataBkp = AssetDatabase.LoadAssetAtPath<TerrainData>(terrainDataBkpPath);
|
||||
if (terrainDataBkp != null)
|
||||
{
|
||||
// A terrainData backup already exists. Log an error and exit.
|
||||
isTerrainDataDirty = true;
|
||||
Debug.LogError("Cannot backup terrainData for [" + terrain.terrainData.name + "]. A backup already exists. Please exit Play mode to fix.");
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (treeInstances == null || treeInstances.Length == 0 || treePrototypes == null || treePrototypes.Length == 0)
|
||||
{
|
||||
Debug.LogWarning("No trees found on terrain. Nothing to manage.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (treePrototypes.Length != destructibleTrees.Count)
|
||||
{
|
||||
Debug.LogWarning("Tree prototypes do not match DestroyIt's tree stand-in prefabs. Please click the \"Update Trees\" button on the TreeManager script.");
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < treePrototypes.Length; i++)
|
||||
{
|
||||
if (destructibleTrees[i].Prefab == null || treePrototypes[i].prefab == null || treePrototypes[i].prefab.name != destructibleTrees[i].Prefab.name)
|
||||
{
|
||||
Debug.LogWarning("Tree prototype names do not match Destructible tree stand-in prefab names. You may need to click the \"Update Trees\" button on the TreeManager script.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Capture original tree instances so we can reset them on application quit
|
||||
originalTreeInstances = treeInstances;
|
||||
currentTreeInstances = new List<TreeInstance>(treeInstances);
|
||||
treesToReset = new List<TreeReset>();
|
||||
|
||||
#if UNITY_EDITOR
|
||||
// Save the original terrainData object to a Resources folder, just in case there is a crash.
|
||||
// This way, the TerrainPreserver can check for any Resources data to load and ask the user if he/she wants to restore terrainData.
|
||||
if (backupTerrain)
|
||||
{
|
||||
AssetDatabase.CopyAsset(terrainDataPath, terrainDataBkpPath);
|
||||
AssetDatabase.Refresh();
|
||||
}
|
||||
#endif
|
||||
|
||||
// For each terrain tree, place a stripped-down tree prototype object at its location
|
||||
for (int i = 0; i < treeInstances.Length; i++)
|
||||
{
|
||||
TreeInstance tree = treeInstances[i];
|
||||
DestructibleTree destructibleTree = destructibleTrees.Find(x => x.prototypeIndex == tree.prototypeIndex);
|
||||
if (destructibleTree == null) continue;
|
||||
|
||||
GameObject treeObj = Instantiate(destructibleTree.Prefab, terrain.transform.parent, true);
|
||||
treeObj.transform.position = terrain.WorldPositionOfTree(i);
|
||||
treeObj.transform.localScale = new Vector3(tree.widthScale, tree.heightScale, tree.widthScale);
|
||||
treeObj.transform.rotation = Quaternion.AngleAxis(tree.rotation * Mathf.Rad2Deg, Vector3.up);
|
||||
}
|
||||
|
||||
SceneManager.activeSceneChanged += OnActiveSceneChanged;
|
||||
}
|
||||
|
||||
private void FixedUpdate()
|
||||
{
|
||||
if (treesToReset == null || treesToReset.Count == 0) return;
|
||||
List<TreeReset> treesReset = new List<TreeReset>();
|
||||
foreach (TreeReset tree in treesToReset)
|
||||
{
|
||||
if (DateTime.Now >= tree.resetTime)
|
||||
{
|
||||
TreeInstance treeInstance = new TreeInstance
|
||||
{
|
||||
position = tree.position,
|
||||
color = Color.white,
|
||||
heightScale = 1,
|
||||
widthScale = 1,
|
||||
prototypeIndex = tree.prototypeIndex
|
||||
};
|
||||
terrain.AddTreeInstance(treeInstance);
|
||||
treesReset.Add(tree);
|
||||
}
|
||||
}
|
||||
|
||||
foreach(TreeReset tree in treesReset)
|
||||
treesToReset.Remove(tree);
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
public void UpdateTrees()
|
||||
{
|
||||
// Get the current scene asset so we can find the InstanceId assigned to it.
|
||||
Object sceneAsset = AssetDatabase.LoadAssetAtPath<Object>(SceneManager.GetActiveScene().path);
|
||||
if (sceneAsset == null)
|
||||
{
|
||||
Debug.LogWarning("Could not update trees. You must first save your Scene using File => Save Scene.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the MD5 Hash of the scene and add it to the destructible tree stand-in path so we can manage destructible trees for multiple scenes.
|
||||
string sceneMD5Hash = GetMD5Hash(sceneAsset.name, 8);
|
||||
string path = pathToStandIns + sceneMD5Hash + "/";
|
||||
|
||||
// Clear out all existing tree prototype stand-in prefabs.
|
||||
destructibleTrees = new List<DestructibleTree>();
|
||||
|
||||
if (Directory.Exists(path)) { Directory.Delete(path, true); }
|
||||
Directory.CreateDirectory(path);
|
||||
|
||||
// Get terrain to manage
|
||||
if (terrain == null)
|
||||
terrain = Terrain.activeTerrain;
|
||||
|
||||
if (terrain == null || terrain.terrainData == null)
|
||||
{
|
||||
Debug.LogWarning("No terrain to update trees on.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (terrain.terrainData.treeInstances == null || terrain.terrainData.treeInstances.Length == 0 ||
|
||||
terrain.terrainData.treePrototypes == null || terrain.terrainData.treePrototypes.Length == 0)
|
||||
{
|
||||
Debug.LogWarning("No trees to update.");
|
||||
return;
|
||||
}
|
||||
|
||||
// For each tree prototype prefab in the terrain
|
||||
for (int i = 0; i < terrain.terrainData.treePrototypes.Length; i++)
|
||||
{
|
||||
GameObject treePrefab = terrain.terrainData.treePrototypes[i].prefab;
|
||||
|
||||
// Make a new tree object destructible stand-in
|
||||
GameObject treeObj = Instantiate(treePrefab);
|
||||
treeObj.name = treePrefab.name;
|
||||
|
||||
// If the tree prototype gameobject is a SpeedTree, tag it so we can determine proper rotation of the tree instance at runtime.
|
||||
if (IsSpeedTree(treeObj))
|
||||
treeObj.AddTag(Tag.SpeedTree);
|
||||
|
||||
// Strip the tree object down to essentials-only
|
||||
// NOTE: Add any additional components that you don't want to be removed from trees here
|
||||
foreach (Component comp in treeObj.GetComponentsInChildren<Component>())
|
||||
{
|
||||
if (comp.GetType() != typeof(Transform) && comp.GetType() != typeof(CapsuleCollider) && comp.GetType() != typeof(BoxCollider) &&
|
||||
comp.GetType() != typeof(SphereCollider) && comp.GetType() != typeof(MeshCollider) && comp.GetType() != typeof(Destructible) &&
|
||||
comp.GetType() != typeof(HitEffects) && comp.GetType() != typeof(TagIt) && comp.GetType() != typeof(ParticleSystem) &&
|
||||
comp.GetType() != typeof(ParticleSystemRenderer) && comp.GetType() != typeof(WhenDestroyedResetTree)
|
||||
&& comp.GetType() != typeof(WhenDamaged))
|
||||
DestroyImmediate(comp);
|
||||
}
|
||||
|
||||
// Tag the gameobject as a tree so we will know later to also remove its terrain tree instance if it is destroyed
|
||||
treeObj.AddTag(Tag.TerrainTree);
|
||||
|
||||
// Save the tree object as a prefab
|
||||
string treeName = treeObj.name;
|
||||
string localPath = path + treeName + ".prefab";
|
||||
PrefabUtility.SaveAsPrefabAssetAndConnect(treeObj, localPath, InteractionMode.AutomatedAction);
|
||||
DestroyImmediate(treeObj);
|
||||
|
||||
// Load the new prefab from the Resources folder and add to the collection of destructible trees
|
||||
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path + treeName + ".prefab");
|
||||
destructibleTrees.Add(new DestructibleTree{prototypeIndex = i, Prefab = prefab});
|
||||
}
|
||||
|
||||
Debug.Log(destructibleTrees.Count + " tree stand-ins updated to match prefabs.");
|
||||
}
|
||||
#endif
|
||||
|
||||
// Determines whether a tree prototype game object represents a SpeedTree tree.
|
||||
private bool IsSpeedTree(GameObject treeObj)
|
||||
{
|
||||
MeshRenderer[] meshes = treeObj.gameObject.GetComponentsInChildren<MeshRenderer>();
|
||||
if (meshes == null || meshes.Length <= 0) return false;
|
||||
for (int j = 0; j < meshes.Length; j++)
|
||||
{
|
||||
Material[] mats = meshes[j].sharedMaterials;
|
||||
for (int k = 0; k < mats.Length; k++)
|
||||
{
|
||||
if (mats[k].shader.name.Contains("SpeedTree"))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void DestroyTreeAt(Vector3 worldPoint)
|
||||
{
|
||||
TerrainTree tree = terrain.ClosestTreeToPoint(worldPoint);
|
||||
if (tree == null) return;
|
||||
|
||||
DestroyTree(tree);
|
||||
}
|
||||
|
||||
private void DestroyTree(TerrainTree tree)
|
||||
{
|
||||
// Hide the tree on the terrain by scaling it down to 0,0,0.
|
||||
TreeInstance ti = currentTreeInstances[tree.Index];
|
||||
ti.heightScale = 0f;
|
||||
ti.widthScale = 0f;
|
||||
currentTreeInstances[tree.Index] = ti;
|
||||
|
||||
// Assign the tree instances back to the terrain data.
|
||||
terrain.terrainData.treeInstances = currentTreeInstances.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>Restores the original trees back to the Terrain data on a CLEAN exit or scene change.</summary>
|
||||
public void RestoreTrees()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
if (isTerrainDataDirty)
|
||||
{
|
||||
// Don't modify the terrainData if there is a backup that hasn't been resolved yet.
|
||||
Debug.LogWarning("TerrainData is dirty (there is a backup that has not been resolved). Exiting restore process to prevent overwriting.");
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (originalTreeInstances == null)
|
||||
{
|
||||
//Debug.Log("No original tree instances to restore. Exiting.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (terrain == null)
|
||||
{
|
||||
//Debug.Log("No terrain, therefore no trees to restore on TerrainData.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (terrain.terrainData == null)
|
||||
{
|
||||
//Debug.Log("No TerrainData, therefore no trees to restore on TerrainData.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (terrain.terrainData.treeInstances == null)
|
||||
{
|
||||
//Debug.Log("No tree instances on the terrain. Therefore, nothing to restore.");
|
||||
return;
|
||||
}
|
||||
|
||||
terrain.terrainData.treeInstances = originalTreeInstances;
|
||||
#if UNITY_EDITOR
|
||||
// Delete the backup
|
||||
string terrainDataPath = AssetDatabase.GetAssetPath(terrain.terrainData);
|
||||
string terrainDataBkpPath = terrainDataPath.Replace(".asset", "") + "_bkp.asset";
|
||||
TerrainData terrainDataBkp = AssetDatabase.LoadAssetAtPath<TerrainData>(terrainDataBkpPath);
|
||||
if (terrainDataBkp != null)
|
||||
{
|
||||
AssetDatabase.DeleteAsset(terrainDataBkpPath);
|
||||
AssetDatabase.Refresh();
|
||||
//Debug.Log("TerrainData restored, deleted backup file.");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private void OnActiveSceneChanged(Scene current, Scene next)
|
||||
{
|
||||
RestoreTrees();
|
||||
}
|
||||
|
||||
private void OnApplicationQuit()
|
||||
{
|
||||
RestoreTrees();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
RestoreTrees();
|
||||
}
|
||||
|
||||
private string GetMD5Hash(string input, int length)
|
||||
{
|
||||
MD5 md5 = new MD5CryptoServiceProvider();
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(input);
|
||||
byte[] hash = md5.ComputeHash(bytes);
|
||||
|
||||
int len = length <= hash.Length ? length : hash.Length;
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < len; i++)
|
||||
sb.Append(hash[i].ToString("x2"));
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/ThirdParty/DestroyIt/Scripts/Runtime/Managers/TreeManager.cs.meta
vendored
Normal file
11
Assets/ThirdParty/DestroyIt/Scripts/Runtime/Managers/TreeManager.cs.meta
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 61968be30d0ab51408827d6f430b6cea
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user