Frameset
Menghidupkan imej

Pemprosesan
Buat masa ini permainan dibina dengan bantuan Photoshop. Saya melakukannya dengan niat menggunakan editor ringkas yang menukar semuanya untuk permainan berdasarkan fail .psd. Di dalamnya ada apa yang saya panggil ‘Scene’, contohnya ruang tamu. Setiap lapisan dalam Photoshop bersamaan dengan satu Frameset dalam permainan.
Fail .psd diproses dengan NodeJs dan Python supaya menjadi imej lutsinar yang berasingan dan saya mendapat kontur imej (poligon). Imej kemudian diproses dengan TexturePacker supaya pemuatan dalam permainan pantas.
Ini juga dipanggil AtlasLoader, cuma saya menambah data tambahan pada json yang dibuat TexturePacker, seperti maklumat poligon.
{
"meta": {
"ukey": 20,
"height": 240,
"width": 800,
"pathFindingTop": 140,
"audiosets": [
"assets/audio/character/11/en/audio.mp"
],
"image": "assets/scene/20/atlas.png"
},
"layers": [
{
"name": "background-stone-circle",
"frames": [
{
"visible": true,
"quad": {
"x": 1596,
"y": 369,
"w": 198,
"h": 105
},
"x": 82,
"y": 59,
"order": 1,
"collidable": []
}
],
"collidableA": [
1,
2,
3,
4
],
"collidableB": [
4
],
"collidableRect": [
82,
59,
279,
163
],
"zindexStatic": 13,
"zindexType": "static"
},
...
Zindex
Dalam tambahan atlas.json juga tercatat zindex, iaitu nombor yang menunjukkan kedudukan lapisan dalam Photoshop. Dengan itu semua imej bertindih dalam susunan yang sama.
Zindex lebih penting daripada yang anda sangka, kerana ia menentukan sama ada entity berjalan di depan atau di belakang objek.
Ia nombor yang menentukan sejauh mana ke depan atau ke belakang imej perlu berada dalam permainan, sama seperti susunan lapisan dalam Photoshop.
Saya masih perlu refactor ini sekali lagi, tetapi ia melakukan tepat apa yang saya mahu buat masa ini. Jika kaki pemain berada pada ketinggian yang sama dengan entity lain, zindex dalam permainan berubah secara automatik. Saya juga menentukan sama ada sesuatu mempunyai zindex ‘static’ atau perlu berada ‘above’ entity lain.
function World:updateZindex()
local framesetSortByY = {}
for i = 1, #self.framesetsToProcess do
if self.framesetsToProcess[i] and self.framesetsToProcess[i].parentEntity then
local y = self.framesetsToProcess[i]:getY() or self.framesetsToProcess[i]:getFrameY()
if self.framesetsToProcess[i]:getOriginY() ~= Offset.BOTTOM then
-- @warning we use the max height, not the current frame height, but it is oke
y = y + self.framesetsToProcess[i]:calcFrameMaxHeight()
end
-- special cases
if self.framesetsToProcess[i].zindexType == "static" then
local zindexStatic = self.framesetsToProcess[i].zindexStatic
if zindexStatic ~= nil then
-- force the static zindex to be the given number, don't change it
y = tonumber(zindexStatic)
else
-- @warning we assume that the zindex are in correct order inside the yml files
y = i
end
end
-- temp processing table
table.insert(
framesetSortByY,
{
frameset = self.framesetsToProcess[i],
y = y,
ukey = self.framesetsToProcess[i].parentEntity.ukey
}
)
end
end
-- special cases, after processing
for i = 1, #framesetSortByY do
if framesetSortByY[i].frameset.zindexType == "above" then
-- search this frameset
for i2 = 1, #framesetSortByY do
if framesetSortByY[i].frameset.zindexStatic == framesetSortByY[i2].ukey then
framesetSortByY[i].y = framesetSortByY[i2].y + 1
break
end
end
end
end
-- sort using the calculate y
table.sort(
framesetSortByY,
function(a, b)
return a.y ~= nil and b.y ~= nil and a.y < b.y
end
)
-- now we loop over the this sorted by y and create the new zindex
for i = 1, #framesetSortByY do
framesetSortByY[i].frameset.zindex = i
end
-- now we let the engine do his thing with zindex sorting
World.super.updateZindex(self)
end
Frameset
Tetapi artikel ini sebenarnya tentang apa sebenarnya Frameset dalam engine3. Ia kelas tempat anda boleh meletakkan 1 atau lebih imej. Jika ada beberapa, anda boleh memainkannya seperti animasi. Sesuatu entity boleh mempunyai 1 atau lebih Frameset. Semua maklumat penting tentang imej ada dalam Frameset, dan boleh diubah semasa permainan.
Gambaran ringkas kelas Frameset
---@class Frameset: Object
---@field parentEntity Object The parent entity that going to use this frameset
---@field namespace string For example scene/item/character
---@field name string
---@field visible boolean
---@field config Config
---@field resource love.Image resource A reference to the atlas image
---@field resourceFile string resourceFile Resource path
---@field frames table<Frame> frames A static image contains only one frame
---@field framekey number Current frame key, a non animation have only one 1 frame. (default 1)
---@field isAnimation boolean It is counting the number of frames > 1
---@field zindex integer The z-index order
---@field scaleX? number Scale. A negative scaling factor to flip about its centerline.
---@field scaleY? number Scale. A negative scaling factor to flip about its centerline.
---@field quad? table<love.Quad> quad Default not loaded
---@field x? integer The x position for the full frameset, to overrule the default Frame.x
---@field y? integer The y position for the full frameset, to overrule the default Frame.y
---@field frameMaxWidth? integer The max width from all the frames in a frameset this is only set after calling self:calcFrameMaxWidth()
---@field frameMaxHeight? integer The max height from all the frames in a frameset this is only set after calling self:calcFrameMaxHeight()
---@field frameMaxWidthObj Frame The frame from all the frames in a frameset this is only set after calling self:calcFrameMaxWidth()
---@field frameMaxHeightObj? Frame The frame from all the frames in a frameset this is only set after calling self:calcFrameMaxHeight()
---@field time? integer Animation timer
---@field animationStatus? number See constants IDLE|PLAYING|PAUSE
---@field callbackAfterDraw? table<function> Table with 0 or more callback functions.
---@field callbackAfterUpdate? table<function> Table with 0 or more callback functions.
---@field callbackAfterXy? table<function> Table with 0 or more callback functions.
---@field callbackAfterVisibility? table<function> Table with 0 or more callback functions.
---@field collidableA? table If this frameset is collidable (sides) {1[opt](left),2([opt]right),3([opt]top),4([opt]bottom)}.
---@field collidableB? table If this frameset is collidable (sides) overrule the other collidable target option {1[opt](left),2([opt]right),3([opt]top),4([opt]bottom)}.
---@field collidableRects? table Collidable rects [{x1,y1,x2,y2}, ...]
---@field whitespace? boolean If the source image uses a whitespace around its image
---@field originX? integer Default the x coordinate origin is Offset.LEFT. For example when you use center, the x position is in the frameset center
---@field originY? integer
---@field callback Callback
...
function Frameset:getOriginY()
return self.originY
end
function Frameset:getOriginX()
return self.originX
end
function Frameset:getZindex()
return self.zindex
end
function Frameset:getFrameX()
local currentFrame = self:getCurrentFrame()
return currentFrame.x
end
function Frameset:getFrameX()
local currentFrame = self:getCurrentFrame()
return currentFrame.x
end
function Frameset:getX()
return self.x
end
function Frameset:getFrameY()
local currentFrame = self:getCurrentFrame()
return currentFrame.y
end
function Frameset:getY()
return self.y
end
function Frameset:stop()
self.animationStatus = self.IDLE
self.framekey = 1
self:hide()
end
--- Start the frameset animation.
-- When you start a animation it will visible.
-- time to show, framekey
-- {
-- {0.00, 1},
-- {0.46, 2},
-- {0.95, 5},
-- ...
-- }
---@param timeTable? table Play the tables like the table
---@param timeTableEndCallback? table When the last key is played
---@param ignoreAutoShow? bool Don't execute the show() function
function Frameset:play(timeTable, timeTableEndCallback, ignoreAutoShow)
if timeTable then
self.timeTable = timeTable
for _, date in pairs(timeTable) do
Tick.delay(
function()
self.framekey = date[2]
-- the last key
if self.timeTable[#self.timeTable][1] == date[1] then
if timeTableEndCallback then
timeTableEndCallback()
end
end
end,
date[1]
)
end
end
self.animationStatus = self.PLAYING
if ignoreAutoShow ~= true then
self:show()
end
end
function Frameset:pause(frameKey)
if frameKey ~= nil then
self.framekey = frameKey
self.frames[self.framekey].visible = true
end
self.animationStatus = self.PAUSE
end
function Frameset:update(dt, entity)
local currentFrame = self:getCurrentFrame()
local update = true
-- its not not visible on the screen
if currentFrame.visible == false then
update = false
end
-- its a static image, no need to update things
if self.isAnimation == false then
update = false
end
-- We don't want to play this animation
if self.isAnimation and self.animationStatus ~= self.PLAYING then
update = false
end
-- We manage our own frame time
if self.timeTable ~= nil then
update = false
end
if update then
self.time = self.time + dt
if self.time >= (currentFrame.duration / 1000) then
self.framekey = (self.framekey % #self.frames) + 1
self.time = 0
end
end
self:callback(self.callbackAfterUpdate, entity, self, update, currentFrame, dt)
return update
end
...
Kelas ini kini mengandungi kira-kira 866 baris dan terlalu panjang untuk dipaparkan di sini. Ia hanya mengandungi asas untuk mengawal imej, sama ada animasi atau statik.
Animasi
Apabila sesuatu frameset mempunyai beberapa imej, ia boleh dimainkan berturut-turut.
frameset:play()

Martin berjalan ke kanan