Skip to content

Instantly share code, notes, and snippets.

@SorraTheOrc
Last active February 10, 2024 01:38
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save SorraTheOrc/7c8548c796032707553ac968d0a3a438 to your computer and use it in GitHub Desktop.
Save SorraTheOrc/7c8548c796032707553ac968d0a3a438 to your computer and use it in GitHub Desktop.
A simple optimization trick in Unity - disable/enable objects based on proximity to the player.
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using Sirenix.OdinInspector;
using System.Linq;
using System;
using WizardsCode;
namespace WizardsCode.Optimization
{
/// <summary>
/// The ProximityActivationManager will enable and disable objects based on their proximity to a target object.
/// Place this component on a manager object.
///
/// Objects are automatically regiested with the manager if they have a `ProximityRegistration`
/// behaviour attached to the object.
/// </summary>
public class ProximityActivationManager : MonoBehaviour
{
[Header("Target")]
[SerializeField, Tooltip("The target to detect proximity to. If null the system will attempt to find the player on startup.")]
Transform m_ProximityTarget;
[Header("Tick")]
[SerializeField, Tooltip("The frequency, in seconds, at which to evaluate distances from the target and to enable/disable managed objects. Not that not every object will be evaluated on every tick. Ones that are closer to the target will be evaluated more frequently.")]
float m_TickFrequency = 0.5f;
HashSet<ProximityRegistration> m_FrequentlyManagedObjects = new HashSet<ProximityRegistration>();
HashSet<ProximityRegistration> m_MidFrequencyManagedObjects = new HashSet<ProximityRegistration>();
HashSet<ProximityRegistration> m_InfrequentManagedObjects = new HashSet<ProximityRegistration>();
Transform ProximityTarget
{
get {
return m_ProximityTarget;
}
}
private void Start()
{
StartCoroutine(EvalauteCo());
}
internal void Add(ProximityRegistration obj)
{
obj.Disable();
m_FrequentlyManagedObjects.Add(obj);
}
private IEnumerator EvalauteCo()
{
while (true)
{
// First cycle check all
EvaluateAll(m_InfrequentManagedObjects);
yield return new WaitForSeconds(m_TickFrequency);
EvaluateAll(m_MidFrequencyManagedObjects);
yield return new WaitForSeconds(m_TickFrequency);
EvaluateAll(m_FrequentlyManagedObjects);
yield return new WaitForSeconds(m_TickFrequency);
// Second cycle check only mid and near
EvaluateAll(m_MidFrequencyManagedObjects);
yield return new WaitForSeconds(m_TickFrequency);
EvaluateAll(m_FrequentlyManagedObjects);
yield return new WaitForSeconds(m_TickFrequency);
// Third cycle check only near
EvaluateAll(m_FrequentlyManagedObjects);
yield return new WaitForSeconds(m_TickFrequency);
}
}
private void EvaluateAll(HashSet<ProximityRegistration> set)
{
for (int i = set.Count - 1; i >= 0; i--)
{
Evaluate(set.ElementAt(i), set);
}
}
private void Evaluate(ProximityRegistration obj, HashSet<ProximityRegistration> currentSet)
{
if (!ProximityTarget) return;
float distance = Vector3.SqrMagnitude(ProximityTarget.position - obj.transform.position);
if (!obj.gameObject.activeInHierarchy && obj.DisabledByProximity && distance < obj.NearDistanceSqr)
{
if (currentSet != null && currentSet != m_FrequentlyManagedObjects)
{
currentSet.Remove(obj);
m_FrequentlyManagedObjects.Add(obj);
}
obj.Enable();
}
else if (obj.gameObject.activeInHierarchy && distance > obj.FarDistanceSqr)
{
if (currentSet != null && currentSet != m_InfrequentManagedObjects)
{
currentSet.Remove(obj);
m_InfrequentManagedObjects.Add(obj);
}
obj.Disable();
}
else
{
if (currentSet != null && currentSet != m_MidFrequencyManagedObjects)
{
currentSet.Remove(obj);
m_MidFrequencyManagedObjects.Add(obj);
}
}
}
}
}
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System;
namespace WizardsCode.Optimization
{
/// <summary>
/// This component will cause the object to self register with the `ProximityActivationManager`
/// in the scene when it awakes.
/// </summary>
public class ProximityRegistration : MonoBehaviour
{
[SerializeField, Tooltip("The distance at which the object is considered nearby and thus should be enabled.")]
float m_NearDistance = 20;
[SerializeField, Tooltip("The distance at which the object is considered far away and thus should be disabled.")]
float m_FarDistance = 30;
/// <summary>
/// The square of the distance at which the object is considered nearby and thus should be enabled.
/// </summary>
public float NearDistanceSqr
{
get; private set;
}
/// <summary>
/// The square of the distance at which the object is considered far away and thus should be disabled.
/// </summary>
public float FarDistanceSqr
{
get; private set;
}
/// <summary>
/// Indicates whether this object has been disabled due to proximity to the target or not.
/// </summary>
public bool DisabledByProximity
{
get; private set;
}
/// <summary>
/// Disable this object because of its proximity check.
/// </summary>
public void Disable()
{
DisabledByProximity = true;
gameObject.SetActive(false);
}
/// <summary>
/// Disable this object because of its proximity check.
/// </summary>
public void Enable()
{
DisabledByProximity = false;
gameObject.SetActive(true);
}
private void Awake()
{
NearDistanceSqr = m_NearDistance * m_NearDistance;
FarDistanceSqr = m_FarDistance * m_FarDistance;
//OPTIMIZATION: make the ProximityActivationManager a singleton
ProximityActivationManager manager = GameObject.FindObjectOfType<ProximityActivationManager>();
manager.Add(this);
}
}
}
@NotsoCompany
Copy link

Trying to understand this (still new to C#) ...when I use it,
On start it turns off the objects, great. I get near it turns them on, great, move away they go off, great....
But when I go BACK to the area it does not turn them on again. Am I missing something?

@SorraTheOrc
Copy link
Author

I've not observed that problem... let me look into it.

@rauldeavila
Copy link

rauldeavila commented Nov 17, 2022

Hey @NotsoCompany , it's a simple fix on line 55 from ProximityActivationManager you need to change the code to the following:

                (...)
                EvaluateAll(m_InfrequentManagedObjects); // here's the issue. Infrequent (disabled objects) wasn't returning to the pool.
                yield return new WaitForSeconds(m_TickFrequency);

                EvaluateAll(m_MidFrequencyManagedObjects);
                yield return new WaitForSeconds(m_TickFrequency);

                EvaluateAll(m_FrequentlyManagedObjects);
                yield return new WaitForSeconds(m_TickFrequency);

                // Second cycle check only mid and near
                (...)

@SorraTheOrc
Copy link
Author

Thank you @rauldeavila I am sorry I neglected to update this gist. You are quite correct on the fix and I have included it above

@SawyerK8
Copy link

SawyerK8 commented Apr 10, 2023

Hi!
Really liking your script! Is there a way to make it so that when I start the scene, the objects don't start out as disabled but the opposite?
What I mean by this is when I launch the game, all objects starts out as disabled. I want it so when I launch the game all objects starts out enabled then disable the ones that are far away. This way I can make loading variables possible.
Also there is a "MissingReferenceException" when the Proximity Registration object is destroyed and the script no longer works. Is there a way to put a null reference check to the script?
I'm willing to pay for these changes if needed.
Thank you for your answer!

@SorraTheOrc
Copy link
Author

@SawyerK8 see line 42:

internal void Add(ProximityRegistration obj)
        {
            obj.Disable();
            m_FrequentlyManagedObjects.Add(obj);
        }

Just comment out the obj.Disable() to prevent all objects being disabled on startup. However, this isn't really the right way to do things. It will put significant overhead on your startup process if you have many objects and if you don't then why do you need this script. You can (and should) do all your object configuration in Awake and/or Start and/or OnEnable - all of which are fired before the above disable call.

For the destroyed object problem all you need to do is add an equivalent of the above Add method in the manage, but Remove then have the ProximityRegistration cace the reference to the manager object and call your new manager.Remove(gameObject) from OnDestroy in the proximity registration object.

@JAMS49
Copy link

JAMS49 commented Feb 9, 2024

@SawyerK8 see line 42:

internal void Add(ProximityRegistration obj)
        {
            obj.Disable();
            m_FrequentlyManagedObjects.Add(obj);
        }

Just comment out the obj.Disable() to prevent all objects being disabled on startup. However, this isn't really the right way to do things. It will put significant overhead on your startup process if you have many objects and if you don't then why do you need this script. You can (and should) do all your object configuration in Awake and/or Start and/or OnEnable - all of which are fired before the above disable call.

For the destroyed object problem all you need to do is add an equivalent of the above Add method in the manage, but Remove then have the ProximityRegistration cace the reference to the manager object and call your new manager.Remove(gameObject) from OnDestroy in the proximity registration object.

it's awesome !! instead of culling we can practice this thing for performance.,
Hey Buddy , I was working on my android project, can you consider licensing it under MIT please?

@SorraTheOrc
Copy link
Author

it's awesome !! instead of culling we can practice this thing for performance., Hey Buddy , I was working on my android project, can you consider licensing it under MIT please?

There's no way to license Gusts, and I'm to lazy to add headers when the intent is just to dump it here for notes. Consider all my Gists to be CC0

@JAMS49
Copy link

JAMS49 commented Feb 10, 2024

it's awesome !! instead of culling we can practice this thing for performance., Hey Buddy , I was working on my android project, can you consider licensing it under MIT please?

There's no way to license Gusts, and I'm to lazy to add headers when the intent is just to dump it here for notes. Consider all my Gists to be CC0

Thanks a lot💚

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment