Delegate all audio management to the new audio package, then manage that through a new wolf3d class

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-15 14:33:58 +01:00
parent 070110adae
commit 6eb903cbaa
10 changed files with 249 additions and 81 deletions

View File

@@ -17,21 +17,19 @@ import 'package:wolf_dart/features/player/player.dart';
import 'package:wolf_dart/features/renderer/raycast_painter.dart';
import 'package:wolf_dart/features/renderer/weapon_painter.dart';
import 'package:wolf_dart/features/ui/hud.dart';
import 'package:wolf_dart/sprite_gallery.dart';
import 'package:wolf_dart/wolf_3d.dart';
class WolfRenderer extends StatefulWidget {
const WolfRenderer(
this.data, {
required this.difficulty,
required this.startingEpisode,
super.key,
this.difficulty = Difficulty.bringEmOn,
this.showSpriteGallery = false,
this.isShareware = true,
});
final WolfensteinData data;
final Difficulty difficulty;
final bool showSpriteGallery;
final bool isShareware;
final int startingEpisode;
@override
State<WolfRenderer> createState() => _WolfRendererState();
@@ -56,6 +54,9 @@ class _WolfRendererState extends State<WolfRenderer>
double damageFlashOpacity = 0.0;
late int _currentMapIndex;
late WolfLevel _currentLevel;
List<Entity> entities = [];
@override
@@ -64,7 +65,38 @@ class _WolfRendererState extends State<WolfRenderer>
_initGame();
}
void _loadLevel(int mapIndex) {
// Grab the specific level from the singleton
_currentLevel = Wolf3d.I.levels[mapIndex];
// TODO: Initialize player position, spawn enemies based on difficulty, etc.
debugPrint("Loaded Level: ${_currentLevel.name}");
}
void _onLevelCompleted() {
// When the player hits the elevator switch, advance the map
setState(() {
_currentMapIndex++;
// Check if they beat the episode (each episode is 10 levels)
int maxLevelForEpisode = (widget.startingEpisode * 10) + 9;
if (_currentMapIndex > maxLevelForEpisode) {
// TODO: Handle episode completion (show victory screen, return to menu)
debugPrint("Episode Completed!");
} else {
_loadLevel(_currentMapIndex);
}
});
}
Future<void> _initGame() async {
// 1. Calculate the starting index
_currentMapIndex = widget.startingEpisode * 10;
// 2. Load the initial level data
_loadLevel(_currentMapIndex);
// Get the first level out of the data class
activeLevel = widget.data.levels.first;
@@ -109,7 +141,7 @@ class _WolfRendererState extends State<WolfRenderer>
y + 0.5,
widget.difficulty,
widget.data.sprites.length,
isSharewareMode: widget.isShareware,
isSharewareMode: widget.data.version == GameVersion.shareware,
);
if (newEntity != null) {
@@ -402,10 +434,6 @@ class _WolfRendererState extends State<WolfRenderer>
return const Center(child: CircularProgressIndicator(color: Colors.teal));
}
if (widget.showSpriteGallery) {
return SpriteGallery(sprites: widget.data.sprites);
}
return Scaffold(
backgroundColor: Colors.black,
body: KeyboardListener(

View File

@@ -1,7 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_soloud/flutter_soloud.dart';
import 'package:wolf_3d_data_types/wolf_3d_data_types.dart';
import 'package:wolf_3d_synth/wolf_3d_synth.dart';
import 'package:wolf_dart/features/difficulty/difficulty.dart';
import 'package:wolf_dart/features/renderer/renderer.dart';
import 'package:wolf_dart/wolf_3d.dart';
@@ -16,77 +14,23 @@ class DifficultyScreen extends StatefulWidget {
}
class _DifficultyScreenState extends State<DifficultyScreen> {
AudioSource? _menuMusicSource;
SoundHandle? _menuMusicHandle;
bool get isShareware => Wolf3d.I.activeGame.version == GameVersion.shareware;
@override
void initState() {
super.initState();
_playMenuMusic();
}
Future<void> _playMenuMusic() async {
final soloud = SoLoud.instance;
if (!soloud.isInitialized) {
return;
}
// 2. We only want to play music if the IMF data actually exists
if (Wolf3d.I.music.isNotEmpty) {
// Get the first track (usually the menu theme "Wondering About My Loved Ones")
final music = Wolf3d.I.music.first;
// Render the hardware instructions into PCM and wrap in a WAV header
final pcmSamples = ImfRenderer.render(music);
final wavBytes = ImfRenderer.createWavFile(pcmSamples);
// 3. Load the bytes into SoLoud's memory
// The 'menu_theme.wav' string is just a dummy name to tell SoLoud it's dealing with a WAV format
_menuMusicSource = await soloud.loadMem('menu_theme.wav', wavBytes);
// 4. Play the source and tell it to loop continuously!
_menuMusicHandle = await soloud.play(
_menuMusicSource!,
looping: true,
);
}
}
@override
void dispose() {
_cleanupAudio();
Wolf3d.I.audio.stopMusic();
super.dispose();
}
void _cleanupAudio() {
final soloud = SoLoud.instance;
// Stop the playback
if (_menuMusicHandle != null) {
soloud.stop(_menuMusicHandle!);
}
// Free the raw WAV data from C++ memory
if (_menuMusicSource != null) {
soloud.disposeSource(_menuMusicSource!);
}
}
void _startGame(Difficulty difficulty, {bool showGallery = false}) {
// Stop the music and clear memory right before we push the new route
_cleanupAudio();
Wolf3d.I.audio.stopMusic();
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => WolfRenderer(
Wolf3d.I.activeGame,
difficulty: difficulty,
isShareware: isShareware,
showSpriteGallery: showGallery,
startingEpisode: Wolf3d.I.activeEpisode,
),
),
);

View File

@@ -0,0 +1,97 @@
import 'package:flutter/material.dart';
import 'package:wolf_dart/features/screens/difficulty_screen.dart';
import 'package:wolf_dart/wolf_3d.dart';
class EpisodeScreen extends StatefulWidget {
const EpisodeScreen({super.key});
@override
State<EpisodeScreen> createState() => _EpisodeScreenState();
}
class _EpisodeScreenState extends State<EpisodeScreen> {
final List<String> _episodeNames = [
"Episode 1\nEscape from Wolfenstein",
"Episode 2\nOperation: Eisenfaust",
"Episode 3\nDie, Fuhrer, Die!",
"Episode 4\nA Dark Secret",
"Episode 5\nTrail of the Madman",
"Episode 6\nConfrontation",
];
@override
void initState() {
super.initState();
if (Wolf3d.I.music.isNotEmpty) {
Wolf3d.I.audio.playMusic(Wolf3d.I.music.first);
}
}
void _selectEpisode(int index) {
Wolf3d.I.setActiveEpisode(index);
Navigator.of(context).push(
MaterialPageRoute(
// We pass the audio handles so the next screen can stop them when the game starts
builder: (context) => DifficultyScreen(),
),
);
}
@override
Widget build(BuildContext context) {
// Determine how many episodes are available (10 levels per episode)
final int numberOfEpisodes = (Wolf3d.I.levels.length / 10).floor().clamp(
1,
6,
);
return Scaffold(
backgroundColor: Colors.black,
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'WHICH EPISODE TO PLAY?',
style: TextStyle(
color: Colors.red,
fontSize: 32,
fontWeight: FontWeight.bold,
fontFamily: 'Courier',
),
),
const SizedBox(height: 40),
ListView.builder(
shrinkWrap: true,
itemCount: numberOfEpisodes,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 32.0,
),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blueGrey[900],
foregroundColor: Colors.white,
minimumSize: const Size(300, 60),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
),
),
onPressed: () => _selectEpisode(index),
child: Text(
_episodeNames[index],
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 18),
),
),
);
},
),
],
),
),
);
}
}

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:wolf_3d_data_types/wolf_3d_data_types.dart';
import 'package:wolf_dart/features/difficulty/difficulty_screen.dart';
import 'package:wolf_dart/features/screens/episode_screen.dart';
import 'package:wolf_dart/wolf_3d.dart';
class GameSelectScreen extends StatelessWidget {
@@ -22,7 +22,7 @@ class GameSelectScreen extends StatelessWidget {
Wolf3d.I.setActiveGame(data);
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => DifficultyScreen(),
builder: (context) => const EpisodeScreen(),
),
);
},

View File

@@ -1,17 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter_soloud/flutter_soloud.dart';
import 'package:wolf_dart/game_select_screen.dart';
import 'package:wolf_dart/wolf_3d.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await SoLoud.instance.init(
sampleRate: 44100, // Audio quality
bufferSize: 2048, // Buffer size affects latency
channels: Channels.stereo,
);
await Wolf3d.I.init();
runApp(

View File

@@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:wolf_3d_data/wolf_3d_data.dart';
import 'package:wolf_3d_data_types/wolf_3d_data_types.dart';
import 'package:wolf_3d_synth/wolf_3d_synth.dart';
class Wolf3d {
Wolf3d._();
@@ -12,6 +13,9 @@ class Wolf3d {
final List<WolfensteinData> availableGames = [];
WolfensteinData? _activeGame;
// --- Core Systems ---
final WolfAudio audio = WolfAudio();
// --- Getters ---
WolfensteinData get activeGame {
if (_activeGame == null) {
@@ -20,6 +24,15 @@ class Wolf3d {
return _activeGame!;
}
// --- Episode ---
int _activeEpisode = 0;
int get activeEpisode => _activeEpisode;
void setActiveEpisode(int episodeIndex) {
_activeEpisode = episodeIndex;
}
// Convenience getters for the active game's assets
List<WolfLevel> get levels => activeGame.levels;
List<Sprite> get walls => activeGame.walls;
@@ -36,6 +49,7 @@ class Wolf3d {
/// Initializes the engine by loading available game data.
Future<void> init({String? directory}) async {
await audio.init();
availableGames.clear();
// 1. Bundle asset loading (migrated from GameSelectScreen)

View File

@@ -0,0 +1,92 @@
import 'package:flutter_soloud/flutter_soloud.dart';
import 'package:wolf_3d_data_types/wolf_3d_data_types.dart';
import 'package:wolf_3d_synth/src/imf_renderer.dart';
class WolfAudio {
bool _isInitialized = false;
// --- Music State ---
AudioSource? _currentMusicSource;
SoundHandle? _currentMusicHandle;
/// Initializes the SoLoud audio engine.
Future<void> init() async {
if (_isInitialized) return;
try {
await SoLoud.instance.init(
sampleRate: 44100,
bufferSize: 2048,
channels: Channels.stereo,
);
_isInitialized = true;
print("WolfAudio: SoLoud initialized successfully.");
} catch (e) {
print("WolfAudio: Failed to initialize SoLoud - $e");
}
}
/// Disposes of the audio engine and frees resources.
void dispose() {
stopMusic();
SoLoud.instance.deinit();
_isInitialized = false;
}
// ==========================================
// MUSIC MANAGEMENT
// ==========================================
/// Renders and plays a specific IMF music track.
Future<void> playMusic(ImfMusic track, {bool looping = true}) async {
if (!_isInitialized) return;
// Stop currently playing music to prevent overlap
stopMusic();
try {
// Render hardware instructions into PCM and wrap in WAV
final pcmSamples = ImfRenderer.render(track);
final wavBytes = ImfRenderer.createWavFile(pcmSamples);
_currentMusicSource = await SoLoud.instance.loadMem(
'track.wav',
wavBytes,
);
_currentMusicHandle = await SoLoud.instance.play(
_currentMusicSource!,
looping: looping,
);
} catch (e) {
print("WolfAudio: Error playing music track - $e");
}
}
/// Halts playback and frees memory for the current track.
void stopMusic() {
if (!_isInitialized) return;
if (_currentMusicHandle != null) {
SoLoud.instance.stop(_currentMusicHandle!);
_currentMusicHandle = null;
}
if (_currentMusicSource != null) {
SoLoud.instance.disposeSource(_currentMusicSource!);
_currentMusicSource = null;
}
}
/// Pauses the current track.
void pauseMusic() {
if (_isInitialized && _currentMusicHandle != null) {
SoLoud.instance.setPause(_currentMusicHandle!, true);
}
}
/// Resumes a paused track.
void resumeMusic() {
if (_isInitialized && _currentMusicHandle != null) {
SoLoud.instance.setPause(_currentMusicHandle!, false);
}
}
}

View File

@@ -3,4 +3,4 @@
/// More dartdocs go here.
library;
export 'src/imf_renderer.dart' show ImfRenderer;
export 'src/wolf_3d_audio.dart' show WolfAudio;

View File

@@ -9,6 +9,7 @@ environment:
resolution: workspace
dependencies:
flutter_soloud: ^3.5.1
wolf_3d_data_types:
dev_dependencies:

View File

@@ -12,7 +12,6 @@ dependencies:
wolf_3d_synth: any
flutter:
sdk: flutter
flutter_soloud: ^3.5.1
dev_dependencies:
flutter_test: