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의 앵커, 패딩, 레이아웃 설정에 대한 공부 필요!