An IMGUI.

Sometimes, you just want some unique bits to use as an ID. Maybe you’re working with an immediate mode GUI, or need to generate IDs for cancellable sounds.

In many cases you could technically get away with manually enumerating the various unique IDs in a header somewhere, but this is very inconvenient–especially in the case of IMGUIs: if you do this, every time you add or remove a UI element, you need to make a secondary change elsewhere in the codebase. You won’t be able to freely copy paste UI code.

Table of Contents

Implementations

Here are a few easy ways to conveniently generate unique IDs usable in the context of immediate mode GUIs in C, C++, and Zig.

C/C++

You can generate a unique value at compile time in C and C++ by using the preprocessor to concatenate the current file path and line number. Assuming you only use the macro once per line, the resulting string’s memory address will be unique:

#define CONCAT(lhs, rhs) lhs # rhs
#define CONCAT_WRAPPER(lhs, rhs) CONCAT(lhs, rhs)
#define UNIQUE CONCAT_WRAPPER(__FILE__, __LINE__)

// ...

if (button("click me", UNIQUE))  {
    // do stuff
}

An earlier version of this post mentioned an alternative approach involving the non-standard (but widely supported) macro __COUNTER__. If you only need your value to be unique within a source file this is an easier solution, but that’s not often the case.

Zig

In Zig, we could implement a similar approach with @src() and std.fmt.comptimePrint. However, when Zig gets function-level incremental compilation, this will result in a lot of unnecessary recompilation since functions containing @src() need to be compiled whenever their line numbers change.

Instead, we can use @returnAddress() (and noinline):

pub const Id = enum(usize) {
    _,
    pub noinline fn init() Id {
        return @enumFromInt(@returnAddress());
    }
};

// ...

if (button("click me", Id.init())) {
    // do stuff
}

In fact, if our use case is an IMGUI, the user doesn’t even need to pass in the ID since we can create it inside of button:

noinline fn button(name: []const u8) bool {
    const id = @returnAddress();
    // ...
}

// ...

if (button("click me")) {
    // do stuff
}

Interesting Use Cases

IMGUI Iteration

If you’re using the unique value to create an IMGUI ID, you can pair it with an index to allow for creating UI elements in a loop.

For example, in Zig that might look like this:

pub const Id = struct {
    returnAddress: usize,
    index: usize,
};

pub const ButtonDesc = struct {
    name: []const u8,
    index: usize = 0,
    // ...
};

noinline fn button(desc: ButtonDesc) bool {
    const id = Id{
        .index = desc.index,
        .returnAddress = @returnAddress(),
    };

    // ...
}

// ...

const items = [_][]const u8{
    "foo",
    "bar",
    "baz",
};

for (items, 0..) |item, i| {
    if (button(.{ .name = item, .index = i })) {
        std.debug.print("You clicked {s}!\n", .{item});
    }
}
if (button(.{ .name = "back" })) {
    std.debug.print("closed\n", .{});
}

Alternatively, if you need arbitrarily nested loops and a shared index isn’t an option, you could store a slice of indices. I have not yet had a use case that requires this, so I don’t do it.

Cancellable Sounds

In Way of Rhea, I wanted voice lines to interrupt each other.

I could have each sound return an ID that you can keep track of, and later use to cancel the sound, but that’s a lot of work:

if let Some(id) = self.last_line {
    mixer::cancel_sound(self.last_line);
}
self.last_line = Some(mixer::play_sound(f"dialogue/{sound}.ogg", 0.5));

Instead I have the user pass in a unique ID. When you play a sound, you have the option to pass in the ID, and any sounds already playing with that same ID are cancelled. If you need to explicitly cancel a sound, you can reference it later by the same ID.

The above block becomes a single line:

mixer::play_sound_ex(f"dialogue/{sound}.ogg", 0.5, unique);

(unique is my scripting language’s keyword for generating a unique value at compile time.)

An alternate solution would be to dedicate a mixer channel to voices. This has the benefit of making it easy to control voice volume via channel gain, and to guarantee voices are given priority if too many sounds are playing.

I opted for the unique ID solution because Way of Rhea characters speak gibberish so volume is not important, there are never too many sounds so priority is not an issue, and it only took 15 minutes to implement & lets me cancel other sounds as well.