Random.Range: Int or Float?

How exactly do I determine if using Random.Range is operating to return floats or ints?
It makes a pretty big difference, especially since the max for one is inclusive and the max for the other is exclusive.

Just check the docs - it returns a float

Manual aside, you do have an IDE, right?

9450521--1326767--upload_2023-11-4_22-32-20.png

And then this:
9450521--1326773--upload_2023-11-4_22-34-6.png

1 Like

or an int…

4 Likes

Did you try reading that yourself? Maybe you should scroll down a little…

2 Likes

This is a prime example of a bad method overload: same number of parameters, similar parameters type (numerics). Unity should have had two different methods: .Range and .RangeF(loat) like Microsoft’s implementation (.Next and NextDouble). Well we’re stuck now with it. You just need to learn it the hard way, after being stung 2 or 3 times and spending hours debugging and wondering why the code does not work :slight_smile:

In a sense I agree. On the other hand it really, really bothers me when devs do this:

float value = 13; // implicit conversion: int to float

Get in the habit of writing integral floating point values by suffixing with ‘f’:

float value = 13f;

You won’t have this issue anymore:

float value = Random.Range(0, 1);

And then … the compiler has something to say about this:

int value = Random.Range(0f, 1f);

This is an error: cannot convert float to int.

6 Likes

Well, this is certainly debatable. Note that Random.Range works 100% as number multiplication or division. If both arguments are integers, the computer will carry out an integer multiplication / division regardless of the target type. As soon as one of them is a float, it will be a float division / multiplication and the result will be a float as well.

int can be implicitly converted into floats but not the other way round. This is just basic C# rules.

One should also keep in mind that the return type of a method does not contribute to the selection of an overload. In fact method overloads can not only differ in the return type. In theory one could argue that the compiler could figure out which overload is meant based on the type of the receiving variable or argument. However this could cause many more ambiguities, especially since you can call methods and ignoring the return value. In this case the compiler would have no clue at all which overload was meant. So only the method name and the arguments are taken into account when it comes to selecting the proper overload.

So if you want to complain, the blame would be on C# and the implicit type conversion between int and float.

ps: C# Math.Min also has those several overloads and would behave similar.

Certainly, writing:

float myFloat = 10;

should be treated as an error by our IDE. It was in the past but, for practicability I guess, it was removed.

I’d say it works 99.99999% the same way, since the returns of Random.Range(int a, int b) approaches b, while the returns of Random.Range(float a, float b) includes b. The type matters to the functionality. And across the dozen or so languages/corelibs I have seen, UnityEngine.Random’s the only one with such a snag.

1 Like

In most languages with promotion, it depends on promotion or demotion (down-promotion).

Up-promotion, such as int > float > double, is not an error.
Down-promotion, such as double > float > int, is an error.

1 Like

Well during my experience, bad method overloads caused significant problems in at least two occasions, one in particular, like 10 years ago, in which the whole IIS process of a website crashed randomly without any exception being thrown/caught/logged. The memory just grew exponentially then boom… crash, IIS website restarts and the website worked normally until it happened again few hours/days later. We spent weeks trying to reproduce the issue nothing! We couldn’t reproduce it. The culprit:

public void SendNotification(Notification notification, Guid deviceId)
{
    // send notification to user code here...
}


public void SendNotification(Notification notification, params Guid[] devicesId)
{
    foreach (var id in devicesId)
    {
        SendNotification(notification, id);
    }
}

Yep, it was a stack overflow, the 2nd overload called itself instead of calling the 1st method…

And from that time, I decided to never use method overload with similar argument types and/or the same number of parameters. And I think this is why Microsoft have a .Next and a .NextDouble in their Random implementation.

This is more explicit and cannot be mistaken:

public void SendNotificationToDevices(Notification notification, params Guid[] devicesId)
{
    foreach (var id in devicesId)
    {
        SendNotification(notification, id);
    }
}

This happens when you never bother learning why Unity is doing certain things on the way they are. It’s not a snag. It is a properly documented behavior with the explanation why it is. You just have to learn it. Unity is not .NET enterprise, stop handling it as it were.

1 Like

I didn’t say it was undocumented, and I understand their rationale entirely. It’s simply different from other libraries which chose different rationales, and that can be a snag for those who make assumptions based on experience. The point remains, the type used in the overload here matters to the functionality.

To answer the question, yes, there are 2 different Random.Ranges. One takes 2 ints and returns an int up to 1 less than the 2nd one. The other takes 2 floats and returns a float between either of them. It could return the last one, but since it uses floats, that barely matters: Random.Range(1.0f, 2.0f) could return 1.9, 1.97, 1.9993 … . The big difference is int or float.

The first thing to know is that C# uses a common thing called “overloading”. You can have two functions with the exact same name, but they count as different if the input types are different. The computer will figure it out, or give an error if it can’t. So Random.Range(1,7) rolls 1,2,3,4,5 or 6, and Random.Range(1.0f, 7.0f) can roll 1.2, 5.04, 6.84 and so on. It’s obvious to the computer since we specifically used 1.0f instead of 1 (and same for the 7).

The second thing to know is how C# fixes int/float mismatches. float f=7; is technically an error – putting int 7 into a float. But the computer assumes you wanted 7.0f and fixes it. But it won’t do the opposite int n = 3.6f; gives an error, because clearly the programmer was confused. Even int n=7.0f is an error, for the same reason (7 is correct, so why did they do extra work to be wrong?) So the rule is – ints can turn into floats, but not the other way around.

Knowing that, we can figure out what Random.Range(1, 6.0f) does. The computer can’t turn 6.0f into an int, but it can turn 1 into 1.0f. So this counts as (1.0f, 6.0f) and chooses the float version. The same thing happens with variables: Random.Range(n,f) uses the float version (assuming n is an int and f is a float). The short version: it only uses the int version if both inputs are ints, otherwise it uses the float one.

The last, last part is, what if you want the int version, but have float variables? Well, C# uses the standard way to turn floats into ints: Random.Range((int)f1, (int)f2); rounds them both down to ints first, and course uses the int version.

It’s also good to check yourself for cases when you’re not sure:

void Start() {
  print("==== 1,3.0f");
  for(int i=0;i<10;i++)
    print(Random.Range(1 , 3.0f);// should give things like 1.57

  print("==== (int)f1, (int)f2");
  float f1=5.3f, f2=7.99f;
  for(int i=0;i<10;i++)
    print(Random.Range((int)f1 , (int)f2); // should give only 5 or 6
}
1 Like

That feels like a problem with the compiler not handling “params” correctly. A call with a single Guid should use the first function, which is more specific so was clearly intended. Then, besides this bad choice of always choosing “params”, the compiler allowed you to write the first overload, which can never be used, and gave no error.

In fact, https://stackoverflow.com/questions/28561065/method-overloading-using-params-keyword from 8 years ago says your now works – overload resolution uses “params” as a last resort. So you were the victim of a buggy early compiler (or remembered wrong).

Oh, and of course the first version doesn’t even need to be there, but I can understand how it happened. There was probably a 1-Guid function, someone wanted to add a multi-Guid one, didn’t want to mess with existing code (by simply adding “params” to it) and added the 2nd one.

2 Likes

Exactly. The fact that the float variant includes the top limit is pretty irrelevant since we talk about floats and “one value” greater or smaller is something you can’t / shouldn’t really rely on anyways. Especially when you do math with that value.

Right. I use C# for quite some time now and params was always handled as a last resort. In fact a lot of the standard libraries in the framework often provide several explicit overloads to improve performance and only use the params version when more parameters are needed.

For example string.Concat (which is actually used when you “+” strings together) has several overloads taking 2, 3 or 4 explicit arguments as well as a params implementation to catch everything that requires more than 4. This is a quite common pattern in the CLR libraries and as far as I remember it has been like that for a long time.

1 Like

Yes you’re right.

I don’t remember it exactly but I know it was a method overload bug that caused a stackoverflow. I still have access to the TFS server, I’ll find the bug fix when I have time and post it here.

1 Like

Okay. So why is it doing it this way?
I’ve seen lots of things that I change my mind about once I see the real reason behind it. But I’m not seeing the reasoning here; I just see ambiguity that is real easy for someone (even an expert) to get wrong because a variable wasn’t what they thought it was. Ceil has CeilToInt so we can force an Int value when we need it; what is the reasoning behind Random.Range’s overload design?

1 Like

To help the simples way possible to feed the result into other systems while you’re making a game.
The float version is inclusive on both ends because the most common usage is 0…1, so it returns the most commonly used range 0…1, so you don’t have to mock around with values to get true 0…1 range as most systems expect values.
The int version is minInclusive…maxExclusive because of the most common usage is to select a value from a list or array or similar kind. And we know that’s 0…Length-1. So feeding 0…Length into the range is giving you the value in most commonly used range.
And when once in a blue moon you need maxInclusive behavior you can still add one there. Also once you read the manual how it works and it clicks, it’s easy to remember.
As for what happens when you put one float and one int in, once you learn how types in C# work, it will be clear.

1 Like