GameMaker Depth Sorting Method for GMS2 Objects & Sprites

K

kevins_office

Guest
Object depth sorting methodology has changed between GM:S 1.4 and GMS2. In trying to find a solution that would work for my game i saw a lot of questions online without many answers. YYG has discouraged continuing to use the old "depth = -y;" method in GMS2 with the addition of layers. Posting this is my way of giving back to the community, to help newbies, since i received the help i needed from the community. I did get started with ideas and inspiration from the following sources...
FriendlyCosmonaut Youtube Tutorials:

YYG Dungeon Demo Included in GMS2

Z-Tilting Shader Depth Sorting by Ariak:
https://www.yoyogames.com/blog/458/z-tilting-shader-based-2-5d-depth-sorting
(Although this method isn't for me, someone else might find it useful.)

And this post by Ariak discussing the performance of different methods.
https://forum.yoyogames.com/index.p...rity-list-nested-list-grid-binary-list.13425/

I also asked for suggestions on discord /r/GameMaker
I did like the basic concept of putting objects, that you want to have depth, into a sorted list and manually drawing them to the screen. I also found Ariak's suggestion of a binary list to be the fastest sorting method. That is when you bit shift two values (Instance Id, y) into a single number stored in a ds_list.

After coding my own sorting method using the concepts discussed above i had an issue in that it only works for objects. Which is great for sorting characters, but what about landscape, Trees, rocks, buildings. From what i've seen many developers ended up making their "trees" into objects so it could be depth sorted by the same code.

I felt performance would take a hit after having too many objects. Since GMS2 has asset sprites and tilemaps for the exact purpose of reducing object over head, i wanted my trees (and other things) to be sprites instead of objects. I wanted a method that could depth sort objects and sprites.

Im also lazy and wanted a one and done solution, i made a controller object that once dropped on a layer, would handle all sorting without any setup work for the objects or sprites, no adding them to a list, no making them all children of a special parent object, no adding sorting code to their events, etc.

The process for the method i came up with is:

  • Make a layer that you will put all objects and sprites on that you want sorted. Mark this layer invisible. Making it invisible is to prevent their draw events from automatically running so the code can manually call their draw events in the sorted order.
  • Add the Depth Controller object to a different layer, any layer. Bear in mind this layer is the depth all sorted objects and sprites will be drawn at, in relation to the other layers in your project. The only code change you need to do is define the name of the invisible layer in the Create Event of the Controller object.
  • Now just add all objects and sprites you want sorted to the invisible layer, no need to do anything specifically to them.

That is all there is to it. I will post the Controller Object code below. And discuss the performance i was able to get out of it. I only included Objects and Sprites in the sorting, however it can easily be modified to include tilemaps, backgrounds and particle systems.

I am also asking the community, if anyone can improve upon performance please post an improved version as it will help everyone.

Yes, it is true that using repeat loops are faster than for loops. And ds_list including sorting is faster than ds_grids, ds_maps and ds_priority queues which makes Ariak's binary list the best option IMO.

In my project i do not resize sprites using xscale or yscale so i did not add code to account for that as it would slow down the loops. If your project does change scale on sprites then you should add that in the Pre-Draw Event where it computes width and height (ie. width = sprite width * xscale). Or to speed the code up, hard code a fixed width and height number that would encompass the maximum size of your sprites. Then time wont be used computing it accurately for each sprite.

Code:
/// @desc CREATE EVENT

// Create Instance Variables
depthList = ds_list_create();
sortingLayer = layer_get_id("put_invisible_layer_name_here");
Code:
/// @desc PRE-DRAW EVENT

// Clear Sort List
ds_list_clear(depthList);

// Define Camera View Coordinates
// ***Note: Change view_camera[0] if your project uses something different
var cam_x1 = camera_get_view_x(view_camera[0]);
var cam_y1 = camera_get_view_y(view_camera[0]);
var cam_x2 = cam_x1 + camera_get_view_width(view_camera[0]);
var cam_y2 = cam_y1 + camera_get_view_height(view_camera[0]);

// Define Local Variables
var loop = 0;
var xx = 0;
var yy = 0;
var width = 0;
var height = 0;

// Get All Elements On Invisible Layer And Loop
var elements = layer_get_all_elements(sortingLayer);
repeat (array_length_1d(elements)) {
   xx = undefined;
   // Object Instances
   if (layer_get_element_type(elements[loop]) == layerelementtype_instance) {
       var thisInst = layer_instance_get_instance(elements[loop]);
       xx = thisInst.x;
       yy = thisInst.y;
       width = thisInst.sprite_width;
       height = thisInst.sprite_height;
   }
   // Asset Layer Sprites
   if (layer_get_element_type(elements[loop]) == layerelementtype_sprite) {
       xx = layer_sprite_get_x(elements[loop]);
       yy = layer_sprite_get_y(elements[loop]);
       width = sprite_get_width(layer_sprite_get_sprite(elements[loop]));
       height = sprite_get_height(layer_sprite_get_sprite(elements[loop]));
   }
   // If Within Screen View Add To Sorting List
   if (xx != undefined && xx + width >= cam_x1 && xx - width <= cam_x2 && yy + height >= cam_y1 && yy - height <= cam_y2) {
       ds_list_add(depthList, (elements[loop] | yy << 32) );
   }
   loop++;
}

// Sort List According to Y value
ds_list_sort(depthList, true);
Code:
/// @desc DRAW EVENT

// If you need to add this to BEGIN DRAW EVENT use event_perform(ev_draw, ev_draw_begin);
// If you need to add this to END DRAW EVENT use event_perform(ev_draw, ev_draw_end);

var loop = 0;
repeat (ds_list_size(depthList)) {
   var element_id = depthList[| loop] &$ffffffff;
   if (layer_get_element_type(element_id) == layerelementtype_instance) {
       with (layer_instance_get_instance(element_id)) event_perform(ev_draw, 0);
   }
   if (layer_get_element_type(element_id) == layerelementtype_sprite) {
       draw_sprite(layer_sprite_get_sprite(element_id), -1, layer_sprite_get_x(element_id), layer_sprite_get_y(element_id));
   }
   loop++;
}
Code:
/// @desc CLEAN UP EVENT

ds_list_destroy(depthList);
depthList = undefined;
Now as for performance. I checked times using get_timer() which is in micro seconds. There are 1 million micro seconds in a full Second. When you check the value of delta_time in your project it will report how many micro seconds your CPU is doing per step of your game. On my PC i get around 16,000µs (micro seconds) per step running at 60FPS. Or around 8,000µs per step running at 120FPS.

Since i only sort objects and sprites that are within camera view, i only tested for the amount of objects/sprites in view and didn't concern myself with objects outside of the camera view. All though having thousands of objects/sprites outside of the camera view will add a tiny tiny bit more processing time as it still has to check if they are in view or not, but i felt it was insignificant enough to worry about.

I put 1,000 items on the layer within camera view, 500 objects and 500 sprites.
The sorting being done in Pre-Draw took on average 1,250µs.
The loop to draw those 1,000 items took on average 95µs.
This method used about 1,345µs to depth sort 1,000 items.
Which is 1.345ms (milliseconds) or about 8% of a step at 60FPS.

I also tested 2,000 items on the layer within camera view. 1,000 objects and 1,000 sprites.
Pre-sort took 4,200µs.
Draw loop took 200µs.
Thats 4,400µs || 4.4ms || 27% of a step at 60FPS.

My take away is for most average GMS2 games this method should work great. However if you have thousands of objects / sprites on screen then it could be considered slow. Even though i had a solid 120FPS with 2,000 objects, that's still 54% of the step time spent on just sorting. Doesn't leave a lot of time left for game logic code.

Id love to hear if anyone can improve on this.
 
Last edited by a moderator:
L

Lukas Sebastian

Guest
Thanks for sharing this!

I was wondering, what type is the invisible layer used with your code? An asset layer? If so, are the sprites to be drawn added separately from the object they represent?

I guess I could figure it out by trial and error (or reading the manual), but maybe there are others here pondering the same thing.
 

CMAllen

Member
What you need or want is some way to store the positions of all these depth-sorted draw() calls that aren't associated with objects. For that, you need the code that handles drawing your depth-sorted list to be able to differentiate between an object drawing routine and a simpler sprite drawing routine. Both get inserted into the same list and are depth sorted in the same way, but where one version uses an instance's id to draw what it needs, the other version needs to store the information critical to drawing the correct sprite itself (ie, sprite_index, image_index, x, y, image_xscale, image_yscale, image_rotation, image_alpha).

I'm not sure if a binary list can easily hold the requisite information to accomplish that. I say that because if you were to store the sprite information in an array and pass the array's id into the depth_list in place of the normal object id, I don't know if the array can have an identical id value to the instance_id of an object (which you'd need to test determine if you're drawing an object or a sprite). If any array id can have the same value as an active instance_id, obviously you can't do a simple instance_exists() check to switch between drawing modes. If anyone can answer the lingering question of instance_id vs array id, I think this solution might be the most flexible, powerful option available.
 
K

kevins_office

Guest
Thanks for sharing this!
I was wondering, what type is the invisible layer used with your code? An asset layer? If so, are the sprites to be drawn added separately from the object they represent?
I guess I could figure it out by trial and error (or reading the manual), but maybe there are others here pondering the same thing.
Layer types only exist in the IDE for Drag n Drop, such as a tileset requires different information than an object does. Once the game starts a layer is a layer to GML, there are no different types. Look at `layer_create()` and you will see there is no type declaration. When using code you can add any combination of objects, spites, tilemaps, etc to the same layer.

I personally just used the instance layer in the IDE to create a layer and marked it invisible with nothing on it, then placed my objects and sprites on that layer with code.
You could also just create the invisible layer with code instead of using the IDE to add the layer.


are the sprites to be drawn added separately from the object they represent?
I am not sure if there is a misunderstanding. This is not for drawing the sprites of the objects you want sorted. The objects have their own sprites as part of the object properties, i am not doing anything with those sprites as the objects draw them themselves. The sprites i am talking about here are sprites that you would put on an asset layer. Sprites that do not represent any object.

Say in your game you have static things in your room that never move, are nothing more than cosmetics, like trees. Instead of making your trees objects which add more to the games overhead, you can use a sprite instead of an object/instance.
 
Last edited by a moderator:
K

kevins_office

Guest
What you need or want is some way to store the positions of all these depth-sorted draw() calls that aren't associated with objects. For that, you need the code that handles drawing your depth-sorted list to be able to differentiate between an object drawing routine and a simpler sprite drawing routine. Both get inserted into the same list and are depth sorted in the same way, but where one version uses an instance's id to draw what it needs, the other version needs to store the information critical to drawing the correct sprite itself (ie, sprite_index, image_index, x, y, image_xscale, image_yscale, image_rotation, image_alpha).
This code example *IS* a way to sort both objects and sprites together in the same list. That is the whole point of this post. You can differentiate between the types with layer_get_element_type(). And you do not need to do anything to store information needed to draw the sprite because you have the actual sprite on the layer. It is its own storage of its own information.


I'm not sure if a binary list can easily hold the requisite information to accomplish that. I say that because if you were to store the sprite information in an array and pass the array's id into the depth_list in place of the normal object id, I don't know if the array can have an identical id value to the instance_id of an object (which you'd need to test determine if you're drawing an object or a sprite). If any array id can have the same value as an active instance_id, obviously you can't do a simple instance_exists() check to switch between drawing modes. If anyone can answer the lingering question of instance_id vs array id, I think this solution might be the most flexible, powerful option available.
The above code is doing everything you are saying would need to be figured out. Yes you can store the element ID (and Y value) in a binary list for both sprites and objects. No you do not need to store anything else in the list or put arrays of information in the list. The method of getting whats on a layer uses element ID's not instance ID's.

I do not see any added value of fetching all of the information available on the layer, putting it into arrays, storing those arrays in a list, sorting that list, then using those arrays to draw the objects/sprites vs what is already being done which is just sorting the ID's and then using the information already stored on the layer to draw those objects/sprites.

If you have working code storing everything in arrays in a sorted list that is faster then the above method then please share it with us.
 

CMAllen

Member
This code example *IS* a way to sort both objects and sprites together in the same list. That is the whole point of this post. You can differentiate between the types with layer_get_element_type(). And you do not need to do anything to store information needed to draw the sprite because you have the actual sprite on the layer. It is its own storage of its own information.




The above code is doing everything you are saying would need to be figured out. Yes you can store the element ID (and Y value) in a binary list for both sprites and objects. No you do not need to store anything else in the list or put arrays of information in the list. The method of getting whats on a layer uses element ID's not instance ID's.

I do not see any added value of fetching all of the information available on the layer, putting it into arrays, storing those arrays in a list, sorting that list, then using those arrays to draw the objects/sprites vs what is already being done which is just sorting the ID's and then using the information already stored on the layer to draw those objects/sprites.

If you have working code storing everything in arrays in a sorted list that is faster then the above method then please share it with us.
It only works under the assumption that everything you want to draw is either an object or an asset. Fine for most cases, but not quite as definitive as you're suggesting (object can't keep track of and control drawing multiple sprites at different sorting depths). In addition, in your code, you're doing a double if() check when the results of the first if() can only be one or the other. The sorted depth list should only contain objects or assets. If it's not an object then it has to be an asset (or vice versa). So why check twice?

Don't take any of what I said to be a negative. This is an ingenuous way to do exactly what you've suggested. And I hope further optimizations and even improved flexibility make it even better.
 
K

kevins_office

Guest
It only works under the assumption that everything you want to draw is either an object or an asset. Fine for most cases, but not quite as definitive as you're suggesting
Not true, i mentioned above that "I only included Objects and Sprites in the sorting, however it can easily be modified to include tilemaps, backgrounds and particle systems." Since my project is only sorting objects and sprites i only included if()'s for those two (for performance) but you can add additional if()'s if you want to draw more than just sprites and objects.


In addition, in your code, you're doing a double if() check when the results of the first if() can only be one or the other.
Yes i am using a separate if() to check each type because there could be other types on the layer besides sprite or object. Sure, for your project if you guarantee that layer will only ever have one of those two things you could change one of the if()'s to an else instead. I left the code with the double if() to make it more readable and easily adjustable for newbies who might want to add additional elements like tilemaps or particles.


And I hope further optimizations and even improved flexibility make it even better.
I believe it already has complete flexibility and i am also hoping someone can improve on the performance.
 

Simon Gust

Member
I don't have a better way to depth-sort but I like looking at code and figuring out
which style is the fastest. These are the optimizations I've come up with:
Code:
/// @desc PRE-DRAW EVENT
// Clear Sort List
ds_list_clear(depthList);

// Define Camera View Coordinates
// ***Note: Change view_camera[0] if your project uses something different
var cam_x1 = camera_get_view_x(view_camera[0]);
var cam_y1 = camera_get_view_y(view_camera[0]);
var cam_x2 = cam_x1 + camera_get_view_width(view_camera[0]);
var cam_y2 = cam_y1 + camera_get_view_height(view_camera[0]);

// Define Local Variables
var loop = 0;

// Get All Elements On Invisible Layer And Loop
var elements = layer_get_all_elements(sortingLayer);
repeat (array_length_1d(elements))
{
    // define variables
    var xx = undefined;
    var yy = 0;
    var width = 0;
    var height = 0;
    var layer = elements[loop++];

    // check layer
    switch (layer_get_element_type(layer))
    {
        case layerelementtype_instance:
        {
            var thisInst = layer_instance_get_instance(layer);
            xx = thisInst.x;
            yy = thisInst.y;
            width = thisInst.sprite_width;
            height = thisInst.sprite_height;
        }
        break;
        case layerelementtype_sprite:
        {
            xx = layer_sprite_get_x(layer);
            yy = layer_sprite_get_y(layer);
            width = sprite_get_width(layer_sprite_get_sprite(layer));
            height = sprite_get_height(layer_sprite_get_sprite(layer));
        }
        break;
    }
    
    // add to list
    if (xx != undefined)
    {
        if (rectangle_in_rectangle(xx-width, yy-height, xx+width, yy+height, cam_x1, cam_y1, cam_x2, cam_y2))
        {
            ds_list_add(depthList, (layer | yy << 32));
        }
    }
}

// Sort List According to Y value
ds_list_sort(depthList, true);
They are faster for me, test if they're faster for you too.
 
L

Lukas Sebastian

Guest
Layer types only exist in the IDE for Drag n Drop, such as a tileset requires different information than an object does. Once the game starts a layer is a layer to GML, there are no different types. Look at `layer_create()` and you will see there is no type declaration. When using code you can add any combination of objects, spites, tilemaps, etc to the same layer.

I personally just used the instance layer in the IDE to create a layer and marked it invisible with nothing on it, then placed my objects and sprites on that layer with code.
You could also just create the invisible layer with code instead of using the IDE to add the layer.
Ah, okay. That is excellent. Thanks!

If I may ask, how would you extend your solution to handle depth for tiles then?
I am using GMS2 and was following along a tutorial from GMS1.4 and depth sorting was used with the classical line depth=-y for objects and then depth was also set for the tiles of different layers (using the function tile_set_depth). Now, as far as I can tell, tiles can't have a depth in GMS2, so using a solution such as yours is the way to go I assume. I figured I would add tiles to the invisible layer and check for tile element type, but got stumped as I browsed the manual and saw that layerelementtype_tile is only there for legacy reasons. Would you skip using tiles and add these as sprites (or is it assets?) (trees, houses and such)?

I am not sure if there is a misunderstanding. This is not for drawing the sprites of the objects you want sorted. The objects have their own sprites as part of the object properties, i am not doing anything with those sprites as the objects draw them themselves. The sprites i am talking about here are sprites that you would put on an asset layer. Sprites that do not represent any object.

Say in your game you have static things in your room that never move, are nothing more than cosmetics, like trees. Instead of making your trees objects which add more to the games overhead, you can use a sprite instead of an object/instance.
This was of course a thought sprung from the perspective of using the room editor in which the asset layer will hold sprites, but not objects. Moot anyways, since your answer cleared it up for me.
 
K

kevins_office

Guest
I figured I would add tiles to the invisible layer and check for tile element type, but got stumped as I browsed the manual and saw that layerelementtype_tile is only there for legacy reasons.
layerelementtype_tile is for legacy, but they have layerelementtype_tilemap for GMS2. I have not personally tested sorting tilemaps so i do not know if it would return each tile in the tilemap separately or just return the entire tilemap as one element on the layer. I believe the idea of adding sprites of the tiles to the layer using layer_sprite_create() would work but i would wonder if there is a performance cost since GMS2 says tilemaps are more efficient.

What are you using tiles for in your game that you would sort? Im doing top down and using tiles as ground/dirt/grass so there is no reason to ever sort those as everything is above them. But things like trees i use sprites instead of tiles so the character can walk behind them.
 
L

Lukas Sebastian

Guest
layerelementtype_tile is for legacy, but they have layerelementtype_tilemap for GMS2. I have not personally tested sorting tilemaps so i do not know if it would return each tile in the tilemap separately or just return the entire tilemap as one element on the layer. I believe the idea of adding sprites of the tiles to the layer using layer_sprite_create() would work but i would wonder if there is a performance cost since GMS2 says tilemaps are more efficient.

What are you using tiles for in your game that you would sort? Im doing top down and using tiles as ground/dirt/grass so there is no reason to ever sort those as everything is above them. But things like trees i use sprites instead of tiles so the character can walk behind them.
I'll get back to you when (and if I remember to) I've had a chance to try it out with tilemap. Might take a while. Still, you guessed right - depth for trees and such and not all tiles. I'm actually learning GMS2 by "porting" legacy tutorial code, so I got stuck in the old school tile-thinking. Using sprites seems like a better way to go here.
 
K

kevins_office

Guest
I don't have a better way to depth-sort but I like looking at code and figuring out
which style is the fastest. These are the optimizations I've come up with:
They are faster for me, test if they're faster for you too.
I copied your code example as is and there was an error...
Code:
var layer = elements[loop++];
// Error -- Event: Pre-Draw : cannot redeclare a builtin variable
// Error -- Event: Pre-Draw : malformed assignment statement
Apparently layer is a built in keyword:
https://docs2.yoyogames.com/source/...rence/instances/instance_variables/layer.html
So im not sure how you got better times with your code ;)
NOTE: Anyone else wanting to test the code just change the variable name from layer to something else.

I tested the code with 500 objects and 500 Sprites on screen and only saw a tiny tiny improvement.
Below are the times i was getting in microseconds.

Pre Sort: 1183
Pre Sort: 1269
Pre Sort: 1348
Pre Sort: 1276
Pre Sort: 1180
Pre Sort: 1283
Pre Sort: 1171
Pre Sort: 1204
Pre Sort: 1171
Pre Sort: 1270
Pre Sort: 1223
Pre Sort: 1257

Pre Sort: 1158
Pre Sort: 1208
Pre Sort: 1173
Pre Sort: 1194
Pre Sort: 1130
Pre Sort: 1132
Pre Sort: 1212
Pre Sort: 1195
Pre Sort: 1170
Pre Sort: 1188
Pre Sort: 1153
Pre Sort: 1251

I am hoping there is a way to get times down to 300µs to 500µs.
Are you sure you tested it and got faster speeds with broken code and all? o_O
How many objects and sprites did you test and what speeds where you getting?
 

Simon Gust

Member
I copied your code example as is and there was an error...
Code:
var layer = elements[loop++];
// Error -- Event: Pre-Draw : cannot redeclare a builtin variable
// Error -- Event: Pre-Draw : malformed assignment statement
Apparently layer is a built in keyword:
https://docs2.yoyogames.com/source/...rence/instances/instance_variables/layer.html
So im not sure how you got better times with your code ;)
I just tested the raw code structures with some rando variables and values. I didn't expect layer to be reserved, still using GMS 1.4
 
Depending on how your game works, you can improve performance by maintaining the sorted list. Most objects won't teleport across the view each frame and a partially sorted list is faster to sort than a completely random list. This is often done with collision detection for performance improvement by taking advantage of temporal coherence. But in order to do that and increase performance, you'd likely have to move away from your "lazy" approach of letting the controller object handle everything. At the very least, you would probably have to have the instances add to the list on create and remove from the list on destroy since you won't know if the instances in your list exist. When I first looked at this, I thought it might be possible to use the layer_script_begin to hijack the create/destroy events of instances on the invisible layer but that function only works with draw events. And even then, sprites created and destroyed have no events so you can't be lazy for this improvement. :(

Also saw this: [LINK]. Shows 2 common ways of handling depth sorting. One method using sorting by y values like above and the other by layers without the need for sorting. So depending on how you design your graphics, you could get away with depth sorting without sorting which would be fastest. A combination of both methods would also be possible (sprites using layers and instances like NPCs and players using sorting).
 
K

kevins_office

Guest
you can improve performance by maintaining the sorted list.
But in order to do that and increase performance, you'd likely have to move away from your "lazy" approach of letting the controller object handle everything.
At the very least, you would probably have to have the instances add to the list on create and remove from the list on destroy since you won't know if the instances in your list exist.
I would have to really think about this. Yes with some work using a parent object, you can maintain a sorted list, having the parent add to the list on create and remove from the list on destroy.
However i am not sure how to include the sprites in the list using this method. If you could find a way for sprites to be included in the same sorted list as objects how would you tell them apart in that list?
When using the layer get element it keeps track of the element type for you.

Then what about only drawing objects and sprites in camera view?
Wouldn't that take some kind of pre looping work to compare their current xy to the camera view?
And in doing that loop did you just negate all of the benefit of keeping a sorted list?
 
However i am not sure how to include the sprites in the list using this method. If you could find a way for sprites to be included in the same sorted list as objects how would you tell them apart in that list?
When using the layer get element it keeps track of the element type for you.
Yeah every time I have an idea to implement this, it seems there is a missing function I would need. It would be nice if instances had an element id variable. The only way I see to get that would be looping through all the element ids until you find the correct instance id which would be slow. You could create a blank layer for creating new instances and use layer_get_all_elements(layer) in the create event which would give the element id then use layer_element_move() to move it to the correct layer. When you add a sprite through code, it returns the element id so those would be easy.
Then what about only drawing objects and sprites in camera view?
Wouldn't that take some kind of pre looping work to compare their current xy to the camera view?
And in doing that loop did you just negate all of the benefit of keeping a sorted list?
Not really. Right now for each instance/sprite, you already do this in your code. If you did this for the pre-sorted list, it would be the same. Not sure if it's true or not but I have heard GMS doesn't draw stuff outside view and handles this in the background so it might be worth testing if you even need these checks.
 

Simon Gust

Member
Ok, here's an idea:
Make a surface the size of your screen + buffer
draw every element as a pixel consisting of color data
red: element type (sprite, instance)
green + blue: element id (for instances only) or sprite index
alpha: also nothing
call buffer_get_surface(), this will sort the colors to a buffer the correct way.
loop through the buffer and draw based on information inside the buffer.
You might want to try this but I think it will fail at buffer_get_surface(), I don't know how fast this function is.
Originally I wanted to try and figure out depth sorting with a shader but I couldn't come up with something.

EDIT: Or if the sprite and instances have an index in the element array you can just store the index. That could be easier and only requires 2 color channels at most.
EDIT2: Conclusion: it's horrible, there is no good way to loop through the buffer when drawing. Especially if the buffer has over 4 million entries of data. And overlapping sprites aren't drawn either.
 
Last edited:

Miradur

Member
Here is a good guide to deep and shader:

https://www.yoyogames.com/blog/458/z-tilting-shader-based-2-5d-depth-sorting

I know it sounds stupid, but wait for the next big update of GM.
Tile layer and deep is a single construction site, with many missing commands and only half-heartedly implemented.
Even YoYo scripts refer to commands that fall under the rubric, obsolete commands.
In my opinion, this would have to be fixed with the next update, as a very inconsistent approach is currently being taken.
In addition, yoyo should set an example and show a good solution for this problem(depth and layer).
At the moment, all hobby programmers are trying to do so. However, it would be important for me to get a professional
solution.

With the layers you can help yourself best by splitting up the tile map, one below and one above the player.
then you draw the tree stump underneath and the foliage above and already have a rudimentary deep sort routine.
Like you used to do in the old days(old school forever ;)).

Miradur
 
So I got it working and there was a decent performance boost. Not sure how you are testing it but I'll post the code.
minFPS, maxFPS, avgFPS
217, 258, 251
Code:
/// @desc CREATE EVENT
// Create Instance Variables
depthList = ds_list_create();
sortingLayer = layer_get_id("Instances");
//add instances to room
assetList = ds_list_create();
repeat(500){
    ds_list_add(assetList,true);
}
repeat(500){
    ds_list_add(assetList,false);
}
ds_list_shuffle(assetList);
for(var i = 0; i < ds_list_size(assetList); i++){
 
    if(ds_list_find_value(assetList,i)){
        instance_create_layer(random(room_width),random(room_height),"Instances",obj_inst1); 
    } else {
        layer_sprite_create("Instances",random(room_width),random(room_height),spr_sprite); 
    }
}
/***************
minFPS, maxFPS, avgFPS
217, 258, 251
****************/
Code:
/// @desc PRE-DRAW EVENT
// Clear Sort List
ds_list_clear(depthList);
// Define Camera View Coordinates
// ***Note: Change view_camera[0] if your project uses something different
var cam_x1 = camera_get_view_x(view_camera[0]);
var cam_y1 = camera_get_view_y(view_camera[0]);
var cam_x2 = cam_x1 + camera_get_view_width(view_camera[0]);
var cam_y2 = cam_y1 + camera_get_view_height(view_camera[0]);
// Define Local Variables
var loop = 0;
var xx = 0;
var yy = 0;
var width = 0;
var height = 0;
// Get All Elements On Invisible Layer And Loop
var elements = layer_get_all_elements(sortingLayer);
repeat (array_length_1d(elements)) {
   xx = undefined;
   // Object Instances
   if (layer_get_element_type(elements[loop]) == layerelementtype_instance) {
       var thisInst = layer_instance_get_instance(elements[loop]);
       xx = thisInst.x;
       yy = thisInst.y;
       width = thisInst.sprite_width;
       height = thisInst.sprite_height;
   }
   // Asset Layer Sprites
   if (layer_get_element_type(elements[loop]) == layerelementtype_sprite) {
       xx = layer_sprite_get_x(elements[loop]);
       yy = layer_sprite_get_y(elements[loop]);
       width = sprite_get_width(layer_sprite_get_sprite(elements[loop]));
       height = sprite_get_height(layer_sprite_get_sprite(elements[loop]));
   }
   // If Within Screen View Add To Sorting List
   if (xx != undefined && xx + width >= cam_x1 && xx - width <= cam_x2 && yy + height >= cam_y1 && yy - height <= cam_y2) {
       ds_list_add(depthList, (elements[loop] | yy << 32) );
   }
   loop++;
}
// Sort List According to Y value
ds_list_sort(depthList, true);
Code:
/// @desc DRAW EVENT
// If you need to add this to BEGIN DRAW EVENT use event_perform(ev_draw, ev_draw_begin);
// If you need to add this to END DRAW EVENT use event_perform(ev_draw, ev_draw_end);
var loop = 0;
repeat (ds_list_size(depthList)) {
   var element_id = depthList[| loop] &$ffffffff;
   if (layer_get_element_type(element_id) == layerelementtype_instance) {
       with (layer_instance_get_instance(element_id)) event_perform(ev_draw, 0);
   }
   if (layer_get_element_type(element_id) == layerelementtype_sprite) {
       draw_sprite(layer_sprite_get_sprite(element_id), -1, layer_sprite_get_x(element_id), layer_sprite_get_y(element_id));
   }
   loop++;
}
Code:
/// @desc CLEAN UP EVENT
ds_list_destroy(depthList);
depthList = undefined;
minFPS, maxFPS, avgFPS
294, 361, 350
Code:
/// @desc CREATE EVENT
// Create Instance Variables
global.depthList = ds_list_create();
sortingLayer = layer_get_id("Instances");
assetList = ds_list_create();
repeat(500){
    ds_list_add(assetList,true);
}
repeat(500){
    ds_list_add(assetList,false);
}
ds_list_shuffle(assetList);
var inst;
for(var i = 0; i < ds_list_size(assetList); i++){
 
    if(ds_list_find_value(assetList,i)){
        inst = instance_create_layer(random(room_width),random(room_height),"Blank",obj_inst1);
        with(inst){
            element = layer_get_all_elements("Blank");
            layer_element_move(element[0],"Instances");
            ds_list_add(global.depthList, element[0] | y << 32);
        }
    } else {
        inst = layer_sprite_create("Instances",random(room_width),random(room_height),spr_sprite);
        ds_list_add(global.depthList, inst | layer_sprite_get_y(inst) << 32);
    }
}
/***************
minFPS, maxFPS, avgFPS
294, 361, 350
****************/
Code:
/// @desc PRE-DRAW EVENT
var loop = 0;
repeat (ds_list_size(global.depthList)) {
   var element_id = global.depthList[| loop] &$ffffffff;
   if (layer_get_element_type(element_id) == layerelementtype_instance) {
       with (layer_instance_get_instance(element_id)){
            global.depthList[| loop] = element_id | y << 32;
       }
   }
   loop++;
}
// Sort List According to Y value
ds_list_sort(global.depthList, true);
Code:
/// @desc DRAW EVENT
// If you need to add this to BEGIN DRAW EVENT use event_perform(ev_draw, ev_draw_begin);
// If you need to add this to END DRAW EVENT use event_perform(ev_draw, ev_draw_end);
var loop = 0;
repeat (ds_list_size(global.depthList)) {
   var element_id = global.depthList[| loop] &$ffffffff;
   if (layer_get_element_type(element_id) == layerelementtype_instance) {
       with (layer_instance_get_instance(element_id)) event_perform(ev_draw, 0);
   }
   if (layer_get_element_type(element_id) == layerelementtype_sprite) {
       draw_sprite(layer_sprite_get_sprite(element_id), -1, layer_sprite_get_x(element_id), layer_sprite_get_y(element_id));
   }
   loop++;
}
Code:
/// @desc CLEAN UP EVENT
ds_list_destroy(global.depthList);
global.depthList = undefined;
Code:
/// @desc CREATE EVENT
speed = random(2);
direction = random(360);
dir = random(360);
Code:
/// @desc STEP EVENT
direction += dcos(dir);
dir += random(5);
if(x<64){ hspeed = abs(hspeed); }
if(x>room_width-64){ hspeed = -abs(hspeed); }
if(y<64){ vspeed = abs(vspeed); }
if(y>room_height-64){ vspeed = -abs(vspeed); }
Code:
/// @desc DRAW EVENT
draw_self();

I added another layer called "Blank" that allows me to get the element id of instances. It should be empty unless you are creating an instance which will be moved before another instance is created.
 
K

kevins_office

Guest
Haha, yeah we know. That link is included in the first post of this thread.
And splitting the tree on two tiles is not an option i want to use because that would require all of my trees to be as fat as a tile. I am not making a NES clone.


I know it sounds stupid, but wait for the next big update of GM.
Do you actually know something about when the next update is happening and what features will be added? Or is that just a wishful thinking statement?
And i don't mean the road map, that is YYG's wish list and doesn't always reflect actual new features.
 

Miradur

Member
Hi, more logic than knowing, because even YoYo uses commands that should not be used with the layer system anymore.
Therefore a logical development for me, that they have to catch up the most with tiles and layers(take a look under
obsolete commands).
But let's take it by surprise :).

Miradur
 

CMAllen

Member
I've sometimes wondered if there wasn't a way to incorporate z-depth testing as part of a game's default shader method in place of active draw sorting. Let the GPU do the heavy graphical lifting instead of the CPU. I mean, I know the depth buffer can be used that way, but I'm sure there are trade offs, and I'm less sure how best to pass the requisite information into a shader to achieve the intended effect in the most optimal manner.
 
K

kevins_office

Guest
I've sometimes wondered if there wasn't a way to incorporate z-depth testing as part of a game's default shader method in place of active draw sorting. Let the GPU do the heavy graphical lifting instead of the CPU. I mean, I know the depth buffer can be used that way, but I'm sure there are trade offs, and I'm less sure how best to pass the requisite information into a shader to achieve the intended effect in the most optimal manner.
I do not know hardly anything about shaders, but would it be possible to send a command to the GPU telling it everything it receives after the command goes into a buffer, then another command that tells the GPU the buffer is closed and everything it had received to now draw to the screen sorted? Same idea like setting a surface, drawing to it, then back to "normal state". Then just call it in the pre draw, close it in the post draw, and the GPU does the heavy lifting.
 

CMAllen

Member
I do not know hardly anything about shaders, but would it be possible to send a command to the GPU telling it everything it receives after the command goes into a buffer, then another command that tells the GPU the buffer is closed and everything it had received to now draw to the screen sorted? Same idea like setting a surface, drawing to it, then back to "normal state". Then just call it in the pre draw, close it in the post draw, and the GPU does the heavy lifting.
If you have some object that executes its code before everything else, yes, you can have it turn on a shader that handles all the depth sorting, and then turn it back off again when sorting is no longer needed. It's a question of what information to send to the shader to use the depth buffer appropriately, and what kind of trade offs are involved in using it that way. For example, I recall reading about how the depth buffer has issues with the alpha channel similar to the problem surfaces have with alpha channels, and well all know the headache with how surfaces handle alpha-blending.
 
K

kevins_office

Guest
I think the perfect solution would involve YYG adding a feature into GMS2/GML. In the layer properties, add a checkbox / code variable for "[X] Depth Sorting". Since layers already control the order of whats sent to the GPU, it would just add a built in sub-sort that would sort everything on that layer by Y value. Basically the same thing we are trying to accomplish here but being done in native code with strict variable types it could achieve much better performance.

@Mike how can we submit this suggestion to YYG development?
 
N

Nordwin

Guest
If I may ask, how would you extend your solution to handle depth for tiles then?
I am havin the exact same problem.. I like the idea of your solution, but
Code:
layer_get_all_elements(sortingLayer);
just returns the tilemap and not the tiles on the layer. I am drawing my room using a tileset and having seperate different layers. Unfortunately this isn't enough for certain objects, such as trees. When standing infront of them the head is overlapped by the tile in the upper layer. So I was trying to apply your code for tiles as well, adding all these tiles possibly overlapping the player (only a few on each map) to the depth-ordering-layer. But unfortunately I didn't make it up to now, because of the whole tile thing being legacy and not supported anymore...
Do I really have to create own sprites for every object in my tileset and place the sprites instead?

Furthermore I changed your code reading data from multiple layers, which results in an easier handling for me and you get rid of the whole typechecking as well.. If it is faster as well I may not say, but you are able to use the map editor to create your layers, which is more comfortable for me.
 
K

kevins_office

Guest
So I was trying to apply your code for tiles as well, adding all these tiles possibly overlapping the player (only a few on each map) to the depth-ordering-layer. But unfortunately I didn't make it up to now, because of the whole tile thing being legacy and not supported anymore...
Do I really have to create own sprites for every object in my tileset and place the sprites instead?
The way GMS2 handles tiles now being a tilemap i do not see a way for sorting individual tiles. You have to draw the entire tilemap at once.
The work around as you suggested is using asset sprites instead of tiles.


Furthermore I changed your code reading data from multiple layers, which results in an easier handling for me and you get rid of the whole typechecking as well.. If it is faster as well I may not say, but you are able to use the map editor to create your layers, which is more comfortable for me.
I do not follow you. My code only checks a single layer, not all layers. Type checking is there if you are going to put different items such as asset sprites and objects on that same layer. Sure if you make sure you only put objects, or only put asset sprites on the one layer then you dont need type checking assuming you know what it will always be.
 
N

Nordwin

Guest
The way GMS2 handles tiles now being a tilemap i do not see a way for sorting individual tiles. You have to draw the entire tilemap at once.
The work around as you suggested is using asset sprites instead of tiles.
So just to be sure, the whole map, as I have drawn it in the map editor is saved as a tilemap? Then the whole map is rendered at once, so there will be no chance rendering individual tiles earlier or later? This would mean that I would need to create sprites from these few overlapping objects, but it's porbably the cleaner and better solution anyways. I thought I could get along just using the tilemap (as in GMS1)...
I do not follow you. My code only checks a single layer, not all layers. Type checking is there if you are going to put different items such as asset sprites and objects on that same layer. Sure if you make sure you only put objects, or only put asset sprites on the one layer then you dont need type checking assuming you know what it will always be.
As mentioned this is a question of style and implementation, but I changed your code so that it applies to one layer for instances I want to have depthsorted and one layer for sprites that I want to have depthsorted as well. I like to have my layers seperated rather than mixing instances and sprites into one layer. Seems cleaner to me, but again this is my personal preferation. The speed for my solution is not noticeable faster, although it should theoretically work a bit faster...But really nothing one should care about here. I am still looking for other performance optimizations, but my time for this is tight atm.
 
K

kevins_office

Guest
So just to be sure, the whole map, as I have drawn it in the map editor is saved as a tilemap? Then the whole map is rendered at once, so there will be no chance rendering individual tiles earlier or later? This would mean that I would need to create sprites from these few overlapping objects, but it's porbably the cleaner and better solution anyways. I thought I could get along just using the tilemap (as in GMS1)...
You add your tiles to the tilemap, then you draw the tilemap. You can not draw the individual tiles.
Look into the following commands to get a better understanding of what the IDE is doing.
Code:
layer_tilemap_create();
tilemap_set();
draw_tilemap();
 
N

Nordwin

Guest
You add your tiles to the tilemap, then you draw the tilemap. You can not draw the individual tiles.
Look into the following commands to get a better understanding of what the IDE is doing.
Code:
layer_tilemap_create();
tilemap_set();
draw_tilemap();
Yeah thanks, I think I got it now... The concept is probably better than the old one, when it is coming to performance and you can easily create whole bigger levels using different tilemaps then...
 
A

atmobeat

Guest
I did like the basic concept of putting objects, that you want to have depth, into a sorted list and manually drawing them to the screen. I also found Ariak's suggestion of a binary list to be the fastest sorting method. That is when you bit shift two values (Instance Id, y) into a single number stored in a ds_list...

I felt performance would take a hit after having too many objects. Since GMS2 has asset sprites and tilemaps for the exact purpose of reducing object over head, i wanted my trees (and other things) to be sprites instead of objects. I wanted a method that could depth sort objects and sprites.

Im also lazy and wanted a one and done solution, i made a controller object that once dropped on a layer, would handle all sorting without any setup work for the objects or sprites, no adding them to a list, no making them all children of a special parent object, no adding sorting code to their events, etc...

I am also asking the community, if anyone can improve upon performance please post an improved version as it will help everyone. ...

Id love to hear if anyone can improve on this.
The main performance impact from any method that relies on sorting lists is the sorting of the list. MirthCastle's method does not rely on sorting a list, it also does not hijack an object's draw event either (which is just horrible to me), and it uses GMS2's layers. As far as I can tell, it is strictly better than any sorting method for depth-sorting objects. If you (and I may try it out soon) can hybridize your method with his, you will definitely see a bump in performance. I would love such a method because, like you, I want more than just depth-sorted objects. Sprites and even tiles would be nice. Here's a link for MirthCastle's 2.5d depth-sorting:


What are you using tiles for in your game that you would sort? Im doing top down and using tiles as ground/dirt/grass so there is no reason to ever sort those as everything is above them. But things like trees i use sprites instead of tiles so the character can walk behind them.
In a top-down game, if you have multiple height levels (like a hill/plateau), and aren't doing the weird Zelda-style walls but actually have depth to your hill's "backside" edge, then you would definitely want to have depth-sorted tiles. When a player is on the ground behind the hill, they should disappear behind its backside edge; however, when the player is on top of the hill and standing on that same edge, the player should appear on top of the same tile. The same goes for "thin" walls where on the south side of the wall the character should be drawn over the wall, but when on the north side the character should be drawn behind the wall. Any top-down game with jumping really has to worry about these things. My game unfortunately :(.

Right now, using MirthCastle's method, the backsides of hills and thin walls and anything similar to them has to be an object in my game and that's a pain in my ass when it comes to level design in the room editor. Converting those things into sprites might be great though so I'll give it a try soon and report back. A marriage of your strategy and @MirthCastle 's seems very promising.

You add your tiles to the tilemap, then you draw the tilemap. You can not draw the individual tiles.
This is false. draw_tile can be used to draw a tile at a specific location in the room and it is drawn on the same layer as the calling instance (if I recall correctly but see https://docs2.yoyogames.com/source/...ence/drawing/sprites_and_tiles/draw_tile.html). But that's not much help for depth-sorting if you also want to place your tiles down in the room editor. If you are tiling a room through code, then you could, in principle, depth sort the tiles somehow by moving the tiling object to different layers and then drawing the tile with draw_tile. Sounds like a pain.
 
Top