Creating the Knight

This document comes as support material to the sources of Mediécross.
It focuses on explaining the Knight character controller, the main script of interest can be found in the project directory at 'scripts/character_control.nut'.

Creating the Character Controller


The character controller is a stand-alone scene containing the character model and its skinning hierarchy, the character motions and the various scripts required to control the character using the keyboard. This scene is used in each level scene through an instance.

Modeling and Animation

The only character of the game is the knight who must fight to finish each track in a given amount of time. The knight model was modeled in 3DS Max then transferred to Maya using the Autodesk FBX exchange format for animation.


Early 3DS Max screenshot of the Knight model.

The final setup contains the rigged knight model and all its animations grouped in one take. The final setup is then exported to FBX from MAX or Maya before being imported in the editor.

Additional setup in the Editor

While it is possible to work with the FBX file only, there were a few things we wanted to do that were easier done by directly modifying the scene template after import.


Overview of the knight scene (scenes/knight_controller.nms).

The Knight controller is pretty much the same as the sample Swat controller: A dummy physic item is created and the root of the character skeleton is parented to it. This dummy object is controlled by the Physic engine and the character hierarchy will follow it. We'll refer to it as the Controller since it is the only object really moving and interacting with the level features. The hierarchy merely follows the Controller and the skinned object follows the hierarchy.

The character script code was started from the Swat sample script and extended for the project specific needs.

We wanted to use a special camera rotating around the Knight character for the level start, end and game over. The camera rotation movement is done by using a proxy item directly parented to the Controller to which the special camera is parented. This dummy item uses the 'Rotate Item' builtin script to make the camera orbit around the knight.

The Knight Controller Script


The knight controller script can be found in 'scripts/character_control.nut'. The whole script uses a standard state machine and is fairly well documented so let's focus on the difficult parts only.

Global Structure

This script uses 4 engine event callbacks to do its job:

  1. OnSetup: Here we setup all the elements we need for the character. Most notably the visual FX class instance is created here and the sound FX are loaded here.
  2. OnCollision: This function handles collision with the 'enemy' features of the level. It reacts to the collision depending on the colliding item name.
  3. OnEnterTrigger: Here we detect trigger collisions. Actions are taken depending on the trigger name. One obvious trigger used is the end of level one that is used to determine when to finish the race. The other trigger used is the food item one. We could have detected physics collision with the food item but using a trigger is usually lighter both on memory and CPU as it does not imply having a full-blown rigid body in the physics system.
  4. OnPhysicStep: This function is called after each physic engine solver step is complete. The controls and gameplay are done from this function to guarantee framerate independence. This method provides an easy way to do that without having to scale every forces or impulse we apply to the Controller. This function also applies a state dependent damping by reducing the Controller velocity along the Z axis after each update.

Jumping and Landing

Determining when a 3d character moving on an arbitrary surface starts a jump and lands from that jump is trickier than it sounds. The problem is made a lot easier in Mediécross since we know the level is perfectly flat and the only features we might interact with are well identified and their shape is known and fixed.

Jumping is handled at line 352 of the script. A simple upward linear impulse is applied to the Controller to make it move up in the air. Landing is handled at line 147 after a collision with the ground has been detected. You might think that simply applying an impulse and switching to the jump state, then waiting for the Controller to collide with the ground to switch back to the idle state might be sufficient... That is unfortunately not enough in most cases.


                          
  1. if (KeyboardSeekFunction(DeviceKeyPress, KeySpace))
  2. {
  3. // Switch to the Jump state and motion.
  4. SetMotion(item, motion_bank.Jump)
  5. state = State.Jump
  6.  
  7. // Apply an upward linear impulse to the physics item.
  8. ItemApplyLinearImpulse(item, Vector(0.0, 15.5, 0.0))
  9.  
  10. // Start a sound FX and a visual FX for the jump.
  11. MixerSoundStart(sfx_mixer, sfx_jump)
  12. visual_fx.ShowFX(4, ItemGetWorldPosition(item))
  13.  
  14. // Prevent landing for the next 0.1 seconds.
  15. prevent_landing = Sec(0.1)
  16. }

                        
The code to start a jump (line 352).

Physics engines, while they try very hard not to let penetration happen, cannot guarantee at all times that an object is not slightly sunk into another. What usually happens is that after applying the jump impulse and switching to the Jump state a collision with the ground is immediately reported on the next physic step effectively causing the character to land and the jump to abort. This can be avoided by using a very short timer to prevent landing during the 10ms following a change to a Jump state. In practice this is never felt by the player and efficiently filters out double or triple jump triggering.


                          
  1. // If the prevent landing delay is elapsed...
  2. if (prevent_landing <= 0.0)
  3. {
  4. // Switch to the idle state.
  5. SetMotion(item, motion_bank.Idle)
  6. state = State.Idle
  7.  
  8. // Display a visual FX.
  9. visual_fx.ShowFX(visual_fx.GetLandFXIndex(), ItemGetWorldPosition(item))
  10. }

                        
Only land if the delay is elapsed (line 158).

Time and Score Bonuses

The game incorporates elements such as barrels (hitting a barrel will reduce score and slow the player down) and food (eating food will grant a +0.5 second bonus). Dealing with those gameplay elements is done from the Controller script but the global time counter and score are handled at an higher level by the scene.

The approach I used to cleanly solve this was to store the local time and score bonuses in the Controller script instance and collect them periodically from the level game loop before applying them to the global counters.

Eating Food

When colliding with a food item the Eat() function of the food item must be called. This problem is very easily solved but it is completely dependent on the organization you choose for your data.


The food item template scene organization.

The food item template scene 'scenes/items/food_full.nms' uses a rather strange configuration. It was done this way only because we were rushed by time. A dummy item instantiates the food.nut script while the food trigger and the food mesh are parented to this dummy. When the Controller item enters the food trigger the trigger parent item must be retrieved in order to call its script Eat() function. This is much easier to do than it sounds but we got a bit overboard here and it could have been even easier had we taken 10 seconds to plan things out.


                          
  1. // Get the Food class instance from the trigger parent item and call its Eat member.
  2. ItemGetScriptInstanceFromClass(ItemGetParent(trigger_item), "Food").Eat()