Skip to content
Andreas Rønning edited this page Jan 22, 2014 · 1 revision

Barrage: A declarative scripting language for creating 2D bullet patterns for Haxe shooting games.

Purpose:

A certain breed of 2D shooting games depend on bullet patterns for much of their design. Some shooters have no such dependency on patterns, and instead rely on individual ship AI routines to decide when and where bullets are fired. This type of game tends to be horizontally oriented and depend on ship positioning and scenery dodging for difficulty and balancing. Barrage can aid such games, but Barrage is about creating systems where the bullets themselves are the scenery to be dodged.

Bullet pattern complexity can be finicky to work with for artists. In essence, a bullet pattern is really just a particle emitter or set thereof, and particle behaviors based on age and lifespan. Hardcoding such systems is efficient, but increases iteration times and complicates rebalancing. Therefore, to rapidly iterate on complex bullet firing patterns and behaviors, some form of scripting is useful.

Given that bullet behaviors depend on timings and event triggers, I found a sequential, declarative form to be expressive and readable, as opposed to markup-based approaches such as BulletML.

Ideally, a Barrage script should read like a narrative, describing concrete events with intuitvely implicit event order.

Language:

I use the term "block" when describing a line of script and its associated lines. For instance take the following lines.

bullet called myBullet
	speed is 24

In this case both lines are considered part of the bullet block, though the speed line is also called a speed block.

  • Barrage uses a small set of keywords and indentation for scoping, similar to Python.
  • Barrage is not case intensive, and differences in case are not ambiguous.
  • A barrage script (henceforth "a barrage") is a single file, consisting of a list of declarations of "actions" and "bullets".
  • An "action" is roughly analogous to a function.
  • Action block members are triggered sequentially, top down.
  • Actions can trigger other actions.
  • A "bullet" is a collection of properties that define a bullet/particle's initial state, and an optional action that triggers at the moment the bullet is created.
  • Both actions and bullets can be predefined with names for reference, or defined anonymously locally if no reference is required.
  • A barrage has a required entry-point action named "start".

Barrage definition

barrage called myBarrage

The first, unindented line of a barrage simply defines its name as a root block for the script entire. This name is not used internally for anything of importance, but can be useful for distinction. Note the "called" keyword. Actions, barrages and bullets use "called" to define their unique internal identifier. Also note that Barrage has no internal concept of string types. During parse it simply maps these IDs against sequential numeric UIDs. For this reason, any unrecognized non-numeric word is seen as an identifier, and the use of quotation marks is never needed.

From this line on, every line needs to be indented at least once to be a part of the definition block.

Bullet definition

bullet called myBullet
	speed is 2.5
	direction is 10
	myVariable is 1
	myOtherVariable is 2
	do myAction

Bullet definitions express the initial state of a bullet at the point of creation through the use of "is" blocks. Every bullet has three predefined floating point fields: speed, direction and acceleration. Directions are in degrees, with 0 pointing to 12 o'clock in world space. The actual meaning of the other values expressed in these fields is down to the implementation, though Barrage is designed to work with floating point numbers and normalized values are recommended.

A defined bullet will typically "do" an action, either referred to by identifier or defined locally. In addition to the predefined fields, every bullet can define its own custom float fields simply by setting them in is-blocks. These variables can then be used as arguments by the bullet's action.

bullet called myBullet
	speed is 2
	targetSpeed is 20
	do action
		set speed to targetSpeed over 5 seconds
		wait 5 seconds
		die

In this example, a bullet defines its initial speed, and then a targetSpeed field. It then defines and executes an anonymous action which alters the speed of the bullet over time, before self-destructing. Note that the action's scope, when dealing with bullet properties, always refer to the bullet that triggered the chain of actions that resulted in it being triggered. A bullet being fired can be considered a change of scope.

A bullet can only do/define a single action as part of its definition.

Action definition

action called myAction
	targetSpeed is 20
	changeDuration is 5
	wait 5 seconds
	set speed to targetSpeed over changeDuration seconds

An action has two kinds of block member: Events and declarations. "is" blocks are declarations, while all other types of blocks are events. Declarations are always handled first no matter where in the action block they are defined, while event blocks are turned into an ordered list, triggering top-down. These event elements can be temporally separated with "wait" blocks.

Actions are triggered with "do" blocks, either with an identifier...

do myAction

...or anonymously, in which case the do block becomes an action definition, as in the second bullet definition example.

Named actions that define properties can have those properties overridden by declarations nested under a do block:

do myAction
	targetSpeed is 10

This effectively allows actions to be called with arguments.

repeat blocks allow an action to repeat itself a number of times.

do action
	fire bullet
	wait 1 frames
	repeat 5 times

When parsed, this "loop" is effectively unrolled, meaning the actual resulting action is.

do action
	fire bullet
	wait 1 frames
	fire bullet
	wait 1 frames
	fire bullet
	wait 1 frames
	fire bullet
	wait 1 frames
	fire bullet
	wait 1 frames 

Firing bullets

The basic purpose of an action is to cause bullets to be created. This is accomplished with "fire" blocks. Let's look at a very simple (kind of pointless) barrage.

barrage called gun
	action called start
		fire bullet in absolute direction 0

The fire block causes a bullet to be emitted. The location from which it is emitted depends on the context of the action block. From the starting action, the firing position is defined by the implementation. From actions called by bullets, the firing position is the current position of the triggering bullet. A fire block can either create an anonymous "bullet", again defined by the implementation, or a predefined bullet by name.

fire myBullet

Furthermore, the fire block can override the three built-in properties of a bullet in the following fashion.

fire bullet in absolute direction 0
fire bullet at absolute speed 10
fire bullet with absolute acceleration 1

These overrides can be mixed freely

fire bullet in absolute direction 0 with absolute acceleration 10
fire bullet with absolute acceleration 10 in absolute direction 0 at absolute speed 3

And so forth.

Each of these overrides carry an associated flag, in this example "absolute". The possible values for this flag depend on the property being set. For acceleration and speed, the options are "absolute", "relative" and "incremental". For direction, there is also "aimed".

Absolute values are just that, a simple setting of the property to a value.

Incremental values are offset from the previous value used. Take this example:

do action
	fire bullet at absolute speed 10
		do action
			fire bullet at incremental speed 2
			repeat 10 times

In this action, a bullet is first fired with an absolute speed, defining the "base speed" in this case. Then, an action is triggered that fires progressively faster bullets. The first bullet in the second do-block would be at speed 12, the next 14 and so on.

Relative values are based on the default value of the bullet fired.

bullet called myBullet
	direction is 20
	do action
		fire myBullet in relative direction -5

In this example, the myBullet fired in the action is 20-5=15.

Aimed values are offsets from a direction pointing from the emitter towards the player entity (both defined by the implementation). For instance, firing in aimed direction 0 is directly towards the player, whereas aimed direction 10 is

For directions, the default direction of a bullet is "aimed" with an offset of 0. By using an offset aimed direction in concert with incremental value, we can create fans of bullets based on the player's position at the start of a barrage.

bullet called simplebullet
	someValue is 0
	speed is 10

action called fan
	fire simplebullet in aimed direction -20
	do action
		fire simplebullet in incremental direction 10
		repeat 4 times

First, we've fired a bullet -20 degrees off from the player. Then we fire 4 more bullets, each offset +10 degrees from the preceding bullet. The final angles then are -20, -10, 0, 10, 20: A 5-bullet fan.

Fire blocks can also use nested property overrides much the same way do-blocks can.

fire simplebullet
	speed is 20
	someValue is 10

Animation, time and lifespan

The two primary event blocks for temporal control of bullets are wait and set.

wait blocks tell the action to hold for a set period of time before continuing to the next event.

wait 1 seconds
wait 60 frames

As you can see, wait accepts both a count of updates and a number of seconds as arguments.

set blocks change properties of the current bullet.

set speed to 20

By default, set is simply a way to drive properties mid-action. Where it gets interesting is when you tell it to carry out those changes over time.

set speed to 0 over 1 seconds

In this case, the set-event triggers a linear property tween. Note that this tween does not halt the action from continuing, so you may commonly want to combine set blocks with wait blocks.

set speed to 0 over 2 seconds
wait 2 seconds
die

die blocks simply tell the bullet triggering the action to immediately vanish. This can be used to create splitting bullets, for instance, that create a number of other bullets

HScript expressions

As you can see, Barrage trades in numbers. In some cases, however, you may want more fine control over values and the interactions between variables. In those cases, Barrage supports math expressions and scripting access. This can be applied to any number argument, and is done by wrapping the script in parentheses, like so:

# nonsense math follows
set speed to (5 + myVariable * 0.5)
set direction to (52 - 24*9)

These cases differ in one key way; The latter contains no variables. In this case, math expressions are evaluated during parse, and the result is made constant. For expressions with variables, however, an HScript Expr is stored and evaluated at runtime. As long as the expression returns a number, any hscript is valid.

Key variables exposed to hscript in Barrage:

  • age the number of seconds the bullet has been alive
  • actionTime the number of seconds the current action has been active
  • barrageTime the number of seconds since the barrage was started
  • degRad a scalar for converting degrees to radians
  • radDeg a scalar for converting radians to degrees
  • aimRad a direction towards the player entity in radians
  • aimDeg a direction towards the player entity in degrees
  • Math. the Haxe Math class
  • vars a dynamic object holding every property available in the current scope

Scope

Scope in barrage is best illustrated by example.

action called foo
	myVar is 20
	myOtherVar is 10

do foo 
	myVar is 10
	myOtherVar is 0

This highlights a slight puzzle when using predefined actions. The action "foo" needs to be independent, so it cannot rely on properties in a parent scope to function. This means that it first needs to define defaults, and a triggering action needs to explicitly override those defaults.

bullet called bar
	speed is 10
	do action
		set speed to 5 over 2 seconds

In this case, the action depends wholly on the bullet that triggered it, and can thus safely access properties therein.

In general, it is best practice to rely on constants over variables, and using relative/aimed/incremental in fire blocks to give bullets interesting behavior, rather than script everything. HScript is fast but not that fast, and juggling properties across action and bullet definitions can introduce some headaches.