How better to learn about the capabilities of a technology than to do something it was expressly not designed for? What can we learn about the square peg as we mercilessly shove it into the triangular hole? In an attempt to rebuild Age of Empires 2 using React we'll learn about the limitations and possibilities of the tool we use everyday.
Building Age of Empires 2 in React
Transcription
♪♪ Hello and welcome to recreating Age of Empires II in React or the other subtitle. Why do I keep doing this to myself? If you're wondering who myself is, hello, hi, I'm Joe. I'm a web engineer consultant. I've previously worked for places like the BBC, Monzo, Lego, doing all manner of React and front-end things for people. Nowadays, I kind of shop my skills around for anything view-related, React-related, or general web stuff. And as well as doing web engineering, I also do many, many silly things in my spare time, including stand-up comedy. I also make silly video games and indeed combine them where I do stand-up comedy where we play silly video games as well. If you want to see more details about that, you can check out my Twitter, at Joe Hart, or you can go to my lovely homepage, joehart.co.uk. I spend a lot of time styling it. Please let me know what you think of those styles when you have a look at it. And if you ever want to email me for any reason to tell me how much you didn't like this talk, please drop me an email at joe at joehart.dev. Now, to kind of set the context of where this talk is going, I thought it would be good to talk about an example of the kind of things I've done in the past. One of my favorite games I ever made for a developer event called Smoosh back in the day was called Katamari Node Modules, which is a version of Katamari Damacy, which if you've played that, you'll know where this is going, where it's a little video game where you play a little node modules folder and you roll around the world, and as you do, as you hit into things, things stick onto you, and the aim of the game is to get as large as possible. So to roll around this little world where all the objects in it represent NPM packages of various sizes, you want to get the largest NPM packages as possible and just bloat yourself as much as you can, much like our real Node Modules folders do on our dev machines every single day. So that's to kind of set the context a little bit. And then to kind of explain how I got here and talking about Age of Empires 2, like all good talks, this started with a random tweak because I was a little bored at work, where we were talking about graphing libraries, and I referenced in the meeting this Age of Empires 2 graph, which happens at the end of the game, which kind of renders all this stuff for you, which I think is really beautiful about how it splits up these things and it shows you when certain events happened in the game. And I played, joked around on Twitter for a few people, and then eventually it was Lorenzo who suggested, why don't you submit a CFP to React Advanced for this? And I did, and now we're here. Now, when this first started, I guess I need to add a little bit of context for what is Age of Empires 2, for those of you who may not know what it is. It was a real-time strategy video game. It was released in 1999, I think. I played it a lot when I was a kid, and it was often played on one of these, a giant CRT monitor that went, boing, when you played it. And it was a game where you would assume the command of one of culture's greatest armies, and you would evolve them from the Stone Age up to the Castle Age and the Medieval Age and get more technology, and you would mine resources and build your base out and choose your units and your armies, very carefully compose them, and then go and fight the enemy. But you'd really just spam knights. That would be mostly what you would do, and it would work incredibly effectively. So I started to think, what could I do in the context of Age of Empires 2 and React? What would be an interesting talk to come and put together about React technology and Age of Empires 2? And there are many different ways you could go about it. Age of Empires 2 has relatively famous menus in it. There are these gorgeous skeuomorphic designs. How would it be to try and recreate that and React? And there's lots of really interesting stuff here, like just getting the fonts to render exactly like that, because that's just Georgia. But on the model website, it's a little bit tricky. You have to turn off any kind of font smoothing that's going on in the browser. And even the main logo of Age of Empires, it uses a particularly old version of the Castellan font, which is from 1997 and is really difficult to get a hold of now. I thought maybe I could go and create a jokey framework for creating Age of Empires 2 components, which would be like this oil-oil CSS, and I could build an amazing medieval website generator where people would spend all their time learning my own custom design token system rather than just learning CSS. But I thought I wouldn't do that. I thought that would be not as fun. Instead, I started thinking about, what if I could try and make Age of Empires 2? Or how much of Age of Empires 2 could I make using React? Like, recreate this game in React as much as possible. How hard would it be in general to recreate Age of Empires 2? And if you ever do get the inkling to, I'm going to go and remake one of the greatest strategy video games of all time, here are some tips of good ways of doing it. These would be good decisions to make. You could use a video game engine. This has always grown up me doing video game stuff because they have loads of helper functions and rendering things. They have solved a lot of those common video game problems time and time again, and that will just make it so much easier. Use a typed language because it will just give you a lot more predictability about things that are updating, all your kind of event cycles. You'll be able to build out quite a sizable amount of your app kind of automatically by just making sure that things redline in the correct places. And then if you are going to develop a video game, you do ideally want to be using a kind of like rasterized style rendering method. So if you are going to do it in the web, try and use something like HTML5 Canvas where you can draw actual sprites and images straight onto it. Or even if that's slightly too low for you, use something like 3GS where you could do a pseudo 3D thing. Or even possibly React 3 Fiber would be a great thing for hooking into React state management things. So those would all be really good ways of approaching this project. I decided to instead start entirely from scratch, use only JavaScript and React, and render everything using DOM elements. And the main reason why I decided to do this is because fundamentally I just wanted to make sure that this project was as painful as possible for me, mainly because I think it's more entertaining for you. And I just think you can learn a lot about a tool if you use it in the wrong ways. And that's kind of what we're going to try and do today. So you've gone to your project manager, you've talked about rebuilding Age of Empires 2 and React, you've got approval, you now need to crack out the JIRA and try and figure out what is an Age of Empires 2 MVP. What's the minimum viable Age of Empires that we can make? And for me, I think it's going to be, we want to be able to render this isometric background, this battlefield that's going on. You can't quite see necessarily this isometric here, but there is basically a grid of isometric tiles that all the units can exist on. You want to be able to scroll around the battlefield. So in the game, when you would move your cursor to the top left, it would move to the top left. When you move to the top right, it would move to the top right. You want to be able to click on a unit, a building, in a fairly generic way so we can select things around the battlefield. And then we want to be able to right-click on the ground to move the unit. I think if we can start with that, we can then be able to build out and build the rest of Age of Empires. So rendering an isometric battlefield, this is actually surprisingly easy using CSS. So our render method is very simple. We're just going to have a div, which we'll give a class of grids to, and then I ended up creating two arrays of indices so that we can just map over them. One for the rows, where we add a nested row on each of them, and columns we're mapping over and rendering tiles. Also, I should probably point out now, this is deeply not accessible. I thought trying to make an accessible DOM-based Age of Empires 2 would be slightly more painful than I was quite up for this time. And so our grid, we're just going to make it relative. We're going to pop it at the top left of the page and center it a little bit more. Each row is just going to be a flex row. We're going to make sure there's no wrap on it because we had a weird bug where they were like, if you accidentally shrunk the screen too much, it would pop background, you end up with this jaggedy grid. And we just used some very simple CSS variables to make sure that each block has the correct size and the correct grid. Now, if you're looking at these two pieces of code and you're going, Joe, that just renders a grid, not an isometric one, you would be right. That bit of code that I just showed you would render this kind of thing on the right-hand side. It's just a little grid like that. But then using just a lovely little bit of transform, where we're going to rotate everything in the X and then in the third dimension, doodly-doodly-doo, we're going to rotate the Z a little bit, we end up with this lovely isometric grid, all done with just a few lines of CSS and a render method. Now, you're going to look at that and go, Joe, that doesn't look anything like Age of Empires 2. And I'll be like, you are correct. This is a screenshot from Age of Empires 2 and look at this lovely kind of mushy grass that this unit is standing on. So we want to be able to go in and get those textures. Now I could just go and Google textures for Age of Empires 2, weirdly, they were quite hard to find. So instead, I ended up going and downloading this wonderful modding piece of software called Turtle Pack, which allows you to extract the SLP and DRS files, which were like particular files. I don't know if they were particular to that exact game, but that stored all of this data inside. So I went to this lovely forum, which by the way, I just want to say how much joy it gave me to see a website that was talking about disabling download accelerators and download managers, which has made me very nostalgic for a sense of time. So I managed to download this wonderful little tool and I booted up my CD. And you can see that inside each of these SLPs are these kind of almost like GIF-style storage of these tiles, where it's like a single file, but all these different ones that would be like an animation. But I managed to export one of them as a bitmap. There we go. Boom. We've got our grass tile. There's a whole six pixels there. This is amazing. Now, the problem is we've done that transform on our grid. So if we put that as a background image, it's going to skew that again, which is too far. So quick pop into Photoshop, straighten it up a little bit, which makes it look kind of significantly more muddy. And then we can just set the background image on all of our tiles to boom. We've got our lovely isometric grid. Look how quickly we have smashed through our first requirement. This is going really well. Now, we want to be able to scroll around this battlefield as well. This is going to get a little bit more tricky than our previous one. Now, thankfully, React makes it very, very easy to hook into mouse move events. So we can go on mouse move, have a little handler, and the handler is just going to store the mouse X and the mouse Y in some React state so that we can effectively use that basically however we want. And we're going to use a ref and connect it to that div so that we can pull out properties about that div, about how tall it is, and things like that. And then we're going to write this function. And I say we, I mean I did this, where basically we want to do the logic of when the mouse moves, is it within 50px of the edge of the container? Then scroll a bit in that direction if it is. And the way we're going to do this is there's this scroll method on HTML elements, but the scroll method isn't go and scroll a little bit, it's scroll to this exact point. Kind of because that's what web documents want, right? You want to be able to scroll to a place. You want to scroll to an ATAG or things like that. So we have to do a little bit of maths to calculate where we want the left and the top scroll to be. Where it's pretty easy, where we go and get the width and the height of the current container. Scroll margin is kind of like using it as the distance of how far near the edge of my screen do I want to start scrolling. And then scroll amount is essentially the speed that we're going to scroll. We're going to get the current scroll position, that's left scroll and top scroll. And then we just do, if the mouse is further along than the width of the container minus the margin, so width of the container minus the margin, just there, then add a bit of scroll to the left. And if it's the other way, take a little bit off the left, add a bit to the top, minus off the top. And then we can take this useCallback, which I've wrapped. I've wrapped this method in useCallback, saying that it's dependent on the mouseX and mouseY state that we defined further up. And now we can put that inside a useEffect, and we've got this lovely little kind of game loop thing happening here. This is kind of where I, this was really my idea around I want to try playing around with React for video games stuff, because the useEffect loop is very similar to that gameplay loop that game engines or game software has, where you're constantly doing a little bit of logic. And now, when you move your cursor to the edge of the screen, it goes and moves across. We've got ourselves scrolling across the battlefield. This is fantastic, with a lot of console logs as well. Very necessary, very required. So we've smashed through this super quickly. We've got our first set and our second set of requirements done. Clicking on units and building to select them. So first of all, to be able to click on them, we need to be able to render them. So we're going to move our state that we were keeping in just some state hooks into a little bit more of a reducer, because we're going to be doing a lot more. If we're going to have lots of units in the future, we want to be able to access them all in a reducer. So we're going to create a units array, add one unit in there for now, and then move all of our other stuff into a UI object so it's out of the way. And for now, we're just going to add a reducer in that does the updateMousePosition stuff, so the logic we were storing in state before, but we're now storing it in a reducer. Now, adding a unit and rendering it, we can take a very similar approach to what we were doing with rendering the tiles, where we're just going to add this div that exists inside the grid element, and we're going to map over all the units in our state. We're going to give them a div where they'll have a class of unit, which will give them some generic attributes about their width and their height. And then they have whatever particular type it is. He's a Spearman, so he uses the Spearman background. And then we're going to dynamically set the position as tiles because they're going to move later rather than doing it in the CSS. And that means that we get this wonderful rendering. Now, the reason why this is happening is because the texture of the Spearman is a square, and he's upright. But we've obviously done this transform so that it's down a bit. So we need to find a way to essentially flip him back. And the way we're going to do that is essentially un-rotating him a little bit, where we're going to do a negative Z rotation, rotate the X, and then also scale the Y a little bit, and boom, we have our Spearman standing up, and he's ready for action. But scrolling around a battlefield with one unit in the middle isn't particularly exciting, so how do we make it interactable? Now, this is really where React shines because on-click handlers are bread and butter. So very easily, for every single unit that we're going to render, we're going to give it a handle unit click, where we're going to pass in the unit ID. This handle unit click is actually going to generate a method for each of them, so it's going to create a method that will be called when it's ready, where it will just dispatch an action to select the entity, the SelectEntity action. It's just going to set a selected ID of whatever the current thing that I've clicked on is, and we've just added a little bit of initial state here for null, which should be fine. And now, when we click on him, and with a little bit of extra CSS magic and a little bit of finesse, we're going to click on him and click off. Click on, click off. Who wouldn't play that for several hours? Isn't that fantastic? Now, if we want to be able to right-click to move around, there's a very similar thing, we're just going to have a right-click handler, which this is what that onContextMenu method does. This is essentially your onRightClick method, if it had another name. And we're going to do the same thing we did for the unit, where for every single tile, we're going to give it a handler, where we're going to handle the right-click, we're going to generate a new method from the X and the Y, and dispatch this method. This is a little bit more complicated than the other one, but that's because we want to go and find the currently selected unit, and then basically tell it to move by taking the position from this new one we've just made. We've clicked on a tile, we want to move there. And then we're going to go through the units, update it, and reset the state. So now, click, boom, move. Oh, that's a bit fast. That seems unfair, and doesn't seem too realistic for the medieval era to quite have teleportation yet. But because we're moving it just using CSS variables, we can set transition to just linear, and suddenly we've got this moving unit. Isn't that fantastic? Oh, actually. Well, now the problem with this is it is moving smoothly, but irrelevant of how far he moves, it's going to take one second, which is slightly too long. Which means that if he moves far away, it's super fast, and nearby, super slow. So how could we change this? Well, we could dynamically set that transition time distance based on how far he moved. So now, if you click very, very far away, the distance will be quite long. If you click very short, it will be quite a shorter time. So here's an example of that working, where now he's moving super far, and it's kind of slow, and he moves a little bit, and it's much faster. Much more consistent amount of time. But then, if you start to click around a bit quicker, it's not really working too much, and suddenly this isn't quite exactly what we want. You see, clicking over here, you're clicking on one of them, and it's setting the position. You click on the next place, it speeds up the animation, but the unit hasn't actually moved that far. And also, actually, if we go back to that scrolling that was happening, it's actually only scrolling when you're moving the mouse. So if you move the mouse to the edge of the frame, and you don't actually wiggle it at all, it won't keep scrolling for you. This isn't really the behavior you want. And the reason why this is happening is because that idea I had around using that use effect as a game loop isn't really working, because the use effect is only running when you actually move your mouse. And it would be much more like a game if it was running every single time, where a main loop in a game is usually handle a bunch of input, render a bunch of items, and then recalls itself, and it just keeps going and going and going until you exit. That's kind of fundamentally what a game loop is. So, this leads us to the horrible question that most of us normally ask the opposite of, how can we make React render more? Now, I have decided to reveal many of my dark secrets where we can go and read our React book of dark arts to find all the various ways of making extra renders. We can use a setTimeout to trigger a render by modifying a variable that then triggers our use effect. We can update a state variable amount. I'm sure many of you have done this accidentally as bugs in your own code, where you have a use effect that runs on mount, but inside that use effect, you also change some variable that's also in the dependency tree, and it just runs forever, although that kind of causes more stack overflow errors than fun rendering things. Or we could do a call, call the actual callback inside the use effect itself. So, you do that initial use effect, and then we call the callback in there. But the trouble with these things is these are ways of making it render more, but not necessarily making it render more consistently, which we would want for more kind of animation things. So, for this, we could use the wonderful exclusive React API, window.requestAnimationFrame. I'm joking, it is of course not a React API. It is a browser API. It's one that helps with a lot of animation-based things, but we can hook it into React here, and I decided to nick this lovely little hook from CSS Tricks, where it's useAnimationFrame, and it basically allows you to pass in a callback, which will then be called by requestAnimationFrame. Creating this very smooth way of rendering things where you are able to do it time-based rather than only on events that are happening. This is much, much closer to that game loop that I was talking about. Now, you can just have this one animate function where you're given time, you can do a bunch of logic, you can move the mouse if it's in the margin, you can move the units, you can update sprite animations, you can walla-la-low your way for freedom. And really, it's at this point that you probably should realize that this isn't really about rebuilding Age of Empires 2 and React. The reason why I kind of wanted to talk about this kind of stuff, the kind of thing that you learn as you start to play around the edges of what React can do and what React can't do, is the real question to ask is, what is React good at, and what other tools do we have? I think it's quite easy for us, whenever we're looking at React things, to go, must be in the React API, there must be a React solution for this. But we have to remember that as React developers, we have a large amount of tools in our toolkit. We can build things in HTML5, we can build things in just CSS or Skulls, for some reason, because I couldn't think of a fourth one. Maybe the Skulls is testing, I don't know. But I think it's the things you can learn from making something do terrible things can be quite useful. I hope that you've enjoyed my painful experience of attempting to rebuild Age of Empires 2 and React, which was awful and terrible, and I highly don't recommend it. But I hope that you can go and find something fun to go and do with React that's completely different from Forms or SPAs or any of those things that React is good at. Go and do something that React is terrible at. I've been Joe Hart. If you have any comments about how this took wing, please let me know on Twitter. Go and check out my website or drop me an email and come and have a chat. It's been an absolute pleasure. Thanks. ♪♪♪