18_CharacterDemo.lua 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  1. -- Moving character example.
  2. -- This sample demonstrates:
  3. -- - Controlling a humanoid character through physics
  4. -- - Driving animations using the AnimationController component
  5. -- - Manual control of a bone scene node
  6. -- - Implementing 1st and 3rd person cameras, using raycasts to avoid the 3rd person camera clipping into scenery
  7. -- - Saving and loading the variables of a script object
  8. -- - Using touch inputs/gyroscope for iOS/Android (implemented through an external file)
  9. require "LuaScripts/Utilities/Sample"
  10. require "LuaScripts/Utilities/Touch"
  11. -- Variables used by external file are made global in order to be accessed
  12. CTRL_FORWARD = 1
  13. CTRL_BACK = 2
  14. CTRL_LEFT = 4
  15. CTRL_RIGHT = 8
  16. local CTRL_JUMP = 16
  17. local MOVE_FORCE = 0.8
  18. local INAIR_MOVE_FORCE = 0.02
  19. local BRAKE_FORCE = 0.2
  20. local JUMP_FORCE = 7.0
  21. local YAW_SENSITIVITY = 0.1
  22. local INAIR_THRESHOLD_TIME = 0.1
  23. firstPerson = false -- First person camera flag
  24. local characterNode = nil
  25. function Start()
  26. -- Execute the common startup for samples
  27. SampleStart()
  28. -- Create static scene content
  29. CreateScene()
  30. -- Create the controllable character
  31. CreateCharacter()
  32. -- Create the UI content
  33. CreateInstructions()
  34. -- Set the mouse mode to use in the sample
  35. SampleInitMouseMode(MM_RELATIVE)
  36. -- Subscribe to necessary events
  37. SubscribeToEvents()
  38. end
  39. function CreateScene()
  40. scene_ = Scene()
  41. -- Create scene subsystem components
  42. scene_:CreateComponent("Octree")
  43. scene_:CreateComponent("PhysicsWorld")
  44. -- Create camera and define viewport. Camera does not necessarily have to belong to the scene
  45. cameraNode = Node()
  46. local camera = cameraNode:CreateComponent("Camera")
  47. camera.farClip = 300.0
  48. renderer:SetViewport(0, Viewport:new(scene_, camera))
  49. -- Create a Zone component for ambient lighting & fog control
  50. local zoneNode = scene_:CreateChild("Zone")
  51. local zone = zoneNode:CreateComponent("Zone")
  52. zone.boundingBox = BoundingBox(-1000.0, 1000.0)
  53. zone.ambientColor = Color(0.15, 0.15, 0.15)
  54. zone.fogColor = Color(0.5, 0.5, 0.7)
  55. zone.fogStart = 100.0
  56. zone.fogEnd = 300.0
  57. -- Create a directional light to the world. Enable cascaded shadows on it
  58. local lightNode = scene_:CreateChild("DirectionalLight")
  59. lightNode.direction = Vector3(0.6, -1.0, 0.8)
  60. local light = lightNode:CreateComponent("Light")
  61. light.lightType = LIGHT_DIRECTIONAL
  62. light.castShadows = true
  63. light.shadowBias = BiasParameters(0.00025, 0.5)
  64. -- Set cascade splits at 10, 50 and 200 world units, fade shadows out at 80% of maximum shadow distance
  65. light.shadowCascade = CascadeParameters(10.0, 50.0, 200.0, 0.0, 0.8)
  66. -- Create the floor object
  67. local floorNode = scene_:CreateChild("Floor")
  68. floorNode.position = Vector3(0.0, -0.5, 0.0)
  69. floorNode.scale = Vector3(200.0, 1.0, 200.0)
  70. local object = floorNode:CreateComponent("StaticModel")
  71. object.model = cache:GetResource("Model", "Models/Box.mdl")
  72. object.material = cache:GetResource("Material", "Materials/Stone.xml")
  73. local body = floorNode:CreateComponent("RigidBody")
  74. -- Use collision layer bit 2 to mark world scenery. This is what we will raycast against to prevent camera from going
  75. -- inside geometry
  76. body.collisionLayer = 2
  77. local shape = floorNode:CreateComponent("CollisionShape")
  78. shape:SetBox(Vector3(1.0, 1.0, 1.0))
  79. -- Create mushrooms of varying sizes
  80. local NUM_MUSHROOMS = 60
  81. for i = 1, NUM_MUSHROOMS do
  82. local objectNode = scene_:CreateChild("Mushroom")
  83. objectNode.position = Vector3(Random(180.0) - 90.0, 0.0, Random(180.0) - 90.0)
  84. objectNode.rotation = Quaternion(0.0, Random(360.0), 0.0)
  85. objectNode:SetScale(2.0 + Random(5.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. -- Create movable boxes. Let them fall from the sky at first
  96. local NUM_BOXES = 100
  97. for i = 1, NUM_BOXES do
  98. local scale = Random(2.0) + 0.5
  99. local objectNode = scene_:CreateChild("Box")
  100. objectNode.position = Vector3(Random(180.0) - 90.0, Random(10.0) + 10.0, Random(180.0) - 90.0)
  101. objectNode.rotation = Quaternion(Random(360.0), Random(360.0), Random(360.0))
  102. objectNode:SetScale(scale)
  103. local object = objectNode:CreateComponent("StaticModel")
  104. object.model = cache:GetResource("Model", "Models/Box.mdl")
  105. object.material = cache:GetResource("Material", "Materials/Stone.xml")
  106. object.castShadows = true
  107. local body = objectNode:CreateComponent("RigidBody")
  108. body.collisionLayer = 2
  109. -- Bigger boxes will be heavier and harder to move
  110. body.mass = scale * 2.0
  111. local shape = objectNode:CreateComponent("CollisionShape")
  112. shape:SetBox(Vector3(1.0, 1.0, 1.0))
  113. end
  114. end
  115. function CreateCharacter()
  116. characterNode = scene_:CreateChild("Jack")
  117. characterNode.position = Vector3(0.0, 1.0, 0.0)
  118. -- spin node
  119. local adjNode = characterNode:CreateChild("AdjNode")
  120. adjNode.rotation = Quaternion(180.0, Vector3(0.0, 1.0, 0.0))
  121. -- Create the rendering component + animation controller
  122. local object = adjNode:CreateComponent("AnimatedModel")
  123. object.model = cache:GetResource("Model", "Models/Mutant/Mutant.mdl")
  124. object.material = cache:GetResource("Material", "Models/Mutant/Materials/mutant_M.xml")
  125. object.castShadows = true
  126. adjNode:CreateComponent("AnimationController")
  127. -- Set the head bone for manual control
  128. object.skeleton:GetBone("Mutant:Head").animated = false
  129. -- Create rigidbody, and set non-zero mass so that the body becomes dynamic
  130. local body = characterNode:CreateComponent("RigidBody")
  131. body.collisionLayer = 1
  132. body.mass = 1.0
  133. -- Set zero angular factor so that physics doesn't turn the character on its own.
  134. -- Instead we will control the character yaw manually
  135. body.angularFactor = Vector3(0.0, 0.0, 0.0)
  136. -- Set the rigidbody to signal collision also when in rest, so that we get ground collisions properly
  137. body.collisionEventMode = COLLISION_ALWAYS
  138. -- Set a capsule shape for collision
  139. local shape = characterNode:CreateComponent("CollisionShape")
  140. shape:SetCapsule(0.7, 1.8, Vector3(0.0, 0.9, 0.0))
  141. -- Create the character logic object, which takes care of steering the rigidbody
  142. characterNode:CreateScriptObject("Character")
  143. end
  144. function CreateInstructions()
  145. -- Construct new Text object, set string to display and font to use
  146. local instructionText = ui.root:CreateChild("Text")
  147. instructionText:SetText(
  148. "Use WASD keys and mouse to move\n"..
  149. "Space to jump, F to toggle 1st/3rd person\n"..
  150. "F5 to save scene, F7 to load")
  151. instructionText:SetFont(cache:GetResource("Font", "Fonts/Anonymous Pro.ttf"), 15)
  152. -- The text has multiple rows. Center them in relation to each other
  153. instructionText.textAlignment = HA_CENTER
  154. -- Position the text relative to the screen center
  155. instructionText.horizontalAlignment = HA_CENTER
  156. instructionText.verticalAlignment = VA_CENTER
  157. instructionText:SetPosition(0, ui.root.height / 4)
  158. end
  159. function SubscribeToEvents()
  160. -- Subscribe to Update event for setting the character controls before physics simulation
  161. SubscribeToEvent("Update", "HandleUpdate")
  162. -- Subscribe to PostUpdate event for updating the camera position after physics simulation
  163. SubscribeToEvent("PostUpdate", "HandlePostUpdate")
  164. -- Unsubscribe the SceneUpdate event from base class as the camera node is being controlled in HandlePostUpdate() in this sample
  165. UnsubscribeFromEvent("SceneUpdate")
  166. end
  167. function HandleUpdate(eventType, eventData)
  168. if characterNode == nil then
  169. return
  170. end
  171. local character = characterNode:GetScriptObject()
  172. if character == nil then
  173. return
  174. end
  175. -- Clear previous controls
  176. character.controls:Set(CTRL_FORWARD + CTRL_BACK + CTRL_LEFT + CTRL_RIGHT + CTRL_JUMP, false)
  177. -- Update controls using touch utility
  178. if touchEnabled then UpdateTouches(character.controls) end
  179. -- Update controls using keys
  180. if ui.focusElement == nil then
  181. if not touchEnabled or not useGyroscope then
  182. if input:GetKeyDown(KEY_W) then character.controls:Set(CTRL_FORWARD, true) end
  183. if input:GetKeyDown(KEY_S) then character.controls:Set(CTRL_BACK, true) end
  184. if input:GetKeyDown(KEY_A) then character.controls:Set(CTRL_LEFT, true) end
  185. if input:GetKeyDown(KEY_D) then character.controls:Set(CTRL_RIGHT, true) end
  186. end
  187. if input:GetKeyDown(KEY_SPACE) then character.controls:Set(CTRL_JUMP, true) end
  188. -- Add character yaw & pitch from the mouse motion or touch input
  189. if touchEnabled then
  190. for i=0, input.numTouches - 1 do
  191. local state = input:GetTouch(i)
  192. if not state.touchedElement then -- Touch on empty space
  193. local camera = cameraNode:GetComponent("Camera")
  194. if not camera then return end
  195. character.controls.yaw = character.controls.yaw + TOUCH_SENSITIVITY * camera.fov / graphics.height * state.delta.x
  196. character.controls.pitch = character.controls.pitch + TOUCH_SENSITIVITY * camera.fov / graphics.height * state.delta.y
  197. end
  198. end
  199. else
  200. character.controls.yaw = character.controls.yaw + input.mouseMoveX * YAW_SENSITIVITY
  201. character.controls.pitch = character.controls.pitch + input.mouseMoveY * YAW_SENSITIVITY
  202. end
  203. -- Limit pitch
  204. character.controls.pitch = Clamp(character.controls.pitch, -80.0, 80.0)
  205. -- Set rotation already here so that it's updated every rendering frame instead of every physics frame
  206. characterNode.rotation = Quaternion(character.controls.yaw, Vector3(0.0, 1.0, 0.0))
  207. -- Switch between 1st and 3rd person
  208. if input:GetKeyPress(KEY_F) then
  209. firstPerson = not firstPerson
  210. end
  211. -- Turn on/off gyroscope on mobile platform
  212. if input:GetKeyPress(KEY_G) then
  213. useGyroscope = not useGyroscope
  214. end
  215. -- Check for loading / saving the scene
  216. if input:GetKeyPress(KEY_F5) then
  217. scene_:SaveXML(fileSystem:GetProgramDir().."Data/Scenes/CharacterDemo.xml")
  218. end
  219. if input:GetKeyPress(KEY_F7) then
  220. scene_:LoadXML(fileSystem:GetProgramDir().."Data/Scenes/CharacterDemo.xml")
  221. -- After loading we have to reacquire the character scene node, as it has been recreated
  222. -- Simply find by name as there's only one of them
  223. characterNode = scene_:GetChild("Jack", true)
  224. if characterNode == nil then
  225. return
  226. end
  227. end
  228. end
  229. end
  230. function HandlePostUpdate(eventType, eventData)
  231. if characterNode == nil then
  232. return
  233. end
  234. local character = characterNode:GetScriptObject()
  235. if character == nil then
  236. return
  237. end
  238. -- Get camera lookat dir from character yaw + pitch
  239. local rot = characterNode.rotation
  240. local dir = rot * Quaternion(character.controls.pitch, Vector3(1.0, 0.0, 0.0))
  241. -- Turn head to camera pitch, but limit to avoid unnatural animation
  242. local headNode = characterNode:GetChild("Mutant:Head", true)
  243. local limitPitch = Clamp(character.controls.pitch, -45.0, 45.0)
  244. local headDir = rot * Quaternion(limitPitch, Vector3(1.0, 0.0, 0.0))
  245. -- This could be expanded to look at an arbitrary target, now just look at a point in front
  246. local headWorldTarget = headNode.worldPosition + headDir * Vector3(0.0, 0.0, -1.0)
  247. headNode:LookAt(headWorldTarget, Vector3(0.0, 1.0, 0.0))
  248. if firstPerson then
  249. -- First person camera: position to the head bone + offset slightly forward & up
  250. cameraNode.position = headNode.worldPosition + rot * Vector3(0.0, 0.15, 0.2)
  251. cameraNode.rotation = dir
  252. else
  253. -- Third person camera: position behind the character
  254. local aimPoint = characterNode.position + rot * Vector3(0.0, 1.7, 0.0) -- You can modify x Vector3 value to translate the fixed character position (indicative range[-2;2])
  255. -- Collide camera ray with static physics objects (layer bitmask 2) to ensure we see the character properly
  256. local rayDir = dir * Vector3(0.0, 0.0, -1.0) -- For indoor scenes you can use dir * Vector3(0.0, 0.0, -0.5) to prevent camera from crossing the walls
  257. local rayDistance = cameraDistance
  258. local result = scene_:GetComponent("PhysicsWorld"):RaycastSingle(Ray(aimPoint, rayDir), rayDistance, 2)
  259. if result.body ~= nil then
  260. rayDistance = Min(rayDistance, result.distance)
  261. end
  262. rayDistance = Clamp(rayDistance, CAMERA_MIN_DIST, cameraDistance)
  263. cameraNode.position = aimPoint + rayDir * rayDistance
  264. cameraNode.rotation = dir
  265. end
  266. end
  267. -- Character script object class
  268. Character = ScriptObject()
  269. function Character:Start()
  270. -- Character controls.
  271. self.controls = Controls()
  272. -- Grounded flag for movement.
  273. self.onGround = false
  274. -- Jump flag.
  275. self.okToJump = true
  276. -- In air timer. Due to possible physics inaccuracy, character can be off ground for max. 1/10 second and still be allowed to move.
  277. self.inAirTimer = 0.0
  278. self:SubscribeToEvent(self.node, "NodeCollision", "Character:HandleNodeCollision")
  279. end
  280. function Character:Load(deserializer)
  281. self.controls.yaw = deserializer:ReadFloat()
  282. self.controls.pitch = deserializer:ReadFloat()
  283. end
  284. function Character:Save(serializer)
  285. serializer:WriteFloat(self.controls.yaw)
  286. serializer:WriteFloat(self.controls.pitch)
  287. end
  288. function Character:HandleNodeCollision(eventType, eventData)
  289. local contacts = eventData["Contacts"]:GetBuffer()
  290. while not contacts.eof do
  291. local contactPosition = contacts:ReadVector3()
  292. local contactNormal = contacts:ReadVector3()
  293. local contactDistance = contacts:ReadFloat()
  294. local contactImpulse = contacts:ReadFloat()
  295. -- If contact is below node center and pointing up, assume it's a ground contact
  296. if contactPosition.y < self.node.position.y + 1.0 then
  297. local level = contactNormal.y
  298. if level > 0.75 then
  299. self.onGround = true
  300. end
  301. end
  302. end
  303. end
  304. function Character:FixedUpdate(timeStep)
  305. -- Could cache the components for faster access instead of finding them each frame
  306. local body = self.node:GetComponent("RigidBody")
  307. local animCtrl = self.node:GetComponent("AnimationController", true)
  308. -- Update the in air timer. Reset if grounded
  309. if not self.onGround then
  310. self.inAirTimer = self.inAirTimer + timeStep
  311. else
  312. self.inAirTimer = 0.0
  313. end
  314. -- When character has been in air less than 1/10 second, it's still interpreted as being on ground
  315. local softGrounded = self.inAirTimer < INAIR_THRESHOLD_TIME
  316. -- Update movement & animation
  317. local rot = self.node.rotation
  318. local moveDir = Vector3(0.0, 0.0, 0.0)
  319. local velocity = body.linearVelocity
  320. -- Velocity on the XZ plane
  321. local planeVelocity = Vector3(velocity.x, 0.0, velocity.z)
  322. if self.controls:IsDown(CTRL_FORWARD) then
  323. moveDir = moveDir + Vector3(0.0, 0.0, 1.0)
  324. end
  325. if self.controls:IsDown(CTRL_BACK) then
  326. moveDir = moveDir + Vector3(0.0, 0.0, -1.0)
  327. end
  328. if self.controls:IsDown(CTRL_LEFT) then
  329. moveDir = moveDir + Vector3(-1.0, 0.0, 0.0)
  330. end
  331. if self.controls:IsDown(CTRL_RIGHT) then
  332. moveDir = moveDir + Vector3(1.0, 0.0, 0.0)
  333. end
  334. -- Normalize move vector so that diagonal strafing is not faster
  335. if moveDir:LengthSquared() > 0.0 then
  336. moveDir:Normalize()
  337. end
  338. -- If in air, allow control, but slower than when on ground
  339. if softGrounded then
  340. body:ApplyImpulse(rot * moveDir * MOVE_FORCE)
  341. else
  342. body:ApplyImpulse(rot * moveDir * INAIR_MOVE_FORCE)
  343. end
  344. if softGrounded then
  345. -- When on ground, apply a braking force to limit maximum ground velocity
  346. local brakeForce = planeVelocity * -BRAKE_FORCE
  347. body:ApplyImpulse(brakeForce)
  348. -- Jump. Must release jump control between jumps
  349. if self.controls:IsDown(CTRL_JUMP) then
  350. if self.okToJump then
  351. body:ApplyImpulse(Vector3(0.0, 1.0, 0.0) * JUMP_FORCE)
  352. self.okToJump = false
  353. animCtrl:PlayExclusive("Models/Mutant/Mutant_Jump1.ani", 0, false, 0.2)
  354. end
  355. else
  356. self.okToJump = true
  357. end
  358. end
  359. if not self.onGround then
  360. animCtrl:PlayExclusive("Models/Mutant/Mutant_Jump1.ani", 0, false, 0.2)
  361. else
  362. -- Play walk animation if moving on ground, otherwise fade it out
  363. if softGrounded and not moveDir:Equals(Vector3(0.0, 0.0, 0.0)) then
  364. animCtrl:PlayExclusive("Models/Mutant/Mutant_Run.ani", 0, true, 0.2)
  365. -- Set walk animation speed proportional to velocity
  366. animCtrl:SetSpeed("Models/Mutant/Mutant_Run.ani", planeVelocity:Length() * 0.3)
  367. else
  368. animCtrl:PlayExclusive("Models/Mutant/Mutant_Idle0.ani", 0, true, 0.2)
  369. end
  370. end
  371. -- Reset grounded flag for next frame
  372. self.onGround = false
  373. end
  374. -- Create XML patch instructions for screen joystick layout specific to this sample app
  375. function GetScreenJoystickPatchString()
  376. return
  377. "<patch>" ..
  378. " <add sel=\"/element\">" ..
  379. " <element type=\"Button\">" ..
  380. " <attribute name=\"Name\" value=\"Button3\" />" ..
  381. " <attribute name=\"Position\" value=\"-120 -120\" />" ..
  382. " <attribute name=\"Size\" value=\"96 96\" />" ..
  383. " <attribute name=\"Horiz Alignment\" value=\"Right\" />" ..
  384. " <attribute name=\"Vert Alignment\" value=\"Bottom\" />" ..
  385. " <attribute name=\"Texture\" value=\"Texture2D;Textures/TouchInput.png\" />" ..
  386. " <attribute name=\"Image Rect\" value=\"96 0 192 96\" />" ..
  387. " <attribute name=\"Hover Image Offset\" value=\"0 0\" />" ..
  388. " <attribute name=\"Pressed Image Offset\" value=\"0 0\" />" ..
  389. " <element type=\"Text\">" ..
  390. " <attribute name=\"Name\" value=\"Label\" />" ..
  391. " <attribute name=\"Horiz Alignment\" value=\"Center\" />" ..
  392. " <attribute name=\"Vert Alignment\" value=\"Center\" />" ..
  393. " <attribute name=\"Color\" value=\"0 0 0 1\" />" ..
  394. " <attribute name=\"Text\" value=\"Gyroscope\" />" ..
  395. " </element>" ..
  396. " <element type=\"Text\">" ..
  397. " <attribute name=\"Name\" value=\"KeyBinding\" />" ..
  398. " <attribute name=\"Text\" value=\"G\" />" ..
  399. " </element>" ..
  400. " </element>" ..
  401. " </add>" ..
  402. " <remove sel=\"/element/element[./attribute[@name='Name' and @value='Button0']]/attribute[@name='Is Visible']\" />" ..
  403. " <replace sel=\"/element/element[./attribute[@name='Name' and @value='Button0']]/element[./attribute[@name='Name' and @value='Label']]/attribute[@name='Text']/@value\">1st/3rd</replace>" ..
  404. " <add sel=\"/element/element[./attribute[@name='Name' and @value='Button0']]\">" ..
  405. " <element type=\"Text\">" ..
  406. " <attribute name=\"Name\" value=\"KeyBinding\" />" ..
  407. " <attribute name=\"Text\" value=\"F\" />" ..
  408. " </element>" ..
  409. " </add>" ..
  410. " <remove sel=\"/element/element[./attribute[@name='Name' and @value='Button1']]/attribute[@name='Is Visible']\" />" ..
  411. " <replace sel=\"/element/element[./attribute[@name='Name' and @value='Button1']]/element[./attribute[@name='Name' and @value='Label']]/attribute[@name='Text']/@value\">Jump</replace>" ..
  412. " <add sel=\"/element/element[./attribute[@name='Name' and @value='Button1']]\">" ..
  413. " <element type=\"Text\">" ..
  414. " <attribute name=\"Name\" value=\"KeyBinding\" />" ..
  415. " <attribute name=\"Text\" value=\"SPACE\" />" ..
  416. " </element>" ..
  417. " </add>" ..
  418. "</patch>"
  419. end