50_Urho2DPlatformer.lua 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  1. -- Urho2D platformer example.
  2. -- This sample demonstrates:
  3. -- - Creating an orthogonal 2D scene from tile map file
  4. -- - Displaying the scene using the Renderer subsystem
  5. -- - Handling keyboard to move a character and zoom 2D camera
  6. -- - Generating physics shapes from the tmx file's objects
  7. -- - Mixing physics and translations to move the character
  8. -- - Using Box2D Contact listeners to handle the gameplay
  9. -- - Displaying debug geometry for physics and tile map
  10. -- Note that this sample uses some functions from Sample2D utility class.
  11. require "LuaScripts/Utilities/Sample"
  12. require "LuaScripts/Utilities/2D/Sample2D"
  13. function Start()
  14. -- Set filename for load/save functions
  15. demoFilename = "Platformer2D"
  16. -- Execute the common startup for samples
  17. SampleStart()
  18. -- Create the scene content
  19. CreateScene()
  20. -- Create the UI content
  21. CreateUIContent("PLATFORMER 2D DEMO")
  22. -- Hook up to the frame update events
  23. SubscribeToEvents()
  24. end
  25. function CreateScene()
  26. scene_ = Scene()
  27. -- Create the Octree, DebugRenderer and PhysicsWorld2D components to the scene
  28. scene_:CreateComponent("Octree")
  29. scene_:CreateComponent("DebugRenderer")
  30. scene_:CreateComponent("PhysicsWorld2D")
  31. -- Create camera
  32. cameraNode = Node()
  33. local camera = cameraNode:CreateComponent("Camera")
  34. camera.orthographic = true
  35. camera.orthoSize = graphics.height * PIXEL_SIZE
  36. camera.zoom = 1.8 * Min(graphics.width / 1280, graphics.height / 800) -- Set zoom according to user's resolution to ensure full visibility (initial zoom (1.8) is set for full visibility at 1280x800 resolution)
  37. -- Setup the viewport for displaying the scene
  38. renderer:SetViewport(0, Viewport:new(scene_, camera))
  39. renderer.defaultZone.fogColor = Color(0.2, 0.2, 0.2) -- Set background color for the scene
  40. -- Create tile map from tmx file
  41. local tileMapNode = scene_:CreateChild("TileMap")
  42. local tileMap = tileMapNode:CreateComponent("TileMap2D")
  43. tileMap.tmxFile = cache:GetResource("TmxFile2D", "Urho2D/Tilesets/Ortho.tmx")
  44. local info = tileMap.info
  45. -- Create Spriter Imp character (from sample 33_SpriterAnimation)
  46. CreateCharacter(info, true, 0.8, Vector3(1, 8, 0), 0.2)
  47. -- Generate physics collision shapes from the tmx file's objects located in "Physics" (top) layer
  48. local tileMapLayer = tileMap:GetLayer(tileMap.numLayers - 1)
  49. CreateCollisionShapesFromTMXObjects(tileMapNode, tileMapLayer, info)
  50. -- Instantiate enemies and moving platforms at each placeholder of "MovingEntities" layer (placeholders are Poly Line objects defining a path from points)
  51. PopulateMovingEntities(tileMap:GetLayer(tileMap.numLayers - 2))
  52. -- Instantiate coins to pick at each placeholder of "Coins" layer (placeholders for coins are Rectangle objects)
  53. PopulateCoins(tileMap:GetLayer(tileMap.numLayers - 3))
  54. -- Instantiate triggers (for ropes, ladders, lava, slopes...) at each placeholder of "Triggers" layer (placeholders for triggers are Rectangle objects)
  55. PopulateTriggers(tileMap:GetLayer(tileMap.numLayers - 4))
  56. -- Create background
  57. CreateBackgroundSprite(info, 3.5, "Textures/HeightMap.png", true)
  58. -- Check when scene is rendered
  59. SubscribeToEvent("EndRendering", HandleSceneRendered)
  60. end
  61. function HandleSceneRendered()
  62. UnsubscribeFromEvent("EndRendering")
  63. SaveScene(true) -- Save the scene so we can reload it later
  64. scene_.updateEnabled = false -- Pause the scene as long as the UI is hiding it
  65. end
  66. function SubscribeToEvents()
  67. -- Subscribe HandleUpdate() function for processing update events
  68. SubscribeToEvent("Update", "HandleUpdate")
  69. -- Subscribe HandlePostUpdate() function for processing post update events
  70. SubscribeToEvent("PostUpdate", "HandlePostUpdate")
  71. -- Subscribe to PostRenderUpdate to draw debug geometry
  72. SubscribeToEvent("PostRenderUpdate", "HandlePostRenderUpdate")
  73. -- Subscribe to Box2D contact listeners
  74. SubscribeToEvent("PhysicsBeginContact2D", "HandleCollisionBegin")
  75. SubscribeToEvent("PhysicsEndContact2D", "HandleCollisionEnd")
  76. -- Unsubscribe the SceneUpdate event from base class to prevent camera pitch and yaw in 2D sample
  77. UnsubscribeFromEvent("SceneUpdate")
  78. end
  79. function HandleUpdate(eventType, eventData)
  80. -- Zoom in/out
  81. if cameraNode ~= nil then
  82. Zoom(cameraNode:GetComponent("Camera"))
  83. end
  84. -- Toggle debug geometry with 'Z' key
  85. if input:GetKeyPress(KEY_Z) then drawDebug = not drawDebug end
  86. -- Check for loading / saving the scene
  87. if input:GetKeyPress(KEY_F5) then
  88. SaveScene(false)
  89. end
  90. if input:GetKeyPress(KEY_F7) then
  91. ReloadScene(false)
  92. end
  93. end
  94. function HandlePostUpdate(eventType, eventData)
  95. if character2DNode == nil or cameraNode == nil then
  96. return
  97. end
  98. cameraNode.position = Vector3(character2DNode.position.x, character2DNode.position.y, -10) -- Camera tracks character
  99. end
  100. function HandlePostRenderUpdate(eventType, eventData)
  101. if drawDebug then
  102. scene_:GetComponent("PhysicsWorld2D"):DrawDebugGeometry()
  103. local tileMapNode = scene_:GetChild("TileMap", true)
  104. local map = tileMapNode:GetComponent("TileMap2D")
  105. map:DrawDebugGeometry(scene_:GetComponent("DebugRenderer"), false)
  106. end
  107. end
  108. function HandleCollisionBegin(eventType, eventData)
  109. -- Get colliding node
  110. local hitNode = eventData["NodeA"]:GetPtr("Node")
  111. if hitNode.name == "Imp" then
  112. hitNode = eventData["NodeB"]:GetPtr("Node")
  113. end
  114. local nodeName = hitNode.name
  115. local character = character2DNode:GetScriptObject()
  116. -- Handle ropes and ladders climbing
  117. if nodeName == "Climb" then
  118. if character.isClimbing then -- If transition between rope and top of rope (as we are using split triggers)
  119. character.climb2 = true
  120. else
  121. character.isClimbing = true
  122. -- Override gravity so that the character doesn't fall
  123. local body = character2DNode:GetComponent("RigidBody2D")
  124. body.gravityScale = 0
  125. -- Clear forces so that the character stops (should be performed by setting linear velocity to zero, but currently doesn't work)
  126. body.linearVelocity = Vector2.ZERO
  127. body.awake = false
  128. body.awake = true
  129. end
  130. end
  131. if nodeName == "CanJump" then
  132. character.aboveClimbable = true
  133. end
  134. -- Handle coins picking
  135. if nodeName == "Coin" then
  136. hitNode:Remove()
  137. character.remainingCoins = character.remainingCoins - 1
  138. if character.remainingCoins == 0 then
  139. ui.root:GetChild("Instructions", true).text = "!!! Go to the Exit !!!"
  140. end
  141. ui.root:GetChild("CoinsText", true).text = character.remainingCoins -- Update coins UI counter
  142. PlaySound("Powerup.wav")
  143. end
  144. -- Handle interactions with enemies
  145. if nodeName == "Enemy" or nodeName == "Orc" then
  146. local animatedSprite = character2DNode:GetComponent("AnimatedSprite2D")
  147. local deltaX = character2DNode.position.x - hitNode.position.x
  148. -- Orc killed if character is fighting in its direction when the contact occurs (flowers are not destroyable)
  149. if nodeName == "Orc" and animatedSprite.animation == "attack" and (deltaX < 0 == animatedSprite.flipX) then
  150. hitNode:GetScriptObject().emitTime = 1
  151. if not hitNode:GetChild("Emitter", true) then
  152. hitNode:GetComponent("RigidBody2D"):Remove() -- Remove Orc's body
  153. SpawnEffect(hitNode)
  154. PlaySound("BigExplosion.wav")
  155. end
  156. -- Player killed if not fighting in the direction of the Orc when the contact occurs, or when colliding with a flower
  157. else
  158. if not character2DNode:GetChild("Emitter", true) then
  159. character.wounded = true
  160. if nodeName == "Orc" then
  161. hitNode:GetScriptObject().fightTimer = 1
  162. end
  163. SpawnEffect(character2DNode)
  164. PlaySound("BigExplosion.wav")
  165. end
  166. end
  167. end
  168. -- Handle exiting the level when all coins have been gathered
  169. if nodeName == "Exit" and character.remainingCoins == 0 then
  170. -- Update UI
  171. local instructions = ui.root:GetChild("Instructions", true)
  172. instructions.text = "!!! WELL DONE !!!"
  173. instructions.position = IntVector2.ZERO
  174. -- Put the character outside of the scene and magnify him
  175. character2DNode.position = Vector3(-20, 0, 0)
  176. character2DNode:SetScale(1.2)
  177. end
  178. -- Handle falling into lava
  179. if nodeName == "Lava" then
  180. local body = character2DNode:GetComponent("RigidBody2D")
  181. body:ApplyLinearImpulse(Vector2(0, 1) * MOVE_SPEED, body.massCenter, true) -- Violently project character out of lava
  182. if not character2DNode:GetChild("Emitter", true) then
  183. character.wounded = true
  184. SpawnEffect(character2DNode)
  185. PlaySound("BigExplosion.wav")
  186. end
  187. end
  188. -- Handle climbing a slope
  189. if nodeName == "Slope" then
  190. character.onSlope = true
  191. end
  192. end
  193. function HandleCollisionEnd(eventType, eventData)
  194. -- Get colliding node
  195. local hitNode = eventData["NodeA"]:GetPtr("Node")
  196. if hitNode.name == "Imp" then
  197. hitNode = eventData["NodeB"]:GetPtr("Node")
  198. end
  199. local nodeName = hitNode.name
  200. local character = character2DNode:GetScriptObject()
  201. -- Handle leaving a rope or ladder
  202. if nodeName == "Climb" then
  203. if character.climb2 then
  204. character.climb2 = false
  205. else
  206. character.isClimbing = false
  207. local body = character2DNode:GetComponent("RigidBody2D")
  208. body.gravityScale = 1 -- Restore gravity
  209. end
  210. end
  211. if nodeName == "CanJump" then
  212. character.aboveClimbable = false
  213. end
  214. -- Handle leaving a slope
  215. if nodeName == "Slope" then
  216. character.onSlope = false
  217. -- Clear forces (should be performed by setting linear velocity to zero, but currently doesn't work)
  218. local body = character2DNode:GetComponent("RigidBody2D")
  219. body.linearVelocity = Vector2.ZERO
  220. body.awake = false
  221. body.awake = true
  222. end
  223. end
  224. -- Character2D script object class
  225. Character2D = ScriptObject()
  226. function Character2D:Start()
  227. self.wounded = false
  228. self.killed = false
  229. self.timer = 0
  230. self.maxCoins = 0
  231. self.remainingCoins = 0
  232. self.remainingLifes = 3
  233. self.isClimbing = false
  234. self.climb2 = false -- Used only for ropes, as they are split into 2 shapes
  235. self.aboveClimbable = false
  236. self.onSlope = false
  237. end
  238. function Character2D:Save(serializer)
  239. self.isClimbing = false -- Overwrite before auto-deserialization
  240. end
  241. function Character2D:Update(timeStep)
  242. if character2DNode == nil then
  243. return
  244. end
  245. -- Handle wounded/killed states
  246. if self.killed then
  247. return
  248. end
  249. if self.wounded then
  250. self:HandleWoundedState(timeStep)
  251. return
  252. end
  253. -- Set temporary variables
  254. local node = self.node
  255. local body = node:GetComponent("RigidBody2D")
  256. local animatedSprite = node:GetComponent("AnimatedSprite2D")
  257. local onGround = false
  258. local jump = false
  259. -- Collision detection (AABB query)
  260. local characterHalfSize = Vector2(0.16, 0.16)
  261. local collidingBodies = scene_:GetComponent("PhysicsWorld2D"):GetRigidBodies(Rect(node.worldPosition2D - characterHalfSize - Vector2(0, 0.1), node.worldPosition2D + characterHalfSize))
  262. if table.maxn(collidingBodies) > 1 and not self.isClimbing then
  263. onGround = true
  264. end
  265. -- Set direction
  266. local moveDir = Vector2.ZERO -- Reset
  267. if input:GetKeyDown(KEY_LEFT) or input:GetKeyDown(KEY_A) then
  268. moveDir = moveDir + Vector2.LEFT
  269. animatedSprite.flipX = false -- Flip sprite (reset to default play on the X axis)
  270. end
  271. if input:GetKeyDown(KEY_RIGHT) or input:GetKeyDown(KEY_D) then
  272. moveDir = moveDir + Vector2.RIGHT
  273. animatedSprite.flipX = true -- Flip sprite (flip animation on the X axis)
  274. end
  275. -- Jump
  276. if (onGround or self.aboveClimbable) and (input:GetKeyPress(KEY_UP) or input:GetKeyPress(KEY_W)) then
  277. jump = true
  278. end
  279. -- Climb
  280. if self.isClimbing then
  281. if not self.aboveClimbable and (input:GetKeyDown(KEY_UP) or input:GetKeyDown(KEY_W)) then
  282. moveDir = moveDir + Vector2.UP
  283. end
  284. if input:GetKeyDown(KEY_DOWN) or input:GetKeyDown(KEY_S) then
  285. moveDir = moveDir + Vector2.DOWN
  286. end
  287. end
  288. -- Move
  289. if not moveDir:Equals(Vector2.ZERO) or jump then
  290. if self.onSlope then
  291. body:ApplyForceToCenter(moveDir * MOVE_SPEED / 2, true) -- When climbing a slope, apply force (todo: replace by setting linear velocity to zero when will work)
  292. else
  293. node:Translate(Vector3(moveDir.x, moveDir.y, 0) * timeStep * 1.8)
  294. end
  295. if jump then
  296. body:ApplyLinearImpulse(Vector2(0, 0.17) * MOVE_SPEED, body.massCenter, true)
  297. end
  298. end
  299. -- Animate
  300. if input:GetKeyDown(KEY_SPACE) then
  301. if animatedSprite.animation ~= "attack" then
  302. animatedSprite:SetAnimation("attack", LM_FORCE_LOOPED)
  303. animatedSprite.speed = 1.5
  304. end
  305. elseif not moveDir:Equals(Vector2.ZERO) then
  306. if animatedSprite.animation ~= "run" then
  307. animatedSprite:SetAnimation("run")
  308. end
  309. elseif animatedSprite.animation ~= "idle" then
  310. animatedSprite:SetAnimation("idle")
  311. end
  312. end
  313. function Character2D:HandleWoundedState(timeStep)
  314. local node = self.node
  315. local body = node:GetComponent("RigidBody2D")
  316. local animatedSprite = node:GetComponent("AnimatedSprite2D")
  317. -- Play "hit" animation in loop
  318. if animatedSprite.animation ~= "hit" then
  319. animatedSprite:SetAnimation("hit", LM_FORCE_LOOPED)
  320. end
  321. -- Update timer
  322. self.timer = self.timer + timeStep
  323. -- End of timer
  324. if self.timer > 2 then
  325. -- Reset timer
  326. self.timer = 0
  327. -- Clear forces (should be performed by setting linear velocity to zero, but currently doesn't work)
  328. body.linearVelocity = Vector2.ZERO
  329. body.awake = false
  330. body.awake = true
  331. -- Remove particle emitter
  332. node:GetChild("Emitter", true):Remove()
  333. -- Update lifes UI and counter
  334. self.remainingLifes = self.remainingLifes - 1
  335. ui.root:GetChild("LifeText", true).text = self.remainingLifes -- Update lifes UI counter
  336. -- Reset wounded state
  337. self.wounded = false
  338. -- Handle death
  339. if self.remainingLifes == 0 then
  340. self:HandleDeath()
  341. return
  342. end
  343. -- Re-position the character to the nearest point
  344. if node.position.x < 15 then
  345. node.position = Vector3(1, 8, 0)
  346. else
  347. node.position = Vector3(18.8, 9.2, 0)
  348. end
  349. end
  350. end
  351. function Character2D:HandleDeath()
  352. local node = self.node
  353. local body = node:GetComponent("RigidBody2D")
  354. local animatedSprite = node:GetComponent("AnimatedSprite2D")
  355. -- Set state to 'killed'
  356. self.killed = true
  357. -- Update UI elements
  358. local instructions = ui.root:GetChild("Instructions", true)
  359. instructions.text = "!!! GAME OVER !!!"
  360. ui.root:GetChild("ExitButton", true).visible = true
  361. ui.root:GetChild("PlayButton", true).visible = true
  362. -- Show mouse cursor so that we can click
  363. input.mouseVisible = true
  364. -- Put character outside of the scene and magnify him
  365. node.position = Vector3(-20, 0, 0)
  366. node:SetScale(1.2)
  367. -- Play death animation once
  368. if animatedSprite.animation ~= "dead2" then
  369. animatedSprite:SetAnimation("dead2")
  370. end
  371. end