Skip to content

Instantly share code, notes, and snippets.

@darkwater
Created March 5, 2019 18:53
Show Gist options
  • Save darkwater/000a7800c3b1c96d85b78a6f107eff51 to your computer and use it in GitHub Desktop.
Save darkwater/000a7800c3b1c96d85b78a6f107eff51 to your computer and use it in GitHub Desktop.
part of game prototype
function bullet_new(opts)
local bullet = {
position = opts.position,
momentum = opts.momentum,
radius = opts.radius,
orientation = 0,
color = { 230, 200, 30, 240 },
}
function bullet:update(dt)
self.position.x = self.position.x + self.momentum.x * dt
self.position.y = self.position.y + self.momentum.y * dt
self.orientation = math.atan2(self.momentum.y, self.momentum.x)
self:collide()
end
function bullet:collide()
for _,chunk in pairs(stage.level.chunks) do
for _,seg in chunk:geom_iter() do
local a = seg.a
local b = seg.b
local circle = {
position = self.position,
radius = self.radius
}
local collision = collide.circle_line(circle, a, b)
if collision then
stage:despawn("bullet", self)
stage:spawn("effect", self:impact(collision.hitCenter))
return
end
end
end
end
function bullet:impact(pos)
local effect = {
position = pos,
sparks = {},
color = self.color,
t = 0,
}
for i=1, 10 do
local spark = {
ang = math.random() * math.pi * 2, -- 0.0..tau
mag = math.random() * 0.4 + 0.8 -- 0.8..1.2
}
effect.sparks[i] = vec.from_polar(spark)
end
function effect:update(dt)
self.t = self.t + dt * 4
if self.t >= 1 then
stage:despawn("effect", self)
end
end
function effect:draw()
love.graphics.push()
love.graphics.translate(self.position.x, self.position.y)
local a = self.t * 24
local b = self.t ^ 2 * 24
love.graphics.setColor(self.color)
for _,spark in pairs(self.sparks) do
love.graphics.line(spark.x*a, spark.y*a, spark.x*b, spark.y*b)
end
love.graphics.pop()
end
return effect
end
function bullet:draw(dt)
love.graphics.push()
love.graphics.translate(self.position.x, self.position.y)
love.graphics.rotate(self.orientation)
love.graphics.setColor(self.color)
love.graphics.polygon("line", 5,0 , -5,3 , -5,-3)
love.graphics.pop()
end
return bullet
end
function camera_new()
local camera = {
position = { x=0, y=0 },
speed = 7,
}
function camera:follow(target, dt)
local tx = target.x - ui.width / 2
local ty = target.y - ui.height / 2
local target = { x=tx, y=ty }
self.position = vec.interp(self.position, target, dt * self.speed)
end
function camera:draw()
for _,geom in pairs(self.geometry) do
love.graphics.line(geom)
end
end
return camera
end
function chunk_new()
local chunk = {
geometry = {
{ 0,0 , 500,0 , 1000,200 , 1000,700 , 500,700 , 0,500 , 0,0 },
{ 470,300 , 550,320 , 530,400 , 450,380 , 470,300 },
},
}
function chunk:update(dt)
end
function chunk:draw()
for _,geom in pairs(self.geometry) do
love.graphics.line(geom)
end
end
-- iterate through line segments
-- for some geometry { 1,2 , 3,4 , 5,6 }
-- iterates over { 1,2 , 3,4 }, { 3,4 , 5,6 }
-- 'length' per poly is #geom / 2 - 1
function chunk:geom_iter()
return function (geometry, i)
i.pair = i.pair + 1
local poly = geometry[i.poly]
if i.pair > #poly / 2 - 1 then
i.poly = i.poly + 1
i.pair = 1
poly = geometry[i.poly]
if poly == nil then
return
end
end
local pair_start = (i.pair - 1) * 2 + 1
local a = {
x = poly[pair_start + 0],
y = poly[pair_start + 1],
}
local b = {
x = poly[pair_start + 2],
y = poly[pair_start + 3],
}
return i, { a=a, b=b }
end, self.geometry, { poly=1, pair=0 }
end
return chunk
end
function love.conf(t)
t.window.title = "algame"
t.window.fullscreen = true
end
Enemy = {}
function Enemy.new(opts)
local enemy = {
position = opts.position,
momentum = { x=math.random(-250, 250), y=math.random(-250, 250) },
radius = 20,
}
attach(enemy, Enemy)
return enemy
end
function Enemy:update(dt)
self.position.x = self.position.x + self.momentum.x * dt
self.position.y = self.position.y + self.momentum.y * dt
self:collide()
end
function Enemy:collide()
for _,chunk in pairs(stage.level.chunks) do
for _,seg in chunk:geom_iter() do
local a = seg.a
local b = seg.b
local circle = {
position = self.position,
radius = self.radius
}
local collision = collide.circle_line(circle, a, b)
if collision then
self.position = collision.offset
local antinormal = vec.to_polar(collision.normal).ang + math.pi/2
local momentum = vec.to_polar(self.momentum)
local _oldang = momentum.ang-math.pi
local diff = math.angsub(antinormal, momentum.ang)
momentum.ang = momentum.ang + diff * 2
local _newang = momentum.ang
self.momentum = vec.from_polar(momentum)
stage:spawn("effect", {
pos = vec.clone(self.position),
life = 20,
update = function (self, dt)
self.life = self.life - dt
if self.life <= 0 then
stage:despawn("effect", self)
end
end,
draw = function (self)
love.graphics.push()
love.graphics.translate(self.pos.x, self.pos.y)
love.graphics.setColor(255, 0, 0)
love.graphics.line(0, 0, math.cos(_oldang)*20, math.sin(_oldang)*20)
love.graphics.setColor(0, 255, 0)
love.graphics.line(0, 0, math.cos(antinormal-math.pi/2)*20, math.sin(antinormal-math.pi/2)*20)
love.graphics.setColor(0, 0, 255)
love.graphics.line(0, 0, math.cos(_newang)*20, math.sin(_newang)*20)
love.graphics.pop()
end,
})
end
end
end
end
function Enemy:draw()
love.graphics.push()
love.graphics.translate(self.position.x, self.position.y)
love.graphics.setColor(0, 175, 255)
love.graphics.circle("line", 0, 0, self.radius)
love.graphics.pop()
end
function joystick_new(opts)
local joystick = {
position = opts.position,
input = { x=0, y=0 },
normalized = { x=0, y=0 },
baseSize = 25,
nubSize = 20,
maxMagnitude = 80,
color = opts.color,
control = opts.control,
}
function joystick:update(dt)
self:control()
end
function joystick:sendTouch(touch)
if touch == nil then
return self:reset()
end
self.input.x = touch.x - self.position.x
self.input.y = touch.y - self.position.y
local polar = vec.to_polar(self.input)
polar.mag = math.min(polar.mag, self.maxMagnitude) / self.maxMagnitude
self.normalized = vec.from_polar(polar)
end
function joystick:reset()
self.input.x = 0
self.input.y = 0
self.normalized = self.input
end
function joystick:draw()
love.graphics.push()
love.graphics.translate(self.position.x, self.position.y)
love.graphics.setColor(self.color)
love.graphics.circle("line", 0, 0, self.baseSize, 8)
love.graphics.circle("line", self.normalized.x * self.maxMagnitude, self.normalized.y * self.maxMagnitude, self.nubSize, 8)
love.graphics.pop()
end
return joystick
end
function level_new()
local level = {
chunks = {},
}
table.insert(level.chunks, chunk_new())
function level:update(dt)
if gametime < 0.005 then
stage:spawn("enemy", Enemy.new({ position={ x=300, y=100 }}))
stage:spawn("enemy", Enemy.new({ position={ x=300, y=100 }}))
stage:spawn("enemy", Enemy.new({ position={ x=300, y=100 }}))
stage:spawn("enemy", Enemy.new({ position={ x=300, y=100 }}))
end
end
function level:draw()
love.graphics.setColor(255, 255, 255, 220)
for _,chunk in pairs(self.chunks) do
chunk:draw()
end
end
-- iterate through line segments in geometry in chunks
function level:geom_iter()
error("unimplemented")
end
return level
end
-- require("bullet");
-- require("camera");
-- require("chunk");
-- require("enemy");
-- require("joystick");
-- require("level");
-- require("player");
-- require("stage");
-- require("ui");
-- require("util");
-- globals
gametime = 0
stage = nil
ui = nil
function love.load()
love.graphics.setFont(love.graphics.newFont(32))
stage = stage_new()
ui = ui_new()
end
function love.update(dt)
gametime = gametime + dt
ui:update(dt)
stage:update(dt)
end
function love.draw()
love.graphics.scale(ui.pixelScale)
stage:draw()
ui:draw()
end
function love.focus(has)
if not has then
-- print("-lost focus-")
end
end
function player_new()
local player = {
position = { x=0, y=0 },
momentum = { x=0, y=0 },
hitRadius = 8,
orientation = 0,
turnSpeed = 15,
moveSpeed = 260,
shootingMoveSpeed = 220,
shooting = false,
aimAngle = 0,
lastShot = gametime,
shotDelay = 0.08,
shotVelocity = 600,
}
player.position.x = 1920/2.7/2
player.position.y = 1080/2.7/2
function player:update(dt)
self.position.x = self.position.x + self.momentum.x * dt
self.position.y = self.position.y + self.momentum.y * dt
if self.shooting then
self:tryShoot()
end
local targetOrientation = self.orientation
if self.shooting then
targetOrientation = self.aimAngle
else
local pol = vec.to_polar(self.momentum)
if pol.mag > 1 then
targetOrientation = pol.ang
end
end
local delta = math.angsub(targetOrientation, self.orientation)
local delta = delta * self.turnSpeed * dt
self.orientation = self.orientation + delta
self:collide()
end
function player:tryShoot()
if gametime - self.lastShot >= self.shotDelay then
self.lastShot = gametime
stage:spawn("bullet", bullet_new({
position = vec.clone(self.position),
momentum = vec.from_polar({ ang = self.aimAngle, mag = self.shotVelocity }),
radius = 6,
}))
end
end
function player:collide()
for _,chunk in pairs(stage.level.chunks) do
for _,seg in chunk:geom_iter() do
local a = seg.a
local b = seg.b
local circle = {
position = self.position,
radius = self.hitRadius
}
local collision = collide.circle_line(circle, a, b)
if collision then
self.position = collision.offset
end
end
end
end
function player:getMoveSpeed()
if self.shooting then
return self.shootingMoveSpeed
else
return self.moveSpeed
end
end
function player:inputMovement(mov)
self.momentum = vec.mul(mov, self:getMoveSpeed())
end
function player:inputShooting(shooting, angle)
self.shooting = shooting
self.aimAngle = angle
end
function player:draw()
love.graphics.push()
love.graphics.translate(self.position.x, self.position.y)
love.graphics.rotate(self.orientation)
love.graphics.setColor(230, 200, 30, 240)
love.graphics.polygon("line", 10,0 , -10,8 , -4,0 , -10,-8)
love.graphics.pop()
end
return player
end
function stage_new()
local stage = {
player = player_new(),
level = level_new(),
camera = camera_new(),
entities = {
bullet = {},
effect = {},
enemy = {},
},
}
function stage:update(dt)
self.player:update(dt)
for _,group in pairs(self.entities) do
for _,ent in pairs(group) do
if ent ~= false then
ent:update(dt)
end
end
end
self.level:update(dt)
self.camera:follow(self.player.position, dt)
end
function stage:spawn(type, ent)
local group = self.entities[type]
assert(group, "invalid entity type "..type)
for i=1, #group do
if group[i] == false then
group[i] = ent
return
end
end
table.insert(group, ent)
end
function stage:despawn(type, ent)
local group = self.entities[type]
assert(group, "invalid entity type"..type)
for i=1, #group do
if group[i] == ent then
group[i] = false
return
end
end
error("tried to despawn an entity that doesn't exist")
end
function stage:draw()
local cam = vec.mul(self.camera.position, -1)
love.graphics.push()
love.graphics.translate(cam.x, cam.y)
self.level:draw()
for _,group in pairs(self.entities) do
for _,ent in pairs(group) do
if ent ~= false then
ent:draw()
end
end
end
self.player:draw()
love.graphics.pop()
end
return stage
end
function ui_new()
local pixelScale = love.window.getPixelScale()
local width, height = love.window.getMode()
local ui = {
pixelScale = pixelScale,
width = width / pixelScale,
height = height / pixelScale,
leftJoystick = nil,
rightJoystick = nil,
}
function ui:update(dt)
local touches = love.touch.getTouches()
local leftTouch = nil
local rightTouch = nil
local halfwidth = self.width / 2
for _,v in pairs(touches) do
local x, y = love.touch.getPosition(v)
local touch = {
x = x / self.pixelScale,
y = y / self.pixelScale,
}
if touch.x < halfwidth then
leftTouch = touch
elseif touch.x > halfwidth then
rightTouch = touch
end
end
if self.leftJoystick then
self.leftJoystick:sendTouch(leftTouch)
self.leftJoystick:update(dt)
end
if self.rightJoystick then
self.rightJoystick:sendTouch(rightTouch)
self.rightJoystick:update(dt)
end
end
function ui:draw()
if self.leftJoystick then
self.leftJoystick:draw()
end
if self.rightJoystick then
self.rightJoystick:draw()
end
end
function ui:determineJoystickPosition(side)
local halfwidth = self.width / 2
local centeroffset = halfwidth * 0.6
local y = self.height * 0.5
if side == "left" then
return {
x = halfwidth - centeroffset,
y = y,
}
elseif side == "right" then
return {
x = halfwidth + centeroffset,
y = y,
}
else
error("invalid joystick position " + side)
end
end
ui.leftJoystick = joystick_new({
position = ui:determineJoystickPosition("left"),
color = { 230, 200, 30, 240 },
control = function (self)
stage.player:inputMovement(self.normalized)
end,
})
ui.rightJoystick = joystick_new({
position = ui:determineJoystickPosition("right"),
color = { 240, 20, 30, 240 },
control = function (self)
local pol = vec.to_polar(self.normalized)
stage.player:inputShooting(pol.mag > 0.1, pol.ang)
end,
})
return ui
end
function attach(sub, super)
setmetatable(sub, { __index = function (_, key) return super[key] end })
end
function math.sign(n)
return (n > 0) and 1 or
(n < 0) and -1 or 0
end
function math.angsub(lhs, rhs)
return ((lhs - rhs) + math.pi) % (2*math.pi) - math.pi
end
vec = {}
function vec.clone(pos)
return {
x = pos.x,
y = pos.y,
}
end
function vec.to_polar(pos)
return {
ang = math.atan2(pos.y, pos.x),
mag = math.sqrt(pos.x ^ 2 + pos.y ^ 2),
}
end
function vec.from_polar(pos)
return {
x = math.cos(pos.ang) * pos.mag,
y = math.sin(pos.ang) * pos.mag,
}
end
function vec.sub(lhs, rhs)
return {
x = lhs.x - rhs.x,
y = lhs.y - rhs.y,
}
end
function vec.mul(lhs, scalar)
return {
x = lhs.x * scalar,
y = lhs.y * scalar,
}
end
function vec.div(lhs, scalar)
return {
x = lhs.x / scalar,
y = lhs.y / scalar,
}
end
function vec.dot(lhs, rhs)
return lhs.x * rhs.x + lhs.y * rhs.y
end
function vec.interp(from, to, a)
return {
x = from.x * (1-a) + to.x * a,
y = from.y * (1-a) + to.y * a,
}
end
collide = {}
function collide.circle_line(circle, a, b)
-- simple case: vertical line
if a.x == b.x then
local dx = circle.position.x - a.x
if circle.radius < math.abs(dx) then
return false
end
-- reorder so that a is at the top
if a.y > b.y then
a, b = b, a
end
-- circle is above the line
if circle.position.y < a.y then
if circle.position.y + circle.radius < a.y then
return false
end
-- circle is still touching the line, so we collide the circle with
-- point a, which is basically a circle with radius 0
return collide.circle_circle(circle, { position = a, radius = 0 })
end
-- circle is below the line
if circle.position.y > b.y then
if circle.position.y - circle.radius > a.y then
return false
end
-- collide circle with point b
return collide.circle_circle(circle, { position = b, radius = 0 })
end
return {
normal = {
x = math.sign(dx),
y = 0,
},
offset = {
x = a.x + circle.radius * math.sign(dx),
y = circle.position.y,
},
hitCenter = { x=a.x, y=circle.position.y },
}
end
-- simple case: horizontal line
if a.y == b.y then
local dy = circle.position.y - a.y
if circle.radius < math.abs(dy) then
return false
end
-- reorder so that a is to the left
if a.x > b.x then
a, b = b, a
end
-- circle is to the left of the line
if circle.position.x < a.x then
if circle.position.x + circle.radius < a.x then
return false
end
-- collide circle with point a
return collide.circle_circle(circle, { position = a, radius = 0 })
end
-- circle is to the right of the line
if circle.position.x > b.x then
if circle.position.x - circle.radius > a.x then
return false
end
-- collide circle with point b
return collide.circle_circle(circle, { position = b, radius = 0 })
end
return {
normal = {
x = 0,
y = math.sign(dy),
},
offset = {
x = circle.position.x,
y = a.y + circle.radius * math.sign(dy),
},
hitCenter = { x=circle.position.x, y=a.y },
}
end
-- neither of those? alright, let's do some maths!
-- we basically need our line AB in the format: y = ax + b
-- where a is the slope and b is the intercept, as seen in calculus
-- then we can calculate a perpendicular line PQ that goes through point P
-- and calculate the intersection between lines AB and PQ (Q is the intersection)
-- calculate slope and intercept for line AB
local dx = b.x - a.x
local dy = b.y - a.y
local ab_slope = dy / dx
local ab_intercept = -(ab_slope * a.x - a.y)
-- find the line perpendicular to AB that crosses our point P
-- this will be line PQ, where Q is P projected onto AB
local pq_slope = -(1 / ab_slope)
local pq_intercept = circle.position.y - (pq_slope * circle.position.x)
-- calculate Q
local qx = (ab_intercept - pq_intercept) / (pq_slope - ab_slope)
local q = {
x = qx,
y = pq_slope * qx + pq_intercept,
}
-- check for collision
local dx = circle.position.x - q.x
local dy = circle.position.y - q.y
local dist_sq = dx^2 + dy^2
local radius_sq = circle.radius^2
if radius_sq < dist_sq then
-- distance between center and line is more than circle radius
return false
end
-- check if Q is actually between A and B
-- to do this, we actually check how far Q is from A towards B as a number
-- where 0.0 means Q = A and 1.0 means Q = B
local q_along_ab = (q.x - a.x) / (b.x - a.x)
-- if this number is not in 0..1, that means our line AB either doesn't
-- touch our circle, or it's hitting one of its ends. n < 0 means it's on
-- A's side, while n > 1 means it's on B's side. either way we can solve the
-- collision with a circle - point collision (where point is a radius 0 circle)
if q_along_ab < 0 then
return collide.circle_circle(circle, { position = a, radius = 0 })
elseif q_along_ab > 1 then
return collide.circle_circle(circle, { position = b, radius = 0 })
end
-- the circle is hitting the side of the line
local dist = math.sqrt(dist_sq)
-- now let's calculate point R, which will be on line PQ, `radius` away from Q
-- point R will be where the center of the circle should be to just touch line AB
-- let's first introduce a new point S, which will share one co-ordinate with P
-- and another with Q, making triangle PQS a right triangle with a vertical and a
-- horizontal side, the lengths of these sides are dx and dy from earlier
-- dx and dy can be negative, which is important to put R on the correct side of AB
-- the ratios RQ : PQ, TQ : SQ, RT : PS will be equal, so we use that to calculate R
-- note that RQ is the circle's radius and PQ is the distance from P to line AB
-- we'll calculate the new deltas from Q
local ratio = circle.radius / dist
local ox = dx * ratio
local oy = dy * ratio
-- and now we can calculate our final point R! (and we'll also pass Q)
return {
normal = {
x = dx / dist,
y = dy / dist,
},
offset = {
x = q.x + ox,
y = q.y + oy,
},
hitCenter = q,
}
end
function collide.circle_circle(a, b)
local dx = a.position.x - b.position.x
local dy = a.position.y - b.position.y
local distance_sq = dx^2 + dy^2
local a_radius_sq = a.radius^2
local b_radius_sq = b.radius^2
if distance_sq > a_radius_sq + b_radius_sq then
return false
end
-- the circles touch! let's calculate where to put a so it's right next to b
-- dx and dy are the deltas from b to a, so this normal will be from b to a
local distance = math.sqrt(distance_sq)
local normal = {
x = dx / distance,
y = dy / distance
}
-- calculate the offset
local new_distance = math.sqrt(a_radius_sq) + math.sqrt(b_radius_sq)
local offset = {
x = b.position.x + normal.x * new_distance,
y = b.position.y + normal.y * new_distance,
}
return {
normal = normal,
offset = offset,
hitCenter = b.position,
}
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment