字幕列表 影片播放 列印英文字幕 [MUSIC PLAYING] SPEAKER 1: Hello. Welcome to lecture three of GD50. Today, we're going be talking about Match 3, as shown by the little cubes here on the slide. Match 3 originated a little earlier than 2001, but the first big game that came out that was a sort of genre staple of Match 3 was Bejeweled, shown here on the screen. This is a more modern incarnation of Bejeweled, but it came out originally in 2001. It was actually a web browser game, and the formula is very simple. The premise is you have a grid of different colored or shaped items, usually pretty small, like eight by eight, or so. And your goal is just to simply, like the name says, match three or more of them in a row. If you do, you get a certain number of points. Matching usually more than three gives you more points, or a bonus. And whenever you match three, the blocks will disappear from the grid, and they'll be replaced by more blocks. And the ones that you made holes for, the blocks above them will come down via gravity. This is a more modern incarnation of the formula. This is Candy Crush, which I think most people know. It was a very big hit on mobile devices, and otherwise around 2013, 2012, and that's probably the most recent big Match 3 style game that's come out, but there are a lot of other takes on it-- different versions that try to add new features, and stuff. This is the game that we'll be putting together today, and I'll show you how, and we'll be covering a few other things as well. So the topics today, we'll be covering, first of all, a fundamental concept in dynamic languages, a lot of dynamic languages, and also Lua. It's called anonymous functions, which are functions that are first class, meaning that they operate as data types, and so we can do some fancy stuff with those. Tweening, which means just taking one thing, and interpolating its value between two values from 1, to a destination value over time, which is a very important thing in games. You can do things like move objects. We can also tween their opacity. Just sort of asynchronous behavior, and asynchronous variable manipulation. Timers, very important. We can time something to happen at certain intervals, or after a certain length of time has passed to get us past the idea of storing different timed variables, or different counters, and break away, and keep timer objects that will take care of this for us. We'll see how we do that with a specific library. And then we'll get to the actual details of Match 3, and how to solve matches, and how to account for that. Fill in the grid, account for when we actually solve a match, and repopulate it once we've done so. We'll talk about how to do this procedurally. It's very simple compared to, I think, Breakout's more procedural layout system, but it's still randomisation, and we'll talk about that. And then last, if we have time, we'll talk about sprite art, and palettes, which is a big fundamental thing when you're doing 2D game development, and something that this, and Breakout's sprite sheet took advantage of was the idea of using, on purpose, a restricted set of colors, a palette for creating your 2D art, and there are a lot of really cool, and impressive things we can do with that. But first, I'd like to actually show what we'll be running today in class. So I'm going to come here into my directory. Make sure I'm in the right place, which I am. So this is part of the distribution code, which is online. So there's a Match 3 directory. And would anybody like to come demo it in class? All right, [? Tony. ?] Come on up. All right, so whenever you're ready, go ahead, and just hit Return here. [GAME MUSIC PLAYING] All right, and so this is my implementation of Match 3. It uses a different set of tiles. We have things that are moving over time. It's arrow key based. So if you press Enter on any tile, you can flip it with another tile. It doesn't have to be a match, in this case. So you can-- yeah. Yeah, you kind of got an unlucky board. There, at the very bottom, I see there's a few that-- some brown ones you can match together. So once you match them together, the tiles come down to repopulate. You get new tiles up top. And so notice, we have a timer on the left as well. It's something that's counting down. We'll see how this is actually done with the library we'll be using, as opposed to managing a counter variable keeping track of it over time. A lot of games will actually implement it so you have to-- you can only, and this will be part of the assignment, actually, where you can only move a tile if it creates a match. In this case, there's a-- and we can see the timer counting down, and then once you-- yeah, if you don't get past the goal, there's a game over. But thanks, [? Tony. ?] I appreciate it. AUDIENCE: No problem. SPEAKER 1: So that's the game in a nutshell. And another thing I want to point out to is the transition, the white transitions, and then the level text. Those are all done with timers that we'll be using, and tweens, which we'll be covering in class here as some of our early examples. But there's a lot of stuff that we haven't touched on, but also a lot that we have. It uses sprites, and a sprite sheet, and we've done that thing before. We chop up a sprite sheet, and then take out the whatever individual quads you need, and draw them to the screen. Here's what our goal is, which is we have a title screen with Match 3, start, and quit game in this case. A little bit simpler. No high scores this time just because we've already covered that, but we also have a level screen. It tells us what level we're on before we can actually play, and there will be a transition box with text in it. It'll come down, stop, and then come down again. So almost like chain behavior, which we'll see how we implement that too. And then lastly, on the bottom there is our main game screen, where we have a level, a score, and then a goal. If you get the goal amount of points before the timer runs out, then you go to level 2, and level 3, and level 4, and the score increases by a multiplied factor each time. So the first thing I'd like to start talking about today is how we actually get timer behavior using something a little bit more than just keeping track of some variable that we set to 0, and then adding dt to it every update. There's a better way to do that, but first, why don't we go ahead, and look at timer0. And so what I'm going to do is go into the timer0 directory. I'm going run it, and we can see here, in the middle of the screen, just a very simple-- just a label that just says timer, and then x seconds, where obviously the x is incrementing over time every second. So a crude way, what would be an easy way to implement this? AUDIENCE: Do you like [INAUDIBLE] random in Flappy Bird [INAUDIBLE] randomizer [INAUDIBLE] if you just keep track of your delta prime and add it to some variable outside [INAUDIBLE] you do something else? You could even just display the variable [INAUDIBLE].. SPEAKER 1: Yes. So the response was keep some variable that you modify with dt in update, or display the variable. Yeah, that's definitely a way. Did you have-- AUDIENCE: Yeah, I was just going to say, keep a float variable, and constantly add a dt to it, and display it, but display the truncated version. SPEAKER 1: Yeah. So keep a float variable, but just truncate the delta time off of it. You could do that, definitely. We'll take a look here as to actually how I did do it. It's very similar to that. This is the wrong directory, though. So it's in timer0 in main. So we do have a variable. So the current second here, which we are going to keep track of 0, 1, 2. Lua doesn't really have the notion of truncate a float because when you take a number that's floating point, and you make it into a string, you actually have to do string substitution on it where you use a function called g sub to take off the last part manually because it doesn't really differentiate between ints and floats. It just has a number data type. But we can do this by just keeping track of whether or not we've passed a certain length of time because we know dt is given to us in seconds. We can just add to our variable, and then every time we've gone over 1, because it gives us-- it gives you usually like .013, whatever 1/60 or approximately 1/60 of a second is. Once our timer-- we're going to keep a timer variable-- equals 1, we'll just increment current second by 1, and then we'll set that timer back to 0, and then we'll just repeat over, and over again. We'll actually use modulus so in case we go slightly over 1 second, we can account for that. We do that here. So second timer gets second timer plus delta time, and then if it's greater than 1. So if a full second has elapsed, just increment current second, and then modulo a second timer by 1. And Lua is a little different in that most languages only let you modulo something if it's an integer, but since there is no differentiation, you can actually modulo floats, and you'll get the floating point value leftover. And so that's the basic way of actually doing that, but there's a couple of things wrong with it. So does anybody want to suggest what is potentially bad or unscalable about this kind of approach? Well, I'll show you timer1 so we can maybe get a sense of how this could kind of get out of hand pretty quickly. So let's say-- first, I'll run timer1. So let's go into timer1 here, and notice now we have five labels. They're running at different intervals. The first timer, it's incrementing every 1 second, the second timer is incrementing every 4 seconds, the third one is incrementing every 4 seconds, and then so on, 3, and then 2. So if we wanted to do the same approach that we just did, this is what we would do. We have five variables, five timers. Because we want to keep track of whether or not something's gone over more than just one second, it's not super easy to just put this all in a table, and iterate over it, and use your iteration logic to do that. We actually, because they're in some sort of random, who knows what order, timer2 takes 2 seconds, OK, timer3 takes 4 seconds, timer4 takes 3, and then 2, you have to unmanageably keep all of this in separate variables. [? Yes, Tony ?] AUDIENCE: Couldn't you just use one second timer, or even one variable, and then just in the display [INAUDIBLE] SPEAKER 1: You could. Yeah, in this case, you could. Again, Lua's display, it's a little bit funky when you-- you have to do g sub, and some weird string stuff, but yes, you could do that. AUDIENCE: Couldn't you just do modulo 1, and then take that value as a string? Is that the equivalent of truncating? SPEAKER 1: Modulo 1 would still give you the floating point value because there's only one number type. So if we modulo 1.00157 by 1, we'd get 0.00157. AUDIENCE: Oh. OK. If you subtracted from that value, from I don't know. So it's a value minus the value of modulo 1, I guess. SPEAKER 1: So yeah. It was proposed that we've used modulo, and we could. In short, we could, but what if we're not just printing a value to the screen. What if we have 10 different things, like 10 different creatures that are all doing different things over time, and we don't necessarily want to have to keep a timer for each, and every one of those things. In a simple example like this, yeah, there's probably a-- on purpose, it's also a little bit convoluted just to illustrate the problem. But yes, there are shortcuts for this, but the fundamental problem is how can we get rid of having five different timers for something? And by the way, I'll go to the next slide. Timer0, the simple way. Timer1, the ugly way. Timer2 is the clean way that I found using this ecosystem. There's a wonderful library, and you could implement this yourself. The fundamental idea is have a global timer object that then manages all of these different things going on using the power of what I alluded to earlier, anonymous functions, and I'll show you how that works. So in main.lua of timer2, we have a set of intervals. We have a set of counters. And then what we're doing here is we're just saying for i gets 1 to 5, we're calling a function call timer.every. So if you're familiar with JavaScript programming, there's a set interval function which lets you do something every length of time. So first of all, timer is just a library. We've just required it here. It's part of the knife ecosystem, and then here, we have a couple of functions, timer.every, and timer.after that we'll use. Well, basically, what it does is you give it a length of time-- timer.every seconds. It's in seconds, and you can give it fractional seconds. You're passing in just a function here, just an anonymous function. It doesn't have a name, but because Lua, and a lot of dynamic languages treat functions as first class citizens, as it called, because they are data types, you can just pass them into functions. This allows us to do behavior like this that would otherwise be a little bit tricky to do. We can just say after this block of time, assuming we've built some structure that is probably just storing a table with a bunch of things that have a length of time in them, just call this block of code later. It's called a callback function. We're just going to call it back, and then we're just going to do this. We're going to say counters i gets counters i plus 1. So we have all of these intervals, and all of these counters. So it will just, basically, manage that for us, and now we don't have five variables. You do have to set whatever you want those to be. That's your that's your primitive at this point. You just need the lengths of time, depending on your problem. In this case, that's all we need. You might need more than that, depending on what you want to do with timer. But in this case, we just want to increment in value over time. So keep counters, and then just keep track of the intervals, and then our code's gone from I don't know how many lines. It was a lot larger-- I think was 96 lines, down to 98 lines, down to 70, and this is incredibly scalable. If we wanted to add another one that's 8 for example, we just need to add 8, and then that. I guess I'd have to do this as well. I gets 5 to 6, and then you want to-- the computer that requires me to off my user so I can make changes. We'll just real quickly see if I didn't mess up. And so yeah, basically we're deferring everything to timer now. We get the exact same behavior, but a much smaller length of code. And the nice thing is it's very declarative. We can just say OK, every something seconds, I want this chunk of behavior to happen. I don't have to see OK, I've got timers up here, I've got counters here. OK, down in my draw function, OK, I've got to draw all these. It's iterative, and it's declarative, and that's the ultimate goal. And here, at the very bottom, it did actually work. Now it's working every eight seconds, which is nice. One, two, there we go. Super easy to extend. We're going to be using it a lot in this problem set, and also in future lectures just because it's a lot easier than keeping track of a bunch of counter variables. And there's another function that we're seeing here-- timer.after because sometimes you just want to wait a certain length of time. Maybe you have every 1 second for 5 seconds you want a bomb to tick, and then after 5 seconds, you want it to blow up, and you could also model that with another function that we'll see soon, but these are probably the two core time-based functions. And you can go here to this URL able to see the knife library. There's a bunch of modules that are really nice. We just happen to be using timer, and it's tween, and every after functions primarily in this problem. But we'll use another one called the event in the Zelda [? p-set, ?] where we actually look at how to dispatch events, and triggers, and stuff, and to prevent us from checking every frame. Oh, what do we have to do? If some wall is broken, this frame, then do this. We can just dispatch an event that we blow up a wall. We'll get to that. Any questions at all about how those two models differ, and how they work? OK, cool. So the next thing I want to look at-- so we can tie that back into also, real quick, if we want. Match 3-- whoops, and then there's a timer that's manipulating the text on the screen. All of those letters in the-- this is in the start state of the game code. All of those letters have a color associated with them, but they're on a timer so that after every 0.075 seconds, they'll go to another color, and to another color. And so we don't have to keep track of every letter's individual color, and a timer for it. We can just change them all. If we start the game, something else that's on a timer, the timer, actually, there, which is just decrementing some value. Every one second, decrement timer by 1, and that's it, and we don't have to keep any-- we don't have to say anything more than just that, and that's what's really nice about using that kind of model. So another thing that you probably also noticed, and I'll run it again, is this fade out, and fade in, and also that animation. Those are things that are happening over time. We don't actually-- we can just sort of manipulate them. We can keep track of some sort of counter for it. We can also just say over this length of time, change this value to this value, and that's a much easier way to model the problem mentally. And so we'll illustrate that. First, I'm going to go to tween0. So tween0 is the simple way to do something. So I'm going to illustrate tweening here with Flappy Bird just going left to right. So up there, that's what happens when you print out the number, by the way, just by default. And so if you wanted to truncate it, yeah, you could just g sub the first two, I guess, depending on how large it is, and that would have the effect of displaying it as just an integer. But we can see that over two seconds, we've had-- and there's a little bit of overlap just because delta time can go a little bit over two seconds when you're adding to it because it just adds whatever length of time as a float has elapsed since the last frame. In this case, we're just adding it until it's greater than or equal to 2. So in this case, we went 0.01 over 2 by the time that actually ended. Some iterations will be less than that. So this one will be-- yeah, see, that one was less than 2.01. It just depends on your computer, and your specs. But Flappy Bird starts on the very left. So he's got an x-coordinate, and then at the very end, he's got another x-coordinate. So the simple solution is what? We know that we want this to elapse over two seconds. So what we can do is I'm going to pull up tween0. And MOVE_DURATION here, it's a constant of 2 seconds just for the sake of this example. A sprite here, just a simple image. I'm putting everything in one code file this time, as opposed to breaking it out into subclasses just for simplicity because these are such small examples. But we're setting it's x and y. Oh, and this is another Lua trick, by the way. You can assign two variables to two values using a comma here. So flappyX comma flappyY gets 0, and then VIRTUAL_HEIGHT divided by 2 minus 8. Setting this x to 0, we have a timer here, and then it's end x. So we want it to end at the end of the screen. So we're going to say virtual width minus his width, an then the usual boilerplate for getting a project set to go. If it's less than the move duration-- so if timer is 0 going up to 2, but it's not quite 2 yet, we're going to add dt to it, and then we're going to, basically, assign it to either the lowest of end x, so it will never go higher than end x, or end x times the ratio of timer over move duration. So timer over move duration, if it's less than 2, that's going to be some value, some fractional amount less than 1. So it's going to basically just scale it, depending on how far we've moved the timer between 0 and 2. So just a scaling operation. This happens to only work in the context of moving something from left to right, or from 0 to something else, but it's a crude, basic way of illustrating a very basic tween operation. That's what it essentially is. It's a multiplier of some ratio of how much time has passed, versus how much time we're actually looking to elapse. And that has the effect, once again, of just-- it's scaling the ratio because it's timer over moved duration. It's something over 2, but it's not quite 2 over 2. Until it gets 2 over 2, and it's 1, then end x times 1 is going to be end x. But before that, it's going to be some fraction of end x between 0 and end x. So it has the effect of giving us a very basic tween, but it's a little bit-- we have a little bit to manage here. It doesn't really feel super clean. Do you guys have any questions about how this works? OK. So we're going to go here. So first of all, any thoughts about how that might not be super scalable, looking back at the last example? AUDIENCE: I guess like the situation before, getting a lot of objects moving on the screen. SPEAKER 1: Yeah, and what if your index is different for every single one? Then you have kind of a mess. What if we had something like this? You don't want to keep track of an end to x. They all happened to have the same end x, but notice they're moving at different rates. They're all moving at some sort of random amount. AUDIENCE: How many are there? SPEAKER 1: There's 1,000. We could go crazier if we wanted. This is a fun thing to do, is stress testing. So if I go to tween1, and I go to main, timer max is 10. So we're saying that the longest possible time any bird can take to get from left to right is going be 10 seconds. So right here, we're using a table based approach here. We're actually keeping track of 1,000 birds, and we're saying, OK, here's an empty table from 1 to 1,000. Add a new bird, and in this case, we're not adding a bird object, or anything. It's just a table. They all start x equals 0, left side. Their y is random. So they can be anywhere between the top and the bottom of the screen. So VIRTUAL_HEIGHT minus 24. 24 happens to be the height of the sprite, and I should have probably put Flappy sprite get height right there. And then rate, they're all going to have their own rate. So rate gets math.random. Math.random without a value passed into it gives you a fractional value between 0 and 0.999999. What this has the effect of doing is math.random with just two values, if you pass in 10 and 50, it's going to give you 10 to 50, but they're always going to be integer values. You can't say like 10.0 and 50.0, and assume that it will know what you're talking about. It's just going to be integers. So if you give it one value, it will know. OK, you're asking me for a float between 0 and 0.999999. That's going to act as the fractional part of whatever value we might want to generate using a math.random with a value passed in. So here, we're saying OK, math.random, TIMER_MAX minus 1. So TIMER_MAX is 9. So our TIMER_MAX is 10, sorry. So if we subtract 1 from that, this is math.random 10. So we're going to get a value between 1 and 9. So TIMER_MAX minus 1 is 9. Sorry if I said 10. TIMER_MAX is 10, TIMER_MAX minus 1 is 9, but we're adding math.random, some fractional amount. So whatever value we choose between 1-- actually between-- yeah, between 1 and 9, it's going to be that value, point something. So this is how you get random-- basically, at the end of the day, this is how you get random floating point numbers in Lua and Love2D. Does that make sense? Do you guys have questions about that? AUDIENCE: So your final rate could be anywhere from 0 to-- SPEAKER 1: Final rate is going to be anywhere from 1-- in this case from, 1 to 9.999999. If we wanted it to be 0, we could do that, and that'll give us the effect of taking whatever we get, and subtracting 1. So now it will be between 0 and 8.999999. And if we did this-- AUDIENCE: Couldn't each math.random end up at 0? SPEAKER 1: No, because math.random, if you pass in a value, it'll always do from 1 to some value. The question was will math.random give you 0 if you pass in a value? And math.random, by default, gives you between 1 and something else. And so that's why we do this. That's not why we do this. We do this to add the fractional part so that we can get fractional floats between some value. But yeah, if you wanted it to be between 0, and something else, you would just subtract 1 from the final result because we know that we're always going to get from 1 to some value. If you minus from 1, you will never go below 0. It will always be 0 to something else, and then in that case, we probably would just take off the minus 1 from TIMER_MAX so that it will be between 0 and 9.999999 in this case. But as a design decision, I made it so that we would always have at least a rate of 1 because then it could get really, really slow if it's like 0.005 or something like that. You wouldn't want that. It would take ages. OK, so we have a timer. We're not using knife.timer in this case yet. Basically, in update, we're just saying as long as timer max is less than-- timer is less than timer max. This update logic will only run as long as we haven't gone over timer max. Increment it, and then for each bird, basically, we're doing the same thing that we did before, except we're using that bird's rate as the scale factor. Yeah, the denominator of that ratio, and that will have the effect of multiplying all of those birds individually, based on their own rate, rather than some global rate, if that makes sense. And then here, we're just drawing all of them at their own x and y. And just again, we're just storing birds are just a table with a few variables in this case. Just a very simple shell, and it has the effect of-- oh, and one thing I wanted to do is just add a 0 there. I've got to do this every time because I didn't set my permissions appropriately. But now there's going to be 10,000 birds. So let's see how this looks. So it looks pretty similar, actually, but it's a little more condensed now. I don't notice a frame rate drop. If we wanted, we could go down to love.draw, and love.graphics.printf at FPS, and then to string, love.timer.getFPS, and then let's set it to 4, and then VIRTUAL_HEIGHT minus 16, and then got to do this again. This should have the effect of giving us our frame rate. So we can really stress test this, and see what the-- oh. I made a mistake. OK, let's see. What did I do? Push.start. I'm guessing I missed a-- yeah, I missed a-- do that, and then that, and then save it again. Sorry about that. There won't be too many of these edits, but I figured this would be fun to illustrate. So now, this should work. Printf, so we have that. Oh, right, and then it just needs to be print. It doesn't need to be printf. Got to save again. Sorry. This will be worth it, I hope. There we go, 5160. OK, it takes a couple of seconds. It has to interpolate between the last couple of times that it's pulled for frames, and when you start up, it doesn't have the data it needs. So 10,000, easy. Let's do like a million. So we have 10,000, 100,000, a million. I really got to change those permissions. Getting good at practicing my password, though. Going to go ahead, and do that. Ooh. Oh, my laptop is suffering. Oh man, but they're all moving independent of frame rate. This is just a testament to how powerful delta time is. They all move there after 10 seconds. We've got to 10.6 seconds, which is not good. That means that's how long passed between the last frame, but my laptop clearly can not handle a million birds on the screen, but it can handle 10,000, or maybe even 100,000. And that's just a fun way-- a fun thing to do. In general, when you want to stress test your game, just put the frames per second on, and just go nuts. Just add a lot of stuff. Just see what your computer is capable of because you can find new, fun things that way, I guess, and maybe just see how good your code is too. So tween0, simple way. We have just variables, and one counter. No tables or anything. Tween2 is a good way for if we have a lot of things that we want to manipulate over time. But what if now we want some of them to change their opacity over time or something? It starts to get a little bit more complicated. And this is, by the way, I should've put this earlier in the code, but this is the knife library is responsible for timer, and a lot of other things that we'll be looking at, and it's got a bunch of modules here listed-- behavior for state machines, which is like what we've been doing for state machines, but they have their own version of it. Knife.bind, so you can pre-bind arguments to functions, and create subfunction. It's called currying, but create subfunctions of other functions that have pre-determined variables. Knife.chain, we'll see, actually, how that can be used coming up later. Convoke is for coroutines. We'll see coroutines in the context of Unity, but basically, they're functions that can pause their state for later. Knife.event we'll use in two weeks for Zelda, maybe even next week if I can fit it in for Mario. Memoize is for memoization. It's like a dynamic programming related thing. Serialized system. System is going to be useful to know about in context of Unity as well. Unity uses an entity component system for much of its structure. Knife.test, and then lastly, knife.timer, which is what we'll end up using, and this is probably my favorite library that exists in the Love2D ecosystem. And so with that said, we'll look at tween2 now. I'm going to go here into tween2, and so now we have not just movement, but also their opacity. They all start at different opacity levels, and we want to not only change their movement over time, but also the opacity. So it would get a little bit trickier if we decided to do that with our current situation. Totally doable, but how would we go about changing, just right now, their opacity just as is? How do you change a spite's opacity? AUDIENCE: The variable and the graphics [INAUDIBLE] SPEAKER 1: Is it a variable-- can you say that one more time? AUDIENCE: I forget the exact function name, but it's like love.graphics to put an image on the screen. SPEAKER 1: The love.graphics.draw. AUDIENCE: Wasn't there an argument in that? SPEAKER 1: So it's actually not an argument to that function. So I'll show you now. So in order to draw something at some different opacity, it's actually love.graphics.setcolor, and we do that here. So recall that Love2D is a state machine, and how it draws things. You can basically set a color onto anything that you draw, whether it's a font, an image, or a shape. And if you just pass in 255, 255, 255, that's white. And then if you give it an opacity, which is the fourth parameter, which is the alpha component of that, then that's how transparent it will be. And so we could have done this with other colors too. We could have done this with like if we wanted to tint it red, and also have it be sort of transparent. We could do that if we just did 25500 bird.opacity. But if you just want to manipulate opacity independent of-- or its transparency or its alpha independent of its color, and keep it the same exact color, you just do it white. If you did it black, nothing would show up. Is that true, actually? Let me verify that. Pretty sure that's right, but I could be wrong. I'm right, thankfully. OK. That or they're just black, and there's a black background too. Do you have a question, [? Tony? ?] AUDIENCE: No. SPEAKER 1: Oh, OK. So we'll take it on faith that that is correct. And back to the gist of this example. We have TIMER_MAX again. Actually, we really haven't changed much. What we have changed is we still have our birds. We need to keep track of their x, of their y, of their rate. Well, not necessarily their rate. Oh, well, their rate, yeah, because we're actually going to loop over each of these, and then create a timer between operations for them. And their opacity. Oh right, they start with an opacity of 0, and faded to 55, regardless of their-- their opacity changes at the same rate as their x does. So the farther away, the longer they take, the slower they fade to fully opaque, and we see this here. So for k bird and pairs of birds. So for every bird, we're just going to set a tween, and then this is tween. So timer.tween is I think I have a slide on it here, right? A super cool useful function, super easy to use too, it takes iteration just like timer.every, timer.after, and it takes a definition. So in this case, it doesn't take an anonymous function like the other ones did because we're not really saying I want to do some sort of undefined behavior over the course of this operation. What I want to do is just change some values. I want to interpolate them. So what we're going to do is just pass in a-- this is the syntax for it. We pass in square brackets, the actual thing that we want to change. In this case, I want to change bird. I want bird to change in some way, and then what I want it's values to change toward are these. I want it's x to change to end x, and I want it's opacity to go to 255. And I wanted to do it over bird.rate, and so this bird.rate, every bird is storing it. So for birds that got a rate of 2, then it's x is going to go to end x over the course of 2 seconds, and it's opacity is going to go to 255 over the course of 2 seconds. And you can put as many things as you want, and as many-- you can put as many variables here, and as many entities. Entities being anything that you want to change that has a field, any table-based or class-based structure. You can pass any of those in here, and just tween them all at the same-- if they all have the same rate, and then just get that operation that way. And so that has the effect here of all we need to do is just add-- it's like two lines of code, but now we've easily changed it so that we can just tween two things at once, and that's the power of timer.tween, and we'll see that. So back to, actually, Match 3, if we want to look at that again. This is a tween, that's a tween, that's a tween, and that's a tween. So the white, the foreground there, is just a rectangle that fills the whole screen. It's just a white rectangle. I have it set to timer.tween opacity from 0 to 255. Before that gets called, if we go from the start to the begin game state, and then if we go from at the beginning of the game state, before the level text comes down, it's going from 255 to 0. So it's just the reverse of that is the tween, and then all it's doing is just drawing a rectangle to the screen. But that's how you get a very simple transition. Same thing for fade to black. If you want to fade the whole screen to black, just draw a rectangle the size of the screen, and then just tween its opacity from 0 to 255, and then vise versa. That's how you get a simple transition. It can be any color you want. It can be a red transition. And then the level text, that's just a tween on the y, right? And then I just have some rectangle, love.graphics.rectangle with text, and it just says timer.tween to like VIRTUAL_HEIGHT divided by 2 minus 8, and then timer.after1. So we can actually pull this up if we want. We can see how this works. Today's going to be a little wider on the main distro code just because a lot of this is more conceptual. But in the begin game state-- well, actually, in the start state is when we go. So these colors, and letter table, and stuff that's all for the Match 3 text, if you're curious. So these are all back to what I said earlier about the beginning screen having Match 3 with the different colors going on a timer. These are just tables of colors. So notice this is RGBA, and then I'm just performing a shuffle on them every 0.075 seconds. So 2 will get 1, and vise versa. It'll all go down, and then 6 will come up here to 1 every 0.75 seconds. And then M gets mapped to this one, A gets mapped to this one, T to this one, C, H, 3, and that's it. That's done here at line 44 of the start state. But what I was going to show you was the tween for the transitions. So here in start state, in the update function says, it says if we press Enter, and our current menu item is 1, meaning that we're on start game, not quit game, timer.tween here. And notice that we have a finish function, which will show-- I'm actually going to show you in the next couple of examples, the chain examples. But finish is just a function that you can run after any timer that just says, hey, when this is finished, run this block of code, and notice that takes anonymous function here, just like that. So we can say OK, tween, over the course of one second, notice we're passing self into here because we want to manipulate ourself. We have a value that we want to manipulate. So self.transitionAlpha. So we're saying I want to take my transition alpha, and I want it to go to 255, and we set it to 0 by default. So at the very top, here, at line 60, transition alpha is just our white rectangle that fills the screen. I'm just saying set it to 0 so we don't see it at all. It's going to be invisible. It's still there no matter what. It's hidden, but after we press Enter, tween it to 255 over the course of 1 second, and then when that's finished, notice this is familiar, right? gStateMachine change begin game. We're going to go with the begin game state after that. Our passing level gets 1. We're starting the game, and then we're going to remove this color. Remove that timer from the-- this is actually unnecessary in this circumstance, but you can remove timers from timer. If you have something going constantly-- in this case, the color timer, and let's say we move from this state to the next state, the next state doesn't have all those colors, right, the Match 3 colors. So we don't need to keep-- because timer is a global object, it's going to keep updating over, and over again. We don't need certain timers to exist indefinitely. We can just remove this one because it's not relevant anymore. But this is all it takes just to give us a simple transition from one screen to another. Just give this transition alpha 255 down in the actual render function. Where is it? It is right here. So right here. Draw our transition rect. It's going to be drawn last so that it draws over everything when we finally do get a transition, but self.transitionAlpha, and that's all we really need. We need to keep the variable, and then whenever we want to perform like some sort of operation over time, just use timer.tween. It's that easy. But that was a little bit of a-- it was a relevant tangent. We would have talked about it anyway, but that's the first use case that I think of in this project, and then also the label. I'll show you the label in a little bit. But I think before we do that, let's talk about chaining. So you guys have probably played a lot of games where maybe there's a cut scene, and you're looking at a character, and they walk, and then maybe they turn, and they walk in another direction, and they walk up, and then they speak to you. There's a dialog box, and then maybe they do an animation or something, and then maybe some other things happen that are on some sort of predestined path. It's a very discrete path. It's not random. It's laid out in advance. It's a series of steps, one consecutive. That's the concept of chaining things together is relevant when we get to sort of timing things because when we finish timing something-- because usually, a lot of those things happen over the course of time. Over the course of five seconds, NPC1 will walk up north, and then they'll turn left, and then they'll say something. We want to model that. We don't want to basically have variables that say if NPC1 is at this tile, then do this. If NPC.dialogueOpen, then do this. We basically want to say walk here, do this, do this, do this in a flat, easy-- or at least semi-flat, easy sequence of steps. I have a few examples to illustrate how we can do that using timer for some semi-basic use cases. So chain0 is the first one. So this one is just Flappy Bird. He's going left to right, then he goes down, then he goes back left again, and then he goes up. What's the basic way that we model this-- that we implement this? Just off the cuff. AUDIENCE: We can use that finish thing to do the-- SPEAKER 1: We would. If we didn't know about finish, how would we probably do it? I shouldn't have given away finish before I got to that example. I kind of got ahead of myself. We can imagine somebody maybe saying OK, I want Flappy to move left to right, right to bottom, bottom to left, bottom to up. Maybe they're going to say if Flappy is less than-- or has reached first point, move left, else if he's reached bottom or point 2, move down, and then move left, move up. And in both of those cases, they're changing the x and y value of Flappy, and it's basically just a lot of ifs, and state variables. I see it in a surprising amount of code. Just state being kept all over the place. The first implementation of that that we'll look at uses something similar. So in chain0, and there's only two examples here, actually, for chain. But chain0, there's a movement time, and then a timer. We're going to be semi-clean about it. We have some destinations. OK, so we have destination1. I know that I don't necessarily want to keep track of a bunch of if statements, but I'm going for-- assuming that I don't know what timer can do for us, here, I'm just saying OK, I want this first destination to be virtual width minus his width, and then keep him at y0 So right edge of the screen, assuming that he starts at 0,0. And then I want his second destination to be that same side on the x-axis, but I want the y to be virtual height minus his height. So go to the bottom of the screen. Then I want it to be 0 in his height from the bottom of the screen, and then back to 0,0. So we have those modeled, and then I want to keep a flag in each of those. I want to know whether he's reached that state yet. So I'm going to iterate over that table I just created, and just add a new key to each of these called reached, and just set it to false. Just by default, he hasn't reached all of them yet. And then in the update, basically, I'm going to set a timer to the min of movement time. So it will never go higher than movement time, and then timer plus delta time. And then for every destination in destinations, if it wasn't reached, then set its x and y, FlappyX and FlappyY, which are, in this case, we're uncleanly using global variables to keep track of this. FlappyX and FlappyY gets baseX. So notice another problem. We have to maintain where we are relative to our next spot in order for this math to work because before, we just took Flappy Birds-- basically, the timer divided by movement time was a ratio where we scaled the end destination, and assigned that to Flappy, which had the effect of moving Flappy left to right. But if we do that in the opposite, right to left, the math isn't the same because he's going backwards. He's getting negative values added to his x value. So we need to keep track of a base that he started at for each of these operations baseX, baseY. So at the very beginning, baseX, baseY is 0,0. So it's actually going to be much the same, but as soon as Flappy get to the right edge of the screen, we want baseX to be the right edge of the screen, baseY still 0, and then if he goes down, we want baseY to then be bottom edge of the screen, baseX to be right edge, and so forth. So what we do is we just scale. We're still using a timer over movement time as our scale factor, but we're adding the difference of our destination and our base, and we're multiplying by that scale factor instead. And so this difference, if we add it, whether we're moving left, or right, or down, or up, it's going to have the effect of filling in that gap of bridging that no matter where we are, no matter which direction we want to go. And so this is basically a fairly complete linear interpolation algorithm, which is the basis of tweening. Just interpolate some value between another value. It's usually modeled in geometry as the line between two segments. And then if timer gets movement time, we've reached our destination, reset the timer, reset or baseX and Y, and that has the effect of just doing what we saw earlier, which was just putting him point by point. So any questions as to how this interpolation-- how this way of modeling the problem works? All right, so there is a better way, a much better way thanks to timer.finish, which you can apply to any timer operation, including timer.tween. So we can basically say OK, once that operation is finished, do something. And this is all we have to do, we just have to say timer.tween. We no longer have to interpolate at all. That's taken care of for us by timer. So we're doing timer.tween over movement time. Flappy, set it to-- this was before we add all this in a destinations table with reached flags as well. Now, we just have the x and y here. So on the first movement, we want his x to be right edge of the screen, just like before-- y get zero. Once that's finished, anonymous function with another timer.tween. So we're saying OK, once you're finished, then tween him from the top right edge to the bottom right edge. So y gets VIRTUAL_HEIGHT minus FlappySprite getHeight. And then once that's finished, another anonymous function, another timer.tween, another finish, another anonymous function, another timer.tween. And this is, in its own way, unscalable. It's nested. There's a term for it called call back hell because you just get infinite downwards sloping anonymous functions with all this behavior. There are ways to flatten it, and we potentially will talk about it. It's part of knife.chain. Knife.chain has a way to turn all of these-- basically, it would look something like this. It would be chain, and then it would be like moveFlappy x, y. MoveFlappy x2-- it wouldn't be x2, y2. We'd actually write these out here, but it would have the exact same effect. This is if you're looking to maybe implement a cut scene system, or just some sort of scripting system for your game that's very declarative, and imperative in style. This is the holy grail of changing behavior, and getting it to work, and just making it look nice, and readable. Yeah? AUDIENCE: Well, you could also pass in-- you could pass in a table to your function of x's and y's [INAUDIBLE] simpler for [INAUDIBLE] to chain a lot of things. SPEAKER 1: Yes, you could do that too. The response was you could pass in a table to-- you could iterate over a table, and within that table, generate a timer.tween. The only issue comes about with finish, and there is a way-- I guess you could get a reference back to the timer, and then add a finish block to it, but then you would lose out on-- that does work for the same function if all you're doing is moving something to a bunch of locations, it's absolutely true. But if we wanted Flappy say something, and hero disappear, and then hero flash, it gets a little bit trickier to do stuff like that. But yes, I agree. There are ways of modeling-- this particular example is a little bit repetitive, and could be modeled, I think, better with a function that takes advantage of the fact that timers can be returned, and then given new finish variables. I'd have to experiment with it to see because I'm actually not 100% sure that you can add a finish. No, I think you can, actually. I think you can add a finish function to a reference because it's just a function on an object. So yeah, but independent of that, I think the goal probably is one, knowing how we can now chain behavior, and then two, striving towards flattening it. But in the purpose of this problem set, we'll see this a couple of times, and it's just worlds better than before. What's this, 76 lines? And then tween1 or chain0 was 96. OK, so 20 lines of code. And also the fact that now we have a declarative interface for modeling asynchronous behavior, that's really the fundamental thing, is not having some value that models your duration, or your counter, or whatever value, but just saying, hey, over this length of time, I want you to do this. I want you to do this. I want to do this. I want you to do this. After this length of time, I want you to do this. I always like to try and strive towards making code as declarative as possible just so that you can read it in the future, and then the people working on your code base can also read it well in the future, and I think timer does a pretty awesome job of that. And this is just a reference for timer finish. So it just takes a callback, and then once a timer function tween every or after has completed, it triggers that callback. So we're going to take a break for five minutes now, and then once we get back, we'll talk about swapping, and some of the algorithms we use in Match 3, starting with just rendering a board, getting tiles to swap, tweening them, and then we'll actually look at how we take falling tiles, and account for them, and then repopulate them. All right, we're back. So recall, before break we were looking at timer, and how to take code that was previously managed by timers, and asynchronous, but also very stateful, and sort of messy, and all over the place, and putting it into a more declarative, clean, easy to express format via timer.tween, timer.every, timer.after, timer.finish, timer;finish for any timer objects. So with all that out of the way, now we'll start talking about the actual Match 3 mechanics. And the very first thing that we'll look at is swap0, and this is the sprite sheet for Match 3 that's included in the distro. So as we can see, it's something that we can easily chop up with generate quads, as we saw before. Just a function provided in util.lua. These are all 32 by 32 pixels. So it would be very easy just to go through all of them, and basically just generate quads with the sprite sheet 32, 32, and just get a table with all of these individual things. But notice that they're blocked up into patterns of colors, and this has actual meaning, and value for our game because when something is the same color, and only when something is the same color, a tile is the same color, are we allowed to trigger a match with it. If we get any three or four-- anything higher than three together in a line, vertically or horizontally, that's a match, and we need to remove it from the table. So we need some sort of way of identifying these tiles as being of some color, and then they also happen to have a different pattern on them. This one's got nothing, but then an x, and then a circle, and a square. So those patterns-- it's not implemented in the distro, but part of the assignment. It's actually going to make them relevant. But the part that is implemented is the actual matching of the colors. And so the first thing that we'll need to do, probably, when we actually get into the core code of the distribution is instead of just putting them into one table, categorizing them. Splitting it up into maybe one, two, three, four, five, six, seven, eight, nine times two-- 18 tables, so that we can just say gframes at color. So color being one to 18, and then get the index into that. So there's six within each one. So it'll be one to six. One to six will be the variety, with one being the base variety. And part of the assignment will be make sure that the base variety is the only one that we start with on level one, but then gradually introduce these other varieties. And you can put them in whatever hierarchy you want to, but make them have some sort of value later on top of a few other things the assignment will cover. But that's the spreadsheet in a nutshell, and we'll be splitting it that way. So 18 tables of quads, instead of one quad of whatever this amount is-- eight by 16. Not sure how many that is off the top of my head. But let's take a look at swap0 in the distribution so we can get a sense of what we need to do to begin implementing our Match 3 game. Notice we have require util for GenerateQuads. We're just going to generate for this basic example. We're not going to differentiate between colors, and varieties, or anything. We're just going to put them all into one quad, or one table of quads. So we're just going to use the regular GenerateQuads function. We're not going to differentiate them. We're just going to use our sprite here, our match3.png provided in the distro, which is the exact same image that we just saw on the screen. I'm going to just generate them. They're 32 by 32-- generate the quads for them. They're 32 by 32 pixels. I'm assigning it to a table here called TileQuads. And then here, we're calling generateBoard, and so generateBoard isn't all that dissimilar to what we saw before with maybe the level maker in Breakout, where we just spawn a bunch of bricks, or a bunch of tiles, in this case. Except in this case, they're kept in a nice 2D array that's always going to be eight by eight, and that's never going to change just by one of the constraints of the game. Match 3, traditionally, is an eight by eight grid that's always full of tiles of some variety. So local tiles, it's going to be an empty table, and then we're going to do a nested for loop here-- y to x. The standard is usually y first, and then x, and then we index y before x just because the individual rows in a 2D array, or sprite, or table will be such that, for example, if our table was equal to this-- oops, sorry. Syntax bug. If we did this, and then we have a table here, and a table here, and a table here, and we had like 0, 1, 1, 1, 1, 1, 1, 1, and then 1, 1, 1, 1. If we index into this table-- so table at 0, that's going to give us another table. That's not going to give us-- let's say we wanted to get the value at table 2, 3. The instinct might be to think I'm going to index at x, y just because x, y tends to be more commonly seen. But if we did that, and assumed that x was horizontal, and y is vertical in this arrangement, which is how we have it, then table2 wouldn't be this value here table2 would be this table here. The first value you passed into indexing some table when it's a nested table is just, in fact, the table itself, which is why we actually need to do-- when we want to get some value, we have to index at table y, x. So it's flipped for that reason because x is actually going to be-- the first index is going to be these sub tables, or sub arrays if you're in C, or Java, or something like that. So when you see table y, x, and you're wondering why it's not table x, y, that's the reason. So any questions as to why that is, or how that works? AUDIENCE: [INAUDIBLE] SPEAKER 1: In this case, I was using zero based indexing, but Lua is one index. That was just habit. It was pointed out that I was using zero based indexing in my example. You want to use one based indexing when you're actually programming, and not zero based. But the same principle applies. Zero would be one in that case. In a general purpose language-- most languages, if we were to abstract the problem out in a C 2D array, or C++, or Java, zero would be appropriate there. But anyways, we have a nested for loop. We're starting at y, and then we're going to x, and then basically, that has the effect of y, and then x, x, x, x, x, y, x, x, x, x, x. Just insert a blank table. Fill it with-- we're just using tables here. So we're not using any sort of tile class, or board class, or anything fancy. We're just using raw data types here. So we're just saying insert into tiles y, which by the way, if we're at x equals 1, 8, here, and we're in any given iteration of our outer y loop, tiles y will be the last table that we just inserted-- the last blank table on the first iteration of this x loop. So basically, it's saying, in the inner table that I just put into the table that's going to represent our board, the tiles table, insert a new table. So we have a table of tables of tables. In the third table are the actual tiles themselves. You can think of this table as being a tile data type, more or less. Just implemented using a table. That has an x-coordinate, a y-coordinate, and then a tile, and the tile is going to be a random number that's going to be the index into our quads. So each tile holds x and y, and then notice that we're multiplying by 32 because we're going to use this to draw the tile, and the tiles are 32 pixels tall by high-- sorry, wide by tall. We are going to multiply x minus 1, recall, because even though tables are one indexed, coordinates are zero indexed. So x gets that times 32, y gets that times 32, and then get a random number between 1, and the number of quads in our tile quads table. So recall this number sign here is just a shorthand for length of the table, and then we're going to return it. So we generated our board. It's a y by x grid of table rows of little tables that all have an x, y, and a tile ID, and the tile ID maps to the quads that we just generated. OK, that's it for the init function. Sorry, love.load in this example. The love.draw uses a function called drawBoard, and we pass in 128 by 16. The 128 by 16 is just xy offsets. We're just going to draw our board at 128, 16, and is it going to center our board. And then if we go down to drawBoard at line 89, gets an offsetX, offsetY, nested for loop again. We're just iterating back over the tiles that we got, and recall, actually, generateBoard returns tiles, and then we set board equal to the result of generateBoard. So down here in line 89, again, in drawBoard-- actually at line 95. Within this nested for loop, we're going to get a tile at board y, x, just so we have a shorthand reference for it. We don't have to say board y, x several times, which you would have to hear. We're going to draw the sprite-- the quad at tile, tile, tile.tile, which, recall, is a random number between one and whatever the number of quads we have in our tile quads table. And then that x plus offsetX, and the y plus the offsetY, which has the effect of drawing every single tile in our grid at some given offset. And that has the result-- and I probably should have run this, actually, in advance just so I could illustrate it. But we have a simple board. Looks nice. It's colorful, but it's very, very basic. Just a 2D render of our game. There's no behavior or anything. This is just how we draw the board. So any questions as to how just the drawing, and the creation of the board work? OK. So swap1 is a little bit more complicated. It builds on what we did before, what we just built, which was getting the board implemented, and drawn onto the screen. But there's no behavior at all. It's just a static-- basically, the same thing as drawing an image onto the screen. And so for that, what we want to do is implement swapping. So how do we think we can accomplish this? Anybody have any ideas as to how we can swap? AUDIENCE: [INAUDIBLE] using tweening to have it go in opposite directions. SPEAKER 1: Well, we could. That will have the effect of-- the response was we could use tweeting. We could, and we actually will for swap2, but it's going to be a little bit more complicated than that because they're in a 2D array. So if we just tween their x, y-- AUDIENCE: [INAUDIBLE] SPEAKER 1: They will be in the same place in the array. But yes, ultimate-- AUDIENCE: You could switch the position in the array, and then reload the array. SPEAKER 1: Switch their position in the array, and then reload the array. We will switch the position. We don't have to reload the array, but we will switch their positions. That's effectively, what it is. Literally just take two tiles, and swap in CS50, where we just take two variables, and get a temp variable that points that one variable, gets its values while the second variable gets the values of that one, or vise versa, I think. This one gets this one's values, this one gets this one's values, and then this one comes down to this one, basically. There's the middleman that keep-- because if this one gets this one's values, it's going to get overridden by this one's value. So there would be no reference to it's x, y, or anything. So that's why you need to store this one up here, so this one can come here, and this and come back down here. So we've done a swap, effectively. And there's ways in Lua to do swaps, as we saw before, without even needing a temporary variable. You can just do xy get some other xy, which sort of bypasses that. But when you start to do four things getting swapped at once, and you have four commas, it can get a little tricky, a little bit unwieldy. I'm actually not 100% sure you can unpack more than two things in Lua. I'll have to double check on that, but right off the gate, we're seeing that double assignment here on line 32, highlighted x, highlighted y gets 1, 1. And let me actually run just so we can see. There's actually a couple of pieces here besides just that. Swap1. So we have the board as before, we also have something to show us where to swipe because we have to know where we're swapping the tiles. In an ideal implementation, which is an optional part of the assignment, you would have mouse behavior for your game. So you could just click on two tiles, or click, and drag, and swap them. In this case, we're not doing that. We're just implementing key based behavior. So when I press left, right, up, or down, I can move. If I press Enter on a tile, and then move around, it's an indicator to me that I've selected that tile to swap with something else because it needs to keep track of OK, you want to swap this tile. What do you want to swap it with? I want to swap it with this tile. So they get swapped. I want to swap it with this tile. So they get swapped. Or this tile. So you can swap it with whatever tile you want to. There's no constraints. The actual distro code implements a constraint, and so offhand, what do you think a constraint would be for making sure that we can't-- AUDIENCE: They have to be adjacent. SPEAKER 1: Yeah, they have to be adjacent. So what would that entail? AUDIENCE: That their x [INAUDIBLE] SPEAKER 1: Exactly. And the shorthand for that, really, is if the absolute value of their x's minus their y's is equal to 1. Because if you subtract one's x from another one's x, and then one's y from another one's y, and then you add the differences together, that'll tell you whether they're directly adjacent to each other. It has to equal one. If equals zero, then their x's and y's are the same. If equals two, then it's two tiles away on the x-axis, or it's away on the x and the y, in which case it would be diagonal to it. So the only way is it's x's minus it's x's. Tile1.x minus tile2.x, and tile1.y minus tile2.y, if they're absolute value of their difference is one, then they're adjacent. That's in the implementation. So this is why we have these variables here, highlighted tile. Basically we're setting a flag saying, do we have a highlighted tile? If we do, we're going to perform some drawing logic later down in the draw function. Basically, how would we draw a highlighted tile, do you think? AUDIENCE: Add a rectangle with transparency. SPEAKER 1: Exactly. So the answer was add a rectangle with transparency. That's exactly what we do. I'm going to go down to this part here. So on line 173, if we have a highlighted tile, and basically, this is in the middle of a loop-- our y, x before. We've put it into a draw board. We have the drawBoard function, but x, y, or y, x, the tile is going to be whatever tile we're currently on, and if we do have a highlighted tile, and that tile's gridX-- notice we now have a new variable called gridX, as opposed to it's regular x so that we can check for these sorts of things to see where it is in the array. If it's gridX is equal to whatever we've set highlightedX to, and gridY is equal to highlightedY, then love.graphics.setColor half transparency, and then just draw a rectangle with this 4 at the end of it, which actually draws a rounded rectangle. If you pass in no 4, it will just draw straight rectangle, but if you pass in an int at the very end, that's how many rounded segments basically that rectangle will have. So you can get rounded corners on your rectangles, and it's good for UI drawing, and stuff like that. We use it a little bit in the distro. So that's how you get a highlighted tile. There was also a selected tile, and a selected tile is just draw a rectangle, same thing, but it's a line this time, and there's always going to be a selected tile no matter what. So we're always going to draw it here at the end of our render function. It's just 255-- 234 for the opacity so that it's just kind of transparent, but not super transparent. Set line width to 4 so that it's not just a very thin rectangle. If you set the line width, and then you draw a rectangle with the line format-- the line mode of drawing, it will use whatever the current line width is when drawing the rectangle. So we set it to 4, then draw a line rect at selectedTile.x plus offsetX selectedTile.y offsetY, and we draw it 32 by 32 because that's the size of a tile, and then we set our color-- remember to always set your color back to 255, 255, 255, 255 because if you don't, and I did this when I was debugging, actually, you get some fun stuff. Wait, was that the right one? Oh, I might have fixed it up above where we-- there was an issue. If you don't set, basically, your color, and you set it to red, everything will draw red after you've done something. So if it ever happens, remember to set your color back to 255, 255, 255, 255, anytime you change the color in some way, like I'm doing here. AUDIENCE: Alternatively, you could also just make sure to always set the color before you draw something. is that right? SPEAKER 1: Yes. The response was make sure you always set the color before you draw something. I think that's what I ended up doing in this distro, which is why it's not working anymore. I think it was-- where was it? It was here, but I must have fixed it because I accidentally left that out when I was debugging, and it ended up drawing-- everything was red. So just as an aside just because Love2D is a state machine. Drawing it beforehand is definitely the safer way to go too. So the core of this, because we're running a little low on time-- the core of this overall block of code is just the swap here. So if there is no highlighted tile-- so basically, if we pressed Enter or Return-- now, we have all input handling in love.keypressed key. And by the way, this is input handling to change the x and y of our selected tile. If we press Enter, and we don't have a highlighted tile, then we need to have a highlighted tile, otherwise we should swap them. So we get a reference to tile one and two, we swap, we create temp variables. Recall, we need to have that middle man up here that keeps track of this tile's information. So it's going to keep track of all of tile2's information with tempX, tempY, tempgridX, and tempgridY because we need to not only change their x-coordinates, but also their grid positions. And then we need to create a temp tile here. Basically, here's where we actually swap their places in the board. So tile1.gridY, tile1.gridX gets tile2, and then we're getting a reference to temp tile so that we can-- because if we set board at wherever tile1 is to tile2, we won't have anything where tile2 is. We need to have a temp tile to keep track of-- sorry, we won't have anything at tile1 if we overwrite it with tile2. So we need a reference to tile1 here, so that we can put it into where tile2's spot is, right here. And then we need to do all that before we end up swapping their coordinates and tile grid positions, otherwise you get weird buggy behavior when you're moving the selected tile around. And then we can on the highlight, and then reset our selection because their selection is also going to get changed after we do the swap. So we need to put it to the second tile because it gets swapped with whatever tile we highlighted. And that's the overall gist. It's basically taking two tiles, flipping the information, storing a middleman. Same thing in swap in CS50, a little more complicated, though because these all have subfields that all need to get manipulated. And a lot of this can actually be done mathematically. You can actually have its x and y mathematically derived from it's gridX and gridY. Just multiply by 32. In this case, I just kept them as variables, and separate. But yeah, you could just do that too, and that has the effect of swapping the variables whenever we move them, and then that's the fundamental first step in Match 3, is just swapping any two tiles in a given position. So does that make sense altogether? OK. So this example is actually not that much different at all from swap2. I'm going to show you swap 2 right now. So if we go to swap2, the only change we really have made is that now tiles flipped instead of instantly changing, they tween. And this is a piece of cake at this point. We already know-- what's the function we need to do? Just timer.tween. All we need to do is just take the two, and then just tween tile1.x and tile1.y to tile2.x, and tile2.y, and do the same thing in reverse. Tween tile2.x, and tile2.y to tile1.x, and tile1.y. And so if we open up swap2, go to main, nothing in this program really changes, except in update, where we go to line 99, and we're just doing it here. Notice the definition. Over 0.2 seconds, it takes in the definition table, here, and it's taking in two entities because we're modifying two things. We're modifying tile2, and tile1. We're just setting x to tile1.x, and y to tile1.y, and then tile1 is getting the tempX and tempY because before, it was just getting it directly from the temp, and now it's just tweening it over time. But that was before just a bunch of tile2.x equals tile1.x, tile2.y equals tile2.y, tile1.y equals tile2.y. That's all it is. That's what's really nice about it. Now we don't have to really work hard at all to get nice, smooth transitions in whatever we do, whether it's a UI, or the game. It's just super nice, and convenient. So that's all we need to do to get basic swapping done. That was swap2, the tween swap. And so I put together a set of slides here just to illustrate the algorithm that we use to calculate the matches. So right now we've got swapping in, but we don't know when we've gotten a match. So just offhand, does anybody have any idea as to maybe how we can go about calculating whether we've got any matches? AUDIENCE: Well, we already figured out how to track if a thing is adjacent. So you, I guess, have a table of adjacent-- you go through [INAUDIBLE] block, and if you have an adjacent-- or for every adjacent block, you check if that color equals your color. And if it does, you check if-- well, then I guess you need to figure out what direction it's in, and then you check, continuing in that direction, if there's another of the same color. SPEAKER 1: So their response was when you're looking at tiles, look at all adjacent tiles, and if there is a color that's the same one, then figure out its direction, and then move from there. So like a recursive style. I guess you could implement it recursively. It probably would be a little bit trickier to understand, and probably not as efficient. The way that we are actually going to implement it is going to be a little bit more iterative. So all we really need to do is check every row, and every column one time, and then go basically, left to right. So in this case, we have to check every row and column one time in this direction, and then one time in this direction because we can get vertical and horizontal matches. So we start off. Let's just arbitrarily decide we want to start going left to right down the data structure. So we'll go, what color is this? That's brown. OK, check the next one. Is it the same color? If it is, then say OK, the number of matching tiles that we've found so far is two. If it's greater than three, then later on we'll need to add that group as a match to our list of matches, basically. But if it's not, OK, then the number matches is one again. So set it to one, and then do the same thing. Same color? No. OK, number of matches is one. In this case, here we have the number of matches is going to be two because this is blue, and then we're going to go ahead, and then same color again. The number matches is three, and then we've gotten to the end of the row. So we can say OK, what was our last number of matches? Was it greater than or equal to three? If it was, add that group of tiles to our table of matches if we've gotten a match, and then move on. And we do that over, and over again, and if it's in the middle of a group, like it is here-- so this isn't at the end of the row. This is just in the middle of the row. What we do is number of matches one, two, three, and then we go here, and it's set to one. Well, first of all, we check number of matches when we get to a different color. We say, OK, this isn't the same color as this tile. This is purple, and this is gray, but number of matches is three. So what we do is we just add these three tiles to our-- we're keeping a table of matches because we're going to go through, and we're going to delete all of them. And then, eventually, we're going to do some tweening as well, but we're going to delete all of these. And then in order to do that, we need to walk backwards. We need to say, basically, for x gets position minus number of tiles in the match, just add that to a match, add that to a match, add that to a match, and then add the match to our table of matches. And that's it for the x direction. And for the y direction, it's the exact same thing. Going down here-- different color, different color, different color, different color, and then same color, same color, different color, but number of matches is three because one, two, three, and then it's going to walk backwards, up to the top, add that match, and then just continue down here. Same thing, same thing, and then same thing there. This is at the end of the column. So it's going to get to the end. It's not actually going to look for the next tile because there are no more tiles, but every time we complete a row, or a column, we check at the very end, do we have the number of matches equal to three or greater? If we do, then we need to do the same logic as we did before by adding that match to our list of matches. So it's actually quite a simple algorithm, and this is the set of steps that I just illustrated. We have a match found there. Oh, sorry. [? Tony, ?] yes? AUDIENCE: If you complete two matches at once, would it see both? SPEAKER 1: It would. The question was if you complete two matches at once, would it see both? Yes. If you complete-- and it wouldn't if you deleted them as you went because let's say you had like one, two, three here. I'm assuming that's what you mean, one, two, three, one, two, three. If you just deleted them as you went, then no, it wouldn't see them. It would go here, it would get these three, delete them, and then it would just see these two. But because we walk over the entire thing, and then we only delete matches after all of the matches have processed, we're going to add this one first, and then when we do our vertical one, we're also going to see this one, and so it's going to count as two matches. And you could make your code a little bit more complicated if you wanted to, and say if there's an intersection between two matches, I want to give the player more points. Or I want to give him some sort of effect like in Candy Crush, I think you get like explosions, or Bejeweled, you get explosions if you get like a T pattern. And if you get four in a row, you get a laser or something across the screen. And actually, part of assignment is clear a row. If you get four in a row, you should clear that row, or call them. If you do that, then yeah, you can have logic. But currently, all the distro does is just this simple iteration-- horizontally, then vertically, and adding matches as you go. And actually, there is an optimization that you can make. If you go here, for example, let's say we're going here, here, and then we're here, and we're at a different color than the last one. We can just go to the next one. We can just skip because we know we only have two left. There's no point in looking for a match if you're at the n minus 2 because there's no possible way to get a match. So that's just a shortcut. A little optimization you can make, and that's actually in the code. Just break off if you're at-- in the code, it's x or y equals 7. Just break out of that for loop basically, and go to the next row, or column. Any more questions as to how that works? If you're actually looking in the code, we won't go over it in too much detail in class. It's fairly straightforward, I think, once I walk you through the algorithm a little bit, but I'll point you to the relevant lines. It's in the play state. No, sorry. It's in the board in the calculate matches function. Here, on line 50, calculate matches. So horizontal matches, y gets 1 to 8. You keep a color to match, and basically, you just keep track of how many you've matched. Match numbers one always when you're doing a brand new color, and then starting at x 2 to 8, because we already got the first tile, basically, if the color is the same, increment matchNum. Otherwise, set our current color to that color, the next tile. If we've done this, and our match is greater than or equal to 3, then we found a match. We can add a match. We create a new table. We go backwards from where we are with x 2 gets x minus 1, and then x minus matchNum. So it works for no matter how long the match is, whether it's three, four, or five. And then we're subtracting 1, and then you just insert into that match, the tile at that x 2 position because the matches are made of tiles. So a match is just a group of tiles put together, and so you can intersect to any given match just by comparing the tiles, and just seeing if they have the same tiles, basically. That's how you'd get a cross match. And then after that's all done, just insert into matches that match. Here's a little optimization. If x is greater than or equal to 7, and this is in part of the loop where we already know that we're on a new color from the last color, we'll just break, and then set matchNum to 1 if we haven't gotten to that point yet. And then this is the part of the code that accounts for a last row-- the row ending with a match because we're not going to be on the next loop to see whether we're going to a different color. We just need to check to make sure at the end of any row iteration, or column iteration when we go to the next row, or column. Before we go to the next row or column, that matchNum is greater than or equal to 3, and if so, then do the same logic here, but start x at 8. And same thing for vertical matches. Exact same logic, just x and y are inverted. And then that's it. And then self.matches, we keep a reference to self.matches in the board so that later we can remove them here, and I believe we use it for something else. And then we return, basically, if the number of matches is greater than 0, we're going to return matches, else we're just going to return false. And we can use this later. We can say if matches from our play state, then we can call a few other functions, and bring in new tiles, and stuff like that. But just for the sake of being thorough as an illustration, this is how the algorithm works. In this case, actually, this was before I made the optimization. We wouldn't actually do this in this particular case. This would have shorted down to the next column before it even checked this, but if your algorithm didn't make that optimization, then yeah, I would just see two tiles there. Go to the next one, nothing there, no matches. Same thing here. There is a match there, and the match would be found not at the end of the diagram here, it would be calculated when it's pointed here, but it knows matchNum is greater than or equal to 3 at that point. And it does the exact same thing here. We just go column wise, and then nothing there, nothing there, and then we've got one right there. And so the next part-- oh, any questions on how that works at all? The next part-- we have the matches now. We have them in tables. We have the tiles. We have references to the tiles. How do we remove the tiles once we have-- how do we get rid of them as soon as we have the matches? Assuming that our board is a table, a 2D table, and each array within there just has a tile object, how would we clear the board of our tiles? AUDIENCE: Are you including what you're shifting [INAUDIBLE]?? SPEAKER 1: No, just remove it from-- just like like. Just remove it from play. AUDIENCE: Oh. I guess you can [INAUDIBLE] SPEAKER 1: Yeah, you could. Yeah, with a little bit of finagling, you could get it to where you could set a tile to be invisible, and then you could just give it a new tile ID, I guess, and then shift it up above, and then make it come. Well, I don't know if that approach necessarily works super well for this because of gravity because the tiles have to come down. So then you'd have to bring the lower ones, if they were at the bottom here. Those would have to come down. That kind of approach would be a little tricky. You could make it work, I think. The simple approach, which we used in this distro, is actually just setting them to nil because if you set something to nil, it's just not going to render, in this case. So we're just setting all of these tiles that were previously there to nil. They're nothing at this point. They effectively would render like this if you tried to render them, assuming that your code accounted for it, or it didn't break. And then the next stage would be the actual getting the board fixed because we have the tiles removed. So now, we have this thing here, but there is a step that has to happen before we get new tiles, and that's gravity. We have to actually shift everything down. So how do we go about shifting tiles down? So this first column, we don't really have to do anything, right? This column is all set, but what about this column? How would we shift? How would we get that tile to go down? AUDIENCE: [INAUDIBLE] SPEAKER 1: Sorry? AUDIENCE: Tweens again? SPEAKER 1: Tweens. Yes, we could do it with tweens, but from a data structure standpoint, how would we-- because that will just tween the xy, but that won't necessarily fix-- the underlying data structure still has to represent-- because we're going to do iterations over it, we have to have references to the tiles in the right spots. AUDIENCE: So just shifting it was the table from the fourth row to the fifth row. SPEAKER 1: So how would you start by getting this tile down to this position? AUDIENCE: Switching it from the fourth row to the fifth row. SPEAKER 1: You would. How would your algorithm work step by step to making sure that would happen? AUDIENCE: Is it start from the bottom, and if that's nil, it would go up more SPEAKER 1: Exactly. That's exactly you do. You start from the bottom, and then whenever we have anything that's nil, we need to look for the first tile above it that's not nil, and shift it down. So in this case, we start from the bottom, and we go up this way. Not nil, not nil, not nil, not nil, not nil. So none of those are spaces in the code. It's called spaceY, or space and spaceY. So we go to the next column over, and we only have to check vertically in this case. We don't have to do a horizontal check for anything because gravity can only follow in one direction. So we just go over here. So we're only looping through this code, effectively, in this case, five times, but in our code, eight times. But it needs to be a while loop rather than a for loop, and we'll see why in a second. But start here. We see oh, we have a space there. So what we need to do is say, OK, the lowest space is here. So we need to look for the next tile above it, and shift it down. So we keep a reference to this, and we look up here, and we say oh, this is a tile. Perfect. So I'm just going to take this tile, and I'm going to set that space index to that tile, and then I'm going to set this index to nil. And then we just have to start again, though, from here because this tile is now space. So we have to look up here, and say OK, so basically, our y counter stays at that thing, and then just goes back up because our y counter could, theoretically, come all the way up here before it finds a tile, and then shift it all the way down, but we can't just-- or here, let's say there are two tiles right here. Our y counter might end up here because these are all spaces, and the tile gets shifted down here, but we can't just start our y counter back here again, and go up to the next tile, and look for spaces because we have all these spaces down here. So it's a while loop. So while, basically, there are no spaces on any of these points, we need to make sure that we keep lowering the tile. So keep a reference here, tile here, bring it down, space here. So we keep a reference with space. We say oh, there's a space here now. We can look all the way up, but there's no tiles anywhere. So we know that we can just move onto the next iteration of the loop. We haven't found any tiles. We don't need to bother with it. Same thing here. We have a space reference here, tile, found a tile, shift it down, space here, tile here, shift it down, space here, tile here, shift it down, space, space, done, and then we rinse, and repeat that. It's kind of almost like a bubble sort type of algorithm. Not a sort, but it has the same sort of look and behavior to it, more or less. Here's just a visual illustration of it. So start from the bottom, go up, we're looking for spaces here. No spaces; column is perfectly stable. We found a space here, tile is there, shift it down. Restart the loop from the tile, space, space, space, no more spaces; column stable. Space found, tile found, shift, space found, tile found, shift, space found, tile found, shift, and so on, and so forth. And so that's the gist. Super, super basic. But now we actually have to replace the tiles. AUDIENCE: You don't even need to check. Once you shift a block down, you don't even need to check the space above it, whether it's a space or not because you know that's automatically a space when you just shifted a block, right? SPEAKER 1: Yeah. Actually, that's true. Yeah, I guess in that case, you wouldn't need to. But we do need a reference to that space, and keep checking above it. But yeah, I guess you probably don't need, necessarily, to check whether it's a space or not. You can just assume it's a space, and I actually think my code does that. I'm not 100% sure off the top of my head. We can check, and see. I think it's down here. No? Is it? Oh no, it's in get falling tiles, I think, which is on line 177. So for 1 to 8 in x, we keep a spaceY. So spaceY, we set it zero because that's just a variable. We don't have a space yet. So just because you can't index a tile-- you can index Lua tables by zero, but because they're not by default, we're just setting the zero as like our false space flag. y gets 8, starting at the bottom. So while y is greater than or equal to 1, tile gets self.tiles y of x. In that case, it's going to be at the eighth position. So space is set to false, but space is our space found flag, and also whether or not the tile that we just looked at was a space. Sorry, no, it's just our space flag. We check to see if there is a tile at our current position. So recall, everything gets set to nil. So we can just say local tile gets self.tiles y x. This will be nil if there was no tile there. So if tile, which means if it's not nil, if it equal something, spaceY of x is going to equal that tile. We keep a reference to spaceY, which is our last space. We set tile.gridY to spaceY because we have to reset it to gridY. We're going to tween it here. This is how we actually get the falling, tweening behavior. We're going to tween it's y to tile.gridY minus 1 times 32, recall, because coordinates are zero based, but Lua tables are one indexed. Space is false, y is spaceY, and then spaceY gets zero. Basically, we're going to start at the-- we're going to put spaceY to that tile, and then we're going to set spaceY to 0. I think it actually does, in this case, it is actually checking that tile to make sure that it's-- yeah, because it's just getting set to the tile-- spaceY being the tile that we just replaced, and just put into an actual spot. So it does actually make the check up above to see whether that's a space or not. Only one caveat though actually is-- actually, no, that wouldn't be true. I was going to say, if you're at the top of the screen, but no because there's no way we can be at the top of the screen, and have-- yeah, I don't think it would work. A small optimization you could make is you just assume always a space. Yeah. That's the get falling tiles in a nut shell, or at least the ones that are falling from gravity. And then we also have tiles that we want to add to replace them, and so we'll see that here. So this code. So what we need to do to replace-- what do you guys think we need to do to get replacement tiles? AUDIENCE: Check response, and check if it's empty, [INAUDIBLE] but if it's not, then you're done. SPEAKER 1: Yeah, exactly. So the response was check to see from the top if there are any tiles that are empty, and if there are, then spawn some tiles, and then ideally, tween them to their new positions. You can basically just assign them to their values here. So what we need to do, actually, though, is if we spawn a tile up here to put into any of these positions, their gridY's need to be set in advance because they're going to occupy that space anyway. Their actual y position needs to be tweened. So because the x and the y are separate from the gridY, and the gridX, those are just table indices, but not their coordinates. We can tween those, and it won't actually have any effect on the data structure. The data structure itself can maintain-- we can still use the data structure-- put a tile in its right spot in our table, and then give it the right gridX, and gridY, but tween the x and y value. We can do whatever we want with those. We can make them spin around, and stuff as long as the data structure is intact, and ideally, as long as we can't input while it's doing it's movement, and stuff like that because that could create some visual bugs. And so what we do is we actually disable input when a swap is taking place, and you'll see that in the distribution code. But yes, count how many spaces there are. Spawn four tiles, spawn two tiles, spawn two tiles spawn four tiles that have already been given their right gridX, gridY, and then just tween their y to wherever it needs to go. It's gridY times 32-- gridY minus 1 times 32. And so that's what we're doing here. We're just count, and then boop. That was my favorite part of putting this show together. And so we're going to get into a couple of minutes of talking about sprites, and palettes, but I think the one thing-- blanking for a second. I was going to talk about one last thing. Let me see if I can figure out what that was. Oh, right, so in the board-- sorry, in the play state, I believe, is where this is, there is a function. So play state has it's own calculateMatches, basically, where it waits for you to-- where once you've basically swapped any two tiles, it will calculate whether those tiles have made a match. And we're going to get matches via self.board calculateMatches, the function that we were looking at before. If there are any matches-- well, we play a sound effect here for every match. This is where you also calculate the score. You just multiply the number of tiles in a match by 50, and part of the assignment will be adding some value to the individual varieties of the tiles. Here, we tween. So we return also from the board class a table of tweens for all of the new tiles that we just spawned, and so what we're going to end up doing is tweening all of them here. So notice that we're passing in a timer.tween, this variable, tilesToFall. That's a definition file that we're just returning from our board class. And so once those are all finished, then we get new tiles, and then we tween here. I think this line is redundant, actually. I think this might have been a debugging line. I don't think we need this. No, we don't need this at all. So sorry. This is the important part. We're going to tween-- wait, we do need it. Self.board getNewTiles. What am I thinking of? Sorry, a little bit confused for a second. I thought this was an empty function that I defined. Get new tiles. Yeah, this returns an empty table. But basically, the gist of it is the play state, when it calls this function, it will call itself every time. And I think this is actually having the result of doing it instantly here because newTiles is just an empty table. I think all this should be is just this inside all of this like that. But that has the result of calling itself again because when we get new tiles coming from the top of the screen, we could potentially have a case where we've gotten some matches, and it's not shown here, but new falling tiles could give us new matches. So after we calculate matches, let's say maybe this tile dropped here, but it was a purple, and these two were already there. We've already calculated matches, but then we need to do it again, and then do it again if it keeps happening. And so you should be recursively call self calculateMatches in that case, which will have the effect of accomplishing that because this will always look for matches. And so when we call self calculateMatches here, over, and over again, until there are no matches-- as long as there are matches, this should keep happening. You should keep getting scores, and tiles should keep getting cleared. But as soon as that's not the case anymore, then self.canInput equals true, and we're not calculating matches anymore. We don't recursively call the function anymore, and we're done. And so that's just the point I wanted to illustrate. Got slightly confused by, I think, what was a vestige of my old code. Maybe I was trying something, but I think this, ultimately, should just be this, and I'll test it, and make sure, and then push the change. And it doesn't need to be over 0.25 seconds. It can just be instant. Palettes, really quickly, with something I wanted to cover, which was just the idea of taking art, and then just-- and I have a couple of cool examples to show. Just taking some sort of picture, and then giving it-- only using or some sort of image, and only using 32, in this case, or some arbitrary number of colors. This is some fancy stuff that some person named DawnBringer online did. He generated a very famous 32 color palette called DawnBringer's 32 color palette. But basically, it allows-- this is done with just 32 colors we see on the screen. Those are all dithered. Dithering is a term which means to just draw two colors pixel by pixel, interleaved, so that from far away it looks like a brand new color, and this is a dithering chart. This just shows you every color here at the very top. These are all 32 colors. These are 32, and those are 32 intersected with each other such that they're just like dot, dot, dot, dot, dot. Every other dot is every other color. And so you can do some pretty amazing things with just a few colors. This is actually done with 16 colors. All four of those are only 16 colors. This is just to show you what it looks like when you do it to an actual image. This is an example of what using a color palette on an image that doesn't work well with it looks like. So this is a regular image with I don't know how many colors, thousands of millions of colors, and this is using DawnBringer's 32 color palette. So still looks very similar to what it should. It's a cat, but there's a lot of weird things going on in the background because taking an image with a lot of blur, and a lot of distorted color, has the effect of giving you blotchy patterns when you go down to a few colors. But this is an example of an image that has a lot of flatter colors. There's still a lot of colors in this image. There are some shades, and stuff like that, but this is thousands of colors, and this is 32 colors. So clearly, if you do it on the right thing, you can actually get really good effects with it. And so again, not a whole lot of difference, but this one's got I don't know how many hundreds of thousands of colors, and this one's only got 32. And so how it ties back into what we're doing is this is using a 32-bit color or 32 color palette on purpose. This is actually DawnBringer's 32 color palette. Breakout used the same palette, 32 colors, and a lot of our 2D future lectures will use limited color palettes. If you're trying to draw sprite art, and you want some quick, and easy ways just to give your work a little bit of consistency, I recommend trying to pick 8, or 16, or 32 colors, and just adhering to using just those. And you'd be surprised at how much you get out of it, and how much more cohesive your work will look just by imposing that constraint on you. It's an artifact of a real world constraint of former hardware. The NES only had so many colors it could color each sprite, like four colors, or something that. And so you also get a-- if you're going for an authentic retro look, it will help you in that sense. And then different from palettes, but related is palette swapping, which is another term you've probably heard, which is basically all of these Mario sprites-- they'll probably start with a gray scale Mario, some like gray version where each of these separate colors are mapped out to some table where one equals red, two equals blue, or whatever. And then you can just shift all of them, and then you get all of these different nice effects, assuming that you've created a good palette. You can get a lot of reuse, and this is actually how Super Mario Bros. used to do some of its programming. The clouds and the bushes were the same sprite. One was just colored green. It was palette swapped green from the white that the cloud was colored. So that's the gist of Match 3. Assignment 3 is going to have a few parts to it. So time addition on matches. So when you get a match, you should get time added to the clock. Currently, right now, you only get 60 seconds. It's a little bit hard to actually get past level two at this point. So getting points for every tile in a match. Make it so that level one starts with simple flat blocks. So earlier, we saw the array of tiles, and it was flat tiles on the first index of every color row, but there were several other patterns like x's, and circles, and triangles, and stuff. Make those worth some higher amount of value, each one. Create random shiny variants of blocks that will destroy an entire row when you get a match. So have a block. It should have some field, shiny or something. If it's shiny, render it with something to make it look shiny. You can use particle effect if you want. You can put a very opaque, or a very transparent maybe yellowish or whitish rectangle on it to give it a brighter look. And then if it's in a match, that entire row should get cleared instead of just that match. Only allow swapping when it results in a match. This is an important thing because right now, mathematically, it's actually very unlikely that you'll get a board that has matches on it to begin with. So you're going to have to pick a subset of tiles in your implementation, and actually use those instead of just using all of them. Pick six tiles, which you can get variants on, or just whatever flat colors, and then use only those to spawn your board. Don't use all 18, or however many there are. And then optional, if you're curious, if you want, probably, an arguably better gaming experience with this, just implement actually playing with the mouse. Being able to click and drag, or just click individual tiles. And to do that, you will need to convert-- because we use the push library for virtual resolution, you'll need to convert the window mouse coordinates to push coordinates so that they'll map into the game space appropriately, and so you'll use a function called push to game, where it takes an x and y, where the x and the y will be your mouse coordinates. Next time, we're actually going to get into a little bit more robust of a game, arguably, like a Mario clone. This is actually where this course started was I taught a seminar on Super Mario Bros. We won't be using Super Mario Bros. assets because of copyright, but we'll be using this tile sheet here, which is very similar. It's got a nice aesthetic. We'll cover tile maps. So how to generate levels using individual tiles. 2D animation, so rather than just like static things that we've had going on so far, you'll have characters that actually walk, and jump, and do different things. We'll talk about how to actually procedure generate platformer levels, which isn't terribly difficult. It sounds kind of difficult, but it's actually pretty-- for very simple stuff, it's not too bad. Basic platformer physics, so hitting blocks, and jumping, and stuff like that. We've covered a lot of that with Flappy Bird, actually, and actually, the bricks from Breakout kind of tie into it a little bit. Hurt boxes so we can have enemies that hurt you, and visa versa. And power ups so that you can change your state in some way to make you larger, or invincible, or whatnot. That was Match 3. Thanks a lot, and see you next time.
B1 中級 Match 3 (Lua教程) - CS50的遊戲開發入門。 (Match 3 (Lua Tutorial) - CS50's Intro to Game Development) 11 1 林宜悉 發佈於 2021 年 01 月 14 日 更多分享 分享 收藏 回報 影片單字