Camera
Guardalo con una camera

Viewport
Nell’articolo su engine3 hai già visto com’è la classe Camera. Una cosa che mi preoccupava e su cui avevo dei dubbi era come avrei scalato tutto.
In breve, il problema è che ogni dispositivo ha una risoluzione dello schermo diversa. A un certo punto ho deciso che in Photoshop l’altezza massima è di 240 pixel e la larghezza è illimitata. La configurazione di love2d è poi impostata così.
-- https://en.wikipedia.org/wiki/Display_resolution
--
-- 4:3
-- 1x 320 x 240 -- < minimal source background height
-- 2x 640 x 480
--
-- 16:9
-- 4x 1280x720
-- 6x 1920x1080 (fullHD)
-- 12x 3840x2160 (4K)
t.window.width = 1280 -- The window width (number)
t.window.height = 720 -- The window height (number)
t.window.usedpiscale = true -- Enable automatic DPI scaling (boolean)
t.window.highdpi = true -- Enable high-dpi mode for the window on a Retina display (boolean)
t.window.vsync = 1 -- Vertical sync mode (number)
t.window.msaa = 0 -- The number of samples to use with multi-sampled antialiasing (number)
t.window.depth = nil -- The number of bits per sample in the depth buffer
t.window.stencil = nil -- The number of bits per sample in the stencil buffer
È importante sapere che tutto è disegnato pixel perfect, quindi le immagini si possono ingrandire con ‘Nearest Neighbor (preserve hard edges)’. È un grande vantaggio rispetto allo smoothing, che richiederebbe un metodo di ingrandimento complicato.
È importante anche che in love2d imposti quanto segue
love.graphics.setDefaultFilter("nearest", "nearest")
love.graphics.setLineStyle("rough")
Canvas
In love2d uso un canvas su cui viene disegnato tutto il gioco. Altezza e larghezza del canvas sono stabilite dalla mia configurazione, e anche Canvas è una classe di engine3.
config.lua
self.canvas = {
width = 320,
height = 240,
integerScaling = true,
subpixels = 4
}
function Canvas:new(data)
...
self.canvas = love.graphics.newCanvas(self.width * self.subpixels, self.height * self.subpixels)
self.canvas:setFilter("nearest", "nearest")
...
end
---Only execute this on init and window resize
function Canvas:calculateRenderInfo()
local _, _, windowWidth, windowHeight = love.window.getSafeArea()
local canvasWidth, canvasHeight = self:getDimensions()
-- Fill as much of the window as possible with the canvas while preserving the aspect ratio.
self.scale = math.min(windowWidth / canvasWidth, windowHeight / canvasHeight)
--self.scale = windowHeight / canvasHeight -- This would fill the height and possibly cut off the sides.
if self.integerScaling then
self.scale = math.floor(self.scale * self:getSubpixels()) / self:getSubpixels()
self.scale = math.max(self.scale, 1 / self:getSubpixels()) -- Avoid self.scale =0 if the window is tiny!
end
self.scaledWidth = canvasWidth * self.scale
self.scaledHeight = canvasHeight * self.scale
-- Center the canvas on the screen.
self:setX(math.floor((windowWidth - self.scaledWidth) / 2))
self:setY(math.floor((windowHeight - self.scaledHeight) / 2))
-- These are cached now
return self:getX(), self:getY(), self:getScale(), self:getPixelScale()
end
function love.resize(w, h)
-- re-calculate so we cache the new variables
-- @warning this is needed first, before we forward to the world
self.canvas:calculateRenderInfo()
-- forward to other entities
if self.world then
self.world:windowResizeEvent(
{
width = w,
height = h
}
)
end
end
E questo è tutto per ingrandire all’infinito tutto partendo da un’altezza minima dello schermo di 240 pixel. Devo ancora provare più schermi, ma finora sembra buono con diverse risoluzioni, e anche il ridimensionamento della finestra funziona bene.
Camera
Ho impostato la camera in modo che possa seguire un’entity scelta, che diventa il centro verso cui è puntata la camera. La cosa importante è che il Canvas ha sempre una certa altezza e larghezza, mentre la ‘Scene’ è molto più larga, per esempio 2000px. Per questo la camera riceve anche la larghezza e l’altezza della viewport (canvas) e, quando l’entity da seguire si muove, non si muove lei stessa, ma cambia il modo in cui love2d disegna tutte le coordinate sul canvas. Così hai l’illusione che la camera si sposti a destra. In realtà sono tutti gli oggetti a spostarsi a sinistra.
function Camera:attach()
love.graphics.push("all")
-- Scroll to the camera's position.
local cornerX = self.x - self.width / 2
local cornerY = self.y - self.height / 2
-- Negative values because as the camera moves to the right, the world moves to the left etc.
love.graphics.translate(
Geometry.round(-cornerX, self.config.canvas.subpixels),
Geometry.round(-cornerY, self.config.canvas.subpixels)
)
end
function Camera:detach()
love.graphics.pop()
end
---@param x integer
---@param y integer
function Camera:setTarget(x, y)
if self:getWaitUntilOnTarget() then
if Tween:isActive("cameraFollow") then
print("camera is busy and getWaitUntilOnTarget is in use.")
return
end
-- at the moment if you want to wait then you don't use the player static camera
-- it will follow the camera from a to b
self:setTargetX(x)
self:setTargetY(y)
Tween.to(self, self.followSpeed, {x = self.targetX, y = self.targetY}, "cameraFollow"):ease("linear"):onupdate(
function()
if self.bound then
self.x =
math.min(
math.max(self.x, self.bounds_min_x + self.width / 2),
self.bounds_max_x - self.width / 2
)
self.y =
math.min(
math.max(self.y, self.bounds_min_y + self.height / 2),
self.bounds_max_y - self.height / 2
)
end
end
):oncomplete(
function()
self:callback(self.callbackAfterStop, self)
end
)
else
self:setTargetX(x)
self:setTargetY(y)
self.x = self.targetX
self.y = self.targetY
-- Ensure the camera stays within bounds
if self.bound then
self.x = math.min(math.max(self.x, self.bounds_min_x + self.width / 2), self.bounds_max_x - self.width / 2)
self.y =
math.min(math.max(self.y, self.bounds_min_y + self.height / 2), self.bounds_max_y - self.height / 2)
end
end
end
Ordine di disegno
In un altro articolo hai già letto dello zindex, ma questo riguarda l’ordine delle entity sullo schermo, del canvas e della camera. Questo è il ciclo principale che gestisce tutto nel modo corretto.
...
function Engine3:draw()
if self.world == nil then
return
end
-- Draw to the canvas
love.graphics.push("all")
love.graphics.setCanvas(self.canvas:getCanvas())
love.graphics.clear()
love.graphics.scale(self.canvas:getSubpixels())
if self.camera then
self.camera:attach()
end
self.world:draw()
-- Draw things on screen, not in the canvas and translations, for example a pointer
for _, entity in pairs(self:getEntitiesToDraw("onCanvas")) do
entity:draw()
end
if
self.loadingScreen ~= nil and self.loadingScreen:getIsVisible() == true and
self.loadingScreen:getRenderOtherLayers() == false
then
self.loadingScreen:draw()
self.loadingScreen.textObj:draw()
end
if self.camera then
self.camera:detach()
self.camera:draw() -- camera effects if needed
end
-- draw things within the canvas but without the moving camera
for _, entity in pairs(self:getEntitiesToDraw("insideCanvasWithoutCameraMovement")) do
entity:draw()
end
-- Canvas drawing end
love.graphics.pop()
love.graphics.clear(0, 0, 0)
-- don't draw the canvas if there is a loadingScreen
if
self.loadingScreen == nil or self.loadingScreen:getIsVisible() == false or
self.loadingScreen:getRenderOtherLayers() == true
then
love.graphics.draw(self.canvas:getCanvas(), self.canvas:getX(), self.canvas:getY(), 0, self.canvas:getScale())
-- debug
-- love.graphics.rectangle(
-- "line",
-- self.canvas:getX(),
-- self.canvas:getY(),
-- self.canvas:getWidth() * self.canvas:getPixelScale(),
-- self.canvas:getHeight() * self.canvas:getPixelScale()
-- )
end
-- Draw things on screen, not in the canvas and translations, for example a pointer
for _, entity in pairs(self:getEntitiesToDraw("onScreen")) do
entity:draw()
end
if self.config.showFPS then
love.graphics.print("FPS: " .. tostring(love.timer.getFPS()), 10, 10)
love.graphics.print(
string.format(
"Memory: gfx=%.1fMiB lua=%.1fMiB",
love.graphics.getStats().texturememory / 1024 ^ 2,
collectgarbage "count" / 1024
),
10,
25
)
end
if self.config.showMouseXY then
local mouseXOnScreen, mouseYOnScreen = love.mouse.getPosition()
local text = string.format("\nMouse screen: %d,%d", mouseXOnScreen, mouseYOnScreen)
if self.pointer then
text = text .. string.format("\nScreenToWorld: %d,%d", self.pointer:getScreenToWorld(mouseXOnScreen, mouseYOnScreen))
end
love.graphics.print(text, 10, 30)
end
end
...
Per finire
La camera è stata una parte di codice difficile da capire. Per fortuna ho ricevuto molti esempi e molto aiuto sul forum di love2d. Senza quello probabilmente mi avrebbe richiesto troppo tempo e non so come sarebbe andata a finire.
Avevo anche problemi di jitter, cioè la camera era instabile. L’ho risolto cambiando solo la x mentre segue l’entity.
-- don't jitter, we only move left <> right
local newTargetY = self.targetY
if self.targetY == nil then
if self.followEntity.activeFrameset then
newTargetY = self.followEntity.activeFrameset:getY()
else
newTargetY = self.followEntity:getY() or self.followEntity:getFrameY()
end
end