GML Torigara's Room Transitions Revisited

TheouAegis

Member
GM Version: Studio (includes some legacy code examples)
Target Platform: Windows
Download: n/a
Links: n/a

Summary:
I loved Torigara's tutorial on custom room transitions in the legacy versions of Game Maker (GM7, GM8) and when the old forums finally went down, I was sad that this great tutorial would be lost. Fortunately the Wayback Machine had a copy of it, but I still wanted to preserve it. (I think I have the demo file on my flash drive.) I am bringing it back in spite of it being so old because the principles behind it are still applicable in Studio, albeit with a bit more work. These are long reads, so I wrapped them in spoiler tags.

If you have any custom room transitions written for legacy GM, I'd be happy to see them, because I remember not very many people sharing any on the old forums. They're obsolete for the most part now, but I sometimes find obsolete code interesting. 🤓


Part 1: Torigara's GM7 Transitions Tutorial
Introduction
Game Maker 7 provides a way to define your own room transition effect. However, the explanation of the mechanism is very condensed (stuffed into the description of one function) and hard to get. This tutorial describes how to define a script and register it to be used on room transitions.

The first example contains scripts to emulate all of in-built room transitions (plus a few in the Room Transitions extension.) Not so impressive, but you can take basic expressions out of the code to implement your own effects. The second one contains a number of advanced sample scripts those also can be used as a base of your own one. The third example demonstrates other possibility of custom room transitions, such as keeping HUD and player on the screen during the transition. Useful on implementing so-called Zelda-style and Metroid-style room transitions.

Defining a script
At first, you have to define a transition script which is meant to be repeatedly called from Game Maker during the transition. The script is called with five arguments:
  • argument0 is the surface with the image of the previous room.
  • argument1 is the surface with the image of the next room.
  • argument2 and argument3 are the width and height of the surface, respectively. (It is also the size of the drawing region.)
  • argument4 is fraction of the transition. This takes a value between 0 and 1.
No need to [be] afraid of "surface"; as to creating room transitions, you can simply think it as a sort of large sprite. The only difference is that you use functions like draw_surface instead of draw_sprite to draw it on the screen.
So right out of the gate, the tutorial tells us the most important aspect of any room transition is the use of two surfaces -- a surface containing an image of the previous room and another surface containing an image of the current room. We will go into this a bit more later, as there were many things going on behind the curtains of GM7.

The last parameter, fraction, is the most important one. Your responsibility is to mix and draw those two images of rooms onto the screen according to the fraction. At the beginning (when fraction is 0) the first image should occupy the entire screen. As the transition goes on, the script is called over and over with gradually increasing fraction, to mix/interpolate/blend two images accordingly. At the end (when fraction is 1) the second image should occupy the screen.
The other important takeaway from this tutorial is room transitions need some way to track how far along the transition has progressed. This is essentially a percentage in legacy GM (a value of 1 is equivalent to 100% completion), but for a room transition written from scratch in Studio, a simple incremental state machine will suffice. You just need to set aside a variable to keep track of how many steps have passed and your transition script will determine what actions to take based on that.

Torigara then gives a simple example of a transition script. In this example, the new room would expand from the right while the old room would shrink toward the left. Knowing what the outcome of the script will be, look it over and see if you can understand what each line of code is doing.

Code:
// Script for "Squeeze from right" transition

var s_prev, s_next, s_width, s_height, fraction;
s_prev = argument0; // surface with the image of the previous room
s_next = argument1; // surface with the image of the next room
s_width = argument2; // the width of the surface
s_height = argument3; // the height of the surface
fraction = argument4; // fraction of the transition (between 0 and 1)

// Draw the previous room stretched at the left side.
// Its left position is fixed to 0, while width changes from s_width to 0.
draw_surface_stretched(s_prev, 0, 0, s_width * (1 - fraction), s_height);

// Draw the next room stretched at the right side.
// Its left position changes from s_width to 0, while width changes from 0 to s_width.
draw_surface_stretched(s_next, s_width * (1 - fraction), 0, s_width * fraction, s_height);
The last part of the tutorial post just talks about how to assign a custom transition in legacy GM. We can ignore all that, except for the last part.

Using the room transition
...Additionally, you can change the speed of the transition with setting transition_steps. It controls how many times the script is called during transition. The larger the number is, the slower the transition gets and takes longer to complete. (With the default value of 80, it takes about 1.2 seconds.)
The reason I bring up this part of the tutorial is to highlight the flexibility of room transition scripts. In legacy GM, you could actually specify how many frames a room transition would take. This makes simple transition scripts (e.g., fades, slides, squeezes, shutters) quite versatile if coded properly to make use of variable transition speed.

Remember all those arguments the tutorial listed? In legacy GM, you don't define those arguments, the program does. The program would automatically create a surface of the previous room, move to the next room, run the creation code of the next room, create another surface and draw the room to that surface, then call the transition script itself with all the proper arguments during the Draw event. A hidden counter tracked how many frames passed and was divided by the transition_steps variable to calculate argument4. This meant when writing a custom transition script for legacy GM, if you wanted your transition to be very regimental, you needed to calculate what step the transition was actually on. To do this, you could solve argument4*transition_steps. The more robust solution, which is applicable to creating transitions in Studio, is to create your own global variable and use that to count how many steps have passed in the transition.

Part 2: TheouAegis' Castlevania Transition
What attracted people to Torigara's tutorial back then was how it could be applied to games like Legend Of Zelda or Metroid. In Legend Of Zelda, moving to the edge of a room would cause the next room to shove the previous room over. Metroid was similar, except with sprite animations amidst the transition, with later games even having scrolling sprites and black matte. Rookies will typically just create one vast, unwieldy, cumbersome room with a static view and disable everything outside the view. Torigara had examples of both kinds of transitions -- and many more. I built off his Metroid-style transition and wrote a Castlevania-style transition, replete with varied scroll speed, multiple sprite animations, timed audio effects, and instance control (I had never even considered the latter two in GM's room transitions). I've included that script here for further reference (and because I was so proud of it at the time).
Looking at Castlevania transitions, it's clearly a simple Push From Left/Right transition with some stuff happening in the middle. ... It gives us two options, though.

Create dozens of transitional rooms that the player will go to when leaving a room -- which will show the player opening a door, walking through it, then closing it behind him -- and then use another room_goto() to send the player to the next room. You can use transition_kind=15 for both entering and exiting the transitional room to create the same effect as in Castlevania.

or, better yet...

Create custom Push From Left/Right transitions that will draw the walking and door animation as part of the transition itself.


Unfortunately, there is no way to do this with a simple room transition. We will need to take care of a few things outside of the transition in order to make a clean, presentable Castlevania room transition. The following issues will need to be addressed:

The only instances visible during the room transition should be the player and the status bar, if present.
No instances from the next room should be visible.
No instances can be referenced during the transition itself.
The surface contains scaled images if views are used and view_wport is not equal to view_wview.
The duration of the transition depends on how long it takes the player to pass through the door.
Room transitions should run at 60 fps, but it depends on the computer.

The next-to-last bullet is important. Essentially, we will be making a timeline. Everything will be scripted out step-by-step. The demo is based on Castlevania III: Dracula's Curse, with Trevor animated 1 frame every 8 steps and moving 1 pixel per step. If this doesn't match the metrics of your own game, you will need to adjust the script accordingly.
Code:
//Set scale to window_get_region_width()/256 before transition.
//Set py to obj_Belmont.y*scale before transition.
//Set px to (obj_Belmont.x-view_xview)*scale before transition.

if argument4==0
    step = 0;
else
    step += 1;

/* OPTIONAL code if your door doesn't have the black "wall" already
if step >= $8C
{
draw_set_color(nes_0D);
draw_rectangle($70*scale,py-$30*scale,$80*scale,py,0);
}
*/

if step < $0C
{
    draw_surface(argument0,0,1);
    draw_sprite_ext(spr_Belm_walk, 0, px, py, scale, scale, 0, c_white, 1);
}
else
if step < $8C
{
    px          -=  scale;
    draw_surface(argument0, -(step -$0C)*scale,1);
    draw_surface(argument1, argument2-(step -$0C)*scale,1);
    draw_sprite_ext(spr_Belm_walk, 0, px, py, scale, scale, 0, c_white, 1);
}
else
if step == $8C
{
    draw_surface(argument1, 1+(argument2>>1),1);
    draw_surface(argument0,1-(argument2>>1),1);
    draw_sprite_ext(spr_door_rt, 0, $70*scale, py-$30*scale, scale, scale, 0, c_white, 1);
    draw_sprite_ext(spr_Belm_walk, 0, px, py, scale, scale, 0, c_white, 1);
}
else
if step < $96
{
    draw_surface(argument1, 1+(argument2>>1),1);
    draw_surface(argument0,1-(argument2>>1),1);
    draw_sprite_ext(spr_door_rt, 1, $70*scale, py-$30*scale, scale, scale, 0, c_white, 1);
    draw_sprite_ext(spr_Belm_walk, 0, px, py, scale, scale, 0, c_white, 1);
}
else
if step < $C8
{
    if step == $96
        sound_play(snd_DoorSlam);
    draw_surface(argument1, 1+(argument2>>1),1);
    draw_surface(argument0,1-(argument2>>1),1);
    draw_sprite_ext(spr_door_rt, 2, $70*scale, py-$30*scale, scale, scale, 0, c_white, 1);
    draw_sprite_ext(spr_Belm_walk, 0, px, py, scale, scale, 0, c_white, 1);
}
else
if step < $F8
{
    px          +=  scale;
    draw_surface(argument1, 1+(argument2>>1),1);
    draw_surface(argument0,1-(argument2>>1),1);
    draw_sprite_ext(spr_door_rt, 2, $70*scale, py-$30*scale, scale, scale, 0, c_white, 1);;
    draw_sprite_ext(spr_Belm_walk, (step -$C7-1)>>3, px, py, scale, scale, 0, c_white, 1);
}
else
if step < $102
{
    draw_surface(argument1, 1+(argument2>>1),1);
    draw_surface(argument0,1-(argument2>>1),1);
    draw_sprite_ext(spr_door_rt, 2, $70*scale, py-$30*scale, scale, scale, 0, c_white, 1);
    draw_sprite_ext(spr_Belm_walk, 1, px, py, scale, scale, 0, c_white, 1);
}
else
if step < $10C
{
    draw_surface(argument1, 1+(argument2>>1),1);
    draw_surface(argument0,1-(argument2>>1),1);
    draw_sprite_ext(spr_door_rt, 1, $70*scale, py-$30*scale, scale, scale, 0, c_white, 1);  
    draw_sprite_ext(spr_Belm_walk, 1, px, py, scale, scale, 0, c_white, 1);
    if step == $10B
        sound_play(snd_DoorSlam);
}
else
{
    px          -=  scale;
    draw_surface(argument0, scale*-(step -$10B)-argument2/2,1);
    draw_surface(argument1, argument2 - argument2/2 - scale*(step -$10B),1);
    draw_sprite_ext(spr_Belm_walk, 1, px, py, scale, scale, 0, c_white, 1);
    if argument4 == 1
    {
        obj_Belmont.x    =    px;
        obj_Belmont.y    =    py;
    }
}

draw_status(scale);
Ooh, wee! That's some girthy code! It is actually pretty simplistic, though. At its core, it's just a branching state machine. The variables px and py were global variables holding the player's x and y coordinates in the previous room. I honestly don't remember why I handled it this way; it doesn't ultimately matter because it only applies to legacy GM. I also had the global variable counting the number of steps. I also used a variable to keep track of the aspect ratio of the game, because not adjusting for the aspect ratio would adversely affect the sizes of sprites and how fast the room moved (this was for a pixel-perfect game). Now for a breakdown of each state.
  • Nothing happens for the first 12 ($C) steps, it just shows the player standing at the door of the previous room
  • For the next 128 ($80) steps the player stands still while the old room and player shift to the left as the new room appears
  • When both rooms are equally on-screen, a door sprite is drawn, which is then "animated" across many steps
  • At step 150 ($96) the audio for the door is played
  • Starting at step 200 ($C8), the player walks past the door for 64 steps
  • Then the door is animated shut
  • Once again the audio for the door is played at step 267 ($10B)
  • Then the two rooms and inanimate player shift to the left again until the transition ends
  • The player's coordinates are updated
  • The status bar is drawn to the screen (this was before the GUI layer was added)
It's a bit of math and a lot of repetetive code, but the effect is worth it.

Part 3: Modern Problems Require Modern Solutions
Nothing about legacy GM's custom room transitions prevents them from being used in Studio. In fact, Studio makes some things a little easier. However, since room transitions are not an integrated part of Studio, a little extra work is required to implement them.

First off, you will need a persistent "control" or "system" object in your project. This object will handle not just the drawing of the room transitions, but also the flow of the game. Personally, I feel you should always have a control object. In this object, you nwill need to create a few variables: surface_old, surface_new, surface_width, surface_height, transition_step, transition_length, transition_running. You don't have to use those variable names, but they are descriptive enough.

When you are about to change rooms, you need to copy application_surface to surface_old. Unfortunately, this will save the sprites of all the instances into that screenshot as well. Typically this is unwanted, so you will want to disable all instances except the control object and allow the Draw event to cycle at least one time. When you are ready to change rooms, set transition_running to true and transition_step to -2. This will allow you to check that you are trying to transition to another room, but need to still copy the view of the old room. Since you want the Draw event to have run its course, use the Post Draw event to copy the surface and go to the next room. We want the same thing to happen in the new room also, which means you will probably want to either deactivate all instances except the control object inside the Room Start event of the control object, or tell all instances to just be in some inactive state where you don't have to worry about them moving while the transition is running; the latter can be handled by simply checking of the control object's transition_running is true.
NOTE: In a true Metroid game, you do not want deactivated or idle instances, but more on that later.

Code:
if transition_running {
    if transition_step == -2 {
        if !surface_exists(surface_old)
            surface_old = surface_create(surface_get_width(application_surface),surface_get_height(application_surface));
        surface_copy(surface_old, 0,0, application_surface);
        transition_step++;
    }
    else
    if transition_step == -1 {
        if !surface_exists(surface_new)
            surface_new = surface_create(surface_get_width(application_surface, surface_get_height(application_surface));
        surface_copy(surface_new, 0,0, application_surface);
        transition_step++;
    }
}
Drawing the transition itself can be handled in the Draw End event or the GUI event, assuming you have your GUI layer set up properly. I personally prefer the GUI event because you typically don't have to worry about unwanted scaling. Figure out what works best for your project. Whichever event you use, you will want to run the transition until transition_step is equal to transition_length. At that point, set transition_running to false, then you can activate the instances you deactivated.


Status Bars and HUDs
Use the GUI event for status bars. Make things easier for yourself. You won't have to worry about scaling or redrawing the status bar every step. Remember how I had to draw the status bar inside the room transition itself in the Castlevania code? That's because legacy GM didn't have a GUI event. Everything was being drawn to the primary surface, so the status bar was getting copied to both the old and new surface and scrolling over. This created the effect of the status bar looping across the screen unless I willfully redrew the status bar over both surfaces in the transition script. There's no need for that in Studio! I know a lot of you like making UI buttons inside normal Step and Draw events so you can just use the nice little mouse_x and mouse_y variables, but learning to do proper GUI buttons will make room transitions so much simpler for you.


Afterword: Metroid Transitions
Metroid transitions are actually far more complicated than in the example Torigara used in his demo. It's not a simple panning with sprite animations like Castlevania. In a true Metroid transition, instances are still active during the transition, although they are not aggressive yet. This isn't a problem for instances in the new room, since you can just make them invisible, allow them to move as normal, and then draw their sprites at their coordinates offset by the scroll factor. Unfortunately this does not work with he instances in the old room, which are deleted as soon as the room ends. You would need to store a whole bunch of individual instance data in a global array, as well as info regarding the terrain of the old room. This alone is almost reason enough to not use a room transition for a true Metroid game.

However, one idea comes to mind. You would need to save the terrain data for the current screen from the old room into a global array, then use that data to append the terrain to the new room. Do the same with the instances in the old room, but flag them as "temporary". During the transition, let the enemies in the old room move about without being aggressive, and let the enemies in the new room move about without being aggressive. Then when the transition has ended (when transition_step is equal to transition_length), destroy all the "temporary" instances. ...Unfortunately, if you are using tiles, I don't think this method is feasible out of the box in Studio 2 due to how restrictive tilemap functionality is in Studio 2. Unless I'm mistaken, to use this method, you'd have to append one screen width worth of room to each side of each room and have the main part of each room in the middle. This would mean checking coordinates against (0,0) or (room_width,room_height) would have to all be adjusted to (screen_width, screen_height) or (room_width-screen_width, room_height-screen_height). This is not an issue in Studio 1, although tiles aren't explicitly tiles in Studio 1 either.
 
Last edited:
Top