diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f650c5..7ed7e67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- **actor2D:** Make hitbox modifiable. + +- **actor2D:** Add multiple hitbox support. + +- **assets:** Add a way to get current relative frame + +- **assets:** Add more wrapper around animator functions in Sprite + +- **world:** Add more wrapper around sprite functions in BaseActor + +- **assets:** Add a new getCurrentAnimation function + +- **world:** Add a way to automatically load hitbox from a file + +### Changed + +- **world2D:** Use a list for bodies (hitboxes, etc) and one other for actors + +- **world2D:** Make the hitbox an object, owned by the actor + ### Fixed - **world:** Remove a forgotten camera debug function diff --git a/examples/gameplay/moveplayer/actors/parent.lua b/examples/gameplay/moveplayer/actors/parent.lua index 502d7b6..3ca81f5 100644 --- a/examples/gameplay/moveplayer/actors/parent.lua +++ b/examples/gameplay/moveplayer/actors/parent.lua @@ -7,7 +7,7 @@ function Parent:new(world, type, x, y, w, h, isSolid) end function Parent:draw() - self:drawHitbox() + self:drawMainHitbox() end return Parent diff --git a/examples/gameplay/moveplayer/actors/wall.lua b/examples/gameplay/moveplayer/actors/wall.lua index 2981cb4..bc35e3a 100644 --- a/examples/gameplay/moveplayer/actors/wall.lua +++ b/examples/gameplay/moveplayer/actors/wall.lua @@ -7,7 +7,7 @@ function Wall:new(world, x, y, w, h) end function Wall:draw() - self:drawHitbox() + self:drawMainHitbox() utils.graphics.resetColor( ) end diff --git a/examples/gameplay/plateform/actors/player.lua b/examples/gameplay/plateform/actors/player.lua index 74b0173..1c95872 100644 --- a/examples/gameplay/plateform/actors/player.lua +++ b/examples/gameplay/plateform/actors/player.lua @@ -6,6 +6,12 @@ function Player:new(world, x, y, id) self:setSprite("player", 8, 12) self:cloneSprite() self:setYGravity(480) + + self.isPunching = false + self.direction = 1 + + self.punchName = "" + self:setHitboxFile("examples.gameplay.plateform.assets.playerhitbox") end function Player:updateStart(dt) @@ -15,15 +21,27 @@ function Player:updateStart(dt) self.ysp = -280 end if self.keys["down"].isDown then - --self.ysp = 120 + self.mainHitbox:modify(0, 8, 16, 16) + else + self.mainHitbox:modify(0, 0, 16, 24) end - if self.keys["left"].isDown then + if self.keys["left"].isDown and (not self.isPunching) then self.xsp = -120 end - if self.keys["right"].isDown then + if self.keys["right"].isDown and (not self.isPunching) then self.xsp = 120 end + if self.keys["A"].isDown then + self.isPunching = true + else + self.isPunching = false + end + + if (self.isPunching) then + self:checkHitboxesCollisions() + end + if self.keys["start"].isPressed then --self.world:switchActivity() --self.assets:switchActivity() @@ -41,6 +59,9 @@ end function Player:setAnimation() self:setCustomSpeed(math.abs(self.xsp) / 12) + if (self.isPunching) then + self:changeAnimation("punch", false) + else if (self.onGround) then if math.abs(self.xsp) > 0 then self:changeAnimation("walk", false) @@ -50,22 +71,36 @@ function Player:setAnimation() else self:changeAnimation("jump", true) end + end end function Player:setDirection(direction) direction = direction or 0 if direction ~= 0 then direction = utils.math.sign(direction) + self.direction = direction self:setSpriteScallingX(direction) end end function Player:collisionResponse(collision) if collision.other.type == "coin" then - collision.other:takeCoin(self) + collision.other.owner:takeCoin(self) end end +function Player:hitboxResponse(name, type, collision) + if (collision.other.type == "coin") and (type == "punch") then + collision.other.owner:takeCoin(self) + end +end + +function Player:draw() + Player.super.draw(self) + self:drawHitboxes() + utils.graphics.resetColor() +end + function Player:drawHUD(id) love.graphics.print(id .. " test", 4, 4) end diff --git a/examples/gameplay/plateform/assets/monkey_lad.lua b/examples/gameplay/plateform/assets/monkey_lad.lua index a854bdd..89bced3 100644 --- a/examples/gameplay/plateform/assets/monkey_lad.lua +++ b/examples/gameplay/plateform/assets/monkey_lad.lua @@ -43,8 +43,8 @@ return { pauseAtEnd = false, }, ["punch"] = { - startAt = 10, - endAt = 10, + startAt = 11, + endAt = 11, loop = 1, speed = 0, pauseAtEnd = false, diff --git a/examples/gameplay/plateform/assets/playerhitbox.lua b/examples/gameplay/plateform/assets/playerhitbox.lua new file mode 100644 index 0000000..d60c018 --- /dev/null +++ b/examples/gameplay/plateform/assets/playerhitbox.lua @@ -0,0 +1,13 @@ +return { + ["idle"] = { + { + {"main", 0, 0, 16, 24, true} + } + }, + ["punch"] = { + { + {"main", 0, 0, 16, 24, true}, + {"punch", 16, 6, 12, 12, false} + } + } +} diff --git a/gamecore/modules/assets/animator.lua b/gamecore/modules/assets/animator.lua index 1498153..17fa139 100644 --- a/gamecore/modules/assets/animator.lua +++ b/gamecore/modules/assets/animator.lua @@ -94,6 +94,10 @@ end -- INFO FUNCTIONS -- get information with these functions +function Animator:getCurrentAnimation() + return self.currentAnimation +end + function Animator:getAnimationDuration(animation) return (self.animationData.endAt - self.animationData.startAt) / self.animationData.speed end @@ -102,6 +106,10 @@ function Animator:getFrame() return self.frame end +function Animator:getRelativeFrame() + return self.frame - (self.animationData.startAt) + 1 +end + function Animator:animationExist(name) return (self.sprite.data.animations[self.currentAnimation] ~= nil) end diff --git a/gamecore/modules/assets/sprites.lua b/gamecore/modules/assets/sprites.lua index ddf4f89..f33b919 100644 --- a/gamecore/modules/assets/sprites.lua +++ b/gamecore/modules/assets/sprites.lua @@ -66,6 +66,10 @@ end -- INFO FUNCTIONS -- get information with these functions +function Sprite:getCurrentAnimation() + return self.animator:getCurrentAnimation() +end + function Sprite:animationExist(name) return self.animator:animationExist(name) end @@ -74,6 +78,18 @@ function Sprite:getDimensions() return self.tileset:getDimensions() end +function Sprite:getFrame() + return self.animator:getFrame() +end + +function Sprite:getAnimationDuration(animation) + return self.animator:getAnimationDuration(animation) +end + +function Sprite:getRelativeFrame() + return self.animator:getRelativeFrame() +end + -- DRAW FUNCTIONS -- Draw sprites using these functions diff --git a/gamecore/modules/world/actors/actor2D.lua b/gamecore/modules/world/actors/actor2D.lua index a6613d2..ef96360 100644 --- a/gamecore/modules/world/actors/actor2D.lua +++ b/gamecore/modules/world/actors/actor2D.lua @@ -26,12 +26,21 @@ local cwd = (...):gsub('%.actor2D$', '') .. "." local BaseActor = require(cwd .. "baseactor") local Actor2D = BaseActor:extend() +local Hitbox = require(cwd .. "utils.hitbox2D") + -- INIT FUNCTIONS -- Initialise the actor and its base functions function Actor2D:new(world, type, x, y, w, h, isSolid) - self:initHitbox(x, y, w, h) + self:setCoordinate(x, y) Actor2D.super.new(self, world, type, isSolid) + self:initHitboxes(w, h) +end + +function BaseActor:destroy() + self.world:removeActor(self) + self.world:removeBody(self.mainHitbox) + self.isDestroyed = true end -- MOVEMENT FUNCTIONS @@ -46,6 +55,7 @@ function Actor2D:initMovement() end function Actor2D:autoMove(dt) + self:updateHitboxes() self.onGround = false self:applyGravity(dt) @@ -117,7 +127,8 @@ end function Actor2D:move(dx, dy) local cols, colNumber = {}, 0 if (self.isDestroyed == false) then - self.x, self.y, cols, colNumber = self.world:moveActor(self, dx, dy, self.filter) + self.x, self.y, cols, colNumber = self.mainHitbox:checkCollision(dx, dy, self.filter) + self.mainHitbox:updatePosition() end return self.x, self.y, cols, colNumber end @@ -125,7 +136,7 @@ end function Actor2D:checkCollision(dx, dy) local x, y, cols, colNumber = dx, dy, {}, 0 if (self.isDestroyed == false) then - x, y, cols, colNumber = self.world:moveActor(self, dx, dy, self.filter) + x, y, cols, colNumber = self.mainHitbox:checkCollision(dx, dy, self.filter) end return self.x, self.y, cols, colNumber end @@ -165,14 +176,167 @@ function Actor2D:applyGravity(dt) end end --- COORDINATE FUNCTIONS --- Functions related to coordinate and hitbox +-- HITBOXES FUNCTIONS +-- Functions related to actor hitboxes -function Actor2D:initHitbox(x, y, w, h) - self.x = x or 0 - self.y = y or 0 +function Actor2D:initHitboxes(w, h) self.w = w or 0 self.h = h or 0 + + self:initMainHitbox() + + self.hitboxes = {} + self.hitboxListFile = "" + self.hitboxList = nil +end + +function Actor2D:setHitboxFile(file) + self.hitboxList = require(file) + self.hitboxListFile = file +end + +function Actor2D:getAutomaticHitboxLoading() + return (self.hitboxList ~= nil) +end + +function Actor2D:getHitboxFile() + return self.hitboxListFile +end + +function Actor2D:getHitboxList(animation, frame) + if (animation == nil) or (self.hitboxList == nil) then + return self.hitboxList + else + local list = self.hitboxList[animation] + + if (frame == nil) or (list == nil) then + return list + else + return list[frame] + end + end +end + +function Actor2D:updateHitboxes() + if (self.hitboxList ~= nil) then + self:purgeHitbox() + local animation, frame + animation = self:getCurrentAnimation() + frame = self:getRelativeFrame() + local hitboxList = self:getHitboxList(animation, frame) + + if (hitboxList ~= nil) then + for i,v in ipairs(hitboxList) do + self:addHitboxFromFrameData(v, animation, frame, i) + end + end + end +end + +function Actor2D:addHitboxFromFrameData(framedata, animationID, frameID, hitboxID) + local sx, sy = self:getSpriteScalling() + local type = framedata[1] + local ox = framedata[2] + local oy = framedata[3] + local w = framedata[4] + local h = framedata[5] + local isSolid = framedata[6] or false + local anim = animationID or "null" + local frame = frameID or 0 + local id = hitboxID or 0 + if (sx < 0) then + ox = self.w - ox - w + end + if (sy < 0) then + oy = self.h - oy - h + end + + if (type == "main") then + self.mainHitbox:modify(ox, oy, w, h) + else + local hitboxName = anim .. frame .. type .. id + self:addHitbox(hitboxName, type, ox, oy, w, h, isSolid) + return hitboxName + end +end + +function Actor2D:initMainHitbox() + self.mainHitbox = Hitbox(self, self.type, 0, 0, self.w, self.h, self.isSolid) + self.world:registerBody(self.mainHitbox) +end + +function Actor2D:addHitbox(name, type, ox, oy, w, h, isSolid) + if (self.hitboxes[name] ~= nil) then + print("ERROR:", "The hitbox " .. name .. " already exists") + else + local hitbox = Hitbox(self, type, ox, oy, w, h, isSolid) + self.hitboxes[name] = hitbox + self.world:registerBody(self.hitboxes[name]) + return hitbox + end +end + +function Actor2D:checkHitboxesCollisions(filter) + for k,v in pairs(self.hitboxes) do + self:checkHitboxCollisionsAtPoint(k, self.x, self.y, filter) + end +end + +function Actor2D:checkHitboxCollisions(name, filter) + self:checkHitboxCollisionsAtPoint(name, self.x, self.y, filter) +end + +function Actor2D:checkHitboxCollisionsAtPoint(name, dx, dy, filter) + local x, y, cols, colNumber = dx, dy, {}, 0 + local filter = filter or self.filter + if (self.isDestroyed == false) and (self.hitboxes[name] ~= nil) then + x, y, cols, colNumber = self.hitboxes[name]:checkCollision(dx, dy, filter) + local type = self.hitboxes[name].type + + for i, col in ipairs(cols) do + self:hitboxResponse(name, type, col) + end + end + + return x, y, cols, colNumber +end + +function Actor2D:hitboxResponse(name, type, collision) + -- just a blank placeholder function +end + +function Actor2D:removeHitbox(name) + if (self.hitboxes[name] ~= nil) then + self.world:removeBody(self.hitboxes[name]) + self.hitboxes[name] = nil + end +end + +function Actor2D:purgeHitbox() + for k,v in pairs(self.hitboxes) do + self.world:removeBody(v) + end + self.hitboxes = {} +end + +function Actor2D:drawHitboxes() + for k,v in pairs(self.hitboxes) do + v:draw() + end + self.mainHitbox:draw() +end + + +function Actor2D:drawMainHitbox() + self.mainHitbox:draw() +end + +-- COORDINATE FUNCTION +-- Handle the coordinate system + +function Actor2D:setCoordinate(x, y) + self.x = x or self.x + self.y = y or self.y end function Actor2D:getCenter() @@ -183,12 +347,6 @@ function Actor2D:getViewCenter() return self:getCenter() end -function Actor2D:drawHitbox() - local x, y = math.floor(self.x), math.floor(self.y) - love.graphics.setColor(self.debug.r, self.debug.g, self.debug.b, 1) - utils.graphics.box(x, y, self.w, self.h) -end - -- DRAW FUNCTIONS -- Draw the actors. diff --git a/gamecore/modules/world/actors/baseactor.lua b/gamecore/modules/world/actors/baseactor.lua index 5512449..987fbea 100644 --- a/gamecore/modules/world/actors/baseactor.lua +++ b/gamecore/modules/world/actors/baseactor.lua @@ -87,7 +87,10 @@ end function BaseActor:setFilter() -- Init the bump filter self.filter = function(item, other) - if (other.isSolid) then + if (other.owner == self) then + -- ignore every collision with our own hitboxes + return nil + elseif (other.isSolid) then return "slide" else return "cross" @@ -236,6 +239,49 @@ function BaseActor:setSpriteScallingY(sy) self.sprite.sy = sy end +function BaseActor:getCurrentAnimation() + if (self.sprite.clone == nil) then + return self.assets.sprites[self.sprite.name]:getCurrentAnimation() + else + return self.sprite.clone:getCurrentAnimation() + end +end + + +function BaseActor:getSpriteScalling() + return self.sprite.sx, self.sprite.sy +end + +function BaseActor:getFrame() + if (self.sprite.name ~= nil) then + if (self.sprite.clone ~= nil) then + return self.sprite.clone:getFrame() + else + return self.assets.sprites[self.sprite.name]:getFrame() + end + end +end + +function BaseActor:getRelativeFrame() + if (self.sprite.name ~= nil) then + if (self.sprite.clone ~= nil) then + return self.sprite.clone:getRelativeFrame() + else + return self.assets.sprites[self.sprite.name]:getRelativeFrame() + end + end +end + +function BaseActor:getAnimationDuration() + if (self.sprite.name ~= nil) then + if (self.sprite.clone ~= nil) then + return self.sprite.clone:getAnimationDuration() + else + return self.assets.sprites[self.sprite.name]:getAnimationDuration() + end + end +end + function BaseActor:drawSprite(x, y, r, sx, sy, ox, oy, kx, ky) if (self.sprite.name ~= nil) then local x = x + self.sprite.ox diff --git a/gamecore/modules/world/actors/utils/hitbox2D.lua b/gamecore/modules/world/actors/utils/hitbox2D.lua new file mode 100644 index 0000000..f20c8ff --- /dev/null +++ b/gamecore/modules/world/actors/utils/hitbox2D.lua @@ -0,0 +1,108 @@ +-- hitbox2D.lua :: a basic 2D hitbox object. It's used by the actors to check +-- collisions and to handle different type of responses. + +--[[ + Copyright © 2019 Kazhnuz + + Permission is hereby granted, free of charge, to any person obtaining a copy of + this software and associated documentation files (the "Software"), to deal in + the Software without restriction, including without limitation the rights to + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + the Software, and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +]] + +local Hitbox2D = Object:extend() + +-- INIT FUNCTIONS +-- Initialise the actor and its base functions + +function Hitbox2D:new(owner, type, ox, oy, w, h, isSolid) + self.owner = owner + self.world = owner.world + + self.type = type + self.ox = ox + self.oy = oy + self.x, self.y = self:getPosition() + self.w = w + self.h = h + self.isSolid = isSolid + + self:setDebugColor(0,0,0) +end + +function Hitbox2D:modify(ox, oy, w, h) + self.ox = ox + self.oy = oy + self.x, self.y = self:getPosition() + self.w = w + self.h = h +end + +function Hitbox2D:setDebugColor(r,g,b) + self.debug = {} + self.debug.r = r + self.debug.g = g + self.debug.b = b +end + +-- COORDINATE FUNCTIONS +-- Handle Hitbox position + +function Hitbox2D:updatePosition() + self.x, self.y = self:getPosition() + self.world:updateBody(self) + return self.x, self.y +end + +function Hitbox2D:getPosition() + return self.ox + self.owner.x, self.oy + self.owner.y +end + +function Hitbox2D:getOwnerPosition() + return self.x - self.ox, self.y - self.oy +end + +function Hitbox2D:getNewOwnerPosition(x, y) + return x - self.ox, y - self.oy +end + +function Hitbox2D:getCenter() + return self.x + (self.w/2), self.y + (self.h/2) +end + +-- COLLISION FUNCTIONS +-- Handle Hitbox position + +function Hitbox2D:checkCollision(dx, dy, filter) + self:updatePosition() + + local dx, dy = self.ox + dx, self.oy + dy + local x, y, cols, colNumber = self.world:checkCollision(self, dx, dy, filter) + local newx, newy = self:getNewOwnerPosition(x, y) + + return newx, newy, cols, colNumber +end + +-- DRAW FUNCTIONS +-- Just some debug function to draw hitbox + +function Hitbox2D:draw() + local x, y = self:getPosition() + love.graphics.setColor(self.debug.r, self.debug.g, self.debug.b, 1) + utils.graphics.box(x, y, self.w, self.h) + utils.graphics.resetColor() +end + +return Hitbox2D diff --git a/gamecore/modules/world/baseworld.lua b/gamecore/modules/world/baseworld.lua index db3fbf0..02007ad 100644 --- a/gamecore/modules/world/baseworld.lua +++ b/gamecore/modules/world/baseworld.lua @@ -121,6 +121,49 @@ function BaseWorld:removeActor(actor) end end +function BaseWorld:countActors() + return #self.actors +end + +function BaseWorld:getActors() + return self.actors +end + +function BaseWorld:getVisibleActors(id) + local camx, camy, camw, camh = self.cameras:getViewCoordinate(id) + local paddingw = camw * PADDING_VALUE + local paddingh = camh * PADDING_VALUE + local x = camx - paddingw + local y = camy - paddingh + local w = camw + paddingw * 2 + local h = camh + paddingh * 2 + + local query = self:queryRect(x, y, w, h) + local returnquery = {} + + for i,v in ipairs(query) do + table.insert(returnquery, v.owner) + end + + return returnquery +end + +-- BODIES MANAGEMENT FUNCTIONS +-- Basic function to handle bodies. Empty function here as baseworld doesn't +-- handle bodies + +function BaseWorld:registerBody(body) + return nil +end + +function BaseWorld:updateBody(body) + return x, y, {}, 0 +end + +function BaseWorld:removeBody(body) + return nil +end + function BaseWorld:moveActor(actor, x, y, filter) -- as the baseworld have no collision function, we return empty collision -- datas, but from the same type than bump2D will return @@ -147,25 +190,6 @@ function BaseWorld:queryRect(x, y, w, h) return v end -function BaseWorld:countActors() - return #self.actors -end - -function BaseWorld:getActors() - return self.actors -end - -function BaseWorld:getVisibleActors(id) - local camx, camy, camw, camh = self.cameras:getViewCoordinate(id) - local paddingw = camw * PADDING_VALUE - local paddingh = camh * PADDING_VALUE - local x = camx - paddingw - local y = camy - paddingh - local w = camw + paddingw * 2 - local h = camh + paddingh * 2 - return self:queryRect(x, y, w, h) -end - -- INFO FUNCTIONS -- Give infos about the world diff --git a/gamecore/modules/world/world2D.lua b/gamecore/modules/world/world2D.lua index 7375c46..98ce64e 100644 --- a/gamecore/modules/world/world2D.lua +++ b/gamecore/modules/world/world2D.lua @@ -35,41 +35,43 @@ function World2D:new(scene, actorlist, mapfile) end -- ACTORS FUNCTIONS --- Wrappers around Bump2D functions +-- Add support for bodies in Actor functions function World2D:initActors() self.currentCreationID = 0 - self.actors = Bump.newWorld(50) + self.actors = {} + self.bodies = Bump.newWorld(50) end function World2D:registerActor(actor) - actor.creationID = self.currentCreationID - self.currentCreationID = self.currentCreationID + 1 - return self.actors:add(actor, actor.x, actor.y, actor.w, actor.h) + World2D.super.registerActor(self, actor) end -function World2D:removeActor(actor) - return self.actors:remove(actor) +function World2D:registerBody(body) + return self.bodies:add(body, body.x, body.y, body.w, body.h) +end + +-- ACTORS FUNCTIONS +-- Wrappers around Bump2D functions + +function World2D:updateBody(body) + return self.bodies:update(body, body.x, body.y, body.w, body.h) +end + +function World2D:removeBody(body) + return self.bodies:remove(body) end function World2D:moveActor(actor, x, y, filter) - return self.actors:move(actor, x, y, filter) + return self.bodies:move(actor, x, y, filter) end function World2D:checkCollision(actor, x, y, filter) - return self.actors:check(actor, x, y, filter) + return self.bodies:check(actor, x, y, filter) end function World2D:queryRect(x, y, w, h) - return self.actors:queryRect(x, y, w, h) -end - -function World2D:countActors() - return self.actors:countItems() -end - -function World2D:getActors() - return self.actors:getItems() + return self.bodies:queryRect(x, y, w, h) end return World2D