Trending
Opinion: How will Project 2025 impact game developers?
The Heritage Foundation's manifesto for the possible next administration could do great harm to many, including large portions of the game development community.
In this first of a three-part series, seasoned console developers, formerly of Criterion, describe the process of moving to HTML5 development, discussing the differences between working in C and JavaScript, and explaining dealing with the difficult environment of the browser.
February 7, 2013
Author: by Duncan Tebbs
In this first of a three-part series, seasoned console developers, formerly of Criterion, describe the process of moving to HTML5 development, discussing the differences between working in C and JavaScript, and explaining dealing with the difficult environment of the browser.
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 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.
As a development platform, the browser has several advantages compared to other platforms. In general, iteration is extremely fast (JavaScript requires no compile step during normal development and refreshing the page immediately loads and runs the latest version of the code). Web developers have long enjoyed access to a range of tools for debugging and profiling, and these make up a very effective development environment. More details are given later in this article.
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.
Only one general-purpose programming language can be used in the browser: JavaScript. It's also known by its official name: ECMAScript. This is not intended to be a guide or introduction to the language, but we have highlighted some aspects that stood out to us or had unexpected performance implications when approaching JavaScript from a C/C++ background.
Particularly when moving a big project from C/C++ to JavaScript for the first time, developers will likely face some challenges. The JavaScript syntax resembles C, but the language has more in common with functional languages like Lisp; it has closures and first-class functions.
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.
JavaScript has first-class functions, meaning that functions can be passed as parameters, returned from other functions, assigned to variables and stored in dictionaries. Strings can be compiled at runtime into functions, although this is not recommended for security reasons as the string could come from an unknown source and could compromise other parts of the code.
The language has object-oriented features, but there are no classes. JavaScript has constructors and prototype-oriented inheritance. Objects behave like key-value dictionaries, able to optimally store and retrieve anything by name. We found that trying to access values by the wrong name was a common error, usually a typo in the code that stores or retrieves the data.
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.
JavaScript objects do not have destructors. The JavaScript runtime schedules their destruction once there are no remaining references to them, and JavaScript code does not receive any notification when that happens. We found it non-trivial to find memory leaks in JavaScript code (that is, hanging references to objects which are in turn never garbage collected). Some JavaScript profilers provide object counts on their heap snapshots, which does help, but they require that objects be created from non-literal constructors, using the new operator, in order to differentiate between them. Some profilers also provide reference tracking in their heap snapshots which helps to identify why a given object has not been garbage collected.
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.
JavaScript does not have static types, so variables are allowed to have different types over the life of the program and some automatic conversions will happen if different types are used in the same operation. For example, adding numbers to strings will result in a string concatenation of the number converted to a string. This can result in errors that are very hard to find and can have performance implications.
Another subtle point and possible source of bugs are the JavaScript bitwise operations, which convert parameters to signed 32-bit integers with big-endian order and in two's complement format. For example, the following two statements are true:
(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.
Although JavaScript variables can have Boolean values, every other basic type can be used on a conditional expression, with the following values being equivalent to false: null, undefined, the empty string, the number zero, the number NaN.
JavaScript does not have block scope; it has a global scope and function scope. Variables can be created anywhere in a function, and will be accessible everywhere, as if they were declared at the top of the function. This will confuse experienced C++ developers who are new to JavaScript.
Exceptions are supported in JavaScript, but the language does not support an exception hierarchy. The throw operator can be used on anything; an object, a number, a string, and it is caught by the first try / catch surrounding the code. There is no way to catch only objects of a specific type.
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.
We found JavaScript to be a powerful language because of its dynamic nature and functional features, but also a complicated language for big projects. With multiple developers working in parallel on a code base of hundreds of thousands of lines of code, mistakes will happen infuriatingly often without discipline and the right development tools.
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.
When a new software engineer joins Turbulenz, they receive a copy of JavaScript: The Good Parts by Douglas Crockford. We consider this book a very good introduction to JavaScript, explaining common mistakes and good patterns.
Conversion Tools
Some developers may be interested in the translation tools available for converting other languages to JavaScript. They allow development in one language -- such as one with static typing in order to minimize runtime errors -- which is then converted to JavaScript.
Some of the more popular tools include haXe, Dart and Emscripten. The TypeScript project is a relatively recent addition that extends JavaScript to allow type information to be specified. This is particularly interesting since, as well as providing advantages in terms of error checking and development tools, TypeScript is "backwards compatible" with JavaScript.
We would encourage anyone considering using these tools to make sure they fully understand the implications. Automatically generated JavaScript code may provide suboptimal performance or a substantial size increase over hand-written JavaScript code. What works well in one environment may fail in a different one, requiring multiple code paths. Debugging problems and tracing back to the original offending code may prove difficult, and in some cases fixes may require tweaking the original source code to "trick" the compiler into generating safe JavaScript. Support for specific browser features may not exist, which will also limit what can be achieved.
However, these tools do have some interesting properties (such as the ability to turn C++ code into JavaScript that does essentially no garbage collection), and they are continually evolving and improving. It makes sense to be aware of them as an option, and to monitor their development.
Editors
Editors for static typed languages can provide a lot of functionality that is harder to provide for dynamic languages.
Good editors supporting JavaScript usually provide:
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.
Features such as auto-completion for custom objects, global variables from other files etc. are still lacking for dynamic languages. Some editors are starting to parse and execute all the JavaScript code in the current project in their own JavaScript engines to better judge the availability of variables and properties at the current point in the code. However these editors will frequently generate incorrect or incomplete suggestions. Writing your code in specific ways may help the editor with its suggestions, for example by initializing all the properties in the constructor. In this case the editor may be able to track which constructor was used to build the current object and inspect it for properties to suggest.
JavaScript runs on a Virtual Machine with tight security controls and a limited set of APIs to interact with the external world. Recent JavaScript engines employ Just-In-Time (or JIT) compilation to generate machine code that executes much faster than the traditional interpreted bytecode of the past. Modern engines evolve at a fast pace, improving performance and reducing memory usage with every release. As one would expect, performance is not up to the level of well-written C / C++ (we found JavaScript to be 4 to 10 times slower depending on the browser) but the gap keeps closing with every new version.
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.
Memory allocation will become an issue for big games. What fits well in a console with 512 MB can take over a gigabyte in a browser. Every JavaScript object has a higher overhead compared to the C++ equivalent.
The type of objects used can also affect memory size. JavaScript engines implement numbers as either 32-bit signed integers or 64-bit floating points, and usually both take the same amount of memory (8 bytes).
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.
As mentioned above, JavaScript uses garbage collection to dispose of objects that are no longer referenced. Garbage collectors employ mark-and-sweep algorithms to detect objects that can safely be destroyed. Some also use a concept of multiple generations to optimize the allocation and deallocation of short-lived objects, and incremental algorithms may be employed to distribute the cost of marking and sweeping. This is also an area of heavy active development.
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.
The Space Ark title is an example of where we worked very closely with the developer to optimize the game using some of the ideas given here. By drastically reducing the number of dynamically created objects we were able to essentially remove garbage collection pauses during gameplay on modern JavaScript runtimes, and still maintain the visual quality of the original Xbox Live Arcade version, including character animations and particle effects.
Execution of JavaScript code can be assumed to happen in a single thread. The latest browsers do support an API to create an equivalent to multiple JavaScript processes, known as Web Workers (described in a future article) that can only communicate via messages. No direct data sharing between processes is allowed.
Since execution is essentially single threaded, if JavaScript code executes for too long without yielding, the browser may prompt the user to stop the runaway code. If the user agrees, execution of the code will stop immediately and without warning. Even if the user does not stop the code, repetitive dialog boxes warning about long running code are likely to be annoying. Therefore we recommend that long-running work be done in small increments, using the timers and intervals provided by the browser to schedule functions to be executed in the future.
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.
Debuggers may run in the same process and the same JavaScript context as the code they try to debug, which reduces their stability. We found that all debuggers will eventually crash or fail either when code complexity becomes too high for them, or when they try to show too much information about watched variables. We also suffered from timer callbacks being invoked while stepping through code, which can break certain debuggers and can be very confusing for the developer. However, these tools are extremely valuable and are improving as the browsers are developed.
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.
Read more about:
FeaturesYou May Also Like