BulletScript language reference

Overview of the system
Script Structure
Language Reference


Overview of the system

BulletScript emits and (optionally) controls objects. These objects can be more or less anything, although some things lend themselves better to it than others. Particles, shapes, bullets, even sound effects are all good candidates. In order to use BulletScript, the user must create a system which can interface with it. For each system, a core "type" is declared, and then the user sets BulletScript to interface with this type's system by specifying function callbacks. The implementation of these callbacks is entirely up to the user, although there will in practice be a general pattern to them. This is discussed in the Getting Started guide.

Once user systems have been bound to BulletScript, and scripts written and compiled, the user must create some BulletScript objects in order to do any work. These two objects are emitter and controller and are explained in the next section. These objects are then updated once a frame, or at whatever interval the user wants. This will control object emission. If the user wants BulletScript to control the objects after emission, then for each object, the user must manually call an update function from BulletScript. This is essentially all the interaction needed to use BulletScript. The system will update controllers and emitters automatically, and the user can choose to have objects controlled by BulletScript with one line of code when they update the objects.

BulletScript by default uses 2d positions: x & y. If you wish to use 3d coordinates then uncomment BS_Z_DIMENSION in bsConfig.h. Where x & y coordinates are mentioned, it can be assumed that z is also available/necessary if BS_Z_DIMENSION is defined.


Script structure

A script contains emitter and controller definitions in any order. Emitters are the classes which create objects, and controllers are used to control emitters.

Emitters

An emitter definition looks like this:
	emitter emit_name
	{
		// 0 or more member variables
		// 0 or more affector instances
		// 0 or more functions
		// 1 or more states
	}

Member variables

Emitters are vaguely similar to classes in object-oriented languages, and as such have a persistent state. This partially takes the form of variables which persist over the course of the emitter's lifetime. Member variables must be declared before anything else. Their declaration is simple:
	mem_var = 10 * rand(30);
	speed = g_gravity * 5;
There are several built-in members: x, y and angle. Z as well if BS_Z_DIMENSION is defined. These are read-only to the emitter, and can only be modified by controllers, or in native code. They are named This_X, This_Y, This_Z and This_Angle. Member variables may only be used in states. They may not be used in functions or affector instance arguments, because emitted objects may persist beyond the lifetime of the emitter that created them, at which point any member variables will be inaccessible.

States

Everything in the language is built around making emission and subsequent control as flexible and powerful as possible. An emitter is the core object that performs these tasks. Emitters contain at least one state. A state is essentially a function which takes no arguments, and which loops back to the beginning once it is complete, rather than returning. An emitter always has a current state, and when updated, it executes the code until either it changes state or it is told to suspend execution. A simple state might look like this:
	Main = state
	{
		emit bullet();
		wait(1);
	}
This state will emit a bullet every second, until either the emitter is destroyed, or a controller changes its state (this is discussed later). The script suspends execution for a second after every emission. Note that if the wait statement were not there, execution would never stop and the virtual machine would most likely crash or enter an infinite loop. States can be switched between using the goto statement. This changes state and immediately starts executing it.
	Stage1 = state
	{
		emit bullet(180);
		wait(1);
		goto Stage2;
	}

	Stage2 = state
	{
		emit bullet(135);
		wait(2);
		goto Stage1;
	}
One can use more control structures to make things more complex:
	Stage1 = state
	{
		// emit bullet at angle 0
		emit bullet(0);
		goto Stage2;
	}

	Stage2 = state
	{
		// declare and set local variable
		i = 0;

		// emit 10 bullets, spaced evenly from 0 to 180 degrees, with an increasing delay between each one
		while (i < 10)
		{
			emit bullet(i * 20);
			wait(i / 10);
			i++;
		}

		// 30% chance that we change state at this point
		if (rand(10) < 3)
		{
			goto Stage1;
		}
	}
An emitter always starts in the first defined state.

Functions

Emitters can also have special functions, which are declared inline, and must be declared before states. These are used to control the behaviour of emitted objects.
	Explode = function(time)
	{
		// tell the object to suspend script execution for time seconds.
		wait(time);

		// emit 10 'explosion particles'
		i = 0;
		while (i < 10)
		{
			emit bullet();
			i++;
		}

		// and tell the object to kill itself.
		die;

		// at this point, do not do anything which requires the object!  It is alright to set locals, member variables, etc,
		// but pretty much anything else will probably cause a crash, because the object has just been killed.
	}

	Stage1 = state
	{
		// emit a bullet, telling it to use the Explode function.  Thus, any bullets emitted by this line will die after 5 seconds.
		emit bullet() : Explode(5);
		wait(1);
	}
An emitted object can have at most one function. Emitter functions have a normal control flow: when they reach the end of their statement list, they return, and do not loop back like states do. It is important to note that when a function ends, the emitted object that it is controlling does not get killed. The only way to kill an object from script is explicitly via the die statement in a function.

Properties

Emitted objects can have properties defined. Properties are values of BulletScript's value type (normally 32-bit float), which can be controlled by BulletScript. They can be set in one of two ways, either instantly or gradually. Properties are prefixed with $.
	Aim = function(dir)
	{
		$angle = 10; 			// set 'angle' property to 10
		$angle = {10 + dir, 2}; 	// set 'angle' property to (10 + dir) over 2 seconds
		$angle += {5, 3};		// increase 'angle' property by 5 over 2 seconds
		var = $angle;			// get angle
	}
In the case of setting a property over time, subsequent sets will override this command, unless it has completed:
	$angle = {10, 5};
	$angle = {20, 4}; // this overrides the first command

	// this is the proper way to do it
	$angle = {10, 5};
	wait(5);
	$angle = {20, 4};
Properties can only be used in emitter functions. Apart from their extended 'set value over time' functionality, they can be used in the same way as any normal variable. The number of properties definable per type is limited due to performance reasons. The value can be set by changing BS_MAX_PROPERTIES in bsConfig.h. There are some built-in properties: $x, $y, $angle.

Affectors and affector instances

Affectors are C++ functions supplied by the user which take an object, and a number of arguments. Each update, the function is run on the object with the arguments. Affectors are different from emitter functions in that a) the arguments update as necessary over time and b) the function is run in its entirity every update, whereas an emitter function may take several updates to run just the once, and then never again. Thus, affectors are useful for implementing things such as gravity, homing missiles, etc. Alternatively they can be used for performance-critical control, where script is just not fast enough. Affectors may only take a limited number of arguments. This value is set by BS_MAX_AFFECTOR_ARGS in bsConfig.h.

Thus, in addition to an emitter function, emitted objects can use a number of affector instances as well. Affector instances are simply a call of the specified affector function, with specific arguments. They are defined per emitter, and due to performance reasons the number allowed is limited. Its value can be set by changing BS_MAX_EMITTER_AFFECTORS in bsConfig.h. Affector instance declarations must be declared before any functions, and look like this:

	grav = affector Gravity(9.81);
This declares grav as an affector instance which can be used in an emit statement. You can have different-named instances which use the same affector function, with the same or different arguments, although of course the number of arguments must match those required by the C++ affector function. As mentioned, arguments update, thus if you use member or global variables in an argument, the value of the argument will change whenever the variable changes. As an example, you could have modifiable gravity, simply by defining a global g_gravity, and an affector instance grav = affector Gravity(g_gravity);. Then all that is required is to modify the global from C++ code.

Simple examples:

	emit bullet() : grav;
	emit bullet() : grav, track, aim_func(10); 
Using an expression function as an argument will cause the argument to be updated every time, because BulletScript cannot predict the return value of the function. Like emitter functions, you may not use member variables as arguments to functions.

Anchors

Anchors are the third and final method for controlling emitted objects. Anchors tie various properties of the object to the emitter that created it. Anchors are prefixed with &. The available anchors are:
		&x &y &angle &orbit &kill
x, y and angle tie the object's position and rotation to the emitter. They can be used, for instance, to attach beam weapons to ships, or forcefields which rotate to face attacking objects. orbit is used to keep an object in rotation around the emitter. kill is a special anchor: by default, when an emitter is destroyed, any anchored objects will be 'de-anchored'. By using kill, objects are destroyed instead. This is useful for laser beams, for instance. Anchors are used similarly to functions and affector instances:
	emit bullet() : grav, func(1, 2), &angle, &orbit, &kill;

Controllers

Controllers are used to control one or more emitter. They can change their position, angle, member variables (technically, position and angle are member variables), enable or disable them, or force them to wait indefinitely ("suspend"). Controllers are defined in the following form:

	controller controller_name
	{
		member_variable_list
		emitter_list
		event_list
		state_list
	}
Member variables are defined in the same way as emitter member variables, and are subject to the same restrictions for initialisation.

Emitter variables are instances of named emitters which are to be controlled. Their declaration is as follows:

	emitter_var = emitter emit_type;
	emitter_var = emitter emit_type(constant_list);
The second declaration allows for setting the emitter's member variables on creation. The values here must be constants, not expressions involving functions or variables. This is an efficiency consideration. The only variables that are set are the built-in members: x, y, (z), and angle. Any extra values are ignored. Examples:
	emitter1 = emitter Blaster;
	emitter2 = emitter Blaster(0, 10, 0); // x offset = 0, y offset = 0, angle offset = 0
	emitter3 = emitter Laser(1, 2, 3, 4); // value '4' is ignored 
	emitter4 = emitter Laser(5 + rand()); // error, not constant

Events

Events are used in controllers as a way for the user to communicate with the script. They are either raised explicitly in code by the user, or in a controller state with the raise keyword.
	onDie = event()
	{
	}

	onHit = event(damage)
	{
	}

	Go = state
	{
		raise onDie;
		raise onHit(10);
	}
Emitter variables are referred to in controller states and events prefixed by $, like properties. Emitter variable members (that is, member variables of emitter instances) are accessed as follows:
	$emitter1.m_member = 10;
	x = $emitter1.This_Angle;
The similarity to properties extends further. Emitter members can be interpolated smoothly:
	$emitter.This_X += {50, 3}; // move emitter along x axis for seconds
Thus controller states and events can be used to externally modify specific emitters. This is useful because emitters cannot modify their built-in members themselves. Controllers have further functionality for controlling emitters:
	disable emitter_var; // stops emitter1 updating.
	enable emitter_var; // starts it again.

Suspend and Signal

Controllers may suspend their execution in the following way:
	suspend;
	suspend(constant_list);
Here, the constant arguments are "blocks" which prevent the controller from resuming execution. If no blocks are given, as in the first example, then the controller suspends indefinitely. These blocks can be removed with signal.
	signal;
	signal(constant_list);
The second example removes any of the specified blocks, if they exist on the controller. If the controller has been suspended indefinitely, both examples will resume it. Blocks can be added and removed at any time. Duplicate blocks are effectively ignored: that is, calling:
	suspend(2, 2);
	signal(2); // removes both blocks
If a state is suspended, it can only be signalled to from an event, however suspend can be used from either states or events. Suspending is useful for waiting upon certain events from native code. For instance, if you want a controller to do something only after two specific events have occured, you could set it up like this:
	controller Ctrl
	{
		gun = emitter Blaster;

		HalfHealth = event
		{
			signal(1);
		}

		OutOfAmmo = event
		{
			signal(2);
		}

		Stage1 = state
		{
			suspend(1, 2);
			// stop all weapons firing
			disable $gun;			
		}
	}
Thus, if this controller is used on an enemy boss, with the native code raising the events when they occur, the controller will disable its guns once the boss has run out of ammo and is at half health. In other words, suspend/signal is useful for implementing combinatorial logic.


Language Reference

Data types

BulletScript has one data type. As such it is an untyped language. By default, the data type is a 32-bit float, although this can be changed (but it is not recommended that this is done!) by modifying the relevant #define in bsConfig.h. For the default floating point data type, literal values are expressed as:
	1
	1.
	1.2345
	.2345
As long as the number contains at most one decimal point, and only numeric characters, it is valid.

Expressions

Expressions consist of literal values, variables and function calls, combined together with operators. Some example expressions:
	5
	12 + var
	rand(360) + (x / 2) > 100
	foo(bar(z, 12))
BulletScript uses the following operators. Operators cannot be overloaded by users.
	+ - * / % > < >= <= == != && ||
Notes:
  • The modulus operator implements fractional modulus, ie fmod().
  • The equivalence operators (> < >= <= == !=) and the logical operators (&& ||) return 1 if the condition is true, and 0 if false.

    Comments

    BulletScript uses standard comments: // for single line comments and /* */ to enclose multiple-line ones.
    	// single line comment
    	/* multiple
    	   line
    	   comment
    	*/
    

    Variables

    Variables do not need special syntax for declaration and are declared as soon as they are used for the first time on the left hand side of an expression. Member variables exist within the scope of the class which declares them, and global variables are accessible from anywhere within the system. Global variables may only be declared from code, and not script, and unlike locals and members may be declared constant, and hence read-only to the script. Variables are assigned expressions like so:
    	x = 5;
    
    There are also other assignment operators: += -= *= /= ++ --
    These are statements and not expressions.
    	x += 5; 	// ok (x = x + 5)
    	x++;		// ok (x = x + 1)
    	x = y += 5; 	// error
    
    Note: the ++ -- operators are post-increment only.

    Native Functions

    Native functions can be bound and used in expressions:
    	function_name(expression_list);
    	value = function_name(expression_list);
    
    For instance:
    	ret = get_value();
    	r = rand(360 * time);
    
    Functions may return a single value, or nothing, and take a list of comma-separated expressions as arguments.

    Conditional statements

    Conditional statements take the form of an if-else chain.
    	if (expression_statement)
    	{
    		statement_list
    	}
    	else if(expression_statement)
    	{
    		statement_list
    	}
    	else
    	{
    		statement_list
    	}
    
    Enclosing braces must be used for every block of code, even if it only has one line.

    Iteration statements

    Iteration is achieved with while:
    	while(expression_statement)
    	{
    		statement_list
    	}
    
    The list statements is executed as long as the expression evalulates to non-zero. Example:
    	i = 0;
    	while(i < 10)
    	{
    		// do something
    
    		i++;
    	}
    
    There are two special keywords for iteration statements which control its flow: break and continue. break immediately jumps out of the loop and carries on straight after the closing brace. continue jumps forward to the start of the next iteration of the loop.

    Wait statements

    Wait statements are used to stop processing the script and leave it for a certain period of time.
    	wait(expression_statement);
    
    This waits for the specified number of seconds. If the value is less than or equal to zero, it has no effect.

    Emit statements

    	emit_function emit_type (expression_list) [ : [control_func] [affector_list] [anchor_list] ];
    

    Die statements

    	die;
    

    Controller statements

    	raise event_name;
    	raise event_name(constant_list);
    	suspend;
    	suspend(constant_list);
    	signal;
    	signal(constant_list);