Thanks for watching! Should we continue looking at this 2000-hour 2D Game Engine? Also don't forget you can try everything Brilliant has to offer-free-for a full 30 days! Visit brilliant.org/TheCherno - the first 200 of you will get 20% off Brilliant’s annual premium subscription!
At 2:23 Cherno mentioned he doesn't know the tool Rene. This is because it is literally me. The person who created this Engine is one of my friends and I asked him to add me as a tool because I gave some recommendations about the UI of the Engine :)
Hello!! Thanks for reviewing my game engine for some reason my previuos commentar got deleted. it looks like im missing a concept about pointers 😅. I did discuss this vector SpriteRepository structure with my teacher (He is a teacher teaching mainly about os's) (Im still in highschool, 2nd grade) and he actually agreed with this structure. It's cool seing different opinions coming together, but i will defenetly value the advice of a professional gameengine dev more. I can't wait to see the part 2! by the way, if you guys got any questions about my game engine feel free to ask here :) 7 month edit: Yo guys, i have finally implemented all the stuff cherno advised, and im happy AF i love the new project structure
Hey, that's amazing on several fronts - that you accepted Cherno's critique as a grown man (and even fixed it according to his suggestions) and also that you're so young and yet managed to write an engine!
Yeah I really enjoy these kinds of videos, learning about how game engines work and are usually structured, and especially WHY it's done this way. It feels like this developer was trying to reinvent the wheel, but to be fair, how do you know you're reinventing the wheel if you don't even know how a wheel looks like and how it works? I started my own game engine a while ago, following Yan's amazing Game Engine series (except I was doing it in a language called Jai). I managed to render simple images, but I stopped following the series when it got to the ECS stuff, as I didn't want to have that in my engine. I just wanted to continue working on the rendering, but most of the stuff was being off-screened, so I really felt stuck when trying to actually create the structure for the rendering system. It's soooo complex, there's just sooooo much to think about. In various projects I've tried to explore there's RenderingContexts, RenderingConfigurations, all sorts of Buffers and Passes, rendering on separate threads, making all of this mess API-agnostic... I just gave up. I would love a series of videos explaining how all of that stuff works, explained with simple text and drawings just like in this video.
Instead of UUIDs for referencing entities during runtime you can also use generational indices. Keeps the index simple and small and takes care of validity.
@@HairyPixels Extremely slow to generate? What are you on an Amiga? Also, you don't need to generate UUIDs every single frame. You generate them only when you create the instances. It doesn't have to be generate UUIDs from a cryptographically secure random number generator either.
In my opinion, this was one of your best code reviews, and you didn't even open the project yet. The way you explained everything visually was very cool and extremely informative. I would love to see a part two!
As someone who isn't a game dev but is peripherally interested, I really liked the conceptual overviews, thanks. This kind of thing is still relevant to other areas of system design.
12:40 Can releate haha. At first I used to avoid dynamic memory management, then I started to embrace pointers after I got the hang of them and considered smart pointers 'pointless', that is after attaching address sanitizer and spending approximately an hour trying to fix memory leaks and memory corruption on a small sized project and later realized how important ownership is and how raw pointers laked ownership.
Please make another video about this engine. I'm sure a deeper look into the engine would reveal many more memory management related issues for you to get stuck on for half an hour.
I rarely comment on videos. I really liked this video and would love to see more of this engine reviewed. The drawing really helped me understand the concepts. My initial impression is, that a bit more spacing and logically connected structuring would really boost my understanding and make it easier to follow. e.g. stacked in a way that matches the data flow top to bottom. like Scene -> Sprite Objects -> Sprites -> How the memory is referenced / handled. I probably missed a layer in this example. Writing that, a summary in such a structure at the end would've probably really helped me recap and form a better understanding of what concepts you just pointed out. You partly or may have done that in the part about Scenes. Long comment, really loved the video. Hope you can take something from my feedback. :-)
Yep you are right! I should have done it like this, i actually do have a scene system but its kinda wierd lol. It just loads the contents from for example scene 2 in memory and deletes the content from Scene 1. I should have wrapped that content that gets loaded into a scene object, like you did it in your comment also managing scenes (refreshing/loading/deleting) etc is much easier with this way
This project really seems to have some meat to it so i love that you can really talk about stuff. Would love to see that disected even more in other videos.
I like the code review without even looking at code directly xd It was very interesting to hear this UUID idea. I also was writing some small projects, and came up with the idea of storing the integer instead of the ptr myself, but it always felt awkward for me, and kind of unpreffessional, idk. thanks for the great video
31:31 Yes... Yan. This is a beautiful format. Can't wait for future reviews. Also, the PacMan Game you were going to build yourself. Would really appreciate it, if you can manage to take some time.
You should do a follow up on this. There was still plenty for people to learn from here. But the kid states at the very start of the documentation that it may be outdated. It would be cool to have a look at the actual code and engine and offer some advice on that.
25:45 Having a specific 64-bit hash is 1/2^64 (1 in 18 quintillion). Even if you got 100 000 sprites, you got 1 in 3.7 billion chance any of them collided. e^(-n(n-1)/2^64) where n is the number of sprites and e is Euler's number
Cherno, with the video content you create, you are very inspiring for people like me who want to become a game engine developer in the future. Thank you for that, keep that great work up!
I once stored current config in a pointer of the class but quickly found out that resizing the vector invalidated that pointer immediately like you mentioned may happen. So I then stored just the index and used getter function by ID or string name. I am surprised the code review program isn't constantly crashing because of similar.
I'm more of a beginner in C++, but I won't make pointers for the sake of having a pointer. From what I learned, it could lead to unsafe memory handling. If I have to pass something to a function, I'll pass it by reference instead. If I need to make something on the heap, I'll use a smart pointer instead. I made a command line scientific calculator at one point and used std::function to create a function map instead of using raw function pointers.
Pointers to functions are perfectly safe. Real bugs start happening when you have a pointer to a variable that no longer exists. Like, if you spawn a character in a game, refer to it using a pointer, then destroy the character. Now you have a pointer that no longer refers to the character it once referred to (because you deleted it). If you ever made a game in Unity and you've coded something in C#, this situation will result in the quite common "null-reference" error. This is because you are trying to use a pointer that refers to something that no longer exists. This problem never occurs if you use a pointer that refers to something that always exists (like a function) or the pointer is on the stack and it refers to another variable previously on the stack. Just ensure that the thing your pointer refers to still exists and doesn't move around. This is quite a simple rule to keep in mind, and it's generally only broken when there's something fundamentally wrong with the logic/structure of your program. std::vector is a unique exception because a vector will move its contents around when new elements get added to it, so be careful when using std::vector. Also, & (reference type) is essentially just a pointer, so using pointers as references is perfectly safe and quite common. Just use smart pointers when you are allocating on the heap. std::shared_ptr is a good alternative to normal pointers but it has overhead (performance cost) so use it only when you need it. Edit: the use of pointers in this video is particularly not good because entities in games can be spawned or deleted anytime, which as I said before can lead to these issues.
But Cherno... The reality is that under the hood, std::vector is very much like std::vector. Whether you push_back the sprite or the pointer, they're still both going to be heap allocated unless you've got your own allocator. The difference being that with operator new(), your sprites aren't going to be contiguous, but your pointers will be and you've got to to destroy them yourself. The vector iterator is still just a pointer.
I have read more game egnine books than you could shake a shared pointer at, but discussing something real is what solidifies the concepts in my mind. Much appreciated for this content, and please keep going with this engine.
You can use apps/sites that have extensions for IDEs, like Wakatime, there's several others. There's also some apps that track OS windows and window titles to figure out what you're working on and then you can categorize, but I only know specific ones for mac(ie. Timing), but I'm sure there's similar ones for Windows.
You can also introduce some typed ID (for different types of objects) by using syntax sugar of C++ and allocate this ID by increment atomic int variable (or even non atomic). Only downside if too many threads do this frequently with atomic variable, but there is always way to pre-allocate N ID's very fast and unroll some loops or make like ranged limited local ID allocator that use ID ranges from global ect.
This was fun but…very little discussion of abstraction at the very end. Also, discuss user of software vs developer of software (which often have different priorities). Instead you kind of jump into details (charmingly so, yes). API design before implementation helps; you could do that with the implied api here and clean it up, and then walk through possible implementation details afterwards. ;-)
25:48 probability would be 1 in 2^64, or 1 in ~18 quintillion or 1 in ~18 billion billion so probably more likely a cosmic ray would case the program to crash
Most of the lines of code I have ever written in C++ have turned out to be undefined behaviour... It's astonishing what you can miss without formal training!
That's only if you're either pushing from the front, or pushing from the back, but the memory size isn't enough, so it has to make a new vector and copy those items to another location. But it's also specifically only when you are referencing data in the vector, e.g.: pointer* = &vectorName[1] or reference& = vectorName[1] Which really isn't that common. It isn't true for vectors of pointers, e.g. vector Pushing elements to these doesn't invalidate their addresses because they point elsewhere.
I'm c# developer, and I consider myself something between beginner & intermediate lvl but generally I can follow up with you.. please stay with this format
As someone in a similar spot as the guy here, (not 2k hrs deep but grinding through my own engine as basically a higher level novice) I want to shoot two questions I have into the void in hopes I may finally get answers. For my scenes I have the main loop run their update function and every now and then a different one for a transition that may send data over to the new scene (like if I move to the main menu from settings I keep the music playing vs from title screen which has it's own music.) The main just gets a pointer to the new scene while the old one clears itself of all objects and such. Main then runs and creates the new scene. While memory usage is still low I wonder if this is okay due to a not exactly memory leak I am seeing. Basically if I move between scene 1 and 2 the memory use will rise but will stop after a bit. Move to scene 3 and the cap rises. Scene 4 and so on. I have no clue what maybe causing this as I am deleting everything and if it were a true leak would it not continue indefinitely? Is it just my computer managing data in that way or an actual issue with my code.
My first line was written around 2009 in Java (i was 11 btw), and when I learned pointers and c++ around 2015 I felt super smart and used them everywhere, and now it's 2024, and I rarely use pointers, so yeah the statement is true.
19:40 Wow, it's really exciting to me, because I have once done exactly that mistake And i had some really nasty bugs because of that It took me quite a lot of time to realise what's going on. I managed to do that, because I was at the same time trying some optymalization on vector, so i preallocated some memory for it and then program lasted longer. This way I realised, the problem is that when vector resizes all the references to it's elements become invalid So yea, I'm not the only one who made this mistake.
I'd really like to hear more about this UUID storage works. I don't believe I'm properly grasping it, obviously (I assume) you're not using the id as the index of the vector or whatever container you're using right? Would like to hear more if possible, and please continue the code review if you have time. Thanks.
I'm not sure I interpreted this correctly. How would you use a random GUID or UUID as an index for a vector? Wouldn't these IDs potentially be massive (i.e. 0 to 2^64 - 1) meaning the vector would have a ridiculous number of elements?
@@tardistrailers But how do you get around losing the contiguous memory? Does it make sense to store a map of (UUID -> Index) and keep that updated somehow? The whole point of this contiguous allocation is better performance during iteration (ECS etc.).
Most of the times engines have to move objects around in the heap to fill the holes created by freed memory. If you access pointers directly, you either can't move objects or you can't use memory efficiently.
25:45 The odds of two random 64 bit numbers colliding is 2.916x10^-39% AKA 0.0000000000000000000000000000000000000002196% With more decimals after, of course, but roughly that It is a chance of 1 in 340,282,366,920,938,463,463,374,607,431,768,211,456 chance or a 1 in 2^128 chance or a 1 in 340.2 undecillion chance. Very very unlikely. If this ever happens to you, go buy a lottery ticket. You're likely to win the lottery 11,645,529,300,000,000,000,000,000,000 times more likely to win the lottery than get two 64 bit integers being the same Edit: I made a mistake, its not 1 in 2^127, but 2^128
That doesn't make sense. Shouldn't the chance of two random 64 bit numbers be a straight up 1 in 2^64? Since the second number has to be exactly the first number and can be any of 2^64 numbers?
How can it be a 1 in 2^127 when there's only 64 bits? This makes no sense. If we both pick a number between 1 and 10, the chance we pick the same number is 1/10 (the number you pick doesn't matter, the only thing that matters is that I pick the same). Following the same logic, the chance for a computer to pick two identical numbers between 1 and 2^64 is 1 in 2^64.
@@felixp535 Lets make it a bit simpler. Imagine you have two dice and you want them both to roll 6s. Each die has a 1 in 6 chance of landing 6, so dice A has 1/6 chance, and dice 2 has 1/6 chance. These are statistically independent events, therefore to calculate the odds of them both happening we multiply them, therefore both dice have a 1/6 chance. This is called taking the intersection of the probabilities, when two events happen together. The odds of having one number from a 64 bit integer is 1 in 2^64. Assuming perfect randomness in the generation we multiply it (aka square it) just like the dice. 2^64 squared is 2^128, therefore we have odds of 1 in 2^128.
@@felixp535 this is a common misconception. The probability of person 1 picking does not affect the probability of person 2. This means they are independent events. The probability for independent events is calculated P = P(A) * P(B). So in your example the probability of both picking the same number out of ten is 1/10 * 1/10 = 1/100. This is because you have to consider every combination they can pick the same number.
@@Rizza257 You made me doubt so I double checked using Excel. The probability of both picking the same number is indeed 1/10 and not 1/100 as you mention. The number chosen by A doesn't matter. The only thing that matters is that B also chooses the same one, which has a 1/10 chance to happen. The events are dependant because B must pick the same number as A. When you think about it, A does not have to pick the same number as B, so P(A) is the probability that A picks any number, which is 1.
Hi Cherno, thanks for another code review. By the way, could you maybe consider reviewing game entity design in Unreal engine? (namely Actors and components) And maybe a UI system? And maybe Compare it with Unity or Cryengine or Hazel or something. The reason is that there's this aura of ideal AAA grade around Unreal and everything related to it's design is perfect, but for me it's exactly the opposite, after unity an O3DE. Every other engineer I have spoken to (Unreal devs) don't even get what's wrong (from my personal opinion ofk) with their favorite engine etc. I think that could be a realy insightful and interesting kind of video
Minute 9 is when I started asking myself like "where the fuck is the engine" rather than a few classes and a half (that aren't even handy to use). It's pretty much like making a game from scratch, but using some questionable libraries.
I have a stupid question maybe, but is just curiosity I guess, the case you talked in the video and some people are talking in the comments about a really low chance of a 128bit random nr coliding or repeating, isn't it a good idea to make the nr and check if it already exists? Just to make it impossible to happen at all. I just say that, because in this case, the sprites will be loaded once only, and they stay in memory or cache or whatever, it will slow down the loading of the sprites cause it has an if in every sprite nr generation, but it will only happen once right? at the beggining of the game launch. then you just ask to print a specific sprite that is already there or to hide it. I don't know if im making myself clear in what I mean. Is kind of my logic, but I never made a game or engine or anything usefull at all, just few projects that I started to try to learn it, but never finished, or needed to load gigabytes of stuff. Only things with small content. Few images to make buildings with colision and my character images, the way I did it if im not mistaken was with class sprites and push pull to the class array of sprites, kind of what you did with the cherno engine in the begining I believe.
Even in games such as Elite Dangerous or No Man's Sky, they don't come close to having to make use of 500 million UUIDs at once, let alone 4 billion, so this really is a non-issue with any scale of video game. The game won't store all the planets in one data structure - I'd imagine more of a tree where you segment the universe into quadrants, each quadrant stores its system or planets, then each planet stores its own entities. With careful consideration to structure, the UUIDs only have to be unique per level of the tree, rather than globally. That's where you start considering the difference between UUID and GUID
Architecture review is code review. It's way more important to catch wrong assumptions about how stuff works than nitpicking variable names or any easily refactorizable code.
I agree with your statement about pointers. I hate them...not that I don't understand them, but it's a pain to keep track of them when they are juggled around and it's better to avoid them whenever possible. It's nothing else, really.
@@oussamasoudassi Have you ever worked on projects that have dozens (maybe hundreds) of files, each 2000+ lines long and full of pointers going here and there without indicating who owns what? Yes, fixing a memory leak due to badly un-referenced pointers in those projects is like fixing a hole in the Hoover damn with duct tape. I myself would be careful, but when projects grows out of hand, it is unavoidable to have those leaks, and finding them is a nightmare. Yes, C++20 has helped to mitigate that issue, but still pretty bad!
I liked spending hundreds of hours coding games, engines, applications.. It was really fun and I was satisfied of the speed. And then, I learned about the cache, cpu optimizations, ram latency, and now, all I can see is wasted performance and spaghetti code 😂
Thank you for extremely helpful video especially since I recently made a sprite rendering system with a c style array of size 2048 storing pointer to sprites lmao
Si j'avais à faire une revue, déjà je commencerais par poser une question majeur. Est-ce de la responsabilité de l'objet Game de savoir comment créer un sprite et comment manager sa persistance ?! non. Un objet SpriteManager capable de créer des sprites transitant ou persisté serait plus optim. Conception SOLID et le code deviendra plus simple et logique. Ensuite sur le "new", pourquoi sur-utiliser la stl ? qu'il était doux le temps ou nous savions faire la part des choses et ne pas utiliser une class inutile quand elle n'est pas nécessaire.
wow this was basic, anyway what this is really missing is a decent description on pointer arithmetic and why that is a very inexpensive operation. This is of course assuming that all the sprite objects have an equal, known, size at compile time and no further de-referencing to other data on the heap happens, cause then the original method of just storing pointers is actually more efficient when drawing the sprite. If you have to explain this to someone, this really should really include an in depth discussion of stack and heap allocations and what that actually means, as that is the underlying optimization issue.
I don't understand why is better to storing the objects in that vector than storing pointers to them?! I can imagine one reason: if ALL of the stored object are the same type (aka same size in memory)? (Ok, I haven't program any game engine yet, but in the other programming tasks I experienced that storing pointers [of course unique, shared or weak] is the solution.)
If you store all the objects in a vector directly, you are guaranteed that they are contiguous in memory. If you store pointers, you have no guarantee that the pointed-to addresses (where the object data would be) are spatially local in memory. This has huge implications because accessing memory is SIGNIFICANTLY slower for a CPU than arithmetic/logical operations. CPU architecture attempts to solve this problem by having "cache." Every time you access a memory location, the CPU first checks whether it has that memory in its cache. If it is, great! (That's called a "cache hit" and is very fast.) If not, it must fall back to RAM and grab it (called a "cache miss" and is very slow). Contiguous memory is important because when CPU has a cache miss and fetches the memory from RAM, it grabs more than just the memory you requested. It actually grabs larger block determined by the size of the "cache line." The size of the cache line depends on the CPU & cache level, but its typically ~64 bytes for the closest (fastest) cache. So if I linearly access a vector of 4 byte integers, the first access would be a slow cache miss and then the next 15 integers would be very fast since they were put in cache by the initial miss. In practice it gets a bit more complicated (i.e. the CPU has ways to predict cache misses and compensate for them and compilers can do clever tricks to optimize code around cache access) but the takeaway is that linear, contiguous memory accesses are fast and random accesses are slow.
@@machimanta Hi i didnt quite get your explanation, so what advantage do i actually have when i store the objects as objects and not pointers? So do i like always have my sprites in cache to access them always with a cache hit when i store them as object, and when i store them as pointers i just need to go back to memory because i have the pointers in the cache and do a cache miss?
@@y4ni608 In order to have an object, it has to be kept /somewhere/ in memory. So storing the objects directly within the vector (rather than having a vector of pointers to objects) is the most efficient way to do it from the eyes of the CPU. If you're currently storing pointers to objects, then you're likely allocating them individually with calls to new/make_shared/make_unique/malloc/etc. (pick your poison). Doing so means they are more-or-less stored "randomly" in memory. That's bad since it means the CPU will stall a lot trying to find their data (cache miss penalty). Computers are good at doing predictable things, so having to randomly jump around in memory from object-to-object is not ideal
When somebody writes "You can give the pointer an address by calling ..." you know that this person does not understand how memory allocation works in C++. Which is absurd after 2000 hours :D
I apolozige for any confusion in the Readme file, to put it simply my english is just very bad. i meant with this line "You can give the Sprite ptr a address by calling the s2d::Sprite::getSpriteByName("name"); " that well since s2d::Sprite* my_ptr is a nullptr on start you are assigning the nullptr a adress i dont really know how to put it in good english for you. So thats what i meant, and yeah i do agree that it sounds wierd when you say "Give the pointer a adress" but i dont know how to say it better, sorry.
This was helpful by all means, but this isn't a code review to me. There was no analysis or explanation of actual code, which is what you've brought us to expect from your code reviews - you could have at least gone into the code and made the exact same analysis from within the code rather than the github readme. Regardless, I'd be interested to see more
The downside to a vector of objects is that when the vector resizes, the all the objects need to be moved, instead of just the pointers. Depending on the size of the objects, this could make a big difference.
Traversing the vector is usually done muuuuch more frequently than vector resizing, so optimizing that is a priority. If resizing is happening so frequently that is slows you down, you have an architectural design issue
Thanks for watching! Should we continue looking at this 2000-hour 2D Game Engine?
Also don't forget you can try everything Brilliant has to offer-free-for a full 30 days! Visit brilliant.org/TheCherno - the first 200 of you will get 20% off Brilliant’s annual premium subscription!
do itttttt please!
part 2 when
Let's dive in please!
very helpful ... continue
Yes
At 2:23 Cherno mentioned he doesn't know the tool Rene. This is because it is literally me. The person who created this Engine is one of my friends and I asked him to add me as a tool because I gave some recommendations about the UI of the Engine :)
Lmao. Loved it😅
Lol
wait, add you as a... tool? lmfao XD
you mean the egnine?
He can now add cherno as well
"I won't roast you too much"
*proceeds to making a 30-minute video of roasing the README file alone*
After reading the README file, I've seen it all
Hello!! Thanks for reviewing my game engine for some reason my previuos commentar got deleted. it looks like im missing a concept about pointers 😅. I did discuss this vector SpriteRepository structure with my teacher (He is a teacher teaching mainly about os's) (Im still in highschool, 2nd grade) and he actually agreed with this structure. It's cool seing different opinions coming together, but i will defenetly value the advice of a professional gameengine dev more.
I can't wait to see the part 2!
by the way, if you guys got any questions about my game engine feel free to ask here :)
7 month edit: Yo guys, i have finally implemented all the stuff cherno advised, and im happy AF i love the new project structure
He agreed with your structure cause for an highschool student is really good enough lol. Good job
Congrats bro!
Hey, that's amazing on several fronts - that you accepted Cherno's critique as a grown man (and even fixed it according to his suggestions) and also that you're so young and yet managed to write an engine!
Cherno is a type of guy can do code review without looking the code. :D
He does look into the code that is presented in the README though, so that's not true.
I haven't finished the video yet, but i think there might be part 2 and maybe more.
senior dev sorcery
This is accurate, except its actually an insult not a compliment.
@@kzone272 This is only a joke. This is an internet meme. It's not an insult or anything. Cherno has an important place in my life.
Please continue the review on a future video. I’m really curious to see the engine running!
Please continue with this engine. This is super informational! I also like the high level design aspect of this review
Yeah I really enjoy these kinds of videos, learning about how game engines work and are usually structured, and especially WHY it's done this way.
It feels like this developer was trying to reinvent the wheel, but to be fair, how do you know you're reinventing the wheel if you don't even know how a wheel looks like and how it works?
I started my own game engine a while ago, following Yan's amazing Game Engine series (except I was doing it in a language called Jai). I managed to render simple images, but I stopped following the series when it got to the ECS stuff, as I didn't want to have that in my engine. I just wanted to continue working on the rendering, but most of the stuff was being off-screened, so I really felt stuck when trying to actually create the structure for the rendering system. It's soooo complex, there's just sooooo much to think about. In various projects I've tried to explore there's RenderingContexts, RenderingConfigurations, all sorts of Buffers and Passes, rendering on separate threads, making all of this mess API-agnostic...
I just gave up.
I would love a series of videos explaining how all of that stuff works, explained with simple text and drawings just like in this video.
@@felixp535 Hello, yeah i kinda wanted to try something new, but i can see what issues i have here, i really didnt think that much about it
@@felixp535exactly. JAI?! You got access? How do you like it?
12:24 that literally can become the best bell curve programming meme ever.
Instead of UUIDs for referencing entities during runtime you can also use generational indices. Keeps the index simple and small and takes care of validity.
UUIDs are extremely slow to generate too.
@@HairyPixels how so? UUID v4 is just a random 128-bit number with a couple of the bits set to specific values
@@Mungoes it's how the numbers are generated I guess. Maybe for sprites it's no so bad but if you start making them real time it's measurable.
@@HairyPixelscreate them in batches and utilize the mm registers maybe
@@HairyPixels Extremely slow to generate? What are you on an Amiga? Also, you don't need to generate UUIDs every single frame. You generate them only when you create the instances. It doesn't have to be generate UUIDs from a cryptographically secure random number generator either.
That raw pointer confidence chart is way too relatable. I can't believe how much I trusted myself with them before.
In my opinion, this was one of your best code reviews, and you didn't even open the project yet. The way you explained everything visually was very cool and extremely informative. I would love to see a part two!
As someone who isn't a game dev but is peripherally interested, I really liked the conceptual overviews, thanks. This kind of thing is still relevant to other areas of system design.
12:40 Can releate haha.
At first I used to avoid dynamic memory management, then I started to embrace pointers after I got the hang of them and considered smart pointers 'pointless', that is after attaching address sanitizer and spending approximately an hour trying to fix memory leaks and memory corruption on a small sized project and later realized how important ownership is and how raw pointers laked ownership.
Please make another video about this engine. I'm sure a deeper look into the engine would reveal many more memory management related issues for you to get stuck on for half an hour.
I rarely comment on videos. I really liked this video and would love to see more of this engine reviewed.
The drawing really helped me understand the concepts.
My initial impression is, that a bit more spacing and logically connected structuring would really boost my understanding and make it easier to follow.
e.g. stacked in a way that matches the data flow top to bottom.
like Scene -> Sprite Objects -> Sprites -> How the memory is referenced / handled. I probably missed a layer in this example.
Writing that, a summary in such a structure at the end would've probably really helped me recap and form a better understanding of what concepts you just pointed out. You partly or may have done that in the part about Scenes.
Long comment, really loved the video. Hope you can take something from my feedback. :-)
Yep you are right! I should have done it like this, i actually do have a scene system but its kinda wierd lol. It just loads the contents from for example scene 2 in memory and deletes the content from Scene 1. I should have wrapped that content that gets loaded into a scene object, like you did it in your comment also managing scenes (refreshing/loading/deleting) etc is much easier with this way
This project really seems to have some meat to it so i love that you can really talk about stuff. Would love to see that disected even more in other videos.
I like the code review without even looking at code directly xd
It was very interesting to hear this UUID idea. I also was writing some small projects, and came up with the idea of storing the integer instead of the ptr myself, but it always felt awkward for me, and kind of unpreffessional, idk.
thanks for the great video
31:31 Yes... Yan. This is a beautiful format.
Can't wait for future reviews.
Also, the PacMan Game you were going to build yourself.
Would really appreciate it, if you can manage to take some time.
currently making my own 3d engine and this is the exact design problem i was thinking about
1:41 how can you build a game engine and not know about relative paths?
Many (Generation-X and older) Germans call the "Strg" key the "String" key, and it really grinds my gears. :D
This guy roasted the whole engine without even seeing the source code
Which is questionable
Isn't that what everyone does
He just needs ad revenue and relevancy, and took the easiest part to pick on. He ran out of libraries to use in his own engine lmao
@@topg3067 you can't say it wasn't educational
You should do a follow up on this. There was still plenty for people to learn from here. But the kid states at the very start of the documentation that it may be outdated. It would be cool to have a look at the actual code and engine and offer some advice on that.
Why didn't you roast that the readme had code without syntax highlighting?
For some reason it doesnt work
Yes, it's really helpful, let's continue!
I was expecting something more of a "continue we shall" with that pfp
25:45 Having a specific 64-bit hash is 1/2^64 (1 in 18 quintillion). Even if you got 100 000 sprites, you got 1 in 3.7 billion chance any of them collided.
e^(-n(n-1)/2^64) where n is the number of sprites and e is Euler's number
Please keep roasting this engine. I feel like amazing lessons could come out of it. Thanks :)
Would be cool if you did a vid on how to properly design a project
Cherno, with the video content you create, you are very inspiring for people like me who want to become a game engine developer in the future. Thank you for that, keep that great work up!
I once stored current config in a pointer of the class but quickly found out that resizing the vector invalidated that pointer immediately like you mentioned may happen.
So I then stored just the index and used getter function by ID or string name. I am surprised the code review program isn't constantly crashing because of similar.
I'm more of a beginner in C++, but I won't make pointers for the sake of having a pointer. From what I learned, it could lead to unsafe memory handling. If I have to pass something to a function, I'll pass it by reference instead. If I need to make something on the heap, I'll use a smart pointer instead. I made a command line scientific calculator at one point and used std::function to create a function map instead of using raw function pointers.
Pointers to functions are perfectly safe. Real bugs start happening when you have a pointer to a variable that no longer exists. Like, if you spawn a character in a game, refer to it using a pointer, then destroy the character. Now you have a pointer that no longer refers to the character it once referred to (because you deleted it). If you ever made a game in Unity and you've coded something in C#, this situation will result in the quite common "null-reference" error. This is because you are trying to use a pointer that refers to something that no longer exists. This problem never occurs if you use a pointer that refers to something that always exists (like a function) or the pointer is on the stack and it refers to another variable previously on the stack. Just ensure that the thing your pointer refers to still exists and doesn't move around. This is quite a simple rule to keep in mind, and it's generally only broken when there's something fundamentally wrong with the logic/structure of your program. std::vector is a unique exception because a vector will move its contents around when new elements get added to it, so be careful when using std::vector.
Also, & (reference type) is essentially just a pointer, so using pointers as references is perfectly safe and quite common. Just use smart pointers when you are allocating on the heap. std::shared_ptr is a good alternative to normal pointers but it has overhead (performance cost) so use it only when you need it.
Edit: the use of pointers in this video is particularly not good because entities in games can be spawned or deleted anytime, which as I said before can lead to these issues.
Very helpful explanation on pointers vs identifiers. Would love to see you review the rest of this code base.
Man i like this style a lot more plz more
But Cherno... The reality is that under the hood, std::vector is very much like std::vector. Whether you push_back the sprite or the pointer, they're still both going to be heap allocated unless you've got your own allocator. The difference being that with operator new(), your sprites aren't going to be contiguous, but your pointers will be and you've got to to destroy them yourself. The vector iterator is still just a pointer.
loved your review of this, please cover more of this.
I have read more game egnine books than you could shake a shared pointer at, but discussing something real is what solidifies the concepts in my mind.
Much appreciated for this content, and please keep going with this engine.
What is a good way to track hours spent in VSCode/Game Engine? Something bit more automatic that just stopwatch and a spreadsheet?
You can use apps/sites that have extensions for IDEs, like Wakatime, there's several others. There's also some apps that track OS windows and window titles to figure out what you're working on and then you can categorize, but I only know specific ones for mac(ie. Timing), but I'm sure there's similar ones for Windows.
Wakatime
You sounded so excited whilst reading your Brilliant sponsor script... hahaha
1:59 Boy was he right about this being a ride lol 🤣
You can also introduce some typed ID (for different types of objects) by using syntax sugar of C++ and allocate this ID by increment atomic int variable (or even non atomic). Only downside if too many threads do this frequently with atomic variable, but there is always way to pre-allocate N ID's very fast and unroll some loops or make like ranged limited local ID allocator that use ID ranges from global ect.
This was fun but…very little discussion of abstraction at the very end. Also, discuss user of software vs developer of software (which often have different priorities). Instead you kind of jump into details (charmingly so, yes). API design before implementation helps; you could do that with the implied api here and clean it up, and then walk through possible implementation details afterwards. ;-)
25:48 probability would be 1 in 2^64, or 1 in ~18 quintillion or 1 in ~18 billion billion so probably more likely a cosmic ray would case the program to crash
bro gave the entire CODE review without looking at the the Code 😂😂😂❤
Whoa, I didn't know that pushing a new element into a vector would invalidate all earlier references.
Most of the lines of code I have ever written in C++ have turned out to be undefined behaviour... It's astonishing what you can miss without formal training!
That's only if you're either pushing from the front, or pushing from the back, but the memory size isn't enough, so it has to make a new vector and copy those items to another location. But it's also specifically only when you are referencing data in the vector, e.g.:
pointer* = &vectorName[1]
or
reference& = vectorName[1]
Which really isn't that common.
It isn't true for vectors of pointers, e.g.
vector
Pushing elements to these doesn't invalidate their addresses because they point elsewhere.
I'm c# developer, and I consider myself something between beginner & intermediate lvl but generally I can follow up with you.. please stay with this format
Thank you Yan that was very helpful and appreciated as always can't wait for more!
That sprite stunlock truly is something
Please continue! This is a great breakdown
Please keep going!
@TheCherno, what do you use to draw on the screen and whiteboard these webpages?
7:04 - What the hell? That pronunciation was so accurate
i wonder if thecherno would review a c styled rust codebase
As someone in a similar spot as the guy here, (not 2k hrs deep but grinding through my own engine as basically a higher level novice) I want to shoot two questions I have into the void in hopes I may finally get answers.
For my scenes I have the main loop run their update function and every now and then a different one for a transition that may send data over to the new scene (like if I move to the main menu from settings I keep the music playing vs from title screen which has it's own music.) The main just gets a pointer to the new scene while the old one clears itself of all objects and such. Main then runs and creates the new scene. While memory usage is still low I wonder if this is okay due to a not exactly memory leak I am seeing. Basically if I move between scene 1 and 2 the memory use will rise but will stop after a bit. Move to scene 3 and the cap rises. Scene 4 and so on. I have no clue what maybe causing this as I am deleting everything and if it were a true leak would it not continue indefinitely? Is it just my computer managing data in that way or an actual issue with my code.
Really helpful video, thank you !
Hey I was wondering if you could do an updated coding tutorial for opengl please?
Basically a 30 minute sprite review
My first line was written around 2009 in Java (i was 11 btw), and when I learned pointers and c++ around 2015 I felt super smart and used them everywhere, and now it's 2024, and I rarely use pointers, so yeah the statement is true.
I'm confused, if you used a 64-bit UUID wouldn't the vector size be huge? Or are you saying to use a map instead?
I wanna see more of this engine
19:40
Wow, it's really exciting to me, because I have once done exactly that mistake
And i had some really nasty bugs because of that
It took me quite a lot of time to realise what's going on.
I managed to do that, because I was at the same time trying some optymalization on vector, so i preallocated some memory for it and then program lasted longer.
This way I realised, the problem is that when vector resizes all the references to it's elements become invalid
So yea, I'm not the only one who made this mistake.
I'd really like to hear more about this UUID storage works. I don't believe I'm properly grasping it, obviously (I assume) you're not using the id as the index of the vector or whatever container you're using right? Would like to hear more if possible, and please continue the code review if you have time. Thanks.
I'm not sure I interpreted this correctly. How would you use a random GUID or UUID as an index for a vector? Wouldn't these IDs potentially be massive (i.e. 0 to 2^64 - 1) meaning the vector would have a ridiculous number of elements?
You could use a hashmap instead of a vector in that case.
Lol, just buy more ram...
@@tardistrailers But how do you get around losing the contiguous memory? Does it make sense to store a map of (UUID -> Index) and keep that updated somehow? The whole point of this contiguous allocation is better performance during iteration (ECS etc.).
Hi Cherno, I want to ask what steps are needed to create a game engine?
Most of the times engines have to move objects around in the heap to fill the holes created by freed memory. If you access pointers directly, you either can't move objects or you can't use memory efficiently.
25:45
The odds of two random 64 bit numbers colliding is 2.916x10^-39% AKA
0.0000000000000000000000000000000000000002196%
With more decimals after, of course, but roughly that
It is a chance of 1 in 340,282,366,920,938,463,463,374,607,431,768,211,456 chance or a 1 in 2^128 chance or a 1 in 340.2 undecillion chance. Very very unlikely. If this ever happens to you, go buy a lottery ticket. You're likely to win the lottery 11,645,529,300,000,000,000,000,000,000 times more likely to win the lottery than get two 64 bit integers being the same
Edit: I made a mistake, its not 1 in 2^127, but 2^128
That doesn't make sense. Shouldn't the chance of two random 64 bit numbers be a straight up 1 in 2^64? Since the second number has to be exactly the first number and can be any of 2^64 numbers?
How can it be a 1 in 2^127 when there's only 64 bits? This makes no sense.
If we both pick a number between 1 and 10, the chance we pick the same number is 1/10 (the number you pick doesn't matter, the only thing that matters is that I pick the same).
Following the same logic, the chance for a computer to pick two identical numbers between 1 and 2^64 is 1 in 2^64.
@@felixp535 Lets make it a bit simpler.
Imagine you have two dice and you want them both to roll 6s.
Each die has a 1 in 6 chance of landing 6, so dice A has 1/6 chance, and dice 2 has 1/6 chance. These are statistically independent events, therefore to calculate the odds of them both happening we multiply them, therefore both dice have a 1/6 chance. This is called taking the intersection of the probabilities, when two events happen together.
The odds of having one number from a 64 bit integer is 1 in 2^64. Assuming perfect randomness in the generation we multiply it (aka square it) just like the dice. 2^64 squared is 2^128, therefore we have odds of 1 in 2^128.
@@felixp535 this is a common misconception. The probability of person 1 picking does not affect the probability of person 2. This means they are independent events. The probability for independent events is calculated P = P(A) * P(B). So in your example the probability of both picking the same number out of ten is 1/10 * 1/10 = 1/100.
This is because you have to consider every combination they can pick the same number.
@@Rizza257 You made me doubt so I double checked using Excel. The probability of both picking the same number is indeed 1/10 and not 1/100 as you mention.
The number chosen by A doesn't matter. The only thing that matters is that B also chooses the same one, which has a 1/10 chance to happen.
The events are dependant because B must pick the same number as A. When you think about it, A does not have to pick the same number as B, so P(A) is the probability that A picks any number, which is 1.
Hi Cherno, thanks for another code review.
By the way, could you maybe consider reviewing game entity design in Unreal engine? (namely Actors and components) And maybe a UI system? And maybe Compare it with Unity or Cryengine or Hazel or something. The reason is that there's this aura of ideal AAA grade around Unreal and everything related to it's design is perfect, but for me it's exactly the opposite, after unity an O3DE.
Every other engineer I have spoken to (Unreal devs) don't even get what's wrong (from my personal opinion ofk) with their favorite engine etc. I think that could be a realy insightful and interesting kind of video
Yeah. Unreal is incredibly overhyped thanks to it's features, to the point, where we cannot have a normal discussions about it's shortcomings.
@@GonziHereI'd rather use polymorphism as well instead of creating an array for every type of an entity
People should pay for content like this, this is my kind of videos really, thx again
Minute 9 is when I started asking myself like "where the fuck is the engine" rather than a few classes and a half (that aren't even handy to use). It's pretty much like making a game from scratch, but using some questionable libraries.
I love your content, keep going😄
I have a stupid question maybe, but is just curiosity I guess, the case you talked in the video and some people are talking in the comments about a really low chance of a 128bit random nr coliding or repeating, isn't it a good idea to make the nr and check if it already exists? Just to make it impossible to happen at all.
I just say that, because in this case, the sprites will be loaded once only, and they stay in memory or cache or whatever, it will slow down the loading of the sprites cause it has an if in every sprite nr generation, but it will only happen once right? at the beggining of the game launch. then you just ask to print a specific sprite that is already there or to hide it.
I don't know if im making myself clear in what I mean. Is kind of my logic, but I never made a game or engine or anything usefull at all, just few projects that I started to try to learn it, but never finished, or needed to load gigabytes of stuff. Only things with small content. Few images to make buildings with colision and my character images, the way I did it if im not mistaken was with class sprites and push pull to the class array of sprites, kind of what you did with the cherno engine in the begining I believe.
i'd love to se another part about this engine
With 64 bit identifiers you have a 0.7% chance of a collision with 500 million uids, and 35% with 4 billion.
Even in games such as Elite Dangerous or No Man's Sky, they don't come close to having to make use of 500 million UUIDs at once, let alone 4 billion, so this really is a non-issue with any scale of video game. The game won't store all the planets in one data structure - I'd imagine more of a tree where you segment the universe into quadrants, each quadrant stores its system or planets, then each planet stores its own entities. With careful consideration to structure, the UUIDs only have to be unique per level of the tree, rather than globally. That's where you start considering the difference between UUID and GUID
@@kplays_6000 All true, he just asked someone to put the collision chance in the comments in the video.
I hope they make the changes, this seems imperative.
Please continue with this
"Code Review" - Never even looks at the code. He just saw the new keyword and went off on a tangent about entity component systems...
Architecture review is code review.
It's way more important to catch wrong assumptions about how stuff works than nitpicking variable names or any easily refactorizable code.
If you don't get the architecture correct, the code basically doesn't matter
I agree with your statement about pointers. I hate them...not that I don't understand them, but it's a pain to keep track of them when they are juggled around and it's better to avoid them whenever possible. It's nothing else, really.
If you know who owns whatever pointer and who is responsible for clean up why hating and avoiding them??
@@oussamasoudassi Have you ever worked on projects that have dozens (maybe hundreds) of files, each 2000+ lines long and full of pointers going here and there without indicating who owns what? Yes, fixing a memory leak due to badly un-referenced pointers in those projects is like fixing a hole in the Hoover damn with duct tape.
I myself would be careful, but when projects grows out of hand, it is unavoidable to have those leaks, and finding them is a nightmare. Yes, C++20 has helped to mitigate that issue, but still pretty bad!
Nice egnine, I love it
Thanks :D
00:18 Ah, looks like a typo for "Egg nine"
Another video of this engine
I liked spending hundreds of hours coding games, engines, applications.. It was really fun and I was satisfied of the speed. And then, I learned about the cache, cpu optimizations, ram latency, and now, all I can see is wasted performance and spaghetti code 😂
Back to the engine.
Ive been working on this for about 2000 hrs now, actual project.....
Hello world
great video
Thank you for extremely helpful video especially since I recently made a sprite rendering system with a c style array of size 2048 storing pointer to sprites lmao
Si j'avais à faire une revue, déjà je commencerais par poser une question majeur. Est-ce de la responsabilité de l'objet Game de savoir comment créer un sprite et comment manager sa persistance ?!
non.
Un objet SpriteManager capable de créer des sprites transitant ou persisté serait plus optim. Conception SOLID et le code deviendra plus simple et logique.
Ensuite sur le "new", pourquoi sur-utiliser la stl ?
qu'il était doux le temps ou nous savions faire la part des choses et ne pas utiliser une class inutile quand elle n'est pas nécessaire.
I expected some code in code review :D But that was useful anyway! I would like to see a continuation about this engine. I miss code reviews from you!
wow this was basic, anyway what this is really missing is a decent description on pointer arithmetic and why that is a very inexpensive operation. This is of course assuming that all the sprite objects have an equal, known, size at compile time and no further de-referencing to other data on the heap happens, cause then the original method of just storing pointers is actually more efficient when drawing the sprite. If you have to explain this to someone, this really should really include an in depth discussion of stack and heap allocations and what that actually means, as that is the underlying optimization issue.
That was some incredible insight to how a programmer thinks.
I don't understand why is better to storing the objects in that vector than storing pointers to them?!
I can imagine one reason: if ALL of the stored object are the same type (aka same size in memory)?
(Ok, I haven't program any game engine yet, but in the other programming tasks I experienced that storing pointers [of course unique, shared or weak] is the solution.)
If you store all the objects in a vector directly, you are guaranteed that they are contiguous in memory. If you store pointers, you have no guarantee that the pointed-to addresses (where the object data would be) are spatially local in memory.
This has huge implications because accessing memory is SIGNIFICANTLY slower for a CPU than arithmetic/logical operations. CPU architecture attempts to solve this problem by having "cache." Every time you access a memory location, the CPU first checks whether it has that memory in its cache. If it is, great! (That's called a "cache hit" and is very fast.) If not, it must fall back to RAM and grab it (called a "cache miss" and is very slow).
Contiguous memory is important because when CPU has a cache miss and fetches the memory from RAM, it grabs more than just the memory you requested. It actually grabs larger block determined by the size of the "cache line." The size of the cache line depends on the CPU & cache level, but its typically ~64 bytes for the closest (fastest) cache. So if I linearly access a vector of 4 byte integers, the first access would be a slow cache miss and then the next 15 integers would be very fast since they were put in cache by the initial miss.
In practice it gets a bit more complicated (i.e. the CPU has ways to predict cache misses and compensate for them and compilers can do clever tricks to optimize code around cache access) but the takeaway is that linear, contiguous memory accesses are fast and random accesses are slow.
@@machimanta Hi i didnt quite get your explanation, so what advantage do i actually have when i store the objects as objects and not pointers? So do i like always have my sprites in cache to access them always with a cache hit when i store them as object, and when i store them as pointers i just need to go back to memory because i have the pointers in the cache and do a cache miss?
@@y4ni608 In order to have an object, it has to be kept /somewhere/ in memory. So storing the objects directly within the vector (rather than having a vector of pointers to objects) is the most efficient way to do it from the eyes of the CPU.
If you're currently storing pointers to objects, then you're likely allocating them individually with calls to new/make_shared/make_unique/malloc/etc. (pick your poison). Doing so means they are more-or-less stored "randomly" in memory. That's bad since it means the CPU will stall a lot trying to find their data (cache miss penalty).
Computers are good at doing predictable things, so having to randomly jump around in memory from object-to-object is not ideal
Structure of arrays enthusiasts unite
more please❤
When somebody writes "You can give the pointer an address by calling ..." you know that this person does not understand how memory allocation works in C++. Which is absurd after 2000 hours :D
I apolozige for any confusion in the Readme file, to put it simply my english is just very bad. i meant with this line "You can give the Sprite ptr a address by calling the s2d::Sprite::getSpriteByName("name"); " that well since s2d::Sprite* my_ptr is a nullptr on start you are assigning the nullptr a adress i dont really know how to put it in good english for you. So thats what i meant, and yeah i do agree that it sounds wierd when you say "Give the pointer a adress" but i dont know how to say it better, sorry.
This was helpful by all means, but this isn't a code review to me. There was no analysis or explanation of actual code, which is what you've brought us to expect from your code reviews - you could have at least gone into the code and made the exact same analysis from within the code rather than the github readme. Regardless, I'd be interested to see more
He says at the start - "This isn't a code review. Instead, it's an email review"
@@brianewing1428 and then proceeds to title the video afterwards as "Code Review"
@@kplays_6000 To draw people in, knowing the twist is explained in the first 2 minutes
i would like to see more
Bro spent over 2k hours on this project and hasn't got 10 minutes to set up a flawless project setup everywhere...
i have now, thankfully
I expected this to be about optimizing drawcalls or something.. didn't even get that far! 🤣
An egnine is three short of a dozen, but just enough for 2 dosen and an egnine
The downside to a vector of objects is that when the vector resizes, the all the objects need to be moved, instead of just the pointers. Depending on the size of the objects, this could make a big difference.
Traversing the vector is usually done muuuuch more frequently than vector resizing, so optimizing that is a priority. If resizing is happening so frequently that is slows you down, you have an architectural design issue
eh :D so pro :D im looking at basic c++ videos... it was 6 years ago... this is like level: GOD :D
the chances of two random numbers in the range of 0 - 2^64 to be the same is 1/2^64
naaaaaaaaaaaaaaaah bro is a genius