Chunking, Pt. II: Chunk Loading and Unloading
Previously, we arrived at a method of generating regions as a connected graph that can grow infinitely in any direction, creating the potential for an essentially boundless explorable world.
In this article, I want to achieve a good understanding of how we can implement a system for smoothly and efficiently loading these regions into the application's working memory when needed and unloading them when no longer needed, to realize the goal of an infinite world on very finite hardware. This leads us to the concept of "juice," which is an attempt to model the engagement and interest of the player in a given region, and use that to determine how computing resources are allocated.
The dream here is that the player, traveling through the world, will frequently encounter interesting developing situations, and feel compelled to interact in some way. For instance, a human village might be getting attacked by a tribe of bugbears, using goblins as cannon fodder. This sort of situation should arise from the evolution of the situation within each region and between the regions and their neighbors. The connections between regions, the edges between the vertices of our graph, are not just passages for the player to travel but potential avenues for trade, communication, or invasion.
So we will know if we have this algorithm tuned smoothly if a player wanders into a chunk and feels as though they have arrived "just in the nick of time." We can fudge this a bit by decreasing the offensive power of attacks when the player isn't present (although not to a degree that it strains belief) and by noting, but simultaneously diminishing, the effective disparities of power within and between regions. Thus, things tend to happen when the player is around, and not happen so much when they're not around.
I call this "juice" in the sense of gasoline or energy. The region currently occupied by the player has the highest juice; those neighboring have somewhat less, and those at a higher degree of separation have still less. Juice "lingers" after a player leaves a region; if the player leaves a nasty situation, we don't want to just pause it until they get back. We want them to return and see the aftermath.
When a region's juice is sufficiently low, there's no reason to keep it in memory anymore. Then it can be unloaded.
We can describe Jcurrent, the total current juice of the region, as follows:
- Jcurrent = wproximity ⋅ Jproximity + wlinger ⋅ Jlinger
- wproximity is some fractional weight for the proximity factor
- wlinger is some fractional weight for the lingering factor
- Jproximity (d) = J0 ⋅ e-λd
- Jproximity (d) is the juice level at distance d
- J0 is the juice level at the player's location
- λ is the decay constant, determining how quickly the juice level decreases with distance
- d is the distance from the player’s current position.
- Jlinger (t) = L ÷ ( 1 + e-k(t - t0) )
- Jlinger (t) is the juice level at time t
- L is the maximum juice level
- k is the steepness of the curve, determining how quickly the juice level falls off
- t0 is the time at which the player leaves the region, serving as the midpoint of the sigmoid curve
If Jcurrent falls below some threshold value Jmin, the region is declared to be effectively devoid of interest and can be unloaded until such a time that its juice once again exceeds that threshold.
The following visualization simulates these changes to Jcurrent; the character moves between chunks, and chunks are loaded (invisible -> visible) and are unloaded (visible -> invisible).
In addition, we can also monitor a longer-term cumulative trend:
- Jengagement = Jengagement + Δ J ⋅ t
- Jengagement is a cumulative measurement of how much a player has engaged with a region.
- Δ J ⋅ t is a measure of how much the juice has changed this turn. This will always be at least zero, though it may be rounded down to zero judiciously.
And we can compare the current state to that historic trend:
- Jdecay = Jengagement ⋅ e-λ(t - t0)
- Jdecay is an indicator of how much a player has engaged with a region recently
- λ is the decay constant, determining how quickly the juice level decreases with time since last engagement
- t0 is the time at which the player last interacted with the region
This can be used as a trigger for "hey, remember me?" type events:
- the player receives a letter/newspaper/warning/threat/bounty hunter from a person in
$region
- a loose end from the current quest ties off with a loose end from a quest in
$region
- etc
That's not particularly important right now; we'll deal with other aspects of chunk management moving forward.