NEW v4.0! [Guide and Resources] How to MASSIVELY improve performance in GDevelop with Object Culling! Making any Project Run Smoothly!

Quick Disclaimer - I dont know how to make extensions so i wont be making one, but if anyone who knows what their doing wants to do it, you have my blessing, iv already done 99% of the work.

What the Current Version 4.0 can do:

  • The primary function is Object Culling

  • Objects whos Bounding Box go outside of the “Buffer area” are deleted

  • Before being deleted Object data is stored for restoring later and to be acessed even while the object is Culled

  • This version will stored all importante Data like, Object Variables, Object Timers, Hidden status, Angle, Object Size, Coordinates (X Y Position) and so on…

  • When any part of the Culled Objects Bounding Box comes inside the “Buffer area”, the object is Recreated and all of its Data is Restored.

  • You can create Object Groups for Excluding Objects from being Culled

  • You can create Object Groups for Objects you dont want to be Recreated. This is usefull for deleting “Bullets” that go off screen.

  • You can set the “Buffer area” with a Variable.

If you want some extra Features added to this, leave it in a Post bellow and ill see what i can do!

To drastically improve performance in GDevelop, theres 2 ways:

  1. Improve your Event Logic
  2. Cull resources not in use

This guide is all about Object Culling

I have done all the heavy lifting for you, all you have to do now is create a couple events and copy paste a script, all super simple and easy!

First let me show you the difference between no object culling and using object culling.

On a scene with a game world size of 100x100 tiles, and tile size of 64x64, with a super imposed grid made of hidden 64x64 tiles.

Essentially (100x100)*2 = 20.000 Objects

With No Object Culling
I get between 30.00 - 35.00ms

Using Object Culling
I get between 4.00 - 5.00ms

So how to get this massive boost in performance?
Its nice and easy! Just follow these steps!

IF you just want to cull objects outside of Camera view with no exceptions, then SKIP to “Step 4”

IF you want to have a group of Objects that WONT get Culled, then follow All Steps.

FIRST Lets make the Object Groups that we will be using to either Exclude object from being culled or to Cull permanently, the permanent group is good for objects like “Bullets” that fly off screen.

The Regular object culling used by the script dosent require a group, it will simply cull objects outside of the buffer zone and recreate them once they come back inside the buffer zone.

For better results: Create the following events at the bottom of your Event Sheet.

Step 1

  1. Make 2 Object Groupx and call it something like “ExcludeObjects” and “KeepCulled”.

  2. Put all the objects you dont want to be culled in the group “ExcludeObjects”.

  3. Put all objects you want to permanently cull when they go off screen in “KeepCulled”.

Step 2

  1. Go to the Event Sheet and create an Event Group for our object culling events.

  2. Add a **Repeat for each object" event and on the “Object” to repeat for, put “ExcludeObjects”.

  3. In Conditions add At the beginning of the scene

  4. In Conditions add The boolean variable “ExcludeCull” of “ExcludeObjects” is “False”

  5. In Actions add Change Object Boolean “ExcludeCull” of “ExcludeObjects” set to “True”

Step 3

  1. Copy / Paste the previouse “Repeat for each object” event to duplicate it.

  2. Delete the Condition “At the beginning of the scene” from the duplicated event

  3. Now do Copy / Paste the new event and change the object name from “ExcludeObjects” to “KeepCulled” and the variable name from “ExcludeCull” to simply “Cull”.

Step 4
This step is where you set the buffer for your object culling, the variable “CullBuffer” measures in pixels how far from the edge of the camera objects can be before they get deleted of recreated.

  1. Create new event.

  2. In Conditions add At the beginning of the scene

  3. In Actions add "Change scene number variable “CullBuffer” set to “A number of your choice, lets use 300 as example”

Final Step - The Script

  1. Bellow the last 3 events, add a Java Script event.

  2. Delete everything inside the Java Script window.

  3. Copy / Paste the following script into the Java Script window:

Java Script for culling objects outside of view

NEW VERSION 4.0 - This is the new shinny version that has been improved for performance and can store and restoring object data like Size, Variables, Object Timers, Hidden status and so on…

// Initialize the deletedObjects object if it doesn't exist
if (!runtimeScene.deletedObjects) {
    runtimeScene.deletedObjects = {};
}

// Get the camera position and size once
const cameraLayer = runtimeScene.getLayer("");
const cameraX = cameraLayer.getCameraX();
const cameraY = cameraLayer.getCameraY();
const cameraWidth = cameraLayer.getCameraWidth();
const cameraHeight = cameraLayer.getCameraHeight();

// Get the value of the CullBuffer scene variable once
const cullBuffer = runtimeScene.getVariables().get("CullBuffer").getAsNumber();

// Calculate the boundaries for object culling based on the CullBuffer variable once
const leftBoundary = cameraX - cameraWidth / 2 - cullBuffer;
const rightBoundary = cameraX + cameraWidth / 2 + cullBuffer;
const topBoundary = cameraY - cameraHeight / 2 - cullBuffer;
const bottomBoundary = cameraY + cameraHeight / 2 + cullBuffer;

// Get all instances of objects in the scene
const allObjectsInstances = runtimeScene.getAdhocListOfAllInstances();

// Arrays to hold objects for deletion and recreation
const objectsToDelete = [];
const objectsToStoreAndDelete = [];

// Process a subset of objects each frame to avoid spikes in CPU usage
const maxObjectsPerFrame = 50; // Adjust this number based on performance needs
const startIndex = runtimeScene.getVariables().has("CullStartIndex")
    ? runtimeScene.getVariables().get("CullStartIndex").getAsNumber()
    : 0;
const endIndex = Math.min(startIndex + maxObjectsPerFrame, allObjectsInstances.length);

runtimeScene.getVariables().get("CullStartIndex").setNumber(endIndex === allObjectsInstances.length ? 0 : endIndex);

// Get the current scene time
const currentTime = runtimeScene.getTimeManager().getTimeFromStart() / 1000; // Time in seconds

// Iterate over a subset of objects in the scene
for (let i = startIndex; i < endIndex; i++) {
    const object = allObjectsInstances[i];
    const objectName = object.getName();
    const objectLayer = object.getLayer();

    // Only process objects on the base layer
    if (objectLayer !== "") continue;

    const variables = object.getVariables();

    // Check if the object has a variable named "ExcludeCull" and its value is true
    if (variables.has("ExcludeCull") && variables.get("ExcludeCull").getAsBoolean()) {
        continue; // Skip culling for this object if "ExcludeCull" is true
    }

    // Get the bounding box of the object
    const aabb = object.getAABB();
    const objectLeft = aabb.min[0];
    const objectRight = aabb.max[0];
    const objectTop = aabb.min[1];
    const objectBottom = aabb.max[1];

    // Check if the object is completely outside the culling boundaries
    if (objectRight < leftBoundary || objectLeft > rightBoundary || objectBottom < topBoundary || objectTop > bottomBoundary) {
        // Check if the object has a variable named "Cull" and its value is true
        if (variables.has("Cull") && variables.get("Cull").getAsBoolean()) {
            objectsToDelete.push(object); // Queue the object for direct deletion
        } else {
            // Store the position, name, properties, timers, and culling time to recreate later
            if (!runtimeScene.deletedObjects[objectName]) {
                runtimeScene.deletedObjects[objectName] = [];
            }

            // Common properties for all objects
            const objectProperties = {
                x: object.getX(),
                y: object.getY(),
                zOrder: object.getZOrder(),
                angle: object.getAngle(),
                layer: objectLayer,
                hidden: object.isHidden(), // Store hidden state
                variables: {},
                timers: {}, // Object to store timer names and elapsed time
                cullTime: currentTime // Store the current time when the object is culled
            };

            // Store object variables
            const objectVariablesList = variables._variables.items;
            for (const variableName in objectVariablesList) {
                if (objectVariablesList.hasOwnProperty(variableName)) {
                    objectProperties.variables[variableName] = objectVariablesList[variableName].getAsString();
                }
            }

            // Check for and store optional properties
            if (object.getWidth) {
                objectProperties.width = object.getWidth();
            }
            if (object.getHeight) {
                objectProperties.height = object.getHeight();
            }
            if (object.getOpacity) {
                objectProperties.opacity = object.getOpacity();
            }

            // Store object timers directly from _timers
            if (object._timers && object._timers.items) {
                const timerItems = object._timers.items;
                for (const timerName in timerItems) {
                    if (timerItems.hasOwnProperty(timerName)) {
                        objectProperties.timers[timerName] = timerItems[timerName].getTime() / 1000.0; // Store the timer value in seconds
                        console.log(`Stored timer ${timerName} for object ${objectName} with time ${objectProperties.timers[timerName]}`);
                    }
                }
            } else {
                console.log(`Object ${objectName} does not support timers or has no timers.`);
            }

            runtimeScene.deletedObjects[objectName].push(objectProperties);

            // Queue the object for deletion and storage
            objectsToStoreAndDelete.push(object);
        }
    }
}

// Perform deletion of objects that are fully culled
for (let i = 0; i < objectsToDelete.length; i++) {
    objectsToDelete[i].deleteFromScene(runtimeScene);
}

// Perform deletion of objects that are stored and then deleted
for (let i = 0; i < objectsToStoreAndDelete.length; i++) {
    objectsToStoreAndDelete[i].deleteFromScene(runtimeScene);
}

// Get the current scene time for restoring objects
const restoreTime = runtimeScene.getTimeManager().getTimeFromStart() / 1000; // Time in seconds

// Recreate objects within the boundaries
const deletedObjects = runtimeScene.deletedObjects;
for (let objectName in deletedObjects) {
    const objectList = deletedObjects[objectName];
    for (let i = objectList.length - 1; i >= 0; i--) {
        const data = objectList[i];
        const objectLeft = data.x;
        const objectRight = data.x + (data.width || 0);
        const objectTop = data.y;
        const objectBottom = data.y + (data.height || 0);

        // Check if the position is within the recreation boundaries
        if (objectRight >= leftBoundary && objectLeft <= rightBoundary && objectBottom >= topBoundary && objectTop <= bottomBoundary) {
            // Calculate the elapsed time since culling
            const elapsedTime = restoreTime - data.cullTime;

            // Recreate the object
            const newObject = runtimeScene.createObject(objectName);
            newObject.setPosition(data.x, data.y);
            newObject.setZOrder(data.zOrder);
            newObject.setAngle(data.angle);
            newObject.setLayer(data.layer);

            // Restore optional properties
            if (data.width !== undefined && newObject.setWidth) {
                newObject.setWidth(data.width);
            }
            if (data.height !== undefined && newObject.setHeight) {
                newObject.setHeight(data.height);
            }
            if (data.opacity !== undefined && newObject.setOpacity) {
                newObject.setOpacity(data.opacity);
            }

            // Restore hidden state
            if (data.hidden !== undefined) {
                newObject.hide(data.hidden);
            }

            // Restore variables
            const targetVariables = newObject.getVariables();
            const sourceVariables = data.variables;
            for (const variableName in sourceVariables) {
                if (sourceVariables.hasOwnProperty(variableName)) {
                    targetVariables.get(variableName).setString(sourceVariables[variableName]);
                }
            }

            // Restore timers and add elapsed time
            const objectTimers = data.timers;
            for (const timerName in objectTimers) {
                const timerValue = objectTimers[timerName];
                newObject.resetTimer(timerName); // Reset the timer with the same name
                newObject._timers.get(timerName).setTime((timerValue + elapsedTime) * 1000); // Add elapsed time to the timer value
                console.log(`Restored timer ${timerName} for object ${objectName} with time ${timerValue + elapsedTime} seconds`);
            }

            // Remove from the list after recreation
            objectList.splice(i, 1);
        }
    }
}

// Cleanup empty lists to free memory
for (let objectName in deletedObjects) {
    if (deletedObjects[objectName].length === 0) {
        delete deletedObjects[objectName];
    }
}

OLD VERSION - This old version dosent do anything facy like keep data or restore variables, it just directly culls objects that are outiside the buffer zone and then recreates them when they get back inside.

This version can still exclude objects from being culled if they have a boolean variable with the name ExcludeCull and that its set to True.

// Initialize the deletedObjects object if it doesn't exist
if (!runtimeScene.deletedObjects) {
    runtimeScene.deletedObjects = {};
}

// Get the camera position and size
const cameraX = runtimeScene.getLayer("").getCameraX();
const cameraY = runtimeScene.getLayer("").getCameraY();
const cameraWidth = runtimeScene.getLayer("").getCameraWidth();
const cameraHeight = runtimeScene.getLayer("").getCameraHeight();

// Get the value of the CullBuffer scene variable
const cullBuffer = runtimeScene.getVariables().get("CullBuffer").getAsNumber();

// Calculate the boundaries for object culling based on the CullBuffer variable
const leftBoundary = cameraX - cameraWidth / 2 - cullBuffer;
const rightBoundary = cameraX + cameraWidth / 2 + cullBuffer;
const topBoundary = cameraY - cameraHeight / 2 - cullBuffer;
const bottomBoundary = cameraY + cameraHeight / 2 + cullBuffer;

// Get all instances of objects in the scene
const allObjectsInstances = runtimeScene.getAdhocListOfAllInstances();

// Arrays to hold objects for deletion and recreation
const objectsToDelete = [];
const objectsToRecreate = [];

// Iterate over all objects in the scene
for (let i = 0; i < allObjectsInstances.length; i++) {
    const object = allObjectsInstances[i];
    const objectName = object.getName();
    const objectX = object.getDrawableX();
    const objectY = object.getDrawableY();
    const objectLayer = object.getLayer();

    // Only process objects on the base layer
    if (objectLayer === "") {
        
        // Check if the object has a variable named "ExcludeCull" and its value is true
        const variables = object.getVariables();
        const excludeCullVariable = variables.has("ExcludeCull") ? variables.get("ExcludeCull").getAsBoolean() : false;
        if (excludeCullVariable) {
            continue; // Skip culling for this object if "ExcludeCull" is true
        }

        // Check if the object is outside the culling boundaries
        if (objectX < leftBoundary || objectX > rightBoundary || objectY < topBoundary || objectY > bottomBoundary) {
            // Store the position and name to recreate later
            if (!runtimeScene.deletedObjects[objectName]) {
                runtimeScene.deletedObjects[objectName] = [];
            }
            runtimeScene.deletedObjects[objectName].push({
                x: objectX,
                y: objectY
            });

            // Queue the object for deletion
            objectsToDelete.push(object);
        }
    }
}

// Perform deletion of objects
for (let i = 0; i < objectsToDelete.length; i++) {
    objectsToDelete[i].deleteFromScene(runtimeScene);
}

// Recreate objects within the boundaries
for (let objectName in runtimeScene.deletedObjects) {
    runtimeScene.deletedObjects[objectName] = runtimeScene.deletedObjects[objectName].filter(data => {
        const objectX = data.x;
        const objectY = data.y;

        // Check if the position is within the recreation boundaries
        if (objectX >= leftBoundary && objectX <= rightBoundary && objectY >= topBoundary && objectY <= bottomBoundary) {
            // Recreate the object
            const newObject = runtimeScene.createObject(objectName);
            newObject.setPosition(objectX, objectY);

            return false; // Remove from deletedObjects after recreation
        }

        return true; // Keep in deletedObjects if not recreated
    });
}

Your all done!! The whole thing should look like this!

This works flawlessly and has been imrpoved and tested a few times for performance.

Id love it if the devs could bake this in to GDevelop, but for now this is simple enough to add to any project!

I made sure that this was flexible enough to be used by anyone in any project!!

  • Just be sure that the boolean variables “ExcludeCull”, “Cull” and scene number variabe “CullBuffer” are spell correctly, the Script requires them to have those names to work!
9 Likes

Not to pat myself on the back or anything, but this stuff is seriously good and helpful.

It opens up GDevelop completely, removes all size constrictions, improves performance for older machines, etc…

I should get a freaking medal for this lol …or at least a pretzel :smiley:

2 Likes

Although this can be seen as extremely arrogant, I totally understand the feeling of elation at having reduced the processing time by such a large margin, and you have every right to feel euphoric It is so, so satisfying to have slaved over a problem for ages, and to finally conquer it. That’s a sign of a true geek :smiley: (that is a compliment).
.

1 Like

I was just joking :slight_smile: Hence the Pretzel :stuck_out_tongue:

Iv been working on this for days… i dont know the first thing about scripting or how GDevelop works, so i did a bunch of reading on the documentation and fed a bunch of it into ChatGPT 4.0, then tested it, gave it feedback, adjust, and repeat…

Im exhausted and my brain is tapping out… BUT I DID IT!!! Booya!! It all works smooth as butter!

I tried it on every project i have, its all massive improvements!

Im so happy <3

Im taking the big win :smiley:

Also, thanks for the compliment! I do love geeking out over this stuff :slight_smile:

1 Like

This looks amazing.

I did a very similar thing for an old game model, but not in code, just with events.

It kept a registry of each object with it’s position and, since the objects data kept changing even when they were off-screen (and that’s why I used to keep all the object information in arrays of structures instead of object variables), it automatically created and destroyed the objects when they enter and leave the screen (but all the data of the object remained in existence without the object itself… this allowed me to make complex entities like enemies or NPCs to keep interacting with the world outside of the screen without an existent object).

I don’t remember the statistics (this was years ago), but I remember it was a lot of improvement in performance.

At some point I thought of port it to JavaScript, but I became lazy, and since some news about sprite handling improvements were made in previous versions I just assume the implementation of object culling without affecting object variables was the obviously taken route inside the engine.

It seems it wasn’t.

Your version of this improvement seems much more direct and efficient, and I can build my object data registry around it.

I wonder if it’s possible to improve your version by stopping the display of the sprite instead of deleting and recreating the objects, keeping the object data and variables intact.

Congratulations, this is an amazing work.

1 Like

Oh that is a good point with the keeping the object data!

I might try to make that mod now that i have ChatGPT tuned in on how GDevelop works… for the most part.

Right now i need a little break, this broke my brain haha

…but ill surely add it in considering i want to do things like planting trees and letting them grow with time, so ill have to work out how to do that with object culling.

@erdo
EDIT: You can also just add to this with your own events and programming. That was a thinking too inside the box moment for me.

I was trying to make object groups work in the script, but it kept giving me an error… and then i had the idea to handle Object Groups in GDevelop like normal, using events, and instead run a check for a variable in the script!

That worked instantly!! :smiley:

Think you can do the same, you can create your structure system keeping the object info, and using the script to simply handle the culling part.

1 Like

Honestly yeah, that’s exactly what I’m going to do, I want to build my object data registry with events around your code (if this happens I’ll give you all the credit you deserve).

Now we have the Spine-GDevelop implementation, I’ve been playing with this idea of creating a 2D game with cut-out style characters and objects made in Spine.

Since every part of the composed Spine animated sprite is rendered as a separate sprite in runtime (right?), this should be the way to keep a decent performance once the game grows.

I would love to see all of these systems implemented in the engine some day.

1 Like

I see that you have made great progress, congratulations! I hope it can be useful to me too. For me it is very important to improve performance, and it is something I have been talking about for a long time. What is not clear to me, I believed that Gdevelop did not render objects off the screen, does it really? Or is it not done correctly?

1 Like

You dont have to give me credit bud, i made the guide to share what i found with people so that it may help them.

Im actually gonna try and make a second version of the script and add it separately so people can choose.

One version is gonna be the simple one we have now.

The second ill try to make, will store object variables before deleting them and add them in after recreating.

Ill keep them seperate so people can pick and choose :slight_smile:

1 Like

Iv already had a couple people test this, and someone who has a low end machine whent from running my 2d minecraft style game at 20 - 30 fps, to hitting the 60fps, on an old i3 with 4gb ram.

Also, yes GDevelop does stop rendering objects off screen, but not rendering isnt the same as deleting.

When an object stops being redered, that just means it stops being seen.

So its a relieve on the Render, but the engine is still handling everything to do with that object, its variables, its position, interactions, collision, behaviors, everything really… you just cant see it, thats the difference.

When you cull an object, or delete it, you remove the object completely, no more calculations, no more behaviors, no more anything, its gone.

So if you dont need an object to exist all the time, cutting it off completely from the equation, massively increases performance.

1 Like

Oh sure, now I understand. It makes a lot of sense haha. I think I won’t be able to apply it myself because what I’m working on does require objects to interact even without them being seen. Even so, I hope to be able to apply it in the future. It is a good contribution to the engine that you are making. Good job!

1 Like

New version now works again and is better than ever!! :smiley:

1 Like

GJ…
just a question.
Since the cull is binded by camera…
it will works with the follow camera extensions we have rite now…eg what happen when the camera is forced to follow an object in zoom mode
just to be sure before implementing it

Yes, it works with follow camera, smooth camera, all that jazz.

It will only cull objects from the base layer tho, thats something to keep in mind.

If you need to cull objects from other layers, i can try to add the mod to choose the layer.

at the moment base layer is enugh…im gonna try it when i have time

1 Like

Let me know if you have any issues with it or want any mod to it! :slight_smile:

Got a new and much improved Script that ill be adding in soon!!
The new script even handles object variables!!

But for now, check this out, im doing some stress testing on it and…

Objects are 64x64 and the resolution 1280x720
The culling buffer is set to 300

On a game world with 15.000 Objects living in the scene

On a game world with 30.000 Objects living in the scene

On a game world with 120.000 Objects living in the scene

  • The script is running at 4.00ms
  • This is a completely un optimized way of using it, at this world size, we should be dividing it into chunks to cull first and only then apply our script
  • This is only for stress testing

1 Like

HUMMnnn… As just a small update, i accidentally made something even better than i thought lol :smiley:

So as it turns out when i asked for data storage to be implemented it made it able for us to check objects data, like position, even tho the object is culled.

So you can still use an object position, even tho its not there, and because its not actually there, it dosent suck up massive amounts of performance!!!

Dude, this is seriously gonna change GDevelop, no exageration.

I honestly have no idea why this isnt a thing already.

Ill be posting the new script soon! I just want to add one more thing to it!!

2 Likes

I asked ChatGPT to try and explain what was going on to me, this is what he said, in case anyone is interested how the accidental awesomeness happened lol

The reply:

I’m glad to hear that the accidental change is working well for your project! Based on the description of your scenario, I can explain what’s likely happening in the script and why your world generation is still functioning correctly even after the culling.

Explanation:

  1. Object Culling vs. Object Data Storage:

    • When the script culls an object, it deletes the object instance from the scene to save resources. However, before deleting the object, the script saves the object’s data, including its position and variables, in runtimeScene.deletedObjects.
    • This means that although the object instance is removed from the scene, its essential information is still stored in memory.
  2. Recreation Logic:

    • The stored data in runtimeScene.deletedObjects allows the script to recreate the object when it comes back within the culling boundaries.
    • Since the object’s data (including its variables) is stored and restored, the object can continue to serve its purpose when recreated, maintaining any necessary state or behavior.
  3. Continued Functionality of the World Generator:

    • It appears that your world generation logic relies on the data or state of the object, rather than the presence of the object instance itself.
    • Even if the object is culled, as long as the object’s data is retained and can be restored, the world generator can still function based on that stored data.

Key Points in the Script:

  • Data Storage:

    // Store the position, name, and variables to recreate later
    if (!runtimeScene.deletedObjects[objectName]) {
        runtimeScene.deletedObjects[objectName] = [];
    }
    const objectVariables = {};
    const objectVariablesList = variables._variables.items;
    for (const variableName in objectVariablesList) {
        if (objectVariablesList.hasOwnProperty(variableName)) {
            objectVariables[variableName] = objectVariablesList[variableName].getAsString();
        }
    }
    runtimeScene.deletedObjects[objectName].push({
        x: objectX,
        y: objectY,
        variables: objectVariables
    });
    
  • Data Restoration:

    // Restore variables
    const targetVariables = newObject.getVariables();
    const sourceVariables = data.variables;
    for (const variableName in sourceVariables) {
        if (sourceVariables.hasOwnProperty(variableName)) {
            targetVariables.get(variableName).setString(sourceVariables[variableName]);
        }
    }
    

Summary:

The script ensures that all necessary data for objects is stored before they are culled. This stored data includes positions and variables, which are crucial for maintaining the object’s state. When objects are outside the camera view and thus culled, the data is preserved. When objects come back within the view, they are recreated with their original state, including all variables.

This means that your world generation logic can still rely on the stored data of the culled objects. Since the objects’ essential information is retained and recreated as needed, the world generation process continues to function correctly even if the objects themselves are temporarily removed from the scene.

Conclusion:

In essence, the script’s data storage mechanism before culling and its data restoration mechanism upon recreation are why your world generation continues to work seamlessly. This approach ensures that the logical state of your game world remains consistent, even if certain objects are not actively present in the scene.

4 Likes

Now that i noticed i’m already using this “culling” method (since it is basically a delete and recreate) but by rooms.
I just have an event who check if the player is in the room and delete everything outside…I wonder which one is simplier.
Stil GL whit yur script