Prototyping - Loading NPC Ships
We'll load the NPC ships in to the game, which includes assigning their mission path from th game data files. We start by converting the mission data file to a text-friendly format like we did with the maps. Then we create an object that links to each ship. Finally, we load the mission path in to the AI so that the ships will follow their path. By the end of this part, we'll have all the NPC ships in the world and following their scripted task.
Objects
We'll use two new objects in this part. First is an AI specific object that ties to ships. Second is a controller-like object that will have several duties by the end of the prototype
Concept | Object | Notes |
---|---|---|
NPC AI | obj_ship_ai | Each ship gets an associated AI object (even the player ship). This object will pass control commands to the ship like the player does with the keyboard. We'll only make the passive path-following AI for now. |
Headquarters | obj_hq | A multipurpose object that we'll use to control the mission. We'll give it the task of loading the NPC ships from the mission data files. |
C Code
The mission files have the same problem with ASCII reading. Some of the byte values can't be accurately read as text. We also can't simply shift the byte values up by 0x20 because the byte values are too diverse. They cover both the C0 and C1 control space. We'll use the following program to make the data in to multi-byte format that can be easily read. We'll have to undo the changes in GameMaker later. See the video for more detailed explanations.
NEWMSN.c:
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 |
/* Shifts Traffic Department 2192 Mission file data out of C0 and C1 * argv[1] points to input mission file */ #include <stdio.h> #include <string.h> #include <stdint.h> void write_utfsafe_byte(uint16_t new_byte, uint8_t shift, FILE *fout) { fputc(shift, fout); fputc(new_byte, fout); fputc('\r', fout); fputc('\n', fout); } int main(int argc, char* argv[]) { uint8_t FILENAME_LEN; char fname_out[12]; FILE *fin; FILE *fout; uint16_t current_byte; uint8_t shift; uint8_t old_byte; uint16_t expanded_byte; uint8_t new_byte; /* Make output file name based on input name */ FILENAME_LEN = strlen(argv[1]); strncpy(fname_out, argv[1], FILENAME_LEN); fname_out[FILENAME_LEN-3]='M'; fname_out[FILENAME_LEN-2]='S'; fname_out[FILENAME_LEN]='\0'; /* Open Mission Files */ fin = fopen(argv[1],"rb"); fout = fopen(fname_out,"wb"); /* Process file sequentially */ for (current_byte=0x00; current_byte < 0x4e70; current_byte++) { shift=0x20; old_byte = fgetc(fin); /* Ensure no bytes in C0 */ expanded_byte = old_byte+0x20; /* shift andcount bytes between 0x20 and 0x7f */ while (expanded_byte > 0x7f) { expanded_byte-=0x60; shift+=0x01; } /* expanded byte should be valid */ new_byte = expanded_byte; /* Write ship types */ if (current_byte >= 0x28 && current_byte <= 0x3b) write_utfsafe_byte(new_byte, shift, fout); /* Write x coordinate for 20 ships */ if (current_byte >= 0x50 && current_byte <= 0x275f) write_utfsafe_byte(new_byte, shift, fout); /* Write y coordinate for 20 ships */ if (current_byte >= 0x2760 && current_byte <= 0x4e70) write_utfsafe_byte(new_byte, shift, fout); } fclose(fin); fclose(fout); return 0; } |
GML Code
We have to make it though two new objects this time along with some scripts.
obj_ship_ai: Create 1
We're not registering a keyboard for once! We'll start with some state enumerators for the AI object.
1 2 3 4 5 6 7 8 9 10 |
///AI enums enum AIMODE { inactive, move, path_seek, hunt, fire, finished } |
obj_ship_ai: Create 2
We'll have some variables including AI mode, associated ship, and tracking information for the world. The AI object starts in the Move mode.
1 2 3 4 5 6 7 8 9 10 |
///Init vars mode = AIMODE.move mission_complete = false /* Did the AI complete their mission? */ next_path_position=0 agent = 0 /* The SHIP that the AI controls */ player_ship = find_player_ship() target = 0 ai_timer = irandom_range(1,30) |
obj_ship_ai: Create 3
We'll need some data structures to support the paths and A* algorithm. Note that in the video, we didn't put the A* components in during the video, but I'll put the code in here that we backfill in the AI video.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
///Path structures /* Fixed paths from the mission file */ for (var i=0;i<500;i++) { fixed_path_x[i]=-1 fixed_path_y[i]=-1 } /* A* support */ astar_goal_x = 0 astar_goal_y = 0 astar_frontier_pq = ds_priority_create() /* Reachable map points, but not checked */ astar_closed_list = ds_list_create() /* Nodes already checked */ astar_relation_map = ds_map_create() /* Holds relationship between checked nodes and parents */ astar_cost_map = ds_map_create() /* Holds cost values on the frontier */ astar_path_stack = ds_stack_create() /* A* output - remaining path */ ai_next_x = -1 ai_next_y = -1 |
obj_ship_ai: Room Start 1
Set our single reference
1 2 3 4 5 |
///Start Init if agent.is_player mode = AIMODE.inactive player_ship = find_player_ship() |
obj_ship_ai: Room Start 1
Check if the AI is attached to the player, if so, make it inactive. No sense having the player battle for control of the ship from the AI.
1 2 3 4 5 |
///Start Init if agent.is_player mode = AIMODE.inactive player_ship = find_player_ship() |
obj_ship_ai: Begin Step 1
If the AI is in move mode, compare the current location with the path to determine which way to go.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
///Move AI Update if mode == AIMODE.move && agent > 0 { var diff_map_x = fixed_path_x[next_path_position] - agent.map_x; var diff_map_y = fixed_path_y[next_path_position] - agent.map_y; if diff_map_x > 0 then ds_stack_push(agent.command_stack, "ai_right") if diff_map_x < 0 then ds_stack_push(agent.command_stack, "ai_left") if diff_map_y < 0 then ds_stack_push(agent.command_stack, "ai_up") if diff_map_y > 0 then ds_stack_push(agent.command_stack, "ai_down") if diff_map_x == 0 && diff_map_y == 0 next_path_position++ if fixed_path_x[next_path_position] <= 0 && fixed_path_y[next_path_position] <= 0 mode = AIMODE.finished } |
obj_ship_ai: Begin Step 2
If the AI has completed the path, then remove the ship from play.
1 2 3 4 5 6 7 8 9 10 11 |
///Finished AI Update if mode == AIMODE.finished { mission_complete = true if agent > 0 { with agent instance_destroy() } } |
obj_hq: Create 1
Set variables for the overall mission, player, and ship handles.
1 2 3 4 5 6 7 8 9 10 11 12 |
///Init vars /* Overall mission variables */ night = false player_kills = 0 player_ship_type = SHIPTYPE.hornet /* ship handles */ player = 0 /* deferred creation */ for (var i=0;i<21;i++) npc_ship[i] = 0 |
obj_hq: Room Start 1
The usual references
1 2 3 |
///Set references gamedata = instance_find(obj_currentgame,0) hud = instance_find(obj_hud,0) |
obj_hq: Room Start 2
This is where we load the ships by reading our data file, to create the ships, then assigning their paths. We'll do the actual ship creation in another script that we'll call here.
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 |
///Load Ships /* Create player ship */ var shp = ship_create(3450, 3500, player_ship_type); shp.is_player = true player = shp /* Find mission data file */ var file = file_text_open_read(get_mission_file(gamedata.current_mission)) if file == -1 file = file_text_open_read("M1.MS1") /* Create NPC ships and their type */ var new_type, new_x, new_y; var new_line, shift; for (var i=0;i<20;i++) { new_line = file_text_read_string(file); shift = ord(string_char_at(new_line,1))-$20; new_type = ord(string_char_at(new_line,2))+(shift*$60)-$20 if new_type < $0A npc_ship[i] = ship_create(0,0,new_type) else npc_ship[i] = 0 file_text_readln(file) } /* Get x waypoints */ for (var j=0;j<20;j++) { for (var i=0;i<501;i++) { if npc_ship[j] > 0 { new_line = file_text_read_string(file); shift = ord(string_char_at(new_line,1))-$20; new_x = ord(string_char_at(new_line,2))+(shift*$60)-$20 npc_ship[j].ai.fixed_path_x[i] = new_x } file_text_readln(file) if j==19 && i==480 then i=501 /* Last ship only has a path of 480 points */ } } nbsp; /* Get y waypoints */ for (var j=0;j<20;j++) { for (var i=0;i<501;i++) { if npc_ship[j] > 0 { new_line = file_text_read_string(file); shift = ord(string_char_at(new_line,1))-$20; new_y = ord(string_char_at(new_line,2))+(shift*$60)-$20 npc_ship[j].ai.fixed_path_y[i] = new_y } file_text_readln(file) if j==19 && i==480 then i=501 /* Last ship only has a path of 480 points */ } } file_text_close(file) /* Set initial position to start of the path */ for (var i=0; i<20; i++) { if npc_ship[i] > 0 { npc_ship[i].ship_index = i npc_ship[i].x = npc_ship[i].ai.fixed_path_x[0]*32+16 npc_ship[i].y = npc_ship[i].ai.fixed_path_y[0]*32+16 } } |
Script: ship_create
We have this script to encapsulate the ship creation with the AI object creation.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
/* Ship creator interface arg0 = initial x position arg1 = initial y position arg2 = ship type */ var shp = instance_create(argument0, argument1, obj_ship); shp.ship_type = argument2 shp.x = argument0 shp.y = argument1 var shp_ai = instance_create(0,0,obj_ship_ai); shp_ai.agent = shp /* Attaches AI object to a ship */ shp.ai = shp_ai /* Attaches ship to its AI */ with shp event_user(0) return shp |
Script: get_mission_file
A small script that simply returns the valid file name for the mission data.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
/* Returns the file name of the mission data arg0 is the current mission */ var file_ext, mission_num, fname; if argument0 < 60 then file_ext = ".MS3" if argument0 < 41 then file_ext = ".MS2" if argument0 < 21 then file_ext = ".MS1" mission_num = argument0-20*floor((argument0-1)/20) fname = "M"+string(mission_num)+file_ext return fname |