Prototyping - Programming Ship Actions & Collisions
It's time to implement the ship controls based on our design from part 23. We'll focus on the player ship since most of our work will apply to the NPC ships too. In addition to controls, we'll add some explosion effects that trigger when shots hit a target and ships are destroyed.
Objects
We can implement the ship functions with four objects:
Concept | Object | Notes |
---|---|---|
Ships | obj_ship | The object that we'll use for all game ships, player and NPC |
Weapon shots | obj_fire | The protoype object for all weapons |
Explosions | obj_explosion | Single explosion effect used for weapon hits and ship destruction |
Screen flash | obj_flash | An object that makes the screen flashing effect when a ship is destroyed |
GML Code
We'll create the four objects in GameMaker and modify our camera object from last time to focus on the player ship
obj_ship: Create 1
Register keyboard and set references. Note that the ai reference will be null because it doesn't exist yet. The parent object will manually associate it
1 2 3 4 5 6 7 |
///Set References & keyboard keyboard_register(id) map = instance_find(obj_map,0) mission = instance_find(obj_hq,0) hud = instance_find(obj_hud,0) ai = 0 |
obj_ship: Create 2
Creates the targets priority queue used to hold valid targets for AI and HUD
1 2 3 4 5 6 7 |
///Set Data Structures targets_pq = ds_priority_create() /* HUD targets box */ for (var i=0; i<8; i++) targets_box[i]=0 |
obj_ship: Create 3
Sets the four enum types used in ship objects. Note that actions are handled as bit flags.
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 |
///Ship Enums enum ACTION { wait = 0, up = 1 << 0, right = 1 << 1, down = 1 << 2, left = 1 << 3, fire_weapon = 1 << 4, change_weapon = 1 << 5, afterburner = 1 << 6, self_destruct = 1 << 7 }; enum WEAPON { none, gun_weak, gun_normal, gun_strong, missile_weak, missile_normal, missile_strong }; enum SHIPTYPE { truck, transport, stiletto, stingray, hornet, javelin, viii, vi, vii }; enum ALIGNMENT { ally, neutral, enemy }; |
obj_ship: Create 4
Ship variables set up for the default ship used by the player. I'll leave this in it's final form but we'll make some changes during testing.
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 |
///Init Vars shields_max = 100 shields = shields_max armor_max = 100 armor = armor_max speed_max = 5 has_afterburner = false alignment = ALIGNMENT.neutral actions = 0 /* bit field */ active = true ship_index = 0 wave_number = 1 dir_to = 0 turn_delay = 0 fire_delay = 0 last_hit = 0 is_player = false ship_type = SHIPTYPE.hornet x = 3450 y = 3500 map_x = map_pos(x) map_y = map_pos(y) weapon[0] = WEAPON.gun_normal weapon[1] = WEAPON.missile_normal missile_count = 4 active_weapon = 0 |
obj_ship: Destroy 1
Ships explode when destroyed.
1 2 3 4 5 6 7 8 9 10 |
///Make Explosions if armor < 1 then { instance_create(x,y,obj_explosion) instance_create(x+32,y+32,obj_explosion) instance_create(x-32,y+32,obj_explosion) instance_create(x+32,y-32,obj_explosion) instance_create(x-32,y-32,obj_explosion) instance_create(x,y,obj_flash) } |
obj_ship: Destroy 2
Remove keyboard reference and free data structures.
1 2 3 |
///Free data structures keyboard_deregister(id) ds_priority_destroy(targets_pq) |
obj_ship: Step 1
Process input for ships. This time, I'll demonstrate a bitfield to set actions. Essentially, one interger variable represents the collective combination of keyboard inputs. This method was ubiquitous in the era of Traffic Department and is still somewhat common in lightweight solutions like embedded systems.
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 |
///Process Input actions = 0 /* reset action bitfield */ while !ds_stack_empty(command_stack) { var cmd = ds_stack_pop(command_stack) if active { if is_player { switch cmd { case "hold_up": actions |= ACTION.up; break; case "hold_down": actions |= ACTION.down; break; case "hold_left": actions |= ACTION.left; break; case "hold_right": actions |= ACTION.right; break; case "fire": actions |= ACTION.fire_weapon; break; case "tab": actions |= ACTION.change_weapon; break; case "escape": actions |= ACTION.self_destruct; break; } } else /* must be AI ship */ { switch cmd { case "ai_up": actions |= ACTION.up; break; case "ai_down": actions |= ACTION.down; break; case "ai_left": actions |= ACTION.left; break; case "ai_right": actions |= ACTION.right; break; case "ai_fire": actions |= ACTION.fire_weapon; break; } } } } |
obj_ship: Step 2
Handle movement turning commands by reading from the bitfield
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 |
///Execute Move Turn Commands var dir_diff; /* find intended direction */ if ACTION.up == (actions & ACTION.up) then dir_to = 90 if ACTION.right == (actions & ACTION.right) then dir_to = 0 if ACTION.down == (actions & ACTION.down) then dir_to = 270 if ACTION.left == (actions & ACTION.left) then dir_to = 180 if (ACTION.up | ACTION.right) == (actions & (ACTION.up | ACTION.right)) then dir_to = 45 if (ACTION.up | ACTION.left) == (actions & (ACTION.up | ACTION.left)) then dir_to = 135 if (ACTION.down | ACTION.left) == (actions & (ACTION.down | ACTION.left)) then dir_to = 225 if (ACTION.down | ACTION.right) == (actions & (ACTION.down | ACTION.right)) then dir_to = 315 /* are we moving in the same direction that we want to? */ dir_diff = angle_difference(direction, dir_to) /* Turn if necessary */ if dir_diff < 0 && dir_diff > -180 { if turn_delay < 1 { direction+=45 turn_delay = 6 } } if dir_diff > 0 && dir_diff < 180 { if turn_delay < 1 { direction-=45 turn_delay = 6 } } |
***********************************************************
obj_ship: Step 3
Handle movement speed changes
1 2 3 4 5 6 7 8 9 10 11 12 |
///Execute Move Speed if actions & (ACTION.up | ACTION.right | ACTION.left | ACTION.down) { if (direction - dir_to) mod 180 == 0 then /* We're going exactly forward or backward */ { if dir_to == direction speed+=1 else speed-=1 } speed = clamp(speed, -speed_max, speed_max) } |
obj_ship: Step 4
Ship firing of various weapon types. I'm going to fill out the code block here as it is in the final version of the prototype. We don't actually add the weak weapons until the final video.
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 |
///Execute fire commands if (actions & ACTION.fire_weapon) { if fire_delay < 1 { switch weapon[active_weapon] { case WEAPON.gun_weak: { fire_shot(1, WEAPON.gun_weak) fire_shot(2, WEAPON.gun_weak) fire_shot(3, WEAPON.gun_weak) fire_shot(4, WEAPON.gun_weak) fire_delay = 10 if !is_player then fire_delay+=irandom_range(20,40) audio_stop_sound(snd_hornetfire) audio_play_sound(snd_hornetfire,0,false) } break; case WEAPON.gun_normal: { fire_shot(1, WEAPON.gun_normal) fire_shot(2, WEAPON.gun_normal) fire_delay = 10 if !is_player then fire_delay+=irandom_range(20,40) audio_stop_sound(snd_hornetfire) audio_play_sound(snd_hornetfire,0,false) } break; case WEAPON.missile_normal: { if missile_count > 0 { fire_shot(0, WEAPON.missile_normal) fire_delay = 20 if !is_player then fire_delay+=irandom_range(40,80) audio_stop_sound(snd_missilefire) audio_play_sound(snd_missilefire,0,false) missile_count-- } } break; } } } |
obj_ship: Step 5
Weapon changes, self destruct, and afterburners. We won't build a case for the final just yet. Since it doesn't appear in mission 1, we may not get to it at all in this demo
1 2 3 4 5 6 7 8 9 10 |
///Execute Other commands if actions & ACTION.change_weapon then active_weapon = ++active_weapon mod 2 if actions & ACTION.self_destruct then armor = 0 if actions & ACTION.afterburner then { } |
obj_ship: Step 6
Check for collisions by checking the point at exactly the front and back of the ship. If the points are asserted in the collision map, then we reverse the speed. Note that I used the absolute value, whch seem redundant, but this is actually critical to keep the direction of the ship correct in subsequent frames. In a nutshell, the front point should always force a negative speed, while the rear point a positive speed relative to the dircetion vector (re +velocity). Refer to the video for a detailed explanation of why this works.
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 |
///Collision check var front_x = x+lengthdir_x(16,direction) var front_y = y+lengthdir_y(16,direction) var back_x = x+lengthdir_x(-16,direction) var back_y = y+lengthdir_y(-16,direction) if ds_grid_get(map.collision_map, map_pos(back_x), map_pos(back_y)) > 0 speed = abs(speed) if ds_grid_get(map.collision_map, map_pos(front_x), map_pos(front_y)) > 0 speed = -abs(speed) /* Edge barrier */ if is_player { if (map_pos(back_x) < 10 || map_pos(back_x) > 190 || map_pos(back_y) < 10 || map_pos(back_y) > 190) then { speed = abs(speed) hud_message("Stay in your jurisdiction, velasquez") } if (map_pos(front_x) < 10 || map_pos(front_x) > 190 || map_pos(front_y) < 10 || map_pos(front_y) > 190) then { speed = -abs(speed) hud_message("Stay in your jurisdiction, velasquez") } } |
obj_ship: Step 7
Check the ship for damage or death
1 2 3 4 5 6 7 8 9 10 |
///Shields & Death check if shields < shields_max { shields+=10 if shields > shields_max shields = shields_max } if armor <= 0 instance_destroy() |
obj_ship: Begin Step 1
Iterate timers and apply our friction loss factor
1 2 3 4 5 6 7 8 |
///Timers, Shields, Physics updates if turn_delay > 0 turn_delay-- if fire_delay > 0 fire_delay-- speed *= 0.95 |
Script: map_pos
Calculate the ships absolute pixel position to a grid coordinate on our 201x201 map. We use this for lookups
1 2 |
/* returns the grid coordinate of a pixel location */ return floor(argument0/32) |
obj_camera: Room Start 1
In the last video, we had the camera locked to itself. Now we want it to always lock to the player ship.
1 2 |
///Lock camera to player camera_target = find_player_ship() |
Script: find_player_ship
Returns a handle (object id) to the player ship
1 2 3 4 5 6 7 8 9 10 11 |
var ship; var ship_id = -1; for (var i=0; i<instance_number(obj_ship); i++) { ship = instance_find(obj_ship,i) if ship.is_player == true ship_id = ship } return ship_id |
obj_fire: Create 1
Set object references
1 2 3 |
///Set references map = instance_find(obj_map,0) hud = instance_find(obj_hud,0) |
obj_fire: Create 2
Variables for the shot including who, and how much damage
1 2 3 4 5 6 7 |
///Init Vars source = 0 /* who shot */ type = 0 spr = spr_gun_normal life_time = 60 map_x = 0 map_y = 0 |
obj_fire: Step 1
Update the map position
1 2 3 |
///Update position map_x = map_pos(x) map_y = map_pos(y) |
obj_fire: Step 2
Check for hits. If a ship is hit, we make explosions and transfer damage. The 'trick' here is that all damage is applied to the shields and any negative value rolls over to the armor at half rate (shields reset to 0 if negative).
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 |
///Hit check /* Map edge */ if map_pos(x) < 0 || map_pos(x) > 201 || map_pos(y) < 0 || map_pos(y) > 201 then instance_destroy() if ds_grid_get(map.collision_map, map_pos(x), map_pos(y)) > 0 instance_destroy() /* Damage Ships (future code) */ if place_meeting(x,y,obj_ship) { var hit = instance_place(x,y,obj_ship) if hit != source && hit.visible { /* explosions */ instance_create(x,y,obj_explosion) /* do damage */ hit.shields-=damage if hit.shields < 0 then { hit.armor+=hit.shields/2 /* moves half of the negative damage to armor */ hit.shields=0 } if hit.armor < 1 hit.last_hit = source instance_destroy() } } |
obj_fire: Step 3
Update life timers, which gives the shots a limited range
1 2 3 4 5 |
///Timers life_timer-- if life_timer < 1 then instance_destroy() |
obj_fire: Room start 1
I'm going to use this event to hold creation polymorphic events. This would probably be best in a custom event, which I'll do for ships. Technically no obj_fire objects exist when the room starts so this will never run by itself. I'll call it manually.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
///Set polymorphic cases switch type { case WEAPON.gun_normal: { speed = 15 life_timer = 60 damage = 350 spr = spr_gun_normal } break; case WEAPON.missile_normal: { speed = 12 life_timer = 120 damage = 3000 spr = spr_missile_normal } break; } |
obj_fire: Draw 1
Draws the shot
1 2 |
///Render shot draw_sprite_ext(spr,0,x,y,1,1,direction,c_white,1) |
Script: fire_shot
Creates a single volley based on the ship and weapon. Most have two but some have four. Shots appear on points some distance in front of the ship.
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 |
/* callable only from obj_ship arg0 = gun position 0 = center 1 = outside left 2 = outside right 3 = inside left 4 = inside right arg1 = gun type */ var new_x, new_y switch argument0 { case 0: new_x = x+lengthdir_x(16,direction); new_y = y+lengthdir_y(16,direction); break; case 1: new_x = x+lengthdir_x(8,direction+90); new_y = y+lengthdir_y(8,direction+90); break; case 2: new_x = x+lengthdir_x(8,direction-90); new_y = y+lengthdir_y(8,direction-90); break; case 3: new_x = x+lengthdir_x(8,direction+20); new_y = y+lengthdir_y(8,direction+20); break; case 4: new_x = x+lengthdir_x(8,direction-20); new_y = y+lengthdir_y(8,direction-20); break; } var shot = instance_create(new_x, new_y, obj_fire); shot.source = id shot.direction = direction shot.type = argument1 with shot event_perform(ev_other, ev_room_start) /* polymorphic support */ |
obj_explosion: Create 1
Sets the explosion timer.
1 2 |
///Vars life_timer = 7 |
obj_explosion: Step 1
Iterates the timer
1 2 3 4 5 |
///Timer life_timer-- if life_timer < 1 instance_destroy() |
obj_flash: Create 1
Sets the flash timer
1 |
life_timer = 10 |
obj_flash: Step 1
Iterates the timer
1 2 3 4 5 6 |
///Timer life_timer-- if life_timer < 1 instance_destroy() |
obj_flash: Draw 1
Draws a yellow box on the screen with a partial alpha and fades out in 10 frames.
1 2 3 4 5 |
///Draw screen flash draw_set_alpha(life_timer/30) draw_set_color(c_yellow) draw_rectangle(view_xview[0], view_yview[0], view_xview[0]+view_wview[0], view_yview[0]+view_hview[0], false) draw_set_alpha(1) |