목차
멋쟁이사자처럼 로켓단 인턴쉽
25.09.01
회의록
>> 안건
- 업무 업데이트
- 질문 혹은 피드백 요청
- 주요 안건
- 앱 출시 작업 전 간단한 QA 및 밸런싱 논의
- 향후 마일스톤
- 앱 출시 및 안정화 작업
>> 회의 내용
- 퇴근하기 버튼을 누른 뒤 페이드 아웃 연출이 나오는 동안 버튼 클릭 모션이 나오지 않게
- 결과창 UI 비율 재정렬
- 결과창 숫자 카운팅 사운드 재할당 (소스찾기)
- 숫자 올라갈 때 효과음
- 카운팅 끝나고 완료 효과음
- 결과창 로드 후 아래에서 돈다발 터지는 효과
- 게임 오버(일과 종료 패널) 효과음
- 4차 밸런싱 회의
: 초반부 피버타임 때 증감 체감이 적은 부분을 보완하기위해 피버전용 입력딜레이 추가.
- 서류 연출 딜레이 시간 조정 : (기존) 항상 0.6/0.5/0.4/0.3/0.2 --> (변경) 피버 때만 0.4/0.35/0.3/0.25/0.2
- 추후 5차 밸런싱 필요
- 3,4,5 단계 밸런스조정 필요해보입니다. 2,3,4단계에 걸쳐서 시간이 너무 많이 모입니다. =>컴퓨터로 대충해도 140초 이상 모입니다.
- 5단계에서 시간이 줄어야하는데 현재 피버시에 클릭 잘하면 시간이 원상복구됩니다.(컴퓨터기준)
- 모바일로 하면 시간을 더 많이 채울걸로 예상됩니다. =>모바일 테스트 후 조정 필요
--> 2,3,4단계 조정 필요
--> 5단계에서 일과시간 비축량이 너무 늘어나는 상황이 아니라 잃은 시간을 한 턴 정도 복구하는 정도에 그친다면 5단계는 문제가 없음.
결과창 UI 재정렬
: 지난 시간에 재시작 버튼을 추가하면서 기존에 세팅해놓은 UI 위치가 조금 틀어진 것 같다. 결과창에 있는 글자들이 전체적으로 살짝 위로 올라간 느낌이라 다시 정렬하였다.
--> Vertical Layout Group의 Padding 중 Top 값과 Spacing을 조정해서 수정
게임 오버 효과음 추가
: 게임 오버시, 효과음이 나왔으면 좋겠다는 팀원들의 의견에 따라 효과음을 찾아보았다.
--> 호루라기 소리로 무료 SFX 중에 하나 찾아서 게임에 적용했다.
>> 찾은 효과음
https://pixabay.com/ko/sound-effects/whistle-84607/
>> 적용
: 만든 AudioManager를 통해 효과음을 적용해줄 함수를 작성하고, 함수에 찾은 SFX를 바인딩
└ 최종 코드
>> SFXController.cs
: SerializeField로 gameOver 사운드를 받아서 이를 재생해주는 public 함수를 작성
<hide/>
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class SFXController : MonoBehaviour
{
// SFX를 추가하실 때 여기에 추가해주세요.
[Header("Common")]
[SerializeField] private AudioClip buttonClick;
[Header("InGame")]
[SerializeField] private AudioClip stamp;
[SerializeField] private AudioClip docSuccess;
[SerializeField] private AudioClip docFail;
[SerializeField] private AudioClip docSwap;
[SerializeField] private AudioClip obsBugPostHit;
[SerializeField] private AudioClip obsProcessTry;
[SerializeField] private AudioClip obsHandHit;
[SerializeField] private AudioClip obsFileEnvelopeOut;
[SerializeField] private AudioClip newRecordResult;
[SerializeField] private AudioClip newRecordScoreBar;
[SerializeField] private AudioClip speedUp;
[SerializeField] private AudioClip fever;
[SerializeField] private AudioClip timeOutAlert;
[SerializeField] private AudioClip gameStart;
[SerializeField] private AudioClip scoreCalculating;
[SerializeField] private AudioClip scoreCalculated;
[SerializeField] private AudioClip gameOver;
// 여기까지
private List<AudioSource> _sfxSources; // 단발성 AudioSource (풀링)
private int _poolSize = 20; // 단발성 AudioSource 풀의 개수
private Dictionary<AudioClip, List<AudioSource>> _activeSFX; // 개별 단발 SFX 추적
private Dictionary<AudioClip, AudioSource> _loopSources; // 반복용 AudioSource
private bool _isSFXOn = true; // SFX가 켜져있는지 여부
public bool GetIsSFXOn() => _isSFXOn;
private void Awake()
{
if (AudioManager.Instance != null)
AudioManager.Instance.SetSFXController(this);
SceneManager.sceneLoaded += OnSceneLoaded;
// AudioSource 초기화
_sfxSources = new List<AudioSource>();
_loopSources = new Dictionary<AudioClip, AudioSource>();
_activeSFX = new Dictionary<AudioClip, List<AudioSource>>();
// 풀 초기화
for (int i = 0; i < _poolSize; i++)
{
var src = gameObject.AddComponent<AudioSource>();
src.playOnAwake = false;
_sfxSources.Add(src);
}
}
private void OnSceneLoaded(Scene scene, LoadSceneMode loadSceneMode)
{
}
// SFX를 추가하신 뒤, 아래 함수 모음에 재생 함수를 작성해주세요. 그리고 작성하신 함수를 통해 사용하시면 됩니다.
// 1번 재생 : PlaySFX()
// 반복 재생 : PlayLoopSFX()
// 반복 재생 중지 : StopLoopSFX()
#region PlaySFX 함수 모음
public void PlayButtonClick() => PlaySFX(buttonClick);
public void PlayStamp() => PlaySFX(stamp);
public void PlayDocSuccess() => PlaySFX(docSuccess, 0.6f);
public void PlayDocFail() => PlaySFX(docFail, 0.6f);
public void PlayDocSwap() => PlaySFX(docSwap);
public void PlayObsBugPostHit() => PlaySFX(obsBugPostHit);
public void PlayObsProcessTry() => PlaySFX(obsProcessTry);
public void PlayObsHandHit() => PlaySFX(obsHandHit);
public void PlayObsFileEnvelopeOut() => PlaySFX(obsFileEnvelopeOut);
public void PlaySpeedUp() => PlaySFX(speedUp);
public void PlayFever() => PlaySFX(fever);
public void PlayTimeOutAlert() => PlayLoopSFX(timeOutAlert);
public void StopTimeOutAlert() => StopLoopSFX(timeOutAlert);
public void PlayNewRecordResult() => PlaySFX(newRecordResult);
public void PlayNewRecordScoreBar() => PlaySFX(newRecordScoreBar);
public void PlayGameStart() => PlaySFX(gameStart);
public void PlayScoreCalculating() => PlaySFX(scoreCalculating);
public void StopScoreCalculating() => StopSFX(scoreCalculating);
public void PlayScoreCalculated() => PlaySFX(scoreCalculated);
public void PlayGameOver() => PlaySFX(gameOver);
#endregion
// 단발 SFX 재생 (풀링 + 추적)
private void PlaySFX(AudioClip clip, float volume = 1f)
{
if (!_isSFXOn || clip == null) return;
AudioSource src = GetAvailableSource();
src.clip = clip;
src.volume = volume;
src.mute = !_isSFXOn;
src.Play();
// 개별 SFX 추적
if (!_activeSFX.ContainsKey(clip))
_activeSFX[clip] = new List<AudioSource>();
_activeSFX[clip].Add(src);
// 재생 완료 후 제거
StartCoroutine(RemoveAfterPlay(src, clip));
}
private System.Collections.IEnumerator RemoveAfterPlay(AudioSource src, AudioClip clip)
{
yield return new WaitWhile(() => src.isPlaying);
if (_activeSFX.ContainsKey(clip))
_activeSFX[clip].Remove(src);
}
// 사용 가능한 AudioSource 가져오기 (풀링)
private AudioSource GetAvailableSource()
{
foreach (var src in _sfxSources)
{
if (!src.isPlaying)
return src;
}
// 모두 사용 중이면 새로 생성
var newSrc = gameObject.AddComponent<AudioSource>();
newSrc.playOnAwake = false;
_sfxSources.Add(newSrc);
return newSrc;
}
// 특정 단발 SFX 강제 정지
public void StopSFX(AudioClip clip)
{
if (!_activeSFX.ContainsKey(clip)) return;
foreach (var src in _activeSFX[clip])
{
if (src != null && src.isPlaying)
src.Stop();
}
_activeSFX[clip].Clear();
}
// 모든 단발 SFX 강제 정지
public void StopAllSFX()
{
foreach (var kv in _activeSFX)
{
foreach (var src in kv.Value)
{
if (src != null && src.isPlaying)
src.Stop();
}
kv.Value.Clear();
}
}
// SFX 반복 재생
private void PlayLoopSFX(AudioClip clip)
{
if (clip == null) return;
if (_loopSources.ContainsKey(clip)) return; // 이미 재생 중이면 패스
var src = gameObject.AddComponent<AudioSource>();
src.playOnAwake = false;
src.loop = true;
src.clip = clip;
if (!_isSFXOn) src.mute = true;
src.Play();
_loopSources[clip] = src;
}
// SFX 반복 재생 중지
private void StopLoopSFX(AudioClip clip)
{
if (clip == null || !_loopSources.ContainsKey(clip)) return;
var src = _loopSources[clip];
if (src != null && src.isPlaying)
{
src.Stop();
Destroy(src);
}
_loopSources.Remove(clip);
}
// _isSFXOn 조정
public void SetSFXOn(bool isSFXOn)
{
_isSFXOn = isSFXOn;
_isSFXOn = isSFXOn;
// 단발
foreach (var src in _sfxSources)
{
if (src != null) src.mute = !_isSFXOn;
}
// 루프
foreach (var key in _loopSources)
{
if (key.Value != null) key.Value.mute = !_isSFXOn;
}
}
}
>> InGameController.cs
: EndGame()에서 결과창이 호출될 때 효과음 재생코드 작성
<hide/>
using System.Collections;
using UnityEngine;
using Object = UnityEngine.Object;
//인게임 주요 로직들을 제어합니다.
public class InGameController
{
public TimeController timeController;
public DocumentController docController;
public Classification classification;
public bool Initialized;
private bool _initComplete;
private bool _gameStarted;
private bool _gameFinished;
private bool _quitGame;
private bool _skipResultUI;
private bool _useRetry;
private bool _newRecordOn;
private bool _endAdmob;
public IEnumerator Initialize()
{
//TODO: 게임 실행시 초기화 할 로직
//classification = new Classification();
//씬 내 타이머, 문서생성 오브젝트 찾기
if (timeController == null)
{
yield return new WaitUntil(() => timeController = Object.FindObjectOfType<TimeController>());
}
if (docController == null)
{
yield return new WaitUntil(() => docController = Object.FindObjectOfType<DocumentController>());
}
if(classification == null)
{
yield return new WaitUntil(() => classification = Object.FindObjectOfType<Classification>());
classification.Initialize();
}
Initialized = true;
}
//게임이 시작되면 이 시퀀스를 통해 진행됩니다.
public IEnumerator RunSequence()
{
//실행 필수 초기화 진행 체크
yield return new WaitUntil(() => Initialized);
//게임 시퀀스 실행
yield return new WaitUntil(() => _initComplete);
yield return StartGame();
yield return new WaitUntil(() => _gameFinished);
yield return EndGame();
}
//게임 시작 전 초기화
public IEnumerator SetInitGame()
{
//실행 필수 초기화 완료전 까지 대기
yield return new WaitUntil(() => Initialized);
//TODO: 게임 시작시 초기화 할 로직
_initComplete = false;
_gameStarted = false;
_gameFinished = false;
_quitGame = false;
_skipResultUI = false;
_useRetry = false;
_newRecordOn = false;
_endAdmob = false;
// BGM 초기화
AudioManager.Instance.BGM.SetBGMVolumeMax(); // 볼륨을 최대로
AudioManager.Instance.BGM.SetBGMSpeedNormal(); // 배속을 기본으로
//타이머 초기화
timeController.InitTimeController();
//서류 풀 초기화
docController.ReloadDocument(true);
//classification.InitScore();
GameManager.Instance.GetClassification().InitScore();
_initComplete = true;
// NewRecord 이미지 위치 초기화
InGameUIController.Instance.scoreUIController.InitNewRecordImage();
// 시계 프레임 색상 초기화
InGameUIController.Instance.clockUIController.InitClockFrameColor();
}
//게임 시작
public IEnumerator StartGame()
{
_gameStarted = true;
//인게임UI 보이기
UIManager.Instance.inGameUIController.ShowTimeUI();
UIManager.Instance.inGameUIController.ShowInteractionUI();
UIManager.Instance.inGameUIController.ShowScoreUI();
UIManager.Instance.inGameUIController.ShowFeverUI();
UIManager.Instance.inGameUIController.ShowBackgroundUI();
UIManager.Instance.inGameUIController.ShowClockUI();
UIManager.Instance.inGameUIController.ShowClassificationUI();
UIManager.Instance.inGameUIController.ShowWaitThreeSecondsUI();
UIManager.Instance.inGameUIController.ShowDifficultyUpEffectUI();
// 인게임 BGM 재생
AudioManager.Instance.BGM.PlayBGMByState(GameManager.Instance.GetGameState());
//321
yield return UIManager.Instance.StartCoroutine(
UIManager.Instance.inGameUIController.waitThreeSecondsUI.WaitThreeSeconds()
);
//난이도 상승시점 모니터링 시작
DifficultyManager.Instance.InitLevelMonitor();
//타이머, 문서 생성 시작.
timeController.StartRunningTimer();
docController.InitDocuments();
while (!_gameFinished)
{
yield return null;
}
}
//게임 종료. 결과 보고
public IEnumerator EndGame()
{
//TODO: 게임 끝낼 시 실행할 로직
//시간 정지
timeController.StopTime();
docController._isClickable = false;
// 시간 부족 SFX 반복 재생 중지
AudioManager.Instance.SFX.StopTimeOutAlert();
//ex.게임오버 연출, 결과창UI등
var popupController = UIManager.Instance.popupUIController;
if (!_useRetry)//재시작 활성화 시 엔드연출 스킵
{
//게임오버
var gameOverUI = popupController.gameOverUIController;
AudioManager.Instance.SFX.PlayGameOver();
yield return gameOverUI.ShowSequence(); //게임오버 연출동안 딜레이
}
if (!_skipResultUI)//필요 시 스킵
{
//결과창
var resultUI = popupController.resultUIController;
popupController.ShowResultUI();
resultUI.InitResultItem(new GameResultData(
timeController._day,
GameManager.Instance.GetClassification().GetMaxCombo(),
GameManager.Instance.GetClassification().GetScore()));
}
//게임이 끝난 후, 바로 돌아가지 않고 대기.
while (!_quitGame)
{
yield return null;
}
//결과창이 떠야 게임 한판을 완료했다는 것이므로
if (!_skipResultUI)
{
//광고호출
NetworkManager.Instance.ShowAd();
//광고 끝나기 전까지 대기
while (!_endAdmob)
{
yield return null;
}
//광고 제어 변수 초기화
_endAdmob = false;
//다음 광고 로드
NetworkManager.Instance.LoadAd();
}
//초기화.
_initComplete = false;
_skipResultUI = false;
//재시작 필요 시.
if (_useRetry)
{
//새 게임 코루틴 활성화
yield return GameManager.Instance.StartCoroutine(SetInitGame());
GameManager.Instance.StartCoroutine(RunSequence());
//재시작을 위해 타이틀로 복귀하지 않고 기존 코루틴을 중단한다.
yield break;
}
// BGM 초기화
AudioManager.Instance.BGM.SetBGMVolumeMax(); // 볼륨을 최대로
AudioManager.Instance.BGM.SetBGMSpeedNormal(); // 배속을 기본으로
//인게임 UI 닫기
UIManager.Instance.inGameUIController.HideInGameUI();
UIManager.Instance.inGameUIController.HideTimeUI();
UIManager.Instance.inGameUIController.HideInteractionUI();
UIManager.Instance.inGameUIController.HideScoreUI();
UIManager.Instance.inGameUIController.HideFeverUI();
UIManager.Instance.inGameUIController.HideBackgroundUI();
UIManager.Instance.inGameUIController.HideClockUI();
UIManager.Instance.inGameUIController.HideClassificationUI();
UIManager.Instance.inGameUIController.HideWaitThreeSecondsUI();
UIManager.Instance.inGameUIController.HideDifficultyUpEffectUI();
//타이틀 씬으로 복귀
GameManager.Instance.ReturnToTitle();
}
// New Record 체크 및 연출 재생
public void CheckNewRecord(float currentScore)
{
if (_newRecordOn) return; // 이미 NewRecord 연출이 재생됐으면 넘기도록
float bestScore = PlayerPrefs.GetFloat("BestScore", 0f);
if (currentScore > bestScore)
{
// New Record 연출
InGameUIController.Instance.scoreUIController.ShowNewRecordImage();
AudioManager.Instance.SFX.PlayNewRecordScoreBar();
_newRecordOn = true;
}
}
///게임 끝내기, 호출 시 진행중인 게임이 끝납니다.
public void Dispose()
{
//게임이 시작했을때만
if(_gameStarted) _gameFinished = true;
}
///게임 끝난 후, 호출 시 타이틀로 돌아갑니다.
public void QuitGame()
{
if(_gameFinished) _quitGame = true;
}
///결과 UI 스킵필요 시 게임 종료 전 호출
public void SkipResultUI()
{
_skipResultUI = true;
}
//재시작 필요 시 먼저 호출.
public void UseRetry()
{
_useRetry = true;
}
public bool GetGameStarted()
{
return _gameStarted;
}
public void EndAdMob()
{
_endAdmob = true;
}
}
'Development > Internship' 카테고리의 다른 글
[멋사 로켓단 인턴쉽] 20일차 - 스토어 출시 전 점검 (0) | 2025.09.03 |
---|---|
[멋사 로켓단 인턴쉽] 19일차 - QA 및 밸런스 조정 (0) | 2025.09.02 |
[멋사 로켓단 인턴쉽] 17일차 - 게임 완성도 올리기 (1) | 2025.08.29 |
[멋사 로켓단 인턴쉽] 16일차 - 밸런싱 및 기타 작업 (2) | 2025.08.28 |
[멋사 로켓단 인턴쉽] 15일차 - NewRecord를 PlayerPrefs와 연동 (2) | 2025.08.27 |