Jump to content
  • Advertisement

evanofsky

Member
  • Content count

    492
  • Joined

  • Last visited

Everything posted by evanofsky

  1. evanofsky

    The Poor Man's Netcode

    The more you know about a given topic, the more you realize that no one knows anything. For some reason (why God, why?) my topic of choice is game development. Everyone in that field agrees: don't add networked multiplayer to an existing game, you drunken clown. Well, I did it anyway because I hate myself. Somehow it turned out great. None of us know anything. Problem #1: assets My first question was: how do I tell a client to use such-and-such mesh to render an object? Serialize the whole mesh? Nah, they already have it on disk. Send its filename? Nah, that's inefficient and insecure. Okay, just a string identifier then? Fortunately, before I had time to implement any of my own terrible ideas, I watched a talk from Mike Acton where he mentions the danger of "lazy decision-making". One of his points was: strings let you lazily ignore decisions until runtime, when it's too late to fix. If I rename a texture, I don't want to get a bug report from a player with a screenshot like this: I had never thought about how powerful and complex strings are. Half the field of computer science deals with strings and what they can do. They usually require a heap allocation, or something even more complex like ropes and interning. I usually don't bother to limit their length, so a single string expands the possibility space to infinity, destroying whatever limited ability I had to predict runtime behavior. And here I am using these complex beasts to identify objects. Heck, I've even used strings to access object properties. What madness! Long story short, I cultivated a firm conviction to avoid strings where possible. I wrote a pre-processor that outputs header files like this at build time: namespace Asset { namespace Mesh { const int count = 3; const AssetID player = 0; const AssetID enemy = 1; const AssetID projectile = 2; } } So I can reference meshes like this: renderer->mesh = Asset::Mesh::player; If I rename a mesh, the compiler makes it my problem instead of some poor player's problem. That's good! The bad news is, I still have to interact with the file system, which requires the use of strings. The good news is the pre-processor can save the day. const char* Asset::Mesh::filenames[] = { "assets/player.msh", "assets/enemy.msh", "assets/projectile.msh", 0, }; With all this in place, I can easily send assets across the network. They're just numbers! I can even verify them. if (mesh < 0 || mesh >= Asset::Mesh::count) net_error(); // just what are you trying to pull, buddy? Problem #2: object references My next question was: how do I tell a client to please move/delete/frobnicate "that one object from before, you know the one". Once again, I was lucky enough to hear from smart people before I could shoot myself in the foot. From the start, I knew I needed a bunch of lists of different kinds of objects, like this: Array<Turret> Turret::list; Array<Projectile> Projectile::list; Array<Avatar> Avatar::list; Let's say I want to reference the first object in the Avatar list, even without networking, just on our local machine. My first idea is to just use a pointer: Avatar* avatar; avatar = &Avatar::list[0]; This introduces a ton of non-obvious problems. First, I'm compiling for a 64 bit architecture, which means that pointer takes up 8 whole bytes of memory, even though most of it is probably zeroes. And memory is the number one performance bottleneck in games. Second, if I add enough objects to the array, it will get reallocated to a different place in memory, and the pointer will point to garbage. Okay, fine. I'll use an ID instead. template<typename Type> struct Ref { short id; inline Type* ref() { return &Type::list[id]; } // overloaded "=" operator omitted }; Ref<Avatar> avatar = &Avatar::list[0]; avatar.ref()->frobnicate(); Second problem: if I remove that Avatar from the list, some other Avatar will get moved into its place without me knowing. The program will continue, blissfully and silently screwing things up, until some player sends a bug report that the game is "acting weird". I much prefer the program to explode instantly so I at least get a crash dump with a line number. Okay, fine. Instead of actually removing the avatar, I'll put a revision number on it: struct Avatar { short revision; }; template<typename Type> struct Ref { short id; short revision; inline Type* ref() { Type* t = &Type::list[id]; return t->revision == revision ? t : nullptr; } }; Instead of actually deleting the avatar, I'll mark it dead and increment the revision number. Now anything trying to access it will give a null pointer exception. And serializing a reference across the network is just a matter of sending two easily verifiable numbers. Problem #3: delta compression If I had to cut this article down to one line, it would just be a link to Glenn Fiedler's blog. Which by the way is here: gafferongames.com As I set out to implement my own version of Glenn's netcode, I read this article, which details one of the biggest challenges of multiplayer games. Namely, if you just blast the entire world state across the network 60 times a second, you could gobble up 17 mbps of bandwidth. Per client. Delta compression is one of the best ways to cut down bandwidth usage. If a client already knows where an object is, and it hasn't moved, then I don't need to send its position again. This can be tricky to get right. The first part is the trickiest: does the client really know where the object is? Just because I sent the position doesn't mean the client actually received it. The client might send an acknowledgement back that says "hey I received packet #218, but that was 0.5 seconds ago and I haven't gotten anything since." So to send a new packet to that client, I have to remember what the world looked like when I sent out packet #218, and delta compress the new packet against that. Another client might have received everything up to packet #224, so I can delta compress the new packet differently for them. Point is, we need to store a whole bunch of separate copies of the entire world. Someone on Reddit asked "isn't that a huge memory hog"? No, it is not. Actually I store 255 world copies in memory. All in a single giant array. Not only that, but each copy has enough room for the maximum number of objects (2048) even if only 2 objects are active. If you store an object's state as a position and orientation, that's 7 floats. 3 for XYZ coordinates and 4 for a quaternion. Each float takes 4 bytes. My game supports up to 2048 objects. 7 floats * 4 bytes * 2048 objects * 255 copies = ... 14 MB. That's like, half of one texture these days. I can see myself writing this system five years ago in C#. I would start off immediately worried about memory usage, just like that Redditor, without stopping to think about the actual data involved. I would write some unnecessary, crazy fancy, bug-ridden compression system. Taking a second to stop and think about actual data like this is called Data-Oriented Design. When I talk to people about DOD, many immediately say, "Woah, that's really low-level. I guess you want to wring out every last bit of performance. I don't have time for that. Anyway, my code runs fine." Let's break down the assumptions in this statement. Assumption 1: "That's really low-level". Look, I multiplied four numbers together. It's not rocket science. Assumption 2: "You sacrifice readability and simplicity for performance." Let's picture two different solutions to this netcode problem. For clarity, let's pretend we only need 3 world copies, each containing up to 2 objects. Here's the solution I just described. Everything is statically allocated in the .bss segment. It never moves around. Everything is the same size. No pointers at all. Here's the idiomatic C# solution. Everything is scattered randomly throughout the heap. Things can get reallocated or moved right in the middle of a frame. The array is jagged. 64-bit pointers all over the place. Which is simpler? The second diagram is actually far from exhaustive. C#-land is a lot more complex in reality. Check the comments and you'll probably find someone correcting me about how C# actually works. But that's my point. With my solution, I can easily construct a "good enough" mental model to understand what's actually happening on the machine. I've barely scratched the surface with the C# solution. I have no idea how it will behave at runtime. Assumption 3: "Performance is the only reason you would code like this." To me, performance is a nice side benefit of data-oriented design. The main benefit is clarity of thought. Five years ago, when I sat down to solve a problem, my first thought was not about the problem itself, but how to shoehorn it into classes and interfaces. I witnessed this analysis paralysis first-hand at a game jam recently. My friend got stuck designing a grid for a 2048-like game. He couldn't figure out if each number was an object, or if each grid cell was an object, or both. I said, "the grid is an array of numbers. Each operation is a function that mutates the grid." Suddenly everything became crystal clear to him. Assumption 4: "My code runs fine". Again, performance is not the main concern, but it's important. The whole world switched from Firefox to Chrome because of it. Try this experiment: open up calc.exe. Now copy a 100 MB file from one folder to another. I don't know what calc.exe is doing during that 300ms eternity, but you can draw your own conclusions from my two minutes of research: calc.exe actually launches a process called Calculator.exe, and one of the command line arguments is called "-ServerName". Does calc.exe "run fine"? Did throwing a server in simplify things at all, or is it just slower and more complex? I don't want to get side-tracked. The point is, I want to think about the actual problem and the data involved, not about classes and interfaces. Most of the arguments against this mindset amount to "it's different than what I know". Problem #4: lag I now hand-wave us through to the part of the story where the netcode is somewhat operational. Right off the bat I ran into problems dealing with network lag. Games need to respond to players immediately, even if it takes 150ms to get a packet from the server. Projectiles were particularly useless under laggy network conditions. They were impossible to aim. I decided to re-use those 14 MB of world copies. When the server receives a command to fire a projectile, it steps the world back 150ms to the way the world appeared to the player when they hit the fire button. Then it simulates the projectile and steps the world forward until it's up to date with the present. That's where it creates the projectile. I ended up having the client create a fake projectile immediately, then as soon as it hears back from the server that the projectile was created, it deletes the fake and replaces it with the real thing. If all goes well, they should be in the same place due to the server's timey-wimey magic. Here it is in action. The fake projectile appears immediately but goes right through the wall. The server receives the message and fast-forwards the projectile straight to the part where it hits the wall. 150ms later the client gets the packet and sees the impact particle effect. The problem with netcode is, each mechanic requires a different approach to lag compensation. For example, my game has an "active armor" ability. If players react quick enough, they can reflect damage back at enemies. This breaks down in high lag scenarios. By the time the player sees the projectile hitting their character, the server has already registered the hit 100ms ago. The packet just hasn't made it to the client yet. This means you have to anticipate incoming damage and react long before it hits. Notice in the gif above how early I had to hit the button. To correct this, the server implements something I call "damage buffering". Instead of applying damage instantly, the server puts the damage into a buffer for 100ms, or whatever the round-trip time is to the client. At the end of that time, it either applies the damage, or if the player reacted, reflects it back. Here it is in action. You can see the 200ms delay between the projectile hitting me and the damage actually being applied. Here's another example. In my game, players can launch themselves at enemies. Enemies die instantly to perfect shots, but they deflect glancing blows and send you flying like this: Which direction should the player bounce? The client has to simulate the bounce before the server knows about it. The server and client need to agree which direction to bounce or they'll get out of sync, and they have no time to communicate beforehand. At first I tried quantizing the collision vector so that there were only six possible directions. This made it more likely that the client and server would choose the same direction, but it didn't guarantee anything. Finally I implemented another buffer system. Both client and server, when they detect a hit, enter a "buffer" state where the player sits and waits for the remote host to confirm the hit. To minimize jankiness, the server always defers to the client as to which direction to bounce. If the client never acknowledges the hit, the server acts like nothing happened and continues the player on their original course, fast-forwarding them to make up for the time they sat still waiting for confirmation. Problem #5: jitter My server sends out packets 60 times per second. What about players whose computers run faster than that? They'll see jittery animation. Interpolation is the industry-standard solution. Instead of immediately applying position data received from the server, you buffer it a little bit, then you blend smoothly between whatever data that you have. In my previous attempt at networked multiplayer, I tried to have each object keep track of its position data and smooth itself out. I ended up getting confused and it never worked well. This time, since I could already easily store the entire world state in a struct, I was able to write just two functions to make it work. One function takes two world states and blends them together. Another function takes a world state and applies it to the game. How big should the buffer delay be? I originally used a constant until I watched a video from the Overwatch devs where they mention adaptive interpolation delay. The buffer delay should smooth out not only the framerate from the server, but also any variance in packet delivery time. This was an easy win. Clients start out with a short interpolation delay, and any time they're missing a packet to interpolate toward, they increase their "lag score". Once it crosses a certain threshold, they tell the server to switch them to a higher interpolation delay. Of course, automated systems like this often act against the user's wishes, so it's important to add switches and knobs to the algorithm! Problem #6: joining servers mid-match Wait, I already have a way to serialize the entire game state. What's the hold up? Turns out, it takes more than one packet to serialize a fresh game state from scratch. And each packet may take multiple attempts to make it to the client. It may take a few hundred milliseconds to get the full state, and as we've seen already, that's an eternity. If the game is already in progress, that's enough time to send 20 packets' worth of new messages, which the client is not ready to process because it hasn't loaded yet. The solution is—you guessed it—another buffer. I changed the messaging system to support two separate streams of messages in the same packet. The first stream contains the map data, which is processed as soon as it comes in. The second stream is just the usual fire-hose of game messages that come in while the client is loading. The client buffers these messages until it's done loading, then processes them all until it's caught up. Problem #7: cross-cutting concerns This next part may be the most controversial. Remember that bit of gamedev wisdom from the beginning? "don't add networked multiplayer to an existing game"? Well, most of the netcode in this game is literally tacked on. It lives in its own 5000-line source file. It reaches into the game, pokes stuff into memory, and the game renders it. Just listen a second before stoning me. Is it better to group all network code in one place, or spread it out inside each game object? I think both approaches have advantages and disadvantages. In fact, I use both approaches in different parts of the game, for various reasons human and technical. But some design paradigms (*cough* OOP) leave no room for you to make this decision. Of course you put the netcode inside the object! Its data is private, so you'll have to write an interface to access it anyway. Might as well put all the smarts in there too. Conclusion I'm not saying you should write netcode like I do; only that this approach has worked for me so far. Read the code and judge for yourself. There is an objectively optimal approach for each use case, although people may disagree on which one it is. You should be free to choose based on actual constraints rather than arbitrary ones set forth by some paradigm. Thanks for reading. DECEIVER is launching on Kickstarter soon. Sign up to play the demo here!
  2. evanofsky

    The Poor Man's Netcode

    The more you know about a given topic, the more you realize that no one knows anything. For some reason (why God, why?) my topic of choice is game development. Everyone in that field agrees: don't add networked multiplayer to an existing game, you drunken clown. Well, I did it anyway because I hate myself. Somehow it turned out great. None of us know anything. Problem #1: assets My first question was: how do I tell a client to use such-and-such mesh to render an object? Serialize the whole mesh? Nah, they already have it on disk. Send its filename? Nah, that's inefficient and insecure. Okay, just a string identifier then? Fortunately, before I had time to implement any of my own terrible ideas, I watched a talk from Mike Acton where he mentions the danger of "lazy decision-making". One of his points was: strings let you lazily ignore decisions until runtime, when it's too late to fix. If I rename a texture, I don't want to get a bug report from a player with a screenshot like this: I had never thought about how powerful and complex strings are. Half the field of computer science deals with strings and what they can do. They usually require a heap allocation, or something even more complex like ropes and interning. I usually don't bother to limit their length, so a single string expands the possibility space to infinity, destroying whatever limited ability I had to predict runtime behavior. And here I am using these complex beasts to identify objects. Heck, I've even used strings to access object properties. What madness! Long story short, I cultivated a firm conviction to avoid strings where possible. I wrote a pre-processor that outputs header files like this at build time: namespace Asset { namespace Mesh { const int count = 3; const AssetID player = 0; const AssetID enemy = 1; const AssetID projectile = 2; } } So I can reference meshes like this: renderer->mesh = Asset::Mesh::player; If I rename a mesh, the compiler makes it my problem instead of some poor player's problem. That's good! The bad news is, I still have to interact with the file system, which requires the use of strings. The good news is the pre-processor can save the day. const char* Asset::Mesh::filenames[] = { "assets/player.msh", "assets/enemy.msh", "assets/projectile.msh", 0, }; With all this in place, I can easily send assets across the network. They're just numbers! I can even verify them. if (mesh < 0 || mesh >= Asset::Mesh::count) net_error(); // just what are you trying to pull, buddy? Problem #2: object references My next question was: how do I tell a client to please move/delete/frobnicate "that one object from before, you know the one". Once again, I was lucky enough to hear from smart people before I could shoot myself in the foot. From the start, I knew I needed a bunch of lists of different kinds of objects, like this: Array<Turret> Turret::list; Array<Projectile> Projectile::list; Array<Avatar> Avatar::list; Let's say I want to reference the first object in the Avatar list, even without networking, just on our local machine. My first idea is to just use a pointer: Avatar* avatar; avatar = &Avatar::list[0]; This introduces a ton of non-obvious problems. First, I'm compiling for a 64 bit architecture, which means that pointer takes up 8 whole bytes of memory, even though most of it is probably zeroes. And memory is the number one performance bottleneck in games. Second, if I add enough objects to the array, it will get reallocated to a different place in memory, and the pointer will point to garbage. Okay, fine. I'll use an ID instead. template<typename Type> struct Ref { short id; inline Type* ref() { return &Type::list[id]; } // overloaded "=" operator omitted }; Ref<Avatar> avatar = &Avatar::list[0]; avatar.ref()->frobnicate(); Second problem: if I remove that Avatar from the list, some other Avatar will get moved into its place without me knowing. The program will continue, blissfully and silently screwing things up, until some player sends a bug report that the game is "acting weird". I much prefer the program to explode instantly so I at least get a crash dump with a line number. Okay, fine. Instead of actually removing the avatar, I'll put a revision number on it: struct Avatar { short revision; }; template<typename Type> struct Ref { short id; short revision; inline Type* ref() { Type* t = &Type::list[id]; return t->revision == revision ? t : nullptr; } }; Instead of actually deleting the avatar, I'll mark it dead and increment the revision number. Now anything trying to access it will give a null pointer exception. And serializing a reference across the network is just a matter of sending two easily verifiable numbers. Problem #3: delta compression If I had to cut this article down to one line, it would just be a link to Glenn Fiedler's blog. Which by the way is here: gafferongames.com As I set out to implement my own version of Glenn's netcode, I read this article, which details one of the biggest challenges of multiplayer games. Namely, if you just blast the entire world state across the network 60 times a second, you could gobble up 17 mbps of bandwidth. Per client. Delta compression is one of the best ways to cut down bandwidth usage. If a client already knows where an object is, and it hasn't moved, then I don't need to send its position again. This can be tricky to get right. The first part is the trickiest: does the client really know where the object is? Just because I sent the position doesn't mean the client actually received it. The client might send an acknowledgement back that says "hey I received packet #218, but that was 0.5 seconds ago and I haven't gotten anything since." So to send a new packet to that client, I have to remember what the world looked like when I sent out packet #218, and delta compress the new packet against that. Another client might have received everything up to packet #224, so I can delta compress the new packet differently for them. Point is, we need to store a whole bunch of separate copies of the entire world. Someone on Reddit asked "isn't that a huge memory hog"? No, it is not. Actually I store 255 world copies in memory. All in a single giant array. Not only that, but each copy has enough room for the maximum number of objects (2048) even if only 2 objects are active. If you store an object's state as a position and orientation, that's 7 floats. 3 for XYZ coordinates and 4 for a quaternion. Each float takes 4 bytes. My game supports up to 2048 objects. 7 floats * 4 bytes * 2048 objects * 255 copies = ... 14 MB. That's like, half of one texture these days. I can see myself writing this system five years ago in C#. I would start off immediately worried about memory usage, just like that Redditor, without stopping to think about the actual data involved. I would write some unnecessary, crazy fancy, bug-ridden compression system. Taking a second to stop and think about actual data like this is called Data-Oriented Design. When I talk to people about DOD, many immediately say, "Woah, that's really low-level. I guess you want to wring out every last bit of performance. I don't have time for that. Anyway, my code runs fine." Let's break down the assumptions in this statement. Assumption 1: "That's really low-level". Look, I multiplied four numbers together. It's not rocket science. Assumption 2: "You sacrifice readability and simplicity for performance." Let's picture two different solutions to this netcode problem. For clarity, let's pretend we only need 3 world copies, each containing up to 2 objects. Here's the solution I just described. Everything is statically allocated in the .bss segment. It never moves around. Everything is the same size. No pointers at all. Here's the idiomatic C# solution. Everything is scattered randomly throughout the heap. Things can get reallocated or moved right in the middle of a frame. The array is jagged. 64-bit pointers all over the place. Which is simpler? The second diagram is actually far from exhaustive. C#-land is a lot more complex in reality. Check the comments and you'll probably find someone correcting me about how C# actually works. But that's my point. With my solution, I can easily construct a "good enough" mental model to understand what's actually happening on the machine. I've barely scratched the surface with the C# solution. I have no idea how it will behave at runtime. Assumption 3: "Performance is the only reason you would code like this." To me, performance is a nice side benefit of data-oriented design. The main benefit is clarity of thought. Five years ago, when I sat down to solve a problem, my first thought was not about the problem itself, but how to shoehorn it into classes and interfaces. I witnessed this analysis paralysis first-hand at a game jam recently. My friend got stuck designing a grid for a 2048-like game. He couldn't figure out if each number was an object, or if each grid cell was an object, or both. I said, "the grid is an array of numbers. Each operation is a function that mutates the grid." Suddenly everything became crystal clear to him. Assumption 4: "My code runs fine". Again, performance is not the main concern, but it's important. The whole world switched from Firefox to Chrome because of it. Try this experiment: open up calc.exe. Now copy a 100 MB file from one folder to another. I don't know what calc.exe is doing during that 300ms eternity, but you can draw your own conclusions from my two minutes of research: calc.exe actually launches a process called Calculator.exe, and one of the command line arguments is called "-ServerName". Does calc.exe "run fine"? Did throwing a server in simplify things at all, or is it just slower and more complex? I don't want to get side-tracked. The point is, I want to think about the actual problem and the data involved, not about classes and interfaces. Most of the arguments against this mindset amount to "it's different than what I know". Problem #4: lag I now hand-wave us through to the part of the story where the netcode is somewhat operational. Right off the bat I ran into problems dealing with network lag. Games need to respond to players immediately, even if it takes 150ms to get a packet from the server. Projectiles were particularly useless under laggy network conditions. They were impossible to aim. I decided to re-use those 14 MB of world copies. When the server receives a command to fire a projectile, it steps the world back 150ms to the way the world appeared to the player when they hit the fire button. Then it simulates the projectile and steps the world forward until it's up to date with the present. That's where it creates the projectile. I ended up having the client create a fake projectile immediately, then as soon as it hears back from the server that the projectile was created, it deletes the fake and replaces it with the real thing. If all goes well, they should be in the same place due to the server's timey-wimey magic. Here it is in action. The fake projectile appears immediately but goes right through the wall. The server receives the message and fast-forwards the projectile straight to the part where it hits the wall. 150ms later the client gets the packet and sees the impact particle effect. The problem with netcode is, each mechanic requires a different approach to lag compensation. For example, my game has an "active armor" ability. If players react quick enough, they can reflect damage back at enemies. This breaks down in high lag scenarios. By the time the player sees the projectile hitting their character, the server has already registered the hit 100ms ago. The packet just hasn't made it to the client yet. This means you have to anticipate incoming damage and react long before it hits. Notice in the gif above how early I had to hit the button. To correct this, the server implements something I call "damage buffering". Instead of applying damage instantly, the server puts the damage into a buffer for 100ms, or whatever the round-trip time is to the client. At the end of that time, it either applies the damage, or if the player reacted, reflects it back. Here it is in action. You can see the 200ms delay between the projectile hitting me and the damage actually being applied. Here's another example. In my game, players can launch themselves at enemies. Enemies die instantly to perfect shots, but they deflect glancing blows and send you flying like this: Which direction should the player bounce? The client has to simulate the bounce before the server knows about it. The server and client need to agree which direction to bounce or they'll get out of sync, and they have no time to communicate beforehand. At first I tried quantizing the collision vector so that there were only six possible directions. This made it more likely that the client and server would choose the same direction, but it didn't guarantee anything. Finally I implemented another buffer system. Both client and server, when they detect a hit, enter a "buffer" state where the player sits and waits for the remote host to confirm the hit. To minimize jankiness, the server always defers to the client as to which direction to bounce. If the client never acknowledges the hit, the server acts like nothing happened and continues the player on their original course, fast-forwarding them to make up for the time they sat still waiting for confirmation. Problem #5: jitter My server sends out packets 60 times per second. What about players whose computers run faster than that? They'll see jittery animation. Interpolation is the industry-standard solution. Instead of immediately applying position data received from the server, you buffer it a little bit, then you blend smoothly between whatever data that you have. In my previous attempt at networked multiplayer, I tried to have each object keep track of its position data and smooth itself out. I ended up getting confused and it never worked well. This time, since I could already easily store the entire world state in a struct, I was able to write just two functions to make it work. One function takes two world states and blends them together. Another function takes a world state and applies it to the game. How big should the buffer delay be? I originally used a constant until I watched a video from the Overwatch devs where they mention adaptive interpolation delay. The buffer delay should smooth out not only the framerate from the server, but also any variance in packet delivery time. This was an easy win. Clients start out with a short interpolation delay, and any time they're missing a packet to interpolate toward, they increase their "lag score". Once it crosses a certain threshold, they tell the server to switch them to a higher interpolation delay. Of course, automated systems like this often act against the user's wishes, so it's important to add switches and knobs to the algorithm! Problem #6: joining servers mid-match Wait, I already have a way to serialize the entire game state. What's the hold up? Turns out, it takes more than one packet to serialize a fresh game state from scratch. And each packet may take multiple attempts to make it to the client. It may take a few hundred milliseconds to get the full state, and as we've seen already, that's an eternity. If the game is already in progress, that's enough time to send 20 packets' worth of new messages, which the client is not ready to process because it hasn't loaded yet. The solution is—you guessed it—another buffer. I changed the messaging system to support two separate streams of messages in the same packet. The first stream contains the map data, which is processed as soon as it comes in. The second stream is just the usual fire-hose of game messages that come in while the client is loading. The client buffers these messages until it's done loading, then processes them all until it's caught up. Problem #7: cross-cutting concerns This next part may be the most controversial. Remember that bit of gamedev wisdom from the beginning? "don't add networked multiplayer to an existing game"? Well, most of the netcode in this game is literally tacked on. It lives in its own 5000-line source file. It reaches into the game, pokes stuff into memory, and the game renders it. Just listen a second before stoning me. Is it better to group all network code in one place, or spread it out inside each game object? I think both approaches have advantages and disadvantages. In fact, I use both approaches in different parts of the game, for various reasons human and technical. But some design paradigms (*cough* OOP) leave no room for you to make this decision. Of course you put the netcode inside the object! Its data is private, so you'll have to write an interface to access it anyway. Might as well put all the smarts in there too. Conclusion I'm not saying you should write netcode like I do; only that this approach has worked for me so far. Read the code and judge for yourself. There is an objectively optimal approach for each use case, although people may disagree on which one it is. You should be free to choose based on actual constraints rather than arbitrary ones set forth by some paradigm. Thanks for reading. DECEIVER is launching on Kickstarter soon. Sign up to play the demo here!
  3. evanofsky

    Nimbatus - How a free demo got our game funded

    This article is amazing, thank you for this! I'm in a similar situation, looking at doing a Kickstarter, and while I doubt I'll be able to do as well as you guys, it's very encouraging to see what's possible even with only a small following. Everyone these days is preaching doom and gloom unless you have 20,000 newsletter signups before you even start.
  4. evanofsky

    DECEIVER - announce trailer!

    After two and a half years, it's finally, officially announced. And it got some attention! Here's PC Gamer: http://www.pcgamer.com/deceiver-is-a-philosophical-shooter-that-lets-you-shoot-drones-through-enemies/ And Rock Paper Shotgun: https://www.rockpapershotgun.com/2018/01/29/deceiver-is-a-neat-looking-parkour-game-with-added-spiderbots/ And two articles on Kotaku! https://kotaku.com/1822533385 https://www.kotaku.com.au/2018/01/854413/ The bad news is, I'm completely broke. Will I pull off this crazy thing? Stay tuned to find out.
  5. evanofsky

    The Poor Man's 3D Camera

    Each of us have our own giants to face. This is a story about one of my giants. Something I never imagined could make a grown man cry, until it did. A 3D camera. No one can face your giants for you. This is a story, not a walkthrough. Expect no useful information. For that I recommend 50 Game Camera Mistakes by thatgamecompany's John Nesky. His job title is literally "camera designer". The story starts in 2014 with a seemingly harmless seven-day game jam. 7DFPS 2014 Exhausted from crunching on my first game, I decide it would be good to take a break and make a first-person shooter for 7DFPS. This is a terrible decision. The idea is simple: an FPS where you are the bullet. Click to jump from wall to wall. If someone's head happens to be between you and the next wall, sucks for them. The main problem is that, when you shoot to a new surface, the game necessarily buries your face directly into that surface, filling the whole screen with a solid blue color. You can see my first solution above. After landing, the game "reflects" your direction off the surface you landed on, and tweens the camera to face the new direction. So when you shoot into a wall head-on, the camera does a complete 180. Of course, I can't place the camera directly on the surface, or there would be clipping artifacts. So I space the camera a bit out from the wall, which leads to another problem: Here the extra space between the camera and the surface allows you to aim at any location on the surface and launch there. In theory, your "character" is actually flush with the surface, which should make this physically impossible. I throw a band-aid on this problem by clamping the camera so that it can't aim at or behind the surface it's attached to. I end up working 96 hours in a week to finish this masterpiece that will surely take the world by storm. Two YouTubers play it for a grand total of 10,000 views. Like every good vacation, the experience leaves me exhausted and depressed on Monday morning. Over the next 8 months, I finish my other game, take an actual vacation, then, instead of throwing away the Unity prototype like any sane person would, I start rebuilding it in a brand new custom C++ engine. The new game is an expansion of the prototype, which means more features, more gameplay, more pain and suffering! One of the first new features is the ability to crawl along walls, floors, and ceilings: A couple months later, camera problems start to manifest. As players crawl around different surfaces, I have to forcefully nudge the camera to keep it from staring straight into a wall. I use spherical linear interpolation (slerp) to smooth out the transition between angled surfaces, but it's still not great. In the gif below, the player on the right does not manually move their camera at all; the wall forces them to look up automatically. At this point, everything is completely unreadable and confusing. The first-person view prevents players from seeing their character, and they often take several minutes to realize they're playing as a creepy crawly spider bot. Once they finally get it, they can't figure out why the camera keeps getting forced around, and why they can't aim at the surface they're attached to. I slap on another band-aid in the form of a shader that blacks out everything "behind" you, everything that's physically impossible to reach. I also have it darken out of range areas. Now the game is not only unreadable and confusing, but also ugly! By sheer chance, I happen to turn on a debug flag that moves the camera back from the player two units. Literally the poor man's third-person camera. And behold, it was not the worst thing in the world. I finally realize I'm making a third-person game, and so set about implementing it for real. The Culling Saga One of the deadliest scourges of humanity, right after Facebook and taxes, is a third-person camera that constantly zooms and bumps around to avoid in-game geometry. Rather than move the camera, I decide to just cull anything that blocks the camera's view. Half of the job is done right out the gate because my game engine, like most engines, automatically culls triangles that face away from the camera (also known as "back-faces"). I write a shader to handle the rest. It culls everything within a certain distance of the camera. What's that you say? My screenshots look weird with spider bots floating in empty space because the wall got culled? It'll be fine. It's fine. Don't worry about it. What, what now? You can catch glimpses of stuff you're not supposed to see, including the skybox? Look, I'm sure it's just a minor thing. Okay. That's pretty bad. What if instead of carving spheres around the player and camera, I use a cylinder instead? And what if I actually render a solid black cylinder whenever the camera is inside a wall? So close, yet so far. The cylinder blocks out most of the ugly geometry it needs to, but sometimes you can still see through things. I could extend the cylinder to block more, but I would have to clip it so it stays inside the level geometry. Even now, the cylinder sometimes blocks too much, because I only clip it against the player's current wall. Here I only want the cylinder to block the ugly insides of the ledge. Instead it blocks everything behind the player. I would have to query all the geometry surrounding the player and clip the cylinder against all those planes. And that's assuming I can figure out what's "inside" and "outside" the level geometry, which is not air-tight. There are overlapping faces, shapes that clip into each other, and plenty of other lazy hacks— I mean, clever tricks. What if I just turn off back-face culling? Use the same culling shader, and instead of the cylinder, rely on the back-faces to block anything you're not supposed to see. This works perfectly in some cases: Other times it does a great job of showing off my lazy hacks, like this ramp that simply extends into the floor rather than meeting it cleanly: It doesn't help that the back-faces still receive accurate lighting. Feels like the level is a hollow shell. Fine. I'll spend a couple weeks welding level geometry together, and I'll render the back-faces in solid black. How about now? Getting less terrible all the time. With a little depth buffer bias, a special tag in the G-buffer for back-faces, and a few extra texture taps in the composite shader, I can filter out those pesky lines too: The gifs above also show off the new culling shape. Instead of a cylinder, which indiscriminately carves out an uncouth shape much larger than necessary: I switch to a cone, to ensure I only cull things what need culling. But the cone has problems too. It's possible for the player to back up so close to a wall that the cone intersects the wall in an incredibly narrow circle, leaving a tiny hole for the player to peer through. From somewhere deep inside my repressed memories of Calc II springs an exotic mathematical creature known as a paraboloid: A quick trip to the grimoire to remind myself of the formula, a few incantations in a darkened room with a mirror, and the creature is summoned into GLSL form. The culled circle still tapers a little, but it's enough to see fairly well. Good enough for government work. Ship it! UX UX is, like, super important. Take it from me, a full-stack growth-hacking happiness engineer co-founder. Let me tell you about Our Incredible Journey. At first, the game had a terrible retention rate. I did some A/B testing, streamlined the user onboarding, pivoted to clickbait, bought some fraudulent traffic, and now I'm at $10k MRR. I'm also taking cold showers every morning and learning Farsi on Duolingo. If you can keep your eyes from rolling back completely and irreparably inside your head, "User Experience" is a decent descriptor for the interactive aspects of game development, the other contender being "Game Feel". It's the thing you can only get by interacting with the software yourself. It's the thing that makes you smile with surprise when you pick up the controller for the first time, even though you've spent the last hour watching someone else play. It's also basically impossible for me to get right on the first ten tries. Here's an example of my terrible camera UX. When you hit an enemy player without killing them, you bounce off. The camera instantly snaps around to follow your new trajectory. Of course I slerp the rotation a little, but still, you can barely see it happen. I have no reason to lock the camera like this, it's just a carry-over from when the game was first-person, when it made sense to always point the camera where the player was headed next. It takes someone at a convention telling me what an idiot I am to make me finally unlock the camera. A one-line change that takes a whole year. Later, someone on Twitch yells out a self-evidently brilliant suggestion: let the camera lag behind the player a little when they launch. This also takes only a handful of lines to test out, although the numbers don't feel good until a week or two of tweaking. So many of my design decisions are simply carried over from old and outdated assumptions, even though they stopped making sense several versions ago. Here's an example. Remember how I clamp the camera rotation to keep the player from aiming at the surface they're currently attached to? Turns out this gets old after a while, especially in third-person. People want freedom, they don't want you shoving their camera around. I grudgingly loosen the shackles a bit by clamping the rotation against a cone instead of a plane. I use spherical interpolation again to smoothly swing the cone around and nudge the camera away from the wall. Unfortunately, slerp doesn't always rotate the way you want it to. Sometimes the cone pushes the camera in a completely different direction as it rotates to match the surface. Instead of rotating the cone, I decide to instantly snap it to the surface, but scale it up slowly. So it starts as a thin sliver and slowly expands into a fat cone, pushing the camera out as it grows. Everything's going just swimmingly until Momin Khan asks me... is it even necessary to clamp the camera any more? It made sense when the game was first-person, but that was two years ago. I immediately get defensive. How else can I keep the camera from staring straight at the wall for 90% of the game? But I slowly realize he's right. I compromise in the end. I nudge the camera for a split second after landing, then shrink the cone back down to zero to allow glorious unfettered camera freedom. Full range of motion, baybee Now that the player can aim at their currently attached surface, what happens when they try to launch there? I eventually solve this problem by allowing spider bots to "dash" along the surface. I have no physical explanation for how they do it. It's a video game. There's another problem, however. Normally, the game does a raycast starting at the third-person camera and continuing straight through the reticle. When the spider bot launches, it compensates and launches where the player wants it to go, like this: However, sometimes the camera can see and aim at a point the spider bot can't reach. I solve this by ruling everything behind the player's current surface "out of bounds". I even have a shader to darken the forbidden areas. Look, Simba. Everything the light touches is our kingdom. This is really frustrating and confusing to people. Finally Nathan Fouts from Mommy's Best Games tells me to just cheat and let the player go there even if it doesn't make physical sense. I don't like the idea of breaking my game physics so I compromise by having the spider bot dash to the edge and then fly the rest of the way. It all happens so fast, you don't even notice it. Here it is in slow motion: Conclusion Some tasks cannot be sped up. If I had put "make 3D camera" on a Gantt chart, I would have stopped 10% of the way through this article and called it good. The point is, most of these ideas are simple and obvious in hindsight, but I had to make a lot of mistakes to find them. It's nearly impossible to coalesce good design decisions straight from the ether. The only reliable method is iteration and trial and error. Thanks for reading. What embarrassingly obvious design improvements have you been slapped in the face with? Deceiver is set to launch on Kickstarter in early February 2018. Join the mail list to be notified of its release, or check out the source code on GitHub.
  6. evanofsky

    Thirteen Years of Bad Game Code

    Alone on a Friday night, in need of some inspiration, you decide to relive some of your past programming conquests. The old archive hard drive slowly spins up, and the source code of the glory days scrolls by... Oh no. This is not at all what you expected. Were things really this bad? Why did no one tell you? Why were you like this? Is it even possible to have that many gotos in a single function? You quickly close the project. For a brief second, you consider deleting it and scrubbing the hard drive. What follows is a compilation of lessons, snippets, and words of warning salvaged from my own excursion into the past. Names have not been changed, to expose the guilty. 2004 I was thirteen. The project was called Red Moon -- a wildly ambitious third-person jet combat game. The few bits of code that were not copied verbatim out of Developing Games in Java were patently atrocious. Let's look at an example. I wanted to give the player multiple weapons to switch between. The plan was to rotate the weapon model down inside the player model, swap it out for the next weapon, then rotate it back. Here's the animation code. Don't think about it too hard.public void updateAnimation(long eTime) { if(group.getGroup("gun") == null) { group.addGroup((PolygonGroup)gun.clone()); } changeTime -= eTime; if(changing && changeTime = 0; i = Math.Min(this.bindings.Count - 1, i - 1)) this.bindings.OnChanged(this); } }} Every single field in the game, down to the last boolean, had an unwieldy dynamically allocated array attached to it. Take a look at the loop that notifies the bindings of a property change to get an idea of the issues I ran into with this paradigm. It has to iterate through the binding list backward, since a binding could actually add or delete UI elements, causing the binding list to change. Still, I loved data binding so much that I built the entire game on top of it. I broke down objects into components and bound their properties together. Things quickly got out of hand.jump.Add(new Binding(jump.Crouched, player.Character.Crouched));jump.Add(new TwoWayBinding(player.Character.IsSupported, jump.IsSupported));jump.Add(new TwoWayBinding(player.Character.HasTraction, jump.HasTraction));jump.Add(new TwoWayBinding(player.Character.LinearVelocity, jump.LinearVelocity));jump.Add(new TwoWayBinding(jump.SupportEntity, player.Character.SupportEntity));jump.Add(new TwoWayBinding(jump.SupportVelocity, player.Character.SupportVelocity));jump.Add(new Binding(jump.AbsoluteMovementDirection, player.Character.MovementDirection));jump.Add(new Binding(jump.WallRunState, wallRun.CurrentState));jump.Add(new Binding(jump.Rotation, rotation.Rotation));jump.Add(new Binding(jump.Position, transform.Position));jump.Add(new Binding(jump.FloorPosition, floor));jump.Add(new Binding(jump.MaxSpeed, player.Character.MaxSpeed));jump.Add(new Binding(jump.JumpSpeed, player.Character.JumpSpeed));jump.Add(new Binding(jump.Mass, player.Character.Mass));jump.Add(new Binding(jump.LastRollKickEnded, rollKickSlide.LastRollKickEnded));jump.Add(new Binding(jump.WallRunMap, wallRun.WallRunVoxel));jump.Add(new Binding(jump.WallDirection, wallRun.WallDirection));jump.Add(new CommandBinding(jump.WalkedOn, footsteps.WalkedOn));jump.Add(new CommandBinding(jump.DeactivateWallRun, (Action)wallRun.Deactivate));jump.FallDamage = fallDamage;jump.Predictor = predictor;jump.Bind(model);jump.Add(new TwoWayBinding(wallRun.LastWallRunMap, jump.LastWallRunMap));jump.Add(new TwoWayBinding(wallRun.LastWallDirection, jump.LastWallDirection));jump.Add(new TwoWayBinding(rollKickSlide.CanKick, jump.CanKick));jump.Add(new TwoWayBinding(player.Character.LastSupportedSpeed, jump.LastSupportedSpeed));wallRun.Add(new Binding(wallRun.IsSwimming, player.Character.IsSwimming));wallRun.Add(new TwoWayBinding(player.Character.LinearVelocity, wallRun.LinearVelocity));wallRun.Add(new TwoWayBinding(transform.Position, wallRun.Position));wallRun.Add(new TwoWayBinding(player.Character.IsSupported, wallRun.IsSupported));wallRun.Add(new CommandBinding(wallRun.LockRotation, (Action)rotation.Lock));wallRun.Add(new CommandBinding(wallRun.UpdateLockedRotation, rotation.UpdateLockedRotation));vault.Add(new CommandBinding(wallRun.Vault, delegate() { vault.Go(true); }));wallRun.Predictor = predictor;wallRun.Add(new Binding(wallRun.Height, player.Character.Height));wallRun.Add(new Binding(wallRun.JumpSpeed, player.Character.JumpSpeed));wallRun.Add(new Binding(wallRun.MaxSpeed, player.Character.MaxSpeed));wallRun.Add(new TwoWayBinding(rotation.Rotation, wallRun.Rotation));wallRun.Add(new TwoWayBinding(player.Character.AllowUncrouch, wallRun.AllowUncrouch));wallRun.Add(new TwoWayBinding(player.Character.HasTraction, wallRun.HasTraction));wallRun.Add(new Binding(wallRun.LastWallJump, jump.LastWallJump));wallRun.Add(new Binding(player.Character.LastSupportedSpeed, wallRun.LastSupportedSpeed));player.Add(new Binding(player.Character.WallRunState, wallRun.CurrentState));input.Bind(rollKickSlide.RollKickButton, settings.RollKick);rollKickSlide.Add(new Binding(rollKickSlide.EnableCrouch, player.EnableCrouch));rollKickSlide.Add(new Binding(rollKickSlide.Rotation, rotation.Rotation));rollKickSlide.Add(new Binding(rollKickSlide.IsSwimming, player.Character.IsSwimming));rollKickSlide.Add(new Binding(rollKickSlide.IsSupported, player.Character.IsSupported));rollKickSlide.Add(new Binding(rollKickSlide.FloorPosition, floor));rollKickSlide.Add(new Binding(rollKickSlide.Height, player.Character.Height));rollKickSlide.Add(new Binding(rollKickSlide.MaxSpeed, player.Character.MaxSpeed));rollKickSlide.Add(new Binding(rollKickSlide.JumpSpeed, player.Character.JumpSpeed));rollKickSlide.Add(new Binding(rollKickSlide.SupportVelocity, player.Character.SupportVelocity));rollKickSlide.Add(new TwoWayBinding(wallRun.EnableEnhancedWallRun, rollKickSlide.EnableEnhancedRollSlide));rollKickSlide.Add(new TwoWayBinding(player.Character.AllowUncrouch, rollKickSlide.AllowUncrouch));rollKickSlide.Add(new TwoWayBinding(player.Character.Crouched, rollKickSlide.Crouched));rollKickSlide.Add(new TwoWayBinding(player.Character.EnableWalking, rollKickSlide.EnableWalking));rollKickSlide.Add(new TwoWayBinding(player.Character.LinearVelocity, rollKickSlide.LinearVelocity));rollKickSlide.Add(new TwoWayBinding(transform.Position, rollKickSlide.Position));rollKickSlide.Predictor = predictor;rollKickSlide.Bind(model);rollKickSlide.VoxelTools = voxelTools;rollKickSlide.Add(new CommandBinding(rollKickSlide.DeactivateWallRun, (Action)wallRun.Deactivate));rollKickSlide.Add(new CommandBinding(rollKickSlide.Footstep, footsteps.Footstep)); I ran into tons of problems. I created binding cycles that caused infinite loops. I found out that initialization order is often important, and initialization is a nightmare with data binding, with some properties getting initialized multiple times as bindings are added. When it came time to add animation, I found that data binding made it difficult and non-intuitive to animate between two states. And this isn't just me. Watch this Netflix talk which gushes about how great React is before explaining how they have to turn it off any time they run an animation. I too realized the power of turning a binding on or off, so I added a new field:class Binding{ public bool Enabled;} Unfortunately, this defeated the purpose of data binding. I wanted to get rid of UI state, and this code actually added some. How can I eliminate this state? I know! Data binding!class Binding{ public Property Enabled = new Property { Value = true };} Yes, I really did try this briefly. It was bindings all the way down. I soon realized how crazy it was. How can we improve on data binding? Try making your UI actually functional and stateless. dear imgui is a great example of this. Separate behavior and state as much as possible. Avoid techniques that make it easy to create state. It should be a pain for you to create state. Conclusion There are many, many more embarrassing mistakes to discuss. I discovered another "creative" method to avoid globals. For some time I was obsessed with closures. I designed an "entity" "component" "system" that was anything but. I tried to multithread a voxel engine by sprinkling locks everywhere. Here's the takeaway: Make decisions upfront instead of lazily leaving them to the computer. Separate behavior and state. Write pure functions. Write the client code first. Write boring code. That's my story. What horrors from your past are you willing to share? If you enjoyed this article, try these: The Poor Man's Voxel Engine The Poor Man's Character Controller One Weird Trick to Write Better Code
  7. evanofsky

    Thirteen Years of Bad Game Code

    I think we're in agreement; we just have different definitions of what a singleton is. To me, a singleton is defined as a function that lazily initializes a single global instance of an object. Without that lazy check, it's just... a regular global. I am totally cool with grouping global variables into a structure or object to "logically split them out and divy up code and responsibilities". I do it all the time. I only take issue when you put a singleton in front of it.
  8. Best explanation I've seen of quaternions, complete with Blender practical application tips https://t.co/MVxF1Pb5cE
  9. RT @MarkPuente: The best letter to the editor in today's @TB_Times. https://t.co/E60kou9BXw
  10. woah guys i actually kinda did an animation https://t.co/6Q7hoBIm1R
  11. RT @Oniropolis: Never tire of this. The video game as poetry. https://t.co/VsAX4olICo https://t.co/nFycOUPXp3
  12. YES! More awesome devs coming to the Midwest! We live cheap! And we don't even mind if you pick that state up north https://t.co/R1Dhjdq22V
  13. What percentage of a relationship would you say happens over text these days?
  14. Anyone planning on attending Handmade Con 2016? https://t.co/x3Rs6ruCdw
  15. Allow me to regale you with an exciting tale: the birth of a janky dialogue and voice system. I have a JSON file with all the localized strings in my game, like this:{ "danger": "Danger", "level": "Level %d", ...} A preprocessor takes this and generates a header file with integer constants for each string, like this:namespace strings{ const int danger = 0; const int level = 1; // ...} At runtime, it loads the JSON file and hooks up the integer IDs to localized strings. A function called "_" takes an integer ID and returns the corresponding localized string. I use it like this:draw_string(_(strings::danger), position); This all worked (and still works) pretty well for UI strings. Not so much for dialogue. To write dialogue, I had to come up with a unique ID for each line, then add it to the strings file, like this:{ "hello_penelope": "Hello! I am Penelope.", "nice_meet_you": "Nice to meet you.", ...} Yes, the preprocessor generated a new integer ID in the header file every time I added a line of dialogue. Gross. I construct dialogue trees in Dialogger. With this setup, I had to use IDs like "hello_penelope" rather than actual English strings. Also gross. A better way I keep the string system, but extend it to support "dynamic" strings loaded at runtime that do not have integer IDs in the header file. Now I can write plain English in the dialogue trees. The preprocessor goes through all of them and extracts the strings into a separate JSON file, using the SHA-1 hash of each string for its ID. Once everything is loaded, I discard all string IDs in favor of integer IDs. I couldn't find a simple straightforward SHA-1 implementation that worked on plain C strings, so here's one for you. The point of all this is: I now have a single JSON file containing all the dialogue in the game. Ripe for automation... Speak and spell Penelope is an AI character. I'm using text-to-speech for her voice, at least for now. I don't want to integrate a text-to-speech engine in the game; that's way too much work. And I don't want to manually export WAVs from a text-to-speech program. Also too much work. I create a free IBM Bluemix account. They have a dead simple text-to-speech API: make an HTTP request with basic HTTP authentication, get a WAV file back. I write an 82-line Python script that goes through all the dialogue strings and makes an HTTP request for each one. It keeps track of which strings have previously been voiced, to facilitate incremental updates. Now I have a folder of WAV files, each one named after a SHA-1 hash. I'm using Wwise for audio, so the next step requires a bit of manual involvement. I drag all the WAVs into the project and batch create events for them. Now when I display a dialogue string, I just have to look up the SHA-1 hash and play the audio event. Easy. Disaster strikes I don't hear anything. All signs indicate the audio is playing correctly, but nothing comes out of my speakers. I look at one of the audio files in Wwise. Looks like the file is corrupted. I play the WAV in a number of different programs. Some play it fine, others don't play it at all. I edit my text-to-speech script to use Python's wave library to load the WAV file after downloading it from IBM. Sure enough, the library doesn't know what to make of it. Too lazy to care, I edit the wave library in-place in my Python distribution. YOLO. After a bit of printf debugging, I pinpoint the issue. The WAV format is based on RIFF, a binary format which breaks the file into "chunks". According to Wikipedia, the format of each chunk is as follows: 4 bytes: an ASCII identifier for this chunk (examples are "fmt " and "data"; note the space in "fmt "). 4 bytes: an unsigned, little-endian 32-bit integer with the length of this chunk (except this field itself and the chunk identifier). variable-sized field: the chunk data itself, of the size given in the previous field. a pad byte, if the chunk's length is not even. Turns out, IBM's text-to-speech API generates streaming WAV files, which means it sets the "length" field to 0. Some WAV players can handle it, while others choke. Wwise falls in the latter category. Fortunately, I can easily figure out the chunk length based on the file size, modify it using the wave library, and write it back out to the WAV file. Like so. Problem solved. Wwise is happy. Next I set up some Wwise callbacks to detect the current volume of Penelope's voice, and when she's done speaking. Here's the result, along with some rope physics in the background being destroyed by the wonky framerate caused by my GIF recorder: If you want to hear it, check out the IBM text-to-speech demo here. Thanks for reading! Mirrored on my blog
  16. RT @ADAMATOMIC: @fasterthanlime "dada are you just randomly trying different sin() / cos() -1/1 variations until you get what you want" "..…
  17. It's not just cygwin, it's actual Linux binaries running on Windows. It intercepts and translates Linux syscalls. https://t.co/YlloZZTGwl
  18. nav mesh generation now under 30 seconds https://t.co/5SW9aPp5om
  19. RT @mitsuhiko: Only in javascript land can you implement "is positive integer" with three dependencies. https://t.co/Rh5V1DcFEh
  20. The game industry hit Peak Advice Blog a while ago. Every day I [s]read[/s] skim ten articles telling me how to live. Fear not! I would never give you useful advice. This series is about me writing bad code and you laughing at my pain. First Contact Say you have some voxels which occasionally get modified. You regenerate their geometry like so:voxel.Regenerate(); Because you are a masochist, you want to do this on a separate thread. Rather than redesign your engine, you simply spawn a worker thread and give it a list of voxels to process. Easy in C#:Queue workQueue;static void worker(){ while (true) { Voxel v = workQueue.Dequeue(); lock (v) { v.Regenerate(); } }}new Thread(new ThreadStart(worker)).Start();// And awaaaaaaay we go!workQueue.Enqueue(voxel); The [font=consolas]lock[/font] signals to other threads "hey, I'm using this". If the other threads also acquire locks before using the object, then only one thread will access it at a time. Kaboom! It crashes when thread A dequeues an item at the exact moment when thread B is enqueueing one. You need a lock on the queue as well. This is Hard and Boring and Slow Turns out, without a lock you can't trust even a single boolean variable to act sane between threads. (Not entirely true. For wizards, there are atomic operations and something about fences. Out of scope here!) Sprinkling locks everywhere is tedious, error-prone, and terrible for performance. Sadly, you need to rethink your engine from the ground up with threads in mind. The Right Way Why am I even including this? Ugh. You read a few articles on modern AAA engines, where you find a diagram like this: This is the job graph from Destiny. AAA engines split their workload into "jobs" or "fibers". Some jobs depend on others. The graph has bottlenecks that split the work into phases. Within phase 1, tons of jobs execute in parallel, but all of them must finish before phase 2 starts. With jobs, you wouldn't have to lock individual pieces of data. The dependency graph ensures that jobs run in the right order, and that nothing runs in parallel unless you're okay with it. You also don't have to think about individual threads -- a scheduler delegates jobs to a pool of threads. Here's another diagram you stumble across: This shows the workload of the GPU and each CPU over time as the game renders a single frame. The goal is to fill all those holes so you use every bit of available compute power at maximum efficiency. The Quick and Dirty Way In a brief flash of clarity, you realize that you are not Bungie. You check your bank account, which sadly reports a number slightly lower than $500 million. You recall the Pareto Principle, also known as the "80/20 rule". You decide to write 80% of a decent architecture for only 20% of the work. You start with a typical game loop:while (true){ // Process window and input events SDL_PumpEvents(); SDL_Event sdl_event; while (SDL_PollEvent(&sdl_event)) { // ... } physics_step(); game_logic(); render(); // Present! SDL_GL_SwapWindow(window);} [size=2]Side note: a while back you also switched to C++. Masochism level up. What can you move off the main thread? If you touch OpenGL or anything within a mile of the windowing system from another thread, the universe explodes. For now, you keep graphics and input ([font=consolas]SDL_PollEvent[/font]) on the main thread. That leaves physics and game logic. You give each its own thread. Since you need to spawn/modify/query physics entities in game logic, no other physics can happen while you're in a game logic update. The rest of the time, the physics thread can work in the background. Sounds like a perfect case for a lock:std::mutex physics_mutex;void physics_loop(){ while (true) { std::lock_guard lock(physics_mutex); physics_step(); }}void game_logic_loop(){ while (true) { { std::lock_guard lock(physics_mutex); game_logic(); } render(); }} In this setup, your [font=consolas]render()[/font] function can't read or write any physics data for fear of explosions. No problem! In fact, that limitation might be considered a feature. However, the [font=consolas]render()[/font] function also can't make any OpenGL calls since it's not on the main thread. You re-watch the Destiny GDC presentation and notice a lot of talk about "data extraction". In a nutshell, Destiny executes game logic, then extracts data from the game state and lines it up for a huge fan-out array of render threads to process efficiently. That's essentially what your [font=consolas]render()[/font] function will do: go through the game state, generate graphics commands, and queue them up. In your case, you only have one render thread to execute those commands. It might look like this:enum class RenderOp{ LoadMesh, FreeMesh, LoadTexture, FreeTexture, DrawMesh, Clear, // etc.};BlockingQueue render_queue;void main_loop(){ while (true) { RenderOp op = render_queue.read(); switch (op) { case LoadMesh: int count = render_queue.read(); Vec3* vertices = render_queue.read(count); // etc... break; case FreeMesh: // etc... } }} It's a graphics virtual machine. You could give it a pretentious name like GLVM. Now in your [font=consolas]render()[/font] function, you just write commands to the queue:void render(){ render_queue.write(RenderOp::Clear); for (MeshRenderer& mesh : mesh_renderers) { render_queue.write(RenderOp::DrawMesh); render_queue.write(mesh.id); // etc. } render_queue.write(RenderOp::Swap);} This will work, but it's not the best. You have to lock the queue every time you read from or write to it. That's slow. Also, you need to somehow get input data from the main thread to the game logic thread. It doesn't make sense to have queues going both directions. Instead, you allocate two copies of everything. Now, the game logic thread can work on one copy, while the main thread works on the other. When both threads are done, they swap. Now you only have to use a lock once per frame, during the swap. Once the threads are synced, the swap operation is just two pointer reassignments. Furthermore, you can keep the render command lists allocated between frames, which is great for performance. To clear one, just reset the pointer to the start of the list. Now you don't have to worry about implementing a queue with a ring buffer. It's just an array. Our Bright and Glorious Future For wizards, this is all wrong, because graphics drivers do command queueing anyway, and jobs and fibers are the right way. This is like Baby's First Threaded Renderer. But it's simple, it gets you thinking in terms of data flow between threads, and if you eventually end up needing a job system, you're already halfway there. This setup might also make the switch to Vulkan easier. If you keep all your data extraction in one place and make it read-only, it should be trivial to split into multiple threads, each with their own render queue. You can see a poorly-commented cross-platform implementation of this idea here. Potentially useful parts include the SDL loop, GLVM, and the swapper. If you enjoyed this article, try these: The Poor Man's Character Controller The Poor Man's Voxel Engine The Poor Man's Dialogue Tree But here's a better idea: watch the Destiny GDC talk. Thanks for reading! Mirrored on my blog
  21. I shouldn't poke fun, Saudi women have it pretty bad. The shutterstock watermark got me though.
  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

Participate in the game development conversation and more when you create an account on GameDev.net!

Sign me up!