nominally working hopping demo

This commit is contained in:
Nick Stokoe
2022-07-02 18:34:16 +01:00
parent 8ff275adac
commit 2367bf83cf
3 changed files with 345 additions and 0 deletions

84
arena.lua Normal file
View File

@@ -0,0 +1,84 @@
local sideHeight = 35
local width = 55
local topHeight = 20
local polys = {
{
0, 0, width/2, topHeight/2,
0, topHeight, -width/2, topHeight/2,
},
{
0, topHeight, width/2, topHeight/2,
width/2, topHeight/2 + sideHeight, 0, topHeight + sideHeight,
},
{
0, topHeight, -width/2, topHeight/2,
-width/2, topHeight/2 + sideHeight, 0, topHeight + sideHeight,
},
}
return function(size, discs, topCol, rightCol, leftCol)
assert(size > 0)
for disc in pairs(discs) do
assert(disc >= -size and disc <= size)
end
local colours = {topCol, rightCol, leftCol}
local function drawCube(x, y)
love.graphics.push()
love.graphics.translate(x, y)
for ix, poly in ipairs(polys) do
love.graphics.setColor(colours[ix])
love.graphics.polygon("fill", poly)
end
love.graphics.pop()
end
local function getHeight()
return size*(topHeight/2 + sideHeight) + topHeight/2
end
local x = love.graphics.getWidth()/2
local y = love.graphics.getHeight()/2 - getHeight()/2;
local function qpos2Coords(se, sw)
return x + se * width/2 - sw * width/2,
y + (se + sw)*(topHeight/2+sideHeight)
end
return {
getWidth = function()
return size*width*2
end,
getHeight = getHeight,
getSize = function() return size; end,
getCubeSideHeight = function() return sideHeight; end,
getCubeTopHeight = function() return topHeight; end,
getCubeHeight = function() return sideHeight+topHeight; end,
getCubeWidth = function() return width; end,
getRowHeight = function() return sideHeight+topHeight/2; end,
getColWidth = function() return width/2; end,
qpos2Coords = qpos2Coords,
draw = function()
for se = 0, size-1 do
for sw = 0, size-1 do
if (se + sw < size) then
local x, y = qpos2Coords(se, sw)
drawCube(x, y)
end
end
end
end,
update = function(dt)
end,
}
end

50
main.lua Normal file
View File

@@ -0,0 +1,50 @@
local newArena = require "arena"
local newQbert = require "qbert"
local arena = newArena(
7,
{3, -3},
{255, 0, 0}, {0, 255, 0}, {0, 0, 255}
)
local time = 0
local qbert = newQbert(
arena
)
function love.load()
end
function love.draw()
arena.draw()
qbert.draw()
end
local dirs = {"se", "sw", "ne", "nw"}
local ix = 1
function love.update(dt)
--[[ time = time + dt
if (time > 2) then
time = 0
local dir = dirs[ix]
ix = (ix+1)%4 + 1
qbert.jump(dir)
end
]]--
if love.keyboard.isDown('i') then
qbert.jump('ne')
end
if love.keyboard.isDown('u') then
qbert.jump('nw')
end
if love.keyboard.isDown('k') then
qbert.jump('se')
end
if love.keyboard.isDown('j') then
qbert.jump('sw')
end
arena.update(dt);
qbert.update(dt);
end

211
qbert.lua Normal file
View File

@@ -0,0 +1,211 @@
local height = 10
local width = height
local jumpHeight = 10
local jumpDist = 20
local jumpDirs = {
-- { se-axis, sw-axis, path x-multiplier, path reverse }
ne = {0, -1, -1, true},
nw = {-1, 0, 1, true},
se = {1, 0, 1, false},
sw = {0, 1, -1, false},
}
--[[
Jump equation, assuming y is up the screen and gravity down, chosen so
that vy and ay are +ve:
y = y0 - vy*t + ay*t*t (A)
x = x0 + vx*t (B)
Jump starts at t0, y0 and x0.
Jump ends at tend, yend and xend.
Jump peak is at ypeak.
These are all known.
We want to infer vx, ay and vy.
We can get vx thus:
xend = x0 + vx*tend
vx = (xend - x0) / tend (C)
Next to get ay and vy. For simplicity, let:
yend2 = yend - y0
ypeak2 = ypeak - y0
Using (A) to get vy:
y = y0 - vy*t + ay*t*t
vy*t = y0 - y + ay*t*t
vy*t = ay*t*t - yend2
vy = (ay*t*t - yend2)/t
vy = ay*t - yend2/t (D)
Using (D) at time tend:
vy = ay*tend - yend2/tend (E)
Using (D) at time tpeak:
vy = ay*tpeak - ypeak2/tpeak (F)
Unfortunately we still don't know tpeak, but we can eliminate it.
Knowing that dy/dt = 0 means:
dy/dt = -vy + 2*ay*t = 0
By definition the time at this point is tpeak, so:
vy = 2*ay*tpeak
tpeak = 0.5*vy/ay
So eliminating tpeak in (F):
vy = ay*tpeak - ypeak2/tpeak = ay*(0.5*vy/ay) - ypeak2/(0.5*vy/ay)
vy = 0.5*vy - ypeak2*ay/(0.5*vy)
vy = 0.5*vy - 2*ypeak2*ay/vy
2*vy = vy - 4*ypeak2*ay/vy
4*ypeak2*ay/vy = vy - 2*vy
4*ypeak2*ay/vy = -vy
4*ypeak2*ay = -vy^2
ay = -0.25*vy^2/ypeak2 (G)
vy = sqrt(-4*ypeak2*ay)
vy = 2*sqrt(-ypeak2*ay) (H)
Eliminating vy using (H) and (E):
vy = 2*sqrt(-ypeak2*ay) = ay*tend - yend2/tend
-4*ypeak2*ay = (ay*tend - yend2/tend)^2
-4*ypeak2*ay = ay^2*tend^2 + yend2^2/tend^2 - 2*ay*tend*yend2/tend
0 = ay^2*tend^2 + yend2^2/tend^2 - 2*ay*yend2 + 4*ypeak2*ay
0 = ay^2*tend^2 + 4*ypeak2*ay - 2*ay*yend2 + yend2^2/tend^2
0 = ay^2*tend^2 + ay*(4*ypeak2 - 2*yend2) + yend2^2/tend^2
This is a quadratic of the form:
0 = a*ay^2 + b*ay + c
Using the quadratic solution equation:
ay = (-b +/- sqrt(b^2 - 4*a*c)/(2*a)
With
a = tend^2
b = 4*ypeak2 - 2*yend2
c = yend2^2/tend^2
Having calculated ay, we can get vy from
vy = ay*tend - yend2/tend (E)
]]--
local function newJumpPath(jumpDist, jumpHeight)
local steps = 5
local vx = jumpDist/steps
local x0 = 0
local y0 = 0
local ypeak = -jumpHeight/2
local yend = jumpHeight
local ypeak2 = ypeak - y0
local yend2 = yend - y0
local tend = 5
local a = tend^2
local b = 4*ypeak2 - 2*yend2
local c = yend2^2/tend^2
local ay = 0.5*(-b + math.sqrt(b^2 - 4*a*c))/a
local vy = ay*tend - yend2/tend
local path = {}
-- print(vy, ay) # DEBUG
for t = 0,tend do
table.insert(path, {x0 + vx*t,
y0 - vy*t + ay*t*t})
end
--[[ DEBUG
for i in ipairs(path) do
print (i, path[i][1], path[i][2])
end
]]--
return path
end
local frameDuration = 0.1 -- millis
local function time2frame(time)
return 1 + math.floor(time / frameDuration)
end
local function jumpDelta(frame, dir, path)
local _, _, mx, isReverse = unpack(jumpDirs[dir])
local ox, oy = unpack(path[1]) -- path offset
if isReverse then -- reverse the path sequence
frame = 1 + #path - frame
ox, oy = unpack(path[#path])
end
local dx, dy = unpack(path[frame])
return dx*mx-ox*mx, dy-oy
end
-- constructor
return function(arena)
assert(arena ~= nil, "arena must not be nil")
local jumpTime = nil
local jumpDir = "se"
local jumpFrame = 1
local se, sw = 0, 0
local jumpPath = newJumpPath(arena.getColWidth(), arena.getRowHeight())
return {
getWidth = function()
return width
end,
getHeight = function()
return height
end,
jump = function(dir)
if jumpTime ~= nil then
return -- already jumping
end
-- print("jump",dir) -- DEBUG
if jumpDirs[dir] then -- it's a valid direction
jumpTime = 0
jumpDir = dir
end
end,
draw = function()
local cx, cy = arena.qpos2Coords(se, sw)
local dx, dy = jumpDelta(jumpFrame, jumpDir, jumpPath)
love.graphics.setColor(128,128,0);
love.graphics.circle("fill", cx+dx, cy+dy, height)
end,
update = function(dt)
if jumpTime ~= nil then
jumpTime = jumpTime + dt
jumpFrame = time2frame(jumpTime)
if jumpFrame > #jumpPath then
jumpTime = nil -- finished
jumpFrame = 1
-- update qpos (se, sw)
local dse, dsw = unpack(jumpDirs[jumpDir])
-- print("update ", se, sw, dse, dsw) -- DEBUG
se = se + dse
sw = sw + dsw
end
end
end,
}
end