19_VehicleDemo.as 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  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. #include "Scripts/Utilities/Sample.as"
  7. const int CTRL_FORWARD = 1;
  8. const int CTRL_BACK = 2;
  9. const int CTRL_LEFT = 4;
  10. const int CTRL_RIGHT = 8;
  11. const float CAMERA_DISTANCE = 10.0f;
  12. const float YAW_SENSITIVITY = 0.1f;
  13. const float ENGINE_POWER = 10.0f;
  14. const float DOWN_FORCE = 10.0f;
  15. const float MAX_WHEEL_ANGLE = 22.5f;
  16. Node@ vehicleNode;
  17. void Start()
  18. {
  19. // Execute the common startup for samples
  20. SampleStart();
  21. // Create static scene content
  22. CreateScene();
  23. // Create the controllable vehicle
  24. CreateVehicle();
  25. // Create the UI content
  26. CreateInstructions();
  27. // Set the mouse mode to use in the sample
  28. SampleInitMouseMode(MM_RELATIVE);
  29. // Subscribe to necessary events
  30. SubscribeToEvents();
  31. }
  32. void CreateScene()
  33. {
  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. Camera@ camera = cameraNode.CreateComponent("Camera");
  41. camera.farClip = 500.0f;
  42. renderer.viewports[0] = Viewport(scene_, camera);
  43. // Create static scene content. First create a zone for ambient lighting and fog control
  44. Node@ zoneNode = scene_.CreateChild("Zone");
  45. Zone@ zone = zoneNode.CreateComponent("Zone");
  46. zone.ambientColor = Color(0.15f, 0.15f, 0.15f);
  47. zone.fogColor = Color(0.5f, 0.5f, 0.7f);
  48. zone.fogStart = 300.0f;
  49. zone.fogEnd = 500.0f;
  50. zone.boundingBox = BoundingBox(-2000.0f, 2000.0f);
  51. // Create a directional light to the world. Enable cascaded shadows on it
  52. Node@ lightNode = scene_.CreateChild("DirectionalLight");
  53. lightNode.direction = Vector3(0.3f, -0.5f, 0.425f);
  54. Light@ light = lightNode.CreateComponent("Light");
  55. light.lightType = LIGHT_DIRECTIONAL;
  56. light.castShadows = true;
  57. light.shadowBias = BiasParameters(0.00025f, 0.5f);
  58. light.shadowCascade = CascadeParameters(10.0f, 50.0f, 200.0f, 0.0f, 0.8f);
  59. light.specularIntensity = 0.5f;
  60. // Create heightmap terrain with collision
  61. Node@ terrainNode = scene_.CreateChild("Terrain");
  62. terrainNode.position = Vector3::ZERO;
  63. Terrain@ terrain = terrainNode.CreateComponent("Terrain");
  64. terrain.patchSize = 64;
  65. terrain.spacing = Vector3(2.0f, 0.1f, 2.0f); // 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. RigidBody@ body = terrainNode.CreateComponent("RigidBody");
  73. body.collisionLayer = 2; // Use layer bitmask 2 for static geometry
  74. CollisionShape@ shape = terrainNode.CreateComponent("CollisionShape");
  75. shape.SetTerrain();
  76. // Create 1000 mushrooms in the terrain. Always face outward along the terrain normal
  77. const uint NUM_MUSHROOMS = 1000;
  78. for (uint i = 0; i < NUM_MUSHROOMS; ++i)
  79. {
  80. Node@ objectNode = scene_.CreateChild("Mushroom");
  81. Vector3 position(Random(2000.0f) - 1000.0f, 0.0f, Random(2000.0f) - 1000.0f);
  82. position.y = terrain.GetHeight(position) - 0.1f;
  83. objectNode.position = position;
  84. // Create a rotation quaternion from up vector to terrain normal
  85. objectNode.rotation = Quaternion(Vector3::UP, terrain.GetNormal(position));
  86. objectNode.SetScale(3.0f);
  87. StaticModel@ object = objectNode.CreateComponent("StaticModel");
  88. object.model = cache.GetResource("Model", "Models/Mushroom.mdl");
  89. object.material = cache.GetResource("Material", "Materials/Mushroom.xml");
  90. object.castShadows = true;
  91. RigidBody@ body = objectNode.CreateComponent("RigidBody");
  92. body.collisionLayer = 2;
  93. CollisionShape@ shape = objectNode.CreateComponent("CollisionShape");
  94. shape.SetTriangleMesh(object.model, 0);
  95. }
  96. }
  97. void CreateVehicle()
  98. {
  99. vehicleNode = scene_.CreateChild("Vehicle");
  100. vehicleNode.position = Vector3(0.0f, 5.0f, 0.0f);
  101. // Create the vehicle logic script object
  102. Vehicle@ vehicle = cast<Vehicle>(vehicleNode.CreateScriptObject(scriptFile, "Vehicle"));
  103. // Create the rendering and physics components
  104. vehicle.Init();
  105. }
  106. void CreateInstructions()
  107. {
  108. // Construct new Text object, set string to display and font to use
  109. Text@ instructionText = ui.root.CreateChild("Text");
  110. instructionText.text = "Use WASD keys to drive, mouse/touch to rotate camera\n"
  111. "F5 to save scene, F7 to load";
  112. instructionText.SetFont(cache.GetResource("Font", "Fonts/Anonymous Pro.ttf"), 15);
  113. // The text has multiple rows. Center them in relation to each other
  114. instructionText.textAlignment = HA_CENTER;
  115. // Position the text relative to the screen center
  116. instructionText.horizontalAlignment = HA_CENTER;
  117. instructionText.verticalAlignment = VA_CENTER;
  118. instructionText.SetPosition(0, ui.root.height / 4);
  119. }
  120. void SubscribeToEvents()
  121. {
  122. // Subscribe to Update event for setting the vehicle controls before physics simulation
  123. SubscribeToEvent("Update", "HandleUpdate");
  124. // Subscribe to PostUpdate event for updating the camera position after physics simulation
  125. SubscribeToEvent("PostUpdate", "HandlePostUpdate");
  126. // Unsubscribe the SceneUpdate event from base class as the camera node is being controlled in HandlePostUpdate() in this sample
  127. UnsubscribeFromEvent("SceneUpdate");
  128. }
  129. void HandleUpdate(StringHash eventType, VariantMap& eventData)
  130. {
  131. if (vehicleNode is null)
  132. return;
  133. Vehicle@ vehicle = cast<Vehicle>(vehicleNode.scriptObject);
  134. if (vehicle is null)
  135. return;
  136. // Get movement controls and assign them to the vehicle component. If UI has a focused element, clear controls
  137. if (ui.focusElement is null)
  138. {
  139. vehicle.controls.Set(CTRL_FORWARD, input.keyDown[KEY_W]);
  140. vehicle.controls.Set(CTRL_BACK, input.keyDown[KEY_S]);
  141. vehicle.controls.Set(CTRL_LEFT, input.keyDown[KEY_A]);
  142. vehicle.controls.Set(CTRL_RIGHT, input.keyDown[KEY_D]);
  143. // Add yaw & pitch from the mouse motion. Used only for the camera, does not affect motion
  144. if (touchEnabled)
  145. {
  146. for (uint i = 0; i < input.numTouches; ++i)
  147. {
  148. TouchState@ state = input.touches[i];
  149. if (state.touchedElement is null) // Touch on empty space
  150. {
  151. Camera@ camera = cameraNode.GetComponent("Camera");
  152. if (camera is null)
  153. return;
  154. vehicle.controls.yaw += TOUCH_SENSITIVITY * camera.fov / graphics.height * state.delta.x;
  155. vehicle.controls.pitch += TOUCH_SENSITIVITY * camera.fov / graphics.height * state.delta.y;
  156. }
  157. }
  158. }
  159. else
  160. {
  161. vehicle.controls.yaw += input.mouseMoveX * YAW_SENSITIVITY;
  162. vehicle.controls.pitch += input.mouseMoveY * YAW_SENSITIVITY;
  163. }
  164. // Limit pitch
  165. vehicle.controls.pitch = Clamp(vehicle.controls.pitch, 0.0f, 80.0f);
  166. // Check for loading / saving the scene
  167. if (input.keyPress[KEY_F5])
  168. {
  169. File saveFile(fileSystem.programDir + "Data/Scenes/VehicleDemo.xml", FILE_WRITE);
  170. scene_.SaveXML(saveFile);
  171. }
  172. if (input.keyPress[KEY_F7])
  173. {
  174. File loadFile(fileSystem.programDir + "Data/Scenes/VehicleDemo.xml", FILE_READ);
  175. scene_.LoadXML(loadFile);
  176. // After loading we have to reacquire the vehicle scene node, as it has been recreated
  177. // Simply find by name as there's only one of them
  178. vehicleNode = scene_.GetChild("Vehicle", true);
  179. }
  180. }
  181. else
  182. vehicle.controls.Set(CTRL_FORWARD | CTRL_BACK | CTRL_LEFT | CTRL_RIGHT, false);
  183. }
  184. void HandlePostUpdate(StringHash eventType, VariantMap& eventData)
  185. {
  186. if (vehicleNode is null)
  187. return;
  188. Vehicle@ vehicle = cast<Vehicle>(vehicleNode.scriptObject);
  189. if (vehicle is null)
  190. return;
  191. // Physics update has completed. Position camera behind vehicle
  192. Quaternion dir(vehicleNode.rotation.yaw, Vector3::UP);
  193. dir = dir * Quaternion(vehicle.controls.yaw, Vector3::UP);
  194. dir = dir * Quaternion(vehicle.controls.pitch, Vector3::RIGHT);
  195. Vector3 cameraTargetPos = vehicleNode.position - dir * Vector3(0.0f, 0.0f, CAMERA_DISTANCE);
  196. Vector3 cameraStartPos = vehicleNode.position;
  197. // Raycast camera against static objects (physics collision mask 2)
  198. // and move it closer to the vehicle if something in between
  199. Ray cameraRay(cameraStartPos, (cameraTargetPos - cameraStartPos).Normalized());
  200. float cameraRayLength = (cameraTargetPos - cameraStartPos).length;
  201. PhysicsRaycastResult result = scene_.physicsWorld.RaycastSingle(cameraRay, cameraRayLength, 2);
  202. if (result.body !is null)
  203. cameraTargetPos = cameraStartPos + cameraRay.direction * (result.distance - 0.5f);
  204. cameraNode.position = cameraTargetPos;
  205. cameraNode.rotation = dir;
  206. }
  207. // Vehicle script object class
  208. //
  209. // When saving, the node and component handles are automatically converted into nodeID or componentID attributes
  210. // and are acquired from the scene when loading. The steering member variable will likewise be saved automatically.
  211. // The Controls object can not be automatically saved, so handle it manually in the Load() and Save() methods
  212. class Vehicle : ScriptObject
  213. {
  214. Node@ frontLeft;
  215. Node@ frontRight;
  216. Node@ rearLeft;
  217. Node@ rearRight;
  218. Constraint@ frontLeftAxis;
  219. Constraint@ frontRightAxis;
  220. RigidBody@ hullBody;
  221. RigidBody@ frontLeftBody;
  222. RigidBody@ frontRightBody;
  223. RigidBody@ rearLeftBody;
  224. RigidBody@ rearRightBody;
  225. // Current left/right steering amount (-1 to 1.)
  226. float steering = 0.0f;
  227. // Vehicle controls.
  228. Controls controls;
  229. void Load(Deserializer& deserializer)
  230. {
  231. controls.yaw = deserializer.ReadFloat();
  232. controls.pitch = deserializer.ReadFloat();
  233. }
  234. void Save(Serializer& serializer)
  235. {
  236. serializer.WriteFloat(controls.yaw);
  237. serializer.WriteFloat(controls.pitch);
  238. }
  239. void Init()
  240. {
  241. // This function is called only from the main program when initially creating the vehicle, not on scene load
  242. StaticModel@ hullObject = node.CreateComponent("StaticModel");
  243. hullBody = node.CreateComponent("RigidBody");
  244. CollisionShape@ hullShape = node.CreateComponent("CollisionShape");
  245. node.scale = Vector3(1.5f, 1.0f, 3.0f);
  246. hullObject.model = cache.GetResource("Model", "Models/Box.mdl");
  247. hullObject.material = cache.GetResource("Material", "Materials/Stone.xml");
  248. hullObject.castShadows = true;
  249. hullShape.SetBox(Vector3::ONE);
  250. hullBody.mass = 4.0f;
  251. hullBody.linearDamping = 0.2f; // Some air resistance
  252. hullBody.angularDamping = 0.5f;
  253. hullBody.collisionLayer = 1;
  254. frontLeft = InitWheel("FrontLeft", Vector3(-0.6f, -0.4f, 0.3f));
  255. frontRight = InitWheel("FrontRight", Vector3(0.6f, -0.4f, 0.3f));
  256. rearLeft = InitWheel("RearLeft", Vector3(-0.6f, -0.4f, -0.3f));
  257. rearRight = InitWheel("RearRight", Vector3(0.6f, -0.4f, -0.3f));
  258. frontLeftAxis = frontLeft.GetComponent("Constraint");
  259. frontRightAxis = frontRight.GetComponent("Constraint");
  260. frontLeftBody = frontLeft.GetComponent("RigidBody");
  261. frontRightBody = frontRight.GetComponent("RigidBody");
  262. rearLeftBody = rearLeft.GetComponent("RigidBody");
  263. rearRightBody = rearRight.GetComponent("RigidBody");
  264. }
  265. Node@ InitWheel(const String&in name, const Vector3&in offset)
  266. {
  267. // Note: do not parent the wheel to the hull scene node. Instead create it on the root level and let the physics
  268. // constraint keep it together
  269. Node@ wheelNode = scene.CreateChild(name);
  270. wheelNode.position = node.LocalToWorld(offset);
  271. wheelNode.rotation = node.worldRotation * (offset.x >= 0.0f ? Quaternion(0.0f, 0.0f, -90.0f) :
  272. Quaternion(0.0f, 0.0f, 90.0f));
  273. wheelNode.scale = Vector3(0.8f, 0.5f, 0.8f);
  274. StaticModel@ wheelObject = wheelNode.CreateComponent("StaticModel");
  275. RigidBody@ wheelBody = wheelNode.CreateComponent("RigidBody");
  276. CollisionShape@ wheelShape = wheelNode.CreateComponent("CollisionShape");
  277. Constraint@ wheelConstraint = wheelNode.CreateComponent("Constraint");
  278. wheelObject.model = cache.GetResource("Model", "Models/Cylinder.mdl");
  279. wheelObject.material = cache.GetResource("Material", "Materials/Stone.xml");
  280. wheelObject.castShadows = true;
  281. wheelShape.SetSphere(1.0f);
  282. wheelBody.friction = 1;
  283. wheelBody.mass = 1;
  284. wheelBody.linearDamping = 0.2f; // Some air resistance
  285. wheelBody.angularDamping = 0.75f; // Could also use rolling friction
  286. wheelBody.collisionLayer = 1;
  287. wheelConstraint.constraintType = CONSTRAINT_HINGE;
  288. wheelConstraint.otherBody = node.GetComponent("RigidBody");
  289. wheelConstraint.worldPosition = wheelNode.worldPosition; // Set constraint's both ends at wheel's location
  290. wheelConstraint.axis = Vector3::UP; // Wheel rotates around its local Y-axis
  291. wheelConstraint.otherAxis = offset.x >= 0.0f ? Vector3::RIGHT : Vector3::LEFT; // Wheel's hull axis points either left or right
  292. wheelConstraint.lowLimit = Vector2(-180.0f, 0.0f); // Let the wheel rotate freely around the axis
  293. wheelConstraint.highLimit = Vector2(180.0f, 0.0f);
  294. wheelConstraint.disableCollision = true; // Let the wheel intersect the vehicle hull
  295. return wheelNode;
  296. }
  297. void FixedUpdate(float timeStep)
  298. {
  299. float newSteering = 0.0f;
  300. float accelerator = 0.0f;
  301. if (controls.IsDown(CTRL_LEFT))
  302. newSteering = -1.0f;
  303. if (controls.IsDown(CTRL_RIGHT))
  304. newSteering = 1.0f;
  305. if (controls.IsDown(CTRL_FORWARD))
  306. accelerator = 1.0f;
  307. if (controls.IsDown(CTRL_BACK))
  308. accelerator = -0.5f;
  309. // When steering, wake up the wheel rigidbodies so that their orientation is updated
  310. if (newSteering != 0.0f)
  311. {
  312. frontLeftBody.Activate();
  313. frontRightBody.Activate();
  314. steering = steering * 0.95f + newSteering * 0.05f;
  315. }
  316. else
  317. steering = steering * 0.8f + newSteering * 0.2f;
  318. Quaternion steeringRot(0.0f, steering * MAX_WHEEL_ANGLE, 0.0f);
  319. frontLeftAxis.otherAxis = steeringRot * Vector3::LEFT;
  320. frontRightAxis.otherAxis = steeringRot * Vector3::RIGHT;
  321. if (accelerator != 0.0f)
  322. {
  323. // Torques are applied in world space, so need to take the vehicle & wheel rotation into account
  324. Vector3 torqueVec = Vector3(ENGINE_POWER * accelerator, 0.0f, 0.0f);
  325. frontLeftBody.ApplyTorque(node.rotation * steeringRot * torqueVec);
  326. frontRightBody.ApplyTorque(node.rotation * steeringRot * torqueVec);
  327. rearLeftBody.ApplyTorque(node.rotation * torqueVec);
  328. rearRightBody.ApplyTorque(node.rotation * torqueVec);
  329. }
  330. // Apply downforce proportional to velocity
  331. Vector3 localVelocity = hullBody.rotation.Inverse() * hullBody.linearVelocity;
  332. hullBody.ApplyForce(hullBody.rotation * Vector3::DOWN * Abs(localVelocity.z) * DOWN_FORCE);
  333. }
  334. }
  335. // Create XML patch instructions for screen joystick layout specific to this sample app
  336. String patchInstructions = "";