Browse Source

CharacterDemo & VehicleDemo in script.

Lasse Öörni 12 years ago
parent
commit
55ef6dd6c3

+ 4 - 4
Bin/Data/Scripts/17_SceneReplication.as

@@ -329,13 +329,13 @@ void HandlePhysicsPreStep(StringHash eventType, VariantMap& eventData)
             // Movement torque is applied before each simulation step, which happen at 60 FPS. This makes the simulation
             // independent from rendering framerate. We could also apply forces (which would enable in-air control),
             // but want to emphasize that it's a ball which should only control its motion by rolling along the ground
-            if (connection.controls.buttons & CTRL_FORWARD != 0)
+            if (connection.controls.IsDown(CTRL_FORWARD))
                 body.ApplyTorque(rotation * Vector3(1.0f, 0.0f, 0.0f) * MOVE_TORQUE);
-            if (connection.controls.buttons & CTRL_BACK != 0)
+            if (connection.controls.IsDown(CTRL_BACK))
                 body.ApplyTorque(rotation * Vector3(-1.0f, 0.0f, 0.0f) * MOVE_TORQUE);
-            if (connection.controls.buttons & CTRL_LEFT != 0)
+            if (connection.controls.IsDown(CTRL_LEFT))
                 body.ApplyTorque(rotation * Vector3(0.0f, 0.0f, 1.0f) * MOVE_TORQUE);
-            if (connection.controls.buttons & CTRL_RIGHT != 0)
+            if (connection.controls.IsDown(CTRL_RIGHT))
                 body.ApplyTorque(rotation * Vector3(0.0f, 0.0f, -1.0f) * MOVE_TORQUE);
         }
     }

+ 379 - 0
Bin/Data/Scripts/18_CharacterDemo.as

@@ -0,0 +1,379 @@
+// Moving character example.
+// This sample demonstrates:
+//     - Controlling a humanoid character through physics;
+//     - Driving animations using the AnimationController component;
+//     - Implementing 1st and 3rd person cameras, using raycasts to avoid the 3rd person camera
+//       clipping into scenery
+
+#include "Utilities/Sample.as"
+
+const int CTRL_FORWARD = 1;
+const int CTRL_BACK = 2;
+const int CTRL_LEFT = 4;
+const int CTRL_RIGHT = 8;
+const int CTRL_JUMP = 16;
+
+const float MOVE_FORCE = 0.8f;
+const float INAIR_MOVE_FORCE = 0.02f;
+const float BRAKE_FORCE = 0.2f;
+const float JUMP_FORCE = 7.0f;
+const float YAW_SENSITIVITY = 0.1f;
+const float INAIR_THRESHOLD_TIME = 0.1f;
+
+const float CAMERA_MIN_DIST = 1.0f;
+const float CAMERA_MAX_DIST = 5.0f;
+
+Scene@ scene_;
+Node@ cameraNode;
+Node@ characterNode;
+bool firstPerson;
+
+void Start()
+{
+    // Execute the common startup for samples
+    SampleStart();
+
+    // Create static scene content
+    CreateScene();
+    
+    // Create the controllable character
+    CreateCharacter();
+    
+    // Create the UI content
+    CreateInstructions();
+    
+    // Subscribe to necessary events
+    SubscribeToEvents();
+}
+
+void CreateScene()
+{
+    scene_ = Scene();
+    
+    // Create scene subsystem components
+    scene_.CreateComponent("Octree");
+    scene_.CreateComponent("PhysicsWorld");
+    
+    // Create camera and define viewport. Camera does not necessarily have to belong to the scene
+    cameraNode = Node();
+    Camera@ camera = cameraNode.CreateComponent("Camera");
+    camera.farClip = 300.0f;
+    renderer.viewports[0] = Viewport(scene_, camera);
+
+    // Create a Zone component for ambient lighting & fog control
+    Node@ zoneNode = scene_.CreateChild("Zone");
+    Zone@ zone = zoneNode.CreateComponent("Zone");
+    zone.boundingBox = BoundingBox(-1000.0f, 1000.0f);
+    zone.ambientColor = Color(0.15f, 0.15f, 0.15f);
+    zone.fogColor = Color(0.5f, 0.5f, 0.7f);
+    zone.fogStart = 100.0f;
+    zone.fogEnd = 300.0f;
+
+    // Create a directional light to the world. Enable cascaded shadows on it
+    Node@ lightNode = scene_.CreateChild("DirectionalLight");
+    lightNode.direction = Vector3(0.6f, -1.0f, 0.8f);
+    Light@ light = lightNode.CreateComponent("Light");
+    light.lightType = LIGHT_DIRECTIONAL;
+    light.castShadows = true;
+    light.shadowBias = BiasParameters(0.0001f, 0.5f);
+    // Set cascade splits at 10, 50 and 200 world units, fade shadows out at 80% of maximum shadow distance
+    light.shadowCascade = CascadeParameters(10.0f, 50.0f, 200.0f, 0.0f, 0.8f);
+
+    // Create the floor object
+    Node@ floorNode = scene_.CreateChild("Floor");
+    floorNode.position = Vector3(0.0f, -0.5f, 0.0f);
+    floorNode.scale = Vector3(200.0f, 1.0f, 200.0f);
+    StaticModel@ object = floorNode.CreateComponent("StaticModel");
+    object.model = cache.GetResource("Model", "Models/Box.mdl");
+    object.material = cache.GetResource("Material", "Materials/Stone.xml");
+    
+    RigidBody@ body = floorNode.CreateComponent("RigidBody");
+    // Use collision layer bit 2 to mark world scenery. This is what we will raycast against to prevent camera from going
+    // inside geometry
+    body.collisionLayer = 2;
+    CollisionShape@ shape = floorNode.CreateComponent("CollisionShape");
+    shape.SetBox(Vector3(1.0f, 1.0f, 1.0f));
+    
+    // Create mushrooms of varying sizes
+    const uint NUM_MUSHROOMS = 60;
+    for (uint i = 0; i < NUM_MUSHROOMS; ++i)
+    {
+        Node@ objectNode = scene_.CreateChild("Mushroom");
+        objectNode.position = Vector3(Random(180.0f) - 90.0f, 0.0f, Random(180.0f) - 90.0f);
+        objectNode.rotation = Quaternion(0.0f, Random(360.0f), 0.0f);
+        objectNode.SetScale(2.0f + Random(5.0f));
+        StaticModel@ object = objectNode.CreateComponent("StaticModel");
+        object.model = cache.GetResource("Model", "Models/Mushroom.mdl");
+        object.material = cache.GetResource("Material", "Materials/Mushroom.xml");
+        object.castShadows = true;
+        
+        RigidBody@ body = objectNode.CreateComponent("RigidBody");
+        body.collisionLayer = 2;
+        CollisionShape@ shape = objectNode.CreateComponent("CollisionShape");
+        shape.SetTriangleMesh(object.model, 0);
+    }
+    
+    // Create movable boxes. Let them fall from the sky at first
+    const uint NUM_BOXES = 100;
+    for (uint i = 0; i < NUM_BOXES; ++i)
+    {
+        float scale = Random(2.0f) + 0.5f;
+        
+        Node@ objectNode = scene_.CreateChild("Box");
+        objectNode.position = Vector3(Random(180.0f) - 90.0f, Random(10.0f) + 10.0f, Random(180.0f) - 90.0f);
+        objectNode.rotation = Quaternion(Random(360.0f), Random(360.0f), Random(360.0f));
+        objectNode.SetScale(scale);
+        StaticModel@ object = objectNode.CreateComponent("StaticModel");
+        object.model = cache.GetResource("Model", "Models/Box.mdl");
+        object.material = cache.GetResource("Material", "Materials/Stone.xml");
+        object.castShadows = true;
+        
+        RigidBody@ body = objectNode.CreateComponent("RigidBody");
+        body.collisionLayer = 2;
+        // Bigger boxes will be heavier and harder to move
+        body.mass = scale * 2.0f;
+        CollisionShape@ shape = objectNode.CreateComponent("CollisionShape");
+        shape.SetBox(Vector3(1.0f, 1.0f, 1.0f));
+    }
+}
+
+void CreateCharacter()
+{
+    characterNode = scene_.CreateChild("Jack");
+    characterNode.position = Vector3(0.0f, 1.0f, 0.0f);
+    
+    // Create the rendering component + animation controller
+    AnimatedModel@ object = characterNode.CreateComponent("AnimatedModel");
+    object.model = cache.GetResource("Model", "Models/Jack.mdl");
+    object.material = cache.GetResource("Material", "Materials/Jack.xml");
+    object.castShadows = true;
+    characterNode.CreateComponent("AnimationController");
+
+    // Create rigidbody, and set non-zero mass so that the body becomes dynamic
+    RigidBody@ body = characterNode.CreateComponent("RigidBody");
+    body.collisionLayer = 1;
+    body.mass = 1.0f;
+    
+    // Set zero angular factor so that physics doesn't turn the character on its own.
+    // Instead we will control the character yaw manually
+    body.angularFactor = Vector3(0.0f, 0.0f, 0.0f);
+    
+    // Set the rigidbody to signal collision also when in rest, so that we get ground collisions properly
+    body.collisionEventMode = COLLISION_ALWAYS;
+    
+    // Set a capsule shape for collision
+    CollisionShape@ shape = characterNode.CreateComponent("CollisionShape");
+    shape.SetCapsule(0.7f, 1.8f, Vector3(0.0f, 0.9f, 0.0f));
+    
+    // Create the character logic object, which takes care of steering the rigidbody
+    characterNode.CreateScriptObject(scriptFile, "Character");
+}
+
+void CreateInstructions()
+{
+    // Construct new Text object, set string to display and font to use
+    Text@ instructionText = ui.root.CreateChild("Text");
+    instructionText.text =
+        "Use WASD keys and mouse to move\n"
+        "Space to jump, F to toggle 1st/3rd person";
+    instructionText.SetFont(cache.GetResource("Font", "Fonts/Anonymous Pro.ttf"), 15);
+    // The text has multiple rows. Center them in relation to each other
+    instructionText.textAlignment = HA_CENTER;
+
+    // Position the text relative to the screen center
+    instructionText.horizontalAlignment = HA_CENTER;
+    instructionText.verticalAlignment = VA_CENTER;
+    instructionText.SetPosition(0, ui.root.height / 4);
+}
+
+void SubscribeToEvents()
+{
+    // Subscribe to Update event for setting the character controls before physics simulation
+    SubscribeToEvent("Update", "HandleUpdate");
+
+    // Subscribe to PostUpdate event for updating the camera position after physics simulation
+    SubscribeToEvent("PostUpdate", "HandlePostUpdate");
+}
+
+void HandleUpdate(StringHash eventType, VariantMap& eventData)
+{
+    if (characterNode is null)
+        return;
+
+    Character@ character = cast<Character>(characterNode.scriptObject);
+    if (character is null)
+        return;
+
+    // Get movement controls and assign them to the character logic component. If UI has a focused element, clear controls
+    if (ui.focusElement is null)
+    {
+        character.controls.Set(CTRL_FORWARD, input.keyDown['W']);
+        character.controls.Set(CTRL_BACK, input.keyDown['S']);
+        character.controls.Set(CTRL_LEFT, input.keyDown['A']);
+        character.controls.Set(CTRL_RIGHT, input.keyDown['D']);
+        character.controls.Set(CTRL_JUMP, input.keyDown[KEY_SPACE]);
+        
+        // Add character yaw & pitch from the mouse motion
+        character.controls.yaw += input.mouseMoveX * YAW_SENSITIVITY;
+        character.controls.pitch += input.mouseMoveY * YAW_SENSITIVITY;
+        // Limit pitch
+        character.controls.pitch = Clamp(character.controls.pitch, -80.0f, 80.0f);
+
+        // Switch between 1st and 3rd person
+        if (input.keyPress['F'])
+            firstPerson = !firstPerson;
+    }
+    else
+        character.controls.Set(CTRL_FORWARD | CTRL_BACK | CTRL_LEFT | CTRL_RIGHT | CTRL_JUMP, false);
+    
+    // Set rotation already here so that it's updated every rendering frame instead of every physics frame
+    characterNode.rotation = Quaternion(character.controls.yaw, Vector3(0.0f, 1.0f, 0.0f));
+}
+
+void HandlePostUpdate(StringHash eventType, VariantMap& eventData)
+{
+    if (characterNode is null)
+        return;
+
+    Character@ character = cast<Character>(characterNode.scriptObject);
+    if (character is null)
+        return;
+
+    // Get camera lookat dir from character yaw + pitch
+    Quaternion rot = characterNode.rotation;
+    Quaternion dir = rot * Quaternion(character.controls.pitch, Vector3(1.0f, 0.0f, 0.0f));
+
+    if (firstPerson)
+    {
+        // First person camera: position to the head bone + offset slightly forward & up
+        Node@ headNode = characterNode.GetChild("Bip01_Head", true);
+        if (headNode !is null)
+        {
+            cameraNode.position = headNode.worldPosition + rot * Vector3(0.0f, 0.15f, 0.2f);
+            cameraNode.rotation = dir;
+        }
+    }
+    else
+    {
+        // Third person camera: position behind the character
+        Vector3 aimPoint = characterNode.position + rot * Vector3(0.0f, 1.7f, 0.0f);
+        
+        // Collide camera ray with static physics objects (layer bitmask 2) to ensure we see the character properly
+        Vector3 rayDir = dir * Vector3(0.0f, 0.0f, -1.0f);
+        float rayDistance = CAMERA_MAX_DIST;
+        PhysicsRaycastResult result = scene_.physicsWorld.RaycastSingle(Ray(aimPoint, rayDir), rayDistance, 2);
+        if (result.body !is null)
+            rayDistance = Min(rayDistance, result.distance);
+        rayDistance = Clamp(rayDistance, CAMERA_MIN_DIST, CAMERA_MAX_DIST);
+        
+        cameraNode.position = aimPoint + rayDir * rayDistance;
+        cameraNode.rotation = dir;
+    }
+}
+
+// Character script object class
+class Character : ScriptObject
+{
+    // Character controls.
+    Controls controls;
+    // Grounded flag for movement.
+    bool onGround = false;
+    // Jump flag.
+    bool okToJump = true;
+    // In air timer. Due to possible physics inaccuracy, character can be off ground for max. 1/10 second and still be allowed to move.
+    float inAirTimer = 0.0f;
+    
+    void Start()
+    {
+        SubscribeToEvent(node, "NodeCollision", "HandleNodeCollision");
+    }
+    
+    void HandleNodeCollision(StringHash eventType, VariantMap& eventData)
+    {
+        VectorBuffer contacts = eventData["Contacts"].GetBuffer();
+
+        while (!contacts.eof)
+        {
+            Vector3 contactPosition = contacts.ReadVector3();
+            Vector3 contactNormal = contacts.ReadVector3();
+            float contactDistance = contacts.ReadFloat();
+            float contactImpulse = contacts.ReadFloat();
+
+            // If contact is below node center and mostly vertical, assume it's a ground contact
+            if (contactPosition.y < (node.position.y + 1.0f))
+            {
+                float level = Abs(contactNormal.y);
+                if (level > 0.75)
+                    onGround = true;
+            }
+        }
+    }
+    
+    void FixedUpdate(float timeStep)
+    {
+        /// \todo Could cache the components for faster access instead of finding them each frame
+        RigidBody@ body = node.GetComponent("RigidBody");
+        AnimationController@ animCtrl = node.GetComponent("AnimationController");
+        
+        // Update the in air timer. Reset if grounded
+        if (!onGround)
+            inAirTimer += timeStep;
+        else
+            inAirTimer = 0.0f;
+        // When character has been in air less than 1/10 second, it's still interpreted as being on ground
+        bool softGrounded = inAirTimer < INAIR_THRESHOLD_TIME;
+        
+        // Update movement & animation
+        Quaternion rot = node.rotation;
+        Vector3 moveDir(0.0f, 0.0f, 0.0f);
+        Vector3 velocity = body.linearVelocity;
+        // Velocity on the XZ plane
+        Vector3 planeVelocity(velocity.x, 0.0f, velocity.z);
+        
+        if (controls.IsDown(CTRL_FORWARD))
+            moveDir += Vector3(0.0f, 0.0f, 1.0f);
+        if (controls.IsDown(CTRL_BACK))
+            moveDir += Vector3(0.0f, 0.0f, -1.0f);
+        if (controls.IsDown(CTRL_LEFT))
+            moveDir += Vector3(-1.0f, 0.0f, 0.0f);
+        if (controls.IsDown(CTRL_RIGHT))
+            moveDir += Vector3(1.0f, 0.0f, 0.0f);
+        
+        // Normalize move vector so that diagonal strafing is not faster
+        if (moveDir.lengthSquared > 0.0f)
+            moveDir.Normalize();
+        
+        // If in air, allow control, but slower than when on ground
+        body.ApplyImpulse(rot * moveDir * (softGrounded ? MOVE_FORCE : INAIR_MOVE_FORCE));
+        
+        if (softGrounded)
+        {
+            // When on ground, apply a braking force to limit maximum ground velocity
+            Vector3 brakeForce = -planeVelocity * BRAKE_FORCE;
+            body.ApplyImpulse(brakeForce);
+            
+            // Jump. Must release jump control inbetween jumps
+            if (controls.IsDown(CTRL_JUMP))
+            {
+                if (okToJump)
+                {
+                    body.ApplyImpulse(Vector3(0.0f, 1.0f, 0.0f) * JUMP_FORCE);
+                    okToJump = false;
+                }
+            }
+            else
+                okToJump = true;
+        }
+        
+        // Play walk animation if moving on ground, otherwise fade it out
+        if (softGrounded && !moveDir.Equals(Vector3(0.0f, 0.0f, 0.0f)))
+            animCtrl.PlayExclusive("Models/Jack_Walk.ani", 0, true, 0.2f);
+        else
+            animCtrl.Stop("Models/Jack_Walk.ani", 0.2f);
+        // Set walk animation speed proportional to velocity
+        animCtrl.SetSpeed("Models/Jack_Walk.ani", planeVelocity.length * 0.3f);
+
+        // Reset grounded flag for next frame
+        onGround = false;
+    }
+}

+ 334 - 0
Bin/Data/Scripts/19_VehicleDemo.as

@@ -0,0 +1,334 @@
+// Vehicle example.
+// This sample demonstrates:
+//     - Creating a heightmap terrain with collision;
+//     - Constructing a physical vehicle with rigid bodies for the hull and the wheels, joined with constraints;
+
+#include "Utilities/Sample.as"
+
+const int CTRL_FORWARD = 1;
+const int CTRL_BACK = 2;
+const int CTRL_LEFT = 4;
+const int CTRL_RIGHT = 8;
+
+const float CAMERA_DISTANCE = 10.0f;
+const float YAW_SENSITIVITY = 0.1f;
+const float ENGINE_POWER = 10.0f;
+const float DOWN_FORCE = 10.0f;
+const float MAX_WHEEL_ANGLE = 22.5f;
+
+Scene@ scene_;
+Node@ cameraNode;
+Node@ vehicleNode;
+
+void Start()
+{
+    // Execute the common startup for samples
+    SampleStart();
+    
+    // Create static scene content
+    CreateScene();
+
+    // Create the controllable vehicle
+    CreateVehicle();
+
+    // Create the UI content
+    CreateInstructions();
+
+    // Subscribe to necessary events
+    SubscribeToEvents();
+}
+
+void CreateScene()
+{
+    scene_ = Scene();
+
+    // Create scene subsystem components
+    scene_.CreateComponent("Octree");
+    scene_.CreateComponent("PhysicsWorld");
+
+    // Create camera and define viewport. Camera does not necessarily have to belong to the scene
+    cameraNode = Node();
+    Camera@ camera = cameraNode.CreateComponent("Camera");
+    camera.farClip = 500.0f;
+    renderer.viewports[0] = Viewport(scene_, camera);
+    
+    // Create static scene content. First create a zone for ambient lighting and fog control
+    Node@ zoneNode = scene_.CreateChild("Zone");
+    Zone@ zone = zoneNode.CreateComponent("Zone");
+    zone.ambientColor = Color(0.15f, 0.15f, 0.15f);
+    zone.fogColor = Color(0.5f, 0.5f, 0.7f);
+    zone.fogStart = 300.0f;
+    zone.fogEnd = 500.0f;
+    zone.boundingBox = BoundingBox(-2000.0f, 2000.0f);
+
+    // Create a directional light to the world. Enable cascaded shadows on it
+    Node@ lightNode = scene_.CreateChild("DirectionalLight");
+    lightNode.direction = Vector3(0.3f, -0.5f, 0.425f);
+    Light@ light = lightNode.CreateComponent("Light");
+    light.lightType = LIGHT_DIRECTIONAL;
+    light.castShadows = true;
+    light.shadowBias = BiasParameters(0.0001f, 0.5f);
+    light.shadowCascade = CascadeParameters(10.0f, 50.0f, 200.0f, 0.0f, 0.8f);
+    light.specularIntensity = 0.5f;
+
+    // Create heightmap terrain with collision
+    Node@ terrainNode = scene_.CreateChild("Terrain");
+    terrainNode.position = Vector3(0.0f, 0.0f, 0.0f);
+    Terrain@ terrain = terrainNode.CreateComponent("Terrain");
+    terrain.patchSize = 64;
+    terrain.spacing = Vector3(2.0f, 0.1f, 2.0f); // Spacing between vertices and vertical resolution of the height map
+    terrain.smoothing = true;
+    terrain.heightMap = cache.GetResource("Image", "Textures/HeightMap.png");
+    terrain.material = cache.GetResource("Material", "Materials/Terrain.xml");
+    // The terrain consists of large triangles, which fits well for occlusion rendering, as a hill can occlude all
+    // terrain patches and other objects behind it
+    terrain.occluder = true;
+    
+    RigidBody@ body = terrainNode.CreateComponent("RigidBody");
+    body.collisionLayer = 2; // Use layer bitmask 2 for static geometry
+    CollisionShape@ shape = terrainNode.CreateComponent("CollisionShape");
+    shape.SetTerrain();
+
+    // Create 1000 mushrooms in the terrain. Always face outward along the terrain normal
+    const uint NUM_MUSHROOMS = 1000;
+    for (uint i = 0; i < NUM_MUSHROOMS; ++i)
+    {
+        Node@ objectNode = scene_.CreateChild("Mushroom");
+        Vector3 position(Random(2000.0f) - 1000.0f, 0.0f, Random(2000.0f) - 1000.0f);
+        position.y = terrain.GetHeight(position) - 0.1f;
+        objectNode.position = position;
+        // Create a rotation quaternion from up vector to terrain normal
+        objectNode.rotation = Quaternion(Vector3(0.0f, 1.0f, 0.0), terrain.GetNormal(position));
+        objectNode.SetScale(3.0f);
+        StaticModel@ object = objectNode.CreateComponent("StaticModel");
+        object.model = cache.GetResource("Model", "Models/Mushroom.mdl");
+        object.material = cache.GetResource("Material", "Materials/Mushroom.xml");
+        object.castShadows = true;
+        
+        RigidBody@ body = objectNode.CreateComponent("RigidBody");
+        body.collisionLayer = 2;
+        CollisionShape@ shape = objectNode.CreateComponent("CollisionShape");
+        shape.SetTriangleMesh(object.model, 0);
+    }
+}
+
+void CreateVehicle()
+{
+    vehicleNode = scene_.CreateChild("Vehicle");
+    vehicleNode.position = Vector3(0.0f, 5.0f, 0.0f);
+    
+    // Create the vehicle logic script object
+    Vehicle@ vehicle = cast<Vehicle>(vehicleNode.CreateScriptObject(scriptFile, "Vehicle"));
+    // Create the rendering and physics components
+    vehicle.Init();
+}
+
+void CreateInstructions()
+{
+    // Construct new Text object, set string to display and font to use
+    Text@ instructionText = ui.root.CreateChild("Text");
+    instructionText.text = "Use WASD keys to drive, mouse to rotate camera";
+    instructionText.SetFont(cache.GetResource("Font", "Fonts/Anonymous Pro.ttf"), 15);
+
+    // Position the text relative to the screen center
+    instructionText.horizontalAlignment = HA_CENTER;
+    instructionText.verticalAlignment = VA_CENTER;
+    instructionText.SetPosition(0, ui.root.height / 4);
+}
+
+void SubscribeToEvents()
+{
+    // Subscribe to Update event for setting the vehicle controls before physics simulation
+    SubscribeToEvent("Update", "HandleUpdate");
+    
+    // Subscribe to PostUpdate event for updating the camera position after physics simulation
+    SubscribeToEvent("PostUpdate", "HandlePostUpdate");
+}
+
+void HandleUpdate(StringHash eventType, VariantMap& eventData)
+{
+    if (vehicleNode is null)
+        return;
+    
+    Vehicle@ vehicle = cast<Vehicle>(vehicleNode.scriptObject);
+    if (vehicle is null)
+        return;
+
+    // Get movement controls and assign them to the vehicle component. If UI has a focused element, clear controls
+    if (ui.focusElement is null)
+    {
+        vehicle.controls.Set(CTRL_FORWARD, input.keyDown['W']);
+        vehicle.controls.Set(CTRL_BACK, input.keyDown['S']);
+        vehicle.controls.Set(CTRL_LEFT, input.keyDown['A']);
+        vehicle.controls.Set(CTRL_RIGHT, input.keyDown['D']);
+    
+        // Add yaw & pitch from the mouse motion. Used only for the camera, does not affect motion
+        vehicle.controls.yaw += input.mouseMoveX * YAW_SENSITIVITY;
+        vehicle.controls.pitch += input.mouseMoveY * YAW_SENSITIVITY;
+        // Limit pitch
+        vehicle.controls.pitch = Clamp(vehicle.controls.pitch, 0.0f, 80.0f);
+    }
+    else
+        vehicle.controls.Set(CTRL_FORWARD | CTRL_BACK | CTRL_LEFT | CTRL_RIGHT, false);
+}
+
+void HandlePostUpdate(StringHash eventType, VariantMap& eventData)
+{
+    if (vehicleNode is null)
+        return;
+    
+    Vehicle@ vehicle = cast<Vehicle>(vehicleNode.scriptObject);
+    if (vehicle is null)
+        return;
+
+    // Physics update has completed. Position camera behind vehicle
+    Quaternion dir(vehicleNode.rotation.yaw, Vector3(0.0f, 1.0f, 0.0f));
+    dir = dir * Quaternion(vehicle.controls.yaw, Vector3(0.0f, 1.0f, 0.0f));
+    dir = dir * Quaternion(vehicle.controls.pitch, Vector3(1.0f, 0.0f, 0.0f));
+    
+    Vector3 cameraTargetPos = vehicleNode.position - dir * Vector3(0.0f, 0.0f, CAMERA_DISTANCE);
+    Vector3 cameraStartPos = vehicleNode.position;
+
+    // Raycast camera against static objects (physics collision mask 2)
+    // and move it closer to the vehicle if something in between
+    Ray cameraRay(cameraStartPos, (cameraTargetPos - cameraStartPos).Normalized());
+    float cameraRayLength = (cameraTargetPos - cameraStartPos).length;
+    PhysicsRaycastResult result = scene_.physicsWorld.RaycastSingle(cameraRay, cameraRayLength, 2);
+    if (result.body !is null)
+        cameraTargetPos = cameraStartPos + cameraRay.direction * (result.distance - 0.5f);
+
+    cameraNode.position = cameraTargetPos;
+    cameraNode.rotation = dir;
+}
+
+// Vehicle script object class
+class Vehicle : ScriptObject
+{
+    Node@ frontLeft;
+    Node@ frontRight;
+    Node@ rearLeft;
+    Node@ rearRight;
+    Constraint@ frontLeftAxis;
+    Constraint@ frontRightAxis;
+    RigidBody@ hullBody;
+    RigidBody@ frontLeftBody;
+    RigidBody@ frontRightBody;
+    RigidBody@ rearLeftBody;
+    RigidBody@ rearRightBody;
+
+    // Current left/right steering amount (-1 to 1.)
+    float steering = 0.0f;
+    // Vehicle controls.
+    Controls controls;
+
+    void Init()
+    {
+        StaticModel@ hullObject = node.CreateComponent("StaticModel");
+        hullBody = node.CreateComponent("RigidBody");
+        CollisionShape@ hullShape = node.CreateComponent("CollisionShape");
+
+        node.scale = Vector3(1.5f, 1.0f, 3.0f);
+        hullObject.model = cache.GetResource("Model", "Models/Box.mdl");
+        hullObject.material = cache.GetResource("Material", "Materials/Stone.xml");
+        hullObject.castShadows = true;
+        hullShape.SetBox(Vector3(1.0f, 1.0f, 1.0f));
+        hullBody.mass = 4.0f;
+        hullBody.linearDamping = 0.2f; // Some air resistance
+        hullBody.angularDamping = 0.5f;
+        hullBody.collisionLayer = 1;
+    
+        frontLeft = InitWheel("FrontLeft", Vector3(-0.6f, -0.4f, 0.3f));
+        frontRight = InitWheel("FrontRight", Vector3(0.6f, -0.4f, 0.3f));
+        rearLeft = InitWheel("RearLeft", Vector3(-0.6f, -0.4f, -0.3f));
+        rearRight = InitWheel("RearRight", Vector3(0.6f, -0.4f, -0.3f));
+
+        frontLeftAxis = frontLeft.GetComponent("Constraint");
+        frontRightAxis = frontRight.GetComponent("Constraint");
+        frontLeftBody = frontLeft.GetComponent("RigidBody");
+        frontRightBody = frontRight.GetComponent("RigidBody");
+        rearLeftBody = rearLeft.GetComponent("RigidBody");
+        rearRightBody = rearRight.GetComponent("RigidBody");
+    }
+
+    Node@ InitWheel(const String&in name, const Vector3&in offset)
+    {
+        // Note: do not parent the wheel to the hull scene node. Instead create it on the root level and let the physics
+        // constraint keep it together
+        Node@ wheelNode = scene.CreateChild(name);
+        wheelNode.position = node.LocalToWorld(offset);
+        wheelNode.rotation = node.worldRotation * (offset.x >= 0.0f ? Quaternion(0.0f, 0.0f, -90.0f) :
+            Quaternion(0.0f, 0.0f, 90.0f));
+        wheelNode.scale = Vector3(0.8f, 0.5f, 0.8f);
+
+        StaticModel@ wheelObject = wheelNode.CreateComponent("StaticModel");
+        RigidBody@ wheelBody = wheelNode.CreateComponent("RigidBody");
+        CollisionShape@ wheelShape = wheelNode.CreateComponent("CollisionShape");
+        Constraint@ wheelConstraint = wheelNode.CreateComponent("Constraint");
+
+        wheelObject.model = cache.GetResource("Model", "Models/Cylinder.mdl");
+        wheelObject.material = cache.GetResource("Material", "Materials/Stone.xml");
+        wheelObject.castShadows = true;
+        wheelShape.SetSphere(1.0f);
+        wheelBody.friction = 1;
+        wheelBody.mass = 1;
+        wheelBody.linearDamping = 0.2f; // Some air resistance
+        wheelBody.angularDamping = 0.75f; // Could also use rolling friction
+        wheelBody.collisionLayer = 1;
+        wheelConstraint.constraintType = CONSTRAINT_HINGE;
+        wheelConstraint.otherBody = node.GetComponent("RigidBody");
+        wheelConstraint.worldPosition = wheelNode.worldPosition; // Set constraint's both ends at wheel's location
+        wheelConstraint.axis = Vector3(0.0f, 1.0f, 0.0f); // Wheel rotates around its local Y-axis
+        wheelConstraint.otherAxis = offset.x >= 0.0f ? Vector3(1.0f, 0.0f, 0.0f) : Vector3(-1.0f, 0.0f, 0.0f); // Wheel's hull axis points either left or right
+        wheelConstraint.lowLimit = Vector2(-180.0f, 0.0f); // Let the wheel rotate freely around the axis
+        wheelConstraint.highLimit = Vector2(180.0f, 0.0f);
+        wheelConstraint.disableCollision = true; // Let the wheel intersect the vehicle hull
+    
+        return wheelNode;
+    }
+
+    void FixedUpdate(float timeStep)
+    {
+        float newSteering = 0.0f;
+        float accelerator = 0.0f;
+
+        if (controls.IsDown(CTRL_LEFT))
+            newSteering = -1.0f;
+        if (controls.IsDown(CTRL_RIGHT))
+            newSteering = 1.0f;
+        if (controls.IsDown(CTRL_FORWARD))
+            accelerator = 1.0f;
+        if (controls.IsDown(CTRL_BACK))
+            accelerator = -0.5f;
+
+        // When steering, wake up the wheel rigidbodies so that their orientation is updated
+        if (newSteering != 0.0f)
+        {
+            frontLeftBody.Activate();
+            frontRightBody.Activate();
+            steering = steering * 0.95f + newSteering * 0.05f;
+        }
+        else
+            steering = steering * 0.8f + newSteering * 0.2f;
+
+        Quaternion steeringRot(0.0f, steering * MAX_WHEEL_ANGLE, 0.0f);
+
+        frontLeftAxis.otherAxis = steeringRot * Vector3(-1.0f, 0.0f, 0.0f);
+        frontRightAxis.otherAxis = steeringRot * Vector3(1.0f, 0.0f, 0.0f);
+
+        if (accelerator != 0.0f)
+        {
+            // Torques are applied in world space, so need to take the vehicle & wheel rotation into account
+            Vector3 torqueVec = Vector3(ENGINE_POWER * accelerator, 0.0f, 0.0f);
+            
+            frontLeftBody.ApplyTorque(node.rotation * steeringRot * torqueVec);
+            frontRightBody.ApplyTorque(node.rotation * steeringRot * torqueVec);
+            rearLeftBody.ApplyTorque(node.rotation * torqueVec);
+            rearRightBody.ApplyTorque(node.rotation * torqueVec);
+        }
+
+        // Apply downforce proportional to velocity
+        Vector3 localVelocity = hullBody.rotation.Inverse() * hullBody.linearVelocity;
+        hullBody.ApplyForce(hullBody.rotation * Vector3(0.0f, -1.0f, 0.0f) * Abs(localVelocity.z) * DOWN_FORCE);
+    }
+}
+

+ 2 - 2
Source/Samples/18_CharacterDemo/CharacterDemo.cpp

@@ -150,8 +150,8 @@ void CharacterDemo::CreateScene()
     }
     
     // Create movable boxes. Let them fall from the sky at first
-    const unsigned NUM_SMALL_BOXES = 100;
-    for (unsigned i = 0; i < NUM_SMALL_BOXES; ++i)
+    const unsigned NUM_BOXES = 100;
+    for (unsigned i = 0; i < NUM_BOXES; ++i)
     {
         float scale = Random(2.0f) + 0.5f;
         

+ 1 - 1
Source/Samples/19_VehicleDemo/VehicleDemo.cpp

@@ -186,7 +186,7 @@ void VehicleDemo::CreateInstructions()
 
 void VehicleDemo::SubscribeToEvents()
 {
-    // Subscribe to Update event for setting the character controls before physics simulation
+    // Subscribe to Update event for setting the vehicle controls before physics simulation
     SubscribeToEvent(E_UPDATE, HANDLER(VehicleDemo, HandleUpdate));
     
     // Subscribe to PostUpdate event for updating the camera position after physics simulation