50_Urho2DPlatformer.as 16 KB

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