Kamera
Melihatnya lewat kamera

Viewport
Di artikel tentang engine3 kamu sudah bisa melihat seperti apa class Camera. Sesuatu yang membuat saya khawatir dan bertanya-tanya adalah bagaimana saya akan menskalakan semuanya.
Singkatnya, masalahnya adalah setiap perangkat punya resolusi layar yang berbeda. Pada suatu saat saya memutuskan bahwa tinggi maksimal di Photoshop adalah 240 piksel dan lebarnya tidak terbatas. Lalu konfigurasi love2d diatur seperti berikut.
-- 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
Yang penting diketahui, semuanya digambar secara pixel perfect, sehingga gambar bisa diperbesar dengan ‘Nearest Neighbor (preserve hard edges)’. Ini keuntungan besar dibanding memakai smoothing, yang butuh metode pembesaran yang rumit.
Yang juga penting adalah saya mengatur hal berikut di love2d
love.graphics.setDefaultFilter("nearest", "nearest")
love.graphics.setLineStyle("rough")
Canvas
Di love2d saya memakai sebuah canvas tempat seluruh game digambar. Tinggi dan lebar canvas ditentukan oleh konfigurasi saya sendiri, dan Canvas juga merupakan class 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
Dan hanya itu yang dibutuhkan untuk memperbesar semuanya tanpa batas mulai dari tinggi layar minimal 240 piksel. Saya masih harus menguji lebih banyak layar, tapi sejauh ini hasilnya bagus di berbagai resolusi, dan mengubah ukuran jendela juga berjalan baik.
Kamera
Saya mengatur kamera agar bisa mengikuti entity yang dipilih, jadi itulah titik tengah yang menjadi fokus kamera. Yang penting adalah Canvas selalu punya tinggi dan lebar tertentu, sedangkan ‘Scene’ jauh lebih lebar, misalnya 2000px. Karena itu kamera juga mendapat lebar dan tinggi viewport (canvas), dan saat entity yang diikuti bergerak, kamera itu sendiri tidak bergerak, tapi mengubah cara love2d menggambar semua koordinat di canvas. Dengan begitu muncul ilusi bahwa kamera bergerak ke kanan. Padahal sebenarnya semua objek bergerak ke kiri.
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
Urutan menggambar
Kamu sudah bisa membaca tentang zindex di artikel lain, tapi ini tentang urutan entity di layar, canvas, dan kamera. Ini adalah main loop yang menangani semuanya dengan benar.
...
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
...
Penutup
Kamera adalah bagian kode yang sulit dipahami. Untungnya saya mendapat banyak contoh dan bantuan di forum love2d. Kalau tidak, mungkin ini akan makan terlalu banyak waktu dan saya tidak tahu bagaimana akhirnya.
Saya juga mengalami jitter, yaitu kamera yang tidak stabil. Ini saya atasi dengan hanya mengubah x saat mengikuti 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