Advertisement
UI & Interface Sounds

How to Build a Voice Blip System in Defold for Dialogue-Driven Games

A practical system-level guide to dialogue voice blips in Defold, covering the sound component setup, the typewriter library integration, the message-passing architecture that decouples dialogue from audio, and the voice-buffer and gating constraints that cause blips to break.

Defold does not ship with a dialogue system, a typewriter effect, or a voice blip feature. This is not an oversight; the engine is deliberately minimal, and the dialogue layer is something the developer builds from a small set of primitives. The upside is that a voice blip system in Defold can be built without fighting the engine. The downside is that every piece of the system has to be assembled deliberately, and the pieces that are easy to get wrong are the ones that determine whether the blips sound like a voice or like a broken printer.

This article covers the complete system: the sound component setup, the typewriter library integration, the message-passing architecture that decouples the dialogue logic from the audio logic, and the specific engine constraints — voice buffers, gating, and stream caching — that cause blips to break in ways that are hard to diagnose. The procedural blip generation guide covers the alternative approach of generating the blips at runtime; this article is about playing pre-rendered blip files through Defold's sound system.

The dialogue design itself — which characters speak, what text appears, how the player advances — is covered in the typewriter blip guide and the visual novel blip guide. What follows is the Defold implementation.

How do you set up the blip sound component in Defold?

Defold's sound system is built around the Sound component, which is attached to a game object and plays a single audio file. The component has a small set of properties that determine how the file plays, and three of them matter for voice blips[reference:0].

Property Setting for a blip Why
Sound The blip WAV or Ogg file The engine supports Wave, Ogg Vorbis, and Ogg Opus at 16-bit depth
Looping Off A blip plays once per character; looping would smear the text reveal
Group A custom “ui_blip” group Allows independent volume control from music and sound effects
Gain 0.5 to 0.8 Blips should sit below dialogue and music, not compete with them
Pan 0 (center) Dialogue blips are not spatialized
Speed 1.0 Pitch variation is applied per-playback, not on the component

The Group property is the one most projects skip and later regret. Defold's sound groups allow a single function call to change the gain of every sound in the group, which is what makes it possible to duck the blips under voice acting or to provide a separate volume slider for UI audio[reference:1]. The group is assigned on the component, and the gain is adjusted at runtime with sound.set_group_gain().

The blip file itself should be short: 30 to 60 milliseconds, in the 800 to 1400 Hz range, with a sine or triangle waveform. The square-wave synthesis guide covers the parameter ranges; the Defold-specific part is that the file format matters. WAV at 44.1 kHz and 16-bit is the safest choice because it is the engine's native format and decodes without the CPU cost of a compressed stream[reference:2].

How does the typewriter library drive the blip timing?

The typewriter effect in Defold is provided by a community library called library-defold-typewriter, which handles the character-by-character reveal and exposes a hook for playing a sound on each character[reference:3]. The alternative is defold-printer, which is designed specifically for rich dialogue and has a play_sound function that the developer overrides[reference:4].

The typewriter library does not play sounds itself. It reveals characters and calls a function that the developer provides. The function is where the blip logic lives, and the logic has three responsibilities: filter non-alphanumeric characters, throttle the blip rate, and play the blip with a small random variation.

-- In the GUI script that owns the typewriter
local dtypewriter = require("dtypewriter.dtypewriter")

local function play_blip(self)
    -- Throttle: enforce a minimum gap between blips
    local now = os.clock()
    if now - self.last_blip_time < 0.06 then
        return
    end
    self.last_blip_time = now

    -- Random pitch variation: ±3%
    local speed = 0.97 + math.random() * 0.06
    sound.play("blip#sound", {
        gain = 0.6,
        speed = speed
    })
end

function init(self)
    dtypewriter.init(self, "dialogue_text", "fonts/main_font")
    self.last_blip_time = 0
end

function update(self, dt)
    -- dtypewriter calls the callback per character
    dtypewriter.update(self, dt, play_blip)
end

The speed parameter is Defold's playback-rate control. A value of 1.0 is normal speed, 0.5 is half speed, and 2.0 is double speed[reference:5]. For a blip, the speed change affects both pitch and duration, which is the same behavior as the tape-machine effect described in the variation system guide. A range of 0.97 to 1.03 is enough to break repetition without making the blip sound like a different sound.

The 0.06 second throttle is the maximum blip rate. At the default text speed of 30 characters per second, the typewriter fires a character every 33 milliseconds, which is faster than the throttle allows. The result is a blip every 60 milliseconds, or about 16 blips per second, which is at the upper end of the range the ear can distinguish as separate events. A throttle of 0.08 produces about 12 blips per second, which is more comfortable for long dialogue sessions.

Why should the blip logic live in a separate game object?

Defold's message-passing architecture makes it natural to separate the dialogue logic from the audio logic. The GUI script that owns the typewriter sends a message to a separate “audio” game object, and that object owns the sound component and the playback logic[reference:6].

-- In the GUI script (dialogue logic)
local function on_character_revealed(char)
    if not char:match("%w") then return end
    msg.post("audio#blip", "play_blip")
end

-- In the audio game object's script
function on_message(self, message_id, message, sender)
    if message_id == hash("play_blip") then
        local speed = 0.97 + math.random() * 0.06
        sound.play("#sound", { gain = 0.6, speed = speed })
    end
end

The separation has three advantages. The audio object can be reused for other UI blips (menu clicks, hover feedback) without duplicating the playback logic. The dialogue script does not need to know about sound groups or gain settings. And the audio object can be moved to a different collection or excluded from a build without touching the dialogue code.

The message-passing pattern is the same one that the Defold documentation uses for its own examples, where one script sends a message and another script responds to it in on_message[reference:7]. For a dialogue system, the messages are frequent (one per character), so the payload should be minimal. The example above sends only the message ID and no data, because the audio object does not need to know which character was revealed. A project that wants per-character voices can include the character name in the message data.

What are the engine constraints that cause blips to break?

Defold's sound system has two constraints that are not obvious from the API and that cause blips to fail in ways that are hard to trace. The first is the voice buffer limit. The second is the stream cache.

The voice buffer limit. Defold's sound system has a fixed number of voices, and every sound.play() call consumes one. The documentation states that “each message sent to a component will cause it to play another instance of the sound, until the available sound buffer is full and the engine will print errors in the console”[reference:8]. A typewriter that reveals 30 characters per second and fires a blip on every character will exhaust the voice buffer within a few seconds if the blips are longer than the gap between them.

The fix is the throttle described earlier. A blip that plays once every 60 milliseconds uses a new voice every 60 milliseconds, and the voice is released when the blip finishes. A 50-millisecond blip never has more than one voice active at a time. Without the throttle, a 40-millisecond blip at 30 characters per second would have two voices active simultaneously, and the voice count would grow as the text reveals.

The stream cache. Defold streams sound files rather than loading them entirely into memory. The amount of memory the sound system uses is controlled by the sound.stream_cache_size setting in game.project[reference:9]. A project with many small blip files can exhaust the cache if the cache is set too small, and the engine will evict the least recently used chunks, which can cause a blip to fail to play or to play with a delay.

The practical fix is to set the stream cache to a size that comfortably holds every sound the game needs simultaneously, and to keep the blip files small. A 30-millisecond WAV at 44.1 kHz and 16-bit is about 2.6 KB, so a set of 20 blips is about 52 KB, which is well within any reasonable cache size. The file size reduction guide covers the file format tradeoffs; for Defold, WAV is the right choice for short blips because the decode cost is lower than a compressed format and the file size is small enough that compression is not necessary.

How do you handle the voice-acting conflict?

A dialogue-driven game often has voice acting for the same lines that have text blips. The two sounds serve the same function, and playing both at once produces a muddled result where the voice competes with the blips for the player's attention.

The standard fix is to disable the blips when the line is voiced. The dialogue system knows whether a line has a voice file, and the GUI script can set a flag before calling the typewriter:

-- In the dialogue script
self.is_voiced = line.has_voice

-- In the character callback
local function on_character_revealed(char)
    if self.is_voiced then return end
    -- ... blip logic
end

The alternative is to keep the blips at a reduced volume during voiced lines, using the sound group's gain to duck the blips when the voice channel is active. Defold does not have automatic sidechain compression, so the ducking has to be scripted: a function that checks whether the voice is playing and adjusts the blip group's gain accordingly. The ducking guide covers the general principle; in Defold, the implementation is a manual gain adjustment rather than an automatic sidechain.

The choice between disabling and ducking depends on the game's aesthetic. A visual novel with full voice acting usually disables the blips, because the voice is the primary audio. A game with partial voice acting (grunts and exclamations rather than full lines) keeps the blips at full volume and lets the two sounds coexist.

What does a complete blip system look like in a Defold project?

A complete system has four parts: a sound component on a dedicated audio game object, a typewriter library that reveals characters and calls a callback, a playback function that throttles and varies the blips, and a message that connects the dialogue script to the audio script.

Component Where it lives Responsibility
Sound component “audio” game object Plays the blip file, assigned to a “ui_blip” group
Audio script “audio” game object Receives play_blip messages and calls sound.play()
Typewriter library GUI script Reveals characters and calls the per-character callback
Dialogue script GUI script Filters non-alphanumeric characters, throttles, sends the message

The four parts are independent. The sound component can be reused for other UI sounds. The audio script can be extended to handle other messages (menu clicks, hover feedback) without changing the dialogue script. The typewriter library can be swapped for a different implementation without changing the audio logic. The message-passing pattern is what makes the independence possible, and it is the part that most tutorials on dialogue systems skip.

The typewriter library is the only external dependency, and it is optional. A project that does not want a dependency can implement the character reveal with a timer and a string counter, using the same callback pattern. The Web Audio blip tutorial covers the character-by-character approach in a different engine; the Defold version is simpler because the sound component and the message system do most of the work.

Voice blips in Defold are not a built-in feature, and they are not a single library. They are a pattern: a sound component in a dedicated audio object, a callback from the typewriter, a throttled playback function, and a message that connects the two. The pattern is small enough to implement in an afternoon, and it scales from a single speaking character to a full cast with per-character blip sets. The engine constraints — voice buffers and stream cache — are the parts that require attention, and the throttle is the single parameter that prevents both of them from becoming a problem.

Generate the Blip Files for the Defold System

Open the SfxMaker blip generator and create the blip set for the project's speaking characters, using the 800–1400 Hz range and a 30–50 ms duration. Export the files as 16-bit 44.1 kHz WAV, which is the format Defold decodes without the CPU cost of a compressed stream.

Open Blip Sound Generator →
Advertisement