CS50 2D - Lecture 3 - Match 3
1:54:59

CS50 2D - Lecture 3 - Match 3

CS50 24.04.2026 3 921 просмотров 102 лайков

Machine-readable: Markdown · JSON API · Site index

Поделиться Telegram VK Бот
Транскрипт Скачать .md
Анализ с AI
Описание видео
This is Lecture 3 of CS50 2D — explore how to build a smooth & engaging Match-3 game using tile grids, tweening, timers, and color palettes for fluid gameplay and polished visuals. To take the course for a certificate, register at cs50.edx.org/2d. *** This is CS50, Harvard University's introduction to the intellectual enterprises of computer science and the art of programming. *** HOW TO SUBSCRIBE http://www.youtube.com/subscription_center?add_user=cs50tv HOW TO TAKE CS50 edX: https://cs50.edx.org/ Harvard Extension School: https://cs50.harvard.edu/extension Harvard Summer School: https://cs50.harvard.edu/summer OpenCourseWare: https://cs50.harvard.edu/x HOW TO JOIN CS50 COMMUNITIES Bluesky: https://bsky.app/profile/cs50.harvard.edu Discord: https://discord.gg/cs50 Ed: https://cs50.edx.org/ed Facebook Group: https://www.facebook.com/groups/cs50/ Faceboook Page: https://www.facebook.com/cs50/ GitHub: https://github.com/cs50 Gitter: https://gitter.im/cs50/x Instagram: https://instagram.com/cs50 LinkedIn Group: https://www.linkedin.com/groups/7437240/ LinkedIn Page: https://www.linkedin.com/school/cs50/ Medium: https://cs50.medium.com/ Quora: https://www.quora.com/topic/CS50 Reddit: https://www.reddit.com/r/cs50/ Slack: https://cs50.edx.org/slack Snapchat: https://www.snapchat.com/add/cs50 SoundCloud: https://soundcloud.com/cs50 Stack Exchange: https://cs50.stackexchange.com/ Telegram: https://t.me/cs50x Threads: https://www.threads.net/@cs50 TikTok: https://www.tiktok.com/@cs50 Twitter: https://twitter.com/cs50 Twitter Community: https://twitter.com/i/communities/1722308663522594923 YouTube: http://www.youtube.com/cs50 HOW TO FOLLOW DAVID J. MALAN Facebook: https://www.facebook.com/dmalan GitHub: https://github.com/dmalan Instagram: https://www.instagram.com/davidjmalan/ LinkedIn: https://www.linkedin.com/in/malan/ Quora: https://www.quora.com/profile/David-J-Malan Threads: https://www.threads.net/@davidjmalan TikTok: https://www.tiktok.com/@davidjmalan Twitter: https://twitter.com/davidjmalan *** CS50 SHOP https://cs50.harvardshop.com/ *** LICENSE CC BY-NC-SA 4.0 Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License https://creativecommons.org/licenses/by-nc-sa/4.0/ David J. Malan https://cs.harvard.edu/malan malan@harvard.edu

Оглавление (23 сегментов)

Segment 1 (00:00 - 05:00)

Hello world. This is CS502D. This is week three and this week we're going to take a look at a classic game that a lot of people are probably familiar with in recent times on their phones is called match three well the genre and the game probably originally in its most famous initial incarnation started with a game called Bejeweled. It had a predecessor for DOS back in the 90s but Bejeweled was a game that was very popular. I remember playing this in the 2010s circa 2012-2013 quite a bit as a sort of addicting game that was kind of easy to play on phones or just on the computer what have you and it's a very simple formula really it's literally in the name which is kind of funny but essentially it is you have this grid of tiles it can be whatever you want this particular case of Bejeweled it's themed after jewelry and I think the sort of the more glitzy or appetizing the theme is the better typically for engagement. But in this case we have various jewels of different colors they're very individually recognizable and they're all arranged such that you know essentially they're orthogonal to one another you can swap any two that are directly adjacent to each other and what you get from this is essentially once you have let's say these examples up here these orange and blue gems if you were to swap these two together such that now you have three contiguous orange gems well now you've completed a match and then you'll get some points for that at which point the tiles will replace to fill in the gap that gets created from those gems and there's all sorts of other effects. More people probably nowadays are familiar with Candy Crush and I've even played this relatively recently and it's also the same exact idea in fact if you were to go between the two of these they're almost indistinguishable at a glance except you know this one being a little bit more candy themed but you know if we think about last week we talked about sprite sheets we talked about breaking a texture apart into these individual tiles and this in some ways is a continuation of the same ideas that we looked at last week but we'll also be looking at a lot of other very interesting ideas. In particular tweening and timers are the two biggest ones this lecture iteration. We looked at anonymous functions in Flappy Bird when we introduced the state machine and the ability to have anonymous functions instantiate the states within that but in this week's lecture we'll look at that a little bit in a different use case in conjunction with tweening which is essentially just interpolating a value between two points timers which will allow us to perform that but allow us to do lots of other very interesting things to think of our game as a set of operations over time for things that should animate or transition smoothly up till now we've only been doing essentially automated movement based on velocity DX and DY things have been otherwise very discreet and very instantaneous and as a result we don't have that fluidity of motion that allows us to do certain things like transition things from one spot to another so we'll be able to do stuff like that. We'll look at how to solve matches which is the key to match three how do we detect that a match has actually happened and then replace those tiles in the grid. We'll look at how to fill that grid up with tiles and we'll look at how tiles and ID sort of map one to one in this version of how we're going to implement things using quads which we looked at last week tile sheets and then lastly at the very end is a little bit of fun we'll look at sprite art and palettes which are it was another detail that is really important in the aesthetics of a game and which we're capitalizing on this week with our sprite sheet which we as you can see here through a few previews of what our goal is to implement today we have a somewhat feature rich game example implementing the same exact principles of match three using its own version of its own tile set therein but I think it'd be nice to actually get a chance to take a look at what it looks like and I'm hoping maybe if we could get a volunteer to come up and take a look at the game and try it out. I think we might hit it. Dan right? No David. Oh okay nice to meet you okay. So I'm going to go ahead and run the code here. Okay. This is this week's final example all yours. So arrow keys will move between start and put game enter to hit to start the game. Notice we have a transition to white we have this moving thing transition as well. So the controls are arrow keys will move your cursor around Okay. and then if you press enter you'll select a tile to swap and then you can swap it with another tile adjacent to it directly orthogonal left or right top to bottom. So then you going to so that will click it in place so then you want to move to the tile that you want to swap to. Oh so click it once. So you had it selected there so now you have that one selected so now you can move into you know top right left or bottom. — I'm playing this well and then you can move those two you can swap those two if you wanted to. Okay. then we have the

Segment 2 (05:00 - 10:00)

swap operation performed notice they happened sort of smoothly they you know they tween positions. Yeah okay I can do this one now. Yep. Oh I think I know what I'm doing now. Oh subtle color that one's a purple color and that one's a pink color. — Very subtle color difference it's in the highlight here. This is this particular sprite sheet has a couple colors that are very similar. All right I'm going to move this one up. There we go. — Amazing and then tiles notice the tiles move down to fill it in. How did I lose? I just won. It's a you know it's a it's balanced a little bit for you know high scores but it's we have all the pieces here for you know match three but you know thank you very much appreciate it. See you next time. Go ahead and enter press enter there to restart it. So I think we saw a bunch of interesting pieces looking just at that example. The biggest pieces that jump out to me and I think the pieces that are going to be relevant for this week's lecture in particular that transcends just this lecture are the transitions themselves. So if we I'm going to restart the game just so we can look at a couple of pieces real fast but we'll notice one right off the gate match three the letters are rotating their colors on a timer. It's very fast in this case but it is operating on a timer. The background is scrolling but that's not anything related necessarily to this week's lecture but if I hit start we'll notice that we fade to white and then from white into a new state and then we have this transition of this or rather this this translation of our level text to the middle of the screen and then off screen so this Y position interpolation. You'll notice also at the in this box of text to the left we have a timer which is counting down to zero at which point the this current level will end in the game will the player will lose the game if they were to go over their score of 1250 they go to level two at which point their score threshold will be higher for them to not lose effectively and you know as we saw even moving two tiles they don't instantly switch positions they interpolate between the two positions and the same thing were to happen if you were to watch the match occur here you'll see that the space gets filled by the tiles above it and also the new tiles that come down will interpolate into their in this case their Y position in all cases their Y position. But there are a lot of sort of similar pieces there but they all come from the same idea of using interpolation using tweening and using a timer. We've done everything based on automatic sort of position calculations using physics in the case of Breakout and Pong and Flappy Bird you know we did have gravity applying to the bird but the pipes were moving sort of like on their own from right to left but also the way that the pipes were generated was a sort of primitive version of the timer that we'll end up looking at. And so I think in order to set the stage for us going forward before we even get into the timer stuff I think it's important that we have a little bit of a review in terms of the quads that we looked at last week with Breakout. That was one of the bigger themes of last week's lecture last sorry last lecture Breakout and that is this idea of having a larger image that you can then slice up and then draw an individual piece of that texture. So if I were to go to my source code here I'm going to go ahead and close out this match three main repo here and then let's go to quads zero. Quad zero is very simple in fact it's not really anything that people haven't seen yet but here to start us off with is just the texture that we're working with and in the case of last week we saw with Breakout there was a texture that was similar to this it was a texture atlas it had a lot of different types of elements on it whether they were bricks they were paddles they were the balls or the hearts different colors different arrangements different sizes for the paddles chopping up that texture was a little bit of work this particular week's lecture we're only using one texture and this texture is uniformly the same size for all of these tiles therein these are all 32 by 32 tiles and we can split this up into one just singular array or multi-dimensional array if we want to based on color but or based on the rows and they're all just 32 by 32 pixels so in some ways a lot easier to deal with and think about than last week but you can also envision that each of these individual tiles could therefore if we're just chopping it up into one you know array or table 2D or singular dimensional they all can just get a number an ID essentially that maps to whatever that tile is and then that allows us to say oh okay which three numbers maybe are the same that are contiguous or so on and so forth. So I'm going to go ahead and look at quads zero here real quick. This is a very simple example. I'm going to run it.

Segment 3 (10:00 - 15:00)

it. We'll see that it literally just prints the texture cuz the goal is for us to get started. Just what do we have to work with? Where are we moving towards? So, this is just literally drawing the texture. It's nothing that folks really haven't seen yet, but we're loading it. We're getting things ready to go. Just a very simple love. graphics. draw of the texture. Now, as we saw last week, if we wanted to draw a simple piece of that or an individual piece of that texture, we need what's called a quad. So, if I go ahead and go back to the code and let's go and open up quads one now instead of quad zero. We can see that we have, as we used last week more or less, a single quad at the top left of the screen. Which in this case literally just the first quad of the texture, the first rather sprite or element or texture image within the actual atlas, which is just the singular block at the top left. Recall if we were to just open the texture again, it's right inside this same folder. It's the same just simple sort of khaki-colored texture tech tile. In this case, if we remember from last week, we can take the same image and then same load the same texture and then draw it kind of largely the same. But now we're going to create a new quad, which is from 0 on the X and Y to 32 Y. So, we're defining a 32 by 32 sort of square rectangle as our quad. And then if we come down here, remember, we can just draw quad as the second argument to love. graphics. draw and that has the result of the texture gets drawn, but only through that window of that quad. So, quads two and, you know, caution for folks, this might visually look a little bit might be a little bit intense for a second. I won't keep it on screen too long. But, we can sort of, if we were to chop up our texture into all of those individual quads, right now in that last example, all we did was we just created one quad. But if we want to have this idea of every single tile in our game, we can generate every single quad per the generate quads function we looked at last week, which is in this repo as well. We can store all of those into a data structure and then index into it index into this table, this array. And then the index is effectively just the ID of the tile. Right? If we open up the in this case, we'll let's open up Well, we can look at this texture. But if we imagine that this is ID one, if we were to break up this whole texture into just one large array of quads, each of them are 32 by 32, then this one at the very top could be ID one, this could be ID two, three, four, five, six, and then seven, eight, nine, 10, 11, 12, so on and so forth, if you were to just iterate throughout the entire chopping up of your texture into quads. And that's essentially what we mean by an ID whenever we're trying to draw some kind of a sprite or some kind of tile or whatever we want to talk about. This will be relevant especially next week, too, when we get into Mario into worlds and such. But these IDs, these indices into this array is effectively the way that we can think about these tiles as entities that are orderable or comparable. So, and also their color and their As you can see, if we look at this texture, for example, we have these different colors that can be grouped into their own tables, their own sub-arrays, which have variants. In this week's lecture, we won't look at variants as a relevant factor. That's going to be part of the problem set, but you could imagine comparing that it's in this color range if for equality, but then whatever the variant actually is on that tile, stored as a separate flag, is some sort of score multiplier or the like. So, if we go to quads two, and caution again, this is going to be visually kind of a lot, but I'm just going to do it very briefly for a second, and you'll see a texture, rather the set of quads very rapidly changing on the screen. And so, in order for us to have this idea of a board of tiles that's consistent, right? It's one thing to draw the tiles, but it's another thing to actually have a board that's a data structure that we can compare tiles within and actually have some persistence. Right? We can draw quads, we can draw rectangles, we can draw the texture, but we need some persistence. So, this is illustrating the first step towards, I guess, that idea is illustrating that things aren't persistent now, but we can draw any at any point in that quad table any given quad. So, let's go to quads two. So, we'll notice the one big thing here is rather than just creating an individual quad, we're going to use that generate quads function that we had previously. And if we I embedded it here just at the top of main. Typically, this is in util. lua within each project. But all we're doing is we're essentially saying

Segment 4 (15:00 - 20:00)

saying, you know, given a texture and what's the width and height of the tile within that texture. We're assuming a uniformity of size for all of the texture all of the tiles, rather. And this applies to animations, various other things we'll see later. But we're going to get whatever the width and height is in tiles of this texture, and then we're going to create this quads table that we're just going to using a 2D loop for every Y going, you know, every row throughout the entire texture, every column within each row, we're going to create at quad counter within quads that new quad. That's effectively essentially the representation of this tile, the window of looking at that tile in our texture. And then just increase quad counter and then we're storing essentially just all of the quads that make up the entirety of that texture. And then as a result, we can then just randomly choose any one quad to draw per frame and you see the result on the screen. We have just this quickly going through all of these random texture or at all these random tiles, right? Because we're drawing it in the same place. We're drawing texture, but we're deciding to randomly choose an index within that quads table that we've created from generate quads. And in case we never mentioned it previously, this number sign is essentially how you get the length of a table in Lua. So, we're just saying a math. random between one and the length of the table quads so that we just get a random number each time. So cool. We have the ability to draw whatever random tile we want from that texture. Now, we can sort of get closer to approximating our grid if we were to think about, okay, well, to create a grid, I need a random assortment 2D assortment of tiles, so let's just draw that. Warning for folks, it's very visually kind of intense, but that is if we were to think about, okay, now let's draw over an entire range of tiles, not just the one corner, but let's actually pretend we're drawing a grid of tiles and then do the same thing. We are getting closer. So, if we go to quads three, we're using the same logic essentially. But instead of in draw doing a singular draw at the top left, we're going to go ahead and draw from 288 to 32 backwards and basically from zero to 288 going backwards in 32 pixel increments and then 512. Sorry, that's from the Y axis going from top down to left to right. We're going to draw in 32 pixel blocks a random tile. And that's effectively what we're doing when we draw the board in the actual distro, except here we're not storing it. This is just ephemeral information, which ultimately isn't that useful because it's not preservable. We can't perform any operations on it, but we're getting closer to our end goal. And so, that was full screen quads. And then lastly, what we want to do is we want to save these. We want to have these be persistent because this is how we actually have a board state that is checkable. We can actually iterate through it and say, okay, this ID or this whatever we decide to store, this tile is equivalent in color or equivalent in tier to this particular tile. So, if we go ahead to quads four and just run this, we'll see it's no longer jumping all over the place. We have a consistently rendered set of tiles. Very cleanly fills the screen. And all we're essentially doing now is, well, the one piece left is we have to store We're generating this random number every frame, but we just need to store it in place. And then if we store it in place in a data structure, we can just iterate over it X over Y or Y over X. And then just draw it the same each frame. So, let's go ahead and take a look at that. In our love. load, you can see we have a tiles table. So, this is where we'll store things. Same thing everywhere else. We do have a refresh tiles function, which is going to generate things. So, if I reload this, you can see I can hit, I believe, space bar to refresh tiles, call that same function. I have a key handler set up. And all this is doing is running that randomization, except it is storing them each time I do it. So, I can effectively do the same thing. Almost imitate what we were just looking at. But we've essentially got our table now. We've got our Well, I say table. We have our data structure representing our game, our board state. If we come down here, refresh tiles is just going to store numbers. Right? That's all we're doing. We're just storing the number of whatever that quad is that we want. And then we can just reference that number later when we do that two-dimensional iteration over our data structure down here in draw, we'll just say draw the texture at quads at tiles YX, which we're using a 2D array to in this case to store the tiles. So, if we come

Segment 5 (20:00 - 25:00)

up here as well, and an important detail here is that rather than storing all of these necessarily in one contiguous array, we can choose to store them in a set of arrays or an array of arrays, a 2D array. Which some folks prefer to store it singularly, in 2D. I typically tend to prefer to store arrays in 2D, but there's it's relatively simple math to convert between the two, and there are certain environments where storing in a single array is going to be more performant. But, in this case, we're going to store everything in a 2D array, and so to do that, what we're doing is when we refresh tiles, tile starts off as just this empty table here, but as we're iterating over Y going from one up until 288 / 32, which I think is somewhere around seven, seven or eight, um what we can do it Oh, sorry, that's nine. Yeah, I can't do math. Um but, what we're doing is saying from essentially from one to nine, we're going to insert a empty table here, which is going to then store that row of tiles. So, at tiles Y, we're then going to have this tile this other table, which is the row there. So, Y being the rows, X being the columns. So, at this row, we'll then store we'll insert just some random ID. In this case, we're just math. random the length of quads. And then what that's what allow us to do is as we come down here, we can just iterate over quads tiles YX is going to be the actual array our 2D array, our tiles our data structure for the board, quads being our quads table of IDs. So, we'll fetch the ID, link it rather our our table of quads that are effectively like thinking of our tiles like either IDs, which we'll do through here. So, by offsetting into quads, we then are able to redraw the exact tile that maps to that ID, that random number between one and the size overall of our number of tiles. And you might want to adjust that to account for color and the fact that our color is spaced apart in groups of six, for example, and two groupings per row in our sprite sheet, which is tackled in the distro, but that's one consideration when you're splicing up individual textures, sometimes they don't always align perfectly. Sometimes you might still have to navigate the semantics of the tile sheet versus just the overall straightforwardness of the operation. And so, with that, now we have essentially got the entirety of what it means to store the board, at least in terms of it visually and also for persistence, um for right now. All right, so with that, now we've looked at how to think of tiles sort of as data. Right now, it's literally just a number. It's just a random number that we're effectively using to index into our sort of array or table of quads. And we're going to put that aside for now. We've got our quads, we've got rectangles, we've got textures drawing, board state largely sort of figured out. But, now we need to take on, I think, one of the bigger parts of this week's lecture, which is the idea of timers and things that are happening over time, or if it's happening after a certain period of time. And I want to just re-look at a couple of things. So, in particular, I think to start with, I should say, the first time we looked at this was in Flappy Bird, when Flappy Bird had pipes that were moving from sort of right to left on the screen, and that was occurring on a timer. And I'm going to show sort of a naive approach to how we could do this ourselves. And let's go ahead and to do that, I'm going to go ahead and close all of these. There were a few things that we've seen, and we'll take a look again at those things as we're looking at all these and how they apply to the game. So, timer, we'll look at timer zero. And I'm just going to run timer zero just to show it for now to get started. It's very simple. It's just a timer, literally just a text string changing, some number that's incrementing every second. And this was effectively what we were doing with the pipes in Flappy Bird, except they were moving they were instead of a timer a second going up every two seconds or so, we would have pipes that would be moving from right to left, or spawning to move then move right to left, and then their position would change over time until they got to the end of the screen, and then they would despawn. And that's fine. It works in a simple sense, but it is a little bit cumbersome, especially when you think about what if I have 10, 20, 30, 50 things that are all moving around on the screen, right? Let's take a look at how we're implementing it here in timer zero. So, all we're doing is literally just we know that we need to draw a second on the screen, and we know we need a timer to measure how much delta time has passed, so that we can eventually say, "Okay, a full second has passed. Let's increment second and let's wrap right back around to zero accumulated milliseconds effectively or

Segment 6 (25:00 - 30:00)

fractional seconds. So, here in update, we do indeed have the timer is getting its DT increased, and then if second timer is greater than one, in this case second time meaning like number of seconds, then we'll increment current second, and then we'll just modulo by one to loop it back around to 0. whatever something. And the result of this is that, sure, we have this looping timer. Every time zero this number that's zero is going to accumulate up to one, it will go back to zero, and then this will, you know, over time track things perfectly the way that we want to. It's fine for now. Um but, I would argue that it's not the most elegant or efficient way. It can be quite ugly, especially if we were to look at, for example, the idea of having maybe multiple timers that have different intervals, for example. So, let's go ahead and open up timer one. And I'm just going to run this. And we'll see that, okay, we've got different timers running now. We've got a timer running every second, every two, every four, every three, and another every two seconds. And they're keeping in step fine. It's great. But, let's look at how we actually implemented it with our current approach. It's not the most elegant solution in the world. There are, of course, ways to improve the way this is structured and organized and whatnot. That's essentially the point of what we're going to start looking at here. But, you can see that this approach, even mentally if we're just thinking about it, it can be a little bit cumbersome. We have all of these timers, actual values that are getting adjusted and incremented, and it's just not a pleasure to deal with. Especially in update, all of these have different times, different durations that they need to be kept track of. DT needs to apply to all of them. It's just not it's not great. It's not a great approach. So, what we can do is we'll take a look at timer two, which is the clean way to do this. So, there's a library in this ecosystem. Um you could get something similar to this analogous to this in many different environments. In particular, what we're going to be looking at is similar to a lot of what happens in the web world or the JavaScript world, but this library is called knife, but you could even make something like this yourself if you wanted to. It's got a lot of really cool modules. We'll be looking at only a couple today and throughout the course of the term. It's got a lot of various things that are really neat, but timer in particular at the bottom is going to be our biggest one today, and also chain. We'll be looking at chain a bit. These will recur throughout the rest of the term. They are they are ubiquitously useful tools to use to make your games a lot more smooth, and in and also just conceptualizing time and things moving over time in a way that's not as manual or procedural. I think that's really the bigger takeaway that I want to emphasize. And so, there are a couple of functions that we want to take a look at here. So, timer. every, timer. after. If we think about this, when we want to do something in our world, often when we think about transitions, for example, every one second, I want to increment the counter or decrement the counter. We already saw that with the timer in the game. Or after one second, I want this thing to move over here. I don't want it to do it every second, but maybe something moves into a position, and then after one second, I want it to then move to another position. We did see that in the transition state of the game, which had the level text. It moved from the top to the middle, then from the middle to the bottom after about one second or so. And these two alone allow us to do so many different things based on time, based on thinking in terms of time, and not having to manage time in a sort of unintuitive sense. And so, that's what this library, in particular timer, allows us to do as part of the knife library. So, let's go ahead and take a look at, before we go to that next part, we're going to use this to clean up our code. So, we have manually managed timers up till now. I'm going to go ahead and transition over to timer two, and I'm going to run it first. And you'll see it largely looks the same, except I've added a an 8-second timer at the very bottom. We'll let it get to at least that point. But, you can see they all operate in lockstep very cleanly. It's very similar to what we just saw. I'm going to go over back to timer two. And the key difference here is that instead of doing all the things that we were doing previously, I can set a group of intervals. I can then counters. And then I can just iterate over all of those. So, when recall we had every one second, every 2 seconds, every 4 seconds. And then we have the counters that should map to those, the actual incremented numbers to show how many of those blocks of time have elapsed. Rather than keeping track of all of the delta time and updating them and then

Segment 7 (30:00 - 35:00)

having all of that chaos of code, we can just use timer. every and then just say, "Okay, in a loop over that list of intervals and say every whatever that interval is. " Notice I'm passing it an anonymous function. So, it's basically saying, "Okay, it's making a contract that says, 'Okay, after uh after that thing happens or after that every increment of that period of time happening, I'm going to execute this block of code that you give me. '" This is again the anonymous function idea we looked at with state machines. Here it allows us to give it behavior that should happen at a certain point in time. It literally is just taking this in as if it were a data like anything else, like a number or a string. And it keeps track of that. It holds a reference to it. It doesn't have to be declared anywhere. named anything. And once that interval has been passed, every time I should say it passes, it will perform this block of code. And we're aligning the interval with the counter here such that we then cleanly have everything getting updated in clean lockstep. And the best part about this, if you remember how big our update function was, is all we have to do now is just update timer. timer. update_dt and it will manage all of these variables behind the scenes for us in terms of the dt and how much time has passed and whether we need to call the thing and it stores the block of code. And it's very powerful. Here we're just using an example of a very simple just incrementing some value, but you can put because it's an anonymous function, you can put whatever code you want in there. And we'll see this used in multiple instances throughout this repo. But as you can see, you know, we've gone down to 72 lines. If we were to open up timer one, it's I mean, it's 100 lines and I'm sure you could clean this up in some way to not be as, you know, bulky. But just in terms of thinking, it's a lot easier to think in terms of like every X second, I want to do this thing or after X seconds, than it is about thinking in terms of I have six timers that I want to update. And think about if you have not just timers in simple numbers, but if you have positions of things moving around in space or properties of things that need to get changed or various whatever you can imagine happening and it gets very complicated very quickly. This allows us to solve that problem. So, that's essentially what timers allow us to do. Every X seconds or after X seconds, we get to do stuff. I'll draw your attention again to where we can see this immediately in our source code. Just as a reminder, match three every 0. 075 seconds approximately, those letters are changing. You can see after 1 second, the level one stayed there in the middle of the screen. And also the timer is going down 1 second. We don't have to have timers to manage these things anymore. And this allows us to a lot of really cool things. This allows us to go into not only more visually interesting domains, but just smooth domains that feel more natural and organic and allow us to do things like, for example, if you can imagine that you're playing a game, a strategy game or something and you want to move a unit between two squares. You know, you don't want to do it just instantly discretely. Sometimes you want it to move between the squares. Um things like that are very important. And that's another part of what we're going to be talking about today, which is also tweening, which is the next step. So, speaking of tweening, so that's sort of like the first step in terms of like time-based thinking is after a certain period of time, every amount of time, let this thing happen, do this thing. Don't make me think about the details about managing the time and moduloing back to zero and doing all this stuff. Just let me say after every X do Y. But okay, that's fine. What if we want to move something smoothly between two positions or to do something eq- between two values in a way that is continuous and not just instant? So, I'm going to bring up match three again just to illustrate this. So, in particular, notice that when I hit start, the transition from this state to the next. And also that operation there. So, that level operation was a combination of ultimately the timer. after and then the tweening operation, which we're about to talk about. But we faded to white and then we faded out from white into the next state. We had this sort of smooth transition between the two. And previously in all of the lectures, we just instantly switched. But now we have the ability or we I posit that we will have the ability to do something a little bit more elegant in this way. And I'll illustrate that later concretely in the code base. But for right now, let's actually understand how tweening works. I'm going to go ahead and open up

Segment 8 (35:00 - 40:00)

tween zero. Between just being like between two values. Let's go ahead. I'm going to close the others here. Let's just run this. And you see that we have sort of the Flappy Bird graphic from previously moving from left to right. Now, if we were to think about, "Okay, how do we move something with over the space of 2 seconds from one area to the next? " I mean, sort of previously we did that in a way with the pipes. Although we didn't explicitly know that they should move between right to left over a certain period of time. They just had a velocity. Well, every 2 seconds we spawned a pipe. And then we approximated some somewhere in there that over a certain period of seconds, that'll be how many pipes move from right to left. But here, you know, up till now we've essentially just been letting things move around through physics or let the bird let gravity apply, you know, positive Y velocity to the bird. Here we're actually deciding, "Okay, I want this bird to move from the left of the screen to the right of the screen over 2 seconds. " Concretely, we don't really have a way to solve that yet. So, I posit there's a way to do it sort of naively and crudely. We could essentially say, "Okay, the bird's X position is going to start at zero. And we know that we want its X position to end at essentially virtual width minus its width. " Cuz then that'll count for its width touching just against the wall. And then we can say, "Okay, I'm going to maybe multiply some that end value. I want eventually that I want it its X value to eventually be that value. So, I can essentially just multiply some value by one or I can I can try to get some coefficient to be the value one against that value and set its X to that. So, I can essentially say, "Okay, let's if it's going to be 2 seconds, we need to add up 2 seconds worth of delta time. And then if once that becomes that ratio becomes one, delta time over two becomes one, multiplied by that end destination, we will have reached that point. And assuming that we're interpolating it, we're we're doing it every frame, we're adding that every frame, we'll see it move from left to right. So, I'm going to go ahead down here. We'll see we set it at just zero to start with. The Y is irrelevant for this example. We have a timer. We're doing this manually here. We're not tweening ourselves. And we'll say that there's a move duration. We've defined this to be two up here. 2 seconds. So, again, another manual operation. And then we're going to set the X of the bird that we're going to draw to the lower of and this is going to clamp it so it can't get any farther than the right side of the screen. And X being that destination that we talked about times timer over move duration. Meaning that timer, we want that to be 2 seconds. We want it to move over 2 seconds. This is going to start at zero and effectively gradually move towards one as this approaches two. 2 seconds worth of time. At which point this will essentially be multiplying by one and we will have arrived at end X. So, this is a very crude sort of linear interpolation assuming that we start at zero here, which we'll see a more robust version of that coming forth. Which is fine. This works. This works when you assume that you start from zero, which isn't going to apply to the majority of cases probably, but it gives us a starting point and allows us to do interpolation over a set period of time. We know that we can set this to whatever we want to. We can set move duration to 4 seconds if we want and run it. And it's going to take 4 seconds instead of 2 seconds. And that number at the very top is just showing a little bit of overlap when delta time is just going to slightly go over four between frames and that's fine. That's expected. But you can see, it works. But it is a little bit heavy-handed still. We're dealing with timers. So, we clearly have some iteration. We have some refinement we can do. Let's improve it a little bit. I'm going to go over to tween one. I'm just going to go ahead and run this to start with so we see what's going on. You can see we've got a lot more birds. And they're sort of all moving at different rates, which is good. We've got in this 10 seconds capped. I'm at this at 24 frames per second. But we have essentially now 100 I think or so birds that are all spawning. They're all just tables with a rate that they're preserving and some change over time. Which is cool. It's fine. It's great. It showcases that we have some way to scale this at least.

Segment 9 (40:00 - 45:00)

I'm going to go ahead and show here we're inserting into birds one over 100 at the start of main. We start them all at zero on the X. We start them at just some random Y values so we can actually see them all sort of moving with each other. And then we set a rate for them as well. So this rate will allow them to all independently move and we're going to set that to be some sort of random value between one and the max. Some sort of value between point something and the end of um whatever our the 10 second value is. So we're going to It'll be between 10 seconds and 0. It'll be have some sort of fractional amount in there. We have to do this. math. random will give you between 0 and 1 floating point. And the reason we're doing this is because if you were to set it just to be math. random as an integer, it will only give you integerial numbers. And so this allows us to have a fractional value between 0 and whatever that end value is fractionally as well. So it'll add point whatever it could be 0. 5 0. 2 to 1 through 9 so that we end up getting between 0 and 9. 99999. Or actually well technically it's from 0 to 1 so we could get 10. Now, this is fine. And then ultimately we have a timer like some sort of global timer. And then we're going to in our update we're still doing things manually here. We have this timer that's updating and then we're going to take the timer which is acting as this sort of like global clock for all of our objects. And we're going to do the same interpolation that we did last time except now we're using the bird's internally stored rate to determine whether that ratio has reached one. And it's fine. It works but there are issues with it. Like we have this global timer that now is being checked against everything. If we have new things that start after the timer is already started, there's going to be difficulty coordinating that. It's not going to scale very well. We're still manually thinking about things in terms of timers. So there's a better way I posit than any of these approaches which is the timer. tween way. And so timer. tween is cool. It's essentially the same kind of function as timer. every timer. after. In this case, it's going to take some duration and then a definition. And this definition is going to be okay. I have some period of time that I want some operation to occur, some interpolation to occur. What do I want to interpolate? And what's cool is as we'll see in a second, love or Lua allows you to define table keys using tables so we can say, okay, I want this object to interpolate its certain fields over this certain period of time. And this not only gives us a nice sort of syntax for describing this operation but flexibility. We were just doing position there and it had an interpolation sort of expression there which was just based on the X value. But what if we wanted to do other things, other fields? We'll see how that works in just a second here. But this essentially is going to be the way that we accomplish exactly what I'm talking about without having to worry in terms of this global timer and this clumsy sort of way of thinking about time manually which you want to get out of the habit of doing. So I'm going to go ahead and go to timer or tween two rather. Let's run this. See what it looks like. You'll see we have a lot of birds. That looks like more than 100 birds. And not only are they all moving from right to left as they were previously but they also are interpolating in another attribute within their table definition, their object if you want to think of it that way. And that is their opacity. They were They started off opaque or They started off transparent and gradually as they moved from right to left or left to right, they became fully opaque. So and as we'll see here, I'm going to go ahead and open up this code. We're using knife. timer which I'll also if I hadn't alluded to that previously, these are This will be in all the repos, all the examples. We're just importing it as an object. It's just a library just like pushes, just like classes that we've used so far. And this will be in every example. We're going to go ahead and see all of the We're We do 1,000 birds. This is It's a lot of birds. But we're also setting the rate just like we did last time. They each have a rate still. We want them all to move at their own speed and keep track of it. Also an opacity that starts at zero. And here is the function in question that's important which is timer. tween. So for every bird that we are defining in this sort of list of this table of birds and we're just using a very simple not even using class, we're just using a table to sort of represent a simple class, simple object in line. We're going to go ahead and call over each bird's rate. So we're iterating over every bird, all 1,000 birds. We're setting it's using its rate, we're going to define a tween. We're going to set timer to tween that value.

Segment 10 (45:00 - 50:00)

Which is essentially saying here, manage this operation for us. Again, this is all happening in load. So this is only happening once. It's happening up front. We don't have to then worry about it too much. And then notice again, we're we're passing the bird in as a key. It's going to take a table here. That's what this is. So the bird Lua allows you to use tables as keys. So this is one of the benefits of it is you can define with your library sort of this cool You sort of DSLs of a sort to sort of express ideas. But here we're saying I want to take the bird and interpolate its X to end X and its opacity to 255. So whatever it is at this moment in time, it's going to perform an interpolation on that for us. And we can pass in any number of fields we want to give it assuming that we assign their the assuming that field exists within the bird and it's going to have some sort of end as the left-hand side rather exists already and this end value is an expression that you know, is interpretable or is uh derivable for right now. What this ends up being is that now, okay, every bird is going to be a reference to it is going to exist as a key. timer. tween is going to do the equivalent of that interpolation but we are not going to have to manage it ourselves anymore. Now it's just literally timer. update dt. It's that simple. And now we have scalability for any number of uh fields that we care about. Just run it again just cuz it's fun to look at but not only do we get X but we also get opacity for free and any other attribute. And this is very important for any of the future lectures including today's lecture. This tweening we saw multiple instances of it. Let me remind people of that. If we look at match three, let me just run it. We'll see that opacity of that full white to transparent of that rectangle is a tween operation. So is the position of the top to the middle to the bottom. And then if we move any two tiles together that's a tween. It happens very quickly but those two tiles are tweened to be opposite in this case in the Y axis. We can do the same thing between these two right here. And then if I solve a match all of those tiles, every single one of those tiles that came from the top have this Y value tweened to be where it needs to be now in the grid. So those are a lot of the main features, ideas that we need to use to expand our repertoire so to speak. The last big piece that timer gives us or knife the library gives us is this idea of chaining. And chaining is cool because it allows us to essentially think of operations in sequence throughout time which right now it's kind of difficult. So if I want to think about, okay, well the birds, yeah, they all move in one direction. The birds will move from, you know, right to left or left to right and then they their opacity changes. But what if we want some character on this on the battlefield or whatnot to move around in a series of steps and to have that be smooth? Or we want some thing to move around on the screen at different positions. Almost like a I guess I'm envisioning the old screen savers that would do something like that. Well, let's go ahead and run chain zero. I'm going to go ahead and show what this looks like. So we have a an implementation of this idea. We can see that the bird is moving starts from right to left to right, then it moves from top to bottom, bottom right to the left and then it moves from the bottom left to the top left back in its original position. Now we could do an approach of that is sort of naive in a similar sense that we've done so far. In that we could say, okay, I have this set of destinations. I know that the birds are going to need to move left, you know, it's here to this X and Y, it's going to need to move to this X and Y, then to this X and Y. Each destination should probably be marked as being reached or not cuz we're going to iterate through them in sequence and ultimately determine, okay, I need to interpolate its position between these two values, right? So if we come down here to update, we're not using timer at all here. This is just our own in-house implementation of this idea. But we can iterate over all of the pairs in that destination that we looked at and say, okay, if it wasn't reached, we'll just set our X and Y to be the interpolated sort of next value of whatever the destination is relative to our base. And so far thus far we've only been interpolating from 0 to 255 effectively. With timer. tween, we could set the start point and end point with how we want to. It's fine. In this particular example, uh this is also to illustrate a more robust way to just define the interpolation algorithm, which is that your X should be some sort of constant value that you start from that you then interpolate towards your destination value from. We've been starting it at 0 implicitly. You can

Segment 11 (50:00 - 55:00)

start this at 1. whatever you want as long as it's different from your ending value. The interpolation math will work out such that if you were to subtract the destination value from your actual base value, and then multiply the timer over movement in order to eventually make that coefficient be 1 by adding that difference to wherever you are right now or wherever you were at the base uh of when the interpolation was figured out, you will eventually arrive there once X seconds have passed by. So, this is essentially kind of a way we were implementing the time manual timer approach previously when we were looking at uh tweening. But you can see here, once it reaches movement time, cool will mark it as reached. Cool, we went through destination 1. Cool, we reached that. Okay, destination 2 is not reached. Okay, we'll move down there now. Okay, cool. Setting our base X and Y to be whatever that last destination was, which is what we do right here, because the interpolation assumes a base. It assumes this X and Y here. Any value that you interpolate needs a base in order for the math to work out such that you go from the one position to the other over that span of time. You essentially to create that line segment between the two destinations. So, we have to set our base and base X and base Y to our destination, and then reset our timer, and then we can re rinse and repeat this idea over and over again. But what kind of isn't great about this approach is that it's quite cumbersome. It's not very scalable again. We're thinking in terms of you know, the actual interpolation. We're thinking We're not thinking in terms of like, okay, the bird is going to move here, and then after that it's there, then after that it's going to move there in this sort of linear way of thinking about time. The destinations are described linearly in the sense that they are structured linearly, but the way that it's implemented and described and shown, I would say isn't quite the mental model that I like to think about this kind of stuff, and I think most people prefer to think about this kind of stuff. So, if instead we were to go to chain one, which is the better way to do this, in my opinion, using knives chain functionality, which is essentially fit this finish function here. Timer calls it chain. That's just the object that they define as these callback as these finish functions that result off of these tween operations. You can apply it to any function that you want to. So, timer. finish, which is their their idea of chaining things together. So, you can call this after any timer function as shown here, whether it's after a tween, after an every. This is the idea of once one thing happens, we should transition to doing this other thing right after that. And we can describe it therefore in linear time, or rather we can describe it in linear code, and not have to think about it in this sort of disjointed way of thinking about interpolations and managing time, doing all these things that are rather cumbersome and rather sort of separate from the problem at hand. And so, with this, we'll be able to begin looking at our idea of moving the bird through space in a quite straightforward way. If we transition back to here, we'll see we have our initial bird's fields here, just a very simple object of X, Y, opacity. We're setting opacity to 255 to start with. And then now what we're doing, you'll see that this is there's this odd sort of like nested callback kind of code here, which this is a common pattern, especially in web, especially in historical web code. Uh this idea of a promise, of this thing that is going to happen after some period of time happens, you're promised to get this value back for this or for this callback function to call to do something for you. And that's essentially what this is, only that's just called through with different words here. In the web, you might see {dot}then. Here, we're going to see colon. finish. So, after this thing is finished, this timed operation is finished, this movement time uh has the Flappy Bird moving to virtual width minus its width, so the bird's width itself, so moving all the way to the right side of the screen. We're also doing changing the opacity. This is the same syntax, you know, for this this is a tween function, which we looked at. Once it's finished, it's going to call this anonymous function, which is going to itself have a timer. tween within it, which we can then, over the same movement time, pass in the same Flappy with different values. X equal to virtual width minus Flappy Sprite. get_width. So, it's going to still be at the right side, but then we're going to move down to the bottom of the screen. We're going to say Y gets virtual height minus its height, and then we're going to In this case, we set its opacity to 0 for the first movement. We're going to set it back to 255 when it moves down, and then we're going to repeat that exact same operation essentially the rest of the way to get the exact same code that we saw previously. Only again, all we have in our update is just this timer. update. I'll run this. You can see not only are we interpolating over time this set of individual steps our bird moving, but it's also sort of

Segment 12 (55:00 - 60:00)

phasing in and out of existence, kind of like a ghost in Mario or something, which is cool, because now we can just think in terms of the actual linear nature of what we're trying to conceptualize over time. I don't want to have to worry about managing timers and doing all this sort of manual work of thinking and creating a data structure to represent these movements. Let me just call these blocks of code in sequence, and you can if up to your choice to your taste, you could choose to maybe not have them all be uh at this level of indentation. You could move them back and then just kind of have it like this ultimately, and then sort of do the same thing here if you prefer. And then you're sort of really looking at it in terms of just very linearly and not thinking in terms of the callback actually happening. But timer is going to essentially just store this function, and then just finish is going to call once its time operation finishes for each of these, and just trigger that callback function. And you can call another timer. tween or timer. whatever you want, just like you could increment the counters that we looked at previously um when we did timer. every and timer. after, kind of a similar idea. And so, with this idea, now we can represent essentially almost anything over time. You know, on finish for any particular thing, we can just say, "Hey, move this thing here. Perform this calculation. After I swap two tiles, calculate, was there a match? " And then, you know, after the tween is finished, and then perform them the actual moving of the tiles. And then after that, you know, set the cursor to be some color or do something else. And that's this is one of the bigger pieces of this lecture is this ability of thinking about time through your game and being able to do smoother animations and transition transitions between states, but also just between objects interacting within your states. So, with that, we've looked at time, we looked at managing time, transitions, movement, tweening, and timers particularly, and also a look at anonymous functions again, which is all the more going to be prevalent throughout the rest of the code. Um But I think sort of the big elephant in the room, we started off the lecture with the grid. We started out laying out the tiles and persisting them. We can move things around, but we need to actually be able to start interacting with the grid before we can really be productive in match 3. And so, let's go ahead and start with swap 0, which is going to be our very initial uh example to illustrate. And again, a reminder, this is what we're dealing with the with the tiles. We just have a list of essentially quads defined left to right, top to bottom. These are all getting IDs effectively through the and we're indexing into that quad table that we're creating. And with that, we can essentially start to compare two tiles and move them around as well. Let's go ahead and close out all of these examples that were there before, and let's go over to match or swap 0. Let's run this. So, this is largely what we had before, except now it's not filling up the entirety of the screen. This is actually in line with what we expect for the game itself. It's an 8 by 8 grid of tiles. These are all just random, and they're persistent. And with that, we can effectively begin to if we want, we could draw a cursor. We start moving the cursor around, and then we can start thinking about, okay, we have this grid. What if I want to move two blocks around? Actually swap them, right? We have the grid has actually be interactive so that we can then apply the check in order for this to be match 3, properly match 3. I'm going to go ahead and let's just take a look at what we're doing here real quick just to establish the scene. Nothing too much that we haven't seen already. We have a generate board function, which is handling the actual board generation. Everything else is largely pretty germane. We have a draw board function, which takes an offset on the X and the Y so that we can center it appropriately once we have it drawn. It's not just drawing at the top left anymore. It's actually within the confines of the screen. So, generate board here. We saw this before. All this is doing is just that 2D array. It's going to go through row by row, column by column within each row, insert within those column into those row tables, rather, these uh IDs that end up actually being what we index into our quads table to draw the quad at. So, pretty overall pretty simple right now. Um The uh the only difference is that rather than the ID just being the only thing we're storing, we're also storing everything as a within a table right here. So, we have an actual full table within which the tile itself is preserving that ID, and then we have an X and a Y coordinate because we do need to draw these tiles, and as some folks might be able to predict when we do things like tweening those tiles are going to have some sort of index into this array, but they're going to need those X and Y values that actually move left and right, top to bottom as they as they're swapped. Those are going to have to interpolate, and so they have to be some they have to actually be a stored value within this object that we can move around.

Segment 13 (60:00 - 65:00)

So, with that and again, just for illustration, too, just to bring home the point, this is an illustration this is a just a code rough mock-up of what in memory this would look like at in a simple form previously especially. Right now, they're tables of they have essentially that number behind a tile field, but you can envision our grid as essentially this. It's essentially just a array of arrays of numbers that are sort of elegantly wrapped in a table which also will carry with it color and variety metadata, and then the X and the Y that we can then interpolate. This is essentially the mental model for which we can navigate looking at our grid of tiles, and this transcends into all other domains within which we'll look at tile-based anything. In particular, next week when we look at Mario, this is essentially the foundation of that lecture as well. Let's go to swap one now. And here, we're just going to illustrate a swap that doesn't actually have much of a visual sort of thing going for it, but illustrates what it means to swap the tiles. I'm going to go ahead and open up swap one. Not going to be smooth, animated, not going to be fancy, but I can move this cursor around. And if I want to move, for example, this tile here, I can just do it, and it's instant. But, you know, it's depending on your preferences, maybe that's okay. It's certainly not, I think, what most people expect, especially playing games like Candy Crush or Bejeweled, there's some sort of animation that should happen, but we have the gist behind having this a grid of tiles, this grid of numbers that we're essentially able to move these numbers around with. So, let's go ahead and transition back to the code itself. Now, we have our board as usual. We have whether we are currently highlighting a tile, meaning that we are ready to swap a tile. So, recall actually, if tile, notice that there's a rectangle a highlighted rectangle actually applied to whatever we just chose there. We're not really doing any bounds checking currently in this example, so we could just swap these two, for example, if we wanted to, and it's totally legal in this code. Uh this is not how the main distro works, but just to illustrate. But, we do have a highlighted rectangle to illustrate, okay, this is what you chose, and then here's where you're going to swap. And so, there's a reference that has to be made to that. So, that's what we do here. We have a highlighted tile, and then um the actual Y X and Y will preserve that when we are highlight highlighting it, but we don't need to uh this isn't being used until we actually have clicked enter and are highlighting the tile. In this case, we do also want to be able to select or hovering over tiles is going to be the indicator for our little red cursor to show, okay, we're moving around, we need some way to show which tile we want to select, which actual highlight and then swap. So, that's what that's for. Here in love. keypressed, if we end up pressing up, down, left, or right, we're going to clamp our X Y uh our X and Y values by the board's size, which is what we're doing here. So, we could press enter or return, and what that'll allow us to do is if we are highlighted already on a tile, um it will perform the actual swap operation, it'll do the check, and then it'll actually swap the tiles. If we're not highlighted, well, then we haven't got an initial tile to swap, so it's not going to do that, it's going to actually highlight whatever tile we want to by setting the highlighted X and Y to be whatever that tile's grid X and Y are, and then setting highlighted tile to true. So, in the event that we've already got the highlighted tile, what we're going to want to do is swap them by getting a reference to the both of the tiles here. So, and then at that point, we can just essentially swap their information by getting a temp X and temp Y to be equal to tile two, so that'll just preserve essentially its X Y and grid X and grid Y, we're going to save them both as separate variables, at which point then we can set the we can swap their places in the board, and then we can do the sort of like classic swap operation where you have a temp variable, you have a in this case, we have an X Y a grid X and a grid Y. So, we're going to take whatever the temp variables were and give them to tile one cuz we took them from tile two, but tile two, we're just going to then take whatever tile one currently has before we do that. And then therefore, they're going to have swapped data, and then they'll be able to highlight unhighlight them, rather, and then um that'll be it. Our selected tile is going to be tile two, which is where we end up on um we're going to be still at tile two for our cursor. And there is some folks might see, you know, a few magic numbers in here, 1 8 various numbers like this. This whole game was largely designed around a consistent aesthetic and arrangement of tiles, but and if you wanted things to be a little bit more robust, and this transcends also from here into the match three distro proper, but you could set eight, for example, to be like grid

Segment 14 (65:00 - 70:00)

width or grid height or grid size in this case, and then you could tweak that if you wanted to play a game with, you know, four, five, six, seven, eight sized dimensions on your X and Y, but in here in this case, things are very specifically tailored for um a few specific numbers, so that's why you'll see numbers that way. So, that's essentially how you can swap the variables. They're essentially just like taking the metadata of those two tiles, and swapping them, literally just like changing the X Y and the grid X and the grid Y, and then that's it. And then we can draw it here. We saw that before. The generate board all the still the same, and that's pretty much it. Here, we're also drawing the red rectangle that's going to go around our cursor. Here, that's what this is. The set line width to four is just like the stroke size essentially of that rectangle, and then we just draw it over whatever our selected tile is. And then we also have this offset we're using to draw the you know, again, the board to a different um It's right here, this offset X and Y, so that we can draw it centered on the screen. This is applying to the cursor as well, so that the cursor gets moved around. Um it's not drawn relative necessarily to zero on the top left of the screen, it's getting offset. And then we set our You might see this as well a few times, the set color 1 1 1. If I didn't allude that to that last time, that's because when you set color in love2D, this is sort of like a global color. So, if you forget to do this and you see weird greens and blues and other colors like that, uh just make sure you set your color back to 1 1 1, which is pure RGB white color at full opacity. Make sure to set it to that just so that whatever the next thing you draw isn't preserving, for example, this red color, which would be the case if you were to like not set that back to full white. So, that's the static swap, so just essentially a data swap, but, you know, if we are looking at this world of timers and tween operations and whatnot, well, we can do a better job if we were to do a tween-based swap instead. And really, it's essentially going to be the same thing except now, instead of hard setting the X and the Y between the grid, rather the grid X or rather yeah, their X and Y, not their grid X and grid Y, those will stay small and discrete, but their X and Y values will actually be tweenable, right? To sort of spoil it, I suppose. Right now, we're just hard swapping them, but if we were to tween them back and forth to whatever the other tile's value was, well, we might be able to achieve a slightly better effect. So, I'm going to go ahead and bring this over to swap two. If we run this, so it's largely the same, except now, if I were to select this tile here, you'll notice that they swapped smoothly over time. It wasn't just an instant sort of transition that happened. We've decided on some amount of time, point whatever seconds, point oh something seconds, or point maybe point two seconds. And what this is going to do is the X and the Y, rather than being just like instantly, okay, this tile was now was this tile's X and Y, and now this tile it'll be some sort of like let's smoothly interpolate, and then that gives us whatever this tweening effect is. We do it again here, you'll see it just happens smoothly. Same thing that happens when the tiles come down from the top of the screen, or when tiles that are currently in line get a space below them because of a match, they come down, they tween themselves down, rather than just hard setting their position to be that new value. They'll move in the actual table where they're stored, they actually get swapped in the table, but they preserve a reference to their actual X and Y position, so that timer. tween can then like smoothly put it into the right place effectively. And we'll go ahead and we'll just look at main here. If we come down to our love. keypressed, there's all the same stuff kind of that we saw previously, except rather than the hard transition to X and Y are they their hard X Y is getting swapped here in the code, notice that now we're setting a timer. tween. No finish or anything like that, just a simple tween, but here, we're just passing in tile two, and we're saying its X should be equal whatever tile one. x is and whatever tile one. y is, and then tau1 is just going to essentially get whatever the temps is. Its x will be the whatever the temp x is and the y will be whatever the temp y is. And as a result these are going to now, thanks to timer, have a nice transition between them and things look visually a lot smoother. And it's just kind of easier once you have a lot of these things going on to not have to manage them quite as much as meticulously. Um it's still unavoidable at a certain point. There's certain things you're going to have to manage, grid positions, being discreet for checks and then the actual positions of the x and y moving around independently of that, but you

Segment 15 (70:00 - 75:00)

know, at least we've saved ourselves a little bit in some sense, but also we've gained a lot in terms of visual uh smoothness and fluidity. So, that's the extent of the sort of examples that we'll look at before we end up applying taking a look at some of the aspects of the code that are relevant for the problem set. We're going to look now at matches. So, matches this is the most fundamental aspect of the lecture itself or I should probably shouldn't say that necessarily, but it is the underpinning of what makes match three work. Certainly right now we can swap all we want to, things aren't really going to work. We can't have a way of gauging whether we've won the game or cleared a level or whatnot. This is the last big piece is how do we determine that we have a match? And then once we have a match, how do we determine, okay, well, we're going to have to take tiles out. How do we put them back? How do we get new tiles? How do we shift tiles? So, matches are relatively easy in that essentially if we have just a grid of tiles here, we can think of any match as being just three or more contiguous tiles of the same color. We can define this however we want to. For this example, we're going to say it's just the same color three in a row in either the x axis or the y axis, not diagonals, but just orthogonal axes matches in a row. Those will be a match. So, if we start at the very top and let's assume that we're going to look at every row first. We have to first calculate, okay, we're going to have to take care of the x axis. We need to check all horizontal matches. Then we're going to need to do all of that again, but oppositely 90° on the y axis to calculate vertical matches, but let's just start with horizontal matches. So, we'll start here and then we're just going to essentially say, okay, we're at this first row, we'll check this block. This is khaki color. Okay. Cool. We'll check the next color. Okay, it's red, so it's a different color than this last one. So, okay, we're not going to preserve any idea of a match. That's fine. Okay. Next one's blue. Same thing. Okay, it's not pink. It's not going to be the same color. Okay, what about the next tile? Okay, it's blue. Okay, so we're in it we're currently in a match. We're going to reset the color. And then okay, we found a match here. This is three in a row. Um and we can keep going until we get to different color and in the case that we get four colors, but here we've also reached the end of the row. We We're also going to check, okay, did we because we're at the end, if we're not we can't check whether the next color is different because there is no next color. So, we're going to basically say, okay, do we have at least three? Let's return the match. We're going to do the same thing now for the next row, just going one by one, you know, we're at blue. Okay, red's not blue, so it's not a match. Green's not red, so it's or pink, so it's not a match. Yellow's not green, so it's not a match, but then we got two yellow, but now we're at the end of the row and it's only two. It's not three, so it's not a match. Doesn't count. We'll do the same thing, kind of going through. We're always just checking, okay, is this color the same as the last color? If it was, preserve that, else the new color is whatever that last whatever this current color on this tile is. Do the same thing here. Okay, we got two, but it's not a match. And then here, gray is not khaki, but gray is gray. That's a match. And then as soon as we hit purple, we determine that's a match and we save it. We'll do the same thing. We're going to go vertically. In this case, we're just essentially crawling through the rows now. You know, y is incrementing. We're doing the same check. Once we get to the bottom of the grid, we know, okay, no matches here. Okay, oh, we got two in a row. We got three in a row. Cool. That's a match. As soon as we know that we've gotten to the gray one, we can say the match is done with. It's not a four tile match or anything like that. We got two here, but again, we're at the end of the row or end of the column, so it's not a match. And then we're just going to loop through this whole process one more time, going through every single column. And then here we get to our last match. It's three in a row. The very end, kind of the same thing. At the very end, check, do we have are we at the end? We can't check the next color, so this is a match because we've got we're storing in state in some sort of loop. We've got three contiguous tiles. And then once we're done with all of that, we can essentially say, okay, we've got four matches in this particular instance. And we can represent them as just sort of we could choose to represent them however we want to. We can say, okay, these will just become four tables that store these tiles and then we'll just essentially say, okay, let's just get rid of them. Cool. So, those are empty going to be empty spaces now in the table. We're going to have nil values here. Now, the next, you know, step of that, obviously, we can't just leave it like this. We're going to have to say, okay, well, what next? Now we need to first, before we even generate new tiles, we need to probably let gravity do its thing and say, okay, let's bring all these tiles down such that tiles can come in from the top then and fill in these gaps that

Segment 16 (75:00 - 80:00)

exist. So, what we'll do is we'll just we can essentially just think of this in terms of from the bottom up. We'll say, okay, is there a hole? Like if I go from the bottom to the top, are there any gaps such that there's a empty space below a tile? And we can do that by just starting at y equals the bottom. We'll just go up. Okay, this was solid. Cool. Solid. No problem. Solid. No problem. Nothing needed to be shifted. We'll go to the next column over or and it'll be on the x axis. So, we'll see immediately we have a space where there should be a tile. So, this will be a nil index into our table. And so, we can say, okay, I need to find whatever the next tile is vertically and that's going to need to come down to wherever this space is. So, we'll just go up a tile. We find it right away. And so, we'll just say, okay, now that we found it, let's just put it right back to where the space was. Cool. And then we can say, let's do that same exact operation starting up above. The tile that we just placed down here. And so, we find a space immediately and we're going to do the same logic. We're going to say, okay, I found a space. I mean, I need to find a tile to replace. So, it's going to go up. It's going to look up again. It's going to go up again, but it's going to reach the top and it's going to say, oh, there are no tiles. So, actually, okay, we don't need to put any bricks down to the bottom where we found that first space. We can just end this iteration of the loop and go on to the next column, which we'll do here. So, right out the gate, we find a space. So, we'll do the same thing. We'll go up. Okay, we'll replace the we'll put this tile down where we had the space. Cool. We'll go up. Oh, another space. Okay, let's do the same thing. Okay, tile comes down. Go up. And then we eventually get to the ceiling, quote unquote, of our grid where there are no spaces to fill in. There are no tiles to fill the spaces, but we have effectively shifted these down. And then we can do the same thing again for the next column and then the next column. It's all the same logic. Look for a space. Look for the highest or the lowest tile to then fall down, match that space and then it repeat until you effectively go all the way up to the top of the grid, which is the ceiling in the grid. And then once that's finished, well, okay, we've got the we cleared out all the matches as the first step. We then applied gravity to the tiles that existed already. And now what we need to do is fill in the spaces that now exist. There will always be spaces at the top of the board that need to be populated with tiles. So, what we can essentially do kind of an inverse of what we just did in that we're going to start from the top in each of these rows, but we're going to just like count down how many spaces we get to before we reach the bottom. And in the case of a full tile here, it would just be none, so we won't have to generate any tiles. So, if I go ahead and start here, uh we'll assume I guess that we don't have to here, but essentially we'd look here and say, okay, it's zero or there's a tile here already. Zero tiles need to be filled in. But we'll go here and we'll say, okay, there's a space here. And then okay, another space here. We're taking y at this column and we're essentially saying, okay, y gets one, there's a space. Two, there's a space. Okay, that's two tiles. Three spaces. Four spaces. Just checking for nil in our board. And then finally, a tile. So, it's going to stop generating tile generating uh knowing that it needs to generate a tile for this column here and it's going to move over to the next. Okay, one space, two space. And then four spaces here. And then what that's going to do is it's going to say, okay, I need four batches effectively of tiles or you can think of it in terms of individual tiles. Um in the distro, we think of it in terms of individual groupings of tiles for that row or for that column. And so, what we'll do is we'll say, okay, we're just going to essentially tween in a bunch of tiles to fill in those gaps. We'll we'll populate them. We'll put in the tiles with the grid x and grid y. We'll set all of their actual x and y to be some number above the ceiling and then by just performing a y interpolation a timer. tween onto their value, we get the effect of effectively a slide in from the top of all of the tiles that match up to the spaces that were left in our board state. And now all of the board is back into a relatively good shape. Things are fine. Unless you have matches again, in which case now is the time that you would want to check for matches, and then repeat this process. People that have played Bejeweled and Candy Crush and these kinds of games know that the most fun that you probably have when you play a game like this is when you just like get chain after chain of matches in a row and the board is exploding and doing all this crazy stuff. But, that's essentially what we do here until the board is in the sort of stable state of there are no matches left. So, we no longer have to do any replacement, any checks for anything. The user is back again allowed to move their cursor around and try to swap tiles to affect a match. And so, let's go ahead now and take a few minutes to see how these concepts apply to the code in the actual

Segment 17 (80:00 - 85:00)

distribution. So, I'm going to go over to the code here. We haven't looked terribly closely at the code in the course. It's in the distro itself for the week. But, folks might notice it's slightly less uh dense than the breakout code in that it doesn't have quite as many bells and whistles. Um so, we'll be able to focus, I think, and narrow in on some of the ideas a little bit more. But, it is still functionally a full game in the sense that you can go through the full process of it, but we have elected not to do like high scores and various things like that just to focus in on what was relevant for the week. So, I'm going to go ahead now and look at a couple of these files. So, I'm going to get rid of this main. lua. So, board is important, tile is important, and then some of the states are important that we want to look at. Now, the tile is very simple. Tile is just a It's going to have a grid X, a grid Y. regular X and a regular Y for interpolation, for movement. And then it's going to have its color and variety. And remember when we talked about color is essentially the way that we are differentiating between tiles and allowing them to be compared for matches. And that's the field that we care about most for that purpose. And then it's a lot of the same stuff that we've seen previously. We're indexing into a frame array to draw the actual texture and just get whatever that ID is for the color. We have a utility function that's actually splitting the tiles up rather than it being a contiguous set of just a uni a uni-dimensional array of tile rectangles. What we have is a function that is actually creating a 2D array off of So, we have our frames sort of table, which is divided into multiple different kinds of frames by convention in this particular game's example. We're not going to have a lot of other types of categories of frames that we care about, but this will be relevant for other lectures. But, this 2D array here is actually dividing things into color and variety. So, if we go ahead and look at, for example, in util, generate tile quads is a build up of generate quads. There's It's essentially a an extension of the idea, whereas instead of going through the entire spreadsheet or the entire sprite sheet based on the unit of whatever the size of the tile is and creating one table, we're going to go ahead and recall the texture in this particular example, if I just pull it up here, is actually split up. The rows are split up into two groups of six. So, we have six khaki, six magenta-ish, six like whatever green this is, and then six pink, and then six darker green, six red. And then that's essentially all that this does or all this pays attention to is the fact that we can twice within each loop iteration for each row, we can essentially say from one to six and then grab whatever the tile is within that group, offsetting X, and then do the same thing um essentially doing it twice will allow us to split things up into two. Yeah. In this particular tile set, are the shapes on the tiles meant to be distractions from the colors? The tiles The shapes themselves are not so much meant to be distractions as they are meant to in my mind, it was probably relatively ambiguous based on what the designer of the sprite sheet initially went for, but I interpret it as a way to have a higher value on that color, so that you can imagine if you started off all of these they kind of initially have no design on them. But, you could imagine having, for example, if you chose this star to be some sort of special tile that you then add a highlight to or a particle effect or something. Or this X, maybe you you tint it some color or you do something with it to designate it as being a bad color. Or you just treat all of these as different values that are slightly higher than regular flat color. I see them as variants that can either be point multipliers or some sort of uh feature for the gameplay. Uh a punishment, a benefit. The assignment itself is to create a tile such that if you were to match it, some variant, some shiny variant, that will destroy an entire row in addition to other aspects of the problem set. And you could designate that via some particle effect or through maybe the star in addition to the particle effect. There's really a lot of options, I think, but the overall goal, probably, if I had to guess, would be that these were probably point multipliers or feature multipliers or some other variant thereof. But, essentially what this variant of generate quads is doing is iterating over each row, creating a row sheet, and

Segment 18 (85:00 - 90:00)

then um rather a half row sheet, which then gets populated with just the colors, just the six colors, one six color block at a time, all the way through the sprite sheet. Which allows us then to do what we see here, where we can index into the tile sheet at color and then also at variety. Variety will just be a one through six in that case into that array, into that table. And color will just be from one to 12 or one to nine by I think it's nine I think it's 18. One through 18 is essentially the range of colors, which will allow you to choose, you know, red is six or whatever. Indexing into six will be that six uh will be that particular sub row of red tiles. And the variant or the variety, you could then choose by indexing into this latter part of the 2D array that offshoots off of this. Really, this is a 3D array, but the 2D array that we're concerned about uh is just these two, these two branching off of G frames tiles. So, that's tile. Board is a bit more complicated. Board's got a lot more going on for it. It's got the actual initializer, very similar to what we've looked at previously, except now we'll see that our constructor for tile takes in a random number between one and 18, which is the color range, and then math. random six, which is the variant or variety range. So, again, if we look at tile, that's what the constructor does here. Just takes an X Y, a color, and a variety, and that's what uh gets stored here. And then here, we can see that in initialize tiles, we want to also do something interesting, kind of. And that's we want to calculate the matches right when we generate the tiles, because if we start the game and there's already matches, then we're sort of in this weird thing where we're going to start to see matches happening when we start gameplay. We didn't even do anything. So, we want to make sure that we start on essentially a slate that is completely clean that doesn't have any of these matches that is ready for us to create matches. So, what we can do is we can take a look at calculate matches. And essentially the logic here is that we're just going to reinitialize over and over again until we hit a point. Mathematically, it's very easy with how number of variants that there are right now, but in the world where you might have only a few variants, you might want to instead just perform the actual matches and then just keep the same board state and just repeat that over and over again until you get to a stable board state. That's going to be faster and easier than at least in terms of processing for a smaller range of tiles than this approach. This approach is just expedient, but ultimately isn't the ideal sort of, I think, for a especially if you start to lower your range of tile uh colors and is what we essentially just took a look at in the slides, and that's what I want to focus on probably the most, this and a couple of other pieces, throughout the rest of the uh lecture and what we talk about before signing things off and talking about the problem set, but calculate matches is essentially the logic that we exactly just talked about, which is, remember again, the going through from the left to right, top to bottom, we're going to check how many contiguous tiles of the same color are there in a row on either of those axes, and then let's return those back to the calling bit of code. code is in play state. So, in play state, you know, there's going to be stuff happening. The user's going to move their cursor, they're going to highlight things, exactly like we showed. The examples that we showed are very similar to what's actually in the code. But, when we actually and we'll take a look at a couple instances of where timer is relevant for the code as well. But, if we go down here to At the very end of this part of the update function, so, we'll see this is essentially if they press enter. So, this is again basically kind of what we were looking at previously, but when they press enter, we're going to either highlight a tile and get ready to swap on the next enter click or unhighlight. Or what we're going to do is perform the actual swap. And then when we perform the actual swap, it's all the same stuff that we did previously. sort of like moving the X and Y back and forth between the two or unhighlighting things in that particular case we were just looking at. But, all the swapping is basically the same code. All the same, all the same code. Um for the most part. Even here there's a tween and in this case we're doing it over 0. 1 seconds between the two tiles. But once this tween is finished, once the two tiles have actually finished moving back and forth, we want to actually do self calculate matches. So this gets called from play state after we've actually performed a tween a move between two tiles. And we can do that in from board or actually well, I should say we we're going to call board's calculate matches from self. In this case self has a function called calculate matches. Let's take a look at that first. That's going to be more relevant. It wraps a couple of things. So the first thing that we'll do is we'll say, "Okay, let's get the matches back. " Recall that we showed that in the slides. It was you know, we go through

Segment 19 (90:00 - 95:00)

all of the thing of all the process of going through XY. Okay, do we have a match here? Get all four in the case that we saw. We want to return those back to the play state because you know, the important thing about matches is that they are effectively our score. Like the more matches we get, the tiles are each worth some amount of points. So we can do is very simply just say, "Okay, if we did get matches back from board calculate matches, which we'll look at in a second, which is the bulk of the does heavy lifting, we'll just okay, for every match in pairs of matches, we'll just whatever the length of that is, we'll multiply that by 50 and then add that to our score. That's it. That's all we have to do. And then after that, we'll do the remove matches step. We saw that we alluded to that in the code, right? We'll remove the matches. So we need to actually set those tiles to nil so we have these spaces. Then let's get the falling tiles and that includes the tiles that are in the board. So the boards will actually the tiles that are in the board will fall down and they'll get displaced and then the tiles will come up from the top of the ceiling and fall down. Those happen at the same time so everything looks as moving in unison. And then that essentially what we'll get back from this function is a set of tween operations, right? So set of XYs XY interpolations. We can just pass that directly into tween. And then once that's finished, well, again, we're going to need to make sure that we don't have more matches left cuz who knows what tiles are going to get generated? They're all random. So if there's more matches, well, let's just perform it again. And then this is a recursive function because self calculate matches is the function that we're in right now. So if there are more matches, well, we're just going to keep doing this over and over again and sometimes, you know, again, some of the most fun you have in these kinds of games is just like putting together a smart match that just gives you a ton of points and just kind of ricochets and does all this stuff. But that's essentially how we're achieving this sort of infinitely processable swap operation. It's going to calculate the matches, get all the falling blocks, do all that stuff and it's going to call itself again if it needs to, if there are matches that have to actually process. So the first step, the main step in calculate matches is going to be the actual board's calculate matches. So we won't spend maybe a terrible amount of time looking at it in tremendous detail. I'll go through it so that so folks can follow along. It's going to take too much time doing it, but essentially we're just going to go through uh every row. We're going to look through X and Y, right? Do we have horizontal matches? Is this color the same as the last one? Okay. Is was it also Yep. Okay, we got at least three. Okay. And then once we get to the next color that's not the same color, cool. However many we had and that we were storing a reference to, let's take that away from let's store it in a match and we're going to return it for points. But then also we're going to need to flag all those matches are going to get stored and they're going to get removed from the board as part of the remove matches function. So here we can see we have like a color that we're going to match, which is the first color in the loop. Every horizontal tile from X to from two to eight, we don't need to calculate one cuz we already did that by default. We're just going to say, "Is it the same color? Is the tile there at YX the same color? " And this is traditional with 2D arrays and in games typically you're going to go based off of YX because Y being each row, right? Cuz if you go top to bottom, you're going one row at a time. So Y X. color will be whatever the as we're going left to right each row. If it's the same color, cool. Increase the match number. And then if it's not, okay, well, then now we need to change the color to be whatever this color is. Did we get more Did we get at least three tiles? And if we did, let's create a match. Let's put it into a table, which we're going to insert into this matches table that we're going to return. And then we're going to go backwards from where we are currently in the match and then we're going to insert the tiles that we had found up until this point into this table. And then this table is going to get added to a table that gets returned back. And then if we're at greater than or equal to seven and it wasn't a match, we don't even need to worry about the last two. It's impossible for a two tiles that aren't part of a match at the end to be a match. So we just kind of shortcut the loop here. And we do the exact same thing for the vertical matches way down here. And then it's essentially how we get the entirety of all the matches. We just are iterating through the vertical Well, we're iterating horizontally by going down through each row and then we're going through each column by going left to right. So each we're going each column and then within each column we're able to look at all of the vertical tiles within that column to determine um if we have any matches. And then once that's all done, if there's you know, greater than zero matches within the actual match table, we'll return it. Otherwise, we'll return false. At which point we can then use that in play state. If play state gets a falsey value from this function, it's not going to have any matches. We don't have to

Segment 20 (95:00 - 100:00)

worry about it. It's not going to do anything. actually do all of this code here, recall. But if it does, we want to remove the matches. Which we're storing a self. matches reference to these matches for this purpose. So remove matches is simple. All it is essentially just for every match that we have, let's iterate through all the tiles and set them to nil in our table. The result of this is that we'll just we'll try to index into whatever that particular uh index was in our array. It's not going to be anything. It's going to be nil. So we don't have to worry about actually anything there. But then what we can do is add tiles to fill it. And then interpolate them into those new positions. So they'll get erased because they won't exist. They won't they'll lose that color value and won't be rendering. But the new the tiles above it will fall and then the new tiles will come in to populate it. So that brings us to falling tiles, which is sort of the last piece of the puzzle, so to speak. Again, we're returning a tween table. We're saying, "Okay, we want this table to come back that's got all this tween information, which is XY values in our case. " So we're saying, "Okay, remember we saw this visually, but we're just going to go through each column up until we hit the top of the actual the ceiling, so to speak, of our grid, looking for spaces and then looking for the nearest tile to bring down to that space and then just kind of going up until we reach the top of the ceiling. So here we see we start we just have a simple space. What's the Y of the where that space was at? You know, uh while Y is greater than equal to one, so until we get to the top of the we're assuming bottom would be eight, top would be one. And then we're going to get the tile that's there. We're going to try to index into array. If it's nil, this then it's not going to be a tile. That's going to be where we should flag it as having a space if we haven't already. If there isn't a space and the tile is nil, let's set space to true. If space Y is zero, then that means that it's not a space that we've actually found yet. So we can just assign our space Y to zero or rather Y and keep track of that. So as we go up, that's where we're going to need to make sure that you know, for example, if there's a tile at the bottom, we need to make sure that we put the next tile that's and let's say there's a tile, a space and then a tile, we're going to want to make sure that we put that top tile not all the way at the bottom, but above the tile that existed there. So we that's why we maintain a reference to the Y that needs to exist there. And then in the event that we have a space flagged, but we find a tile when we're going up, well, then we'll just set that actual space Y area to be whatever that tile is. We'll set that tile to nil. Setting the grid Y to be that whatever that space Y is that we had set. We're going to flag the tile itself its grid Y to be that space Y. And then it's actual cuz it's a it's discrete spot in the grid, but it's actual Y is going to go into this tween table. So this is going to move it down using its grid Y as a reference point minus one cuz of zero coordinate based access versus table based one indexing. But times 32, which is the tile size. And so this is just going to eventually accumulate a bunch of these tiles that need to move down. They all are going to be tiles that move down until we return at the very bottom the tween operation or the the table of tween operations. That's for the in the case that we're talking about right now, we're talking about the tiles that are already in the grid that need to move down to fill the spaces. But as we saw, the very last example visually illustrated was the top of the actual grid has spaces that need to get filled with new tiles, which is how the game is sort of infinitely playable. The tiles that go away have to be replenished. And so we do that here, you know, again from the top, we look down. Are there tiles at all in this or are there spaces? And if we see spaces, we're going to count the number of spaces going from Y equals one to Y equals eight. And then we're just going to essentially add those to our tween operations. We're going to add them into the table, but we're also going to add a tween operation to them that has their Y position tween from -32 is where it starts at to whatever it's grid Y minus one times 32 is. So, it starts at the very top of the screen and then comes all the way back down to where ever it's resting place is in the grid, which is going to be multiplied by 32 pixels. So, all of that and we'll just again visually illustrate it. Then we'll take a couple seconds just to like again re-look at some of the other things the tweening and timer based things we looked at. But we'll move over here. See if I can get a match. But you know, I'm moving around so these grid X's and Y's. We've got this tile here. I want to move this here. The three tiles above it

Segment 21 (100:00 - 105:00)

are going to tween down. Three tiles above that in to replace those tiles. So, we do that. You can see that happen there. And then if I let's say I move if I can find a match. Uh let's see. Let's move. Kind of tricky sometimes. Okay, let's move this one here. So again we're going to have a sort of uh the red purple yellow are going to move down to fill this space. Which they did indeed and then we had three tiles that spawned above that. And then if you were to get sufficiently lucky or skilled at playing something like this and have many matches occurring and and filling in the space or rather many matches that would sort of organically come up to be because you got a bunch of matches figured out somehow. Um you would just essentially see this repeat over and over again. Um I'm going to restart it one more time because we should look at also again draw your attention to the transitions. The last two pieces that we'll look at are the transition between this state and the next state and then also the sort of moving down of the level text, which are the last sort of pieces I think that are relatively new that represent the tweening and timer based operations that we looked at. Let's go ahead and start this. So again there's a imagine here you know, if you were to think about it, right? Like how do we make this screen white fully white? Well there's not really an easy way to do that other than a maybe a rectangle that we could draw to the screen on top of everything else that was just transparent and then transitions to full opacity and that's exactly what we do. And then you know, how do we go back from the next scene or the next state to yeah, rather the transition at the start of that. How do we go from that to seeing to having the white sort of fade out? And that's just the same thing in reverse. If we just start that state with a fully opaque rectangle that's white and then just fade it to zero opacity, well we get this illusion that we've transitioned smoothly between these states. They've been like very actually still the same abrupt transition. It's just that we are like tricking the user the viewer into not seeing that through clever uses of timers. So, here's the rectangle. It exists already. It's not being rendered. It's going to tween its opacity to one. And then it starts off at one and tweens it to zero in that begin game state. Then it gets the sort of level text transition down then transition at the very bottom. We'll go ahead and just see what that looks like here. I'll close these examples. And in particular we care about the start state because that's where we're going to keep the actual transition rectangle for the transition to the begin game state. If we look up here either it's down here. We have a transition alpha that's zero. And if we press enter you'll see that we do indeed do a tween operation between self in this case you can pass self if you're within an object. You can do that. It'll just use whatever self. transition alpha is. And then transition that to one. That'll go from zero to one over one second. That's what this remember this initial argument is. And then again at which point we've seen this. This is the chaining idea. This is at finish an anonymous function that has our state machine change that takes us to level one of the begin game state. So, we are including our state machine transition within this finish callback of the timer. tween operation. This new thing these building blocks that we've put together this whole time. And then we're removing a color timer here because it maintains a reference to things that are going to not exist in this you can remove timers from timer if you maintain a reference to them. If they're going to refer to values that won't exist at a certain point. This is good because you'll crash your game if you don't do that. Um it'll try to tween a value that gets deallocated effectively. But if we come down here you'll see that we have a rectangle here that we just draw. It's a fill rectangle the whole width of the and height of the screen. We initially set transition alpha right before that a rather we set the color using transition alpha such that the rectangle gets drawn with that color with that opacity and then we remember to set our marker our sort of global color tracker to full 1111. And then so the result of that is we transition to one over one second in opacity which gets us to be full white at which point we instantly transition to the begin game state. So, once we open that up this has its own transition alpha that's already set to one such that we can immediately on enter remember that all of these states expect an enter and an exit function. In this case enter

Segment 22 (105:00 - 110:00)

gets called right as soon as the state machine gets loaded into the state machine this the state object does. So, we are going to just immediately begin the transition from one to zero on this transition alpha. So, we're essentially just going to from zero to one cutting to a new state that has already pre-baked into it a transition alpha of one. So, it's a full rectangle that's white. At which point once this is finished we have a finish callback that's going to over 0. 25 seconds move this thing called level label Y down to halfway down the screen. That's the level literally the label the rectangle that's semi-transparent with the text in it. It's going to move it there. On finish we're going to say after one second let's tween. So, this is like the timer. after we talked about. We haven't really seen after too many times. We saw timer. every timer. after we just care about one second I want to then trigger another tween of that same label text down towards the bottom of the screen. At which point we can then call GStateMachine. change to play and then we're in the play state and then we can begin our game loop and do all that stuff and everything is very smoothly and cleanly transitioned to and from each other. And I think that is the majority of all the pieces that you will need in order to do the problem set for match three. It's a quite a lot of pieces. There's a lot of uh various new time based mechanics that we've talked about and I think it'd be kind of fun at the very end just to kind of look at a couple of things that are I think they're that are important for me at least when I'm thinking about games and aesthetics. It matter to especially in 2D, but I think it applies globally. But we used deliberately a sprite sheet that is a limited palette sprite sheet that uses I believe this exact same palette Don Breaker, which is a Don Bringer sorry Don Bringer 32 color palette. He has Don Bringer they have multiple different numbered colored palettes. But it's a very sort of famous in the at least in the retro community. And this one's really cool because it's very the colors are created in a line such that you get a very tremendous gamut with only 32 colors using dithering. So, this is essentially like pixel patterning the colors such that like if you mix any two colors and you off color them in a particular way you can get colors that look to the human eye as if they're actually between shades of color these sort of larger palette shades of color. And so here this is just you know, the 32 color by 32 color matrix in sequence and it's quite impressive. It looks like a full range of color. But um game art historically was palletized in older consoles. There were limitations that made it such that color value color size was only a certain size and you had to have a certain bit depth for every pixel on screen that was represented in different ways depending on the console. But um with just a very primitive seemingly palette on the surface you can achieve very nice aesthetic visuals and coherence to your design. It's kind of the one of the benefits of it too is that there's a mistake that you can make by choosing palettes that are overly rich sometimes especially in art like this where the just sheer number of choices leads to more opportunity for conflicts for overly harsh contrast on the eyes. And so if you have just a set of very well designed colors your risk of that is pretty minimal. And so it's common in games nowadays I think in retro community especially to try to use some of these ideas. And we used it today in all the match three art. It was all 32-bit or 32 uh just 32 colored uh 32 color palette. This is an example of if you were to use the same palette on a real image you can preserve almost all the detail. You do see some of the artifacting and some of the sort of weirdness of it. But like it's impressive what you can represent. And here's a better example. This is just a comic cover. This is a this is the base color and then if you apply Don Bringer to it the 32 this is what it looks like and it's very little actual loss in color, but you still get like pretty much everything exactly as you would want it. And it's cool and impressive and super fun. And I think it's important to take that into consideration if you're developing a game, try it as an exercise use only 16, 32, maybe even eight colors or be ambitious and use like two colors like Gameboy or four colors. Um it gets to be trickier that way, but it's interesting what comes about through limitations. This applies also, it's the same idea in palette swapping. So, in consoles like the NES or the Super Nintendo, you would have a palette that's loaded in memory that can be swapped out and then all of the sort of video work that's being done by in the NES, it was the PPU, I believe, um the picture processing unit pixel processing unit and it would be looking at this range of memory addresses for the colors that it would then draw onto the screen and every piece of art that's on the screen would be therefore easily changeable on the fly to be some different color. And this is something that's used in a lot

Segment 23 (110:00 - 114:00)

of retro games to good effect. Super Nintendo is a console that I loved and has a lot of instances of this. Um but it's just a cool technique and you know, it's related to the idea of palettes. It's essentially the same thing of having this set of 32, 16, eight colors that are at that time in hardware but nowadays in software sort of like limited for aesthetic or functional purposes. So, assignment three is going to be you know, so far what we've done is we've seen that by playing the game you sort of have a hard set 60 seconds when you're playing and it's pretty difficult, but it would be better if you could just add some time if you were to get matches, be actually incentivized and rewarded for your time or for your performance. So, it'll be add the ability to get time added to your timer rather refunded to you effectively such that you can keep playing and not run the risk of losing quite so easily. The blocks that we saw so far were all using variants, but the problem set is going to encourage folks to use variants that scale up with difficulty and actually mean something for point multiplication. So, rather than just having all of them be the same value, like take the flat block, have that be some value and then incrementally add these variants as the level increases, as your performance throughout the game goes up and then allow that to be a multiplier for how many points you get for that color. The sort of thing that I always think about when I think of Bejeweled when I think about Candy Crush is like these blocks or these candies or whatnot that have like just this explosive power or this this board wiping effect and this problem set's going to require that folks determine some block generate at random that has this shiny flag or that has some sort of variant on it that's visually different and allows you to destroy a whole row of blocks when that gets cleared out. And also, in addition to that create some kind of a particle effect such that it's visually also represented on all of the tiles that it affects. Then the right now um you can swap any two blocks that you want, but you should only be allowed to swap blocks when there's a match and you should also uh calculate such that if there is no possible one swap that will result in a match then you should restart the board or have the board reset. This is a thing that happens in Candy Crush and Bejeweled if you can't do a swap that actually will give you a match, it'll just completely wipe the board for you and give you a whole new board that does have tiles that you can swap in order to create a match. So, you want to do that. And then lastly, implement matching with the mouse. Right now, we don't have that ability. You just have to use the cursor, but you know, match three and Bejeweled at least on phones would typically be mobile games that you would use your cursor or your finger to actually create a match with. And so, allow the ability to do that and you'll need as a hint here in order for this to work well, virtual resolutions in the push library, you'll need this function push. to game of XY. So, next time, that's the problem set for this week, but next time, which is this certainly is one of my favorite lectures coming up, but a lot of the things that we talked about are going to be applicable particularly for the maps and idea of talking about tiles as IDs and having that be meaningful. But we're going to look at Mario, Super Mario Brothers and a an example here using artwork that is sort of uh open source public domain art of a variant thereof, but a very robust tile set. But we'll be looking at tile maps, which is very similar to what we looked at with today's grid, except tile map is sort of this way of representing a world in 2D space and then having all of having your world be essentially subdivided even in terms of like Minecraft does, you know, in blocks but in 2D, it was sort of like the original Minecraft in that sense, I suppose. Um but you know, what how does the sky differ from uh block on the ground versus a bush versus a pipe or something? These are all going to be represented as tiles, which we will deal with collision and other things related to those. 2D animation. Your character should move around on the screen, but they can't be like a stiff like just translating sprite. So, we're going to need to animate movement and jumping and various things like that. Platformer level generation, we'll look at an actual meaningful procedural generation sort of approach and look at some really cool level designs we can get for free just through code, which will be exciting. Um and then platformer physics, which is just essentially an extension on AABB collision detection, but it has some slight differences in the world of platformers. And then we'll also look at hurt boxes for how to actually interact with enemies and then lastly, power-ups and how those work and how we can use triggers to create them. So, that was all for match three. It was a bit of a sort of return to modernity from last week. We've sort of gone back and forth and then next week we'll be going back again to Mario, but you know, that era has so much nostalgia for me, Mario and Zelda and all these different games. So, I'm very excited for that. This is CS50 2D. This was match three. We'll see you next week.

Другие видео автора — CS50

Ctrl+V

Экстракт Знаний в Telegram

Экстракты и дистилляты из лучших YouTube-каналов — сразу после публикации.

Подписаться

Дайджест Экстрактов

Лучшие методички за неделю — каждый понедельник