COLTON OGDEN: All right, I think we're alive now.
Sorry for the delay.
This is CS50 on Twitch.
My name is Colton Ogden and today we're going
to continue the stream that we did last week on Friday where
we started implementing the game Snake from scratch.
Recall, we ended up putting something together a little bit like this.
So we had a little-- oh, sorry.
I forgot to switch to actual machine over there.
There we go.
We had something that look a little bit like this.
So almost like a little Etch A Sketch going but not exactly Snake
in the sense that most people I think recognize Snake.
But a lot of the pieces are still there.
You know, we went from having a cube that moved across the screen
to having a cube that sort of moved discreetly across the screen
in like a grid sort of way.
Talking about how we can actually divide our game space up into
a grid, which allows us to then start transitioning
into games like Rogue Lights and other games that are more tile based,
which we could definitely get into in the future.
And then we started talking about a Snake data structure
and we talked about some basic drawing routines and Love 2D
and all sorts of other stuff.
Won't go too much into the details because the VOD will be up
and this and the other video will also be going on YouTube.
But today, a couple of the problems that we want to solve
are one, make sure that when we do eat an apple in our game,
rather than our snake kind of just drawing an infinite line,
we want to actually get rid of the tail.
We want to pop the tail while continuing to draw, you know,
the head is just moving in another direction.
And another big thing that we want to bite off today
is making sure that when we collide with other parts of the snake
that we trigger a game over, as I would have just done right there.
And those are two sort of big pieces that we're going to bite off
and they're actually not terribly difficult.
I spent a little bit time over the weekend actually thinking
through the problem and I was a little bit tired last time
but we should be good today.
And then if we do have time, which I think we will--
we're going to be on for a three hour stream today.
We'll take a look at having an intro screen.
Like a-- you know, the word snake, press enter
to start because right now you kind of just
get right into the action, which is a little bit
tricky to jump just right into.
And then also a game over screen so that when we do intersect with the snake,
the game should stop.
Maybe transition us to another screen with some text on it
that says, "you've got a game over" and then display your score.
[? Sue Leith ?] says hello.
Hello, [? Sue Leith. ?] Good to see you.
[? Bavick Night ?] says hey, how's it going.
[? Bavick Night, ?] good to see you again.
And [? Sue Leith ?] asks, will this be re uploaded?
Yes.
So last week's video and this video will be up--
uploaded to YouTube.
They should be uploaded today, tonight, if not tomorrow morning.
And the VOD from last week is actually-- should
be accessible on this Twitch account.
The get hub for the actual code itself is on this URL.
So, github.com/coltonoscopy/snake50.
And you'll be able to see the main.lua file that's in there just by itself.
And recall main.lua is the entry point for LOVE 2D,
which is the framework that we used last Friday.
If unfamiliar, if you're just joining the stream for the first time, LOVE 2D
is the framework that we're using to do all of the game programming.
All the graphics, all the windowing, all the input, all that stuff.
So you can go to love2d.org.
By default, it should take you to a page that looks like this
and then you can download the version of ProCreate for your operating system.
So I'm on a Mac, I'm on Mojave, so you can just download here.
If you want to download other versions and older versions,
you have an option to do so here.
[? Bavick Night ?] says glad to be here.
Glad to have you, with us [? Bavick Night. ?] Thanks for joining.
All right.
So let's dive right in.
Briefly I will just summarize the code that we have.
So we have a constant table up here.
So just a set of constants that just basically say,
oh what's the window width and height?
What's the tile size?
Tile size being of our grid size, so we could think of it that way as well.
The number of tiles on the X and Y. And then a few constants
to represent particular slots in the grid
whether a tile is empty, whether it's a snake head, whether it's a snake body,
whether it's an apple.
Those are the main sort of game play mechanics at play.
And then lastly, a constant for the time in seconds
that should elapse before the snake actually moves in an individual grid
tile in the game.
We have a few other variables here.
So a font, a score, which is important, the actual grid for our tiles.
This is the data structure that actually holds the zeros,
one's, two's and three's that represent what's going on in our game world.
The snake X and the snake Y, which is where the snake's head is located,
whether our snake-- what direction or snake is moving in.
Which, this should just probably be called local snake direction
equals right.
But we've already called it snake moving, so we'll just keep it that way.
And then a snake timer because remember we
do have to keep track of how much time has actually
elapsed over the course of all the frames
before we end up moving our snake.
So we basically check snake timer against snake speed.
If snake timer is greater than snake speed,
then we should move to the next location.
And then here, we do need a data structure to represent our snake.
So because we need to make sure that we pop our tail off of the snake
whenever we move, we need to keep track of all the nodes
that we add to the snake as time goes on.
[? Sue Leith ?] says, where did you learn all of this?
Is there a Love 2D manual?
So, yeah.
Actually, Love 2D has some excellent documentation.
So there's a very basic set of tutorials just on the main page.
So here you can see there's some examples of basic application
where you draw text, draw on image, play a sound.
There are some full games you can take a look at.
I believe some of these have source code and some of these
are actually commercial Steam projects, which is cool.
Which goes to show you that you can actually
use this to make a published game on Steam if that's
something that's of interest to you.
The actual documentation is here.
So at the very bottom right, you can just click on any of those modules
and that will take you to the page for that.
[? CPU Intensive ?] says, tall hair.
Yeah, I know.
I need a haircut super badly, like really bad.
But if you go to love2d.org/wiki/love is where you can actually see
the documentation.
And there's a bunch of different name spaces here.
Love, love.audio, love.data, love.event.
We're going to be using pretty much just love.graphics.
We use love.window, as well, and then the core functions
that you can access in love.
So love.load, love.update, love.draw.
These are the functions that make up our game loop.
If you're familiar, we talked about this last week.
Every frame, which is usually 1/60 of a second.
Love2D will execute some code in a function
called love.update and love.draw each frame, update happening before draw.
And at the very start of your program it'll call a function called love.load.
Love.load sort of sets up everything, if you
have some variables that need to be initialized
or some resources that need to be loaded.
Optionally, as we did up here, you could just
put them at the very top of your script, and they'll all execute
in advance in much the same way.
But as is sort of tradition, you'll see a lot of these functions
like love.window.setTitle, love.graphics.setFont,
love.window.setMode, a lot of these functions that sort of set up the game,
set up the state machine that is Love2D, in a sense.
Those all exist here, as does setting the random number
generator, which we did last week.
And maybe initializing some data, and such.
[? JPguy ?] says hello again.
Hello, JP.
Good to see you.
I wanted to have the Twitch chat enabled in today's stream,
but we're having a little bit of difficulties with Streamlabs.
So we're going to try again.
Next week we should actually have the embedded chat in the final video,
so that folks watching online on YouTube or what
not after the fact can see what people in the chat saying.
So looking forward to--
hopefully tomorrow.
So tomorrow we're having another stream with Kareem Zidane
and if anybody's familiar.
He's going to be doing the Git and GitHub stream.
Elias says, "Hello, again from Morocco."
Hello, Elias.
Good to see you, again.
Thanks for coming in.
Just to cover a little bit more of what we have going on
in our love.keypressed function.
It takes a key.
Remember that we were testing for input.
So if we pressed left, right, up, or down,
we should set our moving variable to left, right, up, or down accordingly.
And then we do a check in our update function.
So remember, update executes once every 1/60 of a second,
approximately every frame.
And if we're moving in any of these given directions,
then we should increment or decrement our snakeX and snakeY variables.
Which, remember, that's where our head is going to be,
and that's sort of where its index is in the grid.
Originally, that was our pixel value, so we were measuring our square in pixels.
But remember, that was kind of continuous movement.
It wasn't actually adhering to a grid.
So a little bit trickier to do collision detection that way.
So we divided it up into a grid and we make
sure to move our snake in increments of 32 pixels, instead of just one pixel.
JP says, "How long have you been streaming?
I just got home from work."
Just for a couple of minutes.
So we're just reviewing all of the code that we did last Friday.
JP, I know you were there, so this is all old hat to you.
But in case anybody is watching who needs a refresher,
this sort of is a recap of everything that we did.
And then in drawGrid, we're basically just checking
each tile in a nested loop in our tile grid table, at yx for 1 to MAX_TILES y,
and one to MAX_TILES x on the y and the x-axes.
If it's empty, don't try anything.
If it's apple, draw red.
If it's head, draw a lighter shade of green.
So kind of a cyan green, since we have-- remember, love.graphics has that color,
takes in four variables--
red, green, blue, and alpha.
For a cyan-ish value, it needs to be 1 and 0.5.
1 and 1 here.
G and B both thing 1 would be completely cyan.
But that's all a bit too bright, so we're just going
to make it 0.5 on the blue component.
And then the body itself we're just making kind of a dark green.
So instead of setting G all the way to 1,
we're sitting at the 0.5, which just kind of is halfway
between black and full green, effectively.
And then at our xy, we subtract 1 from the x and the y,
multiply that value by 32, because tables are one index by default.
But coordinate systems are zero indexed, so we need to decrement our xy,
and then multiply the end value by tile size.
That will have the effect of drawing that rectangle,
that square at the appropriate pixel coordinate in our game.
The drawSnake function we ended up actually not using,
so I think I'm just going to delete that.
I don't think we actually need to draw that.
The grid itself is going to handle drawing the snake,
so I'm going to take away the drawSnake call.
I'm going to take away the bit of code down near the drawSnake function,
going to save it.
Initializing the grid is fine.
And then up in our update function was sort
of where we had the last bit of problem-solving,
before we ended the stream at the three-hour mark.
And that was checking for an apple, and then adding a head node
to the data structure and trying to pop the tail off if we do--
if we don't get an apple, rather, we should pop the tail off
of the data structure and set to empty that node in the 2D array,
in the 2D table.
If we don't get an apple on the next grid
index, where our snake head is moving, what we should do is still push--
basically, the algorithm that it's going to be
is we still want to push an element onto the front of the snake.
So basically add a head element.
But then we're going to pop off the tail,
so that it's going to have an effect of shifting the snake one tile in whatever
direction we're moving.
And this will work even if our snake is only one tile large,
because that snake, as soon as we do that, it will have two elements.
So it will have a head and will have a tail with which we can pop.
That tail being the head--
as it just was in the last frame--
at just size one.
And so all of this code is a little bit convoluted,
and doesn't quite get the job done in a fantastic way.
So what I'm going to do is actually-- from like 95 down the line 119,
after we've deleted the other things-- basically,
checking for the apple all the way to the bottom of this--
checking whether the size of the snake tiles table is greater than 1.
I'm just going to delete all of that, and we're
going to think about how we want to solve this problem of getting our snake
to move, and also keep its body size, if it eats more apples.
So if I run this after deleting all that, it should still operate.
Oh, no, it's not.
OK, so it's not actually moving, currently.
Let me just verify which part of that was the update location.
Right, OK.
Because we're not actually updating the head location.
So a couple of things that we want to do.
So the first thing we want to do, when we're moving in our snake world,
is we want to pop a new head location after we've
decided what direction are we moving in, assuming that we've increased--
remember, snakeTimer keeps track of delta time every frame.
And if it exceeds 0.1, which is 1/10 of a second, at that point,
it will have exceeded snake speed.
We can then add a new head element onto the snake.
So what I'm going to do is basically say, push a new head element
onto the snake data structure.
And so what this basically means is I'm going
to do a table.insert, because this is how we
push new elements into a table in Lua.
So our snake data structure-- remember, that's this thing up here,
the snakeTiles right here.
Which, by default, just has one element.
Our snakeX, snakeY.
So what I want to do--
I'm getting a little bit lost here.
OK.
So in our update function-- or approximately line 96,
if you're following along--
into snakeTiles at index 1--
so table.insert can take an optional second parameter,
which is where in the table we want to insert that element.
In this case, I want to insert it at the very front.
So I want to insert it at index 1.
Because remember, tables in Lua are one index.
So 1 is the very first index.
0 normally is the first index in a programming language, but 1 in Lua
is the first index.
So we're going to use number 1.
Sit up just a little bit here.
OK.
So at index 1 at the very front of our snake,
we're going to want to insert the element that
is going to be its head next.
So it's quite simply snakeX and snakeY, which
is what we've just altered up here in this if statement,
this series of if statements.
If we're moving left, right, up, down, increment or decrement
snakeX or snakeY.
So we're going to do that.
So now our data structure has the next head element set in place,
and whatever the head was on the last frame is now one index below that,
if that makes sense.
So, good.
That's easy, that's straightforward.
That doesn't actually influence the view of our application.
So folks familiar with MVC might think of the snake tiles data structure
as the M, the model of our application.
Whereas the grid is the view, the V our application.
So we've updated the model, and now we need
to reflect this change in the view, as well, effectively.
We can think of it in that sort of term.
So before we do that, though, the tile grid itself also
keeps track of where the apple is.
And the tile grid is kind of like a view and a model, in a sense, as well.
So we're not completely, cleanly splitting up our applications
infrastructure in as clean of a way as MVC,
but you can sort of conceptualize it in a similar way.
But basically, what we need to do is we need to push this element.
But then we also need to check to see whether or not
the next element is an apple.
And if it is, then we need to change our behavior.
We can't override that value with snake head just yet,
because then we won't know whether it was actually an apple that we consumed,
or whether we moved into another slot.
Or for that matter, as we'll see later on, whether we've
collided with our own body, which is going
to be an important consideration when we do collision detection.
So let's do that.
So the next step is going to be, basically, check to see
are we eating an apple?
So if tileGrid snakeY snakeX is equal to TILE_APPLE--
and I don't actually need parentheses here.
Something that's a hard habit to break, coming from a more C-like programming
languages, sometimes I still write parentheses in my conditions.
But Python and Lua do not require that in your conditions.
It'll still work, but it's not necessary.
So if it an apple, then what we need to do--
because remember, we looked at this last week.
If we're eating an apple, then we need to basically keep
the element pushed under the stack, and update the view,
and also update our score, and then change the board
to have a new apple somewhere else.
And then we also need to not pop our tail element off of the snake,
because the snake should increase in size.
Therefore, we need to keep the tail, and then
also add a head element, which will have that effect.
Which we looked at last week, when we saw the video.
So if we ate an apple--
remember, last week when we did this, we talked about random number generation
and getting a new random x and y values, in which
we're going to spawn a new apple.
So let's do that.
So I'm going to say local appleX, appleY gets math.random(MAX_TILES_X)
and math.random(MAX_TILES_Y).
Hopefully you can see that.
Might be a little bit small, actually.
Let's increase that size a little bit.
I'm going to take this off.
If the chat can confirm that the text size looks OK on this,
I would appreciate it.
So we've created the new the two new variables
that we're going to need for our new apple.
So what I'm going to do is tileGrid--
first thing's first, score gets score + 1.
And then tileGrid at appleY, appleX is equal to TILE_APPLE.
So now we're going to increase our score--
awesome, cool.
Thanks JP, thanks [? Duwanda. ?]
We're going to increase our score.
We're going to then generate two random variables, our x and y, which
we're going to place a new apple into the grid.
And then we're going to set that grid element at yx to the TILE_APPLE value.
Now, if it's not the case that we ate an apple,
then we need to pop the tail, which will give us the effect of moving forward.
And so this tail popping is sort of conditional on the fact
that we ate an apple to begin with.
So I'm going to go ahead and say local tail
gets snakeTiles at index number of snake tiles, which
will have the effect of indexing into our snakeTiles table
at the element of whatever the size, which will give us
the last element in our table.
And then this tail element should be deleted from the grid.
So this is where we trace our tail from the grid itself.
So our model, remember, is the snakeTiles,
and then our view is the tileGrid.
So each time you move, you're checking whether or not you've eaten an apple,
right?
Correct.
Yeah, we check first, because that's going to determine
whether we pop our tail element.
And we don't want to override that tileGrid element with our head value,
because then we won't know whether we've eaten an apple,
because it just erases that information from the grid permanently.
It overrides it.
So we're going to get our tail element from our snakeTiles data structure.
So this will be our tail, our last element in our snake table.
As soon as we get the tail, what we want to do is update our view.
So tileGrid grid at tail 2, tail 1.
Because, remember, our elements are xy pairs--
sorry, rather-- yeah, in here.
So in our snakeTiles data structure-- so snakeX, snakeY,
in this particular case.
But it'll always be an xy pair.
So every element in here at index 1 will be our x value,
every element in index 2 will be our y value.
There are cleaner ways to do this.
We could have written it something like this, so x = snakeY
and then y = snakeX.
And then we could have done something like snakeTiles.x, snakeTiles.y,
which would be cleaner and a little bit more robust.
But just as a first game example, we're going to kind of take things
a little bit more literally.
And we're going to just use numerical indices, which is the default Love2D
and Lua behavior, rather.
So basically, now you can see that this element at index 2
is going to be our y, and index 1 is going to be our x.
We can set that to TILE_EMPTY.
And then the last thing we need to do is we
need to actually pop that element of our snake data structure.
We need to delete it from the snake.
And so what we can do is do table.remove(snakeTiles).
And so what that does--
by default, table.remove can take an index,
but you can also just give it the table.
And what the result of that is going to be
is just the last element in snakeTiles.
Which is perfect for this use case, because we
want to take the last element from snakeTiles
and remove it from the data structure.
So that's a big chunk consumed here.
The head element is going to get pushed onto the data structure
then we're going to choose whether we eat an apple.
If we did eat an apple, we're going to increase our score,
generate a new apple.
If we didn't we're, going to pop our tail off and erase the tail.
So the next thing that we need to do--
remember, we added a head element, but we haven't actually
adjusted our view, because we needed to check for the apple in our view, right?
So then one thing we need to do here is say tileGrid at snakeY snakeX
should be equal to TILE_SNAKE_HEAD.
Update the view with the next snake head location.
And then we can write some comments here, which just says, otherwise--
oh, if we're eating an apple, increase score and generate new apple.
Otherwise, what we want to do is pop the tail and erase from the grid.
And then update the view with the next snake head location.
If everything has gone according to plan,
this should work as soon, as I hit Command-L here.
So I'm going to go ahead, eat this, and then voila.
Now we have a snake that's moving.
And last week, remember, we had only two elements max,
because our algorithm wasn't quite perfect last week,
and we were sort of special casing it a little bit more than we needed to.
But now, it seems to work just fine.
We have a snake, our score is increasing every time we eat an apple.
And also importantly, our snake body is growing in size
and our tail is popping off as needed, when we move around,
so that it looks like we have this continuous creature in our game space.
So it's looking pretty good.
There's a couple of issues.
So one, you might have just noticed, I can actually move backwards
and we get some graphical bugs, which isn't exactly behavior that we want.
And then another important thing is that if we collide with ourself,
it doesn't actually trigger a game over.
And those two issues are sort of compounded with one another.
And we're not actually drawing the body as a different color, which
is one of the things we wanted to do.
That bit is actually fairly easy, if I'm not mistaken.
By the way, in the chat, [? Bavick Night ?] says, "Yas."
Thanks, [? Bavick Night. ?] Appreciate the support.
And JP, "Damn, that looks good."
Thank you.
I agree.
I'm pretty happy.
Last stream it was pretty rough.
Had a little bit of a brain fart.
But the algorithm is fairly straightforward.
It's fairly simple.
Once you break it down and make it a little bit cleaner,
it all sort of makes sense.
Let's bite off another part.
So we have the snake drawing as one color,
but we'd like the snake head to be a different color from the body.
So this should be as simple as tileGrid priorHeadY priorHeadX
this needs to be in a condition, because it could be the case
that we have only one--
oh wait, no.
Is that the case, though?
Because this is always going to run after we've--
oh, because it is outside of the condition of eating an apple,
we do need this to be special cased.
So if it's the case that our snake is greater than 1 size--
if our snake is greater than 1 tile long,
we need to set the prior head value to a body value.
And TILE_SNAKE_BODY.
And so what this will do is, assuming that our snake is at least
2 units long, when we move forward--
remember, that we're always writing a snake head value to the next tile,
but we want to write a snake body tile to whatever the head was
on the last frame, so that it looks like our snake has a body.
It's disjointed from the rest of it.
[? Arrowman123 ?] says, "Hello.
It is really nice that you started this on Twitch."
Thanks.
Yeah, I'm super looking forward to seeing where it goes.
I like the fact that we can sort of have a conversation and talk back and forth,
and maybe people can suggest techniques or ideas.
Somebody suggested an idea, who was on stream last time, for a new game
that we'll be implementing on Friday called
Concentration, which is like a card matching game.
So I'm kind of looking forward to seeing where that goes.
But OK.
So assuming that I did this appropriately,
if our snake is greater than one tile long,
and I run-- so currently, it's just one tile long.
If I eat this apple, now, our snake actually
has a body segment that's a different color.
And it will increase in size, but our head will always
be the color of the head element.
So now, we can better keep track what direction we're going.
And not that it's terribly difficult to see normally,
but just a slight little change that has a sort of an accessibility feature,
if you will.
OK.
Pretty basic, after all, despite last week kind
of fizzing out a little bit at the end.
But now we have a few new features to think about.
Excuse me.
So first feature that we should probably think about
is triggering the actual game over.
Because right now, we can collect apples forever,
but we're never going to lose the game.
So the game isn't really--
I guess it kind of is a game, but it's not really a full game,
because we're missing that piece of the puzzle that actually lets us lose.
Kind of important, because that's how you can measure your skill.
I've got to stay hydrated a little bit.
So yeah, next piece of the puzzle.
How can I detect whether the snake--
or rather, most and more importantly, the snake head--
is going to be eating a piece of itself?
Bella [INAUDIBLE] says, "Hi, Colton.
Happy to be here today."
Thanks for joining, Bella.
I appreciate that you're here.
We just ended up fixing up our snake game, so that now we can run it.
And unlike last week, where we ended on it basically being a glorified Etch A
Sketch, with the snake not deleting itself,
now our snake can grow and move around as an actual entity in our game world.
So a very satisfying.
We've come a long way.
And so let's decide how we're going to do this.
So I'm thinking we can probably do something up here,
where we check for an apple.
And maybe before we check if it's an apple,
we can probably just do something as simple as if tileGrid snakeY at snakeX
is equal to TILE_SNAKE_BODY--
because it should never be able to move into another tileSnake head--
then I want to do some sort of game over logic.
Maybe I want to set some game over value to true.
And then if the game over value is true, then I
should probably change how I'm rendering the game, probably down in my draw
function, instead of drawing the grid.
I can probably say if not gameOver, then draw the grid.
Else drawGameOver maybe.
End.
And then these two, the score and the other print statement,
should probably go in here with this.
So print score.
So drawGameOver basically is going to be the function that
has this sort of different view of the game
that we want to take into consideration.
So drawGameOver.
I'm thinking something simple probably.
Maybe something like a really large font.
Just game over.
And then below that, in a smaller font, maybe your score was 8.
And then if you press Enter on that screen,
maybe we should be able to restart the game,
and then continue to be able to press Escape to quit the game.
All right.
So fairly straightforward.
So let's go over to back to our update function.
And remember, we have to set this gameOver variable to true.
And then we need this gameOver variable to be stored somewhere,
so I'm just going to store it up here with our other snake--
actually, I'm going to start it with our score.
So local gameOver = false.
It should be false by default.
[ALARM SOUNDS]
You might be able to hear that police siren.
Now, remake it in Scratch, says JP.
Yeah, actually, that wouldn't be a bad demonstration
maybe for folks taking CS50 for the first time.
I'll have to take a look at that.
I'm guessing it was you who said that you tried to make the snake dynamically
resize in Scratch, right?
And you said you were having a little bit of difficulty there?
[INAUDIBLE]
Yeah.
A snake beginner game series might actually be compelling.
Let's see.
OK, so line 99.
So if we do intersect the TILE_SNAKE_BODY tile,
if our snake head intersects, we can set gameOver to true,
and then we can sort of make this an else if.
Because we don't want to check apple if that happens,
we basically want to change our flow of our logic
from going to check for an apple to just skipping that altogether, and skipping
this--
better yet, we can probably just return out of this, correct?
If my logic is right--
yeah, I think we can just return.
So basically, cut this function short altogether,
not actually worry about any of this other stuff.
We don't need to do any of this.
We need to update our timer or anything.
So what we can do--
would we want to update the head to reflect the collision?
No, because we're probably going to transition right
into the game over screen.
Although, what we could do to show that we've got a game over is--
yeah, we could have it so that we have our game over text above our grids,
so that we can see where we died.
Because that way we can at least see what
our whole snake looked like before the game actually cut short.
So I think I want to do it that way.
So maybe I don't want to return off of this.
I just want to set this gameOver flag to true.
I will want to update this TILE_SNAKE_HEAD,
and then I kind of want to wrap this whole entire thing in a condition.
So if not gameOver then--
and I'm going to indent all of this code.
So all of this is only going to execute if we're not in a gameOver.
So if not gameOver--
so if it's not the case that we are in a game
over, which we trigger by this intersecting with a snake body
tile, or head intersecting with the snake body.
Do all of that stuff, else, if we are in a game over--
actually, no.
We're not going to need to do anything, in that case.
Our update's just not going to do anything
at all when we get into a game over.
And what we're going to do to break out of a game over
is we're actually going to add another condition
in our love.keypressed function.
So I can say if key == enter or key == return and gameOver then--
or rather, we'll do it this way.
If gameOver then if key == enter or key == return then--
and the or equal to return is a Mac thing.
So Macs don't have an enter key, theirs is called Return.
Or, I think Enter is Shift-Return.
But by default, people are going to hit Return,
and on Windows it's going to be Enter.
So you want to mix those two together.
I kind of want to initialize everything again, so I can do initializeGrid,
and then I can set snakeTiles--
or rather, what it can do is I can say snakeX, snakeY = 1, 1.
And then snakeTiles equal to a table with snakeX, snakeY.
So what this basically does is it initializes our snake.
I guess I can take this out, call a function called initializeSnake.
Come down here at the very bottom to function initializeSnake().
Paste those lines of code in there.
So now we have basically just setting the snakeX and snakeY to 1, 1.
And then also creating the snakeTiles table,
which has that first element with snakeX and snakeY.
And then back up at the very top--
oh.
I guess snakeMoving equals right.
I guess I can do that, as well.
So snakeMoving = 'right'.
And then I can initializeSnake() here, which I don't need to--
I guess it's kind of superfluous at this point, but just for consistency.
That should work.
It's not necessary.
I could just declare all these variables as local snakeTiles, local snakeX
snakeY, local snakeMoving, but it at least
gives us a clearer sense of what's going on,
when we're looking at our load function.
We could say, oh initializeGrid, initializeSnake, what does that mean?
Go to initializeSnake.
It means set the snakeY to 1, 1, set the moving to right,
and set the snakeTiles equal to snakeX, snakeY.
And then initializeGrid.
Remember, all initializeGrid does is do a nested loop, where
it basically creates a bunch of empty inner tables for our rows,
and then fills those for each column with a 0.
So just an empty tile, while also generating a new apple
somewhere completely random.
So that's all done.
So if gameOver, and we press Enter or Return,
we can initializeGrid and initializeSnake again.
And then score should be initialized back to 0.
So we basically want to complete fresh restart to the game.
So that's all pretty straightforward.
The last thing that we should do, and it looks like we did do already, was--
oh, the drawGameOver function.
Let's do that.
Function drawGameOver().
And actually, what we're going to do is we're just going to if gameOver() then
drawGameOver().
Because what we want to do--
actually, rather, sorry, this goes afterwards.
So we're going to draw the grid and the score.
We're going to draw the grid and the score, no matter what.
But if it's the case that we are--
sorry, gameOver is not a function, gameOver is a variable.
If it is a gameOver, then we're going to draw the game over layout
on top of that.
And the drawGameOver function is going to be something as simple
as love.graphics.printf().
So there's a printf function, not just a print function.
We're going to say game over.
We're going to take a x and y.
This is going to be a little bit strange,
this is how the printf function works.
We're going to say at 0, and then the window height divided by 2 minus 32--
actually, what should it be?
Minus 64.
And then we're going to specify an alignment width, which basically says,
within this amount of text, I want you to format it
based on the format specifier that we're going to say as the last argument.
So I'm going to say WINDOW_WIDTH.
So it's going to basically center it within the window width, starting at 0
all the way until WINDOW_WIDTH.
It's going to center it.
And then I want to specify that it's centered.
So that last parameter, that last argument to the function
is a string that can be left, right, or center.
And so if it's left, within the bounds of WINDOW_WIDTH,
it will basically just draw it at that xy.
If you say right, it'll basically pad on the left side-- or rather,
on the left side of your string, it'll pad it with whitespace
until the end of the that WINDOW_WIDTH variable, the WINDOW_WIDTH
size that we specify here.
So you're basically specifying a left and a right, kind of,
with 0 and WINDOW_WIDTH.
It's more like a left, and then a number of pixels after that size.
So basically, within the size of the window,
I want to center this game over text.
And then I want to also do the same thing with Press Enter
to Restart at 0 WINDOW_WIDTH.
And then I want to do WINDOW_WIDTH plus--
sorry, WINDOW_HEIGHT divided by 2 plus 64, and then WINDOW--
plus 96, actually.
And then WINDOW_WIDTH and then 'center'.
And so what that's going to do is it's going to draw this string a little bit
below.
This is what the y value here-- remember, x is 0,
and then y at WINDOW_HEIGHT divided by 2 plus 96.
That's going to draw the second string of text
a little bit lower than the gameOver string.
Both of these strings are not going to be large enough,
so I want to make a large font.
Actually, a really large font.
I want to make a huge font.
So that's what we can do here.
So a local hugeFont equals love.graphics.newFont.
And we're going to make this one 128 pixels.
Which is why I did the minus 64 pixels earlier for drawing it on the y,
because we want to shift it up 1/2 the size of the text,
so that it's perfectly centered vertically on our window.
So I'm going to do that.
So hugeFont gets that size, and then when
I draw my gameOver, which is down here, I
want to do love.graphics.setFont to hugeFont.
And then I want love.graphics.setFont to largeFont here.
So two separate sizes.
I want the game over to be really big, and I want
the press enter to restart to be big.
Like the same size as our score, but not as big.
So I'm going to save.
It I'm going to run it.
Let's see if this is working.
So remember, this should now--
I'm going to get my snake up to maybe five or six tiles,
and then I'm going to try and intersect with myself.
Oh, it worked.
So game over.
Press Enter to restart.
It's a little bit low, visibly, that we can see in the game view there,
but that's because my window is a little bit shifted down, because my monitor
resolution is a little bit funky.
But this should be approximately centered in the game view.
Now, press Enter.
Oh, right.
One other important thing.
When you press Enter, make sure--
where is it?
In our key pressed--
anybody want to predict what I messed up here in this condition here?
What did I forget to do?
I know you guys got this one.
This is an easy one.
Simple mistake that I made.
[? JPGuy ?] says, "Whoops."
[? JPGuy ?] says, "Woohoo."
Yeah, it's exciting.
A little bit of a mess up there.
Let's see if anybody's got it.
Well, the key is that I forgot to set gameOver back to false.
So now, whenever press Enter to restart, I instantly
go into a gameOver equals false.
Is there a clear function?
love.graphics.clear will work for that case,
but it will just draw everything again, because in our draw function,
we have this loop.
We basically have it saying draw the grid every time.
The initializeGrid function is here.
So what that should do is just set the grid equal to emptiness.
So let's try that again.
Let's make sure I'm bug-free, beyond this in another way, hopefully.
So if I move over here--
OK.
Enter to start.
And then, oh-- so it actually didn't reinitialize our grids.
Now we have our old snake there, which isn't what we want, ideally.
But I can grab that apple from before, and now it's
going to have two apples that are constantly joining.
So this is a bug.
And I can probably keep doing this over and over again,
and I'll have infinite snakes.
And I can keep running into other snakes.
So we're not clearing our grid quite appropriately.
And why is that?
Let me see.
initializeGrid.
I thought I called initializeGrid.
Oh, OK.
That's why.
So in our initializeGrid function, what we're doing is
we're not actually clearing the grid by setting everything to 0.
We're actually adding new empty tables to the grid.
What we need to do is say tileGrid equals empty grid first,
so that it starts completely fresh.
[? Bavick's ?] got it right, as we did last time.
So let's try that one more time.
I'm going to grab one or apple, and then try to collide with myself.
OK.
Boom.
OK, so it's working perfectly.
That's great.
So yeah, something as simple as that.
So the initializeGrid, basically, it was--
because it does this table.insert tileGrid and a new empty table.
And so that's just going to keep adding new tiles.
It's going to keep adding new empty tiles to the end of the grid,
and then for each of the tables that existed already,
it's going to just add empty tiles beyond where we can visibly see.
And so that's a bug.
But what we've done now is we fixed, so that is no longer an issue.
[? JPGuy ?] "Good stuff."
Bella [INAUDIBLE] says, "Great."
Thank you.
Yeah, it's nice seeing it all sort of come together.
OK, so we have a game over.
We have the ability to restart our game from scratch.
So now we should think about collision.
Or not collision, rather, but the ability to go backwards--
or to go in the opposite direction of where we're moving,
which causes issues.
Because then we're instantly colliding with ourselves,
and we also get some weird rendering issues-- well,
we won't anymore, because we made it so that we collide.
But we shouldn't be able to go backwards, basically.
And so this part is actually pretty easy.
If anybody wants to suggest anything as to how we can go about doing it.
I suspect it's probably somewhere up here in the update function.
So we have the snake timer itself checking
whether we've gone past our speed variable, our speed constant, 0.1.
And this is sort of where we check to see-- oh, rather,
up here, in our love.keypressed function.
This is where we check the key.
And if we're going left, we should move left.
If we're going right, we should move right.
If we're going up, we should move up.
If we're going down, we should move down.
Right?
So here, all that it feels like we need to do,
really, is just change the conditions.
So if the key is equal to left, and snakeMoving not--
rather, equal to 'right'.
So if we're moving right, and we press Left,
we shouldn't be able to go left, because that
will be a reversal of our direction.
Same here.
And snakeMoving is not equal--
whoops.
Enable dictation?
No, I'll pass on that.
Not equal to 'left'.
So this ~= is a the weird Lua way of doing not equals.
In a lot of languages you'll see it like that, the !=, but in Lua, it's ~=.
That's not equals to.
So if key is equal to up and snakeMoving not equal to down,
if key is equal to 'down' and snakeMoving is not equal to 'up'.
So basically, now we're saying, if we're pressing Left,
and we're not moving right-- so if we're moving up or down,
basically-- then move left.
And same thing for all the other directions.
Basically, don't let us move in the opposite direction
that we're already moving, effectively.
So if I hit this, and I try to move left, I actually can't.
But I can move down.
I can't move up, which is nice.
But basically, now, no matter what, I actually can't collide with myself.
So we're in a state where the snake is incapable of going backwards, and thus
instantly colliding with itself.
So that's kind of the majority of the Snake features.
Does anybody have any questions on what we've talked about thus far?
Want me to step through anything in the code?
Anybody have any interesting features that they think the game is missing?
We still have a couple hours left, so we could do some more stuff.
But we can also have some Q&A and talk about what's
going on here with the code base.
We could also make a title screen before we start.
So that's probably pretty straightforward.
Let's see.
We probably want to have like a local gameStart equals true.
It'll be like the beginning of the game.
What we can do is basically do an if statement, kind of with the game over.
If it's equal to gameStart, then just draw, welcome to snake,
press Enter to begin, or whatever.
And then when we go from the game over back,
we can just go straight to game start, not straight to the game,
just so we can sort of see what's going on in advance.
Cool.
So gameStart is true.
By the way, this code here, I'm realizing now
we can we can still change the direction of our snake,
even when we're in game over.
Not that it'll mean anything or it will be visible, but this bit of logic
here is always executing.
So the better style would probably if not gameOver then do all this stuff.
And then we could probably make an else here,
but that's only in a gameOver state.
We maybe don't want that to happen.
If gameOver or gameStart, though.
That could work, right?
And then in our draw function, we could do something like if gameStart draw,
welcome to snake.
"Wouldn't it be better to include the gameOver check in the initialize part?"
The gameOver check in the initialize part.
I'm sorry, JP, would you explain?
[INAUDIBLE] check for gameOver at runtime.
Not exactly.
This is kind of a minor optimization at this point,
just because this bit of code is just going
to be constantly checking for conditions when we're in a game over.
So it kind of makes sense to make one if statement,
and then be able to get out of that, if it's the case that we're in a game
over.
Because ideally, you don't want lots of--
I mean, this is a small example, and not really worth worrying about.
For this kind of game it doesn't matter at all.
But for a large game with maybe more complicated input,
you probably don't want it registering while you're in some state where
it's completely irrelevant.
Because in a game over, ideally, you're probably doing other things
and displaying other things.
And if you're taking input and using CPU cycles
for things that aren't germane to the scene at all,
and are completely being wasted, it's just kind of messy.
Kind of unnecessary.
But again, for this use case, it's a very simple thing.
It's just worth mentioning because I happened to notice it was there.
Good question.
Oh, right.
And then if gameStart then--
else.
We can put all this in the else, because if we're in gameStart,
we don't need to--
these two, this gameOver and drawGrid rendering stuff
doesn't need to take place in the game start.
Let's just assume we're going to have a black screen that just says, Snake,
and then press Enter.
We can store a gameOver in a variable.
When the game is over, we change into something else,
and check if that variable in game over, start new window, set initial stuff.
Yeah, that's effectively what we're doing.
Correct.
Yeah, the gameOver variable, we declare it right up here at the top.
gameOver is false.
And then we even have a gameStart value.
"OOP In Lua."
Yeah, we'll talk about that, actually.
So object-oriented programming in Lua is a bit weird.
By default, it uses these things called metatables,
and the syntax is a little messy.
But there is a really nice library that I use that allows you--
and I teach this in the games course-- that
allows you to just use very simple syntax for declaring classes.
I think I'll introduce that in the concentration stream
on Friday, where we make the memory pair game.
And we can maybe make some classes, like a card class, something like that.
But yeah, it's totally possible using libraries.
It's actually quite nice.
Yeah.
OK, so this is where we're actually rendering.
We're going to render the game start screen, so the very beginning, when
we start the game.
if gameStart render SNAKE at 0 VIRTUAL_HEIGHT 2 minus 64.
Because we want it halfway in the middle of the screen vertically,
shifted up half of 128.
We're going to take the padding width to be the WINDOW_WIDTH,
so we're going to center it within WINDOW_WIDTH, starting at 0.
And then we're going to make sure it's in center mode.
And then we're going to do the same sort of thing that we did here, actually.
Except it's not going to be press Enter to restart,
it's just going to be press Enter to start.
And then as before, actually, love.graphics.setFont--
remember, you have to set the font for whatever drawing operations you want,
because Love2D is a state machine, it doesn't allow you to draw with a font,
I guess.
You have to set a font, draw, and then set a font, draw, et cetera.
So if you forget to unset a color or a font or something,
and rendering looks a little bit messed up,
it's probably because you forgot to change the state machine
to reflect whatever changes you want.
So if the game is started, blah, blah, blah.
So that's working great.
And then if gameOver or gameStart.
OK, so this is where it's actually going to--
we could do it this way. gameStart = false.
gameOver and gameStart equals false.
Yeah, actually, this should work perfectly fine.
This should work perfectly fine, crash.
Oh, I didn't see what that said.
Oh, I'm using VIRTUAL_HEIGHT.
Sorry.
I'm so used to writing VIRTUAL_HEIGHT as a constant, because I
use that in my games all the time.
But what I want is WINDOW_HEIGHT.
So we'll look into VIRTUAL_HEIGHT, as we get into some other more retro-looking
games, because I like to generally program
games to fit to sort of old retro console aesthetics.
Like the Gameboy Advanced is a really good resolution.
240 by 160, I believe is what it is.
And there's a library called push, which I use in my games course, which
allows you to say, oh, I want my game window to be rendered at x resolution,
not like an actual native resolution.
So I could say, fire up a window that's 240 by 160 pixels,
and it'll draw it just like that, and scale everything,
and it'll look like a nice retro game, pixel perfect, while also being
the same size as a full-windowed game.
So you really do get that zoomed-in pixelated look.
And right now, all we're doing is just squares.
So it's pretty basic.
We don't really need to worry about that too much.
But if we get into like concentration, where maybe we
have pictures of elements, or maybe we make an RPG
or a Super Nintendo looking game, or NES looking game,
or a Gameboy Advanced looking game, I think we should definitely
dive into that a little bit.
But we won't worry about that this time.
We'll take a look at some more features on Friday.
So let's make sure that I have everything working.
So I put up the game, it says, SNAKE, Press Enter to Start.
I press Enter to start.
I'm going.
I have my snake.
Boom.
I can't crash into myself if I'm going the opposite direction, which
is really nice.
But I can collide with myself just like that.
I get into a game over state.
I can press Enter, and now I'm back into it.
Just like that.
So now we have multiple what are called game states.
And we'll also take a look, going into the future,
at what are called state machines.
And what a state machine can let us do to sort of break apart
our game into a little bit more modular and abstract components,
so that we can say, I want a title screen state
with its own update, its own render, its own input detection.
I want a play state, I want a game over state, I want a start state.
All these different things that sort of have enclosed blocks of functionality.
But for right now, our game is actually working pretty well.
I guess one thing that we could add would be like static obstacles
into our game.
So I think Snake, by default, will have random--
they'd be like brown obstacles, almost like stakes
in the ground, or something, where you can't actually collide with them.
And if you do, it's game over, just like the body.
"Don't forget to plug your edX and GitHub stuff in the chat
and in the description."
Good point, JP.
In case people don't catch the stream when it's live.
That's a good point.
I'll make sure to edit that.
Oh, also a good reminder.
I'm going to push to my GitHub.
Let's see, where am I at right now?
dev/streams/snake.
It should be that.
So git status, git commit.
Complete snake with title and start screens.
I should've been committing a little bit better.
Git push.
And I configured my git, as well, because last week,
when I tried to do anything related to get, it was a little bit funky.
Because I have what's called two-factor authentication enabled on my account.
And that causes issues, if you're using git at the CLI.
"I added different levels in Snake in Scratch, in one of that I did stones.
If snake hit the stones, game over."
Yeah, so we can do that.
We can absolutely do that.
Let me just refresh this.
So now it's three commits.
So if you go to the--
whoops, let's go back just to the main.lua.
If you go to the repo--
it's this repo.
Actually, I don't think I'm signed in.
Let me sign in here really quickly.
Am I not signed in here either?
"Stuff like that.
Obstacles would be nice.
Stuff like that.
With levels, increased speed."
Oh, the increased speed of the snake.
Yes, yes, yes, yes, yes.
Correct.
So I'm going to pop off camera just for a second here.
I should be signed in here, I think.
I apologize if people were able to hear that audio.
250 followers on the Twitch, as well.
That's awesome.
And also, thanks for tuning in, everybody who's here now.
OK, let me just make sure that we're at right spots.
OK.
All right.
So that's the URL which will have the GitHub.
If you're curious, if you want to grab that,
get the latest commit, mess around with it a little bit,
that's got all the stuff that we've looked at today.
Let's go back to the monitor here.
And right, obstacles.
So currently, we have our title screen, we have the ability to move around,
we have a score.
Our background is a little bit boring.
So very simply, just using some code that we've already got,
which is just our randomizing the--
"You can also program a Twitch bot for the stream live.
That would be super meta, albeit not game related."
Yeah.
That would actually be pretty cool.
I'm not familiar with Twitch bot programming,
but I'll definitely look at, that because that'd
be pretty cool, actually.
Very interesting.
I'm assuming it's probably like JavaScript or something,
which I am fairly familiar with.
But not as much so as Python and Lua, probably.
Yeah.
So we have the foundation laid.
Let's say I want to generate stones.
Let's say I want gray blocks generated randomly in my level,
and those are stones.
And if we collide with a stone, then that
should trigger a game over, just like colliding with my body.
So where do I have the code for generating an apple?
It's up here, right?
Here's what we're going to do.
I'm going to take these two lines of code out here,
and I'm going to copy them.
I'm going to call a function I haven't written yet called generateObstacle,
and then it's going to take a value.
So TITLE_APPLE, in this case.
And then I'm going to define some new constants.
Well, a new constant, for now.
TILE_STONE is 4.
I'm going to come down here at the very bottom,
where I have all my initialize stuff.
And just above my initialize stuff, I'm going
to say function generateObstacle(obstacle).
And I'm going to paste those two lines of code that I had before,
which is just going to be setting a couple of variables.
So obstacleX and obstacleY.
Same here.
And then I'm going to set the value at that actual index
in the tileGrid to whatever the value is passed to me as the parameter obstacle.
So now I can use this for anything.
I can use this to generate random apples, or stones,
or whatever other tiles I might design as a designer.
So elseif tileGrid y x--
by the way, now I'm in the drawGrid function,
so I want to be able to render this appropriately.
TILE_STONE.
And this could be just like a--
oh, I realized this doesn't need to be-- change the color
to light green for snake it.
Had an outdated comma there.
This is to be a light gray.
And so light gray is kind of like all the same numbers on the RGB,
but just not 1 and not 0.
Ideally higher.
So we'll say 0.8.
So I'll say love.graphics.setColor(0.8, 0.8, 0.8).
And then one for full opacity.
love.graphics.rectangle.
We can just copy this line of code, actually, and then paste it there.
And the thing is this is only going to generate an obstacle of an apple here.
So this should still work.
So it's going to generate an apple.
I'm going to pick up the apple.
It's going to generate a new one.
That's fine.
Is it generating the apple up here, as well?
Oh, you know what it is?
It's in the initializeGrid, I think.
Yeah.
So back down in our initializeGrid function,
we can take out those two lines of code that were kind of the longer ones,
and just say initialize--
sorry, generateObstacle(TILE_APPLE).
Right here, just like that.
So now it's a little bit cleaner.
It's the same logic that we had before.
Should just work right off the gate, which it does.
And then I'm going to figure out a place where I want to initialize some stones.
And I can do it here in initializeGrid, actually.
Because we're only going to one of initialize those stones every time
our grid is initialized from scratch.
So I can probably do something as simple as for i = 1,
10 do generateObstacle(TILE_STONE).
And if I run this, now I have 10 stone obstacles in my game,
but I can still collide with them, and I actually
overwrite them, as a result of that.
So we're kind of in the right spot.
We're generating obstacles, but we still need to implement collision detection.
It's not quite working yet.
So this was in our update function, I do believe.
Yes.
Here.
So we can basically do and or statement here and say
or tileGrid snakeY snakeX is equal to TILE_STONE.
Then do that.
So if I do this, boom.
Game over.
So we overwrote the stone there, collided with it,
and that's our game over.
So now we have randomly generated obstacles,
and we have rendering and collision detection for them.
And so now we have sort of this idea of random levels.
So pretty neat.
Just mess around a little bit.
Got to play test.
It's an important part of the game.
Make sure we don't have any bugs that we haven't anticipated yet.
Let's try going from the other side, which looks good.
The only thing that I would be conscious of is this apple is actually capable
of overwriting a stone, because the generateObstacle function
doesn't actually check to see whether the obstacle that we're overwriting--
that index in the grid--
is empty.
So I should probably do that next.
Just like that.
Pretty slick.
Come through here.
It actually looks pretty nice.
I'm not going to lie.
Very simple, but effective.
I'd be curious to know, with this code base, how high of a score folks
might be able to get.
I probably won't play for too much longer,
but I sort of feel like I'm owed at least an opportunity
to play it just for a couple minutes.
Elias says, "Nice move."
Thank you.
OK.
So we'll just end it right there.
I tried to grab the apple at the last second.
Didn't work.
Got a score of 24.
All right.
So the last thing we should take into consideration, like I said,
is when we generate an obstacle, we should probably
do this in a while loop.
So we probably want to do whatever our generateObstacle function is,
generate those new xy pairs sort of infinitely,
until we get an empty tile in our grid.
And then we can we can set it.
So let's just do do--
I hardly ever use this.
do until the tileGrid obstacleY obstacleX equal to TILE_EMPTY.
So do until.
[? Bavick Night ?] says, "I did levels based on scores.
If the score reaches a number, it will up levels."
That's a good idea.
We could maybe mess around with that a little bit.
Oh yeah, that was another thing we were going to do.
We were going to add increased speed in the game.
So making the snake move a little bit faster, the more points we get.
So we should maybe figure that out a little bit.
That is as simple as just decreasing our snake speed constant,
which would therefore not make it a constant,
it would make it a just a regular variable.
We can mess around with it a little bit.
So again, to cover what this syntax is, on lines 223 here to 225,
I'm using what's called a do until loop.
And in C, and most other languages, rather, it's called a do while loop.
So I'm generating those two random values, the obstacleX, obstacleY,
and I'm getting them as two random values.
But I'm doing it until they're for sure an empty value in our table.
I don't want to overwrite any other tiles that might already exist,
whether it's an apple or a stone or whatnot.
Or if it's an apple even overwriting the snake in the map.
I don't want that to happen.
That'd be buggy behavior.
So I want to go ahead and just do that.
So do until tileGrid obstacleY obstacleX is equal to TILE_EMPTY.
And then set that equal to the obstacle number
that we passed into our obstacle function.
So probably won't-- oh, I think you do need an end after the until.
'End' expected to close the 'do'.
Until-- I never use do until in Lua.
Let me refresh my mind here a little bit.
The syntax-- oh, it's repeat until, not do until.
I'm sorry.
OK.
Repeat until.
There we go.
227, attempt to index a nil value.
generateObstacle-- is this taking place before--
wait, hold on a second.
MAX_TILES_X-- into a nil value.
repeat local obstacleX, obstacleY.
Oh, because they're local to here, I think.
Yeah, that was it.
So I declared obstacleX and obstacleY as local variables
within this repeat block.
And so by doing that, basically, I was erasing these values as soon
as it got to this until statement.
So remember, there is a thing called scope
in programming languages, where if you declare something
as local to something, anything outside of it has no access to it.
So in order to generate these values, I have to declare them up here,
so that they're accessible not only within this block,
but also within this condition here, at the end of the repeat block.
So a little bit of a gotcha just to be aware of.
But yeah, there we go.
Perfect.
Those all clustered up towards the top.
That's interesting.
OK.
[? Bavick Night ?] says, "Gave some lives to the snake, initially.
With a game over, it decreases, so players don't have to play forever."
Yeah, that possibly could work.
[? Tmarg ?] says, "Scoping in Lua seems weird, too."
It is weird.
It is really weird.
Because a global variable is just any variable that you say like this,
obstacleX = 1.
Except in this case, because I declared this is local already--
but let's say I have some value called someFoo = 1.
someFoo is going to be accessible anywhere in the whole project.
It's a it's a little bit of a black sheep, in terms of programming
languages, in a lot of ways.
This being one of them, because a lot of languages
don't give you this sort of global scope.
Like in Python, for example, you declare variables
without some sort of specifier.
You actually have to declare global as a keyword in Python
for it to have the same kind of behavior.
But in Lua, just declaring a variable like this,
in any function or any block, makes it available everywhere
throughout your entire application.
And that can be a source--
like [? Tmarg ?] is saying, "I had lots of trouble
with it in the Mario assignment"-- that can cause a lot of issues.
So it's best practice to definitely keep your local variables
isolated as much as you can.
Even at the top of my module up here, I have
a lot of these global constant values.
But all of my actual gameplay values are just
being declared as local, even though they
are functioning as the global variables for this module.
But at least this way, if I have some other file that imports this main.lua,
it's not going to add these symbols to the scope of that project.
It would be a difficult thing to debug, potentially.
[? Bavick Night ?] says, "If you want to take a look, it was years back,
doesn't grow dynamically."
Sure, why don't we do that?
Let's go and take a look at [? Bavick Night's ?] Twitch project.
If I can open it here.
Should still be silent, hopefully.
[? Bavick ?] [INAUDIBLE] in on the chat one more time,
so I can see it in the live chat on my window here.
Or if anybody minds copying and pasting the link into the--
lorem ipsum nonsense data.
It's scratch.
Can you paste the link again in the chat, just so I can see it live?
I think I can scrub back, but just offhand, it's probably faster to--
there we go.
No, I just wanted the URL, [? Bavick. ?]
All right, let's do it.
Is there is there sound, [? Bavick? ?] Should I enable sound?
Oh, there we go.
Yeah.
"We could start with three to four stones, and then each level,
increase the number of stones, but limit that number."
Yeah.
Absolutely.
That would be an example of easing your player into it.
Just a game design decision.
All right.
I have sound enabled.
All right.
Let's test this out here.
All right.
Welcome to Snake World.
Press Spacebar to start, use the arrow keys play, good luck.
Oh, boy.
Oh, you have continuous snake, that's why.
Yeah, it's a lot harder to do it in a continuous fashion,
because you have to have a bunch of basically squares that
are chained together, rather than having a grid that we used.
So we used the grid for that purpose.
Yeah, very good.
And I'm guessing you get to a certain point, and then you increase the level.
Hey, I mean, you have the basic-- oh, there we go.
Different level.
OK, I see.
I see.
All right.
Oh, and it's faster.
OK, so you have a lot of the mechanics there.
Sort of the difficulty increasing in some fashion.
I can't actually hear if there's if there's music, currently.
Oh, you know why?
I screwed up.
OK, I know what it was.
It's because I have that on.
Sorry.
Let me turn my monitor on, really quick.
OK, that's what that is.
Oh, boy.
It took my input out of the--
I think because I close the Twitch tab, it took my input
out of the Scratch window here.
But it's very good.
No, good job, [? Bavick, ?] on that.
I would say, I don't blame you too badly for not having the growing
functionality, because it's harder to do with a continuously moving square
that's not axis-aligned--
or rather, it's not discretely aligned within your grid.
Because collision detection is then--
you have to do what's called axis-aligned bounding box detection,
which we cover in the G50 course.
But it's more work.
It's not super easy, so I don't blame you.
But no, good job.
Good job on that.
I'm curious, I don't remember offhand Scratch actually
lets you do collision detection.
I think it does.
It just has collision detection built in,
so you can just kind of move it around.
So you just have a list--
yeah.
"Enabling sound might cause copyright issues."
Oh, [? JPGuy ?] good point.
Crap.
I didn't think about that.
OK, hopefully not.
Worst case, YouTube will just quiet that part out.
I might be able to silence it myself, when I cut the video
and push it to YouTube.
Which, it will go to YouTube, by the way.
"There's sound--" Blah, blah, blah.
Rip.
Yep, Rip.
"Have fun.
It was.
I tried, but I didn't have an idea.
It's where I started coding."
Yeah.
No, I totally understand.
It's not an easy problem to solve by any stretch of the imagination.
So no worries there.
All right.
Well, we could do something similar to that.
We could start with the difficulty thing, which Bella [INAUDIBLE]
provided, as well, as a suggestion.
"Twitch might silence it automatically.
That happens sometimes, but you won't get flagged or anything."
Yeah.
Yeah, hopefully nothing.
It should be nothing serious.
Yeah.
OK.
So if we're going to do this difficulty thing, what we can do--
I think we'll just start off with difficulty = 1, maybe.
Or level = 1.
And then what we can do is basically say for i = 1 until level times 2, maybe?
And then maybe set the speed equal to 0.1 minus--
whoops.
We'll do it here.
And then SNAKE_SPEED = 0.1 minus level times--
let's see.
What do we want it to do?
01?
Will that work?
No.
So math.min.
So at the very--
no, rather, math.max.
between 0.01 and 0.1 minus level times 0.01.
So what this is going to do--
"Make it go exponentially."
Yeah, that'll end up in disaster really quick.
No, we'll do a linear function on that for now, I think, just to keep it sane.
What I'm doing now with SNAKE_SPEED was I
used this function called math.max, which
returns the greater of two values.
So basically what I'm doing is I am taking either 0.01,
being the lower bound--
so the fastest our speed will be 0.01, which is really fast.
And I'm doing 0.1 minus level times 0.01.
So 0.11 minus level times 0.01.
So this is going to take the greater of these two values.
So as level gets higher, this value will get higher.
So it'll be 1 times 0.01, 2 times 0.01, 3 times 0.01.
Which will effectively be 0.01, 0.02, 0.03.
And it will subtract from this value, 0.11,
until it gets to be the value of 0.1, in which case math.max is going
to see that 0.01 is actually greater than this value,
and will always return 0.1, no matter what level we're on.
So that's how we can sort of cap the lower bound on our speed.
And we can do the same thing at the very bottom, where we have--
let's see, where is it at?
Here, where we have level times 2, we don't
want this to keep increasing infinitely, because eventually we'll
have so many stones that we won't be able to actually function.
So let's say the most we're going to have is--
what's a good number-- maybe 50, and level times 2.
So math.min is the opposite of math.max, and will
return the lower of two values.
So it'll start off at level times 2 being 2.
And as level increases, we'll eventually get
to a higher and higher point, at which time we will exceed 50,
if someone's good enough.
And once we have exceeded 50 stones--
50 will be the lower value of this, and this function, math.min,
will always return 50.
And so this is how we can clamp our value to be a certain amount.
And to actually get to the next level, we
want to check that we've eaten a certain number of apples.
Let me see.
Where is it at?
This part right here.
So in our update function, when we check to see
that we are eating an apple, what we want to do
is increase our score, as we did usually.
And then it's here that we want to say if score is greater than some amount,
let's say level times 2--
or level times 4.
Should it be level times 4?
Level times 3.
If it's greater than level times 3.
And we're going to math.min 30 in that.
So we'll always be looking for at least--
no, that won't work.
Because score is not going to get reset to 0, so this won't work.
Oh, I guess that actually will.
Yeah.
We'll do level times 3.
That's easier for now.
Our score is cumulative, so--
we can make this an exponential function.
So we can say this.
I think this will work.
So we'll say score is greater than level times half of level times 3.
So it's kind of exponential.
So it's going to be 0.5 times 3.
In this case, 1 times math.max of 1 and level divided by 2.
So in this case, we want to make sure that it's at least 1.
Because level divided by 2 on the first one
is going to be 0.5, which won't work.
That will be 0.5 times 3, which'll be 1.5, which will be a number.
No, it'll work, but it's not as clean.
So we're going to basically say level times math.max of 1 level divided by 2,
which will get bigger and bigger, but at slightly less
of an increased rate than a purely exponential function.
And then we'll multiply that times 3.
Just as some value.
Oh, math.ceiling.
Correct.
Yeah.
We'll do that.
We'll do that, math.ceiling Good suggestion, [? Bavick. ?]
Math.ceiling level divided by 2.
I think we even talked about that last week.
Perfect.
So now that will never be a fractional value.
So if it is greater than that value, we're going to increment the level.
We're going to then initialize the level.
I wonder if it'd be worth having like a press Spacebar to start thing,
just like [? Bavick ?] had, or we could just jump into it.
But I think the level transition, if they're not ready for it,
will be a little jarring.
So I think it makes sense to have like a screen that says,
oh, you're starting level x.
Press Spacebar to start the level.
And then if they press it, then it'll start.
So I think that's what we want to do.
In that case-- do I also want to do this?
One thing that I noticed that before we had was that when we had game over,
we were actually moving the snake into the next obstacle, or whatever it was,
and it was kind of making it hard to see what we collided with.
We couldn't see that we collided with the stone.
So I can say if not gameOver then do all this stuff, where we change the--
sorry.
If it's not a game over, then we can go ahead and move our head forward,
make the body the last tile.
And then if it is a game over, what's going to happen
is it'll stop before it gets to this point,
and we won't overlap that obstacle.
It'll just make it a little bit easier to see what's going on.
[? Metal ?] [? Eagle ?] says, "CS50TV, what language are you using for this?
I apologize if you answered it already.
Just came in the stream."
No problem, thanks for joining us.
This is Lua, and we're using a framework called Love2D.
So you can go to love2d.org, which this isn't the correct page.
Love2d.org, which will give you the list of installers.
Were using version 11.1 here.
If you're running Windows, Mac, or a Linux machine,
there's different distributions here, and other versions as well over here.
They have a great source of documentation.
At the very bottom-right, you can click love.
You can see a few simple examples here on that main page.
And if you have a GitHub account--
or even if you don't have a GitHub account,
you can download the repo for today's code
at GitHub.com/coltonoscopy/snake50.
So thanks for popping in.
OK, so if it's not a game over, what we want to do is--
yeah, don't overwrite the next tile.
I want to be able to see that I collided with the stone, if that's the case.
No problem, [? Metal ?] [? Eagle. ?] Thanks again for joining us.
Let me know if you have any more questions.
OK.
So we did that.
So that'll fix that bug.
So actually, I could possibly run this now.
Except not.
Because level is-- it's up here.
Local level is 1.
On line 130 we're setting the level equal to level plus 1.
Oh, I didn't put it then statement.
Got to do that.
That's important.
So let's make sure that this is working now.
Can actually collide with that stone?
Boom.
Perfect.
So now we collide with the stone, and it doesn't actually overwrite the stone.
We can see it when we collide with it, which is just a little bit cleaner.
We can visually see what's going on a little bit better that way.
Oh, and notice, by the way, we only got two obstacles, instead of the bajillion
obstacles that we had before.
So this is great.
And the actual code to trigger for the next level
isn't executing yet, because we haven't actually tested for that.
Or, we've tested for it, but we haven't actually--
like we're incrementing the level, that we're not actually
initializing the grid to anything new or doing anything else,
so we should probably do that.
"And display the level."
Yes, good point.
We can do that up here, actually.
So if I go to love.draw, and then drawGrid, and then print the score.
Where I have print the score, I'm actually going to copy that.
Do this.
I'm going to print the level.
And I'm going to do love.graphics.printf.
And I'm going to set this to 0, 10 pixels, VIRTUAL--
sorry, WINDOW_WIDTH.
And then I'm going to set this to 'right'.
This is now right-justified, it's not center-justified.
And this is going to help us out by right padding it for us,
so we don't have to worry about it.
So now, we have level equals 0 right there, which is perfect.
And it's right on the right edge, so doesn't quite work as well.
So I'm going to set window with minus--
[? We do ?] minus 5?
Minus 10?
Or, no.
I'll just set this to negative 10 at WINDOW_WIDTH,
and that should have the effect.
Yeah, perfect.
So set the start value of negative 10.
So we're shifting the amount that we're centering, or right-aligning,
by negative 10.
And we're still keeping WINDOW_WIDTH, so it's basically
just shifting this right-aligned label to the left just a hair.
So that way it aligns better with the 10, 10,
that we have up here with our other score.
It's 10 pixels from the left edge.
[INAUDIBLE] says, "Slick hair, bro."
Thanks, bro.
I appreciate it.
All right.
So we have our level.
And we actually see that the level is incrementing,
if I did everything appropriately.
Although level is 0, for some reason, which I'm not--
why is level 0?
Oh, it's because it's the same value as score.
Right.
Let's not do that.
Let's tostring(level).
And now, level 1.
OK, perfect.
Try to get a few apples, see if it increases.
Perfect.
So when we got to score 4, it did increase to level 2.
I was going to say, there's a case to be made
for just increasing the level while you're playing,
and adding more obstacles in the game.
But that could potentially be a big source of frustration,
if like an obstacle generated right in front of your snake as you're moving.
So probably not the best design decision.
You definitely don't want to frustrate your player base.
But we're on level 3 now, which is nice.
Snake is looking good.
Level 4.
OK, so it's working.
I didn't check to see whether speed was updating, which I don't think it is.
Oh, right, because we're not updating it.
Yeah, we're not updating it.
So we can do that.
We're going to do that.
All right.
So a level, increase by 1.
Speed, make sure to set that appropriately.
So remember, it's a function of our level, whatever our current level is.
We have level incrementing, we have the speed adjusting.
We need to have the screen that shows us what level we're on,
and allow us to press Space to actually start the game.
So we're going to need a new variable for that, probably.
And what I generally like to do is kind of have a good game state
variable, that will sort of keep track of it as a string.
"I pause for three to five seconds on the level increase,
and then they know it's going to get tough."
Yeah.
Yeah, that could work, too.
Let me see.
So gameStart, local newLevel = true.
We're going to have a newLevel variable, which
means that we're going to basically let them press Spacebar to continue.
if newLevel then-- we're going to check for Space here.
So if key == 'space' then--
we'll say newLevel equals false.
And we'll set to true by default, which we did.
[? Tmarg ?] says, "With obstacles, you still
have to make sure they generate in a fair way."
Yeah.
So part of that algorithm would be--
I guess there's a few ways you could you could look at it.
I guess what I would probably do is, since the character is going
to always start in the top-left, I would make sure
that the algorithm makes the obstacle spawn beyond maybe five
tiles in every direction, as by just comparing whether the x or the y
is greater than or equal to 5.
But yeah, there are situations where it could potentially create like a wall
completely in front of the character, which would
be a little bit trickier to solve.
A wall, I guess, kind of in the direction the character is going.
But thankfully, since you can kind of wrap around with Snake,
it's not as big of a deal.
We can maybe look at that.
We'll run it a few times, and see if that's an issue that we run into.
But that's a good to anticipate before you release the game,
just because you can have some scenarios where you just
get really screwed over by obstacles.
Yeah.
"Just like you said, you could conceivably
make it so they spawn away from the player or something."
Yeah, exactly.
"Did we check that apples and stones don't overlap?"
Yes, because now in our generateObstacle function,
remember, we have this repeat block.
It'll basically ensure that anytime an obstacle is generated-- and remember,
obstacle is both our apples and our stones--
if this function is called with any obstacle as an argument,
it will make sure that it's empty always.
Because this rule repeat obstacleX, obstacleY
against two random values until that obstacleY and obstacleX in our tileGrid
is equal to tileEmpty.
So by virtue of that logic, we'll never have
an overlap in our obstacle generation.
Good question.
OK, so the Start screen.
So if we're at a new level, we're going to wait for a Space press
to get to a new level.
[? Bavick ?] says, "Cool."
Our drawing mode is right here, gameStart.
What we want also is newLevel, right?
So else if newLevel then love.graphics.setFont.
We'll just make them both large fonts, in this case.
love.graphics.printf.
We're going to say--
actually, no.
We do want huge font.
There's going to be a level, and then two string level.
0 WINDOW_HEIGHT divided by 2 minus 64 WINDOW_WIDTH 'center'.
And then love.graphics.setFont.
OK, and then we'll just do press Enter to start.
And then we have to set it to largeFont.
OK, so this should start-- yep, level 1.
So press Enter to start.
But we can't see the level in advance.
So what I probably want to do is basically in here
is where I want this, actually.
So if we're at a new level--
where we've drawn the grid already, so we can then
draw the level text and the press Enter to start label.
And then if get a game over, we'll draw game over, instead.
So both of those will occur on top of the grid,
with the score and the level currently displayed
on top of the left and the right.
Actually, we don't want that we don't want the score and the level,
I think, drawn up top.
Do we?
I guess it doesn't matter that much, but we could take that out if we wanted to.
OK.
So that should be able to then draw the--
if not gameOver and not newLevel then blah, blah, blah.
So remember, we need to make sure to check for not newLevel, as well.
Because we don't want it to update.
We want the world to update if we're in the new level screen.
Let's see if this works.
So level 1.
For some reason the snake's not drawing.
OK.
Press Enter.
Oh, right.
Because it's Spacebar, not Enter.
But why is the--
it's interesting, the snake actually didn't get set in the initializeGrid.
Oh, because we didn't call initializeSnake in the--
or we did.
Oh, no.
What we did is we didn't do this.
We didn't do this here.
That's very important.
That should be part of the initializeSnake function actually,
probably.
And also, it's not Enter to start.
It should be Space to start, probably.
Let's see if the level transition works.
I'm not sure if it does, just yet.
Nope, it doesn't work yet.
OK.
So that's OK.
So I'm going to update that label.
Because it says press Enter to start.
Should be press Space to start, for this part.
Press Space to start.
So now, that is correct.
Not Enter.
And then what we can do next is determine whether we
are in a new level, which is this here.
So we increase the level, decrease the SNAKE_SPEED,
but we'd actually do newLevel = true.
And if new level equals true, then I believe we should just return
So we're moving around.
OK.
Level 2, press Space to start.
The tricky part about this is that it's not actually displaying the next level.
So that's kind of what we want.
We're going to press Space to start.
And oh, it actually kept the exact same level, and it didn't spawn an apple.
OK, that's a bug.
Oh, is it because we didn't generate Obstacle(TILE_APPLE) here, probably?
newLevel is true.
Oh, I guess we want to generate that here then.
OK.
newLevel is true.
initializeGrid, initializeSnake.
And then let's just bring this line of code to the initializeSnake function,
because it belongs in there.
All right.
Let's see if that works now.
We're going to initialize the grid from scratch,
as soon as we get to the next level.
OK.
Level 2, and now we have four obstacles.
So we're on the way.
We press Space, and then we go.
And I do feel the snake is actually moving a little bit faster now.
OK, so my math doesn't work out here.
OK.
So our score's at 7.
We went straight to level 3, which is fine.
But I think I need to tweak my algorithm a little bit for the score.
That's OK.
It's a little bit better this time.
Let's try this out.
There we go.
It seems to be working pretty well.
We have obstacles.
The first two levels, the math there is a little bit screwy.
So that's OK.
But this all works pretty well.
We have levels, we have the speed going up, we have obstacles generating.
They look like they're getting higher, getting more obstacles.
So this is working out pretty well.
The one thing that I do recognize about the game, which kind of sucks,
is that there's no sound at all.
So I think it'd be kind of cool to dive into sound a little bit.
Before we do, does anybody have any questions
or want to talk about anything on the stream?
Anything that we have done?
And I'm going to commit this, by the way.
Snake working with levels.
So if anybody wants to grab the code, the zip, or clone it,
you can get it at the GitHub.
So once again, GitHub.com/coltonoscopy/snake50.
There should now be four commits on there.
The most recent commit has all of the stuff that we just added.
It's actually getting pretty long.
It's like 200, almost 300 lines of code.
Granted, a lot of this could be cleaned up a bit.
We're doing this live, we're not really taking a super hardcore engineering
approach to this, as our first stream.
Later seems will be a little bit better about this,
but this is a little bit more haphazard.
Kind of do as we go, and not think about it as much.
But it's coming along really well.
So yeah, I think the next step that I'd like to do
is get a little bit of sound going.
And so I think probably the first thing I would do
is add a sound effect for eating an apple.
So whenever we have an apple, just play a little blip, or something.
So one of the programs that I like to use a lot for this is called Bfxr.
And I think it's called the same thing, or Cfxr on a Windows machine.
That's not correct.
It's a free sound-generating program.
I'll try to pull it up here.
Bfxr.
Bfxr.net.
So you can download it for Windows or Mac.
Apologies if you're on a Linux machine.
Sfxr might have a Linux build, maybe.
Yeah, they have Linux built for the Sfxr one.
"Want to give some lives to players, and on game over it
decreases, and on no lives, it ends.
And it would be like real snake games."
Yeah.
We could definitely do something like that, too.
Sorry, I had something in my eye.
A little bit of a life-based approach, that they don't die and lose
all their progress right off the gate.
And you could even have something like where
if they pick up enough apples or something,
they actually increase their life counter.
That's something we could totally do as well.
But for now-- let's see, what time is it?
It's 5:06.
We have a little bit less than an hour.
We have some more stuff we can mess around with here.
Let's go ahead to Bfxr.
If you're looking at, it I'm just going to generate a few sounds.
It might be loud.
OK.
That didn't sound quite good enough.
Notice that there's a bunch of little buttons up here.
So you can generate different categories of sounds, like pickups, lasers,
power ups.
Got some weird sound effects here.
That wasn't bad.
Kind of like a Mario coin, almost.
I'm going to export a wav.
Going to go to where I have it saved.
dev/streams/snake and then we'll just call this apple.wav.
So wav files are generally what a lot of sound editors
will export as their first sound.
"Yes, it was there as well, like one ups, now we're going retro."
Yes.
Yes, this is the good stuff.
"I just did Mario sounds and scratch.
Totally."
Yeah, Mario sounds are what's up.
I love Mario sounds.
Doing sounds in Love2D is actually really, really easy.
So we can go up here, where I have my fonts.
Now, normally, I would have a separate file that has all of my fonts,
all my graphics, all my sounds, all that stuff,
in sort of like a dependencies or a resources file.
But for right now, we're just going to declare them up here.
So local appleSound = love.audio.newSource
and I'm going to call it apple.wav.
And I'm going to go over to where I pick up the apple, because that's
the actual sound object.
So now we can hit play, pause, and all that sort of thing on that object.
You can even loop it, which wouldn't be very
good for that kind of sound effect.
But you would want it for something like a music track, which maybe we
can add a music track, as well.
Got something in my eye, again.
Ouch.
Where we find the apple right here.
So increase the score and generate a new apple,
we're also going to play a sound effect.
So I'm going to go to appleSound:play.
Colon operator is kind of like an object-oriented oriented operator.
It basically calls some function with the self value
plugged in as the first parameter.
And that's kind of the way that Lua does is object-oriented programming.
And a lot of these Love2D classes and objects work with this colon operator.
We haven't implemented any of our own classes,
but we'll see this in the future with future streams.
Possibly even on Friday, when we do concentration.
But for now, it's sufficed to say that in order
to use these sound objects play function,
I could use this colon, not a period.
Make sure you use a colon.
And so once I hit Run--
string expected got no value.
Oh, sorry.
You need a second value on your appleSound, in this case.
And it needs to be a string that specifies whether we're
using static or streaming.
Everybody, gives a shout out to David J [? Malin ?] in the stream
there, everybody.
Trolling.
[INAUDIBLE] "Is this live?"
Yes.
Yes, this is live.
May or may not possibly be the real David J [? Malin. ?] Who knows?
Actually, I think it is.
He's messaging me right now.
"This [INAUDIBLE] everybody's giving me a shoutout here."
OK, awesome.
Thanks for joining us, [? Hard Denmark. ?] Yeah,
[? Bavick Night, ?] yeah, [? professor ?] is here.
Everybody throw some kappas in the stream for David J [? Malin. ?]
Throw a few kappas in there.
Right.
So the thing we're missing from the appleSound function call
was the static string.
So you need some string value in order to--
there we go, there we go.
[? Cosmin HM ?] joined in, as well.
Basically, what static tells this audio source
object is, am I going to store this in memory or am I going to stream
it from disk?
If you store it in memory, it's going to obviously take up more memory,
but it's going to be faster access.
For something like a music track or for a lot of music tracks, that you don't
necessarily need right off the gate, you can declare them
as, I believe it's streaming or something similar to that.
But static is a string that will allow it to be stored in memory permanently,
so your game has instant access to it.
So I do that, and now we have snake running again.
It's no longer broken.
If I hit Space to start here, and I pick up the apple, if all is going well,
it works.
Perfect.
So that's audio.
That's how to make sound effects in your game.
I suppose maybe we could add a victory sound when we go to another level.
So I'm an open up Bfxr again, and maybe I'll mess around with some of the power
up sound effects.
Probably not that one.
That one's pretty good.
We'll use that one.
We'll export that as newlevel.wav.
And then just like we did with this other one,
I can say newlevelSound is love.audio.newSource.
newlevel.wav, we'll make this one static, as well.
And these wav files, because they're sound effects, they'll be pretty small.
So declaring them as static isn't a big deal.
But again, larger, longer audio sources, probably
want to declare them as streaming.
Let's confirm in the Love--
this is a good chance to look at the Love wiki, by the way.
We can go to love.audio.
love.audio.newSource right here, and then the type.
Yep.
Source type streaming or static.
Its stream.
Oh, and this is an interesting feature.
I'm actually not aware of this.
There's a new queue function.
Audio must be manually queued by the user with source queue
since version 11.
OK, I'll have to take a look at what that actually means,
and if that's any use.
But for right now, the two main ones that I've historically done with Love2D
are static and stream.
So again, smaller versus larger.
Or even if you have multiple levels in your game,
and you don't necessarily need all the sound effects or all of the music
for that sound effect loaded up right away,
you can declare them as streaming.
Or you can just dynamically unload and load
the objects, if you have just a smaller, finite set of them.
OK, so we have our newlevel sound effect.
So wherever we declared that we reach a newLevel, which was in our update
function, should be right here.
So this is if the score is greater than level.
And we used our math.ceil level divided by 2 times 3,
our pseudo-exponential function.
I'm going to go ahead and do what we did before.
newsound:play, which will play that newlevel sound whenever we do reach
that score threshold and we go to a new level.
We'll try it out here.
Whoops.
And again, I can't move backwards anymore, which is good.
That's behavior that we want.
But I sort of instinctively want to do that.
So there we go.
As soon as we cleared the level, we played not only the apple sound effect,
but also the newlevel sound effect.
So we have this more robust sort of sensory feedback system in our game.
A little bit more polished.
I'm going to go ahead and add everything to the project,
commit it as sound effects for game, for snake.
I'm going to push that.
So now, if you clone that or download that,
you'll see those new sound effects in the repo.
And when you run it, you should be able to play them, as a result.
OK.
One other feature that I really like to add to games,
typically, is a music track, something like that.
I think that might be another nice feature to add.
If anybody has any questions of what we've done thus far,
definitely throw them in the chat.
I'll be looking back and forth.
For now, I think I'll pull up a music track from one of the other course
examples that I did, just because I know it's a free song.
This is a Unity project, actually.
Resources/Sounds-- I don't remember.
Oh, I think I had it in Music here.
Let me just make sure.
Is it too loud?
That's not bad.
We'll use that.
That was a free music track I pulled off of, I believe, freesound.org.
Which a lot of great material there, by the way.
I don't know if I pubbed them last time.
Freesound.org, lots of free audio samples.
You can go there.
You do need an account, I believe, to download them.
But this is great for prototyping game stuff.
I use it all the time.
And also opengameart.org is a great site to download free sprites,
and other resources and art.
We'll use this in the future, when we make other games.
I might even see if they have like cards that we
can use for concentration on Friday, because that will give us
a chance to actually work with sprites.
And sprites are a little bit more nuanced to deal with than shapes.
And you have to split them up and what are called quads.
Especially if like this picture, for example.
If your images are actually stored grouped together on one image,
you want to split it up into squares.
But more on that on the next stream.
For now, I'm going to take that music file that I copied.
I'm going to go into my repo, and we'll call this just music.mp3.
It works the exact same way as the other sound effects do.
So local musicSound = love.audio.newSource music.mp3,
static, as well.
And then the thing about music is it's a little bit
different, because we want it running the whole time that we're
playing the game.
So I can do something like--
what did I call it?
I called it musicSound.
[? musicSound ?] setLooping to true.
And musicSound:play.
And now, if I start the game, we have music.
And you can still hear the other sound effect on top of it.
I think they're in key.
They sound like they're in tune with each other.
But yeah.
That's how you get music in your game.
So now, we have a pretty layered little Snake demo there.
We have all the major pieces.
We have the sound effects for actually picking up apples, which is important.
We have the music.
So now we have sort of everything that most games would have,
with a few exceptions.
We're missing, for example, persistence of high score.
So that's something that we can look at in the future, when
we look at save data.
So saving your high score to some text file,
so that you can remember it later.
I think one of the features we were going to look at was having lives.
Yes, exactly.
Like [? Bavick ?] says, "Are we going to do lives?"
Yeah, we can take a look at that.
We have about 40 minutes left, so we might as well.
Bella [INAUDIBLE] says, "Awesome, thank you."
So let's think about that.
So we can have lives.
We'll keep you here with the gameOver, and the other state variables
that we're using to keep track of what state we're in.
So local lives.
Let's say we get three lives, by default.
And just to test our view out a little bit here, let's
draw that in the top-center right here.
So love.graphics.setColor(1, 1, 1, 1).
We're going to keep this.
I'm going to just add another string similar to this one, the level one.
And I'm going to make this lives.
By the way, if we didn't talk about this before,
this dot dot is how you add strings together in Lua, the concatenation
operator.
So Lives: space dot dot tostring(lives), because you
can't concatenate a string and a number, in case
we missed looking over that detail.
But I'm going to set the first element to 0, 10 off the y--
so it's a little bit below the top of the screen--
WINDOW_WIDTH, and then center.
So we're going to center the string.
It's going to be the very top-middle, it's
not going to be at the left or the right side, like we did before.
So let's go and run this.
Perfect.
So we have lives 3 at the top-middle of the screen.
And that's it.
So we don't really have anything much else
to show for that, because we haven't actually
implemented the logic for losing lives, which you should do.
Let's take a look at how we would do that.
So normally, if we detect that we collided with the snake body or stone,
we just set gameOver to true.
But what we can do instead is we can say if lives greater than 2--
or rather, we'll set lives = lives minus 1.
if lives greater than 0, what we want to do
is we want to start them off again at the beginning.
So what we can do is we can say newLevel = true else gameOver = true.
And now, I believe this might work as is.
OK, so we wrote over that stone, so there's
a little bit of a flaw in our rendering logic for that piece.
Which we can add another if statement at the bottom of that update
function to take care of that.
But it still says we're at a level 1.
If you press Space--
oh, it doesn't restart us, actually.
So that's another thing we probably want to do.
Oh, that's going to be tricky.
Well, trickier, rather.
Oh, no it's not, because you don't have to retain the snake.
We just have to start the snake fresh.
OK, that's easy enough.
So what we can do is we can set newLevel level to true, initializeSnake().
And so what that should do--
oh, but we're not deleting our old snake.
So actually, there's a bit of a bug here.
But we were able to overwrite the old snake head though.
OK.
"If lives is 0, then game over."
Yeah, it's effectively what this logic is doing.
So if lives are greater than 0, we're going to set newLevel to true,
so that where we get that pop up again.
And then we're going to initialize our snake again,
so that it starts at the top-left.
But we do have to make sure that we don't--
if not gameOver and not newLevel--
because this was basically writing the snake to the grid.
We don't want to write the snake to the grid, because we're not erasing it,
and we also don't want to go into the stone if we collide with it.
We want to see the stone and the snake head kind of touch each other,
so we're aware of just what exactly happened when we lost our life.
So I'm going to go ahead and run this.
Space to start.
Boom.
So once we press Space to start again-- oh, it's still there.
OK.
So it is overwriting the grid at that location.
So let's figure out why that is, exactly.
Why it's not deleting itself.
Do a little old-fashioned debugging.
[? Bavick ?] says, "Yes, saw it."
Oh, because we're returning here.
Are we?
No, this is only if we actually get to the next level.
Oh, because we're not initializing the grid?
"New level, snake clear, draw new snake."
Yep, that is the logic.
"New level."
We don't want to make a new level, necessarily.
So the thing is we're keeping the old level, so that we can retry it.
We're not generating a new one from scratch.
We could do that would.
That would probably be a little bit easier.
But I think it feels appropriate to keep the level as it is,
and then just have the player using the snake to just start from the top-left
again, and just reenact the existing level.
So they feel like they have gone through some sort of discrete progression
of levels that exists, rather than just constantly refreshing and making
level 1 be ephemeral, I guess.
OK.
It's in our rendering code somewhere.
So we are writing to--
if not gameOver and not newLevel initializeSnake, right?
Which is what we're doing.
There's a subtle bug in here somewhere.
Just need to figure out where it is.
It's this line of code that actually writes the--
oh, wait.
We had a prior value.
Oh, yes.
In that case, it's the prior value, not the--
first of all, let's make sure that this is running with multiple segments.
So not just the head, but multiple.
OK, so it's the entire snake.
OK.
So if we do get a new level, we need to actually clear out
all of the snake elements.
We need to clear all of the snake elements, and then finish the level up.
OK.
So what we need to do then is some function called clearSnake,
and then initialize the snake.
So currently, we have all of our snake elements, they exist in the grid,
and then move and collide with something.
All of those grid indices, because we're not refreshing the grid,
are still going to have snake body, snake head elements still on them.
So let's create a new function called clearSnake.
And 4-- oh, this is a great chance for us
to examine how to iterate over a table in Lua.
So this is perfect.
So for k, elem in pairs(snake)--
or rather, snakeTiles do tileGrid elem2 elem1 equal to TILE_EMPTY.
And so what that's going to do--
the only issue with that is that we are inserting a head element into the table
before we actually clear it.
So what we're going to need to do is basically ignore
the first element in the snake.
First, let's make sure that theory is correct.
So I'm going to run this.
If my theory is correct, it will erase the rock when we--
oh, did I call clearSnake?
I did, right?
When writing a function, always make sure you call it, as well.
Yep.
OK, perfect.
So if I do this--
by the way, that's a torturous location for an apple.
Yep.
So it got rid of the obstacle.
Not what we want, right?
Because remember, it pushes the head onto the snake
before it actually does the rendering for it,
because it wants to check to see if the next element's an apple before it
does that.
So what we can do is clear the snake.
There's a couple of ways we could do this.
What I'm going to do is I'm going to ignore the first element.
So if k is greater than 1.
So here's basically iteration in Lua.
It's similar to iterators in other languages,
but basically, it takes every key value pair
that this function called pairs returns you.
So for every k element, so every key value--
in this case, I'm calling k and elem--
in pairs, every key value pair on the table snakeTiles do--
and basically, if k is greater than 1--
so if it's not the first element, so anything beyond the head element--
set elem2 and elem1 in our tileGrid index--
so the y and the x tile grid at that snake unit--
set that to empty.
So if I'm correct, and I get [? up size ?] two, and then
collide with this, it got rid of the snake,
but it didn't get rid of the obstacle, which is exactly the behavior
that we were looking at.
And it did decrement lives to 2.
So let's try it again.
I'm going to go here, I'm going to collide with this.
Boom.
And then I have one life left.
Game over.
Oh, awesome.
And then there's the game over screen.
I can't hit Spacebar, but I can't hit Enter.
And unfortunately, we neglected to set our lives to 3, so that's a bug,
as well.
But lives are working seemingly correctly.
Let's go ahead and fix that last issue.
Let's make sure when we do get a game over after a collision--
which is going to be here--
gameOver is true, lives = 3.
Let's do that.
Oh, actually no.
Because if we do that, gameOver will be set to true,
but when the game over screen pops up, we'll
actually see 3 lives at the top-middle, and that's not what we want at all.
So I'm going to go to where we have the gameOver logic,
and then here I'm going to set it to lives = 3.
So where we set the score back to 0, that's
where I'm going to set lives equal to 3.
And if I run this--
Game over.
Enter.
And now we have three lives again.
Beautiful.
Now, we are missing one detail.
"Clear the snake, start the level again."
Yep, exactly.
We are missing a sound effect that I think would be important,
which would be the death sound effect.
That's a good one.
I like that one.
We'll use that death.wav.
I'm going to go ahead up into here.
Set the local deathsound = love.audio.newSource('death.wav' ,
'static').
And I'm going to go where we have the game over actually registering,
which is here, I'm going to set deathsound:play--
actually, no.
This should be where we just die normally.
So that'll be that.
We're going to want a separate sound for the game over.
So we're down here.
Whoops.
Cool.
So now we have it now we have a sound effect for when we actually die,
which is nice.
Little bit of feedback.
[? Bavick ?] says, "Let's not hard code max lives,
so we can make changes from one place."
Yes, that is good practice.
Yeah, definitely avoid hard coding as much as I've been doing in this demo.
In future demos, we'll try to adopt a little bit more
of the best practices approach to programming,
and more from an engineering perspective.
But in this case, since this is very introductory,
we're focusing a little bit more on the syntax,
just getting everything up and running.
But on Friday, we'll be a little bit more upfront with our engineering,
so to speak.
Excuse me.
OK.
So we have a death sound, which is looking good.
Then I, lastly, want a game over sound.
There we go.
I think that's more what I'm looking for.
All right, game over.
Sounds like a classic Atari sound effect.
But that should work.
All right.
So let's go over here, gameover.wav.
I'm going to come up to the very top.
Yes, exactly the sound a snake would make when it died.
Absolutely.
Just combustion.
s OK.
Then back up here.
gameOverSound:play().
And we run the game--
OK, cool.
Cool.
It's good enough, right?
It does the job.
All right.
I think that's pretty much it, in terms of Snake,
like a fully playable, fully robust version with bells and whistles.
We have obstacles, we have levels, lives, increasing difficulty,
which is important.
But up to a certain extent.
And of course, sound effects, things of that nature.
[? Bavick ?] says, "Yas, we did it."
Yes we did.
Ended last Friday kind of incomplete, but turned it around today.
And now we have a, I would say, very robust Snake implementation.
Before we before anything finishes here, I'm
going to add everything and commit everything.
So we'll just say this is final snake.
With all the sound effects there, all generated sound effects,
with the non-generated music.
But yeah, it came along.
And we did it together.
And that was part of the fun, right?
So I think I'll stick around for a few minutes for questions and stuff.
It looks like we finished a little bit early.
It's a little bit hard to ballpark how long exactly some of these projects
will take to finish.
Friday's concentration stream, I'd say it'll probably approach three hours.
It may or may not go over the three-hour mark.
Hard to say.
Tomorrow, we have Kareem Zidane, if any of you are familiar with Kareem
from the Facebook group.
He's one of our full-time staff members from Egypt,
and he is going to be holding a stream tomorrow with me on Git and GitHub.
So we'll talk about the basics of Git, if any of you
are unfamiliar with Git and/or GitHub, how to use them.
It'll be a nice tie-in to a lot of the streams
that we have planned for the future, mine included.
So if you see what I'm doing with Git, and you're a little bit unsure how
to do it, or how to use your own source control, how to use Git and GitHub,
how to make it work for you, Kareem and I will go over it tomorrow.
He'll be leading the way, and I'll be sort of playing his assistant,
and asking questions, and sort of pretending
like I'm not super familiar with it, just
to provide a different layer to it.
As always, if you have any suggestions on streams that we can host,
games you want me to make, topics for other people in the stream to make,
we are in talks with some other folks, potentially,
about doing a web-based assignment.
So something in JavaScript, maybe React.
We're talking about having possibly a machine learning introduction.
So maybe something like OpenCV or scikit-learn, or something like that,
using just a Python framework probably for ML or AI.
Those sorts of things.
[? Bavick ?] says, "What time tomorrow?"
Tomorrow we'll be doing the stream at 3:00 PM Eastern Standard time,
so same time as today.
And on Friday, the stream is actually going to be at 1:00 PM,
so a bit earlier in the day.
So that folks that are watching from farther out, farther abroad
have a chance to tune in before it's too late.
And we may or may not transition to an earlier schedule.
3:00 PM, I know, for folks, especially that are, say, possibly
in India or Bangladesh or other locations
far away are having kind of a tough time keeping up.
But worst case, all these videos are going to be on our YouTube channel,
so you'll be able to see them a little bit later.
And folks who don't have the chance to tune in live
will be able to at least stay up to date.
If you haven't subscribed to our YouTube channel, do so as well.
And if you're here and chatting, you're already
following the CS50TV Twitch.tv channel.
But if you haven't followed that, and you're watching from YouTube,
do go to twitch.tv/cs50tv.
Hit the follow button, so that we can chat in real time
and make some stuff together as a group.
[? Bavick ?] says, "I'm from India."
Yeah, so I'm sure you'll appreciate the schedule being a little bit earlier,
as well, I imagine.
So thanks for tuning in all the way from India, [? Bavick Night. ?]
All right.
Like I said, just to kind of linger around for some questions.
I see there's 15 people in the stream.
If you have any questions about game programming, or Snake, or anything
that's coming up or will come up, definitely ask now.
I'd be curious, if anybody does--
[? Bavick ?] says, "Adios."
Adios, [? Bavick. ?] Good to have you.
Thanks so much for coming.
I'll see you next time, I'm sure.
JP says, "I have a question."
Shoot, JP.
Let's see your question.
"How are you affiliated with Harvard?"
So I'm a full-time technologist here at Harvard,
and I spend all my time working with CS50.
This year I also taught an Extension School course GD50, so
CS50's intro to game development.
So if you go to CS50.edx.org/games, you'll see this here.
Sorry.
Don't know my location.
Pop-ups are terrible on here.
CS50's Introduction to a Game Development,
where we talk about how to make a lot of classic games,
like Mario, Zelda, Pokemon, Angry Birds, a lot of classics.
And we use Love2D and Lua for the first few lectures, as well.
So this is on edX, and you have a chance to explore that at your leisure.
Bavick Night, "I'll be there to learn Git, GitHub."
Awesome.
Yeah, join.
Let all your friends know.
Suggest that they follow us and join the live chat.
Happy to increase the pool of folks contributing.
The more people we have in the stream, the more
we can maybe look at doing like live collaborative
stuff, which would be interesting.
"How to show a big apple for a few seconds?"
"Not a question, but a suggestion.
Is it possible for you to make a shorter, bit easier
a game or video for beginners [INAUDIBLE]
I've never heard of that term before."
Says [? JPGuy ?]
Elias, "How to show a big apple for a few seconds."
Well, to show a big apple, it would be a little bit trickier.
But there's a few things you could do.
So right now, what we're doing is a grid-based approach.
So this might be something we could better illustrate
if we did a sprite-based game.
Because then you could just scale the sprite,
or you could just have a larger sprite file.
But we're just drawing rectangles, and everything
is aligned on a very discrete grid.
If we wanted to make apples that were, let's say, 2 by 2,
or 3 by 3 blocks wide, you could--
where do we have generateObstacle?
So in generateObstacle, you could say something
like if obstacle is equal to, let's say, TILE_BIG_APPLE--
or just in this case, BIG_APPLE, since it won't be a tile.
I guess it would be TILE_BIG_APPLE, because we
want to make it maybe worth more.
Then you would set tileGrid at obstacleY obstacleX to TILE_BIG_APPLE,
and then tileGrid obstacleY obstacleX plus 1 equal to TILE_BIG_APPLE.
And so on with the y, and then so on with the xy.
And that would have the result of populating four of your grid indices
with the apple tile.
And then you could just detect the intersection with your snake head
in one of those, and just trigger it as a big apple.
And then maybe add three points, instead of four points.
The only issue then is you have to clear all of the all of those four tiles.
And so you have to keep track somewhere else of where those four tiles are,
so that you can delete them from your game scene.
Additionally, you have to also check to see whether or not
all four of those tiles happen to be empty.
Because if you're writing an individual tile,
it's not a big problem, because here we're
checking to see if the obstacleY and X are at that one tile empty.
You would actually have to do this same thing, but for all four
of those big apple tiles, rather than just the one.
So you would basically choose four random pairs--
so one, two, three, four--
and then do basically a combo if statement, that's basically this time
is 4, as well.
And it's a little bit trickier also for the random number generator,
especially with larger concentrations of stones,
to be able to find empty blocks of tiles.
Kind of in the way memory fragmentation works,
where if you have a pool of memory, and you have a bunch of little files
and little blocks of code stashed away amongst it,
and you're looking for a big continuous chunk of memory,
it takes a little bit more time to find that chunk,
if your memory is super fragmented and kind of split up all over the place.
Good question.
I don't think we have enough time quite to implement it from scratch,
but it would be a great exercise, if you are potentially looking
to implement that sort of thing.
And maybe in the future, if we have time, we'll come back to it, as well.
But good question.
""[INAUDIBLE] we make shorter, a bit easier game video for beginners."
Let's see.
So Pong is the first game that I made.
So David kindly added a little excerpt there.
You might like some of the earliest weeks of [INAUDIBLE] some other games,
as well.
And in those chunk of videos, we do cover things
like Pong, which is a pretty straightforward game.
A little bit simpler.
So we can maybe explore that on another stream.
The memory game is going to be very easy, as well, on Friday.
So maybe join us for that stream.
And that should give us, I think, a little bit
easier of a time getting into some of these details.
This is a bit longer, because Snake is a deceptively complex game.
We're looking at 321 lines of code at the very end.
The memory game is probably going to be maybe half of that.
So shouldn't be too difficult. Also a grid-based game.
Yeah, technologists-- It's a very flexible term, JP.
The one, I guess, I'd be categorized as is an educational technologist.
So it's trying to find and work with tools
that help us improve the way we distribute
educational material with CS50, and just content creators
and educators all around, I would say.
But it's a very flexible role, for sure.
Quick question about tomorrow's stream.
What software will I be needing?
I am [? mobile ?] today formatting PC, and setting up new development stuff.
So [? Bavick, ?] if you just can install Git on your machine.
If you're on a Mac, it's really easy.
I think it comes by default. I'm not 100% sure.
You might need the Xcode install tools.
Either way, you can download it fairly easily.
I think they recommend Homebrew for Macs.
Oh no, they have a Git for Mac on here.
So if you go to atlassian.com/git/tutorials/install-git.
It says, if you installed Xcode or Command Line Tools,
Git's already installed.
There's a Git for Mac installer.
There's Git for Windows.
And Linux, as well, has an installer.
We'll just look up Ubuntu, just to get a sense of what that looks like.
Yeah, you could use your Package Manager.
It looks like for Ubuntu, it's apt-get install git-core.
So it depends on your operating system.
But yeah, install Git.
And then ideally, make a GitHub account, if you
don't have a GitHub account already.
So you can do that just by going to--
if I were on a brand new web browser-- so GitHub.com.
It'll take you right to where you can sign up.
Heavily recommend getting a GitHub account.
Andre, "Hi, Colton.
Just got here.
I have a question that's only tangentially related, so no biggie
if you don't answer.
But got any hot tips on where to get cool 3D game assets and anything
for audio?"
So there is a few places.
So if you're using Unity-- which I recommend you probably do,
if you're getting into 3D--
the Unity Asset Store has a ton of free stuff.
And you'll see that actually within your Unity editor.
I believe it's Window, General Asset Store is the menu dropdown.
And then there's some other places where you can get 3D models for free.
If you just type in free 3D models, I believe
most of the first few resources--
I believe I've used TurboSquid, Free3D.
These are all pretty legit.
It's a little bit trickier importing certain file formats than other ones,
but I would just fish around and kind of get a sense.
TurboSquid I think is good, Free3D.
Those are the two that I think I have actually messed with.
But again, if you're using Unity, Asset Store is great.
Lot of great free ones.
And the standard assets pack has a lot of really cool stuff.
And there's a lot of good paid stuff, too.
If you end up going down the Unity dev route,
and you want to actually pay for your resources,
or you don't mind paying for your resources,
definitely worth spending a little bit of money.
Metal Eagle, "I have a question.
Is Lua good programming language to create games,
or is it just limited to just the simple 2D games?
What is Lua actually better at, compared to the other programming languages?"
So Lua is used very often, very frequently
throughout the games industry.
Not only just for 2D stuff, but for 3D stuff.
A ton of engines use Lua commercially.
Just recently, I saw a game [INAUDIBLE] was being modded-- which is a 2D game--
but it was being modded in Lua.
World of Warcraft was modded traditionally in Lua.
I'm not sure if they still mod in Lua, but I think they might.
A ton of game engines, 3D and 2D, use Lua.
The two primary game engines now, Unity and Unreal, don't use Lua,
but you can get Lua embedded into your Unity projects with a special DLL.
So it's totally usable in there.
And Lua is absolutely perfect for 2D game development,
using Love2D and using some other frameworks that utilize it.
Yeah.
No, it's a totally great language.
One of the fastest scripting languages, too, using what's
called LuaJIT, which is the Just-In-Time compiler.
And I believe Love ships with this by default.
So you get really fast, dynamic recompilation of your Lua code.
And it's very fast, compared to other scripting languages.
Good question.
It has some weird syntactical oddities that are specific to Lua itself,
but it's very easy to get over that.
And the syntax, at large, is actually quite nice and pleasant to work in.
And the Love2D API especially is very robust and just pleasant to work with.
"Appreciate the response."
Yeah, no problem, everybody.
"I'm on Windows 10, have GitHub.
Friday CS50 Cool, we'll get it installed."
So, [? Bavick-- ?] have GitHub from CS50.
Oh, from CS50.
Yeah.
Tomorrow, to be clear, is the stream with GitHub
at 3:00 PM with Kareem and I. And then on Friday
will be another stream by myself, where we a brand new game,
Concentration, the memory card game.
So yes.
Metal Eagle, "I did not know that.
Thanks, CS50TV."
No problem.
No problem at all.
All right.
I'll stick around for just a couple more questions.
We're at 5:55.
We have another five minutes before we adjourn.
If anybody wants to talk about anything or has any questions.
But other than that, we're getting close to the wrapping up point.
"Have a good day everyone.
Pleased to be here.
See you tomorrow," says [? Bavick. ?] Thanks, [? Bavick, ?] for coming in.
Have a good day to you, as well.
I'll see you tomorrow.
Bella [INAUDIBLE] says, "That was fun.
Thank you for this amazing stream."
Thanks, Bella, for coming in.
Appreciate it.
Come join us tomorrow.
Come join us Friday.
Make some more games, and do some more cool stuff.
"I have heard that game programming jobs have long hours and very little pay.
It may depend on the area, but what do you know about it?"
I've heard mixed things, as well.
So it depends on what studio you work for.
If you work for a AAA studio, chances are you will be working long hours,
you're probably working 60 hours, especially
towards the end of a game's development lifecycle.
It's not uncommon, based on what I've read, for most studios to approach,
for their core engineering staff, somewhere around 60 hours for work
weeks.
Rumors are going around about Red Dead Redemption 2
having 100-hour work weeks.
But the creators of that have come out and said
that it was just the writers who initially had that,
and that the engineering team were not expected to work 100-hour work weeks.
But if you're an indie developer or you're
working for a company that maybe does other types of games, like not AAA
games, maybe like a mobile development company,
you can expect to see something more along the lines of 40 to 50 hours.
If you're an indie developer, you might still end up spending more than 40,
50, 60 hours finishing your game.
Especially as you approach the end of the development lifecycle,
if you have a hard set deadline.
Just because games are notorious for taking a long time
and having a lot of hard to track down bugs
that manifest themselves at unfortunate times.
I mean, just as you saw, making Snake wasn't a terribly easy thing to do.
And we saw a lot of weird bugs that we didn't anticipate.
And like I said, my first time also implementing Snake from scratch.
But we hopefully got a chance to see just
how true this is with even the most simple and game development.
Extrapolate this out to something massive, like modern AAA titles,
and yeah.
It gets it gets out of hand very quickly, especially
if you're not programming and something is easy to rapidly developing as Lua.
If you're maybe working with a C++ code base and you were doing engine
development, and you're having to recompile everything every time you
make any adjustments.
It gets tricky.
"Thank you, and good night, because it's 11:00 in Morocco."
Thanks, Elias.
Like I said, we'll try and get our streams set
up maybe a little bit earlier.
Fridays will be at 1:00 PM Eastern Standard Time.
So they'll be two hours earlier, so you'll hopefully
be ending a little bit sooner.
"Yes, programming games myself, I see bugs that are very hard to correct."
Yeah, and you'll get better at it.
And you'll also be able to anticipate things a little bit better.
But it can be tricky, for sure.
But I love games, and I think they're a lot of fun.
I don't know.
[INAUDIBLE] from Serbia, "Which book would you recommend for C learning?
Thanks," says [INAUDIBLE]---- oh man, this is going to be a hard one to pronounce.
[INAUDIBLE] I don't know.
I'm sorry.
I'm having a really hard time pronouncing that one.
[INAUDIBLE]
Books for learning C. So the very first book that I ever learned C on was--
let's see if my Amazon--
OK, I'm not signed in.
So C programming.
I heard this book is good, the Programming in C book.
I haven't actually read it.
Everybody talks about The C Programming Language.
This is by Brian Kernighan and Dennis Ritchie,
who are the creators of C and Unix.
So I would probably recommend that.
David's got some links in the chat there.
CS50 has an official C programming list.
So definitely pop into that and see what's up.
Oh, this is a new book.
I haven't seen this one before.
21st Century C. That might be worth looking at.
I'll have to maybe check that one out.
C in a Nutshell.
There's a ton of books.
Honestly, any of these books will probably work.
C Primer Plus is good.
I've read through parts of that one.
Read through several books, watch videos, watch tutorials.
There's a lot of resources now.
Book learning isn't strictly necessary for all of your learning.
Honestly, the first book that I ever learned--
and I'm not too ashamed to admit it--
was actually C for Dummies.
This was the first book-- is it even on here?
Yeah.
C for Dummies.
This was the first book that I ever used to learn programming formally.
I guess you could say formally.
Not in the context of like game programming,
I guess, or like a book that was teaching
scripting, but actual programming.
This is the first book that I learned, and it's pretty good.
I remember I actually enjoyed it.
It's got good reviews.
C++ for Dummies, as well.
I looked at that one.
But yeah.
Whatever book works for you.
Honestly, different people are going to get different things out
of different books.
So different resources work for different people.
And YouTube is rife with tutorials, and even CS50
itself is a good resource for learning C. So there's a ton.
[? Andre ?] [INAUDIBLE] I'm not sure what language that is.
Is that Czech?
[INAUDIBLE] "Thank you for hanging out with us in chat Professor [? Malin," ?]
says JP.
[INAUDIBLE] do not look back, son."
"I do have to head out, but see you next time."
Thanks, David, for coming into the chat.
Everybody bid farewell to David.
Give him some more Twitch emotes.
Give him some kappas.
The kappa's my favorite, so I'm good for kappa every time.
24/7.
There we go.
JP's always happy to oblige with the kappas.
[INAUDIBLE] as well.
All right.
With that, it's 6:02.
So we're going to call it here.
This is the end of our stream.
[? Andre ?] has got the meatboy emoji.
There we go, that's a good one.
Thanks so much, everybody, for coming out for the conclusion to Snake.
We've come a long way from the beginning of the day, where
we had an Etch A Sketch.
And the end of the day, we have a pretty much full, polished game.
Tomorrow, again, Kareem Zidane and I, Git and GitHub.
And on Friday, Concentration.
So this is CS50 on Twitch.
My name is Colton Ogden.
It was a pleasure.
I will see all of you later.
Thanks so much, again.