// 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(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@ vehicles = scene_.GetChildrenWithTag("vehicle"); for (int i = 0; i < vehicles.length; i++) { Vehicle@ vehicleData = cast(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 suspensionRestLength = 0.6f; float suspensionStiffness = 14.0f; float suspensionDamping = 2.0f; float suspensionCompression = 4.0f; float wheelFriction = 1000.0f; float rollInfluence = 0.12f; float maxEngineForce = ENGINE_FORCE; float wheelWidth = 0.4f; float wheelRadius = 0.5f; float brakingForce = 50.0f; Array connectionPoints; Array 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, suspensionRestLength, wheelRadius, isFrontWheel); raycastVehicle.SetWheelSuspensionStiffness(id, suspensionStiffness); raycastVehicle.SetWheelDampingRelaxation(id, suspensionDamping); raycastVehicle.SetWheelDampingCompression(id, suspensionCompression); raycastVehicle.SetWheelFrictionSlip(id, wheelFriction); raycastVehicle.SetWheelRollInfluence(id, rollInfluence); 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 = "";