본문 바로가기
Development/Unity BootCamp

멋쟁이사자처럼부트캠프 Unity 게임 개발 3기 51일차

by Mobics 2025. 2. 10.

 

목차

     


    ※ GitHub Repository가 관리하는 위치 변경하기

    : Build를 하려는데 한글 경로(사용자명) 때문에 오류가 발생해서 Project의 위치를 옮긴 뒤, Local Repository를 추가하여 해결

     

    1. Project의 불필요한 파일들을 제외한 핵심 파일들을 옮기고자 하는 폴더로 옮기기

     

    2. Github Desktop을 이용하여 'Add local Repository'

    --> 기존에 사용하던 repository는 제거 (잘못 지우지 않도록 마우스를 올려 경로를 확인)

    └ Github 홈페이지에서 Repository 자체를 삭제하는 것이 아님, Github Desktop에서 관리하지 않도록 삭제하는 것


    틱택토 게임 만들기

    25.02.10

    SettingsPanel 제작

    >> Sprite 추가

    • back-button
    • settings-icon
    • switch-bg
    • switch-handler

     

    >> Sprite Atlas 생성

    : 한 장의 Image에 여러 Image를 담는 것

    • 장점 : 한 장의 Image로 만들기 때문에 불러오는 시간이 빠르다.
    • 단점 : 불필요한 Image가 담겨있다면 불러올 때 같이 불러오기 때문에 메모리 낭비를 할 수도 있다.

     

    >> SFX Panel 제작

    1. SFX Toggle, BGM Toggle 삭제

     

    2. SFX Panel 생성

    --> Image Component 삭제함

     

    3. Text 생성

     

    4. Switch 생성

    : UI - Image

    --> Color는 MainPanel과 동일하게 만듦

     

    --> 이름을 Switch로 변경함

     

    ※ EventTrigger 로 처리하는 방법도 있다.

     

    5. SwitchHandle 생성

    : UI - Image

     

    6. SwitchController.cs 생성

    • Switch Handle 이동
    • Switch Background Color 변경
    • Click할 때 Audio 재생

     

    7. Click 효과음 추가

    --> 강의에는 효과음으로 'DM-CGS-07'을 사용함

     

    >> Vibration Panel 제작

    1. SFX Panel 복붙

    : SFX Panel과 Vibration Panel의 위치 조정

     

    2. Text 수정

    : '진동'

    NavigationPanel 제작

    ※ GameUIPanel에 있던 GameUIController.cs를 Canvas로 옮김

    : GameUIPanel에서는 삭제, Canvas에서 추가 후 바인딩

     

    >> GameOverButton의 OnClick()에 다시 함수 바인딩

     

    1. GameUIPanel 위치 조정

     

    2. BlockController 위치 조정

     

    3. NavigationPanel 생성

     

    4. BackButton 생성

    --> Anchor는 Alt + Shift로 설정

    --> 크기를 임의로 키워서 Image가 깔끔하지 않음 (원래는 Resource를 수정해야하지만 시간 관계상 패스)

     

    5. 버튼의 OnClcik()에 함수 바인딩

     

    6. SettingsButton 생성

    --> Anchor는 Alt + Shift로 설정

     

    7. Text 생성

     

    8. 각 버튼의 OnClick()에 맞는 Action 바인딩

     

    Android로 빌드

    • Export Project : 안드로이드 프로젝트로 export 할 때 체크
    • Build App Bundle (Google Play) : 구글 플레이스토어에 올릴 때 체크

    ※ Android SDK&NDK Tools가 적용 안 된 모습

    --> 'Builds' 폴더를 생성 후, 거기에 Build

     

    ※ Build 하려고 하면 발생하는 오류

     

    --> 나랑 같은 문제를 겪고 해결하신 분 블로그

    https://1ye1.tistory.com/151

     

    Unity 안드로이드 빌드 오류 (SDK Platform Tools version 0.0 < 32.0.0.) 해결 - 사용자 폴더 이름 변경

    게임을 다 만들어놓고 정작 안드로이드로 빌드가 안돼서 거진 한달동안 정말 답답하게 지냈다.구글링해서 나오는 모든 해결방법들을 다 해봤는데도 해결을 못하다가 드디어 오늘 성공해서 기

    1ye1.tistory.com

     

    --> 사용자명을 한글에서 영어로 바꾸고 해결했다는데.. 나는 왜 바꾸고 와도 안 되는가?!?

     

    ▶ 강사님을 통해 해결한 방법

    : 강의 중 새로 설치한 2022.3.57f1에서는 별 문제 없이 Build에 성공했다.

    --> 강사님께서 말씀하시길, 아마 2022.3.51f1 버전을 설치하는 과정에서 버그가 발생한 것 같다. 2022.3.51f1 버전이 꼭 필요한게 아니라면 이대로 사용하고, 2022.3.51f1 버전이 꼭 필요해서 이를 해결하려면 Window를 재설치 해야할 것 같다.

     

    >> SDK 경로를 2022.3.51f1에서 2022.3.57f1로 변경

     

    ※ Android SDK&NDK Tools가 적용된 모습

     

    >> 폰에서 개발자 옵션 - USB 디버깅 켜기

    >> Build한 apk를 폰에 옮겨보기

     

    └ Android SDK & NDK Tools 설치

    : Unity Hub를 사용하여 컴포넌트를 추가

     

    ※ 지금 내가 사용하는 2022.3.51f1 버전은 허브를 통해 설치하지 않았기 때문에 'Add Modules' 옵션이 나타나지 않음

    --> 왜 Hub를 통해 설치하지 않았는가 돌이켜보니 내 윈도우 사용자명이 한글이라 파일 경로에 한글이 들어가서 설치가 안 됐던 것이다.

    >> 해결방법

    : Unity 사이트에서 자신의 버전에 맞는 추가 모듈을 직접 설치하면 될 줄 알았으나 여전히 실패했다.

    --> 결국 새로 사용자 계정을 만들고 쓰던 버전(2022.3.51f1)을 Hub로 설치하여 해결

     

    1. Unity 사이트 접속

    https://unity.com/releases/editor/archive

     

    다운로드 아카이브

    다운로드 아카이브

    unity.com

     

    2. 자신의 버전 찾기

     

    3. Android Build Support 설치

    : 사진은 예시로 다른 버전으로 캡처함

    --> Windows Build Support도 같이 설치

     

    4. Preferences에서 설치 확인

     

    └ Android Studio

    : 구글에서 다운받은 Android SDK --> SDK의 버전을 달리해야할 때 사용하기 용이하다. (Unity에서는 어렵다)

    >> 설치

    : 이것도 한글 경로 때문에 설치가 안 됐다. --> 사용자 계정을 새로 만들어서 해결

    https://developer.android.com/studio?hl=ko

     

    Android 스튜디오 및 앱 도구 다운로드 - Android 개발자  |  Android Studio  |  Android Developers

    Android Studio provides app builders with an integrated development environment (IDE) optimized for Android apps. Download Android Studio today.

    developer.android.com

    ※ 아마 별 다른 설정 없이 계속 'Next'를 눌러서 설치하면 될 것이다 (Android Studio 설치부터 Setup까지)

     

    >> New Project 생성

    --> 따로 건들지 말고 바로 Finish

     

    >> Device Manager

    --> 에뮬이 켜진 모습

     

    >> SDK Manager

    --> 최대한 낮은 버전으로 빌드하면 더 많은 사람들이 사용할 수 있다.

    --> 밑줄 친 경로 복사

     

    └ Build Settings에서 바로 Build And Run 하도록 세팅

    1. 복사한 경로 열기

    --> C:\Users\(사용자명)\AppData\Local\Android\Sdk

     

    2. 다시 경로 복사

    --> C:\Users\(사용자명)\AppData\Local\Android\Sdk\platform-tools

     

    3. 내 PC - 설정 - 고급 시스템 설정 - 환경 변수

     

    4. Path 추가

    : cmd에서 해당 폴더에 있는 파일들을 그 위치에 직접 가지않고도 사용할 수 있도록 경로를 미리 등록해두는 것

    --> 위의 Path는 현재 사용자만 적용하는 것이고 밑의 Path는 이 컴퓨터 전체에 적용하는 것

    --> '새로 만들기' 후, 복사한 경로 붙여넣기

     

    5. cmd

    : 'adb' 를 입력했을 때, 막 길게 나오면 성공한 것

     

    >> 본인의 휴대폰을 연결한 뒤, 'adb devices' 를 입력했을 때 나오는 모습

    --> 가린 부분이 내 휴대폰

    --> device로 나온다면 성공한 것

     

    ※ 이름 옆에 사진처럼 'device'가 아니라 'unauthorized' 라고 나온다면 휴대폰을 열어 나온 팝업창을 통해 장치를 허용

    --> 팝업창 : 'USB 디버깅을 허용하시겠습니까?'

     

    ※ 원래는 표시한 부분에 목록이 있고 내 휴대폰이 떠야하는데 나는 왜 뜨질 않는가?

    --> 위에서 SDK 경로를 수정하여 해결

     

    버그 수정

    1. UI가 떠있는 상태에서 UI를 클릭하면 Block이 같이 선택되는 버그

    >> Block.cs 수정

    using System;
    using UnityEngine;
    using UnityEngine.EventSystems;
    
    public class Block : MonoBehaviour
    {
        [SerializeField] private Sprite oSprite;
        [SerializeField] private Sprite xSprite;
        [SerializeField] private SpriteRenderer markerSpriteRenderer;
    
        public enum MarkerType { None, O, X }
    
        public delegate void OnBlockClicked(int index); // void를 반환하고 int를 매개변수로 삼는 형태의 함수를 받는다.
        private OnBlockClicked _onBlockClicked;
        private int _blockIndex;
        private SpriteRenderer _spriteRenderer;
        private Color _defaultColor;
    
        private void Awake()
        {
            _spriteRenderer = GetComponent<SpriteRenderer>();
            _defaultColor = _spriteRenderer.color;
        }
    
        /// <summary>
        /// 블럭의 색상을 변경하는 함수
        /// </summary>
        /// <param name="color">색상</param>
        public void SetColor(Color color)
        {
            _spriteRenderer.color = color;
        }
    
        /// <summary>
        /// Block 초기화 함수
        /// </summary>
        /// <param name="blockIndex">Block 인덱스</param>
        /// <param name="onBlockClicked">Block 터치 이벤트</param>
        public void InitMarker(int blockIndex, OnBlockClicked onBlockClicked)
        {
            _blockIndex = blockIndex;
            SetMarker(MarkerType.None);
            this._onBlockClicked = onBlockClicked;
            SetColor(_defaultColor);
        }
    
        /// <summary>
        /// 어떤 마커를 표시할지 전달하는 함수
        /// </summary>
        /// <param name="markerType">마커 타입</param>
        public void SetMarker(MarkerType markerType) // 외부에서 마크 표시
        {
            switch (markerType)
            {
                case MarkerType.O:
                    markerSpriteRenderer.sprite = oSprite;
                    break;
                case MarkerType.X:
                    markerSpriteRenderer.sprite = xSprite;
                    break;
                case MarkerType.None:
                    markerSpriteRenderer.sprite = null;
                    break;
            }
        }
    
        private void OnMouseUpAsButton()
        {
            if (EventSystem.current.IsPointerOverGameObject()) // 버그 수정
            {
                return;
            }
            _onBlockClicked?.Invoke(_blockIndex);
        }
    }

     

    2. 싱글 플레이를 한번 한 뒤, 2인 플레이를 하면 AI가 찍어주는 버그

    : '클로저' 라는 개념과 관련된 문제

     

    >> 문제 원인 (다른 수강생 분이 설명해주심)

    : Singleton으로 GameManager가 동작하긴 하지만, 새롭게 Scene을 로드할 때 GameManager가 생성되어 OnSceneLoaded가 구독 됩니다.
    중복된 GameManager는 Destroy되겠지만 생성되는 시점인 Awake에서 구독을 추가하기 때문에 기존 상태를 유지한 인스턴스가 실행되어 상태가 변경되는 문제가 생깁니다.

    즉, 이 문제를 해결하려면 부모 클래스인 Singleton 스크립트에서 인스턴스가 중복 생성되어도 중복 구독이 발생되지 않도록 처리해 주어야합니다.
    씬 전환시 구독을 해제할 수도 있지만 저는 최초로 생성된 인스턴스에서만 구독을 추가하도록 수정해서 해결했습니다.

     

    >> 문제 예시

    : count는 CreateCounter()를 벗어나면 사라지지만, return에서 한번 실행이 된다.

    static Func<int> CreateCounter(int startNumber)
    {
    	int count = startNumber;
        
        return () => 
        {
        	count++;
            return count;
        };
    }
    
    static void Main()
    {
    	// 두 개의 서로 다른 카운터를 생성
        var counter1 = CreateCounter(0);
        var counter2 = CreateCounter(10);
        
        // counter1 사용
        Console.Writeline($"Counter1: {counter1()}"); // 출력 1
        Console.Writeline($"Counter1: {counter1()}"); // 출력 2
        Console.Writeline($"Counter1: {counter1()}"); // 출력 3
        
        // counter2 사용
        Console.Writeline($"Counter2: {counter2()}"); // 출력 11
        Console.Writeline($"Counter2: {counter2()}"); // 출력 12
        Console.Writeline($"Counter2: {counter2()}"); // 출력 13
    }

     

    >> Singleton.cs 수정

     

    ※ 근데 테스트 해보니까 저는 문제가 발생하지 않음 --> 애초에 강사님이 수정했다고 올린 코드가 지난 번에 수정한 코드에 포함되어 있는 것 같다.

     

    최종 코드

    >> GameManager.cs

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.SceneManagement;
    
    public class GameManager : Singleton<GameManager>
    {
        [SerializeField] private GameObject settingsPanel;
        [SerializeField] private GameObject confirmPanel;
        
        private BlockController _blockController;
        private GameUIController _gameUIController;
        private Canvas _canvas;
    
        public enum PlayerType { None, PlayerA, PlayerB }
        private PlayerType[,] _board;
        
        public enum TurnType { PlayerA, PlayerB }
        private enum GameResult { None, Win, Lose, Draw }
        
        public enum GameType { SinglePlayer, DualPlayer }
        private GameType _gameType;
    
        public void ChangeToGameScene(GameType gameType)
        {
            _gameType = gameType;
            SceneManager.LoadScene("Game");
        }
    
        public void ChangeToMainScene()
        {
            SceneManager.LoadScene("Main");
        }
    
        public void OpenSettingsPanel()
        {
            if (_canvas != null)
            {
                var settingsPanelObject = Instantiate(settingsPanel, _canvas.transform);
                settingsPanelObject.GetComponent<PanelController>().Show();
            }
        }
    
        public void OpenConfirmPanel(string message, ConfirmPanelController.OnConfirmButtonClick onConfirmButtonClick)
        {
            if (_canvas != null)
            {
                var confirmPanelObject = Instantiate(confirmPanel, _canvas.transform);
                confirmPanelObject.GetComponent<ConfirmPanelController>()
                    .Show(message, onConfirmButtonClick);
            }
        }
    
        /// <summary>
        /// 게임 시작
        /// </summary>
        private void StartGame()
        {
            // _board 초기화
            _board = new PlayerType[3, 3];
            
            // Block 초기화
            _blockController.InitBlocks();
            
            // Game UI 초기화
            _gameUIController.SetGameUIMode(GameUIController.GameUIMode.Init);
            
            //panelManager.ShowPanel(PanelManager.PanelType.BattlePanel);
            
            // 턴 시작
            SetTurn(TurnType.PlayerA);
        }
        
        /// <summary>
        /// 게임 오버 시, 호출되는 함수
        /// gameResult에 따라 결과 출력
        /// </summary>
        /// <param name="gameResult">win, lose, draw</param>
        private void EndGame(GameResult gameResult)
        {
            // 게임오버 표시
            _gameUIController.SetGameUIMode(GameUIController.GameUIMode.GameOver);
            _blockController.OnBlockClickedDelegate = null; // 게임이 끝났을 때 board를 Click해도 바뀌지 않도록
            
            // TODO: 나중에 구현!
            switch (gameResult)
            {
                case GameResult.Win:
                    break;
                case GameResult.Lose:
                    break;
                case GameResult.Draw:
                    break;
            }
        }
    
        /// <summary>
        /// _board에 새로운 값을 할당하는 함수
        /// </summary>
        /// <param name="playerType">할당하고자 하는 플레이어 타입</param>
        /// <param name="row">Row</param>
        /// <param name="col">Col</param>
        /// <returns>False : 할당할 수 없음, True : 할당이 완료됨</returns>
        private bool SetNewBoardValue(PlayerType playerType, int row, int col)
        {
            if (_board[row, col] != PlayerType.None) return false; // 중복 체크 방지
            
            if (playerType == PlayerType.PlayerA)
            {
                _board[row, col] = playerType;
                _blockController.PlaceMarker(Block.MarkerType.O, row, col);
                return true;
            }
            else if (playerType == PlayerType.PlayerB)
            {
                _board[row, col] = playerType;
                _blockController.PlaceMarker(Block.MarkerType.X, row, col);
                return true;
            }
            return false;
        }
    
        private void SetTurn(TurnType turnType)
        {
            switch (turnType)
            {
                case TurnType.PlayerA:
                    _gameUIController.SetGameUIMode(GameUIController.GameUIMode.TurnA);
                    _blockController.OnBlockClickedDelegate = (row, col) =>
                    {
                        if (SetNewBoardValue(PlayerType.PlayerA, row, col))
                        {
                            var gameResult = CheckGameResult();
                            if (gameResult == GameResult.None)
                                SetTurn(TurnType.PlayerB);
                            else
                                EndGame(gameResult);
                        }
                    };
                    break;
                case TurnType.PlayerB:
                    _gameUIController.SetGameUIMode(GameUIController.GameUIMode.TurnB);
    
                    if (_gameType == GameType.SinglePlayer)
                    {
                        var result = MinimaxAIController.GetBestMove(_board);
                        if (result.HasValue)
                        {
                            if (SetNewBoardValue(PlayerType.PlayerB, result.Value.row, result.Value.col))
                            {
                                var gameResult = CheckGameResult();
                                if (gameResult == GameResult.None)
                                    SetTurn(TurnType.PlayerA);
                                else
                                    EndGame(gameResult);
                            }
                        }
                        else
                        {
                            EndGame(GameResult.Win);
                        }
                    }
                    else if (_gameType == GameType.DualPlayer)
                    {
                        _blockController.OnBlockClickedDelegate = (row, col) =>
                        {
                            if (SetNewBoardValue(PlayerType.PlayerB, row, col))
                            {
                                var gameResult = CheckGameResult();
                                if (gameResult == GameResult.None)
                                    SetTurn(TurnType.PlayerA);
                                else
                                    EndGame(gameResult);
                            }
                        };    
                    }
                    break;
            }
        }
    
        /// <summary>
        /// 게임 결과 확인 함수
        /// </summary>
        /// <returns>플레이어 기준 게임 결과</returns>
        private GameResult CheckGameResult()
        {
            if (CheckGameWin(PlayerType.PlayerA)) return GameResult.Win;
            if (CheckGameWin(PlayerType.PlayerB)) return GameResult.Lose;
            if (MinimaxAIController.IsAllBlocksPlaced(_board)) return GameResult.Draw;
            
            return GameResult.None;
        }
        
        // 게임의 승패를 판단하는 함수
        private bool CheckGameWin(PlayerType playerType)
        {
            // 가로로 마커가 일치하는지 확인 
            for (var row = 0; row < _board.GetLength(0); row++)
            {
                if (_board[row, 0] == playerType && _board[row, 1] == playerType && _board[row, 2] == playerType)
                {
                    (int, int)[] blocks = { (row, 0), (row, 1), (row, 2) };
                    _blockController.SetBlockColor(playerType, blocks);
                    return true;
                }
            }
            
            // 세로로 마커가 일치하는지 확인
            for (var col = 0; col < _board.GetLength(1); col++)
            {
                if (_board[0, col] == playerType && _board[1, col] == playerType && _board[2, col] == playerType)
                {
                    (int, int)[] blocks = { (0, col), (1, col), (2, col) };
                    _blockController.SetBlockColor(playerType, blocks);
                    return true;
                }
            }
            
            // 대각선으로 마커가 일치하는지 확인
            if (_board[0, 0] == playerType && _board[1, 1] == playerType && _board[2, 2] == playerType)
            {
                (int, int)[] blocks = { (0, 0), (1, 1), (2, 2) };
                _blockController.SetBlockColor(playerType, blocks);
                return true;
            }
    
            if (_board[0, 2] == playerType && _board[1, 1] == playerType && _board[2, 0] == playerType)
            {
                (int, int)[] blocks = { (0, 2), (1, 1), (2, 0) };
                _blockController.SetBlockColor(playerType, blocks);
                return true;
            }
            
            return false;
        }
    
        protected override void OnSceneLoaded(Scene scene, LoadSceneMode mode)
        {
            if (scene.name == "Game")
            {
                _blockController = GameObject.FindObjectOfType<BlockController>();
                _gameUIController = GameObject.FindObjectOfType<GameUIController>();
    
                // 게임 시작 --> GameScene에 들어왔을 때만 실행
                StartGame();
            }
    
            // Canvas는 Main과 Game 모두 필요하기 때문에 if 밖에서 찾음
            _canvas = GameObject.FindObjectOfType<Canvas>();
        }
    }

     

    >> GameUIController.cs

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.UI;
    
    public class GameUIController : MonoBehaviour
    {
        [SerializeField] private CanvasGroup canvasGroupA;
        [SerializeField] private CanvasGroup canvasGroupB;
        [SerializeField] private Button gameOverButton;
        
        public enum GameUIMode { Init, TurnA, TurnB, GameOver }
    
        private const float DisableAlpha = 0.5f;
        private const float EnableAlpha = 1f;
    
        public void SetGameUIMode(GameUIMode gameUIMode)
        {
            switch (gameUIMode)
            {
                case GameUIMode.Init:
                    canvasGroupA.gameObject.SetActive(true);
                    canvasGroupB.gameObject.SetActive(true);
                    gameOverButton.gameObject.SetActive(false);
    
                    canvasGroupA.alpha = DisableAlpha;
                    canvasGroupB.alpha = DisableAlpha;
                    break;
                case GameUIMode.TurnA:
                    canvasGroupA.gameObject.SetActive(true);
                    canvasGroupB.gameObject.SetActive(true);
                    gameOverButton.gameObject.SetActive(false);
    
                    canvasGroupA.alpha = EnableAlpha;
                    canvasGroupB.alpha = DisableAlpha;
                    break;
                case GameUIMode.TurnB:
                    canvasGroupA.gameObject.SetActive(true);
                    canvasGroupB.gameObject.SetActive(true);
                    gameOverButton.gameObject.SetActive(false);
    
                    canvasGroupA.alpha = DisableAlpha;
                    canvasGroupB.alpha = EnableAlpha;
                    break;
                case GameUIMode.GameOver:
                    canvasGroupA.gameObject.SetActive(false);
                    canvasGroupB.gameObject.SetActive(false);
                    gameOverButton.gameObject.SetActive(true);
                    break;
            }
        }
    
        public void OnClickGameOverButton()
        {
            GameManager.Instance.OpenConfirmPanel("게임을 종료하시겠습니까?", () =>
            {
                GameManager.Instance.ChangeToMainScene();
            });
        }
    
        public void OnClickSettingsButton()
        {
            GameManager.Instance.OpenSettingsPanel();
        }
    
        public void OnClickBackButton()
        {
            GameManager.Instance.OpenConfirmPanel("게임을 종료하시겠습니까?", () =>
            {
                GameManager.Instance.ChangeToMainScene();
            });
        }
    }

     

    >> SwitchController.cs

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.UI;
    using DG.Tweening;
    
    [RequireComponent(typeof(Image))]
    [RequireComponent(typeof(AudioSource))]
    public class SwitchController : MonoBehaviour
    {
        [SerializeField] private Image handleImage;
        [SerializeField] private AudioClip clickSound;
    
        public delegate void OnSwitchChangedDelegate(bool isOn);
        public OnSwitchChangedDelegate OnSwitchChanged;
    
        private static readonly Color32 OnColor = new Color32(242, 68, 149, 255);
        private static readonly Color32 OffColor = new Color32(101, 98, 140, 255);
        
        private RectTransform _handleRectTransform;
        private Image _backgroudImage;
        private AudioSource _audioSource;
        
        private bool _isOn;
    
        private void Awake()
        {
            _handleRectTransform = handleImage.GetComponent<RectTransform>();
            _backgroudImage = GetComponent<Image>();
            _audioSource = GetComponent<AudioSource>();
        }
    
        private void Start()
        {
            _handleRectTransform.anchoredPosition = new Vector2(-14, 0);
            _backgroudImage.color = OffColor;
            _isOn = false;
        }
    
        private void SetOn(bool isOn)
        {
            if (isOn)
            {
                _handleRectTransform.DOAnchorPosX(14, 0.2f);
                _backgroudImage.DOBlendableColor(OnColor, 0.2f);
            }
            else
            {
                _handleRectTransform.DOAnchorPosX(-14, 0.2f);
                _backgroudImage.DOBlendableColor(OffColor, 0.2f);
            }
            
            // 효과음 재생
            _audioSource.PlayOneShot(clickSound);
            
            OnSwitchChanged?.Invoke(isOn);
            _isOn = isOn;
        }
    
        public void OnClickSwitch()
        {
            SetOn(!_isOn);
        }
    }

     

    추후 개선점

    • 여러 효과음 추가 ex) Marker를 놓을 때 소리, 게임 BGM 등
    • 이후 멀티플레이 추가 예정

    2D 게임 만들기

    2D 게임 프로젝트

    • Unity 버전 : 2022.3.51f1
    • 템플릿 : Universal 2D
    • Connect to Unity Cloud 체크 해제
    • 이름 : 2d-game

     

    LibreSprite 설치 및 실행

    >> 설치

    : Pixel을 찍는 무료 Tool

     

    https://libresprite.github.io/#!/

     

    LibreSprite

     

    libresprite.github.io

    --> 압축 풀고 실행

     

    >> 실행

    --> Pixel을 크게 하고 싶으면 Size의 Width와 Height를 16px로 하면 된다

     

    >> 마음껏 그리기

    : 단, 가급적 정면이 아니라 측면을 바라보도록

     

    >> 저장 후 Project에 추가

    : Ctrl + S

    - 형식 : PNG

    - 파일 경로 설정

     

    ▶ 체크사항

    • Pixels Per Unit : 1유닛을 100에서 32로 수정 --> 1유닛 공간에 32x32의 Pixel Image를 넣을 것이기 때문에
    • Filter Mode : 도트 Image는 Point 설정
    • Compression : None --> 도트 Image는 픽셀이 많지 않아서 압축할 필요가 없음 (압축하면 오히려 압축하는 과정에서 올바르지 않게 나오는 경우가 발생)

     

    >> Hierarchy에 추가

    : Gizmo 때문에 잘 안 보이므로 조정

    --> 캐릭터는 다른 수강생 분이 공유해주신 것 임시로 사용


    C# 단기 교육 보강

    5일차

    Monster Wave

    ※ Main Camera 위치 강사님과 비슷하게 조정

    --> 강의에서는 Scene에서 카메라를 옮길 위치로 이동한 후, Main Camera를 선택하고 Ctrl + Shift + F 를 사용

     

    └ Animation

    >> Goblin

    : 기존에 Asset에 있던 Animator 수정

     

    >> Exit Time 늘리기

    --> AnyState -> damage 도 마찬가지로 늘리기

     

     

     

    ※ 죽을 때, 시체가 땅을 뚫고 떨어져 사라지는 문제 해결

    : Goblin Prefab의 Use Gravity 체크 해제

    --> HobGoblin과 Troll의 Prefab에도 마찬가지로 적용

    --> Script의 Is Move도 체크 확인 (몬스터가 제자리 걸음하는 문제 해결)

     

    >> HobGoblin, Troll

    : 수정한 Goblin Animator를 복붙한 뒤, Animation만 각 Monster에 해당하는 Animation으로 바인딩

    --> 각 Monster Prefab의 Animator에 복붙으로 만든 Animator 바인딩

     

    ※ Monster가 걸을 때, 흙먼지 날리는 효과는 어떻게 껐는지 모르겠다..

    : 질문하여 해결 방법 찾음! --> Monster의 Prefab에 들어가서 'Effect_Dust'의 Active를 해제

    --> HobGoblin과 Troll에도 적용시켜주자

     

    ※ Monster를 광클하면 죽어도 계속 걸어가는 버그 존재

    : 다음 날 해결, 다음 글을 보자.

     

    └ Turret

    ※ Canon의 위치 변경

     

    >> Animator 수정

    1. 기존의 Idle 삭제 및 Empty State 생성 후, Set as Layer Default State

     

    2. Any State -> Shoot 으로 Transition 추가 후, Conditions 추가

     

    >> Turret.cs 추가

    : Script 생성 및 추가

     

    >> 총구 화염 파티클 추가

    : Prefab을 Hierarchy에 추가

     

    >> 'gunpoint'의 자식으로 넣은 후, Transform 조정

     

    >> Particle System 설정

    • Play On Awake : Unity에서 Play 되자마자 실행시키는 것 --> 체크 해제
    • Stop Action : Particle이 실행된 후에 실행할 액션 --> None (Particle이 한번 재생된 후에 파괴되면 안되기 때문)

     

    >> Bullet

    >> 빈 게임 오브젝트로 총알 발사 위치 잡기

     

    >> Prefab을 Hierarchy에 추가

     

    >> Bullet Transform 조정

     

    >> Bullet 축 바로 잡기

    : Create Empty Parent 를 하면 기존 Bullet의 Position은 (0, 0, 0)으로 초기화 되고, 만들어진 Parent의 Position에 적용된다

    --> Bullet의 회전축은 알아서 올바르게 잡힌다.

     

    >> 이후 Prefab화

     

    >> Hierarchy에서 Bullet 지우고 Prefab으로 이동

    : Transform 초기화 후, Component 추가 및 설정

     

    >> Turret에 각각 바인딩

     

    >> Bullet과 Monster 충돌 처리

    : Bullet.cs 작성 후, Bullet Prefab에 추가

     

    >> Monster들의 Tag 수정

    --> HobGoblin과 Troll에도 동일 적용

     

    >> Turret이 Target을 감지하여 발사하도록

    >> Sphere Collider 추가

     

    >> Turret.cs 수정 후, 바인딩

    --> Current Target은 자동으로 감지할 것이기 때문에 None이 맞음

    --> 바로 아래에 있는 문제를 해결하기 위해 최상위 Object인 Canon을 바인딩한 것

    ※ 원래는 몸체 전부 회전하다보면 높이에 따라 이상하게 기울어질 수도 있기 때문에 좋지 않음

     

    >> Turret이 Target을 쳐다보지 않는 문제 수정

    : Animation이 Rotation을 잡고 있기 때문

     

    >> 해결 방법 1

    1. Turret의 Prefab 해제

     

    2. Animator의 Apply Root Motion 체크

     

    >> 해결방법 2

    : 원본 Animation 수정 --> 원본 Animation은 Read-Only로 잠겨있기 때문에 복사하여 수정

     

    1. 원본 Animation 복붙

     

    2. Edit Shoot의 Animation에서 Rotation 삭제

     

    3. Animator에서 Shoot에 연결된 Animation을 복붙한 'Edit Shoot'으로 변경

     

    4. 다시 원래 하려던대로 바인딩

    : Apply Root Motion도 해결방법 1 때문에 체크했던 것이기 때문에 다시 해제

     

    └ 최종 코드

    >> Monster.cs

    using System;
    using UnityEngine;
    
    public class Monster : MonoBehaviour
    {
        private Animator anim;
        
        public float speed;
        public float hp;
    
        public bool isMove = true;
    
        protected virtual void Init()
        {
            anim = GetComponent<Animator>();
        }
    
        private void Start()
        {
            Init();
        }
    
        private void Update()
        {
            Move();
        }
        
        private void OnMouseDown()
        {
            Hit(1);
        }
    
        public void OnHit(float damage) // 외부에서 접근하기 위한 함수
        {
            Hit(damage);
        }
    
        protected virtual void Hit(float damage)
        {
            hp -= damage;
            isMove = false;
    
            if (hp <= 0)
            {
                anim.SetTrigger("dead");
                this.GetComponent<Collider>().enabled = false;
                Destroy(gameObject, 5f);
            }
            else
            {
                anim.SetTrigger("hit");
                Invoke("DelayMove", 0.5f); // Invoke("함수명", 시간) : 지연 호출 함수
            }
        }
    
        private void DelayMove()
        {
            isMove = true;
        }
    
        protected void Move()
        {
            if (isMove)
            {
                this.transform.Translate(Vector3.forward * (Time.deltaTime * speed));    
            }
        }
    }

     

    >> Goblin.cs

    using UnityEngine;
    
    public class Goblin : Monster
    {
        protected override void Init()
        {
            base.Init();
            
            speed = 3f;
            hp = 1f;
        }
    }

     

    >> HobGoblin.cs

    using UnityEngine;
    
    public class HobGoblin : Monster
    {
        protected override void Init()
        {
            base.Init();
    
            speed = 2f;
            hp = 2f;
        }
    }

     

    >> Troll.cs

    using UnityEngine;
    
    public class Troll : Monster
    {
        protected override void Init()
        {
            base.Init();
    
            speed = 1f;
            hp = 3f;
        }
    }

     

    >> Turret.cs

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class Turret : MonoBehaviour
    {
        // 타겟
        // Damage
    
        public ParticleSystem ps;
        public GameObject bulletPrefab;
        public Transform shootTransform;
        public Transform currentTarget;
        public Transform headTransform;
        public float shootCooldown = 0.5f;
        
        private float _timer;
        private Animator _animator;
    
        private void Start()
        {
            _animator = GetComponent<Animator>();
        }
    
        private void Update()
        {
            if (currentTarget != null)
            {
                headTransform.LookAt(currentTarget); // Target을 바라보는 코드
            }
            
            Shoot();
        }
    
        public void OnTriggerEnter(Collider other)
        {
            if (other.CompareTag("Monster"))
            {
                currentTarget = other.transform;
            }
        }
    
        public void OnTriggerExit(Collider other)
        {
            if (other.CompareTag("Monster"))
            {
                currentTarget = null;
            }
        }
    
        private void Shoot()
        {
            _timer += Time.deltaTime;
            if (_timer >= shootCooldown)
            {
                _timer = 0f;
                ps.Play(); // Particle 재생
                _animator.SetTrigger("Shoot"); // 발사 애니메이션 실행
                CreateBullet();
            }
        }
    
        private void CreateBullet()
        {
            GameObject bulletObj = Instantiate(bulletPrefab, shootTransform.position, Quaternion.identity);
            bulletObj.GetComponent<Rigidbody>().AddForce(shootTransform.forward * 50f, ForceMode.Impulse);
        }
    }

     

    >> Bullet.cs

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class Bullet : MonoBehaviour
    {
        public void OnTriggerEnter(Collider other)
        {
            if (other.CompareTag("Monster"))
            {
                // 몬스터에게 데미지 적용
                other.GetComponent<Monster>().OnHit(1);
            }
        }
    }