Floating-point math is fast these days, but fixed-point still has a purpose: we can use it to store real numbers in less than 32 bits. Saving a measly 16 or 24 bits off a float
might not sound appealing, but cutting the data size in half or quarter often does when multiplied across large amounts of real numbers. We can shrink downloads, improve load times, save memory, and fit more into the CPU’s data caches. So today we’ll look at storing numbers in fixed-point formats and see how easy it can be to shrink our data!
Fixed-Point: Shrink Data Sizes 4x
NativeArray2D
Unity provides exactly one collection: NativeArray<T>
. Compared to managed arrays in C#, these must be one-dimensional. So today we’re building a two-dimensional version of it: NativeArray<T>
. We’ll add this to the NativeCollections GitHub repository for easy inclusion into any project. Read on to learn more about the collection!
Native Memory Allocators: More Than Just a Lifetime
Unity provides different types of native memory allocators via the Allocator
enum: Persistent
, TempJob
, and Temp
. These names indicate the lifetime of the memory allocation: forever, a few frames, and one frame. They also indicate an underlying algorithm to perform the allocation and deallocation. These algorithms have varied results on our code that uses them, and that’s what we’ll explore in today’s article.
Using Managed Types In Jobs
Job structs can’t contain managed types like string
, class
instances, or delegates. This is currently a pain as a lot of the Unity API relies on these and so we’re forced to deal with them. Today we’ll talk about how we can use managed types in our jobs to bridge the gap.
Adding Bit Fields to C#
Bit fields give us the ability to use less than one byte to store an integer. This can be really useful to fit more data into a CPU cache or to reduce memory usage in general. Unfortunately, bit fields aren’t built into C#. Today we’ll work around that by creating our own!
Strongly-Typed Integers
An int
can be anything: points, health, currency, time, etc. We often make mistakes using one int
where another int
was supposed to go. Imagine a function DoDamage(int, int)
. It’s not obvious what the parameters mean. Today we’ll use the C# type system to make the code much more readable and less error-prone!
Simple Formula Evaluator Revisited
A couple years ago, I wrote an article showing how to empower game designers with the ability to write simple formulas like PlayerLevel*100+100
. These are much more useful than just constants and don’t require any of the complexity of a real programming language. Today we’ll bring it into the Burst-compatible world and also improve its ability to handle more complex formulas.
Burst-Compiled Generic Algorithms
Many algorithms get used over and over: searching, sorting, filtering, etc. C# makes these available with LINQ and functions like Array.Sort
, but these can’t be compiled by Burst because interfaces and delegates aren’t supported. So how do we implement these generic algorithms to avoid re-writing them over and over? Today we’ll explore one technique to solve this problem. Read on to learn how!
Steal Some More Bits
Today we continue stealing float
bits, but in an entirely different way this time. We’ll end up with the ability to switch between float
and 21-bit integer modes and to know which mode we’re in. We can do all of this without using any more than four bytes just by exploiting a little knowledge of the float
data format. Read on to learn how!
Optional<T>: A Nullable<T> Alternative
Today we’ll make a new type that addresses some of the deficiencies in Nullable<T>
. We’ll end up with a good tool for dealing with operations that may or may not produce a result or take a parameter, even in Burst-compiled code. Read on to see how it works!