fix(audio): 모바일 백그라운드 시 오디오 완전 정지
- AudioService: pauseAll()/resumeAll() 메서드 추가 - _isPaused 플래그로 백그라운드 시 새로운 재생 요청 차단 - playBgm/playSfx에서 일시정지 상태 체크 - game_play_screen: pauseAll() 사용으로 BGM+SFX 동시 정지 - 포그라운드 복귀 시 resumeAll() 호출 후 화면 재로드
This commit is contained in:
@@ -39,6 +39,9 @@ class AudioService {
|
|||||||
// 사용자 상호작용 발생 여부 (웹 자동재생 정책 우회용)
|
// 사용자 상호작용 발생 여부 (웹 자동재생 정책 우회용)
|
||||||
bool _userInteracted = false;
|
bool _userInteracted = false;
|
||||||
|
|
||||||
|
// 오디오 일시정지 상태 (앱 백그라운드 시)
|
||||||
|
bool _isPaused = false;
|
||||||
|
|
||||||
/// 서비스 초기화
|
/// 서비스 초기화
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
if (_initialized || _initFailed) return;
|
if (_initialized || _initFailed) return;
|
||||||
@@ -83,6 +86,7 @@ class AudioService {
|
|||||||
/// 다음 SFX 재생 시 함께 시작됩니다.
|
/// 다음 SFX 재생 시 함께 시작됩니다.
|
||||||
Future<void> playBgm(String name) async {
|
Future<void> playBgm(String name) async {
|
||||||
if (_initFailed) return; // 초기화 실패 시 무시
|
if (_initFailed) return; // 초기화 실패 시 무시
|
||||||
|
if (_isPaused) return; // 일시정지 상태면 무시
|
||||||
if (!_initialized) await init();
|
if (!_initialized) await init();
|
||||||
if (_initFailed || !_initialized) return;
|
if (_initFailed || !_initialized) return;
|
||||||
if (_currentBgm == name) return; // 이미 재생 중
|
if (_currentBgm == name) return; // 이미 재생 중
|
||||||
@@ -128,6 +132,33 @@ class AudioService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 전체 오디오 일시정지 (앱 백그라운드 시)
|
||||||
|
///
|
||||||
|
/// BGM을 정지하고, 새로운 재생 요청을 무시합니다.
|
||||||
|
Future<void> pauseAll() async {
|
||||||
|
_isPaused = true;
|
||||||
|
if (!_initialized) return;
|
||||||
|
|
||||||
|
// BGM 정지 및 상태 초기화
|
||||||
|
await _bgmPlayer?.stop();
|
||||||
|
_currentBgm = null;
|
||||||
|
|
||||||
|
// 모든 SFX 정지
|
||||||
|
for (final player in _sfxPlayers) {
|
||||||
|
await player.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint('[AudioService] All audio paused');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 전체 오디오 재개 (앱 포그라운드 복귀 시)
|
||||||
|
///
|
||||||
|
/// 일시정지 상태를 해제하고 이전 BGM을 재개합니다.
|
||||||
|
Future<void> resumeAll() async {
|
||||||
|
_isPaused = false;
|
||||||
|
debugPrint('[AudioService] Audio resumed');
|
||||||
|
}
|
||||||
|
|
||||||
/// SFX 재생
|
/// SFX 재생
|
||||||
///
|
///
|
||||||
/// [name]은 assets/audio/sfx/ 폴더 내 파일명 (확장자 제외)
|
/// [name]은 assets/audio/sfx/ 폴더 내 파일명 (확장자 제외)
|
||||||
@@ -136,6 +167,7 @@ class AudioService {
|
|||||||
/// 웹에서 대기 중인 BGM이 있으면 함께 재생 시작합니다.
|
/// 웹에서 대기 중인 BGM이 있으면 함께 재생 시작합니다.
|
||||||
Future<void> playSfx(String name) async {
|
Future<void> playSfx(String name) async {
|
||||||
if (_initFailed) return; // 초기화 실패 시 무시
|
if (_initFailed) return; // 초기화 실패 시 무시
|
||||||
|
if (_isPaused) return; // 일시정지 상태면 무시
|
||||||
if (!_initialized) await init();
|
if (!_initialized) await init();
|
||||||
if (_initFailed || !_initialized) return;
|
if (_initFailed || !_initialized) return;
|
||||||
if (_sfxVolume == 0) return; // 볼륨이 0이면 재생 안함
|
if (_sfxVolume == 0) return; // 볼륨이 0이면 재생 안함
|
||||||
|
|||||||
@@ -96,8 +96,8 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
// 전투 이벤트 추적 (마지막 처리된 이벤트 수)
|
// 전투 이벤트 추적 (마지막 처리된 이벤트 수)
|
||||||
int _lastProcessedEventCount = 0;
|
int _lastProcessedEventCount = 0;
|
||||||
|
|
||||||
// 오디오 상태 추적
|
// 오디오 상태 추적 (TaskType 기반)
|
||||||
bool _wasInCombat = false;
|
bool _wasInBattleTask = false;
|
||||||
|
|
||||||
// 사운드 볼륨 상태 (모바일 설정 UI용)
|
// 사운드 볼륨 상태 (모바일 설정 UI용)
|
||||||
double _bgmVolume = 0.7;
|
double _bgmVolume = 0.7;
|
||||||
@@ -116,8 +116,8 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
// 전투 이벤트 처리 (Combat Events)
|
// 전투 이벤트 처리 (Combat Events)
|
||||||
_processCombatEvents(state);
|
_processCombatEvents(state);
|
||||||
|
|
||||||
// 오디오: 전투 상태 변경 시 BGM 전환
|
// 오디오: TaskType 변경 시 BGM 전환 (애니메이션과 동기화)
|
||||||
_updateBgmForCombatState(state);
|
_updateBgmForTaskType(state);
|
||||||
|
|
||||||
// 레벨업 감지
|
// 레벨업 감지
|
||||||
if (state.traits.level > _lastLevel && _lastLevel > 0) {
|
if (state.traits.level > _lastLevel && _lastLevel > 0) {
|
||||||
@@ -222,15 +222,17 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 초기 BGM 재생 (게임 시작/로드 시)
|
/// 초기 BGM 재생 (게임 시작/로드 시)
|
||||||
|
///
|
||||||
|
/// TaskType 기반으로 BGM 결정 (애니메이션과 동기화)
|
||||||
void _playInitialBgm(GameState state) {
|
void _playInitialBgm(GameState state) {
|
||||||
final audio = widget.audioService;
|
final audio = widget.audioService;
|
||||||
if (audio == null) return;
|
if (audio == null) return;
|
||||||
|
|
||||||
final combat = state.progress.currentCombat;
|
final taskType = state.progress.currentTask.type;
|
||||||
final isInCombat = combat != null && combat.isActive;
|
final isInBattleTask = taskType == TaskType.kill;
|
||||||
|
|
||||||
if (isInCombat) {
|
if (isInBattleTask) {
|
||||||
// 전투 중: 보스 여부에 따라 BGM 선택
|
// 전투 태스크: 보스 여부에 따라 BGM 선택
|
||||||
final monsterLevel = state.progress.currentTask.monsterLevel ?? 0;
|
final monsterLevel = state.progress.currentTask.monsterLevel ?? 0;
|
||||||
final playerLevel = state.traits.level;
|
final playerLevel = state.traits.level;
|
||||||
final isBoss = monsterLevel >= playerLevel + 5;
|
final isBoss = monsterLevel >= playerLevel + 5;
|
||||||
@@ -241,22 +243,25 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
audio.playBgm('battle');
|
audio.playBgm('battle');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 비전투: 마을 BGM
|
// 비전투 태스크: 마을 BGM
|
||||||
audio.playBgm('town');
|
audio.playBgm('town');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_wasInBattleTask = isInBattleTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 전투 상태에 따른 BGM 전환
|
/// TaskType 기반 BGM 전환 (애니메이션과 동기화)
|
||||||
void _updateBgmForCombatState(GameState state) {
|
///
|
||||||
|
/// 애니메이션은 TaskType으로 결정되므로, BGM도 동일한 기준 사용
|
||||||
|
void _updateBgmForTaskType(GameState state) {
|
||||||
final audio = widget.audioService;
|
final audio = widget.audioService;
|
||||||
if (audio == null) return;
|
if (audio == null) return;
|
||||||
|
|
||||||
final combat = state.progress.currentCombat;
|
final taskType = state.progress.currentTask.type;
|
||||||
final isInCombat = combat != null && combat.isActive;
|
final isInBattleTask = taskType == TaskType.kill;
|
||||||
|
|
||||||
if (isInCombat && !_wasInCombat) {
|
if (isInBattleTask && !_wasInBattleTask) {
|
||||||
// 전투 시작: 보스 여부에 따라 BGM 선택
|
// 전투 태스크 시작: 보스 여부에 따라 BGM 선택
|
||||||
// 몬스터 레벨이 플레이어보다 5 이상 높으면 보스로 간주
|
|
||||||
final monsterLevel = state.progress.currentTask.monsterLevel ?? 0;
|
final monsterLevel = state.progress.currentTask.monsterLevel ?? 0;
|
||||||
final playerLevel = state.traits.level;
|
final playerLevel = state.traits.level;
|
||||||
final isBoss = monsterLevel >= playerLevel + 5;
|
final isBoss = monsterLevel >= playerLevel + 5;
|
||||||
@@ -266,12 +271,12 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
} else {
|
} else {
|
||||||
audio.playBgm('battle');
|
audio.playBgm('battle');
|
||||||
}
|
}
|
||||||
} else if (!isInCombat && _wasInCombat) {
|
} else if (!isInBattleTask && _wasInBattleTask) {
|
||||||
// 전투 종료: 마을 BGM으로 복귀
|
// 전투 태스크 종료: 마을 BGM으로 복귀
|
||||||
audio.playBgm('town');
|
audio.playBgm('town');
|
||||||
}
|
}
|
||||||
|
|
||||||
_wasInCombat = isInCombat;
|
_wasInBattleTask = isInBattleTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 전투 이벤트에 따른 SFX 재생
|
/// 전투 이벤트에 따른 SFX 재생
|
||||||
@@ -475,11 +480,7 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
_lastPlotStageCount = state.progress.plotStageCount;
|
_lastPlotStageCount = state.progress.plotStageCount;
|
||||||
_lastAct = getActForLevel(state.traits.level);
|
_lastAct = getActForLevel(state.traits.level);
|
||||||
|
|
||||||
// 초기 전투 상태 확인 및 BGM 설정
|
// 초기 BGM 재생 (TaskType 기반, _wasInBattleTask도 함께 설정)
|
||||||
final combat = state.progress.currentCombat;
|
|
||||||
_wasInCombat = combat != null && combat.isActive;
|
|
||||||
|
|
||||||
// 초기 BGM 재생 (전투 상태에 따라)
|
|
||||||
_playInitialBgm(state);
|
_playInitialBgm(state);
|
||||||
} else {
|
} else {
|
||||||
// 상태가 없으면 기본 마을 BGM
|
// 상태가 없으면 기본 마을 BGM
|
||||||
@@ -528,15 +529,16 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
// 저장
|
// 저장
|
||||||
_saveGameState();
|
_saveGameState();
|
||||||
|
|
||||||
// 모바일: 게임 일시정지 + 사운드 정지
|
// 모바일: 게임 일시정지 + 전체 오디오 정지
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
widget.controller.pause(saveOnStop: false);
|
widget.controller.pause(saveOnStop: false);
|
||||||
widget.audioService?.stopBgm();
|
widget.audioService?.pauseAll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 모바일: 앱이 포그라운드로 돌아올 때 전체 재로드
|
// 모바일: 앱이 포그라운드로 돌아올 때 전체 재로드
|
||||||
if (appState == AppLifecycleState.resumed && isMobile) {
|
if (appState == AppLifecycleState.resumed && isMobile) {
|
||||||
|
widget.audioService?.resumeAll();
|
||||||
_reloadGameScreen();
|
_reloadGameScreen();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user