Kamera

Se det gjennom et kamera

viewport

Viewport

I artikkelen om engine3 kunne du allerede se hvordan klassen Camera ser ut. Noe jeg var bekymret for og satte spørsmålstegn ved, var hvordan jeg skulle skalere opp alt.

Kort fortalt er problemet at hver enhet har en annen skjermoppløsning. På et tidspunkt bestemte jeg at maksimal høyde i Photoshop er 240 piksler og at bredden er ubegrenset. Deretter er love2d konfigurert på denne måten.

-- 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 som er viktig å vite, er at alt er tegnet pixel perfect, slik at bildene kan skaleres opp med ‘Nearest Neighbor (preserve hard edges)’. Det er en stor fordel sammenlignet med utjevning, som krever en komplisert oppskaleringsmetode.

Det er også viktig at jeg angir følgende i love2d

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

Canvas

I love2d bruker jeg et canvas som hele spillet tegnes på. Høyden og bredden på canvaset bestemmes av min egen konfigurasjon, 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 som skal til for å forstørre alt uendelig fra en minimal skjermhøyde på 240 piksler. Jeg må fortsatt teste flere skjermer, men så langt ser det bra ut på ulike oppløsninger, og det fungerer også fint å endre størrelse på vinduet.

Kamera

Jeg har satt opp kameraet slik at det kan følge en valgt entity, altså midtpunktet kameraet er rettet mot. Det viktige er at Canvas alltid har en bestemt høyde og bredde, mens ‘Scene’ er mye bredere, for eksempel 2000px. Kameraet får derfor også bredden og høyden til viewporten (canvas) og beveger seg ikke selv når entityen det følger beveger seg. I stedet endres hele måten love2d tegner alle koordinatene på canvaset. Dermed får du illusjonen av at kameraet beveger seg mot høyre. Men i virkeligheten beveger alle objektene seg mot 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

Tegnerekkefølge

Du har allerede kunnet lese om zindex i en annen artikkel, men dette handler om rekkefølgen på entities på skjermen, canvaset og kameraet. Dette er hovedløkken som håndterer alt på riktig måte.

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

Kameraet var en vanskelig bit kode å forstå. Heldigvis fikk jeg mange eksempler og mye hjelp på love2d-forumet. Hadde jeg ikke hatt det, ville det nok tatt for lang tid, og jeg vet ikke hvordan det hadde endt.

Jeg hadde også problemer med jitter, altså at kameraet var urolig. Det løste jeg ved bare å endre 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