본문 바로가기
Development/C#

[멋쟁이사자처럼 부트캠프 TIL 회고] Unity 게임 개발 3기 13일차

by Mobics 2024. 12. 6.

 

목차


    Stack을 활용하여 Undo와 Redo 만들기 (with Command)

    >> 전체 코드

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public interface ICommand
    {
        void Execute();
        void Undo();
    }
    
    public class CommandManager : MonoBehaviour
    {
        private Stack<ICommand> undoStack = new Stack<ICommand>();
        private Stack<ICommand> redoStack = new Stack<ICommand>();
    
        public void ExecuteCommand(ICommand command)
        {
            command.Execute(); // Command 객체의 execute를 실행한다.
            undoStack.Push(command); // undo 했을 때 command의 undo를 호출하기 위해 undoStack에 넣는다
            redoStack.Clear(); // ExecuteCommand가 호출되면 가장 최신의 작업을 한 것이므로 redoStack은 비운다. 
        }
    
        public void Undo()
        {
            if (undoStack.Count > 0)
            {
                ICommand command = undoStack.Pop(); // 가장 최근에 실행된 command를 가져와서
                command.Undo(); // Undo 시킨다.
                redoStack.Push(command); // 다시 Redo할 수 있기 때문에 redoStack에 넣는다.
            }
        }
    
        public void Redo()
        {
            if (redoStack.Count > 0)
            {
                ICommand command = redoStack.Pop(); // 가장 최근에 Undo된 command를 가져와서
                command.Execute(); // 실행시킨다.
                undoStack.Push(command); // 다시 Undo할 수 있기 때문에 undoStack에 넣는다.
            }
        }
        
        [SerializeField] private float speed = 10.0f;
        [SerializeField] private float rotateSpeed = 3.0f;
    
        [SerializeField] private Vector3 MoveDelta = Vector3.zero;
        [SerializeField] private Vector3 RotateDelta = Vector3.zero;
        
        void Update()
        {
            Vector3 movePos = Vector3.zero;
            Vector3 deltaRot = Vector3.zero;
    
            if (Input.GetKey(KeyCode.W))
            {
                movePos += transform.forward;
            }
            if (Input.GetKey(KeyCode.S))
            {
                movePos -= transform.forward;
            }
            if (Input.GetKey(KeyCode.A))
            {
                movePos -= transform.right;
            }
            if (Input.GetKey(KeyCode.D))
            {
                movePos += transform.right;
            }
            if (Input.GetKey(KeyCode.R))
            {
                deltaRot += transform.right * (Time.deltaTime * rotateSpeed);
            }
            if (Input.GetKey(KeyCode.Q))
            {
                deltaRot -= transform.right * (Time.deltaTime * rotateSpeed);
            }
            
            Vector3 addtivePosition = speed * Time.deltaTime * movePos.normalized;
            
            if (movePos == Vector3.zero && MoveDelta != Vector3.zero)
            {
                var moveCommand = new MoveCommand(transform, transform.position - MoveDelta);
                ExecuteCommand(moveCommand);
                MoveDelta = Vector3.zero;
                return;
            }
            
            if (deltaRot == Vector3.zero && RotateDelta != Vector3.zero)
            {
                var rotateCommand = new RotateCommand(transform, Quaternion.LookRotation(transform.forward - RotateDelta, Vector3.up));
                ExecuteCommand(rotateCommand);
                RotateDelta = Vector3.zero;
                return;
            }
    
            // 왔던 포지션으로 되돌아가는 코드
            if (Input.GetKeyDown(KeyCode.Space))
            {
                Undo();
                return;
            }
            
            transform.position += addtivePosition;
            transform.rotation = Quaternion.LookRotation(transform.forward + deltaRot, Vector3.up);
    
            MoveDelta += addtivePosition;
            RotateDelta += deltaRot;
        }
    }
    
    public class MoveCommand : ICommand
    {
        private Transform _transform;
        private Vector3 _oldPosition;
        private Vector3 _newPosition;
        
        public MoveCommand(Transform transform, Vector3 rollbackPosition)
        {
            _transform = transform; // 이동하려는 transform 객체를 참조한다.
            _oldPosition = rollbackPosition; // Undo할 때 돌아갈 position을 저장한다.
            _newPosition = transform.position; // execute 할 때에 세팅될 position 값을 저장한다.
        }
        
        public void Execute()
        {
            _transform.position = _newPosition; // _newPosition으로 갱신한다.
        }
    
        public void Undo()
        {
            _transform.position = _oldPosition; // _oldPosition으로 Undo한다.
        }
    }
    
    public class RotateCommand : ICommand
    {
        private Transform _transform;
        private Quaternion _oldRotation;
        private Quaternion _newRotation;
        
        public RotateCommand(Transform transform, Quaternion rollbackRotation)
        {
            _transform = transform; // 이동하려는 transform 객체를 참조한다.
            _oldRotation = rollbackRotation; // Undo할 때 돌아갈 rotation을 저장한다.
            _newRotation = _transform.rotation; // execute 할 때에 세팅될 rotation 값을 저장한다.
        }
        
        public void Execute()
        {
            _transform.rotation = _newRotation; // _newRotation으로 갱신한다.
        }
    
        public void Undo()
        {
            _transform.rotation = _oldRotation; // _oldRotation으로 Undo한다.
        }
    }

     

    └ Interface

    : Interface에서 선언한 함수에 대한 구현이 없으면 Class를 사용할 수 없게끔 만드는 것 --> Abstract와 비슷함

    장점 : Class는 다중 상속이 어려우나, Interface는 다중 상속이 가능하기 때문에 주로 사용된다.

    >> 이름 맨 앞에 'I'를 붙이는 것이 공식

    >> 변수 선언, 함수 구현 불가능, Property와 함수 선언만 가능 ※ Property : { get, set };

    ※ vs Abstract : Interface는 default가 public이라 public을 안 써도 다른 곳에서 다 쓸 수 있다. + abstract나 override도 쓸 필요 없다.

     

    └ Command Pattern

    : 객체의 상태를 변화시키는 행위를 다른 객체(Command 객체)에 두는 것

    >> Command 객체는 행위를 저장하고, 그 행위와 관련된 부가적인 정보도 함께 저장한다.

    https://blog.naver.com/okcharles/222025378159

     

    C# 디자인 패턴 - Command Pattern

    https://scottlilly.com/c-design-patterns-the-command-pattern/ 위 블로그의 번역 요약 이 디자인 패턴은...

    blog.naver.com

     

    └ 추가 개선점

    1. 다시 코드를 보니 Redo가 구현되어 있지 않다. --> 다음에 내가 구현해보자

    2. 회전과 이동의 Undo가 같이 구현되어 있다. --> 따로 작동하도록 구현해보자


    LINQ (Language Integrated Query)

    : C#에서 데이터 쿼리 및 조작을 위한 강력한 기능이다.

    주요 특징

    • 데이터 소스에 대한 통일된 쿼리 구문
    • 컬렉션, 배열, XML 등 다양한 데이터 소스 지원
    • 강력한 필터링, 정렬, 그룹화 기능
    • 코드 가독성과 유지보수성 향상

    LINQ가 없다면..

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public struct MonsterTest
    {
        public string name;
        public int health;
    }
    
    public class LinqExample : MonoBehaviour
    {
        public List<MonsterTest> monsters = new List<MonsterTest>()
        {
            new MonsterTest() { name = "A", health = 100 },
            new MonsterTest() { name = "A", health = 30 },
            new MonsterTest() { name = "B", health = 100 },
            new MonsterTest() { name = "B", health = 30 },
            new MonsterTest() { name = "C", health = 100 },
            new MonsterTest() { name = "C", health = 30 },
        };
        
        void Start()
        {
            // 몬스터 테스트 그룹에서 A 네임을 가진 hp 30이상의 오브젝트들을 리스트화해서 체력 높은순으로 출력하기
    
            List<MonsterTest> filters = new List<MonsterTest>();
            for (var i = 0; i < monsters.Count; i++)
            {
                if (monsters[i].name == "A" && monsters[i].health >= 30)
                {
                    filters.Add(monsters[i]);
                }
            }
            
            filters.Sort((l,r) => l.health >= r.health ? -1 : 1);
            for (var i = 0; i < filters.Count; i++)
            {
                Debug.Log($" Name : {filters[i].name}, Health : {filters[i].health}");
            }
        }
    }

    ※ List<T>.Sort 함수

    https://learn.microsoft.com/ko-kr/dotnet/api/system.collections.generic.list-1.sort?view=net-8.0

     

    List<T>.Sort 메서드 (System.Collections.Generic)

    지정된 또는 기본 IComparer<T> 구현 또는 제공된 Comparison<T> 대리자를 사용하여 List<T>의 요소 또는 요소의 일부를 정렬하여 목록 요소를 비교합니다.

    learn.microsoft.com

    └ Lambda

    : 무명 함수 / 익명 함수를 생성하는 간결한 문법으로, 함수를 직접 정의하지 않고도 간단히 함수를 전달하거나 정의할 수 있다.

    >> 기본 구조

    (parameters) => expression_or_statement_block
    • parameters : Lambda 식이 받을 매개변수 목록 --> 매개변수가 없을 수도 있다.
    • '=>' : 람다 연산자(arrow operator)로, 매개변수와 식 또는 문장 블록을 분리한다.
    • expression_or_statement_block : lambda 식의 본문으로, 표현식이나 문장 블록으로 구성된다.

    https://learn.microsoft.com/ko-kr/dotnet/csharp/language-reference/operators/lambda-expressions

     

    람다 식 - 람다 식 및 무명 함수 - C# reference

    익명 함수 및 식 본문 멤버를 만드는 데 사용되는 C# 람다 식입니다.

    learn.microsoft.com

     

    LINQ를 사용한다면..

    • Select : 사용할 데이터를 선택
    • Where : 데이터를 조건에 맞는 요소들만 필터링
    • OrderByDescending : 지정된 기준에 따라 요소들을 내림차순으로 정렬 <-> OrderBy : 지정된 기준에 따라 요소들을 오름차순으로 정렬
    • Average : 숫자 시퀀스의 평균을 계산
    using System.Collections;
    using System.Collections.Generic;
    using System.Linq; // LINQ 사용을 위해 선언
    using UnityEngine;
    
    public struct MonsterTest
    {
        public string name;
        public int health;
    }
    
    public class LinqExample : MonoBehaviour
    {
        public List<MonsterTest> monsters = new List<MonsterTest>()
        {
            // new MonsterTest() { name = "A", health = 100 },
            // --> 원래 위처럼 구현했으나, 간소화 가능
            new() { name = "A", health = 100 },
            new() { name = "A", health = 30 },
            new() { name = "B", health = 100 },
            new() { name = "B", health = 30 },
            new() { name = "C", health = 100 },
            new() { name = "C", health = 30 }
        };
        
        void Start()
        {   
            // LINQ 사용 [함수형 LINQ 사용된 방식] --> Select 사용
            // var linqFilter = monsters.Select(e => e).
            // Where(e => e.name == "A" && e.health >= 30).
            // OrderByDescending(e => e.health).ToList();
            
            // LINQ 사용 [함수형 LINQ 사용된 방식] --> Select 생략
            var linqFilter = monsters.Where(e => e is {name: "A", health: >= 30}).
                OrderByDescending(e => e.health).ToList();
            
            // LINQ 사용 [Query를 예약어로 쓰는 방식]
            var linqFilter2 = (
                from e in monsters 
                where e is { health: >= 30, name: "A" }
                orderby e.health 
                descending 
                select e
                ).ToList();
             // select new { e.name, e.health } ).ToList(); --> 이렇게도 작성 가능
            
            for (var i = 0; i < linqFilter.Count; i++) // for로 작성 예시
            {
                Debug.Log($"Name : {linqFilter[i].name}, Health : {linqFilter[i].health}");
            }
    
            foreach (var t in linqFilter2) // foreach로 작성 예시
            {
                Debug.Log($"Name : {t.name}, Health : {t.health}");
            }
        }
    }

     

    └ LINQ Query문

    : 데이터 소스에서 데이터를 검색하는 식

     

    쿼리 작업의 세 부분

    1. 데이터 소스 가져오기
    2. 쿼리 만들기
    3. 쿼리 실행하기

    https://learn.microsoft.com/ko-kr/dotnet/csharp/linq/get-started/introduction-to-linq-queries

     

    LINQ 쿼리 소개 - C#

    LINQ는 다양한 데이터 원본 및 형식의 데이터 쿼리에 대한 일관된 모델을 제공합니다. LINQ 쿼리에서는 항상 개체로 작업합니다.

    learn.microsoft.com

     

    └ Tuple (튜플)

    // 일반적인 사용법
    (float, int) attack = (3.5f, 8);
    life -= attack.Item1 * attack.Item2;
    
    // 또 다른 사용법
    (float Power, int Count) attack = (3.5f, 8);
    life -= attack.Power * attack.Count;
    
    var (power, count) = (3.5f, 8);
    life -= power * count;
    
    // 스왑도 간단하게 표현 가능
    (a, b) = (b, a)

    https://learn.microsoft.com/ko-kr/dotnet/csharp/language-reference/builtin-types/value-tuples

     

    튜플 형식 - C# reference

    C# 튜플: 느슨하게 관련된 데이터 요소를 그룹화하기 위해 사용할 수 있는 경량 데이터 구조입니다. 튜플은 여러 공용 멤버를 포함하는 형식을 도입합니다.

    learn.microsoft.com


    Coroutine을 사용하여 Battle Timer 사용하기

    >> 전체 코드

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class GameManager : MonoBehaviour
    {
        public float battleTime = 30.0f;
    
        IEnumerator BattlerTimer()
        {
            while (battleTime >= 0.0f)
            {
                Debug.Log(battleTime);
                
                yield return new WaitForSeconds(1.0f); // 이 함수는 1초동안 쉰다.
                
                battleTime -= 1.0f;
            }
        }
        
        void Start()
        {
            // 코루틴 함수를 시작한다.
            StartCoroutine(BattlerTimer());
        }
    	
        // Update()로 coroutine 만들기
        private float _stepBattleDuration = 1.0f;
        
        void Update()
        {
            // Update()로 coroutine 만들기
            // if (0 >= battleTime)
            //     return;
            //
            // if (_stepBattleDuration >= 1.0f)
            // {
            //     Debug.Log(battleTime);
            //     
            //     battleTime -= 1.0f;
            //     _stepBattleDuration = 0.0f;
            // }
            //
            // _stepBattleDuration += Time.deltaTime;
        }
    }

    ※ Coroutine 관련 문법

    • yield return : coroutine에서 반환한다는 의미
    • yield return new WaitForSeconds(float); : 'float' 초 동안 쉰다.
    • yield return new WaitUntil(); : 어떠한 값이 참이 될 때까지 기다리는 YieldInstruction
    • yield return new FixedUpdate(); : 물리 적용이 끝난 시점까지 기다리는 Coroutine

    ※ Time.deltaTime;

    1초당 60프레임이면 1/60 = time.deltaTime이 된다.
    1초당 120프레임이면 1/120 = time.deltaTime이 된다.

     

    [예제] 인간 경마 순위

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Linq;
    using UnityEngine;
    using Random = UnityEngine.Random;
    
    
    [Serializable]
    public class PlayerData
    {
        public string playerName;
        
        [NonSerialized]
        public float Distance;
    }
    
    public class GameManager : MonoBehaviour
    {
        public float battleTime = 30.0f;
        
        // 경마에 참여할 플레이어 리스트
        public List<PlayerData> Players = new List<PlayerData>();
    
        IEnumerator BattlerTimer()
        {
            while (battleTime >= 0.0f)
            {
                Debug.Log(battleTime);
                
                yield return new WaitForSeconds(1.0f); // 이 함수는 1초동안 쉰다.
                
                foreach (var playerData in Players)
                {
                    playerData.Distance += Random.Range(0.0f, 1.0f);   
                }
                
                var ranks = (from p in Players orderby p.Distance select p).ToList ();
    
                for (var i = 0; i < ranks.Count; i++)
                {
                    Debug.Log($"Rank {i+1} : {ranks[i].playerName} / distance : {ranks[i].Distance}");
                }
                
                battleTime -= 1.0f;
            }
        }
        
        void Start()
        {
            // 코루틴 함수를 시작한다.
            StartCoroutine(BattlerTimer());
        }
    }

    ※ 사실 Sort를 쓰면 되지만 억지로 query를 써본 것

    └ Unity에서 구현

    UI 만들기

    1. Canvas 만들기 --> UI - Canvas

    2. Canvas 안에 Button 만들기 --> 뜨는 TMP Impoter 창에서 Import 해주기

    3. Button의 Inspector에서 Size Width 200, Height 100 정도로 수정

    4. Canvas 안에 Image 만들고 Anchor 설정해주고 Left, Right, Top, Bottom 전부 0으로 맞추기

    5. 폰트 넣기

    https://cho22.tistory.com/61

     

    [Unity] UGUI TextMeshPro 한글 폰트 추가하기

    UGUI 한글 입력 문제를 해결하기위해 TextMeshPro로 텍스트를 변경하고 한글을 입력했더니 한글이 안나오는 이슈가 있었다. 이유는 셋팅된 폰트에 한글이 없기 때문이었고, 한글폰트를 추가해주기

    cho22.tistory.com

    font asset creator --> resolution 4096 4096 / custom range / sequence에 값 넣기

    ※ 값 : 32-126,44032-55203,12593-12643,8200-9900

    6. canvas 안에 panel 만들고 InspectorImage 삭제, Canvas Renderer 삭제, vertical layout group 추가해서 Control Child Size를 Width, Height 둘다 체크 / Anchor 가운데꺼, width 500, height 800, PosY -5 세팅

    ※ Control Child Size를 Width, Height 둘다 체크 : 버튼이 늘어도 화면을 벗어나지 않게 세팅

    7. 이후 Buttonpanel에 상속

    8. Button InspectorRaceButton Script 추가(Add Component Script를 만듦)

    9. 코드 작성

    using System.Collections;
    using System.Collections.Generic;
    using TMPro;
    using UnityEngine;
    
    public class RaceButton : MonoBehaviour
    {
        public TMP_Text text;
    }

    10. Button Inspector에서 Script Text 바인딩

    11. Button Prefab 하고, (이름은 RaceButton) 있던 Button 삭제

     

    Unity로 띄우기

    1. 코드 작성

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using System.Linq;
    using Random = UnityEngine.Random;
    
    [Serializable]
    public class PlayerData
    {
        public string playerName;
    
        [NonSerialized] public float Distance;
    }
    
    public class GameManager : MonoBehaviour
    {
        public float battleTime = 30.0f;
    
        // 경마에 참여할 플레이어 리스트
        public List<PlayerData> players = new List<PlayerData>();
        // UI에 표현될 Button Prefab --> RaceButton이 포함된 Prefab만 들어가도록
        public RaceButton templateButton;
        // Button들이 붙을 부모Object
        public Transform raceButtonParent;
        // 생성된 버튼들 관리
        private List<RaceButton> _raceButtons = new List<RaceButton>();
    
        IEnumerator BattlerTimer() // IEnumerator : coroutine에서 반복한다는 의미
        {
            for (var i = 0; i < players.Count; i++)
            {
                // Object 생성하기
                var newObj = Instantiate(templateButton.gameObject, raceButtonParent);
                // RaceButton Component 캐싱하기
                _raceButtons.Add(newObj.GetComponent<RaceButton>());
            }
    
            while (battleTime >= 0.0f)
            {
                Debug.Log(battleTime);
    
                yield return new WaitForSeconds(1.0f); // 이 함수는 1초 동안 쉰다.
    
                foreach (var playerData in players)
                {
                    playerData.Distance += Random.Range(0.0f, 1.0f);
                }
    
                var ranks = (from p in players orderby p.Distance descending select p).ToList();
    
                for (var i = 0; i < ranks.Count; i++)
                {
                    Debug.Log($"Rank {i+1} : {ranks[i].playerName} / distance : {ranks[i].Distance}");
                    _raceButtons[i].text.text = ranks[i].playerName; // ranks[i].playerName 대신 "" 를 써서 비워도 됨
                }
    
                battleTime -= 1.0f;
            }
        }
    
        void Start()
        {
            StartCoroutine(BattlerTimer()); // Coroutine 함수를 시작
        }
    }

     

    2. Game Manager에 있는 Players 할당 및 이름 작성

    3. Game Manager에 있는 Template Button, Race Button Parent 에 각각 RaceButton Prefab, Panel 바인딩

     

    └ 동적인 움직임을 가지도록 구현

    ※ 선형 보간을 사용(?)

     

    >> 버튼 코드

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using TMPro;
    using UnityEngine;
    using UnityEngine.UI;
    
    public class RaceButton : MonoBehaviour
    {
        public TMP_Text text;
        public RectTransform rect;
        public Button clickButton;
        
        private void Awake() // 생성자랑 같다.
        {
            rect = GetComponent<RectTransform>();
            clickButton = GetComponent<Button>();
        }
    }

     

    >> GameManager 코드

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Linq;
    using UnityEngine;
    using UnityEngine.UI;
    using Random = UnityEngine.Random;
    
    
    [Serializable]
    public class PlayerData
    {
        public string playerName;
        
        [NonSerialized] public float Distance;
    
        [NonSerialized] public int Rank;
    
        [NonSerialized] public RaceButton RaceButton;
    }
    
    public class GameManager : MonoBehaviour
    {
        public float battleTime = 30.0f;
    
        // 경마에 참여할 플레이어 리스트
        public List<PlayerData> Players = new List<PlayerData>();
    
        // ui에 표현 될 버튼 프리팹
        public RaceButton templateButton;
    
        // 버튼들이 붙을 부모오브젝트
        public Transform RaceButtonParent;
    
        IEnumerator GoToNextPosition(PlayerData pd, int newRank, Vector2 newPosition)
        {
            pd.Rank = newRank;
            pd.RaceButton.text.text = $"{pd.playerName} / {pd.Distance.ToString("0.00") + " km"}";
    
            RectTransform target = pd.RaceButton.rect;
            float time = 0.0f;
            const float lerpTime = 0.3f;
            // 처음 시작하는 좌표를 기억
            Vector2 initPosition = target.anchoredPosition;
    
            // 0.3초 안에 이동하도록
            while (lerpTime >= time)
            {
                // lerp 보간을 이용해서 0 ~ 1 사잇값이 나와서 position이 자연스럽게 옮겨가도록 한다.
                target.anchoredPosition = Vector2.Lerp(initPosition, newPosition, time / lerpTime);
    
                // 시간을 더해준다.
                time += Time.deltaTime;
                yield return null;
            }
    
            // 혹시 끝났을 때 좌표가 안 맞을 경우를 대비해서 newPosition으로 완전히 세팅한다.
            target.anchoredPosition = newPosition;
        }
    
        IEnumerator BattlerTimer()
        {
            // UI들이 자동으로 정렬된 Position을 가지기 위해 처음 저장해 놓는 UI들의 위치
            List<Vector2> ui_positions = new List<Vector2>();
    
            for (var i = 0; i < Players.Count; i++)
            {
                // 오브젝트 생성하기
                var newObj = Instantiate(templateButton.gameObject, RaceButtonParent);
    
                RaceButton raceButton = newObj.GetComponent<RaceButton>();
                raceButton.text.text = Players[i].playerName; // 0점일 때 이름순으로 배치
    
                // RaceButton 컴포넌트 캐싱하기
                Players[i].RaceButton = raceButton;
    
                Players[i].Rank = i;
            }
    
            // 한프레임 쉬겠다.
            yield return null;
    
            for (var i = 0; i < Players.Count; i++)
            {
                ui_positions.Add(Players[i].RaceButton.rect.anchoredPosition);
            }
    
            // 정렬해준건 고맙지만 너는 여기까지만 역할이야
            RaceButtonParent.GetComponent<VerticalLayoutGroup>().enabled = false;
    
            while (battleTime >= 0.0f)
            {
                Debug.Log(battleTime);
    
                // 이 함수는 1초동안 쉰다.
                yield return new WaitForSeconds(1.0f);
    
                foreach (var playerData in Players)
                {
                    playerData.Distance += Random.Range(0.0f, 1.0f);
                    Debug.Log(playerData.Distance);
                }
    
                var ranks = (from p in Players orderby p.Distance descending select p).ToList();
    
                // 현재 정해진 i가 랭크 순위이므로 그 위치로 이동시킨다.
                for (var i = 0; i < ranks.Count; i++)
                {
                    StartCoroutine(GoToNextPosition(ranks[i], i, ui_positions[i]));
                }
    
                battleTime -= 1.0f;
            }
        }
    
        void Start()
        {
            // 코루틴 함수를 시작한다.
            StartCoroutine(BattlerTimer());
        }
    }