Compare commits
2 Commits
18af93824b
...
0ee6ef8493
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ee6ef8493 | ||
|
|
05a8c03892 |
@@ -33,6 +33,12 @@ class AudioService {
|
|||||||
// 초기화 실패 여부 (WASM 등에서 오디오 지원 안됨)
|
// 초기화 실패 여부 (WASM 등에서 오디오 지원 안됨)
|
||||||
bool _initFailed = false;
|
bool _initFailed = false;
|
||||||
|
|
||||||
|
// 웹에서 사용자 상호작용 대기 중인 BGM (자동재생 정책 대응)
|
||||||
|
String? _pendingBgm;
|
||||||
|
|
||||||
|
// 사용자 상호작용 발생 여부 (웹 자동재생 정책 우회용)
|
||||||
|
bool _userInteracted = false;
|
||||||
|
|
||||||
/// 서비스 초기화
|
/// 서비스 초기화
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
if (_initialized || _initFailed) return;
|
if (_initialized || _initFailed) return;
|
||||||
@@ -55,7 +61,11 @@ class AudioService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
if (kIsWeb) {
|
|
||||||
|
// 모바일/데스크톱에서는 자동재생 제한 없음
|
||||||
|
if (!kIsWeb) {
|
||||||
|
_userInteracted = true;
|
||||||
|
} else {
|
||||||
debugPrint('[AudioService] Initialized on Web platform');
|
debugPrint('[AudioService] Initialized on Web platform');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -68,6 +78,9 @@ class AudioService {
|
|||||||
///
|
///
|
||||||
/// [name]은 assets/audio/bgm/ 폴더 내 파일명 (확장자 제외)
|
/// [name]은 assets/audio/bgm/ 폴더 내 파일명 (확장자 제외)
|
||||||
/// 예: playBgm('battle') → assets/audio/bgm/battle.mp3
|
/// 예: playBgm('battle') → assets/audio/bgm/battle.mp3
|
||||||
|
///
|
||||||
|
/// 웹에서 사용자 상호작용 없이 호출되면 대기 상태로 저장되고,
|
||||||
|
/// 다음 SFX 재생 시 함께 시작됩니다.
|
||||||
Future<void> playBgm(String name) async {
|
Future<void> playBgm(String name) async {
|
||||||
if (_initFailed) return; // 초기화 실패 시 무시
|
if (_initFailed) return; // 초기화 실패 시 무시
|
||||||
if (!_initialized) await init();
|
if (!_initialized) await init();
|
||||||
@@ -75,13 +88,20 @@ class AudioService {
|
|||||||
if (_currentBgm == name) return; // 이미 재생 중
|
if (_currentBgm == name) return; // 이미 재생 중
|
||||||
|
|
||||||
try {
|
try {
|
||||||
_currentBgm = name;
|
|
||||||
await _bgmPlayer!.setAsset('assets/audio/bgm/$name.mp3');
|
await _bgmPlayer!.setAsset('assets/audio/bgm/$name.mp3');
|
||||||
await _bgmPlayer!.play();
|
await _bgmPlayer!.play();
|
||||||
|
_currentBgm = name;
|
||||||
|
_pendingBgm = null;
|
||||||
|
_userInteracted = true; // 재생 성공 → 상호작용 확인됨
|
||||||
debugPrint('[AudioService] Playing BGM: $name');
|
debugPrint('[AudioService] Playing BGM: $name');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 파일이 없으면 무시 (개발 중 에셋 미추가 상태)
|
// 웹 자동재생 정책으로 실패 시 대기 상태로 저장
|
||||||
|
if (kIsWeb && e.toString().contains('NotAllowedError')) {
|
||||||
|
_pendingBgm = name;
|
||||||
|
debugPrint('[AudioService] BGM $name pending (waiting for user interaction)');
|
||||||
|
} else {
|
||||||
debugPrint('[AudioService] Failed to play BGM $name: $e');
|
debugPrint('[AudioService] Failed to play BGM $name: $e');
|
||||||
|
}
|
||||||
_currentBgm = null;
|
_currentBgm = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -112,6 +132,8 @@ class AudioService {
|
|||||||
///
|
///
|
||||||
/// [name]은 assets/audio/sfx/ 폴더 내 파일명 (확장자 제외)
|
/// [name]은 assets/audio/sfx/ 폴더 내 파일명 (확장자 제외)
|
||||||
/// 예: playSfx('attack') → assets/audio/sfx/attack.mp3
|
/// 예: playSfx('attack') → assets/audio/sfx/attack.mp3
|
||||||
|
///
|
||||||
|
/// 웹에서 대기 중인 BGM이 있으면 함께 재생 시작합니다.
|
||||||
Future<void> playSfx(String name) async {
|
Future<void> playSfx(String name) async {
|
||||||
if (_initFailed) return; // 초기화 실패 시 무시
|
if (_initFailed) return; // 초기화 실패 시 무시
|
||||||
if (!_initialized) await init();
|
if (!_initialized) await init();
|
||||||
@@ -119,6 +141,15 @@ class AudioService {
|
|||||||
if (_sfxVolume == 0) return; // 볼륨이 0이면 재생 안함
|
if (_sfxVolume == 0) return; // 볼륨이 0이면 재생 안함
|
||||||
if (_sfxPlayers.isEmpty) return;
|
if (_sfxPlayers.isEmpty) return;
|
||||||
|
|
||||||
|
// 웹에서 대기 중인 BGM 재생 시도 (사용자 상호작용 발생)
|
||||||
|
if (!_userInteracted && _pendingBgm != null) {
|
||||||
|
_userInteracted = true;
|
||||||
|
final pending = _pendingBgm;
|
||||||
|
_pendingBgm = null;
|
||||||
|
// BGM 재생 (비동기로 진행)
|
||||||
|
playBgm(pending!);
|
||||||
|
}
|
||||||
|
|
||||||
// 사용 가능한 플레이어 찾기
|
// 사용 가능한 플레이어 찾기
|
||||||
AudioPlayer? availablePlayer;
|
AudioPlayer? availablePlayer;
|
||||||
for (final player in _sfxPlayers) {
|
for (final player in _sfxPlayers) {
|
||||||
@@ -170,6 +201,21 @@ class AudioService {
|
|||||||
/// 현재 재생 중인 BGM
|
/// 현재 재생 중인 BGM
|
||||||
String? get currentBgm => _currentBgm;
|
String? get currentBgm => _currentBgm;
|
||||||
|
|
||||||
|
/// 사용자 상호작용 발생 알림 (웹 자동재생 정책 우회)
|
||||||
|
///
|
||||||
|
/// 버튼 클릭 등 사용자 상호작용 시 호출하면
|
||||||
|
/// 대기 중인 BGM이 재생됩니다.
|
||||||
|
Future<void> notifyUserInteraction() async {
|
||||||
|
if (_userInteracted) return;
|
||||||
|
_userInteracted = true;
|
||||||
|
|
||||||
|
if (_pendingBgm != null) {
|
||||||
|
final pending = _pendingBgm;
|
||||||
|
_pendingBgm = null;
|
||||||
|
await playBgm(pending!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 서비스 정리
|
/// 서비스 정리
|
||||||
Future<void> dispose() async {
|
Future<void> dispose() async {
|
||||||
await _bgmPlayer?.dispose();
|
await _bgmPlayer?.dispose();
|
||||||
|
|||||||
@@ -745,6 +745,9 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
setState(() => _sfxVolume = volume);
|
setState(() => _sfxVolume = volume);
|
||||||
widget.audioService?.setSfxVolume(volume);
|
widget.audioService?.setSfxVolume(volume);
|
||||||
},
|
},
|
||||||
|
// 통계 및 도움말
|
||||||
|
onShowStatistics: () => _showStatisticsDialog(context),
|
||||||
|
onShowHelp: () => HelpDialog.show(context),
|
||||||
),
|
),
|
||||||
// 사망 오버레이
|
// 사망 오버레이
|
||||||
if (state.isDead && state.deathInfo != null)
|
if (state.isDead && state.deathInfo != null)
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ class MobileCarouselLayout extends StatefulWidget {
|
|||||||
this.sfxVolume = 0.8,
|
this.sfxVolume = 0.8,
|
||||||
this.onBgmVolumeChange,
|
this.onBgmVolumeChange,
|
||||||
this.onSfxVolumeChange,
|
this.onSfxVolumeChange,
|
||||||
|
this.onShowStatistics,
|
||||||
|
this.onShowHelp,
|
||||||
});
|
});
|
||||||
|
|
||||||
final GameState state;
|
final GameState state;
|
||||||
@@ -72,6 +74,12 @@ class MobileCarouselLayout extends StatefulWidget {
|
|||||||
/// SFX 볼륨 변경 콜백
|
/// SFX 볼륨 변경 콜백
|
||||||
final void Function(double volume)? onSfxVolumeChange;
|
final void Function(double volume)? onSfxVolumeChange;
|
||||||
|
|
||||||
|
/// 통계 표시 콜백
|
||||||
|
final VoidCallback? onShowStatistics;
|
||||||
|
|
||||||
|
/// 도움말 표시 콜백
|
||||||
|
final VoidCallback? onShowHelp;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<MobileCarouselLayout> createState() => _MobileCarouselLayoutState();
|
State<MobileCarouselLayout> createState() => _MobileCarouselLayoutState();
|
||||||
}
|
}
|
||||||
@@ -349,7 +357,12 @@ class _MobileCarouselLayoutState extends State<MobileCarouselLayout> {
|
|||||||
|
|
||||||
showModalBottomSheet<void>(
|
showModalBottomSheet<void>(
|
||||||
context: context,
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxHeight: MediaQuery.of(context).size.height * 0.7,
|
||||||
|
),
|
||||||
builder: (context) => SafeArea(
|
builder: (context) => SafeArea(
|
||||||
|
child: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@@ -410,6 +423,32 @@ class _MobileCarouselLayoutState extends State<MobileCarouselLayout> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
|
const Divider(),
|
||||||
|
|
||||||
|
// 통계
|
||||||
|
if (widget.onShowStatistics != null)
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.bar_chart, color: Colors.blue),
|
||||||
|
title: Text(l10n.uiStatistics),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
widget.onShowStatistics?.call();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
// 도움말
|
||||||
|
if (widget.onShowHelp != null)
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.help_outline, color: Colors.green),
|
||||||
|
title: Text(l10n.uiHelp),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
widget.onShowHelp?.call();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
const Divider(),
|
||||||
|
|
||||||
// 언어 변경
|
// 언어 변경
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.language, color: Colors.teal),
|
leading: const Icon(Icons.language, color: Colors.teal),
|
||||||
@@ -507,6 +546,7 @@ class _MobileCarouselLayoutState extends State<MobileCarouselLayout> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user