From 0293409dd926f56d53d89af479ddeda50f3ba241 Mon Sep 17 00:00:00 2001 From: Neckrat Date: Sat, 2 Aug 2025 02:16:20 +0300 Subject: [PATCH] camera refactor Co-authored-by: Ivan Yuriev --- camera.lua | 45 ++++++++++ lib/camera.lua | 215 -------------------------------------------- lib/tree.lua | 7 +- lib/vec3.lua | 60 ++++++------- lib/wheelscroll.lua | 22 +++++ main.lua | 52 ++--------- 6 files changed, 109 insertions(+), 292 deletions(-) create mode 100644 camera.lua delete mode 100644 lib/camera.lua create mode 100644 lib/wheelscroll.lua diff --git a/camera.lua b/camera.lua new file mode 100644 index 0000000..633aed1 --- /dev/null +++ b/camera.lua @@ -0,0 +1,45 @@ +local Vec3 = require "lib/vec3" +local tree = require "lib/tree" + +--- @class (exact) Camera +--- @field target Vec3 +--- @field speed number +--- @field pixelsPerMeter integer +local camera = { + target = Vec3 {}, + speed = 5, + pixelsPerMeter = 100 +} + +function camera:update(dt) + local ws = tree.instance().wheelscroll + if ws.delta:length() > 0 then + local worldDelta = ws.delta * (1 / self.pixelsPerMeter) + self.target = self.target + worldDelta + return + end + + if love.keyboard.isDown("w") then self.target = self.target + Vec3({ 0, -dt * self.speed }) end + if love.keyboard.isDown("a") then self.target = self.target + Vec3({ -dt * self.speed }) end + if love.keyboard.isDown("s") then self.target = self.target + Vec3({ 0, dt * self.speed }) end + if love.keyboard.isDown("d") then self.target = self.target + Vec3({ dt * self.speed }) end +end + +function camera:attach() + love.graphics.push() + love.graphics.scale(self.pixelsPerMeter) + love.graphics.translate(-self.target.x, -self.target.y) +end + +function camera:detach() + love.graphics.pop() +end + +--- @return Camera +local function new() + return setmetatable({}, { + __index = camera + }) +end + +return { new = new } diff --git a/lib/camera.lua b/lib/camera.lua deleted file mode 100644 index 55a7e96..0000000 --- a/lib/camera.lua +++ /dev/null @@ -1,215 +0,0 @@ ---[[ -Copyright (c) 2010-2015 Matthias Richter - -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. - -Except as contained in this notice, the name(s) of the above copyright holders -shall not be used in advertising or otherwise to promote the sale, use or -other dealings in this Software without prior written authorization. - -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. -]] -- - -local _PATH = (...):match('^(.*[%./])[^%.%/]+$') or '' -local cos, sin = math.cos, math.sin - -local camera = {} -camera.__index = camera - --- Movement interpolators (for camera locking/windowing) -camera.smooth = {} - -function camera.smooth.none() - return function(dx, dy) return dx, dy end -end - -function camera.smooth.linear(speed) - assert(type(speed) == "number", "Invalid parameter: speed = " .. tostring(speed)) - return function(dx, dy, s) - -- normalize direction - local d = math.sqrt(dx * dx + dy * dy) - local dts = math.min((s or speed) * love.timer.getDelta(), d) -- prevent overshooting the goal - if d > 0 then - dx, dy = dx / d, dy / d - end - - return dx * dts, dy * dts - end -end - -function camera.smooth.damped(stiffness) - assert(type(stiffness) == "number", "Invalid parameter: stiffness = " .. tostring(stiffness)) - return function(dx, dy, s) - local dts = love.timer.getDelta() * (s or stiffness) - return dx * dts, dy * dts - end -end - -local function new(x, y, zoom, rot, smoother) - x, y = x or love.graphics.getWidth() / 2, y or love.graphics.getHeight() / 2 - zoom = zoom or 1 - rot = rot or 0 - smoother = smoother or camera.smooth.none() -- for locking, see below - return setmetatable({ x = x, y = y, scale = zoom, rot = rot, smoother = smoother }, camera) -end - -function camera:lookAt(x, y) - self.x, self.y = x, y - return self -end - -function camera:move(dx, dy) - self.x, self.y = self.x + dx, self.y + dy - return self -end - -function camera:position() - return self.x, self.y -end - -function camera:rotate(phi) - self.rot = self.rot + phi - return self -end - -function camera:rotateTo(phi) - self.rot = phi - return self -end - -function camera:zoom(mul) - self.scale = self.scale * mul - return self -end - -function camera:zoomTo(zoom) - self.scale = zoom - return self -end - -function camera:attach(x, y, w, h, noclip) - x, y = x or 0, y or 0 - w, h = w or love.graphics.getWidth(), h or love.graphics.getHeight() - - self._sx, self._sy, self._sw, self._sh = love.graphics.getScissor() - if not noclip then - love.graphics.setScissor(x, y, w, h) - end - - local cx, cy = x + w / 2, y + h / 2 - love.graphics.push() - love.graphics.translate(cx, cy) - love.graphics.scale(self.scale) - love.graphics.rotate(self.rot) - love.graphics.translate(-self.x, -self.y) -end - -function camera:detach() - love.graphics.pop() - love.graphics.setScissor(self._sx, self._sy, self._sw, self._sh) -end - -function camera:draw(...) - local x, y, w, h, noclip, func - local nargs = select("#", ...) - if nargs == 1 then - func = ... - elseif nargs == 5 then - x, y, w, h, func = ... - elseif nargs == 6 then - x, y, w, h, noclip, func = ... - else - error("Invalid arguments to camera:draw()") - end - - self:attach(x, y, w, h, noclip) - func() - self:detach() -end - --- world coordinates to camera coordinates -function camera:cameraCoords(x, y, ox, oy, w, h) - ox, oy = ox or 0, oy or 0 - w, h = w or love.graphics.getWidth(), h or love.graphics.getHeight() - - -- x,y = ((x,y) - (self.x, self.y)):rotated(self.rot) * self.scale + center - local c, s = cos(self.rot), sin(self.rot) - x, y = x - self.x, y - self.y - x, y = c * x - s * y, s * x + c * y - return x * self.scale + w / 2 + ox, y * self.scale + h / 2 + oy -end - --- camera coordinates to world coordinates -function camera:worldCoords(x, y, ox, oy, w, h) - ox, oy = ox or 0, oy or 0 - w, h = w or love.graphics.getWidth(), h or love.graphics.getHeight() - - -- x,y = (((x,y) - center) / self.scale):rotated(-self.rot) + (self.x,self.y) - local c, s = cos(-self.rot), sin(-self.rot) - x, y = (x - w / 2 - ox) / self.scale, (y - h / 2 - oy) / self.scale - x, y = c * x - s * y, s * x + c * y - return x + self.x, y + self.y -end - -function camera:mousePosition(ox, oy, w, h) - local mx, my = love.mouse.getPosition() - return self:worldCoords(mx, my, ox, oy, w, h) -end - --- camera scrolling utilities -function camera:lockX(x, smoother, ...) - local dx, dy = (smoother or self.smoother)(x - self.x, self.y, ...) - self.x = self.x + dx - return self -end - -function camera:lockY(y, smoother, ...) - local dx, dy = (smoother or self.smoother)(self.x, y - self.y, ...) - self.y = self.y + dy - return self -end - -function camera:lockPosition(x, y, smoother, ...) - return self:move((smoother or self.smoother)(x - self.x, y - self.y, ...)) -end - -function camera:lockWindow(x, y, x_min, x_max, y_min, y_max, smoother, ...) - -- figure out displacement in camera coordinates - x, y = self:cameraCoords(x, y) - local dx, dy = 0, 0 - if x < x_min then - dx = x - x_min - elseif x > x_max then - dx = x - x_max - end - if y < y_min then - dy = y - y_min - elseif y > y_max then - dy = y - y_max - end - - -- transform displacement to movement in world coordinates - local c, s = cos(-self.rot), sin(-self.rot) - dx, dy = (c * dx - s * dy) / self.scale, (s * dx + c * dy) / self.scale - - -- move - self:move((smoother or self.smoother)(dx, dy, ...)) -end - --- the module -return setmetatable({ new = new, smooth = camera.smooth }, - { __call = function(_, ...) return new(...) end }) diff --git a/lib/tree.lua b/lib/tree.lua index 95edc48..dd2a2b5 100644 --- a/lib/tree.lua +++ b/lib/tree.lua @@ -2,11 +2,16 @@ --- Типа при запуске загружаем в него сейв и потом оно работает --- В love.update обновлять, в love.draw читать + local tree local function instance() tree = tree or { - level = nil -- current level | nil + wheelscroll = require "lib/wheelscroll" } return tree end + +return { + instance = instance +} diff --git a/lib/vec3.lua b/lib/vec3.lua index 79a6342..2f9ae49 100644 --- a/lib/vec3.lua +++ b/lib/vec3.lua @@ -8,32 +8,8 @@ local __Vec3 = { z = 0, } ----Vec3 constructor ----@param vec number[] ----@return Vec3 -function Vec3(vec) - return setmetatable({ - x = vec[1] or 0, - y = vec[2] or 0, - z = vec[3] or 0, - }, { - __index = __Vec3, - __tostring = __Vec3.__tostring, - __add = __Vec3.add, - __mul = __Vec3.scale, - __unm = function(self) - return __Vec3.scale(self, -1) - end, - __sub = function(self, other) - return self + -other - end, - __eq = function(self, other) - return - self.x == other.x - and self.y == other.y - and self.z == other.z - end, - }) +local function __tostring(self) + return "Vec3{" .. self.x .. ", " .. self.y .. ", " .. self.z .. "}" end --- @param other Vec3 @@ -70,13 +46,37 @@ function __Vec3:dot(other) return self.x * other.x + self.y * other.y + self.z * other.z end -function __Vec3:__tostring() - return "Vec3{" .. self.x .. ", " .. self.y .. ", " .. self.z .. "}" -end - --- @param other Vec3 function __Vec3:angle_to(other) return (other - self):direction() end +---Vec3 constructor +---@param vec number[] +---@return Vec3 +function Vec3(vec) + return setmetatable({ + x = vec[1] or 0, + y = vec[2] or 0, + z = vec[3] or 0, + }, { + __index = __Vec3, + __tostring = __tostring, + __add = __Vec3.add, + __mul = __Vec3.scale, + __unm = function(self) + return __Vec3.scale(self, -1) + end, + __sub = function(self, other) + return self + -other + end, + __eq = function(self, other) + return + self.x == other.x + and self.y == other.y + and self.z == other.z + end, + }) +end + return Vec3 diff --git a/lib/wheelscroll.lua b/lib/wheelscroll.lua new file mode 100644 index 0000000..3de81d5 --- /dev/null +++ b/lib/wheelscroll.lua @@ -0,0 +1,22 @@ +local Vec3 = require "lib/vec3" +local MIDDLE_BUTTON = 3 + +--- @class (exact) WheelScroll +--- @field x number +--- @field y number +--- @field delta Vec3 +local wheelscroll = { + x = 0.0, + y = 0.0, + delta = Vec3 {} +} + +function love.mousepressed(x, y, button) + if button == MIDDLE_BUTTON then + wheelscroll.delta = Vec3 { wheelscroll.x, wheelscroll.y } - Vec3 { x, y } + wheelscroll.x = x + wheelscroll.y = y + end +end + +return wheelscroll diff --git a/main.lua b/main.lua index 8c87fb2..9fe7488 100644 --- a/main.lua +++ b/main.lua @@ -2,38 +2,15 @@ require "character" +local camera = require 'camera' local Vec3 = require "lib/vec3" function love.conf(t) t.console = true end -Camera = { - target = Vec3 {}, - position = Vec3 {}, - lerp_speed = 5.0, - max_offset = 1, -- на сколько метров камера может отдалиться от целевой позиции - max_offset_distance = 5 -- на сколько метров надо отвести мышь для максимального отдаления камеры -} -PIXELS_PER_METER = 100 - -function Camera:update(dt) - if not self.target then return end - - local mouse_offset = Vec3 { 0, 0, 0 } - local offset_distance = 0 - local adjusted_target = self.target * (self.max_offset * offset_distance / self.max_offset_distance) - - local to_target = adjusted_target - self.position - self.position = self.position + to_target * (dt * self.lerp_speed) -end - --- Преобразует вектор мировых координат (в метрах) в вектоор экранных координат (в пикселях) -local function worldToScreen(worldPos) - return (worldPos - Camera.position) * PIXELS_PER_METER -end - function love.load() + Camera = camera.new() Camera.target = Vec3({}) -- PlayerFaction = Faction @@ -49,16 +26,11 @@ end -- Camera:lockPosition(cameraPos.x, cameraPos.y, Camera.smooth.damped(10)) function love.update(dt) - if love.keyboard.isDown("w") then Camera.target = Camera.target + Vec3({ 0, -dt }) end - if love.keyboard.isDown("a") then Camera.target = Camera.target + Vec3({ -dt }) end - if love.keyboard.isDown("s") then Camera.target = Camera.target + Vec3({ 0, dt }) end - if love.keyboard.isDown("d") then Camera.target = Camera.target + Vec3({ dt }) end - Camera:update(dt) end function love.draw() - -- Camera:attach() + Camera:attach() local uiSize = 0.2 @@ -69,23 +41,11 @@ function love.draw() love.graphics.setColor(139 / 255, 195 / 255, 74 / 255, 1) - love.graphics.rectangle('fill', worldToScreen(Vec3 {}).x, worldToScreen(Vec3 {}).y, width * PIXELS_PER_METER, - height * PIXELS_PER_METER) + love.graphics.rectangle('fill', 0, 0, width, height) love.graphics.setColor(244 / 255, 67 / 255, 54 / 255, 1) - love.graphics.rectangle('fill', worldToScreen(Vec3 {}).x, worldToScreen(Vec3 {}).y, 1 * PIXELS_PER_METER, - 1 * PIXELS_PER_METER) + love.graphics.rectangle('fill', 0, 0, 1, 1) - -- if windowWidth / windowHeight > width / height then - -- local fieldWidth = windowHeight * width / height - -- love.graphics.rectangle('fill', 0.5 * (windowWidth - fieldWidth), 0, - -- fieldWidth, windowHeight) - -- else - -- local fieldHeight = windowWidth * height / width - -- love.graphics.rectangle('fill', 0, 0.5 * (windowHeight - fieldHeight), - -- windowWidth, fieldHeight) - -- end - - -- Camera:detach() + Camera:detach() end local Level = {}