Let's Pack Heat!


In my previous post I described the process for adding new monsters to Ecliptic. This time, let's talk about some weapons that can be used to turn those monsters into green goo or a fine red mist.


Just your standard sci-fi armory - laser carbine, grenade launcher, pulse rifle, flamethrower and shotgun. I really like the look of the grenade launcher but the pulse rifle needs some more work.

Weapons in Ecliptic are Items - objects that the robots can interact with in some way. Pretty much everything in the game that isn't a Player or Monster is an Item. The game engine doesn't have any particular knowledge of Weapons, it just provides operations that can be used to implement them - target selection, animations, area effects and damage. 

Most weapons are organized into a type hierarchy with a root type defining some common attributes and base types for melee and ranged weapons. Some items that you would classify as a weapon don't really fit into this hierarchy but that's OK - there's no rule that says only Official Weapons can be used to smash aliens into little bits. However, in this post, I'm mostly going to talk about ranged weapons.

Ranged weapons are weapons that throw some sort of projectile. They vary in power, range and the type of damage they do. A base type provides some common attributes such as charge - the number of shots left before a reload is required, max_charge - the maximum number of shots the weapon can hold, and range - the distance available for target selection.

The base type also defines some events that occur while the weapon is being used: needs_reload - emitted when you're out of ammo and fired - emitted when a target has been successfully selected.

Begin Item
    ID = ranged_weapon_base
    Base = $game.item.weapon_base
    Static = True
    Begin Attribute
        ID = charge
        BitCount = 4
        Value = 0
    End
    Begin Attribute
        ID = maxCharge
        BitCount = 4
        Value = 0
    End
    Begin Event
        ID = needs_reload
    End
    Begin Event
        ID = fired
    End

When the player attempts to use the weapon, the game will request they select a target from the map. You can select any square of the map visible to the robot - it doesn't matter if there's an enemy there or not.

;
; Invoked when the player uses a ranged weapon.
;
Begin Handler
  EventID = $game.event.use
  MatchTarget = true
  Begin
    *select_target
    ; 
    ; Out of ammo?
    ;
    Push $game.item.ranged_weapon_base.attribute.charge
    Push 0
    Cmp
    Equal
    Jump need_reload
      
    Push[en] "Select a target"
    Push 0
    Log
    ;
    ; Ask the player to choose
    ; a target location.
    ;
    *arg !Event:Object !selected
    *select_map_tile $game.item.ranged_weapon_base.event.target_selected $game.item.ranged_weapon_base.attribute.range 1
    Exit
    ; 
    ; No ammo so as for a reload.
    ;
    Label need_reload
    *emit_event $game.item.ranged_weapon_base.event.needs_reload 0
  End

The *select_map_tile macro triggers an event that switches the game into selection mode. By default, the player is prompted to choose a location on the map but it has options for specifying selecting monsters, other robots or items from the map or inventory.

Once the player has selected a location, the reply event - $game.item.ranged_weapon_base.event.target_selected - will be triggered and the next event handler will run.

Begin Handler
  EventID = $game.item.ranged_weapon_base.event.target_selected
  MatchObject = True
  Begin
    ;
    ; Emit an event that causes the robot's
    ; action points to be reduced.
    ;
    *emit_object_used
    ;
    ; Elide some stuff that generates a 
    ; "Target Selected" log message
    ;
    ;
    ; Reduce the ammo count.
    ; 
    *select_object
    Push 1
    Push $game.item.ranged_weapon_base.attribute.charge
    Sub
    Store $game.item.ranged_weapon_base.attribute.charge
    
    ;
    ; Trigger the derived type's fired handler
    ;
    *emit_event $game.item.ranged_weapon_base.event.fired 0
  End
End

THIS IS MY BOOMSTICK!

The type that derives from ranged_weapon_base needs to provide a handler for the fired event that will trigger the weapon's effect. This handler is from the Shotgun weapon.

Begin Handler   
  EventID = $game.item.ranged_weapon_base.event.fired   
  MatchObject = true   
  Begin     
    ;     
    ; Trigger the "bullet_large_fire" effect     
    ; starting from the robot's location and     
    ; ending at the selected map location.     
    ;                       
    *select_actor     
    *arg !Effect:Position !Player:Position
    *select_event 
    *arg !Effect:TargetPosition !Event:Position 
    *arg $game.effect.bullet_large_fire.attribute.hit_strength 20 
    *start_effect $game.effect.bullet_large_fire 2 
    ; 
    ; Play the shotgun sound. 
    ; 
    *start_sound $game.soundset.shotgun.sound.shotgun_fire 0 
  End
End

Effective!

Similar to the way monster attacks work, weapons trigger effects when they are used. An effect is a re-usable object that can define its own events, handlers and attributes. In this case, the shotgun triggers the $game.effect.bullet_large_fire effect that plays a muzzle flash animation and then shows a bullet moving to the target. This effect is used by any weapon that throws a large bullet and can be customized with different hit_strength values.

Begin Effect
  ID = bullet_large_fire
  ; 
  ; Attribute use to control how much
  ; damage the effect will do. 
  ;
  Begin Attribute
    ID = hit_strength
    BitCount = 8
  End
  ; 
  ; Event triggered when the muzzle
  ; flash animation completes.
  ;
  Begin Event
    ID = bullet_large_launch
  End
  ;
  ; Event triggered when the bullet
  ; reaches the target.
  ;
  Begin Event
    ID = bullet_large_done
  End
  ;
  ; Even triggered when the "hit" 
  ; animation completes.
  ;
  Begin Event
    ID = bullet_large_hit_done
  End
  ;
  ; Handler for the "effect_started" event
  ; This is the entry-point of the effect.
  ;
  Begin Handler
    EventID = $game.event.effect_started
    MatchObject = True
    Begin
      ;
      ; Work out the direction between
      ; the robot and the target so we
      ; can play the correct muzzle flash
      ; animation. 
      ;
      *select_actor
      *arg !Anim:Position !Player:Position        
      Push !Player:Position
      *select_object
      Push !Effect:TargetPosition
      Facing
      Push #!Anim:Facing
      ;
      ; Trigger the muzzle flash
      ;
      *arg !Anim:CompletionEventID $game.effect.bullet_large_fire.event.bullet_large_launch
      *arg !Anim:Target !selected
      *arg !Anim:Delay 10
      *start_anim $game.animset.muzzle_flash 5
      ;
      ; Make the game wait until the effect completes.
      ;
      *arg !Wait:WaitEventID $game.event.effect_completed
      Push 1
      Wait
  End
End

The next step is to play the actual bullet animation from the robot to the target.

Begin Handler
  EventID = $game.effect.bullet_large_fire.event.bullet_large_launch
  MatchObject = True
  Begin
    *select_actor
    *arg !Anim:Position !Player:Position *select_object
    *arg !Anim:TargetPosition !Effect:TargetPosition
    *arg !Anim:CompletionEventID $game.effect.bullet_large_fire.event.bullet_large_done
    *arg !Anim:Target !selected
    *start_anim $game.animset.bullet_large 4
 End
End

Then we decide whether the target has taken a hit.

Begin Handler
  EventID = $game.effect.bullet_large_fire.event.bullet_large_done
  MatchObject = True
  Begin
    ;
    ; Play a "splat" animation
    ; at the target position.
    ;
    *select_object
    *arg !Anim:Position !Effect:TargetPosition
    *arg !Anim:CompletionEventID $game.effect.bullet_large_fire.event.bullet_large_hit_done
    *arg !Anim:Target !selected
    *start_anim $game.anim.hit_1 3
    ;
    ; Generate a "hit" on the target
    ; position.
    ;
    Push !Effect:TargetPosition
    Store !Map
    *arg !Hit:Target !Map:Occupant
    *arg !Hit:Type #hit_projectile
    *select_owner
    *arg !Hit:Strength $game.effect.bullet_large_fire.attribute.hit_strength
    *hit 3
  End
End

Finally, we emit an effect_completed event, signalling to the game that the effect is finished and the game can continue.

Begin Handler
  EventID = $game.effect.bullet_large_fire.event.bullet_large_hit_done
  MatchObject = True
  Begin 
    *emit_event $game.event.effect_completed 0
  End
End

Circle Work

Ranged weapons all follow this pattern - from the piddly little Cutters the robots start with through to the heavy stuff like the Grenade Launcher. One place where the grenade launcher differs is what happens when the grenade clunks off the forehead of some unlucky dweeb. Rather than a simple hit, this one triggers a second effect - a $game.effect.explosion!

Explosions make use of an operation called an Area. This operation takes a radius and a set of octants and executes an event handler for each affected map position. Octants divide an area of the map into 45 degree segments that can be operated on with obstacle detection - this is used by the lighting and vision code. The Area operation makes this available to code running in the virtual machine.

Octants

This algorithm came from a really helpful blog post named What The Hero Sees by Bob Nystrom. I've implemented it as a C++ class called OctantMapEnumerator that can be used like this:

OctantMapEnumerator mapEnum;
mapEnum.init(x, y, octants, radius);
while (mapEnum.nextOctant()) {
  while (mapEnum.nextRow()) {
    while (mapEnum.nextTile()) {
      if (mapEnum.isVisible() == FALSE) {
        continue;
      }
      // Do something with current tile
    }
  }
}

There are a number of other MapEnumerator classes with similar interfaces that are used for efficiently examining and manipulating the game map - RectMapEnumerator for rectangular areas, LineMapEnumerator for tracing a straight line between two tiles, AStarMapEnumerator for path finding an so on.

The result of the grenade launcher's Area Effect looks something like this:

Fire It Up!

Most of the weapons shown at the top of the post use this same pattern, just with different hit types and strengths, animations and sounds. One exception is the flamethrower, it isn't a ranged weapon. When the player uses the flamethrower, it triggers a "fire" effect that immediately blasts flames out in front of the robot.


The flames also use the Area operation. In this case, instead of being applied to all octants, it is only applied to those in the direction the robot is facing.

Thanks for reading, I hope you found this post interesting! I need to go and draw a better pulse rifle, I think.

Get Ecliptic

Download NowName your own price

Comments

Log in with itch.io to leave a comment.

Looks cool! Thanks for the detailed post.