Forum begins after the advertisement:

 


[Part 21] How to handle 100s of gems on screen

Home Forums Video Game Tutorial Series Creating a Rogue-like Shoot-em Up in Unity [Part 21] How to handle 100s of gems on screen

Viewing 10 posts - 1 through 10 (of 10 total)
  • Author
    Posts
  • #18172
    Yip Si Yuan
    Level 7
    Participant
    Helpful?
    Up
    2
    ::

    While playtesting the wave system, I observed significant frame drops during large enemy waves. The game’s framerate, which typically stayed around 200–300 FPS, would drop as low as 15–30 FPS. After profiling the game, I traced the issue to the Gem pickups.

    Gems were never being destroyed unless collected by the player, meaning they remained in the scene indefinitely—even when off-screen. Over time, this accumulation of gems caused a noticeable performance hit.

    As a temporary solution, I implemented a timed self-destruction mechanism for the gems. After a set duration, uncollected gems are automatically destroyed. This approach prevents off-screen gems from piling up and has helped stabilize performance to around 90 FPS during heavy waves.

    using System.Collections;
    using UnityEngine;
    
    public class Pickup : Sortable
    {
        public float lifespan = 0.5f;
        protected PlayerStats target; // If the pickup has a target, then fly towards the target.
        protected float speed; // The speed at which the pickup travels.
        Vector2 initialPosition;
        float initialOffset;
    
        // To represent the bobbing animation of the object.
        [System.Serializable]
        public struct BobbingAnimation
        {
            public float frequency;
            public Vector2 direction;
        }
        public BobbingAnimation bobbingAnimation = new BobbingAnimation {
            frequency = 2f, direction = new Vector2(0,0.3f)
        };
    
        [Header("Bonuses")]
        public int experience;
        public int health;
    
        protected override void Start()
        {
            base.Start();
            initialPosition = transform.position;
            initialOffset = Random.Range(0, bobbingAnimation.frequency);
            StartCoroutine(InActive());
    
    
        }
    
        protected virtual void Update()
        {
            if (target)
            {
                // Move it towards the player and check the distance between.
                Vector2 distance = target.transform.position - transform.position;
                if (distance.sqrMagnitude > speed * speed * Time.deltaTime)
                    transform.position += (Vector3)distance.normalized * speed * Time.deltaTime;
                else
                    Destroy(gameObject);
    
            }
            else
            {
                // Handle the animation of the object.
                transform.position = initialPosition + bobbingAnimation.direction * Mathf.Sin((Time.time + initialOffset) * bobbingAnimation.frequency);
            }
    
            gameObject.SetActive(sorted.isVisible);
        }
    
        public virtual bool Collect(PlayerStats target, float speed, float lifespan = 0f)
        {
            if (!this.target)
            {
                this.target = target;
                this.speed = speed;
                if (lifespan > 0) this.lifespan = lifespan;
                Destroy(gameObject, Mathf.Max(0.01f, this.lifespan));
                return true;
            }
            return false;
        }
    
        protected virtual void OnDestroy()
        {
            if (!target) return;
            target.IncreaseExperience(experience);
            target.RestoreHealth(health);
        }
    
    public IEnumerator InActive()
        {
            yield return new WaitForSeconds(15f);
            sorted.enabled = false;
        }
    
    }

    I hope this solution has been helpful. That said, a steady 90 FPS is still a significant drop from the original 200–300 FPS, so I’m open to any suggestions for further optimization. If there are other strategies we can explore to improve performance, I’d be glad to hear them.

    and 1 other person have upvoted this post.
    #18178
    Terence
    Level 31
    Keymaster
    Helpful?
    Up
    0
    ::

    Just to add on, instead of adding a lifespan to each of the drops, we can also make our objects inactive when they are outside of the screen. This is an easy way to keep all your gems on screen without having to destroy them:

    using UnityEngine;
    
    /// <summary>
    /// This is a class that can be subclassed by any other class to make the sprites
    /// of the class automatically sort themselves by the y-axis.
    /// </summary>
    [RequireComponent(typeof(SpriteRenderer))]
    public abstract class Sortable : MonoBehaviour
    {
    
        SpriteRenderer sorted;
        public bool sortingActive = true; // Allows us to deactivate this on certain objects.
        public const float MIN_DISTANCE = 0.2f;
        int lastSortOrder = 0;
    
        // Start is called before the first frame update
        protected virtual void Start()
        {
            sorted = GetComponent<SpriteRenderer>();    
        }
    
        protected virtual void Update() {
            // Only make the object active if it is visible on a camera.
            gameObject.SetActive(sorted.isVisible);
        }
    
        // Update is called once per frame
        protected virtual void LateUpdate()
        {
            if (!sorted) return;
            int newSortOrder = (int)(-transform.position.y / MIN_DISTANCE);
            if (lastSortOrder != newSortOrder) {
                lastSortOrder = sorted.sortingOrder;
                sorted.sortingOrder = newSortOrder;
            }
        }
    }

    EDIT: Code doesn’t entirely work, please read on further to find the fix.

    #18181
    Grim Rubbish
    Level 21
    Silver Supporter (Patron)
    Helpful?
    Up
    0
    ::

    Hey guys, I updated the latest version of the Sortable class, but it had no effect. The items outside of the camera won’t be set to “not active”. Do I have to change something else to make it work? Thank you in advance!

    using UnityEngine;
    
    /// <summary>
    /// This is a class that can be subclassed by any other class to make the sprites
    /// of the class automatically sort themselves by the y-axis.
    /// </summary>
    [RequireComponent(typeof(SpriteRenderer))]
    public abstract class Sortable : MonoBehaviour
    {
    
        SpriteRenderer sorted;
        public bool sortingActive = true; // Allows us to deactivate this on certain objects.
        public const float MIN_DISTANCE = 0.2f;
        int lastSortOrder = 0;
    
        // Start is called before the first frame update
        protected virtual void Start()
        {
            sorted = GetComponent<SpriteRenderer>();
        }
    
        void Update()
        {
            // Only make the object active if it is visible on a camera.
            gameObject.SetActive(sorted.isVisible);
        }
    
        // Update is called once per frame
        protected virtual void LateUpdate()
        {
            if (!sorted) return;
            int newSortOrder = (int)(-transform.position.y / MIN_DISTANCE);
            if (lastSortOrder != newSortOrder)
            {
                lastSortOrder = sorted.sortingOrder;
                sorted.sortingOrder = newSortOrder;
            }
        }
    }
    #18182
    Alp Apustaja
    Level 7
    AI Assistant
    (BETA)
    Helpful?
    Up
    0
    ::

    It looks like you’re trying to deactivate objects that are not visible to the camera using the sorted.isVisible property in your Update method. However, if the objects are not getting set to inactive as expected, there are a few things you can check:

    1. Camera Setup: Ensure that your camera is correctly set up to view the objects. If the objects are outside the camera’s frustum, they won’t be marked as visible.

    2. Sorting Layer and Order: Make sure that the sorting layer and order of the SpriteRenderer are set up correctly. If another object is obscuring them, they may not be visible even if the camera can technically see them.

    3. Update Method Execution: Confirm that the Update method is being called for your Sortable objects. You can add a debug log inside the Update method to check if it is executing as expected:

      void Update()
      {
          Debug.Log($"{gameObject.name} visible: {sorted.isVisible}");
          gameObject.SetActive(sorted.isVisible);
      }
    4. Camera’s Culling Mask: Ensure that the camera’s culling mask includes the layer that your objects are on.

    5. Object Scale and Position: If your objects are scaled down to zero or positioned in a way that they are effectively off-screen, they might not be considered visible.

    If you still face issues, you could implement a more manual check to determine if the object is within the camera bounds. Here’s a quick way to do that:

    void Update()
    {
        Renderer renderer = GetComponent();
    
        if (renderer != null)
        {
            if (GeometryUtility.TestPlanesAABB(GeometryUtility.CalculateFrustumPlanes(Camera.main), renderer.bounds))
            {
                gameObject.SetActive(true);
            }
            else
            {
                gameObject.SetActive(false);
            }
        }
    }

    This code checks if the object’s bounds are within the camera’s frustum, which can be more reliable in certain situations.

    #18183
    Grim Rubbish
    Level 21
    Silver Supporter (Patron)
    Helpful?
    Up
    0
    ::

    If I’m using the last script solution, items getting deactivated but not activated again.

    #18184
    Alp Apustaja
    Level 7
    AI Assistant
    (BETA)
    Helpful?
    Up
    0
    ::

    It seems like the issue is that while your objects are deactivating correctly when they go out of the camera’s view, they are not reactivating properly when they come back into view. This could happen if the Update method is not being called correctly or if the conditions for reactivation are not being met.

    Let’s ensure that the Renderer component is being checked correctly. Here’s a modified version of the script that should handle both activation and deactivation:

    void Update()
    {
        Renderer renderer = GetComponent();
    
        if (renderer != null)
        {
            // Check if the object is within the camera's view
            if (GeometryUtility.TestPlanesAABB(GeometryUtility.CalculateFrustumPlanes(Camera.main), renderer.bounds))
            {
                // Only activate if it was previously inactive
                if (!gameObject.activeSelf)
                {
                    gameObject.SetActive(true);
                }
            }
            else
            {
                // Only deactivate if it was previously active
                if (gameObject.activeSelf)
                {
                    gameObject.SetActive(false);
                }
            }
        }
    }

    Explanation:

    1. Check for Active State: The script checks if the game object is already active before calling SetActive(true), which helps avoid unnecessary calls to SetActive and can improve performance slightly.

    2. Deactivation Check: Similarly, it checks if the object is already inactive before calling SetActive(false).

    Additional Debugging:

    If the problem persists, you can add debug logs to see what’s happening:

    void Update()
    {
        Renderer renderer = GetComponent();
    
        if (renderer != null)
        {
            bool isVisible = GeometryUtility.TestPlanesAABB(GeometryUtility.CalculateFrustumPlanes(Camera.main), renderer.bounds);
    
            if (isVisible)
            {
                if (!gameObject.activeSelf)
                {
                    Debug.Log($"{gameObject.name} activated");
                    gameObject.SetActive(true);
                }
            }
            else
            {
                if (gameObject.activeSelf)
                {
                    Debug.Log($"{gameObject.name} deactivated");
                    gameObject.SetActive(false);
                }
            }
        }
    }

    These logs will help you see when objects are being activated and deactivated, which can provide insight into why they might not be reactivating as expected.

    #18185
    Grim Rubbish
    Level 21
    Silver Supporter (Patron)
    Helpful?
    Up
    0
    ::

    Objects/Enemys are still getting deactivated but not activated again when in camera view.

    #18186
    Alp Apustaja
    Level 7
    AI Assistant
    (BETA)
    Helpful?
    Up
    0
    ::

    Could you provide more details about your setup? Specifically:

    1. Are you using the Sortable script directly on enemies?
    2. What camera are you using (Main Camera, custom camera)?
    3. Can you confirm the layer settings of your objects and camera?

    Without additional context, I can suggest checking:

    • Ensure Camera.main is correctly referencing your main camera
    • Verify the camera’s culling mask includes the layer of your objects
    • Check that the Renderer component is properly attached

    Would you be willing to share more specifics about your implementation?

    #18187
    Terence
    Level 31
    Keymaster
    Helpful?
    Up
    0
    ::

    Grim, apologies, slipped my mind that the Update() code will not run when the object is inactive.

    Here’s how I fixed it (added sections highlighted in green):

    using UnityEngine;
    using System.Collections.Generic;
    
    /// <summary>
    /// This is a class that can be subclassed by any other class to make the sprites
    /// of the class automatically sort themselves by the y-axis.
    /// </summary>
    [RequireComponent(typeof(SpriteRenderer))]
    public abstract class Sortable : MonoBehaviour
    {
    
        SpriteRenderer sorted;
        public bool sortingActive = true; // Allows us to deactivate this on certain objects.
        public const float MIN_DISTANCE = 0.2f;
        int lastSortOrder = 0;
    
        public static List<SpriteRenderer> all = new List<SpriteRenderer>();
    
        // Start is called before the first frame update
        protected virtual void Start()
        {
            sorted = GetComponent<SpriteRenderer>();
            all.Add(sorted);
        }
    
        protected virtual void Update() {
            // Only make the object active if it is visible on a camera.
            gameObject.SetActive(sorted.isVisible);
        }
    
        // Reactivates all inactive sortables. To be called by another class.
        public static void ReactivateAll() {
            all.RemoveAll(item => item == null);
            foreach(SpriteRenderer r in all) {
                if(!r.gameObject.activeSelf && r.isVisible)
                    r.gameObject.SetActive(true);
            }
        }
    
        // Update is called once per frame
        protected virtual void LateUpdate()
        {
            if (!sorted) return;
            int newSortOrder = (int)(-transform.position.y / MIN_DISTANCE);
            if (lastSortOrder != newSortOrder) {
                lastSortOrder = sorted.sortingOrder;
                sorted.sortingOrder = newSortOrder;
            }
        }
    }

    For this to work, you will need to call ReactivateAll() from the Update() of another script. Any script will do, but it has to be always active. GameManager works, for example.

    public class GameManager {
        ...
        void Update() {
            ...
            Sortable.ReactivateAll();
        }
    }

    Alp, can you provide a deeper explanation of the code above? Especially the parts highlighted green (which denote the edits to the code).

    One last thing, isVisible might not be working because anything that is in the Scene view is also considered to be isVisible = true. You will need to use the Hierarchy to confirm if the objects are getting deactivated instead (the name will be greyed out).

    #18188
    Alp Apustaja
    Level 7
    AI Assistant
    (BETA)
    Helpful?
    Up
    1
    ::

    Here’s a detailed explanation of the green-highlighted code:

    1. public static List all = new List();

      • Creates a static list to keep track of all SpriteRenderers that inherit from Sortable
      • Allows tracking of all sortable objects across the entire game
    2. all.Add(sorted); in the Start() method

      • Adds each Sortable object’s SpriteRenderer to the static list when it’s initialized
      • Ensures the list maintains a record of all sortable objects
    3. public static void ReactivateAll()

      • A static method that can be called from any script
      • all.RemoveAll(item => item == null); cleans up the list by removing any null references
      • Iterates through all tracked SpriteRenderers
      • Reactivates objects that are currently inactive but visible
      • Solves the problem of objects not reactivating when they become visible again

    The solution works by maintaining a global list of sortable objects and providing a method to reactivate them, bypassing the issue of inactive objects not running their Update() method.

    Would you like me to elaborate on any specific part of the implementation?

    has upvoted this post.
Viewing 10 posts - 1 through 10 (of 10 total)
  • You must be logged in to reply to this topic.

Go to Login Page →


Advertisement below: