Coding Feature.

[Unity 3D] 3D 퐁 만들기 #7 Camera 매니저, Camera Shake 효과, 옵션 창, JSON 형식 파일 저장, 슬로우 모션 효과, HyperDrive 효과, 공 기준선 구현 본문

Toy Project/MICRO-PONG [Unity3D]

[Unity 3D] 3D 퐁 만들기 #7 Camera 매니저, Camera Shake 효과, 옵션 창, JSON 형식 파일 저장, 슬로우 모션 효과, HyperDrive 효과, 공 기준선 구현

codingfeature 2024. 1. 21. 17:25

Camera Manager, Camera Shake 효과

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

public class CameraManager : MonoBehaviour
{
    private static CameraManager instance;

    public GameObject CameraHolder;
    public GameObject MainCamera;

    bool isCameraShakeOn;

    public static CameraManager Instance
    {
        get
        {
            return instance;
        }
    }

    private void Awake()
    {
        if (instance)
        {
            Destroy(instance);
            return;
        }

        instance = this;
        DontDestroyOnLoad(this.gameObject);
    }

    private void Start()
    {
        isCameraShakeOn = true;
    }

    public IEnumerator ShakeCamera(float duration, float magnitude)
    {
        yield return null;
        if (isCameraShakeOn)
        {
            Vector3 originalPosition = MainCamera.transform.localPosition;
            // shake camera
            while (duration > 0)
            {
                Debug.Log(MainCamera.transform.localPosition);

                float x = Random.Range(-1f, 1f) * magnitude;
                float y = Random.Range(-1f, 1f) * magnitude;
                float z = Random.Range(-1f, 1f) * magnitude;

                MainCamera.transform.localPosition = new Vector3(x, y, z);

                duration -= 0.015f;
                yield return new WaitForSeconds(0.015f);
            }


            MainCamera.transform.localPosition = originalPosition;
        }
    }

    public void SetCameraShake(bool isOn)
    {
        isCameraShakeOn = isOn;
    }
}

 

현재 카메라 매니저 스크립트에서는 Camera Shake 효과를 주기 위한 함수들만 우선 구현했습니다.

그리고 isCameraShakeOn 불리언을 통해서 나중에 옵션에서 카메라 흔들림 효과 여부를 컨트롤 할 수 있도록 했습니다!

 

위 Shake 효과는 공이 벽 또는 플레이어에 닿을때마다 호출해줬습니다.

            AudioManager.Instance.Play("Bump");

            if (go.CompareTag("Wall"))
            {
                StartCoroutine(CameraManager.Instance.ShakeCamera(0.1f, 0.03f));
                ParticleManager.Instance.SetSparkPosition(transform.position);
                ParticleManager.Instance.PlaySpark();
            }

 

 

 

카메라 Shake 효과는 아래 영상을 참고했습니다!

CAMERA SHAKE in Unity (youtube.com)

 

 

옵션 창 구현

그리고 옵션 Panel을 추가해서 게임 시작 전 옵션 창에 들어가 해상도, Fullscreen, 효과음, 음악, 카메라 흔들림 효과 등을 제어할 수 있도록 해보겠습니다.

 

 

 

캔버스에 옵션 Panel을 추가하고 다음과 같이 UI 요소들을 추가했습니다.

 

전체 화면 토글은 다음과 같은 함수를 통해 토글되도록 했습니다.

    public void ToggleFullScreen(bool isOn)
    {
        Screen.fullScreen = isOn;
    }

 

 

그리고 Music과 SFX는 다음 함수를 통해 제어하도록 했습니다.

    public void ToggleBackground(bool isOn)
    {
        foreach (Sound s in sounds)
        {
            if(s.soundType == Sound.SoundTypes.background)
            {
                if (isOn)
                {
                    s.source.volume = s.originalVolume;
                }
                else
                {
                    s.source.volume = 0f;
                }
            }
        }
    }

    public void ToggleSoundEffect(bool isOn)
    {
        foreach (Sound s in sounds)
        {
            if (s.soundType == Sound.SoundTypes.effect)
            {
                if (isOn)
                {
                    s.source.volume = s.originalVolume;
                }
                else
                {
                    s.source.volume = 0f;
                }
            }
        }
    }

 

 

s.originalVolume은 이전에 구현했던 Sound 클래스에서 추가한 변수로 원래 설정했던 소리의 음량 크기를 저장합니다.

 

 

해상도 조절 드롭다운, JSON 형식 파일 저장, 로드

그리고 해상도 조절 드롭다운 기능을 구현하기 위해 다음 영상을 참조했습니다.

https://youtu.be/YOaYQrN1oYQ?si=6869qquZbFk8CsV9

 

다만 위 영상에서는 유니티 엔진이 사용자의 컴퓨터에 따라 조절가능한 해상도를 다르게 드롭다운에 표시하도록 하기 때문에 그 결과를 개발하는 입장에서 유추해내기 어렵다는 단점이 있습니다.

 

따라서 제가 직접 특정 해상도(16:9 비율)들을 미리 지정해서 그 해상도만 설정할 수 있도록 기능을 구현하기로 했습니다.

그리고 마지막으로 지정된 해상도를 플레이어가 게임을 종료하고도 기억할 수 있도록 JSON 형식 파일로 그 데이터(해상도의 width, height)를 저장해서 나중에 게임을 다시 킬 때 그 데이터를 읽어들여 설정할 수 있도록 해보겠습니다.

 

우선 해상도의 세팅을 저장할 데이터 구조 클래스를 정의하겠습니다.

[System.Serializable]
public class Settings
{
    public int resolution_width;
    public int resolution_height;
}

 

그리고 이를 JSON 형식 파일로 저장하거나 불러오는 함수를 정의했습니다.

 

    public void SaveScreenSettingsToJSON(int width, int height)
    {
        Settings setting = new Settings();
        setting.resolution_width = width;
        setting.resolution_height = height;

        string json = JsonUtility.ToJson(setting, true);
        File.WriteAllText(Application.dataPath + "SettingsFile.json", json);
    }

    public Settings LoadScreenSettingsToJSON()
    {
        string json = File.ReadAllText(Application.dataPath + "SettingsFile.json");
        Settings setting = JsonUtility.FromJson<Settings>(json);

        return setting;
    }

 

먼저 JSON으로 저장하는 함수에서는 Setting 클래스의 객체를 생성하고 인자로 받은 width, height 값을 저장한 뒤에, JSON 형식으로 변환하고, SettingsFile.json 파일명으로 저장하게 했습니다.

 

반대의 경우, 마찬가지로 파일을 읽고 JSON 형식을 Settings 클래스 데이터 형식으로 변환한 뒤에 반환하도록 했습니다.

 

 

그리고 위 함수들을 사용하기 위해 아래 코드를 구현하였습니다.

..
public class UIManager : MonoBehaviour
{
..
    int currentResolutionIndex;

    List<string> options; // dropdown 옵션
    List<Vector2Int> resolutions;
..

    private void Start()
    {
        isCursorOutOfScreen = true;

        resolutionDropdown.ClearOptions();

        options = new List<string>();
        resolutions = new List<Vector2Int>();

        // 초기 옵션 설정 (추후에 리팩토링 필요.)
        options.Add("1280 X 720");
        resolutions.Add(new Vector2Int(1280, 720));
        options.Add("1600 X 900");
        resolutions.Add(new Vector2Int(1600, 900));
        options.Add("1920 X 1080");
        resolutions.Add(new Vector2Int(1920, 1080));

        // JSON 파일에서 해상도 읽어온 뒤에 설정.

        Settings setting = new Settings();

        if (File.Exists(Application.dataPath + "SettingsFile.json")) // 파일이 있는 경우,
        {
            setting = LoadScreenSettingsToJSON();
        }
        else // JSON 형식 세팅 파일이 없는 경우, 1280x720 해상도 기본 지정.
        {
            SaveScreenSettingsToJSON(1280, 720);
            setting.resolution_width = 1280;
            setting.resolution_height = 720;
        }
		
        // setting 객체 내 해상도 값으로 화면 지정.
        
        SetResolutionByValue(setting.resolution_width, setting.resolution_height);
        for (int i = 0; i < resolutions.Count; i++)
        {
            if (resolutions[i].x == Screen.width && resolutions[i].y == Screen.height)
            {
                currentResolutionIndex = i; // 현재 해상도의 드롭다운 index 값.
            }
        }

        resolutionDropdown.AddOptions(options);
        resolutionDropdown.value = currentResolutionIndex;
        resolutionDropdown.RefreshShownValue();

        FullscreenToggleUI.isOn = Screen.fullScreen;

        screenRect = new Rect(0, 0, Screen.width, Screen.height);
    }
..
}

 

 

options은 string 형 리스트로, 드롭다운에 나타나는 값들을 저장합니다.

그리고 resolutions는 Vector2Int 형 리스트로 실제 해상도를 설정할 때 사용되는 해상도 값을 저장합니다.

예를 들어 1280x720 해상도라면 options에는 "1280X720" string 값이,

resolutions에는 x 값이 1280, y 값이 720로 저장됩니다.

 

Start 함수에서,

1280x720,

1600x900,

1920x1080,

해상도를 options과 resolutions에 저장합니다.

 

그다음 JSON 형식 세팅 파일이 존재하는지 확인합니다.

만약 JSON 파일이 없다면, 아직 생성되지 않은 것으로 간주하고 1280x720 해상도를 기본으로 설정하고 JSON을 새로 생성하도록 합니다.

이미 파일이 있을 경우, 그 데이터를 읽어옵니다.

 

참고로 Screen.width, height과 Screen.currentResolution.width, height은 차이가 있습니다.

 Screen에서 바로 구하는 width, height는 게임 창의 크기를 나타내고(풀스크린, 창 모드 스크린이 서로 다를 수 있음), Screen.currentResolution은 창 모드로 바뀐다고 해도 플레이어의 실제 창의 크기를 반환한다는 차이점이 있습니다. 아래 링크를 참고해보시면 좋습니다!

Screen.currentResolution gives wrong value. How to get actual resolution? - Questions & Answers - Unity Discussions

 

Screen.currentResolution gives wrong value. How to get actual resolution?

Just reading the documentation should explain everything: Screen.currentResolution: The current screen resolution (Read Only). If the player is running in window mode, this returns the current resolution of the desktop. Screen.width: The current width of t

discussions.unity.com

 

 

 

해상도를 바꾸는 방식은 총 두 가지로 정했습니다.

하나는 직접 값을 주어서 바꾸는 방식이고 다른 하나는 드롭다운 방식을 사용해서 바꾸는 방식입니다.

    public void SetResolutionByValue(int width, int height)
    {
        Screen.SetResolution(width, height, Screen.fullScreen);
        screenRect = new Rect(0, 0, width, height);
    }
    public void SetResolutionByIndex(int index)
    {
        Screen.SetResolution(resolutions[index].x, resolutions[index].y, Screen.fullScreen);
        screenRect = new Rect(0, 0, resolutions[index].x, resolutions[index].y);
        SaveScreenSettingsToJSON(resolutions[index].x, resolutions[index].y);
    }

 

드롭다운(options)의 INDEX를 통해서 바꾸는 함수는 options와 resolution index가 같은 것을 가정했기 때문에 resolutions를 options의 index를 통해 접근해서 변경이 가능합니다.

 

그리고 세팅을 바꿀때마다 JSON 파일을 새로 저장하게 합니다.

 

 

JSON 형식 파일을 통해 세팅값을 게임이 종료되어도 저장하는 방식은 데이터 보안 차원에서 안전한 방법은 아닙니다. 사용자가 쉽게 파일을 열고 수정할 수 있기 때문입니다. 현재는 해상도 세팅 값만 저장하기 때문에 수정되어도 그렇게 게임 내부에서 큰 문제가 일어나지는 않지만 사용자가 수정하는 것에 대해서도 따로 코드로 처리할 필요는 있습니다.

 

이 부분에 대해서는 추후에 수정해보도록 하겠습니다!

 

JSON 파일 생성, 관리 관련 내용은 아래 영상을 참고했습니다.

https://youtu.be/g0k4XStzkLQ?si=jJvXRxCUvXuniu0Q

 

 

 

슬로우모션

이제 게임 내적으로 슬로우모션 기능을 추가하기로 했습니다.

 

슬로우 모션을 이용해서 사용자가 10점을 얻을 때마다 슬로우모션 효과로 타격감과 성취감을 강조해보도록 하겠습니다.

 

우선 아래 코루틴을 구현하였습니다.

    public IEnumerator SlowTime(float timeScale, float duration)
    {
        yield return null;
        Time.timeScale = timeScale;
        yield return new WaitForSecondsRealtime(duration);
        Time.timeScale = 1f;
    }

 

timeScale은 게임 속도를 조절하고, duration은 그 속도가 지속되는 시간을 지정하게 됩니다.

그리고 WaitForSecondsRealtime을 사용하는데 그냥 WaitForSeconds를 사용하면 바뀐 속도를 duration에 적용하기 때문에 실제 사용자의 시간에 맞춰서 할 수 없습니다.

 

그리고 플레이어 스크립트에 다음 코드를 추가했습니다.

    private void OnCollisionEnter(Collision collision)
    {
        if (collision.gameObject.CompareTag("Ball")) // 플레이어가 공에 닿을 경우,
        {
            GameManager.Instance.score += 1;

            ball.GetComponent<BallScript>().AddBallSpeed(GameManager.Instance.ballSpeedIncrement);
            ball.GetComponent<BallScript>().RotateBallVector(Random.Range(0f, 360f));
            StartCoroutine(UIManager.Instance.ShowScore());

            AudioManager.Instance.Play("Score");
            StartCoroutine(CameraManager.Instance.ShakeCamera(0.2f, 0.2f));

            if(GameManager.Instance.score % 10 == 0) // 10점 낼때마다 효과.
            {
                StartCoroutine(GameManager.Instance.SlowTime(0.1f, 1.5f));
                AudioManager.Instance.Play("Explosion");
                AudioManager.Instance.Play("Milestone");
            }
        }
    }

 

score가 10점 단위일때마다 SlowTime 코루틴을 호출해서 느리게 합니다.

 

Hyperdrive 효과 구현

Hyperdrive는 아래 영상과 같은 효과를 의미합니다.

https://youtu.be/6ga4IICXyCE?si=rB5yfI86N4p5APEY

 

 

이 효과를 플레이하는 동안 뒷배경으로 사용하면 좋겠다고 생각해서 다음 영상을 참고하면서 구현해보았습니다!

https://youtu.be/4hlCOUoc6aQ?si=LP8B3kTo59PMIsh-

 

파티클 시스템을 사용하였고 이 부분도 마찬가지로 Material에 emission 효과를 부여해서 더 빛이 나도록 했습니다.

 

Shape 속성에는 Cone 으로 바꾼 뒤에 Angle을 줄여주었고,

Renderer에는 Render 모드를 Stretched Billboard 형식으로 바꾸어서 빛이 길게 늘어지도록 했습니다.

 

 

공, 플레이어 기준선

 

위 사진과 같이 플레이어가 움직이는 선, 공이 움직이는 선을 가시화해서 플레이어가 더욱 쉽게 공이 어느 위치에 있는지 확인할 수 있도록 했습니다.

 

선은 모두 Plane 오브젝트로 구현하였으며 공 스크립트에서 선이 공을 따라가도록 구현하였습니다.

    void Update()
    {
...
        ballLine.transform.position = new Vector3(ballLine.transform.position.x, ballLine.transform.position.y, transform.position.z);
    }

 

 

 

 

 

 

다음에는 게임 내에서 발생할 수 있는 여러 버그들을 수정하고 아이템 시스템을 만들어보겠습니다!