I use a Viewport obejct for representing the area of the game actually rendered onto the the screen, but I use it also like a camera, because it is possible to lock the center of the viewport on a specific object (usually the player).
I would like to develop the appearance of a location sign in the upper corner of the screen when then player cross athe border between two adjacent maps.
Now, the feature itself is pretty easy and it works, there's anyway a bug I would like to address, because could be a problem for a number of future features of the game I have in mind.
When I cross the border the sign appears, but when the player moves the sign trembles. I could think about three possible causes:
1. The sign is temporarly rendered at not integer coords
2. The x and y of the viewport are modified during the rendering
3. The use of
Code: Select all
math.floor
Code: Select all
math.ceil
I looked into each of this reason, but I still have no answer.
I can say a similar problem (related to cause number 2) occured with the player rendering when I first tried to add more maps to one world. There I realized that the player should have relative coords to the map and some renderCoords, which the viewport should refer to. When I implemented this solution I first tried to set those renderCoords in the player directly when I rendered it, but this approach made the player "tremble" then I resolved initializing the renderCoords when I first loaded the player in the world and then updating them in the same tween where I updated the player map coords.
There are the code of the World and the Viewport class:
Code: Select all
local EventNameEnum = require 'data/enum/event/EventNameEnum'
local constants = require 'src/constants'
local EntityStateEnum = require 'data/enum/world/EntityStateEnum'
local EntityProximityMapBorderEnum = require 'data/enum/world/EntityProximityMapBorderEnum'
local Timer = require 'lib/knife/timer'
local LinesNamespaceEnum = require 'data/enum/general/LinesNamespaceEnum'
local DrawnPanel = require 'src/gui/panel/DrawnPanel'
local FixedText = require 'src/gui/text/FixedText'
---@class World
local World = Class {}
function World:init(name, maps, mapPositions, eventManager, linesProvider, viewport)
local timer = Timer
local showLocationSign = false
local locationSignPanel = DrawnPanel {
width = 4 * constants.TILE_SIZE,
height = 1 * constants.TILE_SIZE
}
local locationSignText = FixedText {
text = '',
align = 'center',
width = 4 * constants.TILE_SIZE,
height = 1 * constants.TILE_SIZE,
color = constants.COLORS.black,
isVisible = true,
isActive = true,
opacity = 1
}
function self.getName() return name end
function self.getMaps() return maps end
function self.getMapPositions() return mapPositions end
---@param mapId string
---@param toX integer
---@param toY integer
---@return string?, integer?, integer?, EntityProximityMapBorderEnum?
local function getDestinationMap(mapId, toX, toY)
local originMapPosition = mapPositions[mapId]
local destinationX = originMapPosition.x + toX * constants.TILE_SIZE
local destinationY = originMapPosition.y + toY * constants.TILE_SIZE
for mapName, position in pairs(mapPositions) do
local mapWidth = maps[mapName].getWidth() * constants.TILE_SIZE
local mapHeight = maps[mapName].getHeight() * constants.TILE_SIZE
if destinationX >= position.x and destinationX < position.x + mapWidth and
destinationY >= position.y and destinationY < position.y + mapHeight then
local tileX = math.floor((destinationX - position.x) / constants.TILE_SIZE)
local tileY = math.floor((destinationY - position.y) / constants.TILE_SIZE)
local relativePosition
if position.x < originMapPosition.x then
relativePosition = EntityProximityMapBorderEnum.West
elseif position.x > originMapPosition.x then
relativePosition = EntityProximityMapBorderEnum.East
elseif position.y < originMapPosition.y then
relativePosition = EntityProximityMapBorderEnum.North
elseif position.y > originMapPosition.y then
relativePosition = EntityProximityMapBorderEnum.South
end
return mapName, tileX, tileY, relativePosition
end
end
return nil, nil, nil
end
function self.initialize()
---@param _ EntityProximityMapBorderEnum
eventManager.subscribe(EventNameEnum.ProximityToBorderEvent, self, function (_)
-- TODO: this intersect if the entity is near the map border, could be useful
end)
---@param data {entity: Entity, toX: integer, toY: integer}
eventManager.subscribe(EventNameEnum.DestinationOutOfBorderEvent, self, function (data)
local entity, toX, toY = data.entity, data.toX, data.toY
-- get the current map
local originMap = maps[entity.getMapId()]
-- get the destination map
local destinationMapId, destinationMapX, destinationMapY, relativePosition = getDestinationMap(
entity.getMapId(), toX, toY)
if destinationMapId and destinationMapX and destinationMapY then
local destinationMap = maps[destinationMapId]
-- add the entity to the destination map
entity.setMapId(destinationMapId)
local destx, desty = destinationMapX, destinationMapY
if relativePosition == EntityProximityMapBorderEnum.West then
destx = destx + 1
elseif relativePosition == EntityProximityMapBorderEnum.North then
desty = desty + 1
elseif relativePosition == EntityProximityMapBorderEnum.East then
destx = destx - 1
elseif relativePosition == EntityProximityMapBorderEnum.South then
desty = desty - 1
end
entity.setMapX(destx)
entity.setMapY(desty)
entity.setReservedX(destinationMapX)
entity.setReservedY(destinationMapY)
destinationMap.addEntity(entity, entity.getId())
-- remove the entity from the origin map
originMap.removeEntity(entity.getId())
if destinationMap.getMetadata().outdoor then
locationSignText.updateChunks(linesProvider.getLine(LinesNamespaceEnum.World, entity.getMapId()))
showLocationSign = true
timer.after(3, function ()
locationSignText.setText('')
showLocationSign = false
end)
end
return
end
entity.changeState(EntityStateEnum.Idle)
end)
end
function self.update(dt)
timer.update(dt)
for _, map in pairs(maps) do
map.update(dt)
end
end
function self.render()
-- render maps tiles without priority
for mapName, map in pairs(maps) do
map.renderTiles(mapPositions[mapName].x, mapPositions[mapName].y, false)
end
-- render entities
for mapName, map in pairs(maps) do
map.renderEntities(mapPositions[mapName].x, mapPositions[mapName].y)
map.renderEntityAnimations(mapPositions[mapName].x, mapPositions[mapName].y)
end
-- render maps tiles with priority
for mapName, map in pairs(maps) do
map.renderTiles(mapPositions[mapName].x, mapPositions[mapName].y, true)
end
if showLocationSign then
locationSignPanel.render(math.floor(viewport.getX()) + 2, math.floor(viewport.getY()) + 2)
locationSignText.render(math.floor(viewport.getX()) + 2, math.floor(viewport.getY()) + 2)
end
end
end
return World
Code: Select all
---@class Viewport
local Viewport = Class {}
function Viewport:init(params)
local virtualWidth = params.virtualWidth
local virtualHeight = params.virtualHeight
local tileSize = params.tileSize
local x = math.floor(params.x) or 0
local y = math.floor(params.y) or 0
local width = math.floor(params.width) or virtualWidth
local height = math.floor(params.height) or virtualHeight
local object = nil
local center = {x = 0, y = 0}
local function setCenter()
center = {
x = math.floor(x + (width / 2)),
y = math.floor(y + (height / 2))
}
end
setCenter()
function self.getX() return x end
function self.getY() return y end
function self.update()
if object then
x = math.floor(object.x - virtualWidth * 0.5)
y = math.floor(object.y - virtualHeight * 0.5)
end
end
function self.render()
if object then
love.graphics.translate(
math.floor(-object.x + virtualWidth * 0.5),
math.floor(-object.y + virtualHeight * 0.5)
)
end
end
function self.setOrigin()
x = math.floor(center.x - (width / 2))
y = math.floor(center.y - (height / 2))
end
function self.pointToObject(o)
object = o
x = object.x - virtualWidth * 0.5
y = object.y - virtualWidth * 0.5
end
function self.contains(ox, oy, owidth, oheight)
if (
x < ox + owidth + 8 and
x + width + 8 > ox and
y < oy + oheight + 8 and
y + height + 8 > oy
) then
return true
end
return false
end
end
return Viewport