Skip to content

Instantly share code, notes, and snippets.

@nadar71
Last active August 29, 2015 14:17
Show Gist options
  • Save nadar71/1083f3b230a0cb49f21e to your computer and use it in GitHub Desktop.
Save nadar71/1083f3b230a0cb49f21e to your computer and use it in GitHub Desktop.
--[[
Perspective v2.0.2
A library for easily and smoothly integrating a virtual camera into your game.
Based on modified version of the Dusk camera system.
v2.0.2 adds a more stable tracking system and re-implements scrollX and scrollY
--]]
local lib_perspective = {}
--------------------------------------------------------------------------------
-- Localize
--------------------------------------------------------------------------------
local display_newGroup = display.newGroup
local display_remove = display.remove
local type = type
local table_insert = table.insert
local math_huge = math.huge
local math_nhuge = -math.huge
local clamp = function(v, l, h) return (v < l and l) or (v > h and h) or v end
--------------------------------------------------------------------------------
-- Create View
--------------------------------------------------------------------------------
lib_perspective.createView = function(layerCount)
------------------------------------------------------------------------------
-- Create view, internal object, and layers
------------------------------------------------------------------------------
local view = display_newGroup()
view.damping = 1
view.snapWhenFocused = true -- Do we instantly snap to the object when :setFocus() is called?
local isTracking
local internal -- So we can access it from inside the declaration
internal = {
trackingLevel = 1,
damping = 1,
scaleBoundsToScreen = true,
xScale = 1,
yScale = 1,
addX = display.contentCenterX,
addY = display.contentCenterY,
bounds = {
xMin = math_nhuge,
xMax = math_huge,
yMin = math_nhuge,
yMax = math_huge
},
scaledBounds = {
xMin = math_nhuge,
xMax = math_huge,
yMin = math_nhuge,
yMax = math_huge
},
trackFocus = true,
focus = nil,
viewX = 0,
viewY = 0,
getViewXY = function() if internal.focus then return internal.focus.x, internal.focus.y else return internal.viewX, internal.viewY end end,
layer = {},
updateAddXY = function() internal.addX = display.contentCenterX / view.xScale internal.addY = display.contentCenterY / view.yScale end
}
local layers = {}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
-- Internal Methods
------------------------------------------------------------------------------
------------------------------------------------------------------------------
------------------------------------------------------------------------------
-- Scale Bounds
------------------------------------------------------------------------------
internal.scaleBounds = function(doX, doY)
if internal.scaleBoundsToScreen then
local xMin = internal.bounds.xMin
local xMax = internal.bounds.xMax
local yMin = internal.bounds.yMin
local yMax = internal.bounds.yMax
local doX = doX and not ((xMin == math_nhuge) or (xMax == math_huge))
local doY = doY and not ((yMin == math_nhuge) or (yMax == math_huge))
if doX then
local scaled_xMin = xMin / view.xScale
local scaled_xMax = xMax - (scaled_xMin - xMin)
if scaled_xMax < scaled_xMin then
local hopDist = scaled_xMin - scaled_xMax
local halfDist = hopDist * 0.5
scaled_xMax = scaled_xMax + halfDist
scaled_xMin = scaled_xMin - halfDist
end
internal.scaledBounds.xMin = scaled_xMin
internal.scaledBounds.xMax = scaled_xMax
end
if doY then
local scaled_yMin = yMin / view.yScale
local scaled_yMax = yMax - (scaled_yMin - yMin)
if scaled_yMax < scaled_yMin then
local hopDist = scaled_yMin - scaled_yMax
local halfDist = hopDist * 0.5
scaled_yMax = scaled_yMax + halfDist
scaled_yMin = scaled_yMin - halfDist
end
internal.scaledBounds.yMin = scaled_yMin
internal.scaledBounds.yMax = scaled_yMax
end
else
camera.scaledBounds.xMin, camera.scaledBounds.xMax, camera.scaledBounds.yMin, camera.scaledBounds.yMax = camera.bounds.xMin, camera.bounds.xMax, camera.bounds.yMin, camera.bounds.yMax
end
end
------------------------------------------------------------------------------
-- Process Viewpoint
------------------------------------------------------------------------------
internal.processViewpoint = function()
if internal.damping ~= view.damping then internal.trackingLevel = 1 / view.damping internal.damping = view.damping end
if internal.trackFocus then
local x, y = internal.getViewXY()
if view.xScale ~= internal.xScale or view.yScale ~= internal.yScale then internal.updateAddXY() end
if view.xScale ~= internal.xScale then internal.xScale = view.xScale internal.scaleBounds(true, false) end
if view.yScale ~= internal.yScale then internal.yScale = view.yScale internal.scaleBounds(false, true) end
x = clamp(x, internal.scaledBounds.xMin, internal.scaledBounds.xMax)
y = clamp(y, internal.scaledBounds.yMin, internal.scaledBounds.yMax)
internal.viewX, internal.viewY = x, y
end
end
------------------------------------------------------------------------------
------------------------------------------------------------------------------
-- Public Methods
------------------------------------------------------------------------------
------------------------------------------------------------------------------
------------------------------------------------------------------------------
-- Append Layer
------------------------------------------------------------------------------
view.appendLayer = function()
local layer = display_newGroup()
layer.xParallax, layer.yParallax = 1, 1
view:insert(layer)
layer:toBack()
table_insert(layers, layer)
layer._perspectiveIndex = #layers
internal.layer[#layers] = {
x = 0,
y = 0,
xOffset = 0,
yOffset = 0
}
function layer:setCameraOffset(x, y) internal.layer[layer._perspectiveIndex].xOffset, internal.layer[layer._perspectiveIndex].yOffset = x, y end
end
------------------------------------------------------------------------------
-- Add an Object to the Camera
------------------------------------------------------------------------------
function view:add(obj, l, isFocus)
local l = l or 4
layers[l]:insert(obj)
obj._perspectiveLayer = l
if isFocus then view:setFocus(obj) end
-- Move an object to a layer
function obj:toLayer(newLayer) if layer[newLayer] then layer[newLayer]:insert(obj) obj._perspectiveLayer = newLayer end end
--Move an object back a layer
function obj:back() if layer[obj._perspectiveLayer + 1] then layer[obj._perspectiveLayer + 1]:insert(obj) obj._perspectiveLayer = obj.layer + 1 end end
--Moves an object forwards a layer
function obj:forward() if layer[obj._perspectiveLayer - 1] then layer[obj._perspectiveLayer - 1]:insert(obj) obj._perspectiveLayer = obj.layer - 1 end end
--Moves an object to the very front of the camera
function obj:toCameraFront() layer[1]:insert(obj) obj._perspectiveLayer = 1 obj:toFront() end
--Moves an object to the very back of the camera
function obj:toCameraBack() layer[#layers]:insert(obj) obj._perspectiveLayer = #layers obj:toBack() end
end
------------------------------------------------------------------------------
-- Main Tracking Function
------------------------------------------------------------------------------
function view:trackFocus()
internal.processViewpoint()
local viewX, viewY = internal.viewX, internal.viewY
layers[1].xParallax, layers[1].yParallax = 1, 1
for i = 1, #layers do
local addX, addY = internal.addX, internal.addY
local layerX, layerY = internal.layer[i].x, internal.layer[i].y
local diffX = (-viewX - layerX)
local diffY = (-viewY - layerY)
local incrX = diffX
local incrY = diffY
internal.layer[i].x = layerX + incrX
internal.layer[i].y = layerY + incrY
layers[i].x = (layers[i].x - (layers[i].x - (internal.layer[i].x + addX) * layers[i].xParallax) * internal.trackingLevel)
layers[i].y = (layers[i].y - (layers[i].y - (internal.layer[i].y + addY) * layers[i].yParallax) * internal.trackingLevel)
end
view.scrollX, view.scrollY = layers[1].x, layers[1].y
end
------------------------------------------------------------------------------
-- Set the Camera Bounds
------------------------------------------------------------------------------
function view:setBounds(x1, x2, y1, y2)
local xMin, xMax, yMin, yMax
if x1 ~= nil then if not x1 then xMin = math_nhuge else xMin = x1 end end
if x2 ~= nil then if not x2 then xMax = math_huge else xMax = x2 end end
if y1 ~= nil then if not y1 then yMin = math_nhuge else yMin = y1 end end
if y2 ~= nil then if not y2 then yMax = math_huge else yMax = y2 end end
internal.bounds.xMin = xMin
internal.bounds.xMax = xMax
internal.bounds.yMin = yMin
internal.bounds.yMax = yMax
internal.scaleBounds(true, true)
end
------------------------------------------------------------------------------
-- Miscellaneous Functions
------------------------------------------------------------------------------
-- Begin auto-tracking
function view:track() if not isTracking then Runtime:addEventListener("enterFrame", view.trackFocus) isTracking = true end end
-- Stop auto-tracking
function view:cancel() if isTracking then Runtime:removeEventListener("enterFrame", view.trackFocus) isTracking = false end end
-- Remove an object from the view
function view:remove(obj) if obj and obj._perspectiveLayer then layers[obj._perspectiveLayer]:remove(obj) end end
-- Set the view's focus
function view:setFocus(obj) if obj then internal.focus = obj end if view.snapWhenFocused then view.snap() end end
-- Snap the view to the focus point
function view:snap() local t = internal.trackingLevel local d = internal.damping internal.trackingLevel = 1 internal.damping = view.damping view:trackFocus() internal.trackingLevel = t internal.damping = d end
-- Move the view to a point
function view:toPoint(x, y) view:cancel() local newFocus = {x = x, y = y} view:setFocus(newFocus) view:track() return newFocus end
-- Get a layer of the view
function view:layer(n) return layers[n] end
-- Destroy the view
function view:destroy() view:cancel() for i = 1, #layers do for o = 1, layers[i].numChildren do layers[i]:remove(layers[i][o]) end end display_remove(view) view = nil return true end
-- Set layer parallax
function view:setParallax(...) for i = 1, #arg do if type(arg[i]) == "table" then layers[i].xParallax, layers[i].yParallax = arg[i][1], arg[i][2] else layers[i].xParallax, layers[i].yParallax = arg[i], arg[i] end end end
-- Get number of layers
function view:layerCount() return #layers end
------------------------------------------------------------------------------
-- Build Layers
------------------------------------------------------------------------------
for i = layerCount or 8, 1, -1 do view.appendLayer() end
return view
end
return lib_perspective
local perspective = require("perspective") -- Include the library
local camera = perspective.createView([numLayers]) -- Optional parameter is the number of layers
camera.appendLayer() -- Add a new layer to the back of the camera
camera:add(obj, layer, [isFocus]) -- obj is any display object; layer is the layer to add the object to (lower numbers = front of camera); isFocus is a convenience value for whether to initially set the focus to this object
camera:trackFocus() -- "Tick" the camera once
camera:setBounds(x1, x2, y1, y2) -- Set the bounding box that tracking is confined to. Any values that evaluate to Boolean negative are interpreted as infinite; infinite values apply to an entire axis (if x2 is infinite and x1 is not, X-axis constraint will be disabled)
camera:track() -- Begin auto-tracking. This eliminates the need to update the camera every frame
camera:cancel() -- Stop auto-tracking
camera:remove(obj) -- Remove an object from the camera
camera:setFocus(obj) -- Set the camera focus to an object
camera:snap() -- Snap the camera to the focus point (ignores damping)
camera:toPoint(x, y) -- Change the camera's focus to an X,Y point
camera:layer(n) -- Get a layer object of the camera
camera:destroy() -- Destroy the camera and clear memory
camera:setParallax(...) -- Set parallax quickly for multiple layers. Each value provided will apply to the correspondingly indexed layer. Provide a table with {x, y} values for an argument to set X and Y parallax independently
camera:layerCount() -- Get the number of layers in the camera
----------------------
camera.damping = n -- "Fluidity" the camera implements with tracking. Higher values will make the camera move more slowly; values approaching 1 will make the camera move more rigidly
layer.xParallax = n -- X-parallax ratio of a layer; expressed as fraction of "normal" movement
layer.yParallax = n -- Y-parallax ratio of a layer; expressed as fraction of "normal" movement
--------------------------------------------------------------------------------
-- Perspective Demo
--------------------------------------------------------------------------------
display.setStatusBar(display.HiddenStatusBar)
--------------------------------------------------------------------------------
-- Localize
--------------------------------------------------------------------------------
local require = require
local perspective = require("perspective")
local function forcesByAngle(totalForce, angle) local forces = {} local radians = -math.rad(angle) forces.x = math.cos(radians) * totalForce forces.y = math.sin(radians) * totalForce return forces end
--------------------------------------------------------------------------------
-- Build Camera
--------------------------------------------------------------------------------
local camera = perspective.createView()
--------------------------------------------------------------------------------
-- Build Player
--------------------------------------------------------------------------------
local player = display.newPolygon(0, 0, {-50,-30, -50,30, 50,0})
player.strokeWidth = 6
player:setFillColor(0, 0, 0, 0)
player.anchorX = 0.2 -- Slightly more "realistic" than center-point rotating
-- Some various movement parameters
player.angularVelocity = 0 -- Speed at which player rotates
player.angularAcceleration = 1.05 -- Angular acceleration rate
player.angularDamping = 0.9 -- Angular damping rate
player.angularMax = 10 -- Max angular velocity
player.moveSpeed = 0 -- Current movement speed
player.linearDamping = 0 -- Linear damping rate
player.linearAcceleration = 1.05 -- Linear acceleration rate
player.linearMax = 10 -- Max linear velocity
camera:add(player, 1) -- Add player to layer 1 of the camera
--------------------------------------------------------------------------------
-- "Scenery"
--------------------------------------------------------------------------------
local scene = {}
for i = 1, 100 do
scene[i] = display.newCircle(0, 0, 10)
scene[i].x = math.random(display.screenOriginX, display.contentWidth * 3)
scene[i].y = math.random(display.screenOriginY, display.contentHeight)
scene[i]:setFillColor(math.random(100) * 0.01, math.random(100) * 0.01, math.random(100) * 0.01)
camera:add(scene[i], math.random(2, camera:layerCount()))
end
camera:setParallax(1, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3) -- Here we set parallax for each layer in descending order
--------------------------------------------------------------------------------
-- Movement Buttons
--------------------------------------------------------------------------------
local m = {}
m.result = "none"
m.rotate = {}
m.rotate.left = display.newRect(0, 0, 60, 60)
m.rotate.left.x = display.screenOriginX + m.rotate.left.contentWidth + 10
m.rotate.left.y = display.contentHeight - m.rotate.left.contentHeight - 10
m.rotate.left.result = "rotate:left"
m.rotate.right = display.newRect(0, 0, 60, 60)
m.rotate.right.x = display.contentWidth - display.screenOriginX - m.rotate.right.contentWidth - 10
m.rotate.right.y = display.contentHeight - m.rotate.right.contentHeight - 10
m.rotate.right.result = "rotate:right"
m.forward = display.newRect(0, 0, display.contentWidth * 0.75, 60)
m.forward.x = display.contentCenterX
m.forward.y = display.contentHeight - m.forward.contentHeight - 10
m.forward.result = "move"
--------------------------------------------------------------------------------
-- Touch Movement Buttons
--------------------------------------------------------------------------------
function m.touch(event)
local t = event.target
if "began" == event.phase then
display.getCurrentStage():setFocus(t)
t.isFocus = true
m.result = t.result
if t.result == "rotate:left" then
player.angularVelocity = -2
elseif t.result == "rotate:right" then
player.angularVelocity = 2
elseif t.result == "move" then
player.moveSpeed = 2
end
elseif t.isFocus then
if "moved" == event.phase then
elseif "ended" == event.phase then
display.getCurrentStage():setFocus(nil)
t.isFocus = false
m.result = "none"
end
end
end
m.rotate.left:addEventListener("touch", m.touch)
m.rotate.right:addEventListener("touch", m.touch)
m.forward:addEventListener("touch", m.touch)
--------------------------------------------------------------------------------
-- Runtime Loop
--------------------------------------------------------------------------------
local function enterFrame(event)
if m.result == "rotate:left" then
player.angularVelocity = player.angularVelocity * player.angularAcceleration
player.angularVelocity = math.max(player.angularVelocity, -player.angularMax)
player.moveSpeed = player.moveSpeed * player.linearDamping
elseif m.result == "rotate:right" then
player.angularVelocity = player.angularVelocity * player.angularAcceleration
player.angularVelocity = math.min(player.angularVelocity, player.angularMax)
player.moveSpeed = player.moveSpeed * player.linearDamping
elseif m.result == "move" then
player.moveSpeed = player.moveSpeed * player.linearAcceleration
player.moveSpeed = math.min(player.moveSpeed, player.linearMax)
player.angularVelocity = player.angularVelocity * player.angularDamping
elseif m.result == "none" then
player.angularVelocity = player.angularVelocity * player.angularDamping
player.moveSpeed = player.moveSpeed * player.linearDamping
end
local forces = forcesByAngle(player.moveSpeed, 360 - player.rotation)
player:translate(forces.x, forces.y)
player:rotate(player.angularVelocity)
end
--------------------------------------------------------------------------------
-- Add Listeners
--------------------------------------------------------------------------------
Runtime:addEventListener("enterFrame", enterFrame)
camera.damping = 10 -- A bit more fluid tracking
camera:setFocus(player) -- Set the focus to the player
camera:track() -- Begin auto-tracking
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment