Pages

20180131

Game Progrmming Patterns

Game Programming Patterns
  • Every program has some organization, even if it's just "jam the whole thing into main() and see what happens".
  • For me, good design means that when I make a change, it's as if the entire program was crafted in anticipation of it.
  • The measure of a design is how easily it accommodates changes.
  • Once you understand the problem and the parts of the code it touches, the actual coding is sometimes trivial.
  • I won't get on a soapbox here, but I'll ask you to consider doing more automated testing if you aren't already.
  • Loading code into neurons is so painfully slow that it pays to find strategies to reduce the volume of it.
  • To me, this is a key goal of software architecture: minimize the amount of knowledge you need to have in-cranium before you can make progress.
  • A lot of software architecture is about making your program more flexible. It's about making it take less effort to change it. That means encoding fewer assumptions in the program.
  • But performance is all about assumptions. The practice of optimization thrives on concrete limitations.
  • The faster you can try out ideas and see how they feel, the more you can try and the more likely you are to find something great.
  • Making your program more flexible so you can prototype faster will have some performance cost.
  • Prototyping--slapping together code that's just barely functional enough to answer a design question--is a perfectly legitimate programming practice. There is a very large caveat, though. If you write throwaway code, you must ensure you're able to throw it away.
  • One trick to ensuring your prototype code isn't obliged to become real code is to write it in a language different from the one your game uses. That way, you have to rewrite it before it can end up in your actual game.
  • The implementation that's quickest to write is rarely the quickest to run.
  • Abstraction and decoupling make evolving your program faster and easier, but don't' wast time doing them unless you're confident the code in question needs that flexibility.
  • Think about and design for performance throughout your development cycle, but put off the low-level, nitty-gritty optimizations that lock assumptions into your code until as late as possible.
  • Move quickly to explore your game's design space, but don't go so fast that you leave a mess behind you. You'll have to live with it, after all.
  • If you are going to ditch code, don't wast time making it pretty. Rock stars trash hotel rooms because they know they're going to check out the next day.
  • But, most of all, if you want to make something fun, have fun making it.
  • I think some patterns are overused, while others are underappreciated.
  • "Commands" are an object-oriented replacement for callbacks.
  • If a command object can do things, it's a small step for it to be able to undo them. Without the Command patter, implementing undo is surprisingly hard. With it, it's a piece of cake.
  • Friends don't let friends create singletons.
  • Flyweight, like its name implies, comes into play when you have objects that need to be more lightweight, generally because you have too many of them.
  • With instanced rendering, it's not so much that they take up too much memory as it is they take too much time to push each separate tree over the bus to the GPU, but the basic idea is the same.
  • As always, the golden rule of optimization is profile first. Modern computer hardware is too complex for performance to be a game of pure reason anymore.
  • You can't throw a rock at a computer without hitting an application built using the Model-View-Controller architecture, and underlying that is the Observer pattern.
  • It [the observer pattern] lets one piece of code announce that something interesting happened without actually caring who receives the notification.
  • In architecture, we're most often trying to make systems better, not perfect.
  • Sending a notification is simply walking a list and calling some virtual methods. Granted, it's a bit slower than a statically dispatched call, but that cost is negligible in all but the most performance-critical code.
  • When we get fuzzy about terminology, we lose the ability to communicate clearly and succinctly.
  • If you're responding to an event synchronously, you need to finish and return control as quickly as possible so that the UI doesn't lock up. When you have slow work to do, push it onto another thread or a work queue.
  • Dynamic allocation takes time, as does reclaiming memory, even if it happens automatically.
  • The reason design patterns get a bad rap is because people apply good patterns to the wrong problem and end up making things worse.
  • People--even those of us who've spent enough time in the company of machines to have some of their precise nature rub off on us--are reliably terrible at being reliable. That's why we invented computers: they don't make the mistakes we so often do.
  • They key idea [of the prototype pattern] is that an object can spawn other objects similar to itself.
  • Many people think "object-oriented programming" is synonymous with "classes".
  • OOP lets you define "objects" which bundle data and code together.
  • Compared to structured languages like C and functional languages like Scheme, the defining characteristic of OOP is that it tightly binds state and behavior together.
  • Early games procedurally generated almost everything so they could fit on floppies and old game cartridges. In many games today, the code is just an "engine" that drives the game, which is defined entirely in data.
  • Despite noble intentions, the Singleton pattern described by the Gang of Four usually does more harm than good.
  • There are times when a class cannot perform correctly if there is more than one instance of it. The common case is when the class interacts with an external system that maintains its own global state.
  • Saving memory and CPU cycles is always good.
  • As games got bigger and more complex, architecture and maintainability started to become the bottleneck. We struggled to ship games not because of hardware limitations, but because of productivity limitations.
  • Computer scientists call functions that don't access or modify global state "pure" functions. Pure functions are easier to reason about, easier for the compiler to optimize, and let you do neat things like memoization where you cache and reuse the results from previous calls to the function.
  • An assertion function is a way of embedding a contract into your code.
  • Assertions help us track down bugs as soon as the game does something unexpected, not later when that error finally manifests as something visibly wrong to the user. They are fences in your code-base, corralling bugs so that they can't escape from the code that created them.
  • The general rule is that we want variables to be as narrowly scoped as possible while still getting the job done. The smaller the scope an object has, the fewer places we need to keep in our head while we're working with it.
  • The simplest solution, and often the best, is to simply pass the object you need as an argument to the functions that need it.
  • The goal of removing all global state is admirable, but rarely practical.
  • Many of the techniques compilers now use for parsing programming languages were invented fro parsing human languages.
  • The coders you idolize who always seem to create flawless code aren't simply superhuman programmers. Instead, they have an intuition about which kinds of code are error-prone, and they steer away from them.
  • State machines help you untangle hairy code by enforcing a very constrained structure on it.
  • "Turing complete" means a system (usually a programming language) is powerful enough to implement a Turing machine in it, which means all Turing complete languages are, in some ways, equally expressive.
  • Double Buffer: Cause a series of sequential operations to appear instantaneous or simultaneous.
  • In their hearts, computers are sequential beasts. Their power comes from being able to break down the largest tasks into tiny steps that can be performed one after another. Often, though, our users need to see things occur in a single instantaneous step or see multiple tasks performed simultaneously.
  • A frame-buffer is an array of pixels in memory, a chunk of RAM where each couple of bytes represents the color of a single pixel.
  • This is why we need this pattern. Our program renders the pixels one at a time, but we need the display driver to see them all at once. Double buffering solves this.
  • The core problem that double buffering solves is state being accessed while it's being modified.
  •  A game loop runs continuously during game-play. Each turn of the loop, it processes user input without blocking, updates the game states, and renders the game. It tracks the passage of time to control the rate of game-play.
  • Computers are naturally deterministic; they follow programs mechanically. Non-determinism appears when the messy real world creeps in.
  • Most games use floating point numbers, and those are subject to rounding error. Each time you add two floating point numbers, the answer you get back can be a bit off.
  • Each entity in the game should encapsulate its own behavior.
  • The game world maintains a collection of objects. Each object implements an update method that simulates one frame of the object's behavior. Each frame, the game updates every object in the collection.
  • Favor 'object composition' over 'class inheritance'.
  • Being a proficient programmer takes years of dedicated training, after which you must contend with the sheer scale of your code-base.
  • To make a system that users enjoy, you have to embrace their humanity, including their fallibility. Making mistakes is what people do, and is a fundamental part of the creative process. Handling them  gracefully with features like undo helps your users be more creative and create better work.
  • When you find yourself with a lot of sub-classes, that often means a data-driven approach is better.
  • Under the hood, C++ virtual methods are implemented using something called a "virtual function table", or just "vtable". A vtable is a simple struct containing a set of function pointers, one for each virtual method in a class. There is one vtable in memory for each class. Each instance of a class has a pointer to the vtable for its class.
  • Scripting languages and other higher-level ways of defining game behavior can give us a much needed productivity boost, at the expense of less optimal run-time performance. Since hardware keeps getting better but our brainpower doesn't, that trade-off starts to make more and more sense.
  • Simplest is often best.
  • Once you get the hang of a programming language, writing code to do what you want is actually pretty easy. What's hard is writing code that's easy to adapt when your requirements change.
  • A powerful tool we have for making change easier is decoupling.
  • Humans are mainly visual animals, but hearing is deeply connected to our emotions and our sense of physical space.
  • A queue stores a series of notifications or requests in first-in, first-out order.
  • Complexity slows you down, so treat simplicity as a precious resource.
  • When you have a piece of state that any part of the program can poke at, all sorts of subtle inter-dependencies creep in.
  • In practice, the best way to store a bunch of homogeneous things is almost always a plain old array.
  • There are a bunch of ways to implement queues, but my favorite is called a ring buffer.
  • Optimizing for performance is a deep art that touches all aspects of software.
  • Accelerate memory access by arranging data to take advantage of CPU caching.
  • Sure, we can process data faster than ever, but we can't get that data faster.
  • RAM hasn't been keeping up with increasing CPU speeds.
  • Whenever the chip reads some memory, it gets a whole cache line. The more you can use stuff in that cache line, the faster you go. So the goal then is to organize your data structures so that the things you're processing are next to each other in memory.
  • One of the hallmarks of software architecture is abstraction.
  • Avoid unnecessary work by deferring it until the result is needed.
  • A "dirty" flag tracks when the derived data is out of sync with the primary data. It is set when the primary data changes.
  • Improve performance and memory use by reusing objects from a fixed pool instead of allocating and freeing them individually.
  • Fragmentation means the free space in our heap is broken into smaller pieces of memory instead of one large open block. The total memory available may be large, but the largest contiguous region might be painfully small.
  • Much of software engineering is fighting against complexity.

No comments:

Post a Comment