Randomizing Good and Great Zones for a Skill Check (Unreal Engine)

How a radial skill check places its targets each attempt: a good zone randomized to fit inside the 0-1 dial, a great zone nested at a random offset inside it, and a tunable reaction lead-in so the gauge always starts a fair distance away.

This is step three of the Radial Skill Check in Unreal Engine guide. The rotating gauge advances the angle; this step decides what that angle is trying to hit, and re-rolls it every check so the player can never memorize a spot.

Placing the good zone

The good zone is a start angle plus a size, both normalized. The only subtlety is making sure the whole arc fits on the dial without wrapping past 1. So the random start is clamped to leave room for the zone’s full width:

const float MaxStart = 1.0f - NextGaugeSettings.GoodZoneSize;
ZoneStartAngle = FMath::RandRange(0.0f, MaxStart);

At the default GoodZoneSize = 0.15, the start lands somewhere in 0 .. 0.85, so the zone always ends at or before 1. No wrap-around case to special-case downstream.

Nesting the great zone

The great zone is the narrow band for a perfect hit, and it must sit entirely inside the good zone. Rather than randomize it independently, it is offset from the good zone’s start by a bounded amount:

const float MaxGreatOffset = NextGaugeSettings.GoodZoneSize - NextGaugeSettings.GreatZoneSize;
const float GreatZoneOffset = FMath::RandRange(0.0f, FMath::Max(0.0f, MaxGreatOffset));
GreatZoneStartAngle = ZoneStartAngle + GreatZoneOffset;

Because the maximum offset is the good size minus the great size, even the largest roll leaves exactly enough room for the great zone’s width, so it can slide anywhere from the near edge to the far edge of the good zone but never escape it. With defaults of 0.15 and 0.035, the great zone is a ~3.5% sliver that can sit anywhere within the ~15% good band.

The radial skill check gauge showing the wider good zone with the narrow great zone nested inside it

The reaction lead-in

If the gauge spawned on top of the zone, the player would have no time to react. So the needle starts a distance before the zone, and that distance has a random component so the lead-in is never the same twice:

const float ActualStartDistance = NextGaugeSettings.GaugeStartDistance +
    FMath::RandRange(0.0f, NextGaugeSettings.GaugeStartDistanceVariance);

Where that start sits depends on the gauge direction, because “before the zone” flips for a counter-clockwise sweep:

if (bIsCurrentGaugeClockwise)
{
    CurrentGaugeAngle = ZoneStartAngle - ActualStartDistance;          // approach from below
    if (CurrentGaugeAngle < 0.0f) CurrentGaugeAngle += 1.0f;           // wrap
}
else
{
    CurrentGaugeAngle = ZoneStartAngle + GoodZoneSize + ActualStartDistance; // approach from above
    if (CurrentGaugeAngle >= 1.0f) CurrentGaugeAngle -= 1.0f;          // wrap
}

Clockwise, the needle starts below the zone and climbs into it; counter-clockwise, it starts past the far edge and falls into it. Either way the player gets the same fair gap.

Direction each check

The direction itself is picked per check from the gauge’s direction mode: always clockwise, always counter-clockwise, alternating (flip from last time), or random. Alternating keeps a tracked “last direction” and inverts it; random just rolls a coin. Combined with the re-randomized zones and the variable lead-in, no two checks present the same problem.

The zones are now placed and the gauge is aimed. Next, the arc material: how those numbers become the colored slices you actually see.

Frequently asked questions

Why subtract the zone size from the random range?
If the good zone start could be anywhere in 0-1, a zone near the end would spill past 1 and wrap. Clamping the start to 0 .. (1 - GoodZoneSize) guarantees the entire arc fits on the dial without wrapping.
How is the great zone kept inside the good zone?
It is offset from the good zone's start by a random amount between 0 and (GoodZoneSize - GreatZoneSize). Because the largest offset still leaves room for the great zone's width, it can never poke out of the good zone.
What is the gauge start distance for?
It is the gap the gauge starts before the zone so the player has time to react. The actual gap is GaugeStartDistance plus a random value up to GaugeStartDistanceVariance, so the lead-in is never identical, and the start is mirrored for counter-clockwise checks.