이 것까지 포스팅하는 시점을 기다렸따... 이제부터 아이템 같은 것을 구현해서 간단하게 가방 만들고 넣고 하는 시스템 만들고 그것에 대해서 설명할 것이다. 그러면서 하나의 프로젝트도 달아놓겠다. 이 프로젝트는 이제부터 포스팅하는 것들을 최대한 활용해서 개발할 간단한 게임이다.
데이터와 관련 기능을 캡슐화할 수 있는 값 형식이다. 구조체 형식은 struct 키워드를 사용하여 정의한다.
public struct PotionItem
{
public string name;
public int recoveryPoint;
}
데이터 묶음이다. 여러 변수(필드)를 하나의 구조로 묶는다.
이렇게 하나의 묶어주는 것이 하나 더 있는데 그것을 class라고 하며 class와 유사하지만 값 타입이다. 일반적으로 작고, 불변이며, 자주 생성 되는 데이터를 표현 할 때 사용한다.
구조체를 사용하면서 전반적인 사용과 어떠한 특징들이 있는지 확인해보겠다.
플레이어는 어떠한 데이터의 묶음일까? 사람들이 많이 아는 메이플을 한번 봐볼까?

흠...ㅋㅋㅋㅋㅋㅋ? STR, DEX, INT, LUK HP, MP, 인기도, 하이퍼스텟, 상세스텟... 등등등
그만 알아보도록 하자!!
나는 여기서 공격력, 방어력, 체력/최대체력, 이름, 소지금 정도로 만들겠다.
public struct Player
{
public string Name;
public int HealthPoint; // 체력
public int HealthMaxPoint; // 최대 체력
public int AttackPower; // 공격력
public int DefensePoint; // 방어력
public int HaveGold;
}
이러한 느낌으로 만들고, 아래와 같이 넣어준다.
_player.Name = "DevIRU";
_player.HealthMaxPoint = 10;
_player.HealthPoint = _player.HealthMaxPoint;
_player.AttackPower = 10;
_player.DefensePoint = 2;
포션을 만들 때는 어떤 것들이 필요할까? 일단 메이플을 참고해볼까?
이런 식으로 나와 있다. 그럼 여기서 알 수 있는 것은 몇을 회복 시키는지, 포션의 이름이다. 그런데 나는 여기서 가격을 추가 하겠다. 왜냐하면 플레이어 쪽에 넣어준 HaveGold를 사용해 보고싶기 떄문이다.
public struct Potion
{
public string Name;
public int PotionPoint;
public int Price;
}
이러한 식으로 3개의 필드가 들어간 묶음, Potion을 만들어주었다.
이제 이것들 한쪽에서 편하게 관리하기 위해서 배열을 선언하고, 값 들을 넣어준다.
// 각각 체력 회복 ,공격 상승, 방어력 상승
_potions = new Potion[3];
_potions[0].Name = "체력 회복 포션";
_potions[0].PotionPoint = 2;
_potions[0].Price = 5;
_potions[1].Name = "공격 상승 포션";
_potions[1].PotionPoint = 3;
_potions[1].Price = 15;
_potions[2].Name = "방어력 상승 포션";
_potions[2].PotionPoint = 1;
_potions[2].Price = 10;
이제 선언 및 초기화도 진행을 잘했고~, 그럼 넣어준 값들이 잘 출력 되는지 보자.
private void PrintPlayer()
{
Console.WriteLine($"플레이어 이름 {_player.Name}");
Console.WriteLine($"플레이어 체력 {_player.HealthPoint} / {_player.HealthMaxPoint}");
Console.WriteLine($"플레이어 공격력 {_player.AttackPower}");
Console.WriteLine($"플레이어 방어력 {_player.DefensePoint}");
Console.WriteLine($"플레이어 소지금 {_player.HaveGold}");
}
private void PrintPotion(Potion potion)
{
Console.WriteLine($"포션 이름: {potion.Name}");
Console.WriteLine($"포션 포인트: {potion.PotionPoint}");
Console.WriteLine($"가격: {potion.Price}");
}
private void PrintPotions()
{
if (_potions == null) return;
foreach (Potion potion in _potions)
PrintPotion(potion);
}
플레이어 이름 DevIRU
플레이어 체력 10 / 10
플레이어 공격력 10
플레이어 방어력 2
플레이어 소지금 0
포션 이름: 체력 회복 포션
포션 포인트: 2
가격: 5
포션 이름: 공격 상승 포션
포션 포인트: 3
가격: 15
포션 이름: 방어력 상승 포션
포션 포인트: 1
가격: 10
잘 출력이 된다.
그런데 데이터의 묶음에 기능을 추가할 수 있을까?
왜냐하면 위에 포션의 데이터 정보를 포션이 아닌 녀석이 출력을 해주고 있으니까 말이다.
정답은~ 가능하다. 왜 가능할까?
값 타입도 캡슐화(기능 + 데이터)를 할 수 있게 하려는 의도 때문이다.
C#에서는 객체지향적 사고를 적극 도입했기 때문에 값 타입도 객체처럼 동작할 수 있도록 설계했다.
객체지향적 사고는 클래스를 다루고 다루겠다.
그렇다면 어떻게 코드가 변할까?
public struct Potion
{
public string Name;
public int PotionPoint;
public int Price;
private void Print()
{
Console.WriteLine($"포션 이름: {Name}");
Console.WriteLine($"포션 포인트: {PotionPoint}");
Console.WriteLine($"가격: {Price}");
}
}
private void PrintPotions()
{
if (_potions == null) return;
foreach (Potion potion in _potions)
potion.Print(); // PrintPotion(potion); 이 코드에서 변화됨.
}
이러한 식으로 변화한다. 기존의 포션 출력을 담당하는 PrintPotion을 struct 안으로 옮기면서 이름도 변경하고 밖에서는 함수에서 접근하기 위해서 매개변수로 포션의 정보를 담은 struct를 넘겨줬지만, potion안에 옮기면서 매개변수가 필요 없어졌다. 그 이유는 내부에 함수를 선언 하여서 바로 접근 가능한 범위이기 때문이다. 그렇다면 플레이어 옮기겠다.
public struct Player
{
public string Name;
public int HealthPoint; // 체력
public int HealthMaxPoint; // 최대 체력
public int AttackPower; // 공격력
public int DefensePoint; // 방어력
public int HaveGold;
// 플레이어 정보를 콘솔에서 확인할 수 있는 함수
public void Print()
{
Console.WriteLine($"플레이어 이름 {Name}");
Console.WriteLine($"플레이어 체력 {HealthPoint} / {HealthMaxPoint}");
Console.WriteLine($"플레이어 공격력 {AttackPower}");
Console.WriteLine($"플레이어 방어력 {DefensePoint}");
Console.WriteLine($"플레이어 소지금 {HaveGold}");
}
}
이제 가격도 있으니, 간단하게 상점 주인을 만들어주고
대충 대사도 넣어주고
말도 안되게 큰 가방을 매고 있는 상인이 보인다
[상인]: 안녕하신가? 모험가여?
[상인]: 뭐 살것이 있는가? 없어도 한번 보고가지
쿵!.. (엄청 큰 가방을 내려놓는 소리)
어떠한 기능을 만들 것인가?
private void Shop(ref Player player)
{
Console.WriteLine("말도 안되게 큰 가방을 매고 있는 상인이 보인다");
Console.WriteLine("[상인]: 안녕하신가? 모험가여?");
Console.WriteLine("[상인]: 뭐 살것이 있는가? 없어도 한번 보고가지");
Console.WriteLine("쿵!.. (엄청 큰 가방을 내려놓는 소리)");
Console.WriteLine();
Console.WriteLine("==========물품 리스트==========");
// 가지고 있는 물품 출력
Potion potion;
for (int i = 0; i < _potions.Length; i++)
{
potion = _potions[i];
Console.WriteLine($"{i + 1}. [{potion.Name}] [G {potion.Price}]");
}
Console.WriteLine();
Console.WriteLine("==========플레이어 정보==========");
// 플레이어가 가지고 있는 소지금 현황
// 여기서는 기존에 만들어 놓은 함수 사용
player.Print();
// 잘못 입력했는지 판별
Console.Write("\n[구매 할 물품 번호 입력]: ");
int tryBayIdx;
bool isUsable= int.TryParse(Console.ReadLine(), out tryBayIdx);
if (!isUsable || 1 > tryBayIdx || _potions.Length < tryBayIdx)
{
Console.WriteLine("잘못된 선택을 하였습니다!!");
return;
}
// 가지고 있는 물품 판매가능 여부 판단
tryBayIdx -= 1;
Potion bayPotion = _potions[tryBayIdx];
if (bayPotion.Equals(default))
{
Console.WriteLine("선택한 포션의 재고가 없습니다!!");
return;
}
// 플레이어가 사고싶은 것을 구매 가능 여부 판단
if (_player.HaveGold < bayPotion.Price)
{
Console.WriteLine($"양심이 없는가? G {bayPotion.Price - _player.HaveGold}가 부족하지 않나!");
Console.WriteLine("돈이 있을 때, 다시보지!");
return;
}
// 구매
_player.HaveGold -= bayPotion.Price;
Console.WriteLine($"오? 모함가, [{bayPotion.Name}] 구매는 좋은 선택이야");
}
플레이어에게 소지금은 넣어주자.
_player.Name = "DevIRU";
_player.HealthMaxPoint = 10;
_player.HealthPoint = _player.HealthMaxPoint;
_player.AttackPower = 10;
_player.DefensePoint = 2;
_player.HaveGold = 33; // 소지금 33 골드 넣줌
출력을 해보면?
말도 안되게 큰 가방을 매고 있는 상인이 보인다
[상인]: 안녕하신가? 모험가여?
[상인]: 뭐 살것이 있는가? 없어도 한번 보고가지
쿵!.. (엄청 큰 가방을 내려놓는 소리)
==========물품 리스트==========
1. [체력 회복 포션] [G 5]
2. [공격 상승 포션] [G 15]
3. [방어력 상승 포션] [G 10]
==========플레이어 정보==========
플레이어 이름 DevIRU
플레이어 체력 10 / 10
플레이어 공격력 10
플레이어 방어력 2
플레이어 소지금 33
[구매 할 물품 번호 입력]:
> 오? 모함가, [체력 회복 포션] 구매는 좋은 선택이야
> 잘못된 선택을 하였습니다!!
> 양심이 없는가? G 2가 부족하지 않나!
> 돈이 있을 때, 다시보지!
struct의 특징과 여러 사용법을 추가 적으로 설명하고 포스팅을 종료하겠다.
| 구분 | 설명 |
|---|---|
| 값 형식(Value Type) | struct는 값 형식이며 스택(Stack)에 저장됨 (예외: boxing 시 힙) |
| 복사 방식 | 할당하거나 전달할 때 값 복사가 일어남 |
| 상속 불가 | 다른 클래스나 구조체로부터 상속 불가능 |
| 인터페이스 구현 가능 | 인터페이스는 구현 가능 |
| 매개변수 없는 생성자 불가 | 기본 생성자를 직접 정의할 수 없으며, 컴파일러가 자동 제공 |
| 필드 초기화 제한 | 선언 시 필드 초기화 불가 (생성자 내부에서만 가능) |
| 불변 타입에 유리 | 변경되지 않는 데이터를 표현할 때 유리 (예: Vector2, Point 등) |
| 작고 단순한 데이터에 적합 | 메모리 효율상 작고 단순한 데이터 집합에 적합 |
| 박싱(Boxing) 주의 | 참조형으로 변환(Boxing) 시 성능 저하 발생 가능 |
여기에서 박씽과 언박씽의 개념은 클래스를 다루면서 같이 다루겠다.
struct MyStruct
{
public int Value;
}
MyStruct data = new MyStruct { Value = 123 };
// Boxing 발생!
object boxed = data;
Console.WriteLine(((MyStruct)boxed).Value); // Unboxing
과 같이 간단한 박싱과 언박싱 관련 코드이다. 이런 상황에서 힙이라는 메모리에 적용된다는 것이다.
흠... 아니 나중에 이러한 개념을 하면 하이퍼링크를 걸어놓겠다. 이렇게 하나하나 설명을 늘려가면 말이 안되게 늘어날것 같다.