Kamera

Se det gennem et kamera

viewport

Viewport

I artiklen om engine3 kunne du allerede se, hvordan klassen Camera ser ud. Noget, jeg var bekymret for og satte spørgsmålstegn ved, var, hvordan jeg skulle skalere det hele.

Kort sagt er problemet, at hver enhed har en anden skærmopløsning. På et tidspunkt besluttede jeg, at den maksimale højde i Photoshop er 240 pixels, og at bredden er ubegrænset. Derefter er love2d konfigureret på følgende måde.

-- 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

Det vigtige at vide er, at alt er tegnet pixel perfect, så billederne kan skaleres op med ‘Nearest Neighbor (preserve hard edges)’. Det er en stor fordel i forhold til udjævning, som kræver en kompliceret opskaleringsmetode.

Det er også vigtigt, at jeg angiver følgende i love2d

love.graphics.setDefaultFilter("nearest", "nearest")
love.graphics.setLineStyle("rough")

Canvas

I love2d bruger jeg et canvas, som hele spillet bliver tegnet på. Højden og bredden af canvas bestemmes af min egen config, og Canvas er også en engine3-klasse.

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

Og det er alt, der skal til for at forstørre det hele uendeligt fra en minimal skærmhøjde på 240 pixels. Jeg skal stadig teste flere skærme, men indtil nu ser det godt ud på forskellige opløsninger, og det virker også fint at ændre størrelse på vinduet.

Kamera

Jeg har sat kameraet op, så det kan følge en valgt entity. Det er altså midtpunktet, kameraet er rettet mod. Det vigtige er, at Canvas altid har en bestemt højde og bredde, mens ‘Scene’ er meget bredere, fx 2000px. Kameraet får derfor også bredden og højden af viewport (canvas) og bevæger sig ikke selv, når den fulgte entity bevæger sig. I stedet ændrer det hele måden, love2d tegner alle koordinater på canvas. Så får man illusionen af, at kameraet bevæger sig til højre. Men i virkeligheden bevæger alle objekter sig til venstre.

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

Tegnerækkefølge

Du har allerede kunnet læse om zindex i en anden artikel, men det her handler om rækkefølgen af entities på skærmen, canvas og kameraet. Det her er hovedløkken, der håndterer det hele på den rigtige måde.

...
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
...

Til sidst

Kameraet var et svært stykke kode at forstå. Heldigvis fik jeg mange eksempler og meget hjælp på love2d-forummet. Havde jeg ikke haft det, var det nok taget for lang tid, og jeg ved ikke, hvordan det var endt.

Jeg havde også problemer med jitter, altså at kameraet var uroligt. Det løste jeg ved kun at ændre x, mens kameraet følger entityen.

-- 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