User:Wmonroe4/Motion controllers

From Sirikata Wiki
Jump to navigation Jump to search

The motion controller library in std/movement/motion.em is a simple way to program common types of motion for presences in Sirikata without reinventing the wheel. This tutorial is an introduction to show you how to use the library to script interesting movement in a minimal amount of code.

As I work on this, I'll also be working on the automatic documentation for the API reference, so keep an eye on that (http://www.sirikata.com/docs/head/emersonapi/) as well.

Creating a motion controller

At its most basic, setting up a motion controller is as simple as:

system.require('std/movement/motion.em');
// ...
new motion.SomeTypeOfMotion(presence, extra arguments);

As a simple example, with no extra arguments:

new motion.Gravity(presence);

This will set presence falling at about 9.81 meters per second squared downward, until the end of time.

suspend and reset

If this is all you want, then that one line is all you need. If, however, you may want to release the presence from the hold of gravity, or change the direction or acceleration of gravity, you're going to need to save the controller object for later:

var gravity = new motion.Gravity(presence);

Then when you need to, you can stop gravity:

gravity.suspend();

and restart it later:

gravity.reset();

Every motion controller has suspend and reset methods. suspend makes the controller stop acting on the presence, and reset makes it start again. (These methods are not well tested, so let me know if you think you've found a bug in one.)

Extra arguments and fields

Every motion controller also has fields that you can modify at any time to change the behavior of the controller. Gravity has two: presence and accel. By reassigning presence, you can change which presence the gravitational acceleration applies to, and by changing accel (a vector), you can change how fast the presence falls:

gravity.accel = <0, -5, 0>;

or even make the presence fall sideways:

gravity.accel = <4, 0, 0>;

These fields are initialized from arguments to the constructor, so if you simply wanted gravity to be in a bizarre direction to begin with, you could just create it like this:

var gravity = new motion.Gravity(presence, <4, 2, -9>);

The constructor arguments are much more versatile than the dynamic fields, so it can be to your advantage to set up the properties you want in the constructor. Above, you saw that the accel argument to the gravity constructor is optional; if you leave it out, it will assume a reasonable default (down, at standard Earth acceleration). If instead you just want really weak gravity, the constructor can simply take a number:

var gravity = new motion.Gravity(presence, 0.8);

Down is still implied. The dynamic accel field isn't as smart: unlike the constructor argument, the accel field has to be a vector, so assigning 0.8 to gravity.accel won't work—you have to explicitly assign <0, -0.8, 0>.

Using the Collision controller

The motion.Collision controller is probably the most useful of the controllers currently in motion.em, but it involves many complicated interactions, so it can be tricky to get right. Just constructing the controller is the hardest part:

system.require('std/movement/motion.em');
system.require('std/movement/collision.em');
// ...
var collision = new motion.Collision(myPresence, coll.TestSpheres(aBunchOfVisibles), coll.Bounce(0.8));

You need to create a new controller for every presence you want to react to collisions, putting the presence in the first argument to the constructor.

The second argument is the test function, which specifies how the presence detects collisions. In this case, we're using coll.TestSpheres(visibles), which detects collisions with bounding spheres of a specified list of visibles. The "hardest part" I was referring to isn't even in the code above—it's coming up with that list of visibles. Although it's tempting to use system.getProxSet, it's almost always better to use a list that you control more directly, preferably by having created each of the presences the list refers to, if only because getProxSet might include a terrain and other large objects that you don't want to treat as spheres (I've seen other, less predictable problems with getProxSet).

The third argument is the response function, which says how the presence reacts to a collision. Here we use the predefined response template coll.Bounce(elasticity), which causes the presence to bounce with a certain fraction of its original momentum. The 0.8 is optional (defaults to 1), but don't forget the parentheses! Simply passing in coll.Bounce won't do what you want—you need to call it with an optional parameter, which it uses to construct and return a customized response function for you. You then pass this returned function in to the Collision constructor. coll.TestSpheres follows the same pattern; don't forget to call it!

Executing the last line above many times, with a different myPresence each time, will set up a bunch of presences to bounce off the visibles you specified. If the presences themselves are among the visibles, they will bounce off each other as well! (This is a very common use case, and to make things easier, it's fine to have myPresence be among the visibles—a presence will never detect collisions with itself. This will let you use the same set of visibles for all of the controllers.)

Making your own response functions

A bouncing ball is fun to watch for a while, but it gets old quickly. Usually you'll want to have other interesting things happen when collisions occur. There's nothing that says you can only use functions in collision.em to make a collision controller! Making your own response functions is the best way to add interesting functionality to your application. A correct response function looks like this:

function explodeOnCollision(presence, event) {
    makePresenceExplode(presence);
    /* No, there's no built-in makePresenceExplode function.  You'll have
       to write that one yourself.                                        */
}
// ...
var collision = new motion.Collision(system.self, coll.TestSpheres(bunchaVisibles), explodeOnCollision);

Metafunctions

Hopefully you noticed that in the above line, you don't want to call explodeOnCollision, you just pass it in to the Collision constructor. The built-in coll.Foo functions are different because they are implemented as templates or metafunctions, functions that return functions. If you check out the collision.em implementation, the response functions look like this:

coll.Bounce = function(elasticity) {
    return function(presence, event) {
        // do stuff to presence that depends on elasticity
    };
}

This means that Bounce is not a response function, but something that generates response functions. This can be handy for code reuse, and is used throughout the coll library for consistency, but it is by no means necessary. Ultimately, the Collision constructor is looking for something along the lines of explodeOnCollision.

The collision event

Of course, if all you could do with a presence on a collision was to make it unilaterally explode, the collision system would be pretty useless. The really interesting information that you can use in the response function comes from the second parameter, which above is called event. event is an object that looks like this:

{
    self: {
        id: '7a145faf-9be5-4858-910d-93198e30e822:12345678-1111-1111-1111-defa01759ace',
        mass: 1,
        velocity: <-4.32338956906, 0, 0>
    },
    other: {
        plane: {
            anchor: <0, 0, 0>,
            normal: <1, 0, 0>
        },
        mass: 2.5,
        velocity: <0, 0, 0>
    },
    normal: <1, 0, 0>,
    position: <25.5, 0, 25.5>
}

The most interesting pieces of information are the velocities of the two things colliding and the normal vector. event.normal is a unit vector pointing outwards from event.other, perpendicular to the surface that event.self hit. In real-world collisions, all forces occur parallel to the normal vector, so the change in velocity should be the normal vector multiplied by some number. (Of course, there's no reason you have to follow the laws of physics. This is one of the main motivations for having a general-purpose system of motion controllers.) event.position is (approximately) the point of contact of the collision, and can be used for special effects, for example.

The form of self and other is a bit unusual, but this was necessary in order to accommodate collisions with arbitrary shapes. self is guaranteed to refer to a presence; you can test whether other is a visible by comparing typeof(event.other.id) === 'string'. If it is, you can manipulate it by constructing an instance of std.movement.MovableRemote, or by finding it in the system.presences list or another variable if you know it's one of your entity's presences. Usually, however, you'll just want to send a message to it, or leave it alone and let it look after its own collision.

Reusing the built-in response functions

What if you want to make your presence bounce with an elasticity of 0.8 and cause something to explode? Never fear, you don't need to rewrite coll.Bounce or (almost as bad) copy and paste it into your custom function. Simply construct an built-in response function from one of the templates, and immediately call it, passing the two arguments to your response function:

function detonateBombAndBounce(presence, event) {
    makePresenceExplode(bombPresence);
    coll.Bounce(0.8)(presence, event);
}

Yes, that looks a bit weird. It's correct, though: you're calling a function that's returned from another function call.

Making your own test functions

If you're mathematically inclined, or you are trying to do something really strange with the collision system, you may also find it useful to write your own test function. The way you do it is very similar to writing your own response function, but the result is usually much more complicated (and, if you are actually writing a collision detection algorithm, more computationally taxing). Here's what a custom test function looks like:

function TestForBizarreCollision(presence) {
    if(isPresenceInBizarreCollision(presence)) {
        return {
            self: {
                id: presence.toString(),
                mass: motion.util._mass(presence),
                velocity: presence.velocity
            },
            other: {
                bizarre: 'the Matrix',
                mass: 0,
                velocity: <0, 0, 0>
            },
            normal: <0, -1, 0>,
            position: presence.position + <0, presence.scale, 0>
        };
    }

    /* I've been told it's bad style to have a function sometimes return
       a value and sometimes return void (undefined).  If this makes you
       cringe, I believe "return null;" also works here, but I haven't
       tested this, so if you get errors, try just using "return;".      */
}

The Collision controller calls this function to find out if there has been a collision, and if it gets an object back, it passes that object to the response function as the event parameter. In addition, if other has a presence/visible ID, the controller sends the collision event as the collision field of a message to the visible represented by other.id.

One thing to keep in mind when writing a test function is that this function will be called often. The default period for all motion controllers (motion.defaultPeriod) is 0.06 seconds, meaning the test function will be called about 17 times every second. This means that doing heavy computation in this function can be disruptive. Keep things simple: a square root and a trig function here and there are fine, but a Taylor series implemented in Emerson probably isn't.

The Collision controller and the built-in response functions require normal, position, the two masses and the two velocities to be there and to have sensible values (i.e., be vectors, except for the masses, which should be positive numbers or zero to represent infinite mass). They also expect that anything passed in as a string in the id field of self or other, if present, is an actual presence/visible UUID. Other than that, you can add any additional fields you like, though these fields will only be useful if you define a custom response function as well.

Writing your own controller

I've written a few useful types of controllers, but these aren't nearly enough to anticipate every possible use. This is why the majority of motion.em consists of definitions for what are essentially abstract base classes, motion controllers that barely do anything other than provide a template for other motion controllers that you design.

These abstract controllers include:

Each one takes three arguments to the constructor: the presence to control (as usual), a callback function, and optionally the period at which the controller calls the callback.

Writing callbacks

There are two ways to make a custom motion controller using one of these as a template. The easiest is simply to make one and pass in an appropriate function:

var resistance = 0.5;

function airResistanceAccel(pres) {
    return pres.velocity * (-resistance * pres.velocity.length());
}

var myAirResistanceController = new motion.Acceleration(myPresence, airResistanceAccel);

The period is missing, so it is assumed to be motion.defaultPeriod. The acceleration callback returns a vector every time it is called, in order to specify an acceleration every tick; with the Position, Velocity, Orientation, and OrientationVel controllers, you can also return no value ("return;" or just let the function end) to leave the position, velocity, etc. unchanged.

Extending motion.*

The other way to make your own type of motion controller is to inherit from one of these types of motion controllers:

system.require('std/core/bind.em');

airResistance = motion.Acceleration.extend({
    init: function(presence, resistance, period) {
        this.resistance = resistance;
        function acceleration(pres) {
            return pres.velocity * (-this.resistance * pres.velocity.length());
        }
        this._super(presence, std.core.bind(acceleration, this), period);
    }
});

var airResistController = new airResistance(myPresence, 0.5);

This is a bit more complicated, but more extensible, especially if you intend to make your new motion controller part of a library or you want to make many different but similar controllers. Take note of a few things:

  • The motion controller classes use system.Class, which unfortunately (as of 8/10/2011) has very little documentation. There are really only two things you need to do with it, though, and those are to call (Class).extend (with an argument containing all the methods you wish to add or override) and to use this._super within one of your methods to access the base class's corresponding method, if it exists.
  • resistance is copied over to a field of the controller object and from then on accessed only through that field, so it can be modified from outside. This is a feature, not a bug; this is how the motion controllers in motion.em implement dynamic fields.
  • In order to access fields of this from within the inner function, you need to either std.core.bind the inner function to the outer this (like most of the standard library does) or store this in a variable with a different name, such as self (like the motion controller library itself does).
  • There is no need for error handling for a missing period argument in the airResistance class; motion.Acceleration (or more accurately, motion.Motion) handles that for you. You just pass along the undefined that you get in that third slot.