Prototyping - Main Menu Programming
Part writeup and code snippets coming soon.
Objects
We'll be bringing in three new objects and a couple scripts to implement our design from last time.
Concept | Object | Notes |
---|---|---|
Main Menu Controller | obj_mainmenu | Provides the 'gateway' through the game. Displays player interactions with the game menu. |
Loading and Saving | obj_loadsave | Manages player interaction with the loading and saving screen. Includes both display and functional elements. |
Popups | obj_mainpopup | Provides a blocking notification of player intent. |
GML Code
We'll implement our three objects and alter the keyboard object in GML.
obj_mainmenu: Create 1
We'll continue to register the keyboard with most objects.
1 2 |
///Keyboard Register keyboard_register(id) |
obj_mainmenu: Create 2
We'll define a state for each of the menu options
1 2 3 4 5 6 7 8 9 10 11 12 13 |
///Enum enum MAIN { start, save, load, next, help, story, about, order, quit } |
obj_mainmenu: Create 3
We'll manage several variables in the menu, including hardcoding pixel positions for menu options. Normally we wouldn't do this except we know the menu graphic will never change.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
///Init vars select_state = MAIN.start select_alpha = 1 select_option = -1 fade_alpha = 1 fade_out = false fade_in = true /* Hardcoded option y-positions */ select_box_top[MAIN.start] = 30 select_box_top[MAIN.save] = 50 select_box_top[MAIN.load] = 60 select_box_top[MAIN.next] = 80 select_box_top[MAIN.help] = 100 select_box_top[MAIN.story] = 110 select_box_top[MAIN.about] = 120 select_box_top[MAIN.order] = 140 select_box_top[MAIN.quit] = 160 |
obj_mainmenu: Room Start 1
We'll usually grab other object references when the room starts because we can't guarantee creation order until this time.
1 2 |
///Set references gamedata = instance_find(obj_currentgame,0) |
obj_mainmenu: Room Start 2
Not all menu options will be selectable all the time. This is the initial state.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
///Set menu option selectability flags /* Inital flags - no game in progress */ select_box_valid[MAIN.start] = true select_box_valid[MAIN.save] = false select_box_valid[MAIN.load] = true select_box_valid[MAIN.next] = false select_box_valid[MAIN.help] = true select_box_valid[MAIN.story] = true select_box_valid[MAIN.about] = true select_box_valid[MAIN.order] = true select_box_valid[MAIN.quit] = true /* Menu state if a game is in progress */ if gamedata.current_mission > 0 then { select_box_valid[MAIN.save] = true select_box_valid[MAIN.next] = true select_state = MAIN.next } |
obj_mainmenu: Destroy 1
Deregister the keyboard
1 2 |
///Unregister keyboard keyboard_deregister(id) |
obj_mainmenu: Step 1
Here we'll handle the keyboard commands passed to the object stack. Direction keys move the selector and enter or return makes the selection.
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 |
///Process Commands if !ds_stack_empty(command_stack) { var cmd = ds_stack_pop(command_stack); if is_main_active() /* Not blocked by child objects */ { switch cmd { case "push_up": { select_state -= 1 if select_state < MAIN.start select_state = MAIN.quit if select_box_valid[select_state] == false select_state -=1 select_alpha = 0 }; break; case "push_down": { select_state += 1  nbsp; if select_state > MAIN.quit select_state = MAIN.start if select_box_valid[select_state] == false select_state +=1 select_alpha = 0 }; break; case "enter": { select_option = select_state switch select_option { case MAIN.start: { if gamedata.current_mission > 0 then create_popup(2) else fade_out = true } break; case MAIN.save: fade_out = true; break; case MAIN.load: fade_out = true; break; case MAIN.next: fade_out = true; break; case MAIN.help: break; case MAIN.story: break; case MAIN.about: break; case MAIN.order: break; case MAIN.quit: create_popup(1); break; } } break; } } } |
obj_mainmenu: Step 2
Controls the fading, which ultimate changes the room depending on the menu selection.
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 |
///Fading animations /* Main selection fading */ if select_alpha < 1 then select_alpha += 0.05 /* Screen fading */ if fade_in == true fade_alpha -= 0.05 if fade_out == true fade_alpha += 0.05 if fade_alpha < 0 { fade_alpha = 0 fade_in = false } /* Fade out leads to next scene */ if fade_alpha > 1 { fade_alpha = 1 fade_out = false /* next screen */ switch select_option { case MAIN.start: { gamedata.current_mission = 1 room_goto(rm_brief) } break; case MAIN.next: { gamedata.current_mission += 1 room_goto(rm_brief) } break; case MAIN.save: { var inst = instance_create(0,0,obj_loadsave); inst.mode = 1 visible = false } break; case MAIN.load: { var inst = instance_create(0,0,obj_loadsave); inst.mode = 2 visible = false } break; } } |
obj_mainmenu: Draw 1
Render the main menu in layers. Draw the backlight then overlay the sprite.
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 |
///Draw Menu /* draw all backlight rectangles */ draw_set_alpha(1) draw_set_halign(fa_left) draw_set_color(make_color_rgb(64,0,0)) for (var i=MAIN.start; i<=MAIN.quit; i++) { if select_box_valid[i] == true draw_rectangle(0, select_box_top[i], view_wview[0], select_box_top[i]+10, false) } /* Draw select rectangle */ draw_set_color(c_red) draw_set_alpha(select_alpha) draw_rectangle(0, select_box_top[select_state], view_wview[0], select_box_top[select_state]+10, false) /* Draw Menu sprite */ draw_sprite_part_ext(MEN,0,0,50,320,200,0,0,1,1,c_white,1) /* Draw fade effects */ if (fade_in || fade_out) { draw_set_alpha(fade_alpha) draw_set_color(c_black) draw_rectangle(0,0,room_width,room_height,false) } |
Script: is_main_active
This script performs a series of checks to see if the menu is actually. Really we just need to return false if the popup is on the screen.
1 2 3 4 5 6 7 8 |
//Checks various conditions that would make main inactive //Not quite as robust as having a dedicated state for inactive if !instance_exists(obj_mainmenu) return false; if instance_exists(obj_loadsave) return false; if instance_exists(obj_mainpopup) return false; return true |
Script: create_popup
Sometimes selecting an option creates a popup. This script makes that happen.
1 2 |
var inst = instance_create(0,0, obj_mainpopup); inst.popup_option = argument0 |
obj_loadsave: Create 1
Register the keyboard and set refernece objects for interactions.
1 2 3 4 5 |
///Register keyboard & set references keyboard_register(id) gamedata = instance_find(obj_currentgame,0) mainmenu = instance_find(obj_mainmenu,0) |
obj_loadsave: Create 2
Set the variables we'll use. The most important one is the mode that we use to determine whether to save or load.
1 2 3 4 5 6 7 |
///Init vars select_state = 1 mode = 1 /* 1==save, 2==load */ fade_alpha = 1 fade_out = false fade_in = true |
obj_loadsave: Destroy 1
Deregister the keyboard
1 2 |
///Unregister keyboard keyboard_deregister(id) |
obj_loadsave: Step 1
Handle the keyboard commands. Like the menu, we change selection with the arrows and press enter to make the save/load choice. We'll directly save to the file here which normally I wouldn't recommend. But since this is the only interface to saving (no random autosaves,etc) then it really doesn't matter. Lazy speedcoding FTW.
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 |
///Process Commands if !ds_stack_empty(command_stack) { var cmd = ds_stack_pop(command_stack); switch cmd { case "push_up": { select_state -= 1 if select_state < 1 select_state = 10 }; break; case "push_down": { select_state += 1 if select_state > 10 select_state = 1 }; break; case "enter": { if mode == 1 /* Save */ { gamedata.save[select_state,1] = gamedata.current_mission gamedata.save[select_state,2] = gamedata.current_total_kills var file = file_text_open_write("TD.SAV") for (var i=1; i<= 10; i++) { file_text_write_string(file, string(gamedata.save[i,1])) file_text_writeln(file) file_text_write_string(file, string(gamedata.save[i,2])) file_text_writeln(file) } file_text_close(file) fade_out = true } if mode == 2 /* Load */ { if gamedata.save[select_state,1] > 0 then { gamedata.current_mission = gamedata.save[select_state,1] gamedata.current_total_kills = gamedata.save[select_state,2] mainmenu.select_box_valid[MAIN.next] = true mainmenu.select_box_valid[MAIN.save] = true mainmenu.select_state = MAIN.next fade_out = true } } } break; case "escape": fade_out = true; break; } } |
obj_loadsave: Step 2
Fading that looks an awful lot like every other fading routine we've seen. I already wish GameMaker supported multiple inheretence so we could snap in code modules. I won't rail too hard against the engine though. I'm not a big fan of OO design in the first place.
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 |
///Fading animations /* Screen fading */ if fade_in == true fade_alpha -= 0.05 if fade_out == true fade_alpha += 0.05 if fade_alpha < 0 { fade_alpha = 0 fade_in = false } /* fade out leads back to main menu */ if fade_alpha > 1 { fade_alpha = 1 fade_out = false mainmenu.visible = true mainmenu.fade_in = true mainmenu.select_option = -1 instance_destroy() } |
obj_loadsave: Draw 1
Draw all of our save slots. I'm actually going to draw from the bottom up since I have a header. This makes for a very hack slot calculation as you'll see in the code.
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 |
///Draw everything draw_set_font(global.little_font) draw_set_alpha(1) draw_set_halign(fa_left) /* Draw header */ draw_set_color(c_aqua) draw_text(50,3, "GAME NAME") draw_text(200,3, "MISSION") draw_text(250,3, "KILLS") /* draw save slots (Bottom up) */ for (var i=0; i<10;i++) { draw_sprite(SSS,0,0,view_hview[0]-(i*19)) if select_state + i == 10 then draw_set_color(c_white) else draw_set_color(c_red) if gamedata.save[10-i, 1] == 0 draw_text(50, view_hview[0]-(i*19)-11, "--- EMPTY GAME ---") else { draw_text(50, view_hview[0]-(i*19)-11, "SAVED GAME "+string(10-i)) draw_text(215, view_hview[0]-(i*19)-11, string(gamedata.save[10-i,1])) draw_text(255, view_hview[0]-(i*19)-11, string(gamedata.save[10-i,2])) } } /* Fade effects */ if (fade_in || fade_out) { draw_set_alpha(fade_alpha) draw_set_color(c_black) draw_rectangle(0,0,room_width,room_height,false) } |
obj_mainpopup: Create 1
Geting tired of registering the keyboard yet?
1 2 3 4 |
///Register keyboard and set references keyboard_register(id) gamedata = instance_find(obj_currentgame,0) |
obj_mainpopup: Create 2
The popup will need some variables. The most important is probably the option value which determines which message to show. The option number directly ties to the hardcoded y-position of the sprites in the menu sprite sheet.
1 2 3 4 5 6 7 |
///Init vars fade_alpha = 0 popup_option = 0 sprite_top[1] = 250 sprite_top[2] = 282 |
obj_mainpopup: Destroy 1
Deregister the keyboard
1 2 |
///Deregister keyboard keyboard_deregister(id) |
obj_mainpopup: Step 1
We'll need to handle the Yes and No options to leave the popup
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
if !ds_stack_empty(command_stack) { var cmd = ds_stack_pop(command_stack); switch cmd { case "push_y": { if popup_option == 1 then game_end(); if popup_option == 2 then room_goto(rm_brief); } break; case "push_n": instance_destroy(); break; } } |
obj_mainpopup: Draw 1
We'll draw slight fading over the menu and center the banner.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
///Drawing /* Draw fade effect */ if (fade_alpha < 0.5) fade_alpha += 0.05 draw_set_alpha(fade_alpha) draw_set_color(c_black) draw_rectangle(0,0,view_wview[0],view_hview[0],false) draw_set_alpha(1) /* Draw popup sprite */ if popup_option > 0 draw_sprite_part(MEN,0,0,sprite_top[popup_option],320,31,0,view_hview[0]/2-16) |
obj_keyboard: Begin Step 1
We have some buttons to add to our keyboard handler.
1 2 3 4 |
++ if keyboard_check_pressed(ord('Y')) then ds_stack_push(input_stack, "push_y") ++ if keyboard_check_pressed(ord('N')) then ds_stack_push(input_stack, "push_n") ++ if keyboard_check_pressed(vk_enter) || keyboard_check_pressed(vk_return) then ds_stack_push(input_stack, "enter") |