TERMINAL GAME ENGINE! // Code Review

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
this is probably what I have the biggest problem with so we have a vector of a vector it all comes down to one reason memory hey what's up guys my name is the CH and welcome back to the code review series the series in which I take a look at the projects that you send me today we have this Windows console terminal game engine I have never seen a terminal game engine before certainly not from a vet hi there CH I'm 34 years old veterinary medicine doctor I've been working on my own simple game engine which works in Windows console terminal I TR to learn C++ so I've decided on this approach instead of working with open ja libraries that is such a unique approach I've decided on this approach instead of working with openg if you could give me feedback I would really appreciate it I packed the project with two games playable you just need to set the craft shooter game or craft match as startup project to run thanks in advance and this was by the way sent Christmas Christmas last year so this this was my Christmas present from this vet okay here's a link to GitHub read and simple as possible work in Linux and windows let's just have individual studio and take a look at this project so get this I had so much to say about this main file that I actually made a separate video for it I'll have it linked up there check it out I think it's fairly basic and broadly applicable C++ advice so definitely give that a watch if you're interested and now let's get back to the rest of the code so that I don't spend this entire video just talking about one file as I have a tendency to do okay so we actually looks like we actually have three games here craft Mash craft Rogue and craft shooter game and that all of that is part of this thing called console cra C engine so that's good I mean I'm already seeing a bit of a separation here between the engine and the games very important and because this person's actually gone ahead and made three games that reinforces the reason why this is important right because this wouldn't really have worked if it was built into to the the game so it's good to have that separation I expect I haven't looked at it too deeply but I expect that this okay so this actually builds into a dll that's interesting I expected a static Library you know I would always prefer a static Library unless there's a specific reason why you want it to be dynamic mostly because it's easier to deal with it's just just easier to use but then also static libraries allow for more optimization because since the code is all kind of present and apparent during compile time during linkage that is to say it's not loaded dynamically at runtime it means the compiler Linker can actually optimize the code that you're building like using function inlining for example so and then yeah these are these are going to be EXs I noticed by the way that just inside this is a very very short know but inside this solution here we have an actual Visual Studio solution I didn't make this right this was included in the repository as as well as what looks like a cmake lists file so this looks like it like full on supports cmake I'm expecting it does that you know for Linux because this supports Windows and Linux but then for Windows it seems to use a visual studio solution so I would probably not do that I'd probably just use cmake for everything because obviously if you ship it with a solution like this um I mean I guess you could regenerate the solution before you commit it but there's no reason to do that because obviously you can make the solution and the project files from the cmake so I would stick to just cake on all Platforms in this case anyway getting back to this so yeah we have three games let's let's play one of these who look at all those like particles everywhere Ah that's crazy I'm quite interested to see how this how this is put together look at this I love the different colors destruct main scene just press any key to continue I guess it's close that's funny where do I even begin with this code review I just get someone else to do it like AI That's exactly where the sponsor of this video comes in code rabbit AI code rabbit is an AI code reviewer that can help accelerate the process of code reviews leading to faster development and overall code quality improvement there are lots of reasons why code reviews create friction for developers and code rabbit aims to combat that using the power of AI in a natural way and did I mention it's free forever for open source projects simply connect it to GitHub or gitlab configure code rabbit to review your repositories and then you can simply chat to it like you would with a human developer code rabbit integrates into code repositories using GitHub or gitlab web Hooks and monitors events related to pull request and merge request changes you can try it out by starting your 7-Day free trial or you can use it for free for open source projects just go to Cod rabbit. the link will be in the description below huge thank you to Cod rabbit for sponsoring this video and then we finally call start game the engine itself is it looks like it's a uh so it's a class which holds a public vector of scen specifically scene pointers now because the scene gets created in this scope and not deleted inside the scope I'm assuming that the ownership semantics here are anything you put into a scene is now owned by the engine it's now the engine's responsibility to delete it and I'm sure that if we look at clean for example it's going to well it's going to delete the current scene but we have a vector of scenes so what about the rest of them what if I have more than one scene so that's that looks like a memory leak to me it's probably not that bad because again this these scenes technically it looks like they they live for the duration of the application so it's not really a leak in the sense that it's going to accumulate more and more memory or cause any issues really but if we did have multiple scenes here then what happens is we set the current scene which sets Uh current scene equal to whatever pointer we pass in here but then in the clean function we only delete that current scene so in this scenario I have three scenes inside this Vector however I'm only deleting one of them how would we fix that well you'd have to Loop through this so you'd go like for uh how would I actually write this probably for scene scene in scenes and then I would call delete scene and I wouldn't bother deleting current scene you know you wouldn't need to cuz this is going to delete all of them and then what I would do is I would call clear on the vector after this the vector is still going to contain three pointers the memory at the pointers has been freed so they're basically invalid kind of pointers so I would clear this just so that we don't like accidentally keep thinking that we have valid scenes inside this list now the reason why I'm doing that as well is that because this is not the destructor if this was the destructor you could say this is not necessary for this to happen because the vector itself is going to be deleted in a minute but uh because that's specifically a function that like maybe we want to clean and then restart a game or something in that case uh you know we would want to treat this as a function that might run at any time not necessarily at the end of engine's lifetime okay so for the start game what do we do we set the current scene to scene zero uh so what happens if there are no scenes what if we didn't push back a scene well this would probably crash at some point because I think yep so we're going to call in knit on uh scene which we'll get to this in a minute um this wouldn't crash by itself actually if this was null uh because C++ is not Java or C and it wouldn't give you a null reference or a null pointer exception uh it would still call the function but if the function um obviously that's not what we want anyway because we should never be trying to initialize a null scene but uh if the function then uses member variables so data basically uh that's supposed to be in this pointer or at this Pointer's address um then obviously that wouldn't work and that would cause the crash so it's like a indirect crash that would probably happen if this was null but calling the function interestingly would will not crash because it's technically okay to do it it will just call this function with this the this keyword basically being null because there's no valid instance now I think if um I might actually be wrong here I think I am wrong because in this case though generally I'd be right but I'm wrong here I think because this class has a virtual init function and in fact it's just a virtual class in general so to call init it actually does have to look at the V table now this is null so there is no V table so we can't jump to the right init function so I'm pretty sure it would crash at the function call so that's just something to be aware of what happens if this is not the right State this is something that we generally call defensive programming where just putting in extra code to make sure that stuff is going correctly and if it's not you have the capability to recover if you so choose now in this case it doesn't really make sense for us to start up the game with no scene if we're trying to end it a scene that doesn't exist or if we're trying to you know Set current scene here and that it doesn't exist then it probably doesn't make sense for us to run anything because if there are no scenes in this engine in the game then we don't we don't really need to do anything so I you know you could put in an assert so if you have some kind of assert framework or just a normal C++ assert if you must uh then you can do something like I'm just going to make sure that the size is greater than zero here uh and in that case it would you know it would still run this code but it would bring up an assert for you to be like that's an invalid State alternatively you could obviously check that so instead of doing an assert you could just be like if you know SC size for example equals z or in this case you could just run uh empty as well you could also check for not empty by the way then you could just return or and maybe print something to the console to be like there are no scenes inside your game just to maybe give some feedback to the person using this engine as to why nothing's happened because they might be like why is my game not running uh and turns out they just forgot to out of scene so these are the kind of things to think about I think when you're writing functions that use data specifically so start game doesn't really have any almost prerequisites but when you call a function with a parameter and when it accesses member uh variables so current scene is a member variable which again it's not immediately apparent which is why I love to use the mcore convention because now I know that okay this is a member variable and this is a local variable inside this Scope when you're writing a function always consider the inputs and the outputs no outputs here because it's just void doesn't return anything but there is an input it's seen so what are the what are the appropriate or acceptable States for that variable to be in like is it allowed to be null for example if it's not probably should do something here now in Hazel uh which is a game engine that I'm making for those of you who are unaware we have asserts we have verifies we have macros for this kind of thing so we would probably have something like HZ core uh verify a verify is like an assert but it also runs in release builds doesn't run in distribution builds but it does run in release builds and then we could basically just Chuck in some code like this and you again you can do this with a normal C++ assert I like to make my own asserts because you can log it however you like you can display message boxes if you want in certain cases you can turn them off in in certain builds really easily like if you want it just in debug or justtin release so that's why I always advise doing something like this if you have a decent library of code like an engine it's very very helpful but regardless it's very easy for me to just Chuck in some code like this just to validate the state of this because this is not allowed to be null so it's not even a matter of I want to recover from that state it's more of like a we should not have gotten to this point so if that's the case I want to be aware of this like during development and there's no reason to basically not have something like this or at the very least an assert which will only run in debug builds because you know you switch that to release build and it's gone that code gets stripped so it's like we're not checking this whereas if you had something like if not seen then you know error message or whatever which is the same as checking for um null point to be very careful that you don't do this um but you know for Jack and FAL pointer then if we had this instead then This would run at all times so we'd have to either manually strip that out or wrap it in in macros which is basically what that assert or verify is doing so this by the way is not a function I should have mentioned this this is a macro so that way we can turn it off easily for certain build configurations and that's just generally even if you choose not to explicitly do a check like this and do some validation I would still at least be aware of it that's kind of my point that I'm getting to the fact that when you do have stuff like this just be aware of what would happen if this was a different you know value to what I expected okay then we have uh so we do high resolution clock now for some timing we have a start time a start time and a previous time which we update so we basically time how long this takes and we set our previous time to our end time and then we begin again and we use this why to calculate the difference between end time and previous time which gives us our Delta time in seconds and then we multiply by a th000 to get into milliseconds so this is just used for timing in the game very simple is just so that we know how much time has passed between Loops so that we can probably in this case just move the appropriate amount it's interesting that this is happening to be completely fair in a console in like a terminal engine because I don't know what the refresh rate of terminals is uh which means that we probably can't render more frames than the terminal necessarily allows although of course I imagine that would be platform and terminal specific uh so I'm then imagining that instead of rendering more frames you know it might be used for timing I mean let's let's kind of drill down into here and we'll see what it's used for okay so yeah it's used for movement yeah because I guess that makes sense it's just it might not render but we still want to move it the right amount so that if it does have time or if it does want to render more frames it's got the ability to do that and then inside this main Loop yeah pretty simple it's pretty much the same as any game engine like just a normal graphical one with a window as well update input so that goes into a static class over here we got some stuff depending on platform so windows and uh Linux over here as well that's fine and then we can also add listener functions so this is a function we can add so that we can go through and okay so this is interesting this is like an event system basically where we can provide a custom Lambda custom function that we want to be notified whenever a key is pressed so if we had like a game class that wanted to respond to key inputs we could just tell the input system and what looks like like a static way to just basically hey ping me when a key is pressed and that's how we can handle it so that's nice it's basically an event system is just uh it's reduced to be input only I think I don't know if there is actually yeah there is event and there's an event dispatch up so wondering why that doesn't happen and yeah same situation add listener but there's a specific one for input specifically which is interesting I think I would probably try and combine the two just because I mean input events are are events as well so here in the entry we create a new shooter scene the it's clear that scenes here are the building blocks of our engine if we take a look at entry point uh which contains the engine you can see that really the only data that we have to work with here so if we actually collapse all this uh the only data that we have to by the way there is actually a shortcut for that called control M Mo I say as I manually collapse everything that's funny the only data that we have to work with here is scenes so inside a scene what do we have then because the engine has scenes what does the scene have the scene has game objects in the form of of uh a vector a q and a map The Three Amigos The Three Brothers The Three Musketeers Vector q and map so we got a capital G we got a lowercase G like I don't know what's going on with the naming conventions here but either way we have a vector of game objects which I can only take to assume that these are the ones that are in the scene currently these are the ones that are to be in the scene so game objects to spawn and then this is a name to game object map it begs the question of why does this exist if you have a map it is true that it is faster or probably will be yes it will be faster to iterate through a vector compared to like this tree that you have here so that might be valid and you just have pointers anyway so you're not really duplicating you're not really own there's no ownership going on here I'm assuming the vector is in charge of owning it and when you want to delete a a game object you delete it from Vector it's a little bit annoying though that you have to manage two different data structures so especially for what I can imagine is is a terminal based game I'm not sure about performance there but let's just say I feel like we have a lot of performance to uh use up maybe because it's not running that fast and there's no 3D CPU side management that we have to do if we had like a normal 3D Graphics game maybe with physics and with all this other stuff so what I would say is I'd probably cut this out I just have all the game objects here that way you have a central place where they're all owned now if we take a look at how game objects are made though it looks like a game object itself has a transform has a name and has a vector vector of integers which is its Sprite so that I'm assuming is the actual like we had like a Sprite and we have components as well so this is a component based architecture ah this is super annoying as well the fact that some of these are just in the middle of the file some of these members are here like you got to keep your members grouped so if you if you want these to be public cuz you can see they're under public I would still 100% put them at the end but just make another little public section and then put them here this is optional obviously because this is already public but uh it it just as a divider and it's it obviously does nothing so I I do that usually this is fairly standard aside from the fact that the component is you know just a vector so it's not really quite an ECS but again I'm sure it's more than fine for this kind of terminal game engine by the way if there's anyone out there who's like making a super high performance like terminal game engine and who's like no no no we need the absolute fastest technology let me know because maybe I I'm just I I keep saying that you don't need that much performance for a terminal game engine but maybe I'm wrong this is probably what I have the biggest problem with so we have a vector of a vector that is a Sprite and you can see that by default it's set to this 1111 kind of sprite so what I'm imagining is this is basically a Sprite that looks like this so we have four ones most likely arranged in this way and the reason why we have a vector of vectors is because one of them will represent basically like the columns and the other one will represent the rows so these uh Sprite elements in blue are stored basically inside this outer Vector so same thing for these ones they're stored inside this outer one so this uh so to color code this better this is the blue one and this is the green one so the green one has two elements and you can see the blue one also has two elements each so why is this bad this is seemingly a good way to store multi-dimensional in this case two dimensional data the reason why is because by creating two vectors like this inside each other you've created like an array of arrays and generally speaking arrays of arrays are Bad News Bears it'll comes down to one reason memory when you do something like this as you can probably tell from this graphic maybe the vectors inside have their own Heap allocated memory there's one green Vector which holds two different vectors in it and there's one that's over here which holds the two integers so one and one over here and then there's another one let's just put it over here which holds another two integers so that's the problem if you look at this what have we created we've created one two three different Heap memory allocations in random spots in memory so whenever we have to Loop through this Sprite Which I'm assuming is a lot because it's a Sprite so we have to render it every frame and possibly transform it or move it around as well whenever we have to do that we have to look up all these different objects in memory so if we take a look at where this is used uh as an example we have this function over here called rotate Sprite the input here is all the integers of the right in that kind of multi-dimensional array uh and then the output is also the same thing and so what we have to do is we have to Loop through the two Dimensions like this and then apply them back into this after we've done the transformation that is technically a memory nightmare that is very wasteful because we could do exactly the same thing with just a single Dimension array so what I'm talking about is you don't actually need to have two of these you can just have one of them and then if you're aware of the width of each row then you can just basically be like well every two columns every two indices I'm going to jump a row down so basically what I'm saying is instead of having two vectors with two integers each we could have one vector with four integers and so what that means is we just have a single contiguous buffer like this with four elements in it rather than one buffer over here and one buffer over here and also one buffer that stores the two buffers so this is actually quite nice it literally will reduce us from having three buffers in memory to one but what I would say as well is that if you want to go even a step further what you can do is you can think about fixed width Sprites so fixed width I I really mean kind of fixed size you could if you wanted to you could Define a maximum size for Sprites and then just have this be either pre-allocated ahead of time as like some scratch buffer or some persistant buffer it could be something that's allocated somewhere within the renderer that just holds that you could also treat this as a stack thing so if you wanted to you could have an array of integers and let's just say your max size for sprite was nine you could always pass that around I probably wouldn't recommend that necessarily over just having a pool of memory where you can grab these allocations from so what I'm talking about specifically is we have a function like draw objects where we obviously have to go through everything and you can see we're accessing that Sprite Vector in in those two Dimensions we're going through like the X and the Y we're accessing those two vectors for every like pixel every every integer inside that vector across all of the Sprites right so this will go through I mean this looks like this will draw a specific object I don't like that it's called Draw objects it should be called Draw object because we're passing in a specific object for it to draw but outside of this you can see we're going to go through every renderable game object which has moved by the way that's that's cool we're not drawing objects again that haven't moved that's nice and we clear the old one so we can kind of R so it's almost like a retain mode UI on it's retaining uh it's not changing pixels well not really pixels but uh coordinates or characters within the console that haven't changed that's actually pretty cool if it has moved and if it is renderable then we're going to go ahead and draw that object so we're basically like what we're doing really is we're iter we're trying to iterate through every single game object's Sprite so we might have like 10 game objects in the scene each one of them has a Sprite we need to iterate through every single Sprites integer that is inside these vectors for every Sprite in our entire game world so this is where my brain is now being like okay how do we optimize that memory access well what if Sprites were allocated from just one big huge contiguous buffer and every time you ask for a new Sprite it's like how big do you want the Sprite okay a 2X two Sprite sure here are four integers for you to use and then I'll have the next one 3x3 sure here's nine integers for you to use and I'll obviously Advance this and what's the advantage there the fact that this is going to be hot on the CPU cache and when we do want to render it when we do iterate through the whole thing because we need to go and actually render those characters on the screen we have it all in a contigous buffer so see this is like the the Step Up From what I was saying instead of just simply having a one-dimensional uh array like this which is better than the two-dimensional version we can have a one-dimensional array for every Sprite so instead of having a vector of integers for every game object to represent a Sprites you just have a single one for your whole game for your whole engine that represents all of them so that's really the next step up and that's definitely something I consider based on the common use case which here like we know we're going to render every single Sprite we have to so therefore let's think about how we can optimize the memory access there all right I hope you guys did enjoy this video and you learned something if you did enjoy it please don't forget to hit the like button if you want me to review your code send it in ch review gmail.com there will be the email address in the description below along with some instructions on what to include if you want to do a bit of your own code reviewing using the power of AI then definitely check out code rabbit using my link in the description below and I will see you guys next time goodbye oh
Info
Channel: The Cherno
Views: 43,088
Rating: undefined out of 5
Keywords: thecherno, thechernoproject, cherno, c++, programming, gamedev, game development, learn c++, c++ tutorial, game engine, how to make a game engine, terminal game engine, terminal, console, code review, code review series
Id: B6pM9KcIFE4
Channel Id: undefined
Length: 23min 59sec (1439 seconds)
Published: Fri Apr 19 2024
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.