Scheduler not firing a scheduled event?

airoll

Member
Hello I am having an issue with the scheduler that I cannot seem to debug for the life of me.

I have created a class called PickupSpawner. The function of PickupSpawner is to spawn a pickup at a location using SpawnPickup(), and then when the pickup is picked up or deactivated, it calls ScheduleSpawnPickup(), which schedules SpawnPickup() to trigger after m_SpawnDelay seconds. The code for both methods is below.

Now the weird thing that is happening is that the PickupSpawner works fine in the beginning (it spawns new pickups after the previous one was picked up), but after a while, it just stops spawning pickups completely. Sometimes it stops spawning after 1 minute into the scene, sometimes it takes 30 minutes. However, everytime when I inspect the SpawnPickup, I see the following: the scheduled event is not active, and the m_CurrentPickupObject, which is only modified in SpawnPickup(), and ScheduleSpawnPickup(), is set to null. There are no errors or warnings that appear in the console.

1641435243763.png
1641435267799.png

Since m_CurrentPickupObject is null, and Debug.Log("Rescheduled " + gameObject.name") doesn't print out anything to the console, this leads me to believe that the SpawnPickup() action never executed. What I'm wondering is, how is this that even possible? Is there any scenario in which the scheduler doesn't fire a scheduled event?

Note: If it matters, I always set m_SpawnDelay to a positive float like 3f or 15f. I am using Third Party Controller v2.4.4 and Ultimate Inventory System v1.2.4.
Edit: I realized I put this in the wrong forum, it should actually go in UltimateCharacterController > Questions, sorry!

Code:
        /// <summary>
        /// Spawns the item pickup.
        /// </summary>
        public void SpawnPickup()
        {
            GameObject pickupPrefab = SelectPickupCandidate();
            Quaternion rotation = Quaternion.identity;
            Vector3 position = Vector3.zero;

            if (pickupPrefab == null || !m_SpawnPoint.GetPlacement(pickupPrefab, ref position, ref rotation))
            {
                Debug.Log("Rescheduled " + gameObject.name);
                ScheduleSpawnPickup();
            }
            else
            {
                m_CurrentPickupObject = ObjectPool.Instantiate(pickupPrefab, position, rotation);
                m_CurrentPickupObject.transform.parent = m_PickupParent;
                EventHandler.RegisterEvent(m_CurrentPickupObject, "OnRetractStart", ScheduleSpawnPickup);
                EventHandler.RegisterEvent(m_CurrentPickupObject, "OnPickupDespawn", ScheduleSpawnPickup);
            }
        }

        /// <summary>
        /// Schedules the spawn of a pickup.
        /// </summary>
        private void ScheduleSpawnPickup()
        {
            if (m_CurrentPickupObject != null)
            {
                EventHandler.UnregisterEvent(m_CurrentPickupObject, "OnRetractStart", ScheduleSpawnPickup);
                EventHandler.UnregisterEvent(m_CurrentPickupObject, "OnPickupDespawn", ScheduleSpawnPickup);
                // CancelScheduledSpawnPickup();
                m_CurrentPickupObject = null;
                m_ScheduledSpawnPickup = Scheduler.ScheduleFixed(m_SpawnDelay, () => SpawnPickup());
            }
        }
 
Last edited:
For what's it worth, I tried replacing Scheduler.ScheduleFixed(...) with Scheduler.Schedule(...) and it didn't work. Replacing it with a coroutine did not fix the issue. I've also noticed other places in my project where I called Scheduler.ScheduleFixed(...) and it fails to execute as well. Any idea as to why this is the case? If it helps, this occurs when I'm training AI using MLAgents.

EDIT: I have a hypothesis that this has to do with Scheduler using Time.time instead of Time.fixedTime. It would be great to be able to modify the source for it, but when I download it from the Third Party Controller download page, it seems like the Shared Source package is an old version (I get compile errors). Would it be possible to update that?
 
Last edited:
This is really hard to say without being able to debug it. Can you tell me how to reproduce it within the demo scene? We are releasing an update next week and I'll update the shared source then.
 
Hey @Justin I did a bit more debugging and I was able to isolate the source to the way generic events are pooled. I'm not sure the exact cause, but replacing:

Code:
var scheduledEvent = GenericObjectPool.Get<ScheduledEvent<T>>();

with

Code:
var scheduledEvent = new ScheduledEvent<T>();

seemed to fix the issue. I had to do this for all permutations of ScheduledEvent. I did this using the current Opsive Shared Source download for TPC. I know in the changelog a while back for https://opsive.com/news/ultimate-character-controller-2-4-2-released/, it said: "Fixed Scheduler from not correctly pooling generic ScheduledEvents." So I'm not sure if this a regression or something else? Let me know your thoughts!
 
If you are having to do a new event each time then something is going wrong. Are you able to list the steps to reproduce it so I can get a better idea of what is going on?
 
I will work on a project to reproduce it. Have you already updated the Opsive Shared Source? If so, can you provide what the delta was between now and the original pooling code before 2.4.2 where you fixed: "Fixed Scheduler from not correctly pooling generic ScheduledEvents."?

EDIT: Also, could you provide the source for MemberVisibility.cs or anything else required for UltimateInventorySystem? I am trying to test to see if this issue is related to thread safety with the Stack data structure in GenericObjectPool, but if I try to replace Opsive.Shared.Utility.dll with the source files in the Opsive Shared Source, it looks like I am missing MemberVisibility (and possibly other files).
 
Last edited:
Okay now I think I have a hypothesis of what is going on. It's very unobvious unless you read through the source code. Let's take an example of the class below. Let's say we have an object with this Lifespan class, and initially we SetLifespan(3f), such that it will de-spawn in 3 seconds. Now, when SchedulerBase invokes the m_ScheduledDespawn event, it will return the ScheduledEvent back to the generic pool. Now, some other class in our project schedules an event, which calls GenericObjectPool.Get<ScheduledEvent>() and gets the same reference to the scheduled event that was just returned. So now m_ScheduledDespawn points to this new scheduled event, which is active!

Now if my code calls CancelScheduledDespawn(), then it will look at m_ScheduledDespawn, which is not null, and it will try to cancel the event that has been scheduled by our other class! Now this might be avoided by adding a m_ScheduledDespawn = null line to Despawn(), but this pattern is not obvious at all. Furthermore, this pattern requires you to have different methods for scheduling a despawn (which should set m_ScheduledDespawn to null) and manually despawning (in which you should try to cancel m_ScheduledDespawn).

Does that make sense?

C#:
    public class Lifespan : MonoBehaviour
    {
        [Tooltip("The default lifespan of the object (in seconds).")]
        [SerializeField] protected float m_DefaultLifespan = -1f;

        private GameObject m_GameObject;
        private ScheduledEventBase m_ScheduledDespawn;
        private float m_Lifespan;

        /// <summary>
        /// Initialize default values.
        /// </summary>
        private void Awake()
        {
            m_GameObject = gameObject;
        }

        /// <summary>
        /// The drop was enabled (spawned).
        /// </summary>
        private void OnEnable()
        {
            SetLifespan(m_DefaultLifespan);
        }

        /// <summary>
        /// Set the lifespan of the pickup.
        /// </summary>
        public void SetLifespan(float lifespan)
        {
            m_Lifespan = lifespan;
            RestartLifespan();
        }


        /// <summary>
        /// Restarts the lifespan of the pickup.
        /// </summary>
        public void RestartLifespan()
        {
            CancelScheduledDespawn();
            ScheduleDespawn();
        }

        /// <summary>
        /// The pickup was de-spawned.
        /// </summary>
        private void OnDisable()
        {
            CancelScheduledDespawn();
        }

        /// <summary>
        /// Cancels the scheduled despawn.
        /// </summary>
        private void CancelScheduledDespawn()
        {
            if (m_ScheduledDespawn != null)
            {
                Scheduler.Cancel(m_ScheduledDespawn);
                m_ScheduledDespawn = null;
            }
        }

        /// <summary>
        /// Schedules a despawn using the lifespan.
        /// </summary>
        private void ScheduleDespawn()
        {
            if (m_Lifespan > 0f)
            {
                m_ScheduledDespawn = Scheduler.ScheduleFixed(m_Lifespan, Despawn);
                EventHandler.ExecuteEvent(m_GameObject, "OnScheduleDespawn", Time.fixedTime + m_Lifespan);
            }
        }

        /// <summary>
        /// Despawns the pickup.
        /// </summary>
        private void Despawn()
        {
            EventHandler.ExecuteEvent(m_GameObject, "OnDespawn");
            ObjectPool.Destroy(m_GameObject);
        }
 
Ahh, that makes sense, and I think that I fixed an issue similar to that in the shootable weapon. Since the events are pooled you do need to set the pooled object to null when it is done being used. Are you able to find a case within the character controller where I'm not doing that? I thought that I checked this situation in all of the classes.
 
I think we have a similar problem.
When we use the scheduler for some events that need to be executed every 3-5 seconds, the scheduler automatically cancels these events. Sometimes 5 minutes, sometimes 10 minutes, but it always happens.

Our usage of the scheduler is similar to airoll.
 
So I fixed all my code with the new pattern so that this isn't an issue anymore. That said, I think it's worth updating the documentation so that people know how they should be scheduling events on their own.

In terms of the existing character controller code, I think Destroy() in Explosion.cs should set m_DestructionEvent = null before calling Destroy(...), and Deactivate() in Projectile.cs should set m_ScheduledDeactivation = null before calling OnCollision(null).
 
Just as a follow up here, I'm wondering if it's good practice to set the scheduled event variable to null after calling SchedulerBase.Cancel(..) on it? If so, then the following scripts have code where the variable should be set to null after canceling.

VirtualTouchpad.cs
CapsuleColliderPositioner.cs
DamageVisualization.cs
AttributeManager.cs
ParticlePooler.cs
MessageMonitor.cs
EquipUnequip.cs

I also noticed that some of the scheduled methods in EquipUnequip don't actually set the scheduled event to null (e.g. ItemUnequip, ItemEquip, ItemUnequipComplete, ItemEquipComplete).
 
Top