This post continues on from Monkeying Around with mojo3d: arcade plane physics.
Download the code and media below before continuing, and open up island.monkey2 in the default Monkey2 editor, Ted2Go:
IslandDemo source and media [4.6 MB; note that this is NOT a secure server, but the zip file contains only source code — text — and the 3D media. Run it through your virus scanner for peace of mind!]
Again, the intention of this post is that you scroll through the code at the same time, in order to see what each section is doing.
Tidying up the previous post
This time, we will look at control of the plane, via the PlaneBehaviour class… but let’s quickly get the application’s remaining OnRender method out of the way:
Method OnRender (canvas:Canvas) Override If Keyboard.KeyHit (Key.Escape) Then App.Terminate () RequestRender () scene.Update () camera.Update (plane) camera.Render (canvas) canvas.DrawText ("FPS: " + App.FPS, 0, 0) canvas.DrawText ("Y: " + plane.Entity.Y, 0, 20) End
Pretty simple — it checks if the Escape key has been hit, and exits the application if so.
It also asks mojo (the graphics engine) to render a new frame when it’s ready to do so.
The most important line for us here, which we’ll come back to in a sec, is:
scene.Update ()
There follows a call to the AerialCamera’s update method, to which we pass the PlaneBehaviour object, and, from this, AerialCamera will obtain the entity and rigid body information necessary to visually track the plane. (This is irrelevant to the point of the post, and specific to the custom AerialCamera class.)
Finally, there’s a call to render the scene from this camera; the remaining two lines simply print some text on-screen.
Going back to scene.Update, this is in fact where all the physics magic happens!
Without scene.Update, no physics processing occurs, so it’s vital you call this if you want your physics objects to do anything.
(Note that the scene handle was set up at the very start of the OnCreateWindow method.)
Within scene.Update, mojo3d iterates through all of the 3D entities in the scene (models, lights, etc) and asks them to update themselves; it then runs the core physics processing against all rigid bodies in the world (which are typically attached to entities) and everything moves, falls and collides automatically.
That iteration through all scene entities, asking them to update themselves, is where Behaviours come in.
Behaviours
Behaviours provide a way to easily manage all physics-based entities in the scene, without needing to manually track or process them using lists or other collections.
Aside: Note that Monkey2’s Behaviour class, unlike its Color class, uses the non-US spelling!
For physics-based entities, you’ll generally create and position an entity (a model, camera, or whatever), attach a collider and rigid body and leave scene.Update to process and move the body within the scene, according to gravity or any other forces, such as collisions between objects.
If you want to then apply forces manually, you’ll need a way to track and manage all of these objects, such as a globally-accessible list, which would then also require manual iteration in order to visit each object and apply any relevant forces.
Not with Behaviours!
These are automatically managed by scene.Update; you just need to define a Behaviour class for any given object type and during scene.Update the Behaviour’s OnUpdate method will be called.
It means you can simply place a physics-based entity in the world and leave it to its own devices, with no additional management code.
Here’s the plane’s Behaviour class in full (minus comments):
Class PlaneBehaviour Extends Behaviour Property RollRate:Float () Return roll_rate End Property PitchRate:Float () Return pitch_rate End Field plane_body:RigidBody Field roll_rate:Float = 500000.0 Field pitch_rate:Float = 200000.0 Field throttle:Float = 1000000.0 Method New (entity:Entity) Super.New (entity) AddInstance () End Method OnStart () Override plane_body = Entity.GetComponent <RigidBody> () plane_body.ApplyImpulse (Entity.Basis * New Vec3f (0.0, 0.0, 500000.0)) End Method OnUpdate (elapsed:Float) Override plane_body.ApplyForce (Entity.Basis * New Vec3f (0.0, 0.0, throttle)) If Keyboard.KeyDown (Key.A) throttle = throttle + 10000.0 Endif If Keyboard.KeyDown (Key.Z) throttle = throttle - 10000.0 Endif If Keyboard.KeyDown (Key.Left) plane_body.ApplyTorque (Entity.Basis * New Vec3f (0.0, 0.0, RollRate)) Endif If Keyboard.KeyDown (Key.Right) plane_body.ApplyTorque (Entity.Basis * New Vec3f (0.0, 0.0, -RollRate)) Endif If Keyboard.KeyDown (Key.Up) plane_body.ApplyTorque (Entity.Basis * New Vec3f (PitchRate, 0.0, 0.0)) Endif If Keyboard.KeyDown (Key.Down) plane_body.ApplyTorque (Entity.Basis * New Vec3f (-PitchRate, 0.0, 0.0)) Endif End End
Taking away the blank lines, that’s only about 40 lines of code; in fact, we could remove a further 6 lines of unnecessary code due to the properties, which I’ve just realised aren’t really doing anything useful here! The working lines of code here therefore total no more than 35 lines.
So, let’s take a closer look.
The properties here are redundant, frankly having been added too early on, and so the later references to both RollRate and PitchRate can simply be replaced with roll_rate and pitch_rate respectively.
Class PlaneBehaviour Extends Behaviour Property RollRate:Float () Return roll_rate End Property PitchRate:Float () Return pitch_rate End Field plane_body:RigidBody Field roll_rate:Float = 500000.0 Field pitch_rate:Float = 200000.0 Field throttle:Float = 1000000.0 ...
We hold a reference to the plane’s RigidBody — technically, this is not necessary, but saves a lot of typing later on!
The remaining fields define some hard-coded values later used when applying torques (rotational forces around an axis) and directional forces (those applied along an axis).
(Physicists will quite rightly point out that these fields are mis-named!)
The large values seen here are due to use of a real-world mass value when setting up the rigid body in the previous post, the Jaguar aircraft used as a physics reference weighing in at around 10,000 kg. (With the default mass of 1.0 kg given to a rigid body, you would normally be using values around 1/10,000th of the sizes shown here.)
Next up is the Behaviour’s New method:
Method New (entity:Entity) Super.New (entity) AddInstance () End
This is boilerplate code — just copy and paste! (It technically binds the Behaviour to the entity being passed in — the plane model in this case — and then places it in the scene.)
Note that New is passed in a 3D ‘entity’ upon creation — in this case, the entity is the plane model we loaded earlier. That entity then gains the physics behaviours defined here.
You can see New being called earlier in the code, with the plane model being passed in, after the physics collider and rigid body were set up:
plane = New PlaneBehaviour (plane_model)
Boilerplate out of the way, let’s move towards the interesting stuff!
OnStart
The OnStart method is called the first time scene.Update runs, and allows us to carry out a little initialisation:
Method OnStart () Override ' Storing this just for typing convenience... plane_body = Entity.GetComponent <RigidBody> () ' Give it a shove (instantly applied, which is ' not physically possible), to get it started... plane_body.ApplyImpulse (Entity.Basis * New Vec3f (0.0, 0.0, 500000.0)) End
Note that we defined the plane_body (RigidBody) field earlier; by assigning the entity’s rigid body here in the startup phase to a handy variable, it means we don’t later need to type the fairly complex GetComponent statement every time we need to access it.
Note: The GetComponent line refers to an Entity; note the capitalisation here. Behaviours hold a reference to the entity they were attached to, accessible via the Entity property.
Entities have a GetComponent method by which you can obtain a handle to any attached ‘components’ such as rigid bodies, and that’s what’s happening here — we’re obtaining a handle to the entity’s rigid body that we set up earlier.
So, from a Behaviour, you can:
- obtain the entity it applies to, via Entity, and
- from the resulting entity, obtain the physics body being operated upon, via GetComponent.
This is the magic that means you don’t need to manually track lists of objects in order to work upon them each frame!
Finally, the Start method applies an initial force to the plane’s physics body:
plane_body.ApplyImpulse (Entity.Basis * New Vec3f (0.0, 0.0, 500000.0))
Important! ApplyImpulse is a special case of force application that you shouldn’t normally use.
Forces, importantly, apply over time (physics students will note that force = mass x acceleration, acceleration being a difference in velocity over time). It’s physically impossible for a force to fully apply instantly in the real world.
The intent of this line is to start the plane moving immediately at a desired velocity, so we need the force to apply instantly. ApplyImpulse achieves this, but in most circumstances, where you would be repeatedly applying forces each frame, you should be using ApplyForce.
Being called within OnStart, this line is called only once, and so the impulse force applies only once, instantly applying 500,000 ‘Newtons’ of force along the plane’s z-axis by the time the first frame runs, giving our plane an instant speed.
Generally-speaking, you should use ApplyImpulse only once in order to instantly set the initial speed of an object — a bullet is a good example, though even that is technically not correct in the real world. (In reality, a very large force applies over a very short period of time.)
Note that all other forces in this demo are applied per-frame via ApplyForce or ApplyTorque in order to correctly apply over time.
If you find you need it, there is also an impulse equivalent for torque forces, ApplyTorqueImpulse — you could try adding this in OnStart to give the plane a starting roll rate, for example.
Entity.Basis and forces
Lastly, an important factor here is that reference to Entity.Basis and the 3D vector!
mojo3d’s physics are based upon a subset of the Bullet Physics SDK‘s functionality.
Bullet will apply forces according to the vector supplied (note the x, y and z values within New Vec3f), but always in relation to the orientation of the world itself.
That means that a force with z-value of 10 will always apply to zero degrees ‘north’ within the world, regardless of a physics body’s orientation.
Here’s a view of the plane, looking down from above:
If we apply a force with z-component of 10, the plane will not move in the intended (red) direction, but in the unintended (yellow) direction, because the force operates in world-space.
To apply the force relative to the plane’s own orientation, we multiply the force by the plane’s ‘basis’.
An entity’s Basis property is just a handy reference to its orientation — how it’s currently rotated in space.
Multiplying the force by the entity’s basis will allow the force to apply along the entity’s own axes; in this case, along its z-axis.
Important: Due to complex mathematical properties — ie. reasons I don’t pretend to understand — you must always place the basis first in this multiplication!
Yes:
Basis * Force Vector
No!
Force Vector * Basis
That’s all you need to know in order to correctly apply a force based upon an entity’s orientation.
Our plane happens to be pointing ‘north’ by default, so is pushed away from the camera along its intended path.
OnUpdate
The OnUpdate method is where the action really happens! As scene.Update iterates through the scene’s entities, it looks for any attached components (a Behaviour is a special case of Component) and calls their OnUpdate methods — and that’s the code we’re defining here.
Method OnUpdate (elapsed:Float) Override ' -Scene.GetCurrent ().World.Gravity.Y * 1.0 plane_body.ApplyForce (Entity.Basis * New Vec3f (0.0, 0.0, throttle)) If Keyboard.KeyDown (Key.A) throttle = throttle + 10000.0 Endif If Keyboard.KeyDown (Key.Z) throttle = throttle - 10000.0 Endif ...
Ignore the commented-out line referring to gravity.
Each frame, we start by applying the throttle force along the plane’s z-axis. Note again that this force vector is multiplied by the plane entity’s bundle of rotation information, Entity.Basis, to ensure it operates along the plane’s own z-axis and not that of the world.
Next we check for two keys, A and Z, having been pressed, and amend the throttle force up or down by 10,000 Newtons — an amount determined by simple trial-and-error.
Next time this method is called, the new throttle value will apply.
We’re applying this force every frame, so why doesn’t the plane accelerate off out of control? It’s kept in check by the body’s LinearDamping value defined in the previous post. Rotational forces are similarly restricted by AngularDamping. For our purposes, these values therefore have an effect similar to air resistance.
Following this, we check the cursor keys and apply torque — loosely speaking, rotational force — around the relevant axes.
Left and right cursors operate around the z-axis (the nose-to-tail axis of the plane). These are also multiplied by the plane’s rotational Basis information, as they would otherwise operate according to the orientation of the world itself:
If Keyboard.KeyDown (Key.Left) plane_body.ApplyTorque (Entity.Basis * New Vec3f (0.0, 0.0, RollRate)) Endif If Keyboard.KeyDown (Key.Right) plane_body.ApplyTorque (Entity.Basis * New Vec3f (0.0, 0.0, -RollRate)) Endif
And finally, the up and down cursors operate on the plane’s x-axis — the left-to-right axis along the wings:
If Keyboard.KeyDown (Key.Up) plane_body.ApplyTorque (Entity.Basis * New Vec3f (PitchRate, 0.0, 0.0)) Endif If Keyboard.KeyDown (Key.Down) plane_body.ApplyTorque (Entity.Basis * New Vec3f (-PitchRate, 0.0, 0.0)) Endif
That’s it! A simple physics-based arcade plane!
(The remainder of the code consists of nothing more than a few support functions, most, if not all, of which should really be moved into the AerialCamera class where they’re used!)
In the real world, you probably wouldn’t want to have the collider and rigid body setup carried out in the main body of the code, but instead do this within the OnStart method of the Behaviour so as to encapsulate all of the plane’s data.
As the New method of Behaviour only allows the passing-in of an entity parameter, this can make things a little tricky, so I’ll address my solution to this in the next post.
Happy flying!