SCRMaps
/
Sign in

Choreography as a timetable: Queue, Plan, Exec

Stop computing the animation every cycle. Write the whole thing down the moment the skill is cast, and pop each step when its time comes.

14 min read

Every skill animation so far had the same shape: each cycle, ask whether the skill is running, work out which frame it is on, compute the position for that frame, and create one effect. For a simple animation that is right. It falls apart once the animation is “dash forward, pause, burst into three, then pull back” - every stage grows its own condition, state has to be handed between stages, and changing one stage shifts the frame numbers of everything after it.

There is another shape. At the moment of the cast, compute the whole animation and write it into a queue; from then on, all you ever do is pop the entries whose time has come. The corpus splits it into two functions, Plan and Exec.

// 같은 길이의 큐 네 개를 나란히 씽니다 / four parallel queues, same length
var qX  = EUDQueue(1000);   // x
var qY  = EUDQueue(1000);   // y
var qT  = EUDQueue(1000);   // 절대 프레임 / absolute frame
var qF  = EUDQueue(1000);   // 단계 / phase

function Plan(epd, px, py, dir) {
    const now = Frame;
    foreach(i : py_range(12)) {                  // 진짜 전반 / the dash
        const x, y = lengthdir_256(i * 10, dir);
        qX.append(px + x); qY.append(py + y);
        qT.append(now + i); qF.append(1);
    }
    foreach(k : py_range(3)) {                   // 세 갈래 / the three-way burst
        const x, y = lengthdir_256(120, dir + k * 64 - 64);
        qX.append(px + x); qY.append(py + y);
        qT.append(now + 14); qF.append(2);
    }
    Stun(epd, 126);                              // 시전자를 잡아 둡니다
}
Plan runs once. Nothing inside it needs a condition - it is all ordinary arithmetic.
function Exec() {
    const n = qT.length;                 // 두 번 읽지 않습니다 / read ONCE
    foreach(i : py_range(n)) {
        const x = qX.popleft(), y = qY.popleft();
        const t = qT.popleft(), f = qF.popleft();
        if (t <= Frame) {
            setloc(loc, x, y);
            TrgUnit("Nuclear Missile").groundWeapon = WEAPON_OF[f];
            CreateUnit(1, "Nuclear Missile", loc, P1);
        } else {
            qX.append(x); qY.append(y);      // 아직 아니면 뒤로 다시 / not yet: re-append
            qT.append(t); qF.append(f);
        }
    }
}
Exec runs once a cycle: pop everything, and put back whatever is not due.

Read the loop bound exactly once, before the loop. Put qT.length in the loop condition itself and the entries you re-appended get popped again in the same pass, so the queue never drains. It is the standard mistake with this structure and the symptom is the game simply stopping.

What the structure buys
Stages become data“three-way at frame 14” is three rows in a queue, not an if. Changing the animation becomes changing numbers in Plan.
A different weapon per stageCarrying a phase column lets Exec attach that stage's weapon as it pops. The dash hits softly and the burst hits hard, with no branch.
The time is absoluteYou store the actual Frame, not “n frames after the start”. Because of that, overlapping casts all run through the same one queue.
The caster is heldStun(epd, 126) holds the hero still for the duration. If the caster could move mid-animation, precomputed coordinates would be wrong.

With lengthdir_256, +64 is exactly a quarter turn: a full revolution is 256 steps, so a quarter is 64. That is all dir + k * 64 - 64 above is saying - left, centre, right.

It is equally clear when not to use it. A skill that must stop when it meets an enemy, or bounce off terrain, does not fit: a timetable written in advance knows nothing about what happens in between. Those skills want the per-cycle version.

Precomputing the whole thing at compile timeFive ways to decide who is in range
Sign in to track your progress