Difference between revisions of "Game Camera"
From GiderosMobile
m (Text replacement - "<source" to "<syntaxhighlight") |
m (Text replacement - "</source>" to "</syntaxhighlight>") |
||
Line 15: | Line 15: | ||
camera = GCam.new(yourScene [, anchorX, anchorY]) -- anchor by default is (0.5, 0.5) | camera = GCam.new(yourScene [, anchorX, anchorY]) -- anchor by default is (0.5, 0.5) | ||
stage:addChild(camera) | stage:addChild(camera) | ||
− | </ | + | </syntaxhighlight> |
'''The full class''' | '''The full class''' | ||
Line 722: | Line 722: | ||
self:updateClip() | self:updateClip() | ||
end | end | ||
− | </ | + | </syntaxhighlight> |
=== Kinetic Zoom Camera === | === Kinetic Zoom Camera === | ||
Line 1,069: | Line 1,069: | ||
end | end | ||
end | end | ||
− | </ | + | </syntaxhighlight> |
Revision as of 14:29, 13 July 2023
Here you will find various resources to help you create games and apps in Gideros Studio.
note: you may have to provide your own assets (fonts, gfx, …).
GCam
A fantastic camera for Gideros from MultiPain https://github.com/MultiPain/Gideros_GCam
It has zoom, shake, follow, and more...
Usage:
-- yourScene is a Sprite in which you put all your graphics
camera = GCam.new(yourScene [, anchorX, anchorY]) -- anchor by default is (0.5, 0.5)
stage:addChild(camera)
The full class
local atan2,sqrt,cos,sin,log,random = math.atan2,math.sqrt,math.cos,math.sin,math.log,math.random
local PI = math.pi
-- ref:
-- https://www.gamedev.net/tutorials/programming/general-and-gameplay-programming/a-brief-introduction-to-lerp-r4954/#:~:text=Linear%20interpolation%20(sometimes%20called%20'lerp,0..1%5D%20range.
local function smoothOver(dt, smoothTime, convergenceFraction) return 1 - (1 - convergenceFraction)^(dt / smoothTime) end
local function lerp(a,b,t) return a + (b-a) * t end
local function clamp(v,mn,mx) return (v><mx)<>mn end
local function map(v, minSrc, maxSrc, minDst, maxDst, clampValue)
local newV = (v - minSrc) / (maxSrc - minSrc) * (maxDst - minDst) + minDst
return not clampValue and newV or clamp(newV, minDst >< maxDst, minDst <> maxDst)
end
local function distance(x1,y1, x2,y2) return (x2-x1)^2 + (y2-y1)^2 end
local function distanceSq(x1,y1, x2,y2) return sqrt((x2-x1)^2 + (y2-y1)^2) end
local function angle(x1,y1, x2,y2) return atan2(y2-y1,x2-x1) end
local function setMeshAsCircle(m, ox, oy, rad_in_x, rad_in_y, rad_out_x, rad_out_y, color, alpha, edges)
edges = edges or 16
local step = (PI*2)/edges
local vi = m:getVertexArraySize() + 1
local ii = m:getIndexArraySize() + 1
local svi = vi
local sii = ii
for i = 0, edges-1 do
local ang = i * step
local cosa = cos(ang)
local sina = sin(ang)
local x_in = ox + rad_in_x * cosa
local y_in = oy + rad_in_y * sina
local x_out = ox + rad_out_x * cosa
local y_out = oy + rad_out_y * sina
m:setVertex(vi+0,x_in,y_in)
m:setVertex(vi+1,x_out,y_out)
m:setColor(vi+0,color, alpha)
m:setColor(vi+1,color, alpha)
vi += 2
if i <= edges-2 then
local si = (svi-1)+((i+1)*2)-1
m:setIndex(ii+0,si)
m:setIndex(ii+1,si+1)
m:setIndex(ii+2,si+3)
m:setIndex(ii+3,si)
m:setIndex(ii+4,si+3)
m:setIndex(ii+5,si+2)
ii += 6
end
end
local si = (svi-1)+(edges*2)-1
m:setIndex(ii+0,si)
m:setIndex(ii+1,si+1)
m:setIndex(ii+2,svi)
m:setIndex(ii+3,si+1)
m:setIndex(ii+4,svi+1)
m:setIndex(ii+5,svi)
end
local function outExponential(ratio) if ratio == 1 then return 1 end return 1-2^(-10 * ratio) end
GCam = Core.class(Sprite)
GCam.SHAKE_DELAY = 10
function GCam:init(content, ax, ay)
assert(content ~= stage, "bad argument #1 (сontent should be different from the 'stage')")
self.viewport = Viewport.new()
self.viewport:setContent(content)
self.content = Sprite.new()
self.content:addChild(self.viewport)
self:addChild(self.content)
self.matrix = Matrix.new()
self.viewport:setMatrix(self.matrix)
-- some vars
self.w = 0
self.h = 0
self.ax = ax or 0.5
self.ay = ay or 0.5
self.x = 0
self.y = 0
self.zoomFactor = 1
self.rotation = 0
self.followOX = 0
self.followOY = 0
-- Bounds
self.leftBound = -1000000
self.rightBound = 1000000
self.topBound = -1000000
self.bottomBound = 1000000
-- Shaker
self.shakeTimer = Timer.new(GCam.SHAKE_DELAY, 1)
self.shakeDistance = 0
self.shakeCount = 0
self.shakeAmount = 0
self.shakeTimer:addEventListener("timerComplete", self.shakeDone, self)
self.shakeTimer:addEventListener("timer", self.shakeUpdate, self)
self.shakeTimer:stop()
-- Follow
-- 0 - instant move
self.smoothX = 0.9
self.smoothY = 0.9
-- Dead zone
self.deadWidth = 50
self.deadHeight = 50
self.deadRadius = 25
-- Soft zone
self.softWidth = 150
self.softHeight = 150
self.softRadius = 75
---------------------------------------
------------- debug stuff -------------
---------------------------------------
self.__debugSoftColor = 0xffff00
self.__debugAnchorColor = 0xff0000
self.__debugDotColor = 0x00ff00
self.__debugAlpha = 0.5
self.__debugRMesh = Mesh.new()
self.__debugRMesh:setIndexArray(1,3,4, 1,2,4, 1,3,7, 3,5,7, 2,4,8, 4,8,6, 5,6,8, 5,8,7, 9,10,11, 9,11,12, 13,14,15, 13,15,16, 17,18,19, 17,19,20)
self.__debugRMesh:setColorArray(self.__debugSoftColor,self.__debugAlpha, self.__debugSoftColor,self.__debugAlpha, self.__debugSoftColor,self.__debugAlpha, self.__debugSoftColor,self.__debugAlpha, self.__debugSoftColor,self.__debugAlpha, self.__debugSoftColor,self.__debugAlpha, self.__debugSoftColor,self.__debugAlpha, self.__debugSoftColor,self.__debugAlpha, self.__debugAnchorColor,self.__debugAlpha, self.__debugAnchorColor,self.__debugAlpha, self.__debugAnchorColor,self.__debugAlpha, self.__debugAnchorColor,self.__debugAlpha, self.__debugAnchorColor,self.__debugAlpha, self.__debugAnchorColor,self.__debugAlpha, self.__debugAnchorColor,self.__debugAlpha, self.__debugAnchorColor,self.__debugAlpha, self.__debugDotColor,self.__debugAlpha, self.__debugDotColor,self.__debugAlpha, self.__debugDotColor,self.__debugAlpha, self.__debugDotColor,self.__debugAlpha)
self.__debugCMesh = Mesh.new()
---------------------------------------
---------------------------------------
---------------------------------------
self:setShape("rectangle")
self:setAnchor(self.ax,self.ay)
self:updateClip()
end
---------------------------------------------------
------------------- DEBUG STUFF -------------------
---------------------------------------------------
function GCam:setDebug(flag)
self.__debug__ = flag
if flag then
if self.shapeType == "rectangle" then
self.__debugCMesh:removeFromParent()
self:addChild(self.__debugRMesh)
elseif self.shapeType == "circle" then
self.__debugRMesh:removeFromParent()
self:addChild(self.__debugCMesh)
end
self:debugUpdate()
self:debugUpdate(true, 0, 0)
else
self.__debugCMesh:removeFromParent()
self.__debugRMesh:removeFromParent()
end
end
function GCam:switchDebug()
self:setDebug(not self.__debug__)
end
function GCam:debugMeshUpdate()
local w,h = self.w, self.h
local zoom = self.zoomFactor
local rot = self.rotation
local ax,ay = w * self.ax,h * self.ay
local TS = 1
local off = w <> h
if self.shapeType == "rectangle" then
local dw = (self.deadWidth * zoom) / 2
local dh = (self.deadHeight * zoom) / 2
local sw = (self.softWidth * zoom) / 2
local sh = (self.softHeight * zoom) / 2
--[[
Mesh vertices
1-----------------2
| \ soft zone / |
| 3-----------4 |
| | dead zone | |
| 5-----------6 |
| / \ |
7-----------------8
]]
self.__debugRMesh:setVertexArray(
ax-sw,ay-sh,
ax+sw,ay-sh,
ax-dw,ay-dh,
ax+dw,ay-dh,
ax-dw,ay+dh,
ax+dw,ay+dh,
ax-sw,ay+sh,
ax+sw,ay+sh,
ax-TS,-off, ax+TS,-off,
ax+TS,h+off, ax-TS,h+off,
-off,ay-TS, -off,ay+TS,
w+off,ay+TS, w+off,ay-TS
)
self.__debugRMesh:setAnchorPosition(ax,ay)
self.__debugRMesh:setPosition(ax,ay)
self.__debugRMesh:setRotation(rot)
elseif self.shapeType == "circle" then
--[[
Mesh:
-- first 4 vertex is green target point
1--2
| |
4--3
next, vertical anchor line
5--6
| |
| |
| |
8--7
next, horizontal anchor line
9--------10
| |
12-------11
and finaly, circle
8 edges "circle" look like this:
24--------------26--------------28
| \ soft zone | / |
| 23-----------25-----------27 |
| | | |
| | dead | |
22--21 zone 13--14
| | | |
| | | |
| 19-----------17-----------15 |
| / | \ |
20--------------18--------------16
]]
local dr = self.deadRadius * zoom
local sr = self.softRadius * zoom
self.__debugCMesh:setVertexArray(0,0,0,0,0,0,0,0,ax-TS,-off, ax+TS,-off,ax+TS,h+off, ax-TS,h+off, -off,ay-TS, -off,ay+TS, w+off,ay+TS, w+off,ay-TS)
self.__debugCMesh:setIndexArray(1,2,3, 1,3,4, 5,6,7, 5,7,8, 9,10,11, 9,11,12)
self.__debugCMesh:setColorArray(self.__debugDotColor,self.__debugAlpha, self.__debugDotColor,self.__debugAlpha, self.__debugDotColor,self.__debugAlpha, self.__debugDotColor,self.__debugAlpha, self.__debugAnchorColor,self.__debugAlpha, self.__debugAnchorColor,self.__debugAlpha, self.__debugAnchorColor,self.__debugAlpha, self.__debugAnchorColor,self.__debugAlpha, self.__debugAnchorColor,self.__debugAlpha, self.__debugAnchorColor,self.__debugAlpha, self.__debugAnchorColor,self.__debugAlpha, self.__debugAnchorColor,self.__debugAlpha)
setMeshAsCircle(self.__debugCMesh, ax,ay, dr, dr, sr, sr, self.__debugSoftColor,self.__debugAlpha, 32)
self.__debugCMesh:setAnchorPosition(ax,ay)
self.__debugCMesh:setPosition(ax,ay)
self.__debugCMesh:setRotation(rot)
end
end
function GCam:debugUpdate(dotOnly, gx,gy)
if self.__debug__ then
if dotOnly then
local zoom = self:getZoom()
local ax = self.w * self.ax
local ay = self.h * self.ay
local size = 4 * zoom
local x = (gx * zoom - self.x * zoom) + ax
local y = (gy * zoom - self.y * zoom) + ay
if self.shapeType == "rectangle" then
self.__debugRMesh:setVertex(17, x-size,y-size)
self.__debugRMesh:setVertex(18, x+size,y-size)
self.__debugRMesh:setVertex(19, x+size,y+size)
self.__debugRMesh:setVertex(20, x-size,y+size)
elseif self.shapeType == "circle" then
self.__debugCMesh:setVertex(1, x-size,y-size)
self.__debugCMesh:setVertex(2, x+size,y-size)
self.__debugCMesh:setVertex(3, x+size,y+size)
self.__debugCMesh:setVertex(4, x-size,y+size)
end
else
self:debugMeshUpdate()
end
end
end
---------------------------------------------------
----------------- RESIZE LISTENER -----------------
---------------------------------------------------
-- set camera size to window size
function GCam:setAutoSize(flag)
if flag then
self:addEventListener(Event.APPLICATION_RESIZE, self.appResize, self)
self:appResize()
elseif self:hasEventListener(Event.APPLICATION_RESIZE) then
self:removeEventListener(Event.APPLICATION_RESIZE, self.appResize, self)
end
end
function GCam:appResize()
local minX,minY,maxX,maxY = application:getLogicalBounds()
self.w = maxX+minX
self.h = maxY+minY
self.matrix:setPosition(self.w * self.ax,self.h * self.ay)
self.viewport:setMatrix(self.matrix)
self:debugUpdate()
self:updateClip()
end
---------------------------------------------------
---------------------- SHAPES ---------------------
---------------------------------------------------
function GCam:rectangle(dt,x,y)
local sw = self.softWidth / 2
local sh = self.softHeight / 2
local dw = self.deadWidth / 2
local dh = self.deadHeight / 2
local dstX = self.x
local dstY = self.y
-- X smoothing
if x > self.x + dw then -- out of dead zone on right side
local dx = x - self.x - dw
local fx = smoothOver(dt, self.smoothX, 0.99)
dstX = lerp(self.x, self.x + dx, fx)
elseif x < self.x - dw then -- out of dead zone on left side
local dx = self.x - dw - x
local fx = smoothOver(dt, self.smoothX, 0.99)
dstX = lerp(self.x, self.x - dx, fx)
end
-- clamp to soft zone
dstX = clamp(dstX, x - sw,x + sw)
-- Y smoothing
if y > self.y + dh then -- out of dead zone on bottom side
local dy = y - self.y - dh
local fy = smoothOver(dt, self.smoothY, 0.99)
dstY = lerp(self.y, self.y + dy, fy)
elseif y < self.y - dh then -- out of dead zone on top side
local dy = self.y - dh - y
local fy = smoothOver(dt, self.smoothY, 0.99)
dstY = lerp(self.y, self.y - dy, fy)
end
-- clamp to soft zone
dstY = clamp(dstY, y - sh,y + sh)
return dstX, dstY
end
function GCam:circle(dt,x,y)
local dr = self.deadRadius
local sr = self.softRadius
local dstX, dstY = self.x, self.y
local d = distanceSq(self.x, self.y, x, y)
if d > dr and d <= sr then -- out of dead zone on bottom side
local offset = d-dr
local ang = angle(self.x, self.y, x, y)
local fx = smoothOver(dt, self.smoothX, 0.99)
local fy = smoothOver(dt, self.smoothY, 0.99)
dstX = lerp(self.x, self.x + cos(ang) * offset, fx)
dstY = lerp(self.y, self.y + sin(ang) * offset, fy)
elseif d > sr then
local ang = angle(self.x, self.y, x, y)
local offset = d-sr+120*dt
dstX = self.x + cos(ang) * offset
dstY = self.y + sin(ang) * offset
end
return dstX, dstY
end
-- shapeType(string): function name
-- can be "rectangle" or "circle"
-- you can create custom shape by
-- adding a new method to a class
-- then use its name as shapeType
function GCam:setShape(shapeType)
self.shapeType = shapeType
self.shapeFunction = self[shapeType]
assert(self.shapeFunction ~= nil, "[GCam]: shape with name \""..shapeType.."\" does not exist")
assert(type(self.shapeFunction) == "function", "[GCam]: incorrect shape type. Must be\"function\", but was: "..type(shapeFunction))
-- DEBUG --
self:setDebug(self.__debug__)
self:debugUpdate()
self:debugUpdate(true, 0, 0)
end
---------------------------------------------------
---------------------- UPDATE ---------------------
---------------------------------------------------
function GCam:update(dt)
local obj = self.followObj
if obj then
local x,y = obj:getPosition()
x += self.followOX
y += self.followOY
local dstX, dstY = self:shapeFunction(dt,x,y)
if self.x ~= dstX or self.y ~= dstY then
self:goto(dstX,dstY)
end
self:debugUpdate(true,x,y)
end
self:updateClip()
end
---------------------------------------------------
--------------------- FOLLOW ----------------------
---------------------------------------------------
function GCam:setFollow(obj)
self.followObj = obj
end
function GCam:setFollowOffset(x,y)
self.followOX = x
self.followOY = y
end
---------------------------------------------------
---------------------- SHAKE ----------------------
---------------------------------------------------
-- duration (number): time is s.
-- distance (number): maximum shake offset
function GCam:shake(duration, distance)
self.shaking = true
self.shakeCount = 0
self.shakeDistance = distance or 100
self.shakeAmount = (duration*1000) // GCam.SHAKE_DELAY
self.shakeTimer:reset()
self.shakeTimer:setRepeatCount(self.shakeAmount)
self.shakeTimer:start()
end
function GCam:shakeDone()
self.shaking = false
self.shakeCount = 0
self.content:setPosition(0,0)
end
function GCam:shakeUpdate()
self.shakeCount += 1
local amplitude = 1 - outExponential(self.shakeCount/self.shakeAmount)
local hd = self.shakeDistance / 2
local x = random(-hd,hd)*amplitude
local y = random(-hd,hd)*amplitude
self.content:setPosition(x, y)
end
--------------------------------------------------
--------------------- ZONES ----------------------
--------------------------------------------------
-- Camera intepolate its position towards target
-- w (number): soft zone width
-- h (number): soft zone height
function GCam:setSoftSize(w,h)
self.softWidth = w
self.softHeight = h or w
self:debugUpdate()
end
function GCam:setSoftWidth(w)
self.softWidth = w
self:debugUpdate()
end
function GCam:setSoftHeight(h)
self.softHeight = h
self:debugUpdate()
end
-- r (number): soft zone radius (only if shape type is "circle")
function GCam:setSoftRadius(r)
self.softRadius = r
self:debugUpdate()
end
-- Camera does not move in dead zone
-- w (number): dead zone width
-- h (number): dead zone height
function GCam:setDeadSize(w,h)
self.deadWidth = w
self.deadHeight = h or w
self:debugUpdate()
end
function GCam:setDeadWidth(w)
self.deadWidth = w
self:debugUpdate()
end
function GCam:setDeadHeight(h)
self.deadHeight = h
self:debugUpdate()
end
function GCam:setDeadRadius(r)
self.deadRadius = r
self:debugUpdate()
end
-- Smooth factor
-- x (number):
-- y (number):
function GCam:setSmooth(x,y)
self.smoothX = x
self.smoothY = y or x
end
function GCam:setSmoothX(x)
self.smoothX = x
end
function GCam:setSmoothY(y)
self.smoothY = y
end
--------------------------------------------------
--------------------- BOUNDS ---------------------
--------------------------------------------------
function GCam:updateBounds()
local x = clamp(self.x, self.leftBound, self.rightBound)
local y = clamp(self.y, self.topBound, self.bottomBound)
if x ~= self.x or y ~= self.y then
self:goto(x,y)
end
end
-- Camera can move only inside given bbox
function GCam:setBounds(left, top, right, bottom)
self.leftBound = left or 0
self.topBound = top or 0
self.rightBound = right or 0
self.bottomBound = bottom or 0
self:updateBounds()
end
function GCam:setLeftBound(left)
self.leftBound = left or 0
self:updateBounds()
end
function GCam:setTopBound(top)
self.topBound = top or 0
self:updateBounds()
end
function GCam:setRightBound(right)
self.rightBound = right or 0
self:updateBounds()
end
function GCam:setBottomBound(bottom)
self.bottomBound = bottom or 0
self:updateBounds()
end
function GCam:getBounds()
return self.leftBound, self.topBound, self.rightBound, self.bottomBound
end
---------------------------------------------------
----------------- TRANSFORMATIONS -----------------
---------------------------------------------------
function GCam:move(dx, dy)
self:goto(self.x + dx, self.y + dy)
end
function GCam:zoom(value)
local v = self.zoomFactor + value
if v > 0 then
self:setZoom(v)
end
end
function GCam:rotate(ang)
self.rotation += ang
self:setAngle(self.rotation)
end
------------------------------------------
---------------- POSITION ----------------
------------------------------------------
function GCam:rawGoto(x,y)
x = clamp(x, self.leftBound, self.rightBound)
y = clamp(y, self.topBound, self.bottomBound)
self.matrix:setAnchorPosition(x,y)
self.viewport:setMatrix(self.matrix)
end
function GCam:goto(x,y)
x = clamp(x, self.leftBound, self.rightBound)
y = clamp(y, self.topBound, self.bottomBound)
self.x = x
self.y = y
self.matrix:setAnchorPosition(x,y)
self.viewport:setMatrix(self.matrix)
end
function GCam:gotoX(x)
x = clamp(x, self.leftBound, self.rightBound)
self.x = x
self.matrix:setAnchorPosition(x,self.y)
self.viewport:setMatrix(self.matrix)
end
function GCam:gotoY(y)
y = clamp(y, self.topBound, self.bottomBound)
self.y = y
self.matrix:setAnchorPosition(self.x,y)
self.viewport:setMatrix(self.matrix)
end
------------------------------------------
------------------ ZOOM ------------------
------------------------------------------
function GCam:setZoom(zoom)
self.zoomFactor = zoom
self.matrix:setScale(zoom, zoom, 1)
self.viewport:setMatrix(self.matrix)
self:debugUpdate()
end
function GCam:getZoom()
return self.zoomFactor
end
------------------------------------------
---------------- ROTATION ----------------
------------------------------------------
function GCam:setAngle(angle)
self.rotation = angle
self.matrix:setRotationZ(angle)
self.viewport:setMatrix(self.matrix)
self:debugUpdate()
end
function GCam:getAngle()
return self.matrix:getRotationZ()
end
------------------------------------------
-------------- ANCHOR POINT --------------
------------------------------------------
function GCam:setAnchor(anchorX, anchorY)
self.ax = anchorX
self.ay = anchorY
self.matrix:setPosition(self.w * anchorX,self.h * anchorY)
self.viewport:setMatrix(self.matrix)
self:debugUpdate()
end
function GCam:setAnchorX(anchorX)
self.ax = anchorX
self.matrix:setPosition(self.w * anchorX,self.h * self.ay)
self.viewport:setMatrix(self.matrix)
self:debugUpdate()
end
function GCam:setAnchorY(anchorY)
self.ay = anchorY
self.matrix:setPosition(self.w * self.ax,self.h * anchorY)
self.viewport:setMatrix(self.matrix)
self:debugUpdate()
end
function GCam:getAnchor()
return self.ax, self.ay
end
------------------------------------------
------------------ SIZE ------------------
------------------------------------------
function GCam:updateClip()
local ax = self.w * self.ax
local ay = self.h * self.ay
--self.viewport:setClip(self.x-ax,self.y-ay,self.w,self.h+ay)
--self.viewport:setAnchorPosition(self.x,self.y)
end
function GCam:setSize(w,h)
self.w = w
self.h = h
self:debugUpdate()
self:updateClip()
end
Kinetic Zoom Camera
-- Kinetic Zoom Camera
-- https://github.com/nshafer/KineticZoomCamera
-- The MIT License (MIT)
-- Copyright (c) 2013 Nathan Shafer
-- Permission is hereby granted, free of charge, to any person obtaining a copy
-- of this software and associated documentation files (the "Software"), to deal
-- in the Software without restriction, including without limitation the rights
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-- copies of the Software, and to permit persons to whom the Software is
-- furnished to do so, subject to the following conditions:
-- The above copyright notice and this permission notice shall be included in
-- all copies or substantial portions of the Software.
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-- THE SOFTWARE.
--[[
This implements a camera class that allows the user to drag and zoom a virtual camera. It works
basically by having child elements that are bigger than the view size of the given device. There
isn't really a camera that moves, rather it just moves itself and any child elements relative
to the devices normal view. Furthermore, when the user lifts their finger in the middle of a drag, the
drag movement will continue with some kinetic energy and slow down based on simulated friction.
This has two distinct modes, DRAG and SCALE, based on how many touches are detected. It doesn't
combine them, but could be modified to do so. It will change smoothly between the modes, however.
Usage:
local camera = Camera.new()
local camera = Camera.new({maxZoom=1.5,friction=.5})
stage:addChild(camera)
-- Add whatever you want as a child of the camera
local image = Bitmap.new(Texture.new("sky_world_big.png"))
camera:addChild(image)
-- If you want to center the camera on a child element, such as a player, you can do:
local player = Sprite.new() -- example player sprite
camera:centerPoint(player:getX(), player:getY())
-- If you want to process touch events relative to where the camera is, you can translate the event
function onTouchBegin(event)
local point = camera:translateEvent(event)
-- point.x = x position of the touch relative to the camera
-- point.y = y position of the touch relative to the camera
end
--]]
Camera = Core.class(Sprite)
-- Constants
Camera.DRAG = 1
Camera.SCALE = 2
function Camera:init(options)
local options = options or {}
self.maxZoom = options.maxZoom or 2 -- Maximum scale allowed. 1 = normal unzoomed
self.friction = options.friction or .85 -- Percentage to slow the drag down by on every frame. Lower = more slippery
self.maxPoints = options.maxPoints or 10 -- Number of history points to keep in memory
self.minPoints = options.minPoints or 3 -- Minimum points to enable kinetic scroll
-- These are tables that store a history of touch events and times
self.previousPoints = nil
self.previousTimes = nil
-- We maintain an anchor that is the center of the "camera"
self.anchorX = 0
self.anchorY = 0
-- Add our event listeners for touch events
self:addEventListener(Event.TOUCHES_BEGIN, self.onTouchesBegin, self)
self:addEventListener(Event.TOUCHES_MOVE, self.onTouchesMove, self)
self:addEventListener(Event.TOUCHES_END, self.onTouchesEnd, self)
self:addEventListener(Event.TOUCHES_CANCEL, self.onTouchesCancel, self)
end
-- Override the Sprite position functions so that we can enforce boundaries
function Camera:setX(x) -- override
-- Check boundaries
x = math.max(x, application:getContentWidth() - self:getWidth())
x = math.min(x, 0)
Sprite.setX(self, x)
end
function Camera:setY(y) -- override
-- Check boundaries
y = math.max(y, application:getContentHeight() - self:getHeight())
y = math.min(y, 0)
Sprite.setY(self, y)
end
function Camera:setPosition(x, y) -- override
self:setX(x)
self:setY(y or x)
end
-- Override the Sprite setScale function so we can enforce boundaries
function Camera:setScale(scaleX, scaleY) -- override
-- Calculate boundaries
local minScaleX = application:getContentWidth() / (self:getWidth() * (1/self:getScaleX()))
local minScaleY = application:getContentHeight() / (self:getHeight() * (1/self:getScaleY()))
-- Check the boundaries
scaleX = math.max(scaleX, minScaleX, minScaleY)
scaleX = math.min(scaleX, self.maxZoom)
scaleY = math.max(scaleY or scaleX, minScaleY, minScaleX)
scaleY = math.min(scaleY or scaleX, self.maxZoom)
Sprite.setScale(self, scaleX, scaleY)
end
-- Update our anchor point. That's the middle of the "camera". This should be called
-- whenever you change the camera position
function Camera:updateAnchor()
self.anchorX = (-self:getX() + application:getContentWidth()/2) * (1/self:getScaleX())
self.anchorY = (-self:getY() + application:getContentHeight()/2) * (1/self:getScaleY())
end
-- Center our anchor. This should be called anytime you change the scale to recenter the
-- view on the anchor, which will get moved by changing the scale since scaling will be
-- changed based on the 0,0 anchor of the sprite, but we want it to zoom based on the
-- center of the camera view
function Camera:centerAnchor()
self:centerPoint(self.anchorX, self.anchorY)
end
-- Center the camera on a point relative to the child element(s)
function Camera:centerPoint(x, y)
self:setX(-(x * self:getScaleX() - application:getContentWidth()/2))
self:setY(-(y * self:getScaleY() - application:getContentHeight()/2))
self:updateAnchor()
end
-- Translate the x/y coordinates of an event to the cameras coordinates. It
-- takes both position and scale into consideration.
function Camera:translateEvent(event)
local point = {x=0,y=0}
point.x = (-self:getX() + event.x or event.touch.x) * (1/self:getScaleX())
point.y = (-self:getY() + event.y or event.touch.y) * (1/self:getScaleY())
return(point)
end
-- Calculate distance between two points
function Camera:getDistance(p1, p2)
local dx = p2.x - p1.x
local dy = p2.y - p1.y
return(math.sqrt(dx^2 + dy^2))
end
-- Stop the camera from moving any more
function Camera:stop()
self:removeEventListener(Event.ENTER_FRAME, self.onEnterFrame, self)
self.velocity = nil
self.time = nil
end
-- A finger or mouse is pressed
function Camera:onTouchesBegin(event)
if self:hitTestPoint(event.touch.x, event.touch.y) then
self.isFocus = true
if #event.allTouches <= 1 then
self.mode = Camera.DRAG
-- Record the starting point
self.x0 = event.touch.x
self.y0 = event.touch.y
-- Stop any current camera movement
self:stop()
-- Initialize our touch histories
self.previousPoints = {{x=event.touch.x,y=event.touch.y}}
self.previousTimes = {os.timer()}
else
self.mode = Camera.SCALE
-- Only look at the last finger to touch, ignore intermediate fingers
if event.touch.id == event.allTouches[#event.allTouches].id then
-- Figure out initial distance
self.initialDistance = self:getDistance(event.touch, event.allTouches[1])
self.initialScale = self:getScale()
self.initialX = self:getX()
self.initialY = self:getY()
end
end
event:stopPropagation()
end
end
function Camera:onTouchesMove(event)
if self.isFocus then
if self.mode == Camera.DRAG then
-- Figure out how far we moved since last time
local dx = event.touch.x - self.x0
local dy = event.touch.y - self.y0
-- Move the camera
self:setX(self:getX() + dx)
self:setY(self:getY() + dy)
-- Update our location
self.x0 = event.touch.x
self.y0 = event.touch.y
-- Update the anchor point
self:updateAnchor()
-- Add to the stack for velocity calculations later
table.insert(self.previousPoints, {x=event.touch.x,y=event.touch.y})
table.insert(self.previousTimes, os.timer())
-- Clean up old points
-- NOTE: This is not the most efficient way to implement a stack with tables
-- in LUA, but it's the simplest and performs fine for our purposes
while #self.previousPoints > self.maxPoints do
table.remove(self.previousPoints, 1)
table.remove(self.previousTimes, 1)
end
elseif self.mode == Camera.SCALE then
if #event.allTouches > 1 then
-- Only look at the last finger to touch, ignore intermediate fingers
if event.touch.id == event.allTouches[#event.allTouches].id then
-- Figure out current distance
local currentDistance = self:getDistance(event.touch, event.allTouches[1])
-- Change our scale
self:setScale(currentDistance / self.initialDistance * self.initialScale)
-- Center on our anchor
self:centerAnchor()
end
end
end
event:stopPropagation()
end
end
function Camera:onTouchesEnd(event)
if self.isFocus then
if self.mode == Camera.DRAG then
if self.previousPoints and #self.previousPoints > self.minPoints then
-- calculate vectors between now and x points ago
local new_time = os.timer()
local vx = event.touch.x - self.previousPoints[1].x
local vy = event.touch.y - self.previousPoints[1].y
local vt = new_time - self.previousTimes[1]
-- Calculate our velocities
self.velocity = {x=vx/vt, y=vy/vt}
self.time = new_time
-- add an event listener to finish drawing the movement
self:addEventListener(Event.ENTER_FRAME, self.onEnterFrame, self)
end
self.isFocus = false
elseif self.mode == Camera.SCALE then
-- If we're left with just 2 touches, then go back to DRAG mode
if #event.allTouches == 2 then
self.mode = Camera.DRAG
-- reset our last position based on whatever finger is left for a smooth
-- transition back to DRAG mode
if event.allTouches[1].id == event.touch.id then
self.x0 = event.allTouches[2].x
self.y0 = event.allTouches[2].y
else
self.x0 = event.allTouches[1].x
self.y0 = event.allTouches[1].y
end
-- Reset our histories
self:stop()
self.previousPoints = {{x=self.x0,y=self.y0}}
self.previousTimes = {os.timer()}
end
end
event:stopPropagation()
end
end
function Camera:onTouchesCancel(event)
if self.isFocus then
print("Camera TOUCHES_CANCEL", self.mode)
self.isFocus = false
event:stopPropogation()
end
end
-- This will continue moving the camera based on the velocities that were imparted on it,
-- eventually slowing to a stop based on the friction.
function Camera:onEnterFrame(event)
if self.mode == Camera.DRAG then
-- Figure out how much time has passed since the last frame
local new_time = os.timer()
local dt = new_time - self.time
self.time = new_time
-- Calculate the distance we should move this frame
local sx = self.velocity.x * dt
local sy = self.velocity.y * dt
-- Apply friction
self.velocity.x = self.velocity.x * self.friction
self.velocity.y = self.velocity.y * self.friction
-- Check if we're slow enough to just stop
if math.abs(self.velocity.x) < .1 then self.velocity.x = 0 end
if math.abs(self.velocity.y) < .1 then self.velocity.y = 0 end
if self.velocity.x == 0 and self.velocity.y == 0 then
self:stop()
else
-- Move us
self:setX(self:getX() + sx)
self:setY(self:getY() + sy)
-- Update our anchor
self:updateAnchor()
end
end
end