365 lines
15 KiB
Lua
365 lines
15 KiB
Lua
|
Shake = Object:extend()
|
||
|
function Shake:init(amplitude, duration, frequency)
|
||
|
self.amplitude = amplitude or 0
|
||
|
self.duration = duration or 0
|
||
|
self.frequency = frequency or 60
|
||
|
self.samples = {}
|
||
|
for i = 1, (duration/1000)*frequency do self.samples[i] = 2*love.math.random()-1 end
|
||
|
self.ti = love.timer.getTime()*1000
|
||
|
self.t = 0
|
||
|
self.shaking = true
|
||
|
end
|
||
|
|
||
|
|
||
|
function Shake:update(dt)
|
||
|
self.t = love.timer.getTime()*1000 - self.ti
|
||
|
if self.t > self.duration then
|
||
|
self.shaking = false
|
||
|
end
|
||
|
end
|
||
|
|
||
|
|
||
|
function Shake:get_noise(s)
|
||
|
return self.samples[s] or 0
|
||
|
end
|
||
|
|
||
|
|
||
|
function Shake:get_decay(t)
|
||
|
if t > self.duration then return end
|
||
|
return (self.duration - t)/self.duration
|
||
|
end
|
||
|
|
||
|
|
||
|
function Shake:get_amplitude(t)
|
||
|
if not t then
|
||
|
if not self.shaking then return 0 end
|
||
|
t = self.t
|
||
|
end
|
||
|
local s = (t/1000)*self.frequency
|
||
|
local s0 = math.floor(s)
|
||
|
local s1 = s0 + 1
|
||
|
local k = self:get_decay(t)
|
||
|
return self.amplitude*(self:get_noise(s0) + (s-s0)*(self:get_noise(s1) - self:get_noise(s0)))*k
|
||
|
end
|
||
|
|
||
|
|
||
|
-- The camera object. A global instance of this called "camera" is created automatically, and the Game class also contains its own instance of in the .camera attribute.
|
||
|
-- This class handles drawing of the game world through a viewport and the functions needed to make that happen.
|
||
|
-- A common setup looks like this:
|
||
|
--[[
|
||
|
function Arena:on_enter()
|
||
|
game.camera.follow_style = 'lockon'
|
||
|
game.camera.lerp.x = 0.1
|
||
|
game.camera.lerp.y = 0.1
|
||
|
|
||
|
self.main = Group(game.camera):set_as_physics_world(192, 0, 0, {'player', 'enemy', 'projectile', 'solid'})
|
||
|
self.effects = Group(game.camera)
|
||
|
self.floor = Group(game.camera)
|
||
|
self.ui = Group()
|
||
|
|
||
|
self.player = Player(gw/2, gh/2)
|
||
|
self.main:add_object(self.player)
|
||
|
end
|
||
|
|
||
|
function Arena:update(dt)
|
||
|
game.camera:follow_object(self.player)
|
||
|
game.camera.x = math.floor(game.camera.x)
|
||
|
game.camera.y = math.floor(game.camera.y)
|
||
|
|
||
|
self.main:update(dt)
|
||
|
self.floor:update(dt)
|
||
|
self.effects:update(dt)
|
||
|
self.ui:update(dt)
|
||
|
end
|
||
|
|
||
|
function Arena:draw()
|
||
|
self.floor:draw()
|
||
|
self.main:sort_by_y()
|
||
|
self.main:draw()
|
||
|
self.effects:draw()
|
||
|
self.ui:draw()
|
||
|
end
|
||
|
]]--
|
||
|
|
||
|
-- Here we see 4 groups being created and for 3 of them the game's camera instance is attached.
|
||
|
-- If a group has a camera instance attached to it then whenever group:draw() is called it will draw all objects to the camera's canvas affected by the camera's translation, zoom and rotation.
|
||
|
-- Additionally, we also see some basic following functionality, as the camera is told to follow the player with the 'lockon' follow style and with some lerping going on.
|
||
|
-- Finally, the 4th group has no camera attached to it, because for UI elements we want them to be drawn in static screen positions.
|
||
|
--
|
||
|
-- The arguments passed in to create a camera area:
|
||
|
-- x, y - the camera's position in world coordinates, the camera is always centered around its x, y coordinates
|
||
|
-- w, h - the camera's size, generally this should be the same as game_width and game_height (or gw and gh) passed in engine_run
|
||
|
Camera = Object:extend()
|
||
|
function Camera:init(x, y, w, h)
|
||
|
self.x, self.y = x, y
|
||
|
self.w, self.h = w or gw, h or gh
|
||
|
self.r, self.sx, self.sy = 0, 1, 1
|
||
|
self.mouse = Vector(0, 0)
|
||
|
self.last_mouse = Vector(0, 0)
|
||
|
self.mouse_dt = Vector(0, 0)
|
||
|
self.shakes = {x = {}, y = {}}
|
||
|
self.spring = {x = Spring(), y = Spring()}
|
||
|
self.lerp = Vector(1, 1)
|
||
|
self.lead = Vector(1, 1)
|
||
|
self.impulse = Vector(0, 0)
|
||
|
self.follow_style = "no_deadzone"
|
||
|
self.shake_amount = Vector(0, 0)
|
||
|
self.last_shake_amount = Vector(0, 0)
|
||
|
self.last_target = Vector(0, 0)
|
||
|
self.scroll = Vector(0, 0)
|
||
|
end
|
||
|
|
||
|
|
||
|
-- Set ignore_scale to true when you want drawing of objects to be scaled when drawing the canvas rather than the objects themselves
|
||
|
-- That is useful for mipmapping a pixelated canvas, for instance, such that zooming out doesn't create lots of ugly artifacts
|
||
|
-- In that case you want to set ignore_scale to true, and then when drawing the main canvas multiply it by camera.sx*sx, camera.sy*sy instead of only sx, sy
|
||
|
-- You'll also want to create a canvas that is bigger than normal to account for the fact that you're zooming out of it
|
||
|
-- TODO: classify and unify all disparate methods of drawing in this class, also move "main canvas" here
|
||
|
|
||
|
-- Attaches the camera, meaning all further draw operations will be affected by its transform.
|
||
|
-- Accepts two values that go from 0 to 1 that represent how much parallaxing there should be for the next operations.
|
||
|
-- A value of 1 means no parallaxing, meaning the elements drawn will move at the same rate as all other elements, while a value of 0 means maximum parallaxing, which means the elements won't move at all.
|
||
|
-- Groups automatically call this function, but you can still pass these scroll factors to their draw calls.
|
||
|
-- In general if you have multiple layers of backgrounds and you want them to be parallaxed you'd do something like this:
|
||
|
--[[
|
||
|
function init()
|
||
|
back_backgrounds = Group(game.camera)
|
||
|
middle_backgrounds = Group(game.camera)
|
||
|
front_backgrounds = Group(game.camera)
|
||
|
main = Group(game.camera)
|
||
|
end
|
||
|
|
||
|
function draw()
|
||
|
back_backgrounds:draw(0.5)
|
||
|
middle_backgrounds:draw(0.75)
|
||
|
front_backgrounds:draw(0.9)
|
||
|
main:draw()
|
||
|
end
|
||
|
]]--
|
||
|
--
|
||
|
-- In this example we have 4 layers are all drawn with different levels of parallax: 0.5 (half speed), 0.75, 0.9 and 1 (normal speed = no parallax).
|
||
|
function Camera:attach(scroll_factor_x, scroll_factor_y)
|
||
|
self.bx, self.by = self.x, self.y
|
||
|
self.x = self.bx*(scroll_factor_x or 1)
|
||
|
self.y = self.by*(scroll_factor_y or scroll_factor_x or 1)
|
||
|
love.graphics.push()
|
||
|
love.graphics.translate(self.w/2, self.h/2)
|
||
|
if not self.ignore_scale then love.graphics.scale(self.sx, self.sy) end
|
||
|
love.graphics.rotate(self.r)
|
||
|
love.graphics.translate(-self.x*(scroll_factor_x or 1), -self.y*(scroll_factor_y or 1))
|
||
|
end
|
||
|
|
||
|
|
||
|
-- Detaches the camera, meaning all further draw operations won't be affected by its transform.
|
||
|
-- This is also called automatically by groups that have cameras attached to them.
|
||
|
function Camera:detach()
|
||
|
love.graphics.pop()
|
||
|
self.x, self.y = self.bx, self.by
|
||
|
end
|
||
|
|
||
|
|
||
|
-- Returns the values passed in in world coordinates. This is useful when transforming things from screen space to world space, like when the mouse clicks on something.
|
||
|
-- If you look at camera:get_mouse_position you'll see that it uses this function on the values returned by love.mouse.getPosition (which return values in screen coordinates).
|
||
|
-- camera:get_world_coords(love.mouse.getPosition())
|
||
|
function Camera:get_world_coords(x, y)
|
||
|
local c, s = math.cos(-self.r), math.sin(-self.r)
|
||
|
x, y = (x - sx*self.w/2)/(sx*self.sx), (y - sy*self.h/2)/(sy*self.sy)
|
||
|
x, y = c*x - s*y, s*x + c*y
|
||
|
return x + self.x*(v or 1), y + self.y*(v or 1)
|
||
|
end
|
||
|
|
||
|
|
||
|
-- Returns the values passed in in local coordinates. This is useful when transforming things from world space to screen space, like when displaying UI according to the position of game objects.
|
||
|
-- x, y = camera:get_local_coords(player.x, player.y)
|
||
|
function Camera:get_local_coords(x, y)
|
||
|
local c, s = math.cos(self.r), math.sin(self.r)
|
||
|
x, y = x - self.x*(v or 1), y - self.y*(v or 1)
|
||
|
x, y = c*x - s*y, s*x + c*y
|
||
|
return x*self.sx + self.w/2, y*self.sy + self.h/2
|
||
|
end
|
||
|
|
||
|
|
||
|
function Camera:update(dt)
|
||
|
self.mouse.x, self.mouse.y = self:get_mouse_position()
|
||
|
self.mouse_dt.x, self.mouse_dt.y = self.mouse.x - self.last_mouse.x, self.mouse.y - self.last_mouse.y
|
||
|
self.shake_amount:set(0, 0)
|
||
|
for _, z in ipairs({"x", "y"}) do
|
||
|
for i = #self.shakes[z], 1, -1 do
|
||
|
self.shakes[z][i]:update(dt)
|
||
|
self.shake_amount[z] = self.shake_amount[z] + self.shakes[z][i]:get_amplitude()
|
||
|
if not self.shakes[z][i].shaking then
|
||
|
table.remove(self.shakes[z], i)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
self.spring.x:update(dt)
|
||
|
self.spring.y:update(dt)
|
||
|
self.shake_amount:add(self.spring.x.x, self.spring.y.x)
|
||
|
self.x, self.y = self.x - self.last_shake_amount.x, self.y - self.last_shake_amount.y
|
||
|
self.x, self.y = self.x + self.shake_amount.x, self.y + self.shake_amount.y
|
||
|
self.last_shake_amount:set(self.shake_amount)
|
||
|
self.x = self.x + self.impulse.x*dt
|
||
|
self.y = self.y + self.impulse.y*dt
|
||
|
self.impulse:mul(0.9*refresh_rate*dt)
|
||
|
|
||
|
if self.bound then
|
||
|
self.x = math.min(math.max(self.x, self.bounds_min.x + self.w/2), self.bounds_max.x - self.w/2)
|
||
|
self.y = math.min(math.max(self.y, self.bounds_min.y + self.h/2), self.bounds_max.y - self.h/2)
|
||
|
end
|
||
|
|
||
|
self.last_mouse.x, self.last_mouse.y = self.mouse.x, self.mouse.y
|
||
|
if not self.target then return end
|
||
|
|
||
|
if self.follow_style == "lockon" then
|
||
|
local w, h = self.w/16, self.w/16
|
||
|
self:set_deadzone((self.w - w)/2, (self.h - h)/2, w, h)
|
||
|
elseif self.follow_style == "lockon_tight" then
|
||
|
local w, h = self.w/64, self.w/64
|
||
|
self:set_deadzone((self.w - w)/2, (self.h - h)/2, w, h)
|
||
|
elseif self.follow_style == "lockon_loose" then
|
||
|
local w, h = self.w/4, self.w/4
|
||
|
self:set_deadzone((self.w - w)/2, (self.h - h)/2, w, h)
|
||
|
elseif self.follow_style == "platformer" then
|
||
|
local w, h = self.w/8, self.h/3
|
||
|
self:set_deadzone((self.w - w)/2, (self.h - h)/2 - h*0.25, w, h)
|
||
|
elseif self.follow_style == "topdown" then
|
||
|
local s = math.max(self.w, self.h)/4
|
||
|
self:set_deadzone((self.w - s)/2, (self.h - s)/2, s, s)
|
||
|
elseif self.follow_style == "topdown_tight" then
|
||
|
local s = math.max(self.w, self.h)/8
|
||
|
self:set_deadzone((self.w - s)/2, (self.h - s)/2, s, s)
|
||
|
elseif self.follow_style == "screen_by_screen" then
|
||
|
self:set_deadzone(0, 0, 0, 0)
|
||
|
elseif self.follow_style == "no_deadzone" then
|
||
|
self.deadzone = nil
|
||
|
end
|
||
|
|
||
|
if not self.deadzone then
|
||
|
self.x, self.y = self.target.x, self.target.y
|
||
|
if self.bound then
|
||
|
self.x = math.min(math.max(self.x, self.bounds_min.x + self.w/2), self.bounds_max.x - self.w/2)
|
||
|
self.y = math.min(math.max(self.y, self.bounds_min.y + self.h/2), self.bounds_max.y - self.h/2)
|
||
|
end
|
||
|
return
|
||
|
end
|
||
|
|
||
|
local dx1, dy1, dx2, dy2 = self.deadzone.x, self.deadzone.y, self.deadzone.x + self.deadzone.w, self.deadzone.y + self.deadzone.h
|
||
|
local scroll_x, scroll_y = 0, 0
|
||
|
local target_x, target_y = self:get_local_coords(self.target.x, self.target.y)
|
||
|
local x, y = self:get_local_coords(self.x, self.y)
|
||
|
|
||
|
if self.follow_style == "screen_by_screen" then
|
||
|
if self.bound then
|
||
|
if self.x > self.bounds_min.x + self.w/2 and target_x < 0 then self.scroll.x = math.snap_center(self.scroll.x - self.w/self.sx, self.w/self.sx) end
|
||
|
if self.x < self.bounds_max.x - self.w/2 and target_x >= self.w then self.scroll.x = math.snap_center(self.scroll.x + self.w/self.sx, self.w/self.sx) end
|
||
|
if self.y > self.bounds_min.y + self.h/2 and target_y < 0 then self.scroll.y = math.snap_center(self.scroll.y - self.h/self.sy, self.h/self.sy) end
|
||
|
if self.y < self.bounds_max.y - self.h/2 and target_y >= self.h then self.scroll.y = math.snap_center(self.scroll.y + self.h/self.sy, self.h/self.sy) end
|
||
|
else
|
||
|
if target_x < 0 then self.scroll.x = math.snap_center(self.scroll.x - self.w/self.sx, self.w/self.sx) end
|
||
|
if target_x >= self.w then self.scroll.x = math.snap_center(self.scroll.x + self.w/self.sx, self.w/self.sx) end
|
||
|
if target_y < 0 then self.scroll.y = math.snap_center(self.scroll.y - self.h/self.sy, self.h/self.sy) end
|
||
|
if target_y >= self.h then self.scroll.y = math.snap_center(self.scroll.y + self.h/self.sy, self.h/self.sy) end
|
||
|
end
|
||
|
self.x = math.lerp(self.lerp.x, self.x, self.scroll.x)
|
||
|
self.y = math.lerp(self.lerp.y, self.y, self.scroll.y)
|
||
|
|
||
|
if self.bound then
|
||
|
self.x = math.min(math.max(self.x, self.bounds_min.x + self.w/2), self.bounds_max.x - self.w/2)
|
||
|
self.y = math.min(math.max(self.y, self.bounds_min.y + self.h/2), self.bounds_max.y - self.h/2)
|
||
|
end
|
||
|
|
||
|
else
|
||
|
if target_x < x + (dx1 + dx2 - x) then
|
||
|
local d = target_x - dx1
|
||
|
if d < 0 then scroll_x = d end
|
||
|
end
|
||
|
if target_x > x - (dx1 + dx2 - x) then
|
||
|
local d = target_x - dx2
|
||
|
if d > 0 then scroll_x = d end
|
||
|
end
|
||
|
if target_y < y + (dy1 + dy2 - y) then
|
||
|
local d = target_y - dy1
|
||
|
if d < 0 then scroll_y = d end
|
||
|
end
|
||
|
if target_y > y - (dy1 + dy2 - y) then
|
||
|
local d = target_y - dy2
|
||
|
if d > 0 then scroll_y = d end
|
||
|
end
|
||
|
|
||
|
if not self.last_target.x and not self.last_target.y then self.last_target.x, self.last_target.y = self.target.x, self.target.y end
|
||
|
scroll_x = scroll_x + (self.target.x - self.last_target.x)*self.lead.x
|
||
|
scroll_y = scroll_y + (self.target.y - self.last_target.y)*self.lead.y
|
||
|
self.last_target.x, self.last_target.y = self.target.x, self.target.y
|
||
|
self.x = math.lerp(self.lerp.x, self.x, self.x + scroll_x)
|
||
|
self.y = math.lerp(self.lerp.y, self.y, self.y + scroll_y)
|
||
|
|
||
|
if self.bound then
|
||
|
self.x = math.min(math.max(self.x, self.bounds_min.x + self.w/2), self.bounds_max.x - self.w/2)
|
||
|
self.y = math.min(math.max(self.y, self.bounds_min.y + self.h/2), self.bounds_max.y - self.h/2)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
|
||
|
-- Shakes the camera with a certain intensity for duration seconds and with the specified frequency
|
||
|
-- Higher frequency means jerkier movement, lower frequency means smoother movement
|
||
|
-- camera:shake(10, 1, 120) -> shakes the camera with 10 intensity for 1 second and 120 frequency
|
||
|
function Camera:shake(intensity, duration, frequency)
|
||
|
table.insert(self.shakes.x, Shake(intensity, 1000*(duration or 0), frequency or 60))
|
||
|
table.insert(self.shakes.y, Shake(intensity, 1000*(duration or 0), frequency or 60))
|
||
|
end
|
||
|
|
||
|
|
||
|
-- Shakes the camera with a certain intensity towards angle r using a spring mechanism
|
||
|
-- k and d are stiffness and damping spring values (see spring file for more)
|
||
|
-- camera:shake(10, math.pi/4) -> shakes the camera with 10 intensity diagonally
|
||
|
function Camera:spring_shake(intensity, r, k, d)
|
||
|
self.spring.x:pull(-intensity*math.cos(r or 0), k, d)
|
||
|
self.spring.y:pull(-intensity*math.sin(r or 0), k, d)
|
||
|
end
|
||
|
|
||
|
|
||
|
-- When following an object, this function sets a deadzone in which the camera can move freely in before having to go back to following the object
|
||
|
-- TODO: add STALKER-X documentation examples of all deadzones and lerping modes here
|
||
|
function Camera:set_deadzone(x, y, w, h)
|
||
|
self.deadzone = {x = x, y = y, w = w, h = h}
|
||
|
end
|
||
|
|
||
|
|
||
|
-- Sets the boundaries of the camera as a rectangle.
|
||
|
-- The camera's center will not be allowed to move outside the boundaries of this rectangle.
|
||
|
-- TODO: this doesn't seem to work when zooming levels are different than 1, fix it
|
||
|
function Camera:set_bounds(x, y, w, h)
|
||
|
self.bound = true
|
||
|
self.bounds_min = {x = x - w/2, y = y - h/2}
|
||
|
self.bounds_max = {x = x + w/2, y = y + h/2}
|
||
|
end
|
||
|
|
||
|
|
||
|
-- Tells the camera to follow the object
|
||
|
-- Previously set lerp, lead, follow styles and deadzone variables apply when following the object
|
||
|
function Camera:follow_object(obj)
|
||
|
self.target = obj
|
||
|
end
|
||
|
|
||
|
|
||
|
-- Returns the mouse's position in world coordinates
|
||
|
-- x, y = camera:get_mouse_position()
|
||
|
function Camera:get_mouse_position()
|
||
|
return self:get_world_coords(love.mouse.getPosition())
|
||
|
end
|
||
|
|
||
|
|
||
|
-- Returns the angle from a point to the mouse
|
||
|
-- x, y = camera:angle_to_mouse(point.x, point.y)
|
||
|
function Camera:angle_to_mouse(x, y)
|
||
|
local mx, my = self:get_mouse_position()
|
||
|
return math.angle(x, y, mx, my)
|
||
|
end
|
||
|
|
||
|
|
||
|
function Camera:apply_impulse(f, r)
|
||
|
self.impulse:set(f*math.cos(r), f*math.sin(r))
|
||
|
end
|