0631979496
[ci skip]
441 lines
17 KiB
Lua
441 lines
17 KiB
Lua
-- 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
|