| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440 |
- -- 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
|