Last active
February 11, 2016 11:05
-
-
Save selenologist/6d710b1eaf051aafb89f to your computer and use it in GitHub Desktop.
Love2d Tutorial for Luke
This file contains 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
-- 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