Placeholder Image

字幕列表 影片播放

  • [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.

[MUSIC PLAYING]

字幕與單字

單字即點即查 點擊單字可以查詢單字解釋

B1 中級

Match 3 (Lua教程) - CS50的遊戲開發入門。 (Match 3 (Lua Tutorial) - CS50's Intro to Game Development)

  • 11 1
    林宜悉 發佈於 2021 年 01 月 14 日
影片單字