How to build a game engine
Learn how to create a game loop, a render loop, and — if time allows — the camera and viewport. Will King will teach us how to do it in TypeScript (and talk a little bit about why he chose Gleam for his own game engine).
Resources & Links
Read the transcript
Captions provided by White Coat Captioning (https://whitecoatcaptioning.com/). Communication Access Realtime Translation (CART) is provided in order to facilitate communication accessibility and may not be a totally verbatim record of the proceedings.
JASON LENGSTORF: Hello, everyone. Today, on the show, we are going to be tackling something that I think a lot of developers find pretty intimidating, which is the idea of building a game engine. We're going to figure out how that works. I've never seen it myself, so I'm very excited. We're bringing in Will King.
Will, how you doing?
WILL KING: I'm doing pretty good. I got in a battle with my 1yearold and he won. We're doing all right. [Laughter].
JASON LENGSTORF: Always a good time. It's probably good you let him win.
WILL KING: Definitely let the kiddos win. They can learn when they're older.
JASON LENGSTORF: You give them a false sense of security and establish dominance until that outgrow you. That is how it was in my family. [Laughter].
WILL KING: Yeah, yeah, no. You should establish dominance for an embarrassingly long amount of time where your kids are like, well, let's just give this one to dad. That's how I see this going. [Laughter].
JASON LENGSTORF: Good. Good. Outside of the parenting advice, for folks who don't know you, can you tell us a little bit about who you are, what you do?
WILL KING: I am a dad of three. I've got a lovely wife, her name's Ally. Ally's favorite pastime is trolling me on Twitter. I have a beautiful wife with a hilarious sense of humor and her favorite way to use it is to troll me. I talk about dev stuff. I work at Crunchy Data. We're a managed Postgres service. Anything related to products, digital, design, I just love it all.
JASON LENGSTORF: Very cool. So, I want to talk a little bit, today, about the idea of a game engine. Because, I guess, first and foremost, because that's what we said we're going to talk about. [Laughter]. I think a game engine is one of those things that, on its face, kind of feels like reinventing the wheel a little bit. There's a lot of tooling out there, there are a lot of platforms out there that will kind of give you a game engine out of the box. What inspired you to take this on, in the first place, and learn how this works?
WILL KING: All right. So, my first piece of advice to anyone listening is, don't build a game engine on your own. If your goal is to build a game and sell it, that's your business, so, like, if you want to build a game and make money off of it, this is not the path to doing that. But, it is an incredible path if you're not sure what project to take on to, like, grow and learn in, like, very wide varieties of, like, facets. So, the reason that I wanted to take this on was, I had recently started streaming and I'm a design engineer, I love design. I've got a background in industrial design and I've been coding for seven or so years. What is a project that kind of spans all of those very wide variety of skill sets? Game engine, because, one, you've got to obviously be able to develop the code to build it. There's also, like, graphics and rendering. There's also this, like, new facet of stuff that I'm not as familiar with, that I wanted to learn, which is being able to create story, story arks, characters, be able to emotionally connect with a story. I think that is extremely valuable these days because, like, one thing that is very hard is distribution and connecting with people. With your product and the way that happens is by, like, one, you can have, like, obviously the best product that there ever was and people want to use it no matter what. But, like, a way to reinforce that is by having a brand or, like, a story that people care about. I want to learn how to do that. A game felt like a very fun way to practice that skill set.
JASON LENGSTORF: All right. You got me�
WILL KING: I wanted to take on an ambitious project to learn a whole lot of new skills.
JASON LENGSTORF: And I love this. For me, the� pursuing something for the sake of the journey of learning it, right, is� one of the more fulfilling things that I've found in my career is really just tucking into something that's really hard, not because I have a specific, I want to deliver a game, but because the process of going through all of this is going to give me so many advantages down the road. Right. I think that� that really is, like� I don't know. I really like that, as a general approach, and it's driven a lot of the ways I build, a lot of the projects that I make. I don't have a purpose for them. Just kind of like, that seems fun to figure out that puzzle and off we go.
WILL KING: 100%. I always tell people I owe my entire career to these ambitious projects because if I had not done any of them� and I've got, like, a giant list of them, of ambitious projects to take on. I got no special skill set, I've got no competitive advantage. But I just, like, decided to do something, buckled down and beat my head against a wall sometimes until I figured it out.
And, like, I can name at least three of my many jobs that I got specifically because of something that had nothing to do with my previous, like, work experience. You know?
JASON LENGSTORF: Right. You took on something gnarly and people noticed it and that was enough. I love it. I love it.
So, let's talk about game engines in the abstract for a second.
WILL KING: Perfect.
JASON LENGSTORF: So when we have a game, there's clearly a lot going on under the hood, but I don't know that I have sort of, like, the buckets of what's happening when I'm thinking about a specific game. So, in my head, everything is a very specific and challenging thing. I'm thinking about� you know, I've got a character and there's something making that character animate and move, there's actually controlling it, there's maps, there's actions, attacks, jumps, collision detection, whether or not you run into a trap or an enemy projectile or whatever it is you do in the games. All of that, to me, feels like kind of way outside of anything I've done. Where do you start? What are the big buckets?
WILL KING: Same. [Laughter]. That is exactly my experience when I started this and that's why I wanted to do it. For this project, I was, like, specifically testing how much can I do with AI assisting me. Not AI writing the code. I specifically picked a language, like, to write my game engine to where� not because I knew AI wouldn't be as helpful. I've been interested in this language, it's a very new language and AI's not going to be able to write the code, but AI can help me think, help me research.
JASON LENGSTORF: Help you get the concepts.
WILL KING: Exactly. The way I've begun leveraging it in my daytoday is exactly that. What can this help me learn the language of? If I'm learning something new, give me the language that this speaks. What makes developers really good at solving problems? Googling, researching, you know all the terminology. When you enter something new, it's hard to get going fast because of knowing the terminology. When you're thinking about how do I get started going on a game engine, your question was, what are the buckets? How do we define the system we're trying to build.
JASON LENGSTORF: The core parts of a game.
WILL KING: I've not finished building a game engine. We'll share the very naive version. You've got your game loop. Your game loop is a closed system that is constantly looping at FPS, FramesPerSecond. Your game loop consists of two� two subloops. One is your update loop and your update loop is what says, okay, like, what has happened since the last loop that we've run and let's mutate our game state based on those updates. That is the only thing it does. It is responsible for taking in events, updating game state based on those events and the rules you've defined in your game and the render loop is a pure function of, give me the game state and I will render that to a screen. So, I think what makes it a little easier is once you kind of get in there, what you realize is everything is, like, very isolated, split off, like, subgroups where, like, you're not really� you're� the way you phrased it when you started, you've got this and your collision detection. When you get in there, everything is not all meshed together. You've got independent subsystems and their job�
JASON LENGSTORF: Not one, giant function, if character and enemies and enemies are a coordinate matrix. I imagine that is sort of what's happening, but, like�
WILL KING: When you're writing it, you're not necessarily thinking about that. I'm not holding the entire system in your head. You're thinking� very similar to most programming. Once you get down to the atomic ideas, you've narrowed the inputs and outputs that you're expecting.
JASON LENGSTORF: Gotchu. Cool. So, you mentioned that you built your game engine using a tool� a language that was new. Did you say what it was?
WILL KING: It's called Gleam. If you go to gleam.run, for any of you interested. It's based of, like� I won't say it's fullybased off of it, but very influenced by Elm, which had its heyday. It's a functioning programming language. Based off of [Indiscernible] style syntax. You get your Typesafe functions. Everything's strongly typed, statically typed. You get immutable data structures from the jump and then Gleam� what's interesting and why I picked Gleam, when you're making a game engine, you've got to be able to render in the browser. So it's like, okay, obviously, that means JavaScript. I didn't want to use TypeScript. I wanted to learn something new.
Gleam compiles to TypeScript. Gleam also compiles to the Erlang. What makes that interesting is it's known for its ability to accept a whole lot of connections. It would be very efficient on the server side and then on the browser side, you have your JavaScript to do all your client stuff. So, in a game engine, what you can do is, you can have all of your game logic, what's running the game on your client and instead of having to write a completely different language for your backend, Gleam also compiles to a very efficient backend language for when you need to track and manage state updates to your game. Any time there's a safe state, you can send this off. Let's say your game gets extremely popular and you have tons and tons of people on your game at the same time. Everyone's running their own version of the game in their browser. They're not worried about performance issues. When you send off your request to the backend, Erlang is efficient. You've got your optimized backend, your optimized frontend and a single language you can write in the middle is what I viewed this project.
JASON LENGSTORF: Okay. I'm with ya. I'm with ya. So, we're not going to do Gleam today. Mainly because nobody wants to watch me get my first steps with any language. It involves too much cursing for a livestream. [Laughter].
WILL KING: Oh, yeah. That would just be ridiculous and� we want people to learn as much as they can, as well, watching and so let's pick something familiar to get the concepts.
JASON LENGSTORF: Yeah, so we're going to dive in using JavaScript and TypeScript. So, is there anything� highlevel, is there anything else we should run through before we get handson keyboard? Are there questions I should have asked that I didn't?
WILL KING: That is a very interesting question. I wouldn't say there's necessarily any questions before jumping into the code. I think you kind of covered� like, as much as you can� this is one of those very specific cases where you really do have to uncover it as you go because of a couple reasons. One, like, what kind of game are you making? What kind of game are we making?
JASON LENGSTORF: That's a great question.
WILL KING: If we're making a firstperson shooterstyle game�
JASON LENGSTORF: Different requirements if they're doing a Zelda.
WILL KING: Or cardstyle game. All of those are game engines and if you're using, you know, one of the big guys, um, they have all of those abilities built into them but when you're writing something from scratch, like, you're writing it very finetuned to the exact type of game that you're trying to make and although highlevel concepts may be the same, like, once you get into the actual updating and rendering portions, like, that's where everything's completely different, so...
JASON LENGSTORF: Got it. Very cool. Let's do this then. Let me cut over to this side, here, I'm going to throw a banner up so people know what we're doing and I'm going to first, and foremost, direct everything to your social media. You said you are a� an X user primarily so let me throw that one up for everybody. And this episode is being livecaptioned, you can find upcoming episodes on CodeTV website. Maybe consider subscribing on YouTube, get early access to new content. You can get those captions by going to lwj.dev/captions. That is being done today by our captioner, Vanessa, that's my email, we don't need that up on the screen. What's going on now?
[Laughter].
So, yeah. This is Vanessa, from White Coat Captioning, taking down all these words we're saying. If we need that, let me put a banner on the screen for a moment...
WILL KING: That's impressive.
JASON LENGSTORF: And here is the link, as well, on the screen. So, with that being said, Will?
WILL KING: Yes?
JASON LENGSTORF: If I want to get started, what is the first thing that I should do?
WILL KING: So, the first thing that you� I would recommend doing is just being able to, like, get a canvas up and going and have, like, a game� a game loop running.
JASON LENGSTORF: Okay. Is there, like, a specific framework or tool or, like, library that we're going to use today?
WILL KING: There are not. So, like, the� I think we can just write this whole� like, we're just focused on getting the� the concepts, I think, today.
JASON LENGSTORF: So we're going Vanilla?
WILL KING: There are lots of libraries. You're not writing a game engine.
JASON LENGSTORF: They're giving you the game engine.
WILL KING: You're plugandplaying. We're going to go from Vanilla here.
JASON LENGSTORF: I have a notification going off that I'm going to silence. Give me just a moment...so, we're going to use Vite. Here's VS Code. Let me...let's get to GitHub and...Learn With Jason. We're going to NPMcreate Vite and this is going to give us...let's see...build a Game Engine DS and you want to keep it straight Vanilla?
WILL KING: Vanilla. Let's go.
JASON LENGSTORF: TypeScript. Let's get in there. I'm going to open this one up in its own window. We'll close this one down and...init. Npminstall...and, let's reload this window. There we go, so now that it's not dimmed. And, let's take a look at the default here, so this is our� here's our starting point, which is a plain Vanilla Vite TypeScript with the kind of basic thing going on there. Let me make that a tad bigger for everything.
All right. So...the place that I would start is I would start come over here. I would create a README and call a shot here. So�
[Laughter].
Build a Game Engine in TypeScript...and we're going to set up a todo. So, for me, what I� what I would want to do is, I guess, first and foremost, decide what kind of game we're making.
WILL KING: Perfect.
JASON LENGSTORF: And then� it's not going to autocomplete this for me? Boo! Now that we've got that...um...this is where I kind of get lost. So we� so we would need the main loop.
WILL KING: Right. So the first� the first thing I would do after that is set up canvas rendering and a loop to repaint your canvas. It's, like, the very first step that I will start with and then we can� we can kind of take it from there.
JASON LENGSTORF: And then once we've got that in place, I guess the rest of that will depend on�
WILL KING: The type of game. Right. I may make�
JASON LENGSTORF: You've got one? Let's play it safe. [Laughter].
WILL KING: Chat's going to come out here with, yeah, make a firstperson shooter in the browser in 30 minutes. [Laughter].
JASON LENGSTORF: Build a Gold Mine. [Laughter].
WILL KING: We're going to build a Legends of Zeldastyle game, a topdown, tilebased game. You're have your character, your character will move around along a screen, that kind of thing.
JASON LENGSTORF: Okay. All right. So that makes sense. I like those kinds of games. You get� one move is one square kind of thing. That makes sense.
WILL KING: 100%.
JASON LENGSTORF: I hear you can build Doom with TypeScript. That is true, you can build Doom with TypeScript.
WILL KING: You can build a lot of stuff with TypeScript. There are a lot of people building a lot of awesome stuff. There's a project called� ooohh, I'm going to blank on it. I'm send it in chat. There's somebody who built a turnbased strategy game in TypeScript. They've got the whole codebase opensourced. I was trying to learn, as well. So I'll find that and make sure we get that sent up.
JASON LENGSTORF: Cool. We've decided what kind of game we're making so now that we know that, we're going to set up canvas rendering, we're going to set up a loop to repaint the canvas. The things that� just to do basic idea capture here...the things that immediately come to mind is, we need, like, characters. We need movement. We need collision detection. We would need, like, start, restart capabilities so if you die, you can start a new game, sort of thing, which I guess we also need death. [Laughter].
WILL KING: It comes to us all. [Laughter].
JASON LENGSTORF: And then, what else? What else do we need?
WILL KING: Um, what you will also need is, um, you'll need, like, a decision on, like, what size of tiles that you're going to make.
JASON LENGSTORF: Oh, right. Map tiles.
WILL KING: Map tiles. You will need aspect ratio, like, what is your, like, width and height of your tile map going to be? Other things that you'll need� I guess with this one, which you've kind of captured under, like, movement and you'll need, like, a way to, like, attack, like, actions would be one. Those all fall under this concept of your, like, event messaging system.
JASON LENGSTORF: Okay. This would be set up, event messaging.
WILL KING: Yes. I'd say, like, the next thing after loop to repaint the canvas is, like, what you'll� what we'll want to do after that is, okay, now that we're rendering a basic canvas in a loop, let's go ahead and actually decide on in size and render our, like, map, our grid of what each map would be. Now, this is� what's interesting is when you think about a map, in a game, like, it's the entire world. So, what we're actually talking about rendering is� is, like, your camera. Your view port, if you think about it. So, like, your camera is, like, the� what is currently viewable and what might be a much larger world.
JASON LENGSTORF: There's everywhere you could go and where we are. That makes sense. Okay.
WILL KING: So, yeah...and once we've kind of got a� a map to render, what we can render, after that, is a character on the map somewhere. And then after we're rendering a character somewhere on the map, what we can do is we can then set up our event messaging to tell that character what we want it to do.
JASON LENGSTORF: Okay. All right. So, this feels like a good starting point. We'll probably come up with more along the way. So let's start here and I assume I'm going to set that up in my main ts here.
WILL KING: Uhhuh.
JASON LENGSTORF: And then how do you want me to start with this? Do you want me to blow this away and go empty?
WILL KING: You can blow it all away, completely from scratch, and that's how we'll get started.
JASON LENGSTORF: Okay. So let me delete some of these files we don't need anymore...and this, probably empty. This is fine. And then we've got our index here. All right.
WILL KING: Perfect.
JASON LENGSTORF: And that will leave us with a blank screen, but the game� the app is still running so as we update things, we'll be able to keep it rocking.
WILL KING: Absolutely.
JASON LENGSTORF: Good, good. So...first thing's first.
WILL KING: First thing's first. A blank file. What you will need� what we are going to do, since we're doing it in the browser, you've got to pick a rendering context. That means there are two choices, generally with the browser. You can do a WebGL 3D canvas or a 2D canvas. We're going to pick a 2D canvas, which means� go ahead?
JASON LENGSTORF: Do you want me to put this in as a canvas element here?
WILL KING: I generally will just generate out of the TS file. You can put it in the document and fetch it by "find by ID" or make it. You already have your reference for it there.
JASON LENGSTORF: In a canvas, I feel like there's one you to have to do a different "create element."
WILL KING: NSG is the one, if my old memory� from having to do that� is good. Perfect.
JASON LENGSTORF: I was doing something a while ago and got absolutely cooked. Lost an hour trying to figure that out. [Laughter].
WILL KING: Oh, goodness. Those are always fun. The next thing we'll want to do is a width and a height, so we have something that shows on the screen.
JASON LENGSTORF: Is this set attribute or straight on?
WILL KING: Straight on, width and height.
JASON LENGSTORF: And, did you have a preference for this?
WILL KING: No, this is going to change in our next steps. You can pick something so we have something that shows up on the screen.
JASON LENGSTORF: We'll make it 800x800. Our app was "document get element by ID. And that was "app." And I will "app.appendchild" and we'll drop our canvas in there.
WILL KING: Yep.
JASON LENGSTORF: And theoretically speaking, we have a canvas on here. Zoom in a little bit. Console. Zoom out here so that we have reasonablesized canvas. Okay. Okay. So we got a canvas.
WILL KING: We will need some sort of looping mechanism to, like, paint something to the browser. So, what would you expect?
JASON LENGSTORF: Um, so what I would probably do is I would create, like, a function called "render" and then I would, um...I would probably use "request animation frame."
WILL KING: Nailed it. There's "set interval" or "set timeout" as options for forloops. "Request animation frame" is good for two main reasons. One, it is based on when the browser would like to be updated, which is always nice. And then, two, a nice builtin feature is, like, games are expensive from a resource perspective. It's very nice that if you change your frame, like, your tab in your browser, that your game does not continue to consume all of those resources. [Laughter].
JASON LENGSTORF: Ah, yes.
WILL KING: When you change tabs, your "request animation frame" pauses for you.
JASON LENGSTORF: Okay. So, right now, we're just kind of doing nothing bullet we're actually running a game loop.
WILL KING: That is correct. And, what I would actually� what I would actually do there is, um, you can just pass� I mean, I guess you could do it that way. Never mind. Oh, interesting...we'll see if this working and then we'll keep going. So, inside�
JASON LENGSTORF: That's my favorite way to do it. I'm pretty sure you did something that's going to break, but let's watch. [Laughter].
WILL KING: Inside of your render, we want to actually paint something, just to get something on the screen. So what we need to do in messing with painting on 2D is you use a "Git context."
JASON LENGSTORF: And I should "Git context" outside?
WILL KING: You can get it inside so we're not fetching it on every single loop. Inside the argument, you pass 2D.
JASON LENGSTORF: There it is, okay.
WILL KING: And now you've got a context and what we will usually want to do here is, um...we want to set� the first thing we want to set is a fill style. Fill style tells us what color the next shape we're painting is going to be. You can choose whatever color you want and then after that, what� what you can do is just do, um...if, earlier on, you could say, "if there's no context, just return and bail."
JASON LENGSTORF: So we'll give ourselves that bailout.
WILL KING: We want to use "context.fill.rect." There are four arguments� there you go. Your x and your y, where your rectangle starts in the corner and how wide and how tall you want the rectangle to be.
JASON LENGSTORF: Assuming we did this right, it is going to put a red square, that's 100pixel square. There it is. Look at us go! Okay. But, this is happening now, every frame, which I believe browsers are running 60 frames per second, is that correct?
WILL KING: Um, that is actually what I was going to do for our next step, in just a second. Let's just go ahead and do that now because that's an important� important thing to actually� for us to be able to talk about which� so, one thing we want to do is inside of your request animation frame argument, let's actually use the timestamp that they're giving us.
JASON LENGSTORF: Use the timestamp.
WILL KING: The very first argument that they're sending is the current time of, like, when this request animation frame is run and so this� this variable is, like, extremely important for a game loop because the whole way that our game loop timing works is by measuring delta of time from when did the last update run and what time is it now? What is that change in time, so we can update our game based on how much time has elapsed.
JASON LENGSTORF: If your start at 0 and then you are at 100, the delta is 100. If you run it at 150� delta is the difference between two numbers.
WILL KING: Perfect. That is exactly correct. What we want to� what we can start doing here is if you want to create a new variable outside of our render function, this is going to be the beginning of our game state.
JASON LENGSTORF: Is this mutable, I assume?
WILL KING: It doesn't have to be. It is because it'll be an object and objects are�
JASON LENGSTORF: This is going to be our state.
WILL KING: Correct. And let's give this a property of "previous time." And we can give it an initial value of performance.now, which is basically what timestamp� it's basically called performance.now inside of each loop but I'll give it to you as an argument.
JASON LENGSTORF: We would want state.previoustime to equal timestamp on each update?
WILL KING: Maybe? We will, at some point. [Laughter].
JASON LENGSTORF: I'm helping. [Laughter].
WILL KING: You are helping. [Laughter]. Each loop, as we move on to the next one, we want to track what the current one was. We can calculate our framepersecond and get an idea of how fast the browser is rendering everything. So, let me get that pulled up. Framespersecond is a timestamp� our timestamp is in milliseconds. Whatever our timestamp minus our previous time is our delta and our change. So, this is� this is our time since last update, is what you could call it, delta. And then, to get our FPS, you could console.log 1,000 divided by the delta. These numbers are all in milliseconds and 1, 000 is 1 second in milliseconds.
JASON LENGSTORF: Update this and make sure we're keeping�
WILL KING: Exactly. You'll see a number get� a ton of numbers getting printed. So what you'll see is that you are probably rendering way faster because browsers are extremely performant and very awesome. The first thing you have to be able to do is you want to control how fast your game is rendering. You don't want that to be, like, some unknown, very variable number�
JASON LENGSTORF: If the computer's memory bogs down, your game actually slows down and, like, projectiles would slow down and then everything speeds up and that would suck.
WILL KING: It would. If it's over 650 framespersecond, everything moves very fast. Like, suddenly, you're, like� your character that's animation is, like, bouncing up and down is looking like somebody who's had five Cokes, like our 2yearold, when their grandmother let them have Coke for the first time.
We want to create a fixed time� fixed delta time system, which is basically just, like, we will� we will leverage an accumulator and, like, the time difference to be able to, like, render our updates in, like, fixed increments. So, on to our state, what we'll want to add is an accumulator that starts at zero. Perfect.
And then, inside of our Request Animation Frame, what we� what we'll want to do now is take the� the game state accumulator and add on to it our delta every time it's looped because sometimes that delta is a lot, sometimes it's a little. But our accumulator kind of lets us know what's, like, the total number of, like, the total amount of time that we are, like, behind, that we need to, like, update for.
So, let's say our accumulator is at 67 and our framespersecond is 60, we'll have seven milliseconds left over so the next time it loops, let's say it's 113, those seven milliseconds added on to that give us two full frames to account for so this allows to� you account for every single update, no matter what the drift in relative time is compared to, like, how long a render might take.
JASON LENGSTORF: Okay.
WILL KING: So, we have our accumulator. Now that you have your accumulator, you can update your previous time. You've got that. We've consolelogged our framespersecond. Go ahead?
JASON LENGSTORF: We've got our framespersecond. As we're controlling, we probably want this to change to reflect what our accumulator's doing?
WILL KING: Yes. 100%. So, the next thing that we want to do is we want to define, like, what our framespersecond is in a constant so we want 60 framespersecond. In a constant somewhere, we're going to have our fixed delta, 1,000 milliseconds divided by 60. This gives us, like, the actual amount of milliseconds that would equal 60 frames happening within a second.
JASON LENGSTORF: Right. That makes sense. I gotchu.
WILL KING: We'll make a whileloop that makes says, "while our accumulator is greater than or equal to our framespersecond, we want to loop through this."
JASON LENGSTORF: Okay.
WILL KING: So this will basically give us a chance to make our updates as long as there are, like, expected frames to be running.
JASON LENGSTORF: Okay.
WILL KING: And that is where we will have, um...like, event updates, et cetera, et cetera, for telling people� like, telling our characters what to do and at the end of the whileloop, we will have to update our accumulator so that it actually eventually goes away. So, like, you'll subtract from the accumulator, your framespersecond.
JASON LENGSTORF: On each whileloop to knock it down.
WILL KING: Yes. Perfect. Here we go.
JASON LENGSTORF: So now we are� now we're running a game loop and this game loop is theoretically running at 60 framespersecond and our 60 framespersecond is giving us an opportunity to do our event loop here.
WILL KING: Yes, that is correct.
JASON LENGSTORF: I'm with you. So, that's cool. I guess, what's the next step? Mathematically, I understand what's happening. But I feel like it's a long way between a red square and Legend of Zelda. [Laughter].
WILL KING: You are correct. The next step we're going to take is less about it. To add an active element to our game, we have to have a map for the active element to actually work on. So, let's define how many, like, our tile grid. How many rows? How many columns do we want? Do we want them to move around? That's a highlevel constant that we'll define how many tiles across and how many tiles down.
JASON LENGSTORF: So if we stick with this 800, we could do an 8x8?
WILL KING: It could very well be. If we're starting to talk about games, one thing that is interesting is� this is now where we're getting into these other disciplines, like, as you are making art for a game, what are common art sizes? The beauty of a browser is you can make it whatever the heck you wanted. You really could� you really could do 100 pixel tiles. If you're influenced by Linked to the past or Sprite, your graphics for characters, there are some historical patterns for, like, what you use. Historically, the patterns exists because of hardware limitations.
And so, like, when you pick one, you can pick a style that you really like. So, like, a lot of the game is, how do you feel? What is the art direction you want to take it?
JASON LENGSTORF: In keeping with the Link thing, that's 8bit art, right?
WILL KING: It depends how far you go. There's 8bit, 16bit, 32bit, 40� all these kind of things, like that, depending. So, 64� how much time do you want to spend? If you're making pixelbased art, 64 pixel tiles is a lot of work. So, like, generallyspeaking for a hobby project, 16 pixel tiles is, like, a pretty easy way to get started because you're not having to, like, paint a ton of detail into them. And there's a lot of history and reference for, like, 16 pixel by 16 pixel tile art so I thing that would be a good one. 32 is also a pretty good one. You get a little more detail in there, if you want to spend the time to do it, and there's also plenty of reference for that, too.
JASON LENGSTORF: If we pick 16 as our tile size, then we would want to pick number of tiles across and down. Is there a number you found to be sort of a good, safe bet?
WILL KING: This one is totally up to you. You could pick a historical resolution if you wanted to. You could theoretically do any number, times your tile size, to get a good number of columns.
JASON LENGSTORF: 1024 divided by 16, I know those are numbers that add up. So, we'll do map width and map height and it's going to be 768, which is 48. So we're doing a classic size so now we know what the width and height of our canvas should be, we know what our tile sizes are. 64 tiles across, 48 tiles tall.
WILL KING: Uhhuh.
JASON LENGSTORF: All right. So, what are we doing next?
WILL KING: So the next thing that we want to do is actually render said tiles. It might be good to go ahead and pull the 64 and 48 into their own constants, as well, as columns and rows. That will be a value that we, like, regularly reference.
JASON LENGSTORF: Got it. Let's do it. And then we can do columns...and...rows is going to be 48...all right.
WILL KING: Perfect. So now we know that our canvas width and height should be the same as our, like, map width and height.
JASON LENGSTORF: Okay. So, I want that to be map width. We want this to be map height. And, down here, we would theoretically draw our tiles.
WILL KING: Our render function, we want to loop through each row and each column so that we have, like, an x and y coordinate to those and now that we have our xy coordinate, we can say, what exists at this coordinate?
JASON LENGSTORF: Okay. To set that up� to make sure I understand, I'm going to set up a forloop through the columns and then a nested forloop for the rows and we're going to use the x and y values as we count up to the maximum number to build out�
WILL KING: Exactly.
JASON LENGSTORF: What is it? Let x in columns. Is it "in" or" of."
WILL KING: So, this one might be "of." I use a, for real, oldschool forloop. Let's see if "of" works.
JASON LENGSTORF: We might have to try this, let y of rows. And then we will be able to do...so, to figure out our x and y value� for each of these, we're going to want to context fill rect and we're going to want to use not� this isn't our actual x value. This is more of our indexes.
WILL KING: This is our, what tile are we rendering? When we think about it as a size, each tile is, like, its coordinate times its tile width and height. And since our tile width and height are the exact same� our actual, like, xy would be tile column number times tile width.
JASON LENGSTORF: Right. Times...wait, did we say tile size? Tile size is what I said.
WILL KING: Yep. Since we're using numbers, I think we'll have to use a traditional forloop.
JASON LENGSTORF: Does that really not work?
WILL KING: Syntaxes for arrays and objects.
JASON LENGSTORF: Booo. That sucks. Fine. Fine!
[Laughter].
So, zero, um...this is why everybody uses "i." Is less than� we want less than, not less than and equal to and tile call number, plus, plus.
WILL KING: I think you actually do want "equals," too, but we'll find out.
JASON LENGSTORF: Okay. Um�
WILL KING: No, "equals" is good. Let's do "not" for now.
JASON LENGSTORF: Let's do this. I'm going to take you, drop you there. There we go. So now we've got tile column number is equal to our tile size is our x value. We've got our row number is our y value. And then we're going to use tile size twice.
WILL KING: Absolutely.
JASON LENGSTORF: Okay. So, theoreticallyspeaking, what will happen?
WILL KING: We will probably not want red for our underlying color, so, if you want to use, like, x value, um, that seems a little more like� let's do, like, a sand color. You can use one that is BD, it's a hex, so it will start with the pounds. BD 1434.
JASON LENGSTORF: Okay.
WILL KING: That's� I gave you the color of our� our next step. So, try this one instead: F5E1E2. This is taking me back to my IT HelpDesk days. One of the biggest things you'd do is reset a Password.
So, now we have� we have a map. And we have our map rendering in 60 framespersecond.
JASON LENGSTORF: Here's our map. Our map is rendering. We have� we are doing stuff, so if I was� can't remember where this is. I'm not going to worry about it. If we put a stroke style on this, we could see our individual tiles, but I'm not going to stress about it.
WILL KING: We could.
JASON LENGSTORF: Context.strokes.
WILL KING: You could do Stroke Style�
JASON LENGSTORF: I was thinking white.
WILL KING: That would be a good way to break it up.
JASON LENGSTORF: And then we need a stroke width?
WILL KING: What you'll do next is you have to actually draw a stroke rectangle. You'll want to draw a stroke rect with the same values.
JASON LENGSTORF: Ah, there we go. Everybody can see that, I hope.
WILL KING: Perfect.
JASON LENGSTORF: If I zoom way in, you can see it. When I zoom out, this is our 64x48 grid.
WILL KING: If you want to make it extrainteresting, copy stroke rect and paste it before fill rect, real quick, because I want to show something. This is, like, something that you don't necessarily think about too much before you get started, but the way that, um...go ahead� did you save that?
JASON LENGSTORF: I did, because� wait, is it supposed to do something different?
WILL KING: It is, normally.
JASON LENGSTORF: It did, so watch. It becomes�
WILL KING: Okay. Yes. Yeah, yeah, yeah. So that's, like, with canvas, the order that you render your elements matters because if you render whatever comes first is the bottom layer of your render stack. And so, that becomes a very fun problem as you have more and more elements in your game and knowing which things belong on top of other things. So, fun thing to keep in mind for all of you out there.
All right. So, let's do something that is, like, actually more interactive. Let's add� we're going to add our character. So, what we can do here is, um� and what might be a little better for this, let's go� just for temporary sake, let's cut our columns and width� columns and rows in half, just for the sake of making them a little bigger on the screen. I think that will be� that will be� and then what we can do here, next, is you can� that'll be� that'll work. Let's just do it like that. Perfect. In our state, what we are going to store now is a new value in our state object, which is, like, our character.
JASON LENGSTORF: Got it. Is it Character Position?
WILL KING: It is. What we are going to store� again, very naive because we're just getting concepts. Character concepts. You can think of it as an x and a y.
JASON LENGSTORF: If we start it up here, we would then probably want to check� so, if state.character� there's not an easy comparison for this� how dare you. At zero equals "tile call num" and, we'll just copy this whole thing...is this what you wanted me to do or am I going rogue here?
WILL KING: You're going rogue, but you're nailing it. [Laughter]. That's exactly the step.
JASON LENGSTORF: Okay. So, then what we would want to do is probably set the context. Fill style is maybe a different color?
WILL KING: Correct. You can do the other call. BD1434.
JASON LENGSTORF: Okay. Else. We're going to set it in here, instead.
WILL KING: Nailed it, because that's also a fun thing. Fill style is global. If you change it for one element� not that� not that that is generally something you're running into with, like, games, because you're generally going to be rendering sprites instead of rectangles and stuff. It is important to remember it is a global variable and if you change it and don't change it back to something else, everything will change to that color.
JASON LENGSTORF: I'm in an older version of TypeScript. I'm going to fix it so that it works properly.
WILL KING: What you'll probably want to do is not even use an "else" because you're always going to be rendering a tile. Even if your character is there, on that tile, you still want to render the tile under it, because your character is not necessarily always going to be�
JASON LENGSTORF: You're throwing� you're throwing a full�
WILL KING: You're now wanting to thing about your character as a separate element in�
JASON LENGSTORF: Placed on top.
WILL KING: Exactly.
JASON LENGSTORF: Instead, we would do something kind of like this. Oh, no, I did it wrong, because it's the paint order so we need to put it below?
WILL KING: Yes. We will put this down at the very bottom.
JASON LENGSTORF: Got it.
WILL KING: Just to make sure that we're rendering this, if it exists, it's getting rendered on top. Perfect.
So now we have a character, but your character does absolutely nothing. You can't� he just stays there, where you told him to be and he doesn't go anywhere else.
JASON LENGSTORF: If we manually change that, so if� two rows down, the character's moving.
WILL KING: Uhhuh. Now we need a system� we need some kind of system to where we know how to update the state of our character in our loop. And�
JASON LENGSTORF: Right. And so�
WILL KING: Our loop is a closed system. There is no, like, passing outside variables into, like, the function, per se.
JASON LENGSTORF: Okay.
WILL KING: So what we need, instead, is a twolayered system here.
JASON LENGSTORF: Okay.
WILL KING: So, the first layer is, we need what I would call our messaging queue. It is something that is listening to global messages that are happening, that you're defining are, like, possible messages for your game to receive.
JASON LENGSTORF: And that does that live in our state or is it a separate thing?
WILL KING: This one lives outside of our state. This could be anything. The reason this is global and can live outside of this and it will just be an array we'll pass things into. If you are moving your game from, like, a ready state to a paused state, that is, like� that is not necessarily affecting the current state of, like, the game being played.
JASON LENGSTORF: Yeah. Okay. I gotchu.
WILL KING: That's the state of the game loop, itself. So you've got to be able to tell the difference between more global messages and messages that should send something to your game.
JASON LENGSTORF: Okay. I'm with you.
WILL KING: Your game state. You'll have your messaging queue. It will be an array of events. If we want to do a little TypeScript here and define some fun types to make live a little easier, you could have a type that is "message" and your message could be a union of, like, different message types.
One of those messages being that a player cues an event.
JASON LENGSTORF: Um...
WILL KING: If you want to change interface.
JASON LENGSTORF: Maybe talk me through this.
WILL KING: For a uniontype, you have to use type. "Type message equals." And then you define� then you can define an object state instead of it. So we'll call this one Type instead of it, as the argument, and give it a name. The way I like� the way I like to name things is very influenced by time I spent in Elm and how their messaging system works which is, like, who is doing� like, who's sending the message? What's the action that the person is doing and what are they doing the action on? So, player cuing. The player is cuing an event.
JASON LENGSTORF: And then how do you like to split these up? You can do the snake case kind of thing. You can do, like, a split character, like that. I've seen dots.
WILL KING: I generally do CamelCase. I don't have strong opinions about this kind of stuff, you know?
JASON LENGSTORF: Sure. We got player cue event.
WILL KING: We have a player cue event. What type of event? It's a cue event and it's going to accept an event type and an event type is, like, what is getting put into our event queue. So, you could define another type that will� it will look very similar to our message type, but it's just called a game event type instead.
JASON LENGSTORF: And that's going to be what this is.
WILL KING: Exactly. You could do "type, game event. "For instance, what we'd want to have happen is, like, we want the� we want it to move. Now, this is not necessarily�
JASON LENGSTORF: Does it operate on something?
WILL KING: I wouldn't say that this one necessarily does. You can just call this one "player move" and then the direction you're trying to send our player.
JASON LENGSTORF: And this would be, like, up, down...
WILL KING: Yup.
JASON LENGSTORF: Right. Okay. And we'll maybe start there and then this, then, would be, um...any valid game event.
WILL KING: Uhhuh. And we won't� we won't worry about any other messages. If we have time, we can come back and look at our browser pause and unpause because that's another fun feature you have to account for. But we can just start here. Now what we need is a way to listen� listen for those� like, what do we want to send, like, what are we listening to, to send these events, so we know that somebody's trying to tell the� the player to go up, down, left or right?
JASON LENGSTORF: Got it. Okay. Um...so we would add an onkey press or something?
WILL KING: Uhhuh.
JASON LENGSTORF: You're not adding that in the render loop?
WILL KING: Nope. These would be global. Global listeners. This is how, like, now� now we're thinking of a whole new system. This is our event messaging system and what we're writing down would be our listener or, like, controller system, which is, like, where our inputs coming from and how do we set up all of our listeners to listen to our controller, which case, the keyboard is the controller here.
JASON LENGSTORF: Okay. And it would be event key, is that the right one?
WILL KING: Yep, sure is.
JASON LENGSTORF: I'm making an assumption that we want some of these�
WILL KING: Arrow up, with capital �a.�
JASON LENGSTORF: Like that?
WILL KING: Yep.
JASON LENGSTORF: Okay.
WILL KING: In this case, what we want to do is push that this event happened into our messaging queue, using the type that we defined.
JASON LENGSTORF: Push. And it was type...player queue event and event type. And event type is Player Move?
WILL KING: Another object, which will be Player Move and the direction.
JASON LENGSTORF: Oh, right. Right. I was� and now that we have that, it's going to be...direction and...up.
WILL KING: Yep. And we just duplicate that for every one of those directions that we want to go at. Also, that's always a fun one, is when you press� when you set up your events� this is what happened to me the first time I was working through this. You set up your events and forget that switch cases have to have the breaks and then you press down and your character moves 20 times and you're like, what's happening?
[Laughter].
JASON LENGSTORF: That's one of those, like, trauma memories. Like, you know when you have a response that you don't even know you're doing� why did I not type a "t" on "left" either time? Up, down, left, right. And then, we default� default, we can just ignore because it'll do nothing or do we need to specifically ignore the default case?
WILL KING: Oh, you can just do it this way. This is perfect.
JASON LENGSTORF: So this will build our event queue, but now we need to set up a loop�
WILL KING: Now we need to process or messaging queue. Now we go down to our� our rendering loop that we've kind of starting defining. And what this is starting to turn into is less of just a render loop and now we're getting into, like, our game loop and our render loop, like, our game loop is, like, the overarching, what's going on, and then we have our update portion and our render portion. So currently, we're kind of mooshing them all together.
JASON LENGSTORF: We probably want to pull these a part?
WILL KING: Eventually. Before doing any updates on our game state, including any changes to our accumulator or anything, what we want to do is pull our highlevel messaging queue and we want to apply those messages to our game state.
JASON LENGSTORF: Okay.
WILL KING: And then make updates to, like, our actual game state. You'll want to pull all of the messages out of the queue and store them into a variable and then clear that messages� that messaging queue array so we effectively have taken the whole queue.
JASON LENGSTORF: Can we do a straight copy like this?
WILL KING: If you use Splice and start with zero and use the length of the current queue, it pulls all of it and empties all of it at the same time, which is nice.
JASON LENGSTORF: No, not that one. There we go.
WILL KING: What we will now do here is we can loop through each message and apply them, which is basically, like� yep.
JASON LENGSTORF: Each.
WILL KING: And then we have our message. Then, like, what you'll want to do is� over time, you'll have a bunch of different types of messages and you'll probably have some kind of giant switch statement that says, if it's this message, this is what you do with it.
JASON LENGSTORF: Yeah, that seems like a good call. Let's actually set that up properly. It would very quickly outgrow the "if" statements so we'll do it the right way, right out of the gate, keep our lives simpler in the future. So, we do case, player queue event and that's going to do something and then break and then I can do this and so if it's a player queue event, right now, add it to the state, right?
WILL KING: Exactly. We want to push it. We want to push this into our event queue.
JASON LENGSTORF: And that would be event� message.event type. I named this wrong. Because it shouldn't be Event Type, it should be Event.
WILL KING: Yeah. It would be� it's a full event.
JASON LENGSTORF: Because I'm already confused by my own naming. I'm going to fix that now. [Laughter]. Why are you mad? Not assignable to type Never because we need to do the thing up here that I was going to ignore. So, we'll do...each of these...all of these are...number, except for you, which is an array of numbers and you are an array of game event. Okay. Now you'll stop yelling at me and I can continue with my life. Good. Okay. So now we're� we are looping through our messages. If it is a player message, we're putting it into the current game state.
WILL KING: If it's a pause or something else, what this does by putting it up here, is it allows us to� if an event has come in that should stop the game, for any reason, we're doing it before we do any state changes.
So now that we are done with that, what we will do is inside of our While loop� our whileloop is our update loop, the one where we're doing� we're only doing these updates if it's currently in a 60 framepersecond state. It's basically the exact same thing, structurally, which is pull all your events off of our state and empty it and then loop through those events because you definitely don't want events sticking around in state and getting run multiple times.
JASON LENGSTORF: Okay. So, we'll run this and event.state.eventqueue.length and Event for each event and inside here, we will say "switch on event.type" and, um, take it a case of Player Move...then, we want to� I think I know what we want to do, in this case...and then�
WILL KING: See� so this is� this is exactly what we were talking about when we started. If you think about, I need to listen to key presses and our character moving around. Now you've gotten down to� you're processing, okay, the player pressed a button, I know it's a player move. I'm not thinking of all these crazy things. I'm just moving a player. I only have to care about, like, the exact event that I'm receiving, which is moving a player, this is the direction, let's just� you're only worried about this atomic action. Like you said, you immediately were like, oh, I know what I should do here. You've been writing a game engine for 45 minutes. [Laughter].
JASON LENGSTORF: Right. Right. Right. So I want my new x and my new y, right, and those are going to be what we set up and then we would say...so, this is� this is going to be kind of a gross "if" statement, right, because we kind of have an "if." I already don't like what I'm doing here, so let's set these like this...and then I can say, um, "if event direction equals up, then I would want new y to equal math.min of zero or state.character.one, minus one." Am I doing that math right?
WILL KING: You are doing it right. Why don't you explain why you did map.min?
JASON LENGSTORF: I don't want my character to move off and go any further up and same general idea. When we get to the bottom, we need to check that it's not more than the number of tiles tall, which I guess I can do really quickly right here. And we'll say "else, if direction is down, then we're going to do math max." And what was my number of columns? Columns? No, rows. And it would be a plus one this time. Let's check that I'm doing this right before I write the rest of these. So, now that I've got this set, we need to actually just� I just need to update the character, right?
WILL KING: Yeah. What you'll do is you'll make sure that the character on the state is updated with their new coordinates.
JASON LENGSTORF: And do I do it right here?
WILL KING: That's all you have to� yeah, you do it right here. This is your update. This is your update state.
JASON LENGSTORF: Equals new x, new y.
WILL KING: You might have a small bit of an issue because there's not a default there.
JASON LENGSTORF: You are correct. Let's go state.character.zero; state.character.1. So now...it's not doing anything. And it's not doing anything for reasons I don't understand. Do you see� do you see something that I did wrong? Should I start consolelogging things?
WILL KING: Go ahead and console.log and let's go ahead and...no, that all looks good.
JASON LENGSTORF: Okay. So� all right. My listener's not working.
WILL KING: Yeah, let's look at our listeners to see if that's getting pushed into the queue.
JASON LENGSTORF: Let's go "messaging queue." Put it down at the bottom to make sure I'm actually do it. "Are you listening at all?"
WILL KING: Instead of "key press," let's do "key down."
JASON LENGSTORF: Oh, there we go. Okay, so that was something horribly wrong, but let's see...how did I get to�
WILL KING: Oh, I think you still want. Interesting. Let's go to your "down." Your down math.
JASON LENGSTORF: My down math...
WILL KING: Because�
JASON LENGSTORF: Whhoooaaaa. I got these backwards.
WILL KING: Yes.
JASON LENGSTORF: Because max� I always do that wrong.
WILL KING: Yeah.
JASON LENGSTORF: However...let's go, everybody. That part's broken. That part works.
WILL KING: Maybe it's�
JASON LENGSTORF: Oh, I'm getting� I'm getting one more, so it needs to be rows minus one and you�
WILL KING: Correct. Our coordinate system, the final row is 23 or whatever.
JASON LENGSTORF: And as I'm pressing down, I can't go any further. Excellent. Let's do the exact same thing, but for our left and our right and� oh, my god, why can't I write "left" today? Jeez. [Laughter]. This is columns. And this is our y value and these both...X. okay. Now we can move around.
WILL KING: And now we have our character that moves around our map.
JASON LENGSTORF: Sick. And can't escape out of bounds. Beautiful. Okay. So, I mean, this is� it's a pretty simple game, but we literally built a game.
WILL KING: And you've also, like� I think what's interesting is you've done some very� not naive, important, but also you said "collision detection." And when you think of collision detection, it sounds horrifying. You're collisiondetecting the edge of your map.
JASON LENGSTORF: You're basically saying, my character cannot move into certain coordinates so more advanced collision detection would be defining areas within the tiles that are also not allowed.
WILL KING: You're just rendering a tile. The only thing you care about is what exists at the tile that I'm currently trying to, like, render or in this case, with the update, okay, I have a target tile. I have a tile I'm trying to move towards. What exists on that� only that tile. We don't care about the whole map, we don't care about the whole world. What's on the tile? Is it going to restrict my movement?
JASON LENGSTORF: Extremely cool. Okay. Looking back at our README, because unfortunately, we are a little short on time, but we got canvas rendering, we got our size and render and got a character on the map� whoa, whatever that was didn't work.
We set up an event system. We also got movement done. We didn't get actions. We got a basic kind of collision detection done. We didn't get to these. We did get to the map tiles. We did get to our aspect ratio. There's quite a bit going on, in this game. So, this is kind of the next step. Now, we don't have time to build this, but can we talk a little bit about how you start to think about this? Like, I think, the [Indiscernible] enemy AI and attacks are the last unanswered questions for me.
WILL KING: If you think about it�
JASON LENGSTORF: I'm getting a request for a Part 2. You want to do another one where we finish this out?
WILL KING: Let's do it. We can start working on the same game. Maybe for next time, I'm design and tile sprits and character and enemy sprits so we're not boring rectangle and fun graphics.
JASON LENGSTORF: That would be amazing. Thank you for being down. I realize I put you on the spot in a live scenario. [Laughter].
WILL KING: That's what's all about.
JASON LENGSTORF: We will come back and in Part 2 of this series, we are going to figure out actions, so we'll get some attacks in place, maybe, you know, something else. Get enemies on the board and we will maybe figure out how we can handle damage, life meters, things like that.
WILL KING: Yes, that'll be awesome.
JASON LENGSTORF: This was a blast. Thank you all, so much, for hanging out. Let's talk, one more time, about where you can find Will. [Laughter].
WILL KING: Someone said game development taking longer than expected? Never heard of it? Which is the classic. Always takes longer. [Laughter].
JASON LENGSTORF: Excellent. Make sure you go and give Will a follow. You can find him on other social media, as well. Make sure you go and check out the rest of what's going on. We've got lots of good stuff coming up. We will be learning how to use stuff with Sarah and SFTP.
Lots of things coming up. If there's something you want to see on the show, reach out to us. You can reach out to us on CodeTV on Bluesky. Have I shared it? You go to codetv.dev...oh, my god. Not very good at Bluesky apparently.
[Laughter].
What is going on here? Whatever. We're just going to� we're going to post the straightup, the ID, whatever that means. We're codetv.dev. Go find us and send us messages.
Will, any parting words for everybody before we wrap this one up?
WILL KING: The only parting words that I would like to share with everyone is, I still don't know that much about game engines. I still have not built a full game engine, but I have learned a ton. There's been a lot of, like, fun and a lot of community that I've gotten to be a part of, this livestream, for example, and a ton of great people who are interested when you take on ambitious projects you might be scared to do. Just go for it and share and ask questions and you'll be surprised. One thing I always like to say, your project doesn't have to succeed to be successful. You could never finish it or find a lot of money from building something. You'll build community, you'll help others learn. You're going to help yourself learn. There are other benefits coming from going out of your comfort zone.
JASON LENGSTORF: Even if the thing you learn never serves a purpose, the practice of getting comfortable with being uncomfortable 1,000% will serve you throughout your career. Cannot recommend it highly enough.
Also, we have a Discord if you want to get into the CodeTV Discord. Let me drop a link to it here. We will� in this episode, like every episode, has been livecaptioned. You can see everything right here, thank you so much to Vanessa from White Coat Captioning for being here and being our live human transcriptioner today.
And I think that's everything. There's probably stuff I forgot to say. If you enjoyed this, like, subscribe, leave a comment because YouTube really likes that and we will see you next time. Thanks, y'all.