GMS 2 Top down RPG reflecting the sky at night

Tails1000

Member
I'm creating a top down RPG and am planning on having water sources such as lakes reflect the sky above.
This is easy to achieve during the day, I just make a transparent water tile and change my background to be clouds or something.
But it's when day turns to night is where it becomes tricky. I currently have a surface that automatically adjusts it's color depending on the time of day, and when it's night it darkens everything.
I use an additive blend mode to create lights around lanterns and such, but now I want to illuminate the moon being reflected in the water.
I so far made three attempts with no results, and have been unable to find anybody online who has solved a similar problem.
Here is a pic of the results of all 3 attempts and I'll explain my code for each one...
(assets not mine obviously)


So in attempt number 1 I simply tried to add a blend mode my background. I created an object called "obj_back" and added it to my room (underneath the tile layer). The code read...
Code:
---Draw Event---

var lay_id = layer_get_id("Background");
var back_id = layer_background_get_id(lay_id);
gpu_set_blendmode(bm_add);
layer_background_sprite(back_id, spr_moon);
gpu_set_blendmode(bm_normal);
This did literally nothing, so I moved on to attempt #2.

I gave "obj_back" a simple white circle for a sprite and placed it in the lake (Underneath the tile layer of course) and gave it this code...
Code:
----draw event----

gpu_set_blendmode(bm_add);
draw_self();
gpu_set_blendmode(bm_normal);
It drew the circle, and underneath the tile layer... but it provided no illumination. So I moved onto my 3rd attempt.

I went into my lighting object. It controls the surface and is drawing that light around my player. I kept "obj_back" in the same spot but tried to draw the sprite from this object. My lighting object is above my tiles, because that's how I darken them.
I added this code...
Code:
----Draw event----
gpu_set_blendmode(bm_add);
if instance_exists(obj_back){
        with (obj_back) {
            draw_self();
        }
    }
gpu_set_blendmode(bm_normal)
That got me result #3, which is almost what I want... but since I drew it at a depth of 0 it goes on top of the tiles which is exactly what I'm trying to avoid. But when I drew it at a lower depth it didn't illuminate...
Basically I hit a brick wall, I'm not entire sure how to solve this since I'm still pretty inexperienced.

If anyone could offer advice it would be much appreciated. Or if this problem has been solved before I'd love to get pointed in the right direction. Thanks in advance!
 

Tails1000

Member
That idea does make sense, but I don't have the slightest idea how to pull that off. I'm still pretty new to gml.
My ground is made out of tiles and has multiple layers, so I don't know how to draw it using code alone.
Plus my water may be made out of transparent pixels... but they still count as pixels right? That makes me think the surface would still be drawn over them.
I'll share my code for my lighting object, it's long so I'm putting it in a spoiler...
Code:
----Create----
light_surf = surface_create(room_width, room_height);
surface_set_target(light_surf);
draw_clear_alpha(c_white, 0);
surface_reset_target();

//Sky colors
c_day = c_white;
c_sunset = c_orange;
c_twilight = merge_color(c_gray,c_purple,0.3);
c_evening = merge_color(c_gray, c_navy, 0.3);
c_night = merge_color(merge_color(c_black,c_dkgray,0.5), c_navy, 0.2);
c_dawn = merge_color(c_gray,c_blue,0.1);
c_sunrise = merge_color(c_orange,c_white,0.3);

//Starting time
global.seconds = 0;
global.minutes = 0;
global.hours = 0;

global.day = 1;
global.season = 1;
global.year = 1;


time_increment = 50;    //seconds per step
light_color = c_black;    //default light color

//Time periods
enum phase {
    dawn = 4,
    sunrise = 6,
    morning = 7,
    day = 8,
    sunset = 18,
    twilight = 19,
    evening = 20,
    night = 23,
}
Code:
----Step----
#region Phases and variables
    var colors, phase_start, phase_end;

    if (global.hours > phase.dawn and global.hours <= phase.sunrise){                //dawn
        colors        = [c_night, c_dawn];
        phase_start = phase.dawn;
        phase_end    = phase.sunrise;
    } else if (global.hours > phase.sunrise and global.hours <= phase.morning){        //sunrise
        colors        = [c_dawn, c_sunrise];
        phase_start = phase.sunrise;
        phase_end    = phase.morning;
    } else if (global.hours > phase.morning and global.hours <= phase.day){            //morning
        colors        = [c_sunrise];
        phase_start = phase.morning;
        phase_end    = phase.day;
    } else if (global.hours > phase.day and global.hours <= phase.sunset){            //day
        colors        = [c_sunrise, c_day, c_day, c_day, c_day, c_day, c_day, c_day, c_day, c_sunset];
        phase_start = phase.day;
        phase_end    = phase.sunset;
    } else if (global.hours > phase.sunset and global.hours <= phase.twilight){        //sunset
        colors        = [c_sunset];
        phase_start = phase.sunset;
        phase_end    = phase.twilight;
    } else if (global.hours > phase.twilight and global.hours <= phase.evening){    //twilight
        colors        = [c_sunset, c_twilight];
        phase_start = phase.twilight;
        phase_end    = phase.evening;
    } else if (global.hours > phase.evening and global.hours <= phase.night){        //evening
        colors        = [c_twilight, c_evening, c_night];
        phase_start = phase.evening;
        phase_end    = phase.night;
    } else {                                                                        //night
        colors = [c_night];
        phase_start = phase.night;
        phase_end = phase.dawn;
    }
    #region Alter  colors
    
    //Color transition code
    if (phase_start == phase.night){ light_color = colors[0];}
    else {
        var cc = ((global.hours - phase_start) / (phase_end - phase_start))*(array_length_1d(colors)-1);
        var c1 = colors[floor(cc)];
        var c2 = colors[ceil(cc)];
    
        light_color = merge_color(c1, c2, cc-floor(cc));
    }

#endregion
#endregion


//Internal clock

global.seconds += time_increment;
global.minutes = global.seconds/60;
global.hours = global.minutes/60;

if (global.hours >= 24) {
    global.seconds = 0;
    global.day += 1;
    if(global.day > 30){
        global.day = 1;
        global.season += 1;
        if(global.season > 4){
            global.season = 1;
            global.year += 1;
        }
    }
}
Code:
-----Draw-----
if !surface_exists(light_surf){                                    //If the Surface DOESN'T exist then...
    light_surf = surface_create(room_width,room_height)            //Creating (not drawing) the surface
    surface_set_target(light_surf);
    draw_clear_alpha(c_white,0)                                    //Clearing old surfaces
    surface_reset_target();                                        //Standard surface reset
} else {
    if (view_current == 0) {
        
        gpu_set_blendmode_ext(bm_zero,bm_src_colour);            //Multiply blendmode
        draw_surface(light_surf, 0, 0);                           
        gpu_set_blendmode(bm_normal);
    }
    surface_set_target(light_surf);
    
    //set the darkness
    draw_set_color(light_color);                                //color of the rectangle
    draw_set_alpha(1);                                            //alpha of the rectangle
    draw_rectangle(0, 0, room_width, room_height, false);        //the rectangle
    
    //draw the circles
    if(global.hours >= 20 or global.hours < 6) {                //Nighttime timer
        gpu_set_blendmode(bm_add);                                //set blendmode
        with (obj_player4) {                               
            draw_sprite_ext(spr_light4, 0, x ,y-10, 1, 1, 0, c_white, 0.8)   
        }
        with (obj_lamp) {
            draw_sprite_ext(spr_light4, 0, x+15 ,y+50,
                            choose(1,1.01,0.99,1), choose(1,1.01,0.99,1), 0,
                            merge_color(c_orange,c_orange,0.1),
                            choose(0.8,0.75,0.7,0.85,0.9,0.8,0.8,0.8))           
        }
    }
    
    //reset draws
    gpu_set_blendmode(bm_normal);
    draw_set_alpha(1);
    surface_reset_target();
}

I also have still been looking around and found someone with a similar problem:
https://www.reddit.com/r/gamemaker/comments/59u6hd/how_to_draw_background_without_affected_by_light/
One of the suggestions was to exclude a certain depth from the surface. But looking around I couldn't figure out how to do that either.

Thank you for the reply, but yeah I'm still kinda confused as to what to do next :confused:
 

Tails1000

Member
Posting an update. I've made 2 more attempts using the advice I've gotten. Though unfortunately things haven't been solved yet...
I'll post a pic of my results and the code I used to get them...


So on attempt #4 I tried drawing all the tiles and objects I'm using to the lighting surface. I figured out how to draw tiles, but the surface still drew over all the transparent pixels :/
This is the code I used, but I may have done it wrong...

Code:
----Draw----

if !surface_exists(light_surf){                                    
    light_surf = surface_create(room_width,room_height)            
    surface_set_target(light_surf);
    draw_clear_alpha(c_white,0)                             
    surface_reset_target();                                        
} else {
    if (view_current == 0) {  
        tileMapA = layer_tilemap_get_id(layer_get_id("Tiles_1"));
        tileMapB = layer_tilemap_get_id(layer_get_id("Tiles_2"));
        draw_tilemap(tileMapA,x,y);
        draw_tilemap(tileMapB,x,y);
   
        with (obj_player4) {
            draw_self()
        }
        with (obj_tree) {
            draw_self()
        }
        with (obj_lamp) {
            draw_self()
        }
        gpu_set_blendmode_ext(bm_zero,bm_src_colour);            
        draw_surface(light_surf, 0, 0);                          
        gpu_set_blendmode(bm_normal);
       
    }
    surface_set_target(light_surf);
For attempt #5 I took Sumbeard's advice and tried using a mask. The result is actually the closest I've gotten to my goal! But as you see in the pic it's not quite right...
I'll post the code then explain...
Code:
if !surface_exists(light_surf){                                  
    light_surf = surface_create(room_width,room_height)          
    surface_set_target(light_surf);
    draw_clear_alpha(c_white,0)                          
    surface_reset_target();                                        
} else {
    if (view_current == 0) {
       
        gpu_set_blendenable(false)
        gpu_set_colorwriteenable(false,false,false,true)
        draw_set_alpha(0)
        draw_rectangle(0,0,room_width,room_height,false)
        draw_set_alpha(1)
        tileMapA = layer_tilemap_get_id(layer_get_id("Tiles_1"));
        tileMapB = layer_tilemap_get_id(layer_get_id("Tiles_2"));
        draw_tilemap(tileMapA,x,y);
        draw_tilemap(tileMapB,x,y);
   
        with (obj_player4) {
            draw_self()
        }
        with (obj_tree) {
            draw_self()
        }
        with (obj_lamp) {
            draw_self()
        }
   
        gpu_set_blendenable(true)
        gpu_set_colorwriteenable(true,true,true,true)
        gpu_set_blendmode_ext(bm_dest_alpha,bm_inv_dest_alpha)
        gpu_set_alphatestenable(true)
        draw_surface(light_surf, 0, 0);  
        gpu_set_alphatestenable(false)
        gpu_set_blendmode(bm_normal)
    }
    surface_set_target(light_surf);
So in order to get this to work I had to ditch my multiply blendmode (Old code in previous post). As a result the color is opaque :/
I tried to squeeze in the multiply blendmode in different spots but the result wasn't right.
Also now the surface ignores all transparent pixels... which is what I wanted! But that also includes the transparent pixels around my player and other objects... which I'm not looking for.
But this really is as close to success I've gotten so far. I'm going to keep playing around with things to try and crack this thing :p

Thank you for posting advice Sumbeard! I think that mask idea is the step forward I needed!
 
C

CreatorAtNight

Guest
I think if you would draw everything like you had it at the beginning on a new surface and put it on top of the second code it would look good.
 

Tails1000

Member
I think if you would draw everything like you had it at the beginning on a new surface and put it on top of the second code it would look good.
Hey I never thought to use 2 surfaces, I gave it a try and it did work! Sorta...
I still need to iron some kinks in this code, but here are the results and the code I used...

Code:
----draw----
if !surface_exists(light_surf){                                  
    light_surf = surface_create(room_width,room_height)          
    surface_set_target(light_surf);
    draw_clear_alpha(c_white,0)                                  
    surface_reset_target();                                      
} else {
    if (view_current == 0) {
        gpu_set_blendmode_ext(bm_zero,bm_src_colour);          
        draw_surface(light_surf, 0, 0);                          
        gpu_set_blendmode(bm_normal);
    }
    surface_set_target(light_surf);
   
if !surface_exists(light_surf2){                                  
    light_surf2 = surface_create(room_width,room_height)          
    surface_set_target(light_surf2);
    draw_clear_alpha(c_white,0)                                  
    surface_reset_target();                                  
} else {
    if (view_current == 0) {
        gpu_set_blendenable(false)
        gpu_set_colorwriteenable(false,false,false,true)
        tileMapA = layer_tilemap_get_id(layer_get_id("Tiles_4"));
        draw_tilemap(tileMapA,x,y);
        with (obj_player4) {
            draw_self()
        }
        with (obj_tree) {
            draw_self()
        }
        with (obj_lamp) {
            draw_self()
        }
   
        gpu_set_blendenable(true)
        gpu_set_colorwriteenable(true,true,true,true)
        gpu_set_blendmode_ext(bm_dest_alpha,bm_inv_dest_alpha)
        gpu_set_alphatestenable(true)
        draw_surface(light_surf2, 0, 0);  
        gpu_set_alphatestenable(false)
        gpu_set_blendmode(bm_normal)
        surface_reset_target()
    }
    surface_set_target(light_surf2);

   
    draw_set_color(light_color);                          
    draw_set_alpha(1);                                          
    draw_rectangle(0, 0, room_width, room_height, false);

surface_reset_target()
draw_set_alpha(1);
    surface_reset_target();
So the issues I still need to fix:
  • Objects with transparent pixels still are unaffected by the darkness. The player is kinda an exception since his transparent pixels update when he moves, so he only ends up looking weird when he stays still for too long.
  • When my player walks over the water at night he drags darkness with him like a paintbrush. If I walk over that same spot during daytime it'll erase the darkness
  • Framerate took a big hit, not sure if its because of my code or just the fact that it's 2 surfaces.
  • When I minimize my game I get the error: "Unbalanced surface stack. You MUST use surface_reset_target() for each set." I tried to fix this on my own with no luck, but I'll keep trying.
Looks like one step forward 2 steps back, but I'll keep messing around with it to see what I can do. Thanks again for your aid!
 

Tails1000

Member
Bumping with an update since I managed to fix some of issues I had before. It's looking almost perfect, though unfortunately that pesky smearing still exists...

I modified my code by using both the Step event and Draw End event now. Also the transparency glitch from earlier was caused by the gpu_set_blendenable(false) I had set up. Took that out and things are looking pretty good!
Code:
----Step----

if (surface_exists(light_surf)){
   
} else {
    light_surf = surface_create(room_width, room_height);
    surface_set_target(light_surf);
    draw_clear_alpha(c_white, 0);
    surface_reset_target();  
}

if (surface_exists(light_surf2)){
    surface_set_target(light_surf);
    gpu_set_colorwriteenable(false,false,false,true)
    tileMapA = layer_tilemap_get_id(layer_get_id("Tiles_4"));
    draw_tilemap(tileMapA,x,y);
    with (obj_light_parent) {
        draw_self()
    }
    gpu_set_colorwriteenable(true,true,true,true)
    surface_reset_target();
} else {
    light_surf = surface_create(room_width, room_height);
    surface_set_target(light_surf);
    draw_clear_alpha(c_white, 0);
    surface_reset_target();
}

surface_set_target(light_surf2);

   
    draw_set_color(light_color);                          
    draw_set_alpha(1);                                          
    draw_rectangle(0, 0, room_width, room_height, false);

draw_set_alpha(1);
    surface_reset_target();
Code:
----Draw End----

if surface_exists(light_surf){                                  
    gpu_set_blendmode_ext(bm_zero,bm_src_colour);          
    draw_surface(light_surf, 0, 0);                          
    gpu_set_blendmode(bm_normal);
} else {
    light_surf = surface_create(room_width, room_height);
    surface_set_target(light_surf);
    draw_clear_alpha(c_white, 0);
    surface_reset_target();
}

if surface_exists(light_surf2){                                  
    surface_set_target(light_surf);
    gpu_set_blendmode_ext(bm_dest_alpha,bm_inv_dest_alpha)
    gpu_set_alphatestenable(true)
    draw_surface(light_surf2, 0, 0);  
    gpu_set_alphatestenable(false)
    gpu_set_blendmode(bm_normal)
    surface_reset_target();
} else {
    light_surf2 = surface_create(room_width, room_height);
    surface_set_target(light_surf2);
    draw_clear_alpha(c_white, 0);
    surface_reset_target();
}

Lag has been reduced but still pops up from time to time while playing. I'm honestly not sure where to start trying to fix that graphical bug, but if I find a solution I'll post it here
 
C

CreatorAtNight

Guest
Good job!
Are you clearing your surfaces at the beginning of each step? That might help. : )

Code:
surface_set_target(surfaceID);
draw_clear_alpha(c_white,0);
surface_reset_target();
I do this for all my surfaces.

The way I do it is, I create the surfaces in my obj_controller, which is the first object in my room (so the first that get's executed) and then I draw all the surfaces in the draw event after which I clear all of them. Then everything that get's drawn onto them by other object will be executed after that.
 
Top