The game is being written in Rust programming language.
For me having mostly C++ background this is an experiment. Being 3 months in I must say it turns out well so far.
In many areas Rust has slight advantage over C++, none of these is significant on its own, but together they accumulate in a noticable productivity boost.
One things that stroke me pretty early: how easy is to work with third party code in Rust.
There are number of reasons for this:
1. Build System
Rust comes with a standard build system. It is called Cargo and it does not suck. You may argue that there are plenty of great build systems available for C++, but this often turns into a problem. Integrating third party code often starts with learning a new build system. MSBuild, CMake, GNU Make, QMake, NMake, SCons, you name it. Honestly it is so divergent and and time consuming that people start praising header-only libraries to avoid this problem. Dealing with headers is another problem that Rust elimintes.
Another process that Rust’s Cargo unifies - distribution of libraries. Adding a library (a “Crate” in Rust terms) dependency - is a matter of adding a single line into the build script.
2. Standard Library and Traits
Another reason why it is easy to use third party code - is a sane portable standard library. Most of libraries use it and it makes interoperation easier.
Functionality-wise it is not as rich as in Python, but I had a good experience so far with: containers (I know, it is easy to be faster than STL, but still), UTF8-strings, IO, FS and Networking. It also exposes set of reusable extractions as traits, that are actually useful.
Trait is akin an interface, but it can be interchangably used in the context of generic (template) or dynamic dispatch (virtual interface). They can be implemented outside of type definition making a lot of dependencies optional and considerably improve diagnostics in comparison with C++ templates.
3. Enforced Style
Rust Compiler enforced particular style. This may sound like a minor detail, but it makes a difference when reading code. You see, it is not just easy to use third party library in Rust, but it also easy to read through the code. As soon as Cargo has fetched the library (after first build) - you can step into functions and follow the code without being distracted by formatting or naming nuances.
A lot of elements in the game are modelled as finite state machines. Implementing these as Rust enums is a pleasure. How is it different from traditional C++/C# enums? They are actually discriminated unions. Some Rusteceans call them algebraic data types, but essentially one can add fields to variants of enum. Here is some simple example:
Here PreJuimp, Jump and Flight states have additional field. There is a number of ways how this can be implemented in C++/C#, either a struct containing all the fields or something like std::variant<>, but all these options have disadvantages: either mash up of fields belonging to different place with unclear lifetimes, risk of misusing incorrect union fields, or inability to use
Furthermore rust offers syntax that allows easily to decompose enums, i.e. combining what would be a switch statement in C++ with an access to its members. I.e.
Another place where rust enums shine - networking code, I use them extensively for both server states and protocol messages.
Rust uses macro for code generation and it saves a lot of time on boilerplate code, for things like tracing, cloning, serialization, hashing, comparisons or reflection. Commonly used Serde-serialization crate is fast and easy to use.
Safety and Performance
For client game code Rust strikes nice balance between safety and performance. As it does not use garbage collection runtime performance is predictable. Yet borrow checking and strict aliasing rules eliminate whole class of bugs that are typical for C/C++ software, namely: use after free, dangling pointers.
Bounds checking having its cost helped me to catch a lot of bugs early on.
Move sematics is done well, deep-copies are always explicit and easily avoided when possible.
Although I have not done much except for server, but writing threaded code is a breeze and its easy to implement exhaustive error handling thanks to Result<> type and compiler diagnostics associated with it.
There are couple things that caused frustration for me:
- Compile times are in C++ land. Sometimes I even have to turn off debug symbols to keep iteration on debug builds lower.
- Performance of debug builds can be 10 to 30 slower in release. Especially when using code that is heavy on “zero-cost” abstraction, notably iterators. This forced me to split project into multiple crates, just so I keep performance critical things like physics, rendering and sound optimized even in debug builds.
- It took me around six months of casual Rust programming to get comfortable with borrow checker. It gets predictable when you understand borrowing rules, but on the way there it is frustating, especially if you’re porting existing C++ code.
- Code quality in Crates.io is rather mixed and library choice is still limited.
Overall I feel like I was more productive and the code ends up being more maintable after three months of work.