Check out a free preview of the full Blazingly Fast JavaScript course
The "Web Sockets Game Demo" Lesson is part of the full, Blazingly Fast JavaScript course featured in this preview video. Here's what you'd learn in this lesson:
ThePrimeagen provides an overview of a basic game program they have created. The lesson also covers the code for the server, runner, ticker, and game loop, as well as addressing questions from the audience. The instructor mentions a problem with frames overflowing or underflowing and sets a goal to reduce this issue to below 25%.
Transcript from the "Web Sockets Game Demo" Lesson
[00:00:00]
>> So, we're gonna talk about the basic game, or the basic little program we're gonna be making. I made a really dumb shooter program that looks something like this. This is obviously, there's no graphics, it just runs in Node, so I made my own pictorial version of it so that we can see it and see how beautiful it is.
[00:00:15]
Effectively, two people are standing far apart. They start shooting, at some rate. The bullets travel at some other rate. As the bullets get near each other, they may collide, removing both bullets. It's only played in a plane, it's only on a straight line, nothing fantastic. And once, of course, the bullet reaches the final guy and touches one side or the other, well, that person dies and that player loses.
[00:00:36]
Then we send back a nice message saying, hey, you won, you shot this many bullets, some basic statistics about what happened. And the other person, hey, you lost, but hey, you got all these things, and so there we go. Fun game, it's a scale issue for the person that died, I know feels bad for that guy.
[00:00:50]
And so there we go, that is what the program does. And so now we're gonna walk through what the code is doing kind of line by line, so it shouldn't be too bad. So, see next, okay, so the first thing is just we started really simple server. If you're not familiar, just do a little WebSocket server, it's gonna be played over WebSockets, cuz you played with UDP, probably UDP might be slightly better, but there's a lot of complication when it comes to UDP.
[00:01:15]
Just because I mean, you're still sending TCP and at the end of the day a WebSocket header that's sending a small amount of information only adds two bytes of information and there's an XORing of the packet. So, I mean, it's not much, could be four bytes or six bytes if you have the XOR part of it, so it's not like it's a huge amount of overhead on these things.
[00:01:31]
So, we're not gonna worry about the fact that we're using WebSockets. All right, so the server goes pretty simple. We start the server, we create a runner for all the games, and then on every new connection I hand the socket off to the runner, pretty simple. After that, we start listening on the server.
[00:01:48]
If we listen, awesome, I'm gonna print it out. If we fail, I'm gonna print out, hey, we can't start the server, just so we know all these things are happening, okay? Everyone should be pretty comfortable with that. And so let's look at the runner. The runner is pretty straightforward, all it does is it has a waiting WebSocket.
[00:02:05]
And when we have no waiting WebSocket, we just add the first socket that comes in and then returns, if we have two, we're gonna play the game, right? It's two people shooting at each other, so once we have two WebSockets, we play. And then we just repeat this motion over and over and over again.
[00:02:20]
Okay, pretty straightforward, right? Now playing the game gets a little bit more complicated, and I didn't really try to write this code in the most beautiful sense, so I hope everyone can just follow along. First thing we do is obviously we wait for the sockets both to be open, we don't want them to be in some sort of opening state or connecting state.
[00:02:37]
So once they're both opened, we send a start command to both of them. If this was a real game, obviously you'd send some sort of future time, you'd count down, not allow key presses, all that. This is not a production game, this is a fake game. Next, we create some state for each player.
[00:02:51]
After that, we listen to a bunch of different messages for each one of the sockets, hey, do we have a message? If we have a message, we need to add it to that player's queue. If we got a close message, we need to say the player closed. If we got an error, we need to say the player errors.
[00:03:02]
That way we can kinda keep track of all the state of the socket. Next, we create a ticker. A ticker simply is going to, as reliably as possible, create a 16.66, or really an FPS amount of frames. And so, with this one, it's pretty simple, we'll go over it here shortly.
[00:03:18]
Next, create a new game, and then after that I simply await for each time I'm supposed to run my update loop, and it will hopefully update as steadily as possible. If for whatever reason, we update longer than 16 milliseconds, I do kind of do multiple updates. And the reason why I do that is that the collision formula for two circles with time updates are fairly complicated.
[00:03:38]
Cated so I ensured that the bullets are big enough, and they're moving fast enough that at 16 milliseconds, even if they're next to each other, they'll still be overlapping at the end of the day. I'm not a great game programmer and I know these things, and so pretty straightforward.
[00:03:51]
After that we check to see are there any messages, if there are messages, we'll call a fire, and then after that we'll clear out the queue. If the has ended or player one has closed, or player two has closed, or player one has errored, or player two has errored, we quit this loop, and then we simply just get the stats out.
[00:04:09]
If both players have screwed up, don't do anything. If one player screwed up, send hey, the other opponent disconnected. If the other player do that, else send out the stats. Do a little bit of bookkeeping, and that's it. So that is really our kind of our game loop for running the games over and over again.
[00:04:24]
So pretty straightforward, pretty simple, I would say. Next, we have the ticker, the ticker is pretty straightforward. All it does is it just simply calculates intervals, and we have a next. Next just simply means, every single next is gonna just be a plus rate, rate being the FPS, so 16.66666 forever.
[00:04:43]
And so it's just gonna keep on saying, hey, that's the next point in time in which we need to do something. If from the last time to this time being called it has been less than the tick rate, I'm going to sleep for a second and just let whatever happens happen, and then I'm gonna come back and keep doing it.
[00:04:59]
You If I've slept a little bit extra than I was expecting to, I make sure I keep track of that amount, and then I return out to the player or to the game. Hey, you were asleep for this amount of ticks, or in other words, this amount of milliseconds, so I expected 16 but I woke up and the extra was about 18.
[00:05:16]
It's gonna come back so that way we can update properly and we know kind of where we're at. All right, so that is really most of the game. So the last thing, last piece of code we want to go over is going to be the game itself, which isn't all that hard so I'll just jump down to, let's go to update.
[00:05:33]
Update is pretty straightforward. Update, all we're doing is we go over all the player two bullets, we update the location and see if any of them have hit player one. We do the same thing for player one, we go over all player one's bullets, update their location, see if they hit player two, and then after that we go through each one of player two's bullets and see, hey, do we collide with you?
[00:05:54]
If we collide with you, I add this little hold bullets remove list because you cannot edit a collection. Bullets are a set, so you can't edit a set collection while iterating over it. I don't know what the effects are gonna be, but largely that is considered a pretty negative thing to do.
[00:06:09]
So I add it to a list, and then at the end, if there's anything left, we just simply delete the bullets out of the set. So we have player one and player two's sets. And I just kind of blindly delete each one, I don't really try to be precise about that this come from player one set or players to set, we just hope that we get it correct.
[00:06:26]
Fire is really, really simple, make sure you just achieve the fire rate in creating a bullet, really, really simple, just add a bullet to a set, and that's it. And so that's really the entirety of the game. So there you go, you got the whole nine yards, it's not a lot of programming, it's a pretty straightforward small piece of code.
[00:06:43]
I wanted to create something that had enough problems, but at the exact same time isn't terribly complicated so that even if you're just okay at programming, you can jump in and go, okay, I understand this, okay, I understand this. We didn't try to do anything too fancy. All right, there we go.
[00:07:03]
All right, so everyone feels comfortable, we feel good enough, right? Okay, good, all right, so we have a problem statement, which is that our internal CI server show that our games are not delivering frames. We expect frames to be 15, 16, or 17 millisecond frames. It's just JavaScript, of course, meaning we don't have precision, you cannot sleep for at 16.66666, right?
[00:07:24]
You sleep for if it's 16.66, you sleep for 16, the next time, that's gonna be 17, the next time that would be kind of 15. You're gonna kind of do this little jumping back and forth to where you're trying to make a 16.66 average, and so you're largely gonna be in the 16s, in the 17 millisecond sleeps.
[00:07:41]
We really want to quit having frames overflow or underflow. And so your job, you boss said, hey, we have to be at least below 25% of all frames being either overflowing or underflowing. And overflow would be 18 or more milliseconds, and underflow would be 15 or 14 or less milliseconds.
[00:08:01]
So that means if you did like a 19 sleep, the next one would have to come at much, much lower for you to be able to make up, and so you don't want to be bouncing around as a server, you just want nice smooth updates happening. Of course, you want to have a job so you're like, I like this idea.
[00:08:16]
And second, you want to get nice Christmas bonus, so there you go. We wanna do good job, we wanna be able to get some W. So we wanna do a really fantastic job and take something back that makes everybody happy. All right, everyone excited? Does that sound good?
[00:08:28]
We got a question?
>> Why did you use sets instead of objects to record the bullets?
>> Okay, why did I use sets instead of objects to record the bullets? The problem is I'm not really sure how I would use objects versus sets in this situation, because how do you iterate over an object of bullets?
[00:08:45]
I would have to have some sort of key, so is it a 1, 2, 3, is it an A, B, C sets? I can simply just iterate over the collection that's underneath the set by just doing this, and so it's a pretty simple data structure, plus when I want to remove it from the set, I get a remove it at constant time.
[00:09:01]
And so when I wanna look up something, it's a constant time lookup, and when I wanna delete something, it's a constant time deletion. So that feels a lot better than, say, like an object I'd have to a, know the key, and then deleting a key off an object.
[00:09:15]
I don't know how it's stored underneath the hood. I know it's not the same as a set, a set is gonna be much different than an object. Is that a log and delete? I have no idea. And plus generally, from what I've seen, objects just aren't as fast as maps when you're just looking up a bunch of random keys, they seem to be a little bit better.
[00:09:33]
I can't say that's 100% true, but my brief playing around, I just typically reach for maps or sets when it comes to looking up something. Fair, everybody else? I'm pretty sure pretty much every single person casually enjoys using sets or maps pretty regularly, right? Yeah, now, sort of, okay.
[00:09:52]
>> One quick question.
>> Yeah.
>> How do we close the WebSocket after the user closes the browser?
>> I don't, that's a great question. Do I even explicitly call close on them? I don't think I explicitly call close on them. Did I forget to do that? I mean, on the Rust side we call close, which inevitably gets a called closed on the other side.
[00:10:15]
They should get automatically closed down. The test client does a 1000 close on it, so the other side should get closed down. I guess one thing I probably didn't even think about or even try to do was make sure I clean up after the sockets. That's a great point, I don't even think I did that.
[00:10:30]
I'm unsure if I have to clean up after these, I'd have to look into that. I didn't see any memory leak. I've ran this program for quite some time and the memory just. Sat there, so I don't think I have a problem on that, but it is something I should have probably just looked into, I just completely forgot to do that.
[00:10:45]
I'm so used to the Rust way of doing things where when the drop happens for me, I just don't even have to think about it, but we could do that. You could add a p1.off, what is it? Is there is a remove all listeners, right? Boom, and add that, there you go, you'd have that.
[00:11:03]
In fact, at the end of the course we can even try to see, does that actually make a material difference? Is it actually affecting things? We'll do that.
Learn Straight from the Experts Who Shape the Modern Web
- In-depth Courses
- Industry Leading Experts
- Learning Paths
- Live Interactive Workshops