Monkeying Around with mojo3d: wrapping physics behaviours

This post continues on from Monkeying Around with mojo3d: physics behaviours.

As always, 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.

Note that this time, I have moved the PlaneBehaviour class into its own file, to more accurately reflect how a real-world project would be organised.

Download the updated code and media below before continuing:

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!]

Open up both island.monkey2 and planebehaviour.monkey2 in the Ted2Go editor, and double-click the island.monkey2 tab to set it as build file; a litte + sign will appear in the tab to make this clear. Making island.monkey2 the build file means that even if you’re tweaking the PlaneBehaviour class, you can just hit Ted2Go’s Run command (F5 or the rocket icon) to build island.monkey2.

You can double-click the tab again to de-select island.monkey2 as the build file. When no file is set as the build file, whichever tab you hit Run from will be treated as the build file.

Splitting into separate files makes the main file much simpler, and makes jumping between the PlaneBehaviour definition and the main code a simple switch between tabs, rather than scrolling up and down between class and main code.

Importing Code

Note that the main code file now features this line near the top to import the PlaneBehaviour file, making the PlaneBehaviour class accessible to island.monkey2:

#Import "planebehaviour"

You can specify the full name, like so, if you prefer:

#Import "planebehaviour.monkey2"

 

Before we proceed, a ‘Mea Culpa’…

In the last post, I ended by saying:

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.

While the first line is true, the second line is… well, an embarrassing mistake!

I had thought that the Behaviour class’s New method could only take the default entity:Entity parameter listed in the Monkey2 documentation.

However, PlaneBehaviour extends Behaviour, and we can in fact define our own overridden New methods!

What this means, in short, is that we can easily pass in any parameters we want to PlaneBehaviour, and therefore set everything up inside the PlaneBehaviour class, instead of partly in the main program and partly inside PlaneBehaviour (without the trickery I had intended to demonstrate!).

The Old

Previously, the plane was set up outside the PlaneBehaviour class, in the main program (within OnCreateWindow), like so:


		Local plane_size:Float					= 16.83 * 0.5
		Local plane_box:Boxf					= New Boxf (-plane_size, -plane_size, -plane_size, plane_size, plane_size, plane_size)
		
		Local plane_model:Model					= Model.Load ("asset::1397 Jet_gltf_3B3Pa6BHXn1_fKZwaiJPXpf\1397 Jet.gltf")

			plane_model.Mesh.FitVertices (plane_box)
			plane_model.Mesh.Rotate (0, 180, 0)
			
			plane_model.Move (0.0, 5.0, 0.0)

		' Cull hidden tris...
		
		For Local mat:Material = Eachin plane_model.Materials
			mat.CullMode = CullMode.Back	
		Next

		' Physics setup...
		
		Local plane_collider:SphereCollider		= plane_model.AddComponent <SphereCollider> ()

			plane_collider.Radius				= plane_size * 0.6
		
		Local plane_body:RigidBody				= plane_model.AddComponent <RigidBody> ()

			plane_body.Mass						= 10954.0
			plane_body.Restitution				= 0.5
			plane_body.AngularDamping			= 0.9
			plane_body.LinearDamping			= 0.1
			plane_body.Friction					= 0.0

		plane									= New PlaneBehaviour (plane_model)

This is not ideal, as it places all of the technical setup in the main body of the code, reducing the simplicity of setting up another plane, or several planes, as you have to define a collider and rigid body every time. When this complexity is all wrapped-up into the class, setting up a new plane can be greatly simplified, requiring nothing more than a simple call to New PlaneBehaviour.

The New

In this revised version, the setup code simply loads the model and sets up some basic values for physics processing:


		Local plane_model:Model					= Model.Load ("asset::1397 Jet_gltf_3B3Pa6BHXn1_fKZwaiJPXpf\1397 Jet.gltf")

		' Core values for physics...

		Local mass:Float						= 10954.0
		Local roll_rate:Float					= 550000.0
		Local pitch_rate:Float					= 250000.0
	
		' And pass plane_model into PlaneBehaviour, where physics
		' will be applied...
		
		plane = New PlaneBehaviour (plane_model, mass, roll_rate, pitch_rate)
		

The model is loaded and then a few core values are set up — mass, roll rate and pitch rate (technically single elements of the torques that will be applied to specific axes).

Finally, New PlaneBehaviour is called, passing in:

  • plane_model
    the model loaded and set up earlier;
  • mass
    the mass, defined above as 10954 kg;
  • roll_rate
    the roll rate (element of torque to be applied to the z-axis);
  • pitch_rate
    the pitch rate (element of torque to be applied to the x-axis).

These values are arbitrary — just the values I chose to pass in — but you could easily amend this to suit yourself.

For example, if you decided to simply hard-code the mass, roll rate and pitch rate as fixed values inside the class, you could remove these elements (from both the call here, and the PlaneBehaviour New definition) and simply set them inside PlaneBehaviour’s New method.

The idea here is simply to demonstrate the ability to pass in changeable values to New PlaneBehaviour.

In a professional setup, other programmers might be calling your New PlaneBehaviour method to set up enemy aircraft, for example.

If you’ve hard-coded these values inside PlaneBehaviour, anyone (including you!) trying to tweak these for different types of enemy plane would have to edit the PlaneBehaviour class, affecting all other planes in the game! By allowing these values to be passed in per-plane, they can be defined by the calling programmer without affecting any other aircraft behaviours.

Down inside PlaneBehaviour, we’ve gone from this:


	Method New (entity:Entity)
		
		Super.New (entity)
		AddInstance ()

	End
	

… to something that looks much more complex, but is in fact much the same code as before, just moved from outside the class to inside the class:


	Method New (entity:Entity, mass:Float, roll_rate:Float, pitch_rate:Float)
		
		Super.New (entity)
		
		AddInstance ()

		Local plane_size:Float					= 16.83 * 0.5
		Local plane_box:Boxf					= New Boxf (-plane_size, -plane_size, -plane_size, plane_size, plane_size, plane_size)
	
		' Get a Model-specific handle to the entity:
		
		Local plane_model:Model					= Cast <Model> (entity)
		
		plane_model.Mesh.FitVertices (plane_box)
		plane_model.Mesh.Rotate (0, 180, 0)
			
		plane_model.Move (0.0, 5.0, 0.0)

		' Cull hidden tris...
		
		For Local mat:Material = Eachin plane_model.Materials
			mat.CullMode = CullMode.Back	
		Next

		' Physics setup...

		Local plane_collider:SphereCollider		= entity.AddComponent <SphereCollider> ()

			plane_collider.Radius				= plane_size * 0.6
		
		Local plane_body:RigidBody				= entity.AddComponent <RigidBody> ()

			plane_body.Mass						= mass
			plane_body.Restitution				= 0.5
			plane_body.AngularDamping			= 0.9
			plane_body.LinearDamping			= 0.75
			plane_body.Friction					= 0.0

		roll_torque								= roll_rate
		pitch_torque							= pitch_rate
		
	End
	

We’re now loading and adjusting the model here in PlaneBehaviour.New and doing setup of the physics collider and rigid body where they really belong. (The code remains much the same as it was before, when it was defined in OnCreateWindow, but is now properly encapsulated within the class.)

The first two lines remain as before, creating a Behaviour from the entity (plane model), and placing it into the scene; just treat these as necessary boilerplate lines, as mentioned last time — copy and paste!

We’ve then moved all the loading and positioning code into the class itself, setting up the plane’s size, bounding box and adjusting the mesh to fit, then positioning the plane at its start point.

One line that will almost certainly need explanation is this, just after the size and bounding box definition:

Local plane_model:Model					= Cast <Model> (entity)

Behaviour requires that an Entity object be passed in, Entity being the ‘lowest common denominator’ class of 3D object.

All 3D objects in mojo3d are variations of the Entity class: Model, Light and Camera are all extended versions of class Entity (each of these extends the Entity class), but each has its own domain-specific functionalities, some of which would include:

  • Model: Holds a mesh and materials to be applied to the mesh;
  • Light: Holds a direction, range and colour;
  • Camera: Holds near and far ranges, field of view, ability to render to a Canvas, etc.

Entity, on the other hand, gives us the core positioning and rotation methods used by Model, Light and Camera, as well as many other basic functionality.

Because Behaviour.New only gives us a reference to an entity (not a Model, or a Camera, for example), we need to ‘cast’ the entity to the specific class of entity we expect to operate upon; in this case, a Model.

We create a local variable to hold this Model reference:

Local plane_model:Model ...

… and call Cast (entity) with the modifier to receive the type of entity required:

... Cast <Model> (entity)

The plane_model variable therefore now holds a reference to the entity upon which we can call Model-specific methods.

After setting up the model, we cull any backfacing triangles, as before, and then set up the collider and rigid body — again, as before.

Lastly, the renamed roll_torque and pitch_torque fields (which are still technically misnamed!) are set up from the roll_rate and pitch_rate parameters passed in.

In Summary

  • Use the extended Behaviour’s New method to get the entity (usually a model*) and any required data into the class, and set up your model and its physics collider and rigid body. (There is certainly a case to be made for leaving the model adjustment outside of the class, but this will be down to your own judgement.)* Note that there’s no reason you can’t pass in other types of entity, such as a camera you’d like to be physically controlled.
  • Use OnStart to do any initial startup processing; in this case, we get a handy typing shortcut to the physics body and set the plane’s initial impulse force.
  • Use OnUpdate to process any custom updates, such as applying player input forces to the entity, as happens here.

Other Tweaks

  • The rigid body’s LinearDamping value has been tweaked to 0.75; previously, in a tight turn, the plane’s rotation appeared unrealistically tight. This looks a little better.
  • The plane’s collider visualisation (the semi-transparent sphere you could enable) has been removed.
  • Support functions have been to aerialcamera, which is the only place they’re used.

This post has probably been rather technical, but just look at the code itself and cross-reference here for anything you don’t quite ‘get’!

In the next post, I’ll demonstrate the power of Behaviours a little more clearly by adding bullets, created as Behaviours, placed in the world and then left to their own devices, with no additional management code necessary!

Comments are closed.