phys = {} local world = {} phys.world = world world.boxes = {} world.layers = {} -- There are a few primary physics layers: -- -- * Hard: for hard collisions that do not allow things in whatsoever -- * Soft: for collisions that push other things away over time -- * Player_Hurtbox: collision box that hurts the player -- * Player_Hitbox: collision box from the player that damages things -- * Enemy_Hurtbox: collision box that hurts enemies -- * Enemy_Hitbox: collision box from an enemy that damages things -- -- You can use whatever physics layer you want, but these are the standard ones local function aabb(ax, ay, aw, ah, bx, by, bw, bh) return ax < bx + bw and bx < ax + aw and ay < by + bh and by < ay + ah end local function add_box_to_layers(box) for _, layer in ipairs(box.layers) do if not world.layers[layer] then world.layers[layer] = {} end table.insert(world.layers[layer], box) end end phys.Box = {} phys.Box.__index = phys.Box function phys.Box.new( x, y, width, height, opts ) opts = opts or {} local self = setmetatable({}, phys.Box) self.x = x self.y = y self.offsetx = opts.offsetx or 0 self.offsety = opts.offsety or 0 self.width = width self.height = height self.layers = opts.layers or { "Hard" } self.mask = opts.mask or {} local scene = get_current_scene() assert(scene, "No scene set up.") self.tilemap_mask = opts.tilemap_mask or { scene.tilemap } self.touchx = 0 self.touchy = 0 add_box_to_layers(self) return self end function phys.Box:touching_left() return self.touchx < 0 end function phys.Box:touching_right() return self.touchx > 0 end function phys.Box:touching_horizontal() return self.touchx ~= 0 end function phys.Box:touching_up() return self.touchy < 0 end function phys.Box:touching_down() return self.touchy > 0 end function phys.Box:touching_vertical() return self.touchy ~= 0 end function phys.Box:touching() return self.touchx ~= 0 or self.touchy ~= 0 end function phys.Box:get_rect() return self.x + self.offsetx, self.y + self.offsety, self.width, self.height end local function get_tilemap_collision_rect(map, box) local x, y, width, height = box:get_rect() if has_tile(map, to_tile_coords(x, y)) then return true, get_tile_rect(x, y) end if has_tile(map, to_tile_coords(x + width, y)) then return true, get_tile_rect(x + width, y) end if has_tile(map, to_tile_coords(x, y + height)) then return true, get_tile_rect(x, y + height) end if has_tile(map, to_tile_coords(x + width, y + height)) then return true, get_tile_rect(x + width, y + height) end local extra_x = math.floor(width / (TILESIZE - 1)) local step = width / (extra_x + 1) for i = 1, extra_x do if has_tile(map, to_tile_coords(x + step * i, y)) then return true, get_tile_rect(x + step * i, y) end if has_tile(map, to_tile_coords(x + step * i, y + height)) then return true, get_tile_rect(x + step * i, y + height) end end local extra_y = math.floor(height / (TILESIZE - 1)) step = height / (extra_y + 1) for i = 1, extra_y do if has_tile(map, to_tile_coords(x, y + step * i)) then return true, get_tile_rect(x, y + step * i) end if has_tile(map, to_tile_coords(x + width, y + step * i)) then return true, get_tile_rect(x + width, y * step * i) end end return false end local function get_collision_rect(box) local box_x, box_y, box_width, box_height = box:get_rect() local other_x, other_y, other_width, other_height for _, tilemap in ipairs(box.tilemap_mask) do local ok, tx, ty, t_width, t_height = get_tilemap_collision_rect( tilemap, box ) if ok then return true, tx, ty, t_width, t_height end end for _, layer in ipairs(box.mask) do for _, other in ipairs(world.layers[layer] or {}) do if other == box then goto collision_check_continue end other_x, other_y, other_width, other_height = other:get_rect() if aabb( box_x, box_y, box_width, box_height, other_x, other_y, other_width, other_height ) then return true, other_x, other_y, other_width, other_height end ::collision_check_continue:: end end return false end function phys.Box:update(velx, vely, dt) local small_side = math.min(self.width, self.height) local steps = math.ceil(vec_len(velx * dt, vely * dt) / small_side) self.touchx = 0 self.touchy = 0 -- this block is fat. much like a certain croatian... -- not anymore :D -- shut up -- welc for i = 1, steps do local p = i / steps self.x = self.x + velx * dt * p local ok, otherx, othery, other_width, other_height = get_collision_rect(self) if ok then self.touchx = sign(velx) velx = 0 if self.touchx > 0 then -- this - 0.001 is a hack to prevent some silly bug I can't figure out self.x = otherx - self.width - self.offsetx - 0.001 elseif self.touchx < 0 then self.x = otherx + other_width - self.offsetx end end self.y = self.y + vely * dt * p ok, otherx, othery, other_width, other_height = get_collision_rect(self) if ok then self.touchy = sign(vely) vely = 0 if self.touchy > 0 then self.y = othery - self.height - self.offsety - 0.001 elseif self.touchy < 0 then self.y = othery + other_height - self.offsety end end if self:touching() then break end end return velx, vely end function phys.Box:draw() local x, y, w, h = self:get_rect() lg.rectangle("line", x, y, w, h) end register_comp("Body", function (ent, x, y, w, h, opts) ent.vx = 0 ent.vy = 0 ent.box = phys.Box.new(x, y, w, h, opts) end) function body_sys(ent, dt) ent.vx, ent.vy = ent.box:update(ent.vx, ent.vy, dt) ent.x = ent.box.x ent.y = ent.box.y end