loquens02 / TextRPG

C#과 유니티로 만드는 MMORPG 게임 개발 시리즈-Part1

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

TextRPG

C#과 유니티로 만드는 MMORPG 게임 개발 시리즈-Part1

강의 리뷰 C#과 유니티로 만드는 MMORPG 게임 개발 시리즈/ 루키스 www.inflearn.com/roadmaps/355

개괄

1. 강의자의 게임 도메인 지식에 입각해서 왜 그러한 변수 타입을 써야하는지 예제와 함께 알려준다

  • 정수 형식

보통은 몇 Byte 차이 신경 안 쓰지만, 네트워크 프로그래밍에서 지연을 최소로 하려면 데이터 형식에도 신경써야한다

  • 2 10 16 진수

2진수 ~16진수 변환이 쉽다. 0b10001111 <=> 0x8f

  • 정수 범위

Byte type 에서 최상위 비트를 + - 를 표현하는 것으로 한다. 1 000 0000 은 0과의 중복을 방지하기 위해 '-128' 로 정의. 음의 보수 개념

  • 불리언/ 소수

불리언: 1Byte 인 이유. 1bit보다 연산이 빨라서

소수: 정수보다 저장 방식이 복잡하다. 부동소수점 등등. 따라서 비싼 연산이므로 아껴써야 한다.

  • 형식 변환

소수에서는 저장 형식에 따라 소수점에서 먼 아랫 자리 수들이 박살이 나므로 주의 정확한 숫자가 아니라 인접한 숫자로 된다. 따라서 소수 비교 때는 == 가 아니라 어느 정도의 오차를 감안해야 한다.

명시적 형변환은 위험하다. 저장 형식이 다르고, 저장 범위를 벗어난 부분은 어떻게 될 지 모르므로

int a3= 0xffff ffff 는 C# 에서는 불가능하다.모든 비트가 1이라서 -1 이 되야 할 것 같지만 ffff ffff 는 10진수로 42억, 즉 int 의 범위를 넘어서기 때문에

2. 간단한 건 명쾌하게 알려준다

  • 데이터 연산 연산자 우선순위를 외울 필요는 없다. 모르는 사람도 바로 파악 가능하도록 괄호 ( ) 쳐주는 게 최선
  • 비트 연산

<< 상위로 n칸 밀기 Windows 계산기> 프로그래밍 모드로 보여줌

최상위 비트로 1이 들어가면 전혀 다른 숫자가 된다

데이터 할당 var: 컴파일러가 알아서 형식을 찾도록 하는 것

3. 예제 및 실습을 통해 방금 들은 것을 내 손으로 직접 쳐보도록 기회를 준다.

  • 흐름제어/ 가위바위보

입력: 사용자

결과: 가위0, 바위1, 보2

컴퓨터: random

문법: enum 은 enum Type이기 때문에 Switch(int)~case: 에 넣으려면 (int)로 형변환이 필요하다

  • 흐름제어/ 소수

  • 재사용/ 구구단

  • 재사용/ 별 삼각형

  • 재사용/ 팩토리얼

4. OOP 필요성 절감하기/ 절차지향 형태로 Text RPG 구현해보도록 기회를 준다

  • TextRPG 직업 고르기

입력: 1~3 혹은 그외

로직: 그외 값을 입력하면 다시 선택하도록

출력: 입력값

  • TextRPG 플레이어 생성

로직: 직업 선택 => 캐릭터 생성

  • TextRPG 몬스터 생성

로직: 직업 선택창으로 돌아가기 => 필드 진입 => 랜덤 몬스터 생성

Player 구조체에 type을 새로 넣으려니, 모든 코드를 고쳐야 되더라. 여기서는 패스

  • TextRPG 전투

로직: 몬스터와 전투 => 전투하기 전에 확률적으로 도망

Fight 구현하려고 봤더니 Player의 정보가 필요하더라. 그래서 Main 에서부터 Play 정보를 계속 넘겨줌.

없던 인자를 새로 만들어주며. >> 절차 지향(함수 조립형)의 한계.

5. 절차지향이 뭔가 쎄하다는 걸 알려주고, 객체지향을 위한 문법을 알려준다.

  • 절차지향 vs 객체지향

절차 == procedure. 함수 기반으로 만들겠다. 함수 조립형

객체 == object / 속성(데이터) + 기능 / 물리적 추상적인 모든 것이 다 객체

  • 복사(값)와 참조

구조체는 기본적으로 복사로 넘기고 (짝퉁)

class는 기본적으로 참조를 한다 (진퉁)

둘을 독립된 개체로 만들되 기본 값을 같게 하고 싶다면

이게 영 귀찮으니까 생성할 때 기존 값을 하나하나 복사해넣자 (깊은 복사)

  • 스택과 힙

stack: 함수 호출, 변수 생성, ref 키워드. 알아서 메모리 관리가 된다

Heap: 인스턴스의 본체(=new Class() ). 실행 중에 동적할당. 참조 대상에는 주소가 들어있다. 주소는 '본체'를 가리킴

  • 생성자

인스턴스 새로 만들 때마다 초기화하는 거 귀찮으니까

클래스랑 이름이 똑같고 반환 형식이 없는 것

클래스이름똑같 ()

클래스이름똑같 (하나만) : this()

  • 상속성, 은닉성, 클래스 형식 변환, 다형성

은닉: 보안성을 올림. 누구로부터 숨기나? 다른 개발자, API 사용자 등

클래스 형식 변환:

Knight : Player
Mage : Player
상황에서 인자로 Knight, Mage 각각을 받은 메서드 오버로딩을 2번 하는 것보다
인자로 Player 만 넘겨주는 게 깔끔하다 (나중에 직업이 50개가 되어도)

다만, 넘겨받은 직후에 Mage 에게만 있는 MP 를 쓰고 싶다고 할 때
player에는 MP가 없으므로 그냥은 방법이 없다.

따라서 Player 형식을 Mage로 바꿔줘야 한다

인스턴스 as Mage
// return: Null or 변환된 타입
체크랑 변환을 동시에 해줌. 인스턴스 is Mage 보다 더 추천하는 방법

다형성: 필요한 이유- 부모에서 만든 메서드 말고 자식에서도 같은 이름의 메서드 선언 Knight와 Mage가 다른 방식으로 Move() 할 수도 있으니

6. 당장 필요하지 않은 문법은 정말 필요하거나 이제는 이해할 수 있을 법하다 싶을 때 알려준다

  • static의 정체

class (붕어빵 틀)안에서 속성을 static으로 선언하면

인스턴스가 아닌 붕어빵 틀에 종속적인 속성이 된다

인스턴스를 아무리 만들어도, static 멤버의 값은 딱 1개만 존재하는 것.

인스턴스 끼리 공유 가능

  • 문자열 둘러보기

이런 게 있다는 것 정도의 소개

Contains

IndexOf

ToLower

ToUpper

Replace

Split

Substring

7. OOP 와 강의자 분의 정수를 담은 코드를 보고 느낄 수 있도록 한다.

  • TextRPG2 플레이어 생성
  1. 파일 분리하기- class 단위
  2. namespace가 같다면, include 필요없이 그냥 인스턴스 만들면 된다
  3. enum 은 사용하는 class와 같은 파일에 두는 게 좋다
  4. 꼭 class 별로 파일을 다 분리할 필요는 없다 (5:00/13:43)
  5. 반드시 정보가 있는 Player만 만들도록 하려면, 인자있는 생성자만 두면 된다 인자 없는 기본 생성자가 없으니, 없는 채로 만드려면 에러난다.
  6. 사실 Player를 그냥 만드는 것도 찝찝하니, 직업 선택해야 만들어지도록 하자> 생성자를 protected 밖에서 Player 생성자 호출 불가. 이제는 Knight(), Archer(), Mage() 로만 생성가능
  7. naming convention- 플젝by플젝 이지만, 멤버변수 앞에 '_' 를 붙이면 알아보기 쉽다 this 없어도 되고
  8. 멤버변수에 기본값을 넣어준다. null 덜 보고, 탈출 로직 짤 때도 낫겠지
  9. 여기선 하드코딩하지만, 게임에서 데이터에 대한 값은 나중에 파일로 읽어서 넣는다
  • TextRPG2 몬스터 생성
  1. Player와 Monster랑 공통 부분이 많다. Creature class 로 따로 빼자 복붙 지양
  2. 객체지향으로 만드니 Fight()에서 PvP 로의 변형도 쉽게 된다
  • TextRPG2 게임 진행
  1. 어떻게 싸움을 붙여야 할까
  2. 보이지는 않지만 존재하는 '로비'나 '게임 스테이지'도 객체로 만들 수 있지 진행은 함수로 하지만 모드(게임 스테이지) 자체를 enum 으로 처리해서 상태를 변경하면 그리로 가도록. 어렵.
  3. '게임 진행'을 객체(class)로 만듦
  4. Player를 Lobby에서 생성할 필요가 없다. Game 진행하는 내내 있는 거니까
  5. 로비나 마을로 가는 건 private으로 충분. 전체를 총괄하는 Process가 있으니까
  6. 직업 선택했으면 마을로 가도록 설정 _mode= GameMode.Town
  7. 절차지향 때는 Lobby 다음에 꼭 Town 이었는데 이제는 Town에 왔어도 이전의 위치가 정해진 것이 아니다. 그 다음에 갈 곳만 정해줬을 뿐. 이제 기획자가 와서 로비 다음에 필드로 뛰라고 해도 _mode= GameMode.Town 부분만 .Field로 바꾸면 된다
  • TextRPG2 마무리
  1. GameMode _mode= GameMode.Lobby 디자인 패턴::스테이트 패턴
  2. Random은 자주 쓰니까, Monster도 게임 진행 내내 필요하니까 Game 클래스에 멤버로 포함시킴
  3. 필드에 입장하는 거랑 몬스터를 랜덤 생성하는 것은 다른 내용이니 함수로 분리한다. 그럼 읽기가 더 쉬워짐

8. 자료구조 맛보기

  • 배열, 다차원 배열
  1. 변수 1000개를 일일이 만들고 쓰기도 어려우니 배열을 사용
  2. 다른 타입은 못 넣는다.
  3. 기본형 문법 나오고, 점차 편의에 따라 생략하는 것도 추가해줌
    1. int[] a= new int[5]{1,2,3,4,5}
    2. int[] a= new int[]{1,2,3,4,5}
    3. int[] a= {1,2,3,4,5}
  1. new 라는 키워드를 살려둬야 '동적으로 할당한다' 는 걸 명시적으로 알 수 있어서 좋음
  2. 동적으로 할당한다
    int[] b= a
    a와 b는 같은 본체를 참조하고 있기 때문에
    b[0] 을 바꾸면 a[0] 도 바뀐다

다차원 배열- Console에서 map만들 때 쓴다

  • List 주의: 동적 배열 방식이기에 추가/삭제 비용이 비싸다
ex. 추가
{0 1 2 3 4}
{0 1 _ 2 3 4}
      공간을 확보하고
      기존 애들을 옆으로 이사보내고
{0 1 9 2 3 4}
빈 공간에 추가
  • Dictionary

List 단점 극복(몬스터 id 를 List 로 저장)

100만 마리 중에 103 번째 몬스터를 찾으려면?

루프를 도는 중에 102 번째 몬스터가 죽는다면?? ㄷㄷ

구현: HashTable
1만개 공을
큰 상자 1개 대신, 중간 크기 상자 10개에 나눠놓음
7777번째 공은 777번째 상자 안에 있을 것

9. 어렵지만 알게되면 인생이 편해지는 꿀맛 C# 문법 + 자연스레 이해하는 디자인 패턴

  • Generic
  필요성: 임의로 만든 Type을 받아주는 List를 각각 만든다?? 너무 힘듦
var과 달리 Object는 그 자체로 Type. 최상위 객체.
  
C# 만의 고유 문법- Generic 한정자
class MyList<T> where t: struct
T 는 반드시 값 형식이어야 한다
class MyList<T> where T: class
T 는 반드시 참조 형식이어야 한다
  • 추상 클래스 interface
추상 클래스 필요성: Monster 라면 모두 소리지르도록 만들고 싶음
Shout() 를 무조건 재정의하도록 하고 싶은데, 기존의 virtual~override 로는 강제할 수 없음
추상 클래스 한계: 인스턴스도 만들 수 없다 


인터페이스 필요성: C# 에서는 다중상속이 안 된다. 두 부모가 같은 조상을 섬기고 있을 때, 부모가 똑같이 품고 있는 Shout() 둘 중에 무얼 호출할지 애매하므로.

인터페이스 장점: 구현이 안 되어있으니, 하고 싶은 만큼 마음껏 상속(구현) 해줘도 되고, 자식에게 구현의 의무 또한 지어줄 수 있다.
  • 프로퍼티. Property
  getter setter 의 지옥에서 벗어나게 해주는 C#만의 마법
public int Hp { get; set; }

쓸 때도 마치 멤버변수에 직접 접근하듯이 쓰지만, 실제로는 은닉성의 모든 이점도 누릴 수 있다.
knight.Hp = 200;
  • Delegate. 콜백
  콜백이란? 
"사장님 지금 안 계세요. 연락처랑 용건 남기시면 나중에 연락드릴게요"
상황되면 나중에 연락 받을 수 있다.

콜백 기능을 위한 C# 초창기 문법
콜백 필요성: 여태까진 함수 요청하면 바로바로 원하는 동작을 하도록 만들었지만
실제 상황에서는 항상 그렇게 되긴 어렵다
      1. UI/ Button 누르고 관련 기능을 다 추가하면, UI 로직과 다른 기능적인 로직이 섞인다
      2. 함수 자체를 수정할 수 없는 경우
            Console.WriteLine() 그냥 쓰듯이
            ButtonPressed() 자체도 수정할 수 없는 형태로 제공받을 수 있다.
특히 Unity 유니티 엔진 코드… 

delegate int OnClicked();
      이건 함수가 아니다. 인자로 넘겨줄 '형식'임
      반환은 int, 입력은 void 인 형식
      
대상 함수에 '형식'을 넘겨줌
void ButtonPressed(OnClicked clickedFunction)
{ clickedFunction(); # 나중에 호출}
      유니티가 제공하는 함수
      
반환이 int, 입력이 void인 함수를 아무거나 만들어줌(인자 용)
int TestDelegate() { Console.WriteLine("Hello Delegate"); return 0; }
      우리가 손댈 수 있는 부분
      
그리고 인자로는 '함수'를 넣어줌
      형식을 넘겨주는 게 아니라 함수를!!
      방법1. C++ 의 함수 포인터와 비슷하게 구현한 것
void Main(string[] args)
{ ButtonPressed(TestDelegate); }
  • Event. 구독
  필요성: Delegate가 유용하긴 한데 아무나 다 호출할 수 있으니
Delegate를 맵핑해서 만든 게 Event

> Event
public delegate void OnInputKey(); // 형식
public event OnInputKey InputKey; // 형식대로 event 만듦
      delegate, event 중 하나가 public이면 둘 다 public 이어야 에러가 안난다 - 일관성
event 첫 글자가 대문자인 것: 강의자 습관- 외부노출 하는 거

> 구독신청
만든 event 에 대해 알려달라
      유튜브 구독이랑 똑같다고 보면 된다.
class InputManager
{ public delegate void OnInputKey(); // delegate 반환타입 delegate형식이름 (입력타입)
  public event OnInputKey InputKey; // event delegate형식 event이름
  public void Update()
  {
    if(Console.KeyAvailable == false) return;
    ConsoleKeyInfo info = ConsoleReadKey();
    // 구독자에게 알려준다!
    if( info.Key == ConsoleKey.A) { InputKey(); }
  }
}

키 A 를 누르면 InputKey() 가 호출되면서, 키가 눌렸다고 안내방송을 시작할 것.
  • Lambda
  일회용 함수를 만드는 데 쓰는 문법
이거 자체는 간단한데, Lambda 가 필요한 이유가 메인!

> 필요성
아이템을 장착 부위별, 희귀도별로 만들고 해당 아이템 종류가 있는지 찾고 싶음
4*3 가지 Find() 함수를 만들 수는 없으니까
조건을 만족시키기만 하면 해당 함수를 호출해주는 delegate 형식으로 두었는데

1. Item 을 받아서 유효한지 아닌지 판단하는 것
2. 을 delegate 형식으로 만듦
3. 해당 delegate 형식(ItemSelector)을 인자로 받는 함수를 하나 만들어서
item이 selector 조건을 충족한다면 item을 반환하겠다고 함
4. 되게 깔끔해졌긴 했는데,
5. 결국은 그 인자로 넘겨주기 위한 함수를 만들어야 한단 말이지
6. 근데 인자로만 쓰고 안 쓸건데, 따로 라인 먹으며 코드 안에 두는 게 별로임
      이게 또 12개씩 붙어있으면 더욱 지지

> 익명 함수
7. 그래서 1회용으로만 쓰겠다. 


익명 함수 완성 (무명함수. Anonymous Function). 이건 Lambda 식 아님.

> 무조건 일회용만 되는 건 아니고, delegate 객체에 저장하면 재사용 가능


결론: Func<> 와 Lambda 를 같이 쓰면 코드가 매우 우아해진다
  • Exception
try{ } : 에러 날 것 같은 부분
catch{ }: 에러를 잡음
finally{ }: 에러나도 실행. DB connection 정리 등

게임에서는 잘 안 쓴다. 에러가 나면 crush 되도록 냅두고, 나중에 로직을 고친다
catch(Exception) 은 작은 범위부터 써준다. 위에서 catch 되면 그대로 나간다. if ~ else if 처럼 생각
  • Reflection
  RunTime 중에 class 구성 요소를 뜯어보고 분석할 수 있다
유니티~C# 에서 이 기능을 지원하기에, script 변경사항을 편하게 유니티에서 볼 수 있다.

언리얼 엔진~C++ 에서는 이게 없어서, 복잡하게 parsing 해야 한다
  • Nullable
  필요성: 값을 return했는데 이게 과연 제대로 연산한 건지, 에러난 건지
return 0 만으로는 불분명하기에.

변수에 null을 할당할 수 있도록 한다

타입? 변수1= 값;
      null 할당 가능해짐
타입 변수2= 변수1 ?? null일때값2;
=(변수1 != null) ? 변수1 : null일때값2 

About

C#과 유니티로 만드는 MMORPG 게임 개발 시리즈-Part1


Languages

Language:C# 100.0%