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
- This topic has 5 replies, 4 voices, and was last updated 1 week, 5 days ago by
Alp Apustaja.
-
AuthorPosts
-
May 15, 2025 at 11:41 am #18172::
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. May 17, 2025 at 9:46 pm #18178::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.
May 19, 2025 at 2:30 pm #18181::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; } } }
May 19, 2025 at 2:36 pm #18182::It looks like you’re trying to deactivate objects that are not visible to the camera using the
sorted.isVisible
property in yourUpdate
method. However, if the objects are not getting set to inactive as expected, there are a few things you can check:-
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.
-
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. -
Update Method Execution: Confirm that the
Update
method is being called for yourSortable
objects. You can add a debug log inside theUpdate
method to check if it is executing as expected:void Update() { Debug.Log($"{gameObject.name} visible: {sorted.isVisible}"); gameObject.SetActive(sorted.isVisible); }
-
Camera’s Culling Mask: Ensure that the camera’s culling mask includes the layer that your objects are on.
-
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.
May 19, 2025 at 3:15 pm #18183::If I’m using the last script solution, items getting deactivated but not activated again.
May 19, 2025 at 3:21 pm #18184::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:
-
Check for Active State: The script checks if the game object is already active before calling
SetActive(true)
, which helps avoid unnecessary calls toSetActive
and can improve performance slightly. -
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.
May 19, 2025 at 3:26 pm #18185::Objects/Enemys are still getting deactivated but not activated again when in camera view.
May 19, 2025 at 3:31 pm #18186::Could you provide more details about your setup? Specifically:
- Are you using the
Sortable
script directly on enemies? - What camera are you using (Main Camera, custom camera)?
- 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?
May 20, 2025 at 1:14 am #18187::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 theUpdate()
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 beisVisible = true
. You will need to use the Hierarchy to confirm if the objects are getting deactivated instead (the name will be greyed out).May 20, 2025 at 1:16 am #18188::Here’s a detailed explanation of the green-highlighted code:
-
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
-
all.Add(sorted);
in theStart()
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
-
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. -
-
AuthorPosts
- You must be logged in to reply to this topic.
Advertisement below: