Transcript for:
Unreal Engine Code Review Insights

We're back, but this time with protection. Hey, what's up guys? My name is TH. Welcome back to my code review series. A series in which you guys send me your code and I take a look at it. This time in a VM. So, today we have 23year-old developer. So, this is like more than twice the age of our previous code review. So, I'm expecting twice the quality. I'm a 23-year-old developer who currently has a day job working in Unreal Engine. So, I'm guessing this means like using Unreal Engine or making games with Unreal Engine or whatever else with Unreal Engine rather than working on Unreal Engine. As I was initially learning how to use the engine, I felt my understanding of fundamental C++ was lacking. For some reason, I felt the only reasonable choice to learn C++ better was to make a game engine completely from scratch with as few libraries as possible. Somehow that worked out. Currently, there are only two libraries in use, Glad and STB image. I love this like was to make a game engine completely from scratch and then like there are two libraries though. Like I mean to be fair like Glad is more of like just a loader of OpenGL functions. It's hardly like a library. It just makes the process of loading OpenGL functions from a DLL by name, by string, a lot easier because it just does all of that for you. An STB image, yeah, I mean, for certain image formats like JPEG and PNG, especially if PNG is compressed, it can be a little bit of work to decode those formats on your own. But of course, if you use image formats like BMP, that is pretty trivial. So, you could have done that. But anyway, you know, the rest is low-level C++/ Windows API. I think the most interesting part of this project worth discussing is the architecture. Okay, good. So, we can focus on that. There are a lot of lower level Windows APIs which need to be abstracted. Yes, I haven't been able to make all code completely platform agnostic. Again, day job, but I've hidden as much as possible. The math directory also has some potentially interesting stuff for lower level optimization, something I'm not super comfortable in. So, I'm expecting like SSE stuff probably going on here, but we'll see. Here is the GitHub page. Instructions for installation, as well as a GIF of a man collecting apples are in the readme. Yes, this is a very important feature of a game engine. Hope you have a fun time. Kind regards, Axel. Okay, cool. So, we have Greg engine. What a fantastic name. And yes, and here is the basket man example. Out of all the example games you could have made, Basket Man is what we get. Okay, so pretty good read me. You know, game engine utilizing open gel written in C++ setting up generate pressure files.bat, which will run pre-make, which I'm pretty familiar with, of course. Currently only supports Windows. There is no support system for Mac or Linux. pretty standard as well. Although of course, whilst it would have been a good thing to support, you know, at least something like Linux, the fact that you're doing everything yourself here with no third party libraries makes that a much more challenging task. Now, to quickly touch on this whole concept of making a game engine without any like or with minimal third party libraries, I do think that it's generally not a bad idea to do that for educational purposes. And I say for educational purposes because the problem with making game engines on your own, like the biggest problem, and I've been making my own game engines for like well over a decade for sure. And of course, I've also worked at EA on their game engines. And the biggest takeaway I would say from all of my experience on all of that is just like the sheer amount of resources required to build a game engine. And so when you decide to not use third-party libraries for things that like you know would would not negatively affect your engine or your software whatsoever. So like for example using GLFW for your like windowing and input and all of that stuff. The benefit there is that it includes basically all of the features that you need and if something's missing well just get in there and write some wind32 API code. that's it's open source you know you can do that but then it also gets you that like other platform support like Mac Linux some other ones as well you essentially get that like for free it's like work that has been done for you by not just a single person but essentially by like the open- source community JLFW is an example of a library that has lots and lots of different users right like lots of people use JLFW and so whenever there's like a bug or like something isn't supported or there's an issue those users since it's their kind best interest to fix that issue. They can just like submit pull requests back into that actual repository. And so what you're getting is this product that is tried and tested by a wide variety of different users who have vastly different problems to solve and they're all kind of using this library. You're getting all of that for free. It's such a good deal. You know what else is a good deal? Code Rabbit AI, the sponsor of this video. Guys, if you can't get me to do a code review for you, it's cool. Just get Code Rabbit to do it. Code Rabbit gives your code AIdriven contextual feedback no matter what language you're using. It integrates with GitHub or GitLab and scans your pull requests in real time as they're coming in, highlighting potential bugs and security issues, as well as just giving you general advice to improve your code. Code reviews are super important because we humans tend to miss a lot. So, getting Code Rabbit to just sit there quietly in the background checking over your code as it's coming in and having your back is really a no-brainer. It's also really great if you're learning programming. It'll help catch your mistakes and you can chat to it if you want some more information like an interactive mentor. And the best thing is Code Rabbit is completely free for open-source projects or you can give it a go for your private projects with a 14-day free trial. Just go to code rabbit.ai. Link will be in the description below and give it a go. Thank you to Code Rabbit for sponsoring this video. Okay, so back to Basket Man. Let's have a look at this project as this engine this time in a VM. I have actually disabled the network for this, but otherwise like I don't know. This obviously doesn't have access to my computer, so we should be kind of safe. I've cloned the repository obviously. Let's go ahead and run generate project files.bat, which should probably run programs third party pre-make. So, we have like an included pre-make exe here, which now I've learned to be like suspicious of, but anyway, let's run this. I don't know if it's worth actually passing all the binaries through like a like virus total.com or something. Anyway, whatever. I'm in a VM, so I should be fine. So, let's uh go ahead and open up Greg Engine.solution. And so that obviously generated this solution and I'm kind of interested to I guess we'll try running this first and then we'll have a look at like the architecture. Okay. So first of all I mean we can already start looking at the architecture. We have this example games like directory kind of it's not really a directory but like filter or whatever up here and then we have basket man in there as a separate project to Greg engine. So if we have a look you can see that Greg engine is a static library and then this of course is an actual executable. So that's already really good. I think I spoke about this a while ago when people were sending me engines that had like the editor and the actual game and the runtime all like in one. I still think that if you're like it depends what kind of engine you're making. Some engines are made in a way that like they're more like a framework and so you you have to write like a lot of code. There's no like graphical user interface to use. Others have like a dedicated editor. If you do have a dedicated editor that in my eyes is kind of like it's an application that's based off of the core engine. So it is a separate application and in that sense you would obviously want that to be like another project another executable that's not your runtime. Hazel for example has like the core Hazel engine which is just a static library and then it has hazelnut which is an application and .exe uses that Hazel static library and then Hazel runtime is a separate .exe a separate application that also uses that Hazel core library. And Hazel runtime would in this case kind of be like the basket man project in a way. It's got lots of boiler plate in there already ready to go so that you can just you know make your game and you don't have to worry about how to actually use Hazel to like play your game. But if you want to add additional C++ additional whatever else you can do that obviously within that project. Okay. Anyway, and then we have what we got a bunch of stuff here. So libraries which is just going to be glad. These are all filters anyway. If we press on show all files for this project then you can actually see like the directory structure a bit better. But of course we can just stick to the filters. That's fine. So we got quite a lot of stuff. Gregorian engine. We got quite quite a lot of stuff in here. Like if I just go alt shift O, then you can see we got like 89 files in the solution. All right, so not too bad. We've got a little bit of code here that we can take a look at. And then Basket Man is going to be Let's go back to the filter view. Basket man is going to be an example game. So Basket Man CP is probably the main function. Cool. Okay, so how am I going to start taking a look at this and investigating this? I'm going to start with the main function, which this is like the game, right? Basket man.cpp. This is our game project. I'm going to go from the point of view of like the code that is using the API of the core engine and that's going to be the way that we kind of look through the code. There are lots of different ways that I read code for these code reviews obviously, but in this case since we seem to have like an actual, you know, more or less decent like structure here, we can probably begin following that structure naturally. Speaking of following the structure naturally though, I would like to compile this code and actually run it. So, we're just going to do a debug build over here. Oh, I need to install VS color output. That would be nice. Okay, I think these are just warnings. Most of them. Yeah, they're all warnings, I think, which is fine, but warnings are something that I would definitely pay attention to, especially if you're looking for feedback. Most of these are probably just yeah, conversions from like size to int. So, if we have a look at these like arbitrarily, then yeah, you can see that like components. This is a size t, that's an int. So, when you do like these comparisons and whatever else, they're not strictly correct to do. So, the solution would be to just make this int a size t because that's like the proper type, I guess, to use here. Otherwise, you'd probably have to cast like this into an int, which would be a little bit weird as well. Sorry, this comparison I mentioned this comparison. This comparison is obviously fine to make. It's just typically in a for loop you would not be going through it backwards like this. In fact, that's something that we can talk about. Why are we going through this vector backwards? That's not that's not good. Anyway, we're getting ahead of ourselves. We got 10 errors as well. Okay, I did not expect this. So, back to Cherno fixes your code. Every time that I get a code review submission that actually like compiles and runs correctly out of the box, I think we should like celebrate. I think there should be like literally something that we do because it's that rare. All right. Anyway, so we need to include string. I really would have thought string view would have included string, but I I guess not. I guess not. So that needs to be included. Okay, that was actually the only problem. So for some reason, I guess this particular version of the compiler or this header or whatever else didn't actually include it. Maybe for axle it did actually include it in that version of the compiler or whatever else and that therefore it built. I don't know. All right, let's hit F5 and run this in a VM. Okay, so yes, there is Basket Man uh WD to to Oh, that was like Yo, why is there such a frame drop every time I pick up an apple? I mean, we're in debug, but that shouldn't matter. Look, you can kind of see that every time I pick up an apple. All right, I've seen enough. I've seen enough. Okay, so how do we set this up? So, in the main function, we've got this unique pointer of level, which you can see is actually a reference to Gregorian engine get make level. So, this is interesting because this is actually a reference. So let's have a look at what this is doing. So this returns a reference of make level. So I'm assuming that it stores it somewhere here. So it makes a new level, a new instance of this level class, stores it in the actual Gregorian engine class, and then returns a reference to that over here. I I don't think I've ever seen this before. I'm just trying to think through whether or not that's fine. I don't know if I love this in like a broader context. I think for your purposes, this is probably fine. But the reason why it makes me feel a little bit uneasy is because levels to me are like kind of like assets, right? They're like scenes. So you have like a scene inside your engine and then that is such a complex like context of data, right? Like there's lots of stuff that stem from a scene, lots of different resources, lots of behaviors, lots of like implications that it has like for the rest of the engine. So in a way that's kind of what's happening. We're going into the actual engine codebase, right? Gregorian engine is inside like the actual engine module. We're going into that codebase and we're like taking a reference to level because we obviously want to use it. In my mind, that should kind of be like an owning reference. And the reason why is because we don't want the process of this being unloaded, removed from memory to happen like within the engine itself and then like the client app is kind of stuck without it anymore, right? Because what will happen is when you transition from one level to another level, like I'm assuming you would probably run this function again or something or maybe you'd have like a set current level and it would actually change current level because you have a reference to this specific thing here. That would mean that client side you'd immediately switch over to that other level. So, first of all, you lose whatever you were currently doing with that level, which might include like, I don't know, having an instance of it so that you can save it or so that you can properly unload resources or whatever. That could even happen on a different thread. I mean, we're not even going to talk about other threads because obviously this would break apart if you had many threads, but even with a single thread, you have to be really careful with the order of operations because since you don't actually retain like a copy of that level, you're at the mercy of the internal code of the engine like not deleting it because again, it's a unique pointer. So, there's only like one place, one thing that owns it and that's it. To fix this, I would just have this be a shared pointer and I would like pass ownership onto the client as well. That way, sure, the engine can switch to like some other scene, some other level, but then you still have it if you need to still access data from it if you want to properly unload resources from it, all of that stuff. And it's not even about just retaining that data and not deleting it until all the users of it have actually finished with it, which is what a shared pointer allows you to do. It's not even that. It's also the fact that like you're not this level's not going to get switched from under you without you even knowing you know like if this was a shared pointer that you retrieved like this it would be super clear when you had to like reoptain like the next level or something like that. So yes we are on literally the first line of code but already there are so many like thoughts and so many like implications from the way that this is actually architected. I mean even like a pointer right? So a pointer is a little bit more dangerous in this case than obviously a unique pointer reference. The reason why is because what will happen is if this was just a raw pointer like this, which I guess this player over here as a game object is. The reason why this is a little bit more dangerous I guess than this is because if the level does actually switch this level variable over here that's a reference, it will automatically I guess point in a way. It'll reference the next level or like whatever that level changed to because it's a reference. It's like literally referencing that variable that we have inside here current level. So whatever this is set to, like when you just reassign this to something else, we have that here automatically. It's a reference to it. This is a pointer. And a pointer is its own variable. It's just an integer. It's a memory address. I have a whole video on pointers linked up there. It's not a pointer reference. If it was a reference to a pointer, then yes, it would also change. But since it's not, it's like a copy of that memory address. If something inside the engine changes it to point to something else because you're making a new instance of a new level, then this is still pointing to the old level. And therefore, if you try to access it, presuming that that was actually deleted, that would be like an access violation and you'd probably crash. If you wanted to mix this up a little bit where you had the functionality like it where it was safe like it is with a unique pointer, but you didn't automatically switch to the next level, you could use a weak pointer. And I have a video about weak pointers up there. But anyway, um yeah, just interesting. Like it's fascinating to me sometimes how much I can say about a couple lines of code. But again, it's just because there's there really is a lot of depth to them. Okay, so once we have the player, we can go ahead and start adding components to it. Now, how does adding components work? So, we have a component with a static assert over here that says T has to inherit from component. This is nice. It's good to have some safety usually. Oh, I mean these days, you'd probably use like a concept uh in C++ to actually make that happen. I might make a video about that actually soon because that would probably be useful. But basically, you can just specify that this function has certain requirements like for the template arguments instead of having to use a static assert. But static asserts are also fine. I would I would say the main point of a static assert of course is just for this to not compile if this is false. Right? So in other words, if t if whatever you put in here for add components such as player movement or sprite, if one of those types does not have component as a base, then this assertion will fail. We'll get a compile error and we can fix that up. Okay. And then we forward a bunch of stuff into this make unique constructor. So all we're doing here is we're just constructing a component with whatever arguments, whatever variable arguments we actually had over here. That just allows us to pass in the arguments like this directly into the parameters of this add component function and they will get forwarded to the actual sprite constructor in this case which would obviously take in a game object owner and a string. So there's that string coming in and then the owner is what this is over here. Then we're getting a raw pointer out of that component and we're moving that. Okay, we're moving the unique pointer to there and then we're returning the component pointer. I don't know if I love this whole raw pointer design. Like this this seems like it's very focused around raw pointers. I just worry that like that all that means it's fine. Like it's fine, but all it means is that you you do have to be careful about lifetimes. You do you do have to like realize that essentially you've just got references to component memory and then like it's I mean it's it's kind of fixed in a way, but it also makes sense that it's fixed because these are unique pointers. So these are just like oneoff heap allocations. Yeah, I think it's probably okay from like a semantics and from like a programming point of view. Meaning that I don't know I don't think you'll get any apart from like things being switched out. I mean this is obviously isn't a reference. So it's very different to like what we have up here. It's just the raw pointer of that and that's going to be totally fine until that unique pointer actually dies. But first of all, you don't know when the unique pointer dies. But second of all, it also means that everything has to be its own unique pointer, right? Like every component here is its own little unique pointer, a tiny little fragment of memory somewhere in your heap. This obviously is very very far from like a real entity component system. But I think what at least would be good to do would be instead of just having like a vector of unique pointers of components. You could at least have a single block of memory that contains all of the components of like a particular type. So instead of this, I would have something like here are your sprites. And then obviously you wouldn't store this inside a single game object. You'd store this probably inside like your scene or inside your level class. And then inside your game object, you would need to be like aware of like what components it actually has and obviously what index inside the vector it might potentially reference. You also wouldn't want to store raw pointers to specific memory blocks or like a specific sprite within this vector just because the vector can resize and then your pointer that is referencing one of those elements there would no longer be valid. Technically speaking, in more advanced systems as well, instead of just having a simple index into here, you might also have like a UYU ID or some kind of identifier that might then map like it'll basically give you a level of indirection and that might be important for like various memory organizational things. So, it just detaches like the actual implementation details of where like a block of memory is even within like a vector and it just makes it completely arbitrary and the system under the hood can actually control that. That's typically what like a real, you know, game engine would have inside its entity component system. You don't have to overengineer it maybe to that capacity. But all I'm saying is that this is just such a fragmented system and such a performance intensive part of your engine that I would really uh you know I I would since this is clearly like an educational project and you're you're going as far as not even to use any libraries because you want to learn more about how stuff I guess works. I would spend a lot of time uh going through and trying to figure out a way to actually keep your memory close together and have a system where every component inside every cuz cuz you got like you got game objects being created as unique pointers. So they're all fragmented and then within them they also have pointers to like you know other components that are in random spots inside your heap. So you wind up with this situation where every single piece of hot data very commonly accessed and used data that you need is just like in a random spot in memory and that little like chain of like oh I got to go there, I got to go there, I got to go there to get all my data is just yeah it's very very bad for performance. Okay, anyway let's get past all these components. So we go into run and that's going to run things. I want to have a look at how the window class works cuz that's that obviously supposed to be like raw wind32 API stuff. So we have window here which again is a unique pointer of course and inside window we have a whole bunch of functions and yep here's our wind proc function and then here's a bunch of like handles for both our instance our application instance and our window as well as some kind of yeah open gel context and a device context. So this is just like standard win32 API stuff. So if we go to the CPP file I'm expecting we're going to see a lot a lot of stuff like this. I I'd probably say that like if you guys have never done anything like this before where you haven't actually gone through like you know if Windows especially as a game developer or someone who's interested in game engines I would say Windows is probably your like primary platform you know if you're interested in like a bit of a challenge and also to just like you know learn more about how that actual operating system works. I would go ahead and try and do something like this. I think everyone should probably do this at some point in their lives once you know how this works. You know I would say just use something like GFW for sure. But if you have no idea how any of this works, it's fun to have a look at it because you'll learn a lot. And then also, this is exactly what GLFW does under the hood. You just look at the GLFW source code, find like the Win32 window C, I think it's called. I even know the file name. It's probably not exactly that. I actually I think it is exactly that. And so this can help if you ever need to go in and like either debug something or potentially add a feature that's missing or add something specialized that might not be included inside the actual library. Anyway, I'm not going to go through this in great depth. Uh, it looks pretty normal. This is all just like standard Win32 boilerplate code to set up a window as well as the actual OpenGL like context for it as well. Here we have our standard like win prog call back. So, this is basically our message loop that will happen. So over here when we make the window you can see we actually set that function pointer over here and that just means that this will get called like whenever there are messages to send and then we have to handle that message. So obviously if it's like a close message we'll just destroy the window, shut down the application. If it's you know a resize event then we want to resize the viewport etc. It's not that different actually to like how JFW does it but it will JFW will just do it a little bit cleaner and it will obviously abstract it so that it works on like any operating system any platform without being like specific to Windows as this code here is. Okay, cool. So we've created the window. We set the title resize viewport. This stuff is pretty boring. Running true. So we have window process messages. So that's different to okay right. So this code over here which we manually execute uh every iteration of this while loop that will actually peak the message see if there's any message for us to do. If it's quit it will just return false otherwise it will actually dispatch it and that's what's going to hit that window procedure function that we have over here and actually handle this. And in terms of the game we'll do a clear of the actual screen. We'll update and then we'll draw. Wonder why the clear happens up here. I would have thought this would be something that could actually happen inside like the drawing code or something, but I'm guessing that like this is just like the this happens for every frame no matter what level you've got loaded, but this is level specific code. So, I guess that could also make sense as well. So, current level update will probably go through all of the game objects. I'm really curious why you go through the game objects in reverse, though, because again, that's not cash friendly. I don't understand why you're going through this backwards. Like, I just don't get it. If this is the proper way to go through this, store it this way. But otherwise, like I guess you're trying to draw stuff maybe from back to front or something. I I I honestly don't know why this you're going through this backwards. Maybe I'll see it later. But like I Yeah, I don't understand. Okay, so we have a destroy object function which will Okay, I see. So I mean this is just not worth it though because these are all unique pointers anyway. So what this function is doing is it's basically saying that like if you have a vector, right? All right. So, here's my vector of all of my game objects in my scene, right, in my level. So, what happens is if I decide to remove this one, since this is obviously going to leave a gap here, right, unless it like reallocates the memory or like shifts everything here, it's basically saying, let me do that shift for you. So, what I'm going to do is I'm going to actually take this and I'm going to move it into the spot that used to be taken up by something else. Rewire it, tell it that its index has changed, obviously, so we can still reference it properly. And then I'm going to erase the very last element, which just means that we're going to, I guess, avoid having to fill this gap by moving every element over like that, which isn't a bad idea. But because these are just unique pointers inside game object, I don't think that will be that big of a benefit. I mean, it probably depends on how big that vector is as well. I don't know. That's kind of interesting. I haven't really seen anyone do that manually. I think it would definitely be like if these if these were stored like kind of like this. So like if they were like kind of like stack objectsish, but they were stored like not as simple pointers, but actual like data inside there, then I could see that being probably better than just like having to shift around like all the memory like in a staggered way, especially in a big vector, but with pointers like this. I don't know. But anyway, interesting. Okay. And level update will do physics 2D tick and camera 2D get update. Okay. So a couple interesting things here. So, first of all, you know, we're living in a bit of a static world here. Um, because both of these things are kind of static, but this actually has an instance because we have to call get first to then, you know, get the camera. So, I don't really know why that's a thing. If it's a singleton anyway, we could just have the data be static and uh that would be fine. Kind of like how probably physics 2D is set up. Yeah. So, this is just static. So, why is this why is this class all static and then camera 2D is not? I'm not sure. Either way, I probably wouldn't have either of these things be static. I think they should just be just normal members probably inside your level. I feel like a level should own a physics like world, you know, and a camera as well. Camera should probably just be an entity anyway. It should probably be a game object. You might have multiple cameras as well, which you might want to switch between or have like, you know, split screen or whatever else. But like even with physics 2D, like I think that the physics world is going to be like a bunch of objects that you have in here. So if you had it not be static, first of all, you could like start loading the next level by creating a new instance of it before this one's even done. So you could like preload stuff, but then you could also like not have to manually probably remove all the objects from here when you're done with it because you just drop the whole object and then all the data inside it would disappear. So interesting choice to have this like that in terms of architecture. I uh would not I would yeah just not have these things be static. You have to be really careful, I think, with making things static because I like in a way I feel like for for like someone with not much experience, it's like it makes things easier because it just like you don't have to think about how everything is wired together. But then I think as you get better and as you get more experience and as you start working on more complicated like complex systems, you begin to realize that that wiring things together and giving it some thought, that's actually what makes it simpler in the future. And that like suddenly all these things that are static, it's like making them static in a way makes them independent, you know, and suddenly nothing's tied together. Everything's static. Everything's independent. And so you have to go through and chase everything up. And if everything is wired together correctly and obviously as members and blah blah blah, then you have like you have code that almost writes itself because if you drop that object, well, it will drop the other object and everything below it and everything is kind of a little bit more coherent. So in terms of architecture, yeah, I would definitely give that like some serious thought as to like okay, why are you making this static? Is there a reason for that or are you just being like a little bit lazy in the sense that like you don't want to set it up properly? Now, I will also say that systems like if you're trying to look at this from the point of view of like a system that does processing to some data and like you want that to obviously be static, that makes sense. I mean that doesn't even need to be static. It just could just be like a free floating function or something. However, that's not what's happening here because obviously inside physics 2D, we have this here. We have data here that is static. So that doesn't make any sense. Like this stuff should not be static. And then if you want the actual you want to treat this as like a system, then obviously when you do do tick, for example, when you call the tick function, you would actually pass in like the data context of like what it's actually going to be working with. And then it would either write in place there or maybe output some somewhere else. And you could have this be like kind of like a functional system. But if you want this to actually be like a physics world that has its data, since that obviously relates to the current level, it makes a lot of sense to just have that be, you know, like you'd have like an instance of physics 2D over here as a member of this level. Okay, I I'm going to take a quick look at the rendering. I mean, you also mentioned math stuff, so I'm guessing, wow, you've written a lot of matrix classes over here, and they are just uh what look like normal functions. I was I was expecting like some SSE or something. So yeah, again this is something I would not really recommend. I have done this before even like I think I did like a Flappy Bird series in Java on YouTube or maybe it was even like a I made Flappy Bird in one like 3-hour long video in Java back like 10 years ago or something. I'll have that linked up there for fun. I think in there I did write my own Matrix stuff just because I was trying to kind of kind of make itish from scratch. It was using like LWJGL which uses OpenGL. So I did end up using like that library for the game as well as like used OpenGL for the rendering and everything. But the actual math I didn't use any math libraries. So I just wrote out some matrix code that I would need by hand. That's fine. I think as a learning thing but the problem with this is that math libraries are very very very particular. Obviously you're dealing with mathematics like it has to be precise. It has to be correct. Uh and so you know I would expect to see a lot of unit tests for something like this. I'm not one to mention unit tests that often. I don't think they're that useful for most things inside a game engine. For a math library they're very useful. So, you know, you're not really doing I don't think unit tests here. So, obviously like just for fun, for like as like a learning thing, that's totally fine to do. But for anything serious, you know, I would just use something like GLM, which is a really really great math library. Uh it actually includes SSE support as well if you want to basically do all your math like four times faster. I mean, not all of it, but anyway, the point is that's a a much kind of better library than anything that you would really be able to write yourself. Now, as I mentioned, I want to take a look at the rendering code quickly and then we'll wrap this up. So how does the rendering work? So we have a render target that we draw to. So this is just like an interface. So we don't actually know what this is. But if we go to I don't even know where this. So let's have a look at No. So level draw actually has a target passed into it. So that means over here when we draw Okay, window. So the window is like a window is a render target. Interesting. I would have thought like the window would have a render target rather than be a render target, but I guess that might work as well. And then the com. We'll go through every component and we'll do components draw. Okay. But components, lots of components don't do any drawing. Like what if it's an audio component? I guess it could do debug rendering. But anyway, I'm guessing so the component class over here just has a bunch of virtual functions. You can obviously override them. You don't have to override them. It' be nice if they were like this so that you wouldn't actually um Oh, actually they are. They're over here. I just realized I thought they were like pure virtual for a second, but they're not. So if we have a look at like that sprite component that obviously does some drawing, I expect. So that uh overrides the draw function and then that will do primitive draw. So there's we have a sprite primitive inside the sprite and then that will have yeah like a texture ID and a vertex array object over here which I'm guessing we will create here. Okay. So this is another thing that I would not probably do. Every single sprite here, every single sprite primitive has its own um vertex array which they're all the same, right? I'm expecting. So, the only thing that I could really imagine being different here, obviously the texture, right? You might have a different texture per sprite. That's fine. And that may end up like using different texture coordinates. If it's like inside a sprite sheet, you might have like a sub texture inside there. But otherwise, like you know, the element buffer here, the index buffer, the actual vertices of the sprite, like you're going to have the it's just it's just a quad. you know, you're just going to you're going to have the exact same thing every single time, the exact same shader. You know, even images like I I wouldn't have a sprite primitive actually, you know, like here are the indices for example. I wouldn't have the sprite primitive load the image because that might be an image that you use for like a number of different things. Like this image should really be like some kind of texture maybe that's passed in here. I mean, preferably it would be like just some kind of like asset handle, but if you don't have a system like that, then it would be just like a I don't know, like a a shared pointer of a texture object that would actually have this. Because what this is doing is basically saying that like every single time you want to create a sprite that uses that particular image and like is of this shape, you have to go through the process of creating an entirely new copy of it of everything. And obviously, this stuff you don't really want to do like needlessly. Now, sprite rendering in general, uh, you know, is a topic. I have like this series on my channel from a while ago. It's like the batch rendering series that goes through how to actually render like lots of sprites and stuff. I think the game engine series probably also, you know, the the 2D version of that does something like this. Maybe it's time to do like a little refresh of like fast sprite rendering in Vulcan or something or in OpenGL, I don't know. But anyway, um it is interesting that like it looks like inside the vertex buffer though, we have like this coordinates pointer of floats which we actually get like I don't know from so get window coordinates are from here. Coordinates relative to window like what is this? And they're kind of based on texture width and height as well. So this actually gets put into the vertex buffer. Where are the attributes? Okay, so there's two attributes. So this is going to be the position in 3D I guess which is seems unnecessary. I mean, I guess you can use the zed for like uh ordering. And then we have two floats over here, which I'm guessing is a texture coordinate. And that's how that's put together. Yeah. So, we have like elements and elements is like this little output here, vertex elements. And that gets set to 20, I guess, because there are three vec 3es, which is 12, sorry, four ve 3es, which are 12 floats. And then the other eight are just two floats per texture coordinate, and it's a chord. So, we have four of them. Yeah, these are the texture coordinates and these are the actual but these are also going to be Oh, I see. Okay, I was kind of misreading this a little bit. So, basically it actually sets up the vertices to be dependent on I guess basically like the not just the aspect ratio but also the pixel scale of the image. So, like if you have like a you know, let's just pretend this is like a 32x 64 image or something, then you would actually relative to like the window width and height determine what the vertices would have to be in clipping space. I'm guessing cuz you're dividing it. Yeah. Well, potentially not not in like 1:1 space, but in like 0ero to one space, you would decide how many like units it would take up so that you could actually make something that when you rendered it, it would be proportionate. But again, I wouldn't do this at the level of like a vertex buffer. I would just use a transformation matrix to do that because if you did that you would need exactly one instance of this. One vertex array, one vertex buffer, one index buffer for every single sprite, every single quad you want to render in your entire scene, just one instead of having it like per sprite. So yes, that uh would be a quite a large improvement I think to the rendering. All right, that's pretty much all I've got to say here for today. So, let me know what you guys thought of this particular code base and well, everything I said in this code review. As always, feel free to share your opinions in the comment section below. If you want me to take a look at your code, then send it into chennre [email protected]. That email address and some instructions will be in the description below. I'll see you guys next time. Goodbye.