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:
- Improve your Event Logic
- 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
-
Make 2 Object Groupx and call it something like “ExcludeObjects” and “KeepCulled”.
-
Put all the objects you dont want to be culled in the group “ExcludeObjects”.
-
Put all objects you want to permanently cull when they go off screen in “KeepCulled”.
Step 2
-
Go to the Event Sheet and create an Event Group for our object culling events.
-
Add a **Repeat for each object" event and on the “Object” to repeat for, put “ExcludeObjects”.
-
In Conditions add At the beginning of the scene
-
In Conditions add The boolean variable “ExcludeCull” of “ExcludeObjects” is “False”
-
In Actions add Change Object Boolean “ExcludeCull” of “ExcludeObjects” set to “True”
Step 3
-
Copy / Paste the previouse “Repeat for each object” event to duplicate it.
-
Delete the Condition “At the beginning of the scene” from the duplicated event
-
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.
-
Create new event.
-
In Conditions add At the beginning of the scene
-
In Actions add "Change scene number variable “CullBuffer” set to “A number of your choice, lets use 300 as example”
Final Step - The Script
-
Bellow the last 3 events, add a Java Script event.
-
Delete everything inside the Java Script window.
-
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!