티스토리 뷰
1. 덱과 카드 관리
먼저 Mirror에서
Command나 ClientRpc 등의 속성을 가진 메서드를 사용할 때
class의 인스턴스를 매개변수로 전달할 수 없습니다.
기본적으로 직렬화 가능한 기본 자료형(int, float, string, bool)만 주고 받을 수 있기 때문입니다.
대신 카드 오브젝트를 int 형 ID로 주고받을 수 있습니다.
using UnityEngine;
public class Card : MonoBehaviour
{
// 카드의 고유 ID (외부에서 수정 불가, 읽기 전용)
public int Id { get; private set; }
// 카드의 이름
public string cardName;
// 카드 ID를 설정할 수 있는 초기화 메서드
public void Initialize(int id, string name)
{
Id = id; // 생성 시에만 ID 설정
cardName = name;
}
}
카드 객체마다 고유의 ID를 가질 수 있게 만듭니다.
그런 카드를 모으고 섞어 사용하는 것을 덱이라고 부릅니다.
즉 덱은 카드들의 자료구조라고 할 수 있습니다.
using System.Collections.Generic;
using UnityEngine;
public class CardManager : MonoBehaviour
{
// 카드 리스트
public List<Card> cardList = new List<Card>();
// 카드 프리팹 (씬에 배치된 Card 오브젝트를 프리팹으로 설정해야 함)
public Card cardPrefab;
void Start()
{
// 카드 오브젝트를 Instantiate(생성)하고 초기화
Card card1 = Instantiate(cardPrefab);
card1.Initialize(3, "Fireball");
cardList.Add(card1);
Card card2 = Instantiate(cardPrefab);
card2.Initialize(1, "Shield");
cardList.Add(card2);
Card card3 = Instantiate(cardPrefab);
card3.Initialize(2, "Heal");
cardList.Add(card3);
// 카드 ID 순으로 정렬
cardList.Sort((card1, card2) => card1.Id.CompareTo(card2.Id));
// 정렬된 카드 리스트 확인
foreach (Card card in cardList)
{
Debug.Log($"Card ID: {card.Id}, Name: {card.cardName}");
}
}
}
먼저 List<Card> 자료구조를 만듭니다.
이 List의 요소를 카드들의 고유 ID 순으로 정렬하면
ID와 같은 인덱스로 해당 카드에 접근할 수 있을 것입니다.
using System.Collections.Generic;
using UnityEngine;
public class CardManager : MonoBehaviour
{
// 카드 딕셔너리: ID를 키로, 카드를 값으로
public Dictionary<int, Card> cardDictionary = new Dictionary<int, Card>();
// 카드 프리팹 (씬에 배치된 Card 오브젝트를 프리팹으로 설정해야 함)
public Card cardPrefab;
void Start()
{
// 카드 오브젝트를 Instantiate(생성)하고 초기화 후 딕셔너리에 추가
Card card1 = Instantiate(cardPrefab);
card1.Initialize(3, "Fireball");
cardDictionary.Add(card1.Id, card1);
Card card2 = Instantiate(cardPrefab);
card2.Initialize(1, "Shield");
cardDictionary.Add(card2.Id, card2);
Card card3 = Instantiate(cardPrefab);
card3.Initialize(2, "Heal");
cardDictionary.Add(card3.Id, card3);
// 딕셔너리에서 특정 카드에 접근할 수 있음
if (cardDictionary.TryGetValue(2, out Card cardWithID2))
{
Debug.Log($"Card with ID 2: {cardWithID2.cardName}");
}
}
}
Dictionary<int, Card>를 사용하는 것도 좋은 방법입니다.
ID를 키 값으로 하여 Card를 저장하는 것입니다.
동적으로 카드 오브젝트를 생성하는 시점에
List나 Dictionary에 추가하는 것은 마땅한 방법이지만,
이미 Scene에 있는 카드들을 추가하는 방법은
어떤 방식으로든 카드들을 탐색해야할 것입니다.
FindObjectsOfType 같은 함수를 사용해서 말입니다.
그러나 카드의 수가 많아지면 부담이 되는 작업일 것입니다.
제작 중인 게임은 지정된 54장의 카드만 사용기 때문에
동적으로 카드를 생성하는 대신
Scene에 미리 카드 오브젝트를 배치하고
인스펙터에서 직접 참조시켰습니다.
2. SyncList<int>
게임에는 두 개의 자료구조가 필요합니다.
하나는 Card를 저장하는 자료구조이며
나머지 하나는 카드의 ID를 저장하는 int형 자료구조입니다.
Card의 자료구조는 ID 값을 사용하여 Card 자체를 반환받습니다.
int형 자료구조는 실제 덱의 역할을 하는 무작위 순서의 자료구조입니다.
덱의 카드를 int형 자료구조로 관리할 때
동기화에 대해 고민할 수 있습니다.
Mirror는 SyncList라는 자료구조를 제공합니다.
SyncList는 역시 기본 자료형(int, float, string, bool...)만 사용할 수 있습니다.
서버에서 SyncList가 수정되면 모든 클라이언트에서 동기화 됩니다.
카드 게임에서 덱의 보편적인 역할과 기능을 정리할 수 있습니다.
//게임에 사용되는 모든 카드를 담고 있는 Array와, ID를 인덱스로써 카드를 반환하는 함수
[SerializeField] Card[] cards;
public Card GetCard(int ID) { return cards[ID]; }
//현재 덱의 카드를 ID로 저장하는 int형 List
public readonly SyncList<int> deck = new SyncList<int>();
1. 카드를 모아두는 곳입니다. 게임의 특징에 따라 다르겠지만 플레이어의 패나 필드 등에 사용되지 않는 카드들이 모여 있는 곳입니다.
public int Count { get { return deck.Count; } }
[ServerCallback]
void Shuffle()
{
for (int i = 0; i < Count - 1; i++)
{
int rand = Random.Range(i, Count); // i부터 Count-1까지의 인덱스를 랜덤으로 선택
int tmp = deck[i];
deck[i] = deck[rand];
deck[rand] = tmp;
}
}
2. 무작위 순서로 섞여 있습니다.
/// <returns>덱 맨 위의 카드 ID를 반환합니다. 덱 맨 위의 카드란, 리스트의 첫 번째 요소입니다.</returns>
[Server]
public int DrawCardID()
{
int drawNumber = deck[0];
deck.Remove(drawNumber);
return drawNumber;
}
3. 카드 드로우는 보통 덱 맨 위의 카드를 가져오는 것을 가리킵니다. 뽑은 카드는 덱에서 제거합니다.
플레이어의 손 패로 카드를 가져오는 드로우 이외의 경우에도 사용할 수 있습니다.
이를 위해 이름을 맥락에 맞게 수정할 수 있을 것입니다.
예를 들어 덱 맨 위의 카드를 가리키는 정도의 의미로 말입니다...
/// <summary>
/// 덱에서 카드를 무작위로 꺼내는 경우...
/// </summary>
[Server]
public int RandomPickUpID()
{
int drawNumber = deck[Random.Range(0, Count)];
deck.Remove(drawNumber);
return drawNumber;
}
4. 무작위로 덱의 카드를 가져오는 경우도 있을 것입니다.
SyncList를 변경하는 작업은 서버에서 이루어져야 하므로 Server 속성의 함수로 구현할 필요가 있습니다.
[Server]
void ReturnID(int id, bool placeOnTop)
{
//이미 덱 안에 있는 카드라면 다시 덱에 넣을 수 없다.
if (deck.Contains(id)) return;
// placeOnTop이 true면 덱의 맨 위에, false면 랜덤 위치에 삽입
if (Count == 0)
{
deck.Add(id); // 덱이 비어있다면 그냥 추가
}
else
{
deck.Insert(placeOnTop ? 0 : Random.Range(0, Count), id);
}
}
5. 덱으로 카드를 되돌리는 경우도 있을 것입니다.
이럴땐 보통 덱에 넣고 섞는 경우가 있고 덱 맨위에 카드를 두는 경우도 있을 것입니다.
선택적 매개변수를 활용하여 보편적인 경우를 결정하도록 하였습니다.
대신 셔플보다는 임의의 위치에 삽입하는 것을 택하였습니다.
셔플의 반복작업보다 작업이 간단하다고 결과적으로 큰 차이가 없다고 생각했기 때문입니다.
'게임 개발 > 게임 개발 프로젝트' 카테고리의 다른 글
카드 게임 개발 일지 #03 (0) | 2024.09.10 |
---|---|
카드 게임 개발 일지 #01 (0) | 2024.09.07 |
카드 게임 개발 일지 #00 (0) | 2024.09.07 |
[팀 프로젝트] 'The Last Hunt' 개발 후기 - 03 (0) | 2024.08.23 |
[팀 프로젝트] 'The Last Hunt' 개발 후기 - 02 (0) | 2024.08.20 |