Random event - is this a bad idea?

it works, but i doubt the efficiency of it. In a simple 2D game its prob not as much of a problem. but i want to learn best practices.

randomNum1 = Random.Range(1, 1000);
dropMine = 100;

if (dropmine == randomNum1) {
    DropMine();
}

What’s wrong with random? If it fits your game then there’s nothing wrong.

was just thinking of having the processor running loads of random numbers in the background. its still pretty small on processing? was just curious.

The processor only calculates the next random number when you ask it too.

And that calculation isn’t really all that difficult to calculate. It’s just some basic arithmetic. The algorithm of the random number dictates what that arithmetic is… but it’s usually just some multiplication/modulo/addition/bit shifting. All things your processor can do VERY FAST. When your CPU says it does 2ghz, it means that it can do 2 billion of any of those operations per second. So I doubt 5 or so of them is all that bad. Hell… things like calculating the distance between 2 objects in 3d-space is probably more expensive (3 subtractions, 3 squares, 2 additions, 1 squareroot). And people do those every frame all the time. And squareroots are way more expensive than any mult/sub/add/mod/bitshift

Now I would argue the determination of when that would drop is… uhhhh… really hard to say. You have a 1:1000 odds every frame. My beef with this approach is there’s no real easy way to suggest WHEN that will happen. And even harder to adjust the odds.

Like what does the “100” mean? Nothing reallly… it could be 0, it could be 732. All values have the same meaning… 1:1000 chance of dropping every frame.

Also it means your drop frequency is tied to framerate… If I play on a better computer I actually could get way more mines being dropped. Is that really fair?

Also, our distribution odds are getting shared. Note that Random uses a global random state. But a pseudo-random number generator distributes evenly. This means that if you call a Range(1,1000) 1000 times you should (in theory) get every possible result in that range once (in theory of course… really the underlying range is guaranteed usually being something like 0->RNG mod range. How that distributes across your range and float conversion/error may cause ripples in your distribution). Thing is… what if another script is ALSO calling Random and it steals your 100 from you… now you have to wait another max(1000) frames for another chance.

Instead…

Why not something like this:

public float DropFrequencyMin = 0.5f; //minimum time between mine drops
public float DropFrequencyMax = 1.5f; //maximum time between mine drops

private float _timer;

void Start()
{
    _timer = Random.Range(DropFrequencyMin, DropFrequencyMax);
}

void Update()
{
    _timer -= Time.deltaTime;
    if (_timer <= 0f)
    {
        _timer = Random.Range(DropFrequencyMin, DropFrequencyMax);
        DropMine();
    }
}

Now we can better gauge the drop frequency.

Of course there are other ways you may want to control it… maybe instead of duration of time, it could be distances, or a constant number of mines but scattered over a region… they all would be different algorithms. This one I showed is just one.

4 Likes

One key takeaway of what @lordofduct outlines above is that the way you pick random numbers can have a profound outcome on the “nature” or “feel” of the resulting gameplay.

For instance if I say I will ring a bell once per day, and I pick the random time of the day, when that time comes up I ring the bell, you can know that I will ring that bell once a day for sure.

But if I slice up the day into 24 hours and every hour I roll a 24-sided dice and if it comes up 1 I ring the bell, you might get no bells for a very long time, or a bunch of bells all scattered around the middle of the afternoon for example.

There’s LOTS of ways to pick random numbers, each one with different costs and implications to the feel. Another construct often used is a deck, as in a deck of cards. To do the “one bell per day” thing above, I could make a deck of 24 cards, scribble a bell on only one card, and then shuffle the cards each day, and each hour pick a card. In a particular hour if I pick the bell card I ring it, that gives me one per day.

The deck is useful if you want many things to happen in a very reliable timeframe. Let’s say I want you to ring a bell once a day, and bang a drum three times per day, and all the other hours, please be quiet. I’d make a deck with three drums, one bell and 20 blank cards, shuffle up and go.

Notice that there is absolutely nothing in your original post to suggest that the code runs frequently or in the background.

1 Like

True but come on, this is likely in Update(). :slight_smile:

I don’t know why you think that. Update() would be a poor place for that code, because that would mean mines drop at a framerate-dependent frequency.

That’s the way I took it to mean combined with their follow-up post. I mean… where else would that code go for dropping mines? No matter where it was ran it would be a pretty bad way to drop mines.

Which is why in my post I said that it’d make for terrible random mine dropping as it would be framerate dependent.

In the same respect, from what was posted, I also suspected OP was unaware of the fact it would be framerate dependent. They were concerned Random.Range would be taxing on the CPU.

It could be in FixedUpdate, or it could be in some sort of level-generation code, or it could be triggered whenever a specific event happens (like a collision, or when your gun shoots, or when you activate your hyperdrive).

I mean, yes, the follow-up post certainly implies that it’s running frequently in the background. My whole point is that this is information that should have been in the OP, because it is in no way obvious if you don’t actually say it.

Yeah, it could be.

Not disagreeing OP is vague.

I’m just saying I also took it that way. People on these forums are often stupidly vague, I often post that we need more information when it happens (as do you I’ve seen). Regardless of that though, I still can infer an implication from a post, and that was the implication I inferred.

Of course I don’t know why Thygrrr would say it’s “obvious”, but I didn’t say that. Maybe it was just obvious to them.

I also don’t know why there is this much back and forth about it.

The good news is that if I know Epoch, they’re listening to all of it and soaking up. If you doubt me, go look at the last month or two of Epoch’s posts… Epoch is doing all kinds of crazy stuff in Unity… it’s GREAT!! Gun turrets, particles, walls, colliders, long scrolling backgrounds, sailing ships, moving 2D seas, room-dungeon creation, code optimization… I’m telling you, @epochplus5 is my Unity3D forum HERO!

1 Like

lol

:looks through post history:

Damn, I wanna see this dude’s game/s now.

1 Like

I am nearing the completion of my first full game now, its just an updated Asteroids clone.
But i have been toying with concepts for a while now. Just things that come to mind. All 2D though, think it was a good place to start.

Sorry if i seemed vague, didn’t think you guys would dig in as deep as you did. I was just curious about the random number method i came up with. Was just thinking does the computer have to keep generating this huge list of numbers for such a simple result. anyway my code looks like this:

void FixedUpdate()
    {
        CheckWhenFollow();
        FollowPlayer();
        randomNum1 = Random.Range(1, 1000);
        dropMine = 100;
    }

I like working on multiple concepts at once, so if i get stuck there is always something else to do while i wait for replys or help to push me in the right direction. Have to say that the guys on here have been amazing! if this forum didnt exist i would have given up a long time ago.

1 Like

Ha! Me too. If I lose interest in one, I dig through the morass of projects, break out another one. I always learn something in every sitting it seems.

Random numbers can be pretty “lightweight” to calculate, depending on how you do it. Here’s what Unity uses:

https://docs.unity3d.com/ScriptReference/Random.html

The XorShift routine is just a few bitshifts and XOR operations, all of which are super-cheap CPU-wise.

It’s always a pleasure to help people who are jumping in 100% and it always seems you are.

I just want to add something that may have been looked over. While most (see below) is true what others have said here, you should ask yourself how often you really want that event to happen in the min / max case. Because rolling a new random number every frame means, in case you’re really lucky, you can get two events in two consecutive frames. That’s absolutely possible since it’s random. If you’re even much much more lucky you could get 10 events in 10 frames (though the chance that this happens is almost zero).

Rolling a new number every frame is also a frame dependent process. So picking one out of 1000 means you have a one in 1000 chance to pick that number. On average that means you get roughly one event every 1000 frames. Keep in mind that is just the “long term average”. At 60 fps, rolling one value every frame, that’s about one event every 16.66 seconds. Again, just the average, as I said you could get two events in two frames or just one event in 5 minutes if you’re unlucky.

Generally for random events you want to roll that random number on a fix timer. For example rolling one random value between 0 and 100 every 0.1 seconds would roughly yield one event every 10 seconds. In this case the minimum time between two events is our 0.1 seconds since it can not be faster than that. You can always increase the time interval between your rolls while at the same time you reduce random range accordingly. This would result in roughly a similar average chance but in a different minimum delay between those events.

For example if you want an event to happen say about once every 10 minutes. You could roll one random value every frame like this:

// every frame
int max = (int)(10*60f / Time.deltaTime);
if (Random.Range(0,max) == 0)
{
    // event
}

If our framerate is 60 fps our max value will be 36000 in that case. However instead we could use a timer that runs say once every 10 seconds and in the timer we do

// once every 10 seconds
int max = 10*60 / 10;
if (Random.Range(0, max) == 0)
{
    // event
}

Here we get the roughly the same probability (roughly one event every 10 minutes) but the minimum delay between two events is at least 10 seconds since that’s how fast our timer is ticking. Of course you can’t expect no change in the probability since almost all random number generators which are restricted to a certain range are not perfectly uniformly distributed. You have to look for a “fair” random number generator. But theoretically running this every 5 minutes would also result in an average of one event every 10 minutes:

// once every 5 minutes
if (Random.Range(0, 2) == 0)
{
    // event
}

So each time our timer runs we have a 50% chance of triggering an event. Of course events can only be generated once every 5 minutes in this case.

While the chance of getting two events in two consecutive frames is really low, events just a couple of frames apart may also be undesired. So for long term events I would always recommend to use a relatively slow timer to increase the min delay between two events.

So this is not really about the computational complexity but more about the probability and if you want / can live with the fact that the event may be triggered twice only a couple of frames apart.

ps:
The original code used

    randomNum1 = Random.Range(1, 1000);
    dropMine = 100;
   
    if (dropmine == randomNum1) {
        DropMine();
    }

Note that it makes no difference if you compare the random number to 100, 42 or any other number from the range you selected (1 - 999 in this case) since they are all more or less equally likely.

1 Like

Looking more closely at earlier stuff…

Ahhh, NO! It absolutely does not mean that!

A pseudo-random generator will eventually loop. You can put an upper bound on how long that loop is by the size of the generator’s internal state. Unity uses Xorshift 128, which means the internal state is 128 bits, which means it will loop after no more than 2^128 outputs (because there are 2^128 possible internal states, so that’s the longest it could possibly go before repeating one of them). It might loop in less than that–many generators go through less than all of their states before they loop (though hopefully only slightly less). I think Xorshift 128 actually maxes out with a period of 2^128 - 1, rather than 2^128 exactly.

Important: The period does not change depending on whether you are calling Range(0, 10) or Range(0, 1000000). You still need to go 2^128 - 1 steps before you loop.

Because there’s a loop, if you asked for 2^128 - 1 numbers and wrote them all down, and then asked for another 2^128 - 1 numbers, the second batch of numbers would look the same as the first.

A good generator should also output all possible numbers approximately equally often. So within each batch of 2^128 - 1, you should expect each output to be approximately equally common. (Whoever designed the generator will have tried to make it close to equal, but you can’t count on any given random generator being perfectly ideal in any given metric. So while the two batches will look exactly like each other, it’s not guaranteed that that they will be exactly evenly-distributed.)

But calling Range(0, 10) ten times is definitely not guaranteed to give you every answer once. In fact, the generator will be intentionally designed so that it looks like those 10 answers are basically independent–as if you rolled a 10-sided die 10 times.

Similarly, calling Range(0, 1000) 1000 times will look like you rolled a 1000-sided die 1000 times. It is very unlikely that you will get every number exactly once.

This absolutely does not make any practical difference. Unless you are making something close to 2^128 calls to the random number generator, the results will look independently random (like rolling a die). You will not notice a difference from whether someone else is also asking for numbers.

That’s why using a single global generator (like Unity does) is standard practice.

Generally, the only reason you need to isolate your random number generator from other users is if you are trying to get a reproducible sequence from a fixed seed.

Technically 1:998 odds. Random.Range with integers uses an exclusive upper bound, so Random.Range(1, 1000) outputs in the interval [1, 999] (i.e. it will never output 1000 exactly), so the probability of any particular number is 1/999, which is equivalent to odds of 1:998.

2 Likes

Absolutely spot on ^^. I haven’t even read that part ^^. I think I should reword my introduction ^^.

Jesus christ dude it’s called an over-simplification. Note I said:

(noting that when I said RNG mod range, I’m referring the range the RNG loops at).

I over simplified for generalized sake so I didn’t have to spend an hour explaining the underlying facets of how RNG is done to someone I wasn’t quite exactly sure about their skill level.

My main point just being… the logic is completely arbitrary as to when/how it may occur.

Well actually no… Unity uses an inclusive upper range.

As @Kurt-Dekker linked:
https://docs.unity3d.com/ScriptReference/Random.html?_ga=2.131595407.1074440568.1615666135-1657631297.1606177784

Though I do think they are using the int one, which is exlussive. So you’re correct with that method. Which is only another gripe on my list of annoyances with the Unity API.

…

I swear… once in a blue moon I decide to NOT do my usuall wall’o’text because I’d done it 100 times that day already and people generally roll their eyes at me because of my walls’o’text.

And that one time I don’t… per usual Antistone comes out of the wood work to “well actually” me with his autismoray.

I apologize Antistone… and anyone else who read that paragraph of my post… next time I’ll remember to be way more specific and explanatory in what I meant rather than blithly rambling like I sometimes do. I sometimes forget that rambling is against the forum rules. And I’m serious, that’s my fault… I reread my post and see that the way I worded things could be misconstrued in a different way then they sounded in my head. You taking them that way is in the plain text as it stands there. That’s on me. I was honestly being lazy with my post. I’m not being sarcastic when I say that… lordofduct, you should be less lazy.

Back to my sarcasm though…

It’s a good thing the post police showed up, otherwise it could have confused more people in the future.

Yes. People love to talk about the details and how they can be “cracked” and so on. But for most purposes you can think of it just like a die roll. Even Random.Range(5,12+1) is rolling a die with however many faces with 5 through 12 written on them.

Want to know if Random.Range(1,100) rolls a 1 every hundred rolls? Well, do the percentile dice in your dice bag do that?

As far as “good” randomness, I often use everything together: a timer so you can’t get one within 0.5 seconds of the last, but always get one after 10 seconds. An increasing chance to get one as time goes by. Often I’ll roll “time until next”, with extra IF’s (like a 5% change to wait a long 7-9 seconds, but not if the last was that way). Also an increasing chance based on their score. Sometimes even an overall “mode”: clumps of 3-4 with 8 seconds in-between, or 1 exactly every second for 8-12 seconds, or placing them slowly then faster and faster for 15 seconds. Just playtest and see how it looks then think “That’s OK, but I wish it would…”.