1
0
mirror of https://github.com/flutter/samples.git synced 2025-11-11 23:39:14 +00:00

Add game_template (#1180)

Adds a template / sample for games built in Flutter, with all the bells and whistles, like ads, in-app purchases, audio, main menu, settings, and so on.

Co-authored-by: Parker Lougheed
Co-authored-by: Shams Zakhour
This commit is contained in:
Filip Hracek
2022-05-10 15:08:43 +02:00
committed by GitHub
parent 5143bcf302
commit daa024a829
208 changed files with 8993 additions and 0 deletions

View File

@@ -0,0 +1,271 @@
// Copyright 2022, the Flutter project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:collection';
import 'dart:math';
import 'package:audioplayers/audioplayers.dart' hide Logger;
import 'package:flutter/widgets.dart';
import 'package:logging/logging.dart';
import '../settings/settings.dart';
import 'songs.dart';
import 'sounds.dart';
/// Allows playing music and sound. A facade to `package:audioplayers`.
class AudioController {
static final _log = Logger('AudioController');
late AudioCache _musicCache;
late AudioCache _sfxCache;
final AudioPlayer _musicPlayer;
/// This is a list of [AudioPlayer] instances which are rotated to play
/// sound effects.
///
/// Normally, we would just call [AudioCache.play] and let it procure its
/// own [AudioPlayer] every time. But this seems to lead to errors and
/// bad performance on iOS devices.
final List<AudioPlayer> _sfxPlayers;
int _currentSfxPlayer = 0;
final Queue<Song> _playlist;
final Random _random = Random();
SettingsController? _settings;
ValueNotifier<AppLifecycleState>? _lifecycleNotifier;
/// Creates an instance that plays music and sound.
///
/// Use [polyphony] to configure the number of sound effects (SFX) that can
/// play at the same time. A [polyphony] of `1` will always only play one
/// sound (a new sound will stop the previous one). See discussion
/// of [_sfxPlayers] to learn why this is the case.
///
/// Background music does not count into the [polyphony] limit. Music will
/// never be overridden by sound effects.
AudioController({int polyphony = 2})
: assert(polyphony >= 1),
_musicPlayer = AudioPlayer(playerId: 'musicPlayer'),
_sfxPlayers = Iterable.generate(
polyphony,
(i) => AudioPlayer(
playerId: 'sfxPlayer#$i',
mode: PlayerMode.LOW_LATENCY)).toList(growable: false),
_playlist = Queue.of(List<Song>.of(songs)..shuffle()) {
_musicCache = AudioCache(
fixedPlayer: _musicPlayer,
prefix: 'assets/music/',
);
_sfxCache = AudioCache(
fixedPlayer: _sfxPlayers.first,
prefix: 'assets/sfx/',
);
_musicPlayer.onPlayerCompletion.listen(_changeSong);
}
/// Enables the [AudioController] to listen to [AppLifecycleState] events,
/// and therefore do things like stopping playback when the game
/// goes into the background.
void attachLifecycleNotifier(
ValueNotifier<AppLifecycleState> lifecycleNotifier) {
_lifecycleNotifier?.removeListener(_handleAppLifecycle);
lifecycleNotifier.addListener(_handleAppLifecycle);
_lifecycleNotifier = lifecycleNotifier;
}
/// Enables the [AudioController] to track changes to settings.
/// Namely, when any of [SettingsController.muted],
/// [SettingsController.musicOn] or [SettingsController.soundsOn] changes,
/// the audio controller will act accordingly.
void attachSettings(SettingsController settingsController) {
if (_settings == settingsController) {
// Already attached to this instance. Nothing to do.
return;
}
// Remove handlers from the old settings controller if present
final oldSettings = _settings;
if (oldSettings != null) {
oldSettings.muted.removeListener(_mutedHandler);
oldSettings.musicOn.removeListener(_musicOnHandler);
oldSettings.soundsOn.removeListener(_soundsOnHandler);
}
_settings = settingsController;
// Add handlers to the new settings controller
settingsController.muted.addListener(_mutedHandler);
settingsController.musicOn.addListener(_musicOnHandler);
settingsController.soundsOn.addListener(_soundsOnHandler);
if (!settingsController.muted.value && settingsController.musicOn.value) {
_startMusic();
}
}
void dispose() {
_lifecycleNotifier?.removeListener(_handleAppLifecycle);
_stopAllSound();
_musicPlayer.dispose();
for (final player in _sfxPlayers) {
player.dispose();
}
}
/// Preloads all sound effects.
Future<void> initialize() async {
_log.info('Preloading sound effects');
// This assumes there is only a limited number of sound effects in the game.
// If there are hundreds of long sound effect files, it's better
// to be more selective when preloading.
await _sfxCache
.loadAll(SfxType.values.expand(soundTypeToFilename).toList());
}
/// Plays a single sound effect, defined by [type].
///
/// The controller will ignore this call when the attached settings'
/// [SettingsController.muted] is `true` or if its
/// [SettingsController.soundsOn] is `false`.
void playSfx(SfxType type) {
final muted = _settings?.muted.value ?? true;
if (muted) {
_log.info(() => 'Ignoring playing sound ($type) because audio is muted.');
return;
}
final soundsOn = _settings?.soundsOn.value ?? false;
if (!soundsOn) {
_log.info(() =>
'Ignoring playing sound ($type) because sounds are turned off.');
return;
}
_log.info(() => 'Playing sound: $type');
final options = soundTypeToFilename(type);
final filename = options[_random.nextInt(options.length)];
_log.info(() => '- Chosen filename: $filename');
_sfxCache.play(filename, volume: soundTypeToVolume(type));
_currentSfxPlayer = (_currentSfxPlayer + 1) % _sfxPlayers.length;
_sfxCache.fixedPlayer = _sfxPlayers[_currentSfxPlayer];
}
void _changeSong(void _) {
_log.info('Last song finished playing.');
// Put the song that just finished playing to the end of the playlist.
_playlist.addLast(_playlist.removeFirst());
// Play the next song.
_log.info(() => 'Playing ${_playlist.first} now.');
_musicCache.play(_playlist.first.filename);
}
void _handleAppLifecycle() {
switch (_lifecycleNotifier!.value) {
case AppLifecycleState.paused:
case AppLifecycleState.detached:
_stopAllSound();
break;
case AppLifecycleState.resumed:
if (!_settings!.muted.value && _settings!.musicOn.value) {
_resumeMusic();
}
break;
case AppLifecycleState.inactive:
// No need to react to this state change.
break;
}
}
void _musicOnHandler() {
if (_settings!.musicOn.value) {
// Music got turned on.
if (!_settings!.muted.value) {
_resumeMusic();
}
} else {
// Music got turned off.
_stopMusic();
}
}
void _mutedHandler() {
if (_settings!.muted.value) {
// All sound just got muted.
_stopAllSound();
} else {
// All sound just got un-muted.
if (_settings!.musicOn.value) {
_resumeMusic();
}
}
}
Future<void> _resumeMusic() async {
_log.info('Resuming music');
switch (_musicPlayer.state) {
case PlayerState.PAUSED:
_log.info('Calling _musicPlayer.resume()');
try {
await _musicPlayer.resume();
} catch (e) {
// Sometimes, resuming fails with an "Unexpected" error.
_log.severe(e);
await _musicCache.play(_playlist.first.filename);
}
break;
case PlayerState.STOPPED:
_log.info("resumeMusic() called when music is stopped. "
"This probably means we haven't yet started the music. "
"For example, the game was started with sound off.");
await _musicCache.play(_playlist.first.filename);
break;
case PlayerState.PLAYING:
_log.warning('resumeMusic() called when music is playing. '
'Nothing to do.');
break;
case PlayerState.COMPLETED:
_log.warning('resumeMusic() called when music is completed. '
"Music should never be 'completed' as it's either not playing "
"or looping forever.");
await _musicCache.play(_playlist.first.filename);
break;
}
}
void _soundsOnHandler() {
for (final player in _sfxPlayers) {
if (player.state == PlayerState.PLAYING) {
player.stop();
}
}
}
void _startMusic() {
_log.info('starting music');
_musicCache.play(_playlist.first.filename);
}
void _stopAllSound() {
if (_musicPlayer.state == PlayerState.PLAYING) {
_musicPlayer.pause();
}
for (final player in _sfxPlayers) {
player.stop();
}
}
void _stopMusic() {
_log.info('Stopping music');
if (_musicPlayer.state == PlayerState.PLAYING) {
_musicPlayer.pause();
}
}
}

View File

@@ -0,0 +1,24 @@
// Copyright 2022, the Flutter project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
const Set<Song> songs = {
// Filenames with whitespace break package:audioplayers on iOS
// (as of February 2022), so we use no whitespace.
Song('Mr_Smith-Azul.mp3', 'Azul', artist: 'Mr Smith'),
Song('Mr_Smith-Sonorus.mp3', 'Sonorus', artist: 'Mr Smith'),
Song('Mr_Smith-Sunday_Solitude.mp3', 'SundaySolitude', artist: 'Mr Smith'),
};
class Song {
final String filename;
final String name;
final String? artist;
const Song(this.filename, this.name, {this.artist});
@override
String toString() => 'Song<$filename>';
}

View File

@@ -0,0 +1,71 @@
// Copyright 2022, the Flutter project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
List<String> soundTypeToFilename(SfxType type) {
switch (type) {
case SfxType.huhsh:
return const [
'hash1.mp3',
'hash2.mp3',
'hash3.mp3',
];
case SfxType.wssh:
return const [
'wssh1.mp3',
'wssh2.mp3',
'dsht1.mp3',
'ws1.mp3',
'spsh1.mp3',
'hh1.mp3',
'hh2.mp3',
'kss1.mp3',
];
case SfxType.buttonTap:
return const [
'k1.mp3',
'k2.mp3',
'p1.mp3',
'p2.mp3',
];
case SfxType.congrats:
return const [
'yay1.mp3',
'wehee1.mp3',
'oo1.mp3',
];
case SfxType.erase:
return const [
'fwfwfwfwfw1.mp3',
'fwfwfwfw1.mp3',
];
case SfxType.swishSwish:
return const [
'swishswish1.mp3',
];
}
}
/// Allows control over loudness of different SFX types.
double soundTypeToVolume(SfxType type) {
switch (type) {
case SfxType.huhsh:
return 0.4;
case SfxType.wssh:
return 0.2;
case SfxType.buttonTap:
case SfxType.congrats:
case SfxType.erase:
case SfxType.swishSwish:
return 1.0;
}
}
enum SfxType {
huhsh,
wssh,
buttonTap,
congrats,
erase,
swishSwish,
}