[C#과 유니티로 만드는 MMORPG 게임 개발 시리즈] Part1: C# 기초 프로그래밍 입문 강의 노트

17 분 소요

개론

오리엔테이션

  • 유니티와 언리얼 에셋 스토어에서 제공하는 무료 에셋들을 최대한 활용한다.
  • 노베이스 상태에서 강의 진행
  • 최종적인 목표: 500-1000명 사이의 MMORPG 인디 게임을 출시  

환경 설정

  • 그냥 VS와 C# 환경 설치
  • installer에서 .NET 데스크톱 개발 체크.
  • Visual Assist라는 어썸한 플러그인이 있으나 돈을 내야함 ㅡㅡ. 코드가 예쁘게 보인다.  

 

데이터 갖고 놀기

정수 형식

  • 주석, 정수 설명
bytes
C++
C#
4 bytes
long
int
8 bytes
long long
long

2진수, 10진수, 16진수

  • 진법 설명  

정수 범위의 비밀

  • 2의 보수 설명  

불리언, 소수, 문자, 문자열 형식

  • float 변수는 rValue에 f를 붙여줘야됨(double이 c# 기본이라)
float f = 3.14f;
  • char 자료형은 1 바이트가 아니라 2 바이트다.  

형식 변환

  • 캐스팅 설명  

데이터 연산

  • 연산자 설명  

비트 연산

  • 최상위 비트에 1이 있는데 오른쪽 시프트 연산을 하면 최상위 비트 1은 유지된채로 시프트한다.
  • 고로 이런 케이스에선 헷갈리지 않게 쓰기 위해 uint를 쓰는게 좋을 듯.  

데이터 마무리

  • C#에는 타입을 정하지 않고 편하게(?) 사용하는 var 자료형이 있다. 그러나 C#, C++의 장점은 타입을 명시해서 코드를 보는 사람이 명확히 이해할 수 있는 것이다. 그러므로 비추!  

 

코드의 흐름 제어

if와 else

  • if/else 설명  

switch

  • switch, 삼항 연산자 설명  

가위-바위-보 게임

Random rand = new Random();
int aiChoice = rand.Next(0,3);
int choice = Convert.ToInt32(Console.ReadLine());

상수와 열거형

int ROCK = 1;
const int PAPER = 2;
switch (choice)
{
    case ROCK: // ERROR!!!
        ~~~~~~ 
    case PAPER: // OK!!! Because "const"
        ~~~~~~
}
enum Choice // 열거형
{
    Rock = 1, // 기본은 0부터 시퀀스
    Paper = 2,
    Scissors = 0
}

static void Main(...)
{
    switch (choice)
    {
        // 타입이 enum이므로 int로 형변환
        case (int)Choice.Scissors:
            Console.WriteLine("가위");
            break;
        ....
    }
}

while

do
{
    bool a = true; 
} while (a) // ERROR!!! 변수 a는 do 안에 {} 사이에서만 유효하므로!!

for

  • for문 설명  

break, continue

  • break, continue 설명  

함수

class Program
{
    // 복사 덧셈 함수
    static void CopyAddOne(int number)
    {
        number = number + 1;
    }

    // 참조 덧셈 함수
    static void RefAddOne(ref int number)
    {
        number = number + 1;
    }

    static void Main(String[] args)
    {
        // 복사(짭퉁), 참조(진퉁)
        int a = 0;
        
        Program.CopyAddOne(a);
        Console.WriteLine(a); // 출력: 0 (덧셈 안됨)

        Program.RefAddOne(ref a);
        Console.WriteLine(a); // 출력: 1 (덧셈 완료)
    }
}

ref, out

// ref 예제
static void Swap(ref int a, ref int b)
{
    int temp = a;
    a = b;
    b = temp;
}
// out 예제
static void Divide(int a, int b, out int result1, out int result2)
{
    result1 = a / b;
    result2 = a % b;
}

static void Main(string[] args)
{
    int num1 = 10;
    int num2 = 3;

    int result1;
    int result2;
    Divide(10, 3, out result1, out result2);

    Console.WriteLine(result1); // 3
    Console.WriteLine(result2); // 1
}

오버로딩

static int Add(int a, int b, int c = 0, float d = 1.0f, double e = 3.0)
{
    Console.WriteLine("Add int 호출");
    return a + b + c;
}

static void Main(string[] args)
{
    Program.Add(1, 2, d:2.0f) // C++는 매개변수 순서를 지켜줘야 되지만 C#은 그렇지 않아도 된다!
}

연습 문제

  • 구구단 출력, 피라미드 별찍기, 팩토리얼  

 

TextRPG

디버깅 기초

  • breakpoint 조건식 검사 (‘브레이크 포인트 우클릭’ - ‘조건’) image
  • 다음에 실행될 문 변경 (화살표를 위 아래로 드래그해서 사용) image  

TextRPG 직업 고르기

  • enum 개념을 구글링으로 확실히 다지기!!!  

TextRPG 플레이어 생성

// 주목: enum과 struct

enum ClassType
{
    None = 0,
    Knight = 1,
    Archer = 2,
    Mage = 3
}

struct Player
{
    public int hp;
    public int attack;
}

static void CreatePlayer(ClassType choice, out Player player) // 매개변수 주의! "out Player player"
{
    // 기사, 궁수, 법사에 맞게 hp, attack 조정
    switch (choice)
    {
        case ClassType.Knight:
            player.hp = 100;
            player.attack = 10;
            break;
        ...
    }
}

static void Main(string[] args)
{
    while(true)
    {
        ClassType choice = ChooseClass();
        if (choice != ClassType.None) 
        {
            Player player;
            CreatePlayer(choice, out Player)
        }
    }
}

TextRPG 몬스터 생성

  • 몬스터 관련 코딩  

TextRPG 전투

  • 전투 관련 코딩  

 

객체지향 여행

객체지향의 시작

  • 널 크래쉬

널인 객체의 속성 등을 호출할때 발생  

복사(값)와 참조

  • “struct는 복사 / class는 참조”를 해서 작업
class Knight
{
    public int hp;
    public int attack;

    public void Move() {...}
    
    public void Attack() {...}
}

struct Mage
{
    public int hp;
    public int attack;
}

class Program
{
    static void KillMage(Mage mage)
    {
        mage.hp = 0;
    }

    static void KillKnight(Knight knight)
    {
        knight.hp = 0;
    }

    static void Main(...)
    {
        Mage mage;
        mage.hp = 100;
        mage.attack = 50;
        KillMage(mage); // 복사이므로 이 함수를 실행해도 hp가 0으로 되지 않음

        Knight knight = new Knight();
        knight.hp = 100;
        knight.attack = 10;
        KillKnight(knight); // 참조이므로 hp가 0으로 변함
    }
}
class Person
{
        public string name;
        public int age;
        public Car car;              

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

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

        public Car Car
        {
            get { return this.car; }
            set { this.car = value; }
        }

        public object ShallowCopy()
        {
            // MemberwiseClone: 새 개체를 만든 다음 현재 개체의 비정적 필드를 새 개체에 복사하여 단순 복사본을 만듬.         
            return this.MemberwiseClone();
        }

        public Object Clone()
        {
            Person person = new Person();
            person.name = this.name;
            person.age = this.age;
            person.car = new Car();
            person.car.model = this.car.model;
        }
}

class Car 
{             
        public string model;                 
        public string Model
        {
            get { return this.model; }
            set { this.model = value; }
        }
}

static void Main(string[] args)
{
    // 원본 객체(person)
    Person person = new Person();
    person.Name = "홍길동";
    person.Age = 33;
    person.Car = new Car();
    person.Car.Model = "뉴카이런";

    // 얕은 복사된 객체(객체)
    Person person2 = (Person) person.ShallowCopy();
    person2.Name = "진시황";
    person2.Age = 55;
    person2.Car.Model = "뉴산타페":

    // 두 객체 비교 - 두 객체간 동일한 Car 참조를 가지고 있다.
    // WriteLine: 파라미터의 속성들을 모두 출력
    // person과 person2가 동일한 뉴산타페를 가지고 있음 (그 외는 다름)
    WriteLine(person);
    WriteLine(person2);

    // 깊은 복사된 객체
    Person person3 = (Person)person.Clone();
    person3.Name = "진시황";
    person3.Age = 55;
    person3.Car.Model = "뉴산타페";

    // 두 객체 비교 - 두 객체간 서로 다른 Car 참조를 가지고 있다.
    WriteLine(person);
    WriteLine(person3);

}

스택과 힙

  • 스택
    • 불안정하고 임시적으로 사용하는 메모리
    • 함수안에 변수 저장공간은 다 스택 영역에 저장된다.
    • 스택에 참조 변수(주소값) -> 가리키는 본체는 힙 영역
    • 참조 타입의 본체가 있는 곳
    • 동적으로 할당

 

  • 스택 영역은 함수가 호출되고 종료되는 순간에 알아서 스케일 업/다운 하기 때문에 신경을 쓸 필요가 없다. 그러나 힙 영역은 메모리를 할당했으면 계속 메모리에 남게 된다.
  • C#은 C++과는 달리 힙 영역을 프로그래머가 delete할 필요 없이 알아서 날려준다.
  • 본체가 힙에만 있진 않는다. (Main 함수 변수(스택)를 ref로 선언)  

생성자

public Knight()
{
    hp = 100;
    attack = 10;
    Console.WriteLine("생성자 호출!");
}

public Knight(int hp) : this()
{
    this.hp = hp;
    Console.WriteLine("int 생성자 호출!");
}

// 출력
// 생성자 호출!
// int 생성자 호출!

static의 정체

  • static 함수에서는 비 static 필드에 접근을 못할까?

    비 static 필드의 값은 각 객체마다 다를 수 있는데 공용 static 함수에서 사용할 수 없기 때문!

Console.WriteLine() // WriteLine: Console 클래스의 static 함수

Random rand = new Random();
rand.Next(0, 2); // Next: 비 static 함수

상속성

  • 상속된 클래스의 인스턴스 생성시 부모 클래스의 생성자를 먼저 호출하고 자식 클래스의 생성자를 호출한다!!
class Knight : Player
{
    public Knight() : base(100) // 부모 클래스의 생성자 선택
    {
        base.hp = 100; // 부모 클래스의 필드 사용
    }
}
  • 부모 클래스의 함수도 자식 클래스의 인스턴스가 호출할 수 있다.  

은닉성

  • 필드를 public으로 접근 제어 하지 않고 private으로 선언 후 setter/getter로 제어하는 장점은?
    • 그냥 public으로 접근 제어시 프로그램이 비대해지면 누가 필드를 변경했는지 알기 어렵다.
    • set 메서드에 브레이크 포인트를 걸어서 누가 필드를 제어할려고 하는지 디버깅할때 용이
class Foo
{
    int a; // 접근 지정자를 붙이지 않으면 기본 private!!
}

클래스 형식 변환

// 기본 코드 베이스
class Player
{
    protected int hp;
    protected int attack;
}

class Knight : Player
{

}

class Mage : Player
{
    public int mp;
}
Knight knight = new Knight();
Mage mage = new Mage();

// Mage 타입 -> Player 타입 (업 캐스팅) (위험하지 않다!)
// Player 타입 -> Mage 타입 (다운 캐스팅) (될 수도 있고 안 될 수도 있고 case by case)
Player magePlayer = mage;
Mage mage2 = (Mage)magePlayer;
static void EnterGame(Player player)
{
    Mage mage = (Mage)player;
    mage.mp = 10; // 매개변수로 knight가 들어오면 캐스팅 실패
}

static void Main(string[] args)
{
    Knight knight = new Knight();
    Mage mage = new Mage();

    EnterGame(knight);
}
// 첫 번째 해결 방법
static void EnterGame(Player player)
{
    bool isMage = (player is Mage); // 핵심!
    if (isMage)
    {
        Mage mage = (Mage)player;
        mage.mp = 10;
    }
}
// 두 번째 해결 방법 (추천!!!!!)
static void EnterGame(Player player)
{
    Mage mage = (player as Mage); // 핵심!, 형변환 가능하면 형변환, 안되면 null
    if (mage != null)
    {
        mage.mp = 10;
    }
}

다형성

// 오버라이딩 X
class Player
{
    public void Move()
    {
        Console.WriteLine("Player 이동!");
    }
}

class Knight : Player
{
    // 부모 클래스인 Player에도 Move 메서드가 있음.
    // 새로 작성하기 위해 new 키워드로 선언.
    public new void Move() 
    {
        Console.WriteLine("Knight 이동!");
    }
}
  • 오버로딩(함수 이름의 재사용) / 오버라이딩(다형성을 이용하는것)
// 오버라이딩 O
class Player
{
    public virtual void Move()
    {
        Console.WriteLine("Player 이동!");
    }
}

class Knight : Player
{
    public override void Move()
    {
        Console.WriteLine("Knight 이동!");
    }
}
static void EnterGame(Player player)
{
    // 만약 오버라이딩을 안 했다면 Player 클래스의 Move를 호출하지만
    // 오버라이딩을 했다면 해당하는 타입의 클래스의(Mage)의 Move를 호출한다.
    player.Move();

    Mage mage = (player as Mage);
    if (mage != null)
    {
        mage.mp = 10;
    }
}

static void Main(string[] args)
{
    Knight knight = new Knight();
    Mage mage = new Mage();

    EnterGame(knight);
}

override 키워드를 사용했다면 상위 클래스가 해당 메소드에 최소한 한 번은 virtual 키워드로 선언해야 한다!

class Knight : Player
{
    public override void Move()
    {
        base.Move(); // 상위 클래스의 Move 호출

        Console.WriteLine("Knight 이동!");
    }
}
// C++에는 없는 C#에는 있는 문법: sealed
// Knight를 상속받는 자식들은 더이상 해당 메서드를 건들지 말아라!! 라는 뜻
class Knight : Player
{
    public sealed override void Move() // 핵심
    {
        Console.WriteLine("Knight 이동!");
    }
}

virtual로 선언한 메서드는 일반 메서드보다 성능에 더 부담을 준다.

  • 가상 메소드는 일반적으로 함수 포인터가 저장되는 소위 가상 메서드 테이블(vtable)을 통해 구현된다. 이렇게 하면 실제 호출에 방향성이 추가된다. (vtable에서 호출할 함수의 주소를 가져온 다음 호출해야 함). 그러므로 성능이 더 떨어진다.
  • 그러나 이것은 보통 큰 문제가 되진 않는다.

 

문자열 둘러보기

string name = "Harry Potter";

// 1. 찾기
bool found = name.Contains("Harry"); // true
int index = name.IndexOf('P'); // 6
int index = name.IndexOf('z'); // -1

// 2. 변형
name = name + " Junior";
string lowerCaseName = name.ToLower(); // 소문자로
string upperCaseName = name.ToUpper(); // 대문자로
string newName = name.Replace('r', 'l');

// 3. 분할
string[] names = name.Split(new char[] { ' ' });
string substringName = name.SubString(6); // Potter Junior

 

TextRPG2

TextRPG2 플레이어 생성

  • C#은 파일 분할 시 include 작업 없이 동일 네임스페이스에서 똑같이 작업 가능하다.
  • 매개변수가 있는 생성자를 만들었다면 디폴트 생성자는 더 이상 사용할 수 없다.  

TextRPG2 몬스터 생성

  • 구현 내용  

TextRPG2 게임 진행

  • 구현 내용  

TextRPG2 마무리

  • 구현 내용  

 

자료구조 맛보기

배열

// foreach
int[] scores = new int[5];
foreach(int score in scores)
{
    Console.WriteLine(score);
}
// 배열 초기화 3가지 방법
int[] scores = new int[5] { 10, 20, 30, 40, 50 }; // 5개를 빼먹으면 에러가 뜬다!
int[] scores = new int[] { 10, 20, 30, 40, 50 };
int[] scores = { 10, 20, 30, 40, 50 };

연습 문제

  • 디버깅시 무한 루프로 프로그램이 끝나지 않을 때 멈춰서 해당 지점을 알 수 있는 팁

image

  • 면접 할때 Sort 알고리즘 4개 정돈 물어본다.  

다차원 배열

int[,] arr = new int[2, 3];
// 2차원 배열 초기화 3가지 방법
int[,] arr = new int[2, 3] { {1,2,3}, {1,2,3} };
int[,] arr = new int[,] { {1,2,3}, {1,2,3} };
int[,] arr = { {1,2,3}, {1,2,3} };
class Map
{
    int[,] tiles = {
        {1,1,1,1,1},
        {1,0,0,0,1},
        {1,0,0,0,1},
        {1,0,0,0,1},
        {1,1,1,1,1}
    };

    public void Render()
    {
        var defaultColor = Console.ForegroundColor;

        for (int y = 0; y < tiles.GetLength(1); y++)
        {
            for (int x = 0; x < tiles.GetLength(0); x++)
            {
                if (tiles[y,x] == 1)
                    Console.ForegroundColor = ConsoleColor.Red;
                else
                    Console.ForegroundColor = ConsoleColor.Green;

                Console.Write('■');
            }
            Console.WriteLine();
        }

        Console.ForegroundColor = defaultColor;
    }
}

static void Main(string[] args)
{
    Map map = new Map();
    map.Render();
}
// 가변 배열

// [ . . ]
// [ . . . . . .]
// [ . . . ]
int[][] a = new int[3][];
a[0] = new int[3];
a[1] = new int[6];
a[2] = new int[2];

a[0][0] = 1;

List

  • insert

    O(N)

  • Add
    • Count가 Capacity보다 작으면 O(1)
    • 새로운 요소를 수용하기 위해 용량을 늘려야하는 경우 O(N) 여기서 N은 Count
  • Remove

    O(N) (왜냐면 남은 요소를 shift 해야 하므로)

 

List<int> list = new List<int>();

// [ 0, 1, 2, 3, 4]
for (int i = 0; i < 5; i++)
    list.Add(i);

// 삽입 삭제
// [ 0, 1, 999, 2, 3, 4]
list.Insert(2, 999);
// 3이 여러개 있다면 처음 만난 3만 삭제
bool success = list.Remove(3);

list.RemoveAt(0);

list.Clear();

for (int i = 0; i< list.Count; i++)
    Console.WriteLine(list[i]);

foreach (int num in list)
    Console.WriteLine(num);

Dictionary

  • Hashtable로 구현돼있다.
    • 공이 들어있는 아주 큰 박스 [ … ] 1만개 (1~1만)
    • 박스 여러개로 쪼개놓는다. [1~10] [11~20] [] [] [] [] [] [] 1천개
    • 7777번 공은 어디에 있나? -> 777번째 박스에서 찾으면 된다!
    • 메모리 손해
    • 즉, 메모리를 내주고, 성능을 취한다!

 

class Monster
{
    public int id;

    public Monster(int id) { this.id = id; }
}

static void Main(string[] args)
{
    // Key -> Value
    Dictionary<int, Monster> dic = new Dictionary<int, Monster>();

    /* 삽입
    dic.Add(1, new Monster(1));
    dic[5] = new Monster(5);
    */

    for (int i = 0; i < 10000; i++)
    {
        dic.Add(i, new Monster(i));
    }

    /* 위험한 코드! 해당 key에 value가 없다면? -> 크래쉬
    Monster mon = dic[5000];
    */

    Monster mon;
    bool found = dic.TryGetValue(20000, out mon); // found: false, mon: null

    dic.Remove(7777);
    dic.Clear();
}

 

알아두면 유용한 기타 문법

Generic (일반화)

  • var과 object 차이점
    • var: 컴파일러가 형을 알아서 맞춰줌 ex) var -> string
    • object: 진짜 형이 object

object는 참조 타입 (힙을 사용)

class MyList<T>
{
    T[] arr = new T[10];

    public T GetItem(int i)
    {
        return arr[i];
    }
}
static void Test<T>(T input)
{

}

static void Main(string[] args)
{
    Test<int>(3);
    Test<float>(3.0f);
}
// T는 값 형식 이어야한다!
class MyList<T> where T : struct
{
    T[] arr = new T[10];

    public T GetItem(int i)
    {
        return arr[i];
    }
}
// T는 참조 형식 이어야한다!
class MyList<T> where T : class
{
    T[] arr = new T[10];

    public T GetItem(int i)
    {
        return arr[i];
    }
}
// T는 Monster, 혹은 Monster를 상속받은 클래스 이어야한다!
class MyList<T> where T : Monster
{
    T[] arr = new T[10];

    public T GetItem(int i)
    {
        return arr[i];
    }
}

Interface (인터페이스)

// 추상클래스
abstract class Monster // new Monster() 불가능 (추상클래스 이므로)
{
    public abstract void Shout(); // {} 같은 본문이 있으면 안됨
}

class Skeleton : Monster
{
    // 에러가 뜬다!!
    // 왜냐하면 상속된 추상 멤버 Shout를 구현하지 않았기 때문
}
  • C#에선 다중상속을 할 수 없다. -> 인터페이스로 사용해야함

    다중상속의 단점: 죽음의 다이아몬드

// 인터페이스
interface IFlyable
{
    void Fly(); // 접근지정자, abstract 키워드 등을 쓸 필요없다.
}

class FlyableOrc : Orc, IFlyable
{
    public void Fly() // void Fly()를 무조건 구현해야한다.
    {

    }
}

static void DoFly(IFlyable flyable)
{
    flyable.Fly();
}

static void Main(string[] args)
{
    FlyableOrc orc = new FlyableOrc();
    DoFly(orc); 
}
interface IFace
{
    void Method();
}

abstract class Face
{
    public virtual void Method()
    {
        Console.WriteLine("Face Class 호출");
    }
}

class DerivedFace : Face, IFace
{
    public override void Method()
    {
        base.Method();
    }

    void IFace.Method()
    {
        Console.WriteLine("DerivedFace에서 IFace 오버라이딩");
    }
}

class Program
{
    static void Main(string[] args)
    {
        DerivedFace df = new DerivedFace();
        df.Method(); // 출력: "Face Class 호출"

        // 에러
        // IFace f = new IFace();

        IFace f = new DerivedFace();
        f.Method(); // 출력: "DerivedFace에서 IFace 오버라이딩"
    }
}

Property (프로퍼티)

class Knight
{
    protected int hp;

    public int Hp
    {
        get {return hp;}
        set {hp=value;}
    }
}

static void Main(string[] args)
{
    // 프로퍼티
    // 은닉성 보장
    Knight knight = new Knight();
    knight.Hp = 100;
    int hp = knight.Hp;
}
public int Hp
{
    get {return hp;}
    private set {hp=value;} // 해당 클래스 내에만 사용가능.

    void Test()
    {
        Hp = 100; // OK!
    }
}

static void Main(string[] args)
{
    Knight knight = new Knight();
    knight.Hp = 100; // 에러!
}
// 자동 구현 프로퍼티
class Knight
{
    public int Hp {get; set;}
    public int Mp {get; set;} = 100; // 초기화: C# 7.0 부터 추가된 기능
}

Delegate (대리자)

  • 해당 강좌에서 가장 중요함  

  • 콜백: 함수 자체를 인자로 넘겨준 다음에 나중에 필요로할때 안쪽에서 역으로 호출

    업체 사장 - 사장님의 비서 - 우리의 연락처/용건을 받음 - 거꾸로 연락을 주세요!

delegate int OnClicked();
// delegate -> 형식은 형식인데, 함수 자체를 인자로 넘겨주는 그런 형식
// 반환: int 입력: void
// OnClicked이 delegate 형식의 이름이다!

static void ButtonPressed(OnClicked clickedFunction)
{
    clickedFunction();
}

static int TestDelegate()
{
    Console.WriteLine("Hello Delegate");
    return 0;
}

static int TestDelegate2()
{
    Console.WriteLine("Hello Delegate 2");
    return 0;
}

static void Main(string[] args)
{
    // case 1
    ButtonPressed(TestDelegate); // Hello Delegate가 출력됨

    // case 2 Delegate Chaining
    OnClicked clicked = new OnClicked(TestDelegate);
    clicked += TestDelegate2;

    ButtonPressed(clicked);
    // 출력
    // Hello Delegate
    // Hello Delegate 2
}

Event (이벤트)

  • delegate의 단점: delegate가 다 좋은데(?), 이 delegate를 외부에서 멋대로 호출하는 문제가 있다. -> event로 해결
// 1에서만 호출되어야 하는데 2에서도 호출이 된다
// 만약 clicked이 굉장히 중요한 정보면? -> 설계 미스

static void Main(string[] args)
{
    OnClicked clicked = new OnClicked(TestDelegate);
    clicked += TestDelegate2;

    // 1
    ButtonPressed(clicked);

    // 2
    clicked();
}
// InputManager.cs

// Observer Pattern
class InputManager
{
    // 둘다 접근지정자를 똑같이 맞춰줘야됨
    public delegate void OnInputKey();
    public event OnInputKey InputKey();

    public void Update()
    {
        if (Console.KeyAvailable == false)
            return;

        ConsoleKeyInfo info = Console.ReadKey();
        if (info.key == ConsoleKey.A)
        {
            // 모두에게 알려준다!
            InputKey();
        }
    }
}
// Program.cs

// 무한루프 돌다가 A를 누르면 "Input Received" 메시지가 출력됨!
class Program
{
    static void OnInputTest()
    {
        Console.WriteLine("Input Received");
    }

    static void Main(string[] args)
    {
        InputManager inputManager = new InputManager();

        inputManager.InputKey += OnInputTest(); // 이벤트 구독

        while (true)
        {
            inputManager.Update();
        }

        inputManager.InputKey(); // Error!
    }
}

Lambda (람다식)

  • Lambda: 일회용 함수를 만드는데 사용하는 문법이다.
enum ItemType
{
    Weapon,
    Armor,
    Amulet,
    Ring
}

enum Rarity
{
    Normal,
    Uncommon,
    Rare
}

class Item
{
    public ItemType ItemType;
    public Rarity Rarity;
}

class Program
{
    static List<Item> _items = new List<Item>();

    delegate bool ItemSelector(Item item);

    static Item FindItem(ItemSelector selector)
    {
        foreach (Item item in _items)
        {
            if (selector(item))
                return item;
        }
        return null;
    }

    static void Main(string[] args)
    {
        _items.Add(new Item() {ItemType = ItemType.Weapon, Rarity = Rarity.Normal});
        _items.Add(new Item() {ItemType = ItemType.Armor, Rarity = Rarity.Uncommon});
        _items.Add(new Item() {ItemType = ItemType.Ring, Rarity = Rarity.Rare});

        // Anonymous Function: 무명 함수 / 익명 함수
        Item item = FindItem(delegate (Item item) {return item.ItemType == ItemType.Weapon;});
        // or
        Item item = FindItem((Item item) => {return item.ItemType == ItemType.Weapon;});
        // or
        ItemSelector selector = new ItemSelector((Item item) => {return item.ItemType == ItemType.Weapon;});
        Item item = FindItem(selector);


    }
}
class Program
{
    static List<Item> _items = new List<Item>();

    // 기존 ItemSelector는 item에 한정적이었지만 이 방식은 좀 더 범용적이다.
    // 공용 델리게이트
    delegate Return MyFunc<T, Return>(T item);

    static Item FindItem(MyFunc<Item, bool> selector)...;
    static Item FindItem(Func<Item, bool> selector)...;

    static void Main(string[] args)
    {
        _items.Add(new Item() {ItemType = ItemType.Weapon, Rarity = Rarity.Normal});
        _items.Add(new Item() {ItemType = ItemType.Armor, Rarity = Rarity.Uncommon});
        _items.Add(new Item() {ItemType = ItemType.Ring, Rarity = Rarity.Rare});

       MyFunc<Item, bool> selector = (Item item) => {return item.ItemType == ItemType.Weapon;};
       Item item = FindItem(selector);

       // c#에서 제공: Func
       // delegate를 직접 선언하지 않아도, 이미 만들어진 애들이 존재한다.
       // 반환 타입이 있을 경우: Func
       // 반환 타입이 없으면: Action
       Func<Item, bool> selector = (Item item) => {return item.ItemType == ItemType.Weapon;};
    }
}

Exception (예외 처리)

class TestException : Exception
{

}

static void Main(...)
{
    try
    {
        throw new TestException();
    }
    catch (Exception e)
    {

    }
    finally
    {

    }
}

Reflection (리플렉션)

  • Reflection: X-Ray
class Program
{
    class Important : System.Attribute
    {
        string message;

        public Important(string message) {this.message = message;}
    }

    class Monster
    {
        // Attribute
        [Important("Very Important")]
        public int hp;
        protected int attack;
        private float speed;

        void Attack() {}
    }

    static void Main(...)
    {
        // Reflection 예제
        Monster monster = new Monster();
        Type type = monster.GetType();

        // type.XXX 하면 많은 정보를 알 수 있음.
        
        var fields = type.GetFields(System.Reflection.BindingFlags.Public
        | System.Reflection.BindingFlags.NonPublic
        | System.Reflection.BindingFlags.Static
        | System.Reflection.BindingFlags.Instance);

        foreach (FieldInfo field in fields)
        {
            string access = "protected";
            if (field.IsPublic)
                access = "public";
            else if (field.IsPrivate)
                access = "private";

            // Attribute 예제
            // 브레이크포인트를 걸고 확인하면 hp 필드에서
            // attributes 값이 Very Important 인 것을 확인할 수 있다.
            var attributes = field.GetCustomAttributes();

            // public int hp와 같이 출력
            Console.WriteLine($"{access} {field.FieldType.Name} {field.Name}");
        }
    }
}

Nullable (널러블)

class Monster
{
    public int Id {get; set;}
}

static void Main(...)
{
    // Nullable -> Null + able

    // number에 5를 넣으면 5가 출력되고 null이면 0을 출력한다.
    int? number = 5; 

    int b = number ?? 0;
    Console.WriteLine(b);

    /*-----------------------*/
    Monster monster = null;

    // null이 아니라면 id를 뽑아주고
    // null이라면 null을 넣어주세요!
    int? id = monster?.Id; 

    /* 위 코드와 아래 코드가 동일
    if (monster == null)
        id = null;
    else
        id = monster.Id;
    */
}