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