Foundations

Lua Loops: Process Collections Without Skipping Work

Choose the right loop, handle collection changes deliberately, and keep game updates bounded.

A looping green conveyor carrying orange gems in an ordered sequence
Original tutorial artwork · PixelDevHub
PixelDevHub editorial desk · Beginner · 4 min read · Core Lua 5.4

Choose what your loop promises

Before selecting a loop, decide which items must be processed and whether order matters. A dense list of quest steps needs ordered traversal. A dictionary of named configuration values usually needs key-based traversal without an ordering promise. A bounded counter works well when the task is simply to generate a known number of positions. Matching the loop to the data contract makes the code easier to understand than choosing a syntax first and reshaping the data afterward.

Also define what happens when the collection changes. Are you processing the items that existed at the start, or can new work enter the same pass? This question matters for event queues, spawned objects, and inventory changes. An explicit policy helps prevent both skipped entries and loops that never finish because each iteration creates another item.

Use a dense sequence for ordered steps

For a simple tutorial, store the names of three quest steps in a sequence and visit them in order with ipairs. Keep the numbered entries consecutive. If an entry is removed by assigning nil in the middle, your traversal contract changes. When an intentional gap is part of the design, use explicit bounds or another representation rather than assuming the same sequence helper will continue to describe the data correctly.

local steps = { "Find the gate", "Collect a key", "Open the gate" }
local visited = {}

for index, label in ipairs(steps) do
    visited[index] = label
    print(index, label)
end

assert(#visited == 3)
assert(visited[2] == "Collect a key")

Remove list entries from the end

Removing an entry with table.remove shifts later positions toward the beginning. A forward traversal can therefore skip the element that moves into the position you just processed. When removing matching entries from a dense list, a reverse numeric loop provides a simple, understandable solution. This technique assumes the list is not being independently modified by some other part of the program during the pass.

local tasks = {
    { name = "A", done = true },
    { name = "B", done = true },
    { name = "C", done = false }
}

for index = #tasks, 1, -1 do
    if tasks[index].done then
        table.remove(tasks, index)
    end
end

assert(#tasks == 1)
assert(tasks[1].name == "C")

Make dictionary ordering deliberate

A configuration dictionary can be inspected with pairs, but an interface should not depend on the order observed in one development run. If the player needs a predictable display, collect the string keys into a dense list, sort that list, and look up each corresponding value. Use a separate explicit order when alphabetical sorting would be inappropriate. A tutorial progression, for example, should not change merely because someone renames a chapter or adds another localization.

Bound work in the update path

A loop that is correct for ten items can still cause a visible pause when the collection grows to thousands. Avoid placing unlimited scans inside a frequently called update. Start with a maximum amount of work per pass and measure the actual frame cost on representative devices. Larger systems may process a queue in batches or resume a cooperative task across updates. A coroutine only helps if the work reaches a yield point; an expensive operation inside one iteration can still block.

Exercise: process a fixed snapshot

Create a reward queue in which processing one entry is allowed to append another. Record the initial queue length, process only those original entries, and leave newly appended work for the next pass. Decide how failures are recorded so the same reward is neither lost nor granted twice. Test empty queues, consecutive removals, and several additions during a pass. Describe your mutation rule in a comment near the loop. That small explanation often prevents more future mistakes than an elaborate iteration abstraction.

Language reference

These examples use core Lua rather than an engine API. Consult the official Lua 5.4 reference manual for the language contract. Embedded runtimes can differ in available libraries, loaders, and scheduling behavior.

← Explore the Lua library

Make collection changes part of the contract

A loop can be correct when read alone and still behave unexpectedly when a callback modifies the same collection. Decide whether callbacks may append entries, remove entries, or reorder pending work. For a first implementation, consider processing a snapshot of identifiers while mutations are queued for a later phase. The snapshot itself must match your needs: copying references to mutable objects does not freeze those objects. Explain the policy near the loop and give the callback a narrow interface. This avoids a situation in which a harmless-looking feature addition changes which items an existing pass visits, even though the loop's own source code has not changed.

Treat ordering as a product decision

A stable order is useful when players compare results, when tests reproduce a sequence, or when a replay needs the same processing decisions. Choose an order based on the feature rather than accepting whatever happened during one run. An alphabetical order may suit a settings display but fail to represent a quest's intended progression. For a job queue, arrival order may matter more than the textual name of a task. Record that choice explicitly and test it with several entries. If equal-priority items are possible, define how ties are handled so the result remains understandable when the collection grows or the loading order changes.

Measure the expensive part of the pass

Counting iterations is a useful first bound, but two iterations can have very different costs. A pass that only adds numbers is unlike one that performs scene queries, file access, or complex allocation. Measure the operation that dominates the task and choose representative input sizes. Keep diagnostic output from becoming the main source of work inside a frequently executed loop. When a performance issue appears, compare an empty pass, a typical pass, and a worst-case collection. Use those observations to decide whether to batch, cache, or redesign the lookup. An unexplained change in loop syntax is unlikely to solve a problem caused by what every iteration asks the host to do.

Find your next tool or idea

Search tools, guides, and Lua resources.

↑ ↓ Browse resultsESC to close

Choose a tool