13일차 요약
인터페이스
- 구현부 없이 선언부만 존재
- 다형성 제공
커맨드 패턴
- 행동을 객체단위로 관리하는 패턴이다.
- 행동에 대한 추적이 편리하여 서버/클라이언트 구조에서 동기화가 용이하다.
- 확정성이 좋다.
Linq
- DB 쿼리와 유사한 형태를 가지는 C#의 쿼리 기능
- where, select, orderby 등 함수 및 키워드 지원
- 반복문 대신 데이터를 필터링할 때 용이
Coroutine
- Unity에서 동시성을 제공하는 기능
- 함수를 적절히 분리하여 실행 가능
- 시간 제어, 조건 제어 등 다양하게 활용 가능
- 코루틴을 사용하면 메모리에 올라가는 것이기 때문에 성능에 이슈가 있을 수 있으니 무분별하게 사용하지 않는 것이 좋다.
- 스레드와는 조금 다른 개념
- 코루틴은 단일 스레드에서 코루틴에 관한 오브젝트를 적절히 스케줄링하여 관리
- 따라서, 스위칭 비용이 적음
인터페이스
인터페이스 클래스는 함수의 구현부가 없이 선언만 존재하는 클래스이다.
이를 상속한 클래스에서 인터페이스에 존재하는 함수들을 필수로 구현해야 한다.
즉, 다수의 클래스들 사이의 상속관계가 명확하지는 않지만 사용하는 함수나 기능등에 대한 시그니처(함수이름)가 동일할 때 사용하면 좋다.
또한, 인터페이스는 다중 상속이 가능하기 때문에 활용성이 좋다.
public interface ICommand
{
void Exectue();
void Undo();
}
인터페이스 클래스를 만들기 위해서는 위와 같이 작성하면 된다.
커맨드 패턴
행동을 객체단위로 관리하는 패턴이다.
이 구조를 사용하면 새로운 명령(예: 점프, 공격 등)을 쉽게 추가할 수 있으며, 복잡한 액션 시퀀스나 매크로 기능도 구현할 수 있다.
또한, 네트워크 게임에서 플레이어 액션을 동기화하는 데에도 유용하게 사용될 수 있다.
public interface ICommand
{
void Exectue();
void Undo();
}
public class MoveCommand : ICommand
{
private Transform transform;
private Vector3 oldPosition;
private Vector3 newPosition;
public MoveCommand(Transform newTransform, Vector3 newPos)
{
transform = newTransform; //이동하려는 트랜스폼 객체 참조
oldPosition = transform.position; //undo시 되돌아갈 포지션 저장
newPosition = newPos; //새로 이동할 포지션 설정
}
public void Exectue()
{
transform.position = newPosition; //position 갱신
}
public void Undo()
{
transform.position = oldPosition; //position 재설정
}
}
이동에 대한 커맨드 클래스를 작성한 것이다.
여기에 추가로 공격, 점프 등 다양한 행동에 대해 커맨드 클래스를 만들고 모두 ICommand라는 인터페이스를 상속받게 하여 관리를 용이하게 할 수 있다.
Linq
LINQ(Language Integrated Query)는 C#에서 데이터 쿼리 및 조작을 위한 강력한 기능이다.
Unity 게임 개발에서도 LINQ를 사용하여 코드를 더 간결하고 효율적으로 만들 수 있다.
LINQ의 주요 특징
- 데이터 소스에 대한 통일된 쿼리 구문
- 컬렉션, 배열, XML 등 다양한 데이터 소스 지원
- 강력한 필터링, 정렬, 그룹화 기능
- 코드의 가독성과 유지보수성 향상
LINQ 예제
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
public class EnemyManager : MonoBehaviour
{
public List<GameObject> enemies;
void Start()
{
foreach (var monster in monsters.Where(m => m.health >= 50).OrderBy(m => m.health))
{
Debug.Log(monster.ToString());
}
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}");
}
}
}
예제 설명
- Where: 조건에 맞는 요소들만 필터링
- OrderBy: 지정된 기준에 따라 요소들을 정렬
- Average: 숫자 시퀀스의 평균을 계산
- Count(): 각각 시퀀스의 요소 수를 반환
- First(): 첫 번째 요소를 반환
Linq는 미리 구현된 함수를 체인으로 쿼리를 진행할 수도 있고 키워드를 통해 하나의 쿼리를 만들 수 있다.
linqFilter2는 후자의 방식을 선택한 것이다.
이때, select new로 쿼리의 결과를 새로운 임시 구조체를 만들 수 있다.
이때 새롭게 만들어지는 임시 구조체는 Anonymous type으로 생성된다.
Anonymous type은 이름이 정해지지 않은 임시 구조체라고 생각하면 된다.
https://learn.microsoft.com/ko-kr/dotnet/csharp/fundamentals/types/anonymous-types
추가적으로 Anonymous type과 비슷하게 임시로 구조체와 비슷한 것을 만드는 방식으로 tuple이 있다.
https://learn.microsoft.com/ko-kr/dotnet/csharp/language-reference/builtin-types/value-tuples
Coroutine
Coroutine을 사용하여 게임 전체의 타이머를 구현했다.
Corutine은 Atomic하지 않은 함수라고 생각하면 된다.
함수를 진행하다 특정 구간에서 잠시 실행을 멈추고 특정 조건이 만족했을 때 다시 실행하는 등의 방식으로 사용할 수 있다.
https://docs.unity3d.com/kr/2022.3/Manual/Coroutines.html
IEnumerator BattleTimer()
{
while (0f <= battleTime)
{
Debug.Log(battleTime);
// 이 함수는 1초동안 쉰다.
yield return new WaitForSeconds(1.0f);
// 어떠한 값이 참이 될때가지 기다리는 YieldInstruction
// yield return new WaitUntil();
// 물리 적용이 끝난 시점까지 기다리는 코루틴
// yield return new FixedUpdate();
battleTime -= 1.0f;
}
}
Linq 사용 예제
코루틴을 사용하여 1초마다 랜덤한 값을 얻고 합계가 많은 순으로 UI가 출력되도록 만들어 보자.
우선, UI를 구성해 보자.
Canvas를 만들고 배경 이미지를 만든다.
이후, Panel이라는 빈 UI용 오브젝트를 만들어 화면 중앙에 위치시킨다.
Panel은 순위를 출력할 버튼들이 들어가는 부모 UI이다.
이제, 순위를 출력할 버튼 UI를 Prefab으로 제작해 런타임에 생성하도록 만들어 보자.
버튼 Prefab의 모습이다.
버튼에서는 자신이 가진 text를 변경해야 하기 때문에 스크립트를 하나 만들어 text를 참조하도록 설정한다.
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
public class RaceButton : MonoBehaviour
{
public TMP_Text text;
}
이를 Manager에서 생성해 보자.
for (int i = 0; i < Players.Count; i++)
{
var newButton = Instantiate(templateButton, panel).GetComponent<RaceButton>();
newButton.text.text = Players[i].playerName;
raceButtons.Add(newButton);
}
Prefab을 이용하여 새로운 객체를 만들고 부모를 panel로 설정한다.
이후, RaceButton이라는 script 컴포넌트를 찾아 해당 스크립트의 text를 설정한다.
이는 코루틴 함수 시작 부분에서 진행된다.
이후, 반복문을 돌며 1초 간격으로 랜덤한 값을 추가하여 순위를 정렬한 후 UI에 출력한다.
while (0f <= battleTime)
{
yield return new WaitForSeconds(1.0f);
foreach (var player in Players)
{
player.distance += Random.Range(0f, 1f);
}
var ranks = (from p in Players
orderby p.distance descending
select p).ToList();
for(int i = 0 ; i < ranks.Count(); i++)
{
raceButtons[i].text.text = ranks[i].playerName;
Debug.Log($"Rank: {i+1}" + ranks[i].ToString());
}
battleTime -= 1.0f;
}
전체 코드는 다음과 같다.
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;
public override string ToString()
{
return $"Name: {playerName} / distance: {distance}";
}
}
public class GameManager : MonoBehaviour
{
[SerializeField] private float battleTime = 30.0f;
[SerializeField] private List<PlayerData> Players = new List<PlayerData>();
[SerializeField] private RaceButton templateButton;
[SerializeField] private Transform panel;
[SerializeField] private List<RaceButton> raceButtons = new List<RaceButton>();
IEnumerator BattleTimer()
{
for (int i = 0; i < Players.Count; i++)
{
var newButton = Instantiate(templateButton, panel).GetComponent<RaceButton>();
newButton.text.text = Players[i].playerName;
raceButtons.Add(newButton);
}
while (0f <= battleTime)
{
yield return new WaitForSeconds(1.0f);
foreach (var player in Players)
{
player.distance += Random.Range(0f, 1f);
}
var ranks = (from p in Players
orderby p.distance descending
select p).ToList();
for(int i = 0 ; i < ranks.Count(); i++)
{
raceButtons[i].text.text = ranks[i].playerName;
Debug.Log($"Rank: {i+1}" + ranks[i].ToString());
}
battleTime -= 1.0f;
}
}
void Start()
{
StartCoroutine(BattleTimer());
}
void Update()
{
}
}
추가로 버튼을 이동시켜 역동적으로 보이게 만들어 보자.
우선, 버튼을 이동시키기 위해서는 VerticalLayoutGroup을 꺼주어야 한다.
하지만, 초기에는 정렬이 되어야 하므로 버튼 생성 이후 정렬을 완료한 뒤 꺼주도록 하자.
그리고 각 버튼들이 랭킹에 맞게 움직이도록 버튼이 초기에 갖고 있던 position을 저장해 둔다.
이때, postion은 RectTransform의 anchoredPosition이다.
for (int i = 0; i < Players.Count; i++)
{
var newButton = Instantiate(templateButton, panel).GetComponent<RaceButton>();
newButton.text.text = Players[i].playerName;
raceButtons.Add(newButton);
Players[i].rank = i;
}
yield return null; //1프레임 쉬기(정렬 기다림)
for (int i = 0; i < Players.Count; i++)
{
uiPositions.Add(raceButtons[i].gameObject.GetComponent<RectTransform>().anchoredPosition);
}
panel.GetComponent<VerticalLayoutGroup>().enabled = false;
이후, 랭킹에 의해 정렬이 된 리스트를 통해 자신이 가야 할 곳을 정하고 또 다른 코루틴을 실행한다.
var ranks = (from p in Players
orderby p.distance descending
select p).ToList();
for(int i = 0 ; i < ranks.Count(); i++)
{
StartCoroutine(MoveToNext(raceButtons[ranks[i].rank].rect, uiPositions[i]));
ranks[i].rank = i;
}