Absolute BS infinite loop error...

So I built this function in my bp function library:

It’s pretty straightforward; select the indicies (from a map of structs) of all hex tiles in range using the cubic coordinate system. The function has inputs for whether to include the current tile in the output, the current tile, the map of structs, and the range we’re testing against…

Now, to test this, I first wrote it in my map manager actor. Worked without issue. Knowing I was going to use this function in jsut about every single element of my game I did the logical thing - I rewrote it in my Blueprint Function Library (the screenshot). It is an identical copy of what was setup, quite literally identical in every facet. However when I call the version from my function library, the game won’t launch and throws out two infinite loop errors - one for the second branch, and another for the compare int.

The size of the map is currently only around 4,800 elements, nowhere near the infinite loop limit in the engine. It’s also not running inside any other loop in the actor blueprint (I’m back to testing it trying to figure out the problem, so keeping it simple).

Soooo… Is there something I’m missing about some limitation in the function library? Because this is really annoying, and I’d rather not have to rewrite the same function across however many classes of blueprints…

Hi @Scatter

At a glance i see no issues, i dont see any accidental loops back into your code , looks clean enough.

So the only thing i can think of is you are calling this somewhere too many times? Have a look you havent got this repeating somehwere by accident

Ie on tick lol. But its easily done , you might be calling too many instances of this function.

Other than that i cant see anything obvious

The script VM does not actually have any “For Loop”, “For Each Loop”, “While Loop” etc. concept. These are just high level flow control mechanisms built on top. They essentially all function the same way:

  1. evaluate some condition whether we are done and jump to either 2. or 3.
  2. if not yet done
    2.1 execute some custom logic
    2.2. possibly update internal state (e.g. for loop would increment its counter)
    2.3 go back to 1.
  3. otherwise we are done and can return

So how does the script engine detect infinite loops then? Well it knows what branches are (jump). So since it cannot count iterations it counts jumps instead. So really the “infinite loop” in the error message is very misleading (internally the counter is called Runaway). While it intends to detect infinite loops it detects when too many jumps are executed which can happen if too much stuff happens in the same tick even if the code executed by the script VM may be perfectly fine and not have any infinite loop (such as yours). You can inspect the code generated for your blueprint with the following (not very well known) command DISASMSCRIPT NameOfYourBlueprint and then search for “jump”. Be warned the output is fairly hard to read. You’ll notice there are already multiple jumps in this function. One hides in the For Each Loop, another two in the Compare Int macro. Then there are the two plain branches as well. So each iteration of your loop has a minimum of three jumps (For Each Loop, first branch, then either second branch or first branch inside the Compare Int macro).
Now one can argue that the infinite loop detection is most likely to report the place that executes the most jumps but it could also just be a very simple function if that happens to hit the limit.

I believe the limit is a project setting so you could just increase that. However I would not recommend doing that as you’re likely to keep expanding the game and would hit the limit again eventually. Another common solution to hitting the runaway limit is to separate expensive (in terms of number of jumps) operations over multiple ticks. E.g. rather than processing all 5k Tiles in one go, process 5 batches of 1k Tiles instead. If you haven’t done so already you might also come up with a different datastructure that allows you to get surrounding tiles faster, for example by grouping tiles using a quadtree and only looking at the tiles in quadtree nodes that are in the desired range. Hitting the runaway limit may also indicate a performance issue so your code might benefit from being moved to C++ as well.

3 Likes

Very well thought out and informative answer, makes more sense understanding the underlying workings!

2 Likes

Thanks for the reply @UnrealEverything, appreciate it. However, I think there may be a glitch in the matrix here. By chance, I changed the function to impure… and it worked. Infinite loop error gone.

So I duplicated the function, made it pure again and threw both of them “into the field” so to speak:


When I hook up the executable function, it works without hitch. When I hook up the pure function, per the screenshot, I get infinite loop errors coming out the wazoo…

1 Like

Aha, that screenshot explains why you’re hitting the limit. I feel like this is another not very well known little quirk to using blueprint. Let me demonstrate with a small example: Pure vs. Impure posted by anonymous | blueprintUE | PasteBin For Unreal Engine
Unfortunately the execution pins on the two functions below the Tick Event don’t render properly, try moving the Print String node between both “Pure Function” nodes, that fixes it for me.
If you paste that into a test blueprint based on Actor and reduce the Tick Interval you’ll notice the following (or similar) output if you drag this Actor into a level:

LogBlueprintUserMessages: [BP_PureImpure_2] PureFunction called
LogBlueprintUserMessages: [BP_PureImpure_2] Using PureFunction result twice (1)
LogBlueprintUserMessages: [BP_PureImpure_2] PureFunction called
LogBlueprintUserMessages: [BP_PureImpure_2] Using PureFunction result twice (2)
LogBlueprintUserMessages: [BP_PureImpure_2] ImpureFunction called
LogBlueprintUserMessages: [BP_PureImpure_2] Using ImpureFunction result twice (1)
LogBlueprintUserMessages: [BP_PureImpure_2] Using ImpureFunction result twice (2)

You’ll notice immediately that the PureFunction was called twice while ImpureFunction was not despite their return values being used the exact same way. The easiest way to explain this is to imagine that impure functions store their results in hidden local variables and using the results will just read whatever value was stored in the hidden variables from the call. Pure functions on the other hand do not store their results in variables automatically. I’m not entirely certain whether this is what actually happens internally but it does explain/describe the behaviour pretty closely. Pure functions are mostly intended for “light weight operations” e.g. simple getters that directly return a variable or contain only very little code. Nested loops or loops over lots of data (such as the 5k tiles) are a bad idea to use together with pure functions unless you cache their result in a (local) variable yourself.

This would not really become a noticable issue with most blueprints or uses of pure functions. However the combination of having a large number of tiles and possibly a large number of returned TilesInRange together with the For Each Loop node that uses the return value from the pure function this becomes a larger problem. The reason for this is how the For Each Loop simply reevaluates its current ArrayIndex against the length of the given Array. For this it also reevaluates the length of the Array each time (since its length may have changed). But because the Array/TilesInRange is not actually cached in a variable, every time the loop condition is checked the pure function is called again to determine the Array. So effectively the number of jumps from the Find All Tiles In Range function multiplies with the number of results returned from it. This can add up pretty quickly in the Runaway counter. Edit: As a rough estimate you can use this calculation for the number of jumps: number of hex tiles * 3 * number of tiles in radius. With number of hex tiles = 5k and number of tiles in radius ~= 100 (for range = 5) we have 1,500,000 jumps already.

As a side note, this is also the reason why one should not assume that pure functions will always return the same result if called multiple times with seamingly the same inputs. If any state has changed that is used by the pure function, the return value may change even if you use the same node and drag another connection from its return value. Because as we have seen in the example above, the pure function will actually be called multiple times.

1 Like

So what you’re saying @UnrealEverything, is that if, for example, I threw the segment that calculates the range to tile (everything that feeds into the compare int in the image in the first post) into a pure function, it wouldn’t have an issue per se. And that also I should be storing my results that i’m checking or checking against in variables before passing them into any future for (each) loop…

This all has me thinking I might use SQLite as my database, rather than an actor. Running into this issue so early in the process is a huge issue as this sort of function (tiles in range, range to tile, ring of tiles at range etc) are going to be spammed pretty mercilessly later on. A SQLite db Is actually built for that sort of thing…

1 Like

I just remembered how much of a royal pain in the butt connecting to sqlite is… :rofl:

So, I’ve taken your advice on board @UnrealEverything, and am restructuring things. For example, the function in the op now has a map of structs output (similar to its input), meaning if I need to repeat the function I can do it against a smaller collection of tiles if necessary; while at the same time, any need for an array of tile indices is just adding a Keys node pulling from that map…

I still feel like I’m dancing on very, very thin ice wearing stupidly heavy clogs, but even if I do have to end up on the SQLite bus, it’d still be better to optimise this sort of thing now either way.

Yes that would be a perfect example for a good pure function where you don’t really need to pay attention to where and how it is used.

I don’t really have a rule to follow in general, you could also just rely on impure functions to do their magic. It’s more of a case by case decision for me.

Just checked a few places in my current project. There are too many loops to check everything though. Most of the time there was either a variable being read or an impure function involved. The occasional pure functions were either fairly simple or implemented natively for example Get Components By Class and Get Overlapping Actors.

Get Components By Class is typically no issue since most of the time there’s only ever going to be one instance of any given component class on an actor. So even if used with a loop there won’t be too many unnecessary calls. Get Overlapping Actors won’t become an issue regarding the Runaway counter either since it is implemented natively as well. But it does collect actors in a set first if they match the given filter and then adds them to the resulting array later, this could potentially become a problem.