1
0

version 0.1

This commit is contained in:
Rokas Puzonas 2022-12-23 13:20:39 +02:00
parent cc709ba84a
commit e2134d76bd
13 changed files with 721 additions and 926 deletions

View File

@ -5,5 +5,4 @@ function love.conf(t)
t.window.width = 854
t.window.height = 480
t.window.resizable = true
end

View File

@ -0,0 +1,69 @@
-- a Collider object, wrapping shape, body, and fixtue
local set_funcs, lp, lg, COLLIDER_TYPES = unpack(
require((...):gsub('collider', '') .. '/utils'))
local Collider = {}
Collider.__index = Collider
function Collider.new(world, collider_type, ...)
-- deprecated
return world:newCollider(collider_type, {...})
end
function Collider:draw_type()
if self.collider_type == 'Edge' or self.collider_type == 'Chain' then
return 'line'
end
return self.collider_type:lower()
end
function Collider:__draw__()
self._draw_type = self._draw_type or self:draw_type()
local args
if self._draw_type == 'line' then
args = {self:getSpatialIdentity()}
else
args = {'line', self:getSpatialIdentity()}
end
love.graphics[self:draw_type()](unpack(args))
end
function Collider:draw()
self:__draw__()
end
function Collider:destroy()
self._world.colliders[self] = nil
self.fixture:setUserData(nil)
self.fixture:destroy()
self.body:destroy()
end
function Collider:getSpatialIdentity()
if self.collider_type == 'Circle' then
return self:getX(), self:getY(), self:getRadius()
else
return self:getWorldPoints(self:getPoints())
end
end
function Collider:collider_contacts()
local contacts = self:getContacts()
local colliders = {}
for i, contact in ipairs(contacts) do
if contact:isTouching() then
local f1, f2 = contact:getFixtures()
if f1 == self.fixture then
colliders[#colliders+1] = f2:getUserData()
else
colliders[#colliders+1] = f1:getUserData()
end
end
end
return colliders
end
return Collider

View File

@ -0,0 +1,24 @@
-- breezefield: init.lua
--[[
implements Collider and World objects
Collider wraps the basic functionality of shape, fixture, and body
World wraps world, and provides automatic drawing simplified collisions
]]--
local bf = {}
local Collider = require(... ..'/collider')
local World = require(... ..'/world')
function bf.newWorld(...)
return bf.World:new(...)
end
bf.Collider = Collider
bf.World = World
return bf

View File

@ -0,0 +1,32 @@
-- function used for both
local function set_funcs(mainobject, subobject)
-- this function assigns functions of a subobject to a primary object
--[[
mainobject: the table to which to assign the functions
subobject: the table whose functions to assign
no output
--]]
for k, v in pairs(subobject.__index) do
if k ~= '__gc' and k ~= '__eq' and k ~= '__index'
and k ~= '__tostring' and k ~= 'destroy' and k ~= 'type'
and k ~= 'typeOf'and k ~= 'getUserData' and k ~= 'setUserData' then
mainobject[k] = function(mainobject, ...)
return v(subobject, ...)
end
end
end
end
local COLLIDER_TYPES = {
CIRCLE = "Circle",
CIRC = "Circle",
RECTANGLE = "Rectangle",
RECT = "Rectangle",
POLYGON = "Polygon",
POLY = "Polygon",
EDGE = 'Edge',
CHAIN = 'Chain'
}
return {set_funcs, love.physics, love.graphics, COLLIDER_TYPES}

View File

@ -0,0 +1,253 @@
-- breezefield: World.lua
--[[
World: has access to all the functions of love.physics.world
additionally stores all Collider objects assigned to it in
self.colliders (as key-value pairs)
can draw all its Colliders
by default, calls :collide on any colliders in it for postSolve
or for beginContact if the colliders are sensors
--]]
-- TODO make updating work from here too
-- TODO: update test and tutorial
local Collider = require((...):gsub('world', '') .. 'collider')
local set_funcs, lp, lg, COLLIDER_TYPES = unpack(
require((...):gsub('world', '') .. '/utils'))
local World = {}
World.__index = World
function World:new(...)
-- create a new physics world
--[[
inputs: (same as love.physics.newWorld)
xg: float, gravity in x direction
yg: float, gravity in y direction
sleep: boolean, whether bodies can sleep
outputs:
w: bf.World, the created world
]]--
local w = {}
setmetatable(w, self)
w._world = lp.newWorld(...)
set_funcs(w, w._world)
w.update = nil -- to use our custom update
w.colliders = {}
-- some functions defined here to use w without being passed it
function w.collide(obja, objb, coll_type, ...)
-- collision event for two Colliders
local function run_coll(obj1, obj2, ...)
if obj1[coll_type] ~= nil then
local e = obj1[coll_type](obj1, obj2, ...)
if type(e) == 'function' then
w.collide_events[#w.collide_events+1] = e
end
end
end
if obja ~= nil and objb ~= nil then
run_coll(obja, objb, ...)
run_coll(objb, obja, ...)
end
end
function w.enter(a, b, ...)
return w.collision(a, b, 'enter', ...)
end
function w.exit(a, b, ...)
return w.collision(a, b, 'exit', ...)
end
function w.preSolve(a, b, ...)
return w.collision(a, b, 'preSolve', ...)
end
function w.postSolve(a, b, ...)
return w.collision(a, b, 'postSolve', ...)
end
function w.collision(a, b, ...)
-- objects that hit one another can have collide methods
-- by default used as postSolve callback
local obja = a:getUserData(a)
local objb = b:getUserData(b)
w.collide(obja, objb, ...)
end
w:setCallbacks(w.enter, w.exit, w.preSolve, w.postSolve)
w.collide_events = {}
return w
end
function World:draw(alpha, draw_over)
-- draw the world
--[[
alpha: sets the alpha of the drawing, defaults to 1
draw_over: draws the collision objects shapes even if their
.draw method is overwritten
--]]
for _, c in pairs(self.colliders) do
c:draw(alpha)
if draw_over then
c:__draw__()
end
end
end
function World:queryRectangleArea(x1, y1, x2, y2)
-- query a bounding-box aligned area for colliders
--[[
inputs:
x1, y1, x2, y2: floats, the x and y coordinates of two points
outputs:
colls: table, all colliders in bounding box
--]]
local colls = {}
local callback = function(fixture)
table.insert(colls, fixture:getUserData())
return true
end
self:queryBoundingBox(x1, y1, x2, y2, callback)
return colls
end
local function check_vertices(vertices)
if #vertices % 2 ~= 0 then
error('vertices must be a multiple of 2')
elseif #vertices < 4 then
error('must have at least 2 vertices with x and y each')
end
end
local function query_region(world, coll_type, args)
local collider = world:newCollider(coll_type, args)
collider:setSensor(true)
world:_disable_callbacks()
world:update(0)
local colls = collider:collider_contacts(collider)
collider:destroy()
world:_enable_callbacks()
return colls
end
function World:_disable_callbacks()
self._callbacks = {self._world:getCallbacks()}
self._world:setCallbacks()
end
function World:_enable_callbacks()
self._world:setCallbacks(unpack(self._callbacks))
end
function World:queryPolygonArea(...)
-- query an area enclosed by the lines connecting a series of points
--[[
inputs:
x1, y1, x2, y2, ... floats, the x and y positions defining polygon
outputs:
colls: table, all Colliders intersecting the area
--]]
local vertices = {...}
if type(vertices[1]) == 'table' then
vertices = vertices[1]
end
check_vertices(vertices)
return query_region(self, 'Polygon', vertices)
end
function World:queryCircleArea(x, y, r)
-- get all colliders in a circle are
--[[
inputs:
x, y, r: floats, x, y and radius of circle
outputs:
colls: table: colliders in area
]]--
return query_region(self, 'Circle', {x, y, r})
end
function World:queryEdgeArea(...)
-- get all colliders along a (series of) line(s)
--[[
inputs:
x1, y1, x2, y2, ... floats, the x and y positions defining lines
outpts:
colls: table: colliders intersecting these lines
--]]
local vertices = {...}
if type(vertices[1]) == 'table' then
vertices = vertices[1]
end
check_vertices(vertices)
return query_region(self, 'Edge', vertices)
end
function World:update(dt)
-- update physics world
self._world:update(dt)
for i, v in pairs(self.collide_events) do
v()
self.collide_events[i] = nil
end
end
--[[
create a new collider in this world
args:
collider_type (string): the type of the collider (not case seinsitive). any of:
circle, rectangle, polygon, edge, chain.
shape_arguments (table): arguments required to instantiate shape.
circle: {x, y, radius}
rectangle: {x, y, width height}
polygon/edge/chain: {x1, y1, x2, y2, ...}
table_to_use (optional, table): table to generate as the collider
]]--
function World:newCollider(collider_type, shape_arguments, table_to_use)
local o = table_to_use or {}
setmetatable(o, Collider)
-- note that you will need to set static vs dynamic later
local _collider_type = COLLIDER_TYPES[collider_type:upper()]
assert(_collider_type ~= nil, "unknown collider type: "..collider_type)
collider_type = _collider_type
if collider_type == 'Circle' then
local x, y, r = unpack(shape_arguments)
o.body = lp.newBody(self._world, x, y, "dynamic")
o.shape = lp.newCircleShape(r)
elseif collider_type == "Rectangle" then
local x, y, w, h = unpack(shape_arguments)
o.body = lp.newBody(self._world, x, y, "dynamic")
o.shape = lp.newRectangleShape(w, h)
collider_type = "Polygon"
else
o.body = lp.newBody(self._world, 0, 0, "dynamic")
o.shape = lp['new'..collider_type..'Shape'](unpack(shape_arguments))
end
o.collider_type = collider_type
o.fixture = lp.newFixture(o.body, o.shape, 1)
o.fixture:setUserData(o)
set_funcs(o, o.body)
set_funcs(o, o.shape)
set_funcs(o, o.fixture)
-- index by self for now
o._world = self
self.colliders[o] = o
return o
end
function World:removeCollider(o)
o._world = nil
o.body:destroy()
self.colliders[o] = nil
end
return World

View File

@ -1,781 +0,0 @@
local bump = {
_VERSION = 'bump v3.1.7',
_URL = 'https://github.com/kikito/bump.lua',
_DESCRIPTION = 'A collision detection library for Lua',
_LICENSE = [[
MIT LICENSE
Copyright (c) 2014 Enrique García Cota
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.
]]
}
------------------------------------------
-- Auxiliary functions
------------------------------------------
local DELTA = 1e-10 -- floating-point margin of error
local abs, floor, ceil, min, max = math.abs, math.floor, math.ceil, math.min, math.max
local function sign(x)
if x > 0 then return 1 end
if x == 0 then return 0 end
return -1
end
local function nearest(x, a, b)
if abs(a - x) < abs(b - x) then return a else return b end
end
local function assertType(desiredType, value, name)
if type(value) ~= desiredType then
error(name .. ' must be a ' .. desiredType .. ', but was ' .. tostring(value) .. '(a ' .. type(value) .. ')')
end
end
local function assertIsPositiveNumber(value, name)
if type(value) ~= 'number' or value <= 0 then
error(name .. ' must be a positive integer, but was ' .. tostring(value) .. '(' .. type(value) .. ')')
end
end
local function assertIsRect(x,y,w,h)
assertType('number', x, 'x')
assertType('number', y, 'y')
assertIsPositiveNumber(w, 'w')
assertIsPositiveNumber(h, 'h')
end
local defaultFilter = function()
return 'slide'
end
------------------------------------------
-- Rectangle functions
------------------------------------------
local function rect_getNearestCorner(x,y,w,h, px, py)
return nearest(px, x, x+w), nearest(py, y, y+h)
end
-- This is a generalized implementation of the liang-barsky algorithm, which also returns
-- the normals of the sides where the segment intersects.
-- Returns nil if the segment never touches the rect
-- Notice that normals are only guaranteed to be accurate when initially ti1, ti2 == -math.huge, math.huge
local function rect_getSegmentIntersectionIndices(x,y,w,h, x1,y1,x2,y2, ti1,ti2)
ti1, ti2 = ti1 or 0, ti2 or 1
local dx, dy = x2-x1, y2-y1
local nx, ny
local nx1, ny1, nx2, ny2 = 0,0,0,0
local p, q, r
for side = 1,4 do
if side == 1 then nx,ny,p,q = -1, 0, -dx, x1 - x -- left
elseif side == 2 then nx,ny,p,q = 1, 0, dx, x + w - x1 -- right
elseif side == 3 then nx,ny,p,q = 0, -1, -dy, y1 - y -- top
else nx,ny,p,q = 0, 1, dy, y + h - y1 -- bottom
end
if p == 0 then
if q <= 0 then return nil end
else
r = q / p
if p < 0 then
if r > ti2 then return nil
elseif r > ti1 then ti1,nx1,ny1 = r,nx,ny
end
else -- p > 0
if r < ti1 then return nil
elseif r < ti2 then ti2,nx2,ny2 = r,nx,ny
end
end
end
end
return ti1,ti2, nx1,ny1, nx2,ny2
end
-- Calculates the minkowsky difference between 2 rects, which is another rect
local function rect_getDiff(x1,y1,w1,h1, x2,y2,w2,h2)
return x2 - x1 - w1,
y2 - y1 - h1,
w1 + w2,
h1 + h2
end
local function rect_containsPoint(x,y,w,h, px,py)
return px - x > DELTA and py - y > DELTA and
x + w - px > DELTA and y + h - py > DELTA
end
local function rect_isIntersecting(x1,y1,w1,h1, x2,y2,w2,h2)
return x1 < x2+w2 and x2 < x1+w1 and
y1 < y2+h2 and y2 < y1+h1
end
local function rect_getSquareDistance(x1,y1,w1,h1, x2,y2,w2,h2)
local dx = x1 - x2 + (w1 - w2)/2
local dy = y1 - y2 + (h1 - h2)/2
return dx*dx + dy*dy
end
local function rect_detectCollision(x1,y1,w1,h1, x2,y2,w2,h2, goalX, goalY)
goalX = goalX or x1
goalY = goalY or y1
local dx, dy = goalX - x1, goalY - y1
local x,y,w,h = rect_getDiff(x1,y1,w1,h1, x2,y2,w2,h2)
local overlaps, ti, nx, ny
if rect_containsPoint(x,y,w,h, 0,0) then -- item was intersecting other
local px, py = rect_getNearestCorner(x,y,w,h, 0, 0)
local wi, hi = min(w1, abs(px)), min(h1, abs(py)) -- area of intersection
ti = -wi * hi -- ti is the negative area of intersection
overlaps = true
else
local ti1,ti2,nx1,ny1 = rect_getSegmentIntersectionIndices(x,y,w,h, 0,0,dx,dy, -math.huge, math.huge)
-- item tunnels into other
if ti1
and ti1 < 1
and (abs(ti1 - ti2) >= DELTA) -- special case for rect going through another rect's corner
and (0 < ti1 + DELTA
or 0 == ti1 and ti2 > 0)
then
ti, nx, ny = ti1, nx1, ny1
overlaps = false
end
end
if not ti then return end
local tx, ty
if overlaps then
if dx == 0 and dy == 0 then
-- intersecting and not moving - use minimum displacement vector
local px, py = rect_getNearestCorner(x,y,w,h, 0,0)
if abs(px) < abs(py) then py = 0 else px = 0 end
nx, ny = sign(px), sign(py)
tx, ty = x1 + px, y1 + py
else
-- intersecting and moving - move in the opposite direction
local ti1, _
ti1,_,nx,ny = rect_getSegmentIntersectionIndices(x,y,w,h, 0,0,dx,dy, -math.huge, 1)
if not ti1 then return end
tx, ty = x1 + dx * ti1, y1 + dy * ti1
end
else -- tunnel
tx, ty = x1 + dx * ti, y1 + dy * ti
end
return {
overlaps = overlaps,
ti = ti,
move = {x = dx, y = dy},
normal = {x = nx, y = ny},
touch = {x = tx, y = ty},
itemRect = {x = x1, y = y1, w = w1, h = h1},
otherRect = {x = x2, y = y2, w = w2, h = h2}
}
end
------------------------------------------
-- Grid functions
------------------------------------------
local function grid_toWorld(cellSize, cx, cy)
return (cx - 1)*cellSize, (cy-1)*cellSize
end
local function grid_toCell(cellSize, x, y)
return floor(x / cellSize) + 1, floor(y / cellSize) + 1
end
-- grid_traverse* functions are based on "A Fast Voxel Traversal Algorithm for Ray Tracing",
-- by John Amanides and Andrew Woo - http://www.cse.yorku.ca/~amana/research/grid.pdf
-- It has been modified to include both cells when the ray "touches a grid corner",
-- and with a different exit condition
local function grid_traverse_initStep(cellSize, ct, t1, t2)
local v = t2 - t1
if v > 0 then
return 1, cellSize / v, ((ct + v) * cellSize - t1) / v
elseif v < 0 then
return -1, -cellSize / v, ((ct + v - 1) * cellSize - t1) / v
else
return 0, math.huge, math.huge
end
end
local function grid_traverse(cellSize, x1,y1,x2,y2, f)
local cx1,cy1 = grid_toCell(cellSize, x1,y1)
local cx2,cy2 = grid_toCell(cellSize, x2,y2)
local stepX, dx, tx = grid_traverse_initStep(cellSize, cx1, x1, x2)
local stepY, dy, ty = grid_traverse_initStep(cellSize, cy1, y1, y2)
local cx,cy = cx1,cy1
f(cx, cy)
-- The default implementation had an infinite loop problem when
-- approaching the last cell in some occassions. We finish iterating
-- when we are *next* to the last cell
while abs(cx - cx2) + abs(cy - cy2) > 1 do
if tx < ty then
tx, cx = tx + dx, cx + stepX
f(cx, cy)
else
-- Addition: include both cells when going through corners
if tx == ty then f(cx + stepX, cy) end
ty, cy = ty + dy, cy + stepY
f(cx, cy)
end
end
-- If we have not arrived to the last cell, use it
if cx ~= cx2 or cy ~= cy2 then f(cx2, cy2) end
end
local function grid_toCellRect(cellSize, x,y,w,h)
local cx,cy = grid_toCell(cellSize, x, y)
local cr,cb = ceil((x+w) / cellSize), ceil((y+h) / cellSize)
return cx, cy, cr - cx + 1, cb - cy + 1
end
------------------------------------------
-- Responses
------------------------------------------
local touch = function(world, col, x,y,w,h, goalX, goalY, filter)
return col.touch.x, col.touch.y, {}, 0
end
local cross = function(world, col, x,y,w,h, goalX, goalY, filter)
local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter)
return goalX, goalY, cols, len
end
local slide = function(world, col, x,y,w,h, goalX, goalY, filter)
goalX = goalX or x
goalY = goalY or y
local tch, move = col.touch, col.move
if move.x ~= 0 or move.y ~= 0 then
if col.normal.x ~= 0 then
goalX = tch.x
else
goalY = tch.y
end
end
col.slide = {x = goalX, y = goalY}
x,y = tch.x, tch.y
local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter)
return goalX, goalY, cols, len
end
local bounce = function(world, col, x,y,w,h, goalX, goalY, filter)
goalX = goalX or x
goalY = goalY or y
local tch, move = col.touch, col.move
local tx, ty = tch.x, tch.y
local bx, by = tx, ty
if move.x ~= 0 or move.y ~= 0 then
local bnx, bny = goalX - tx, goalY - ty
if col.normal.x == 0 then bny = -bny else bnx = -bnx end
bx, by = tx + bnx, ty + bny
end
col.bounce = {x = bx, y = by}
x,y = tch.x, tch.y
goalX, goalY = bx, by
local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter)
return goalX, goalY, cols, len
end
------------------------------------------
-- World
------------------------------------------
local World = {}
local World_mt = {__index = World}
-- Private functions and methods
local function sortByWeight(a,b) return a.weight < b.weight end
local function sortByTiAndDistance(a,b)
if a.ti == b.ti then
local ir, ar, br = a.itemRect, a.otherRect, b.otherRect
local ad = rect_getSquareDistance(ir.x,ir.y,ir.w,ir.h, ar.x,ar.y,ar.w,ar.h)
local bd = rect_getSquareDistance(ir.x,ir.y,ir.w,ir.h, br.x,br.y,br.w,br.h)
return ad < bd
end
return a.ti < b.ti
end
local function addItemToCell(self, item, cx, cy)
self.rows[cy] = self.rows[cy] or setmetatable({}, {__mode = 'v'})
local row = self.rows[cy]
row[cx] = row[cx] or {itemCount = 0, x = cx, y = cy, items = setmetatable({}, {__mode = 'k'})}
local cell = row[cx]
self.nonEmptyCells[cell] = true
if not cell.items[item] then
cell.items[item] = true
cell.itemCount = cell.itemCount + 1
end
end
local function removeItemFromCell(self, item, cx, cy)
local row = self.rows[cy]
if not row or not row[cx] or not row[cx].items[item] then return false end
local cell = row[cx]
cell.items[item] = nil
cell.itemCount = cell.itemCount - 1
if cell.itemCount == 0 then
self.nonEmptyCells[cell] = nil
end
return true
end
local function getDictItemsInCellRect(self, cl,ct,cw,ch)
local items_dict = {}
for cy=ct,ct+ch-1 do
local row = self.rows[cy]
if row then
for cx=cl,cl+cw-1 do
local cell = row[cx]
if cell and cell.itemCount > 0 then -- no cell.itemCount > 1 because tunneling
for item,_ in pairs(cell.items) do
items_dict[item] = true
end
end
end
end
end
return items_dict
end
local function getCellsTouchedBySegment(self, x1,y1,x2,y2)
local cells, cellsLen, visited = {}, 0, {}
grid_traverse(self.cellSize, x1,y1,x2,y2, function(cx, cy)
local row = self.rows[cy]
if not row then return end
local cell = row[cx]
if not cell or visited[cell] then return end
visited[cell] = true
cellsLen = cellsLen + 1
cells[cellsLen] = cell
end)
return cells, cellsLen
end
local function getInfoAboutItemsTouchedBySegment(self, x1,y1, x2,y2, filter)
local cells, len = getCellsTouchedBySegment(self, x1,y1,x2,y2)
local cell, rect, l,t,w,h, ti1,ti2, tii0,tii1
local nx1,ny1
local visited, itemInfo, itemInfoLen = {},{},0
for i=1,len do
cell = cells[i]
for item in pairs(cell.items) do
if not visited[item] then
visited[item] = true
if (not filter or filter(item)) then
rect = self.rects[item]
l,t,w,h = rect.x,rect.y,rect.w,rect.h
ti1,ti2,nx1,ny1 = rect_getSegmentIntersectionIndices(l,t,w,h, x1,y1, x2,y2, 0, 1)
if ti1 and ((0 < ti1 and ti1 < 1) or (0 < ti2 and ti2 < 1)) then
-- the sorting is according to the t of an infinite line, not the segment
tii0,tii1 = rect_getSegmentIntersectionIndices(l,t,w,h, x1,y1, x2,y2, -math.huge, math.huge)
itemInfoLen = itemInfoLen + 1
itemInfo[itemInfoLen] = {
item = item,
ti1 = ti1,
ti2 = ti2,
nx = nx1,
ny = ny1,
weight = min(tii0,tii1)
}
end
end
end
end
end
table.sort(itemInfo, sortByWeight)
return itemInfo, itemInfoLen
end
local function getResponseByName(self, name)
local response = self.responses[name]
if not response then
error(('Unknown collision type: %s (%s)'):format(name, type(name)))
end
return response
end
-- Misc Public Methods
function World:addResponse(name, response)
self.responses[name] = response
end
function World:project(item, x,y,w,h, goalX, goalY, filter)
assertIsRect(x,y,w,h)
goalX = goalX or x
goalY = goalY or y
filter = filter or defaultFilter
local collisions, len = {}, 0
local visited = {}
if item ~= nil then visited[item] = true end
-- This could probably be done with less cells using a polygon raster over the cells instead of a
-- bounding rect of the whole movement. Conditional to building a queryPolygon method
local tl, tt = min(goalX, x), min(goalY, y)
local tr, tb = max(goalX + w, x+w), max(goalY + h, y+h)
local tw, th = tr-tl, tb-tt
local cl,ct,cw,ch = grid_toCellRect(self.cellSize, tl,tt,tw,th)
local dictItemsInCellRect = getDictItemsInCellRect(self, cl,ct,cw,ch)
for other,_ in pairs(dictItemsInCellRect) do
if not visited[other] then
visited[other] = true
local responseName = filter(item, other)
if responseName then
local ox,oy,ow,oh = self:getRect(other)
local col = rect_detectCollision(x,y,w,h, ox,oy,ow,oh, goalX, goalY)
if col then
col.other = other
col.item = item
col.type = responseName
len = len + 1
collisions[len] = col
end
end
end
end
table.sort(collisions, sortByTiAndDistance)
return collisions, len
end
function World:countCells()
local count = 0
for _,row in pairs(self.rows) do
for _,_ in pairs(row) do
count = count + 1
end
end
return count
end
function World:hasItem(item)
return not not self.rects[item]
end
function World:getItems()
local items, len = {}, 0
for item,_ in pairs(self.rects) do
len = len + 1
items[len] = item
end
return items, len
end
function World:countItems()
local len = 0
for _ in pairs(self.rects) do len = len + 1 end
return len
end
function World:getRect(item)
local rect = self.rects[item]
if not rect then
error('Item ' .. tostring(item) .. ' must be added to the world before getting its rect. Use world:add(item, x,y,w,h) to add it first.')
end
return rect.x, rect.y, rect.w, rect.h
end
function World:toWorld(cx, cy)
return grid_toWorld(self.cellSize, cx, cy)
end
function World:toCell(x,y)
return grid_toCell(self.cellSize, x, y)
end
--- Query methods
function World:queryRect(x,y,w,h, filter)
assertIsRect(x,y,w,h)
local cl,ct,cw,ch = grid_toCellRect(self.cellSize, x,y,w,h)
local dictItemsInCellRect = getDictItemsInCellRect(self, cl,ct,cw,ch)
local items, len = {}, 0
local rect
for item,_ in pairs(dictItemsInCellRect) do
rect = self.rects[item]
if (not filter or filter(item))
and rect_isIntersecting(x,y,w,h, rect.x, rect.y, rect.w, rect.h)
then
len = len + 1
items[len] = item
end
end
return items, len
end
function World:queryPoint(x,y, filter)
local cx,cy = self:toCell(x,y)
local dictItemsInCellRect = getDictItemsInCellRect(self, cx,cy,1,1)
local items, len = {}, 0
local rect
for item,_ in pairs(dictItemsInCellRect) do
rect = self.rects[item]
if (not filter or filter(item))
and rect_containsPoint(rect.x, rect.y, rect.w, rect.h, x, y)
then
len = len + 1
items[len] = item
end
end
return items, len
end
function World:querySegment(x1, y1, x2, y2, filter)
local itemInfo, len = getInfoAboutItemsTouchedBySegment(self, x1, y1, x2, y2, filter)
local items = {}
for i=1, len do
items[i] = itemInfo[i].item
end
return items, len
end
function World:querySegmentWithCoords(x1, y1, x2, y2, filter)
local itemInfo, len = getInfoAboutItemsTouchedBySegment(self, x1, y1, x2, y2, filter)
local dx, dy = x2-x1, y2-y1
local info, ti1, ti2
for i=1, len do
info = itemInfo[i]
ti1 = info.ti1
ti2 = info.ti2
info.weight = nil
info.x1 = x1 + dx * ti1
info.y1 = y1 + dy * ti1
info.x2 = x1 + dx * ti2
info.y2 = y1 + dy * ti2
end
return itemInfo, len
end
--- Main methods
function World:add(item, x,y,w,h)
local rect = self.rects[item]
if rect then
error('Item ' .. tostring(item) .. ' added to the world twice.')
end
assertIsRect(x,y,w,h)
self.rects[item] = {x=x,y=y,w=w,h=h}
local cl,ct,cw,ch = grid_toCellRect(self.cellSize, x,y,w,h)
for cy = ct, ct+ch-1 do
for cx = cl, cl+cw-1 do
addItemToCell(self, item, cx, cy)
end
end
return item
end
function World:remove(item)
local x,y,w,h = self:getRect(item)
self.rects[item] = nil
local cl,ct,cw,ch = grid_toCellRect(self.cellSize, x,y,w,h)
for cy = ct, ct+ch-1 do
for cx = cl, cl+cw-1 do
removeItemFromCell(self, item, cx, cy)
end
end
end
function World:update(item, x2,y2,w2,h2)
local x1,y1,w1,h1 = self:getRect(item)
w2,h2 = w2 or w1, h2 or h1
assertIsRect(x2,y2,w2,h2)
if x1 ~= x2 or y1 ~= y2 or w1 ~= w2 or h1 ~= h2 then
local cellSize = self.cellSize
local cl1,ct1,cw1,ch1 = grid_toCellRect(cellSize, x1,y1,w1,h1)
local cl2,ct2,cw2,ch2 = grid_toCellRect(cellSize, x2,y2,w2,h2)
if cl1 ~= cl2 or ct1 ~= ct2 or cw1 ~= cw2 or ch1 ~= ch2 then
local cr1, cb1 = cl1+cw1-1, ct1+ch1-1
local cr2, cb2 = cl2+cw2-1, ct2+ch2-1
local cyOut
for cy = ct1, cb1 do
cyOut = cy < ct2 or cy > cb2
for cx = cl1, cr1 do
if cyOut or cx < cl2 or cx > cr2 then
removeItemFromCell(self, item, cx, cy)
end
end
end
for cy = ct2, cb2 do
cyOut = cy < ct1 or cy > cb1
for cx = cl2, cr2 do
if cyOut or cx < cl1 or cx > cr1 then
addItemToCell(self, item, cx, cy)
end
end
end
end
local rect = self.rects[item]
rect.x, rect.y, rect.w, rect.h = x2,y2,w2,h2
end
end
function World:move(item, goalX, goalY, filter)
local actualX, actualY, cols, len = self:check(item, goalX, goalY, filter)
self:update(item, actualX, actualY)
return actualX, actualY, cols, len
end
function World:check(item, goalX, goalY, filter)
filter = filter or defaultFilter
local visited = {[item] = true}
local visitedFilter = function(itm, other)
if visited[other] then return false end
return filter(itm, other)
end
local cols, len = {}, 0
local x,y,w,h = self:getRect(item)
local projected_cols, projected_len = self:project(item, x,y,w,h, goalX,goalY, visitedFilter)
while projected_len > 0 do
local col = projected_cols[1]
len = len + 1
cols[len] = col
visited[col.other] = true
local response = getResponseByName(self, col.type)
goalX, goalY, projected_cols, projected_len = response(
self,
col,
x, y, w, h,
goalX, goalY,
visitedFilter
)
end
return goalX, goalY, cols, len
end
-- Public library functions
bump.newWorld = function(cellSize)
cellSize = cellSize or 64
assertIsPositiveNumber(cellSize, 'cellSize')
local world = setmetatable({
cellSize = cellSize,
rects = {},
rows = {},
nonEmptyCells = {},
responses = {}
}, World_mt)
world:addResponse('touch', touch)
world:addResponse('cross', cross)
world:addResponse('slide', slide)
world:addResponse('bounce', bounce)
return world
end
bump.rect = {
getNearestCorner = rect_getNearestCorner,
getSegmentIntersectionIndices = rect_getSegmentIntersectionIndices,
getDiff = rect_getDiff,
containsPoint = rect_containsPoint,
isIntersecting = rect_isIntersecting,
getSquareDistance = rect_getSquareDistance,
detectCollision = rect_detectCollision
}
bump.responses = {
touch = touch,
cross = cross,
slide = slide,
bounce = bounce
}
return bump

View File

@ -1,6 +1,8 @@
local Gamestate = require("lib.hump.gamestate")
pprint = require("lib.pprint")
PLAYER_SIZE = 20
function love.load()
love.math.setRandomSeed(love.timer.getTime())
love.keyboard.setKeyRepeat(true)

91
src/player.lua Normal file
View File

@ -0,0 +1,91 @@
local Vec = require("lib.brinevector")
local Player = {}
Player.__index = Player
local SHOOT_COOLDOWN = 1
function Player:new(controls, collision_world, pos)
local player = setmetatable({}, Player)
player.pos = pos
player.vel = Vec()
player.controls = controls
player.collider = collision_world:newCollider("Circle", { pos.x, pos.y, PLAYER_SIZE })
player.pickedup_bolt_speeds = {}
player.shoot_cooldown = nil
player.health = 3
if controls == "joystick" then
local joysticks = love.joystick.getJoysticks()
assert(joysticks[1], "no joystick connected")
player.joystick = joysticks[1]
end
return player
end
local function boolToNum(bool)
return bool and 1 or 0
end
local function getDirectionKey(positive, negative)
return boolToNum(love.keyboard.isDown(positive)) - boolToNum(love.keyboard.isDown(negative))
end
function Player:get_move_dir()
if self.controls == "keyboard" then
local dirx = getDirectionKey("d", "a")
local diry = getDirectionKey("s", "w")
return Vec(dirx, diry).normalized
elseif self.controls == "joystick" then
local dirx = self.joystick:getGamepadAxis("leftx")
local diry = self.joystick:getGamepadAxis("lefty")
local size = dirx^2 + diry^2
if size > 0.1 then
return Vec(dirx, diry).normalized
else
return Vec(0, 0)
end
else
return Vec(0, 0)
end
end
function Player:get_aim_dir()
if self.controls == "keyboard" then
return (Vec(love.mouse.getPosition()) - self.pos).normalized
elseif self.controls == "joystick" then
local dirx = self.joystick:getGamepadAxis("rightx")
local diry = self.joystick:getGamepadAxis("righty")
local dir = Vec(dirx, diry).normalized
local size = dirx^2 + diry^2
if size < 0.1 and self.last_aim_dir then
dir = self.last_aim_dir
end
self.last_aim_dir = dir
return dir
else
return Vec(0, 0)
end
end
function Player:process_shoot()
if not self.on_shoot then return end
local now = love.timer.getTime()
if self.last_shot_time then
if now - self.last_shot_time < SHOOT_COOLDOWN then return end
end
local shoot = false
if self.controls == "keyboard" then
shoot = love.keyboard.isDown("space")
elseif self.controls == "joystick" then
shoot = self.joystick:isGamepadDown("leftshoulder")
end
if shoot then
self:on_shoot()
self.last_shot_time = now
end
end
return Player

View File

@ -1,29 +1,268 @@
local MainState = {}
local nata = require("lib.nata")
local Player = require("player")
local Vec = require("lib.brinevector")
local bf = require("lib.breezefield")
local BOLT_DIST_FROM_PLAYER = 40
local BOLT_SIZE = 8
local DRAW_COLLIDERS = false
local BOLT_DURATION = 2.5
local PLAYER1_MASK = 2
local PLAYER2_MASK = 3
function MainState:enter()
self.ecs = nata.new{
groups = {},
systems = {},
data = {}
}
love.mouse.setVisible(false)
self.world = bf.newWorld(0, 0, true)
self.world:setCallbacks(function(...) self:on_begin_contact(...) end)
local winw, winh = love.graphics.getDimensions()
self.player_1 = Player:new("keyboard", self.world, Vec(50, 200))
self.player_2 = Player:new("joystick", self.world, Vec(winw-50, 200))
self.player_1.collider.fixture:setCategory(PLAYER1_MASK)
self.player_2.collider.fixture:setCategory(PLAYER2_MASK)
self.players = { self.player_1, self.player_2 }
local on_shoot = function(...) self:on_player_shoot(...) end
for _, player in ipairs(self.players) do
player.on_shoot = on_shoot
player.collider.fixture:setUserData("player")
end
self.obstacles = {}
table.insert(self.obstacles, {{90, 90}, {100, 200}, {300, 300}, {200, 100}})
table.insert(self.obstacles, {{390, 90}, {500, 200}, {500, 300}})
table.insert(self.obstacles, {{450, 390}, {500, 200}, {500, 300}})
table.insert(self.obstacles, {{750, 390}, {500, 200}, {500, 300}})
table.insert(self.obstacles, {{750, 290}, {600, 200}, {600, 100}})
table.insert(self.obstacles, {{550, 400}, {670, 500}, {500, 500}})
table.insert(self.obstacles, {{700, 0}, {850, 200}, {850, 0}})
table.insert(self.obstacles, {{250, 350}, {200, 500}, {300, 500}})
self.obstacle_colliders = {}
for _, obstacle in ipairs(self.obstacles) do
local points = {}
for _, p in ipairs(obstacle) do
table.insert(points, p[1])
table.insert(points, p[2])
end
local collider = self.world:newCollider("Polygon", points)
collider:setType("static")
table.insert(self.obstacle_colliders, collider)
end
do
self.world:newCollider("Edge", { 0, 0, winw, 0 }):setType("static") -- top
self.world:newCollider("Edge", { 0, 0, 0, winh }):setType("static") -- left
self.world:newCollider("Edge", { winw, 0, winw, winh }):setType("static") -- right
self.world:newCollider("Edge", { 0, winh, winw, winh }):setType("static") -- bottom
end
self.bolts = {}
self.bolt_colliders = {}
end
function MainState:update(dt)
self.ecs:flush()
self.ecs:emit("update", dt)
for _, player in ipairs(self.players) do
player:process_shoot()
local move_dir = player:get_move_dir()
local acc = move_dir * 300
player.vel = player.vel + acc * dt
player.vel = player.vel * 0.98
player.collider:setLinearVelocity(player.vel.x, player.vel.y)
local now = love.timer.getTime()
for i, bolt in ipairs(self.bolts) do
local collider = self.bolt_colliders[i]
local lifetime = now - bolt.created_at
local velx, vely = collider:getLinearVelocity()
if not bolt.dead then
bolt.max_vel = math.max(bolt.max_vel, (velx^2 + vely^2)^0.5)
end
if lifetime > BOLT_DURATION then
bolt.dead = true
local dampening = 0.9
collider:setLinearVelocity(velx * dampening, vely * dampening)
end
if bolt.dead then
local distance = (bolt.pos - player.pos).length
if distance < BOLT_DIST_FROM_PLAYER then
table.insert(player.pickedup_bolt_speeds, bolt.max_vel)
self:destroy_bolt(i)
end
end
end
end
self.world:update(dt)
for i, bolt in ipairs(self.bolts) do
local collider = self.bolt_colliders[i]
bolt.pos.x = collider:getX()
bolt.pos.y = collider:getY()
end
for _, player in ipairs(self.players) do
player.pos.x = player.collider:getX()
player.pos.y = player.collider:getY()
end
if love.keyboard.isDown("escape") then
love.event.quit()
end
end
function MainState:keypressed(...)
self.ecs:emit("keypressed", ...)
function MainState:destroy_bolt(idx)
local collider = table.remove(self.bolt_colliders, idx)
table.remove(self.bolts, idx)
self.world:removeCollider(collider)
end
function MainState:on_player_shoot(player)
local aim_dir = player:get_aim_dir()
local bolt = {
pos = player.pos + aim_dir * BOLT_DIST_FROM_PLAYER,
created_at = love.timer.getTime(),
max_vel = 0
}
local velocity = 500
local pickup_idx = -1
for i, vel in ipairs(player.pickedup_bolt_speeds) do
if vel > velocity then
velocity = vel
pickup_idx = i
end
end
if pickup_idx ~= -1 then
table.remove(player.pickedup_bolt_speeds, pickup_idx)
end
local collider = self.world:newCollider("Circle", { bolt.pos.x, bolt.pos.y, BOLT_SIZE })
collider:setLinearVelocity(aim_dir.x * velocity, aim_dir.y * velocity)
-- collider:applyLinearImpulse(aim_dir.x * launch_force, aim_dir.y * launch_force)
collider.fixture:setUserData("bolt")
collider:setRestitution(1.2)
if player == self.player_1 then
collider:setMask(PLAYER1_MASK)
else
collider:setMask(PLAYER2_MASK)
end
table.insert(self.bolts, bolt)
table.insert(self.bolt_colliders, collider)
end
function MainState:get_bolt_by_collider(collider)
for i, other_collider in ipairs(self.bolt_colliders) do
if other_collider == collider then
return i
end
end
end
function MainState:get_player_by_collider(collider)
if self.player_1.collider.fixture == collider then
return self.player_1
elseif self.player_2.collider.fixture == collider then
return self.player_2
end
end
function MainState:on_player_touch_bolt(player, bolt_idx)
player.health = player.health - 1
self:destroy_bolt(bolt_idx)
end
function MainState:on_begin_contact(a, b)
local a_data = a:getUserData()
local b_data = b:getUserData()
if a_data == "player" and b_data == "bolt" then
self:on_player_touch_bolt(self:get_player_by_collider(a), self:get_bolt_by_collider(b))
elseif a_data == "bolt" and b_data == "player" then
self:on_player_touch_bolt(self:get_bolt_by_collider(a), self:get_player_by_collider(b))
end
end
function MainState:draw_health(x, y, player, align)
local max_health = 3
local rect_width = 20
local rect_height = 25
local gap = 10
local padding = 10
local health_bar_width = max_health * (rect_width + gap) - gap + 2*padding
if align == "right" then
x = x - health_bar_width
end
love.graphics.setColor(0.1, 0.1, 0.1)
love.graphics.rectangle("fill",
x, y,
health_bar_width,
rect_height + 2*padding
)
for i=1, max_health do
local rect_x = x + (rect_width + gap) * (i-1) + padding
if player.health >= i then
love.graphics.setColor(0.7, 0.2, 0.2)
else
love.graphics.setColor(0.2, 0.2, 0.2)
end
love.graphics.rectangle("fill", rect_x, y+padding, rect_width, rect_height)
end
end
function MainState:draw()
self.ecs:emit("draw")
for _, player in ipairs(self.players) do
love.graphics.setLineWidth(4)
love.graphics.setColor(1, 1, 1)
love.graphics.circle("line", player.pos.x, player.pos.y, PLAYER_SIZE)
love.graphics.setLineWidth(3)
love.graphics.setColor(0.8, 0.1, 0.1)
local bolt_pos = player.pos + player:get_aim_dir() * BOLT_DIST_FROM_PLAYER
love.graphics.circle("line", bolt_pos.x, bolt_pos.y, BOLT_SIZE)
end
for _, bolt in ipairs(self.bolts) do
love.graphics.circle("line", bolt.pos.x, bolt.pos.y, BOLT_SIZE)
end
love.graphics.setLineWidth(3)
love.graphics.setColor(0.33, 0.33, 0.33)
for _, obstacle in ipairs(self.obstacles) do
local n = #obstacle
love.graphics.line(obstacle[1][1], obstacle[1][2], obstacle[n][1], obstacle[n][2])
for i=1, n-1 do
love.graphics.line(obstacle[i][1], obstacle[i][2], obstacle[i+1][1], obstacle[i+1][2])
end
end
local cursor_size = 10
local mx, my = love.mouse.getPosition()
love.graphics.setColor(1, 1, 1)
love.graphics.line(mx-cursor_size, my, mx+cursor_size, my)
love.graphics.line(mx, my-cursor_size, mx, my+cursor_size)
-- love.graphics.print(mx, 200, 10)
-- love.graphics.print(my, 200, 40)
if DRAW_COLLIDERS then
love.graphics.setColor(0.8, 0.1, 0.8)
self.world:draw()
end
local winw, winh = love.graphics.getDimensions()
self:draw_health(10, 10, self.player_1, "left")
self:draw_health(winw-10, 10, self.player_2, "right")
end
return MainState

View File

@ -1,23 +0,0 @@
return function(ui, text, x, y, w, h)
local mx, my = love.mouse.getPosition()
local bg = ui.theme.bg_color
local result = false
if ui.inRect(mx, my, x, y, w, h) then
bg = ui.theme.bg_hover_color
if love.mouse.isDown(1) then
bg = ui.theme.bg_pressed_color
elseif ui:wasDown(1) then
result = true
end
end
love.graphics.setColor(bg)
love.graphics.rectangle("fill", x, y, w, h)
local font_height = ui.theme.font:getHeight()
love.graphics.setFont(ui.theme.font)
love.graphics.setColor(ui.theme.text_color)
love.graphics.printf(text, x, y+(h-font_height)/2, w, "center")
return result
end

View File

@ -1,51 +0,0 @@
local UI = {}
UI.__index = UI
local elements = {
label = require(... .. ".label"),
button = require(... .. ".button"),
textbox = require(... .. ".textbox"),
}
function UI.new(theme)
local self = setmetatable({}, UI)
self.theme = theme or {}
self.mouse_buttons = {}
return self
end
function UI:postDraw()
self.keys_pressed = {}
self.entered_text = nil
self.mouse_buttons[1] = love.mouse.isDown(1)
self.mouse_buttons[2] = love.mouse.isDown(2)
self.mouse_buttons[3] = love.mouse.isDown(3)
end
function UI:keypressed(key)
self.keys_pressed[key] = true
end
function UI:textinput(text)
self.entered_text = text
end
function UI.inRect(mx, my, x, y, w, h)
return (x <= mx and mx < x+w) and (y <= my and my < y+h)
end
function UI:wasDown(...)
for i=1, select("#", ...) do
local button = select(i, ...)
if self.mouse_buttons[button] then
return true
end
end
return false
end
function UI.__index(t, k)
return UI[k] or elements[k]
end
return UI

View File

@ -1,5 +0,0 @@
return function (ui, text, x, y)
love.graphics.setFont(ui.theme.font)
love.graphics.setColor(ui.theme.text_color)
love.graphics.print(text, x, y)
end

View File

@ -1,54 +0,0 @@
local padding = 10
return function(ui, textbox, x, y, w, h)
local mx, my = love.mouse.getPosition()
local bg = ui.theme.bg_color
if ui.inRect(mx, my, x, y, w, h) then
bg = ui.theme.bg_hover_color
if love.mouse.isDown(1) then
bg = ui.theme.bg_pressed_color
elseif ui:wasDown(1) then
textbox.is_editing = true
textbox.cursor = #textbox.text
end
else
if love.mouse.isDown(1) then
textbox.is_editing = false
end
end
love.graphics.setColor(bg)
love.graphics.rectangle("fill", x, y, w, h)
love.graphics.setColor(ui.theme.text_color)
if textbox.is_editing then
if ui.keys_pressed["escape"] then
textbox.is_editing = false
elseif ui.keys_pressed["left"] then
textbox.cursor = math.max(textbox.cursor - 1, 0)
elseif ui.keys_pressed["right"] then
textbox.cursor = math.min(textbox.cursor + 1, #textbox.text)
elseif ui.keys_pressed["backspace"] and textbox.cursor > 0 then
textbox.text = textbox.text:sub(0, textbox.cursor-1) .. textbox.text:sub(textbox.cursor+1)
textbox.cursor = textbox.cursor - 1
elseif ui.keys_pressed["v"] and love.keyboard.isDown("lctrl", "rctrl") then
local text = love.system.getClipboardText()
textbox.text = textbox.text:sub(1, textbox.cursor) .. text .. textbox.text:sub(textbox.cursor+1)
textbox.cursor = textbox.cursor + #text
end
if ui.entered_text then
textbox.text = textbox.text:sub(1, textbox.cursor) .. ui.entered_text .. textbox.text:sub(textbox.cursor+1)
textbox.cursor = textbox.cursor + #ui.entered_text
end
love.graphics.setLineWidth(3)
love.graphics.line(x, y+h-1.5, x+w, y+h-1.5)
local cursor_ox = ui.theme.font:getWidth(textbox.text:sub(1, textbox.cursor))+padding
love.graphics.line(x+cursor_ox, y+padding, x+cursor_ox, y+h-padding)
end
local font_height = ui.theme.font:getHeight()
love.graphics.setFont(ui.theme.font)
love.graphics.print(textbox.text or "", x+padding, y+(h-font_height)/2)
end