diff --git a/engine/client/sprite.lua b/engine/client/sprite.lua index 6ab70b9a..f39782be 100644 --- a/engine/client/sprite.lua +++ b/engine/client/sprite.lua @@ -4,40 +4,57 @@ -- --==========================================================================-- +require( "engine.client.spriteanim" ) + class( "sprite" ) +accessor( sprite, "spriteSheet" ) +accessor( sprite, "spriteSheetName" ) +accessor( sprite, "width" ) +accessor( sprite, "height" ) +accessor( sprite, "frameTime" ) +accessor( sprite, "animations" ) +accessor( sprite, "events" ) + +sprite._commands = { + setFrameTime = 1, + setFrameIndex = 2 +} + function sprite:sprite( spriteSheet ) - local data = require( spriteSheet ) - local image = love.graphics.newImage( data[ "image" ] ) - self.spriteSheet = image - self.width = data[ "width" ] - self.height = data[ "height" ] - self.frametime = data[ "frametime" ] - self.animations = data[ "animations" ] or {} - self.events = data[ "events" ] or {} + -- Make sure this exists before loading a sprite sheet + self.animations = {} + + if (spriteSheet) then + self:setSpriteSheet(spriteSheet) + else + self.spriteSheetName = "" + self.width = 0 + self.height = 0 + self.frameTime = 0 + self.events = {} + end - self.curtime = 0 - self.frame = 1 + self.animInstances = {} + self.curAnim = nil end function sprite:draw() local image = self:getSpriteSheet() + if (not image) then return end + love.graphics.draw( image, self:getQuad() ) end -accessor( sprite, "animation" ) -accessor( sprite, "animationName" ) -accessor( sprite, "animations" ) -accessor( sprite, "events" ) -accessor( sprite, "frametime" ) - function sprite:setFilter( ... ) local image = self:getSpriteSheet() + if (not image) then return end + image:setFilter( ... ) end function sprite:getQuad() - if ( self.quad == nil ) then + if ( self.quad == nil and self.spriteSheet) then local image = self:getSpriteSheet() self.quad = love.graphics.newQuad( 0, @@ -52,60 +69,51 @@ function sprite:getQuad() return self.quad end -accessor( sprite, "spriteSheet" ) -accessor( sprite, "width" ) -accessor( sprite, "height" ) - function sprite:onAnimationEnd( animation ) end -function sprite:onAnimationEvent( event ) +function sprite:onAnimationEvent( instance, event ) end function sprite:setAnimation( animation ) - local animations = self:getAnimations() - local name = animation - animation = animations[ name ] - if ( animation == nil ) then - return - end - - if ( animation == self:getAnimation() ) then - return + if (typeof(animation, "spriteanim")) then + self.curAnim = animation + elseif (type(animation) == "string") then + if (not self.curAnim or (self.curAnim and self.curAnim:getAnimationName() ~= animation)) then + local instance = self:createAnimInstance(animation); + instance:remove() + instance:setSprite(self) + instance.sprIndex = 0 + self.animInstances[0] = instance + self.curAnim = instance + end + elseif (not animation) then + self.curAnim = nil + else + error(string.format("Invalid animation type %q", type(animation))) end - self.animation = animation - self.animationName = name - self.frame = animation.from + self:updateQuad() +end - self:updateFrame() +function sprite:getAnimation() + return self.curAnim end function sprite:update( dt ) - local animation = self:getAnimation() - if ( animation == nil ) then - return - end - - self.curtime = self.curtime + dt - - if ( self.curtime >= self.frametime ) then - self.curtime = 0 - self.frame = self.frame + 1 - - if ( self.frame > animation.to ) then - local name = self:getAnimationName() - self.frame = animation.from - self:onAnimationEnd( name ) + for index = 0, table.getn(self.animInstances) do + local instance = self.animInstances[index] + if (instance and not instance.paused) then + instance:update(dt) end - - self:updateFrame() end end -function sprite:updateFrame() +function sprite:updateQuad() + if (not self.curAnim) then return end + local quad = self:getQuad() - local frame = self.frame - 1 + local frame = self.curAnim.frameIndex - 1 local width = self:getWidth() local height = self:getHeight() local image = self:getSpriteSheet() @@ -113,18 +121,93 @@ function sprite:updateFrame() local x = frame * width % imageWidth local y = math.floor( frame * width / imageWidth ) * height quad:setViewport( x, y, width, height ) +end + +local function processAnimFrame(spr, frame) + if (type(frame) == "number") then + return { { command = sprite._commands.setFrameIndex, value = frame } } + elseif (type(frame) == "function") then + local ret = {} + + while (true) do + local i = frame() + + if (not i) then break end + + table.insert(ret, { command = sprite._commands.setFrameIndex, value = i }) + end + + return ret + elseif (type(frame) == "table") then + if (type(frame.frameTime) == "number" and frame.frames) then + local ret = processAnimFrame(spr, frame.frames) + table.insert(ret, 1, { command = sprite._commands.setFrameTime, value = frame.frameTime }) + table.insert(ret, { command = sprite._commands.setFrameTime, value = spr:getFrameTime() }) + return ret + elseif (type(frame.from) == "number" and type(frame.to) == "number") then + local ret = {} + + for frameIndex = frame.from, frame.to, (frame.from < frame.to and 1 or -1) do + table.insert(ret, { command = sprite._commands.setFrameIndex, value = frameIndex }) + end + + return ret + else + local ret = {} + + for i, v in ipairs(frame) do + table.append(ret, processAnimFrame(spr, v)) + end + + return ret + end + else + assert(false, "Frame table must contain frame indices, a range, or a frame sub-table") + end +end + +function sprite:loadAnimations(animations) + if (not animations) then return end + assert(type(animations) == "table", "Animations must be a table") - local events = self:getEvents() - local event = events[ frame ] - if ( event ) then - self:onAnimationEvent( event ) + for animName, frameTbl in pairs(animations) do + local sequence = processAnimFrame(self, frameTbl) + table.insert(sequence, 1, { command = sprite._commands.setFrameTime, value = self:getFrameTime() }) + self.animations[animName] = sequence end end +function sprite:createAnimInstance(animName) + local animations = self:getAnimations() + local frames = animations[ animName ] + + assert(frames, string.format("Sprite Sheet %q does not contain animation %q", self:getSpriteSheetName(), animName)) + + local instance = spriteanim() + instance:setSprite(self) + instance:setAnimationName(animName) + instance:setSequence(frames) + + table.insert(self.animInstances, instance) + instance.sprIndex = table.getn(self.animInstances) + + return instance +end + + function sprite:__tostring() - local t = getmetatable( self ) - setmetatable( self, {} ) - local s = string.gsub( tostring( self ), "table", "sprite" ) - setmetatable( self, t ) - return s + return string.format("sprite: %q", self.spriteSheetName) +end + +function sprite:setSpriteSheet(spriteSheet) + local data = require( spriteSheet ) + self.spriteSheet = love.graphics.newImage( data[ "image" ] ) + self.spriteSheetName = spriteSheet + + self:setEvents(data[ "events" ] or {}) + self:setFrameTime(data[ "frametime" ]) + self:loadAnimations(data[ "animations" ]) -- load animations after the frametime is set + + self.width = data[ "width" ] + self.height = data[ "height" ] end diff --git a/engine/client/spriteanim.lua b/engine/client/spriteanim.lua new file mode 100644 index 00000000..bd7835cf --- /dev/null +++ b/engine/client/spriteanim.lua @@ -0,0 +1,139 @@ +class "spriteanim" + +accessor( spriteanim, "sprite" ) +accessor( spriteanim, "animationName" ) +accessor( spriteanim, "sequence" ) + +function spriteanim:spriteanim() + self.sprIndex = 0 -- This is the index in the owning sprite's animInstance table. (result of table.insert) + self.curTime = 0 + self.targetFrameTime = 0 + self.paused = false + self.sequence = {} + self.sequenceIndex = 1 + self.frameIndex = 1 + self.singleFrameFinished = false + self.loop = true + self.animEnded = false +end + +function spriteanim:__tostring() + return string.format("sprite animation: %q [frame: %i]", self.animationName, self.frameIndex) +end + +function spriteanim:setSequence(sequence) + self.sequence = sequence + + -- `#sequence == 2` should be true only for single frame anims. The first sequence should be the frameTime command, second should be frameIndex. + -- This prevents single frame animations from constantly calling `spri:onAnimationEnd`, also single frame animations dont need to loop + if (#self.sequence == 2) then + self.loop = false + end + + self:play() +end + +function spriteanim:pause() + self.paused = true +end + +function spriteanim:resume() + self.paused = false +end + +function spriteanim:loop(bShouldLoop) + self.loop = toboolean(bShouldLoop) + + if (bShouldLoop) then + self:play() + end +end + +function spriteanim:play() + self.sequenceIndex = 1 + self.curTime = 0 + self.singleFrameFinished = false + self:resume() + self:pollCommands() +end + +function spriteanim:pollCommands() + if (self.paused) then return end + + local sequence = self.sequence + if (self.sequenceIndex > #sequence) then + self:pause() + return + end + + local command = sequence[self.sequenceIndex] + local spr = self:getSprite() + + if (command.command == sprite._commands.setFrameTime) then + self.targetFrameTime = command.value + self.curTime = 0 + + -- increment sequence index and repoll to prevent frameIndex flickering + self.sequenceIndex = self.sequenceIndex + 1 + self:pollCommands() + return + elseif (command.command == sprite._commands.setFrameIndex) then + self.frameIndex = command.value + + local event = spr.events[self.frameIndex] + if (event) then + if (type(event) ~= "table") then + event = { event } + end + + for i, v in ipairs(event) do + local status, ret = pcall(spr.onAnimationEvent, spr, self, v) + if (not status) then print(ret) end + end + end + else + error(string.format("Invalid sprite command %q", tostring(command.command))) + end + + self.sequenceIndex = self.sequenceIndex + 1 -- Increment after so we dont have to bounds check it because lazy + + if ( self.sequenceIndex > #sequence ) then + if (self.loop) then + self.sequenceIndex = 1 + end + + local name = self:getAnimationName() + local status, ret = pcall(spr.onAnimationEnd, spr, name ) + if (not status) then print(ret) end + end +end + +function spriteanim:update(dt) + if (self.paused) then return end + + local spr = self:getSprite() + if (not spr) then return end + + self.curTime = self.curTime + dt + + if ( self.curTime >= self.targetFrameTime ) then + self.curTime = 0 + + self:pollCommands() + + spr:updateQuad() + self.singleFrameFinished = true + end +end + +function spriteanim:remove() + local spr = self:getSprite() + if (not spr) then return end + + local instances = spr.animInstances + if (instances[self.sprIndex] == self) then -- prevent accidentally removing another anim at the same index.. just in case + table.remove(instances, self.sprIndex) + end + + self:setSprite(nil) +end diff --git a/engine/shared/entities/entity.lua b/engine/shared/entities/entity.lua index 162c6923..7e39e231 100644 --- a/engine/shared/entities/entity.lua +++ b/engine/shared/entities/entity.lua @@ -408,11 +408,9 @@ end if ( _CLIENT ) then function entity:getAnimation() local sprite = self:getSprite() - if ( type( sprite ) ~= "sprite" ) then - return - end + if ( type( sprite ) ~= "sprite" ) then return end - return sprite:getAnimationName() + return sprite:getAnimation() end end @@ -731,7 +729,7 @@ if ( _CLIENT ) then function entity:onAnimationEnd( animation ) end - function entity:onAnimationEvent( event ) + function entity:onAnimationEvent( instance, event ) end end @@ -895,8 +893,8 @@ if ( _CLIENT ) then self:onAnimationEnd( animation ) end - sprite.onAnimationEvent = function( _, event ) - self:onAnimationEvent( event ) + sprite.onAnimationEvent = function( _, instance, event ) + self:onAnimationEvent( instance, event ) end end diff --git a/engine/shared/entities/player.lua b/engine/shared/entities/player.lua index 079119db..2b673ccd 100644 --- a/engine/shared/entities/player.lua +++ b/engine/shared/entities/player.lua @@ -363,7 +363,7 @@ if ( _CLIENT ) then "Plays footstep sounds for players", nil, { "archive" } ) - function player:onAnimationEvent( event ) + function player:onAnimationEvent( instance, event ) if ( cl_footsteps:getBoolean() ) then updateStepSound( self, event ) end diff --git a/images/moveindicator.lua b/images/moveindicator.lua index 16cde4d6..8c0b90f0 100644 --- a/images/moveindicator.lua +++ b/images/moveindicator.lua @@ -4,9 +4,6 @@ return { height = 16, frametime = 0.04, animations = { - click = { - from = 1, - to = 8 - } + click = { from = 1, to = 8 } } } diff --git a/images/player.lua b/images/player.lua index 9b134385..42c053fd 100644 --- a/images/player.lua +++ b/images/player.lua @@ -4,74 +4,26 @@ return { height = 32, frametime = 0.25, animations = { - idlenorth = { - from = 1, - to = 1 - }, - idleeast = { - from = 2, - to = 2 - }, - idlesouth = { - from = 3, - to = 3 - }, - idlewest = { - from = 4, - to = 4 - }, + idlenorth = 1, + idleeast = 2, + idlesouth = 3, + idlewest = 4, -- idlenorth - idlenortheast = { - from = 1, - to = 1, - }, + idlenortheast = 1, -- idlesouth - idlesoutheast = { - from = 3, - to = 3, - }, + idlesoutheast = 3, -- idlesouth - idlesouthwest = { - from = 3, - to = 3, - }, + idlesouthwest = 3, -- idlenorth - idlenorthwest = { - from = 1, - to = 1, - }, - walknorth = { - from = 33, - to = 36 - }, - walkeast = { - from = 65, - to = 68, - }, - walksouth = { - from = 97, - to = 100, - }, - walkwest = { - from = 129, - to = 132, - }, - walknortheast = { - from = 161, - to = 164, - }, - walksoutheast = { - from = 193, - to = 196, - }, - walksouthwest = { - from = 225, - to = 228, - }, - walknorthwest = { - from = 257, - to = 260, - } + idlenorthwest = 1, + walknorth = { from = 33, to = 36 }, + walkeast = { from = 65, to = 68 }, + walksouth = { from = 97, to = 100 }, + walkwest = { from = 129, to = 132 }, + walknortheast = { from = 161, to = 164 }, + walksoutheast = { from = 193, to = 196 }, + walksouthwest = { from = 225, to = 228 }, + walknorthwest = { from = 257, to = 260 } }, events = { -- walknorth