字幕列表 影片播放 列印英文字幕 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. 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. Boom. Boom. 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. 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.
B1 中級 SNAKE FROM SCRATCH (Part 2) - CS50 on Twitch, EP.3 (SNAKE FROM SCRATCH (PART 2) - CS50 on Twitch, EP. 3) 1 0 林宜悉 發佈於 2021 年 01 月 14 日 更多分享 分享 收藏 回報 影片單字