Galaxy Ball/2. 싱글플레이 - 스토리모드

(38) 오디오 제어 & 최종보스 제작

잉ㅇ잉ㅇ 2024. 7. 30. 04:04

오늘은 환경설정으로 오디오가 제어되도록 해보겠다

 

사실 하면서 복잡하고 생각보다 잘 안풀리길래 그냥 환경설정 자체를 빼버릴까도 고민이 많았지만...

그래도 한번 해보자!

 

 

예전에 만들어놓은 환경설정창이다. 하지만 아직 BGM, Sound Effent On/Off 기능이 구현되어 있지 않다

using System;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;

public class SettingButtonManager : MonoBehaviour
{
    public Button BGM_ON;
    public Button BGM_OFF;
    public Button Sound_Effect_ON;
    public Button Sound_Effect_OFF;
    public Button Credit;
    public Button Back;
    public AudioSource ButtonAudio;

    private BGMControl bGMControl;

    private void Start()
    {
        bGMControl = FindObjectOfType<BGMControl>();
        LoadButtonStates();

        SetupButton(BGM_ON, BGM_OFF, () => {
            bGMControl.BGMSwitch = false;
            bGMControl.SaveAudioSettings(); // 상태 저장
        });
        SetupButton(BGM_OFF, BGM_ON, () => {
            bGMControl.BGMSwitch = true;
            bGMControl.SaveAudioSettings(); // 상태 저장
        });
        SetupButton(Sound_Effect_ON, Sound_Effect_OFF, () => {
            bGMControl.SoundEffectSwitch = false;
            bGMControl.SaveAudioSettings(); // 상태 저장
        });
        SetupButton(Sound_Effect_OFF, Sound_Effect_ON, () => {
            bGMControl.SoundEffectSwitch = true;
            bGMControl.SaveAudioSettings(); // 상태 저장
        });

        Credit.onClick.AddListener(() =>
        {
            ButtonAudio.Play();
            SceneManager.LoadScene("Credit Scene");
        });

        Back.onClick.AddListener(() =>
        {
            ButtonAudio.Play();
            SceneManager.LoadScene("Start Scene");
        });
    }

    private void SetupButton(Button onButton, Button offButton, Action action)
    {
        onButton.onClick.AddListener(() =>
        {
            action();
            ButtonAudio.Play();
            onButton.gameObject.SetActive(false);
            offButton.gameObject.SetActive(true);
            SaveButtonStates();
        });
    }

    private void SaveButtonStates()
    {
        PlayerPrefs.SetInt("BGM_ON", BGM_ON.gameObject.activeSelf ? 1 : 0);
        PlayerPrefs.SetInt("BGM_OFF", BGM_OFF.gameObject.activeSelf ? 1 : 0);
        PlayerPrefs.SetInt("Sound_Effect_ON", Sound_Effect_ON.gameObject.activeSelf ? 1 : 0);
        PlayerPrefs.SetInt("Sound_Effect_OFF", Sound_Effect_OFF.gameObject.activeSelf ? 1 : 0);
        PlayerPrefs.Save();
    }

    private void LoadButtonStates()
    {
        BGM_ON.gameObject.SetActive(PlayerPrefs.GetInt("BGM_ON", 1) == 1);
        BGM_OFF.gameObject.SetActive(PlayerPrefs.GetInt("BGM_OFF", 0) == 1);
        Sound_Effect_ON.gameObject.SetActive(PlayerPrefs.GetInt("Sound_Effect_ON", 1) == 1);
        Sound_Effect_OFF.gameObject.SetActive(PlayerPrefs.GetInt("Sound_Effect_OFF", 0) == 1);
    }
}

 

우선 위 사진에 보이는 버튼들을 컨트롤하는 스크립트이다. 

 

On/Off 버튼을 누를때마다 BgmControl 스크립트에 있는 스위치에 값을 주는것을 확인할 수 있다

 

using UnityEngine.SceneManagement;
using UnityEngine;

public class BGMControl : MonoBehaviour
{
    public AudioSource[] SoundEffect;
    public bool BGMSwitch = true;
    public bool SoundEffectSwitch = true;

    private void Awake()
    {
        // 게임 시작 시 저장된 상태를 불러오기
        LoadAudioSettings();
    }

    void Update()
    {
        UpdateAudioSources();
    }

    void UpdateAudioSources()
    {
        ToggleAudioSources(SoundEffect, SoundEffectSwitch);
    }

    void ToggleAudioSources(AudioSource[] sources, bool isEnabled)
    {
        foreach (var source in sources)
        {
            if (!isEnabled && source.isPlaying)
            {
                source.Stop();
            }
            source.enabled = isEnabled;
        }
    }
    private void StopAllAudio(AudioSource[] sources)
    {
        foreach (var source in sources)
        {
            if (source.isPlaying)
            {
                source.Stop();
            }
        }
    }

    // 게임 시작 시 저장된 상태를 불러오기
    private void LoadAudioSettings()
    {
        BGMSwitch = PlayerPrefs.GetInt("BGMSwitch", 1) == 1; // 기본값은 true
        SoundEffectSwitch = PlayerPrefs.GetInt("SoundEffectSwitch", 1) == 1; // 기본값은 true
    }

    // 상태가 변경될 때 저장하기
    public void SaveAudioSettings()
    {
        PlayerPrefs.SetInt("BGMSwitch", BGMSwitch ? 1 : 0);
        PlayerPrefs.SetInt("SoundEffectSwitch", SoundEffectSwitch ? 1 : 0);
        PlayerPrefs.Save();
    }

    // 새로운 메서드 추가
    public void SoundEffectPlay(int index)
    {
        if (index >= 0 && index < SoundEffect.Length)
        {
            SoundEffect[index].Play();
        }
        else
        {
            Debug.LogWarning("SoundEffectPlay: Index out of range.");
        }
    }
}

 

그리고 이게 BGMControl이다. 어느 씬으로 이동하던 스위치 on/off 여부는 저장되어야 하고

한가지 더 추가해 이 여부는 게임을 종료해도 저장되어 있어야 한다

 

 

그렇기 때문에 어느씬에 가던 삭제되지 않는 Start 씬의 GameManager에 BGM Control을 넣어준것이다 

이제 SoundEffect 배열에 오디오소스 5개를 넣어준다

 

이 5개의 소스들은 게임에 사용될 총 5가지의 사운드이펙트이다

그래서 사운드 이펙트가 재생될 일이 생길때마다 bgmControl.SoundEffectPlay에서 골라와 사운드를 재생하는것이다

 

물론 BGMControl에 있는 SoundEffectPlay 메서드는 스위치가 켜져있을 때만 재생이 될것이다

위 사진에 보이는 코드처럼 스위치가 false라면 곧바로 사운드 재생이 정지되도록 걸어놨기 때문

 

이 과정을 거치면 환경설정에서 설정한것에 따라 사운드가 재생되기도, 묵음처리가 되기도 한다

 

BGM 재생에 관해서는 IsPlayBGM이라는 스크립트를 하나 만들어 인게임씬에 넣어주었다

배열에는 총 3개의 오디오소스가 들어가있는데

 

각각 기본 인게임 bgm, 중간보스용, 최종보스용 bgm이 들어가있다

물론 StageClearID를 사용하여 각각 25,45,65일때 원하는걸 뽑아 재생하도록 하였다

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class IsPlayBGM : MonoBehaviour
{
    public AudioSource[] BGM; // 오디오 소스를 배열로 변경
    private BGMControl bGMControl;

    void Start()
    {
        bGMControl = FindObjectOfType<BGMControl>();

        if (bGMControl != null)
        {
            if (bGMControl.BGMSwitch)
            {
                // StageClearID 값에 따라 다른 오디오 소스를 재생
                if (StageState.chooseStage == 25 || StageState.chooseStage == 45)
                {
                    if (BGM.Length > 1 && BGM[1] != null) // 두 번째 오디오 소스가 존재하는지 확인
                    {
                        BGM[1].Play();
                    }
                }
                else if(StageState.chooseStage == 65)
                {
                    BGM[2].Play();
                }
                else
                {
                    if (BGM.Length > 0 && BGM[0] != null) // 첫 번째 오디오 소스가 존재하는지 확인
                    {
                        BGM[0].Play();
                    }
                    else
                    {
                        Debug.LogWarning("First audio source is missing or not assigned.");
                    }
                }
            }
            else
            {
                // 모든 오디오 소스를 멈춤
                foreach (var audioSource in BGM)
                {
                    if (audioSource != null)
                    {
                        audioSource.Stop();
                    }
                }
            }
        }
        else
        {
            Debug.LogError("BGMControl object not found in the scene.");
        }
    }
}

 

이건 뭐 간단하다. BGM 스위치가 켜져 있으면 재생, 없으면 재생하지 않는것이다

 

---------------------------------------

 

그 다음은 최종 보스

 

왼쪽은 이미지, 오른쪽은 자세한 스탯이다

 

원래는 진짜 센걸로 하나 만들지, 아니면 오히려 기믹적인 요소로 심플하게 디자인할지 고민하다 심플한것을 택했다

 

디자인은 기본 적 유닛에 완벽한 검은색으로 크게 다른건 없다

 

대신 기본구체 대신 내구도가 없는 새로운 형태의 총알을 발사한다

 

이건 사실 예전에 만들어놓은 블랙홀 아이템으로, 여기서 사용하게 되었다

 

(20) 아이템 #사용구현

•구체 충돌 시 반사•구체 정지 시 확장•Deadzone•Firezone•발사 구현•확장이 완료 시 고정 & 충돌 피해 X•구체 내구도 부여 & 내구도 소진시 구체 파괴•사운드 이펙트•메인 화면 & 환경설정

sangeun00.tistory.com

 

 

이대로만 끝나면 최종 보스 난이도가 아무 의미 없어지므로 한가지 더 추가한것이 있다

 

using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;

public class BossCenter : MonoBehaviour
{
   
   ............

    private IEnumerator Attack1()
    {
        yield return new WaitForSeconds(Random.Range(MinFireTime, MaxFireTime));

        float targetAngle = Random.Range(MinAngle, MaxAngle);
        float currentAngle = transform.eulerAngles.z;
        float rotationTime = 1f; // ȸ���ϴ� �� �ɸ��� �ð�
        float elapsedTime = 0f;

        while (elapsedTime < rotationTime)
        {
            elapsedTime += Time.deltaTime;
            float angle = Mathf.LerpAngle(currentAngle, targetAngle, elapsedTime / rotationTime);
            transform.eulerAngles = new Vector3(0, 0, angle);
            yield return null;
        }

        yield return new WaitForSeconds(1f);

        if (bossfires != null)
        {
            foreach (var enemy1Fire in bossfires)
            {
                enemy1Fire.SpawnBullet();
            }
        }
    }

    private IEnumerator Attack2()
    {
        yield return new WaitForSeconds(7);

        if (Enemy.Length > 0)
        {
            GameObject enemy1 = Enemy[Random.Range(0, Enemy.Length)];
            GameObject enemy2 = Enemy[Random.Range(0, Enemy.Length)];
            GameObject enemy3 = Enemy[Random.Range(0, Enemy.Length)];
            GameObject enemy4 = Enemy[Random.Range(0, Enemy.Length)];


            Instantiate(enemy1, transform.position + new Vector3(-1.5f, 0, 0), Quaternion.identity);
            Instantiate(enemy2, transform.position + new Vector3(1.5f, 0, 0), Quaternion.identity);
            Instantiate(enemy3, transform.position + new Vector3(0, 1.5f, 0), Quaternion.identity);
            Instantiate(enemy4, transform.position + new Vector3(0, -1.5f, 0), Quaternion.identity);

        }
    }

    

    private IEnumerator RandomAttack()
    {
        while (true)
        {
            int randomAttack = Random.Range(0, 2);
            switch (randomAttack)
            {
                case 0:
                    yield return StartCoroutine(Attack1());
                    break;
                case 1:
                    yield return StartCoroutine(Attack2());
                    break;
            }

            yield return new WaitForSeconds(4f);
        }
    }
}

 

최종 보스를 컨트롤 하는 스크립트 중 일부

 

간단하게 설명하면 최종보스는 Attack1,2 두개 중 하나를 랜덤으로 선택하여 공격한다

Attack1은 앞서 설명한 블랙홀 아이템을 발사하는것

Attack2는 여태 나온 적 유닛들 중 랜덤하게 4개를 선정해 자신의 앞,뒤,양옆에 생성하는것이다

 

그래서 스탯의 가장 아래 Enemy 배열을 추가한것이다

물론 모든 유닛이 다 들어간건 아니고 밸런스를 위해 몇가지 제한한게 있긴하다

 

 

 

이런 느낌이라고 생각하면 된다. 사실상 Attack2가 너무 강력하여 최종보스가 된 감도 없지 않아 있다