├── .gitignore ├── DOC.org ├── LICENSE ├── README.org ├── collider.lua ├── future.org ├── init.lua ├── test.sh ├── test ├── main.lua ├── test_deep │ └── main.lua ├── test_queryareas │ └── main.lua └── test_rendering │ └── main.lua ├── utils.lua └── world.lua /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | test/breezefield 3 | test/mlib 4 | Makefile 5 | \#* -------------------------------------------------------------------------------- /DOC.org: -------------------------------------------------------------------------------- 1 | # TODO write documentation for every method 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Hugo Dictus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | 2 | #+TOC: headlines 2 3 | 4 | * introduction 5 | 6 | ** why breezefield? 7 | The love physics library is very flexible, but doing relatively simple 8 | things with it can be tedious. A good solution is the [[https://github.com/adnzzzzZ/windfield][windfield]] library, 9 | but a few things didn't quite sit right for me. 10 | 11 | I've used windfield for a few practice projects, and I liked it very much. 12 | It makes protoyping faster, and massively reduces the time 13 | and mental effort spent putting together all the pieces of love.physics. 14 | so far however, I've encountered the occaisional issue that was tricky to 15 | track down due to (in particular) the collision-management system in place 16 | in windfield. When I needed to modify parts of it to my purposes, 17 | I found its size and complexity made it take a little longer than it could 18 | have with a simpler library. 19 | 20 | Breezefield is a lightweight alternative that takes the parts that I 21 | liked best about windfield and leaves out what I felt held it back. 22 | 23 | 24 | * functionality 25 | ** easily create physics objects (body + shape + fixture) 26 | #+BEGIN_SRC lua 27 | world:newCollider(, , (optional)) 28 | #+END_SRC 29 | ** query rectangle, circle, edge, or polygon areas 30 | #+BEGIN_SRC lua 31 | world:queryRectangleArea(x1, y1, x2, y2) 32 | #+END_SRC 33 | 34 | ** love.physics. methods mapped to breezefield objects 35 | #+BEGIN_SRC lua 36 | Collider: (e.g. get/setRestitution, get/setX, etc...) 37 | World: (e.g. update) 38 | #+END_SRC 39 | 40 | So you can make a collider move rightwards with 41 | 42 | #+BEGIN_SRC lua 43 | collider:setLinearVelocity(100, 0) 44 | #+END_SRC 45 | 46 | ** Collision handling 47 | You can set collision events for a Collider by overwriting some methods: 48 | - `:enter(other, collision) called on each collider when two colliders come into contact 49 | - `:exit(other, collision) called on each collider when two colliders cease to be in contact 50 | - `:preSolve(other, collision)` called on each collider before their collision(e.g. bounce) has been resolved 51 | - `:postSolve(other, collision, normalimpulse, tangentimpulse)` called on each collider after their collision has been resolved 52 | 53 | The collision argument to each will be the `love.physics.Contact` (https://love2d.org/wiki/Contact) object associated with the collision. 54 | A handy trick to prevent colliders from colliding with each other is to do `collision:setEnabled(False)` inside the preSolve method. 55 | You may notice that these correspond to the four callbacks in https://love2d.org/wiki/Tutorial:PhysicsCollisionCallbacks . 56 | 57 | If you wish to handle collisions differently, you can also just overwrite those callbacks with `world:setCallbacks` 58 | 59 | ** draw physics objects with one command 60 | #+BEGIN_SRC lua 61 | world:draw() 62 | #+END_SRC 63 | 64 | *** can be repurposed to draw in-game shapes/sprites! 65 | Just redefine :draw on your collider objects, 66 | add other objects with :draw methods to world.colliders. 67 | # TODO rename world.colliders to world.to_draw? 68 | Call world:draw(, true) to draw physics boundaries in addition to 69 | self-defined :draw methods 70 | 71 | You can ensure some colliders are drawn over others with `Collider:setDrawOrder(number)`. 72 | Colliders with higher draw orders will be drawn over those with smaller draw orders. 73 | The default is 0, and the draw order can be negative. 74 | ** access to love.physics objects if you have something more creative in mind 75 | World._physworld contains the regular love.physics.world object. 76 | Collider.fixture, Collider.body, Collider.shape all contain the 77 | respective physics objects 78 | *** please let me know if there are any issues 79 | if there are any issues in breezefields implementation that complicates using 80 | love.physics together with it, let me know, or better yet, send a pull request 81 | 82 | * Installation 83 | I reccomend you ensure you understand love.physics, as breezefield mostly just wraps that. You can start [[https://love2d.org/wiki/Tutorial:Physics][here]]. 84 | To install simply clone or download the repository and place breezefield anywhere in your lua path or in your project directory. 85 | 86 | * example/tutorial 87 | ** Basics 88 | *** setting up a basic world 89 | #+BEGIN_SRC lua 90 | bf = require("breezefield") 91 | 92 | function love.load() 93 | world = bf.newWorld(0, 90.81, true) 94 | -- bf.World:new also works 95 | -- any function of love.physics.world should work on World 96 | print(world:getGravity()) 97 | 98 | ground = bf.Collider.new(world, "Polygon", 99 | {0, 550, 650, 550 , 650, 650, 0, 650}) 100 | ground:setType("static") 101 | 102 | ball = bf.Collider.new(world, "Circle", 325, 325, 20) 103 | 104 | ball:setRestitution(0.8) -- any function of shape/body/fixture works 105 | block1 = bf.Collider.new(world, "Polygon", {150, 375, 250, 375, 106 | 250, 425, 150, 425}) 107 | 108 | end 109 | #+END_SRC 110 | *** forces, movement and control 111 | any functions for shape, body, or fixture work on Colliders 112 | #+BEGIN_SRC lua 113 | function love.update(dt) 114 | world:update(dt) 115 | if love.keyboard.isDown("right") then 116 | ball:applyForce(400, 0) 117 | elseif love.keyboard.isDown("left") then 118 | ball:applyForce(-400, 0) 119 | elseif love.keyboard.isDown("up") then 120 | ball:setPosition(325, 325) 121 | ball:setLinearVelocity(0, 0) 122 | elseif love.keyboard.isDown("down") then 123 | ball:applyForce(0, 600) 124 | end 125 | end 126 | 127 | #+END_SRC 128 | 129 | *** easily draw physics 130 | #+BEGIN_SRC lua 131 | function love.draw() 132 | world:draw() 133 | end 134 | #+END_SRC 135 | 136 | ** call functions on collision 137 | default collision callbacks of World will locate the colliders from a fixture's userData and call the relevant :enter :exit :postSolve or :preSolve method 138 | 139 | **** pre: make that function and object to play with 140 | #+BEGIN_SRC lua 141 | little_ball = {} 142 | little_ball.__index = little_ball 143 | setmetatable(little_ball, bf.Collider) -- this is important 144 | -- otherwise setting the new object's metatable to little_ball overwrites 145 | 146 | function spawn_random_ball() 147 | little_ball.new(love.math.random(love.graphics.getWidth()), 0) 148 | end 149 | 150 | function little_ball.new(x, y) 151 | local n = bf.Collider.new(world, 'Circle', x, y, 5) 152 | setmetatable(n, little_ball) 153 | return n 154 | end 155 | 156 | #+END_SRC 157 | 158 | *** define collision function 159 | one feature is that any function callbacks returned by Collider:enter/exit/Solve are run in world:update() 160 | this lets us easily create and destroy objects in collision callbacks without crashing Box2D (love.physics's backend) 161 | #+BEGIN_SRC lua 162 | function ball:postSolve(other) 163 | if other == block1 then 164 | -- creating Collder.new should never be called inside a callback 165 | -- a limitation of (box2d) 166 | -- instead, return a function to be called during World:update() 167 | return spawn_random_ball -- see above for definition 168 | end 169 | end 170 | 171 | #+END_SRC 172 | 173 | ** change appearance of physics objects 174 | simply define the :draw function on your collider 175 | (you can still access the default draw as :__draw__) 176 | #+BEGIN_SRC lua 177 | function little_ball:draw(alpha) 178 | love.graphics.setColor(0.9, 0.9, 0.0) 179 | love.graphics.circle('fill', self:getX(), self:getY(), self:getRadius()) 180 | end 181 | #+END_SRC 182 | 183 | ** query the world (supports rectangle, circle, polygon and edge) 184 | #+BEGIN_SRC lua 185 | function love.mousepressed() 186 | local x, y 187 | local radius = 30 188 | x, y = love.mouse.getPosition() 189 | local colls = world:queryCircleArea(x, y, radius) 190 | for _, collider in ipairs(colls) do 191 | if collider.identity == little_ball then 192 | local dx = love.mouse.getX() - collider:getX() 193 | local dy = love.mouse.getY() - collider:getY() 194 | local power = -5 195 | collider:applyLinearImpulse(power * dx, power * dy) 196 | end 197 | end 198 | end 199 | #+END_SRC 200 | and after little_ball's declaration 201 | #+BEGIN_SRC lua 202 | little_ball.identity = little_ball 203 | #+END_SRC 204 | ** TODO define some form of collision filtering 205 | for now, see: 206 | [[https://love2d.org/wiki/Contact:setEnabled]] 207 | [[https://love2d.org/wiki/Fixture:setFilterData]] 208 | 209 | 210 | 211 | * links 212 | ** forum 213 | [[https://love2d.org/forums/viewtopic.php?f=5&t=86113&p=224718#p224718][forum]] 214 | 215 | -------------------------------------------------------------------------------- /collider.lua: -------------------------------------------------------------------------------- 1 | -- a Collider object, wrapping shape, body, and fixtue 2 | local set_funcs, lp, lg, COLLIDER_TYPES = unpack( 3 | require((...):gsub('collider', '') .. '/utils')) 4 | 5 | local Collider = {} 6 | Collider.__index = Collider 7 | 8 | 9 | 10 | function Collider.new(world, collider_type, ...) 11 | print("Collider.new is deprecated and may be removed in a later version. use world:newCollider instead") 12 | return world:newCollider(collider_type, {...}) 13 | end 14 | 15 | function Collider:draw_type() 16 | if self.collider_type == 'Edge' or self.collider_type == 'Chain' then 17 | return 'line' 18 | end 19 | return self.collider_type:lower() 20 | end 21 | 22 | function Collider:__draw__() 23 | self._draw_type = self._draw_type or self:draw_type() 24 | local args 25 | if self._draw_type == 'line' then 26 | args = {self:getSpatialIdentity()} 27 | else 28 | args = {'line', self:getSpatialIdentity()} 29 | end 30 | love.graphics[self:draw_type()](unpack(args)) 31 | end 32 | 33 | function Collider:setDrawOrder(num) 34 | self._draw_order = num 35 | self._world._draw_order_changed = true 36 | end 37 | 38 | function Collider:getDrawOrder() 39 | return self._draw_order 40 | end 41 | 42 | function Collider:draw() 43 | self:__draw__() 44 | end 45 | 46 | 47 | function Collider:destroy() 48 | self._world:_remove(self) 49 | self.fixture:setUserData(nil) 50 | self.fixture:destroy() 51 | self.body:destroy() 52 | end 53 | 54 | function Collider:getSpatialIdentity() 55 | if self.collider_type == 'Circle' then 56 | return self:getX(), self:getY(), self:getRadius() 57 | else 58 | return self:getWorldPoints(self:getPoints()) 59 | end 60 | end 61 | 62 | function Collider:collider_contacts() 63 | local contacts = self:getContacts() 64 | local colliders = {} 65 | for i, contact in ipairs(contacts) do 66 | if contact:isTouching() then 67 | local f1, f2 = contact:getFixtures() 68 | if f1 == self.fixture then 69 | colliders[#colliders+1] = f2:getUserData() 70 | else 71 | colliders[#colliders+1] = f1:getUserData() 72 | end 73 | end 74 | end 75 | return colliders 76 | end 77 | 78 | return Collider 79 | -------------------------------------------------------------------------------- /future.org: -------------------------------------------------------------------------------- 1 | * future 2 | ideas for future features of breezefield 3 | THESE ARE IDEAS AND ARE NOT CURRENTLY IMPLEMENTED 4 | I also do not guarantee to ever get around to all of them (but feel free to contribute yourself, even if it's just a note of an idea!) 5 | ** make it possible to use either love.physics or HC as a backend 6 | (is this worth it?) 7 | *** implement some 'translation' of love.physics commands to HC and vice versa 8 | ** add features for filtering collisions 9 | *** idea 1: set which colliders to ignore/collide 10 | **** collider:setCollides(colliders) 11 | **** collider:setIgnores(colliders) 12 | **** collider:getIgnores() 13 | **** collider:getCollides() 14 | **** collider:clearCollides() -- reset to default 15 | **** define identity to some value (e.g. table used to generate instances) 16 | ***** if specific table is not set to ignore or collides, check if _collider_class is set to Ignore 17 | *** idea 2: interface to setFilterData/setMask/setCategory/setGroupIndex 18 | **** wrap them to accept list of integers, converting to 16 bit int and passing through 19 | *** idea 3: 20 | **** something to allow :enter :exit to work even without physics collisions 21 | **** idea 1 with collider:setCallCollision(colliders) ... ? 22 | -------------------------------------------------------------------------------- /init.lua: -------------------------------------------------------------------------------- 1 | -- breezefield: init.lua 2 | --[[ 3 | implements Collider and World objects 4 | Collider wraps the basic functionality of shape, fixture, and body 5 | World wraps world, and provides automatic drawing simplified collisions 6 | ]]-- 7 | 8 | 9 | 10 | local bf = {} 11 | 12 | 13 | local Collider = require(... ..'/collider') 14 | local World = require(... ..'/world') 15 | 16 | 17 | function bf.newWorld(...) 18 | return bf.World:new(...) 19 | end 20 | 21 | bf.Collider = Collider 22 | bf.World = World 23 | 24 | return bf 25 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | mkdir test/breezefield 3 | cp * test/breezefield 4 | love test 5 | 6 | mkdir test/test_deep/deps 7 | mkdir test/test_deep/deps/breezefield 8 | cp * test/test_deep/deps/breezefield 9 | love test/test_deep 10 | 11 | mkdir test/test_rendering/breezefield 12 | cp * test/test_rendering/breezefield 13 | love test/test_rendering 14 | 15 | mkdir test/test_queryareas/breezefield 16 | cp * test/test_queryareas/breezefield 17 | love test/test_queryareas -------------------------------------------------------------------------------- /test/main.lua: -------------------------------------------------------------------------------- 1 | local pre_globals = {} 2 | for n, v in pairs(_G) do 3 | pre_globals[n] = v 4 | end 5 | 6 | bf = require("breezefield") 7 | 8 | for n, v in pairs(_G) do 9 | assert(n == 'bf' or pre_globals[n] ~= nil, 'stray global variable: '.. n) 10 | end 11 | 12 | -- TODO split into multiple test scripts 13 | function love.load() 14 | world = bf.newWorld(0, 90.81, true) 15 | 16 | ground = bf.Collider.new(world, 17 | "rect", 325, 600, 650, 100) 18 | 19 | ground:setType("static") 20 | print(ground.collider_type) 21 | 22 | ball = bf.Collider.new(world, "Circle", 325, 325, 20) 23 | 24 | ball:setRestitution(0.8) 25 | block1 = bf.Collider.new(world, "Polygon", {150, 375, 250, 375, 26 | 250, 425, 150, 425}) 27 | little_ball.new( 28 | love.math.random(love.graphics.getWidth()), 0) 29 | 30 | tri = bf.Collider.new(world, "Polygon", {400, 400, 450, 400, 425, 356.7}) 31 | 32 | edge = bf.Collider.new(world, 'Edge', 500, 300, 500, 500) 33 | edge:setType('static') 34 | 35 | chain = bf.Collider.new(world, 'Chain', false, 100, 100, 110, 110, 115, 110, 120, 115, 120, 125, 130, 130) 36 | 37 | 38 | function ball:postSolve(other) 39 | if other == block1 then 40 | -- creating Collder.new should never be called inside a callback 41 | -- a limitation of (box2d) 42 | -- instead, return a function to be called during update() 43 | return spawn_random_ball 44 | end 45 | end 46 | 47 | function ball:enter(other) 48 | if other == tri then 49 | self:setLinearVelocity(0, -200) 50 | end 51 | return 52 | end 53 | 54 | end 55 | 56 | function love.update(dt) 57 | world:update(dt) 58 | if love.keyboard.isDown("right") then 59 | ball:applyForce(400, 0) 60 | elseif love.keyboard.isDown("left") then 61 | ball:applyForce(-400, 0) 62 | elseif love.keyboard.isDown("up") then 63 | ball:setPosition(325, 325) 64 | ball:setLinearVelocity(0, 0) 65 | elseif love.keyboard.isDown("down") then 66 | ball:applyForce(0, 600) 67 | end 68 | end 69 | 70 | 71 | function love.draw() 72 | love.graphics.setColor(1, 1, 1) 73 | world:draw() 74 | if printcircle then 75 | love.graphics.setColor(0.5, 1, 0.5, 0.9) 76 | love.graphics.circle('line', unpack(printcircle)) 77 | end 78 | end 79 | 80 | -- TODO demonstrate collisionn handling 81 | -- TODO demonstrate collision classes 82 | function love.mousepressed(x, y, button) 83 | local radius = 30 84 | local colls = world:queryCircleArea(x, y, radius) 85 | printcircle = {x, y, radius} 86 | for _, collider in ipairs(colls) do 87 | if collider.identity == little_ball then 88 | local dx = love.mouse.getX() - collider:getX() 89 | local dy = love.mouse.getY() - collider:getY() 90 | local power = -5 91 | collider:applyLinearImpulse(power * dx, power * dy) 92 | end 93 | end 94 | end 95 | 96 | little_ball = {} 97 | little_ball.__index = little_ball 98 | little_ball.identity = little_ball 99 | setmetatable(little_ball, bf.Collider) 100 | 101 | function spawn_random_ball() 102 | little_ball.new(love.math.random(love.graphics.getWidth()), 0) 103 | end 104 | 105 | function little_ball.new(x, y) 106 | local n = bf.Collider.new(world, 'Circle', x, y, 5) 107 | setmetatable(n, little_ball) 108 | return n 109 | end 110 | 111 | function little_ball:draw(alpha) 112 | love.graphics.setColor(0.9, 0.9, 0.0) 113 | love.graphics.circle('fill', self:getX(), self:getY(), self:getRadius()) 114 | end 115 | -------------------------------------------------------------------------------- /test/test_deep/main.lua: -------------------------------------------------------------------------------- 1 | local pre_globals = {} 2 | for n, v in pairs(_G) do 3 | pre_globals[n] = v 4 | end 5 | 6 | bf = require("deps.breezefield") 7 | 8 | for n, v in pairs(_G) do 9 | assert(n == 'bf' or pre_globals[n] ~= nil, 'stray global variable: '.. n) 10 | end 11 | 12 | -- TODO split into multiple test scripts 13 | function love.load() 14 | world = bf.newWorld(0, 90.81, true) 15 | ground = bf.Collider.new(world, 16 | "rect", 325, 600, 650, 100) 17 | 18 | ground:setType("static") 19 | print(ground.collider_type) 20 | 21 | ball = bf.Collider.new(world, "Circle", 325, 325, 20) 22 | 23 | ball:setRestitution(0.8) 24 | block1 = bf.Collider.new(world, "Polygon", {150, 375, 250, 375, 25 | 250, 425, 150, 425}) 26 | little_ball.new( 27 | love.math.random(love.graphics.getWidth()), 0) 28 | 29 | tri = bf.Collider.new(world, "Polygon", {400, 400, 450, 400, 425, 356.7}) 30 | edge = bf.Collider.new(world, 'Edge', 500, 300, 500, 500) 31 | edge:setType('static') 32 | 33 | chain = bf.Collider.new(world, 'Chain', false, 100, 100, 110, 110, 115, 110, 120, 115, 120, 125, 130, 130) 34 | 35 | 36 | function ball:postSolve(other) 37 | if other == block1 then 38 | -- creating Collder.new should never be called inside a callback 39 | -- a limitation of (box2d) 40 | -- instead, return a function to be called during update() 41 | return spawn_random_ball 42 | end 43 | end 44 | 45 | function ball:enter(other) 46 | if other == tri then 47 | self:setLinearVelocity(0, -200) 48 | end 49 | return 50 | end 51 | 52 | end 53 | 54 | function love.update(dt) 55 | world:update(dt) 56 | if love.keyboard.isDown("right") then 57 | ball:applyForce(400, 0) 58 | elseif love.keyboard.isDown("left") then 59 | ball:applyForce(-400, 0) 60 | elseif love.keyboard.isDown("up") then 61 | ball:setPosition(325, 325) 62 | ball:setLinearVelocity(0, 0) 63 | elseif love.keyboard.isDown("down") then 64 | ball:applyForce(0, 600) 65 | end 66 | end 67 | 68 | 69 | function love.draw() 70 | world:draw() 71 | end 72 | 73 | -- TODO demonstrate collisionn handling 74 | -- TODO demonstrate collision classes 75 | function love.mousepressed() 76 | local x, y 77 | local radius = 30 78 | x, y = love.mouse.getPosition() 79 | local colls = world:queryCircleArea(x, y, radius) 80 | for _, collider in ipairs(colls) do 81 | if collider.identity == little_ball then 82 | local dx = love.mouse.getX() - collider:getX() 83 | local dy = love.mouse.getY() - collider:getY() 84 | local power = -5 85 | collider:applyLinearImpulse(power * dx, power * dy) 86 | end 87 | end 88 | end 89 | 90 | little_ball = {} 91 | little_ball.__index = little_ball 92 | little_ball.identity = little_ball 93 | setmetatable(little_ball, bf.Collider) 94 | 95 | function spawn_random_ball() 96 | little_ball.new(love.math.random(love.graphics.getWidth()), 0) 97 | end 98 | 99 | function little_ball.new(x, y) 100 | local n = bf.Collider.new(world, 'Circle', x, y, 5) 101 | setmetatable(n, little_ball) 102 | return n 103 | end 104 | 105 | function little_ball:draw(alpha) 106 | love.graphics.setColor(0.9, 0.9, 0.0) 107 | love.graphics.circle('fill', self:getX(), self:getY(), self:getRadius()) 108 | end 109 | -------------------------------------------------------------------------------- /test/test_queryareas/main.lua: -------------------------------------------------------------------------------- 1 | local bf = require('breezefield') 2 | 3 | 4 | function love.load() 5 | world = bf.newWorld(0, 0, true) 6 | ball = bf.Collider.new(world, "Circle", 325, 325, 20) 7 | tri = bf.Collider.new(world, "Polygon", {500, 500, 550, 500, 525, 456.7}) 8 | -- test circle fully in circle 9 | local circleincircle = world:queryCircleArea(325, 325, 30) 10 | assert(circleincircle[1] == ball and #circleincircle == 1) 11 | -- test circle intersects circle 12 | local circleintcircle = world:queryCircleArea(320, 325, 20) 13 | assert(circleintcircle[1] == ball and #circleintcircle == 1) 14 | -- test polygon in circle 15 | local polyincircle = world:queryCircleArea(525, 500, 100) 16 | assert(polyincircle[1] == tri and #polyincircle == 1) 17 | -- test polygon intersects circle 18 | local polyintcircle = world:queryCircleArea(525, 525, 26) 19 | assert(polyintcircle[1] == tri and #polyintcircle == 1) 20 | --test circle in polygon 21 | local circleinpoly = world:queryPolygonArea(300, 300, 300, 350, 350, 350, 350, 300) 22 | assert(circleinpoly[1] == ball and #circleinpoly == 1) 23 | -- test circle intersects polygon 24 | local circleintpoly = world:queryPolygonArea(310, 300, 310, 350, 350, 350, 350, 300) 25 | assert(circleintpoly[1] == ball and #circleintpoly == 1) 26 | -- test polygon inside polygon 27 | local polyinpoly = world:queryPolygonArea(499, 499, 499, 551, 551, 551, 551, 499) 28 | assert(polyinpoly[1] == tri and #polyinpoly == 1) 29 | -- test polygon intersects polygon 30 | local polyintpoly = world:queryPolygonArea(505, 505, 499, 551, 551, 551, 551, 499) 31 | assert(polyintpoly[1] == tri and #polyintpoly == 1) 32 | 33 | -- test region fully inside collider 34 | local circlearoundcircle = world:queryCircleArea(325, 325, 5) 35 | assert(circlearoundcircle[1] == ball and #circlearoundcircle == 1) 36 | 37 | -- test line collider 38 | local aline = bf.Collider.new(world, 'edge', 100, 100, 120, 120, 150, 150) 39 | local edgeinpoly = world:queryRectangleArea(90, 90, 125, 125) 40 | assert(edgeinpoly[1] == aline and #edgeinpoly == 1) 41 | 42 | local circleinlines = world:queryEdgeArea(300, 300, 350, 350) 43 | assert(circleinlines[1] == ball and #circleinlines == 1) 44 | 45 | print('tests passed') 46 | love.event.quit() 47 | end 48 | 49 | -------------------------------------------------------------------------------- /test/test_rendering/main.lua: -------------------------------------------------------------------------------- 1 | bf = require("breezefield") 2 | 3 | function love.load() 4 | world = bf.newWorld() 5 | coll1 = world:newCollider('Circle', {200, 200, 20}) 6 | 7 | coll1:setSensor(true) 8 | coll2 = world:newCollider('Circle', {200, 200, 30}) 9 | function coll2:draw() 10 | love.graphics.setColor(1, 0, 0) 11 | love.graphics.circle('fill', 200, 200, 30) 12 | end 13 | coll1:setDrawOrder(1) 14 | coll2:setDrawOrder(0) 15 | end 16 | 17 | function love.update(dt) 18 | 19 | end 20 | 21 | function love.draw() 22 | world:draw() 23 | end -------------------------------------------------------------------------------- /utils.lua: -------------------------------------------------------------------------------- 1 | -- function used for both 2 | local function set_funcs(mainobject, subobject) 3 | -- this function assigns functions of a subobject to a primary object 4 | --[[ 5 | mainobject: the table to which to assign the functions 6 | subobject: the table whose functions to assign 7 | no output 8 | --]] 9 | for k, v in pairs(subobject.__index) do 10 | if k ~= '__gc' and k ~= '__eq' and k ~= '__index' 11 | and k ~= '__tostring' and k ~= 'destroy' and k ~= 'type' 12 | and k ~= 'typeOf'and k ~= 'getUserData' and k ~= 'setUserData' then 13 | mainobject[k] = function(mainobject, ...) 14 | return v(subobject, ...) 15 | end 16 | end 17 | end 18 | end 19 | 20 | local COLLIDER_TYPES = { 21 | CIRCLE = "Circle", 22 | CIRC = "Circle", 23 | RECTANGLE = "Rectangle", 24 | RECT = "Rectangle", 25 | POLYGON = "Polygon", 26 | POLY = "Polygon", 27 | EDGE = 'Edge', 28 | CHAIN = 'Chain' 29 | } 30 | 31 | 32 | return {set_funcs, love.physics, love.graphics, COLLIDER_TYPES} 33 | -------------------------------------------------------------------------------- /world.lua: -------------------------------------------------------------------------------- 1 | -- breezefield: World.lua 2 | --[[ 3 | World: has access to all the functions of love.physics.world 4 | additionally stores all Collider objects assigned to it in 5 | self.colliders (as key-value pairs) 6 | can draw all its Colliders 7 | by default, calls :collide on any colliders in it for postSolve 8 | or for beginContact if the colliders are sensors 9 | --]] 10 | -- TODO make updating work from here too 11 | -- TODO: update test and tutorial 12 | local Collider = require((...):gsub('world', '') .. 'collider') 13 | local set_funcs, lp, lg, COLLIDER_TYPES = unpack( 14 | require((...):gsub('world', '') .. '/utils')) 15 | 16 | 17 | 18 | local World = {} 19 | World.__index = World 20 | function World:new(...) 21 | -- create a new physics world 22 | --[[ 23 | inputs: (same as love.physics.newWorld) 24 | xg: float, gravity in x direction 25 | yg: float, gravity in y direction 26 | sleep: boolean, whether bodies can sleep 27 | outputs: 28 | w: bf.World, the created world 29 | ]]-- 30 | 31 | local w = {} 32 | setmetatable(w, self) 33 | w._world = lp.newWorld(...) 34 | set_funcs(w, w._world) 35 | w.update = nil -- to use our custom update 36 | w.colliders = {} 37 | 38 | -- some functions defined here to use w without being passed it 39 | 40 | function w.collide(obja, objb, coll_type, ...) 41 | -- collision event for two Colliders 42 | local function run_coll(obj1, obj2, ...) 43 | if obj1[coll_type] ~= nil then 44 | local e = obj1[coll_type](obj1, obj2, ...) 45 | if type(e) == 'function' then 46 | w.collide_events[#w.collide_events+1] = e 47 | end 48 | end 49 | end 50 | 51 | if obja ~= nil and objb ~= nil then 52 | run_coll(obja, objb, ...) 53 | run_coll(objb, obja, ...) 54 | end 55 | end 56 | 57 | function w.enter(a, b, ...) 58 | return w.collision(a, b, 'enter', ...) 59 | end 60 | function w.exit(a, b, ...) 61 | return w.collision(a, b, 'exit', ...) 62 | end 63 | function w.preSolve(a, b, ...) 64 | return w.collision(a, b, 'preSolve', ...) 65 | end 66 | function w.postSolve(a, b, ...) 67 | return w.collision(a, b, 'postSolve', ...) 68 | end 69 | 70 | function w.collision(a, b, ...) 71 | -- objects that hit one another can have collide methods 72 | -- by default used as postSolve callback 73 | local obja = a:getUserData(a) 74 | local objb = b:getUserData(b) 75 | w.collide(obja, objb, ...) 76 | end 77 | 78 | w:setCallbacks(w.enter, w.exit, w.preSolve, w.postSolve) 79 | w.collide_events = {} 80 | return w 81 | end 82 | 83 | function World:_remove(collider) 84 | -- remove collider from table of tracked colliders (does NOT run proper destructors) 85 | --[[ 86 | collider: collider to untrack 87 | --]] 88 | for i, col in ipairs(self.colliders) do 89 | if col == collider then 90 | table.remove(self.colliders, i) 91 | break 92 | end 93 | end 94 | self.colliders[collider] = nil 95 | end 96 | 97 | function World:draw(alpha, draw_over) 98 | -- draw the world 99 | --[[ 100 | alpha: sets the alpha of the drawing, defaults to 1 101 | draw_over: draws the collision objects shapes even if their 102 | .draw method is overwritten 103 | --]] 104 | local color = {love.graphics.getColor()} 105 | if self._draw_order_changed then 106 | table.sort( 107 | self.colliders, 108 | function(a, b) return a:getDrawOrder() < b:getDrawOrder() end 109 | ) 110 | self._draw_order_changed = false 111 | end 112 | for _, c in ipairs(self.colliders) do 113 | love.graphics.setColor(1, 1, 1, alpha or 1) 114 | c:draw(alpha) 115 | if draw_over then 116 | love.graphics.setColor(1, 1, 1, alpha or 1) 117 | c:__draw__() 118 | end 119 | end 120 | love.graphics.setColor(color) 121 | end 122 | 123 | function World:queryRectangleArea(x1, y1, x2, y2) 124 | -- query a bounding-box aligned area for colliders 125 | --[[ 126 | inputs: 127 | x1, y1, x2, y2: floats, the x and y coordinates of two points 128 | outputs: 129 | colls: table, all colliders in bounding box 130 | --]] 131 | 132 | local colls = {} 133 | local callback = function(fixture) 134 | table.insert(colls, fixture:getUserData()) 135 | return true 136 | end 137 | self:queryBoundingBox(x1, y1, x2, y2, callback) 138 | return colls 139 | end 140 | 141 | local function check_vertices(vertices) 142 | if #vertices % 2 ~= 0 then 143 | error('vertices must be a multiple of 2') 144 | elseif #vertices < 4 then 145 | error('must have at least 2 vertices with x and y each') 146 | end 147 | end 148 | 149 | local function is_edgy(colltype) 150 | return colltype == COLLIDER_TYPES.POLY 151 | or colltype == COLLIDER_TYPES.RECT 152 | or colltype == COLLIDER_TYPES.EDGE 153 | or colltype == COLLIDER_TYPES.CHAIN 154 | end 155 | 156 | local function any_intersections(coll1, coll2) 157 | local vertices = {coll1:getSpatialIdentity()} 158 | for i=1,#vertices-3,2 do 159 | local x1, y1 = vertices[i], vertices[i+1] 160 | local x2, y2 = vertices[i+2], vertices[i+3] 161 | if (coll2:rayCast(x1, y1, x2, y2, 1) ~= nil) 162 | or coll2:testPoint(x1, y1) 163 | or coll2:testPoint(x2, y2) 164 | then 165 | return true 166 | end 167 | end 168 | end 169 | 170 | local function poly_circle_intersect(poly, circle) 171 | if any_intersections(poly, circle) then 172 | return true 173 | end 174 | return poly:testPoint(circle:getPosition()) 175 | or circle:testPoint(poly:getMassData()) 176 | end 177 | 178 | local function poly_poly_intersect(poly1, poly2) 179 | return any_intersections(poly1, poly2) 180 | or any_intersections(poly2, poly1) 181 | or poly1:testPoint(poly2:getMassData()) -- poly2 in poly1 182 | or poly2:testPoint(poly2:getMassData()) -- poly1 in poly2 183 | end 184 | 185 | local function are_touching(coll1, coll2) 186 | if coll1.collider_type == COLLIDER_TYPES.CIRCLE and is_edgy(coll2.collider_type) then 187 | return are_touching(coll2, coll1) 188 | end 189 | if is_edgy(coll1.collider_type) and coll2.collider_type == COLLIDER_TYPES.CIRCLE then 190 | return poly_circle_intersect(coll1, coll2) 191 | end 192 | if is_edgy(coll1.collider_type) and is_edgy(coll2.collider_type) then 193 | return poly_poly_intersect(coll1, coll2) 194 | end 195 | if coll1.collider_type == COLLIDER_TYPES.CIRCLE and coll2.collider_type == COLLIDER_TYPES.CIRCLE then 196 | return ((coll1:getX() - coll2:getX())^2 + (coll1:getY() - coll2:getY())) <= 197 | coll1:getRadius() + coll2:getRadius() 198 | end 199 | error("collider types not recognized ".. tostring(coll1.collider_type)..', '..tostring(coll2.collider_type)) 200 | end 201 | 202 | local function query_region(world, coll_type, args) 203 | local collider = world:newCollider(coll_type, args) 204 | collider:setSensor(true) 205 | local colls = {} 206 | local function callback(fixture) 207 | local coll = fixture:getUserData() 208 | if coll ~= collider then 209 | if are_touching(collider, coll) then 210 | table.insert(colls, coll) 211 | end 212 | end 213 | return true 214 | end 215 | local ax, ay, bx, by = collider:getBoundingBox() 216 | local in_bounding_box = world:queryBoundingBox( 217 | ax, ay, bx, by, callback) 218 | collider:destroy() 219 | return colls 220 | end 221 | 222 | function World:_disable_callbacks() 223 | self._callbacks = {self._world:getCallbacks()} 224 | self._world:setCallbacks() 225 | end 226 | 227 | function World:_enable_callbacks() 228 | self._world:setCallbacks(unpack(self._callbacks)) 229 | end 230 | 231 | function World:queryPolygonArea(...) 232 | -- query an area enclosed by the lines connecting a series of points 233 | --[[ 234 | inputs: 235 | x1, y1, x2, y2, ... floats, the x and y positions defining polygon 236 | outputs: 237 | colls: table, all Colliders intersecting the area 238 | --]] 239 | local vertices = {...} 240 | if type(vertices[1]) == 'table' then 241 | vertices = vertices[1] 242 | end 243 | check_vertices(vertices) 244 | return query_region(self, COLLIDER_TYPES.POLYGON, vertices) 245 | end 246 | 247 | function World:queryCircleArea(x, y, r) 248 | -- get all colliders in a circle are 249 | --[[ 250 | inputs: 251 | x, y, r: floats, x, y and radius of circle 252 | outputs: 253 | colls: table: colliders in area 254 | ]]-- 255 | return query_region(self, COLLIDER_TYPES.CIRCLE, {x, y, r}) 256 | end 257 | 258 | function World:queryEdgeArea(...) 259 | -- get all colliders along a (series of) line(s) 260 | --[[ 261 | inputs: 262 | x1, y1, x2, y2, ... floats, the x and y positions defining lines 263 | outpts: 264 | colls: table: colliders intersecting these lines 265 | --]] 266 | local vertices = {...} 267 | if type(vertices[1]) == 'table' then 268 | vertices = vertices[1] 269 | end 270 | check_vertices(vertices) 271 | return query_region(self, 'Edge', vertices) 272 | end 273 | 274 | function World:update(dt) 275 | -- update physics world 276 | self._world:update(dt) 277 | for i, v in pairs(self.collide_events) do 278 | v() 279 | self.collide_events[i] = nil 280 | end 281 | end 282 | 283 | --[[ 284 | create a new collider in this world 285 | 286 | args: 287 | collider_type (string): the type of the collider (not case seinsitive). any of: 288 | circle, rectangle, polygon, edge, chain. 289 | shape_arguments (table): arguments required to instantiate shape. 290 | circle: {x, y, radius} 291 | rectangle: {x, y, width height} 292 | polygon/edge/chain: {x1, y1, x2, y2, ...} 293 | table_to_use (optional, table): table to generate as the collider 294 | ]]-- 295 | function World:newCollider(collider_type, shape_arguments, table_to_use) 296 | local o = table_to_use or {} 297 | setmetatable(o, Collider) 298 | -- note that you will need to set static vs dynamic later 299 | local _collider_type = COLLIDER_TYPES[collider_type:upper()] 300 | assert(_collider_type ~= nil, "unknown collider type: "..collider_type) 301 | collider_type = _collider_type 302 | if collider_type == COLLIDER_TYPES.CIRCLE then 303 | local x, y, r = unpack(shape_arguments) 304 | o.body = lp.newBody(self._world, x, y, "dynamic") 305 | o.shape = lp.newCircleShape(r) 306 | elseif collider_type == "Rectangle" then 307 | local x, y, w, h = unpack(shape_arguments) 308 | o.body = lp.newBody(self._world, x, y, "dynamic") 309 | o.shape = lp.newRectangleShape(w, h) 310 | collider_type = "Polygon" 311 | else 312 | o.body = lp.newBody(self._world, 0, 0, "dynamic") 313 | o.shape = lp['new'..collider_type..'Shape'](unpack(shape_arguments)) 314 | end 315 | 316 | o.collider_type = collider_type 317 | 318 | o.fixture = lp.newFixture(o.body, o.shape, 1) 319 | o.fixture:setUserData(o) 320 | 321 | set_funcs(o, o.body) 322 | set_funcs(o, o.shape) 323 | set_funcs(o, o.fixture) 324 | 325 | -- index by self for now 326 | o._world = self 327 | table.insert(self.colliders, o) 328 | self.colliders[o] = o 329 | o:setDrawOrder(0) 330 | return o 331 | end 332 | 333 | return World 334 | --------------------------------------------------------------------------------