Created
March 5, 2019 18:53
-
-
Save darkwater/000a7800c3b1c96d85b78a6f107eff51 to your computer and use it in GitHub Desktop.
part of game prototype
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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