Camera
Nhìn qua một chiếc camera

Viewport
Trong bài viết về engine3 bạn đã thấy lớp Camera trông như thế nào. Một điều tôi lo lắng và còn băn khoăn là tôi sẽ phóng to mọi thứ ra sao.
Tóm lại, vấn đề là mỗi thiết bị có độ phân giải màn hình khác nhau. Đến một lúc tôi quyết định chiều cao tối đa trong Photoshop là 240 pixel và chiều rộng thì không giới hạn. Sau đó cấu hình của love2d được đặt như sau.
-- 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
Điều quan trọng cần biết là mọi thứ đều được vẽ pixel perfect, nên có thể phóng to hình ảnh bằng ‘Nearest Neighbor (preserve hard edges)’. Đây là ưu điểm lớn so với làm mịn, vốn cần một phương pháp phóng to phức tạp.
Cũng quan trọng là tôi đặt những điều sau trong love2d
love.graphics.setDefaultFilter("nearest", "nearest")
love.graphics.setLineStyle("rough")
Canvas
Trong love2d tôi dùng một canvas để vẽ toàn bộ trò chơi lên đó. Chiều cao và chiều rộng của canvas được quyết định bởi cấu hình riêng của tôi, và Canvas cũng là một lớp của 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
Và chỉ cần vậy là có thể phóng to mọi thứ không giới hạn từ chiều cao màn hình tối thiểu 240 pixel. Tôi vẫn cần thử trên nhiều màn hình hơn, nhưng đến giờ nó hiển thị tốt ở nhiều độ phân giải và việc đổi kích thước cửa sổ cũng hoạt động tốt.
Camera
Tôi thiết lập camera để có thể đi theo một entity được chọn, tức là điểm trung tâm mà camera hướng vào. Điều quan trọng là Canvas luôn có chiều cao và chiều rộng cố định, còn ‘Scene’ thì rộng hơn nhiều, ví dụ 2000px. Vì vậy camera cũng nhận chiều rộng và chiều cao của viewport (canvas), và khi entity được theo dõi di chuyển, camera không tự di chuyển mà thay đổi toàn bộ cách love2d vẽ các tọa độ lên canvas. Nhờ vậy bạn có ảo giác rằng camera đang di chuyển sang phải. Nhưng thực ra mọi đối tượng đang di chuyển sang trái.
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
Thứ tự vẽ
Bạn đã đọc về zindex trong một bài khác, nhưng bài này nói về thứ tự của các entity trên màn hình, canvas và camera. Đây là vòng lặp chính xử lý mọi thứ theo đúng cách.
...
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
...
Lời kết
Camera là một đoạn mã khó hiểu. May là tôi nhận được rất nhiều ví dụ và sự giúp đỡ trên diễn đàn love2d. Nếu không có điều đó, có lẽ tôi đã mất quá nhiều thời gian và không biết kết quả sẽ ra sao.
Tôi cũng từng gặp hiện tượng jitter, tức là camera bị rung. Tôi đã giải quyết bằng cách chỉ thay đổi x khi đi theo 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