Introducing: Viper

Chris McEvoy

At Velan Studios, we build and maintain our own engine and toolchain called Viper. Viper is the engine behind Knockout City and Mario Kart Live: Home Circuit. It includes standard things you might expect from any game engine such as a level editor, renderer, scripting language, etc. It also has less obvious components like network backend services and even a continuous integration system so Viper can build and test itself.

In this blog post we’re going to take a whirlwind tour / random walk of some key Viper ideas. But before we jump into that, let’s start with why we decided to build Viper at all.

Motivation

Early in Velan’s existence we had prototypes for our first two games: Knockout City and Mario Kart Live. True to our mission, each of these games was rather unconventional. Knockout City because it was online competitive dodgeball and Mario Kart Live because it was an RC Car with a webcam strapped atop. Unconventional games bring unique technical challenges, and it became clear to us that the best way we could address those challenges was our own engine.

We believe owning the underlying technology is critical to a game’s success. On Knockout City that meant we needed to own the code behind throwing and catching fast moving physics objects over the internet as well as network services around matchmaking. On Mario Kart Live that meant owning the technology for low latency computer vision and SLAM (the video pipeline).

But more than that, philosophically we believe in:

Removing barriers. As a small-ish team, we should be efficient generalists. We try really hard to make it so that a programmer familiar in one area of our code can be effective in other areas without having to learn some new third party library or language.
Using different tools to get different results. Our games are shaped by the tools we use to make them. If we want to make games that no one else is making, it helps to use technology that no one else is using.

One Big Pile of Code (and Data)

Viper, and all the projects that use it, exist in a single Perforce depot. Most developers work directly on the main branch. We have a continuous integration system that builds and tests Viper on each of its 10 supported target platforms whenever a change is introduced.

By keeping all our code in one place, we avoid release management complexity and make it easier to contemplate sweeping changes to the way Viper works. Fortunately, our games (so far) are small enough that it is practical to sync and build everything together.

Viper is written primarily in two languages (in roughly equal proportion):

C for engine, tools, and network services.
Proprietary scripting language (vscript) for game specific logic.

We’ll discuss these next.

C in Viper

When we started working on Viper (5+ years ago), we knew we wanted to use a mature systems language with nearly ubiquitous availability. This led us to seriously consider C++ and C. Ultimately we chose C, specifically C99, as our primary language. We did this for several reasons:

We wanted to keep the number of language-level concepts to a minimum. For example, C++ classes are an interesting organizational concept, but one that does not really map to the underlying hardware in a meaningful way. In other words, they become a potential source of artificial complexity. To be clear, C is not free of these types of concepts, but it certainly has less than C++.
We wanted there to be a reasonable correlation between the code on screen and the work that the CPU was doing. Translating C code into (pseudo-)assembly can often be done by inspection. C++ concepts like operator overloading and destructors can hide complexity to make it hard to see what’s happening at a glance.
C++ is a large language with sometimes uneven support across different implementations. For this reason, it is hard to rely on certain language features (historically that meant things like exception handling and RTTI). C++ and its libraries also frequently allocate memory, a resource that we like to keep under close control. For these reasons, using C++ to develop a game can be an exercise maintaining a list of what C++ features to avoid. We have not had this experience with C.
We wanted fast compile and link times. Iteration time is quite important in game development. C link times are usually much better than C++, especially in the presence of templates.

Over 5 years into our experiment in C, I occasionally miss having a destructor run when a variable goes out of scope, or operator overloading so I can more naturally add to vec3s. But overall the experience of staying in C has been pleasant, even freeing. C++ brings along patterns and dogma around the “right way” to do things. It is nice to think about how I want to solve technical problems on their merits without feeling guilty that I am not following some OO principle or expressing my solution in the most “elegant” way possible.

That’s not to say C++ is bad. It’s a very complex tool that, to use well, requires care. It may also require more braincells than the author typically has available.

Script in Viper

Early in the development of Knockout City, we decided that we wanted what we call the “simulation” stage of the game to have the following properties:

Ability to rewind state to past frames Deterministic computation
State easily replicated over the network Support for wide parallelism

The simulation phase owns all game objects and their behaviors. A designer or programmer working on gameplay will be writing the logic to drive game objects. We knew that creating interesting game mechanics was going to be hard enough without having to also think about rewind, determinism, replication, and parallelism (RDRP). To ease the cognitive load, we decided to create a scripting language. Any game object behavior implemented in script gets RDRP “for free.”

As development progressed, we implemented more and more things in script. At this point anything a player sees in our games is implemented in part, in script. This has some interesting benefits.

Script can be modified while the game is running. Coupled with an ability to pause and rewind the simulation, this is a powerful debugging tool. For speed, script can be compiled to C and then linked into a game executable, but script can also be interpreted by our virtual machine. Using the latter, we can make hotfixes to game logic and deploy those fixes into a live game client without having to push a full game patch. If you’ve ever seen the “Updating and Restarting” screen when logging into Knockout City, that’s an example of this hot fix in action. This ability has saved our bacon numerous times on Knockout City.

Input, Simulation, and Output

In the last section I mentioned the game simulation stage. In a normal game client, we have 3 stages:

Input stage - Owns window message loop. Reads controller, mouse, keyboard.
Simulation stage - Consumes data from input stage and from the network. Runs game logic and physics. Able to rewind state and is largely deterministic.
Output stage - Consumes data from simulation stage. Renders graphics and audio.

Each stage has an owning thread allowing it to update at a rate largely independent of the other stages. On Knockout City, we vary the simulation rate slightly in response to changing network conditions. But the output stage continues to render at device rate (typically 60Hz) by interpolating simulation data.

While each stage has an owning thread, 3 threads is not enough parallelism. For that reason, we use a job system to run nearly all compute bound work in Viper. In a typical frame, we will run over 1000 jobs.

Apps

Our games are typically composed of several distributed cooperating executables or processes. On Knockout City, that includes the game clients, game server, and network services. On Mario Kart Live, that includes the game client on the console and the firmware running on the vehicle.

During development it can be a hassle to setup and debug in a multi-process environment. This is one of the reasons that Viper introduces the concept of an “app.” An app is an encapsulation of all the elements in a process, but in such a way that we can have multiple apps running side- by-side in a single development process.

Every Viper thread has an owning app which can be accessed via a thread-local variable. Once the current app is obtained, other engine services can be queried from the app.

For example, developers working on Knockout City typically run the game with three primary apps in a single Win32 process: network service app (we call this the backend), the game client app, and the game server app. Each of these apps communicates over network sockets, as they would in production. But by running them all in the same process, we get a few benefits:

Standing up a local development environment is a simple as pressing F5 in Visual Studio.
Because we are running a single process, it’s easy to debug both sides of a network channel. Just place breakpoints at the sender and receiver.

Managing Game Assets

Viper uses a few different systems that form the basis of its approach to game assets.

RTTI – Viper has a runtime type information system that scrapes specially annotated types in engine header files. This allows the runtime to understand the layout and naming of all serializable data.
Marshal – Viper marshalling system is responsible for reading and writing any objects that have associated RTTI. We support JSON and binary file formats.

Import – Data produced outside of Viper (for example a texture saved to a TGA file) can only be used by Viper once it is data with RTTI. Viper’ s file importers convert external files into Viper objects and then use the marshalling system to write those objects to disk.
Datad – Datad is the name of a Viper app (see the previous section) that acts like a file server for Viper data. Other apps connect to datad over network socket and request files. Datad’s job is to return the contents of files to the client. If the client is requesting an imported file, and the import is out-of-date (e.g. the source file is newer), datad will invoke the appropriate importer to bring the file up-to-date before returning its contents. In this way, Viper supports build-on-demand and hot-reload of data for all clients.

To the Future and Beyond

This concludes our random walk of Viper concepts. In future blog posts, we will delve into some of the many areas of Viper not covered here. Until then.

"It prowls the streets in the pursuit of justice. Its origins are secret, its technology 21st century, its existence officially disavowed, but its presence undeniable. The perfect weapon for an imperfect future - Viper!"

—“Winner Take All.” Viper, Created by Danny Bilson and Paul De Meo, season 2, episode 1, Pet Fly Productions, 1996.