Urho3D/bin/Data/LuaScripts/50_Urho2DPlatformer.lua

454 lines
16 KiB
Lua

-- 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