Hi and welcome to the forums!
So, a quick comment about the CS50 courses... yes, they are badly designed in my opinion as well, and i personally would recommend against using that as someone's first project(s).
What i can recommend, however, is both Sheepolution's tutorials, in written and video forms, here:
https://sheepolution.com/learn
I could also recommend the "PiL", or Programming in Lua" book, which is freely available online:
https://www.lua.org/pil/contents.html
The caveat with PiL though is that the online version is for the very first lua version, 5.0, which, compared to what Löve uses (something called luaJIT, that's most similar to lua 5.1), there are a lot of outdated concepts in it, like the module keyword, and a few others; most of it is still useable though, and could be used as a quick guide to the very basics... of course, you guys won't need everything in there.
Also, understanding others' code is always harder than understanding the language, in my opinion, no suprises there. So, instead of writing code specific to the course's expectations, let me just try helping with how i would do a simple breakout game, and hope it'll be helpful instead of more confusing.
No menus, no GUI elements, no push library, no nothing; just how the game should behave.
So, for breakout, with a few power-ups, i would store data about the paddle, its horizontal position, maybe its length; for the ball(s), position on both axes, size, speed, and the heading angle. let's say we'd have two power-ups, multi-ball and enlarge-paddle; the latter is simple, because it just needs to modify the size of the paddle; multi-ball needs to duplicate the balls, and probably mirror the angle on the vertical axis so that they go separate ways. With that, a basic layout can be coded:
Code: Select all
-- Your paddle
-- Using tables to store more complicated data structures (only option in lua, really)
local paddle = {
-- your choice whether x and y represent the top-left corner of the paddle, or the center
-- i"ll go with center
x = 0,
y = 0,
length = 0,
multiplier = 1.0
}
-- The list of balls you can have
local balls = {}
-- This function creates one ball at a given coordinate
function newBall(x, y, s, a, z)
-- Create a new instance
local ball = {
-- something or default is a shorthand you can use in lua to make something optional and give a default value
-- again, your choice whether x and y represent the top-left corner of the paddle, or the center
-- since this is a perfect circle, i'd go with the center
x = x or 0,
y = y or 0,
speed = s or 0,
angle = a or 0,
size = z or 0,
}
-- Since we're keeping tabs on all balls, instead of returning it, we put it at the end of our balls table.
table.insert(balls, ball) -- balls[#balls+1] = ball would have worked as well, # returns the number of elements in a sequential table.
end
-- Two Power-ups
-- This applies the enlarge-paddle powerup, doubling its size
function enlargePaddle()
-- Let's say that 4x is the largest you can make your paddle, so this can be applied, at most, twice.
if paddle.multiplier < 4.0 then
paddle.multiplier = paddle.multiplier * 2.0
end
end
-- This applies the multi-ball powerup, duplicating ALL balls in play
function multiBall()
-- We need to loop through the list of balls we have currently
-- This code will put new balls at the end of the list, but the loop will stop at the last original ball, due to
-- us using an explicit numeric loop.
for i = 1, #balls do
-- Let's create a few temporary variables to manipulate
-- We can assign to all of these at once, lua allows that
local x, y, angle = balls[i].x, balls[i].y, balls[i].angle
-- Let's modify the angle of the ball, only on the horizontal axis... this takes some trigonometrical knowledge
--angle = math.pi - angle
angle = math.atan2(math.sin(angle), -math.cos(angle))
-- Create a temporary variable to store the new ball's data.
local ball = newBall(x, y, balls[i].speed, angle, balls[i].size)
-- Add it to the end of the list
table.insert(balls, ball)
end
end
-- Input - I'm using the mousemoved callback, because it makes most sense.
function love.mousemoved(x, y, dx, dy)
-- Simplest solution, tie the paddle's horizontal position to the mouse's, this works out since the paddle's position is
-- the center of it
paddle.x = x
-- Let's prevent it from going out of bounds though
-- Also, let's define a neat helper so we don't need to type that much
local halflength = (paddle.length * paddle.multiplier / 2)
if paddle.x - halflength < 0 then
-- went off to the left
paddle.x = 0 + halflength
elseif paddle.x + halflength > love.graphics.getWidth() then
-- went off to the right
paddle.x = love.graphics.getWidth() - halflength
end
end
-- Logic, keeping things simple, just updating all the balls and minimal collision detection with the corners of the window,
-- the balls themselves don't collide with each other.
function love.update(dt)
-- Iterate over all balls, and apply the speed and angle to their positions
for i, v in ipairs(balls) do
-- This is a neat iterator loop, gives you back the object itself in the v variable
-- Let's calculate how much we need to modify the coordinates of the current ball
-- Again, trigonometry knowledge, or a quick google search on converting between cartesian and polar coordinates
local dx = v.speed * math.cos(v.angle)
local dy = v.speed * math.sin(v.angle)
-- Apply the difference (delta); we also multiply with delta-time so that the movement is framerate-independent
v.x = v.x + dx * dt
v.y = v.y + dy * dt
-- Check boundaries; lower-boundary erases the ball
local radius = v.size / 2
-- Not a completely correct implementation, but it works
if v.x - radius < 0 then
v.x = 0 + radius
v.angle = math.atan2(math.sin(v.angle), -math.cos(v.angle))
elseif v.x + radius > love.graphics.getWidth() then
v.x = love.graphics.getWidth() - v.size / 2
v.angle = math.atan2(math.sin(v.angle), -math.cos(v.angle))
end
-- Same for the other axis
if v.y - radius < 0 then
v.y = 0 + radius
v.angle = math.atan2(-math.sin(v.angle), math.cos(v.angle))
elseif v.y - radius > love.graphics.getHeight() then
-- This is different, we want to remove the ball
-- However, due to how iteration works, we need to do the removal after
-- all balls have been processed... alternatively we could have iterated backwards,
-- that would have worked... so, a hack is needed this way, let's define a field:
v.deleteme = true
end
-- Check collision between ball and paddle
local halflength = (paddle.length * paddle.multiplier / 2)
-- Here, we need to adjust the angle based on where the ball hit the paddle
-- so if ball's edges are horizontally inside the paddle's length, and if the ball is in the paddle,
-- or went through it, we register a hit.
if v.x - radius > paddle.x - halflength and v.x + radius < paddle.x + halflength and
v.y + radius > paddle.y - 5 then
-- Create the new angle for the ball, also modify its speed
-- We map the horizontal angle relative to the paddle like so: [-1 .. 0 .. 1]
local normal = (v.x - paddle.x) / (halflength) -- should result in a range of [-1,1] exactly, that we need
v.angle = math.atan2(-math.sin(v.angle), normal)
-- let's speed up the ball near the edges; we can reuse the normal we calculated above,
-- although we don't care about the direction in this case, so we take the absolute value, or magnitude
v.speed = v.speed * (math.abs(normal) /2 +.5)
-- [0,1] -> [0,.5] -> [1,1.5] so the edges boost speed by 50%, and the center stays at 100% speed.
end
end
-- Due to our hack, another loop, and we use table.remove to remove balls that need to be deleted;
-- table.remove reorders the table so it fills the gaps, which we need.
for i=#balls, 1, -1 do
if balls[i].deleteme then
table.remove(balls, i)
end
end
-- If there are no more balls in play, spawn a new one (also, probably reset score and stuff)
if #balls == 0 then
local angle = math.atan2(-love.math.random(), love.math.random()*2-1)
newBall(paddle.x, paddle.y - 20, 200, angle, 5)
end
end
-- Draw out the elements
function love.draw()
local halflength = (paddle.length * paddle.multiplier / 2)
love.graphics.rectangle('fill', paddle.x - halflength, paddle.y - 5, halflength*2, 10)
for i,v in ipairs(balls) do
love.graphics.circle('fill', v.x, v.y, v.size)
-- Debug stuff
--love.graphics.arc('line',paddle.x,paddle.y,paddle.length*paddle.multiplier,v.angle,math.atan2(-math.sin(v.angle), (v.x - paddle.x) / (halflength * 2)))
--love.graphics.arc('fill',v.x,v.y,v.speed*10,v.angle+0.01,v.angle-0.01)
--love.graphics.print('speed' .. v.speed, 0, 36)
--love.graphics.print('angle' .. v.angle, 0, 48)
--love.graphics.print('normal' .. (v.x - paddle.x) / (halflength * 2), 0, 60)
end
-- Debug stuff
--love.graphics.print(paddle.x, 0, 0)
--love.graphics.print(paddle.y, 0, 12)
--love.graphics.print(paddle.length, 0, 24)
end
-- Initialize the game
function love.load()
-- Paddle's a global, so it's already there, but we can initialize its position here, since it's full of zeroes.
paddle.x = love.graphics.getWidth() / 2
paddle.y = love.graphics.getHeight() - 10
paddle.length = 50
-- We need to create a ball though: with 100 pixels/second going in a random direction but upwards, and with a size of 5
local angle = math.atan2(-love.math.random(), love.math.random()*2-1)
newBall(paddle.x, paddle.y - 20, 200, angle, 5)
end
-- Testing powerups, press l for making paddle larger, press m for multiball
function love.keypressed(k)
if k == 'l' then
enlargePaddle()
elseif k == 'm' then
multiBall()
end
end
Keep in mind, while the above snippet runs, it can have subtle errors, i did code it in an hour or so; the paddle-ball collision angle is the most suspect, i think.
Further stuff can be done, like adding power-ups to enlarge the balls themselves, and "power-downs", like shrinking the balls, making them go at sonic speeds, or shrinking the paddle itself... also actually spawning the power-ups as in-game items that fall down instead of just cheat-triggering them with keys.
As for bricks, that i didn't include in the above code snippet, the simplest implementation is having a table of them as well, with position being the only two variables in there (sizes should be uniform), that you would check collisions against with the balls in the update function. These can also be expanded with making them have different colors, or storing a number defining how many hits it takes for the brick to disappear, with it being removed when it reaches 0, etc.