| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453 |
- -- Urho2D platformer example.
- -- This sample demonstrates:
- -- - Creating an orthogonal 2D scene from tile map file
- -- - Displaying the scene using the Renderer subsystem
- -- - Handling keyboard to move a character and zoom 2D camera
- -- - Generating physics shapes from the tmx file's objects
- -- - Mixing physics and translations to move the character
- -- - Using Box2D Contact listeners to handle the gameplay
- -- - Displaying debug geometry for physics and tile map
- -- Note that this sample uses some functions from Sample2D utility class.
- require "LuaScripts/Utilities/Sample"
- require "LuaScripts/Utilities/2D/Sample2D"
- function Start()
- -- Set filename for load/save functions
- demoFilename = "Platformer2D"
- -- Execute the common startup for samples
- SampleStart()
- -- Create the scene content
- CreateScene()
- -- Create the UI content
- CreateUIContent("PLATFORMER 2D DEMO")
- -- Hook up to the frame update events
- SubscribeToEvents()
- end
- function CreateScene()
- scene_ = Scene()
- -- Create the Octree, DebugRenderer and PhysicsWorld2D components to the scene
- scene_:CreateComponent("Octree")
- scene_:CreateComponent("DebugRenderer")
- scene_:CreateComponent("PhysicsWorld2D")
- -- Create camera
- cameraNode = Node()
- local camera = cameraNode:CreateComponent("Camera")
- camera.orthographic = true
- camera.orthoSize = graphics.height * PIXEL_SIZE
- 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)
- -- Setup the viewport for displaying the scene
- renderer:SetViewport(0, Viewport:new(scene_, camera))
- renderer.defaultZone.fogColor = Color(0.2, 0.2, 0.2) -- Set background color for the scene
- -- Create tile map from tmx file
- local tileMapNode = scene_:CreateChild("TileMap")
- local tileMap = tileMapNode:CreateComponent("TileMap2D")
- tileMap.tmxFile = cache:GetResource("TmxFile2D", "Urho2D/Tilesets/Ortho.tmx")
- local info = tileMap.info
- -- Create Spriter Imp character (from sample 33_SpriterAnimation)
- CreateCharacter(info, true, 0.8, Vector3(1, 8, 0), 0.2)
- -- Generate physics collision shapes from the tmx file's objects located in "Physics" (top) layer
- local tileMapLayer = tileMap:GetLayer(tileMap.numLayers - 1)
- CreateCollisionShapesFromTMXObjects(tileMapNode, tileMapLayer, info)
- -- Instantiate enemies and moving platforms at each placeholder of "MovingEntities" layer (placeholders are Poly Line objects defining a path from points)
- PopulateMovingEntities(tileMap:GetLayer(tileMap.numLayers - 2))
- -- Instantiate coins to pick at each placeholder of "Coins" layer (placeholders for coins are Rectangle objects)
- PopulateCoins(tileMap:GetLayer(tileMap.numLayers - 3))
- -- Instantiate triggers (for ropes, ladders, lava, slopes...) at each placeholder of "Triggers" layer (placeholders for triggers are Rectangle objects)
- PopulateTriggers(tileMap:GetLayer(tileMap.numLayers - 4))
- -- Create background
- CreateBackgroundSprite(info, 3.5, "Textures/HeightMap.png", true)
- -- Check when scene is rendered
- SubscribeToEvent("EndRendering", HandleSceneRendered)
- end
- function HandleSceneRendered()
- UnsubscribeFromEvent("EndRendering")
- SaveScene(true) -- Save the scene so we can reload it later
- scene_.updateEnabled = false -- Pause the scene as long as the UI is hiding it
- end
- function SubscribeToEvents()
- -- Subscribe HandleUpdate() function for processing update events
- SubscribeToEvent("Update", "HandleUpdate")
- -- Subscribe HandlePostUpdate() function for processing post update events
- SubscribeToEvent("PostUpdate", "HandlePostUpdate")
- -- Subscribe to PostRenderUpdate to draw debug geometry
- SubscribeToEvent("PostRenderUpdate", "HandlePostRenderUpdate")
- -- Subscribe to Box2D contact listeners
- SubscribeToEvent("PhysicsBeginContact2D", "HandleCollisionBegin")
- SubscribeToEvent("PhysicsEndContact2D", "HandleCollisionEnd")
- -- Unsubscribe the SceneUpdate event from base class to prevent camera pitch and yaw in 2D sample
- UnsubscribeFromEvent("SceneUpdate")
- end
- function HandleUpdate(eventType, eventData)
- -- Zoom in/out
- if cameraNode ~= nil then
- Zoom(cameraNode:GetComponent("Camera"))
- end
- -- Toggle debug geometry with 'Z' key
- if input:GetKeyPress(KEY_Z) then drawDebug = not drawDebug end
- -- Check for loading / saving the scene
- if input:GetKeyPress(KEY_F5) then
- SaveScene(false)
- end
- if input:GetKeyPress(KEY_F7) then
- ReloadScene(false)
- end
- end
- function HandlePostUpdate(eventType, eventData)
- if character2DNode == nil or cameraNode == nil then
- return
- end
- cameraNode.position = Vector3(character2DNode.position.x, character2DNode.position.y, -10) -- Camera tracks character
- end
- function HandlePostRenderUpdate(eventType, eventData)
- if drawDebug then
- scene_:GetComponent("PhysicsWorld2D"):DrawDebugGeometry()
- local tileMapNode = scene_:GetChild("TileMap", true)
- local map = tileMapNode:GetComponent("TileMap2D")
- map:DrawDebugGeometry(scene_:GetComponent("DebugRenderer"), false)
- end
- end
- function HandleCollisionBegin(eventType, eventData)
- -- Get colliding node
- local hitNode = eventData["NodeA"]:GetPtr("Node")
- if hitNode.name == "Imp" then
- hitNode = eventData["NodeB"]:GetPtr("Node")
- end
- local nodeName = hitNode.name
- local character = character2DNode:GetScriptObject()
- -- Handle ropes and ladders climbing
- if nodeName == "Climb" then
- if character.isClimbing then -- If transition between rope and top of rope (as we are using split triggers)
- character.climb2 = true
- else
- character.isClimbing = true
- -- Override gravity so that the character doesn't fall
- local body = character2DNode:GetComponent("RigidBody2D")
- body.gravityScale = 0
- -- Clear forces so that the character stops (should be performed by setting linear velocity to zero, but currently doesn't work)
- body.linearVelocity = Vector2.ZERO
- body.awake = false
- body.awake = true
- end
- end
- if nodeName == "CanJump" then
- character.aboveClimbable = true
- end
- -- Handle coins picking
- if nodeName == "Coin" then
- hitNode:Remove()
- character.remainingCoins = character.remainingCoins - 1
- if character.remainingCoins == 0 then
- ui.root:GetChild("Instructions", true).text = "!!! Go to the Exit !!!"
- end
- ui.root:GetChild("CoinsText", true).text = character.remainingCoins -- Update coins UI counter
- PlaySound("Powerup.wav")
- end
- -- Handle interactions with enemies
- if nodeName == "Enemy" or nodeName == "Orc" then
- local animatedSprite = character2DNode:GetComponent("AnimatedSprite2D")
- local deltaX = character2DNode.position.x - hitNode.position.x
- -- Orc killed if character is fighting in its direction when the contact occurs (flowers are not destroyable)
- if nodeName == "Orc" and animatedSprite.animation == "attack" and (deltaX < 0 == animatedSprite.flipX) then
- hitNode:GetScriptObject().emitTime = 1
- if not hitNode:GetChild("Emitter", true) then
- hitNode:GetComponent("RigidBody2D"):Remove() -- Remove Orc's body
- SpawnEffect(hitNode)
- PlaySound("BigExplosion.wav")
- end
- -- Player killed if not fighting in the direction of the Orc when the contact occurs, or when colliding with a flower
- else
- if not character2DNode:GetChild("Emitter", true) then
- character.wounded = true
- if nodeName == "Orc" then
- hitNode:GetScriptObject().fightTimer = 1
- end
- SpawnEffect(character2DNode)
- PlaySound("BigExplosion.wav")
- end
- end
- end
- -- Handle exiting the level when all coins have been gathered
- if nodeName == "Exit" and character.remainingCoins == 0 then
- -- Update UI
- local instructions = ui.root:GetChild("Instructions", true)
- instructions.text = "!!! WELL DONE !!!"
- instructions.position = IntVector2.ZERO
- -- Put the character outside of the scene and magnify him
- character2DNode.position = Vector3(-20, 0, 0)
- character2DNode:SetScale(1.2)
- end
- -- Handle falling into lava
- if nodeName == "Lava" then
- local body = character2DNode:GetComponent("RigidBody2D")
- body:ApplyLinearImpulse(Vector2(0, 1) * MOVE_SPEED, body.massCenter, true) -- Violently project character out of lava
- if not character2DNode:GetChild("Emitter", true) then
- character.wounded = true
- SpawnEffect(character2DNode)
- PlaySound("BigExplosion.wav")
- end
- end
- -- Handle climbing a slope
- if nodeName == "Slope" then
- character.onSlope = true
- end
- end
- function HandleCollisionEnd(eventType, eventData)
- -- Get colliding node
- local hitNode = eventData["NodeA"]:GetPtr("Node")
- if hitNode.name == "Imp" then
- hitNode = eventData["NodeB"]:GetPtr("Node")
- end
- local nodeName = hitNode.name
- local character = character2DNode:GetScriptObject()
- -- Handle leaving a rope or ladder
- if nodeName == "Climb" then
- if character.climb2 then
- character.climb2 = false
- else
- character.isClimbing = false
- local body = character2DNode:GetComponent("RigidBody2D")
- body.gravityScale = 1 -- Restore gravity
- end
- end
- if nodeName == "CanJump" then
- character.aboveClimbable = false
- end
- -- Handle leaving a slope
- if nodeName == "Slope" then
- character.onSlope = false
- -- Clear forces (should be performed by setting linear velocity to zero, but currently doesn't work)
- local body = character2DNode:GetComponent("RigidBody2D")
- body.linearVelocity = Vector2.ZERO
- body.awake = false
- body.awake = true
- end
- end
- -- Character2D script object class
- Character2D = ScriptObject()
- function Character2D:Start()
- self.wounded = false
- self.killed = false
- self.timer = 0
- self.maxCoins = 0
- self.remainingCoins = 0
- self.remainingLifes = 3
- self.isClimbing = false
- self.climb2 = false -- Used only for ropes, as they are split into 2 shapes
- self.aboveClimbable = false
- self.onSlope = false
- end
- function Character2D:Save(serializer)
- self.isClimbing = false -- Overwrite before auto-deserialization
- end
- function Character2D:Update(timeStep)
- if character2DNode == nil then
- return
- end
- -- Handle wounded/killed states
- if self.killed then
- return
- end
- if self.wounded then
- self:HandleWoundedState(timeStep)
- return
- end
- -- Set temporary variables
- local node = self.node
- local body = node:GetComponent("RigidBody2D")
- local animatedSprite = node:GetComponent("AnimatedSprite2D")
- local onGround = false
- local jump = false
- -- Collision detection (AABB query)
- local characterHalfSize = Vector2(0.16, 0.16)
- local collidingBodies = scene_:GetComponent("PhysicsWorld2D"):GetRigidBodies(Rect(node.worldPosition2D - characterHalfSize - Vector2(0, 0.1), node.worldPosition2D + characterHalfSize))
- if table.maxn(collidingBodies) > 1 and not self.isClimbing then
- onGround = true
- end
- -- Set direction
- local moveDir = Vector2.ZERO -- Reset
- if input:GetKeyDown(KEY_LEFT) or input:GetKeyDown(KEY_A) then
- moveDir = moveDir + Vector2.LEFT
- animatedSprite.flipX = false -- Flip sprite (reset to default play on the X axis)
- end
- if input:GetKeyDown(KEY_RIGHT) or input:GetKeyDown(KEY_D) then
- moveDir = moveDir + Vector2.RIGHT
- animatedSprite.flipX = true -- Flip sprite (flip animation on the X axis)
- end
- -- Jump
- if (onGround or self.aboveClimbable) and (input:GetKeyPress(KEY_UP) or input:GetKeyPress(KEY_W)) then
- jump = true
- end
- -- Climb
- if self.isClimbing then
- if not self.aboveClimbable and (input:GetKeyDown(KEY_UP) or input:GetKeyDown(KEY_W)) then
- moveDir = moveDir + Vector2.UP
- end
- if input:GetKeyDown(KEY_DOWN) or input:GetKeyDown(KEY_S) then
- moveDir = moveDir + Vector2.DOWN
- end
- end
- -- Move
- if not moveDir:Equals(Vector2.ZERO) or jump then
- if self.onSlope then
- body:ApplyForceToCenter(moveDir * MOVE_SPEED / 2, true) -- When climbing a slope, apply force (todo: replace by setting linear velocity to zero when will work)
- else
- node:Translate(Vector3(moveDir.x, moveDir.y, 0) * timeStep * 1.8)
- end
- if jump then
- body:ApplyLinearImpulse(Vector2(0, 0.17) * MOVE_SPEED, body.massCenter, true)
- end
- end
- -- Animate
- if input:GetKeyDown(KEY_SPACE) then
- if animatedSprite.animation ~= "attack" then
- animatedSprite:SetAnimation("attack", LM_FORCE_LOOPED)
- animatedSprite.speed = 1.5
- end
- elseif not moveDir:Equals(Vector2.ZERO) then
- if animatedSprite.animation ~= "run" then
- animatedSprite:SetAnimation("run")
- end
- elseif animatedSprite.animation ~= "idle" then
- animatedSprite:SetAnimation("idle")
- end
- end
- function Character2D:HandleWoundedState(timeStep)
- local node = self.node
- local body = node:GetComponent("RigidBody2D")
- local animatedSprite = node:GetComponent("AnimatedSprite2D")
- -- Play "hit" animation in loop
- if animatedSprite.animation ~= "hit" then
- animatedSprite:SetAnimation("hit", LM_FORCE_LOOPED)
- end
- -- Update timer
- self.timer = self.timer + timeStep
- -- End of timer
- if self.timer > 2 then
- -- Reset timer
- self.timer = 0
- -- Clear forces (should be performed by setting linear velocity to zero, but currently doesn't work)
- body.linearVelocity = Vector2.ZERO
- body.awake = false
- body.awake = true
- -- Remove particle emitter
- node:GetChild("Emitter", true):Remove()
- -- Update lifes UI and counter
- self.remainingLifes = self.remainingLifes - 1
- ui.root:GetChild("LifeText", true).text = self.remainingLifes -- Update lifes UI counter
- -- Reset wounded state
- self.wounded = false
- -- Handle death
- if self.remainingLifes == 0 then
- self:HandleDeath()
- return
- end
- -- Re-position the character to the nearest point
- if node.position.x < 15 then
- node.position = Vector3(1, 8, 0)
- else
- node.position = Vector3(18.8, 9.2, 0)
- end
- end
- end
- function Character2D:HandleDeath()
- local node = self.node
- local body = node:GetComponent("RigidBody2D")
- local animatedSprite = node:GetComponent("AnimatedSprite2D")
- -- Set state to 'killed'
- self.killed = true
- -- Update UI elements
- local instructions = ui.root:GetChild("Instructions", true)
- instructions.text = "!!! GAME OVER !!!"
- ui.root:GetChild("ExitButton", true).visible = true
- ui.root:GetChild("PlayButton", true).visible = true
- -- Show mouse cursor so that we can click
- input.mouseVisible = true
- -- Put character outside of the scene and magnify him
- node.position = Vector3(-20, 0, 0)
- node:SetScale(1.2)
- -- Play death animation once
- if animatedSprite.animation ~= "dead2" then
- animatedSprite:SetAnimation("dead2")
- end
- end
|