Prototyping Top-Level Design & Programming
We'll get started with development by addressing the high level concepts including rooms, and objects that provide control throughout the entire game.
Rooms
Back in part 2, we played through the first mission and broke down the game flow in to a series of distinct segments. These segments are analagous to rooms in GameMaker, so we'll create a room for each with the handle rm_*. We'll set the dimensions of each room to roughly 3 size of the original game and create views that automatically scale to fit. Our rooms will be 960x600 with a frame rate of 60. Again, here are the 5 rooms we need to define.
Objects
Our design includes three objects that persist through all the rooms of the game.
Keyboard Interface - obj_keyboard
This is a persistent object that reads the input at the beginning of each step and pushes appropriate commands to game objects that are listening.
Music Controller - obj_music
A persistent object that stops and starts music based on the current game room
Current Game - obj_currentgame
A persistent object that holds the data of the currently active game. It will also hold data on inactive (saved) games.
Here is the outline of our GameMaker objects
GML Code
For each of the following objects, we'll add code to one or more of the listed events:
obj_currentgame: Create 1
This is the cosntructor event. We'll declare important data that's reference during the game and we'll also load the save data from a file. Note that the save game data only consists of a mission number and a kill count. We've created our own save game file, TD.SAV.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
///Game Vars /* Init - No game active */ current_mission=0 current_mission_kills=0 current_total_kills=0 /* Save & Load */ var file = file_text_open_read("TD.SAV") for (var game=1; game<=10; game++) { /* mission */ save[game,1] = real(file_text_read_string(file)); file_text_readln(file) /* kills */ save[game,2] = real(file_text_read_string(file)); file_text_readln(file) } file_text_close(file) |
obj_keyboard: Create 1
The constructor for the keyboard event simply creates two empty data structures. The input_stack will contain the relevent input for the current frame. The target_list contains handles to other game objects that the keyboard object will push commands to. By design, we'll make listen objects register themselves with they keyboard object on initialization.
1 2 3 |
///Init input_stack = ds_stack_create() target_list = ds_list_create() |
obj_keyboard: Begin Step 1
The first thing the keyboard object does every frame is to clear the stack and push a 'command' based on the current input. This is where we would consider adding a separate lookup map that binds keys to commands from a configuration file. As it stands, our design hard codes keys to commands so the user has no ability to change bindings. The original Traffic Department didn't have this feature either.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
///Get Keyboard Input if !ds_stack_empty(input_stack) ds_stack_clear(input_stack) //Holds if keyboard_check(vk_up) then ds_stack_push(input_stack, "hold_up") if keyboard_check(vk_down) then ds_stack_push(input_stack, "hold_down") if keyboard_check(vk_left) then ds_stack_push(input_stack, "hold_left") if keyboard_check(vk_right) then ds_stack_push(input_stack, "hold_right") if keyboard_check(vk_space) then ds_stack_push(input_stack, "fire") //Presses if keyboard_check_pressed(vk_up) then ds_stack_push(input_stack, "push_up") if keyboard_check_pressed(vk_down) then ds_stack_push(input_stack, "push_down") if keyboard_check_pressed(vk_left) then ds_stack_push(input_stack, "push_left") if keyboard_check_pressed(vk_right) then ds_stack_push(input_stack, "push_right") if keyboard_check_pressed(vk_space) then ds_stack_push(input_stack, "space") if keyboard_check_pressed(vk_anykey) then ds_stack_push(input_stack, "any") |
obj_keyboard: Begin Step 2
Now that we have a stack full of commands, we should distribute them to all objects that registered to listen with the keyboard object. If this were a large project, we could refactor our design to include multiple lists segmenting objects by type so that we aren't pushing commands that will never be used by certain objects.
1 2 3 4 5 6 7 8 9 10 11 |
///Distribute Input to Objects while !ds_stack_empty(input_stack) { var input = ds_stack_pop(input_stack) for (var i=0; i<ds_list_size(target_list); i++) { var inst = ds_list_find_value(target_list,i) ds_stack_push(inst.command_stack, input) } } |
obj_keyboard: Room End 1
When a room ends, the keyboard object should flush the object references. All of them are destroyed by this time anyway.
1 2 |
///Clear Registered Instances ds_list_clear(target_list) |
obj_music: Create 1
The constructor for our music controller only initializes two handles.
1 2 3 |
///Init current_music = 0 current_sound = 0 |
obj_music: Room Start 1
We should kick off the appropriate music depending on the room that starts. Note that current_music contains the handle to the playing instance so it can be referenced at any time.
1 2 3 4 5 6 7 8 9 |
///Start music switch room { case rm_intro: current_music = audio_play_sound(mus_intro,0,false); break; case rm_mainmenu: current_music = audio_play_sound(mus_mainmenu,0,false); break; case rm_brief: case rm_debrief: current_music = audio_play_sound(mus_briefing,0,false); break; case rm_mission: current_music = audio_play_sound(mus_gameplay,0,false); break; } |
obj_music: Room End 1
All sound and music should cease when a room ends
1 2 |
///Stop all sounds audio_stop_all() |