Saturday, April 29, 2017

Creating cutscenes for a 2D game

    If you've read some of the more technical posts so far, you'll know by now that Gleaner Heights is being developed in Game Maker: Studio. As such, ideas below will be expressed with that specific program in mind, but this doesn't mean they can't be extrapolated for application in some other game creation medium.


    So...cutscenes. A farming sim sure has quite a number of them. You have the intro. Then maybe the Mayor waits outside your house to inform you about an upcoming town festival. Then maybe you have another cutscene in said festival. Then maybe you have a scene or two with your romantic interest[*]If you think that's a lot of maybes, multiply them by 2 and that's how many cutscenes you will end up with.. Due to the nature of the game, one or more cutscenes may play consecutively. For example:
  • You go to bed to sleep.
  • In the morning, you are teleported outside your house (with a screen fade out/fade in transition). The carpenter informs you that your barn upgrade is complete. The screen fades out.
  • The screen fades in again, and you are still outside, because the mayor came to remind you that the Snail Marathon event is taking place tomorrow. The screen fades out again.
  • You are finally back at home, eating your breakfast (the default scene when waking up).


    Therefore, during cutscenes, your location may change a lot. The screen fades in and out, and you have to be extra careful not to mess things up. I have found out that it helps to consider each cutscene to be as much autonomous as possible, even if it is always preceded or followed by another cutscene. By autonomous, I mean that a single scene should adhere to the following rules:
  1. In the beginning, the screen should fade out. If it is already "black" (due to a prior scene or transition), skip this step.
  2. Each cutscene specifies the starting location where it will play, as well as the player location when it ends. This means that we do not depend on the previously played cutscene to set our current location.
  3. Global game parameters, such as whether the game is paused, the GUI is shown etc, are, again, handled by each cutscene individually.
    These may sound like "duh!" guidelines, but believe me, if you stick to them, testing cutscenes becomes much easier. If you write cutscenes A and B in such a way that the game is paused by scene A and unpaused by scene B, and then later on decide to use only scene B in a separate occasion, the game will not be paused during scene B, because it depended on A for that. For example, going to bed is scene A and waking up eating a donut is B. You may not immediately see where scene B could be played autonomously, but actually it can be used outside of scene A's context, such as after passing out and being transported to the clinic.


    So how exactly do we program a scene? I am using the following way: A cutscene object (different for every scene) is created. This object is persistent, ie it "survives" between room transitions[*]At last, I've found a use for persistent objects, which I avoided using so far.. In the Create event, the instance specifies the initial settings, such as pausing the game etc. Then there's a "phase" instance variable, tracking down the phase of the scene. Then, in the Step event, a (rather large but quite readable) switch statement handles each phase. Also, I set an alarm event (for alarm[0]) to increment the phase by 1 when triggered. Why? Well, suppose you are at phase number 7, and you want to wait 30 steps before executing the next bunch of stuff in your scene. At the end of phase 7, you increment the phase by 1, and then set alarm[0] to 30. Then, we specify that the next phase check in our switch statement is for phase 9 (not 8). What happens?
  • We are at phase 7. Increment phase by 1, then set alarm[0] to 30.
  • For subsequent steps, the phase is 8, but the switch statement finds nothing, because we haven't specified anything for a phase of 8! So we wait...
  • 30 steps pass and alarm[0] event is triggered, incrementing the phase by 1.
  • We are at phase 9. Switch statement has a case for it, and we proceed.
    Finally, because the cutscene instance is persistent, at the end of our scene, we shouldn't forget to destroy it, even if we go to another room! Below is a short example of the innards of the switch statement in action:

1:    case -9:
2:        id.mainchar=instance_create(1063,1210,obj_dummy);
3:        with id.mainchar {
4:            sprite_index=charsprite("idle",c_right);
5:            image_speed=0;
6:            visible=false;
7:        }
8:
9:        id.movestatus=c_movestatus_idle;
10:        
11:        id.woodman=instance_create(1088,1210,obj_dummy);
12:        with id.woodman {
13:            sprite_index=spr_carpenter_walk_left;
14:            image_speed=0;
15:        }      
16:        
17:        id.camera=instance_create((id.mainchar.x+id.woodman.x)/2,id.mainchar.y,obj_dummy);
18:
19:        alarm[0]=10;
20:        id.phase+=1;
21:        break;
22:        
23:    case -7:
24:        fadein(60);
25:        id.phase=4;
26:        break;
27:        
28:    case 4:
29:        if global.fade_alpha=0 then {
30:            message_actor(name,-1,getstring("WOODMAN","ActrKainHouseReady1"));
31:            id.phase+=1;
32:        }
33:        break;
34:
35:    case 5:
36:        if nomessages() then {
37:            alarm[0]=20;
38:            id.phase+=1;
39:        }
40:        break;
41:        
42:    case 7:
43:        with id.camera y+=-1;
44:        if id.camera.y<=1150 then {
45:            alarm[0]=30;
46:            id.phase+=1;
47:        }
48:        break;
49:
50:    case 9:
51:        message_actor(name,-1,getstring("WOODMAN","ActrKainHouseReady2"));
52:        id.phase+=1;
53:        break;  

    I usually use negative phase numbers in the "setup" phases of the scene, ie when I change rooms, create dummy instances for actors etc. But this is just me.


    It usually pays off to break down your scene in lots of small phases. In one phase, you can display a message. In the next one, an actor moves, in the next one, another message. And so on. To be honest, I haven't read (yet) about a more efficient way to go about it. But if you're careful, this way works! In general, using a phase switch has other useful applications, such as determining an enemy's behavior. Each phase corresponds to an action the enemy does, like move, attack, burrow, unburrow etc. At the end of each phase we evaluate if we should jump to another one. It's much cleaner and more efficient than massive nested if blocks.

    Cutscenes aside, I finished the night illumination of building exteriors. I have found that Photoshop's "Hard Light" mode (simulated with a shader) gives pretty good results when it comes to lighting, at least for the style I'm going.


    I also did some research on "true" low-res rendering. You see, in, Game Maker: Studio, when you render a 384 x 216 view to a 1920 x 1080 surface (5x scale), and your x coordinate is something like 207.8 (because you can move at fractional speeds) , the engine multiplies x by 5 and renders your sprite at a x coord of 1039. But if it was a true 384 x 216 game, the only possible locations to render your sprite near that x coordinate would be either 207 x 5 = 1035 or 208 x 5 = 1040 [*]Because when you have 384 pixels for your horizontal resolution, there's really no  such thing like a 207.8 pixel.. The good news is that, in this way, the dreaded staircase effect of low-res, low-speed diagonal movement is greatly reduced. The bad news is that this extra resolution appears when rotating/scaling stuff, and as a result you can end up with various sprites drawn at different resolutions within your game:

Note the tool icons in the left column. They are being rendered at double the resolution of everything else, because their sprites are being drawn at half their original size. The illusion of a low-res game is broken.
    After some searching, I failed to find a satisfactory solution, so I managed to come up with a way that guarantees smooth diagonal movements at whatever speeds. It really works, promise! But, for the moment, I'm using the default hi-res approach, since the low-res one currently messes with my character's collision system and I have to tweak it to conform to the new...errr...way of moving. I should be able to fix it in the future, but it's not a priority right now.

2 comments:

  1. Various latest technologies such as AR and VR are integrated in the game development for mobile to give a better experience and take the gaming level to a whole new level. From 2D game development to 2D advanced games, technologies have drastically changed. Previously, game development for Android came into existence as iOS had limitations. But today’s era has changed. There are thousands of games in the industry, both for Android and iOS users.

    ReplyDelete