• Content count

  • Joined

  • Last visited

Everything posted by evanofsky

  1. 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
  2. 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.
  3. Best explanation I've seen of quaternions, complete with Blender practical application tips https://t.co/MVxF1Pb5cE
  4. RT @MarkPuente: The best letter to the editor in today's @TB_Times. https://t.co/E60kou9BXw
  5. woah guys i actually kinda did an animation https://t.co/6Q7hoBIm1R
  6. 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
  7. What percentage of a relationship would you say happens over text these days?
  8. Anyone planning on attending Handmade Con 2016? https://t.co/x3Rs6ruCdw
  9. 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
  10. RT @ADAMATOMIC: @fasterthanlime "dada are you just randomly trying different sin() / cos() -1/1 variations until you get what you want" "..…
  11. It's not just cygwin, it's actual Linux binaries running on Windows. It intercepts and translates Linux syscalls. https://t.co/YlloZZTGwl
  12. nav mesh generation now under 30 seconds https://t.co/5SW9aPp5om
  13. RT @mitsuhiko: Only in javascript land can you implement "is positive integer" with three dependencies. https://t.co/Rh5V1DcFEh
  14. 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
  15. I shouldn't poke fun, Saudi women have it pretty bad. The shutterstock watermark got me though.
  16. Ludum Dare 34 Postmortem

    Friday 21:15 Fifteen minutes after the theme announcement, my friend Ben Homan walks through my front door. Not really my front door, I'm just a subletter. But this is a first. Normally he ignores our instructions to walk in without knocking. The first time, he texted me from the driveway. 21:30 Jesse Kooner walks in, also unannounced, bearing frozen pizza. Before he can even kick his shoes off, I loudly explain the theme: a never-before-seen tie between "growing" and "two-button controls". 21:45 Jesse has no laptop. I dig out an old one from my closet. I plug it in and start working on a few Windows updates. 72 to be exact. Meanwhile, we decide which technology to use. Jesse's less code-focused skillset leads him to prefer Unity, while Ben wants to use the weekend as an opportunity to become more familiar with Node.js. We decide on Node.js. Jesse will provide creative input and artwork. 22:30 My roommates, a brother and sister, arrive home from an apparently underwhelming Christmas light show. The concept of a game jam is foreign to them, but they're good sports. We spend a half hour playing QWOP with them. 23:00 Our design parameters: Multiplayer. Otherwise, what's the point of Node.js? Probably 2D due to the limited timeframe. Although Jesse is more comfortable working in 3D. Jesse originally suggested doing this competition a few weeks ago, when he wanted to create a mash-up of "Cookie Clicker" and a tower defense game. He resurrects the idea now, only halfway joking. Ben likes the idea of a multiplayer vine growing game. I'm partial to a text-based social game about growing a social media brand. No one commits to anything yet. Ben is not a big gamer, so I pull up a few famous HTML5 games on his laptop. Cursors.io. Agar.io. 2048. This last one interests me in particular, as it involves growing numbers. 23:15 I'm pitching Ben and Jesse on a multiplayer version of 2048. I envision a free-roaming world filled with numbered tiles to collect. Instead of collapsing numbers together against the edges of the board, players would find walls and structures within the world to collapse their numbers against. 23:30 How can two or more games of 2048 occur simultaneously on the same board? In normal 2048, the player controls all tiles on the board. We decide to give each player a unique color, and allow them to assimilate unclaimed, grayed-out tiles. What happens when tiles from two players meet? We decide that whoever moves first gets to own the resulting collapsed tile from a move. How do players traverse through the world? We toss around the idea of procedural generation, but eventually decide on hand-crafted, linear levels linked together via portals. This raises the question: how do the portals work? And what happens to a player's tiles once they exit a level? Top right corner: our confidence that we'll actually finish the thing. Saturday 00:30 We've eaten two pizzas. We have a Git repository and a Slack instance which is ultimately only used to test out amusing Slackbot responses. Although the game is 2D, we decide on a 3D art style with an orthographic projection, to allow Jesse to use his skills in Maya LT, which he promptly installs on my old laptop. Ben works on the server with Node.js, while I start on the client with Three.js. 02:30 Ben heads home first, then Jesse. I pass out on my bedroom floor. You can see we're already struggling with the name. 08:30 I wake to find Ben working in the kitchen. He spends the morning building boiler plate for the server, while I work out some Three.js details. 13:30 I return from a run just in time to catch Jesse pulling up with donuts. After lunch, he and I spend the next few hours working out a pipeline between Maya and Three.js. 16:00 I tweet our first screenshot, featuring colorized instances of Jesse's tree model displayed in a horribly distorted orthographic projection. 18:00 Without the server API to code against, I run out of things to do on the client. Ben finishes the data model, but he has trouble conceptualizing the rules for player movement. I haul my laptop over to indulge in some good old-fashioned pair programming. 19:00 Break for dinner. Burritos. Jokes. 21:00 Jesse continues modelling. With the basic API done, I start working to make the client consume it. Ben works on an image loader. We want to design the levels in GIMP. 23:00 Ben takes off. I've got player movement, animations, and tile numbers done. Sunday 00:30 Multiplayer works. Jesse and I play a few games against each other. It's fun! It's a game! We work out some issues with the level loading code and try to get an interesting level loaded. 02:00 Problem: it's pretty easy for one player to gain the upper hand and quickly assimilate all the tiles on the board, making it impossible for other players to move and grow. 02:30 Solution! Players should only control tiles within a certain radius of their "center". Outlier tiles are grayed out, free to be picked up by other players. 03:15 Fix implemented. Jesse heads home and I turn in. 09:15 Tweet another screenshot before heading to church. At this point, I'm having an existential crisis about the name. I fire off a few panicked texts about it. 12:30 Ben and I are back to the grindstone. Jesse arrives with sandwiches and more donuts! Ben adds a cool username feature, but we eventually axe it to keep things simple. 15:00 Tweaks and bug fixes all day. Ben works on the level format, while Jesse lays out some levels in GIMP. 17:00 More polish. I put in Jesse's cloud models and a "connecting" spinner. 19:00 We finally brainstorm a name: Tile Risers. Jesse whips up a logo in Maya. We go through seven iterations before everything lines up in our janky export pipeline. 20:00 I spin up a Digital Ocean droplet, allocate an S3 bucket, commit the production URLs, and start filling out the submission form. 21:00 Time's up! Fortunately, we have another hour to submit. I later found out I misread the rules, and we actually had a whole extra 24 hours. At any rate, we were done. I commit two small bug fixes after submission, which is within guidelines. Conclusion Three.js is easy and fun. ES5 causes a lot of pain by continuing silently when we ask it to do ridiculous things like "compare an object to an integer". Ludum Dare is a blast and you should do it. Play Tile Risers, view the source, and vote for it! Mirrored on my blog
  17. man this Steam ARG is out of control https://t.co/3B2MoaePWp
  18. Great read about the website obesity crisis. Addresses all fronts: technical, design, infrastructure https://t.co/NMazGqvZ3H
  19. Developers hate him! We'll cover some standard tips and tricks here, but we're not really interested in those. We're looking for the One Weird Trick to rule them all. Hopefully each trick we encounter brings us closer to coding Mecca. In the beginning The first video game I ever wrote was called Ninja Wars. Yes, that is an HTML table of images. I changed the src attribute to move stuff around. The top of the Javascript file looked like this:var x = 314;var y = 8;var prevy= 1;var prevx= 1;var prevsw= 0;var row= 304;var endrow= 142;var sword= 296;var yrow = 0;var yendrow = 186;var I = 0;var e = 0;var counter = 0;var found = 0;var esword = 26;var eprevsw = 8;var bluehealth = 40;var redhealth = 40;var n = 0;var you = 'ninja';var bullet = 'sword';var enemy = 'enemy';var ebullet = 'enemysword';var besieged = 0;var siegecount = 0;var esiegecount = 0;var ebesieged = 0;var healthcount = 0;var player = 0;var starcount = 0;var infortress = false;var prevyou = you;var einfortress = false;var prevenemy = enemy;var previmg = "";var prevbullet= "";var eprevbullet= "";var randnum = 0;var randnum2 = 0;var randnum3 = 0;var randnum4 = 0;var buildcount = 0;var characters = new Array(4);characters = ['ninja','tank','heli','builder'];var bullets = new Array(3);bullets = ['sword','bullet','5cal','sword'];var echaracters = new Array(3);echaracters = ['enemy','tank2','eheli','ebuilder'];var ebullets = new Array(3);ebullets = ['enemysword','bullet2','e5cal','enemysword'];var health = new Array(4);health = [40,30,20,10];var prevorb = 0;var prevnum = 0; Hopefully this looks somewhat familiar, and I'm not the only one to start out writing code like this. Regardless, this debacle demonstrates our first trick: Trick #1: globals are evil We don't even know why they're evil, we just intuitively know. edit: I should clarify that I'm no longer against globals. But that's getting ahead of ourselves. Pretty soon we learn about objects. We can group our variables:class Ninja{ int x, y; int previousX, previousY; int health = 100;}class Sword{ int x, y; int previousX, previousY; int sharpness = 9000;} We can even use inheritance to avoid copy-pasting:class Movable{ int x, y; int previousX, previousY;}class Ninja : public Movable{ int health = 100;}class Sword : public Movable{ int sharpness = 9000;} Inheritance is nice! Nice enough to serve as our next trick: Trick #2: object-oriented programming Object-oriented is so great that it forms the core of many classic games, including Doom 3. Which, coincidentally, is open source. Doom 3 loves inheritance. Don't believe me? Here's a small subset of its class hierarchy:idClass idEntity idAnimatedEntity idWeapon idAFEntity_Base idAFEntity_ClawFourFingers idAFEntity_Vehicle idAFEntity_VehicleFourWheels idAFEntity_VehicleSixWheels idAFEntity_Gibbable idAFEntity_WithAttachedHead idActor idPlayer idAI Imagine you're an employee at id Software. This inheritance hierarchy works great for a few months. Then, one fateful Monday, disaster strikes. The boss comes in and says "Hey, change of plans. The player is now a car." Look at idPlayer and idAFEntity_VehicleFourWheels in the hierarchy. Big Problem #1: we need to move a lot of code. Big Problem #2: the boss comes to his senses and calls off the "player is a car" idea. Instead, we're adding turrets to everything. The car is becoming a Halo Warthog, and the player is getting a giant turret mounted on his back. As lazy programmers, we decide to use inheritance again to avoid copy-pasting. But look at the hierarchy. Where can we put the turret code? The only ancestor shared by idPlayer and idAFEntity_VehicleFourWheels is idAFEntity_Base. We'll probably put the code in idAFEntity_Base and add a boolean flag calledturret_is_active. We'll only set it true for the car and the player. This works, but the terrible, logical conclusion is that our base classes end up loaded with tons of cruft. Here's the source code for idEntity. Go ahead, scroll through it. You don't have to read it all. https://github.com/id-Software/DOOM-3-BFG/blob/9c37079c16015fc58de29d3de366e0d93dc11f8a/neo/d3xp/Entity.h#L163 The point is, that's a lot of code. Notice how every single entity -- down to the last piece of physics debris -- has a concept of a team, and of getting killed. Clearly not ideal. If you're a Unity developer, you already know the solution: components! Here's what they look like: Rather than inheriting functionality, Unity entities are just bags of components. This solves our earlier turret problem easily: just add a turret component to the player and car entities. Here's what Doom 3 might look like if it used components:idPlayer idTransform idHealth idAnimatedModel idAnimator idRigidBody idBipedalCharacterController idPlayerControlleridAFEntity_VehicleFourWheels idTransform idAnimatedModel idRigidBody idFourWheelController... What have we learned? Trick #3: in general, favor composition over inheritance Take a moment to review the tricks we've covered so far: global variables bad, objects good, components better. You won't believe what happens next! Let's take a small detour into the world of low-level performance with a very simple question: which function is faster?double a(double x){ return Math.sqrt(x);}static double[] data;double b(int x){ return data[x];} We'll hand-wave a lot of complexity away and just assume that these two functions eventually compile down to one x86 instruction each. Function a will probably compile to sqrtps, and function b might compile to something like lea("load effective address"). sqrtps takes about 14 CPU cycles on a modern Intel processor, according to Intel's manual. What about lea? The answer is "it's complicated". It depends on where we load data from.Registers ~40 per core, sort of 0 cyclesL1 32KB per core 64B line 4 cyclesL2 256KB per core 64B line 11 cyclesL3 6MB 64B line 40-75 cyclesMain memory 8GB 4KB page 100-300 cycles That last number is important. 100-300 cycles to hit main memory! This means in any given situation, our biggest bottleneck is probably memory access. And from the looks of it, we can improve this by using L1, L2, and L3 cache more often. How do we do that? Let's return to Doom 3 for a real-life example. Here's the Doom 3 update loop:for ( idEntity* ent = activeEntities.Next(); ent != NULL; ent = ent->activeNode.Next() ){ if ( g_cinematic.GetBool() && inCinematic && !ent->cinematic ) { ent->GetPhysics()->UpdateTime( time ); continue; } timer_singlethink.Clear(); timer_singlethink.Start(); RunEntityThink( *ent, cmdMgr ); timer_singlethink.Stop(); ms = timer_singlethink.Milliseconds(); if ( ms >= g_timeentities.GetFloat() ) Printf( "%d: entity '%s': %.1f ms\n", time, ent->name.c_str(), ms ); num++;} From an object-oriented perspective, this code is pretty clean and generic. I'm assuming RunEntityThink calls some virtual Think() method where we could do just about anything. Very extensible. Uh oh, here comes the boss again. He has some questions. What's executing? Er... sorry boss, it depends on what entities are active at the time. We don't really know. In what order is it executing? No idea. We add and remove entities to the list at random times during gameplay. How can we parallelize this? Gee boss, that's a tough one. Since the entities execute randomly, they may be accessing each other's state. If we split them up between threads, who knows what might happen. In short: But wait, there's more! If we look closely, we see the entities are stored in a linked list. Here's how that might look in memory: This makes our L1, L2, and L3 cache very sad. When we access the first item in the list, the cache thinks, "hey, I bet the next thing they'll want is nearby", and it pulls in the next 64 bytes after our initial memory access. But then we immediately jump to a completely different location in memory, and the cache has to wipe out those 64 bytes and pull in new data from RAM. Trick #4: line up data in memory for huge performance gains Like this:for (int i = 0; i < rigid_bodies.length; i++) rigid_bodies.update();for (int i = 0; i < ai_controllers.length; i++) ai_controllers.update();for (int i = 0; i < animated_models.length; i++) animated_models.update();// ... An object-oriented programmer might be infuriated at this. It's not generic enough! But look, now we're iterating over a set of contiguous arrays (not arrays of pointers, mind you). Here's what it looks like in memory: Everything is all lined up in order. The cache is happy. As a side bonus, this version allows us to answer all those pesky questions the boss had. We know what's executing and in what order, and it looks much easier to parallelize. edit: Don't get too hung up on cache optimization. There's a lot more to it than just throwing everything in an array. I only bring it up to prove a point, and I'm only qualified to give the basic introduction. Check out the links at the bottom for more info. Time to wrap this up and get to the point. What's the One Weird Trick? What do tricks 1-4 (and many more) all have in common? The One Weird Trick: data first, not code first Why were globals so evil (trick #1)? Because they allowed us to get away with lazy data design. Why did objects help us (trick #2)? Because they helped us organize our data better. Why did components help us even more (trick #3)? Because they modeled our data better by matching the structure of reality better. Even the CPU likes it when we organize our data correctly (trick #4). No really, what is the trick actually Let's break it down in practical terms. Here's a representation of a typical Unity-like component-based game design. Each component is an object. It has some state variables listed at the top, and then some methods to do stuff with those variables. This is a well-designed object-oriented system, so the variables are private. The only code that can access them is the object's own methods. This is called "encapsulation". Each object has a certain amount of complexity. But fear not! OOP promises that as long as we keep the state private, that complexity will stay encapsulated within the object, and won't spread to the other objects. Unfortunately, this is a lie. Sometimes, we need a function to access two or three objects. We end up either splitting the function between those objects, or writing a bunch of getters and setters so our function can access what it needs. Neither solution is very satisfying. Here is the truth. Some things cannot be represented as objects very well. I propose an alternate paradigm, one which represents every program perfectly: Once we separate process from data, things start to make more sense. Object-oriented helps us write good code because it encourages us to encapsulate complexity (i.e. state). But it forces us to do so in a certain way. Why not instead encapsulate like this, if it makes sense within our problem space? In summary, design data structures to match your specific problem. Don't shoehorn a single concept into a bunch of separate, encapsulated objects. Next, write functions that leave the smallest possible footprint on that data. If possible, write pure stateless functions. That's the trick. Conclusion If this struck a chord with you, it's because I stole most of it. Get it straight from the source: Mike Acton - Data-Oriented Design John Carmack - Inlined Code Casey Muratori - Semantic Compression Thanks for reading! Mirrored on my blog