Browse Source

Merge remote-tracking branch 'slapin/raycast-vehicle'

Lasse Öörni 8 years ago
parent
commit
c145b3e03d

+ 43 - 0
Source/Samples/46_RaycastVehicle/CMakeLists.txt

@@ -0,0 +1,43 @@
+#
+# Copyright (c) 2008-2017 the Urho3D project.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+if (NOT URHO3D_PHYSICS)
+    return ()
+endif ()
+
+# Define target name
+set (TARGET_NAME 46_RaycastVehicle)
+
+# Define source files
+define_source_files (EXTRA_H_FILES ${COMMON_SAMPLE_H_FILES})
+
+# Setup target with resource copying
+if (EMSCRIPTEN)
+    # Override the Urho3D default
+    if (URHO3D_TESTING AND URHO3D_TEST_TIMEOUT LESS 15)
+        set (URHO3D_TEST_TIMEOUT 15)
+    endif ()
+endif ()
+setup_main_executable ()
+
+# Setup test cases
+setup_test ()

+ 323 - 0
Source/Samples/46_RaycastVehicle/RaycastVehicleDemo.cpp

@@ -0,0 +1,323 @@
+//
+// Copyright (c) 2008-2017 the Urho3D project.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+#include <Urho3D/Core/CoreEvents.h>
+#include <Urho3D/Core/ProcessUtils.h>
+#include <Urho3D/Engine/Engine.h>
+#include <Urho3D/Graphics/Camera.h>
+#include <Urho3D/Graphics/Light.h>
+#include <Urho3D/Graphics/Material.h>
+#include <Urho3D/Graphics/Model.h>
+#include <Urho3D/Graphics/Octree.h>
+#include <Urho3D/Graphics/Renderer.h>
+#include <Urho3D/Graphics/StaticModel.h>
+#include <Urho3D/Graphics/Terrain.h>
+#include <Urho3D/Graphics/Zone.h>
+#include <Urho3D/IO/FileSystem.h>
+#include <Urho3D/Input/Input.h>
+#include <Urho3D/Physics/CollisionShape.h>
+#include <Urho3D/Physics/Constraint.h>
+#include <Urho3D/Physics/PhysicsWorld.h>
+#include <Urho3D/Physics/RaycastVehicle.h>
+#include <Urho3D/Physics/RigidBody.h>
+#include <Urho3D/Resource/ResourceCache.h>
+#include <Urho3D/Scene/Scene.h>
+#include <Urho3D/UI/Font.h>
+#include <Urho3D/UI/Text.h>
+#include <Urho3D/UI/UI.h>
+
+#include "RaycastVehicleDemo.h"
+#include "Vehicle.h"
+
+#include <Urho3D/DebugNew.h>
+
+const float CAMERA_DISTANCE = 10.0f;
+
+URHO3D_DEFINE_APPLICATION_MAIN(RaycastVehicleDemo)
+
+RaycastVehicleDemo::RaycastVehicleDemo(Context* context)
+    : Sample(context)
+
+{
+    // Register factory and attributes for the Vehicle component so it can be created via CreateComponent, and loaded / saved
+    Vehicle::RegisterObject(context);
+}
+
+void RaycastVehicleDemo::Start()
+{
+    // Execute base class startup
+    Sample::Start();
+    // Create static scene content
+    CreateScene();
+    // Create the controllable vehicle
+    CreateVehicle();
+    // Create the UI content
+    CreateInstructions();
+    // Subscribe to necessary events
+    SubscribeToEvents();
+    // Set the mouse mode to use in the sample
+    Sample::InitMouseMode(MM_RELATIVE);
+}
+
+void RaycastVehicleDemo::CreateScene()
+{
+    ResourceCache* cache = GetSubsystem<ResourceCache>();
+    scene_ = new Scene(context_);
+    // Create scene subsystem components
+    scene_->CreateComponent<Octree>();
+    scene_->CreateComponent<PhysicsWorld>();
+    // Create camera and define viewport. We will be doing load / save, so it's convenient to create the camera outside the scene,
+    // so that it won't be destroyed and recreated, and we don't have to redefine the viewport on load
+    cameraNode_ = new Node(context_);
+    Camera* camera = cameraNode_->CreateComponent<Camera>();
+    camera->SetFarClip(500.0f);
+    GetSubsystem<Renderer>()->SetViewport(0,
+                                            new Viewport(context_,
+                                                         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->SetAmbientColor(Color(0.15f, 0.15f, 0.15f));
+    zone->SetFogColor(Color(0.5f, 0.5f, 0.7f));
+    zone->SetFogStart(300.0f);
+    zone->SetFogEnd(500.0f);
+    zone->SetBoundingBox(BoundingBox(-2000.0f, 2000.0f));
+    // Create a directional light with cascaded shadow mapping
+    Node* lightNode = scene_->CreateChild("DirectionalLight");
+    lightNode->SetDirection(Vector3(0.3f, -0.5f, 0.425f));
+    Light* light = lightNode->CreateComponent<Light>();
+    light->SetLightType(LIGHT_DIRECTIONAL);
+    light->SetCastShadows(true);
+    light->SetShadowBias(BiasParameters(0.00025f, 0.5f));
+    light->SetShadowCascade(CascadeParameters(10.0f, 50.0f, 200.0f, 0.0f, 0.8f));
+    light->SetSpecularIntensity(0.5f);
+    // Create heightmap terrain with collision
+    Node* terrainNode = scene_->CreateChild("Terrain");
+    terrainNode->SetPosition(Vector3::ZERO);
+    Terrain* terrain = terrainNode->CreateComponent<Terrain>();
+    terrain->SetPatchSize(64);
+    terrain->SetSpacing(Vector3(3.0f, 0.1f, 3.0f)); // Spacing between vertices and vertical resolution of the height map
+    terrain->SetSmoothing(true);
+    terrain->SetHeightMap(cache->GetResource<Image>("Textures/HeightMap.png"));
+    terrain->SetMaterial(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->SetOccluder(true);
+    RigidBody* body = terrainNode->CreateComponent<RigidBody>();
+    body->SetCollisionLayer(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 unsigned NUM_MUSHROOMS = 1000;
+    for (unsigned 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->SetPosition(position);
+        // Create a rotation quaternion from up vector to terrain normal
+        objectNode->SetRotation(Quaternion(Vector3::UP,
+                                           terrain->GetNormal(position)));
+        objectNode->SetScale(3.0f);
+        StaticModel* object =
+            objectNode->CreateComponent<StaticModel>();
+        object->SetModel(cache->GetResource<Model>("Models/Mushroom.mdl"));
+        object->SetMaterial(cache->GetResource<Material>("Materials/Mushroom.xml"));
+        object->SetCastShadows(true);
+        RigidBody* body = objectNode->CreateComponent<RigidBody>();
+        body->SetCollisionLayer(2);
+        CollisionShape* shape =
+            objectNode->CreateComponent<CollisionShape>();
+        shape->SetTriangleMesh(object->GetModel(), 0);
+    }
+}
+
+void RaycastVehicleDemo::CreateVehicle()
+{
+    Node* vehicleNode = scene_->CreateChild("Vehicle");
+    vehicleNode->SetPosition(Vector3(0.0f, 25.0f, 0.0f));
+    // Create the vehicle logic component
+    vehicle_ = vehicleNode->CreateComponent<Vehicle>();
+    // Create the rendering and physics components
+    vehicle_->Init();
+}
+
+void RaycastVehicleDemo::CreateInstructions()
+{
+    ResourceCache* cache = GetSubsystem<ResourceCache>();
+    UI* ui = GetSubsystem<UI>();
+    // Construct new Text object, set string to display and font to use
+    Text* instructionText = ui->GetRoot()->CreateChild<Text>();
+    instructionText->SetText(
+        "Use WASD keys to drive, F to brake, mouse/touch to rotate camera\n"
+        "F5 to save scene, F7 to load");
+    instructionText->SetFont(cache->GetResource<Font>("Fonts/Anonymous Pro.ttf"), 15);
+    // The text has multiple rows. Center them in relation to each other
+    instructionText->SetTextAlignment(HA_CENTER);
+    // Position the text relative to the screen center
+    instructionText->SetHorizontalAlignment(HA_CENTER);
+    instructionText->SetVerticalAlignment(VA_CENTER);
+    instructionText->SetPosition(0, ui->GetRoot()->GetHeight() / 4);
+}
+
+void RaycastVehicleDemo::SubscribeToEvents()
+{
+    // Subscribe to Update event for setting the vehicle controls before physics simulation
+    SubscribeToEvent(E_UPDATE,
+                     URHO3D_HANDLER(RaycastVehicleDemo, HandleUpdate));
+    // Subscribe to PostUpdate event for updating the camera position after physics simulation
+    SubscribeToEvent(E_POSTUPDATE,
+                     URHO3D_HANDLER(RaycastVehicleDemo,
+                                    HandlePostUpdate));
+    // Unsubscribe the SceneUpdate event from base class as the camera node is being controlled in HandlePostUpdate() in this sample
+    UnsubscribeFromEvent(E_SCENEUPDATE);
+}
+
+void RaycastVehicleDemo::HandleUpdate(StringHash eventType,
+                                 VariantMap& eventData)
+{
+    using namespace Update;
+    Input* input = GetSubsystem<Input>();
+    if (vehicle_)
+    {
+        UI* ui = GetSubsystem<UI>();
+        // Get movement controls and assign them to the vehicle component. If UI has a focused element, clear controls
+        if (!ui->GetFocusElement())
+        {
+            vehicle_->controls_.Set(CTRL_FORWARD,
+                                    input->GetKeyDown(KEY_W));
+            vehicle_->controls_.Set(CTRL_BACK,
+                                    input->GetKeyDown(KEY_S));
+            vehicle_->controls_.Set(CTRL_LEFT,
+                                    input->GetKeyDown(KEY_A));
+            vehicle_->controls_.Set(CTRL_RIGHT,
+                                    input->GetKeyDown(KEY_D));
+            vehicle_->controls_.Set(CTRL_BRAKE,
+                                    input->GetKeyDown(KEY_F));
+            // Add yaw & pitch from the mouse motion or touch input. Used only for the camera, does not affect motion
+            if (touchEnabled_)
+            {
+                for (unsigned i = 0; i < input->GetNumTouches(); ++i)
+                {
+                    TouchState* state = input->GetTouch(i);
+                    if (!state->touchedElement_) // Touch on empty space
+                    {
+                        Camera* camera =
+                            cameraNode_->GetComponent<Camera>();
+                        if (!camera)
+                        {
+                            return;
+                        }
+                        Graphics* graphics =
+                            GetSubsystem<Graphics>();
+                        vehicle_->controls_.yaw_ +=
+                            TOUCH_SENSITIVITY * camera->GetFov() /
+                            graphics->GetHeight() *
+                            state->delta_.x_;
+                        vehicle_->controls_.pitch_ +=
+                            TOUCH_SENSITIVITY * camera->GetFov() /
+                            graphics->GetHeight() *
+                            state->delta_.y_;
+                    }
+                }
+            }
+            else
+            {
+                vehicle_->controls_.yaw_ +=
+                    (float)input->GetMouseMoveX() * YAW_SENSITIVITY;
+                vehicle_->controls_.pitch_ +=
+                    (float)input->GetMouseMoveY() * YAW_SENSITIVITY;
+            }
+            // Limit pitch
+            vehicle_->controls_.pitch_ =
+                Clamp(vehicle_->controls_.pitch_, 0.0f, 80.0f);
+            // Check for loading / saving the scene
+            if (input->GetKeyPress(KEY_F5))
+            {
+                File saveFile(context_,
+                              GetSubsystem<FileSystem>()->GetProgramDir() +
+                                  "Data/Scenes/RaycastVehicleDemo.xml",
+                              FILE_WRITE);
+                scene_->SaveXML(saveFile);
+            }
+            if (input->GetKeyPress(KEY_F7))
+            {
+                File loadFile(context_,
+                              GetSubsystem<FileSystem>()->GetProgramDir() +
+                                  "Data/Scenes/RaycastVehicleDemo.xml",
+                              FILE_READ);
+                scene_->LoadXML(loadFile);
+                // After loading we have to reacquire the weak pointer to the Vehicle component, as it has been recreated
+                // Simply find the vehicle's scene node by name as there's only one of them
+                Node* vehicleNode =
+                    scene_->GetChild("Vehicle", true);
+                if (vehicleNode)
+                {
+                    vehicle_ =
+                        vehicleNode->GetComponent<Vehicle>();
+                }
+            }
+        }
+        else
+        {
+            vehicle_->controls_.Set(CTRL_FORWARD | CTRL_BACK | CTRL_LEFT |
+                                        CTRL_RIGHT | CTRL_BRAKE,
+                                    false);
+        }
+    }
+}
+
+void RaycastVehicleDemo::HandlePostUpdate(StringHash eventType,
+                                          VariantMap& eventData)
+{
+    if (!vehicle_)
+    {
+        return;
+    }
+    Node* vehicleNode = vehicle_->GetNode();
+    // Physics update has completed. Position camera behind vehicle
+    Quaternion dir(vehicleNode->GetRotation().YawAngle(), Vector3::UP);
+    dir = dir * Quaternion(vehicle_->controls_.yaw_, Vector3::UP);
+    dir = dir * Quaternion(vehicle_->controls_.pitch_, Vector3::RIGHT);
+    Vector3 cameraTargetPos =
+        vehicleNode->GetPosition() - dir * Vector3(0.0f, 0.0f, CAMERA_DISTANCE);
+    Vector3 cameraStartPos = vehicleNode->GetPosition();
+    // 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);
+    float cameraRayLength = (cameraTargetPos - cameraStartPos).Length();
+    PhysicsRaycastResult result;
+    scene_->GetComponent<PhysicsWorld>()->RaycastSingle(result,
+                                                          cameraRay,
+                                                          cameraRayLength,
+                                                          2);
+    if (result.body_)
+    {
+        cameraTargetPos =
+            cameraStartPos + cameraRay.direction_ * (result.distance_ - 0.5f);
+    }
+    cameraNode_->SetPosition(cameraTargetPos);
+    cameraNode_->SetRotation(dir);
+}

+ 77 - 0
Source/Samples/46_RaycastVehicle/RaycastVehicleDemo.h

@@ -0,0 +1,77 @@
+//
+// Copyright (c) 2008-2017 the Urho3D project.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+#pragma once
+
+#include "Sample.h"
+
+namespace Urho3D
+{
+
+class Node;
+
+class Scene;
+}
+
+class Vehicle;
+
+/// Vehicle example.
+/// This sample demonstrates:
+///     - Creating a heightmap terrain with collision
+///     - Constructing 100 raycast vehicles
+///     - Defining attributes (including node and component references) of a custom component
+///     (Saving and loading is broken now)
+
+class RaycastVehicleDemo : public Sample
+{
+
+    URHO3D_OBJECT(RaycastVehicleDemo, Sample);
+
+public:
+    /// Construct.
+    RaycastVehicleDemo(Context* context);
+
+    /// Setup after engine initialization and before running the main loop.
+    virtual void Start();
+
+private:
+    /// Create static scene content.
+    void CreateScene();
+
+    /// Create the vehicle.
+    void CreateVehicle();
+
+    /// Construct an instruction text to the UI.
+    void CreateInstructions();
+
+    /// Subscribe to necessary events.
+    void SubscribeToEvents();
+
+    /// Handle application update. Set controls to vehicle.
+    void HandleUpdate(StringHash eventType, VariantMap& eventData);
+
+    /// Handle application post-update. Update camera position after vehicle has moved.
+    void HandlePostUpdate(StringHash eventType, VariantMap& eventData);
+
+    /// The controllable vehicle component.
+    WeakPtr< Vehicle > vehicle_;
+};

+ 261 - 0
Source/Samples/46_RaycastVehicle/Vehicle.cpp

@@ -0,0 +1,261 @@
+#include "Vehicle.h"
+#include <Urho3D/Core/Context.h>
+#include <Urho3D/Graphics/DebugRenderer.h>
+#include <Urho3D/Graphics/DecalSet.h>
+#include <Urho3D/Graphics/Material.h>
+#include <Urho3D/Graphics/Model.h>
+#include <Urho3D/Graphics/ParticleEffect.h>
+#include <Urho3D/Graphics/ParticleEmitter.h>
+#include <Urho3D/Graphics/StaticModel.h>
+#include <Urho3D/IO/Log.h>
+#include <Urho3D/Physics/CollisionShape.h>
+#include <Urho3D/Physics/Constraint.h>
+#include <Urho3D/Physics/PhysicsEvents.h>
+#include <Urho3D/Physics/PhysicsUtils.h>
+#include <Urho3D/Physics/PhysicsWorld.h>
+#include <Urho3D/Physics/RaycastVehicle.h>
+#include <Urho3D/Physics/RigidBody.h>
+#include <Urho3D/Resource/ResourceCache.h>
+#include <Urho3D/Scene/Scene.h>
+#include <Urho3D/Urho3D.h>
+
+const float CHASSIS_WIDTH = 2.6f;
+const float WHEEL_WIDTH = 0.4f;
+#define DELETE_NULL(x) \
+    {                  \
+        if (x)         \
+            delete x;  \
+        x = NULL;      \
+    }
+
+void Vehicle::RegisterObject(Context* context)
+{
+    context->RegisterFactory<Vehicle>();
+    URHO3D_ATTRIBUTE("Steering", float, steering_, 0.0f, AM_DEFAULT);
+    URHO3D_ATTRIBUTE("Controls Yaw", float, controls_.yaw_, 0.0f,
+                     AM_DEFAULT);
+    URHO3D_ATTRIBUTE("Controls Pitch", float, controls_.pitch_, 0.0f,
+                     AM_DEFAULT);
+}
+
+Vehicle::Vehicle(Urho3D::Context* context)
+    : LogicComponent(context),
+      steering_(0.0f)
+{
+    SetUpdateEventMask(USE_FIXEDUPDATE | USE_POSTUPDATE);
+    engineForce_ = 0.0f;
+    brakingForce_ = 50.0f;
+    vehicleSteering_ = 0.0f;
+    maxEngineForce_ = 2500.0f;
+    wheelRadius_ = 0.5f;
+    suspensionRestLength_ = 0.6f;
+    wheelWidth_ = 0.4f;
+    suspensionStiffness_ = 14.0f;
+    suspensionDamping_ = 2.0f;
+    suspensionCompression_ = 4.0f;
+    wheelFriction_ = 1000.0f;
+    rollInfluence_ = 0.12f;
+    emittersCreated = false;
+}
+
+Vehicle::~Vehicle()
+{
+}
+
+void Vehicle::Init()
+{
+    RaycastVehicle* vehicle = node_->CreateComponent<RaycastVehicle>();
+    vehicle->Init();
+    RigidBody* hullBody = node_->GetComponent<RigidBody>();
+    hullBody->SetMass(800.0f);
+    hullBody->SetLinearDamping(0.2f); // Some air resistance
+    hullBody->SetAngularDamping(0.5f);
+    hullBody->SetCollisionLayer(1);
+    // This function is called only from the main program when initially creating the vehicle, not on scene load
+    ResourceCache* cache = GetSubsystem<ResourceCache>();
+    StaticModel* hullObject = node_->CreateComponent<StaticModel>();
+    // Setting-up collision shape
+    CollisionShape* hullColShape =
+        node_->CreateComponent<CollisionShape>();
+    Vector3 v3BoxExtents = Vector3::ONE;
+    hullColShape->SetBox(v3BoxExtents);
+    node_->SetScale(Vector3(2.3f, 1.0f, 4.0f));
+    hullObject->SetModel(cache->GetResource<Model>("Models/Box.mdl"));
+    hullObject->SetMaterial(cache->GetResource<Material>("Materials/Stone.xml"));
+    hullObject->SetCastShadows(true);
+    float connectionHeight = -0.4f;
+    bool isFrontWheel = true;
+    Vector3 wheelDirection(0, -1, 0);
+    Vector3 wheelAxle(-1, 0, 0);
+    // We use not scaled coordinates here as everything will be scaled.
+    // Wheels are on bottom at edges of the chassis
+    // Note we don't set wheel nodes as children of hull (while we could) to avoid scaling to affect them.
+    float wheelX = CHASSIS_WIDTH / 2.0f - wheelWidth_;
+    // Front left
+    connectionPoints_[0] =
+        Vector3(-wheelX, connectionHeight, 2.5f - GetWheelRadius() * 2.0f);
+    // Front right
+    connectionPoints_[1] =
+        Vector3(wheelX, connectionHeight, 2.5f - GetWheelRadius() * 2.0f);
+    // Back left
+    connectionPoints_[2] =
+        Vector3(-wheelX, connectionHeight, -2.5f + GetWheelRadius() * 2.0f);
+    // Back right
+    connectionPoints_[3] =
+        Vector3(wheelX, connectionHeight, -2.5f + GetWheelRadius() * 2.0f);
+    const Color LtBrown(0.972f, 0.780f, 0.412f);
+    for (int id = 0;
+         id < sizeof(connectionPoints_) / sizeof(connectionPoints_[0]);
+         id++)
+    {
+        Node* wheelNode = GetScene()->CreateChild();
+        Vector3 connectionPoint = connectionPoints_[id];
+        // Front wheels are at front (z > 0)
+        // back wheels are at z < 0
+        // Setting rotation according to wheel position
+        bool isFrontWheel = connectionPoints_[id].z_ > 0.0f;
+        wheelNode->SetRotation(connectionPoint.x_ >=
+                                       0.0
+                                   ? Quaternion(0.0f, 0.0f,
+                                                -90.0f)
+                                   : Quaternion(0.0f, 0.0f, 90.0f));
+        wheelNode->SetWorldPosition(node_->GetWorldPosition() +
+                                    node_->GetWorldRotation() *
+                                        connectionPoints_[id]);
+        vehicle->AddWheel(wheelNode, wheelDirection, wheelAxle,
+                          suspensionRestLength_, wheelRadius_,
+                          isFrontWheel);
+        vehicle->SetWheelSuspensionStiffness(id, suspensionStiffness_);
+        vehicle->SetWheelDampingRelaxation(id, suspensionDamping_);
+        vehicle->SetWheelDampingCompression(id, suspensionCompression_);
+        vehicle->SetWheelFrictionSlip(id, wheelFriction_);
+        vehicle->SetWheelRollInfluence(id, rollInfluence_);
+        wheelNode->SetScale(Vector3(1.0f, 0.65f, 1.0f));
+        StaticModel* pWheel = wheelNode->CreateComponent<StaticModel>();
+        pWheel->SetModel(cache->GetResource<Model>("Models/Cylinder.mdl"));
+        pWheel->SetMaterial(cache->GetResource<Material>("Materials/Stone.xml"));
+        pWheel->SetCastShadows(true);
+        CreateEmitter(connectionPoints_[id]);
+    }
+    emittersCreated = true;
+    vehicle->ResetWheels();
+}
+
+void Vehicle::CreateEmitter(Vector3 place)
+{
+    ResourceCache* cache = GetSubsystem<ResourceCache>();
+    Node* emitter = GetScene()->CreateChild();
+    emitter->SetWorldPosition(node_->GetWorldPosition() +
+                              node_->GetWorldRotation() * place +
+                              Vector3(0, -wheelRadius_, 0));
+    ParticleEmitter* particleEmitter =
+        emitter->CreateComponent<ParticleEmitter>();
+    particleEmitter->SetEffect(cache->GetResource<ParticleEffect>("Particle/Dust.xml"));
+    particleEmitter->SetEmitting(false);
+    particleEmitterNodeList_.Push(emitter);
+    emitter->SetTemporary(true);
+}
+
+/// Applying attributes
+void Vehicle::ApplyAttributes()
+{
+    RaycastVehicle* vehicle =
+        node_->GetOrCreateComponent<RaycastVehicle>();
+    if (emittersCreated)
+        return;
+    for (int i = 0; i < 4; i++)
+    {
+        CreateEmitter(connectionPoints_[i]);
+    }
+    emittersCreated = true;
+}
+
+void Vehicle::FixedUpdate(float timeStep)
+{
+    float newSteering = 0.0f;
+    float accelerator = 0.0f;
+    bool brake = false;
+    RaycastVehicle* vehicle = node_->GetComponent<RaycastVehicle>();
+    // Read controls
+    if (controls_.buttons_ & CTRL_LEFT)
+    {
+        newSteering = -1.0f;
+    }
+    if (controls_.buttons_ & CTRL_RIGHT)
+    {
+        newSteering = 1.0f;
+    }
+    if (controls_.buttons_ & CTRL_FORWARD)
+    {
+        accelerator = 1.0f;
+    }
+    if (controls_.buttons_ & CTRL_BACK)
+    {
+        accelerator = -0.5f;
+    }
+    if (controls_.buttons_ & CTRL_BRAKE)
+    {
+        brake = true;
+    }
+    // When steering, wake up the wheel rigidbodies so that their orientation is updated
+    if (newSteering != 0.0f)
+    {
+        SetSteering(GetSteering() * 0.95f + newSteering * 0.05f);
+    }
+    else
+    {
+        SetSteering(GetSteering() * 0.8f + newSteering * 0.2f);
+    }
+    // Set front wheel angles
+    vehicleSteering_ = steering_;
+    int wheelIndex = 0;
+    vehicle->SetSteeringValue(wheelIndex, vehicleSteering_);
+    wheelIndex = 1;
+    vehicle->SetSteeringValue(wheelIndex, vehicleSteering_);
+    // apply forces
+    engineForce_ = maxEngineForce_ * accelerator;
+    // 2x wheel drive
+    vehicle->SetEngineForce(2, engineForce_);
+    vehicle->SetEngineForce(3, engineForce_);
+    for (int i = 0; i < vehicle->GetNumWheels(); i++)
+        if (brake)
+        {
+            vehicle->SetBrake(i, brakingForce_);
+        }
+        else
+        {
+            vehicle->SetBrake(i, 0.0f);
+        }
+}
+
+void Vehicle::PostUpdate(float timeStep)
+{
+    RaycastVehicle* vehicle = node_->GetComponent<RaycastVehicle>();
+    RigidBody* vehicleBody = node_->GetComponent<RigidBody>();
+    Vector3 velocity = vehicleBody->GetLinearVelocity();
+    Vector3 accel = (velocity - prevVelocity_) / timeStep;
+    float planeAccel = Vector3(accel.x_, 0.0f, accel.z_).Length();
+    for (int i = 0; i < vehicle->GetNumWheels(); i++)
+    {
+        Node* emitter = particleEmitterNodeList_[i];
+        ParticleEmitter* particleEmitter =
+            emitter->GetComponent<ParticleEmitter>();
+        if (vehicle->WheelIsGrounded(i) && (vehicle->GetWheelSkidInfoCumulative(i) < 0.9f || vehicle->GetBrake(i) > 2.0f || planeAccel > 15.0f))
+        {
+            particleEmitterNodeList_[i]->SetWorldPosition(vehicle->GetContactPosition(i));
+            if (!particleEmitter->IsEmitting())
+            {
+                particleEmitter->SetEmitting(true);
+            }
+            URHO3D_LOGDEBUG("GetWheelSkidInfoCumulative() = " +
+                            String(vehicle->GetWheelSkidInfoCumulative(i)) + " " +
+                            String(vehicle->GetMaxSideSlipSpeed()));
+            /* TODO: Add skid marks here */
+        }
+        else if (particleEmitter->IsEmitting())
+        {
+            particleEmitter->SetEmitting(false);
+        }
+    }
+    prevVelocity_ = velocity;
+}

+ 110 - 0
Source/Samples/46_RaycastVehicle/Vehicle.h

@@ -0,0 +1,110 @@
+#pragma once
+#include <Urho3D/Input/Controls.h>
+#include <Urho3D/Physics/PhysicsUtils.h>
+#include <Urho3D/Scene/LogicComponent.h>
+
+namespace Urho3D
+{
+class Constraint;
+class Node;
+class RigidBody;
+}
+using namespace Urho3D;
+
+const int CTRL_FORWARD = (1 << 0);
+const int CTRL_BACK = (1 << 1);
+const int CTRL_LEFT = (1 << 2);
+const int CTRL_RIGHT = (1 << 3);
+const int CTRL_BRAKE = (1 << 4);
+const float YAW_SENSITIVITY = 0.1f;
+const float ENGINE_POWER = 10.0f;
+const float MAX_WHEEL_ANGLE = 22.5f;
+// Vehicle component, responsible for physical movement according to controls.
+// Encapsulates RaycastVehicle
+class Vehicle : public LogicComponent
+{
+    URHO3D_OBJECT(Vehicle, LogicComponent)
+    public :
+        /// Construct.
+        Vehicle(Context* context);
+    ~Vehicle();
+    /// Register object factory and attributes.
+    static void RegisterObject(Context* context);
+    /// Perform post-load after deserialization. Acquire the components from the scene nodes.
+    virtual void ApplyAttributes();
+
+    /// Initialize the vehicle. Create rendering and physics components. Called by the application.
+    void Init();
+
+    /// Handle physics world update. Called by LogicComponent base class.
+    virtual void FixedUpdate(float timeStep);
+    /// Updating wheel effects here
+    virtual void PostUpdate(float timeStep);
+
+    /// Movement controls.
+    Controls controls_;
+    /// Get steering value
+    float GetSteering()
+    {
+        return steering_;
+    }
+    /// Set steering value
+    void SetSteering(float steering)
+    {
+        steering_ = steering;
+    }
+    /// Get wheel radius
+    float GetWheelRadius()
+    {
+        return wheelRadius_;
+    }
+    /// Get wheel width
+    float GetWheelWidth()
+    {
+        return wheelWidth_;
+    }
+    /// Used for saving - get IDs of particle nodes (memorizing these)
+    VariantVector GetParticleNodeIDsAttr() const;
+    /// Used for loading - set IDs of particle nodes (creating these)
+    void SetParticleNodeIDsAttr(const VariantVector& value);
+
+private:
+    /// Creates particle emitter
+    void CreateEmitter(Vector3 place);
+    /// Current left/right steering amount (-1 to 1.)
+    float steering_;
+    /// tmp storage for steering
+    float vehicleSteering_;
+    /// linear momentum supplied by engine to RigidBody
+    float engineForce_;
+    /// rotational momentum preventing (dampening) wheels rotation
+    float brakingForce_;
+    /// maximum linear momentum supplied by engine to RigidBody
+    float maxEngineForce_;
+    /// stored wheel radius
+    float wheelRadius_;
+    /// suspension rest length (in meters)
+    float suspensionRestLength_;
+    /// width of wheel (used only in calculation of wheel placement)
+    float wheelWidth_;
+    /// suspension stiffness
+    float suspensionStiffness_;
+    /// suspension damping
+    float suspensionDamping_;
+    /// suspension compression
+    float suspensionCompression_;
+    /// wheel friction
+    float wheelFriction_;
+    /// wheel roll influence (how much car will turn sidewise)
+    float rollInfluence_;
+    // emitter data for saving
+    Vector< Node* > particleEmitterNodeList_;
+    // value to calculate acceleration
+    Vector3 prevVelocity_;
+    // emitter data for saving
+    VariantVector emitterSaveData_;
+    // storing points for emitters
+    Vector3 connectionPoints_[4];
+    // Do not recreate emitters if they are already created
+    bool emittersCreated;
+};

+ 57 - 0
Source/Urho3D/AngelScript/PhysicsAPI.cpp

@@ -29,6 +29,7 @@
 #include "../Physics/Constraint.h"
 #include "../Physics/Constraint.h"
 #include "../Physics/PhysicsWorld.h"
 #include "../Physics/PhysicsWorld.h"
 #include "../Physics/RigidBody.h"
 #include "../Physics/RigidBody.h"
+#include "../Physics/RaycastVehicle.h"
 #include "../Scene/Scene.h"
 #include "../Scene/Scene.h"
 
 
 namespace Urho3D
 namespace Urho3D
@@ -342,12 +343,68 @@ static void RegisterPhysicsWorld(asIScriptEngine* engine)
     engine->RegisterGlobalFunction("PhysicsWorld@+ get_physicsWorld()", asFUNCTION(GetPhysicsWorld), asCALL_CDECL);
     engine->RegisterGlobalFunction("PhysicsWorld@+ get_physicsWorld()", asFUNCTION(GetPhysicsWorld), asCALL_CDECL);
 }
 }
 
 
+void RegisterRaycastVehicleAPI(asIScriptEngine* engine)
+{
+    RegisterComponent<RaycastVehicle>(engine, "RaycastVehicle");
+    // please keep these in the same order as in RaycastVehicle.h
+    engine->RegisterObjectMethod("RaycastVehicle", "void AddWheel(Node@+, Vector3, Vector3, float, float, bool)", asMETHOD(RaycastVehicle, AddWheel), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "void ResetSuspension()", asMETHOD(RaycastVehicle, ResetSuspension), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "void UpdateWheelTransform(int, bool)", asMETHOD(RaycastVehicle, UpdateWheelTransform), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "Vector3 GetWheelPosition(int)", asMETHOD(RaycastVehicle, GetWheelPosition), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "Vector3 GetWheelRotation(int)", asMETHOD(RaycastVehicle, GetWheelRotation), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "Vector3 GetWheelConnectionPoint(int)", asMETHOD(RaycastVehicle, GetWheelConnectionPoint), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "int get_numWheels()", asMETHOD(RaycastVehicle, GetNumWheels), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "Node@+ GetWheelNode(int)", asMETHOD(RaycastVehicle, GetWheelNode), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "void SetSteeringValue(int, float)", asMETHOD(RaycastVehicle, SetSteeringValue), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "float GetSteeringValue(int)", asMETHOD(RaycastVehicle, GetSteeringValue), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "void SetWheelSuspensionStiffness(int, float)", asMETHOD(RaycastVehicle, SetWheelSuspensionStiffness), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "float GetWheelSuspensionStiffness(int)", asMETHOD(RaycastVehicle, GetWheelSuspensionStiffness), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "void SetWheelDampingRelaxation(int, float)", asMETHOD(RaycastVehicle, SetWheelDampingRelaxation), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "float GetWheelDampingRelaxation(int)", asMETHOD(RaycastVehicle, GetWheelDampingRelaxation), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "void SetWheelDampingCompression(int, float)", asMETHOD(RaycastVehicle, SetWheelDampingCompression), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "float GetWheelDampingCompression(int)", asMETHOD(RaycastVehicle, GetWheelDampingCompression), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "void SetWheelFrictionSlip(int, float)", asMETHOD(RaycastVehicle, SetWheelFrictionSlip), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "float GetWheelFrictionSlip(int)", asMETHOD(RaycastVehicle, GetWheelFrictionSlip), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "void SetWheelRollInfluence(int, float)", asMETHOD(RaycastVehicle, SetWheelRollInfluence), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "float GetWheelRollInfluence(int)", asMETHOD(RaycastVehicle, GetWheelRollInfluence), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "void SetEngineForce(int, float)", asMETHOD(RaycastVehicle, SetEngineForce), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "float GetEngineForce(int)", asMETHOD(RaycastVehicle, GetEngineForce), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "void SetBrake(int, float)", asMETHOD(RaycastVehicle, SetBrake), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "float GetBrake(int)", asMETHOD(RaycastVehicle, GetBrake), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "void SetWheelRadius(int, float)", asMETHOD(RaycastVehicle, SetWheelRadius), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "float GetWheelRadius(int)", asMETHOD(RaycastVehicle, GetWheelRadius), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "void ResetWheels()", asMETHOD(RaycastVehicle, ResetWheels), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "void SetWheelRestLength(int, float)", asMETHOD(RaycastVehicle, SetWheelRestLength), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "float GetWheelRestLength(int)", asMETHOD(RaycastVehicle, GetWheelRestLength), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "void SetWheelSkidInfo(int, float)", asMETHOD(RaycastVehicle, SetWheelSkidInfo), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "float GetWheelSkidInfo(int)", asMETHOD(RaycastVehicle, GetWheelSkidInfo), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "bool WheelIsGrounded(int)", asMETHOD(RaycastVehicle, WheelIsGrounded), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "void SetMaxSuspensionTravel(int, float)", asMETHOD(RaycastVehicle, SetMaxSuspensionTravel), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "float GetMaxSuspensionTravel(int)", asMETHOD(RaycastVehicle, GetMaxSuspensionTravel), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "void SetWheelDirection(int, Vector3)", asMETHOD(RaycastVehicle, SetWheelDirection), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "Vector3 GetWheelDirection(int)", asMETHOD(RaycastVehicle, GetWheelDirection), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "void SetWheelAxle(int, Vector3)", asMETHOD(RaycastVehicle, SetWheelAxle), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "Vector3 GetWheelAxle(int)", asMETHOD(RaycastVehicle, GetWheelAxle), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "float GetWheelSideSlipSpeed(int)", asMETHOD(RaycastVehicle, GetWheelSideSlipSpeed), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "float get_maxSideSlipSpeed()", asMETHOD(RaycastVehicle, GetMaxSideSlipSpeed), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "void set_maxSideSlipSpeed(float)", asMETHOD(RaycastVehicle, SetMaxSideSlipSpeed), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "void SetWheelSkidInfoCumulative(int, float)", asMETHOD(RaycastVehicle, SetWheelSkidInfoCumulative), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "float GetWheelSkidInfoCumulative(int)", asMETHOD(RaycastVehicle, GetWheelSkidInfoCumulative), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "bool IsFrontWheel(int)", asMETHOD(RaycastVehicle, IsFrontWheel), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "Vector3 GetContactPosition(int)", asMETHOD(RaycastVehicle, GetContactPosition), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "Vector3 GetContactNormal(int)", asMETHOD(RaycastVehicle, GetContactNormal), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "void set_inAirRPM(float)", asMETHOD(RaycastVehicle, SetInAirRPM), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "float get_inAirRPM()", asMETHOD(RaycastVehicle, GetInAirRPM), asCALL_THISCALL);
+    engine->RegisterObjectMethod("RaycastVehicle", "void Init()", asMETHOD(RaycastVehicle, Init), asCALL_THISCALL);
+}
+
 void RegisterPhysicsAPI(asIScriptEngine* engine)
 void RegisterPhysicsAPI(asIScriptEngine* engine)
 {
 {
     RegisterCollisionShape(engine);
     RegisterCollisionShape(engine);
     RegisterRigidBody(engine);
     RegisterRigidBody(engine);
     RegisterConstraint(engine);
     RegisterConstraint(engine);
     RegisterPhysicsWorld(engine);
     RegisterPhysicsWorld(engine);
+    RegisterRaycastVehicleAPI(engine);
 }
 }
 
 
 }
 }

+ 2 - 0
Source/Urho3D/AngelScript/ScriptAPI.h

@@ -62,6 +62,8 @@ void RegisterIKAPI(asIScriptEngine* engine);
 #ifdef URHO3D_PHYSICS
 #ifdef URHO3D_PHYSICS
 /// Register the Physics library to script.
 /// Register the Physics library to script.
 void RegisterPhysicsAPI(asIScriptEngine* engine);
 void RegisterPhysicsAPI(asIScriptEngine* engine);
+/// Register RaycastVehicle component to script.
+void RegisterRaycastVehicleAPI(asIScriptEngine* engine);
 #endif
 #endif
 #ifdef URHO3D_NAVIGATION
 #ifdef URHO3D_NAVIGATION
 /// Register the Navigation library to script.
 /// Register the Navigation library to script.

+ 3 - 1
Source/Urho3D/Engine/Engine.cpp

@@ -52,6 +52,7 @@
 #endif
 #endif
 #ifdef URHO3D_PHYSICS
 #ifdef URHO3D_PHYSICS
 #include "../Physics/PhysicsWorld.h"
 #include "../Physics/PhysicsWorld.h"
+#include "../Physics/RaycastVehicle.h"
 #endif
 #endif
 #include "../Resource/ResourceCache.h"
 #include "../Resource/ResourceCache.h"
 #include "../Resource/Localization.h"
 #include "../Resource/Localization.h"
@@ -145,9 +146,10 @@ Engine::Engine(Context* context) :
 #ifdef URHO3D_IK
 #ifdef URHO3D_IK
     RegisterIKLibrary(context_);
     RegisterIKLibrary(context_);
 #endif
 #endif
-
+    
 #ifdef URHO3D_PHYSICS
 #ifdef URHO3D_PHYSICS
     RegisterPhysicsLibrary(context_);
     RegisterPhysicsLibrary(context_);
+    RegisterRaycastVehicleLibrary(context_);
 #endif
 #endif
 
 
 #ifdef URHO3D_NAVIGATION
 #ifdef URHO3D_NAVIGATION

+ 64 - 0
Source/Urho3D/LuaScript/pkgs/Physics/RaycastVehicle.pkg

@@ -0,0 +1,64 @@
+$#include "Physics/RaycastVehicle.h"
+
+class RaycastVehicle : public LogicComponent
+{
+        RaycastVehicle(Urho3D::Context* context);
+        ~RaycastVehicle();
+        static void RegisterObject(Context* context);
+        virtual void ApplyAttributes();
+        void AddWheel(Node *wheelNode, Vector3 wheelDirection, Vector3 wheelAxle, float restLength, float wheelRadius, bool frontWheel);
+        void ResetSuspension(void);
+        void UpdateWheelTransform(int wheel, bool interpolated);
+        Vector3 GetWheelPosition(int wheel);
+        Quaternion GetWheelRotation(int wheel);
+        Vector3 GetWheelConnectionPoint(int wheel);
+        int GetNumWheels();
+        Node *GetWheelNode(int wheel);
+        void SetSteeringValue(int wheel, float steeringValue);
+        float GetSteeringValue(int wheel) const;
+        void SetWheelSuspensionStiffness(int wheel, float stiffness);
+        float GetWheelSuspensionStiffness(int wheel) const;
+        void SetWheelDampingRelaxation(int wheel, float damping);
+        float GetWheelDampingRelaxation(int wheel) const;
+        void SetWheelDampingCompression(int wheel, float compression);
+        float GetWheelDampingCompression(int wheel) const;
+        void SetWheelFrictionSlip(int wheel, float slip);
+        float GetWheelFrictionSlip(int wheel) const;
+        void SetWheelRollInfluence(int wheel, float rollInfluence);
+        float GetWheelRollInfluence(int wheel) const;
+        void SetEngineForce(int wheel, float force);
+        float GetEngineForce(int wheel) const;
+        void SetBrake(int wheel, float force);
+        float GetBrake(int wheel) const;
+        void SetWheelRadius(int wheel, float wheelRadius);
+        float GetWheelRadius(int wheel) const;
+        void ResetWheels();
+        void SetWheelRestLength(int wheel, float length);
+        float GetWheelRestLength(int wheel) const;
+        void SetWheelSkidInfo(int wheel, float factor);
+        float GetWheelSkidInfo(int wheel) const;
+        bool WheelIsGrounded(int wheel) const;
+        void SetMaxSuspensionTravel(int wheel, float maxSuspensionTravel);
+        float GetMaxSuspensionTravel(int wheel);
+        void SetWheelDirection(int wheel, Vector3 direction);
+        Vector3 GetWheelDirection(int wheel) const;
+        void SetWheelAxle(int wheel, Vector3 axle);
+        Vector3 GetWheelAxle(int wheel) const;
+        float GetWheelSideSlipSpeed(int wheel) const;
+        float GetMaxSideSlipSpeed() const;
+        void SetMaxSideSlipSpeed(float speed);
+        void SetWheelSkidInfoCumulative(int wheel, float skid);
+        float GetWheelSkidInfoCumulative(int wheel) const;
+        bool IsFrontWheel(int wheel) const;
+        Vector3 GetContactPosition(int wheel) const;
+        Vector3 GetContactNormal(int wheel) const;
+        void SetInAirRPM(float rpm);
+        float GetInAirRPM() const;
+        void Init();
+        void FixedUpdate(float timeStep);
+        void FixedPostUpdate(float timeStep);
+        void PostUpdate(float timeStep);
+        VariantVector GetWheelDataForSave() const;
+        void SetLoadedWheelData(const VariantVector& value);
+};
+

+ 1 - 0
Source/Urho3D/LuaScript/pkgs/PhysicsLuaAPI.pkg

@@ -2,6 +2,7 @@ $pfile "Physics/CollisionShape.pkg"
 $pfile "Physics/Constraint.pkg"
 $pfile "Physics/Constraint.pkg"
 $pfile "Physics/PhysicsWorld.pkg"
 $pfile "Physics/PhysicsWorld.pkg"
 $pfile "Physics/RigidBody.pkg"
 $pfile "Physics/RigidBody.pkg"
+$pfile "Physics/RaycastVehicle.pkg"
 
 
 $using namespace Urho3D;
 $using namespace Urho3D;
 $#pragma warning(disable:4800)
 $#pragma warning(disable:4800)

+ 635 - 0
Source/Urho3D/Physics/RaycastVehicle.cpp

@@ -0,0 +1,635 @@
+//
+// Copyright (c) 2008-2017 the Urho3D project.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+#include "../Core/Context.h"
+#include <Bullet/BulletDynamics/Vehicle/btRaycastVehicle.h>
+#include <Bullet/BulletDynamics/Dynamics/btDynamicsWorld.h>
+#include "../Physics/PhysicsUtils.h"
+#include "../Physics/RigidBody.h"
+#include "../Physics/PhysicsWorld.h"
+#include "../Scene/Scene.h"
+#include "../IO/Log.h"
+#include "../Physics/RaycastVehicle.h"
+
+namespace Urho3D
+{
+
+    class RaycastVehicleData
+    {
+            // raycast vehicle
+            btVehicleRaycaster                  *m_vehicleRayCaster;
+            btRaycastVehicle                    *m_vehicle;
+        public:
+            btRaycastVehicle::btVehicleTuning   m_tuning;
+            RaycastVehicleData()
+            {
+                m_vehicleRayCaster = NULL;
+                m_vehicle = NULL;
+            }
+            ~RaycastVehicleData()
+            {
+                if (m_vehicleRayCaster)
+                {
+                    delete m_vehicleRayCaster;
+                }
+                m_vehicleRayCaster = NULL;
+                if (m_vehicle)
+                {
+                    delete m_vehicle;
+                }
+                m_vehicle = NULL;
+            }
+            btRaycastVehicle *Get()
+            {
+                return m_vehicle;
+            }
+            void Init(Scene *scene, RigidBody *body)
+            {
+                int rightIndex = 0;
+                int upIndex = 1;
+                int forwardIndex = 2;
+                PhysicsWorld *pPhysWorld = scene->GetComponent<PhysicsWorld>();
+                btDynamicsWorld *pbtDynWorld = (btDynamicsWorld*) pPhysWorld->GetWorld();
+
+                m_vehicleRayCaster = new btDefaultVehicleRaycaster(pbtDynWorld);
+                btRigidBody *bthullBody = body->GetBody();
+                m_vehicle = new btRaycastVehicle(m_tuning, bthullBody,
+                                                 m_vehicleRayCaster);
+                pbtDynWorld->addVehicle(m_vehicle);
+
+                m_vehicle->setCoordinateSystem(rightIndex, upIndex, forwardIndex);
+            }
+    };
+
+    RaycastVehicle::RaycastVehicle(Context* context)
+        : LogicComponent(context)
+    {
+        // fixed update() for inputs and post update() to sync wheels for rendering
+        SetUpdateEventMask(USE_FIXEDUPDATE | USE_FIXEDPOSTUPDATE | USE_POSTUPDATE);
+        vehicleData_ = new RaycastVehicleData;
+        wheelNodes_.Clear();
+        activate_ = false;
+        inAirRPM_ = 0.0f;
+        maxSideSlipSpeed_ = 4.0f;
+    }
+    RaycastVehicle::~RaycastVehicle()
+    {
+        delete vehicleData_;
+        wheelNodes_.Clear();
+    }
+    const char* wheelElementNames[] =
+    {
+        "Number of wheels",
+        "   Wheel node id",
+        "   Wheel direction",
+        "   Wheel axle",
+        "   Wheel rest length",
+        "   Wheel radius",
+        "   Wheel is front wheel",
+        "   Steering",
+        "   Connection point vector",
+        "   Original rotation",
+        "   Cumulative skid info",
+        "   Side skip speed",
+        "   Grounded",
+        "   Contact position",
+        "   Contact normal",
+        "   Suspension stiffness",
+        "   Damping relaxation",
+        "   Damping compression",
+        "   Friction slip",
+        "   Roll influence",
+        "   Engine force",
+        "   Brake",
+        0
+    };
+    void RaycastVehicle::RegisterObject(Context* context)
+    {
+        context->RegisterFactory<RaycastVehicle>();
+        URHO3D_MIXED_ACCESSOR_VARIANT_VECTOR_STRUCTURE_ATTRIBUTE("Wheel data", GetWheelDataForSave, SetLoadedWheelData,
+                VariantVector, Variant::emptyVariantVector,
+                wheelElementNames, AM_DEFAULT);
+        URHO3D_ATTRIBUTE("Maximum side slip threshold", float, maxSideSlipSpeed_, 4.0f, AM_DEFAULT);
+        URHO3D_ATTRIBUTE("RPM for wheel motors in air (0=calculate)", float, inAirRPM_, 0.0f, AM_DEFAULT);
+    }
+    VariantVector RaycastVehicle::GetWheelDataForSave() const
+    {
+        VariantVector ret;
+        ret.Reserve(GetNumWheels() * 22 + 1);
+        ret.Push(GetNumWheels());
+        for (int i = 0; i < GetNumWheels(); i++)
+        {
+            Node *wNode = GetWheelNode(i);
+            int node_id = wNode->GetID();
+            URHO3D_LOGDEBUG("RaycastVehicle: Saving node id = " + String(node_id));
+            ret.Push(node_id);
+            ret.Push(GetWheelDirection(i));
+            ret.Push(GetWheelAxle(i));
+            ret.Push(GetWheelRestLength(i));
+            ret.Push(GetWheelRadius(i));
+            ret.Push(IsFrontWheel(i));
+            ret.Push(GetSteeringValue(i));
+            ret.Push(GetWheelConnectionPoint(i));
+            ret.Push(origRotation_[i]);
+            ret.Push(GetWheelSkidInfoCumulative(i));
+            ret.Push(GetWheelSideSlipSpeed(i));
+            ret.Push(WheelIsGrounded(i));
+            ret.Push(GetContactPosition(i));
+            ret.Push(GetContactNormal(i));       // 14
+            ret.Push(GetWheelSuspensionStiffness(i));
+            ret.Push(GetWheelDampingRelaxation(i));
+            ret.Push(GetWheelDampingCompression(i));
+            ret.Push(GetWheelFrictionSlip(i));
+            ret.Push(GetWheelRollInfluence(i));
+            ret.Push(GetEngineForce(i));
+            ret.Push(GetBrake(i));
+            ret.Push(GetWheelSkidInfo(i));
+        }
+        URHO3D_LOGDEBUG("RaycastVehicle: saved items: " + String(ret.Size()));
+        URHO3D_LOGDEBUG("maxSideSlipSpeed_ value save: " + String(maxSideSlipSpeed_));
+        return ret;
+    }
+    void RaycastVehicle::SetLoadedWheelData(const VariantVector& value)
+    {
+        if (!vehicleData_)
+        {
+            URHO3D_LOGERROR("RaycastVehicle: vehicleData doesn't exist");
+            return;
+        }
+        if (value.Size() < 2)
+        {
+            URHO3D_LOGERROR("RaycastVehicle: Incorrect vehicleData");
+            return;
+        }
+        loadedWheelData_ = value;
+    }
+    void RaycastVehicle::ApplyAttributes()
+    {
+        int index = 0;
+        hullBody_ = node_->GetOrCreateComponent<RigidBody>();
+        Scene* scene = GetScene();
+        vehicleData_->Init(scene, hullBody_);
+        VariantVector& value = loadedWheelData_;
+        int numObjects = value[index++].GetInt();
+        int wheelIndex = 0;
+        origRotation_.Clear();
+        skidInfoCumulative_.Clear();
+        wheelSideSlipSpeed_.Clear();
+        for (int i = 0; i < numObjects; i++)
+        {
+            int node_id = value[index++].GetInt();
+            Vector3 direction = value[index++].GetVector3();
+            Vector3 axle = value[index++].GetVector3();
+            float restLength = value[index++].GetFloat();
+            float radius = value[index++].GetFloat();
+            bool isFrontWheel = value[index++].GetBool();
+            float steering = value[index++].GetFloat();
+            Vector3 connectionPoint = value[index++].GetVector3();
+            Quaternion origRotation = value[index++].GetQuaternion();
+            float skidInfoC = value[index++].GetFloat();
+            float sideSlipSpeed = value[index++].GetFloat();
+
+            bool isContact = value[index++].GetBool();
+            Vector3 contactPosition = value[index++].GetVector3();
+            Vector3 contactNormal = value[index++].GetVector3();
+            float suspensionStiffness = value[index++].GetFloat();
+            float dampingRelaxation = value[index++].GetFloat();
+            float dampingCompression = value[index++].GetFloat();
+            float frictionSlip = value[index++].GetFloat();
+            float rollInfluence = value[index++].GetFloat();
+            float engineForce = value[index++].GetFloat();
+            float brake = value[index++].GetFloat();
+            float skidInfo = value[index++].GetFloat();
+            Node *wheelNode = GetScene()->GetNode(node_id);
+            if (!wheelNode)
+            {
+                URHO3D_LOGERROR("RaycastVehicle: Incorrect node id = " + String(node_id) + " index: " + String(index));
+                continue;
+            }
+            btRaycastVehicle *m_vehicle = vehicleData_->Get();
+            int id = GetNumWheels();
+            btVector3 connectionPointCS0(connectionPoint.x_, connectionPoint.y_, connectionPoint.z_);
+            btVector3 wheelDirectionCS0(direction.x_, direction.y_, direction.z_);
+            btVector3 wheelAxleCS(axle.x_, axle.y_, axle.z_);
+            btWheelInfo& wheel = m_vehicle->addWheel(connectionPointCS0,
+                                 wheelDirectionCS0,
+                                 wheelAxleCS,
+                                 restLength,
+                                 radius,
+                                 vehicleData_->m_tuning,
+                                 isFrontWheel);
+            wheelNodes_.Push(wheelNode);
+            origRotation_.Push(origRotation);
+            skidInfoCumulative_.Push(skidInfoC);
+            wheelSideSlipSpeed_.Push(sideSlipSpeed);
+            SetSteeringValue(wheelIndex, steering);
+            wheel.m_raycastInfo.m_isInContact = isContact;
+            wheel.m_raycastInfo.m_contactNormalWS = btVector3(contactNormal.x_, contactNormal.y_, contactNormal.z_);
+            wheel.m_raycastInfo.m_contactPointWS = btVector3(contactPosition.x_, contactPosition.y_, contactPosition.z_);
+            wheel.m_suspensionStiffness = suspensionStiffness;
+            wheel.m_wheelsDampingRelaxation = dampingRelaxation;
+            wheel.m_wheelsDampingCompression = dampingCompression;
+            wheel.m_frictionSlip = frictionSlip;
+            wheel.m_rollInfluence = rollInfluence;
+            wheel.m_engineForce = engineForce;
+            wheel.m_brake = brake;
+            wheel.m_skidInfo = skidInfo;
+            wheelIndex++;
+        }
+        URHO3D_LOGDEBUG("maxSideSlipSpeed_ value: " + String(maxSideSlipSpeed_));
+        URHO3D_LOGDEBUG("loaded items: " + String(index));
+        URHO3D_LOGDEBUG("loaded wheels: " + String(GetNumWheels()));
+    }
+    void RaycastVehicle::Init()
+    {
+        hullBody_ = node_->GetOrCreateComponent<RigidBody>();
+        Scene* scene = GetScene();
+        vehicleData_->Init(scene, hullBody_);
+    }
+    void RaycastVehicle::FixedUpdate(float timeStep)
+    {
+        btRaycastVehicle *m_vehicle = vehicleData_->Get();
+        for (int i = 0; i < GetNumWheels(); i++)
+        {
+            btWheelInfo whInfo = m_vehicle->getWheelInfo(i);
+            if (whInfo.m_engineForce != 0.0f || whInfo.m_steering != 0.0f)
+            {
+                hullBody_->Activate();
+                break;
+            }
+        }
+    }
+    void RaycastVehicle::PostUpdate(float timeStep)
+    {
+        btRaycastVehicle *m_vehicle = vehicleData_->Get();
+        for (int i = 0; i < GetNumWheels(); i++)
+        {
+            m_vehicle->updateWheelTransform(i, true);
+            btTransform transform = m_vehicle->getWheelTransformWS(i);
+            Vector3 origin = ToVector3(transform.getOrigin());
+            Quaternion qRot = ToQuaternion(transform.getRotation());
+            Node *pWheel = wheelNodes_[i];
+            pWheel->SetWorldPosition(origin);
+            pWheel->SetWorldRotation(qRot * origRotation_[i]);
+        }
+    }
+    void RaycastVehicle::FixedPostUpdate(float timeStep)
+    {
+        btRaycastVehicle *m_vehicle = vehicleData_->Get();
+        Vector3 velocity = hullBody_->GetLinearVelocity();
+        for (int i = 0; i < GetNumWheels(); i++)
+        {
+            btWheelInfo& whInfo = m_vehicle->getWheelInfo(i);
+            if (!WheelIsGrounded(i) && GetEngineForce(i) != 0.0f)
+            {
+                float delta;
+                if (inAirRPM_ != 0.0f)
+                {
+                    delta = inAirRPM_ * timeStep / 60.0f;
+                }
+                else
+                {
+                    delta = 8.0 * GetEngineForce(i) * timeStep / (hullBody_->GetMass() * GetWheelRadius(i));
+                }
+                if (Abs(whInfo.m_deltaRotation) < Abs(delta))
+                {
+                    whInfo.m_rotation += delta - whInfo.m_deltaRotation;
+                    whInfo.m_deltaRotation = delta;
+                }
+                if (skidInfoCumulative_[i] > 0.05f)
+                {
+                    skidInfoCumulative_[i] -= 0.002;
+                }
+            }
+            else
+            {
+                skidInfoCumulative_[i] = GetWheelSkidInfo(i);
+            }
+            wheelSideSlipSpeed_[i] = Abs(ToVector3(whInfo.m_raycastInfo.m_wheelAxleWS).DotProduct(velocity));
+            if (wheelSideSlipSpeed_[i] > maxSideSlipSpeed_)
+            {
+                skidInfoCumulative_[i] = Clamp(skidInfoCumulative_[i], 0.0f, 0.89f);
+            }
+        }
+    }
+    void RaycastVehicle::SetMaxSideSlipSpeed(float speed)
+    {
+        maxSideSlipSpeed_ = speed;
+    }
+    float RaycastVehicle::GetMaxSideSlipSpeed() const
+    {
+        return maxSideSlipSpeed_;
+    }
+    void RaycastVehicle::SetWheelSkidInfoCumulative(int wheel, float skid)
+    {
+        skidInfoCumulative_[wheel] = skid;
+    }
+    float RaycastVehicle::GetWheelSkidInfoCumulative(int wheel) const
+    {
+        return skidInfoCumulative_[wheel];
+    }
+    void RaycastVehicle::AddWheel(Node *wheelNode,
+                                  Vector3 wheelDirection, Vector3 wheelAxle,
+                                  float restLength, float wheelRadius,
+                                  bool frontWheel)
+    {
+        btRaycastVehicle *m_vehicle = vehicleData_->Get();
+        int id = GetNumWheels();
+        Vector3 connectionPoint = wheelNode->GetWorldPosition() - node_->GetWorldPosition();
+        btVector3 connectionPointCS0(connectionPoint.x_, connectionPoint.y_, connectionPoint.z_);
+        btVector3 wheelDirectionCS0(wheelDirection.x_, wheelDirection.y_, wheelDirection.z_);
+        btVector3 wheelAxleCS(wheelAxle.x_, wheelAxle.y_, wheelAxle.z_);
+        btWheelInfo& wheel = m_vehicle->addWheel(connectionPointCS0,
+                             wheelDirectionCS0,
+                             wheelAxleCS,
+                             restLength,
+                             wheelRadius,
+                             vehicleData_->m_tuning,
+                             frontWheel);
+
+        wheelNodes_.Push(wheelNode);
+        origRotation_.Push(wheelNode->GetWorldRotation());
+        skidInfoCumulative_.Push(1.0f);
+        wheelSideSlipSpeed_.Push(0.0f);
+        wheel.m_raycastInfo.m_isInContact = false;
+    }
+    void RaycastVehicle::ResetSuspension()
+    {
+        btRaycastVehicle *m_vehicle = vehicleData_->Get();
+        m_vehicle->resetSuspension();
+    }
+    void RaycastVehicle::UpdateWheelTransform(int wheel, bool interpolated)
+    {
+        btRaycastVehicle *m_vehicle = vehicleData_->Get();
+        m_vehicle->updateWheelTransform(wheel, interpolated);
+    }
+
+    Vector3 RaycastVehicle::GetWheelPosition(int wheel)
+    {
+        btRaycastVehicle *m_vehicle = vehicleData_->Get();
+        btTransform transform = m_vehicle->getWheelTransformWS(wheel);
+        Vector3 origin = ToVector3(transform.getOrigin());
+        return origin;
+    }
+    Quaternion RaycastVehicle::GetWheelRotation(int wheel)
+    {
+        btRaycastVehicle *m_vehicle = vehicleData_->Get();
+        btTransform transform = m_vehicle->getWheelTransformWS(wheel);
+        Quaternion rotation = ToQuaternion(transform.getRotation());
+        return rotation;
+    }
+    Vector3 RaycastVehicle::GetWheelConnectionPoint(int wheel) const
+    {
+        btRaycastVehicle *m_vehicle = vehicleData_->Get();
+        btWheelInfo whInfo = m_vehicle->getWheelInfo(wheel);
+        return ToVector3(whInfo.m_chassisConnectionPointCS);
+    }
+    void RaycastVehicle::SetSteeringValue(int wheel, float steeringValue)
+    {
+        btRaycastVehicle *m_vehicle = vehicleData_->Get();
+        m_vehicle->setSteeringValue(steeringValue, wheel);
+    }
+    float RaycastVehicle::GetSteeringValue(int wheel) const
+    {
+        btRaycastVehicle *m_vehicle = vehicleData_->Get();
+        btWheelInfo whInfo = m_vehicle->getWheelInfo(wheel);
+        return whInfo.m_steering;
+    }
+    void RaycastVehicle::SetWheelSuspensionStiffness(int wheel, float stiffness)
+    {
+        btRaycastVehicle *m_vehicle = vehicleData_->Get();
+        btWheelInfo& whInfo = m_vehicle->getWheelInfo(wheel);
+        whInfo.m_suspensionStiffness = stiffness;
+    }
+    float RaycastVehicle::GetWheelSuspensionStiffness(int wheel) const
+    {
+        btRaycastVehicle *m_vehicle = vehicleData_->Get();
+        btWheelInfo whInfo = m_vehicle->getWheelInfo(wheel);
+        return whInfo.m_suspensionStiffness;
+    }
+    void RaycastVehicle::SetWheelDampingRelaxation(int wheel, float damping)
+    {
+        btRaycastVehicle *m_vehicle = vehicleData_->Get();
+        btWheelInfo& whInfo = m_vehicle->getWheelInfo(wheel);
+        whInfo.m_wheelsDampingRelaxation = damping;
+    }
+    float RaycastVehicle::GetWheelDampingRelaxation(int wheel) const
+    {
+        btRaycastVehicle *m_vehicle = vehicleData_->Get();
+        btWheelInfo whInfo = m_vehicle->getWheelInfo(wheel);
+        return whInfo.m_wheelsDampingRelaxation;
+    }
+    void RaycastVehicle::SetWheelDampingCompression(int wheel, float compression)
+    {
+        btRaycastVehicle *m_vehicle = vehicleData_->Get();
+        btWheelInfo& whInfo = m_vehicle->getWheelInfo(wheel);
+        whInfo.m_wheelsDampingCompression = compression;
+    }
+    float RaycastVehicle::GetWheelDampingCompression(int wheel) const
+    {
+        btRaycastVehicle *m_vehicle = vehicleData_->Get();
+        btWheelInfo whInfo = m_vehicle->getWheelInfo(wheel);
+        return whInfo.m_wheelsDampingCompression;
+    }
+    void RaycastVehicle::SetWheelFrictionSlip(int wheel, float slip)
+    {
+        btRaycastVehicle *m_vehicle = vehicleData_->Get();
+        btWheelInfo& whInfo = m_vehicle->getWheelInfo(wheel);
+        whInfo.m_frictionSlip = slip;
+    }
+    float RaycastVehicle::GetWheelFrictionSlip(int wheel) const
+    {
+        btRaycastVehicle *m_vehicle = vehicleData_->Get();
+        btWheelInfo whInfo = m_vehicle->getWheelInfo(wheel);
+        return whInfo.m_frictionSlip;
+    }
+    void RaycastVehicle::SetWheelRollInfluence(int wheel, float rollInfluence)
+    {
+        btRaycastVehicle *m_vehicle = vehicleData_->Get();
+        btWheelInfo& whInfo = m_vehicle->getWheelInfo(wheel);
+        whInfo.m_rollInfluence = rollInfluence;
+    }
+    Vector3 RaycastVehicle::GetContactPosition(int wheel) const
+    {
+        btRaycastVehicle *m_vehicle = vehicleData_->Get();
+        btWheelInfo& whInfo = m_vehicle->getWheelInfo(wheel);
+        return ToVector3(whInfo.m_raycastInfo.m_contactPointWS);
+    }
+    Vector3 RaycastVehicle::GetContactNormal(int wheel) const
+    {
+        btRaycastVehicle *m_vehicle = vehicleData_->Get();
+        btWheelInfo& whInfo = m_vehicle->getWheelInfo(wheel);
+        return ToVector3(whInfo.m_raycastInfo.m_contactNormalWS);
+    }
+    float RaycastVehicle::GetWheelSideSlipSpeed(int wheel) const
+    {
+        return wheelSideSlipSpeed_[wheel];
+    }
+    float RaycastVehicle::GetWheelRollInfluence(int wheel) const
+    {
+        btRaycastVehicle *m_vehicle = vehicleData_->Get();
+        btWheelInfo whInfo = m_vehicle->getWheelInfo(wheel);
+        return whInfo.m_rollInfluence;
+    }
+    void RaycastVehicle::SetWheelRadius(int wheel, float wheelRadius)
+    {
+        btRaycastVehicle *m_vehicle = vehicleData_->Get();
+        btWheelInfo& whInfo = m_vehicle->getWheelInfo(wheel);
+        whInfo.m_wheelsRadius = wheelRadius;
+    }
+    float RaycastVehicle::GetWheelRadius(int wheel) const
+    {
+        btRaycastVehicle *m_vehicle = vehicleData_->Get();
+        btWheelInfo& whInfo = m_vehicle->getWheelInfo(wheel);
+        return whInfo.m_wheelsRadius;
+    }
+    void RaycastVehicle::SetEngineForce(int wheel, float force)
+    {
+        btRaycastVehicle *m_vehicle = vehicleData_->Get();
+        m_vehicle->applyEngineForce(force, wheel);
+    }
+    float RaycastVehicle::GetEngineForce(int wheel) const
+    {
+        btRaycastVehicle *m_vehicle = vehicleData_->Get();
+        btWheelInfo whInfo = m_vehicle->getWheelInfo(wheel);
+        return whInfo.m_engineForce;
+    }
+    void RaycastVehicle::SetBrake(int wheel, float force)
+    {
+        btRaycastVehicle *m_vehicle = vehicleData_->Get();
+        m_vehicle->setBrake(force, wheel);
+    }
+    float RaycastVehicle::GetBrake(int wheel) const
+    {
+        btRaycastVehicle *m_vehicle = vehicleData_->Get();
+        btWheelInfo whInfo = m_vehicle->getWheelInfo(wheel);
+        return whInfo.m_brake;
+    }
+    int RaycastVehicle::GetNumWheels() const
+    {
+        btRaycastVehicle *m_vehicle = vehicleData_->Get();
+        return m_vehicle->getNumWheels();
+    }
+    Node *RaycastVehicle::GetWheelNode(int wheel) const
+    {
+        return wheelNodes_[wheel];
+    }
+    void RaycastVehicle::SetMaxSuspensionTravel(int wheel, float maxSuspensionTravel)
+    {
+        btRaycastVehicle *m_vehicle = vehicleData_->Get();
+        btWheelInfo& whInfo = m_vehicle->getWheelInfo(wheel);
+        whInfo.m_maxSuspensionTravelCm = maxSuspensionTravel;
+    }
+    float RaycastVehicle::GetMaxSuspensionTravel(int wheel)
+    {
+        btRaycastVehicle *m_vehicle = vehicleData_->Get();
+        btWheelInfo whInfo = m_vehicle->getWheelInfo(wheel);
+        return whInfo.m_maxSuspensionTravelCm;
+    }
+    void RaycastVehicle::SetWheelDirection(int wheel, Vector3 direction)
+    {
+        btVector3 dir(direction.x_, direction.y_, direction.z_);
+        btRaycastVehicle *m_vehicle = vehicleData_->Get();
+        btWheelInfo& whInfo = m_vehicle->getWheelInfo(wheel);
+        whInfo.m_wheelDirectionCS = dir;
+    }
+    Vector3 RaycastVehicle::GetWheelDirection(int wheel) const
+    {
+        btRaycastVehicle *m_vehicle = vehicleData_->Get();
+        btWheelInfo& whInfo = m_vehicle->getWheelInfo(wheel);
+        return ToVector3(whInfo.m_wheelDirectionCS);
+    }
+    void RaycastVehicle::SetWheelAxle(int wheel, Vector3 axle)
+    {
+        btVector3 ax(axle.x_, axle.y_, axle.z_);
+        btRaycastVehicle *m_vehicle = vehicleData_->Get();
+        btWheelInfo& whInfo = m_vehicle->getWheelInfo(wheel);
+        whInfo.m_wheelAxleCS = ax;
+    }
+    Vector3 RaycastVehicle::GetWheelAxle(int wheel) const
+    {
+        btRaycastVehicle *m_vehicle = vehicleData_->Get();
+        btWheelInfo& whInfo = m_vehicle->getWheelInfo(wheel);
+        return ToVector3(whInfo.m_wheelAxleCS);
+    }
+    void RaycastVehicle::SetWheelRestLength(int wheel, float length)
+    {
+        btRaycastVehicle *m_vehicle = vehicleData_->Get();
+        btWheelInfo& whInfo = m_vehicle->getWheelInfo(wheel);
+        whInfo.m_suspensionRestLength1 = length;
+    }
+    float RaycastVehicle::GetWheelRestLength(int wheel) const
+    {
+        btRaycastVehicle *m_vehicle = vehicleData_->Get();
+        btWheelInfo& whInfo = m_vehicle->getWheelInfo(wheel);
+        return whInfo.m_suspensionRestLength1;
+    }
+    void RaycastVehicle::SetWheelSkidInfo(int wheel, float factor)
+    {
+        btRaycastVehicle *m_vehicle = vehicleData_->Get();
+        btWheelInfo& whInfo = m_vehicle->getWheelInfo(wheel);
+        whInfo.m_skidInfo = factor;
+    }
+    float RaycastVehicle::GetWheelSkidInfo(int wheel) const
+    {
+        btRaycastVehicle *m_vehicle = vehicleData_->Get();
+        btWheelInfo& whInfo = m_vehicle->getWheelInfo(wheel);
+        return whInfo.m_skidInfo;
+    }
+    bool RaycastVehicle::IsFrontWheel(int wheel) const
+    {
+        btRaycastVehicle *m_vehicle = vehicleData_->Get();
+        btWheelInfo& whInfo = m_vehicle->getWheelInfo(wheel);
+        return whInfo.m_bIsFrontWheel;
+    }
+    bool RaycastVehicle::WheelIsGrounded(int wheel) const
+    {
+        btRaycastVehicle *m_vehicle = vehicleData_->Get();
+        btWheelInfo whInfo = m_vehicle->getWheelInfo(wheel);
+        return whInfo.m_raycastInfo.m_isInContact;
+    }
+    void RaycastVehicle::SetInAirRPM(float rpm)
+    {
+        inAirRPM_ = rpm;
+    }
+    float RaycastVehicle::GetInAirRPM() const
+    {
+        return inAirRPM_;
+    }
+    void RaycastVehicle::ResetWheels()
+    {
+        ResetSuspension();
+        for (int i = 0; i < GetNumWheels(); i++)
+        {
+            UpdateWheelTransform(i, true);
+            Vector3 origin = GetWheelPosition(i);
+            Node *wheelNode = GetWheelNode(i);
+            wheelNode->SetWorldPosition(origin);
+        }
+    }
+
+    void RegisterRaycastVehicleLibrary(Context* context)
+    {
+        RaycastVehicle::RegisterObject(context);
+    }
+
+} // namespace Urho3D

+ 180 - 0
Source/Urho3D/Physics/RaycastVehicle.h

@@ -0,0 +1,180 @@
+//
+// Copyright (c) 2008-2017 the Urho3D project.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+#include "../Scene/LogicComponent.h"
+#include "../Physics/PhysicsUtils.h"
+#include "../Physics/RigidBody.h"
+
+namespace Urho3D
+{
+
+    class RaycastVehicleData;
+
+    class URHO3D_API RaycastVehicle : public LogicComponent
+    {
+            URHO3D_OBJECT(RaycastVehicle, LogicComponent)
+        public:
+            /// Construct.
+            RaycastVehicle(Urho3D::Context* context);
+            ~RaycastVehicle();
+            /// Register object factory and attributes.
+            static void RegisterObject(Context* context);
+            /// Perform post-load after deserialization. Acquire the components from the scene nodes.
+            virtual void ApplyAttributes();
+            /// Add a wheel (all parameters are relative to RigidBody/node)
+            void AddWheel(Node *wheelNode, Vector3 wheelDirection, Vector3 wheelAxle, float restLength, float wheelRadius, bool frontWheel);
+            /// Reset all suspension
+            void ResetSuspension(void);
+            /// Update transform for particular wheel
+            void UpdateWheelTransform(int wheel, bool interpolated);
+            /// Get wheel position relative to RigidBody
+            Vector3 GetWheelPosition(int wheel);
+            /// Get wheel rotation relative to RigidBody
+            Quaternion GetWheelRotation(int wheel);
+            /// Get wheel connection point relative to RigidBody
+            Vector3 GetWheelConnectionPoint(int wheel) const;
+            /// Get number of attached wheels
+            int GetNumWheels() const;
+            /// Get Node of the wheel
+            Node *GetWheelNode(int wheel) const;
+            /// Set steering value of particular wheel
+            void SetSteeringValue(int wheel, float steeringValue);
+            /// Get steering value of particular wheel
+            float GetSteeringValue(int wheel) const;
+            /// Set suspension stiffness for particular wheel
+            void SetWheelSuspensionStiffness(int wheel, float stiffness);
+            /// Get suspension stiffness for particular wheel
+            float GetWheelSuspensionStiffness(int wheel) const;
+            /// Set wheel damping relaxation
+            void SetWheelDampingRelaxation(int wheel, float damping);
+            /// Get wheel damping relaxation
+            float GetWheelDampingRelaxation(int wheel) const;
+            /// Set wheel damping compression
+            void SetWheelDampingCompression(int wheel, float compression);
+            /// Get wheel damping compression
+            float GetWheelDampingCompression(int wheel) const;
+            /// Set wheel friction slip
+            void SetWheelFrictionSlip(int wheel, float slip);
+            /// Get wheel friction slip
+            float GetWheelFrictionSlip(int wheel) const;
+            /// Set wheel roll influence
+            void SetWheelRollInfluence(int wheel, float rollInfluence);
+            /// Get wheel roll influence
+            float GetWheelRollInfluence(int wheel) const;
+            /// Set engine force for the wheel
+            void SetEngineForce(int wheel, float force);
+            /// Get engine force for the wheel
+            float GetEngineForce(int wheel) const;
+            /// Set hand brake (wheel rotation blocking force)
+            void SetBrake(int wheel, float force);
+            /// Get hand break value
+            float GetBrake(int wheel) const;
+            /// Set wheel radius
+            void SetWheelRadius(int wheel, float wheelRadius);
+            /// Get wheel radius
+            float GetWheelRadius(int wheel) const;
+            /// Sets node initial positions
+            void ResetWheels();
+            /// Get wheel rest length
+            void SetWheelRestLength(int wheel, float length);
+            /// Get wheel rest length
+            float GetWheelRestLength(int wheel) const;
+            /// Set sliding factor 0 <= x <= 1
+            /// the less the value, more sliding.
+            void SetWheelSkidInfo(int wheel, float factor);
+            /// Sliding factor 0 <= x <= 1
+            float GetWheelSkidInfo(int wheel) const;
+            /// true if wheel touches ground (raycast hits something)
+            bool WheelIsGrounded(int wheel) const;
+            /// Set maximum suspension travel value
+            void SetMaxSuspensionTravel(int wheel, float maxSuspensionTravel);
+            /// Get maximum suspension travel value
+            float GetMaxSuspensionTravel(int wheel);
+            /// Set wheel direction vector
+            void SetWheelDirection(int wheel, Vector3 direction);
+            /// Get wheel direction vector
+            Vector3 GetWheelDirection(int wheel) const;
+            /// Set wheel axle vector
+            void SetWheelAxle(int wheel, Vector3 axle);
+            /// Get wheel axle vector
+            Vector3 GetWheelAxle(int wheel) const;
+            /// Get wheel slide speed
+            float GetWheelSideSlipSpeed(int wheel) const;
+            /// get side speed which is considered sliding
+            float GetMaxSideSlipSpeed() const;
+            /// set side speed which is considered sliding
+            void SetMaxSideSlipSpeed(float speed);
+            /// set cumuative skid info
+            void SetWheelSkidInfoCumulative(int wheel, float skid);
+            /// get cumuative skid info
+            float GetWheelSkidInfoCumulative(int wheel) const;
+            /// true if front wheel, otherwise false
+            bool IsFrontWheel(int wheel) const;
+            /// get wheel contact position
+            Vector3 GetContactPosition(int wheel) const;
+            // get contact normal
+            Vector3 GetContactNormal(int wheel) const;
+            /// Set revolution per minute value for when weel
+            /// doesn't touch ground
+            /// If set to 0 (or not set), calculated from engine force
+            /// (probably not what you want).
+            void SetInAirRPM(float rpm);
+            /// Get revolution per minute value for when weel
+            /// doesn't touch ground
+            float GetInAirRPM() const;
+            /// Init the vehicle component after creation
+            void Init();
+            void FixedUpdate(float timeStep);
+            void FixedPostUpdate(float timeStep);
+            void PostUpdate(float timeStep);
+            VariantVector GetWheelDataForSave() const;
+            void SetLoadedWheelData(const VariantVector& value);
+        private:
+            // If the RigidBody should be activated
+            bool activate_;
+            // Hull RigidBody
+            WeakPtr<RigidBody> hullBody_;
+            // Opaque Bullet data hidden from public
+            RaycastVehicleData *vehicleData_;
+            // Nodes of all wheels
+            Vector<Node*>           wheelNodes_;
+            // All wheels original rotations
+            // These are applied in addition to wheel
+            // rotations by btRaycastVehicle
+            Vector<Quaternion>      origRotation_;
+            // Revolutions per minute value for in-air motor wheels
+            // It is per vehicle. I know, I will fix this later.
+            // FIXME: set this one per wheel
+            float                   inAirRPM_;
+            // Per-wheel extra settings
+            Vector<float> skidInfoCumulative_;
+            // wheel side movement speed
+            Vector<float> wheelSideSlipSpeed_;
+            // side slip speed threshold
+            float maxSideSlipSpeed_;
+            // Loaded data temporarily wait here for ApplyAttributes
+            // to come pick them up
+            VariantVector loadedWheelData_;
+    };
+
+    void RegisterRaycastVehicleLibrary(Context* context);
+}

+ 440 - 0
bin/Data/LuaScripts/46_RaycastVehicleDemo.lua

@@ -0,0 +1,440 @@
+-- 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
+--     - Saving and loading the variables of a script object, including node & component references
+
+require "LuaScripts/Utilities/Sample"
+
+local CTRL_FORWARD = 1
+local CTRL_BACK = 2
+local CTRL_LEFT = 4
+local CTRL_RIGHT = 8
+local CTRL_BRAKE = 16
+
+local CAMERA_DISTANCE = 10.0
+local YAW_SENSITIVITY = 0.1
+local ENGINE_FORCE = 2500.0
+local DOWN_FORCE = 100.0
+local MAX_WHEEL_ANGLE = 22.5
+local CHASSIS_WIDTH = 2.6
+
+local vehicleNode = nil
+
+function Start()
+    -- Execute the common startup for samples
+    SampleStart()
+
+    -- Create static scene content
+    CreateScene()
+
+    -- Create the controllable vehicle
+    CreateVehicle()
+
+    -- Create the UI content
+    CreateInstructions()
+
+    -- Set the mouse mode to use in the sample
+    SampleInitMouseMode(MM_RELATIVE)
+
+    -- Subscribe to necessary events
+    SubscribeToEvents()
+end
+
+function 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()
+    local camera = cameraNode:CreateComponent("Camera")
+    camera.farClip = 500.0
+
+    renderer:SetViewport(0, Viewport:new(scene_, camera))
+
+    -- Create static scene content. First create a zone for ambient lighting and fog control
+    local zoneNode = scene_:CreateChild("Zone")
+    local zone = zoneNode:CreateComponent("Zone")
+    zone.ambientColor = Color(0.15, 0.15, 0.15)
+    zone.fogColor = Color(0.5, 0.5, 0.7)
+    zone.fogStart = 300.0
+    zone.fogEnd = 500.0
+    zone.boundingBox = BoundingBox(-2000.0, 2000.0)
+
+    -- Create a directional light to the world. Enable cascaded shadows on it
+    local lightNode = scene_:CreateChild("DirectionalLight")
+    lightNode.direction = Vector3(0.3, -0.5, 0.425)
+    local light = lightNode:CreateComponent("Light")
+    light.lightType = LIGHT_DIRECTIONAL
+    light.castShadows = true
+    light.shadowBias = BiasParameters(0.00025, 0.5)
+    light.shadowCascade = CascadeParameters(10.0, 50.0, 200.0, 0.0, 0.8)
+    light.specularIntensity = 0.5
+
+    -- Create heightmap terrain with collision
+    local terrainNode = scene_:CreateChild("Terrain")
+    terrainNode.position = Vector3(0.0, 0.0, 0.0)
+    local terrain = terrainNode:CreateComponent("Terrain")
+    terrain.patchSize = 64
+    terrain.spacing = Vector3(2.0, 0.1, 2.0) -- 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
+
+    local body = terrainNode:CreateComponent("RigidBody")
+    body.collisionLayer = 2 -- Use layer bitmask 2 for static geometry
+    local shape = terrainNode:CreateComponent("CollisionShape")
+    shape:SetTerrain()
+
+    -- Create 1000 mushrooms in the terrain. Always face outward along the terrain normal
+    local NUM_MUSHROOMS = 1000
+    for i = 1, NUM_MUSHROOMS do
+        local objectNode = scene_:CreateChild("Mushroom")
+        local position = Vector3(Random(2000.0) - 1000.0, 0.0, Random(2000.0) - 1000.0)
+        position.y = terrain:GetHeight(position) - 0.1
+        objectNode.position = position
+        -- Create a rotation quaternion from up vector to terrain normal
+        objectNode.rotation = Quaternion(Vector3(0.0, 1.0, 0.0), terrain:GetNormal(position))
+        objectNode:SetScale(3.0)
+        local object = objectNode:CreateComponent("StaticModel")
+        object.model = cache:GetResource("Model", "Models/Mushroom.mdl")
+        object.material = cache:GetResource("Material", "Materials/Mushroom.xml")
+        object.castShadows = true
+
+        local body = objectNode:CreateComponent("RigidBody")
+        body.collisionLayer = 2
+        local shape = objectNode:CreateComponent("CollisionShape")
+        shape:SetTriangleMesh(object.model, 0)
+    end
+end
+
+function CreateVehicle()
+    vehicleNode = scene_:CreateChild("Vehicle")
+    vehicleNode.position = Vector3(0.0, 5.0, 0.0)
+
+    -- Create the vehicle logic script object
+    local vehicle = vehicleNode:CreateScriptObject("Vehicle")
+    -- Create the rendering and physics components
+    vehicle:Init()
+    local hullBody = vehicleNode:GetComponent("RigidBody")
+    hullBody.mass = 800.0
+    hullBody.linearDamping = 0.2
+    hullBody.angularDamping = 0.5
+    hullBody.collisionLayer = 1
+end
+
+function CreateInstructions()
+    -- Construct new Text object, set string to display and font to use
+    local instructionText = ui.root:CreateChild("Text")
+    instructionText.text = "Use WASD keys to drive, mouse/touch to rotate camera\n"..
+        "F5 to save scene, F7 to load"
+    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)
+end
+
+function 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")
+
+    -- Unsubscribe the SceneUpdate event from base class as the camera node is being controlled in HandlePostUpdate() in this sample
+    UnsubscribeFromEvent("SceneUpdate")
+end
+
+function HandleUpdate(eventType, eventData)
+    if vehicleNode == nil then
+        return
+    end
+
+    local vehicle = vehicleNode:GetScriptObject()
+    if vehicle == nil then
+        return
+    end
+
+    -- Get movement controls and assign them to the vehicle component. If UI has a focused element, clear controls
+    if ui.focusElement == nil then
+        vehicle.controls:Set(CTRL_FORWARD, input:GetKeyDown(KEY_W))
+        vehicle.controls:Set(CTRL_BACK, input:GetKeyDown(KEY_S))
+        vehicle.controls:Set(CTRL_LEFT, input:GetKeyDown(KEY_A))
+        vehicle.controls:Set(CTRL_RIGHT, input:GetKeyDown(KEY_D))
+        vehicle.controls:Set(CTRL_BRAKE, input:GetKeyDown(KEY_F))
+
+        -- Add yaw & pitch from the mouse motion or touch input. Used only for the camera, does not affect motion
+        if touchEnabled then
+            for i=0, input.numTouches - 1 do
+                local state = input:GetTouch(i)
+                if not state.touchedElement then -- Touch on empty space
+                    local camera = cameraNode:GetComponent("Camera")
+                    if not camera then return end
+
+                    vehicle.controls.yaw = vehicle.controls.yaw + TOUCH_SENSITIVITY * camera.fov / graphics.height * state.delta.x
+                    vehicle.controls.pitch = vehicle.controls.pitch + TOUCH_SENSITIVITY * camera.fov / graphics.height * state.delta.y
+                end
+            end
+        else
+            vehicle.controls.yaw = vehicle.controls.yaw + input.mouseMoveX * YAW_SENSITIVITY
+            vehicle.controls.pitch = vehicle.controls.pitch + input.mouseMoveY * YAW_SENSITIVITY
+        end
+        -- Limit pitch
+        vehicle.controls.pitch = Clamp(vehicle.controls.pitch, 0.0, 80.0)
+
+        -- Check for loading / saving the scene
+        if input:GetKeyPress(KEY_F5) then
+            scene_:SaveXML(fileSystem:GetProgramDir() .. "Data/Scenes/VehicleDemo.xml")
+        end
+        if input:GetKeyPress(KEY_F7) then
+            scene_:LoadXML(fileSystem:GetProgramDir() .. "Data/Scenes/VehicleDemo.xml")
+            vehicleNode = scene_:GetChild("Vehicle", true)
+            vehicleNode:GetScriptObject():PostInit()
+        end
+    else
+        vehicle.controls:Set(CTRL_FORWARD + CTRL_BACK + CTRL_LEFT + CTRL_RIGHT, false)
+    end
+end
+
+function HandlePostUpdate(eventType, eventData)
+    if vehicleNode == nil then
+        return
+    end
+
+    local vehicle = vehicleNode:GetScriptObject()
+    if vehicle == nil then
+        return
+    end
+
+    -- Physics update has completed. Position camera behind vehicle
+    local dir = Quaternion(vehicleNode.rotation:YawAngle(), Vector3(0.0, 1.0, 0.0))
+    dir = dir * Quaternion(vehicle.controls.yaw, Vector3(0.0, 1.0, 0.0))
+    dir = dir * Quaternion(vehicle.controls.pitch, Vector3(1.0, 0.0, 0.0))
+
+    local cameraTargetPos = vehicleNode.position - dir * Vector3(0.0, 0.0, CAMERA_DISTANCE)
+    local cameraStartPos = vehicleNode.position
+    -- Raycast camera against static objects (physics collision mask 2)
+    -- and move it closer to the vehicle if something in between
+    local cameraRay = Ray(cameraStartPos, (cameraTargetPos - cameraStartPos):Normalized())
+    local cameraRayLength = (cameraTargetPos - cameraStartPos):Length();
+    local physicsWorld = scene_:GetComponent("PhysicsWorld")
+    local result = physicsWorld:RaycastSingle(cameraRay, cameraRayLength, 2)
+    if result.body ~= nil then
+        cameraTargetPos = cameraStartPos + cameraRay.direction * (result.distance - 0.5)
+    end
+    cameraNode.position = cameraTargetPos
+    cameraNode.rotation = dir
+end
+
+-- Vehicle script object class
+--
+-- When saving, the node and component handles are automatically converted into nodeID or componentID attributes
+-- and are acquired from the scene when loading. The steering member variable will likewise be saved automatically.
+-- The Controls object can not be automatically saved, so handle it manually in the Load() and Save() methods
+
+Vehicle = ScriptObject()
+
+function Vehicle:Start()
+    -- Current left/right steering amount (-1 to 1.)
+    self.steering = 0.0
+    -- Vehicle controls.
+    self.controls = Controls()
+    self.suspensionRestLength = 0.6
+    self.suspensionStiffness = 14.0
+    self.suspensionDamping = 2.0
+    self.suspensionCompression = 4.0
+    self.wheelFriction = 1000.0
+    self.rollInfluence = 0.12
+    self.maxEngineForce = ENGINE_FORCE
+    self.wheelWidth = 0.4
+    self.wheelRadius = 0.5
+    self.brakingForce = 50.0
+    self.connectionPoints = {}
+    self.particleEmitterNodeList = {}
+    self.prevVelocity = Vector3()
+end
+
+function Vehicle:Load(deserializer)
+    self.controls.yaw = deserializer:ReadFloat()
+    self.controls.pitch = deserializer:ReadFloat()
+end
+
+function Vehicle:Save(serializer)
+    serializer:WriteFloat(self.controls.yaw)
+    serializer:WriteFloat(self.controls.pitch)
+end
+
+function Vehicle:Init()
+    -- This function is called only from the main program when initially creating the vehicle, not on scene load
+    local node = self.node
+    local hullObject = node:CreateComponent("StaticModel")
+    self.hullBody = node:CreateComponent("RigidBody")
+    local hullShape = node:CreateComponent("CollisionShape")
+
+    node.scale = Vector3(2.3, 1.0, 4.0)
+    hullObject.model = cache:GetResource("Model", "Models/Box.mdl")
+    hullObject.material = cache:GetResource("Material", "Materials/Stone.xml")
+    hullObject.castShadows = true
+    hullShape:SetBox(Vector3(1.0, 1.0, 1.0))
+
+    self.hullBody.mass = 800.0
+    self.hullBody.linearDamping = 0.2 -- Some air resistance
+    self.hullBody.angularDamping = 0.5
+    self.hullBody.collisionLayer = 1
+    local raycastVehicle = node:CreateComponent("RaycastVehicle")
+    raycastVehicle:Init()
+    local connectionHeight = -0.4
+    local isFrontWheel = true
+    local wheelDirection = Vector3(0, -1, 0)
+    local wheelAxle = Vector3(-1, 0, 0)
+    local wheelX = CHASSIS_WIDTH / 2.0 - self.wheelWidth
+    -- Front left
+    table.insert(self.connectionPoints, Vector3(-wheelX, connectionHeight, 2.5 - self.wheelRadius * 2.0))
+    -- Front right
+    table.insert(self.connectionPoints, Vector3(wheelX, connectionHeight, 2.5 - self.wheelRadius * 2.0))
+    -- Back left
+    table.insert(self.connectionPoints, Vector3(-wheelX, connectionHeight, -2.5 + self.wheelRadius * 2.0))
+    -- Back right
+    table.insert(self.connectionPoints, Vector3(wheelX, connectionHeight, -2.5 + self.wheelRadius * 2.0))
+
+    local LtBrown = Color(0.972, 0.780, 0.412)
+    for i = 1, #self.connectionPoints do
+        local wheelNode = scene_:CreateChild()
+	local connectionPoint = self.connectionPoints[i]
+	-- Front wheels are at front (z > 0)
+	-- Back wheels are at z < 0
+	-- Setting rotation according to wheel position
+	local isFrontWheel = connectionPoint.z > 0.0
+	if connectionPoint.x >= 0.0 then
+            wheelNode.rotation = Quaternion(0.0, 0.0, -90.0)
+        else
+            wheelNode.rotation = Quaternion(0.0, 0.0, 90.0)
+	end
+	wheelNode.worldPosition = node.worldPosition + node.worldRotation * connectionPoint
+	wheelNode.scale = Vector3(1.0, 0.65, 1.0)
+	raycastVehicle:AddWheel(wheelNode, wheelDirection, wheelAxle, self.suspensionRestLength, self.wheelRadius, isFrontWheel)
+	raycastVehicle:SetWheelSuspensionStiffness(i - 1, self.suspensionStiffness)
+	raycastVehicle:SetWheelDampingRelaxation(i - 1, self.suspensionDamping)
+	raycastVehicle:SetWheelDampingCompression(i - 1, self.suspensionCompression)
+	raycastVehicle:SetWheelRollInfluence(i - 1, self.rollInfluence)
+	local pWheel = wheelNode:CreateComponent("StaticModel")
+	pWheel.model = cache:GetResource("Model", "Models/Cylinder.mdl")
+	pWheel.material = cache:GetResource("Material", "Materials/Stone.xml")
+	pWheel.castShadows = true
+    end
+
+    self:PostInit()
+end
+
+function Vehicle:CreateEmitter(place)
+    local emitter = scene_:CreateChild()
+    local node = self.node
+    emitter.worldPosition = node.worldPosition + node.worldRotation * place + Vector3(0, -self.wheelRadius, 0)
+    local particleEmitter = emitter:CreateComponent("ParticleEmitter")
+    particleEmitter.effect = cache:GetResource("ParticleEffect", "Particle/Dust.xml")
+    particleEmitter.emitting = false
+    emitter.temporary = true
+    table.insert(self.particleEmitterNodeList, emitter)
+end
+
+function Vehicle:CreateEmitters()
+    self.particleEmitterNodeList = {}
+    local node = self.node
+    local raycastVehicle = node:GetComponent("RaycastVehicle")
+    for id = 0, raycastVehicle:GetNumWheels() do
+        local connectionPoint = raycastVehicle:GetWheelConnectionPoint(id)
+	self:CreateEmitter(connectionPoint)
+    end
+end
+
+function Vehicle:PostInit()
+    local node = self.node
+    local raycastVehicle = node:GetComponent("RaycastVehicle")
+    self.hullBody = node:GetComponent("RigidBody")
+    self:CreateEmitters();
+    raycastVehicle:ResetWheels();
+end
+
+function Vehicle:FixedUpdate(timeStep)
+    local node = self.node
+    local newSteering = 0.0
+    local accelerator = 0.0
+    local brake = false
+
+    if self.controls:IsDown(CTRL_LEFT) then
+        newSteering = -1.0
+    end
+    if self.controls:IsDown(CTRL_RIGHT) then
+        newSteering = 1.0
+    end
+    if self.controls:IsDown(CTRL_FORWARD) then
+        accelerator = 1.0
+    end
+    if self.controls:IsDown(CTRL_BACK) then
+        accelerator = -0.5
+    end
+
+    if self.controls:IsDown(CTRL_BRAKE) then
+       brake = true
+    end
+
+    -- When steering, wake up the wheel rigidbodies so that their orientation is updated
+    if newSteering ~= 0.0 then
+        self.steering = self.steering * 0.95 + newSteering * 0.05
+    else
+        self.steering = self.steering * 0.8 + newSteering * 0.2
+    end
+
+    local steeringRot = Quaternion(0.0, self.steering * MAX_WHEEL_ANGLE, 0.0)
+    local raycastVehicle = node:GetComponent("RaycastVehicle")
+    raycastVehicle:SetSteeringValue(0, self.steering)
+    raycastVehicle:SetSteeringValue(1, self.steering)
+    raycastVehicle:SetEngineForce(2, self.maxEngineForce * accelerator)
+    raycastVehicle:SetEngineForce(3, self.maxEngineForce * accelerator)
+    for i = 0, raycastVehicle:GetNumWheels() - 1 do
+	    if brake then
+		    raycastVehicle:SetBrake(i, self.brakingForce)
+	    else
+		    raycastVehicle:SetBrake(i, 0.0)
+	    end
+    end
+
+    -- Apply downforce proportional to velocity
+    local localVelocity = self.hullBody.rotation:Inverse() * self.hullBody.linearVelocity
+    self.hullBody:ApplyForce(self.hullBody.rotation * Vector3(0.0, -1.0, 0.0) * Abs(localVelocity.z) * DOWN_FORCE)
+end
+
+function Vehicle:PostUpdate(timeStep)
+    local node = self.node
+    local raycastVehicle = node:GetComponent("RaycastVehicle")
+    if #self.particleEmitterNodeList == 0 then
+	    return
+    end
+    local velocity = self.hullBody.linearVelocity
+    local accel = (velocity - self.prevVelocity) / timeStep
+    local planeAccel = Vector3(accel.x, 0.0, accel.z):Length()
+    for i = 0, raycastVehicle:GetNumWheels()  - 1 do
+	    local emitter = self.particleEmitterNodeList[i + 1]
+	    local particleEmitter = emitter:GetComponent("ParticleEmitter")
+	    if raycastVehicle:WheelIsGrounded(i) and (raycastVehicle:GetWheelSkidInfoCumulative(i) < 0.9 or raycastVehicle:GetBrake(i) > 2.0 or planeAccel > 15.0) then
+                emitter.worldPosition = raycastVehicle:GetContactPosition(i)
+		if not particleEmitter.emitting then
+			particleEmitter.emitting = true
+		end
+	    else if particleEmitter.emitting then
+		    particleEmitter.emitting = false
+	        end
+	    end
+    end
+    self.prevVelocity = velocity
+end

+ 27 - 0
bin/Data/Particle/Dust.xml

@@ -0,0 +1,27 @@
+<?xml version="1.0"?>
+<particleeffect>
+	<material name="Materials/Smoke.xml" />
+	<numparticles value="200" />
+	<updateinvisible enable="true" />
+	<relative enable="false" />
+	<scaled enable="true" />
+	<sorted enable="true" />
+	<animlodbias value="0" />
+	<emittertype value="Sphere" />
+	<emittersize value="0 0 0" />
+	<direction min="-0.2 0.8 -0.2" max="0.2 1.1 0.2" />
+	<constantforce value="0 -0.4 0" />
+	<dampingforce value="0.1" />
+	<activetime value="0" />
+	<inactivetime value="0" />
+	<emissionrate min="12" max="14" />
+	<particlesize min="0.6 0.6" max="0.6 0.6" />
+	<timetolive min="4" max="4" />
+	<velocity min="0.2" max="0.7" />
+	<rotation min="0" max="0" />
+	<rotationspeed min="-10" max="10" />
+	<sizedelta add="0" mul="1.6" />
+	<colorfade color="0.2 0.2 0.2 0" time="0" />
+	<colorfade color="0.97 0.78 0.5 0.4" time="0" />
+	<colorfade color="0.95 0.82 0.6 0" time="4" />
+</particleeffect>

+ 534 - 0
bin/Data/Scripts/46_RaycastVehicleDemo.as

@@ -0,0 +1,534 @@
+// 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
+//     - Saving and loading the variables of a script object, including node & component references
+
+#include "Scripts/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_BRAKE = 16;
+
+const float CAMERA_DISTANCE = 10.0f;
+const float YAW_SENSITIVITY = 0.1f;
+const float ENGINE_FORCE = 2500.0f;
+const float DOWN_FORCE = 100.0f;
+const float MAX_WHEEL_ANGLE = 22.5f;
+const float CHASSIS_WIDTH = 2.6f;
+
+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();
+    // Set the mouse mode to use in the sample
+    SampleInitMouseMode(MM_RELATIVE);
+    // 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.00025f, 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@ mushroomBody =
+            objectNode.CreateComponent("RigidBody");
+        mushroomBody.collisionLayer = 2;
+        CollisionShape@ mushroomShape =
+            objectNode.CreateComponent("CollisionShape");
+        mushroomShape.SetTriangleMesh(object.model, 0);
+    }
+}
+
+
+void
+CreateVehicle()
+{
+    vehicleNode = scene_.CreateChild("Vehicle");
+    vehicleNode.position = Vector3(0.0f, 5.0f, 0.0f);
+    // First createing player-controlled vehicle
+    // Create the vehicle logic script object
+    Vehicle@ vehicle =
+        cast < Vehicle >
+        (vehicleNode.CreateScriptObject(scriptFile, "Vehicle"));
+    // Initialize vehicle component
+    vehicle.Init();
+    vehicleNode.AddTag("vehicle");
+    // Set RigidBody physics parameters
+    // (The RigidBody was created by vehicle.Init()
+    RigidBody@ hullBody = vehicleNode.GetComponent("RigidBody");
+    hullBody.mass = 800.0f;
+    hullBody.linearDamping = 0.2f;     // Some air resistance
+    hullBody.angularDamping = 0.5f;
+    hullBody.collisionLayer = 1;
+}
+
+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, F to brake, mouse/touch to rotate camera\n"
+        "F5 to save scene, F7 to load";
+    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 vehicle controls before physics simulation
+    SubscribeToEvent("Update", "HandleUpdate");
+    // Subscribe to PostUpdate event for updating the camera position after physics simulation
+    SubscribeToEvent("PostUpdate", "HandlePostUpdate");
+    // Unsubscribe the SceneUpdate event from base class as the camera node is being controlled in HandlePostUpdate() in this sample
+    UnsubscribeFromEvent("SceneUpdate");
+}
+
+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[KEY_W]);
+        vehicle.controls.Set(CTRL_BACK, input.keyDown[KEY_S]);
+        vehicle.controls.Set(CTRL_LEFT, input.keyDown[KEY_A]);
+        vehicle.controls.Set(CTRL_RIGHT, input.keyDown[KEY_D]);
+        vehicle.controls.Set(CTRL_BRAKE, input.keyDown[KEY_F]);
+        // Add yaw & pitch from the mouse motion. Used only for the camera, does not affect motion
+        if (touchEnabled)
+        {
+            for (uint i = 0; i < input.numTouches; ++i)
+            {
+                TouchState@ state = input.touches[i];
+                if (state.touchedElement is null)        // Touch on empty space
+                {
+                    Camera@ camera =
+                        cameraNode.GetComponent("Camera");
+                    if (camera is null)
+                        return;
+                    vehicle.controls.yaw +=
+                        TOUCH_SENSITIVITY * camera.fov /
+                        graphics.height * state.delta.x;
+                    vehicle.controls.pitch +=
+                        TOUCH_SENSITIVITY * camera.fov /
+                        graphics.height * state.delta.y;
+                }
+            }
+        }
+        else
+        {
+            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);
+        // Check for loading / saving the scene
+        if (input.keyPress[KEY_F5])
+        {
+            File saveFile(fileSystem.programDir +
+                          "Data/Scenes/RaycastScriptVehicleDemo.xml",
+                          FILE_WRITE);
+            scene_.SaveXML(saveFile);
+        }
+        if (input.keyPress[KEY_F7])
+        {
+            File loadFile(fileSystem.programDir +
+                          "Data/Scenes/RaycastScriptVehicleDemo.xml",
+                          FILE_READ);
+            scene_.LoadXML(loadFile);
+            // After loading we have to reacquire the vehicle scene node, as it has been recreated
+            // Simply find by name as there's only one of them
+            Array < Node@ >@vehicles =
+                scene_.GetChildrenWithTag("vehicle");
+            for (int i = 0; i < vehicles.length; i++)
+            {
+                Vehicle@ vehicleData =
+                    cast < Vehicle > (vehicles[i].scriptObject);
+                vehicleData.CreateEmitters();
+            }
+            vehicleNode = scene_.GetChild("Vehicle", true);
+        }
+    }
+    else
+        vehicle.
+        controls.Set(CTRL_FORWARD | CTRL_BACK | CTRL_LEFT | CTRL_RIGHT |
+                     CTRL_BRAKE, 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
+//
+// When saving, the node and component handles are automatically converted into nodeID or componentID attributes
+// and are acquired from the scene when loading. The steering member variable will likewise be saved automatically.
+// The Controls object can not be automatically saved, so handle it manually in the Load() and Save() methods
+
+class Vehicle:ScriptObject
+{
+
+    RigidBody@ hullBody;
+
+
+    // Current left/right steering amount (-1 to 1.)
+    float steering = 0.0f;
+
+    // Vehicle controls.
+    Controls controls;
+
+    float m_fsuspensionRestLength = 0.6f;
+
+    float m_fsuspensionStiffness = 14.0f;
+
+    float m_fsuspensionDamping = 2.0f;
+
+    float m_fsuspensionCompression = 4.0f;
+
+    float m_fwheelFriction = 1000.0f;
+
+    float m_frollInfluence = 0.12f;
+
+    float maxEngineForce = ENGINE_FORCE;
+
+    float wheelWidth = 0.4f;
+
+    float wheelRadius = 0.5f;
+
+    float brakingForce = 50.0f;
+
+    Array < Vector3 > connectionPoints;
+
+    Array < Node@ >particleEmitterNodeList;
+
+    protected Vector3 prevVelocity;
+
+
+    void Load(Deserializer& deserializer)
+    {
+        controls.yaw = deserializer.ReadFloat();
+        controls.pitch = deserializer.ReadFloat();
+    }
+
+    void Save(Serializer& serializer)
+    {
+        serializer.WriteFloat(controls.yaw);
+        serializer.WriteFloat(controls.pitch);
+    }
+
+    void Init()
+    {
+        // This function is called only from the main program when initially creating the vehicle, not on scene load
+        StaticModel@ hullObject = node.CreateComponent("StaticModel");
+        hullBody = node.CreateComponent("RigidBody");
+        CollisionShape@ hullShape = node.CreateComponent("CollisionShape");
+        node.scale = Vector3(2.3f, 1.0f, 4.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 = 800.0f;
+        hullBody.linearDamping = 0.2f; // Some air resistance
+        hullBody.angularDamping = 0.5f;
+        hullBody.collisionLayer = 1;
+        RaycastVehicle@ raycastVehicle =
+            node.CreateComponent("RaycastVehicle");
+        raycastVehicle.Init();
+        connectionPoints.Reserve(4);
+        float connectionHeight = -0.4f;      //1.2f;
+        bool isFrontWheel = true;
+        Vector3 wheelDirection(0, -1, 0);
+        Vector3 wheelAxle(-1, 0, 0);
+        float wheelX = CHASSIS_WIDTH / 2.0 - wheelWidth;
+        // Front left
+        connectionPoints.Push(Vector3
+                              (-wheelX, connectionHeight,
+                               2.5f - wheelRadius * 2.0f));
+        // Front right
+        connectionPoints.Push(Vector3
+                              (wheelX, connectionHeight,
+                               2.5f - wheelRadius * 2.0f));
+        // Back left
+        connectionPoints.Push(Vector3
+                              (-wheelX, connectionHeight,
+                               -2.5f + wheelRadius * 2.0f));
+        // Back right
+        connectionPoints.Push(Vector3
+                              (wheelX, connectionHeight,
+                               -2.5f + wheelRadius * 2.0f));
+        const Color LtBrown(0.972f, 0.780f, 0.412f);
+        for (int id = 0; id < connectionPoints.length; id++)
+        {
+            Node@ wheelNode = scene_.CreateChild();
+            Vector3 connectionPoint = connectionPoints[id];
+            // Front wheels are at front (z > 0)
+            // back wheels are at z < 0
+            // Setting rotation according to wheel position
+            bool isFrontWheel = connectionPoints[id].z > 0.0f;
+            wheelNode.rotation =
+                (connectionPoint.x >=
+                 0.0 ? Quaternion(0.0f, 0.0f, -90.0f) : Quaternion(0.0f,
+                         0.0f,
+                         90.0f));
+            wheelNode.worldPosition =
+                (node.worldPosition +
+                 node.worldRotation * connectionPoints[id]);
+            wheelNode.scale = Vector3(1.0f, 0.65f, 1.0f);
+            raycastVehicle.AddWheel(wheelNode, wheelDirection, wheelAxle,
+                                    m_fsuspensionRestLength, wheelRadius,
+                                    isFrontWheel);
+            raycastVehicle.SetWheelSuspensionStiffness(id,
+                    m_fsuspensionStiffness);
+            raycastVehicle.SetWheelDampingRelaxation(id,
+                    m_fsuspensionDamping);
+            raycastVehicle.SetWheelDampingCompression(id,
+                    m_fsuspensionCompression);
+            raycastVehicle.SetWheelFrictionSlip(id, m_fwheelFriction);
+            raycastVehicle.SetWheelRollInfluence(id, m_frollInfluence);
+            StaticModel@ pWheel =
+                wheelNode.CreateComponent("StaticModel");
+            pWheel.model =
+                cache.GetResource("Model", "Models/Cylinder.mdl");
+            pWheel.material =
+                cache.GetResource("Material", "Materials/Stone.xml");
+            pWheel.castShadows = true;
+        }
+        CreateEmitters();
+        raycastVehicle.ResetWheels();
+    }
+
+    void CreateEmitter(Vector3 place)
+    {
+        Node@ emitter = scene_.CreateChild();
+        emitter.worldPosition =
+            node.worldPosition + node.worldRotation * place + Vector3(0,
+                    -wheelRadius,
+                    0);
+        ParticleEmitter@ particleEmitter =
+            emitter.CreateComponent("ParticleEmitter");
+        particleEmitter.effect =
+            cache.GetResource("ParticleEffect", "Particle/Dust.xml");
+        particleEmitter.emitting = false;
+        particleEmitterNodeList.Push(emitter);
+        emitter.temporary = true;
+    }
+
+    void CreateEmitters()
+    {
+        particleEmitterNodeList.Clear();
+        RaycastVehicle@ raycastVehicle =
+            node.GetComponent("RaycastVehicle");
+        for (int id = 0; id < raycastVehicle.numWheels; id++)
+        {
+            Vector3 connectionPoint =
+                raycastVehicle.GetWheelConnectionPoint(id);
+            CreateEmitter(connectionPoint);
+        }
+    }
+
+    void FixedUpdate(float timeStep)
+    {
+        float newSteering = 0.0f;
+        float accelerator = 0.0f;
+        bool brake = false;
+        RaycastVehicle@ raycastVehicle =
+            node.GetComponent("RaycastVehicle");
+        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;
+        if (controls.IsDown(CTRL_BRAKE))
+            brake = true;
+        // When steering, wake up the wheel rigidbodies so that their orientation is updated
+        if (newSteering != 0.0f)
+        {
+            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);
+        raycastVehicle.SetSteeringValue(0, steering);
+        raycastVehicle.SetSteeringValue(1, steering);
+        raycastVehicle.SetEngineForce(2, maxEngineForce * accelerator);
+        raycastVehicle.SetEngineForce(3, maxEngineForce * accelerator);
+        for (int i = 0; i < raycastVehicle.numWheels; i++)
+            if (brake)
+            {
+                raycastVehicle.SetBrake(i, brakingForce);
+            }
+            else
+            {
+                raycastVehicle.SetBrake(i, 0.0f);
+            }
+        // 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);
+    }
+
+    void PostUpdate(float timeStep)
+    {
+        if (particleEmitterNodeList.length == 0)
+            return;
+        RaycastVehicle@ raycastVehicle =
+            node.GetComponent("RaycastVehicle");
+        RigidBody@ vehicleBody = node.GetComponent("RigidBody");
+        Vector3 velocity = hullBody.linearVelocity;
+        Vector3 accel = (velocity - prevVelocity) / timeStep;
+        float planeAccel = Vector3(accel.x, 0.0f, accel.z).length;
+        for (int i = 0; i < raycastVehicle.numWheels; i++)
+        {
+            Node@ emitter = particleEmitterNodeList[i];
+            ParticleEmitter@ particleEmitter =
+                emitter.GetComponent("ParticleEmitter");
+            if (raycastVehicle.WheelIsGrounded(i)
+                    && (
+                        raycastVehicle.GetWheelSkidInfoCumulative(i) < 0.9f
+                        ||
+                        raycastVehicle.GetBrake(i) > 2.0f
+                        ||
+                        planeAccel > 15.0f))
+            {
+                emitter.worldPosition =
+                    raycastVehicle.GetContactPosition(i);
+                if (!particleEmitter.emitting)
+                    particleEmitter.emitting = true;
+            }
+            else if (particleEmitter.emitting)
+                particleEmitter.emitting = false;
+        }
+        prevVelocity = velocity;
+    }
+}
+
+
+// Create XML patch instructions for screen joystick layout specific to this sample app
+String patchInstructions = "";
+