Help with multiball breaklout for the course cs50 intro to Game Development

General discussion about LÖVE, Lua, game development, puns, and unicorns.
Post Reply
tekno_boy
Prole
Posts: 2
Joined: Sun May 10, 2020 11:05 am

Help with multiball breaklout for the course cs50 intro to Game Development

Post by tekno_boy »

My 13-year-old son is doing a course in Lua (love 2d). The course is terrible in that it doesn’t give any course material nor does it really explain anything.

The course is called:
cs50 intro to Game Development

It’s like you need to complete the course, in order to know how to complete the course.

My son is finding Lua hard because there's a lot to learn before you can change someone else’s code. So many functions and calls, classes, states etc. He has completed two assignments and is stuck on the third. To make a powerup for a game (which he has done with collision detection too) and introduce a second ball.

We are stuck creating a second ball and processing it.

We already figured out that the best way to handle this is probably with a table, so we can create a code loop for collision and render aspects of the ball to apply to multiple balls whose data we store in a table. But he is struggling with the structure of how the ball data is handled (variables are created with data passed to a function, but he and I don’t understand the process of how those these variables are set up and so we can’t create the table needed for the power-up). I wondered if any kind person would be prepared to help us through it?

Here is a tiny snippet of the collision code for the ball:

Code: Select all

if self.ball.x + 2 < brick.x and self.ball.dx > 0 then

 -- flip x velocity and reset position outside of brick
self.ball.dx = -self.ball.dx
self.ball.x = brick.x - 8
What we don't know how to do is create a table variable so that we can do something like:

Code: Select all

for i= 1 ,no_of_balls do
and then adjust all code to process our multi-ball table, like this (but we are unsure of syntax).

Code: Select all

if self.ballList[i] + 2 < brick.x and self.ballList[i+3] > 0 then  ....
So that the loop will process all ball instances.

The main problems we face are: We don't understand all the data handled by the self.ball variable.

We don't quite know how to set up the table with the same data as self.ball
(something like)

Code: Select all

self.ballList = {}
table.update (self.ballList, self.ball.x, self.ball.y, self.ball.dx, self.ball2.x, self.ball2.y, self.ball2.dx)
Sorry it's IS a general lack of understanding about LUA and how variable and tables work, but we are trying hard to get to the point where this is clearer (but the course does not help at all).

FYI. The ball parameters are passed into self.ball in this manner:

Code: Select all

function PlayState:enter(params)
self.ball = params.ball
We don't even really understand how that function is called and what data is passed through params, it's a severe lack of understanding that the course fails to help with, through resources, course materials or support.

We have tried very hard to both upskill enough to solve this but we are too noob.

I found this on Reddit which talk about the exact problem and the solution (similar to the way we want to implement it, but we just need some guidance if anyone can).
https://www.reddit.com/r/cs50/comments/ ... akout_for/

Any help every so greatly appreciated.
User avatar
4vZEROv
Party member
Posts: 126
Joined: Wed Jan 02, 2019 8:44 pm

Re: Help with multiball breaklout for the course cs50 intro to Game Development

Post by 4vZEROv »

Lua doesn't have class/states/instances/etc.. Lua only have values.
A table is a value of type 'table'.
A function is a value of type 'function'.
A table can contain any value, so a table can contain any other table.

To explain 'self' :

Code: Select all

local ball = {}
ball.x = 0
-- To declare a function value belonging to the ball table you can do:
ball.move = function(b) b.x = b.x + 10 end 
-- or 
function ball.move(b) b.x = b.x + 10 end
-- or
function ball.move(self) self.x = self.x + 10 end
-- or
function ball:move() self.x = self.x + 10 end -- here ':' is syntactic sugar that pass an argument called 'self' as the first argument

-- to call the move.ball function with the ball as parameter you can do :
ball.move(ball) 
-- or
ball:move() -- here ':' is syntactic sugar that pass the table containing the function as the first argument of the function
For the collision, I believe your code is something like that:

Code: Select all

Playstate.ball = {}
Playstate.bricks = {}
Playstate.walls= {}

function Playstate:new()
	for i = 0, x do 
		local brick = {}
		table.insert(self.bricks, brick)
	end
end

function Playstate:update(dt)
	-- collision code
	for k, brick in self.bricks do 
		if self.ball collide with brick then 
			remove brick and change self.ball direction
		 end
	end
	for k, wall in self.walls do 
		if self.ball collide with wall then 
			change self.ball direction
		 end
	end
end
You can change it to something like that :

Code: Select all

Playstate.balls = {}
Playstate.bricks = {}

function Playstate:new()
	for i = 0, x do 
		local brick = {}
		table.insert(self.bricks, brick)
	end
	local ball = {}
	table.insert(balls, ball)
end

function Playstate:update(dt)

	for k, ball in pairs(self.balls) do 
		for k,brick in pairs(self.bricks) do 
			if ball collide with brick then 
				remove brick and change ball direction
			 end
		end
		for k, wall in pairs(self.walls) do 
			if ball collide with wall then 
				change sball direction
			 end
		end
	end
	
end
User avatar
zorg
Party member
Posts: 3444
Joined: Thu Dec 13, 2012 2:55 pm
Location: Absurdistan, Hungary
Contact:

Re: Help with multiball breaklout for the course cs50 intro to Game Development

Post by zorg »

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. :3

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. :3

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.
Me and my stuff :3True Neutral Aspirant. Why, yes, i do indeed enjoy sarcastically correcting others when they make the most blatant of spelling mistakes. No bullying or trolling the innocent tho.
User avatar
pgimeno
Party member
Posts: 3549
Joined: Sun Oct 18, 2015 2:58 pm

Re: Help with multiball breaklout for the course cs50 intro to Game Development

Post by pgimeno »

zorg wrote: Sun May 10, 2020 7:36 pm 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,
I think "a lot" is an overstatement. There are very few outdated concepts in it. 'string.gfind', 'table.setn', 'table.getn' and 'module' are probably the main ones. The rest are too minor, up to the point that I doubt the PIL mentions them.

Anyway, here is the Lua manual, which is the technical reference of the language; in particular this link points to the section that speaks about the differences between Lua 5.0 and 5.1: https://www.lua.org/manual/5.1/manual.html#7

4vZEROv wrote: Sun May 10, 2020 6:14 pm A table can contain any value,
Except one: the value nil.
tekno_boy
Prole
Posts: 2
Joined: Sun May 10, 2020 11:05 am

Re: Help with multiball breaklout for the course cs50 intro to Game Development

Post by tekno_boy »

I just want to take a moment to say two things. Firstly thanks for the replies, suggestions and links to resources. We will take some time now to study and try and understand enough to move forward, and secondly, thank you for making us feel welcome!!! :awesome:

I'll let you know how we get on!
Post Reply

Who is online

Users browsing this forum: Bing [Bot] and 96 guests