guidebook/storage/test/test.md

8.3 KiB

I want to get a random value in Isaac!

Well I'm glad you asked, because this simple question has many anwsers depending on how much effort you want to put in!

There are 3 types of random you can use in Isaac, going from the one that requires the least amount of effort to the one that requires the most amount of effort. There is no "objectively best" method, as it depends on your usecase, the size of your mod, and how much you truly care.

Why?

For this guide, let's assume your usecase is for adding a random vector to an entity's velocity:

entity.Velocity = entity.Velocity + randomVec

These same methods can be applied to just about anything random in Isaac (like generating a random item), but we'll be specifically using this usecase for our examples.

math.random

Lua's vanilla math.random is what a typical Lua veteran coming to Isaac will choose. And it makes sense; it has a simple, intuitive interface, and it just, kind of works. There's no setup, it's cross-version, cross-API, and works anywhere.

local randomVec = Vector.FromAngle(math.random() * 360)

But there's quite a lot of downsides!

1. math.random can be affected with math.randomseed

If any mod, any mod that you have loaded runs math.randomseed, the math.random seed will be set globally. If Mod A runs math.randomseed, the random values of Mod B will be affected. And this could be happening at a rate of once per render frame, meaning you can potentially never get actually random outputs.

This is a pretty massive dealbreaker for our use-case. You can still kind of get around this by doing:

math.randomseed(Isaac.GetTime())
local randomVec = Vector.FromAngle(math.random() * 360)

But this still has the other 2 downsides.

2. The seed does not update with the game's seed

This is a pretty minor issue, but it matters to some. When Isaac starts a run, it will create a seed (or use a user-given seed). This seed will be used for every element of Isaac's RNG. This means that if two players use the same seed, the first item they'll find in an item room will be the exact same.

Now, if you generate some value with math.random (for instance, how many items you find in an item room), this will no longer be the case; math.random is initially seeded on the amount of seconds your computer has been on for [citation neeeded]. This also means that co-op games with netstart will no longer function correctly; one computer will generate one value, the other will generate another, and this'll result in a desync.

You can still fix this, if you're desperate, however!

math.randomseed(Game():GetSeeds():GetStartSeed())
local randomVec = Vector.FromAngle(math.random() * 360)

This is beginning to be a bit cumbersome, however... Not only will this only generate a unique value once, it still does not protect us against the third downside!

3. math.random does not function correctly cross-platform

Lua 5.3 uses C's rand() for random number generation. C's rand() implementation depends on the compiler used (GCC, Clang, MSVC, etc.) [citation needed]. What this means for your mod is what values you recieve from math.random with the same seed are dependant on the host's OS; if you're running Windows, you'll get one value, if you're running Linux, you'll get another.

Now, this isn't a massive deal breaker either. However, there's no way of fixing this without using an alternative pseudo-random value generator. But what this means for your mod is two people on different OS's will get different values from the same seed, potentially desyncing online co-op games.

It's also worth noting that as of Repentance, only a Windows build is available; thus, Linux and Mac must play on Wine/Proton, emulating Windows's rand() implementation, rather than using the native one. Because of this, this may be irrelevant in your use case - but it's not known if native Linux/Mac builds release, and it will still behave this way on Afterbirth+.

math.random - TL;DR

math.random is a nice, simple, but lazy method for getting a random value. But it suffers from potential exploitation, it ignores the seed, and has issues behaving the same on different operating systems. It's good as a temporary, simple solution, but highly unrecommended to use in larger mods, especially if you want online co-op to function. However, it may also be useful as a simple seeded random number generator.

Random() / RandomVector()

Random and RandomVector are Isaac API functions that return, well, a random int and a random vector, respectively. It's simple and easy, just plug it in and it'll work.

local randomVec = RandomVector()
-- or...
local randomVec = Vector.FromAngle(Random()/(2^32)) -- untested

What's also really nice about it is RandomVector will always return a vector of size 1 - thus you can multiply it to achieve higher velocities with no issue:

local randomVec = RandomVector() * 32 -- will always be of length 32

However, this still faces a familiar downside!

1. Random() is unseeded

Same as math.random, Random and RandomVector aren't seeded on anything. Unlike math.random, however, you also cannot seed it - this prevents potential sabotages from different mods, but doesn't let you seed it with the run's current seed. There's no known way around this, so if you want this functionality out of it, you'll have to use the RNG class.

The RNG class

The RNG class is what the game uses internally for RNG; so because of this, it's the most compatible solution. You'll be able to shove it into vanilla Isaac API functions with no issue. However, due to the Isaac API being, well, the Isaac API, it's also the clunkiest, and it's a bit cumbersome to setup.

First, you must create a new RNG class, either as a global, as an entry in your mod's table, or as a value you pass around:

MyMod.RNG = RNG()

Once you've done this, create a MC_POST_GAME_STARTED callback that'll set the seed of your RNG object:

local RECOMMENDED_SHIFT_IDX = 35 -- keep this as-is!

MyMod:AddCallback(ModCallbacks.MC_POST_GAME_STARTED, function()
  MyMod.RNG:SetSeed(Game():GetSeeds():GetStartSeed(), RECOMMENDED_SHIFT_IDX)
end)

And now, you can generate a random vector!

local randomVec = Vector.FromAngle(MyMod.RNG:RandomFloat() * 360)

However, this isn't the end of it. You'll also have to store your seed in your mod's savefile and load it back up when the run is continued to prevent save-scumming - a player could go inside a room, see a value they don't want to see, exit, continue, and get a different value. However, this is outside of the scope of this guide. Think of it as an exercise for the reader.

Got all that? Good, because that's still not everything. It's preferable to have different RNG classes for different parts of your mod. If you generate items, but use the same RNG object to also generate the movement of an entity, the player could get a different item by taking longer to kill an entity:

MyMod.random = {}
MyMod.random.entities = RNG()
MyMod.random.items = RNG()

-- etc ...

local RECOMMENDED_SHIFT_IDX = 35
MyMod:AddCallback(ModCallbacks.MC_POST_GAME_STARTED, function()
  for _, rng in pairs(MyMod.random) do
    rng:SetSeed(Game():GetSeeds():GetStartSeed(), RECOMMENDED_SHIFT_IDX)
  end
end)

-- then:

local itemId = MyMod.random.items:RandomInt(maxItem)
local randomVec = Vector.FromAngle(MyMod.random.entities:RandomFloat() * 360)

This method truly has no downsides. Besides, well, the exhaustive work you have to do to set it up, treat it right, save it, prevent save scumming, and all sorts of nonsense. So if you're not willing to settle for quick & dirty, this is the easiest option you've got.

Of course, it's recommended to abstract this away into your own little classes and libraries, as is for just about anything in the great, wonderful Isaac API, but completely not necessary.

Conclusions

Use math.random if you simply don't care. Use Random/RandomVector if you care about potentially bad random values. Use RNG if you're a perfectionist. That's about everything!