Taking some notes on Unity RNG, since I can't find any useful info about it online past the official docs.
Unity games are written in C# but the engine is in C++. Devs could use C#'s System.Random but Unity has its own Random class.
From the docs:
The generator is an Xorshift 128 algorithm, based on the paper Xorshift RNGs by George Marsaglia. It is statically initialized with a high-entropy seed from the operating system, and stored in native memory where it will survive domain reloads. This means that the generator is seeded exactly once on process start, and after that is left entirely under script control.
Unity provides the source code for the C# side of the engine, which is mainly C++ bindings. There's a leak of the full source from 2013, but I didn't look at it.
Random.bindings.cs is the relevant file.
Unity also provides debug symbols for its player which you can enable when you build a project. Opening UnityPlayer.dll with the symbols in Ghidra makes it fairly easy to find things. I'm using 2020.3.48f1.
GetScriptingRand() just returns gScriptingRand which is the global RNG struct?:
Rand * __cdecl GetScriptingRand(void)
{
return gScriptingRand;
}
Which seems to be set in InitScriptingRand()
void InitScriptingRand(void)
{
bool bVar1;
uint uVar2;
Rand *pRVar3;
undefined8 *puVar4;
char *pcVar5;
__uint64 _Var6;
__uint64 _Var7;
undefined local_res10 [8];
__uint64 local_68 [2];
__uint64 local_58 [5];
__uint64 local_30;
__uint64 local_28;
longlong local_20;
ulonglong local_18;
__uint64 local_10;
pRVar3 = (Rand *)operator_new[](0x10,0x4f,4,"",0x1a);
if (pRVar3 != (Rand *)0x0) {
*(undefined4 *)pRVar3 = 0;
*(undefined4 *)(pRVar3 + 4) = 1;
*(undefined4 *)(pRVar3 + 8) = 0x6c078966;
*(undefined4 *)(pRVar3 + 0xc) = 0x714acb3f;
bVar1 = GetSystemEntropy((uchar *)pRVar3,0x10);
if (!bVar1) {
puVar4 = (undefined8 *)GetCurrentTimeAsDateTime(local_res10);
local_58[4] = *puVar4;
local_30 = UnityClassic::Baselib_Timer_GetHighPrecisionTimerTicks();
pcVar5 = GetUnityBuildFullVersionNoSpaces();
_Var7 = 0xffffffffffffffff;
_Var6 = 0xffffffffffffffff;
do {
_Var6 = _Var6 + 1;
} while (pcVar5[_Var6] != '\0');
local_68[0] = 0;
local_68[1] = 0;
SpookyHash::Hash128(pcVar5,_Var6,local_68,local_68 + 1);
local_28 = Hash128::PackToUInt64((Hash128 *)local_68);
LOCK();
UNLOCK();
local_20 = (longlong)(DAT_1819b4714 + 1);
DAT_1819b4714 = DAT_1819b4714 + 1;
uVar2 = GetCurrentProcessId();
local_18 = (ulonglong)uVar2;
pcVar5 = systeminfo::GetDeviceUniqueIdentifier();
do {
_Var7 = _Var7 + 1;
} while (pcVar5[_Var7] != '\0');
local_58[0] = 0;
local_58[1] = 0;
SpookyHash::Hash128(pcVar5,_Var7,local_58,local_58 + 1);
local_10 = Hash128::PackToUInt64((Hash128 *)local_58);
local_58[2] = 0;
local_58[3] = 0;
SpookyHash::Hash128(local_58 + 4,0x30,local_58 + 2,local_58 + 3);
*(__uint64 *)pRVar3 = local_58[2];
*(__uint64 *)(pRVar3 + 8) = local_58[3];
}
gScriptingRand = pRVar3;
return;
}
gScriptingRand = (Rand *)0x0;
return;
}
On Windows, GetSystemEntropy() calls BCryptGenRandom. On Linux it calls /dev/urandom or /dev/random. Rest of the code is a fallback.
It seems like each Random function/property/method advances the global RNG state itself, rather than, say, having a single function to advance the state, then converting that value into the desired format. Though, each function seems to use the same algorithm.
There is a Rand struct and a RandN struct. Rand takes 4 values and RandN takes 16. I don't think any of the normal Random functions use RandN, it seems to be used for things like random particles.
Some of the implementations (as far as I can tell):
void __cdecl Random_CUSTOM_InitState(int param_1)
{
Rand *pRVar1;
int iVar2;
pRVar1 = GetScriptingRand();
*(int *)pRVar1 = param_1;
iVar2 = param_1 * 0x6c078965 + 1;
*(int *)(pRVar1 + 4) = iVar2;
iVar2 = iVar2 * 0x6c078965 + 1;
*(int *)(pRVar1 + 8) = iVar2;
*(int *)(pRVar1 + 0xc) = iVar2 * 0x6c078965 + 1;
return;
}
float __cdecl Random_CUSTOM_Range(float param_1,float param_2)
{
uint uVar1;
Rand *pRVar2;
uint uVar3;
float fVar4;
pRVar2 = GetScriptingRand();
uVar3 = *(int *)pRVar2 << 0xb ^ *(uint *)pRVar2;
*(undefined4 *)pRVar2 = *(undefined4 *)(pRVar2 + 4);
*(undefined4 *)(pRVar2 + 4) = *(undefined4 *)(pRVar2 + 8);
uVar1 = *(uint *)(pRVar2 + 0xc);
*(uint *)(pRVar2 + 8) = uVar1;
uVar3 = (uVar1 >> 0xb ^ uVar3) >> 8 ^ uVar1 ^ uVar3;
*(uint *)(pRVar2 + 0xc) = uVar3;
fVar4 = (float)(uVar3 & 0x7fffff) * 1.192093e-07;
return (1.0 - fVar4) * param_2 + fVar4 * param_1;
}
int __cdecl Random_CUSTOM_RandomRangeInt(int param_1,int param_2)
{
uint uVar1;
Rand *pRVar2;
uint uVar3;
pRVar2 = GetScriptingRand();
if (param_1 < param_2) {
uVar3 = *(int *)pRVar2 << 0xb ^ *(uint *)pRVar2;
*(undefined4 *)pRVar2 = *(undefined4 *)(pRVar2 + 4);
*(undefined4 *)(pRVar2 + 4) = *(undefined4 *)(pRVar2 + 8);
uVar1 = *(uint *)(pRVar2 + 0xc);
*(uint *)(pRVar2 + 8) = uVar1;
uVar3 = (uVar1 >> 0xb ^ uVar3) >> 8 ^ uVar1 ^ uVar3;
*(uint *)(pRVar2 + 0xc) = uVar3;
return param_1 + uVar3 % (uint)(param_2 - param_1);
}
if (param_2 < param_1) {
uVar1 = *(uint *)(pRVar2 + 0xc);
uVar3 = *(int *)pRVar2 << 0xb ^ *(uint *)pRVar2;
*(undefined4 *)pRVar2 = *(undefined4 *)(pRVar2 + 4);
*(undefined4 *)(pRVar2 + 4) = *(undefined4 *)(pRVar2 + 8);
*(uint *)(pRVar2 + 8) = uVar1;
uVar3 = (uVar1 >> 0xb ^ uVar3) >> 8 ^ uVar1 ^ uVar3;
*(uint *)(pRVar2 + 0xc) = uVar3;
param_1 = param_1 - uVar3 % (uint)(param_1 - param_2);
}
return param_1;
}
float __cdecl Random_Get_Custom_PropValue(void)
{
uint uVar1;
Rand *pRVar2;
uint uVar3;
pRVar2 = GetScriptingRand();
uVar3 = *(int *)pRVar2 << 0xb ^ *(uint *)pRVar2;
*(undefined4 *)pRVar2 = *(undefined4 *)(pRVar2 + 4);
*(undefined4 *)(pRVar2 + 4) = *(undefined4 *)(pRVar2 + 8);
uVar1 = *(uint *)(pRVar2 + 0xc);
*(uint *)(pRVar2 + 8) = uVar1;
uVar3 = (uVar1 >> 0xb ^ uVar3) >> 8 ^ uVar1 ^ uVar3;
*(uint *)(pRVar2 + 0xc) = uVar3;
return (float)(uVar3 & 0x7fffff) * 1.192093e-07;
}
Simplifying the Random.value code:
Rand* rng = GetScriptingRand();
uint t = rng->s0 << 11 ^ rng->s0;
rng->s0 = rng->s1;
rng->s1 = rng->s2;
uint w = rng->s3
rng->s2 = rng->s3
t = (w >> 11 ^ t) >> 8 ^ w ^ t;
rng->s3 = t;
return (float)(t & 0x7fffff) * 1.192093e-07;
This is the same as the algorithm in the Summary of the Xorshift paper (when you rearrange the xors and shifts, which was not obvious to me):
unsigned long t;
t=(xˆ(x<<11));x=y;y=z;z=w; return( w=(wˆ(w>>19))ˆ(tˆ(t>>8)) );
Initialisation
ulong GetSystemEntropy(uchar *param_1,ulong param_2)
{
__fd = open64("/dev/urandom",0);
uVar1 = read(__fd,param_1,param_2);
}
Where param1 points to the random struct and param2 is the size of the struct (16 bytes).
In the Unity version I tested, first call to GetSystemEntropy is by another function (InitUNETRand), and then InitScriptingRand is second.