Prototyping - Mission Briefing Programming
This is out longest unbroken programming segment thanks to the sheer number of objects and interactions. We're looking at roughly 500 lines a code - a solid day's work for the hobbyist programming.
Let's take a look at the object's we'll be building.
Objects
Our design called for these five objects:
Concept | Object | Notes |
---|---|---|
Briefing Controller | obj_briefing | Loads and holds data for the scene. Spawns child objects as necessary |
Banner | obj_banner | Displays a text-only line in large font centered on the screen |
Setting Splash | obj_scrloader | Displays a full-screen setting for the dialogue sequence including 3 lines of text |
Interstitial Screen | obj_picloader | A partial screen graphic used to show action during dialogue. We're just going to copy the SCR object for now |
Dialogue | obj_dialogue | Manages the speaking between characters. Displays faces, names, and draws dialogue once character at a time. |
GML Code
We'll go through the above objects and a couple of scripts.
obj_briefing: Create 1
Seen this one before? [insert 10th comment about lack of GM multiple inheretence, but hedge away from the dangerous/lazy designs that use it]
1 2 3 4 |
///Register keyboard and set references keyboard_register(id) gamedata = instance_find(obj_currentgame,0) |
obj_briefing: Create 2
We'll use these states in our briefing controller to manage the object creation
1 2 3 4 5 6 7 8 9 |
///Briefing states enum enum BRIEFING { banner, scr, dialogue, pic, fin } |
obj_briefing: Create 3
Let's load the mission titles from the text file that we created by extracting them from the binary (see video)
1 2 3 4 5 6 7 8 9 |
///Load mission titles var file = file_text_open_read("msntitle.txt"); for (var i=1;i<=50;i++) //check for 59 { mission_title[i] = file_text_read_string(file) file_text_readln(file) } file_text_close(file) |
obj_briefing: Create 4
We'll manage the dialogue data with two lists that act as the data holder and the index.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
///Load dialogue dialogue_data = ds_list_create() dialogue_index = ds_list_create() var file, line var line_num = 1 /* Open the right file */ if gamedata.current_mission <= 20 then file = file_text_open_read("DIALOGUE.DAT") else { if gamedata.current_mission <= 40 then file = file_text_open_read("DIALOGU2.DAT") else file = file_text_open_read("DIALOGU3.DAT") } while !file_text_eof(file) { line = file_text_read_string(file) ds_list_add(dialogue_data,line) /* Make dialogue index at the start of each scene - hex 0x06 */ if ord(string_char_at(line,1)) == $06 ds_list_add(dialogue_index, line_num) line_num+=1 file_text_readln(file) } file_text_close(file) |
obj_briefing: Create 5
This is where we load the storyboards for all the missions. For now, we'll set up mission on and add a few more by the end of this series
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
///Briefing storyboard if room = rm_brief { brief[1, 1] = BRIEFING.banner brief[1, 2] = mission_title[1] brief[1, 3] = BRIEFING.scr brief[1, 4] = asset_get_index("sprite11_HANGAR") brief[1, 5] = "Upper hangar" brief[1, 6] = "Traffic Deparmtnet Headquarters" brief[1, 7] = "July 18 – 2192" brief[1, 8] = BRIEFING.dialogue brief[1, 9] = 0 brief[1, 10] = BRIEFING.fin /* Insert more missions here */ } |
obj_briefing: Create 6
Other variables we'll need that track where we are in the storyboard. We'll call a script that we'll define next
1 2 3 4 5 |
///Init variables brief_state = -1 brief_phase = 1 briefing_next() |
Script: briefing_next
Moves the briefing controller to the next important event in the scene. We isolate this activity in a script because it's frequently called by external objects and we want to avoid 'off-by-one' or other such errors as we add objects. Use this safe interface.
1 2 3 4 5 6 7 8 9 10 11 |
/* Go to the next briefing action -- Briefing room only */ var briefing = instance_find(obj_briefing,0); if instance_exists(briefing) { var current_mission = briefing.gamedata.current_mission; /* Briefing phase should always point to the next event */ briefing.brief_state = briefing.brief[current_mission, briefing.brief_phase] briefing.brief_phase+=1 } |
obj_briefing: Destroy 1
Byebye keyboard and adios data structures.
1 2 3 4 5 |
///Unregister keyboard and free data structures keyboard_deregister(id) ds_list_destroy(dialogue_data) ds_list_destroy(dialogue_index) |
obj_briefing: Destroy 2
Clean up any lingering children. This shouldn't be required, but you never know...
1 2 3 4 5 6 7 8 9 10 11 12 |
///Clean up children (if any) with obj_banner instance_destroy() with obj_dialogue instance_destroy() with obj_scrloader instance_destroy() with obj_picloader instance_destroy() |
obj_briefing: Destroy 3
Go to the mission
1 2 |
///Next room room_goto(rm_mission) |
obj_briefing: Step 1
This is where we handle the briefing state to create the child objects and assign their variables from the storyboard.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
///Briefing state transitions switch brief_state { case BRIEFING.banner: { if !instance_exists(obj_banner) { var ban = instance_create(0,0,obj_banner); ban.banner_text = string_upper(brief[gamedata.current_mission,brief_phase]) brief_phase+=1 } } break; case BRIEFING.scr: { if !instance_exists(obj_scrloader) { var obj_scr = instance_create(0,0,obj_scrloader); obj_scr.screen_sprite = brief[gamedata.current_mission,brief_phase] brief_phase+=1 obj_scr.line1 = string_upper(brief[gamedata.current_mission,brief_phase]) brief_phase+=1 obj_scr.line2 = string_upper(brief[gamedata.current_mission,brief_phase]) brief_phase+=1 obj_scr.line3 = string_upper(brief[gamedata.current_mission,brief_phase]) brief_phase+=1 } } break; case BRIEFING.dialogue: { if !instance_exists(obj_dialogue) { var dlg = instance_create(0,0,obj_dialogue); dlg.current_scene = brief[gamedata.current_mission,brief_phase] brief_phase+=1 dlg.current_line = ds_list_find_value(dialogue_index,dlg.current_scene) } } break; case BRIEFING.pic: { if !instance_exists(obj_picloader) { var obj_pic = instance_create(0,0,obj_picloader); obj_pic.screen_sprite = brief[gamedata.current_mission,brief_phase] brief_phase+=1 } } break; case BRIEFING.fin: instance_destroy(); break; } |
obj_briefing: Step 2
Process user input here.
1 2 3 4 5 6 7 8 9 10 |
///Process input if ds_exists(command_stack, ds_type_stack) && !ds_stack_empty(command_stack) { var cmd = ds_stack_pop(command_stack); switch cmd { case "escape": instance_destroy(); break; } } |
obj_banner: Create 1
Keyboard and object references
1 2 3 4 |
///Register keyboard and create references keyboard_register(id) brief = instance_find(obj_briefing,0) |
obj_banner: Create 2
Variables for the banner. These are just placeholders since we set the actual values after the object's constructor runs. We do set a 'just_created' variable to trigger the object to calulate text widths on the first step of it's life.
1 2 3 4 5 6 7 8 9 10 11 12 |
///Init Vars just_created = true text_alpha = 0 fade_in = true fade_out = false /* Drawing variables */ text_x = -1 text_y = -1 banner_text = "" |
obj_banner: Destroy 1
Remove the keyboard...yet...again...
1 2 |
///Deregister keyboard keyboard_deregister(id) |
obj_banner: Destroy 2
Whenver a child object like this banner is destroyed, we must tell the parent to proceed through the storyboard.
1 2 |
///Return control to briefing briefing_next() |
obj_banner: Step 1
Perform the first-step calculations for the banner. In this case, we need to calculate the text width to properly center it. Recall that the text is set remotely by the parent after creation and so we couldn't do this in the constructor. This is the first opportunity. You'll see the pattern several more times, as well as a polymorphic approach using custom events. Not necessary, but this goes with my project theme of solving the same problem in several different ways.
1 2 3 4 5 6 7 8 9 10 |
///Set drawing dimensions on create if just_created { /* Mission title fixed positions */ draw_set_font(global.mission_font) text_x = view_wview[0]/2 - string_width(banner_text)/2 text_y = round(view_hview[0]*0.45) just_created = false } |
obj_banner: Step 2
Banners fade in and out like most other things in the game. Fade outs trigger destroy
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
///Fading if fade_in { if text_alpha < 1 text_alpha += 0.05 else fade_in = false } if fade_out { if text_alpha > 0 text_alpha -= 0.05 else instance_destroy() } |
obj_banner: Step 3
The banner needs to handle keyboard input
1 2 3 4 5 6 7 8 9 10 11 12 13 |
///Process input if ds_exists(command_stack, ds_type_stack) && !ds_stack_empty(command_stack) { var cmd = ds_stack_pop(command_stack); if fade_in | fade_out = 0 { switch cmd { case "any": fade_out = true; break; } } } |
obj_banner: Draw 1
The text is draw in red using our larger mission font and centered based on our previous text size calculations
1 2 3 4 5 6 |
///Draw banner draw_set_alpha(global.mission_font) draw_set_alpha(text_alpha) draw_set_color(c_red) draw_text(text_x, text_y, banner_text) |
obj_scrloader: Create 1
Surprise! The SCR loader also needs to receive commands from the keyboard object
1 2 3 4 |
///Register keyboard and create references keyboard_register(id) brief = instance_find(obj_briefing,0) |
obj_scrloader: Create 2
We have a few variables such as the screen, the line text, and position calculations.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
///Init variables screen_sprite = -1 line1 = "" line2 = "" line3 = "" screen_alpha = 1 fade_in = true fade_out = false /* Fixed text positions */ left_x = round(view_wview[0] * 0.05) line1_y = round(view_hview[0] * 0.83) line2_y = round(view_hview[0] * 0.88) line3_y = round(view_hview[0] * 0.93) |
obj_scrloader: Destroy 1
Deregister the keyboard
1 2 |
///Deregister keyboard keyboard_deregister(id) |
obj_scrloader: Destroy 2
Push the briefing to the next step
1 2 |
///Return control to briefing briefing_next() |
obj_scrloader: Step 1
Fade the SCR object display.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
///Fading if fade_in { if screen_alpha > 0 screen_alpha -= 0.05 else fade_in = false } if fade_out { if screen_alpha < 1 screen_alpha += 0.05 else instance_destroy() } |
obj_scrloader: Step 2
Handle commands pushed on to the stack from the keyboard
1 2 3 4 5 6 7 8 9 10 11 12 13 |
///Process input if ds_exists(command_stack, ds_type_stack) && !ds_stack_empty(command_stack) { var cmd = ds_stack_pop(command_stack); if fade_in | fade_out = 0 { switch cmd { case "any": fade_out = true; break; } } } |
obj_scrloader: Draw 1
Draw the screen and the three text lines. Right now the color and the lines are always the same, but there's actually a few differences from scene to scene in the original game. Also there is more of a shadow outline, while we're going to keep the same shadow concept that we'ved use throughout the project.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
///Render SCR screen draw_set_alpha(1) draw_set_halign(fa_left) draw_set_font(global.little_font) /* Draw Screen */ draw_sprite(screen_sprite,0,0,0) /* Draw text shadows */ draw_set_color(c_black) draw_text(left_x-1, line1_y+1, line1) draw_text(left_x-1, line2_y+1, line2) draw_text(left_x-1, line3_y+1, line3) /* Draw text */ draw_set_color(c_aqua) draw_text(left_x, line1_y, line1) draw_text(left_x, line2_y, line2) draw_text(left_x, line3_y, line3) /* Draw covering black box */ draw_set_alpha(screen_alpha) draw_set_color(c_black) draw_rectangle(0,0,room_width,room_height,false) |
obj_dialogue: Create 1
Keyboard and references
1 2 3 4 |
///Register keyboard and set references keyboard_register(id) brief = instance_find(obj_briefing,0) |
obj_dialogue: Create 2
Create the enum for our states.
1 2 3 4 5 6 7 8 |
///Dialogue states enum enum DIALOGUE { reading, writing, waiting, fin } |
obj_dialogue: Create 3
All the variables we'll need to make the dialogue happen.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
///Initialize variables scene_alpha = 1 dialogue_state = DIALOGUE.reading current_scene = 0 /* passed on create */ current_line = 0 /* set during create in obj_briefing */ /* Variables for scene elements */ face_left = 0 /* sprite index */ face_right = 0 /* sprite index */ name_left = "" name_right = "" code_left = 0 /* dialogue file reference */ code_right = 0 color_left = c_aqua color_right = c_lime active_face = 1 /* 1=left, 2=right */ /* Text buffer line variables */ line_height = sprite_get_height(LT2) * 1.5 line_char_pos = 1 current_line_end = 0 current_line_full = "" current_line_disp = "" /* Text buffer */ for (var i=0; i<=10; i++) { display_text_buffer[i]="" display_color_buffer[i] = c_black } |
obj_dialogue: Destroy 1
Goodbye keyboard
1 2 |
///Deregister keyboard keyboard_deregister(id) |
obj_dialogue: Destroy 2
Move the briefing to the next step
1 2 |
///Return control to briefing briefing_next() |
obj_dialogue: Step 1
Dialogue fading
1 2 3 4 5 6 |
///Fading In only if dialogue_state != DIALOGUE.fin { if scene_alpha > 0 then scene_alpha -= 0.05 } |
obj_dialogue: Step 2
Handle commands from the keyboard. We'll be looking for any key to move to the line and escape to skip the scene.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
///Process input if ds_exists(command_stack, ds_type_stack) && !ds_stack_empty(command_stack) { var cmd = ds_stack_pop(command_stack); if dialogue_state = DIALOGUE.waiting { switch cmd { case "any": dialogue_state = DIALOGUE.reading; break; case "escape": dialogue_state = DIALOGUE.fin; break; } } } |
obj_dialogue: Step 3
Handle all the dialogue states. If we're reading, we check the dialogue file for special control characters or text. This involves matching the first byte and handling each case. If we're writing, we add a character to the drawn line unless it's time to change lines or we reach the end of the line according to the algorithm we discussed in the last part. Finally, if we're waiting, then any user input moves us forward.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
///Dialogue state management switch dialogue_state { case DIALOGUE.reading: { var line, line_len, first_char, second_char; line = ds_list_find_value(brief.dialogue_data,current_line); line_len = string_length(line); first_char = ord(string_char_at(line,1)); /* Primary action code */ second_char = ord(string_char_at(line,2)); /* Secondary action code */ switch first_char { case $01: /* Setting up the scene, names and faces */ { if second_char == 1 /* Set left face info */ { face_left = face_escape(ord(string_char_at(line,3))) code_left = ord(string_char_at(line,4)) name_left = string_upper(string_copy(line,5,line_len-4)) &nbsnbsp;} else /* set right face info */ { face_right = face_escape(ord(string_char_at(line,3))) code_right = ord(string_char_at(line,4)) name_right = string_upper(string_copy(line,5,line_len-4)) } } break; case $02: /* Setting up a text line */ { if ord(string_char_at(line,3)) == code_left { active_face = 1 display_color_buffer[0] = color_left } if ord(string_char_at(line,3)) == code_right { active_face = 2 display_color_buffer[0] = color_right } if line_len < 5 then /* blank line so scroll */ scroll_screen_text_buffer() else /* handle new text line */ { current_line_full = string_upper(string_copy(line,4,line_len-3)) current_line_end = string_length(current_line_full) current_line_disp = "" line_char_pos = 1 dialogue_state = DIALOGUE.writing } } break; case $05: dialogue_state = DIALOGUE.waiting; break; case $64: dialogue_state = DIALOGUE.fin; break; } current_line+=1 } break; case DIALOGUE.writing: { if line_char_pos <= current_line_end { draw_set_font(global.little_font) /* Crazy edge case that reminds me why I hate parsers */ if line_char_pos == 1 && string_length(display_text_buffer[0]) > 1 { /* don't make a space is it's already a space */ if string_char_at(display_text_buffer[0], string_length(display_text_buffer[0])) != " " display_text_buffer[0]+=" " } /* determine if we should start a new line */ if string_char_at(current_line_full,line_char_pos) == " " && string_width(display_text_buffer[0]) > round(view_wview[0] * 0.80) then scroll_screen_text_buffer() else /* keep on printing line */ { display_text_buffer[0] += string_char_at(current_line_full,line_char_pos) line_char_pos+=1 } } else dialogue_state = DIALOGUE.reading } break; case DIALOGUE.fin: { if scene_alpha < 1 then scene_alpha += 0.05 else instance_destroy() } break; } |
obj_dialogue: Draw 1
Draw the whole scene including faces, names, and the dialogue buffer.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
///Render Dialogue Scene draw_set_font(global.little_font) draw_set_halign(fa_left) draw_set_alpha(1) /* Draw text from bottom up */ for (var i=0; i<=10; i++) { draw_set_color(display_color_buffer[i]) draw_text(view_wview[0]*0.03, (view_hview[0] * 0.93)-line_height*i, display_text_buffer[i]) } /* Draw Faces */ draw_sprite(FACES1, face_left,0,0) draw_sprite(FACES1, face_right,view_wview[0]/2,0) /* Draw Names with shadows */ draw_set_halign(fa_center) draw_set_color(c_black) draw_text(view_wview[0]/4-1, round(view_hview[0]*0.45)+1, name_left) draw_text(view_wview[0]*3/4-1, round(view_hview[0]*0.45)+1, name_right) draw_set_color(color_left) draw_text(view_wview[0]/4, round(view_hview[0]*0.45), name_left) draw_set_color(color_right) draw_text(view_wview[0]*3/4, round(view_hview[0]*0.45), name_right) /* Draw inactive face using black rectangle */ draw_set_alpha(0.4) draw_set_color(c_black) if active_face == 2 draw_rectangle(0,0,view_wview[0]/2, view_hview[0]/2, false) else draw_rectangle(view_wview[0]/2,0, view_wview[0],view_hview[0]/2, false) /* Fade scene */ draw_set_alpha(scene_alpha) draw_rectangle(0,0, view_wview[0], view_hview[0], false) draw_set_alpha(1) |
Script: face_escape
We can't directly take the byte value from the file because it's off by one. So we have an interpretation function. We'll see others later on that will be a bit more involved than this.
1 2 3 |
var output = argument0; return --output /* aligns matching face to start by counting from 0 (input assumes 1) |
Script: scroll_screen_text_buffer
This is our inefficient solution for moving through the text buffer. See the last part for more ranting about the virtues of ring buffers versus this linear time solution.
1 2 3 4 5 6 7 8 9 10 11 |
with obj_dialogue { for (var i=10; i>0; i--) { display_text_buffer[i] = display_text_buffer[i-1] display_color_buffer[i] = display_color_buffer[i-1] } /* Clear new line */ display_text_buffer[0] = "" } |