Agrega sonido y música a tu juego de Flutter

1. Antes de comenzar

Los juegos son experiencias audiovisuales. Flutter es una herramienta excelente para crear imágenes atractivas y una IU sólida que te acerca mucho más al aspecto visual. El ingrediente que falta es el audio. En este codelab, aprenderás a usar el complemento flutter_soloud para agregar música y sonido de baja latencia a tu proyecto. Comienzas con un andamiaje básico para poder pasar directamente a las partes interesantes.

Una ilustración dibujada a mano de auriculares.

Puedes, por supuesto, usar lo que aprenderás aquí para agregar audio a tus apps, no solo a juegos. Sin embargo, si bien casi todos los juegos requieren sonido y música, la mayoría de las apps no lo hacen, por lo que este codelab se enfoca en los juegos.

Requisitos previos

  • Conocimientos básicos sobre Flutter
  • Conocimientos para ejecutar y depurar apps de Flutter

Qué aprenderá

  • Cómo reproducir sonidos de un solo intento
  • Cómo reproducir y personalizar bucles de música sin pausas
  • Cómo atenuar los sonidos de entrada y salida
  • Cómo aplicar efectos ambientales a los sonidos
  • Cómo abordar las excepciones
  • Cómo encapsular todas estas funciones en un solo controlador de audio

Requisitos

  • El SDK de Flutter
  • El editor de código que prefieras

2. Configurar

  1. Descarga los siguientes archivos. Si tu conexión es lenta, no te preocupes. Necesitarás los archivos reales más adelante, para que puedas permitir que se descarguen mientras trabajas.
  1. Crea un proyecto de Flutter con el nombre que quieras.
  1. Crea un archivo lib/audio/audio_controller.dart en el proyecto.
  2. En el archivo, ingresa el siguiente código:

lib/audio/audio_controller.dart

import 'dart:async';

import 'package:logging/logging.dart';

class AudioController {
  static final Logger _log = Logger('AudioController');

  Future<void> initialize() async {
    // TODO
  }

  void dispose() {
    // TODO
  }

  Future<void> playSound(String assetKey) async {
    _log.warning('Not implemented yet.');
  }

  Future<void> startMusic() async {
    _log.warning('Not implemented yet.');
  }

  void fadeOutMusic() {
    _log.warning('Not implemented yet.');
  }

  void applyFilter() {
    // TODO
  }

  void removeFilter() {
    // TODO
  }
}

Como puedes ver, esto es solo la estructura de una funcionalidad futura. Implementaremos toda la información durante este codelab.

  1. A continuación, abre el archivo lib/main.dart y reemplaza su contenido con el siguiente código:

lib/main.dart

import 'dart:developer' as dev;

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';

import 'audio/audio_controller.dart';

void main() async {
  // The `flutter_soloud` package logs everything
  // (from severe warnings to fine debug messages)
  // using the standard `package:logging`.
  // You can listen to the logs as shown below.
  Logger.root.level = kDebugMode ? Level.FINE : Level.INFO;
  Logger.root.onRecord.listen((record) {
    dev.log(
      record.message,
      time: record.time,
      level: record.level.value,
      name: record.loggerName,
      zone: record.zone,
      error: record.error,
      stackTrace: record.stackTrace,
    );
  });

  WidgetsFlutterBinding.ensureInitialized();

  final audioController = AudioController();
  await audioController.initialize();

  runApp(
    MyApp(audioController: audioController),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({required this.audioController, super.key});

  final AudioController audioController;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter SoLoud Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.brown),
        useMaterial3: true,
      ),
      home: MyHomePage(audioController: audioController),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.audioController});

  final AudioController audioController;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  static const _gap = SizedBox(height: 16);

  bool filterApplied = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Flutter SoLoud Demo')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            OutlinedButton(
              onPressed: () {
                widget.audioController.playSound('assets/sounds/pew1.mp3');
              },
              child: const Text('Play Sound'),
            ),
            _gap,
            OutlinedButton(
              onPressed: () {
                widget.audioController.startMusic();
              },
              child: const Text('Start Music'),
            ),
            _gap,
            OutlinedButton(
              onPressed: () {
                widget.audioController.fadeOutMusic();
              },
              child: const Text('Fade Out Music'),
            ),
            _gap,
            Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                const Text('Apply Filter'),
                Checkbox(
                  value: filterApplied,
                  onChanged: (value) {
                    setState(() {
                      filterApplied = value!;
                    });
                    if (filterApplied) {
                      widget.audioController.applyFilter();
                    } else {
                      widget.audioController.removeFilter();
                    }
                  },
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}
  1. Después de descargar los archivos de audio, crea un directorio en la raíz de tu proyecto llamado assets.
  2. En el directorio assets, crea dos subdirectorios, uno llamado music y otro llamado sounds.
  3. Mueve los archivos descargados a tu proyecto de modo que el archivo de la canción esté en el archivo assets/music/looped-song.ogg y los sonidos de pew se encuentren en los siguientes archivos:
  • assets/sounds/pew1.mp3
  • assets/sounds/pew2.mp3
  • assets/sounds/pew3.mp3

La estructura de tu proyecto debería ser similar a la siguiente:

Una vista de árbol del proyecto, con carpetas como `android`, `ios`, archivos como `README.md` y `analysis_options.yaml`. Entre estos, podemos ver el directorio `assets` con los subdirectorios `music` y `Sounds`, el directorio `lib` con `main.dart` y un subdirectorio `audio` con `audio_controller.dart` y el archivo `pubspec.yaml`.  Las flechas apuntan a los nuevos directorios y a los archivos que tocaste hasta el momento.

Ahora que los archivos se encuentran allí, debes informar a Flutter sobre ellos.

  1. Abre el archivo pubspec.yaml y reemplaza la sección flutter: en la parte inferior del archivo por lo siguiente:

pubspec.yaml

...

flutter:
  uses-material-design: true

  assets:
    - assets/music/
    - assets/sounds/
  1. Agrega una dependencia en los paquetes flutter_soloud y logging.

pubspec.yaml

...

dependencies:
  flutter:
    sdk: flutter

  flutter_soloud: ^2.0.0
  logging: ^1.2.0

...
  1. Ejecutar el proyecto Todavía no funciona nada porque agregaste la funcionalidad en las siguientes secciones.

10f0f751c9c47038.png

/flutter_soloud/src/filters/filters.cpp:21:24: warning: implicit conversion loses integer precision: 'decltype(__x.base() - __y.base())' (aka 'long') to 'int' [-Wshorten-64-to-32];

Provienen de la biblioteca SoLoud de C++ subyacente. No afectan la funcionalidad y se pueden ignorar de forma segura.

3. Inicializa y cierra

Para reproducir audio, usa el complemento flutter_soloud. Este complemento se basa en el proyecto SoLoud, un motor de audio C++ para juegos que se usa, entre otros, en Nintendo SNES Classic.

8c23849b6d0d09a.png

Para inicializar el motor de audio de SoLoud, sigue estos pasos:

  1. En el archivo audio_controller.dart, importa el paquete flutter_soloud y agrega un campo _soloud privado a la clase.

lib/audio/audio_controller.dart

import 'dart:ui';

import 'package:flutter_soloud/flutter_soloud.dart';  // ← Add this...
import 'package:logging/logging.dart';

class AudioController {
  static final Logger _log = Logger('AudioController');

  SoLoud? _soloud;                                    // ← ... and this.

  Future<void> initialize() async {
    // TODO
  }

  ...

El controlador de audio administra el motor de SoLoud subyacente a través de este campo y desviará todas las llamadas hacia él.

  1. En el método initialize(), ingresa el siguiente código:

lib/audio/audio_controller.dart

...

  Future<void> initialize() async {
    _soloud = SoLoud.instance;
    await _soloud!.init();
  }

...

Esto propaga el campo _soloud y espera la inicialización. Ten en cuenta lo siguiente:

  • SoLoud proporciona un campo singleton instance. No hay forma de crear una instancia de varias instancias de SoLoud. Esto no es algo que permite el motor de C++, por lo que el complemento de Dart tampoco lo permite.
  • La inicialización del complemento es asíncrona y no se completa hasta que se muestre el método init().
  • Para mayor brevedad, en este ejemplo, no se detectarán errores en un bloque try/catch. En el código de producción, debes hacerlo e informar cualquier error al usuario.
  1. En el método dispose(), ingresa el siguiente código:

lib/audio/audio_controller.dart

...

  void dispose() {
    _soloud?.deinit();
  }

...

Se recomienda cerrar SoLoud al salir de la app, aunque todo debería funcionar bien incluso si no lo haces.

  1. Ten en cuenta que el método AudioController.initialize() ya se llama desde la función main(). Esto significa que el reinicio en caliente del proyecto inicializa SoLoud en segundo plano, pero no te hará ningún bien antes de reproducir algunos sonidos.

4. Reproduce sonidos de un solo intento

Cómo cargar un elemento y reproducirlo

Ahora que sabes que SoLoud se inicializó al inicio, puedes pedirle que reproduzca sonidos.

SoLoud diferencia entre una fuente de audio, que son los datos y metadatos que se usan para describir un sonido, y sus "instancias de sonido", que son los sonidos que realmente se reprodujeron. Un ejemplo de una fuente de audio puede ser un archivo MP3 cargado en la memoria, listo para reproducirse y representado por una instancia de la clase AudioSource. Cada vez que reproduces esta fuente de audio, SoLoud crea una "instancia de sonido" que se representa con el tipo SoundHandle.

Para obtener una instancia de AudioSource, debes cargarla. Por ejemplo, si tienes un archivo MP3 en tus recursos, puedes cargarlo para obtener un AudioSource. Luego, dile a SoLoud que reproduzca este AudioSource. Puedes reproducirlo muchas veces, incluso al mismo tiempo.

Cuando termines de usar una fuente de audio, la eliminas con el método SoLoud.disposeSource().

Para cargar un elemento y reproducirlo, sigue estos pasos:

  1. En el método playSound() de la clase AudioController, ingresa el siguiente código:

lib/audio/audio_controller.dart

  ...

  Future<void> playSound(String assetKey) async {
    final source = await _soloud!.loadAsset(assetKey);
    await _soloud!.play(source);
  }

  ...
  1. Guarda el archivo, haz una recarga en caliente y, luego, selecciona Reproducir sonido. Deberías escuchar un sonido de repulsión. Ten en cuenta lo siguiente:
  • El argumento assetKey proporcionado es similar a assets/sounds/pew1.mp3, la misma cadena que brindarías a cualquier otra API de Flutter que cargue elementos, como el widget Image.asset().
  • La instancia de SoLoud proporciona un método loadAsset() que carga de forma asíncrona un archivo de audio desde los recursos del proyecto de Flutter y muestra una instancia de la clase AudioSource. Hay métodos equivalentes para cargar un archivo desde el sistema de archivos (el método loadFile()) y para cargar a través de la red desde una URL (el método loadUrl()).
  • Luego, la instancia AudioSource recién adquirida se pasa al método play() de SoLoud. Este método muestra una instancia del tipo SoundHandle que representa el sonido que se está reproduciendo. Este controlador, a su vez, puede pasarse a otros métodos de SoLoud para realizar acciones como pausar, detener o modificar el volumen del sonido.
  • Si bien play() es un método asíncrono, la reproducción se inicia de forma prácticamente instantánea. El paquete flutter_soloud usa la interfaz de función externa (FFI) de Dart para llamar al código C de forma directa y síncrona. No se encuentran en ninguna parte los mensajes habituales entre el código Dart y el código de la plataforma que son característicos de la mayoría de los complementos de Flutter. El único motivo por el que algunos métodos son asíncronos es que parte del código del complemento se ejecuta en su propio aislamiento y la comunicación entre los elementos aislados de Dart es asíncrona.
  • Solo debes confirmar que el campo _soloud no es nulo con _soloud!. Esto es, de nuevo, por brevedad. El código de producción debe abordar correctamente la situación en la que el desarrollador intenta reproducir un sonido antes de que el controlador de audio haya tenido la oportunidad de inicializarse por completo.

Cómo abordar las excepciones

Es posible que hayas notado que, una vez más, ignoras posibles excepciones. Corrijamos ese problema en este método en particular para fines de aprendizaje. (por cuestiones de brevedad, el codelab vuelve a ignorar excepciones después de esta sección).

  • Para lidiar con las excepciones en este caso, une las dos líneas del método playSound() en un bloque try/catch y captura solo instancias de SoLoudException.

lib/audio/audio_controller.dart

  ...

  Future<void> playSound(String assetKey) async {
    try {
      final source = await _soloud!.loadAsset(assetKey);
      await _soloud!.play(source);
    } on SoLoudException catch (e) {
      _log.severe("Cannot play sound '$assetKey'. Ignoring.", e);
    }
  }

  ...

SoLoud genera varias excepciones, como las excepciones SoLoudNotInitializedException o SoLoudTemporaryFolderFailedException. Los documentos de la API de cada método enumeran los tipos de excepciones que se podrían generar.

SoLoud también proporciona una clase superior para todas sus excepciones, la excepción SoLoudException, para que puedas detectar todos los errores relacionados con la funcionalidad del motor de audio. Esto es especialmente útil en casos en los que la reproducción de audio no es fundamental. Por ejemplo, cuando no quieres que se bloquee la sesión de juego del jugador solo porque no se pudo cargar uno de los sonidos de "pew-pew".

Como es de esperar, el método loadAsset() también puede arrojar un error FlutterError si proporcionas una clave de recurso que no existe. Intentar cargar elementos que no están empaquetados con el juego es, por lo general, algo que se debe abordar; por lo tanto, es un error.

Reproducir diferentes sonidos

Es posible que hayas notado que solo reproduces el archivo pew1.mp3, pero hay otras dos versiones del sonido en el directorio de recursos. Suele sonar más natural cuando los juegos tienen varias versiones del mismo sonido y las reproducen de forma aleatoria o rotativa. Esto evita, por ejemplo, que los pasos y disparos suenen demasiado uniformes y, por lo tanto, falsos.

  • Como ejercicio opcional, modifica el código para reproducir un sonido diferente cada vez que se presione el botón.

Una ilustración de

5. Reproducir bucles de música

Cómo administrar sonidos de ejecución prolongada

Parte del audio está diseñado para reproducirse durante períodos prolongados. La música es el ejemplo obvio, pero muchos juegos también juegan a la atmósfera, como el viento aullando por pasillos, los cantos distantes de los monjes, el crujido de metales centenarios o la tos distante de los pacientes.

Estas son fuentes de audio con tiempos de reproducción que se pueden medir en minutos. Debes hacer un seguimiento de ellos para poder detenerlos o detenerlos cuando sea necesario. A menudo, también están respaldados por archivos grandes y pueden consumir mucha memoria, por lo que otra razón para realizar un seguimiento de ellos es que puedas desechar la instancia AudioSource cuando ya no sea necesaria.

Por ese motivo, introducirás un nuevo campo privado en AudioController. Si es el controlador de la canción que se está reproduciendo en ese momento, si la hubiera. Agrega la siguiente línea:

lib/audio/audio_controller.dart

...

class AudioController {
  static final Logger _log = Logger('AudioController');

  SoLoud? _soloud;

  SoundHandle? _musicHandle;    // ← Add this.

  ...

Iniciar música

En esencia, reproducir música no es diferente a reproducir un sonido de un solo intento. Primero, debes cargar el archivo assets/music/looped-song.ogg como una instancia de la clase AudioSource y, luego, usar el método play() de SoLoud para reproducirlo.

Sin embargo, esta vez, tomarás el controlador de sonido que muestra el método play() para manipular el audio mientras se reproduce.

  • Si lo deseas, implementa el método AudioController.startMusic() por tu cuenta. No te preocupes si no proporcionas algunos detalles correctamente. Lo importante es que la música comience cuando selecciones Iniciar música.

Esta es una implementación de referencia:

lib/audio/audio_controller.dart

...

  Future<void> startMusic() async {
    if (_musicHandle != null) {
      if (_soloud!.getIsValidVoiceHandle(_musicHandle!)) {
        _log.info('Music is already playing. Stopping first.');
        await _soloud!.stop(_musicHandle!);
      }
    }
    final musicSource = await _soloud!
        .loadAsset('assets/music/looped-song.ogg', mode: LoadMode.disk);
    _musicHandle = await _soloud!.play(musicSource);
  }

...

Ten en cuenta que cargarás el archivo de música en modo de disco (la enumeración LoadMode.disk). Esto significa que el archivo solo se carga en fragmentos según sea necesario. Para un audio de ejecución más prolongada, por lo general, es mejor cargarlo en modo de disco. En el caso de efectos de sonido cortos, tiene más sentido cargarlos y descomprimirlos en la memoria (la enumeración LoadMode.memory predeterminada).

Sin embargo, tienes un par de problemas. En primer lugar, la música es demasiado fuerte y domina los sonidos. En la mayoría de los juegos, la música se encuentra en segundo plano la mayor parte del tiempo, lo que da protagonismo al audio más informativo, como el discurso y los efectos de sonido. Esto es fácil de solucionar mediante el parámetro de volumen del método de reproducción. Por ejemplo, puedes probar _soloud!.play(musicSource, volume: 0.6) para reproducir la canción al 60% de volumen. Como alternativa, puedes establecer el volumen más adelante con un parámetro similar a _soloud!.setVolume(_musicHandle, 0.6).

El segundo problema es que la canción se detiene de forma abrupta. Esto se debe a que se trata de una canción que se supone que se reproduce en bucle y que el punto de partida del bucle no es el principio del archivo de audio.

88d2c57fffdfe996.png

Esta es una opción popular para la música de videojuegos porque significa que la canción comienza con una introducción natural y, luego, se reproduce el tiempo que sea necesario sin un punto evidente en el bucle. Cuando el juego necesita hacer una transición fuera de la canción que se está reproduciendo, simplemente se atenúa.

Por suerte, SoLoud ofrece maneras de reproducir audio que se repite indefinidamente. El método play() toma un valor booleano para el parámetro looping y también el valor del punto de partida del bucle como el parámetro loopingStartAt. El código resultante se ve así:

lib/audio/audio_controller.dart

...

_musicHandle = await _soloud!.play(
  musicSource,
  volume: 0.6,
  looping: true,
  // ↓ The exact timestamp of the start of the loop.
  loopingStartAt: const Duration(seconds: 25, milliseconds: 43),
);

...

Si no configuras el parámetro loopingStartAt, el valor predeterminado es Duration.zero (en otras palabras, el inicio del archivo de audio). Si tienes una pista de música que es un bucle perfecto sin ninguna introducción, esto es lo que quieres.

  • Para asegurarte de que la fuente de audio se elimine correctamente una vez que termine de reproducirse, escucha la transmisión de allInstancesFinished que proporciona cada fuente de audio. Con las llamadas de registro agregadas, el método startMusic() se verá de la siguiente manera:

lib/audio/audio_controller.dart

...

  Future<void> startMusic() async {
    if (_musicHandle != null) {
      if (_soloud!.getIsValidVoiceHandle(_musicHandle!)) {
        _log.info('Music is already playing. Stopping first.');
        await _soloud!.stop(_musicHandle!);
      }
    }
    _log.info('Loading music');
    final musicSource = await _soloud!
        .loadAsset('assets/music/looped-song.ogg', mode: LoadMode.disk);
    musicSource.allInstancesFinished.first.then((_) {
      _soloud!.disposeSource(musicSource);
      _log.info('Music source disposed');
      _musicHandle = null;
    });

    _log.info('Playing music');
    _musicHandle = await _soloud!.play(
      musicSource,
      volume: 0.6,
      looping: true,
      loopingStartAt: const Duration(seconds: 25, milliseconds: 43),
    );
  }

...

Atenuación de sonido

Tu próximo problema es que la música nunca termina. Implementemos una atenuación.

Una forma en la que podrías implementar la atenuación sería tener algún tipo de función a la que se llame varias veces por segundo, como Ticker o Timer.periodic, y bajar el volumen de la música en pequeñas disminuciones. Esto funcionaría, pero es mucho trabajo.

Por suerte, SoLoud proporciona métodos convenientes de activar y olvidar que hacen esto por ti. Aquí te mostramos cómo puedes hacer que la música se atenúe en el transcurso de cinco segundos y, luego, detener la instancia de sonido para que no consuma recursos de la CPU innecesariamente. Reemplaza el método fadeOutMusic() con este código:

lib/audio/audio_controller.dart

...

  void fadeOutMusic() {
    if (_musicHandle == null) {
      _log.info('Nothing to fade out');
      return;
    }
    const length = Duration(seconds: 5);
    _soloud!.fadeVolume(_musicHandle!, 0, length);
    _soloud!.scheduleStop(_musicHandle!, length);
  }

...

6. Aplicar efectos

Una gran ventaja de contar con un motor de audio adecuado es que puedes realizar procesamiento de audio, como enrutar algunos sonidos a través de una reverberación, un ecualizador o un filtro de paso bajo.

En los juegos, esto se puede usar para la diferenciación auditiva de las ubicaciones. Por ejemplo, un aplauso suena diferente en un bosque que en un búnker de concreto. Mientras un bosque ayuda a disipar y absorber el sonido, las paredes desnudas de un búnker reflejan las ondas de sonido, lo que genera reverberación. Del mismo modo, las voces de las personas suenan diferentes cuando se escuchan a través de una pared. Las frecuencias más altas de esos sonidos se atenúan más fácilmente a medida que viajan a través del medio sólido, lo que da como resultado un efecto de filtro de paso bajo.

Ilustración de dos personas conversando en una sala. Las ondas de sonido no solo van de una persona a otra directamente, sino que también rebotan en las paredes y el cielorraso.

SoLoud ofrece varios efectos de audio diferentes, que se pueden aplicar al audio.

  • Para que parezca que el reproductor está en una habitación grande, como una catedral o una cueva, usa la enum FilterType.freeverbFilter:

lib/audio/audio_controller.dart

...

  void applyFilter() {
    _soloud!.addGlobalFilter(FilterType.freeverbFilter);
    _soloud!.setFilterParameter(FilterType.freeverbFilter, 0, 0.2);
    _soloud!.setFilterParameter(FilterType.freeverbFilter, 2, 0.9);
  }

  void removeFilter() {
    _soloud!.removeGlobalFilter(FilterType.freeverbFilter);
  }

...

Como puedes ver, con los filtros te adentras en áreas más complejas. La configuración de un parámetro de filtro se realiza con el índice del parámetro. Por ejemplo, el parámetro Wet de freeverb tiene el índice 0, y el parámetro Room Size tiene el índice 2.

Con el código anterior, puedes hacer lo siguiente:

  • Habilita el filtro freeverb de forma global o para toda la mezcla de audio, no solo un sonido.
  • Establece el parámetro Wet en 0.2, lo que significa que el audio resultante será un 80% original y un 20% la salida del efecto de reverberación. Si estableces este parámetro en 1.0, sería como escuchar solo las ondas de sonido que regresan a ti desde las paredes distantes de la habitación y ninguna parte del audio original.
  • Establece el parámetro Room Size en 0.9. Puedes modificar este parámetro según tus preferencias o incluso cambiarlo de forma dinámica. 1.0 es una gran caverna, mientras que 0.0 es un baño.
  • Si quieres, cambia el código y aplica uno de los siguientes filtros o una combinación de ellos:
  • FilterType.biquadResonantFilter (se puede usar como filtro de paso bajo)
  • FilterType.eqFilter
  • FilterType.echoFilter
  • FilterType.lofiFilter
  • FilterType.flangerFilter
  • FilterType.bassboostFilter
  • FilterType.waveShaperFilter
  • FilterType.robotizeFilter
  • FilterType.freeverbFilter

7. Felicitaciones

Implementaste un controlador de audio que reproduce sonidos, repite música en bucle y aplica efectos.

Más información

  • Aprovecha al máximo el control de audio con funciones como la precarga de sonidos en el inicio, la reproducción de canciones en una secuencia o la aplicación gradual de un filtro a lo largo del tiempo.
  • Lee la documentación del paquete de flutter_soloud.
  • Lee la página principal de la biblioteca de C++ subyacente.
  • Obtén más información sobre la FFI de Dart, la tecnología que se usa para interactuar con la biblioteca C++.
  • Mira la charla de Guy Somberg sobre la programación de audio de juegos para inspirarte. (También hay uno más largo). Cuando Guy habla de "middleware", se refiere a bibliotecas como SoLoud y FMOD. El resto del código suele ser específico para cada juego.
  • Compila tu juego y publícalo.

Una ilustración de auriculares