46_RaycastVehicleDemo.lua 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. -- Vehicle example.
  2. -- This sample demonstrates:
  3. -- - Creating a heightmap terrain with collision
  4. -- - Constructing a physical vehicle with rigid bodies for the hull and the wheels, joined with constraints
  5. -- - Saving and loading the variables of a script object, including node & component references
  6. require "LuaScripts/Utilities/Sample"
  7. local CTRL_FORWARD = 1
  8. local CTRL_BACK = 2
  9. local CTRL_LEFT = 4
  10. local CTRL_RIGHT = 8
  11. local CTRL_BRAKE = 16
  12. local CAMERA_DISTANCE = 10.0
  13. local YAW_SENSITIVITY = 0.1
  14. local ENGINE_FORCE = 2500.0
  15. local DOWN_FORCE = 100.0
  16. local MAX_WHEEL_ANGLE = 22.5
  17. local CHASSIS_WIDTH = 2.6
  18. local vehicleNode = nil
  19. function Start()
  20. -- Execute the common startup for samples
  21. SampleStart()
  22. -- Create static scene content
  23. CreateScene()
  24. -- Create the controllable vehicle
  25. CreateVehicle()
  26. -- Create the UI content
  27. CreateInstructions()
  28. -- Set the mouse mode to use in the sample
  29. SampleInitMouseMode(MM_RELATIVE)
  30. -- Subscribe to necessary events
  31. SubscribeToEvents()
  32. end
  33. function CreateScene()
  34. scene_ = Scene()
  35. -- Create scene subsystem components
  36. scene_:CreateComponent("Octree")
  37. scene_:CreateComponent("PhysicsWorld")
  38. -- Create camera and define viewport. Camera does not necessarily have to belong to the scene
  39. cameraNode = Node()
  40. local camera = cameraNode:CreateComponent("Camera")
  41. camera.farClip = 500.0
  42. renderer:SetViewport(0, Viewport:new(scene_, camera))
  43. -- Create static scene content. First create a zone for ambient lighting and fog control
  44. local zoneNode = scene_:CreateChild("Zone")
  45. local zone = zoneNode:CreateComponent("Zone")
  46. zone.ambientColor = Color(0.15, 0.15, 0.15)
  47. zone.fogColor = Color(0.5, 0.5, 0.7)
  48. zone.fogStart = 300.0
  49. zone.fogEnd = 500.0
  50. zone.boundingBox = BoundingBox(-2000.0, 2000.0)
  51. -- Create a directional light to the world. Enable cascaded shadows on it
  52. local lightNode = scene_:CreateChild("DirectionalLight")
  53. lightNode.direction = Vector3(0.3, -0.5, 0.425)
  54. local light = lightNode:CreateComponent("Light")
  55. light.lightType = LIGHT_DIRECTIONAL
  56. light.castShadows = true
  57. light.shadowBias = BiasParameters(0.00025, 0.5)
  58. light.shadowCascade = CascadeParameters(10.0, 50.0, 200.0, 0.0, 0.8)
  59. light.specularIntensity = 0.5
  60. -- Create heightmap terrain with collision
  61. local terrainNode = scene_:CreateChild("Terrain")
  62. terrainNode.position = Vector3(0.0, 0.0, 0.0)
  63. local terrain = terrainNode:CreateComponent("Terrain")
  64. terrain.patchSize = 64
  65. terrain.spacing = Vector3(2.0, 0.1, 2.0) -- Spacing between vertices and vertical resolution of the height map
  66. terrain.smoothing = true
  67. terrain.heightMap = cache:GetResource("Image", "Textures/HeightMap.png")
  68. terrain.material = cache:GetResource("Material", "Materials/Terrain.xml")
  69. -- The terrain consists of large triangles, which fits well for occlusion rendering, as a hill can occlude all
  70. -- terrain patches and other objects behind it
  71. terrain.occluder = true
  72. local body = terrainNode:CreateComponent("RigidBody")
  73. body.collisionLayer = 2 -- Use layer bitmask 2 for static geometry
  74. local shape = terrainNode:CreateComponent("CollisionShape")
  75. shape:SetTerrain()
  76. -- Create 1000 mushrooms in the terrain. Always face outward along the terrain normal
  77. local NUM_MUSHROOMS = 1000
  78. for i = 1, NUM_MUSHROOMS do
  79. local objectNode = scene_:CreateChild("Mushroom")
  80. local position = Vector3(Random(2000.0) - 1000.0, 0.0, Random(2000.0) - 1000.0)
  81. position.y = terrain:GetHeight(position) - 0.1
  82. objectNode.position = position
  83. -- Create a rotation quaternion from up vector to terrain normal
  84. objectNode.rotation = Quaternion(Vector3(0.0, 1.0, 0.0), terrain:GetNormal(position))
  85. objectNode:SetScale(3.0)
  86. local object = objectNode:CreateComponent("StaticModel")
  87. object.model = cache:GetResource("Model", "Models/Mushroom.mdl")
  88. object.material = cache:GetResource("Material", "Materials/Mushroom.xml")
  89. object.castShadows = true
  90. local body = objectNode:CreateComponent("RigidBody")
  91. body.collisionLayer = 2
  92. local shape = objectNode:CreateComponent("CollisionShape")
  93. shape:SetTriangleMesh(object.model, 0)
  94. end
  95. end
  96. function CreateVehicle()
  97. vehicleNode = scene_:CreateChild("Vehicle")
  98. vehicleNode.position = Vector3(0.0, 5.0, 0.0)
  99. -- Create the vehicle logic script object
  100. local vehicle = vehicleNode:CreateScriptObject("Vehicle")
  101. -- Create the rendering and physics components
  102. vehicle:Init()
  103. local hullBody = vehicleNode:GetComponent("RigidBody")
  104. hullBody.mass = 800.0
  105. hullBody.linearDamping = 0.2
  106. hullBody.angularDamping = 0.5
  107. hullBody.collisionLayer = 1
  108. end
  109. function CreateInstructions()
  110. -- Construct new Text object, set string to display and font to use
  111. local instructionText = ui.root:CreateChild("Text")
  112. instructionText.text = "Use WASD keys to drive, mouse/touch to rotate camera\n"..
  113. "F5 to save scene, F7 to load"
  114. instructionText:SetFont(cache:GetResource("Font", "Fonts/Anonymous Pro.ttf"), 15)
  115. -- The text has multiple rows. Center them in relation to each other
  116. instructionText.textAlignment = HA_CENTER
  117. -- Position the text relative to the screen center
  118. instructionText.horizontalAlignment = HA_CENTER
  119. instructionText.verticalAlignment = VA_CENTER
  120. instructionText:SetPosition(0, ui.root.height / 4)
  121. end
  122. function SubscribeToEvents()
  123. -- Subscribe to Update event for setting the vehicle controls before physics simulation
  124. SubscribeToEvent("Update", "HandleUpdate")
  125. -- Subscribe to PostUpdate event for updating the camera position after physics simulation
  126. SubscribeToEvent("PostUpdate", "HandlePostUpdate")
  127. -- Unsubscribe the SceneUpdate event from base class as the camera node is being controlled in HandlePostUpdate() in this sample
  128. UnsubscribeFromEvent("SceneUpdate")
  129. end
  130. function HandleUpdate(eventType, eventData)
  131. if vehicleNode == nil then
  132. return
  133. end
  134. local vehicle = vehicleNode:GetScriptObject()
  135. if vehicle == nil then
  136. return
  137. end
  138. -- Get movement controls and assign them to the vehicle component. If UI has a focused element, clear controls
  139. if ui.focusElement == nil then
  140. vehicle.controls:Set(CTRL_FORWARD, input:GetKeyDown(KEY_W))
  141. vehicle.controls:Set(CTRL_BACK, input:GetKeyDown(KEY_S))
  142. vehicle.controls:Set(CTRL_LEFT, input:GetKeyDown(KEY_A))
  143. vehicle.controls:Set(CTRL_RIGHT, input:GetKeyDown(KEY_D))
  144. vehicle.controls:Set(CTRL_BRAKE, input:GetKeyDown(KEY_F))
  145. -- Add yaw & pitch from the mouse motion or touch input. Used only for the camera, does not affect motion
  146. if touchEnabled then
  147. for i=0, input.numTouches - 1 do
  148. local state = input:GetTouch(i)
  149. if not state.touchedElement then -- Touch on empty space
  150. local camera = cameraNode:GetComponent("Camera")
  151. if not camera then return end
  152. vehicle.controls.yaw = vehicle.controls.yaw + TOUCH_SENSITIVITY * camera.fov / graphics.height * state.delta.x
  153. vehicle.controls.pitch = vehicle.controls.pitch + TOUCH_SENSITIVITY * camera.fov / graphics.height * state.delta.y
  154. end
  155. end
  156. else
  157. vehicle.controls.yaw = vehicle.controls.yaw + input.mouseMoveX * YAW_SENSITIVITY
  158. vehicle.controls.pitch = vehicle.controls.pitch + input.mouseMoveY * YAW_SENSITIVITY
  159. end
  160. -- Limit pitch
  161. vehicle.controls.pitch = Clamp(vehicle.controls.pitch, 0.0, 80.0)
  162. -- Check for loading / saving the scene
  163. if input:GetKeyPress(KEY_F5) then
  164. scene_:SaveXML(fileSystem:GetProgramDir() .. "Data/Scenes/VehicleDemo.xml")
  165. end
  166. if input:GetKeyPress(KEY_F7) then
  167. scene_:LoadXML(fileSystem:GetProgramDir() .. "Data/Scenes/VehicleDemo.xml")
  168. vehicleNode = scene_:GetChild("Vehicle", true)
  169. vehicleNode:GetScriptObject():PostInit()
  170. end
  171. else
  172. vehicle.controls:Set(CTRL_FORWARD + CTRL_BACK + CTRL_LEFT + CTRL_RIGHT, false)
  173. end
  174. end
  175. function HandlePostUpdate(eventType, eventData)
  176. if vehicleNode == nil then
  177. return
  178. end
  179. local vehicle = vehicleNode:GetScriptObject()
  180. if vehicle == nil then
  181. return
  182. end
  183. -- Physics update has completed. Position camera behind vehicle
  184. local dir = Quaternion(vehicleNode.rotation:YawAngle(), Vector3(0.0, 1.0, 0.0))
  185. dir = dir * Quaternion(vehicle.controls.yaw, Vector3(0.0, 1.0, 0.0))
  186. dir = dir * Quaternion(vehicle.controls.pitch, Vector3(1.0, 0.0, 0.0))
  187. local cameraTargetPos = vehicleNode.position - dir * Vector3(0.0, 0.0, CAMERA_DISTANCE)
  188. local cameraStartPos = vehicleNode.position
  189. -- Raycast camera against static objects (physics collision mask 2)
  190. -- and move it closer to the vehicle if something in between
  191. local cameraRay = Ray(cameraStartPos, (cameraTargetPos - cameraStartPos):Normalized())
  192. local cameraRayLength = (cameraTargetPos - cameraStartPos):Length()
  193. local physicsWorld = scene_:GetComponent("PhysicsWorld")
  194. local result = physicsWorld:RaycastSingle(cameraRay, cameraRayLength, 2)
  195. if result.body ~= nil then
  196. cameraTargetPos = cameraStartPos + cameraRay.direction * (result.distance - 0.5)
  197. end
  198. cameraNode.position = cameraTargetPos
  199. cameraNode.rotation = dir
  200. end
  201. -- Vehicle script object class
  202. --
  203. -- When saving, the node and component handles are automatically converted into nodeID or componentID attributes
  204. -- and are acquired from the scene when loading. The steering member variable will likewise be saved automatically.
  205. -- The Controls object can not be automatically saved, so handle it manually in the Load() and Save() methods
  206. Vehicle = ScriptObject()
  207. function Vehicle:Start()
  208. -- Current left/right steering amount (-1 to 1.)
  209. self.steering = 0.0
  210. -- Vehicle controls.
  211. self.controls = Controls()
  212. self.suspensionRestLength = 0.6
  213. self.suspensionStiffness = 14.0
  214. self.suspensionDamping = 2.0
  215. self.suspensionCompression = 4.0
  216. self.wheelFriction = 1000.0
  217. self.rollInfluence = 0.12
  218. self.maxEngineForce = ENGINE_FORCE
  219. self.wheelWidth = 0.4
  220. self.wheelRadius = 0.5
  221. self.brakingForce = 50.0
  222. self.connectionPoints = {}
  223. self.particleEmitterNodeList = {}
  224. self.prevVelocity = Vector3()
  225. end
  226. function Vehicle:Load(deserializer)
  227. self.controls.yaw = deserializer:ReadFloat()
  228. self.controls.pitch = deserializer:ReadFloat()
  229. end
  230. function Vehicle:Save(serializer)
  231. serializer:WriteFloat(self.controls.yaw)
  232. serializer:WriteFloat(self.controls.pitch)
  233. end
  234. function Vehicle:Init()
  235. -- This function is called only from the main program when initially creating the vehicle, not on scene load
  236. local node = self.node
  237. local hullObject = node:CreateComponent("StaticModel")
  238. self.hullBody = node:CreateComponent("RigidBody")
  239. local hullShape = node:CreateComponent("CollisionShape")
  240. node.scale = Vector3(2.3, 1.0, 4.0)
  241. hullObject.model = cache:GetResource("Model", "Models/Box.mdl")
  242. hullObject.material = cache:GetResource("Material", "Materials/Stone.xml")
  243. hullObject.castShadows = true
  244. hullShape:SetBox(Vector3(1.0, 1.0, 1.0))
  245. self.hullBody.mass = 800.0
  246. self.hullBody.linearDamping = 0.2 -- Some air resistance
  247. self.hullBody.angularDamping = 0.5
  248. self.hullBody.collisionLayer = 1
  249. local raycastVehicle = node:CreateComponent("RaycastVehicle")
  250. raycastVehicle:Init()
  251. local connectionHeight = -0.4
  252. local isFrontWheel = true
  253. local wheelDirection = Vector3(0, -1, 0)
  254. local wheelAxle = Vector3(-1, 0, 0)
  255. local wheelX = CHASSIS_WIDTH / 2.0 - self.wheelWidth
  256. -- Front left
  257. table.insert(self.connectionPoints, Vector3(-wheelX, connectionHeight, 2.5 - self.wheelRadius * 2.0))
  258. -- Front right
  259. table.insert(self.connectionPoints, Vector3(wheelX, connectionHeight, 2.5 - self.wheelRadius * 2.0))
  260. -- Back left
  261. table.insert(self.connectionPoints, Vector3(-wheelX, connectionHeight, -2.5 + self.wheelRadius * 2.0))
  262. -- Back right
  263. table.insert(self.connectionPoints, Vector3(wheelX, connectionHeight, -2.5 + self.wheelRadius * 2.0))
  264. local LtBrown = Color(0.972, 0.780, 0.412)
  265. for i = 1, #self.connectionPoints do
  266. local wheelNode = scene_:CreateChild()
  267. local connectionPoint = self.connectionPoints[i]
  268. -- Front wheels are at front (z > 0)
  269. -- Back wheels are at z < 0
  270. -- Setting rotation according to wheel position
  271. local isFrontWheel = connectionPoint.z > 0.0
  272. if connectionPoint.x >= 0.0 then
  273. wheelNode.rotation = Quaternion(0.0, 0.0, -90.0)
  274. else
  275. wheelNode.rotation = Quaternion(0.0, 0.0, 90.0)
  276. end
  277. wheelNode.worldPosition = node.worldPosition + node.worldRotation * connectionPoint
  278. wheelNode.scale = Vector3(1.0, 0.65, 1.0)
  279. raycastVehicle:AddWheel(wheelNode, wheelDirection, wheelAxle, self.suspensionRestLength, self.wheelRadius, isFrontWheel)
  280. raycastVehicle:SetWheelSuspensionStiffness(i - 1, self.suspensionStiffness)
  281. raycastVehicle:SetWheelDampingRelaxation(i - 1, self.suspensionDamping)
  282. raycastVehicle:SetWheelDampingCompression(i - 1, self.suspensionCompression)
  283. raycastVehicle:SetWheelRollInfluence(i - 1, self.rollInfluence)
  284. local pWheel = wheelNode:CreateComponent("StaticModel")
  285. pWheel.model = cache:GetResource("Model", "Models/Cylinder.mdl")
  286. pWheel.material = cache:GetResource("Material", "Materials/Stone.xml")
  287. pWheel.castShadows = true
  288. end
  289. self:PostInit()
  290. end
  291. function Vehicle:CreateEmitter(place)
  292. local emitter = scene_:CreateChild()
  293. local node = self.node
  294. emitter.worldPosition = node.worldPosition + node.worldRotation * place + Vector3(0, -self.wheelRadius, 0)
  295. local particleEmitter = emitter:CreateComponent("ParticleEmitter")
  296. particleEmitter.effect = cache:GetResource("ParticleEffect", "Particle/Dust.xml")
  297. particleEmitter.emitting = false
  298. emitter.temporary = true
  299. table.insert(self.particleEmitterNodeList, emitter)
  300. end
  301. function Vehicle:CreateEmitters()
  302. self.particleEmitterNodeList = {}
  303. local node = self.node
  304. local raycastVehicle = node:GetComponent("RaycastVehicle")
  305. for id = 0, raycastVehicle:GetNumWheels() do
  306. local connectionPoint = raycastVehicle:GetWheelConnectionPoint(id)
  307. self:CreateEmitter(connectionPoint)
  308. end
  309. end
  310. function Vehicle:PostInit()
  311. local node = self.node
  312. local raycastVehicle = node:GetComponent("RaycastVehicle")
  313. self.hullBody = node:GetComponent("RigidBody")
  314. self:CreateEmitters()
  315. raycastVehicle:ResetWheels()
  316. end
  317. function Vehicle:FixedUpdate(timeStep)
  318. local node = self.node
  319. local newSteering = 0.0
  320. local accelerator = 0.0
  321. local brake = false
  322. if self.controls:IsDown(CTRL_LEFT) then
  323. newSteering = -1.0
  324. end
  325. if self.controls:IsDown(CTRL_RIGHT) then
  326. newSteering = 1.0
  327. end
  328. if self.controls:IsDown(CTRL_FORWARD) then
  329. accelerator = 1.0
  330. end
  331. if self.controls:IsDown(CTRL_BACK) then
  332. accelerator = -0.5
  333. end
  334. if self.controls:IsDown(CTRL_BRAKE) then
  335. brake = true
  336. end
  337. -- When steering, wake up the wheel rigidbodies so that their orientation is updated
  338. if newSteering ~= 0.0 then
  339. self.steering = self.steering * 0.95 + newSteering * 0.05
  340. else
  341. self.steering = self.steering * 0.8 + newSteering * 0.2
  342. end
  343. local steeringRot = Quaternion(0.0, self.steering * MAX_WHEEL_ANGLE, 0.0)
  344. local raycastVehicle = node:GetComponent("RaycastVehicle")
  345. raycastVehicle:SetSteeringValue(0, self.steering)
  346. raycastVehicle:SetSteeringValue(1, self.steering)
  347. raycastVehicle:SetEngineForce(2, self.maxEngineForce * accelerator)
  348. raycastVehicle:SetEngineForce(3, self.maxEngineForce * accelerator)
  349. for i = 0, raycastVehicle:GetNumWheels() - 1 do
  350. if brake then
  351. raycastVehicle:SetBrake(i, self.brakingForce)
  352. else
  353. raycastVehicle:SetBrake(i, 0.0)
  354. end
  355. end
  356. -- Apply downforce proportional to velocity
  357. local localVelocity = self.hullBody.rotation:Inverse() * self.hullBody.linearVelocity
  358. self.hullBody:ApplyForce(self.hullBody.rotation * Vector3(0.0, -1.0, 0.0) * Abs(localVelocity.z) * DOWN_FORCE)
  359. end
  360. function Vehicle:PostUpdate(timeStep)
  361. local node = self.node
  362. local raycastVehicle = node:GetComponent("RaycastVehicle")
  363. if #self.particleEmitterNodeList == 0 then
  364. return
  365. end
  366. local velocity = self.hullBody.linearVelocity
  367. local accel = (velocity - self.prevVelocity) / timeStep
  368. local planeAccel = Vector3(accel.x, 0.0, accel.z):Length()
  369. for i = 0, raycastVehicle:GetNumWheels() - 1 do
  370. local emitter = self.particleEmitterNodeList[i + 1]
  371. local particleEmitter = emitter:GetComponent("ParticleEmitter")
  372. if raycastVehicle:WheelIsGrounded(i) and (raycastVehicle:GetWheelSkidInfoCumulative(i) < 0.9 or raycastVehicle:GetBrake(i) > 2.0 or planeAccel > 15.0) then
  373. emitter.worldPosition = raycastVehicle:GetContactPosition(i)
  374. if not particleEmitter.emitting then
  375. particleEmitter.emitting = true
  376. end
  377. else if particleEmitter.emitting then
  378. particleEmitter.emitting = false
  379. end
  380. end
  381. end
  382. self.prevVelocity = velocity
  383. end