Like any platform, HTML5 receives its share of criticism, some valid and some based on rumor or outdated information. However, HTML5 is keenly supported by some of the most successful business leaders in the space, and the games available right now on turbulenz.com are proof that HTML5 is viable for high quality gaming today. We hope the below will help explain why we have backed, and continue to back, HTML5.
HTML5 and related standards are fast making the web a viable platform for high quality games. Games written to target such web standards can run in a variety of contexts while taking advantage of the power of the underlying hardware, as well as the connectivity enjoyed by modern devices.
Turbulenz has invested a significant amount of research and engineering manpower creating a high-performance games platform for the web. With a background in game technology development on consoles, we have approached HTML5 and related technologies from a game developer's perspective with fidelity and performance in mind.
This series of articles describes some of our experience transitioning from consoles to the web: what works well, when to use it, optimization strategies and workarounds for ongoing problems.
Some of the games built using Turbulenz can be played on the turbulenz.com game network, which uses our HTML5 platform and infrastructure to instantly deliver high quality 3D games to gamers online. The company was founded in early 2009 by a group of directors and lead programmers from Electronic Arts, and the team has grown to include developers and business professionals from some of the world's leading entertainment companies.
In this first article we talk about the current state of HTML5 and related technologies for games, and give an overview of the development environment and workflow that game developers can expect.
The Browser as a Game Platform
The browser has become the new OS, bringing with it a host of problems as well as opportunities. In the past, games traditionally ran in isolation, consuming as many resources as possible to extract the full performance from the machine. Game code running in the browser will be competing with the rest of the browser’s processes, including other games running in other tabs or windows (as well as whatever else may be running on the machine).
In some ways, each browser must be treated as a different platform: some interfaces are common across all browsers and some resources are always available, but for more advanced functionality, the developer must often check for features at runtime and provide multiple code paths and workarounds.
The test matrix covering Browser, Browser version, OS, OS version, and Hardware is large. Furthermore, although hardware resources cannot be accessed as directly as with native applications, developers still have to deal with graphics driver bugs as well as bugs in the browser layers that now interface with those drivers.
With the fast pace of browser updates and the fact that some browsers update themselves automatically, it can be difficult to maintain a specific list of supported versions to test against. At Turbulenz we always work with the latest version of each browser and also a list of well-known popular browser versions. At the time of writing, this includes Internet Explorer 8, 9 and 10, Firefox 3.6 and latest, Safari 5.1 and latest (on Mac OS X) and the latest version of Chrome. We test on a range of hardware from slow netbooks to high-end desktops using a selection of popular video cards from Nvidia, AMD, and Intel.
The relative market share of the browsers is always in motion and is usually not consistent across demographics. We recommend finding your target market and studying the browser stats for sites with a similar audience.
In general, when coding for the browser we recommend that developers code defensively, test with a range of browsers and regularly monitor the state of new versions.
We found closures to be very powerful, making asynchronous programming much easier and clearer. However, subtle errors can occur if developers are not fully aware of their behavior.
For example, quite a common mistake is to create closures inside a loop, referencing variables that change during the execution of that loop. This can lead to confusing behavior since variables referenced by a closure contain the value at the point the closure is executed, not at the point of its creation.
Objects can be assigned as prototypes of other objects. If a property is not found on an object then the runtime will check the prototype object, and so on. As functions can be stored on objects and objects can share prototypes, the prototype mechanism allows methods and code reuse in a familiar way. Functions also behave as objects, and can be used to store and retrieve properties by name.
As a best practice, we recommend establishing a clear and well-defined policy of object ownership. This makes it easier to point to the code that is responsible for maintaining (and releasing) the reference to a given object.
(0x80 << 24) !== 0x80000000
(0x80 << 24) === -2147483648
Both would be false if unsigned integers were used. Note the use of the triple-equal operator, because it does not perform type conversion if the operands have different types. In contrast, the double-equal operator does perform type conversion and could hide errors in the code.
The language has evolved considerably from its origin and some features are not entirely standardized across browsers. Any recently added feature should be checked for before use. For example, the ability to define getters and setters for object properties with Object.defineProperty has, for us, become one of the most useful features added recently, however we need to provide extra code paths for when support does not exist. In some cases testing for the existence of a function is enough to check for a supported feature, as in the case of Object.defineProperty. In other cases, the code must try to actually use the feature inside a try / catch block and check whether an exception was raised.
At Turbulenz, we try to minimize development mistakes by employing a development process focused on code quality: strict coding standards, frequent code reviews and automatic unit testing. We also routinely use static analysis tools to check our code for common mistakes. So far we have not found analysis tools with the same level of inspection as those available for C and C++, but the following tools have been helpful in that they do catch a fairly large class of bugs, and they do improve over time: Closure Linter, JSHint, and JSLint.
Editors for static typed languages can provide a lot of functionality that is harder to provide for dynamic languages.
- Syntax highlighting
- Auto-completion suggestions for:
- Default global objects.
- Well-known external libraries like jQuery.
- Variables and functions defined within the current function.
- Global variables and global functions defined within the current file.
- Refactoring at the current file level.
- Integration with JSLint or JSHint.
Runtime and Performance
We use jsPerf to evaluate performance of small snippets of code across browsers and find the fastest way to perform a given operation. Unfortunately there is sometimes no single code snippet that is fastest on all browsers (and all of their versions) and so we are forced to compromise, usually based on market share.
In order to reduce the memory usage of big arrays of numbers we recommend the use of typed arrays where possible. We made a memory saving of 20 percent in one of our demos just by switching arrays holding 3D vectors to use Float32Array.
Note that typed arrays have generally better performance characteristics than standard Arrays. Most JIT compilers understand how to directly address the underlying memory used by typed arrays, and can generate extremely efficient code to access and operate on them when the data type can be correctly predicted.
The total number of objects alive at a given time has a direct effect on the cost of garbage collection. Older VMs will stop the world for seconds during collection if there are millions of active objects. This situation keeps improving and, nowadays, even millions of objects might only stall execution for hundreds of milliseconds, although this still manifests as a perceptible "skip" in a game. Garbage collection is usually triggered by either a lot of object creations in succession, or at fixed periods of time (for example the engine may invoke a full sweep every 10 seconds).
Not surprisingly, we have found it very important for performance to keep the number of objects we create as low as possible. Some of our demos or examples do not create a single object during execution of a frame.
There are several strategies we have found to be useful for reducing object count. Reusing dynamic arrays from function to function and from frame to frame (i.e. using a scratchpad) can be very effective. Also, consider converting Arrays of Objects to flat arrays with interleaved properties. In one of the games on our site, this alone reduced object count by over 75 percent and solved problems with garbage collection pauses.
In some cases, encoding information and commands in custom bytecode can be a way to trade off runtime performance for object count. For example, if storing an SVG path, maintaining a single string that contains instructions for a particular rendering shape and decoding these instructions on the fly will likely use a lot less memory and many fewer objects (although more CPU time) than unpacking the string and storing the instructions as a hierarchy of objects.
Debugging and Profiling
All browsers now provide a debugging environment embedded as part of the browser itself. The debugger tends to be hidden under a Development Tools menu option or similar.
Debugging features provided usually include:
- Traversal and inspection of the HTML tree.
- Recording and inspection of HTTP requests.
- Console logging, a read-eval-print loop for executing snippets of code.
- Debugger with support for: Breakpoints, Stack traces, Variable watching
The profilers provided by the browsers often support call-graph capture (based on a form of instrumentation which can add a noticeable overhead to the execution) and heap snapshots (with object counters, objects size and references between them). However, these features and the implementation quality can vary between the browsers.
This article has been an overview of high-end game development for HTML5, including details of the development environment and workflow. In following articles we will talk more about particular features exposed by HTML5 and related standards that are of interest to games. As well as covering specific areas of game development such as Graphics and Audio, we will give tips and recommendations for how to extract the best quality and performance across a wide range of browsers and platforms.