Using Surfaces


Using Surfaces

OVERVIEW

As there are some minor changes coming to GameMaker Studio 2 update 2.2.1 in regards to surfaces, I thought this would be an opportunity to go back over some basics of how surfaces work.

Surfaces can act either as a new screen to draw to or a sprite to draw from. This makes them incredibly flexible, particularly when doing effects. For example, you could clear the surface, then draw blobs on it and then use this as a cheap lighting engine as shown in Mark Alexander's blog post: Quick and Simple Lighting, or as the basis of a more complex lighting system as shown in my earlier blog: Realtime 2D Lighting.

Or you could create a user-customisable or "real-time deformation" sprite for a car, say, by clearing the surface, rendering components to the surface - like wheels at different angles, a car body with different bits of damage, then accessories such as guns aimed at the current angle, spares, etc., - and then render this surface to the screen as a sprite.

As a final example, you could render part of the current screen (the application_surface) to it using a shader and blend/wobble this surface, then draw the surface back to the application_surface tinted blue so that you have a cool water reflection effect.

Basically put, they are incredibly useful and powerful, and a tool few should ignore. So exactly how do they work?

USING SURFACES

Surfaces are created simply enough using the surface_create(w,h) function and are then freed using surface_free( surf_id ). If you can, you should try and create surfaces that are a power of 2 - that is, sizes like 16,32,64,128, 256 and so on - as some older hardware can't do this. The need for this is becoming a rarity, but worth remembering.

Next, we'll need to set the surface in order to use it, and we do this simply using surface_set_target( surf_id ). Once set, you draw things as normal, as though the viewport is the size of the surface. Restoring the previous surface is done using surface_reset_target();.

You'll notice I said "restoring the previous surface". An important thing to remember is that surface rendering is based on a STACK. This means when you set a surface, you MUST reset/restore them IN ORDER. The reason we use a stack is so that you can have library functions that can set their own surface and clear it without affecting your main code. Stacks are invaluable for library/utility functions.

Now we come to the reason for this blog post and the new changes coming in 2.2.1: You can no longer free or resize surfaces that are in use. This means once you set a surface, you can not free it or resize it until you release it using surface_reset_target(). This means for each surface which is set, you must have a matching surface reset in order to maintain the stack. An example of this is...

surface_set_target( mysurf );
      // draw here....
      surface_set_target( surf2 );
          // draw here....    
      surface_reset_target( );
surface_reset_target( );

As you can see, after each SET you need a matching RESET. A few users had been modifying the current surface or one on the stack without realising this does cause issues - not the least of which, once you pop off the current one you'd automatically have to pop the next one in order to have a valid rendering surface, and this means you'd now have an imbalanced stack. So from now on, if you try and do this you will get a code error and you'll need to add code to reset the surface and free it once you've popped it off the stack.

One other thing to remember about surfaces is that they can be lost. This means before drawing you need to check it still exists. This is done simple enough using if( surface_exists( myself ) ) test.

EXAMPLES

Here is a simple example of normal usage.

Create Event

mysurf = -1;

Draw Event

if( !surface_exists(mysurf) ){
    mysurf = surface_create( 128,128 );
}
surface_set_target( mysurf );

// draw here....

surface_reset_target( );

// draw surface here if need be.
// draw_surface( mysurf, x,y );

This is the normal usage for surfaces and would cover most cases. To use a shader to draw the surface you would simply wrap the draw_surface() in a shader_set() and shader_reset(), making it incredibly simple to use a surface to add effects.

Lastly, here's a short example showing the effect of the surface stack.

Create Event

global.SubSurface = -1;
mysurf = -1;

Draw Event

if( !surface_exists(mysurf) ){
   mysurf = surface_create( 128,128 );
}
surface_set_target( mysurf );

// draw here....
var surf = TestCall( );
draw_surface( surf, x,y );

surface_reset_target( mysurf );

TestCall( ) script

if( !surface_exists(global.SubSurface ) ){
   global.SubSurface  = surface_create( 128,128 );
}
surface_set_target( global.SubSurface  );

// draw here....

surface_reset_target( );
return global.SubSurface ;

Here you can see that a surface is set before calling a script, which also sets its own surface. On the exit from the script, it returns the surface ID to the caller and it then draws it to its own (restored) surface. This example shows the surface stack in use and just how important it is to maintain the stack. As you can imagine, if one of these surfaces was freed accidentally the stack would then be unbalanced and executing a surface_reset_target() at this time for an already-freed surface would cause in-game issues.



Written by Mike Dailly

Mike Dailly was the previous head of development at GameMaker, helping us to transform the game engine during his tenure. He was also the lead game programmer and creator of Lemmings and Grand Theft Auto.