字幕列表 影片播放 列印英文字幕 COLTON OGDEN: All right, we should be live here on CS50's Twitch channel. Apologies for the delay. I'm afraid our PC actually had a blue screen of death. So we brought in a reserve machine. So thanks to Dan Coffey for helping out with that. But, yes, welcome to everybody who's been patiently waiting in the chat here. First of all, for folks who don't know who I am, my name is Colton Ogden. I work full-time with CS50. And I thought as part of our new Twitch channel, we'd take some time today, about a few hours, to from scratch code a game called Snake, which is sort of an old school 2D game. If you ever grew up having a Nokia phone or the like, you've probably played it. And it was played on sort of older computers back in the day. And we'll take a look at what that looks like. But, anyways, thanks so much for tuning in. Let's dive into what we're doing and start to take a look at everything. So I'm going to pull up my computer here. And now we have a little twitch.tv/cs50tv graphic there. So if you're not following the CS50TV account on Twitch so that you can join the live chat with us and we'll have some back and forth conversation, definitely go ahead and follow that link. Hit Follow. If you're watching this on YouTube after the fact, we will be pushing this video to YouTube later. But there is a 10 minute waiting period, I believe, for if you want to actually chat. And I'll see your chat messages here as we go ahead and program. All right, so what you see here is a text editor called VS Code. This is what I'll be using today to program, but you can use sort of any text editor that you want. You could even theoretically use, like, Word Pad on Windows or whatever. I would definitely recommend getting something like VS Code or Atom any of the free sort of text editors that are out now. I personal like VS Code. And it has some cool plugins, one of which I will use for the framework that we'll be taking a look at today to make Snake. Let's take a look at what Snake actually is, I think, before we go too in-depth into what we're doing programming wise. So Snake kind of looks like this. It's basically a grid, right, where you have this green long thing that goes in various directions. And it continuously moves throughout the sort of grid space in the game. And then every time you pick up these red dots or whatever object that you want to frame in the game, you're a snake creature which starts off with just one piece will actually get another piece at the end of it at the tail, and you'll grow. And so the catch is the longer your snake grows sort of the more you run the risk of biting your own tail or your own body, so to speak, because you're continuously moving. And when you bite yourself, the game actually ends. And so the goal is to essentially grow the snake as long as possible by eating apples or however you want to visualize the red dot in the game. Hello to Bhavik Knight there in the stream. Thank you for visiting the chat today, visiting the stream. But, yeah, the game that we're going to be making will look very similar to this. We're not going to get too fancy with it. I actually haven't done any prep or any research into how we're going to make the game. I figured we'll just kind of dive into figuring it out on the fly. So that's Dan Coffey in the stream, everybody. Say thanks to Dan for fixing our broken machine today and getting everything up and running. All right, so first things first. In order to start programming something visual or to program a game, we'll need to find some tool, some game engine, to do this for us. And I'm a big fan of a 2D game engine, a 2D framework, called LOVE, which uses Lua as its scripting language. What's up, Steve Benner? Good to see you again. Thanks for coming by. So LOVE is a 2D framework. It's an OpenGL framework strictly catered towards 2D games. And it has a very nice API for game programming, which makes it pretty easy, nice and simple to get into. But it does allow you a lot of low level flexibility that some bigger engines kind of take away from you or at least kind of hide behind layers of abstraction. I'm thinking of things like UNITY, where there's a big learning curve associated just with kind of getting the engine right, getting the UI correct, and not necessarily diving too deeply into code for at least a while. Hello, Elias from Morocco. Good to see you. D. McDermott, good to see you. OK. So we're going to go ahead and take a look at LOVE2D and sort of Lua and its syntax and the different functions therein. In order to get started, you'll need to if you want to follow along whichever version of LOVE corresponds to your operating system. Today, we'll be using the newest version which is version 11, or 11.1 rather. I'm on a Mac. I'm running Mojave. If you are on a Mac running Mojave, you might have some bugs with actually running version 11. So make sure in your Accessibility panel, which I'll take a look at here just for those running on a Mac, if you go to your Security and Privacy, rather, and then your Privacy section here, there is a section of apps that basically-- part of Mohave, it seems, allows a lot more apps to have accessibility features. I'm not exactly too sure of the low level aspect of what they do. But they basically allow the apps to have more control over your computer than normal. So in this Accessibility panel here, under the Privacy panel, just make sure that LOVE and Terminal and also whatever text editor you're using are checked. And this is Mac OS X Mojave specific, not specific to Windows per se or Linux. And that's also on version 11. I've gone ahead and downloaded it already. So if I go into my console here, assuming that I've put my executable in the right spot, I should be able to just of LOVE normally. And you'll see this sort of thing with a balloon and a tail that says, "no game" and that's perfectly normal. If you're on a Windows machine or if you're on a Mac or if you're on Linux and you run the binary, you should get the exact same thing. And I have a shortcut to that on my desktop here. Let me just hide everything. That doesn't want to hide. But I have a shortcut to love here. If I double click on that, I get the same exact window there as well. So the reason I'm able to access the binary through my command line is because I've actually aliased it. So if anybody is curious, there's actually instructions for this on the Wiki page. But if I go to nano home directory bash profile, you can see that I've aliased to the command to love to be Application.love.app /Contents/MacOS/love. This is another Mac slash Linux specific thing. But for today's examples, it suffices just to double click the executable that you download from LOVE2D and/or drag your project folder onto that executable. And, again, the instructions available for how to do what I did are on the Wiki. I've got to figure out where the exact page is. I think it's on the Wiki here and then the Getting Started page, yeah. So on the love2d.org/wiki/gettingstarted, you can get some more instructions on to actually pull up LOVE2D, you get it to work by itself, get it to work with files specific to your operating system. Again, I'm using Mac OS X today. Mac OS X is similar to Linux, but Windows will be slightly different. But that's LOVE2D. So once you have that all set up and ready to go, if you've double clicked the executable and you've got that window, then we should be ready. So I'm going to open up my text editor here. And I'm just going to fire up a very simple LOVE2D application just so we can see sort of the backbone of how it works. By the way, sorry, I haven't been paying attention to the chat. OK. So Bhavik says, "I will try Atom. I know a bit of Vim. David uses it. I'd like to useful plugins for Vim." I don't use Vim too often, so I can't actually suggest useful plugins. I know that Jordan Hayashi, one of our alums, uses Vim a lot and has a lot of plugin help. Maybe on another stream, if David gets on, we can maybe ask him what plugins he likes to use. I am not a Vim expert by any stretch of the imagination. But, yeah, Atom is great. Visual Studio Code, VS Code, is great as well. So I'll be using those. But perhaps in the future, we'll take a look. And then Sugo Gruber from Germany, Bhavik Knight says, yeah. OK, awesome. So we're going to go ahead and let's say that I want to create a new LOVE2D application on some location on my hard drive. So I'm going to assume that I have a folder called Dev somewhere. Within that, I'm going to say just for the sake of today I have a stream subfolder. And I'm going to create a new folder called Snake. And in that new folder called Snake, I'm first of all going to open that folder with my text editor. So on Mac, you can just click and drag it onto your text editor. And that will open that text editor in a Project view. I'm going to click and close out the other window that I had available. And I'm going to right click, do a New File. And I'm going to call this file main.lua. Now, in LOVE2D, main.lua is sort of like the entry point of your game. And that's going to be necessary in order to tell LOVE2D where to start running your game. And in C, and even in Python in certain situations, you have the same exact idea. In C, you have this function called main, which the C compiler knows to look for and then bootstrap your application. In Python, you can do this check that says, if name equals main. And, basically, that will launch that Script file as the bootstrap of your application if you have multiple script files. In LOVE2D, the same aspect applies. But it looks for a file called main.lua. And this should be in the root of your project directory. We've got some other people, Blow Into The Cartridge, has a Heart Symbol, love the name, Belacura, Bhavik Knight says, "sure, hello all." Thanks very much for coming to today's stream. Hope you're ready to code some Lua, some Snake. OK. So I have my main.lua file, but it's completely empty. So it's obviously not going to be useful for anything. I'm going to just write a couple of functions here. So function love.load is one of them. And this is using Lua, the language Lua. So we've got Lua syntax. So we have function, the keyword that says this is going to be a function called love.load. Notice the function ends with this end keyword, which is a Lua specific thing. I want to make a function called love.update. Notice that load does not take a parameter, but update does take a parameter called dt which means that love.update will have access to this variable within it. And we'll take a look at what that means a little bit later. I'm going to define a function called love.draw. And this is the function that's going to handle everything related to actually rendering things onto the screen. And then that should be it for now. Oh, actually, one more function, I'm going to do this just out of convention. It doesn't really matter where you define these functions in terms of their order. But I'm going to define a function called love.keypressed. And this one takes a key parameter. So this function, as you might surmise, is used to check whether a given key is input on any given frame. And with that key, we can then do some sort of logic. In this case, very simply, I'm going to do a condition here that says, if the key is equivalent to this string called escape, which means if the user pressed Escape key, then I want to call this function called love.event.quit. And very simply, this will allow us to test whether our application is running correctly. So I'm going to, first of all, make sure where I am. I'm in my home directory. I'm going to cd into that folder that we created. So cd dev/streams/snake and then ls there for list, which means I can see sort of what files are there. I can see this main.lua file that I created. And so if I just type love space dot, what I should see is a black window, because I haven't defined any rendering. But if I press Escape, which I did, the window closes. So everything seems to working appropriately. Black screen is completely normal. Notice that when we open that up, it said sort of this untitled word at the top of the window where the window bar is. So if I do love.window.setTitle to, let's say, snake, or snake50 rather, and if I rerun this as I did before, notice that, oh, it did not work. Did I forget to save it? Make sure I'm in the right place. love.window.setTitle snake50 should work. Huh. Strange. OK. Not sure why that's not working, hiccup, minor hiccup. So this is in the Snake directory. Let me make sure. Oh, LOVE 11 may have changed the function actually. Sorry, LOVE2D v11 or love window setTitle. There are a couple of things that LOVE 11 has changed versus LOVE 10. So LOVE 10 was what I taught the Extension School course, the games course. By the way, I'll plug that here, cs50.edxorg/games if you're interested. This course teaches LOVE2D and Lua with version 10.2. And in the middle of the course, they actually updated LOVE to version 11. And they changed a lot of the functions therein. So part of that is going to be me figuring out what some of these new functions are. But it looks like they didn't change the setTitle function, so I'm a little bit curious as to what's going on here. If I run this-- oh, you know what? Hold on. Let's do a drawing example just we can figure out why exactly it's not working. "What is the name of the software you are using," says Elias. So Elias, this text editor it's called VS Code. This is just a text editor. And it's made by Microsoft. And it's kind of like a simplified version of Visual Studio, hence the VS part of it. And then the terminal here is just a shell that you would use on a Mac or PC. So on a PC, it's called CMD for Command prompt. And on a Mac, it's called Terminal. And I'm actually not entirely sure what it's called on a Linux machine. I think it's called Terminal as well. Dan, do you happen to know? It's called Terminal? Yeah, it's called Terminal on a Linux machine as well. And then, obviously-- Chrome to browse the web. But, yeah, the main two things are just the VS Code and the Terminal there. "Black text on black screens," says Dan Coffey. Oh, did I write that in the love.keypress function? Oh. Pff. OK. Sorry, yes. I thought I was in the love.load function. Thanks, Irene, for pointing that out. The love.window.setTitle wasn't working, because I thought I was writing it in the love.load function. But the logic that I was just writing was that if I press the Escape key, set the title to snake, and then quit the game. So we weren't actually seeing it. So that was a silly issue on my part. If I run the code now, now it's says snake50. So thanks, Irene, for pointing that out. And thanks for joining this stream. I appreciate it. I might need you to keep me in check while I make all these mistakes today. Great. So that was how to get a window up and running more or less and how to test for keyboard input. Does anybody have any questions before we get into maybe doing some stuff related to graphics? Is anybody having issue getting LOVE2D or Lua running on their machine? By the way, you can click and drag a folder onto LOVE if you're not comfy with the command line and you would prefer to just use more of a drag and drop interface. If you have a shortcut on Mac or PC and you have a folder within which is your main.lua, you can click and drag the folder onto the executable. And that will function the same way. In this case, interesting-- OK, hold on. There we go. So if you click and drag the-- I'm not sure why it didn't work the first time-- but, normally, you click and drag the folder itself, not the main.lua. Don't drag the main.lua, because the LOVE executable is going to look for a folder. Click and drag the folder onto your shortcut on your desktop if you have it there. And then that will trigger LOVE2D running the application. Blow Into The Cartridge says, "all good. Thanks for this awesome video. Stumbled upon it and already super hooked. Keep it up." Thanks so much, Blow Into The Cartridge, appreciate it. Thanks for coming by. I'm going to go ahead and start messing around a little bit with graphics. So I know that Snake is generally a grid-based game. It looks like it's mostly just made out of squares. So we can sort of visualize our game space as being this 2D grid, right? And in LOVE2D, we can visualize a coordinate system where on the top left corner is our origin or 0, 0 point. And anything in the positive direction is going downwards-- sorry, on the y-axis is going downwards. And on the x-axis, anything that's going sort of to the right would be positive as well on the x-axis. Inversely, if you were to go to the left side past the edge of the screen, that would be negative on the x-axis. And if you were to go above the screen on the y-axis, that would be negative on the y-axis. So you can always visualize anything that you put in your game world, whether it's a sprite or a shape, as having an x, y coordinate. So I want to start thinking about maybe how I can fill this space with my game world with a bunch of squares that sort of represent, you know, the ground and the snake as well. So there's a function in LOVE2D called love.graphics.rectangle, which takes a mode first of all, so whether I want a filled rectangle, so something like filled with color or just an outline of a rectangle. In this case, I do want a filled rectangle. But we're probably going to want something with maybe an edge as well, so that I can see the individual pieces of the snake. And now I just have one contiguous snake if it's just all a bunch of squares sort of chain together, right? And I'm probably thinking I want some way to visualize where the head of the snake is, too. Because especially as the snake gets really big, I'm going to kind of want to keep track of where it is. Otherwise, it'll get lost amongst all the green squares that I'm seeing throughout the game. So I'm going to just pass in a string called fill, which just tells this function we're in fill mode not in line mode. The other one would be line. And then I'm just going to say, I want to draw a square just in the upper left. So I'm going to say 0, 0. And let's just say my snake is going to be 16 pixels-- this square, rather, is going to be 16 pixels wide on this x-axis and 16 pixels tall as by the y-axis. I'm going to save it. I'm going to rerun it. And then notice that up at the very top left-- it's kind of small-- we do have a rectangle, but it's white which is interesting. So I didn't have an option to specify a color here. It just sort of defaulted to, oh, whatever I draw just make it white. It turns out that LOVE2D, just like OpenGL, is based on a state machine, state machine just meaning that the LOVE2D kind of has any one given state at one time. It has a set of variables internally that it manages that allows us to just change the way that it renders things. In this case, I want to do love.graphics.setColor to some value. And that will change the state of LOVE2D such that anything I draw after that will abide by that state transformation. So any color that I pass into this function will apply to this rectangle that I just tried to draw right afterwards. And this was different in LOVE version 10. But in version 11, all of these variables are now between zero and one that I pass into this function. And it's going to take four variables, a red, green, blue, and an alpha component. And these four variables are what determine the color of something. So in this case, I want a green square, because the snake generally is green. So I'm going to say 0 red, 1 green, 0 blue, and 1 alpha. And what that will translate to is no red at all, full green, no blue, and full alpha. And for those unfamiliar what alpha is, is basically how transparent or opaque that color is. So at 1, it's fully opaque, meaning I can see it completely. But if I were to say 0.1 or 0, it would be invisible or slightly visible. Bhavik Knight says, "my Scratch project for CS50 P set 0 was based on Snake, but tail wasn't dynamically increasing." Well, hopefully, we can demonstrate some techniques for potentially making that work today. And there's multiple different ways we could do it. I have a couple ideas in mind, but we'll see what may be best and what we can figure out live. OK. So I've specified the color of my state. Whoop, sorry. I'm going to save it first. Always make sure you save your project and rerun it. And then when I rerun it, notice that, indeed, we have at the top left no longer a white rectangle, but a green one. So it seems to be working quite well. Now, this isn't all that interesting, because, you know, we can't move the snake. It's not doing anything. It's just a static rectangle at the top left. But we do have something that we're drawing to the screen. That's a good start. But let's say now I want to move my snake to the right. We'll do it continuously, I suppose. And so we've been Messing around with a love.draw function, which is where you can define everything that you draw to your scene. We messed around with love.keypressed. Now, we want to sort of take a look at love.update, because it feels like now I want to actually update what's going on in my game. I want to change something in the scene. So in my love.update, I want to think about how I want this green rectangle to move. And I'm thinking I kind of want it to move to the right maybe just infinitely. I'll just infinitely move it to the right. So since we talked earlier about how everything exists on an x, y plane, sort of like this coordinate system, I know that if I want to move it to the right, the right is positive. And it's a positive transformation on the x-axis. So whatever I draw on this x-axis, which is here, this third or fourth variable in the rectangle function which is this x-coordinate, if I just increase that over time, that will have the result of my rectangle moving to the right, right? And if I were to decrease it, it would move the rectangle to the left and it would come out of view. So let's say I want to make this a variable, maybe call it snakeX. And let's go ahead, first of all, declare snakeX up here. local snakeX equals 16. So it's going to initialize itself to the value 16 which we had before. If I rerun it, notice that it stays the exact same. But I want to change this over time. So what I'm going to do is this variable dt is what's probably one of the most important variables in game programming generally. But specifically here in LOVE2D within the update function, this will allow us to update anything by multiplying dt by some value. Because every frame, dt is essentially the amount of time that's passed from one frame to the next. So between frame one and two, about, what is it, 0.16 seconds elapses, which is 1/60 of a second. And if we define a constant and multiply it, it'll move that constant or manipulate it based on how much time has passed between each frame. SP Vendor says, "isn't that the size of the rectangle you're changing rather than the position?" Oh, yes. So sorry, yes. That's correct, my bad. The x, y is actually these first two numbers, not the last two numbers. And in this case, this would need to be 0. So good call, Steve. Thanks for pointing that out. snakeX should be that first parameter. This is why we do it live, so we can see all these wonderful, wonderful blunders. OK. So, anyway, dt is basically going to allow us to multiply any transformation, any value, by the amount of time that's passed between frame one and two or between two and three. Just every frame, this will be a new value. Let's say I want my speed of my snake to be, oh, I don't know, 100 pixels a second, which it would effectively be. This will effectively be how much that moves per second. And if we multiply-- sorry, if we were to assign snakeX to itself plus the snake speed times delta time, what that'll effectively do, just sort of like plus equals in other languages like Java or C-- unfortunately, Lua doesn't have a plus equals operator which is unfortunate. But we can say take my value of snakeX and then add that speed value times delta time. So this will take place every frame, so 1/60 of a second. So this will be 0.016 approximately multiplying by 100. So over the course of 60 frames or 1 second, we'll have moved 100 pixels. It'll basically just update itself consistently. So now if I were to do that, notice that we are moving, indeed, about 100 pixels every second if you were to calculate it out. And the beautiful thing about this is it'll actually stay consistent between different hardware. So if you're running on a machine that's kind of slow and it can only process one frame for every three frames that another machine produces, the dt will be triple the amount in between the individual frames. And so this will still move it 100 pixels per second. Got a few messages in the chat-- "isn't this written-- yes, that's why I guess he's using the function update for moving positions." That's correct, Zad. We are using update to move positions. We do it so that we have access to this dt parameter. Because if I were just to do it without dt, right, and we're not scaling it by the amount of time per frame, this will run absurdly fast. First of all, you didn't even see it move-- well, hardly. Because it's just moving at an insane rate, basically as fast as my CPU is capable of running this operation. Or I guess, rather, I think, it's hard cap to 60 frames in LOVE2D. Often, with game engines, if you don't cap the FPS, this won't run at a cap. It'll run hundreds of times. But in LOVE2D, I do believe that this actually only runs 1/60 of a second either way. So if we're running at 60 frames, we are moving per second, that would be, 6,000 pixels. So we moved 6,000 pixels in the span of a second versus 100 pixels, which is not what we want, generally. That's too fast for the human eye to see. So we use dt to scale everything based on the time that's elapsed per frame. Time difference, Bhavik Knight, it's short for delta time, so, yeah, effectively the time difference between one frame and another, the delta between frame x and frame x plus 1-- I should say, rather, frame x and x minus 1. Awesome. OK. Sounds like everybody's on the same page here. So I have a moving snake, but it's not really responsive. I can't do anything if I were to run this. And I'm pressing the keys. I kind of want to have control of where the snake goes, right? I want it to move down, left, up, and right if I hit those keys. And I'll just kind of infinitely move in one direction. Moreover, I want it to move in such a way that, if it were to go past the right edge of the screen, it should actually come back-- sorry, reverse, mirror's flip. It should actually come back on the left side. So if the snake goes all the way to the right, it should come back to the left. And if it goes all the way up, it should come back from the bottom going up, so that we kind of have this looping world space. Because, otherwise, we won't see where the snake is if it doesn't come back, right? It won't work that way. So we have a few problems that we need to bite off here. The first thing that I think I'd like to tackle is changing the direction which the snake is moving in. OK. So we're going to need a couple of things here. So if we can move in not only the x direction, but also the y direction, I should probably make another variable called snakeY and also set that to zero, right? Because we're going to potentially not just move the x, but also move the y. Andre, "is LOVE's delta time constant, or will it theoretically be a larger value if the frame rate drops? Is there an equivalent to UNITY's fixed delta time in LOVE2D?" The first answer-- yes, it will be larger value if the frame rate drops. And in terms of fixed delta time, I do believe it does. Let's look up the LOVE2D Wiki, LOVE2D fixed timestep. This is somebody using it, I think, in a project, but not actually-- oh, this is like somebody manually creating their own fixed timestep. It may be something that you have to fix yourself. Typically, you use this sort of thing in physics calculations if you need to apply some physics transformation step by step rather than having it apply to some, have it be like, a multiplicative value. In this case, I don't know if LOVE2D-- I actually haven't used it myself. Or at least I think I did once, but it's been a long time. I don't remember offhand. I think you do actually have to write your own fixed timestep. And I think you can actually mess with a LOVE run function-- sorry, not LOVE runs out, LOVE2D run function-- which, underneath the hood, this is LOVE's function. Yeah, you can see it here. I think this is where you would ideally write your own fixed timestep code where you sort of perform physics calculations and whatnot and do sort of like minusing of the dt off of that larger timestep and then apply the update changes but this is where you can sort of see what the overall sort of main function of LOVE2D, or rather the game loop of LOVE2D, looks like. For those unfamiliar, every game has to have a loop where it runs, like I said, every sort of frame over, you know, usually 60 frames per second, every single frame doing some updating, doing some rendering, all that stuff. This love.run is LOVE's example or LOVE's implementation of this game loop. But, yeah, to answer the second part of your question, Andre, I believe in LOVE2D you have to actually implement fixed time step manually, not as it normally exists with update and dt. Good question. OK. So to move our snake, we have the ability to currently just move it in one direction. Let's test it first with being able to move it left and right. So if I were to, for example, press left, then instead of adding the snake speed to my snake, I should probably subtract it. Because that'll take me from going to the right and increasing my x value to going to the left, decreasing the x value. And I realized, again, that my character on the screen is flipped. So if I do key =, for example, left here, then I should be able to say-- let's say I need to keep a variable of this, whoops. snakeMoving = left, right? And then-- else if key = right, snakeMoving = right. And then up here I'm going to need to keep track of that, so snakeMoving = right. So now I have a variable that it'll at least let me toggle back and forth. It's given my snake kind of this state where I can say, OK, am I moving right or left? And if I am moving right, add the value. If I'm moving left, subtract the value. To do that actual math, that's going to take place here in the love.update function. And then what I can do is basically say, if snakeMoving is equal to left, then taking this statement that I wrote before, I should probably, instead of adding the snake speed times delta time, subtract it like that. And then else, it's going to only be for now left or right. I can say snakeX = snakeX + SNAKE_SPEED times delta time. And if everything is going according to plan here, I'm moving right right now. If I press left, oh, indeed, now my cube is moving to the left instead of to the right. And I can switch back and forth as needed. And it's got this sort of continuous motion. So we've taken a step closer in the route that we need to actually get a sort of snake moving around the screen. Does anybody have any questions so far as to sort of what we've been doing? Looks like you haven't had any questions in the chat just yet. Feel free, ask as many questions as you want. I'll be monitoring the chat. I'm happy to answer, happy to talk about whatever folks are interested in. But if there are no questions, we'll just go, I think, dive into sort of how we might do this by moving the snake up and down as well as left and right. "Would it work to do something like snake moving = key without the if else if?" Yes, in theory. But this key can be anything, right? So if I were to hit A, or Escape, or Spacebar, or Shift, or Return, it's going to get that value. So it would only work in this particular case if I input one of those two keys. Now, we do have an else if key-- oh, actually, it's not going to evaluate either these statements as true in that situation. So it's just going to stay on whatever its last value was even if we tap a weird key. But, generally, that's not an ideal practice if you expect that you can get a lot of other values and if you're sort of parsing those values in fairly complicated ways. In this case, it would kind of work. But I would recommend against it probably. It's all a little bit more obvious to know what's going on here, too. Good question, though. It's a good thing to sort of try and stay conscious of. You know, a lot of like hash map related logic can derive from that sort of way of thinking which can optimize your workflow. OK. So let's go ahead and do the following. I'm going to add a couple more conditions here. So I added two more conditions. And these two conditions are just to take into consideration whether you're moving up or down. So in this case, snakeMoving = up, snakeMoving = down. And also, another thing about this particular instance is we're using strings to test for values. Sorry, we're using strings to assign sort of state to something. Generally, you wouldn't always do this, because are a little bit more weighty to use as state variables. In this case, for the key checking, they've defined a bunch of different states. But if you have a sort of snake class or snake object that has maybe 15, 20, 30 different states, I would probably use an int or some value and not a string just to save memory, just be a little more efficient in terms of actually checking for comparison. Because it is a little bit more intensive to check against a string than it is to check against some integer that we've defined. For example, if I've declared a table up here-- by the way, we'll get into tables in a little bit. If I have a States table, for example, up, down, left, right, now I've actually created-- sorry, and an up in this case. These wouldn't exist. But let's say one, two, three, four. And let's say maybe those are defined elsewhere as up, down, left, or right. Now, I can just, for example, up = 1, down = 2, et cetera. Now, we can actually do a comparison against an integer rather than a string and just save us a little bit of processing power. But we won't worry too much about those kinds of optimizations today, because those are not something that we need to worry about for a project this small. That would be for something a little bit larger scale. But, anyways, the next part of our logic-- so we have, you know, the simple basically take a key, assign whatever direction you're moving to its state, right? We have some sort of state with our snake whether its moving up, down, left, or right. But I want to not only test for left or right. I also want to test for up and down. So we can kind of do the same logic that we did before, else if snakeMoving is equal to right, we can do that. Else if-- whoops, it wants to auto-indent on me-- snakeMoving is equal to up, then do this, whatever that might be. Else if snakeMoving is equal to down, then do the following. Now, these are going to differ, because we are no longer going to add or subtract from our x value. We want to add or subtract from our y value. Because our y value is what's representing our up and down in the game. So I'm going to go ahead, I'm going to copy this. I want to just paste this in here, change snakeX to snakeY. Notice that my editor lets me select multiple things and sort of edit them, which is kind of nice. And then this will function in sort of the same way, except this needs to be plus. So if we're going down, remember y increases as it goes down and decreases as it goes up. And, here, don't forget we're always going to be at zero unless you replace our y-coordinate with the hard-coded zero, to the y variable that we're storing now. OK. So let's go ahead and run this. Whoops. Then expected near snakeX on line 30. Ah, right. I forgot a then keyword right here, very important. So, now, I'm moving left or right. If I hit Down, notice that now we can move down. I can move up. And I can keep moving sort of arbitrarily around the scene, so pretty cool. So we've taken a few steps in the right direction here. Anybody have any questions on sort of what's going on here at all? And if not, we'll go ahead and maybe talk about getting everything working in a grid perhaps. OK. Cool. And this will be where we have to spend a little bit more time thinking about sort of the data structures and algorithms that underlie the game. But right now, we just have continuous movement along either the x or y-axis. But I want to make my game, first of all, a little bit easier to deal with. Because if I can have it in a grid, I don't need to actually worry about continuous collision detection which would be maybe a topic for another stream. But I can just have a very discrete layout of squares and basically check is there a snake piece in this square, if there is, and then do whatever logic I need to do. You know, check to see whether my head is biting the tail of the snake or whether there's an apple in that tile, for example. We basically need to think about that way of looking at the problem. But it's just a little bit easier to also render it and just check for those sorts of things, right? Collision detection, if it's continuous, we have a little bit more to worry about. We have to actually put our snake into a series of bounding boxes. Every single piece of this thing needs to have a rectangle that represents its collision. And we need to check to see whether any of those individual rectangles are overlapping some other rectangles. But, again, we won't worry about that today. Because Snake is a grid-based game, we actually get a little bit of that for free. And the same logic sort of applies to tile-based games, too, thankfully. ""[INAUDIBLE] is used all the time in this context, not a time derivative, which would be an infinitesimally small value, whereas delta time is just a small, but real, value, something like 0.01667." Correct. Thanks, Andre, for clarifying that. OK. So, now, we can sort of talk about tables, because tables are very important. In C, you get these things called arrays. And in Python, you get lists. And in Java, you get arrays as well. And JavaScript gives you arrays. Generally, you see them as arrays or lists. In Lua, there isn't the notion of an array or a list, but rather a table which is actually a combination sort of of lists and things called hash tables or hash maps where you can assign some sort of key with some value and pair them together, and then look them up with a very quick lookup time versus having to traverse a list and look at each individual value. Today, we're going to use the table data structure as a list. So I'm going to say I want a local tile grid. And, again, local, I don't know if I talked about it. Local is basically just saying I want this variable to be exclusively within the scope of where it's defined. For example, if I were to declare a value called local val = 1 in my love.load function-- sorry about that. If I have a variable val that's defined as local within this love.load function, I cannot access val outside of this function. So if I were to try to do val = 3 right here-- well, actually, in this case, because there's no typing, it's just going to override this value. But they're not the same. But if I were to do print val, for example, where it needs to access a value, access of variable that's been defined already, it's going to throw me an error and say, oh, val is not declared anywhere. It doesn't recognize that symbol. In the case of local defined here, because we're defining it at the very top of the file outside of any functions, it just means if I have other Lua files in my project they don't have access to that variable. But if I were to say, tile grid without local, then other files that import this module or make reference to it actually can get access to that value which we don't want. So I'm going to do local tileGrid = and then these two curly brackets here which basically tells me I want an empty table here. And then we're going to fill it with whatever information we need. ""[INAUDIBLE] relatively bigger time difference." Correct. OK. So I have a empty table. And I want this table to be representative of my game space, my game window. And I want to figure out how many tiles we can actually fit within my game window. And so I'm going to, I believe it's, love.window.setDimension-- SetMode, sorry. And then this is where you can actually specify how large your window is when you open it. And I think the default, is it like 800 by 600? I'm not entirely sure what the default size is. But let's just say I want it to be 1,280 by 720. And then what were the other parameters? I think it also has some flags. We could say full screen is false. Resizeable is true, those sorts of things, which allow us just to resize. I'm actually going to set realizable to its default which I believe is false. And then I'm going to run this, make sure I used it correctly. Now, notice that it fills my entire screen. Since my resolution on my monitor is currently 1,280 by 720, the window itself now fills the entire monitor which is useful. Now, that I know what my resolution is, I can say, OK, maybe I want to have all of my squares be 16 pixels. Or, actually, let's do 32 pixels. Let's make our square size 32. So I'm going to say TILE_SIZE gets 32. And then just to have good style, I'm going to say window width is 1,280 and window height is 720. I'm going to come down here, replace this with window width, replace this with window height. And now that I have these values, actually I'm going to put this down here since this is gameplay. Whoops. That was not what I meant to do. I didn't copy it. Copy. Paste. I'm separating sort of the more rendering stuff from the gameplay stuff. So the snake speed is 100. The window width and the tile size are up here. I'm going to go ahead and say I need to figure out, basically, how many tiles can I render on the x-axis and how many tiles can I render on the y-axis. Because I'll need to know this when I start assigning positions of all the individual tiles, right? So I'm going to say tiles-- rather, MAX_TILES_x = WINDOW_WIDTH divided by TILE_SIZE. And MAX_TILES_y is WINDOW_HEIGHT divided by TILE_SIZE. And assuming that those divide evenly, this should work out just fine. I'm actually not sure offhand. I think those divide evenly. Let's figure that out. So 1,280 divided by 32 is 40. And 720 divided by 32 is-- oh, that's not, 22.5. OK. So I need to actually, because it's going to truncate it down to 22, because it's an integer-- recall, 1,280 divided by 32. No. I apologize. No, Lua actually does do floating point arithmetic I believe. So toint I believe is the function plus 1. So now this will take whatever the value of the expression that I'm evaluating here is, assign it to a integer, and then add 1 to it. So it's going to bring 22.5 to 22 and then add 1 Actually, as a sanity check, let's make sure that's correct. I'm actually not 100% sure offhand. Lua, I'm going to say 22-- or I'm going to say, what's 1,280 divided by-- sorry, 720 divided by 32? Print 720 divide by 32. OK. It does do floating point as I thought. sorry so integers and floats, they aren't really separate data types in Lua. Lua has this data type called a number type, unlike languages like Python which do have floats and ints. And C, which actually enforces that you use floats and ints and adhere to it more or less, well, with some type coercion, the number type will just automatically do floating point arithmetic when you divide. So unlike some languages where it just gets turned into an integer, in this case, it'll actually get turned into a float. And actually I'm realizing now it's going to be like half of a tile off of the bottom, which is going to look a little bit weird if we get to the bottom. So what I'm actually going to do is I'm not going to add one. I'm just going to take the int of this, so that it'll kind of come up from the bottom. Because we're at a resolution where it doesn't evenly map 32 pixels divided on both axes, we're going to deal with it. There will be a little bit of black space at the bottom, but that's OK, better than it sort of cutting off the snake when it gets to the bottom of the screen. OK. So now we know how many tiles we need to render on the x-axis, how many tiles to render on the y-axis. I'm going to figure out a way to draw this. I'm going to look at the chat really fast just to make sure it didn't miss anything. OK. Hi, Nimble. Good to see you. Thank you. OK. Bhavik Knights says, "take the greatest common denominator?" Yes. OK. Yeah, we could do that if we wanted to enforce our resolution sort of work. But because, generally, 16 by 9 resolutions are kind of the way that we want-- yeah, yeah tile size 40 would absolutely do it. You don't really see tile sizes that are-- yes. And, Jeffrey, you could use the round function as well. That's correct. But, sorry, the 40 pixel size on the tile it's something you don't really see too often in games. You could absolutely do it and be fine. But a lot of the time, graphics, there's like sprites that you get online or that you have your artist create adhere to-- oh, yeah. Irene, good point. Yeah, I would round to 23. But, yeah, if you are looking to round something, my approach is a little bit more-- oh, wait. Actually, yeah, I would round to 23 which was the same point that I made before. Anyway, what I'm trying to say is 32 pixels, because it's- what is it-- a multiple of two, it's something that you'll just generally see more often than something that's 40 pixels. And it's not necessarily because of any technical reason, although this was more so the case back in the day. But, nowadays, you'll see a lot of retro art is just kind of modeled after that old school approach. And it's more of a convention than it is anything else. OpenGL used to enforce that textures were a power of 2. And old hardware just kind of enforced that blocks were drawn in tiles of 32, just because that's the way the hardware was modeled. Because it was just circuit, you know, binary circuitry effectively. But, yeah, suffice to say, 40 pixels would work here. But it's not a trend that you would often see, I don't think, in games unless you wanted to do it as a creative decision. And it wouldn't strictly map well to changing resolutions to weird other-- like, because resolutions can vary all over the place, too. You kind of just have to take one sort of trend for your art or one size and make these sort of decisions where you maybe don't display half of the bottom row of the grid to make 720P work, basically. So good points all around, though. "True, we will need a custom function that will get a second parameter to specify if we are going up or down? True, yeah. And you can you can also use math.ceiling or the ceiling function and the floor function which they will give you the highest integer around it and the lowest integer around it, respectively, regardless of where the number actually is. "Don't forget to change the tile size in the last function," says Steve. OK. Let's take a look here. Oh, right. The actual rectangle being rendered? Correct. Yes, good point. Thank you for bringing that up. And that is why we defined that up there, so TILE_SIZE and TILE_SIZE, right. So what we were getting at is now that we've defined how many grid spaces we want sort of on the x and the y-axis, we should just sort of like sort of draw out everything. We can make sure that we've aligned everything appropriately. So what I'm going to do is I'm going to use a for loop, a nested for loop. I'm going to say for every sort of column in the game. And then within each column, I'm going to say for every row, right-- I guess it's be backwards. For every column and then every row in that column, I want to render a tile in that x, y position, right, times tile size. And we effectively looking in those increments of 32 pixels. So I'm going to set my color for debugging purposes to blue, actually. I'm going to say RGB to 1, right? So if we render this-- whoops, toint-- oh, sorry. Oh, sorry. Is it to number? Oh, sorry, no. You have to use floor, math.floor in this case or math.round, not toint. Is that correct? Hold on. Oh, yeah. Because, duh, there's no int in Lua. So you can't actually do that. So in this case, I'm going to set it to math.floor. And that should work. OK. So now we have a 32 pixel by 32 pixel blue tile moving around, but we're not actually trying to figure out where the blue tile is. I want to make it cyan. I feel like that's easier to see on screen. So I'm going to do something like this. For y = 1 for the-- what do we define it as-- MAX_TILES_y do for x = 1 MAX_TILES_x do. And what this is doing is it's a for loop. This is Lua's version of a for loop. But we're saying we have our initializer here. For y gets 1 until it reaches the value of MAX_TILES_y do whatever's in here and then the same thing for every x-coordinate, x position. For x = 1 until MAX_TILES_x, do this. And so if our number of tiles that we can fit on, let's say, the x-axis is like maybe 40, it'll go from 1 to 40. And then same on the Y-- 1 to 30 or 1 to 40, what have you. Don't forget to do-- OK-- making sure I'm up to date on the chat. And then what I want to do is, for every one of these positions, I wanted to call love.graphics.rectangle. And I just want a line function right now. I want to see my grid visually. So I'm just going to use lined rectangles. I'm not going to use filled rectangles. And then I'm going to hide that left panel there, so we have a little more room here to work with. I'm going to say, because we need to do a x, y first, I'm going to say x minus 1. And this is an important thing to be conscious of, because normally loops in Lua they start at 1. And, also, array indices, tables, start indexing at 1, which is a kind of a weird Lua-based thing. So we want to actually subtract 1 from x and y, so that it maps to our coordinate system. Because our coordinate system actually starts at 0 just like most things in CS usually do. Lua is kind of a black sheep in that it conventionally and syntactically a lot of its things start with 1. So I'm going to say x minus 1 times TILE_SIZE, y minus 1 times TILE_SIZE. I'm going to then say the size of that rectangle on the x-axis and the y-axis, which should be just that. And if I've written everything correctly, I should now see a cyan grid. And I do. Perfect. So this is an indication of what my grid looks like. It kind of gets cut off at the bottom, because my monitor is not quite tall enough for me to see the whole window. But, clearly, we're moving not only anymore in just the, like, black space. But we've outlined this grid that we're going to start adhering to. Now, if you notice, the square doesn't adhere to this grid in any kind of discrete way. It's sort of continuously moving throughout the world space. And you can see it can kind of get like caught up in between coordinate points and a little bit weird. So what we need to do is lock our square to this grid. We need to hard lock it. And I'm actually going to minus 1 off of the tile size on the y-axis, just so we can see it a little bit better. And it looks perfect. There, now, it maps perfectly to it. We're not going down a little bit too far. But, yeah, again, we're going to need to align this square with our grid so that it's completely locked into it. Let's keep the grid active for a little bit. And so what I'm going to do, actually, I'm going to take this code. I'm going to take it out of here. And I'm going to define a new function called drawGrid. And I'm just going to paste that into there just like that. And then I'm going to define another function called drawSnake. And I'm going to call it here. I'm going to say drawGrid, drawSnake, right? Because I want the grid to draw first, and then my snake to draw on top of the grid. I'm going to paste that bit of snake drawing code into the drawSnake function. And then just so I know the difference, the clear difference between them, I'm going to set the color to green on the snake just like that. So if everything's correct, now I have a green snake rendering. And it might be hard to see on stream, but I have a green snake rendering on top of a blue gridded background. So if anybody has any questions, definitely let me know in the chat. The next thing that we'll tackle I think is sort of aligning our snake with this grid, and then apples, and then eating apples, and growing the snake. Well, we'll start with just eating apples. And then we'll add to the end of the snake. So any questions at all? I'll monitor chat for a little bit just in the corner of my eye while we-- oh, I also kept this local val = 1. Let's get that out of there. Don't need that anymore. And, yeah, perfect. So we have this grid table. In this, we're not actually using the grid table for anything. But we will start using the grid table. I'm going to assume that each grid tile, each tile in my grid, should have some value whether that's a 0, meaning that there's nothing there, no snake, no apple, a 1, meaning that-- or maybe a 1 for the snake just so I can differentiate the color. And then a 2 for the snake and then maybe a piece of the snake body. And then 3 for the apple that I want to eat with the snake. And let's just say that that's the extent of our game, the mechanics of our game. You have apples. You have the snake head, and then the snake body which can move around. Basically, what I want to do is I want to take this table. I want to set all of its values to the 0, 1, 2, or 3. And then I want to iterate over the table, basically, and do the same thing that I did down here in drawGrid. So what I need to do is initialize the table. So what I'm going to do is I'm going to make a function called initializeGrid. And I'm not going to make it take any parameters, because our grid is global in this case. So we'll just assume that it stays global. I'm going to go ahead and kind of just do the same thing. I'm actually going to just take this here, right? I'm going to leave it in there, because we're going to sort of need that same logic. But I'm going to copy this. And then I want to do something in here where I can initialize the table to something. Now, for each column I'm basically going to want a table. In order for this to work, for my table to work as a data structure for this example, I'm going to want what's called a 2D array. I'm going to want a 2D grid of integers, basically. So it's 2D array in C. I get a 2D list in Python, although Python's a very flexible language where you can have lists of tables of all sorts of weird stuff. Lua's kind of the same way, but we're going to have just a bunch of tables within this table. And then those tables are going to kind of represent our rows rather. So I'm going to say table.insert into my-- what did I call it-- a tileGrid. I'm going to insert into tileGrid an empty table. So the result of this is that for every row, I'm basically counting down on the different rows. For every single row for every y value, I'm going to add to an empty table into my grid, my initially empty grid variable or table variable. And then within the x loop of this initialize function, I know that I can index into this table my tileGrid at the index that y is. Because whenever I insert a new table into that grid, tileGrid, I will have, basically, tileGrid 1 if y is 1 equal to a new table. If y is equal to 2, this will exist. This will exist. This will exist every time I increment y. So for that reason, I can go into the tileGrid1-- or, rather, y, which will be 1, 2, 3, 4, or 5, until MAX_TILES_y. And then I can insert some value there, because that's where we're actually going to store the integers, the 0, 1, 2, or 3. So I'm going to set them all by default to 0, right? And I should be a little bit better about this. What I'm actually going to do is I'm going to say TILE_EMPTY = 0, TILE_SNAKE_HEAD = 1, TILE_SNAKE_BODY = 2, and TILE_APPLE = 3. So, now, I'm not using what are called magic numbers, which we teach in CS50. They are sort of constant variables. And I can replace this with TILE_EMPTY-- so a nested loop basically going through our entire grid for the size of the tiles on the y times x effectively and then initializing all of those to 0, all of the tile indices to 0. "I've never had the chance to get into Lua, but I saw you easily control the snake with your keyboard. Is it a code you did, or something the LOVE library allows you by default?" How I was able to control the snake was the keypressed function here. Basically, I'm looking to see-- anytime you press any key in LOVE, it's going to call this function keypressed. And it's going to have some value that this key variable gets. And it's going to be a string. And in this case, I can check, for example, is it escape. Did the user press Escape if they pressed a key? And if it is, I can quit. If it's left, right, up, or down, I can change some value that my code lets me do. So snakeMoving is a variable that I declared, which allows me to move left, right, up, or down. And then in my update function, which is a function that occurs every 1/60 of a second approximately depending on how your computer can handle LOVE2D, which generally it's fine, every frame that elapses love.update gets called with a parameter called dt, which is the amount of time that passed since this frame and the last frame. And the snakeMoving variable will have left, right, up, or down set to it, right? So then if it's moving left, I can decrease this snakeX variable that I declared earlier. The snakeX variable is just my position in the 2D coordinate system, so its value on the left and right, the horizontal axis. And same thing with up or down-- letting me manipulate my snakeY variable, so whether the snake is going up or down. And then in this case, what we're doing is we've defined some speed that I want my snake to move, but this will be sort of relevant still. But it won't work exactly the same going forward, because we're going to start transitioning into a grid and discrete movement. But the SNAKE_SPEED variable here we're multiplying by delta time, which means that no matter how many frames have elapsed between now and the last frame, or sort of how many frames are skipped if you're running on a machine that is a little bit slower potentially, this value will be consistent. It will consistently move you to the left, right, up, or down by some value over time. And so we, therefore, use snakeX and snakeY in our love.graphics.rectangle function call down here when we call drawSnake. And then notice that drawSnake itself is called up here with drawGrid in our love.draw function. So there's a lot of pieces. We've put this all together. This isn't something necessarily that LOVE2D has allowed us to do. But it's made it fairly easy in as much as we can check for user input and how much time has elapsed since the last frame. And we have these nice convenient functions for drawing simple graphics. These are the things that LOVE2D gives you. And it's up to you to take them and program them to fit the needs of your particular game, so good question. "How to do a main menu, like a start, play, or option?" That's a little bit more complicated. You would generally use what's called a game state machine and have a sort of certain object that keeps track of what state you're in. So am I in the main menu? Am I in the play state? Am I in the game over state? And if you are in any particular state, you render a specific set of things, whether it's the interface options, like pause game, start game, continue game, whether it's a character running around a field, anything that you name it. But, basically, you have different things you render upon a different state being active at once. And you toggle some variable that keeps track of what state you're in, so good question. We probably won't do this ourselves today. But in a future stream, I'd be happy to talk about implementing a state machine and going into maybe a menu for a game or something like that. Shayna says, "did you align it vertically to the grid, or it's not necessary when the snake moves?" We did not align it vertically yet. So if we run it, we can actually see that we can sort of move it wherever we want to. And see, now it's kind of stuck between different axes, different grid lines. So it's up to us as our next step to actually do this alignment, which I'll demonstrate how we do very shortly. Irene says, "to draw row by row, otherwise you draw a column by column I guess." Bhavik Knight says, "I don't understand why column is the outer loop and the row is the inner loop." You can technically do it either way, but the trend is generally to do the y on the outside. Because if you were to visualize a table as being like this, for example-- and this is essentially what our table's going to look like. If we have a smaller version of our table, you can kind of see that, if we're iterating over the y, we're going table by table by table. Basically, if we were to look at our table row by row, we're actually looking at this being an element, this being an element, and this being an element. So it's easy to iterate first over y, adding a table, adding a table, adding a table on the horizontal and then, once we've added those tables, to iterate over the x and populate them with these values just like this if that makes sense. It's generally easier just to think about the problem that way, but you can, I believe, just technically do it either way and just have your rendering code address it properly. But this is what you'll see vastly more often is the y first and then the x. Good question. OK. So we have a grid available. Let me just make sure I haven't added any unnecessary code. So, now, we've done a little bit of modularizing our code. We've taken some of these statements out. We've put them into functions. We have an initializeGrid function, drawSnake, drawGrid, all these sorts of things. In the love.load, this is where I actually want to initialize the grid. So I actually have to call it somewhere before it'll actually execute, because all we've done is just initialize a function-- or rather, sorry, declare a function. But we haven't called it. So I'm going to call the initialized grid function here, which will actually run that code and populate our tile grid with all that information. So, now, we have a grid of 0s, effectively, being the size in the x and the y of our game window. So what I want to do is, in our drawing code, this is where now I probably want to-- or rather, for the drawGrid function, this is probably where I want to check to see what exists in that grid, right? What's going to be the value at any particular index in our 2D array? Because if it's a 0, remember, I want to draw an empty tile. And if it's a snake head, I want to draw the snake head. If it's a snake body, if it's an apple, I want to draw different things, basically change the color of my OpenGL state, of my LOVE2D state. So I can do if the tileGrid y, x is equal to, let's say-- how did I have them-- TILE_EMPTY, do-- sorry, not do, then. I always get those confused. If the tileGrid at index y, x-- which, remember, arrays slash tables, they start at 1 in Lua by default. If it's equal to TILE_EMPTY at whatever y, x, I want to call love.graphics.rectangle line. And then I'm just going to draw a white grid, actually. So I'm going to set this up here to be 1, 1, 1, 1, the love.graphics.set color. That will make the entire grid white. So all 1s is white. And all 0s is black. And, specifically, these three, this value here is just the transparency. This has to be 1 for it to be pure white. If it's any lower, it will just be kind of a transparent white. But black will render the same either way whether this is 1 or 0. I'm going to draw the x minus 1 times TILE_SIZE y minus 1 times TILE_SIZE. Remember-- because the coordinate system is 0-based, not 1-based, even though we're 1-based currently in LOVE2D's tables. And then I'm going to do TILE_SIZE and TILE_SIZE, right? So if this is correct, then the grid is only going to draw if that index at that grid is a 0, is TILE_EMPTY variable, which we have declared up here to be 0. So if I run this, notice that it indeed works. We now have a white grid instead of a cyan grid. And if I were to go here, else if love.graphics-- or sorry, if tileGrid y, x is equal to TILE_APPLE, for example, then I want to do this same exact thing. I'm just going to copy and paste it. Actually, no, the same thing is going to happen. Sorry. I have the logic mixed up. This is the important part. We're going to draw this either way. Although that's not technically true, because sometimes we want to draw a filled and sometimes we want to draw a solid. OK. Never mind, scratch that. I'm going to do this. I'm actually going to take this setColor function. That's going to go into the if statement. So change the color to white for the grid. And notice that I'm using comments with a dash dash. So that's how you do comments in Lua is using the dash dash. To do block comments would be dash dash square brackets square bracket. And then now anything within that will be considered a block comment. But I'm just going to use a single line comment for now. So we're going to set it to white and then draw a line rectangle if it's equal to TILE_EMPTY. But if it's equal to TILE_APPLE-- which you don't actually have any in the scene yet, but we will add one just to demonstrate. love.graphics.rectangle fill, we want it to be a solid red rectangle, not a line rectangle in this case. I'm going to the same exact thing-- times TILE_SIZE y minus 1 times TILE_SIZE and then TILE_SIZE and TILE_SIZE, right? And then I want to also make sure that I set the color to red, which is the 1 in the R, 0, 0, 1. So red is 1. Green is 0. Blue is 0. Alpha is 1. Change the color to red for an apple. So just a little bit of commenting, documentation goes a long way. Make sure, as you're writing your application, that you try to comment things that are maybe not super obvious in this case. These are pretty fairly obvious things to comment, but it's still helpful at a glance to know what's going on. Elias says, "do you stream every Friday?" Not yet. This is only our second stream, but the goal is definitely to establish a consistent streaming schedule. And I'll probably be doing a lot of game related stuff. But we have other people that were talking about to maybe do some web development or Python stuff, maybe even get some GitHub stuff. So definitely, if you have any ideas and you want us to stream any other type of content, let us know. Write us here or comment on our Facebook. And just let us know sort of what would be useful. Anyways, the logic is now in place for drawing an apple. So if we run this, notice that it's not going to draw any apples at all. Because we don't have any grid indices in our table that are set to 1-- or, sorry, it's 3 I believe is what I made it. Apple to 3, right? So in order to do this, we can initialize our grid. Let's go down to the initialized grid function. As part of our grid initialization, let's just say I want to choose a random x, y and make that an apple so this is actually pretty easy to do. I'm just going to say local appleX, appleY = math.random MAX_TILES_X and then math.random MAX_TILES_Y. And notice that I'm doing dual assignment here. I have appleX, appleY with a comma. I'm assigning it to math.random MAX_TILES_X and math.random MAX_TILES_Y. This basically lets me create two variables and initialize two variables. And the two will match up, assuming that there's the same number of arguments on each side or variables on each side. And there are certain situations where this won't exactly work if you have variables that equate to multiple return types, return variables. But we won't get into that. But basically what this is doing is it's saying get an x and a y-coordinate for the apple that we want to place. And then give us a random value between 1 and some value, so MAX_TILES_X. So give us some value between 1 and MAX_TILES_X, which will be anywhere on the horizontal plane within our game. And then same thing for the y-axis-- math.random MAX_TILES_Y. And if I go into tileGrid y, x, I can assign it to-- sorry, appleY, appleX, I can assign it to TILE_APPLE, right? So I've initialized these two, this X and the Y. And I've added them. Remember Y comes before X. And then I run it. And then, boom, I get a random apple there or some representation of what an apple is in our scene somewhere. And obviously, if I try to go over to it with my current implementation, nothing's going to happen. I should just overlap it, but that is how you would randomly assign something in your game, very simple random generation. OK. So we've done that. Now, something to think about-- every time I run this, pay attention to where the apple is. Notice that it's going to be in the exact same spot every time. That's not very random, because the random number generator in LOVE2D gets initialized to some value that's consistent every time we run the game. So appleX and appleY are always going to get the same value no matter how many times you run this application, even though it says math.random. In order to fix this, we need to do what's called seeding our random number generator. We give it a seed value. And that seed value is going to influence the algorithm that takes place underneath the hood when it comes to actually generating our random value, our random x and y value. The random number generator does a bunch of stuff where it uses some math that gives us the illusion of a random number It's not really random in the technical sense. But it works well enough for us to perceive it as being random. Elias, "thanks for teaching us. Keep up the amazing--" thank you so much for your comment, Elias, really appreciate it. Thank you for tuning in today. I'm going to go ahead and do what's called seeding this random number generator. I'm going to do it in my load function, because that's where it should ideally take place before I initialize the grid, obviously, so that it doesn't use this default seed and then seed the random number generator, right? So I can do math.randomseed. That will allow us to seed the random number generator and give it some value. And then we need to actually give it a value that varies every time we run this game, right? Because if we seed the random number generator with some 0, 1, 2, 3, value, that's effectively the same thing as just letting it give us a random seed that its pre-chosen, right? And so one of the things that we can do to accomplish this is just give this function, this value, called os.time, which is a function that returns the number of milliseconds that have elapsed since, I believe, it's the creation of the first Unix system, which should be some along value that looks that basically looks like that which will be different every single time we run this application. So the next time it might be like that or, you know, whatever. But it will always be different every time when we run our application. So now that I've called math.randomseed os.time, notice that now the apple's in a different spot. And if I run it again, the apple's in another spot. And if I run it again, the apple's in another spot. And so now we've effectively accomplished actually randomly assigning the apple to some location. It's not consistent across every run of our game which would not be interesting. Although you would want this. You would want this behavior if you were trying to debug something that you see. Let's say your debugging Minecraft. And you have a particular world, and you notice this weird graphical glitch. You might not necessarily run into it if you just allow it to initialize to some random value every time. You want to consistently determine how to generate that world the same way every time. And so you would keep that same seed and then rerun it over and over again to debug. The JP Guy says, "hey, everyone, has the stream been live for long?" We've been live for about an hour and a half. So we've been coding Snake from scratch. I'll put all of this in a GitHub repo, so everybody can take a look at it. It'll be github.com. Let me see if I can log in here. Whoops. It's a little that small, or a little bit large I should say. I want to sign in. It's probably going to make me use two-factor authentication, but that is OK. Yup, authentication. So give me just a second. I'll put this on GitHub, so everybody can download it. Let me just go off of the screen here for just a moment while I get this all set up. One, two, seven, eight , six, da, da, da. Verify. OK. I'm going to create a new repo. What will we call this? Twitch stream-- no, we'll call it Snake 50, screw it, Snake demo talk during Twitch livestream. Make this public, create repo. So I don't know if I have Git actually installed on this account. But if you go to this repo, github.com/coltonoscopy-- here, let me bring up my thing here. github.com/coltonoscopy/snake50, you will see that live. And I'll post all of the code to the repo in real time. I might have it available. So I do have Git on here, but I don't think it's functioning. So I'm going to commit and push everything after we're done, because I think I'll get an error if I try to do that. I'll try. So let's see. I'm in-- OK, blah, blah blah. Git init, git add, git commit add, first commit, git remote add origin. I apologize if this is a little dead at the moment. git push origin master, will it work? Crap. So this is where I run into issues. Yeah. Because I have two-factor authentication enabled to my account, there's a few weird things I have to do to get this working. And I'd rather not take too much time. But I'll do this on my other account that I have that I'll set up on, and we'll be good to go. But for now, I'll just run through the code, I guess, a little bit. I think you can also skim through the VOD if you want and sort look at what's happened. But-- bunch of constants for our tile types, our resolution, how many tiles on the x and y-axis we're going to render in a grid, variables to keep track of whether our snake is moving in a particular direction, the x and the y, the grid itself, a function to initialize everything in LOVE2D. So if you're not familiar, LOVE2D is what's going to be the framework which we're using to do all this game programming. And then keypressed, which will press a key, move the snake left, right, up, or down. Update that snake, edit its x and y based on this thing called delta time, which is a floating point value which will give you the amount of time that's elapsed since this frame and the last frame. Functions to draw a grid, which is just a nested for loop to iterate over our grid and its numbers and then render different squares depending on what we've set in that grid. The snake, which will be a rectangle which has our snakeX and snakeY. And then the function that initializes our grid set it to empty. Sets it to a bunch of 0s. "I'm familiar with using C#, but I recently took an interest in this CS50 into course." Oh, awesome. Glad to have you, JP Guy. If you want, I teach this framework on an edX course. So it is cs50.edx.org/games. And we go through all this stuff and make a bunch of different games based on sort of like famous retro games, like Super Mario, Pokémon, Zelda, all those kinds of games. And we use LOVE2D and Lua for most of it and some UNITY towards the end, which you're probably familiar with if you've programmed in C#. Jeffrey Hughes, yes. JP Guy, thanks for sharing the link to the GitHub repo in the chat there, appreciate it. OK. So let's go back to the game. So the next thing we need to do-- so we got our random apple working fine. So now we have our grid that's being rendered. We have a snake which is moving continuously, which we're going to change. It's going to be moving discretely before long. What's the time? 4:38? Good. OK. We still have an hour and a half about. And then we have our apple, which it's basically iterating over this massive grid here and then just checking each individual spot. What's this equal to? Is this a 0? Then render a white empty rectangle, 0, 0, 0, 0, 0. And then eventually, it gets down to here. And this is actually equal to a 3 I believe. Yeah, 3. And instead of drawing an empty white rectangle there, it draws a filled red rectangle. So that's effectively what we're doing. You can kind of visualize this grid as being a grid of numbers that we've just converted to different types of squares. And that's the overall basis behind Snake. Jeffrey Hughes, ""[INAUDIBLE] I love the way you teach. You sound just like Bob Ross." Awesome, thank you. Well, hopefully, I'll maybe be good falling asleep voice for people then. OK. Let's go ahead and figure out now how I want to take this snake that we have moving around and then, instead of making it continuous, I want to make it discrete. I want it to move sort of grid unit by grid unit, not just like continuously, so that it won't get trapped in between different grid lines, right? It'll always be aligned perfectly. It'll be what's called axis aligned, I guess, in this case. Well, this isn't strictly axis aligned, but discreetly aligned based on some tile increment, which in this case is 32 pixels. So that means that we're kind of going to have to get rid of our snake in the sense that it's no longer going to have just an independent x and a y. It's going to have a grid x and a grid y. I guess we're not really getting rid of it as much as we're going to have to do some pretty significant changes to it. First and foremost, it will no longer do what it's been doing in the update function, whereby the x and the y get updated with the snake speed variable. That's no longer going to be the case, right? The logic is going to kind of be the same. But instead of adding delta time to the variable and then adding or subtracting that from the x and the y, we're going to effectively need to have a timer, which that timer can then see has a certain amount of time elapsed in between each individual tile movement. Jeffrey Hughes, "not in that way, but mostly you help people love what you do." Thanks, Jeffrey, really appreciate it. Thank you, man. So instead of having this continuous value, which is going to be moving very cleanly throughout the space, we are going to move it in chunks. So we're going to need to align it with our grid. And to do that, we're going to have some sort of timer, some sort of amount of time in which we determine exists between each separate discrete movement on the axis. So let's spitball and say, maybe I want the snake to move one increment every half a second for now, 500 milliseconds. So we're going to need a couple of things. So I'm going to define a constant. And by the way, these are being capitalized, because these are variables that I'm basically saying they should never be altered at all. In an actual game that you ship, these will probably get altered, honestly, 1,280 by 720 being your window width and window height. Because people change their resolutions. But most of the other ones are not going to be changed. This MAX_TILES_X and MAX_TILES_Y would also probably be changed if these got changed. But we're going to assume that for the sake of this game, this demo, these are all going to be constant. So anytime you see something capitalized and underscored like this, it's a constant variable that will never get changed or should never get change. There are some languages and environments in which you can declare that something is a constant and can't be altered. But in this case, Lua is a dynamic programming language. It's interpreted. And at runtime, it does not enforce whether some variable is to never be altered or not. it. Will just accept whatever variables you put in your namespace and let you do whatever you want with them. And so I'm going to set a variable that I want to assume I'm not going to edit for now, although this can be adjusted later for gameplay purposes. I'm just going to use SNAKE_SPEED actually. And I'm going to set that to 0.5. Now, 0.5, I'm setting it to 0.5 instead of 100, because, previously, we used 100 to be the number of pixels-- excuse me-- per second that we want it to move. But, now, I'm going to basically say the snake speed or, I guess, maybe-- yeah, snake speed is going to be 0.5. So time in seconds that the snake moves one tile, right? So snake speed-- 0.5. And so, now, I can have sort of a counter, like a timer variable that basically takes delta time, which we're using in update and previously we were multiplying by our speed variable, our speed constant. I can take delta time and just add it to counter every frame, to this counter timer variable, every frame. And then once it's added up to 0.5-- because, remember, delta time is going to be given to you in seconds as a floating point value, so usually point 0.1667 whatever every frame. Once that's added up to 0.5, that means that half a second has elapsed. And therefore, we can move the snake one tile in whatever direction, right? So to go along with that, we're going to need some value. I'm actually going to declare it underneath our other snake local variables, or our variables that actually change. And I'm going to say snakeTimer, I going to set that to 0. OK. So, uh-oh, we have a visitor. Everybody say hi to Dan Coffey in the chat, everybody. This is actually Dan Coffey. He's a man of few words. So we have a timer that we've declared. So we need to actually sum dt to that. So all we have to do in this case is say-- whoops, sorry, got a little lost there-- snakeTimer = snakeTimer plus delta time. Remember, Lua does not have the plus equals. Otherwise, you'd probably use that. So snakeTimer = snakeTimer plus delta time. And so now all we need to do-- it looks like you got a little bit of love there in the chat, Dan. JP Guy's showing you some love. We're going to say if snakeTimer is, remember, greater than-- and then up here we declared snake speed, right? We'll say greater than or equal to SNAKE_SPEED, then now this is where we're going to move our snake. Except we're not going to move our snake along pixel wise. We want to move it in increments of 32, so that it stays adhered to our grid. Looking good, Dan. Got a lot of fans for Dan. snakeTimer is greater than or equal to SNAKE_SPEED. We're going to repurpose the snakeX and snakeY that we had before, except now these are going to be indices into the grid, right? So I'm going to say 1, 1. Because, remember, everything is 1 indexed in Lua. And these are going to map to our grid positions later on. We're going to actually keep a record of where our snake is in the grid as well. If it's greater than or equal to SNAKE_SPEED, then snakeX-- oh, and then here we also have to check to see what direction we're moving in. So if snakeMoving is equal to up, then else if snakeMoving is equal to down, then else if snakeMoving if equal to left, then else-- we're not going to check for the last condition, because it will always be one of these four, up, down, left, or right. So it'll be right otherwise. snakeY equals snakeY minus 1. snakeY equals snakeY plus 1. snakeX equals snakeX minus 1. And then lastly, snakeX equals snakeX plus 1. OK. So now we have our snake moving with some timer. And it's going to move every time this snake timer increases past the snake speed constant that we declared before. "There can only be one Bob Ross, but you are pretty awesome. Thanks so much." Thanks, [INAUDIBLE]. I appreciate it. I'll take being second to Bob Ross. That's OK. OK. Well, there's a couple of things here. So I'm going to run it. It should run. It should not run, main 109 saying-- oh, I think I missed a bit of syntax. Let's see. Else-- oh, right. I forgot the end here. So every if needs to have an end. If you see some EOF issue, then you do need an end statement there. Irene, thank you. Irene, appreciate it. OK. So now it's fine. So now notice, though, we're not moving discretely, because we're still keeping track of snakeX, snakeY, still rendering them. But we did get a bit of a delay, a 0.5 second delay before we started moving. So that part is working. But we don't want to move in increments of one. We want to move in increments of 32. And so this is where we need to go in our update function-- sorry, rather, our drawSnake function. And instead of drawing snake here, the snakeX, we're going to treat x and y as grid indices. So I'm going to say just like we did before-- because, remember, coordinate systems are 0 indexed, but tables are generally 1 indexed. So snake minus 1 time TILE_SIZE. so now we're multiplying it by TILE_SIZE. So this 1, 2, 3 pixels that we're moving before continuously are now going to map to perfectly slotted places in the grid. We'll do the same thing here. snakeY minus 1 times TILE_SIZE. I'm going to bring this down here just for readability. We're going to run this. And now notice, though, we're moving so fast that we can't even see the green square after a certain amount of time. It just moves astronomically fast. But we're supposed to be moving, you know, every 0.5 seconds. So what's the issue here? Does anybody know what the main problem is with my logic? It should be working, right? Remember the end at the end of the if, else if, yeah. "What extensions do you use to code in Lua in VS Code?" I use the Pixelbyte LOVE2D extension which allows me to hit Command L on my MacBook. And it will just run. Rather than having to go back to my terminal, which you see me been kind of doing it hidden. But if I were to instead do this, it does the same thing, right? But I don't have to actually go to my terminal. If I had Command L with the extension I have, it gives me that for free. So that's the main extension that I use for Lua. And I think there's other ones. But, for example, if I were doing C# stuff, OmniSharp is a really good extension which allows you to do sort of a live compilation and debugging, which is pretty cool. But, yeah, there's clearly an issue here with my movement of the snake, right? And that is simply the fact that when my timer gets to be 0.5 and higher, it never resets to 0. So in this case, I just want to make sure that whenever you get to 0.5, I go back down to 0. I reinitialize my timer. So I'm going to set my snakeTimer to 0 in here. So if we get to the point where I've gone up to 0.5 or 0.51 or whatever it equates to on that particular frame, I'm going to be back to 0 on the next frame. So if I rerun this, notice that now we are indeed moving along the x direction. And if I hi Down, which I just did, notice that now I'm moving down. And I hit Right, I go right, Left, I go left. And then Up, I go up. So now it's kind of getting there. We're on the right track. It's a bit slow, though, to be quite honest. So I'm going to bump it down. Let's say 0.1, see if that's good enough. OK. Not bad, right? So now this is more or less a workable solution, right? And so if I walk over the apple, nothing happens. We don't have any logic just yet to keep track of whether we've consumed an apple or not. That's going to be kind of one of the next steps, right? The next thing that I probably want to do is grab the apple. And if I eat the apple, I want to respawn a new apple somewhere, right? And then the next part after that is going to be I need a way to map my snake onto the grid, so that I can check in the grid at any given index whether that's owned by a snake tile or not. So that I can, therefore, do collision between my snake head and any part of my snake body if that makes sense. So anybody, let me know if you have any questions thus far about how this works. Otherwise, we'll do a couple of things to get started on actually getting the apples to work. I thought I had a water in here somewhere. Did I? Did I have a water in here, by chance? That's OK All right. Actually, I'm getting a little bit parched. I think I'm going to grab-- SPEAKER 1: [INAUDIBLE] COLTON OGDEN: It's over-- where is it? SPEAKER 1: I'll get it for you. [INAUDIBLE] COLTON OGDEN: You sure? Oh. OK, thanks. Appreciate it. OK. Oh, thanks. Sweet. All right, it's thirsty work. Oh, sorry. Irene says, "does the square continue moving when you're not pressing? I can't tell from here." Correct, yeah. The square will always move, because in our logic we've just basically said when the key is pressed just change the state. But it's not actually checking an update whether the key is being pressed. It's only checking that state variable. So if it's up, down, left, or right and the timer has gone past a certain amount, it'll move it on its own basically. OK. So I'm kind of getting a little bit tired of looking at the grid I think. So what I'm going to do is I'm actually going to nix drawing the grid. Actually, well, no, that's not true. Because what I need to do is I need to draw the grid. But also, if the grid contains the snake and/or the apple, which eventually it will, I still will need to draw it. So what I'm going to do is I'm just going to get rid of this bit of code here. I'm going to comment it out. I can do command/ on my computer. And so now it's a comment, and so it won't actually execute. So, now, I don't have a grid, but I do have a black background and the snake sort of going around. And I can go on the apples or whatnot, but it's not interacting with the apple. So let's go ahead and figure out how we can get the apple rendering-- or, sorry, get the collision between the snake and apple working, and therefore get new snake components and then maybe even have a score. So the score will be easier to start off with. Adding a new element to the end of the snake is going to be a little bit trickier. Oh, and there's actually one more thing that we should take into consideration. And that is the fact that as it stands, if I go up, I don't actually loop back from the bottom. I have to hit down again, and then eventually my snake will come. So what I'm going to do is a simple set of if statements where I have my snake being edited, which should be here. So in my update function, should I put this all into its own function? Most of it's going to be update code for the snake anyway. As your functions get longer, it's generally better practice to sort of break them out into different pieces. But I'll keep most of it in here unless it gets really unwieldy. So what I want to do is notice that we're just kind of moving the snake minus 1 and snakeY plus 1, snakeX minus 1, snakeX plus 1. We're kind of just moving them without really checking for bounds, for checking boundaries on whether we're at the edge of the screen on the up, down, left, or right axes. So what we should do is do some simple checking. If I want to move up, for example, and I'm at index one, well, I should actually move my snake to the bottom index. And if I'm, for example, moving to the right and I happen to hit the last index in the grid, I should probably move my snake back to the index one on the x-axis. So it looks like I went from right to left. So I looped around, came full circle. So let's do that logic here. So I'm debating whether we should make the snake part of the grid first or whether we should just do it. I'm thinking. OK. So we can get rid of draw snake altogether. So let's do that. So let's make the snake part of the grid first, because this will actually be fairly easy to do. If I go to else if in my render function, else if tileGrid y, x is equal to TILE_SNAKE_HEAD. Then I'm going to just copy and paste the apple code. I'm going to set that to 0, R, G. So I'm going to set it to that. But I'm going to make it kind of cyan colored a little bit and just have that be my code here. So I'm actually not going to draw the snake anymore as a separate function. We're just going to make the grid draw the snake. The snake is actually going to be part of our grid just like the apples are just to keep everything kind of simple. And then that way we can check to see. We can look at any particular index in the grid at y, x and know, oh, that's actually a snake body part, for example. And then, yes. Jeffrey Hughes, "you can create a function to remove the redundancy of that code." Yes. Yeah, you probably could. In this particular instance, these are kind of the same. It differs a little bit if you have this code here, which is drawing it as a line. But you could define a function called draw tile, for example, that takes in the draw mode of this, the x and the y, and then the color and then have it do that as well. Just for the sake of time, we won't modularize things to a super extent. Maybe if we have some extra time we'll do that as a creative exercise. But, yeah, good point. You very much could and should do that in a proper, like, full project. Just for illustration, though, we're just going to use a little simple copy and paste just in sake of time. Because we have about an hour left, and it'd be nice to get as close to the end as we can. OK. So we have TILE_SNAKE_HEAD being here. And so now what we want to do is set the-- we need variables, essentially, to keep track of where the tile snake head is. And we can use snakeX and snakeY to do that. So do we want to do it that way? Because the snake is going to actually have to move. So each individual-- oh, wait. No, that's not true. That is not necessarily true, because, yeah, we will need to keep track of each individual snake piece. So let's go ahead. Like I said, I haven't spent a terrible amount of time working through this, because it'd be fun to I think kind of figure it out on the fly. We're going to make a new variable called snakeTiles, which is going to kind of go hand-in-hand with our tile grid, I guess. And then we're going to initialize the first piece here to be snakeX and snakeY. And actually this should go below this piece here, snake data structure. And then every time we want to add a new piece, we're effectively going to take whatever the last piece is, and then we're just going to add-- well, what we're effectively going to do is move this down to here. And then we're going to take the next position, depending on whether we're moving left, right, up, or down, and we're going to fill it in here. But since we only have one index at the moment, we only are just going to put in the snakeX and the snakeY right there. So that works just fine. We might be able to even just keep track of this snake data structure independent of the grid, but it kind of makes it easier for checking the grid for different y, x if we also just kind of add them to the grid. So we'll try it that way. OK. So we have our snake data structure. So in this case, we only really need to keep track of the head of the snake in terms of moving that. Because we're going to move that. And we're going to take the tail, and we're going to basically take the tail and put that where the neck of the snake is. Basically, where the head just moved, we're going to take the tail and move it there. And that'll have the effect of sort of moving the snake all around the map pretty seamlessly without having to manually move every single one of these snake pieces. Because you can sort of visualize if I can pull up Chrome and have the image here, snake game images. If the head is here-- which is kind of actually hard to see in this particular instance. If the head is here and it moves one up this way, really the entire snake is going to stay the same overall. Excuse me. The vast majority of it's going to stay the same. The only thing that's going to change is this tail right here, right? So we can effectively take this tail, remove it from the bottom, and then plug it here. And then that will have the effect of our snake moving one forward when all we've really done is just move the head forward and taken the tail and sort of made it the neck of our snake. So as a performance optimization, now we don't have to manually move every single tile. If our snake is moving, we don't have to move every single one of them every frame. We can just move one piece at a time. So this is a bit of an optimization and just like an easier way to program the snake as well. So that's the algorithm that we'll take to actually solve this. So this will be my head, right, the head of the snake. And every time it moves, whatever is in our tail-- this is, first of all, the head is basically going to shift to be whatever the next title is in the direction we're moving. And then this is going to come down here and fill in that spot basically, should work. Well, we'll see how it looks when we actually implement it. OK. Just make sure everything is working still. It is not. Oh, right. OK, right. Because we're no longer drawing things. OK. So we're no longer drawing the snake, but let's go ahead and change that by making it part of the grid. We should make part of the grid here first. So I'm going to say actually after the grid is initialized, grid at snakeTiles 1, 0 and then snakeTiles 1, 1. Whoop, sorry. I'm 0 indexing when it's not 0 index. There we go. So the grid at snakeTiles 1, 1-- so the first inner table of our snake data structure. And then the first subelement of that table, so snakeX-- or, actually, this should be reverse. Because, remember, it's y, x, not x, y. Oh, actually. Interesting. So snake [INAUDIBLE] 1, 2. No, sorry. I have that backwards. Sorry, having a mild brain fart here. Let's figure this out. So as an aside, "what retro game do you enjoy remaking the most? What about the least?" Game that I enjoy remaking the most? I really like RPGs, so I like a Final Fantasy kind of game I think would be a lot of fun. I did something similar to that. Well, we started with Pokémon in the edx.org course, the games course. The least? I'm not sure. I like a lot of different games, a lot different game types. I'm not a huge fan of physics-based games, I guess, like Angry Birds. We had to make Angry Birds for the course as well. And I didn't find it super interesting I guess. A lot of that is taken care of for you with the physics framework. And I think it's kind of cool to make the different composite physics objects and whatnot. But the actual-- yeah, I don't know. It wasn't as interesting as something a little bit more from scratch I think. But what about you, JP? What would you like to make, I guess, in terms of a game? In the meantime, let me figure out exactly what's going on here. So grid at-- so streamer game fart. Apologize. I'll figure it out. OK. Oh, right. OK. No, I had it. snakeTiles 1, 2 = TILE_SNAKE_HEAD. That's what we needed to do. So basically, take the x and the y from these snakeTiles first inner table and then map that to our grid by putting-- basically, this is the first table within our table and then the second subelement, so the y here, and then the same thing here, but the first subelement, so then this snakeX variable. And then I'm going to assign that to TILE_SNAKE_HEAD, which we've shown down here, right? And so if I run this, it should start off with a-- whoops. Grid 38-- oh, yeah. Not grid, it should be, what is it? tileGrid. So let's call this tileGrid. Run it. OK. So it did work. So it started off with a green tile at the very top. But it's not moving anymore, because we're no longer editing that grid be the tile snake head or tile snake body, however we want to do it, right? So we're going to need to alter that. So let's go ahead. And in our update function, that's where we're going to need to do it. snakeY = snakeY minus 1. So we do still to keep track of our snakeY. And so what we're going to do is that's still fine. We need to do tileGrid snakeY snakeX is equal to TILE_SNAKE_HEAD here, right? So this is still moving and still updating that x and y value that we had before, which should still be being edited underneath the hood. But now notice that we do get our snake rendering. However, its rendering in such a way that it doesn't actually discretely change position. It does, but it keeps the old previous values being written in our grid for the title snake head, which is not what we want. We want to basically set those values to 0-- not to 0, but to whatever the last element is in our grid, right? Yeah. Because the snake head's going to move forward. It's going to create a gap. And if it's just by itself, it's not going to create a gap. It's just going to be moving by itself. But assuming that we have a full snake longer than one segment, it's going to create a gap where the neck is. And so to fill that gap, recall we can take the tail and we can put it where that gap is. And that'll have the effect of moving the snake. So let's go ahead and tileGrid if. Let's do an if statement here, because if we try to index into the table when it's only a size one, if we're trying to get the tail, it's not really going to work. I guess it would work. It would just use the head, which we've already moved forward. So that would have the result of moving it backwards, which wouldn't work at all, right? Because we want to use a tail or nothing at all if it's one segment long. And we can use this thing called this pound symbol, which is a length operator for tables. And we can say if length of tileGrid is greater than 1, then-- whoops. Actually, what we should do-- we're going to need to cache our old values so that we can take the-- so we have to remember their old value of our neck, so that we can put our new variable into that neck, right? So let's go ahead and say-- hold on, let me read the stream real quick. I haven't been keeping up. Dungeon Siege II. Yup. Dungeon Siege II. I've played Dungeon Siege I. I haven't played Dungeon Siege II. But I liked it a lot. It was a lot of fun. Good question. I think I'm on the opposite side. If I could program 2D games, I'd probably make something physicsy, like some 2D Kerbal Space program or Asteroids or something. Yeah, that'd be a cool idea. I think physics games are really cool. And I don't dislike programming them. I think it was just in comparison to, say, Zelda or Mario or Final Fantasy. Those are the kind of games that I enjoyed growing up. So I might have a soft spot for them. But, yeah I know a 2D physics game-- Kerbal Space Program in 2D would be pretty cool, actually. That'd be really cool to see. Shayna, "but each time the snake eats the apple, will the snake become longer by adding new tiles, or the head tile will go back to the tail?" So we will add a new tile. Basically, we're going to need to-- oh, you know, actually what we can do, we can pop off the tail, right? Because we can just get rid of the tail. And we'll just add a new grid element where the head was, so super easy. Yeah. That's a much easier way to do it. "Why not just cut the tail piece and paste it in front of the snake's current head in the appropriate direction as it propagates?" JP Guy, good idea. That would work, too, actually. That's actually probably better. We just need to then flip the snakeX and snakeY variables that are currently pointing at the head to point to the new head, right? But that should be easy enough, right? I actually think that already happens. "Sorry, I just found this channel. Why are you programming this game in Lua instead of, let's say, C#? Just curious, Salmedo." Thanks for joining the stream, Salmedo. Short answer, because it's easy to get into compared to C#. Because C#, you're presumably using it in the context of either X and A, a 2D framework, or using in the context of UNITY most likely of those two options. And UNITY is something that I would like to start teaching on stream, but there's a bit more overhead with that. And I actually myself should spend a little bit more time sort of getting all the nuts and bolts set right with that in my mind. And X and A is honestly a framework that I'm just not super familiar with, although I think is somewhat comparable to what we're doing here with LOVE2D. But good question. I would anticipate seeing UNITY streams in the future, because it's a game engine that I would really like to teach and get really familiar with. But this is a framework that I taught in the game course that took place this last year, cs50.edx.org/games. All right, so, yeah. So whose suggestion was it? "Why not just cut the tail piece and paste it in front of the snake's current head in the appropriate direction as it propagates?" That's an excellent question. And I think we are going to do that probably. "OK. Thank you very much, Salmedo?" No problem, Salmedo. Thanks for the question. Thanks for tuning in. OK. So pop tail, put it 2x tail behind the head. Pop the tail. Put it two times tail behind the head. Put it two times behind the-- I'm not sure I follow, Bhavik Knight what you're saying there. Would you mind elaborating a little bit? OK. Hopefully, we have enough time to do this. Hopefully, I'm not going to blunder my way too far past 6:00 PM here. But let's say, OK. Let me just get reacquainted here with the code. So if we rerun it, we have just the snake moving. OK. So the thing we needed to do was clear where the head used to be. And yes, JP Guy. "You mean when the snake eats an apple?" Yeah, presumably. I think that's what he's talking about. OK. So basically, let's erase, first of all, where the head was before which we can do. So we will need to store all of these snake pieces together. Because we're going to keep track of all of them as we pop the tail. That's going to be important. So we can't just store the information in the grid blindly and use that. We actually have to keep a record of everything. So we're going to keep the same model that we're doing. And then if the tile grid, number of the length of the tile grid is 1-- so this is where we're talking about if we're just ahead or not. If we're just ahead, then we just erase what was behind us. And if we were not, if we were more than 1, then we need to pop the tail. And if we take JP's suggestion, we can put that in front of the neck. And then that will be our new head, basically. But we need to keep track of where the head was before we actually move it. So I'm going to add a couple new local variables here. I'm going to call it priorHeadX and priorHeadY. And I'm going to set that to snakeX and snakeY. So that is going to let me keep a record of where I had the head before, so that I can do something like this. If the number of the tile grid is greater than 1, right, I'm going to-- well, I'm going to leave that. That's going to be a to-do. Else tileGrid at priorSnakeY and priorSnakeX is going to be equal to TILE_EMPTY, right? And so this should work off the bat in terms of giving-- oh, no, it doesn't. OK. OK. priorSnakeY, priorSnakeX is equal to TILE_EMPTY. If the number of the tile grid is greater than 1, which will be in the case that we have a head, and then we have-- oh, this is the tile grid. Sorry. We need to look at the snake grid, the snake tiles. Sorry, that's my bad, snake tiles. So the length of the snake tile is not the tile grid. The tile grid will always be the x times y number of tiles on the screen. Oh, index a nil value, main 78. tileGrid priorSnakeY priorSnakeX is equal to TILE_EMPTY. If snake is moving, so snake will be moving right. OK. Now, this should work. That's interesting. OK. Let's figure this out. This is some live debugging. This is why we're here. So priorSnakeY at priorSnakeX is equal to TILE_EMPTY. And it started to render, but it stopped rendering as soon as we tried to move to the right it looked like. So if that's the case, then snakeX priorHead-- oh, because I called it priorHead, my bad-- priorHead, not priorSnake. OK. That should work. There we go. So, now, notice that we have our snake moving around. And it's part of our grid. Now, what happens if I try to move it beyond the bounds? It doesn't work. And the reason that it doesn't work is because, if it gets to be 0 or some negative value on either axis or a value that exceeds the bounds of the grid, remember we haven't programmed to bounds check for these things. It's going to try to access index 0, or index negative 1, or index, like, 60. And that's not within the range of the game, exactly, out of bounds error. So what we need to do is start checking for bounds. If we're trying to move up, down, left, or right and we're at an edge, then we should accommodate that. So we're going to go to snakeY is snake minus 1. So this is all where we actually update the snake in its position. So if it's up, snakeY is snake y minus 1. Now, let's do an error check with that. We're moving up, right? We need to check to see if we get to be 0. If we're at 0 or less than 0, then we need to loop back down to the bottom of the map. So if snakeY is less than or equal to 0, then snakeY is going to be equal to MAX_TILES_Y else snakeY is going to be equal to snakeY minus 1. And so this will at least check for an out of bounds error going up. Now, if we want to check for down, left, and right, we have to do essentially the same thing just with a little bit different logic. So if snakeY is greater than the MAX_TILES_Y, then-- whoops-- else. So this is if we're going down, right? So if we go down to the bounds, if we go down and our index is at MAX_TILES_Y and we try to go one past that, we're just going to set our snakeY to 1. Because, remember, everything is 1 index, not 0 indexed in Lua. And then this is the same logic as before when we're moving up, but it's on the x-axis. So if snakeX is-- oh, sorry. Actually for less than or equal to 0, we need to check to see if it's less than equal to 1, not less than or equal to 0. Because we don't want to set it to 0 to begin with. We want to make sure when it's at 1 that we loop back around, not 0. Sorry. And then if it's greater than or equal to MAX_TILES_Y, then set it to 1. If snakeX is less than or equal to 1, then I'm going to set snakeX to be MAX_TILES_X else. Whoops. And then I'm going to just copy that, bring that over there. And then we're going to do the same thing here. If snake is greater than or equal to MAX_TILES_X else. And then snakeX equals 1, rather. Now, if I've done this correctly, my logic isn't incorrect, I should be able to go out of bounds and loop back to the bottom, which it looks like I do. I go down as well. Awesome. So, now, we have that basic functionality. So whenever we reach the left, right, bottom, or top edge, we're appropriately moving to the other side of the screen-- so a fairly simple problem to solve. That's a big thing that we needed to correct, but now it's been corrected. "Snake tiles in the tile grid." Yeah. Thanks, Andre. Yeah. Those kinds of things are so easy to miss when you're actually programming it. And then I know how frustrating it is to see someone else miss something so obvious. So apologies for that. JP Guy, sweet. Thanks for the support. OK. So we have now our bounds checking and, also, setting our head on the snake or whatever it previously was to TILE_EMPTY, which is great. Now, we need to go through and write the logic for actually eating the apple. So once we eat the apple, let's also have some score variable, right? And let's monitor that. So we can check how many apples we've eaten. I'm going to create a new variable here called score set to 0. And then down here in the draw function, I'm going to call love.graphics.draw-- or, sorry, just print. I think that should be fine, score: tostringscore. I'm going to draw this at 0, 0. Yeah. I'll draw it at 0, 0 for now. That's easy enough. Rather, I'll draw it at 10, 10. And that should be it actually. So let's draw that. Score is 0. So you see it in red there, because it's actually technically after I've called love.graphics.setColor to red for the apple. But that's at the end of drawGrid, right? That's the last thing it drew in this case. So I need to actually reset the color, so love.graphics.setColor 1, 1, 1, 1. So for white there, we got score 0. So we know that we haven't eaten any apples yet. It's a little bit small. And actually, offhand, I know you used to create a new font. But if we don't have a Font file, love.graphics.new font. Let me just make sure that we can create a new font without a font file type. Can we do that? OK. Cool. Yeah. We can create a new font with a default size using-- it's called I guess-- Vera Sans is the default font in LOVE2D. So let's go ahead and do that. So the font was way too small. In order to draw something at a larger font size in LOVE2D what we need to do is up here, I'm just going to-- actually, I'm going to make it a global font up here. So I'm going to say-- let's do it up here-- local largeFont = love.graphics.newFont size 32. I believe the default size is 16. And then in here, we can say love.graphics.setFont largeFont, because you have to actually set the font. We've created a font object up here, this large font. But we have to actually set it in the actual UNITY, the state machine aspect of UNITY. Sorry, I keep saying UNITY. I just had a UNITY workshop yesterday, so part of it is still kind of in my mind. But in LOVE2D, we need to specify the creation of the font object and then the actual using it, setting the font for the LOVE2D state machine. So this is what we're doing here. And if I'm correct in my programming, we should now see score is 0. It looks actually pretty nice and clean I will say. So cool. So now we have a score meter always visible on our screen, perfectly functional inasmuch as it renders text, not functional in as much as we're actually keeping track of score. "If there's no Comic Sans, I'm unsubbing." JP, I'll get you some Comic Sans in there next time. If we've got to make this stream legit, we're going to get some Comic Sans. We'll get a nice a nice Comic Sans "This is CS50" as well just for fun maybe. But, anyway, I'm going to think about now how we want to start detecting whether we've reached an apple with the snake. And if we have, we should increment our score, right? I'm going to be kind of bummed if we don't get this full thing working by today, by 6:00. But I'll put it on GitHub if not. And then we'll do a follow-up stream maybe on Monday just to finish it off and then maybe talk about some other stuff, and then maybe next Friday start making another game that's maybe a little more complicated. But I'll do my best to see how far we can get. Games take a long time. Anyways, OK, so we have a snake head. We have variables snakeX and snakeY that point to our snake head that are keeping track of where it is at all times. So what I'm going to do is, in our update function, this is sort of where we need to check to see whether we've reached an apple, right? So before we actually set that place in our tile grid, that's where we want to do the check. We want to say, OK, so if tileGrid snakeY, snakeX is equal to TILE_APPLE, then-- and this will be the same either way. We'll still need to update the head location. But check for apple and add to score, right? And this will eventually be where we also add a node to the head, right? "Love me some sans serif font." Yeah. Me, too, Shayna. Excuse me. OK. So if in the tileGrid at snakeY, snakeX, which is where the head currently is, we're going to-- sorry, we're checking it. If it's equal to TILE_APPLE, score equals score plus 1. And then we're going to overwrite it down here. So we don't actually need to change the tile, but we do need to initialize another apple, right? So what we need to do here is say-- and this is kind of where we need to be a little bit careful. We need to basically say if-- or we should rather say, local newAppleX, newAppleY equals math.random MAX_TILES_X and math.random MAX_TILES_Y, right? We're going to get new random variables to be our x, y for our apple, because we want to place in a new random spot. And then we're going to say tileGrid newAppleY, newAppleX = TILE_APPLE. And what this will do is it will blindly look throughout our entire level. Or it won't look through the level. It will just blindly choose two random variables between the limits of our level and set the apple to that value. But there is going to be an issue with that, especially as our snake gets larger. And that's the fact that it could just overwrite one of the snake body parts, right? So we want to be careful with that. We want to make sure that we're actually checking for whether a snake body part exists in the grid at that location. So we can do that. We can absolutely do that. For now, we're just going to blindly do this and make sure that it works, the score and everything else. So I have a syntax error. Oh, right. I'm doing a novice mistake. If tileGrid snakeY, snakeX double equals TILE_APPLE. OK. So now we're moving around. So if I eat the apple, if I can, boom. Cool, we increased our score to 1. And the apple moved to a new location. Awesome. Boom, did it again, did it again, did it again. So it works. It works perfectly well. And it's not an issue that we're not checking for whether it's in the snake's location, because, well, our snake's head is only one segment long at the moment. So it's very unlikely that it'll actually appear in that location, but it's possible. So we're going to add some bounds, or rather, some error checking here soon in order to make that work. But the next piece of the puzzle is going to be how do we start adding segments to our snake, because that's very important, right? If we don't check for segments, we'd never run the risk of ever losing the game. So that's something that we need to take into consideration. And another thing to take into consideration is the fact that we shouldn't be able to move in the reverse direction which we're already moving. Because if we do that, then we'll collide with ourself instantly, right? Because we'll have had a segment in the past direction that we were moving from the last frame, right? So-- two things that we should take into consideration. Snake is a surprisingly complex game when you get down to it. I mean fairly simple and straightforward, but there's a lot of little things you've got to pay attention to in order to get it working just right. All right, so let's think about how we can start getting new segments added onto the snake. So the new segment, the actual part where we add a new segment is going to take place here between lines 96 and 101 where I actually have the snake data structure look at its tail, right? And then put the new piece effectively onto the front of the-- "snake is going to be huge." Yup, that's correct. Well, if I'm good at Snake, it will be. If I'm terrible at Snake, maybe not. So in here, once we have consumed the apple and we've placed it in a new location, we want to actually add a new component to the snake. So what we can do is I'm just going to kind of code through this. And we can figure it out. But I know that I'm probably going to want to insert where our-- I mean, for the sake of ease, I guess I can probably do an error check here first, actually. If the length of the snakeTiles is greater than 1, then do this. Otherwise, it's just going to add to the snake tail, right? So take tail and add to head if greater than one segment. Otherwise, we want to just add a new head segment effectively, right? Because if we only have one segment, then we have no tail besides the current segment we have to add to the front, right? Otherwise, it's kind of like an infinitely recursive operation at that point. So if the number of snake tiles is 1, OK, else. Do that, OK. So if it's greater than 1, I'm not going to worry about that case for now. I'm going to worry about adding a new head segment if we are just at one segment. So our snakeTiles, snakeX and snakeY, is going to be here. And we aren't actually editing the snakeTiles table if I'm not mistaken. All we're doing is we're editing the tile grid. And so what we need to do is actually edit the tiles within the snake itself, too, right? Because that's how we keep track of the tail that's how we pop the tail off the snake. We take the tail. There's going to be a new tail after that. And then we need to put it to the front-- rather, is that true? That's it we're moving. Sorry, that's if we're moving. Then we're going to do that. We take the tail, put it to the front. If we're eating an apple, then we actually keep the tail, we keep the head, and we just add a new snake element to the front. I believe that's correct. Yeah, that would be correct. I'm trying to remember in my head actually how it works when you eat a new segment. Does it add it to the tail, or does it add it to the front? I think it adds it to the front. Let's see. So open a new incognito window. Go to snake game play. And then, oh, it's looking for like Snake classic game play. OK. Snake game. Let's see what it does. So it moves. So the tail's being moved, added to the front. Oh, interesting. OK. So it moved. And on the next frame, it just added a new head without popping the tail. OK. So it doesn't pop the tail on that frame. So that's how we're going to do it. We're just going to keep the tail and not pop it when we add the new element. And that'll work. Cool, easy enough. OK. So priorHead, priorHead is TILE_EMPTY. Whoops. No, this isn't where we need to be. OK. So if we're at just one segment-- and apologies if I'm going a little bit slowly. I just want to make sure I think through this correctly. We're going to move forward. And then we're going to keep the head where it is. So snakeX and snakeY is still going to exist. And table.insert at index 1, the new snakeX and snakeY, correct, and the-- oh, actually what we need to do is snakeTiles-- oh, sorry-- at index 1, this is going to be table.insert into snakeTiles at index 1. I believe this is correct syntax. table.insert takes a optional parameter to tell you where to insert. I have to remember exactly where. table.insert A 115-- OK. So it takes the table, then the index, then the value. Got it. I had that backwards. Table, index-- so we're going to insert into snakeTiles at index 1, so at the beginning of the 2D array, or, sorry, at the beginning of the-- yeah, at the beginning of the table, not a 2D array, just the Snake table. A new element, which is going to have our new head, right? And then the snakeTiles at index 2 is going to be equal to snake-- what is it? snakeTiles 2 is equal to snake-- sorry. I keep having a couple of brain farts. I might need to drink some coffee or something. Let me read the stream for a second. "Add to the front." OK. "It would be cooler if the snake's body had to travel all the way until the original apple tile was right behind this snake's tail-- cool if the snake's body had to travel-- oh, you mean in order for it to fully eat the apple? Like it goes through the entire snake? Because that'd be really difficult. That'd be like expert mode Snake. Expert mode Snake, got to basically draw the snake with the apple kind of. Oh, prior. We got to use prior X, right? So it'd be priorHeadX, priorHeadY. And then it would be we have to erase as well the old-- yeah, see, down here, we're setting it to empty. Oh, but then this case, we're actually going to be at greater than 1. OK. Oh, but that we do still need to-- yeah. This is part of the equation. Part of the actual algorithm for moving the snake is that when it's greater than 1, this is where we actually have to do the thing where we pop the tail, and then we add that to the front of the snake as well. So it's a little bit more complicated than what we did down here, whereby we took the priorHeadY, priorHeadX index of the tile grid, and then we set that TILE_EMPTY. If we're greater than 1, then we need to basically do the pop operation every time. "Will it be easier if you create two apple tiles, one when it's eaten and second one each time it disappears? Example, when the snake eats apple one, it disappears and apple two appears. And it goes on vice versa." Yeah. Oh, wait. "Create two apples, tiles, [INAUDIBLE]." If I'm understanding correctly, I think what we're doing is what you're asking, whereby we just have the single tile apple and its indices that are being-- well, rather, it's being stored in the grid. And then when that happens, we just overwrite the tile with the snake head. And then we just generate a new random tile for the apple. I think it functions kind of similarly to what you're asking if I'm understanding you correctly. Shayna, thanks for the question. OK. Sorry, getting a little distracted. Let's see, tileGrid priorHeadY, priorHeadX. So this part was working fine. And it's this part when we need to start popping and adding. OK. So first of all, let me see if this is even correct. So part of this is the snake tiles itself is just a data structure that we're using to keep track of, effectively, where the tail location is at all times. But it needs to maintain a history of where all the tiles are so that we can change the tail accordingly. So table.insert snakeTiles 1 snakeX, snakeY, so that part is adding the new head to the front, so the snakeX, snakeY variable that we created before. And then snakeTiles 2 is going to be our priorHeadX, priorHeadY. So now that's going to be the new tail, basically, where our previous head used to be. And then once we have a new tail, what we're going to do is we need to set that in our grid. We need to say tileGrid at snakeTiles 2 index 2. And snakeTiles 2 index 1 is going to get equal to TILE_SNAKE. Because it's not going to be TILE_SNAKE_HEAD at this point. It's going to be TILE_SNAKE. And to make it a little clearer, I'm going to also have to change the drawing function for this. Where do I do that? In love.draw, which is-- sorry, rather, the drawGrid function. So else if tileGrid y, x is equal to TILE_SNAKE-- oh, it's body, sorry. Rather, we've defined it to be body. Whoops. Then I'm going to do basically all this same stuff, but I'm going to change the color a little bit. I'm going to change the snake head to be a little bit more cyan color. And then the body is just going to be green. And then I need to go up here where I actually wrote that, so TILE_SNAKE_HEAD and then TILE_SNAKE_BODY right there. And then that will have the effect of basically making the grid reflect where the snake body is. So let's go ahead and see if this actually works first of all. OK. So it is working. So, now, it's registering that we have a body, but it's not moving. So what we're doing is we're effectively doing what we did before. Whoa, that's going to be impossible to get. Oh, yup. I think I screwed myself. Oh, yup. See, I did. That's fine. So we kind of have a step in the right direction where we now recognize, when we eat an apple, we add a new element to our body. But the snake head is overwriting all those other variables-- or sorry, those other tiles in our grid. And it's not sort of keeping track of where the tail is, which was part of this last piece down here. Was it down here, the updating the head location? Because if our snake is only of size one, then our head location is just going to be wherever snakeY, snakeX is, which we do here. We update it in the tile grid. But to erase it, what we did before is we just set the tileGrid at priorHeadY, priorHeadX to be TILE_EMPTY. The logic is different if we have greater than one element. So what we need to do is basically erase wherever the tail was, right? And by setting tile grid, for now we'll just assume that we're of size 2, right? Ba, ba, ba, ba. In this case, we'll assume we're working just with a 2 unit snake, and then we'll move up to multiple. But we're going to need to set tileGrid at snakeTiles 2, 2 and snakeTiles 2, 1, equal to TILE_EMPTY. And then we need set snakeTiles to-- we need this to update basically. So we need to set TILE_SNAKE tiles 2 be equal to priorHeadX and priorHeadY. And so that should work. Let's run it. Let me make sure I'm not crazy. Yup, perfect. So now we have 2 unit snake, but it's only going to work for 2 units, right? Whoops, if I can pick it up. Whoop. OK. So it's still 2 units, right? We've made progress, though. We've basically said, OK, in our tile grid we want to erase whatever the tail tile was last time. In this case, we're hard-coding 2 in, because we're only experimenting with the snake of size 2. And then we want to set that last unit to be equal to whatever the priorHeadX and priorHeadY was equal to. But this isn't going to work for a snake of size, you know, 3 onwards. Because this is hard-coded just for 2. We need slightly different logic in order to handle that use case. OK. "I mean, essentially having an object pool of two apples, it'd probably be harder. You'd have to keep track of which one is active. The tiles here are being rendered on the fly. It's not like, say, UNITY, where you can deactivate the mesh renderer and make objects not be rendered." Yes. Often, object pooling is very good use of-- excuse me-- resources, but typically only when you have a ton of different objects that you need to keep instantiated. And in this case, per Andre's point, yeah, they're being drawn in real time with the size of the grid and not being actually stored necessarily in memory. Although the grid itself is being started in memory, but it's not consuming more memory by having the apples there. There's going to be an integer, a 0, 1, 2, or 3 regardless of what we're doing. "I'm assuming there will also be a bug where an apple spawns onto a tile that's already occupied by a snake tile." Exactly. Yeah, I mentioned that previously. And that's something that we'll have to sort of take into consideration. When we spawn the apple, we want to basically look and say, OK, is tileGrid y, x = apple? Sorry, is it equal to snake head or snake body? And if it is, we need to generate another random x, y value basically. Just keep doing that in a loop until we find a value that actually works. OK. So we have a two part snake here. Let me take a sip of water, little bit parched. How much time we have left? Oh, we have 12 minutes. Oh, well, I'll go a little bit over just because we were a little bit late to start. But now we can sort of think about, OK, I have a two part snake. But let's take the problem to the next level. Let's take it to where we have three parts to my snake. And let's actually do the logic where we take the tail of the snake and then put it in front, essentially keeping the head where it was. And then let's write to the grid those two new locations, right? We're going to need to erase our tail location. That needs to get erased from the grid. And then we need to write our new sort of head location, which will be an empty tile presumably. OK. So let's go ahead and do that. So the first thing I said, we need to pop the tail of the snake. Also, am I actually adding the last piece appropriately? Let's see. Take tail and add to head if greater than one segment. Oh, you know what? We're not actually going to take the tail. We're going to just add a head and keep the tail where it is, right? Because this is in the context of eating an apple. Because we don't want our snake to get smaller. We want our snake to actually increase in size. And so it makes sense for us to take a brand new tile and add it to our snake, not the other way around that we did before. So if the length of snakeTiles is greater than 1, let's go ahead and, first, we need to add to the table. So table.insert into snakeTiles at 1 snakeX and snakeY, right? Because we're going to basically keep what we had before. And then in the case of having a new head segment, we didn't have a tail. But that honestly doesn't really matter. Like I said, yeah, I think we can just add a new-- I think I might have gotten this confused with the actual code with moving. But I think we should just be able to tileGrid at-- sorry-- snakeX-- rather, snakeY, snakeX. We can set that to tile TILE_SNAKE_HEAD. And then at tileGrid where we had priorSnakeY and prior-- whoops, sorry, priorHead. I did the same exact typo before-- priorHeadX. We can set this to TILE_SNAKE_BODY. And this should accomplish the same thing if I'm not mistaken, right? And then the only thing is the head location needs to change. So this is a hard-coded use case where we have a snake of size 2. "That's not a bug. It's a worm." Yes, correct. "I'm assuming [INAUDIBLE]." Yeah, that's true. Very clever, Shayna, very clever. OK. So when we're moving, remember, we got to take the tail. So table.insert-- remember, we're going to insert a new head location, right? Or actually it's going to be the tail. So what we're going to do is we're going to say we're going to say tileGrid priorHead-- oh, whoops. OK. So we're already doing it up here. We're already adding the head location. And then we're going to-- oh, no. This is updating the head location in the actual grid itself. And we need to do this in the snake data structure as well. So snakeTiles, because the head's moved, right? So the head doesn't really need to move I guess. Oh, actually, yes, it does. Does it actually? Because we're always going to look at the tail. And. if the snake is greater than 1, the algorithm kind of changes. So I guess we don't really need to worry too much about checking if we're only a size 1. Or rather, I don't think we have to update the snake's head automatically. OK. So if we're greater than 1, let's take the tail. So local tail gets snakeTiles at length snakeTiles. So this will be our last into the-- do we even need a reference to it? Because we're just popping it, right? Well, I guess we do, because we want to set tileGrid tail 1-- or 2 rather. Did I do that correctly? Yeah. Tail 1 to TILE_EMPTY, right? Because, remember, we're taking the tail and we're making that empty. And then we're going to take the head, and we're just going to add another element to the head, right? So in this case, table.insert into snakeTiles at index 1 a new head called-- because we don't actually necessarily need to move the head to the-- do I need to move the head to-- or, sorry, we don't need to move the tail to the head. We just need to add a new element to the head. Because the tail element is going to have different x, y coordinates than the head element is going to have x, y coordinates. Because the tail is way over here. And the head is over here. By moving the tail up to here, all we've done is basically just added another table with some random numbers that aren't terribly useful if they're completely disparate from what that actual nodule supposed to represent. So I'm just going to add another unit to the front of the head, snakeTiles at index 1. And I'm going to say this gets snakeY, snakeX. This is where our head is right now. So we popped the tail and then added another element to the front. It'll have the result of growing the snake fairly seamlessly. Let's see if I have all the pieces in play here. I'm guessing I probably screwed something up. Yeah, looks like I did. OK. So I'm not actually growing the snake in this case. So why are we not growing the snake? Oh. I don't have an else statement here to grow the snake. Oh, actually because, remember, we basically separated this out. We don't need an if statement. So I can just copy that out, whoops, paste that into here. Make sure these are indented the same way. Let's try that. Does that work? Whoops. Oh. No, crap. It's still not completely working, because we're not erasing. Notice that whenever we get an apple, it's adding a body element, but it's not moving around which is kind of an issue. Because that's another thing, too. We do need to move the snake. Well, actually, we are doing that already. But we're not reflecting the correct tile when we do that. We're drawing the head instead of the body. So let's figure out why that's happening. snakeY, snakeX is equal to TILE_SNAKE_HEAD. And then the previous one-- oh, right. Because if there is a previous one, then we need to draw the body at-- like, our last location needs to be the color of the body, right? So the tail is going to be empty. And then the location of the prior head needs to be the body color. So tileGrid at priorHeadY, priorHeadX is going to be equal to TILE_SNAKE_BODY, right? And if I'm correct, this should at least draw the snake body, the green color that we're looking for, which it does perfectly. I'm noticing, though, that the head is still kind of hard to see relative to the body, because the color's a little bit too similar. But it works well enough. It works well enough. I can make probably make it a little bit easier if I set the body to be something like this, maybe 0.5. Oh, sorry. Yeah, that might work actually. Let's try that. Yeah, there we go. That's good. That's a good color. Now, unfortunately, the snake isn't actually moving. And we can still collide with ourself which isn't ideal. But it's a step in the right direction. So let's go ahead now and figure out why the body is still rendering. Clearly, the tail is not being taken away and not being erased from the grid. So we kind of need to fix that up, right? So if I go to updating the head location, so tail 2, tail 1 is equal to TILE_EMPTY. Now, presumably, this is the case, because tail 2, tail 1 is not getting updated I'm guessing. So let's figure that out. Let me look at the chat and make sure I'm not mentioning it. "Strange, it worked earlier when you limited the total length to two tiles." Yeah, it did. I had a slightly different hard-coded logic in there. That made it a little bit simpler, because we weren't messing with like taking a tail and appending a new element to the table. I think I just kind of added a new element to the thing. And then I was keeping track of them. But we'll figure it out. We're here. We have a few more minutes. It's 6:00 now, but I'll go for a few more minutes so maybe we can get a little bit closer. And then maybe on a Monday follow-up stream we can finish this up sort of live and then have a completed game to at least look at. And then by putting this on GitHub, maybe if everyone wants to experiment on their own and try to figure it out, it'll be a nice cool exercise. We'll come back together on Monday and figure it out. But before I adjourn, I would like to try and make a little bit of leeway. I won't keep folks for too much longer. We'll maybe give another 10 minutes or so. Let's go ahead and-- by the way, the risk of live coding is you never know what to expect. And like I said, I did no research into all of this, but that's part of the fun of programming and problem solving is, you know, sort of trying to figure things out on the fly, figure out a problem that you've never tried to before. It's kind of interesting, right? I get to embarrass myself a little bit live in front of everybody, you know? OK. So the head location is working fine. We can see that by seeing the game and seeing that the cyan sort of colored square is updating. But the tail isn't erasing what's behind it, which is the important thing. And so tail, get snake tails, snakeTiles, priorHeadY, priorHeadX is equal to the TILE_SNAKE_BODY. So we're moving that. So we could see that working. It just needs to erase what's at the end of the tail, which is what this line was intended to do, which was take the last tile sort of in our grid and then edit the-- what is it? Sorry, brain fart. In the tile grid, change the number from 3 to 0 effectively. "Maybe figure out where the head should be after delta time and add tail. There we're making it head color. Make head a part of the body. Live coding's like a box of chocolates." That's true, Andre. There's very true. It is. I think it's fun. I could see why someone might get frustrated watching, hopefully not. "Life's just a huge p set that with a lot of subproblems--" very true, very true, a ton of subproblems. Every problem you solve sort of just adds new little problems. And you know, there's people online that make this game. And they live code it in like 10 or 15 minutes or whatever. So they are a lot more impressive, I guess, to watch. But I guess the point of this more is to kind of illustrate like live problem solving, and to take questions, and to sort of have a conversation about it and facilitate back and forth communication. "Thank you, Colton. I will join you again on Monday. Have a great weekend." Thanks, Steve, appreciate you coming by again. Yeah, we'll, do it again. We'll do a follow up on Monday, and we'll figure it out. "[INAUDIBLE] it's really good, though." Yeah, thank you, appreciate it. Yeah. It's a work in progress for sure. But I would say it's 3/4 of the way there. And on Monday, we can get it to 100%. "Am I the only one scared by the amount of figuring out this requires?" Nowanda 3333. Yeah. I mean, that's kind of, I guess, the point of doing this sort of thing is to kind of have like a demonstration of what coding and solving a new problem feels like. I mean, some people might be better at this than I am. And I'm certainly not the greatest programmer, but I've definitely programmed a bunch of games. So I kind of have a general idea more or less of how to do stuff. But a lot of it, especially if you've done no research, is just kind of figuring out like, OK, what works? What can I guess would work? How can I structure my data to make it work? There's a lot of solutions to every problem, like when we're talking about taking the tail and appending it to the head, adding a brand new head element. Replacing the neck was my initial inclination rather than adding to the head. The head is probably a much cleaner way to do it. There's so many different ways to do it. And even simple games like this-- games are particularly difficult, because they have a lot of stuff going on. You have things occurring over time. And you're messing with graphics. And you're solving abstract problems. It can be difficult. But I find it a lot of fun even if we didn't get 100% of the way there today. I think we can close the loop and do a lot more fun stuff in the future as well. "I couldn't make it work for my Scratch project either, making the snake dynamically grow." Well, we'll definitely get it to work. We'll get it to work on Monday. And I don't know. I guess maybe I can in advance take a look at some algorithms for this kind of stuff. But the figuring it out live kind of feels a little bit more authentic. And I guess it allows us to also have this sort of like two-way conversation which I kind of like, like taking suggestions from everybody and being able to say, oh, maybe this solution is actually better. Maybe this suggestion could be changed in this way. You could do it this way. And then we actually end up making a project together versus me just kind of saying, oh, here is how I figured out how to do it. This is how you do it. Let's do it and kind of like me talking to a wall almost. Not really a wall, but, you know, you get the idea, sort of idea. So I don't know, I personally like it. But let me know what your thoughts are. "I'm a bit confused as to what the difference is between the blocks between 112 and 119." 112 and 119, OK. So this is updating the head location. And 102 and 107, excuse me, 102 and 107. In this case, when we have an apple and we want to add it to our score, we want to not take from the tail. And we're just going to add a new head element to our snake. Because our snake is growing at that point. If we're just moving, the goal of moving is to take the tail element and think about putting it at the front of the snake. So we're kind of like constantly doing this shift operation with the snake. But in reality, we don't really need to take the tail and put at the front, because the front should kind of have the information about where that head element is. And if we take the tail, the tail element has the x, y of the tail. So it doesn't really work. So we kind of just have to pop the tail and then push a new head element with the new x, y information to the front of the snake, so good question. "Thank you for your time. See you Monday. Time in Morocco is 11:00 PM. I need to go to sleep." Thanks, Elias, appreciate you tuning in. I'll see about maybe starting a stream a little bit earlier, maybe like at 12:00 my time, noon here, which should be ending a little bit more around 7:00 or 8:00 your time. We'll see. So thanks for tuning in even though it's really late, appreciate it. JP, "the code's also been writing to look and read intuitively. The movement's pretty straightforward. Drawing the snake is also straightforward." Yeah, Lua's a great language. I really love Lua a lot for that reason. It's pretty easy. It's kind of almost C-like. But it also has a lot of readability. And it's just kind of clean and nice. A lot of the things about it I find-- or not a lot of the things about it. But a couple of the things about it I find a little bit questionable, like the do and then the end keywords and stuff like that. I'd probably change that about it if I were redesigning Lua from the ground up, but it's a good language. I like it. It's fun. Shayna says, love the live coding session. You did a great presentation, very interesting and authentic." Thanks, Shayna, appreciate it. Would have preferred it be a little cleaner, but, you know, we'll get there. This is only stream number two. We got a lot to go. So on Monday, hopefully, we can fix everything up. Nowanda, "it's very fun. I forgot about my tea. And it got cold while watching you twice." Thanks, Nowanda, appreciate it. I hope you get to drink your tea. I hope it's not too late. "You're streaming on Monday is the same time, 3:00 Eastern Standard time?" I think it might be, Elias. I'm going to see about the logistics of getting it up and running at 12:00 Eastern Standard time instead, so it's a little bit easier for folks that are overseas to maybe tune in. You know, we'll see. We'll see what I can do. "My gut feeling tells me that keeping track of the snake's body coordinates in an array or list would actually make the code less readable, but more abstract and most likely more editable." Yeah. Yeah. That's effectively what we are doing. We just need to store them, because we need to keep track of where they all are at any given time. But we also need to edit them. And that might be part of the thing that we're not doing is actually going back through all the snakes. Well, actually, is that true? We don't really need to edit them. Or do we? That might be part of the problem. I think we might actually have to edit them, because the tail information is constantly changing. I don't know. I have to think about that a little bit more. I have to think about that a little bit more. But we do need to keep track of all of the different nodes, so we have the tail from which to, I guess, pop off. I have to think about a little bit more to be quite honest with you. My brain's a little bit fried after the three hours of programming. So I'll spend like an hour or two looking at this and figuring out a plan of action. And then on Monday, we can come take a look at it and finish tackling it. "Great job on the stream." Thanks, appreciate it JP. Thanks for coming in. I guess here is where we'll cut the stream. Again, Monday I'll set up an event. I'll set up a stream to finish this up. I'll look at it over the weekend. We'll get it all sorted and situated. Get it on GitHub as well, so that all of you can download it and mess around with it. If you have suggestions on a next game to implement next week, maybe starting on like a Thursday or Friday, let me know. Someone suggested Pac-Man. So Pac-Man might be fun. That would be a little bit more complicated. So that one for sure would probably take I want to say like four or five streams to fully implement. I could be overshooting it, but it's a bit more complicated than this. And this took more than one stream. So we'll see. But, yeah, let me know on Facebook and on Twitch, wherever. Let me know your suggestions for more games to make. I'd be happy to create some new ones. Thanks, Irene. "It's been great fun today. Thank you. Will probably not be able to catch it on Monday, but we'll watch later." Yeah, we'll post to VOD, so you'll be able to see what's up. Thanks, Irene for tuning in, appreciate it. OK. So I think with that, we're going to call this stream. So, again, this is part one of Snake. We got the snake sort of rendering around, moving around, consuming apples. We have a score. We have a grid. But we do not currently have the snake moving correctly. And we do not have collision with the snake and itself. But those will be the things that we tackle in the next stream. So thanks again so much, everybody, for coming. I will see all of you at some point soon. Good bye.
B1 中級 SNAKE FROM SCRATCH (Part 1) - CS50 on Twitch, EP.2 (SNAKE FROM SCRATCH (PART 1) - CS50 on Twitch, EP. 2) 10 0 林宜悉 發佈於 2021 年 01 月 14 日 更多分享 分享 收藏 回報 影片單字