User:Wmonroe4/Motion controllers

From Sirikata Wiki
< User:Wmonroe4
Revision as of 22:14, 9 August 2011 by Wmonroe4 (talk | contribs) (Add sections on the collision response function)
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);
}
// ...
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.