Programming Thread

Started by the-pi-guy, Mar 13, 2016, 10:39 PM

previous topic - next topic

0 Members and 1 Guest are viewing this topic.

Go Down

Legend

I swear like 90% of the issues that I'm having with writing the program have been declaring the wrong type.  


I apparently tried passing something a double instead of a float, and it wasn't drawing, but the compiler didn't have a problem with it. So I didn't realize my error.  
Which compiler are you using? That'd be really annoying.

I had something similar not too long ago where I used a short instead of a half. Worked just fine from the compiler's perspective but was really giving me a headache.

the-pi-guy

It's impossible to overstate how mindblowingly ahead of it's time the Doom engine was. Outright the father of modern gaming. | ResetEra

Quote
I'm sure a lot of people are like, "yeah, of course, all FPS games after took inspiration from it" but that's underselling what Id Tech 1 did. The things the doom engine did go well beyond FPS games or even rendering graphics period. Doom laid the foundation that virtually all game engines today descend from.

 Several years back, Dreamcast scenester and general amazing dude ChillyWilly had ported Doom to the DC, but his old code no longer builds because of various changes to the KOS library that powers the port. Because of this, earlier this year, I took my first stab at porting doom to a platform:

 

 I began with ChillyWilly's port as a base, then worked backwards. I have worked with modern engines over the years - Source, Unreal Engine 4, Unity - but never really dove deep into how Doom worked. That's not to say I was unfamiliar with Doom, I had been making Doom WADs for years, so I generally knew of things like BSP and lumps, but not really how any of it worked. Around the same time I began porting doom, I also picked up Fabien Sanglard's Doom Engine black book. This was not the first blackbook I'd read. I had read the Wolfenstein black book prior, and years earlier, when learning graphics programming, I read Michael Abrash's Graphics Programming black book (which is, really, the Quake black book). Most draw a linear path between these 3 games: Wolfenstein -> Doom -> Quake. But that really undersells how it all progressed. Things I had thought Quake had come up with, actually came from doom. Further, I learned that Doom and Quake share much more in common than Wolfenstein and Doom share in common. One thing dawned on me: To study Doom, is to study how modern video games work. Doom is, IMO, the very best place for anybody to start if they want to know how modern video games function and operate on a conceptual level. So much of what Doom does directly applies to modern engines.

 With interest in Id Tech 1 growing, and a project in hand, I started where most doom ports begin: converting the 4 main files that are platform dependent. Doom's engine is genius, honest to god genius. It was built with cross platform compatibility in mind. The platform most people play doom on, IBM PC, was NOT the platform Doom was written on. It was written on the NeXT platform, and id knew most people did not have access to NeXT computers. Doom, thusly, is a prime example of how to write a cross platform game, one of the best models. Doom spares no detail in creating everything from the ground up, including fundamental data structures.

 For example, doom shows you how to elegantly sanitize malloc -- malloc is a C function that interfaces with kernels in your operating system to request and reserve a block of memory. But how kernels operate on memory varies not only from OS to OS, but even revision of OS to OS. Malloc, thusly, is malliable, over time, as C has evolved, malloc too has changed. It's not wise to rely on malloc to handle memory in C. People who don't do low level programming but operate in higher programming languages like Java might hear things about how C is a "nightmare" because you have to "manually manage memory." A lot of that "nightmare" comes from how much malloc changes over the years. Doom shows you how to manually manage your memory. It uses malloc in the most broad sense, only using it to request a single large pool of memory, which it then handles and breaks into smaller chunks itself. It, in essence, creates it's own micro OS within that chunk of memory. It is doom, not your OS or its kernel, that fiddles with the memory. To do this, it builds every data type it uses from the ground up. That let carmack and crew design custom, portable data structures in low level memory, bit by bit, byte by byte. That might seem rote to anyone who has done engine programming these days, but that's the point. Doom set that standard. Everybody follows doom's model.

 Going further, Doom impliments OOP in C. Programmers might have heard that C is classically "not an OOP language." That's not quite true. C is a very low level language, exposing memory directly to the programmer. As such, it doesn't provide any OOP abstraction, but you're absolutely free to write your own yourself. When Doom was created, OOP wasn't really a common thing. None the less, Doom implements it's own OOP concepts through pointers, function callbacks, etc. The OLD style of OOP. But Doom's methods are elegant, it's a great OOP implementation. With liberal use of void* pointers and explicit casting, Doom manages to perform a pattern that wouldn't become popularized till really 5 or so years later!

 Back to porting doom: Doom is written to be portable. By writing so much of the engine in custom, operating-system-independent wrappers, it was built so that the core function of the engine was self contained. There are only really 4 files that are platform dependent, those files control the lowest level functions of the engine. Things like calling out to the video card, requesting memory, polling hardware. These 4 files are the core of doom's engine. They are written not to actually do larger functions, but rather to provide a framework of commands that the rest of the engine can call to do complex things. Like, a function to draw pixels on the screen. When you port doom, you write those functions for the hardware, which is how it becomes so portable. If you can write a low level function that places pixels on the screen, matching the interface doom defines, the rest of the engine can use it and thusly do everything it needs to do regarding graphics. That is one of the secrets to why Doom is so widely ported. This concept of portability was not necessarily created by Doom itself -- I've seen Amiga/Genesis/SNES games that work the same way with company-specific libraries that worked the same way which could be placed inside larger assembler code -- but Doom does it very well. More than that, while some other games did this, most did not. It was more common in 1993 for ports of games to essentially be entire rewrites than have modular code like this. And in the Amiga/Genesis/SNES examples I cite, the entire games usually weren't written to be modular, only pieces. Things like the graphics system might work in a modular fashion, but then the memory management might be entirely different among the games. They were often half-ports, half-bespoke-remakes. Doom is truly portable.

 I rewrote those core functions in a few days and got Doom running on the Dreamcast. Really cool, very good lesson. But that isn't going far enough. Doom has way, way more lessons to teach if you dive in. Writing those core functions will teach you how to make a nice modern interface, get you thinking about portability, but the Doom Engine itself will teach you many more concepts the deeper you dive in. For example, Doom teaches you the concept of decoupling the game's internal logic from internal hardware. Old games used to have this problem where how well the game performed was dependent on the hardware it ran on. The logic was usually very closely tied to the hardware, using things like the screen's refresh rate to tie to logic. Doom doesn't do that. Doom implements what is called Fixed Timestep, which uses real-world time as a metric. For example, movement. In many old games, movement is handled in number of spaces jumped per frame. On frame 1, you are at position X, on frame 2, you are on position X+A, on frame 3, you are on position X+A+A, and so forth. Your position depends on that A variable and also which frame you are looking at, A is essentially your speed. This means your motion is not constant if the frames are not constant. If it takes 1 microsecond between frames 1 and 2, but 10 microseconds between frames 2 and 3, your velocity will not be linear, it'll be logarithmic. Doom doesn't work like this. Doom calculates delta between frames. One frame 1, your position is X, on frame 2, your position is X + Delta(frame1), on frame 3, your position is X + Delta(frame2). The delta is how much time passed between the current frame and last frame, calculated using a constant velocity. If you are moving at 1 space per unit of time, and between frame 1 and frame 2, 1 unit of time passes, then the delta is 1 space. But if 10 units of time pass between frames 2 and frames 3, then the delta is 10 spaces, meaning you move at a linear 1 space per unit of time. This decouples the refresh rate of the hardware from the game speed. Game speed is dependent on external, real time, not the load of the computer. Again, this sort of thing is rote today, but largely because Doom showed everyone how it was done.

 Another great example of how Doom establishes the model of modern game design is with its rendering system. People will say things like "Doom isn't 3D" but that's completely wrong. Doom IS 3D, it's the map layouts that are 2D. But Doom's rendering system is pretty much the basis of all modern rendering systems today. The main difference between the way Doom renders and wolfenstein renders is owed to how they process the polygons that make up the world. Wolfenstein uses a raymarcher, basically a system where the game shoots rays out from the players perspective, and paints the screen line by line. It is rendering lines. The world is made up of a bunch of tiny lines that make up walls. Doom does not work like that. Doom renders the world per object. This is a massive difference because, to accomplish this, it needed a way to very speedily traverse the world. Enter Binary Space Partitioning. Binary Space Partioning, at the time of Doom's release, was something really only used in the academic world. It was stuff that, like, the military used for simulations, or NASA used for the space shuttle. It was the most cutting edge 3D traversal system around. BSP works by subdividing the world into many partitions to create nodes (called lumps in doom) that contain single objects. BSP helps order the world into lumps so that, when you render the world around you, you don't have to render everything.

 Kotaku once ran an infamous article where they talked about Fustrum Culling as though it was a new technique Horizon Zero Dawn used to speed things up:

 

 Many people laughed and pointed out that it wasn't unique to that game, it was an old technique, which is correct. Many games used it before Horizon Zero Dawn. BUT NO 3D GAME USED IT BEFORE DOOM. Doom is the genesis of this technique, the magic behind how this technique works is BSP. Even today, modern 3D engines use BSP to speed things up. Without BSP, even modern graphics cards would be too slow to render. BSP is the "magic" that makes rendering large worlds possible. BSP is an ordering technique that sets everything up in a way that graphics technologies can speedily figure out what is or isn't in a fustrum. Without this ability to order objects, if everything was out of order in memory, it would simply be to slow.

 More to the point about doom being actual 3D -- while the map is an arrangement of objects on a flat plane, the objects themselves are NOT 2D. They are fully 3D, with both an X and Z component, plus a height component that is functionally a Y component. That's the trick to how Doom works so fast, it's a bunch of 3D objects, laid out on a 2D plane so that the early BSP traversal implementation didn't have to work so hard. Once CPUs became faster, it became possible to have BSP traversal in 3 dimensions (meaning operating on more data) and the process pretty seamlessly transitioned into 3D maps in Quake. But the core concept was there in Doom, just operating on less data to accomplish the same feat.

 Another example of how Doom teaches great concepts: Event handling. In games prior, things like joystick polling were done at intervals, in simplistic ways. Every bit of the game would have it's own routine to check input. Like, mario. The title screen has a part where it checks input, then determines if start is pressed to proceed. Then, once the game begins, an entirely separate piece of code in the game loop checks input, and figures out what to do if you press right, or A, or B, or whatever. Each screen, each game mode, had it's own input checking stuff. Doom doesn't work like this. There is one centeral, modular piece of technology (one of the core functions I mentioned above) that polls for input, independently of the rest of the game. To let this piece of technology talk to the rest of the game, it implements what is known as an Event Handler, which is a global queue of events that get passed from mode to mode. Events are queued using hardware interrupts, a way for the processor in your computer to receive a message outside of your program, directly from the joystick, which makes it say "oh shame, let's stop running the main code for a bit, and jump to this routine immediately, then when it's done, let's go back to exactly where we were in the main code." In Doom's instance, the IRQ code that is run when the joystick (or keyboard, or mouse) flags input, is a piece of code that takes the information polled, configures it into a package, then places it into the event queue. Then, individual game modes themselves have event handlers. They don't poll the input, they just handle the input if any exists. This means, for example, the menu in Doom does not need to check for input. Nor does the gameloop. Instead, they just have routines that, if input exists at all, will get executed. If no input exists, they don't get executed. This concept of events is a major change to how games work. All game engines today operate off of events, it's the main concept behind blueprints in things like UE4:

 

 Again, this might seem rote today, but that's because DOOM popularized it. It's rote, because everyone is copying doom.

 I could keep going on and on about how this kind of stuff is laid out. Doom standardized game networking, for example, teaching synchronization methods. Doom popularized the concept of releasing editors and tools. Doom did a ton of things that all games today crib off of. Doom is, hands down, the most important and groundbreaking and trendsetting game of the last 30 years. Hell, I might say ever. Probably the most important game ever released.

 Pay your respects to Doom. It's legacy endures, the entire game industry owes it a huge technical debt.  
[/size]

the-pi-guy

I've learned a lot of the basics.

But I have a long way to go.   :-\

*sigh*

Legend

I've learned a lot of the basics.

But I have a long way to go.   :-\

*sigh*
Just focus on how far you've already come!  ;D


the-pi-guy

Just focus on how far you've already come!  ;D


Sometimes I think I'm in a good place with stuff.  
Other times I look at job applications wanting masters degrees or PhD, and feel like I'm falling behind because I feel like I should be there already.  

the-pi-guy


the-pi-guy


Legend

Post image
At this point is that even hacking haha? It's more like a prank that could work on stupid human drivers too.

Go Up