Skip to content

Instantly share code, notes, and snippets.

@selenologist
Last active February 11, 2016 11:05
Show Gist options
  • Save selenologist/6d710b1eaf051aafb89f to your computer and use it in GitHub Desktop.
Save selenologist/6d710b1eaf051aafb89f to your computer and use it in GitHub Desktop.
Love2d Tutorial for Luke
-- Documentation on love's modules (which are just tables containing
-- other modules, and functions) and the functions within them is
-- https://love2d.org/wiki/love here
-- love2d calls functions called "callbacks" when certain events
-- (like a key being pressed, or the game being ready to draw to the screen,
-- etc) occur. These callbacks are normally set to a dummy function that
-- doesn't do anything in particular, but you can easily override them with
-- your own callbacks as you see below.
sprites = {} -- global variable holding all our sprites
sounds = {} -- global variable holding all our sounds
local ball_list = {}
-- split the npc ball function out to global scope
-- we use _ to show that this is an internal function that isn't supposed to
-- be used directly. It is best to avoid names that begin with _ and are all
-- uppercase, as Lua reserves these for special uses (like _VERSION)
function _npcUpdateBall(self, dt)
-- don't forget to use self. or you'll end up trying to use a
-- global variable! Annoying Lua quirk.
local border = 40
if self.direction == "up" then
self.y = self.y - self.speed * dt
if self.y <= border then
self.direction = "left"
end
elseif self.direction == "down" then
self.y = self.y + self.speed * dt
if self.y >= (love.graphics.getHeight() - border) then
self.direction = "right"
end
elseif self.direction == "left" then
self.x = self.x - self.speed * dt
if self.x <= border then
self.direction = "down"
end
elseif self.direction == "right" then
self.x = self.x + self.speed * dt
if self.x >= (love.graphics.getWidth() - border) then
self.direction = "up"
end
else
-- uh oh
-- do nothing
end
end
function _playerUpdateBall(self, dt)
self.x = self.x + self.xVel * dt
self.y = self.y + self.yVel * dt
-- yes, it's really that easy
end
function _playerInputBall(self, key, pressed_released)
-- called when one of the registered keys (above) is pressed or released
-- pressed_released is "pressed" when the key was pressed, "released" when
-- the key was released.
if pressed_released == "pressed" then
self.keysdown[key] = love.timer.getTime() -- store the time the key
-- was pressed
else
self.keysdown[key] = nil -- unset the key
end
-- we can only travel in at most two directions at once.
-- So, get the two most recently held down keys (or just 1 key) and
-- then travel in the direction formed by these two keys.
-- Note that pressing left-right or up-down will cause the
-- resulting velocity to sum to zero.
local firstHighest = nil
local secondHighest = nil
for k, v in pairs(self.keysdown) do
if firstHighest == nil
or firstHighest.v < v then
-- new most-recent key
-- make the previously-thought most recent key the
-- second most recent
-- and then replace it with the new most recent key
secondHighest = firstHighest
firstHighest = {k = k, v = v}
elseif secondHighest == nil
or secondHighest.v < v then
secondHighest = {k = k, v = v}
end
end
local function keyToVelocities(key)
if key == "up" then
return 0, -self.max_speed
elseif key == "left" then
return -self.max_speed, 0
elseif key == "down" then
return 0, self.max_speed
elseif key == "right" then
return self.max_speed, 0
end
-- we shouldn't get here
return 0, 0 -- but just in case this accidentally happens
end
if firstHighest ~= nil then -- if any keys pressed at all
self.xVel, self.yVel = keyToVelocities(firstHighest.k)
if secondHighest ~= nil then -- if a second key pressed
local addX, addY = keyToVelocities(secondHighest.k)
self.xVel = self.xVel + addX
self.yVel = self.yVel + addY
end
else
self.xVel, self.yVel = 0,0
end
end
function _drawBall(self)
love.graphics.draw(self.sprite,
self.x - self.sprite:getWidth() / 2,
self.y - self.sprite:getHeight() / 2)
end
function makeNPCBall(x, y, speed)
-- simple object maker - no use of metatables or other crazy features yet
return {x = x,
y = y,
sprite = sprites.redball,
speed = speed,
direction = "down",
update = _npcUpdateBall,
draw = _drawBall}
end
function makePlayerBall(x, y, max_speed)
-- and since the ball list only cares about the update and draw functions
-- we can still put this in the NPC ball list and it will be updated and
-- drawn as normal!
return {x = x,
y = y,
max_speed = max_speed,
xVel = 0,
yVel = 0,
sprite = sprites.blueball,
keysdown = {},
update = _playerUpdateBall,
draw = _drawBall,
input = _playerInputBall,
keys = { w = "up",
a = "left",
s = "down",
d = "right" }
-- when we do player_ball.keys[KEY], for anything other than those
-- we set to true, we will get nil (which is treated as false).
-- This allows us to quickly and easily see if a key is used by the
-- player ball.
}
end
function addBall(ball)
-- adds balls to the ball list
table.insert(ball_list, ball)
end
function updateBalls(dt)
-- Sometimes we have to remove balls as a result of something that happened
-- We can't just remove the ball from the list while we're working with it
-- or we might end up skipping balls or processing some twice.
-- So, we keep a list of balls to remove and sweep them at the end
local sweep_list = {}
for index, ball in pairs(ball_list) do
if ball.sweep then
table.insert(sweep_list, index)
else
ball:update(dt)
end
end
for _, ball in pairs(sweep_list) do
table.remove(ball_list, ball)-- goodbye ball
end
end
function drawBalls()
for _, ball in ipairs(ball_list) do
ball:draw()
end
end
local bullet_list = {}
function euclideanDistance(x1, y1, x2, y2)
return math.sqrt(math.pow(x2 - x1, 2) + math.pow(y2 - y1, 2))
end
function _bulletUpdate(self, dt)
self.x = self.x + self.xVel * dt
self.y = self.y + self.yVel * dt
-- note, this involves checking every bullet against every ball EVERY FRAME
-- If you had lots of bullets and/or lots of balls, that's going to mean
-- performance problems. There are ways of speeding it up but let's not
-- worry for now.
local sweep = false
for _, ball in pairs(ball_list) do
-- remember how we put the player ball in the ball list? Yeah. Let's
-- make sure we don't accidentally shoot the player ball. We don't
-- want that.
if ball ~= player_ball then
-- if the bullet is within the ball's circle...
-- or more precisely, if the bullet's distance to the center of the
-- ball is less than the ball's radius
--
-- Assumes ball's sprite is square, and that the ball fills its sprite
-- entirely.
if euclideanDistance(self.x, self.y,
ball.x, ball.y) < ball.sprite:getWidth() / 2 then
-- hit a balloon!
love.audio.play(sounds.balloon_pop)
ball.sweep = true -- the ball will be removed by the ball update
-- function
-- also it would make sense for the bullet to disappear after any
-- other collisions that occur this frame are taken care of.
sweep = true
end
end
end
local margin = 15
-- allow the bullet to go at most 15 pixels off screen before being
-- destroyed.
if self.x < -margin or
self.y < -margin or
self.x > love.graphics.getWidth() + margin or
self.y > love.graphics.getHeight() + margin then
sweep = true
end
return {sweep = sweep}
end
function _bulletDraw(self)
local sprite = sprites.bullet
love.graphics.draw(sprite,
self.x - sprite:getWidth() / 2,
self.y - sprite:getHeight() / 2,
self.angle)
end
function makeAndAddBullet(x, y, angle)
local bullet_speed = 600
local bullet = { x = x,
y = y,
xVel = math.cos(angle) * bullet_speed,
yVel = math.sin(angle) * bullet_speed,
angle = angle,
update = _bulletUpdate,
draw = _bulletDraw }
table.insert(bullet_list, bullet)
return bullet
end
function updateBullets(dt)
local sweep_list = {}
for index, bullet in pairs(bullet_list) do
if bullet:update(dt).sweep then
table.insert(sweep_list, index)
end
end
for _, bullet in pairs(sweep_list) do
table.remove(bullet_list, bullet)
end
end
function drawBullets()
for _, bullet in pairs(bullet_list) do
bullet:draw()
end
end
function love.load() -- function love2d calls to let you load things
-- load the ball graphic from disk
sprites.redball = love.graphics.newImage("redball.png")
sprites.blueball = love.graphics.newImage("blueball.png")
sprites.bullet = love.graphics.newImage("bullet.png")
sounds.balloon_pop = love.audio.newSource("balloon_pop.wav", "static")
sounds.bullet_fire = love.audio.newSource("bullet_fire.wav", "static")
for i = 1,5 do
addBall(makeNPCBall(100 * i, 80 * i, 50 + 10 * i))
end
-- Note that I did not use 'local'. I want player_ball to be visible at
-- global scope (across the whole program).
-- Usually it is a bad idea to create new globals inside a function, but
-- it will do for now.
player_ball = makePlayerBall(love.graphics.getWidth() / 2,
love.graphics.getHeight() / 2,
100)
-- we can even add this player ball to our ball list, and it will be used
-- just like the others. We can still do player-ball-specific things by
-- referring to player_ball instead of looking in the ball list.
-- We didn't give the other balls their own name. They exist only in the list.
addBall(player_ball)
end
function love.update(dt) -- function love2d calls at regular intervals
-- You're supposed to update your game world (positions of objects, etc)
-- in this function. It's called at mostly regular intervals - the
-- argument the function is called with is the number of seconds (usually
-- around 0.01666 for 60fps) since the last time the function was called.
-- This is called "delta time", or the difference in time - usually
-- shortened to dt.
updateBalls(dt)
updateBullets(dt)
end
function love.draw() -- function love2d calls every time it's ready to draw
-- the game
width, height = love.graphics.getDimensions(); -- get the size of the
-- window
local function draw_center(object, x, y)
love.graphics.draw(object,
x - object:getWidth() / 2,
y - object:getHeight() / 2)
end
drawBalls()
drawBullets()
end
-- two new callbacks. love.keypressed and love.keyreleased
-- https://love2d.org/wiki/love.keypressed
-- https://love2d.org/wiki/love.keyreleased
-- For convenience we've moved player-ball-specific stuff into player_ball
-- Apparently I'm running a slightly older version of love2d (0.9.1)
-- so the 'scancode' argument is missing.
-- If you're on 0.10.0 or later, you'll want
-- function love.keypressed(key, scancode, isrepeat)
function love.keypressed(key, isrepeat) -- we'll only use key for now
-- anything other than nil is true
if player_ball.keys[key] then
-- if the key is registered by the player ball then
-- call the player ball's input function with the new key
-- and set pressed_released to "pressed" (because this is
-- love.keypressed)
player_ball:input(player_ball.keys[key], "pressed")
end
end
function love.keyreleased(key, isrepeat) -- we'll only use key for now
if player_ball.keys[key] then
player_ball:input(player_ball.keys[key], "released")
end
end
-- again, i'm on love 0.9.1
-- you'll want love.mousepressed(x, y, button, istouch) if this doesn't work
function love.mousepressed(x, y, button)
local xDiff, yDiff =
x - player_ball.x,
y - player_ball.y
local angle = math.atan2(yDiff, xDiff)
makeAndAddBullet(player_ball.x, player_ball.y, angle)
love.audio.play(sounds.bullet_fire)
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment