Some aspects of bulletscript can be modified at compile time, and these are defined in bsConfig.h.
Implementing bulletscript
First off, include header file bsBulletScript.h and the library/DLL for bulletscript in your project.
struct Bullet : public bs::UserTypeBase { float x, y; float angle; float speed; bool _active; };The next step is to create a system to manage the objects. There is a specific, important restriction on how you implement this: the location (memory address) of your objects must not change during an update. This is because bulletscript allows objects to be emitted in emitter functions, which are processed during an object update. Bulletscript relies on the user objects remaining in the same place, and emitting (or destroying) an object may cause other objects to move around mid-update. For static arrays and structures such as linked lists, this will not be an issue, but for dynamically-resizing arrays (such as std::vector) this is an issue. A workaround for the latter case is to emit objects into temporary storage, and copy all new objects into the main structure before an update. This is how the TestBed application handles things. For the purposes of this document, we will define a simple system as follows:
class BulletManager { static const int NUM_BULLETS = 1024; Bullet mBullets[NUM_BULLETS]; public: BulletManager() { for (int i = 0; i < NUM_BULLETS; ++i) mBullets[i]._active = false; } void update(float frameTime) { for (int i = 0; i < NUM_BULLETS; ++i) { if (mBullets[i]._active) { // Assume angle is in radians mBullets[i].x += sin(mBullets[i].angle) * mBullets[i].speed * frameTime; mBullets[i].y += cos(mBullets[i].angle) * mBullets[i].speed * frameTime; } } } };This does a simple update on the active bullets. In practice, you will want to update an object's position by yourself. You are free to do whatever updating (speed, angle, colour, etc) yourself if you wish, but at a minimum you should at least do the position.
The next stage is to get objects emitted. To do this, we create a function with the following signature:
bs::UserTypeBase* (*emit_function) (bstype x, bstype y, bstype angle, const bstype* args, void* user);x, y and angle are the corresponding values of the emitter that called the emit function. args is the list of arguments passed to it, and user is an object that the user can pass in to an emitter when the emitter is created. This can be used, for instance, to specify which system should manage any emitted objects, if you have more than one. The function should return a pointer to the newly created object, or NULL if it is not be managed by bulletscript.
bs::UserTypeBase* emitBullet(float x, float y, float angle, const float* args, void* user) { // Assume that there is a global bullet system. We cannot // directly use member functions as function pointers. return gBullets->addBullet(x, y, angle, args); } bs::UserTypeBase* BulletManager::addBullet(float x, float y, float angle, const float* args) { // Find first free slot for (int i = 0; i < NUM_BULLETS; ++i) { if (!mBullets[i]._active) { mBullets[i]._active = true; mBullets[i].x = x; mBullets[i].y = y; mBullets[i].angle = angle; // Important: the argument array is in fact the top of a stack. // Thus, arguments have negative indices. // The first argument is at args[-num_args], and the last at args[-1]. // Assume that the bullet speed is passed in the first argument. mBullets[i].speed = args[-1]; return &mBullets[i]; } } // If we get here, there are no free slots return 0; }The next stage is to create a function to handle object destruction:
void (*die_function) (bs::UserTypeBase* userType, void* user);userType is the object to destroy, and user is a user-supplied object (if there is one).
void killBullet(bs::UserTypeBase *userType, void* user) { Bullet* b = static_cast<Bullet*>(userType); gBullets->killBullet(b); } void BulletManager::killBullet(Bullet* b) { b->_active = false; }The next step is to create functions for handling properties. There are three properties for which you must provide functions: x, y and angle. These are built into bulletscript, but bulletscript does not provide the functions because it cannot make any assumptions about how the user is structuring things. Property functions look like this:
void (*set_function) (bs::UserTypeBase* userType, bstype value); bstype (*get_function) (bs::UserTypeBase* userType);Thus, property functions for x would look like:
void setX(bs::UserTypeBase* userType, float value) { Bullet* b = static_cast<Bullet*>(userType); b->x = value; } float getX(bs::UserTypeBase* userType) { Bullet* b = static_cast<Bullet*>(userType); return b->x; }Affector functions have the following signature:
void (*affector_function) (bs::UserTypeBase* userType, bstype frameTime, const bstype* args);An 'accelerate' affector could be written as:
void accelerate(bs::UserTypeBase* userType, float frameTime, const float* args) { Bullet* b = static_cast<Bullet*>(userType); // Like emit functions, affector arguments are specified negatively. // First argument here is the acceleration amount, and we multiply // this by the supplied frame time, in seconds. b->speed += args[-1] * frameTime; }Native functions are C++ functions which are usable in script. Some common examples would be random number generation, and square root. A native function is declared as follows:
int (*native_function)(bs::ScriptState& state);Here, state is a reference to bulletscript's internal script management structure, of which there is one for each emitter (and controller). This contains information such as the instruction pointer, the stack and local variables. Thus, native functions have a fair amount of control over the emitter, and can pause/unpause it by setting its suspend time, or push values onto the stack as a way of returning values.
The stack pointer (ScriptState::stack) points to the stack base. ScriptState::stackHead is the 'top' pointer, used to access the top of the stack. It is actually the offset of the next available space on the stack, and thus accessing the stack means using it with a negative offset.
For instance, the script function rand is implemented as follows:
int bm_rand(ScriptState& state) { int rv = rand(); // Get the first argument off the stack. bstype scale = state.stack[state.stackHead - 1]; // Scale our random number to the desired range. bstype result = scale * (rv / (float) RAND_MAX); // Push result onto stack. Normally, when a function ends the arguments must be popped off the stack. This would be // done by subtracting the number of arguments from state.stackHead. However, in this case, we are returning a value, // so this must be pushed onto the stack, taking the argument's place, so we don't need to modify state.stackHead. state.stack[state.stackHead - 1] = result; return ScriptOK; }This function is used in script as:
x = rand(10); // generate random float between 0 and 10The return value is used to tell BulletScript the state of the script. Because the function takes a reference, you are able to modify it, but unless you tell BulletScript how you have done this, errors will occur. If you have just modified the stack, then return ScriptOK. However, you can also set the ScriptState::suspendTime value, which will cause the script to halt execution. This can be used to implement a custom 'wait' function, eg:
int my_wait(ScriptState& state) { float count = state.stack[--state.stackHead]; state.suspendTime = count; return bs::ScriptSuspended; }
bs::Machine machine;We then create a new object type:
machine.createType("bullet");Once this is one, we bind our system as follows:
machine.registerEmitFunction("bullet", "emit", 1, emitBullet);Here, "emit" is the name of the function in script, 1 is the number of arguments it takes, and emitBullet is the function that we defined earlier. We can bind as many emit functions as we want: for instance, we may have one function which emits objects at an angle, one which emits then at a target, etc.
Next, we bind the 'die' function. There can be only one of these per type.
machine.setDieFunction("bullet", killBullet);The next thing to do is to bind the property functions for x, y and angle, plus any other properties we've implemented. x, y and angle are special properties - anchors - so use a different binding function from normal properties.
machine.setAnchorX("bullet", setX, getX); machine.setAnchorY("bullet", setY, getY); machine.setAnchorAngle("bullet", setAngle, getAngle); machine.registerProperty("bullet", "speed", setSpeed, getSpeed);Affectors are bound as follows:
machine.registerAffector("bullet", "accelerate", accelerate);Finally, native functions are bound with:
machine.registerNativeFunction("rand", bm_rand);
The user can also expose variables to script: globals, and controller member variables.
// 'true' refers to the variable being read-only from the script. machine.registerGlobalVariable("ScreenSize_X", true, 640); machine.registerGlobalVariable("ScreenSize_Y", true, 480);To declare controller member variables, use:
machine.declareControllerMemberVariable(ctrl_name, variable_name, initial_value);The variable can then be used in script as a normal member variable.
Once this is done, the next stage is to compile scripts. This is done with:
machine.compileScript(script_buffer, buffer_size);A simple example:
FILE* fp = fopen("blaster.script", "rt"); fseek(fp, 0, SEEK_END); size_t fileLength = ftell(fp); fseek(fp, 0, SEEK_SET); fread(buffer, fileLength, 1, fp); fclose(fp); if (machine.compileScript(buffer, fileLength) != 0) { // get errors }This function returns the number of errors in the script.
Once scripts have been compiled, the next step is to create emitters and controllers. This is done with:
bs:Emitter* emit = machine.createEmitter(emitter_name, x, y, angle, userObject); bs::Controller* ctrl = machine.createController(controller_name, x, y, angle, userObject);userObject can be ignored (it defaults to NULL), if wanted. These objects can then be manipulated (eg changing position/angle, enabling/disabling, raising events) outside of the main update loop. They are destroyed with:
machine.destroyEmitter(emit); machine.destroyController(ctrl);The main update loop is as follows:
machine.preUpdate(frameTime); // Update user system gBullets.update(frameTime); machine.postUpdate(frameTime);Any changes to bulletscript objects, for example emitters and controllers, must be done outside of this loop. This is worth bearing in mind if you have a system which emits objects which use emitters (eg enemy ships). If this is the case, you will have to emit the object to a temporary structure, and add them outside of the loop.
Let's revisit our management class, and add a line to make bulletscript update our objects:
void BulletManager::update(float frameTime) { for (int i = 0; i < NUM_BULLETS; ++i) { if (mBullets[i]._active) { // Assume angle is in radians mBullets[i].x += sin(mBullets[i].angle) * mBullets[i].speed * frameTime; mBullets[i].y += cos(mBullets[i].angle) * mBullets[i].speed * frameTime; // Now, we tell bulletscript to update our objects. // Assume that the bulletscript machine is globally accessible. machine.updateType(&mBullets[i], mBullets[i].x, mBullets[i].y, mBullets[i].angle, frameTime); } } }We don't have to have bulletscript update the objects. If we want, we can just use bulletscript for emitting objects, and do all the update ourselves. This may be necessary if you need a very large number of objects.
int errVal = machine.registerGlobalVariable("Player_X", true, 0); if (errVal != BS_OK) { std::string errMsg = getErrorMessage(errVal); std::cerr << errVal << std::endl; }In the case of script errors, these are written to the bulletscript log file, which can be queried like so:
if(machine.compileScript(buffer, bufferSize) != BS_OK) { const bs::Log& _log = machine.getLog(); std::string msg = _log.getFirstEntry(); while (msg != bs::Log::END) { std::cerr << msg << std::endl; msg = _log.getNextEntry(); } }The code in this document can be found in the minimal example in the /examples/minimal directory.