Sample2D.lua 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  1. -- Convenient functions for Urho2D samples:
  2. -- - Generate collision shapes from a tmx file objects
  3. -- - Create Spriter Imp character
  4. -- - Load Mover script object class from file
  5. -- - Create enemies, coins and platforms to tile map placeholders
  6. -- - Handle camera zoom using PageUp, PageDown and MouseWheel
  7. -- - Create UI interface
  8. -- - Create a particle emitter attached to a given node
  9. -- - Play a non-looping sound effect
  10. -- - Create a background sprite
  11. -- - Set global variables
  12. -- - Set XML patch instructions for screen joystick
  13. CAMERA_MIN_DIST = 0.1
  14. CAMERA_MAX_DIST = 6
  15. MOVE_SPEED = 23 -- Movement speed as world units per second
  16. MOVE_SPEED_X = 1.5 -- Movement speed for isometric maps
  17. MOVE_SPEED_SCALE = 1 -- Scaling factor based on tiles' aspect ratio
  18. LIFES = 3
  19. zoom = 2 -- Speed is scaled according to zoom
  20. demoFilename = ""
  21. character2DNode = nil
  22. function CreateCollisionShapesFromTMXObjects(tileMapNode, tileMapLayer, info)
  23. -- Create rigid body to the root node
  24. local body = tileMapNode:CreateComponent("RigidBody2D")
  25. body.bodyType = BT_STATIC
  26. -- Generate physics collision shapes from the tmx file's objects located in "Physics" layer
  27. for i=0, tileMapLayer:GetNumObjects() -1 do
  28. local tileMapObject = tileMapLayer:GetObject(i) -- Get physics objects (TileMapObject2D)
  29. local objectType = tileMapObject.objectType
  30. -- Create collision shape from tmx object
  31. local shape
  32. if objectType == OT_RECTANGLE then
  33. shape = tileMapNode:CreateComponent("CollisionBox2D")
  34. local size = tileMapObject.size
  35. shape.size = size
  36. if info.orientation == O_ORTHOGONAL then
  37. shape.center = tileMapObject.position + size / 2
  38. else
  39. shape.center = tileMapObject.position + Vector2(info.tileWidth / 2, 0)
  40. shape.angle = 45 -- If our tile map is isometric then shape is losange
  41. end
  42. elseif objectType == OT_ELLIPSE then
  43. shape = tileMapNode:CreateComponent("CollisionCircle2D") -- Ellipse is built as a circle shape as there's no equivalent in Box2D
  44. local size = tileMapObject.size
  45. shape.radius = size.x / 2
  46. if info.orientation == O_ORTHOGONAL then
  47. shape.center = tileMapObject.position + size / 2
  48. else
  49. shape.center = tileMapObject.position + Vector2(info.tileWidth / 2, 0)
  50. end
  51. elseif objectType == OT_POLYGON then
  52. shape = tileMapNode:CreateComponent("CollisionPolygon2D")
  53. elseif objectType == OT_POLYLINE then
  54. shape = tileMapNode:CreateComponent("CollisionChain2D")
  55. else break
  56. end
  57. if objectType == OT_POLYGON or objectType == OT_POLYLINE then -- Build shape from vertices
  58. local numVertices = tileMapObject.numPoints
  59. shape.vertexCount = numVertices
  60. for i=0, numVertices - 1 do
  61. shape:SetVertex(i, tileMapObject:GetPoint(i))
  62. end
  63. end
  64. shape.friction = 0.8
  65. if tileMapObject:HasProperty("Friction") then
  66. shape.friction = ToFloat(tileMapObject:GetProperty("Friction"))
  67. end
  68. end
  69. end
  70. function CreateCharacter(info, createObject, friction, position, scale)
  71. character2DNode = scene_:CreateChild("Imp")
  72. character2DNode.position = position
  73. character2DNode:SetScale(scale)
  74. local animatedSprite = character2DNode:CreateComponent("AnimatedSprite2D")
  75. animatedSprite:SetAnimation(cache:GetResource("AnimationSet2D", "Urho2D/imp/imp.scml"), "idle") -- Get scml file and Play "idle" anim
  76. animatedSprite:SetLayer(2) -- Put character over tile map (which is on layer 0) and over Orcs (which are on layer 1)
  77. local body = character2DNode:CreateComponent("RigidBody2D")
  78. body.bodyType = BT_DYNAMIC
  79. body.allowSleep = false
  80. local shape = character2DNode:CreateComponent("CollisionCircle2D")
  81. shape.radius = 1.1 -- Set shape size
  82. shape.friction = friction -- Set friction
  83. shape.restitution = 0.1 -- Slight bounce
  84. if createObject then
  85. character2DNode:CreateScriptObject("Character2D") -- Create a ScriptObject to handle character behavior
  86. end
  87. -- Scale character's speed on the Y axis according to tiles' aspect ratio (for isometric only)
  88. MOVE_SPEED_SCALE = info.tileHeight / info.tileWidth
  89. end
  90. function CreateTrigger()
  91. local node = scene_:CreateChild("Trigger") -- Clones will be renamed according to object type
  92. local body = node:CreateComponent("RigidBody2D")
  93. body.bodyType = BT_STATIC
  94. local shape = node:CreateComponent("CollisionBox2D") -- Create box shape
  95. shape.trigger = true
  96. return node
  97. end
  98. function CreateEnemy()
  99. local node = scene_:CreateChild("Enemy")
  100. local staticSprite = node:CreateComponent("StaticSprite2D")
  101. staticSprite.sprite = cache:GetResource("Sprite2D", "Urho2D/Aster.png")
  102. local body = node:CreateComponent("RigidBody2D")
  103. body.bodyType = BT_STATIC
  104. local shape = node:CreateComponent("CollisionCircle2D") -- Create circle shape
  105. shape.radius = 0.25 -- Set radius
  106. return node
  107. end
  108. function CreateOrc()
  109. local node = scene_:CreateChild("Orc")
  110. node.scale = character2DNode.scale -- Use same scale as player character
  111. local animatedSprite = node:CreateComponent("AnimatedSprite2D")
  112. animatedSprite:SetAnimation(cache:GetResource("AnimationSet2D", "Urho2D/Orc/Orc.scml"), "run") -- Get scml file and Play "run" anim
  113. animatedSprite:SetLayer(1) -- Make orc always visible
  114. local body = node:CreateComponent("RigidBody2D")
  115. local shape = node:CreateComponent("CollisionCircle2D") -- Create circle shape
  116. shape.radius = 1.3 -- Set shape size
  117. shape.trigger = true
  118. return node
  119. end
  120. function CreateCoin()
  121. local node = scene_:CreateChild("Coin")
  122. node:SetScale(0.5)
  123. local animatedSprite = node:CreateComponent("AnimatedSprite2D")
  124. animatedSprite:SetAnimation(cache:GetResource("AnimationSet2D", "Urho2D/GoldIcon.scml"), "idle") -- Get scml file and Play "idle" anim
  125. local body = node:CreateComponent("RigidBody2D")
  126. body.bodyType = BT_STATIC
  127. local shape = node:CreateComponent("CollisionCircle2D") -- Create circle shape
  128. shape.radius = 0.32 -- Set radius
  129. shape.trigger = true
  130. return node
  131. end
  132. function CreateMovingPlatform()
  133. local node = scene_:CreateChild("MovingPlatform")
  134. node.scale = Vector3(3, 1, 0)
  135. local staticSprite = node:CreateComponent("StaticSprite2D")
  136. staticSprite.sprite = cache:GetResource("Sprite2D", "Urho2D/Box.png")
  137. local body = node:CreateComponent("RigidBody2D")
  138. body.bodyType = BT_STATIC
  139. local shape = node:CreateComponent("CollisionBox2D") -- Create box shape
  140. shape.size = Vector2(0.32, 0.32) -- Set box size
  141. shape.friction = 0.8 -- Set friction
  142. return node
  143. end
  144. function PopulateMovingEntities(movingEntitiesLayer)
  145. -- Create enemy, Orc and moving platform nodes (will be cloned at each placeholder)
  146. local enemyNode = CreateEnemy()
  147. local orcNode = CreateOrc()
  148. local platformNode = CreateMovingPlatform()
  149. -- Instantiate enemies and moving platforms at each placeholder (placeholders are Poly Line objects defining a path from points)
  150. for i=0, movingEntitiesLayer:GetNumObjects() -1 do
  151. -- Get placeholder object (TileMapObject2D)
  152. local movingObject = movingEntitiesLayer:GetObject(i)
  153. if movingObject.objectType == OT_POLYLINE then
  154. -- Clone the moving entity node and position it at placeholder point
  155. local movingClone = nil
  156. local offset = Vector2.ZERO
  157. if movingObject.type == "Enemy" then
  158. movingClone = enemyNode:Clone()
  159. offset = Vector2(0, -0.32)
  160. elseif movingObject.type == "Orc" then
  161. movingClone = orcNode:Clone()
  162. elseif movingObject.type == "MovingPlatform" then
  163. movingClone = platformNode:Clone()
  164. else
  165. break
  166. end
  167. movingClone.position2D = movingObject:GetPoint(0) + offset
  168. -- Create script object that handles entity translation along its path (load from file)
  169. local mover = movingClone:CreateScriptObject("LuaScripts/Utilities/2D/Mover.lua", "Mover")
  170. -- Set path from points
  171. mover.path = CreatePathFromPoints(movingObject, offset)
  172. -- Override default speed
  173. if movingObject:HasProperty("Speed") then
  174. mover.speed = movingObject:GetProperty("Speed")
  175. end
  176. end
  177. end
  178. -- Remove nodes used for cloning purpose
  179. enemyNode:Remove()
  180. orcNode:Remove()
  181. platformNode:Remove()
  182. end
  183. function PopulateCoins(coinsLayer)
  184. -- Create coin (will be cloned at each placeholder)
  185. local coinNode = CreateCoin()
  186. -- Instantiate coins to pick at each placeholder
  187. for i=0, coinsLayer:GetNumObjects() -1 do
  188. local coinObject = coinsLayer:GetObject(i) -- Get placeholder object (TileMapObject2D)
  189. local coinClone = coinNode:Clone()
  190. coinClone.position2D = coinObject.position + coinObject.size / 2 + Vector2(0, 0.16)
  191. end
  192. -- Init coins counters
  193. local character = character2DNode:GetScriptObject()
  194. character.remainingCoins = coinsLayer.numObjects
  195. character.maxCoins = coinsLayer.numObjects
  196. -- Remove node used for cloning purpose
  197. coinNode:Remove()
  198. end
  199. function PopulateTriggers(triggersLayer)
  200. -- Create trigger node (will be cloned at each placeholder)
  201. local triggerNode = CreateTrigger()
  202. -- Instantiate triggers at each placeholder (Rectangle objects)
  203. for i=0, triggersLayer:GetNumObjects() -1 do
  204. local triggerObject = triggersLayer:GetObject(i) -- Get placeholder object (TileMapObject2D)
  205. if triggerObject.objectType == OT_RECTANGLE then
  206. local triggerClone = triggerNode:Clone()
  207. triggerClone.name = triggerObject.type
  208. triggerClone:GetComponent("CollisionBox2D").size = triggerObject.size
  209. triggerClone.position2D = triggerObject.position + triggerObject.size / 2
  210. end
  211. end
  212. end
  213. function Zoom(camera)
  214. if input.mouseMoveWheel then
  215. zoom = Clamp(camera.zoom + input.mouseMoveWheel * 0.1, CAMERA_MIN_DIST, CAMERA_MAX_DIST)
  216. camera.zoom = zoom
  217. end
  218. if input:GetKeyDown(KEY_PAGEUP) then
  219. zoom = Clamp(camera.zoom * 1.01, CAMERA_MIN_DIST, CAMERA_MAX_DIST)
  220. camera.zoom = zoom
  221. end
  222. if input:GetKeyDown(KEY_PAGEDOWN) then
  223. zoom = Clamp(camera.zoom * 0.99, CAMERA_MIN_DIST, CAMERA_MAX_DIST)
  224. camera.zoom = zoom
  225. end
  226. end
  227. function CreatePathFromPoints(object, offset)
  228. local path = {}
  229. for i=0, object.numPoints -1 do
  230. table.insert(path, object:GetPoint(i) + offset)
  231. end
  232. return path
  233. end
  234. function CreateUIContent(demoTitle)
  235. -- Set the default UI style and font
  236. ui.root.defaultStyle = cache:GetResource("XMLFile", "UI/DefaultStyle.xml")
  237. local font = cache:GetResource("Font", "Fonts/Anonymous Pro.ttf")
  238. -- We create in-game UIs (coins and lifes) first so that they are hidden by the fullscreen UI (we could also temporary hide them using SetVisible)
  239. -- Create the UI for displaying the remaining coins
  240. local coinsUI = ui.root:CreateChild("BorderImage", "Coins")
  241. coinsUI.texture = cache:GetResource("Texture2D", "Urho2D/GoldIcon.png")
  242. coinsUI:SetSize(50, 50)
  243. coinsUI.imageRect = IntRect(0, 64, 60, 128)
  244. coinsUI:SetAlignment(HA_LEFT, VA_TOP)
  245. coinsUI:SetPosition(5, 5);
  246. local coinsText = coinsUI:CreateChild("Text", "CoinsText")
  247. coinsText:SetAlignment(HA_CENTER, VA_CENTER)
  248. coinsText:SetFont(font, 24)
  249. coinsText.textEffect = TE_SHADOW
  250. coinsText.text = character2DNode:GetScriptObject().remainingCoins
  251. -- Create the UI for displaying the remaining lifes
  252. local lifeUI = ui.root:CreateChild("BorderImage", "Life")
  253. lifeUI.texture = cache:GetResource("Texture2D", "Urho2D/imp/imp_all.png")
  254. lifeUI.imageRect = IntRect(2, 153, 238, 298)
  255. lifeUI:SetSize(80, 50)
  256. lifeUI:SetAlignment(HA_RIGHT, VA_TOP)
  257. lifeUI:SetPosition(-5, 5);
  258. local lifeText = lifeUI:CreateChild("Text", "LifeText")
  259. lifeText:SetAlignment(HA_CENTER, VA_CENTER)
  260. lifeText:SetFont(font, 24)
  261. lifeText.textEffect = TE_SHADOW
  262. lifeText.text = LIFES
  263. -- Create the fullscreen UI for start/end
  264. local fullUI = ui.root:CreateChild("Window", "FullUI")
  265. fullUI:SetStyleAuto()
  266. fullUI:SetSize(ui.root.width, ui.root.height)
  267. fullUI.enabled = false -- Do not react to input, only the 'Exit' and 'Play' buttons will
  268. -- Create the title
  269. local title = fullUI:CreateChild("BorderImage", "Title")
  270. title:SetMinSize(fullUI.width, 50)
  271. title.texture = cache:GetResource("Texture2D", "Textures/HeightMap.png")
  272. title:SetFullImageRect()
  273. title:SetAlignment(HA_CENTER, VA_TOP)
  274. local titleText = title:CreateChild("Text", "TitleText")
  275. titleText:SetAlignment(HA_CENTER, VA_CENTER)
  276. titleText:SetFont(font, 24)
  277. titleText.text = demoTitle
  278. -- Create the image
  279. local spriteUI = fullUI:CreateChild("BorderImage", "Sprite")
  280. spriteUI.texture = cache:GetResource("Texture2D", "Urho2D/imp/imp_all.png")
  281. spriteUI:SetSize(240, 150)
  282. spriteUI.imageRect = IntRect(2, 153, 238, 149)
  283. spriteUI:SetAlignment(HA_CENTER, VA_CENTER)
  284. spriteUI:SetPosition(0, - ui.root.height / 4)
  285. -- Create the 'EXIT' button
  286. local exitButton = ui.root:CreateChild("Button", "ExitButton")
  287. exitButton:SetStyleAuto()
  288. exitButton.focusMode = FM_RESETFOCUS
  289. exitButton:SetSize(100, 50)
  290. exitButton:SetAlignment(HA_CENTER, VA_CENTER)
  291. exitButton:SetPosition(-100, 0)
  292. local exitText = exitButton:CreateChild("Text", "ExitText")
  293. exitText:SetAlignment(HA_CENTER, VA_CENTER)
  294. exitText:SetFont(font, 24)
  295. exitText.text = "EXIT"
  296. SubscribeToEvent(exitButton, "Released", "HandleExitButton")
  297. -- Create the 'PLAY' button
  298. local playButton = ui.root:CreateChild("Button", "PlayButton")
  299. playButton:SetStyleAuto()
  300. playButton.focusMode = FM_RESETFOCUS
  301. playButton:SetSize(100, 50)
  302. playButton:SetAlignment(HA_CENTER, VA_CENTER)
  303. playButton:SetPosition(100, 0)
  304. local playText = playButton:CreateChild("Text", "PlayText")
  305. playText:SetAlignment(HA_CENTER, VA_CENTER)
  306. playText:SetFont(font, 24)
  307. playText.text = "PLAY"
  308. SubscribeToEvent(playButton, "Released", "HandlePlayButton")
  309. -- Create the instructions
  310. local instructionText = ui.root:CreateChild("Text", "Instructions")
  311. instructionText:SetFont(font, 15)
  312. instructionText.textAlignment = HA_CENTER -- Center rows in relation to each other
  313. instructionText.text = "Use WASD keys or Arrows to move\nPageUp/PageDown/MouseWheel to zoom\nF5/F7 to save/reload scene\n'Z' to toggle debug geometry\nSpace to fight"
  314. instructionText:SetAlignment(HA_CENTER, VA_CENTER)
  315. instructionText:SetPosition(0, ui.root.height / 4)
  316. -- Show mouse cursor
  317. input.mouseVisible = true
  318. end
  319. function HandleExitButton()
  320. engine:Exit()
  321. end
  322. function HandlePlayButton()
  323. -- Remove fullscreen UI and unfreeze the scene
  324. if ui.root:GetChild("FullUI", true) then
  325. ui.root:GetChild("FullUI", true):Remove()
  326. scene_.updateEnabled = true
  327. else
  328. -- Reload the scene
  329. ReloadScene(true)
  330. end
  331. -- Hide Instructions and Play/Exit buttons
  332. ui.root:GetChild("Instructions", true).text = ""
  333. ui.root:GetChild("ExitButton", true).visible = false
  334. ui.root:GetChild("PlayButton", true).visible = false
  335. -- Hide mouse cursor
  336. input.mouseVisible = false
  337. end
  338. function SaveScene(initial)
  339. local filename = demoFilename
  340. if not initial then
  341. filename = demoFilename .. "InGame"
  342. end
  343. scene_:SaveXML(fileSystem:GetProgramDir() .. "Data/Scenes/" .. filename .. ".xml")
  344. end
  345. function ReloadScene(reInit)
  346. local filename = demoFilename
  347. if not reInit then
  348. filename = demoFilename .. "InGame"
  349. end
  350. scene_:LoadXML(fileSystem:GetProgramDir().."Data/Scenes/" .. filename .. ".xml")
  351. -- After loading we have to reacquire the character scene node, as it has been recreated
  352. -- Simply find by name as there's only one of them
  353. character2DNode = scene_:GetChild("Imp", true);
  354. if character2DNode == nil then
  355. return
  356. end
  357. -- Set what value to use depending whether reload is requested from 'PLAY' button (reInit=true) or 'F7' key (reInit=false)
  358. local character = character2DNode:GetScriptObject()
  359. local lifes = character.remainingLifes
  360. local coins =character.remainingCoins
  361. if reInit then
  362. lifes = LIFES
  363. coins = character.maxCoins
  364. end
  365. -- Update lifes UI and value
  366. local lifeText = ui.root:GetChild("LifeText", true)
  367. lifeText.text = lifes
  368. character.remainingLifes = lifes
  369. -- Update coins UI and value
  370. local coinsText = ui.root:GetChild("CoinsText", true)
  371. coinsText.text = coins
  372. character.remainingCoins = coins
  373. end
  374. function SpawnEffect(node)
  375. local particleNode = node:CreateChild("Emitter")
  376. particleNode:SetScale(0.5 / node.scale.x)
  377. local particleEmitter = particleNode:CreateComponent("ParticleEmitter2D")
  378. particleEmitter.effect = cache:GetResource("ParticleEffect2D", "Urho2D/sun.pex")
  379. end
  380. function PlaySound(soundName)
  381. local soundNode = scene_:CreateChild("Sound")
  382. local source = soundNode:CreateComponent("SoundSource")
  383. source:Play(cache:GetResource("Sound", "Sounds/" .. soundName))
  384. end
  385. function CreateBackgroundSprite(info, scale, texture, animate)
  386. local node = scene_:CreateChild("Background")
  387. node.position = Vector3(info.mapWidth, info.mapHeight, 0) / 2
  388. node:SetScale(scale)
  389. local sprite = node:CreateComponent("StaticSprite2D")
  390. sprite.sprite = cache:GetResource("Sprite2D", texture)
  391. SetRandomSeed(time:GetSystemTime()) -- Randomize from system clock
  392. sprite.color = Color(Random(0, 1), Random(0, 1), Random(0, 1), 1)
  393. -- Create rotation animation
  394. if animate then
  395. local animation = ValueAnimation:new()
  396. animation:SetKeyFrame(0, Variant(Quaternion(0, 0, 0)))
  397. animation:SetKeyFrame(1, Variant(Quaternion(0, 0, 180)))
  398. animation:SetKeyFrame(2, Variant(Quaternion(0, 0, 0)))
  399. node:SetAttributeAnimation("Rotation", animation, WM_LOOP, 0.05)
  400. end
  401. end
  402. -- Create XML patch instructions for screen joystick layout specific to this sample app
  403. function GetScreenJoystickPatchString()
  404. return
  405. "<patch>" ..
  406. " <remove sel=\"/element/element[./attribute[@name='Name' and @value='Button0']]/attribute[@name='Is Visible']\" />" ..
  407. " <replace sel=\"/element/element[./attribute[@name='Name' and @value='Button0']]/element[./attribute[@name='Name' and @value='Label']]/attribute[@name='Text']/@value\">Fight</replace>" ..
  408. " <add sel=\"/element/element[./attribute[@name='Name' and @value='Button0']]\">" ..
  409. " <element type=\"Text\">" ..
  410. " <attribute name=\"Name\" value=\"KeyBinding\" />" ..
  411. " <attribute name=\"Text\" value=\"SPACE\" />" ..
  412. " </element>" ..
  413. " </add>" ..
  414. " <remove sel=\"/element/element[./attribute[@name='Name' and @value='Button1']]/attribute[@name='Is Visible']\" />" ..
  415. " <replace sel=\"/element/element[./attribute[@name='Name' and @value='Button1']]/element[./attribute[@name='Name' and @value='Label']]/attribute[@name='Text']/@value\">Jump</replace>" ..
  416. " <add sel=\"/element/element[./attribute[@name='Name' and @value='Button1']]\">" ..
  417. " <element type=\"Text\">" ..
  418. " <attribute name=\"Name\" value=\"KeyBinding\" />" ..
  419. " <attribute name=\"Text\" value=\"UP\" />" ..
  420. " </element>" ..
  421. " </add>" ..
  422. "</patch>"
  423. end