게임 개발 공부 기록

7일차 - C# 2 (배열 ~ 클래스, 객체)

00lwt 2025. 1. 31. 20:47

▷▶ 학습 내용

배열

동일한 자료형의 값들이 연속적으로 저장되는 자료 구조

1) 1차원 배열

  • 동일한 데이터 유형을 가지는 데이터 요소들을 한 번에 모아서 다룰 수 있는 구조
  • 인덱스를 사용하여 요소에 접근 가능
  • 선언된 크기만큼의 공간을 메모리에 할당받음
// 배열 선언
데이터_유형[] 배열_이름;

// 배열 초기화
배열_이름 = new 데이터_유형[크기];

// 배열을 한 줄로 선언 및 초기화
데이터_유형[] 배열_이름 = new 데이터_유형[크기];

// 배열 요소에 접근
배열_이름[인덱스] = 값;
값 = 배열_이름[인덱스];

// ex)
int[] itemPrices = { 100, 200, 300, 400, 500 };
int totalPrice = 0;

for (int i = 0; i < itemPrices.Length; i++)
{
    totalPrice += itemPrices[i];
}

Console.WriteLine("총 아이템 가격: " + totalPrice + " gold");

 

2) 다차원 배열

  • 여러 개의 배열을 하나로 묶어 놓은 배열
  • 행과 열로 이루어진 표 형태와 같은 구조
  • 2차원, 3차원 등의 형태
// 2차원 배열의 선언과 초기화
int[,] array3 = new int[2, 3];  // 2행 3열의 int형 2차원 배열 선언

// 다차원 배열 초기화
array3[0, 0] = 1;
array3[0, 1] = 2;
array3[0, 2] = 3;
array3[1, 0] = 4;
array3[1, 1] = 5;
array3[1, 2] = 6;

// 선언과 함께 초기화
int[,] array2D = new int[3, 4] { { 1, 2, 3, 4 }, { 5, 6, 7, 8 }, { 9, 10, 11, 12 } };


// 3차원 배열의 선언과 초기화
int[,,] array3D = new int[2, 3, 4] 
{
    { { 1, 2, 3, 4 }, { 5, 6, 7, 8 }, { 9, 10, 11, 12 } },
    { { 13, 14, 15, 16 }, { 17, 18, 19, 20 }, { 21, 22, 23, 24 } }
};

 

컬렉션

자료를 모아 놓은 데이터 구조 (사용하기 위해 System.Collections.Generic 네임스페이스 추가)

1) List

  • 가변적인 크기를 갖는 배열
  • List를 생성할 때는 List에 담을 자료형을 지정
List<int> numbers = new List<int>(); // 빈 리스트 생성
numbers.Add(1); // 리스트에 데이터 추가
numbers.Add(2);
numbers.Add(3);
numbers.Remove(2); // 리스트에서 데이터 삭제

foreach(int number in numbers) // 리스트 데이터 출력
{
    Console.WriteLine(number);
}

 

2) Dictionary

  • 키와 값으로 구성된 데이터를 저장
  • 중복된 키를 가질 수 없으며, 키와 값의 쌍을 이루어 저장
using System.Collections.Generic;

Dictionary<string, int> scores = new Dictionary<string, int>(); // 빈 딕셔너리 생성
scores.Add("Alice", 100); // 딕셔너리에 데이터 추가
scores.Add("Bob", 80);
scores.Add("Charlie", 90);
scores.Remove("Bob"); // 딕셔너리에서 데이터 삭제

foreach(KeyValuePair<string, int> pair in scores) // 딕셔너리 데이터 출력
{
    Console.WriteLine(pair.Key + ": " + pair.Value);
}

 

※ 리스트 사용 시 주의할 점

  1. 동적으로 크기를 조정할 수 있기 때문에 배열보다 많은 메모리를 사용한다. 따라서, 많은 데이터를 다루는 경우 리스트를 무분별하게 사용하면 메모리 사용량이 급격히 증가하여 성능이 저하될 수 있다.
  2. 연결 리스트로 구현되어 리스트에서 특정 인덱스의 데이터를 찾기 위해 연결된 노드를 모두 순회해야 하기 때문에 인덱스를 이용한 데이터 접근이 배열보다 느리므로 데이터 접근 시간이 증가하여 성능이 저하될 수 있다.
  3. 동적으로 크기를 조정할 수 있기 때문에 데이터 추가, 삭제 등의 작업이 배열보다 간편하지만 이러한 유연성을 위해 데이터 추가, 삭제 등의 작업을 적절히 처리하는 코드를 작성해야 하므로  코드의 가독성과 유지보수성이 저하될 수 있다.

 

메서드

일련의 코드 블록으로, 특정한 작업을 수행하기 위해 사용되는 독립적인 기능 단위

  • 메서드의 역할과 중요성
    1. 코드의 재사용성: 동일한 작업을 반복하지 않고 필요할 때 메서드를 호출해서 작업을 수행하여 코드 중복을 방지할 수 있다.
    2. 모듈화: 각 메서드는 코드를 작은 단위로 분리해서 관리할 수 있으므로 코드가 간결해진다.
    3. 가독성과 유지보수성:  코드가 간결해지니 가독성이 좋아지고 수정이 필요한 경우 해당 메서드만 수정하면 되므로 유지보수가 용이하다.
    4. 코드의 추상화: 메서드를 통해 작업 단위를 추상화하고, 메서드 이름을 통해 해당 작업이 어떤 역할을 하는지 파악할 수 있다.
  • 메서드 구조
[접근 제한자] [리턴 타입] [메서드 이름]([매개변수])
{
    // 메서드 실행 코드
}


// ex)
// 반환 값이 없는 메서드
public void SayHello()
{
    Console.WriteLine("안녕하세요!");
}

// 매개변수가 있는 메서드
public void GreetPerson(string name)
{
    Console.WriteLine("안녕하세요, " + name + "님!");
}

// 반환 값이 있는 메서드
public int AddNumbers(int a, int b)
{
    int sum = a + b;
    return sum;
}
  • 메서드 호출
[메서드 이름]([전달할 매개변수]);

// ex)
AddNumbers(10, 20);

 

오버로딩

매개변수의 개수, 타입, 순서가 다른 여러 메서드를 동일한 이름으로 정의하여 메서드 호출 시 매개변수의 형태에 따라 적절한 메서드 선택

void PrintMessage(string message)
{
    Console.WriteLine("Message: " + message);
}

void PrintMessage(int number)
{
    Console.WriteLine("Number: " + number);
}

// 메서드 호출
PrintMessage("Hello, World!");  // 문자열 매개변수를 가진 메서드 호출
PrintMessage(10);  // 정수 매개변수를 가진 메서드 호출

 

재귀호출

  • 메서드가 자기 자신을 호출하는 것
  • 작은 부분의 해결 방법이 큰 문제의 해결 방법과 동일한 구조를 갖고 있는 경우 문제를 작은 부분으로 분할하여 해결하는 방법 중 하나

※ 재귀호출 사용 시 주의할 점

  1. 종료 조건을 명확히 정의해야 하며, 종료 조건을 만족하지 못하면 무한히 재귀 호출이 반복되어 스택 오버플로우 등의 오류가 발생할 수 있다.
  2. 재귀 호출은 메모리 사용량이 더 크고 실행 속도가 느릴 수 있으므로, 필요한 경우에만 적절히 사용해야 한다.

 

구조체

  • 여러 개의 데이터를 묶어서 하나의 사용자 정의 형식으로 만들기 위한 방법
  • 값 형식(Value Type)으로 분류되며, 데이터를 저장하고 필요한 기능을 제공
  • 구조체는 struct 키워드를 사용하여 선언하고 멤버는 변수와 메서드로 구성 가능
struct Person
{
    public string Name;
    public int Age;

    public void PrintInfo()
    {
        Console.WriteLine($"Name: {Name}, Age: {Age}");
    }
}
// 변수를 선언하여 사용
Person person1;
// 멤버에 접근할 때 . 연산자를 사용
person1.Name = "John";
person1.Age = 25;
person1.PrintInfo();

 

클래스

  • 객체를 생성하기 위한 템플릿 또는 설계도 역할
  • 필드로 표현되는 속성과 메서드로 표현되는 동작을 가짐
  • 객체를 생성하기 위해서는 클래스를 사용하여 인스턴스를 만들어야 함
  • 붕어빵으로 비유하면, 클래스는 붕어빵을 만들기 위한 틀

 

객체

  • 클래스의 실체화된 형태.
  • 클래스로부터 생성되며, 각 객체는 독립적인 상태 (고유한 데이터)
  • 붕어빵으로 비유하면, 객체는 붕어빵
// 클래스 기본 구조
class Person
{
    public string Name;
    public int Age;

    public void PrintInfo()
    {
        Console.WriteLine("Name: " + Name);
        Console.WriteLine("Age: " + Age);
    }
}

Person p = new Person();
p.Name = "John";
p.Age = 30;
p.PrintInfo(); // 출력: Name: John, Age: 30

 

접근 제한자

클래스, 필드, 메서드 등의 접근 가능한 범위를 지정하는 클래스의 캡슐화를 제어하는 역할을 가진 키워드

class Person
{
    public string Name;         // 외부에서 자유롭게 접근 가능
    private int Age;           // 같은 클래스 내부에서만 접근 가능
    protected string Address;  // 같은 클래스 내부와 상속받은 클래스에서만 접근 가능
}

 

생성자

  • 객체가 생성될 때 호출되는 메서드
  • 클래스의 인스턴스(객체)를 초기화하고 필요한 초기값을 설정하는 역할
  • 생성자는 클래스와 동일한 이름을 가지며 반환 타입이 없음
  • 객체를 생성할 때 new 키워드와 함께 호출
  • 여러 개 정의할 수 있으며 매개변수의 개수와 타입에 따라 다른 생성자를 호출하는 생성자 오버로딩 가능
class Person
{
    private string name;
    private int age;

    // 매개변수가 없는 디폴트 생성자
    public Person()
    {
        name = "Unknown";
        age = 0;
    }

    // 매개변수를 받는 생성자
    public Person(string newName, int newAge)
    {
        name = newName;
        age = newAge;
    }

    public void PrintInfo()
    {
        Console.WriteLine($"Name: {name}, Age: {age}");
    }
}

 

프로퍼티

  • 클래스 멤버로서, 객체의 필드 값을 읽거나 설정하는데 사용되는 접근자(Accessor) 메서드의 조합
  • 객체의 필드에 직접 접근하지 않고, 간접적으로 값을 설정하거나 읽을 수 있도록 함
  • 필드에 대한 접근 제어와 데이터 유효성 검사 등을 수행
  • 필드와 마찬가지로 객체의 상태를 나타내는 데이터 역할을 하지만 외부에서 접근할 때 추가적인 로직 수행
// 프로퍼티 구조
[접근 제한자] [데이터 타입] 프로퍼티명
{
    get
    {
        // 필드를 반환하거나 다른 로직 수행
    }
    set
    {
        // 필드에 값을 설정하거나 다른 로직 수행
    }
}

// ex)
class Person
{
    private string name;
    private int age;

    public string Name
    {
        get { return name; }
        set { name = value; }
    }

    public int Age
    {
        get { return age; }
        set { age = value; }
    }
}

Person person = new Person();
person.Name = "John";   // Name 프로퍼티에 값 설정
person.Age = 25;        // Age 프로퍼티에 값 설정

// Name과 Age 프로퍼티에 접근하여 값을 출력
Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");
// 자동 프로퍼티
class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

Person person = new Person();
person.Name = "John";     // 값 설정
person.Age = 25;          // 값 설정

값을 읽고 출력
Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");

 

▷▶ 실습

배열을 활용한 숫자 맞추기 게임

namespace ChoiceNum2
{
    internal class ChoiceNum2
    {
        static void Main(string[] args)
        {
            int[] numbers = new int[3];
            Random random = new Random();
            int attempt = 0;

            for (int i = 0; i < numbers.Length; i++)
            {
                numbers[i] = random.Next(1, 10);
            }

            while (true)
            {
                Console.WriteLine("3개의 숫자를 입력하세요 (1~9)");
                int[] guesses = new int[3];

                for (int i = 0; i < guesses.Length; i++)
                {
                    guesses[i] = int.Parse(Console.ReadLine());
                }

                int correct = 0;

                for(int i = 0;i < numbers.Length; i++)
                {
                    for(int j = 0; j < guesses.Length; j++)
                    {
                        if (numbers[i] == guesses[j])
                        {
                            correct++;
                            break;
                        }
                    }
                }

                attempt++;
                Console.WriteLine("시도: " + attempt + "회, 맞춘 개수: " + correct + "개");

                if (correct == 3)
                {
                    Console.WriteLine("정답입니다");
                    break;
                }
            }
        }
    }
}

 

TicTacToe 게임

namespace TicTactoe
{
    internal class Program
    {
        static char[] arr = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
        static int player = 1;
        static int choice;
        static int flag = 0;

        static void Main(string[] args)
        {
            do
            {
                Console.Clear();
                Console.WriteLine("플레이어 1: O 와 플레이어 2: X");
                Console.WriteLine("\n");

                if (player == 1)
                {
                    Console.WriteLine("P1 턴");
                }
                else
                {
                    Console.WriteLine("P2 턴");
                }

                Console.WriteLine("\n");
                Board();
                Console.WriteLine("\n");

                Console.Write("입력: ");
                string input = Console.ReadLine();

                // 입력 받은 문자열이 int형으로 변환 가능한지에 대한 bool 값
                bool res = int.TryParse(input, out choice);

                if (res == true)
                {
                    if (arr[choice] != 'X' && arr[choice] != 'O')
                    {
                        if (player == 1)
                        {
                            arr[choice] = 'O';
                        }
                        else
                        {
                            arr[choice] = 'X';
                        }

                        player++;
                    }
                    else
                    {
                        Console.WriteLine("이미 선택된 칸입니다.");
                        Console.ReadLine();
                    }
                }
                else
                {
                    Console.WriteLine("0~9 사이의 숫자를 입력해주세요.");
                }

                flag = CheckWin();
            }
            while (flag != -1 && flag != 1);

            if (flag == 1)
            {
                Console.WriteLine("플레이어 {0}이(가) 이겼습니다.", (player % 2) + 1);
            }
            else
            {
                Console.WriteLine("무승부");
            }

            Console.ReadLine();
        }

        static void Board()
            {
                Console.WriteLine(" {0} {1} {2} ", arr[0], arr[1], arr[2]);
                Console.WriteLine(" {0} {1} {2} ", arr[3], arr[4], arr[5]);
                Console.WriteLine(" {0} {1} {2} ", arr[6], arr[7], arr[8]);
            }

        static int CheckWin()
        {
            {
                // 가로 승리 조건
                if (arr[0] == arr[1] && arr[1] == arr[2])
                {
                    return 1;
                }
                else if (arr[3] == arr[4] && arr[4] == arr[5])
                {
                    return 1;
                }
                else if (arr[6] == arr[7] && arr[7] == arr[8])
                {
                    return 1;
                }

                // 세로 승리 조건
                else if (arr[0] == arr[3] && arr[3] == arr[6])
                {
                    return 1;
                }
                else if (arr[1] == arr[4] && arr[4] == arr[7])
                {
                    return 1;
                }
                else if (arr[2] == arr[5] && arr[5] == arr[8])
                {
                    return 1;
                }

                // 대각선 승리조건
                else if (arr[0] == arr[4] && arr[4] == arr[8])
                {
                    return 1;
                }
                else if (arr[2] == arr[4] && arr[4] == arr[6])
                {
                    return 1;
                }

                // 무승부
                else if (arr[0] != '0' && arr[1] != '1' && arr[2] != '2' && arr[3] != '3' && arr[4] != '4' && arr[5] != '5' &&
                    arr[6] != '6' && arr[7] != '7' && arr[8] != '8')
                {
                    return -1;
                }
                else { return 0; }
            }
        }
    }
}

 

오늘의 회고

작성 해야할 코드가 조금 복잡해졌다고 전체적인 과정이 머리에 잘 그려지지 않았고 힌트를 보지 않고는 혼자 구현하기가 어려웠다.

특히 틱택토 게임을 만들 때 대충 떠올려보니 2차원 배열을 사용해서 만들면 되겠다 싶어서 한번 시도 해봤다. 보드를 만들고 case문을 이용해서 입력받은 문자열별로 case를 나누어 배열 상에서 선택되도록 하려고 했으나 잘못된 방법이었는지 생각과 다르게 막혀버렸다.

풀이를 확인해보니 굳이 2차원 배열로 복잡하게 할 필요없이 1차원으로 쭉 나열한 다음에 포멧팅을 이용해서 임의적으로 보드 모양을 출력시켜서 진행하면 되는 것이었다. 그 외에도 생각하지 못했던 것은 메서드를 이용한 승리 체크 방법, 무승부 조건 그리고 입력 받은 문자열을 사용할 때 TryParse를 이용해서 bool값을 사용하는 방법 등이 있었다. 코드를 보고 이해하는 것과 직접 작성하는 것은 차이가 큰 것 같다.