GMS 2 Custom Tile System

VacantShade

Member
PREVIEW:
I'm implementing a custom tile system into my project, because I'd like more flexibility than what the current system allows. I've already added the system, and it works fine, but I believe it to be very inefficient. Below I'll detail some of my goals and then how my system works. It would be great to hear your ideas as to how you would go about accomplishing the same goals but with a different approach!
NOTE: All code below is pseudo code. As the features all work, I'm just interested in finding a better approach to the concept in general.

BACKGROUND:
Part of my game involves an editor which will allow players to design their own sections of the game world. When designing a section, they should be able to define a number of Palettes to use in the section. However, in the actual game, these sections are combined in a single GM Room. Transitions can be handled with a black fade out though, so it should be possible to do some data swaps between sections.
Current view resolution is [ 256 x 240 ] with [ 16 x 16 ] tile size, for a total of [ ~240 ] tiles onscreen at once. The game actually uses 3 tile layers ( Back , Mid , Front ), but what works for one should work for all of them.

GOALS:
+ This system should work on all exports ( Including HTML5 )
+ Allow Tiles to be Rotated, Mirrored, Flipped ( Just like normal )
+ Draw each individual tile with a specific Palette ( 3 Colors )
+ Let a color in a Palette be defined as Clear ( so 0 Alpha )
+ Allow for Animated tiles of varying frame length ( 4 , 6 , 8 , 10 , 12 , 16 ) ( This is flexible )
+ Allow for Animated tiles with frame offsets ( e.g. So animation starts on frame 2 )
+ Allow for tiles to use Animated Palettes of varying frame length ( 4 , 6 , 8 , 10 , 12 , 16 ) ( This is flexible )
+ Allow for tiles to use Animated Palettes with a frame offset ( e.g. So palette starts on frame 2 )
+ Allow for an Animated Tile to also have an Animated Palette

MY IMPLEMENTATION:
+ Colors are stored as HEX values
- Palettes are stored in a 2d Array, like so: [ Palette 0 ] , [ Color A ] , [ Color B ] , [ Color C ]
- Animated Palettes are always a specific range of indexes, say 16-19
- Animated Palettes follow a similar system in the same Array
Code:
// Setup Palettes

global.PALETTE[ 0 , 0 ] = ( $000000 );// Color A
global.PALETTE[ 0 , 1 ] = ( $000000 );// Color B
global.PALETTE[ 0 , 2 ] = ( $000000 );// Color C

// Setup Animated Palettes

global.PALETTE[ 16 , 0 ] = 0;// Current Frame
global.PALETTE[ 16 , 1 ] = 4;// Total Frames
global.PALETTE[ 16 , 2 ] = 100;// Speed of Animation
global.PALETTE[ 16 , 3 ] = ( $000000 );// Frame 0 - Color A
global.PALETTE[ 16 , 4 ] = ( $000000 );// Frame 0 - Color B
global.PALETTE[ 16 , 5 ] = ( $000000 );// Frame 0 -  Color C
ect...
+ Using the default tile system, setup a monochrome tileset
- By monochrome, there are only 3 colors. (75,75,75) (150,150,150) (225,225,225)
+ Animated Tiles use their own system. They are setup in a 2d Array ahead of time
Code:
// For a 4 Frame Animated Tile
// tile_index could be any number in the tileset

Tile_Is_Animated[ tile_index ] = 1;// Index Pointer
Tile_Is_Animated[ tile_index +1 ] = 1;// Index Pointer
Tile_Is_Animated[ tile_index +2 ] = 1;// Index Pointer
Tile_Is_Animated[ tile_index +3 ] = 1;// Index Pointer
Tile_Animation[ 1 , 0 ] = tile_index;// Base Frame Tile Index
Tile_Animation[ 1 , 1 ] = 4;// Total Frames
Tile_Animation[ 1 , 2 ] = 100;// Base Speed
+ Drawing a single tile involves these steps
- Get the tile data ( extra bits store palette index, animation speed, ect )
Code:
[ Bit ]    [ Value ]        [ Usage ]
=============================================
0    1        \
1    2        |
2    4        |
3    8        |
4    16         > Index Value
5    32        |
6    64        |
7    128        |
8    256        |
9    512        |
10    1024    /

11    2048        \
12    4096        |
13    8192         > Color Index (section relative)
14    16384        |
15    32768        /

16    65536        \
17    131072         > Palette Animation Offset
18    262144        /

19    524288        \
20    1048576     > Animation Speed

21    2097152    \
22    4194304     > Animation Offset
23    8388608    /

24    16777216    ] -- Stuff for later maybe?
25    33554432    ] -- Stuff for later maybe?
26    67108864    ] -- Stuff for later maybe?
27    134217728    ] -- Stuff for later maybe?

28    268435456    ]
29    536870912    > --- Flip
30    1073741824    > --- Mirror
31    2147483648    > --- Rotate
- Using the Palette Array, get the RGB values of each color, and pass them as uniforms to a shader
- Draw the tile using a "palette swap" shader, that recolors the initial monochrome colors
Code:
// Drawing a Tile
data = tilemap_get( tilemap_id , xx , yy );// Get tile data
index = ( data & TILE_INDEX_BITS );// Get tile index + rotation , mirror , flip
            
if( index > 0 ) // Make sure this is a legit tile
{
    palette = (( data & TILE_PALETTE_BITS ) >> 11);// Get tile Palette
    palette_offset = (( data & TILE_PALETTE_OFFSET_BITS ) >> 16);// Get tile Palette Animation Offset
                    
    p_colors = scr_Palette_Get_Color( palette , palette_offset );// Gets an array with the palette colors
    scr_Set_Shader_Uniforms( p_colors[0] , p_colors[1] , p_colors[2] , 1.0 );// Sets the shader uniforms
            
    draw_tile( tileset , index , 0 , draw_x , draw_y );// Draws the Tile
}
Code:
// Simple palette swap fragment shader
// NOTE: One thing I have yet to implement is allowing a color to be Clear, having 0 Alpha

varying vec2 v_vTexcoord;
varying vec4 v_vColour;

uniform vec3 new1;
uniform vec3 new2;
uniform vec3 new3;

uniform float alpha;

float result = 0.0;


void main()
{
    gl_FragColor = v_vColour * texture2D( gm_BaseTexture, v_vTexcoord );
    
    //Make it easier to compare (out of 255 instead of 1)
    vec3 test = vec3(
        gl_FragColor.r * 255.0,
        gl_FragColor.g * 255.0,
        gl_FragColor.b * 255.0
    );

    //Check if it needs to be replaced
    test.r = floor( test.r + 0.5 );
    
    if( mod( test.r , 75.0 ) < 0.5 )
    {
        result = (test.r / 75.0);
    }
    
    if( abs( result - 1.0 ) < 0.2 ){ test = new1; }
    if( abs( result - 2.0 ) < 0.2 ){ test = new2; }
    if( abs( result - 3.0 ) < 0.2 ){ test = new3; }

    //return the result in the original format
    gl_FragColor = vec4(
        test.r / 255.0,
        test.g / 255.0,
        test.b / 255.0,
        gl_FragColor.a * alpha
    );
    
}
+ Have a surface the size of the view, plus small buffer
- When starting, or refocusing game window, draw every tile onto the surface
- When the view moves, copy surface onto a new surface, but offset by 1 tile, draw the new row/column of tiles
- If an Animated Tile is added, or a tile with an Animated Palette, add it to a ds_list
- Every step, run through the ds_list, and check if the tile needs updated, if so draw it to the surface​

VISUALS:
Here are some GIFs of the system in action, to help you get a visual idea what all this stuff is for in the end.

FAULTS:
Profiling through the debugger shows that while non-animated tiles work relatively well, animated tiles/palettes cause major slow down. Part of this is the need to check them each step to see if they have updated. Another issue is the fact I need to "cut them out" of the surface, using a blend mode and drawing a rectangle first. Lastly, a large slowdown comes from passing the uniforms to the shader every draw.
Also, I've yet to implement it into the shader to handle a color being passed as "clear", having no Alpha. Since I'm dealing with a very small amount of colors, I could reserve a color as "clear", and have the shader do an if check for that "color".
Not a fault necessarily, but storing the palette in the tile data means I can only have a small number of palettes available per player designed "section", since indexes can only go so high. This also means that I'll need to re-setup the palette data for every "section" during fade out transitions.

SUMMARY:
There's quite a few moving parts to this machine, but it actually works and accomplishes what it set out to.
However, I'd really like to increase the efficiency, as of right now, I think I would have to limit players to about 20~ animated tiles per screen region, to prevent slowdown. I'd like to get it up to 100~.
My programming experience is still quite limited, so I imagine there are some major improvements that could be made, while maintaining the functionality, or even expanding it. ;)
I look forward to hearing your thoughts!
 

Simon Gust

Member
In my game I am using soley surfaces to draw coloured pixels that act as pointers to
my texture pages. I bet this would probably work well with animations, as drawing a pixel is
faster than drawing a sprite (draw_point_colour).
You can use the RGB components to point to position of general sprite on the texture page (red),
position of subimage on texture page (green) and hue shift (blue) for example.
Since the surface never holds anything (semi) transparent you can just draw over it with new color info.
You can always make another surface with more pixels for even more info.

My code flow is simple:
- find offsets to previous view positions
- redraw surfaces (draw a strip if moved, full surface if game has started)
- define positions of redrawing
- copy main surface to temp surface
- clear main surface
- draw temp surface to main surface with offset
- draw over all animated tiles (coordinates stored in list)
In my draw event I set stage to all surfaces I need
Code:
texture_set_stage(shader_get_sampler_index(tile_shader, "main_surf"),  surface_get_texture(main_surface));
texture_set_stage(shader_get_sampler_index(tile_shader, "extra_surf"), surface_get_texture(extra_surface));
texture_set_stage(shader_get_sampler_index(tile_shader, "whatever_surf"), surface_get_texture(unnecessary_surface));
texture_set_stage(shader_get_sampler_index(tile_shader, "tile_texture"), sprite_get_texture(tsp, 0)); // texture page for sprites
I also draw giant black sprite for this to take effect
The shader then does the rest:
- finding correct position on the texture page
- translating positional data (to get not just a 16x16 colored pixel but the actual sprite)
- coloring the tiles with a hue shift
 

VacantShade

Member
Thanks for sharing your input!
Your idea sparked a thought of my own, so I tried adjusting my system, and managed to achieve a 500% increase in speed while maintaining all the functionality!
Here's a rundown of the changes:

+ I get the palette index from the tile data, then draw a single pixel for each color in the palette onto a "color atlas" surface.
+ Instead of drawing surfaces onto the application surface each frame, and updating those surfaces every time the view moves, I switched to using the internal system for drawing tile layers. This drastically reduces processing time.
+ To be able to draw the tiles with the proper colors, I apply a shader to the entire Tile Layer, and that shader references the color atlas surface to grab the colors for the tile.

Image B.png

I'm still updating the tile indexes manually for animated tiles, so I can have varying speeds and frames. I also update the color atlas surface manually for animated palettes.
There is still probably room for improvement, but it's already way faster!
Thanks for the inspiration! šŸ˜
 
Top