The RNG could literally just be a piece of code that takes the current second, divides by 60 to normalize it to a [0-1] range and nobody would notice the difference.
You would be surprised. And your precise example would show its limits very quickly during world generation, considering the process only takes a few seconds, as there would be big series of identical draws. Of course, splitting the second in a thousand parts instead of the minute in 60 would be better, and may even be enough if no more than one draw occurs before waiting for user input, but otherwise patterns may appear, because there could be the same delay between two draws. Imagine a game where user input is followed by two draws deciding on victory or defeat (near zero (<0.500s) meaning defeat, near one (>0.500s) meaning victory). Imagine that how the game is coded makes for around 600 ms between the two draws. Consequently, most of the time, it would give victory followed by defeat, or defeat followed by victory. Maybe most people would not care, but still there would be a clear pattern, and depending on the gameplay it could really be game breaking.
Now that I think about it, there is the option of checking if the game is a network game. This means if you write some super random number generator, which then happens to cause network desyncs, then we can write code which will make network games use the vanilla code while single player will use your new code. It seems to me that it's a bit messy to not use the same code in both single player and multi player, but it is an option, which can be used if everything else fails.
It would be sad in particular during map generation, where the game didn't yet really begin. However I don't know, maybe all clients generate the world separately, after having agreed on a seed ? That would pose a challenge to the use of a seedless RNG indeed, since the only workaround I see to this would be for all clients to perform all draws directly on the game host, and that would require some additional network coding.
That being said, I will probably come up with a deterministic RNG such as Xorshift as a start, and only after go undeterministic.
That's pretty much what I did with WTP. There was a request for using a random loading screen background each time the loading screen shows up and I ended up using the time passed since the computer booted modulo the number of images. It feels completely random to the player despite not actually containing anything at random at all.
For that very use, it's likely more than enough, especially considering that the time elapsed before the first loading screen, and between two loading screens, is dependent on user input. But here's the problem, some random generation techniques can apply efficiently to some use cases, but not in general. Hence, better have something that's strong in the general case.
Another thing you could have used is the current millisecond count. You can typically get those strait out of the box with most programming languages with what ever time class they use. And it looks random enough.
I've seen games seeding their RNG with the millisecond count, which basically means for the player they will encounter 1000 different game situations tops, in a situation where even with a deterministic RNG they should have around 2147483647 possibilities at the very least. I wonder what came up their mind, and why the heck they didn't stick to the default that's the tick count since boot...
The reason why pseudorandom generators are used in games really isn't to ensure randomness but to ensure repeatable randomness so that you can have features like Civ's "new random seed on reload" which allows you to permit or block save scumming. You can't have that without a seeded RNG.
One can't really prevent save scumming in some cases, and Civ in particular. Let's suppose you attack that archer with your tank and you lose, even when keeping the same seed you will reload, and change the order in which you move your units so that your archer vs tank battle will be evaluated on a different dice roll.
And btw, I never understood why save scumming has been opposed by some developers, it's a game after all, where's the evil if some people enjoy doing that (I personally don't but I can understand some people do) ? In the case of competitive e-sports things are different of course, but usually iron man mode is the rule there, so it doesn't apply.
Me too, because I've mostly been modding Unity games. Not that I would consider myself a C# expert though, actually I started because developers of a game didn't want to implement an easy feature, saying they didn't have the resources for that. I was certain it wasn't that difficult so I tried myself, and it wasn't indeed.
Btw, here's what I mostly use when modding the RNGs in Unity games. In our Civ IV case I would only have to use the integer generation part and the float generation part (the two "Range" functions). The float generation part is the most complicated because I wanted a maximum precision generator, while still being uniform. Most (if not all) floating point RNGs generating in, say, [0,1] will only generate a subset of all the possible values in the interval, many values being unreachable, such as Epsilon (the smallest nonzero value). My RNG keeps all values reachable and can indeed generate all possible floating point values in [0,1], or any given interval, with exceptions though, some viciously crafted intervals being likely to make it freeze, but these are highly unlikely to be used in practice. I may address this in the future however. Oh and please bear with me, there is no exception management at all, I only wanted to replace some game's RNGs and I was lazy about the rest
PHP:
using System.Runtime.InteropServices;
using UnityEngine;
namespace NovHak
{
public sealed class Random
{
[DllImport("Rdrexp.dll")]
private static extern uint rdrget32();
public static int Next()
{
return (int)rdrget32();
}
public static void InitState(int seed)
{
}
private static float GenPFRR(float min, float max)
{
float maxexp = 1.70141183E+38f;
while (maxexp >= max)
maxexp /= 2f;
if (maxexp == 0f) return 0f;
float cval;
do
{
uint mantissa = rdrget32() & 0x7FFFFF;
uint cvali = mantissa + (1u << 23);
cval = cvali;
cval /= (1 << 23);
cval *= maxexp;
float curexp = maxexp;
uint coins = rdrget32();
int rem = 32;
while (((coins & 1u) == 0u) && (curexp > min))
{
coins >>= 1;
rem--;
if (rem == 0) { coins = rdrget32(); rem = 32; }
cval /= 2f;
curexp /= 2f;
}
}
while ((cval < min) || (cval >= max));
return cval;
}
public static float Range(float min, float max)
{
bool negisbig = false;
bool doublesign = false;
if (min >= max) return min;
if ((min < 0f) && (max > 0f)) doublesign = true;
if (-min > max) negisbig = true;
if (!doublesign && !negisbig) return GenPFRR(min, max);
if (!doublesign && negisbig) return -GenPFRR(-max, -min);
float maxpos = max;
float sign, cval;
if (negisbig) maxpos = -min;
uint coins = rdrget32();
int rem = 32;
do
{
if (rem == 0) { coins = rdrget32(); rem = 32; }
if ((coins & 1u) == 1u) sign = -1f; else sign = 1f;
coins >>= 1;
rem--;
cval = sign * GenPFRR(0f, maxpos);
}
while ((cval <= min) || (cval >= max));
return cval;
}
public static Vector3 insideUnitSphere
{
get
{
float norm, x, y, z;
do
{
x = Range(-1f, 1f);
y = Range(-1f, 1f);
z = Range(-1f, 1f);
norm = x * x + y * y + z * z;
}
while (norm > 1f);
Vector3 vect;
vect.x = x;
vect.y = y;
vect.z = z;
return vect;
}
}
public static Vector3 onUnitSphere
{
get
{
float norm, x, y, z;
do
{
x = Range(-1f, 1f);
y = Range(-1f, 1f);
z = Range(-1f, 1f);
norm = x * x + y * y + z * z;
}
while ((norm > 1f) || (norm == 0f));
norm = (float)System.Math.Sqrt(norm);
Vector3 vect;
vect.x = x / norm;
vect.y = y / norm;
vect.z = z / norm;
return vect;
}
}
public static Quaternion rotation
{
get
{
float norm, w, x, y, z;
do
{
w = Range(-1f, 1f);
x = Range(-1f, 1f);
y = Range(-1f, 1f);
z = Range(-1f, 1f);
norm = w * w + x * x + y * y + z * z;
}
while ((norm > 1f) || (norm == 0f));
norm = (float)System.Math.Sqrt(norm);
Quaternion quat;
quat.w = w / norm;
quat.x = x / norm;
quat.y = y / norm;
quat.z = z / norm;
return quat;
}
}
public static int seed
{
get
{
return 0x7FFFFFFF;
}
set
{
}
}
public static Vector2 insideUnitCircle
{
get
{
float norm, x, y;
do
{
x = Range(-1f, 1f);
y = Range(-1f, 1f);
norm = x * x + y * y;
}
while (norm > 1f);
Vector2 vect;
vect.x = x;
vect.y = y;
return vect;
}
}
public static float value
{
get
{
return GenPFRR(0f, 1f);
}
}
public static int Range(int min, int max)
{
if (min >= max) return min;
uint diff = (uint)(max - min) - 1u;
if (diff == 0u) return min;
uint mask = 1u;
while ((diff & mask) != diff)
mask = (mask << 1) | 1u;
uint cval;
do
cval = rdrget32() & mask;
while (cval > diff);
return min + (int)cval;
}
}
}
And here's the platform-dependent DLL (Rdrexp.dll), that invokes the CPU-integrated, seedless RNG. Most CPUs have it nowadays, especially considering I'm only using RDRAND, which in fact is a pseudo-rng that's frequently reseeded with a true RNG. More recent CPUs even have RDSEED, that can be used instead as a true RNG, but my CPU isn't capable. As you can see the code is fairly simple, however, there are no capability checks or exception management, as I was running this on my computer only.
PHP:
#include <immintrin.h>
extern "C" __declspec(dllexport) unsigned int rdrget32() {
unsigned int val = 0;
while (!_rdrand32_step(&val));
return val;
}