--Character controller. Renders point particle. local love = love local marble = {} local oldBuffer = love.graphics.newCanvas() local newBuffer = love.graphics.newCanvas() local oldState, curState, newState local INERTIA = 0.01 local MAXSPEED = 6 local ddx, ddy = 0.0, 0.0 local function State( ) return { t = 0, x = 0, y = 0, dx = 0, dy = 0 } end function marble.Current() return curState end function marble.Next() return newState end function marble.GetAcceleration( ) return ddx, ddy end function marble.Integrate( step ) newState.t = love.timer.getTime() newState.dx = (1.0 - INERTIA) * curState.dx + INERTIA * ddx newState.dy = (1.0 - INERTIA) * curState.dy + INERTIA * ddy newState.x = curState.x + newState.dx * step * MAXSPEED newState.y = curState.y + newState.dy * step * MAXSPEED end function marble.OnImpact( impact, level ) --Adjust current trajectory according to collision. if not impact.dt then return end INERTIA = 0.03 - 0.02 * math.sqrt( level / 120.0 ) MAXSPEED = 2.0 + 4.0 * ( level / 120.0 ) local x, y = impact.r * math.cos( impact.th ), impact.r * math.sin( impact.th ) local vx, vy = newState.dx, newState.dy --Velocity of particle going into collision. local unx, uny = math.cos( impact.normal ), math.sin( impact.normal ) --Outward-facing normal of wave. local uvx, uvy --Unit vector velocity of particle. local speed = math.sqrt( vx * vx + vy * vy ) if speed < 0.01 then --If the marble is motionless, there is no angular velocity wrt 0, --so the wave is headed directly inward. --We handle the collision as if the marble is headed directly outward. uvx, uvy = unx, uny vx, vy = uvx, uvy else uvx, uvy = vx / speed , vy / speed end --Get signed angle between normal and incoming velocity (both unit vectors) local dot = unx * uvy - uny * uvx --Fudge factor: apply an impulse inward so that you don't stick or slide on the wave. local inward = ( dot > 0 ) and dot or -dot inward = inward * inward * inward --Calculate the rotation matrix: --counterclockwise rotation by 2 * pi - 2 * arccos( n dot v ) local c, s = 1 - 2 * dot * dot, - 2 * dot * math.sqrt( 1 - dot * dot ) --Apply: local vxout, vyout = inward * (- math.cos(impact.th) ) - (1.0 - inward) * ( vx * c - vy * s ), inward * (- math.sin(impact.th) ) - (1.0 - inward) * ( x * s + vy * c ) curState.x, curState.y = x, y curState.dx, curState.dy = vxout, vyout --[[debugRenderImpact = { xi = x, yi = y, xf = x - vx, yf = y - vy, xn = 0.2 * unx + x, yn = 0.2 * uny + y, vxout = x + vxout, vyout = y + vyout}]] return marble.Integrate( math.max( impact.dt , 1 / 120 ) ) --Hmm! Maybe this should be a fixed step instead for stability's sake. end function marble.Update() --Roll the log. for k, v in pairs( oldState ) do oldState[k] = curState[k] curState[k] = newState[k] end end function marble.OnKey() ddx = (love.keyboard.isScancodeDown( "d" ) and 1.0 or 0.0) - (love.keyboard.isScancodeDown( "a" ) and 1.0 or 0.0) ddy = (love.keyboard.isScancodeDown( "w" ) and 1.0 or 0.0) - (love.keyboard.isScancodeDown( "s" ) and 1.0 or 0.0) local n = math.sqrt( ddx * ddx + ddy * ddy ) if n < 0.001 then return end ddx, ddy = ddx / n, ddy / n end function marble.OnVictory() --[[marble.Draw = function() --Extrapolate forward for slightly smoother rendering. local dt = love.timer.getTime() - curState.t marble.Integrate( dt + 1 / 60.0 ) local xp, yp = transform:transformPoint( oldState.x, oldState.y ) local xc, yc = transform:transformPoint( curState.x, curState.y ) local xn, yn = transform:transformPoint( newState.x, newState.y ) love.graphics.setColor( 91 / 255, 206 / 255, 250 / 255, 1.0 ) --Trans blue. love.graphics.setLineJoin( "bevel" ) love.graphics.setLineStyle( "smooth" ) love.graphics.setLineWidth( 8.0 ) love.graphics.line( xp, yp, xc, yc, xn, yn) love.graphics.circle( "fill", xn, yn, 4.0 ) love.graphics.setCanvas() end]] end function marble.Draw() --Extrapolate forward for slightly smoother rendering. local dt = love.timer.getTime() - curState.t marble.Integrate( dt + 1 / 60.0 ) local xp, yp = transform:transformPoint( oldState.x, oldState.y ) local xc, yc = transform:transformPoint( curState.x, curState.y ) local xn, yn = transform:transformPoint( newState.x, newState.y ) love.graphics.setCanvas( newBuffer ) love.graphics.setColor( 1.0, 1.0, 1.0 , 0.99 ) -- White. love.graphics.draw( oldBuffer ) --Time-dependent fade: overlay canvas over itself. --Render latest segment in trail. love.graphics.setColor( 245 / 255, 169 / 255, 184 / 255, 1.0 ) --Trans pink. --love.graphics.line( xi, yi, xf, yf ) --Segment stuff: love.graphics.setLineJoin( "bevel" ) love.graphics.setLineStyle( "smooth" ) love.graphics.setLineWidth( 8.0 ) love.graphics.line( xp, yp, xc, yc, xn, yn) love.graphics.circle( "fill", xn, yn, 4.0 ) love.graphics.setCanvas( oldBuffer ) love.graphics.clear( 1.0, 1.0, 1.0, 0.0 ) --Render circle directly to screen. love.graphics.setCanvas() love.graphics.draw( newBuffer ) love.graphics.setColor( 1, 1, 1, 1.0 ) --White. love.graphics.setLineWidth( 1.0 ) --love.graphics.circle( "line", xn, yn, 4 ) oldBuffer, newBuffer = newBuffer, oldBuffer end marble._Draw = marble.Draw function marble.Impact( impact ) end --Window resize. function marble.Resize() newBuffer = love.graphics.newCanvas() --TODO: render oldBuffer to new newBuffer, but scaled down. oldBuffer = love.graphics.newCanvas() end function marble.Reset() INERTIA = 0.05 MAXSPEED = 2.0 oldState, curState, newState = State(), State(), State() marble.Resize() marble.Draw = marble._Draw end function marble.Canvas() return newBuffer end marble.Reset() return marble