Sample2D.lua 20 KB

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