TIL

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

앵발자 2024. 12. 5. 22:56

 

자료구조 4일 차 Start!

 

▼오늘 학습한 내용

Undo, Redo 만들기

Undo, Redo 만들기 ( With Command 패턴 )

지난 시간 과제였던 Undo, Redo 만들기를 오전에 같이 진행했다.

 

전체코드

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 ExcuteCommand(ICommand command)
    {
        // 커맨드 객체의 excute를 실행
        command.Execute();
        // 언두했을 때 command의 undo를 호출하기 위해 undo stack에 넣는다
        undoStack.Push(command);
        // excuteCommand가 호출 되면 가장 최신의 작업을 한 것이므로 redoStack은 비운다
        redoStack.Clear();
    }

    public void Undo()
    {
        if (undoStack.Count > 0)
        {
            // 가장 최근에 실행 된 커멘드를 가져와
            ICommand command = undoStack.Pop();
            // Undo시킨다
            command.Undo();
            // 다시 redo 할수 있기 때문에 redoStack에 넣는다
            redoStack.Push(command);
        }
    }

    public void Redo()
    {
        if (redoStack.Count > 0)
        {
            // 가장 최근에 언두 된 커맨드를 가져와
            ICommand command = redoStack.Pop();
            // Excute해준다
            command.Execute();
            // 다시 Undo할수 있도록 undoStack에 넣어준다
            undoStack.Push(command);
        }
    }
    
    
    // CommandManager의 update 함수 교안에 적용됨
    

    public float Speed = 3.0f;
    public float RotateSpeed = 3.0f;

    // 이동한만큼 롤백하기 위해 이동한 양을 저장
    public Vector3 MoveDelta = Vector3.zero;
    
    public 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 = movePos.normalized * Speed * Time.deltaTime;
        
        // 움직였던 정보 기록하기 위해 키를 땔때마다 위치 기록
        if (movePos == Vector3.zero && MoveDelta != Vector3.zero)
        {
            // 롤백 포지션으로 돌아가기 위해 transform.position에서 MoveDelta를 빼주고
            var moveCommand = new MoveCommand(transform, transform.position - MoveDelta);
            ExcuteCommand(moveCommand);
            // 다시 기록하기 위해 MoveDelta 는 초기화 
            MoveDelta = Vector3.zero;
            //return;
        }

        if (deltaRot == Vector3.zero && RotateDelta != Vector3.zero)
        {
            var rotateCommand = new RotateCommand(transform, Quaternion.LookRotation(transform.forward - RotateDelta, Vector3.up));
            ExcuteCommand(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;
        // 언두할때 돌아갈 포지션을 저장한다.
        _oldPosition = rollbackPosition;
        // excute시에 셋팅될 포지션 값을 저장
        _newPosition = transform.position;
    }
    
    public void Execute()
    {
        // newPosition으로 갱신
        _transform.position = _newPosition;
    }

    public void Undo()
    {
        // oldPosition으로 undo
        _transform.position = _oldPosition;
    }
}

public class RotateCommand : ICommand
{
    private Transform _transform;
    private Quaternion _oldRotation;
    private Quaternion _newRotation;
    
    public RotateCommand(Transform transform, Quaternion rollbackRotation)
    {
        // 이동하려는 트랜스폼 객체를 참조
        _transform = transform;
        // 언두할때 돌아갈 회전값을 저장
        _oldRotation = rollbackRotation;
        // excute시에 셋팅될 회전 값을 저장
        _newRotation = _transform.rotation;
    }
    
    public void Execute()
    {
        // newPosition으로 갱신
        _transform.rotation = _newRotation;
    }

    public void Undo()
    {
        // oldPosition으로 undo
        _transform.rotation = _oldRotation;
    }
}

 

 

 

LINQ

LINQ

: C#에서 데이터 쿼리 및 조작을 위한 강력한 기능으로, 데이터베이스, 배열, 리스트 등의 데이터를 간결하고 직관적인 방식으로 필터링, 정렬, 변환할 수 있게 해주는 도구이다.

LINQ의 주요 특징

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

LINQ 사용하지 않은 코드

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}");
        }
    }

}

 

LINQ 사용 코드

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()
    {   
        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}");
        }

        var linqFilter = monsters.Where(
                e => e is { name: "A", health: >= 30 }
            ).
            OrderByDescending(
                e => e.health
                ).ToList();
        
        for (var i = 0; i < linqFilter.Count; i++)
        {
            Debug.Log($" Name : {linqFilter[i].name}, Health : {linqFilter[i].health}");
        }
        
        var linqFilter2 = (
            from e in monsters 
            where e is { health: >= 30, name: "A" }
            orderby e.health 
            descending 
            select new { e.name, e.health }
         ).ToList();
        
        foreach (var t in linqFilter2)
        {
            Debug.Log($" Name : {t.name}, Health : {t.health}");
        }
    }
}

 

Unity게임 LINQ 사용하는 간단한 예제

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

public class EnemyManager : MonoBehaviour
{
    public List<GameObject> enemies;

    void Start()
    {
        // 체력이 50 이하인 적들만 필터링
        var lowHealthEnemies = enemies.Where(e => e.GetComponent<Enemy>().health <= 50);

        // 적들을 체력 순으로 정렬
        var sortedEnemies = enemies.OrderBy(e => e.GetComponent<Enemy>().health);

        // 적들의 평균 체력 계산
        float averageHealth = enemies.Average(e => e.GetComponent<Enemy>().health);

        // 결과 출력
        Debug.Log($"Low health enemies count: {lowHealthEnemies.Count()}");
        Debug.Log($"First enemy health after sorting: {sortedEnemies.First().GetComponent<Enemy>().health}");
        Debug.Log($"Average enemy health: {averageHealth}");
    }
}

 

코루틴을 사용한 배틀 타이머 구현

using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;

public class GameManager : MonoBehaviour
{
    public float battleTime = 30.0f;

    IEnumerator BattlerTimer()
    {
        while (battleTime >= 0.0f)
        {
            Debug.Log(battleTime);
            
            // 이 함수는 1초동안 쉰다
            yield return new WaitForSeconds(1.0f);

            // 어떤 값이 참이 될때가지 기다리는 YieldInstruction
            // yield return new WaitUntil();

            // 물리 적용이 끝난 시점까지 기다리는 코루틴
            // yield return new FixedUpdate();
            
            battleTime -= 1.0f;
        }
    }
    
    void Start()
    {
        // 코루틴 함수 시작
        StartCoroutine(BattlerTimer());
    }

    private float _stepBattleDuration = 1.0f;
    
    void Update()
    {
        // 1초당 60프레임이다 1/60 = time.deltaTime이 된다.
        // 1초당 120프레임이면 1/120 = time.deltaTime
        // Time.deltaTime;

        // 업데이트를 이용한 방법
        // if (0 >= battleTime)
        //     return;
        //
        // if (_stepBattleDuration >= 1.0f)
        // {
        //     Debug.Log(battleTime);
        //     
        //     battleTime -= 1.0f;
        //     _stepBattleDuration = 0.0f;
        // }
        //
        // _stepBattleDuration += Time.deltaTime;
    }
}

 

인간 경마 순위

캔버스의 자식으로 이미지와 패널을 추가한 뒤, 패널에 버튼 5개를 세로로 정렬하여, 30초 동안 버튼의 순위를 실시간으로 매기는 간단한 게임을 구현했다.

코드

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Unity.VisualScripting;
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);
            
            // 1초동안 쉼
            yield return new WaitForSeconds(1.0f);
            
            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}");
            }
            
            // yield return new WaitUntil();

            // yield return new FixedUpdate();
            
            battleTime -= 1.0f;
        }
    }
    
    // Start is called before the first frame update
    void Start()
    {
        // 코루틴 함수 시작
        StartCoroutine(BattlerTimer());
    }
}

 

오류 발생

굉장히 멋있게 깨졌다.

다시만들고 다시만들고에 반복.. 

 

결과

어찌저찌 1시간동안 헤맨끝에 완성했다.

Panel안에 들어간 버튼이 밖으로 튀어나가서 깨졌던 것 같다.

이번주안에 Anchors 공부를 다시 해봐야겠다.

 

동적인 움직임을 가진 경마 버튼

public class RaceButton : MonoBehaviour
{
    public TMP_Text text;
    public RectTransform rect;
    public Button clickButton;
    
    private void Awake()
    {
        rect = GetComponent<RectTransform>();
        clickButton = GetComponent<Button>();
    }
}
[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;
        
        while (lerpTime >= time)
        {
            target.anchoredPosition = Vector2.Lerp(initPosition, newPosition, time / lerpTime);
            
            time += Time.deltaTime;
            yield return null;
        }
        
        target.anchoredPosition = newPosition;
    }
    
    IEnumerator BattlerTimer()
    {
        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;
            
            // 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);
            
            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]));
            }
            
            // 어떠한 값이 참이 될때가지 기다리는 YieldInstruction
            // yield return new WaitUntil();

            // 물리 적용이 끝난 시점까지 기다리는 코루틴
            // yield return new FixedUpdate();
            
            battleTime -= 1.0f;
        }
    }
    
    // Start is called before the first frame update
    void Start()
    {
        StartCoroutine(BattlerTimer());
    }
}

 

 

 

▼마무리

UI 요소를 다루는 것이 익숙하지 않아 발생한 오류로 진땀을 뺐다.

간단해보이는 UI도 배치도 정렬을하니 원하는 대로 제어가 되지 않아 답답함을 느꼈다. 
UI의 앵커, 패딩, 레이아웃 설정에 대한 공부 필요!