📕학습 개요

유니티 캠프의 2주차가 시작되었다.

앞으로 2주 간 유니티관련 내용은 일절 학습하지 않으며, C#에 대한 공부와 프로젝트를 진행한다고 한다. 평소에 유니티를 다루며 C#도 자연스래 많이 사용하게 되었지만, 정작 C#을 제대로 공부해본 적은 한번도 없는 것 같다. 물론 Java에 익숙해서 적응하는 데 어렵지는 않았지만, 약간 익숙하지 않은 키워드를 마주한 적도 많았다. 이번 기회를 통해 C#을 제대로 학습하고, 문법에 익숙해졌으면 좋겠다.


📖학습 내용

1. 구조체 vs 클래스

구조체와 클래스는 데이터를 담는 사용자 정의 타입이라는 점에서 언뜻 보면 비슷하다고 생각할 수 있지만, 메모리 사용 방식, 성능, 기능 측면에서 큰 차이가 있다.

항목 구조체 (struct) 클래스 (class)
형식 값 형식 (Value Type) 참조 형식 (Reference Type)
메모리 위치 스택(Stack)에 저장 힙(Heap)에 저장 (레퍼런스는 스택에)
기본 상속 System.ValueType 상속 (상속 불가) object 상속, 상속 및 다형성 지원
생성자 매개변수 없는 생성자 정의 불가 (자동 제공) 생성자 자유롭게 정의 가능

즉 구조체는 위치, 색상, 좌표 등 단순 데이터 묶음을 표현할 때 사용하고, 클래스는 더욱 복잡한 객체를 표현하고 상속 및 메서드 같은 다양한 기능을 제공하기 위해 사용하면 적합하다.


2. 예외 처리

예외 처리는 말 그래도 프로그램 실행 중 발생하는 예기치 않은 상황에 대비하는 방식으로, 프로그램을 안정적으로 유지하는 데 큰 도움을 준다. 대표적인 예로 DivideByZeroException이 있다.

try
       {
           int a = 10;
           int b = 0;
           int result = a / b;  // 여기서 예외 발생
           Console.WriteLine(result);
       }
       catch (DivideByZeroException ex)
       {
           Console.WriteLine($"에러 발생: {ex.Message}");
       }
       finally
       {
           Console.WriteLine("예외 처리 완료");
       }
  • try 블록 내에서 예외가 발생할 가능성이 있는 코드를 작성하고, catch블록에서 예외처리한다.
  • catch 블록은 switch문 처럼 위에서부터 순서대로 실행된다.
  • finally 블록은 예외 발생 여부와 상관 없이 항상 실행되며, 생력 가능하다.

사용자 정의 예외 처리

만약 필요하다면, 사용자가 직접 예외 클래스를 작성할 수 있다.

public class NegativeNumberException : Exception
{
    public NegativeNumberException(string message) : base(message)
    {
    }
}

try
{
    int number = -10;
    if (number < 0)
    {
        throw new NegativeNumberException("음수는 처리할 수 없습니다.");
    }
}
catch (NegativeNumberException ex)
{
    Console.WriteLine(ex.Message);
}
catch (Exception ex)
{
    Console.WriteLine("예외가 발생했습니다: " + ex.Message);
}

위 코드와 같이 Exception 클래스를 상속받아 작성한다.


3. 제너릭

제너릭(Generics)데이터 형식에 독립적인 코드를 작성하게 해주고, 재사용성, 타입 안정성, 성능을 모두 챙길 수 있는 매우 강력한 기능이다.

  • 제너릭 클래스 예제
public class Box<T>
{
    public T Value { get; set; }

    public void PrintValue()
    {
        Console.WriteLine($"Box contains: {Value}");
    }
}
Box<int> intBox = new Box<int> { Value = 123 };
intBox.PrintValue();  // 출력: Box contains: 123

Box<string> strBox = new Box<string> { Value = "Hello" };
strBox.PrintValue();  // 출력: Box contains: Hello
  • 제너릭 메서드 예제
public class Util
{
    public static void Swap<T>(ref T a, ref T b)
    {
        T temp = a;
        a = b;
        b = temp;
    }
}
int x = 1, y = 2;
Util.Swap<int>(ref x, ref y);
Console.WriteLine($"{x}, {y}");  // 출력: 2, 1

string a = "A", b = "B";
Util.Swap(ref a, ref b);
Console.WriteLine($"{a}, {b}");  // 출력: B, A

잘못된 타입을 할당하는 것을 사전에 방지하여 형변환 오류를 예방해주는 것이 제너릭의 큰 장점인 것 같다.

제너릭을 잘 사용한다면 클래스나 메서드를 일반화시켜, 코드의 재사용성을 크게 높일 수 있다. 흔히 사용하는 List<>도 대표적인 제너릭 클래스이다.


4. out, ref 키워드

out, ref키워드는 메서드에서 매개변수를 전달할 떄 사용하며, 메서드 내에서 값을 직접 변경하거나 여러 값을 반환할 때 유용하다. 서로 비슷해보이지만 사용 목적과 규칙이 조금 다르다.

  • out 예제
void GetPlayerInfo(out string name, out int score)
{
    name = "Alice";
    score = 100;
}

string playerName;
int playerScore;

GetPlayerInfo(out playerName, out playerScore);
Console.WriteLine($"{playerName}, {playerScore}");  // 출력: Alice, 100
  • ref 예제
void Add(ref int number)
{
    number += 10;
}

int x = 5;
Add(ref x);
Console.WriteLine(x);  // 출력: 15


항목 ref out
초기화 여부(메서드 호출 전) 반드시 초기화되어야 함 초기화되지 않아도 됨
메서드 내부에서 수정할 수도 있고 안 할 수도 있음 반드시 값을 할당해야 함
사용 목적 값을 전달하고 변경된 값을 받아오고 싶을 때 여러 값을 반환할 때(출력 전용)
공통점 참조로 전달되어 원본 변수에 영향을 줌  

🎮콘솔 텍스트 게임 만들기

콘솔창을 통해 플레이 할 수 있는 간단한 게임 두 가지를 C#을 통해 구현해 보았다. 강의를 통해 배운 내용을 최대한 사용해 보기 위해 노력했다.

🐍스네이크 게임

스네이크 게임의 핵심 기능은 다음과 같다.

  • 스네이크 이동 : 플레이어가 wasd 키를 입력할 때마다 한 칸씩 이동한다
  • 몸통 추가 : 먹이를 먹으면 스네이크의 길이가 증가함
  • 충돌 판정 : 벽이나 자기 몸에 부딪히면 게임 오버
  • 먹이 리스폰 : 빈 공간에 무작위로 먹이를 생성
구현 코드
public enum TileType //게임 맵 타일의 종류
    {
        Empty,  //빈 칸
        SnakeBody,  //뱀의 몸
        SnakeHead,  //뱀의 머리
        Food,   //음식
        Wall    //벽
    }
    internal class Program
    {
        static int boardSize = 20;  //보드 크기 (20 x 20)
        static TileType[,] gameBoard = new TileType[boardSize, boardSize];
        static List<Point> snake = new List<Point>();
        static bool isGameOver = false;
        static int score = 0; //게임 스코어
        static void Main(string[] args)
        {
            for (int i = 0; i < boardSize; i++)
            {   //벽 채우기
                gameBoard[0, i] = TileType.Wall;
                gameBoard[boardSize - 1, i] = TileType.Wall;
                gameBoard[i, 0] = TileType.Wall;
                gameBoard[i, boardSize - 1] = TileType.Wall;
            }
            snake.Add(new Point(5, 9));
            snake.Add(new Point(4, 9));
            snake.Add(new Point(3, 9));
            GenerateFood();

            //게임 시작 전 설명
            Console.WriteLine("스네이크 게임에 오신 걸 환영합니다!");
            Console.WriteLine();
            Console.WriteLine("1. WASD 키로 뱀을 움직여보세요!");
            Console.WriteLine("2. 음식을 먹을 때 마다 뱀의 길이가 1 증가합니다.");
            Console.WriteLine("3. 벽이나 뱀의 몸에 머리가 닿으면 게임 오버입니다.");
            Console.WriteLine();
            Console.WriteLine("게임을 시작하려면 아무 키나 입력하세요.");
            Console.ReadKey();

            MoveSnake(1, 0);

            //게임 시작
            while (!isGameOver)
            {
                //Console.Clear();
                DrawGame();
                ConsoleKeyInfo input = Console.ReadKey();

                switch (input.Key)
                {
                    case ConsoleKey.W:
                        MoveSnake(0, -1);
                        break;
                    case ConsoleKey.A:
                        MoveSnake(-1, 0);
                        break;
                    case ConsoleKey.S:
                        MoveSnake(0, 1);
                        break;
                    case ConsoleKey.D:
                        MoveSnake(1, 0);
                        break;
                    default:
                        Console.WriteLine("WASD키로 조작 가능합니다.");
                        continue;
                }
            }
        }

        static void DrawGame() //게임 보드 그림
        {
            Console.SetCursorPosition(0, 0);
            for (int i = 0; i < boardSize; i++)
            {
                for (int j = 0; j < boardSize; j++)
                {
                    switch (gameBoard[i, j])
                    {
                        case TileType.Empty:
                            Console.Write("  ");
                            break;
                        case TileType.Wall:
                            Console.Write("▦");
                            break;
                        case TileType.SnakeHead:
                            Console.Write("◆");
                            break;
                        case TileType.SnakeBody:
                            Console.Write("■");
                            break;
                        case TileType.Food:
                            Console.Write("●");
                            break;
                        default:
                            Console.Write("  ");
                            break;
                    }
                }
                Console.WriteLine();
            }
        }

        static void MoveSnake(int moveX, int moveY)
        {
            //일단 머리가 벽이나 몸에 부딪히는지 확인
            Point nextHead = new Point(snake[0].x + moveX, snake[0].y + moveY);
            if (gameBoard[nextHead.y, nextHead.x] == TileType.Wall ||
                gameBoard[nextHead.y, nextHead.x] == TileType.SnakeBody)
            {
                GameOver();
                return;
            }
            //머리가 음식에 부딪히는 경우
            else if (gameBoard[nextHead.y, nextHead.x] == TileType.Food)
            {
                EatFood(snake[snake.Count - 1]);
            }

            //뱀 움직이기
            for (int i = snake.Count - 1; i >= 0; i--)
            {
                if (i == 0)
                {
                    snake[i] = nextHead;
                }
                else
                {
                    snake[i] = snake[i - 1];
                }
            }

            // 보드 초기화
            for (int i = 0; i < boardSize; i++)
            {
                for (int j = 0; j < boardSize; j++)
                {
                    if (gameBoard[i, j] != TileType.Wall &&
                        gameBoard[i, j] != TileType.Food)
                        gameBoard[i, j] = TileType.Empty;
                }
            }
            //뱀 위치 할당
            for (int i = 0; i < snake.Count; i++)
            {
                if (i == 0)
                {
                    gameBoard[snake[i].y, snake[i].x] = TileType.SnakeHead;
                }
                else
                {
                    gameBoard[snake[i].y, snake[i].x] = TileType.SnakeBody;
                }
            }
        }

        static void EatFood(Point lastTail) //뱀이 음식을 먹었을 시 호출
        {
            //뱀의 꼬리 뒤(움직인 방향과 정반대)에 뱀 칸 추가
            snake.Add(new Point(snake[snake.Count - 1].x - lastTail.x,
                snake[snake.Count - 1].y - lastTail.y));
            score++;
            GenerateFood();
        }

        static void GenerateFood() //음식을 빈칸 중 랜덤 위치에 생성
        {
            Random rand = new Random();
            while (true)
            {
                int x = rand.Next(1, boardSize - 1);
                int y = rand.Next(1, boardSize - 1);

                if (gameBoard[y,x] == TileType.Empty)
                {
                    gameBoard[y, x] = TileType.Food;
                    break;
                }
            }
        }

        static void GameOver()  //게임 오버 시 호출
        {
            isGameOver = true;
            Console.WriteLine("게임 오버");
            Console.WriteLine($"점수 : {score}");
        }

        struct Point //좌표를 저장하기 위한 구조체
        {
            public int x;
            public int y;

            public Point(int x, int y)
            {
                this.x = x;
                this.y = y;
            }

            public bool Equals(Point p)
            {
                if (this.x == p.x && this.y == p.y)
                    return true;
                else
                    return false;
            }
        }
    }

🃏블랙잭 게임

블랙잭 게임의 핵심 기능은 다음과 같다.

  • 카드 덱 생성 및 셔플
  • 플레이어/딜러의 카드 분배
  • Hit/Stand 입력 처리
  • Ace 카드의 유동적인 점수 계산
  • 최종 승패 판단 및 결과 출력
구현 코드
public enum Suit
{
    Heart,
    Spades,
    Diamond,
    Club
}
public enum Rank
{
    Ace = 1,
    Two,
    Three,
    Four,
    Five,
    Six,
    Seven,
    Eight,
    Nine,
    Ten,
    Jack,
    Queen,
    King
}
internal class Program
{
    static List<Card> deck = new List<Card>(); //게임에 사용할 덱.조커를 뺸 총 52장의 카드
    //카드를 뽑을 시 덱에서 완전히 제거하기 위해 배열 대신 리스트 사용
    static List<Card> playerCards = new List<Card>(); //플레이어의 패
    static List<Card> dealerCards = new List<Card>(); //딜러의 패
    static Random rand = new Random();
    static void Main(string[] args)
    {
        Console.OutputEncoding = Encoding.UTF8;

        int playerScore;
        int dealerScore;
        bool isPlaying;
        ConsoleKeyInfo playerInput;
        while (true)
        {
            //게임 환경 초기화
            Console.Clear();
            ShuffleDeck();
            playerScore = 0;
            dealerScore = 0;
            playerCards.Clear();
            dealerCards.Clear();
            isPlaying = true;
            //서로 두장씩 뽑고 시작
            playerCards.Add(DrawCard());
            dealerCards.Add(DrawCard());
            playerCards.Add(DrawCard());
            dealerCards.Add(DrawCard());
            ShowCards(playerCards); //플레이어의 핸드를 보여줌
            while (isPlaying)
            { //플레이어의 점수가 21점을 초과하거나, 스탠드를 선택할때 까지반복
                Console.WriteLine("\n1. Hit    2. Stand");
                playerInput = Console.ReadKey();
                if (playerInput.Key == ConsoleKey.NumPad1 ||
                    playerInput.Key == ConsoleKey.D1)
                {
                    playerCards.Add(DrawCard());
                    ShowCards(playerCards);
                    playerScore = AddScore(playerCards);
                    if (playerScore > 21) //점수가 21을 초과한 경우
                    {
                        isPlaying = false;
                    }
                }
                else if (playerInput.Key == ConsoleKey.NumPad2 ||
                    playerInput.Key == ConsoleKey.D2)
                {
                    isPlaying = false;
                }
                else
                {
                    Console.WriteLine("1 이나 2를 입력하세요");
                }
            }
            if(playerScore > 21)
            {
                Console.WriteLine("21점을 초과했습니다.");
                Console.WriteLine("당신의 패배입니다.");
            }
            else
            {
                DealerRule();
                dealerScore = AddScore(dealerCards);
                playerScore = AddScore(playerCards);

                Console.SetCursorPosition(0, Console.CursorTop);
                Console.Write("딜러 : ");
                ShowCards(dealerCards);
                Console.Write("플레이어 : ");
                ShowCards(playerCards);
                if (dealerScore > 21) //딜러의 점수가 초과된 경우
                {
                    Console.WriteLine("딜러의 점수가 21점을초과했습니다");
                    Console.WriteLine("당신의 승리입니다!");
                }
                else
                {
                    Console.WriteLine($"딜러 : {dealerScore}, 플레이어 :{playerScore}");
                    if (playerScore > dealerScore)
                    {
                        Console.WriteLine("당신의 승리입니다!");
                    }
                    else if (playerScore == dealerScore)
                    {
                        Console.WriteLine("무승부입니다!");
                    }
                    else
                    {
                        Console.WriteLine("당신의 패배입니다.");
                    }
                }
            }
            Console.WriteLine("\n아무 키나 눌러서 다시 시작");
            Console.ReadKey();

        }
    }
    static Card DrawCard() //카드 한장 랜덤 드로우
    {
        int index = rand.Next(0, deck.Count);
        Card draw = deck[index];
        deck.RemoveAt(index);
        return draw;
    }
    static void ShowCards(List<Card> cards) //패를 보여주는 메서드
    {
        foreach (Card card in cards)
        {
            Console.Write($"{card} ");
        }
        Console.WriteLine();
    }
    static void DealerRule() //딜러는 자신의 패가 17점 이상 될때 까지무조건 Hit
    {
       if(AddScore(dealerCards) < 17)
        {
            dealerCards.Add(DrawCard());
            DealerRule();
        }
        else
        {
            return;
        }
    }
    static int AddScore(List<Card> cards) //패의 점수를 계산
    {
        int result = 0;
        int aceCnt = 0;
        foreach (Card card in cards)
        {
            if(card.Rank == Rank.Ace)
            {
                aceCnt++;
            }
            else
            {
                result += card.Value();
            }
        }
        result += aceCnt * 11;  //에이스의 특수성을 고려하여 따로 계산
        while (result > 21 && aceCnt > 0)
        {
            result -= 10;   //에이스를 11대신 1로 계산
            aceCnt--;
        }
        return result;
    }
    static void ShuffleDeck() //덱 초기화
    {
        deck.Clear();
        //덱 생성. 모든 카드 조합 만들기
        foreach (Suit suit in Enum.GetValues(typeof(Suit)))
        {
            foreach (Rank rank in Enum.GetValues(typeof(Rank)))
            {
                deck.Add(new Card(suit, rank));
            }
        }
    }

    class Card
    {
        public Suit Suit { get; private set;}
        public Rank Rank { get; private set;}
        public Card(Suit suit, Rank rank)
        {
            Suit = suit;
            Rank = rank;
        }
        public int Value()
        {
            if(Rank >= Rank.Two && Rank <= Rank.Ten)
            {
                return (int)Rank;
            }
            else if(Rank >= Rank.Jack && Rank <= Rank.King)
            {
                return 10;
            }
            else
                return 11;
        }
        public override string ToString()
        {
            switch (Suit)
            {
                case Suit.Heart:
                    return $"♥{Rank}";
                case Suit.Spades:
                    return $"♠{Rank}";
                case Suit.Diamond:
                    return $"♦{Rank}";
                case Suit.Club:
                    return $"♣{Rank}";
                default:
                    return "";
            }
        }
    }
}

🏁오늘 배운 핵심 한줄 요약

  • 클래스는 참조 형식, 구조체는 값 형식
  • 제너릭은 다양한 타입에 대해 재사용 가능한 클래스/메서드/인터페이스 정의
  • ref, out 키워드를 통해 메서드 내에서 변수 수정 및 다중 반환
  • try-catch문으로 예외 처리 -> 선택이 아닌 필수

태그: ,

업데이트:

댓글남기기