정신과 시간의 방

전체 글 273

카테고리 설명
게임 프로그래머가 되기 위한 청춘의 기록
  • '게임 프로그래밍 패턴' 교재를 학습하며 핵심 내용과 함께 개인적으로 보충한 내용을 정리해보았다. 경량 패턴(Flyweight Pattern; 플라이급 패턴)이란? - 한 개의 고유 상태를 다른 객체들에게 공유하여 메모리 사용을 줄이는 패턴이다. - 생성 객체를 공유한다는 점에서 C++ 포인터와 같은 개념이다. 언제, 왜 사용하는가? - 공통적으로 어떤 객체의 개수가 너무 많아서 좀 더 가볍게 만들고 싶을 때 활용할 수 있다. - 경량 패턴을 사용하면 객체를 마구 늘리지 않고도 객체지향방식의 장점을 취할 수 있으므로 열거형으로 수많은 다중 선택문(switch문)을 만들 때 대안으로 사용할 수 있다. 경량 패턴에서의 객체 데이터 두 종류 1. 고유 상태(=자유 문맥) : 모든 객체의 데이터 값이 같아서 공..

  • '게임 프로그래밍 패턴' 교재를 학습하며 핵심 내용과 함께 개인적으로 보충한 내용을 정리해보았다. 명령 패턴의 정의 - 명령 패턴이란 메서드 호출을 객체로 감싸 객체로 다룰 수 있게 하는, 콜백을 객체 지향적으로 표현한 것이다. - 콜백, 일급 함수, 함수 포인터, 클로저, 부분 적용 함수와 비슷하다. 명령 패턴 활용 예제 - 입력 키 변경에서 명령 패턴을 사용하는 이유? : 대부분의 게임이 입력키를 변경할 수 있도록 구현됨. - 왜? 1. 크로스 플랫폼을 지원하는 게임의 경우 입력키가 기기에 따라 달라질 수 있음 2. 개발 도중 입력에 대한 기획이 변경될 수도 있음 - 명령 패턴을 활용한 입력키 변경 구현의 핵심은? : 함수를 직접 교체하는 것이 아니라 교체 가능한 객체로 만드는 것 - 구체적인 구현 ..

  • C#과 유니티로 만드는 MMORPG 게임 개발 시리즈 강의를 수강하며 환경 설정 파트에서 배운 내용을 정리해보았다. 이번 강의에서는 길찾기 알고리즘을 구현하기 전에 먼저 환경 설정을 해주었다. class Program { static void Main(string[] args) { Console.CursorVisible = false; // 프레임 관리를 위한 시간표시 int lastTick = 0; const int WAIT_TICK = 1000 / 30; const char CIRCLE = '\u25cf'; while (true) { #region 프레임 관리 int currentTick = System.Environment.TickCount; // 만약에 경과한 시간(elapsedTick)이 1/3..

  • C#과 유니티로 만드는 MMORPG 게임 개발 시리즈 강의를 수강하며 배운 내용과 자료구조 교재로 보충 공부한 것들을 한 번에 모아 정리해보았다. Big-O 표기법이란? - 함수 T(n)에서 가장 영향력이 큰 부분이 어딘가를 따지는 것. - 이를 위해서 데이터 수의 증가에 따른 연산횟수의 증가 형태를 표현한다. - 즉, 입력 N(데이터)의 크기(수의 증가)에 따라 성능이 영향(연산횟수 증가)을 받는 정도를 나타낸다. Big-O 표기법을 사용하는 이유 - 두 알고리즘 A와 B의 효율을 비교할 때 다음과 같은 문제가 발생함 1) A와 B의 속도를 비교하는데 정확한 기준이 없기 때문에 애매모호해짐 2) 알고리즘을 비교하기 위해 프로그램을 짜서 실행 속도를 비교하는 것은 환경에 의존적이라 정확하지 않을 수 있음..

  • C#과 유니티로 만드는 MMORPG 게임 개발 시리즈 강의를 수강하며 널러블 파트에서 배우고 느낀 점을 정리해보았다. 왜 널러블이 필요한가 - 아래의 예제를 통해 Null 값을 사용할 수 있는 것은 class 처럼 참조타입으로 동작하는 경우에 한정되어 있음을 알 수 있다. class Program { class Monster { } static Monster FindMonster(int id) { // for () // 몬스터를 범위 내에서 찾고 // return monster; // 있다면 리턴 return null; // 없다면 null을 리턴 } static void Main(string[] args) { // Nullable -> Null + able Mon..

  • C#과 유니티로 만드는 MMORPG 게임 개발 시리즈 강의를 수강하며 리플렉션 파트에서 배우고 느낀 점을 정리해보았다. 리플렉션이란 - 클래스가 가지고 있는 모든 정보들을 Runtime중에 뜯어보고 분석해볼 수 있는, 일종의 X-Ray라고 할 수 있다. 사용 예제 using System.Reflection; namespace CSharpStudy { class Program { class Monster { public int hp; protected int attack; private float speed; void Attack() { } } static void Main(string[] args) { Monster monster = new Monster(); Type type = monster.GetT..

  • C#과 유니티로 만드는 MMORPG 게임 개발 시리즈 강의를 수강하며 예외처리 파트에서 배우고 느낀 점을 정리해보았다. 예외처리란? - 예상치 못한 예외적인 상황에 대한 처리를 뜻한다. - 게임 쪽에서는 잘 안쓰는 경향이 있다. -> 왜? 게임에서는 예외처리를 따로 하기보다는 크래쉬가 나도록 두고, 빠르게 수정하는 쪽으로 처리함. (한 사람이 예외가 발생한다는 건 다른 사람들도 모두 똑같이 예외가 발생한다는 것이므로 한 사람만 따로 예외처리하는 것이 의미가 없다.) -> 단, 심각한 예외가 아니거나 네트워크 관련 예외의 경우에는 catch해서 처리할 수 있다. 예외처리 문법 (try ~ catch, finally) static void Main(string[] args) { try // 1. 먼저 try..

  • C#과 유니티로 만드는 MMORPG 게임 개발 시리즈 강의를 수강하며 이벤트 파트에서 배우고 느낀 점을 정리해보았다. 이벤트를 사용하는 이유? - 델리게이트가 의도하지 않은 곳에서 멋대로 호출될 수 있다는 단점을 가지고 있으므로, 이를 통한 문제 발생을 막기 위해 델리게이트를 맵핑하기 위해서이다. 예제 // 구독자를 모집한 다음 특정 이벤트가 발생했을 때 구독자에게 // 메시지를 뿌리는 방식을 옵저버 패턴이라고 한다. class InputManager // 사용자가 키보드나 마우스를 입력하는 것을 캐치하여 다른 게임 로직, // 프로그램에 알려주는 중간 매개 역할을 하는 클래스 { public delegate void OnInputKey(); // 델리게이트 형식의 보호수준 이벤트와 // 변수 보호수준은..

    C#

    220822 Event (이벤트) NEW

    2022.08.22
    댓글
카테고리
작성일
2022. 8. 28. 22:14
작성자
risehyun

'게임 프로그래밍 패턴' 교재를 학습하며 핵심 내용과 함께 개인적으로 보충한 내용을 정리해보았다.


  • 경량 패턴(Flyweight Pattern; 플라이급 패턴)이란?
    - 한 개의 고유 상태를 다른 객체들에게 공유하여 메모리 사용을 줄이는 패턴이다.
    - 생성 객체를 공유한다는 점에서 C++ 포인터와 같은 개념이다.

  • 언제, 왜 사용하는가?
    - 공통적으로 어떤 객체의 개수가 너무 많아서 좀 더 가볍게 만들고 싶을 때 활용할 수 있다.
    - 경량 패턴을 사용하면 객체를 마구 늘리지 않고도 객체지향방식의 장점을 취할 수 있으므로 열거형으로 수많은 다중 선택문(switch문)을 만들 때 대안으로 사용할 수 있다.

  • 경량 패턴에서의 객체 데이터 두 종류
    1. 고유 상태(=자유 문맥) : 모든 객체의 데이터 값이 같아서 공유할 수 있는 데이터 ex. 나무 형태, 텍스처
    2. 외부 상태 : 인스턴스 별로 값이 다른 데이터 ex. 나무의 위치, 크기, 색

  • 구체적인 사용과 구현 예시
    1. 공유 객체가 명확한 경우
     - 게임 속에 수천 그루가 넘는 월드를 표현해야 할 때, 이 나무들은 공통적으로 다음과 같은 데이터를 가진다.

    폴리곤 메시
    텍스처
    월드에서의 위치와 방향
    크기와 음영 값을 조절할 수 있는 매개변수

     - 이렇게 객체에 든 데이터 대부분이 인스턴스별로 다르지 않다면 같은 내용을 메모리에 중복하여 여러번 올릴 필요가 없다.

     - 따라서 공유 객체인 TreeModel 객체 하나만 존재하게 하고 Tree 클래스에는 인스턴스별로 다른 상태값만 남긴 채 나머지는 공유객체를 참조하도록 구현하면 된다.

     2. 공유 객체가 명확하지 않은 경우
      - 타일 기반의 지형 정보를 구현할 때, 땅이 작은 타일들이 모여 있는 거대 격자이고 모든 타일이 지형 정보 중 하나로 덮여 있는 경우 지형 종류에는 다음과 같이 게임 플레이에 영향을 주는 속성이 들어 있다.

    플레이어가 얼마나 빨리 이동할 수 있을지를 결정하는 이동 비용 값
    강이나 바다처럼 보트로 건너갈 수 있는 곳인지 여부
    렌더링 할 때 사용할 텍스처

      - 이러한 데이터는 하나로 합쳐 캡슐화하는 것이 좋으므로 지형(Terrain) 클래스를 따로 만드는 것이 좋다. 이때 같은 Terrain 객체를 여러 곳에서 공유해서 쓰기 때문에 모든 메서드를 const로 만든다. 단, 이 경우 한 곳에서 값을 바꾸면 그 결과가 여러군데서 동시에 나타나게 된다.

      - 또한 타일마다 Terrain 인스턴스를 하나씩 만드는 비용을 방지하기 위해서 타일의 위치와 같이 타일 객체마다 달라지는 내용을 제외해 모든 지형상태가 고유한 "자유문맥" 상태를 유지하도록 한다. 이렇게 하면 world 클래스 격자 멤버 변수에 열거형이나 terrain 객체 대신 terrain 객체의 포인터를 넣어 지형 종류가 같은 타일들은 모두 같은 terrain 인스턴스 포인터를 가진다.

     - Terrain 인스턴스는 여러곳에서 사용되므로 동적 할당시 생명 주기 관리에 어려움이 있을 수 있기 때문에 World 클래스에 저장한다.

     - World 클래스가 지형의 세부 정보와 커플링 되지 않으므로 타일 속성(지형 속성값)은 World 메서드 대신 Terrain 객체에서 바로 얻을 수 있다.

  • 경량 패턴의 단점? 경량 패턴 사용 시의 성능에 대해
    - 지형 데이터를 포인터로 접근한다는 것은 간접 구조화를 한다는 뜻이므로 포인터를 따라가면 캐시 미스가 발생할 수 있어 성능이  조금 떨어질 수는 있다. 하지만 이는 객체가 메모리에 어떤 식으로 배치 되느냐에 따라 달라질 수 있으므로 성능이 우려 된다면 사전에 프로파일링을 먼저 해보는게 좋다.

  • 관련 자료
    - 경량 객체를 미리 전부 만들고 싶지 않거나 어떤 경량 객체가 실제로 필요한지 예측할 수 없다면 필요할 때 만드는 것이 낫다.

    - 공유 기능을 유지하고 싶을 땐 인스턴스 요청시 이전에 같은 걸 만들어 놓은 게 있는지를 확인하고 그것을 반환한다. 이 때는 객체 생성시 기본 객체의 존재 여부를 먼저 확인할 수 있도록 생성 코드를 인터페이스 밑으로 숨겨둔다. 이렇게 생성자를 숨기는 방식은 팩토리 메서드 패턴의 한 예이기도 하다.

    - 이전에 만든 경량 패턴 객체를 반환하기 위해 이미 생성해 둔 객체를 찾을 수 있는 풀을 관리해야 하며 이때 객체(오브젝트) 풀 패턴이 유용하게 사용 된다.

    - 상태 패턴 사용시 '상태' 객체에 멤버 변수가 하나도 없는 경우 경량 패턴을 사용하면 상태 인스턴스 하나를 여러 상태 기계에서 동시에 재사용할 수 있다.

'CS > 디자인패턴' 카테고리의 다른 글

관찰자 패턴(Observer Pattern)  (0) 2022.08.29
2장. 명령 패턴(Command Pattern)  (0) 2022.08.23
1장. 도입  (0) 2022.08.16
카테고리
작성일
2022. 8. 23. 21:16
작성자
risehyun

'게임 프로그래밍 패턴' 교재를 학습하며 핵심 내용과 함께 개인적으로 보충한 내용을 정리해보았다.


  • 명령 패턴의 정의
    - 명령 패턴이란 메서드 호출을 객체로 감싸 객체로 다룰 수 있게 하는, 콜백을 객체 지향적으로 표현한 것이다.

    - 콜백, 일급 함수, 함수 포인터, 클로저, 부분 적용 함수와 비슷하다.


  • 명령 패턴 활용 예제
    <입력키 변경>
    - 입력 키 변경에서 명령 패턴을 사용하는 이유?
    : 대부분의 게임이 입력키를 변경할 수 있도록 구현됨.
     
    - 왜?
     1. 크로스 플랫폼을 지원하는 게임의 경우 입력키가 기기에 따라 달라질 수 있음

     2. 개발 도중 입력에 대한 기획이 변경될 수도 있음

    - 명령 패턴을 활용한 입력키 변경 구현의 핵심은?
    : 함수를 직접 교체하는 것이 아니라 교체 가능한 객체로 만드는 것

    - 구체적인 구현 방법
    : 1. 게임에서 할 수 있는 행동을 실행하는 공동 상위 클래스(command)를 정의 한다. command는 액터를 제어하는 일급 객체가 되며, 이 때 제어하려는 객체를 함수의 인자로 전달할 수 있게 함으로써 파생 클래스에서 원하는 액터의 메서드를 호출할 수 있다.(ex. actor.jump();)

      2. 상위 클래스를 상속받아 각 행동(ex. jump, attack...)별로 하위 클래스를 구현한다.

      3. 입력 핸들러에 각 버튼별 클래스 포인터를 저장한다. (ex. command buttonx_;) 이 때 enum 타입으로 사용될 키값들을 정의하여 구분해 줄 수도 있다.

      4. 입력 핸들러가 명령을 직접 실행할 수 없는 대신 명령 객체를 리턴하도록 한다. 이를 통해 입력 핸들러가 함수 호출시점을 지연시키는 추상 계층 역할을 할 수 있게된다. 

      5. 명령 객체를 받아서 제어하려는 객체(ex. gameActor)에 적용하는 코드를 작성한다.

     - 이렇게 했을 때의 장점?
    1. 커맨드 객체를 선택하는 것과 이를 실행하는 액터가 디커플링 되었기 때문에 명령을 실행 할 때 액터만 변경해주면 게임의 어느 액터라도 제어할 수 있다.

    2. 앞선 이유와 마찬가지로 로직이 각각 디커플링 상태이기 때문에 코드가 더욱 유연해진다.

    - 이러한 구조를 어떻게 활용할 수 있는가?
    1. AI 엔진과 액터 사이의 인터페이스로 사용할 수 있다. 이 경우 AI 코드가 원하는 Command 객체를 이용할 수 있게 되므로 구체적으로 다음과 같은 상황에 대응이 가능하다.

    ▶ 액터마다 AI 모듈을 다르게 적용할 수 있다.

    ▶ 위와 비슷하게 AI마다 다양한 패턴의 공격 명령을 적용할 수 있다.

    플레이어 캐릭터에 AI를 연결하여 자동으로 실행되는 데모 모드나, 자동전투를 구현할 수 있다.

    - 단, 어떤 일을 하는지를 정의한 명령 객체 하나가 매번 재사용되기 때문에 이를 변형하여 <실행취소와 재실행>에서 다룬 것처럼 특정 시점에 발생될 일을 표현할 수 있다.


    <명령을 큐나 스트림으로 만드는 경우> ※
    - 구조는 AI → 명령 스트림 →액터가 된다.

    - 입력 핸들러나 AI 같은 코드에서 명령 객체를 만들어 스트림에 밀어 넣는다. 이때 명령들을 직렬화 할 수 있게 되면  네트워크로도 전달 할 수 있다. 즉, 플레이어로부터 입력을 받아서 네트워크를 통해 상대방에게 전달해 입력을 재현할 수 있는 것이다.

    - 디스패치나 액터에서는 명령 객체를 받아서 호출한다. 큐를 둘 사이에 끼워 넣음으로써, 생산자와 소비자를 디커플링 할 수 있게 된다.


    <실행취소와 재실행>
    - 명령 패턴에서 명령 객체가 어떤 작업을 실행할 수 있는 기능을 응용하면 실행취소(undo) 기능과 재실행 기능을 구현할 수 있다.

    - 실행 취소 기능은 원치 않는 행동을 되돌릴 수 있는 전략 게임에서도 활용 되며, 게임 개발 툴에서도 레벨 에디터에 실행 취소 기능을 제공할 수 있게 해준다. (흔히 사용하는 ctrl+z)

    - 구체적인 예로는 싱글플레이어 턴제 게임에서의 이동 취소 기능을 들 수 있다. 명령 패턴을 사용하면 이미 명령 객체를 이용해 입력 처리를 추상화 하기 때문에 플레이어 이동도 명령에 캡슐화되어 있다.

    그렇기에 어떤 유닛을 옮기는 명령을 처리할 때, 이전 방법 처럼 명령에서 변경하려는 액터와 명령 사이를 추상화로 격리시키는 것이 아니라 이를 변형하여 이동하려는 유닛과 위치 값을 생성자에서 받아서 명령과 명시적으로 바인딩 할 수 있다.

    이를 통해 특정 시점에 발생될 일을 표현할 수 있다. 구체적으로 말하자면 이전 방식과 달리 command 클래스가 일회용이 되기 때문에, 입력 핸들러 코드는 플레이어가 이동을 선택할 때마다 명령 인스턴스를 생성해야 한다. 

    이런 특징을 활용하면 여러 단계의 실행 취소를 지원할 수 있다. 스택에 최근 명령만 기억하는 대신, 명령 목록을 유지하고 현재 명령이 무엇인지만 알고 있으면 된다. 정리하자면 다음과 같다.

    - 여러 단계의 실행 취소 기능 구현
    ▶ '실행 취소'시 현재 명령을 실행 취소하고 현재 명령을 가리키는 포인터를 뒤로 이동시킨다.

    ▶ '재실행'시 포인터를 다음으로 이동시킨 후에 해당 포인터를 실행한다.
    → 이러한 재실행은 게임에서 잘 쓰이지 않을 수도 있지만 '리플레이'기능의 구현과 동일하다. 기능 자체는 매 프레임마다 전체 게임 상태를 저장하는 식으로 구현가능하지만 이 경우 메모리가 너무 많이 필요하다는 문제가 있다.
    이를 대신해 전체 개체가 실행하는 명령 모두를 매 프레임 저장하고, 게임을 '리플레이'할 때 이전에 저장한 명령들을 순서대로 실행해 게임을 시뮬레이션 하도록 구현할 수 있다.

    ▶ 여러번 '실행취소' 한 다음 새로운 명령을 실행한다면, 현재 명령 뒤에 새로운 명령을 추가한 뒤 그 다음에 붙어 있는 명령들을 버린다. 

  • 명령 패턴에서의 클래스와 함수형
    - C++의 경우 일급 함수를 제대로 지원하지 않으므로 명령 패턴에 클래스를 사용하지만, 클로저를 지원 하는 언어(ex. C#과 자바스크립트)의 경우에는 함수를 사용할 수 있다.


  • 관련자료
    - 명령 패턴에서 사용되는 Command 클래스의 수가 많은 경우 구체 상위 클래스에 여러 가지 편의를 제공하는 상위 레벨 메서드를 만들어 사용하면 하위 클래스에서 바로 원하는 작동을 재정의 할 수 있게 되어 편리하다. (명령 클래스의 execute 메서드가 하위 클래스 샌드박스 패턴으로 발전하게 됨)

    - 어떤 액터가 명령을 처리할지는 명시적 일 수 도 있고 아닐 수도 있다. 이처럼 객체가 명령에 반응할 수도 있고 종속 객체에 명령 처리를 떠넘길 수도 있는 경우는 책임 연쇄 패턴이라고도 볼 수 있다.

    - 어떤 명령은 특정 상태 없이 순수하게 행위만 정의 되어 있을 수도 있다. 이런 클래스는 모든 인스턴스가 동일하기 때문에 여러 인스턴스를 만드는 것은 메모리 낭비다. 이는 경량 패턴을 사용하면 해결 할 수 있는 문제이다.

+)

- 일급 객체란?

: 다른 객체들에 일반적으로 적용가능한 연산을 모두 지원하는 객체를 뜻한다.

- 일급 객체의 조건
1. 변수에 할당할 수 있다.

2. 다른 함수를 인자로 전달 받는다.

3. 다른 함수의 결과로 리턴 될 수 있다.

 

'CS > 디자인패턴' 카테고리의 다른 글

관찰자 패턴(Observer Pattern)  (0) 2022.08.29
3장. 경량 패턴(Flyweight Pattern)  (0) 2022.08.28
1장. 도입  (0) 2022.08.16
카테고리
작성일
2022. 8. 23. 18:17
작성자
risehyun

C#과 유니티로 만드는 MMORPG 게임 개발 시리즈 강의를 수강하며 환경 설정 파트에서 배운 내용을 정리해보았다.


이번 강의에서는 길찾기 알고리즘을 구현하기 전에 먼저 환경 설정을 해주었다.

   class Program
    {
        static void Main(string[] args)
        {
            Console.CursorVisible = false;

            // 프레임 관리를 위한 시간표시
            int lastTick = 0;

            const int WAIT_TICK = 1000 / 30;
            const char CIRCLE = '\u25cf';

            while (true)
            {
                #region 프레임 관리
                int currentTick = System.Environment.TickCount;

                // 만약에 경과한 시간(elapsedTick)이 1/30보다 작다면
                if (currentTick - lastTick < WAIT_TICK)
                    continue;
                lastTick = currentTick; // 1/30초마다 문을 열어줘서 아래로 통과시키고,
                                        // 그것이 아니라면 준비가 되지 않았으니 기다리라는 의도로 설정해 준 것

                // FPS 프레임 (60)프레임 -> OK, 30프레임 이하로 NO)
                // 프레임? -> 루프가 1초에 몇 번이 실행되고 있나?
                // 부드럽다 -> 아주 짧은 시간안에 빠르게 연산을 해서 게임이 부드럽게 돌아가는 것.
                #endregion

                // 입력


                // 로직


                //렌더링

                Console.SetCursorPosition(0, 0);

                for (int i = 0; i < 25; i++)
                {
                    for (int j = 0; j < 25; j++)
                    {
                        Console.ForegroundColor = ConsoleColor.Cyan;
                        Console.Write(CIRCLE);
                    }
                    Console.WriteLine();
                }
            }
        }
    }

코드를 실행한 모습

단순히 도형만 출력한 것이 아니라, 게임의 구현 요소는 크게 1.입력 2.로직. 3.렌더링 으로 나눠져있음을 알게 되었다.

또한 사용자가 프로그램을 종료하기 전까지 계속해서 유지되어야 하기 때문에 무한 루프를 돌아야 한다는 것.

프레임의 정의에 대해서도 학습했다.

카테고리
작성일
2022. 8. 23. 16:21
작성자
risehyun

C#과 유니티로 만드는 MMORPG 게임 개발 시리즈 강의를 수강하며 배운 내용과 자료구조 교재로 보충 공부한 것들을 한 번에 모아 정리해보았다.


  • Big-O 표기법이란?
    - 함수 T(n)에서 가장 영향력이 큰 부분이 어딘가를 따지는 것.
    - 이를 위해서 데이터 수의 증가에 따른 연산횟수의 증가 형태를 표현한다.
    - 즉, 입력 N(데이터)의 크기(수의 증가)에 따라 성능이 영향(연산횟수 증가)을 받는 정도를 나타낸다.


  • Big-O 표기법을 사용하는 이유
    - 두 알고리즘 A와 B의 효율을 비교할 때 다음과 같은 문제가 발생함

    1) A와 B의 속도를 비교하는데 정확한 기준이 없기 때문에 애매모호해짐
    2) 알고리즘을 비교하기 위해 프로그램을 짜서 실행 속도를 비교하는 것은 환경에 의존적이라 정확하지 않을 수 있음
    3) 입력이 적은 구간과 많은 구간에서 성능이 확연하게 차이가 나는 경우가 존재함

    - Big-O 표기법은 상술한 문제를 해결하기 위한 대안으로 사용됨.

  • Big-O 표기법의 사용
    - 1단계 : 대략적인 계산
    : 수행되는 연산(산술, 비교 대입 등)의 개수를 대략적으로 판단한다.
    ex. 단순히 N+N을 리턴하는 ADD 함수는 1개의 연산을 수행한다.
    1개의 for문으로 I를 N번까지 반복하여 값을 더하고 그 합을 리턴하는 ADD 함수는 N+1개의 연산을 수행한다.
    2개의 for문으로 I와 j를 N번까지 반복하여 값을 더하고  그 합을 리턴하는 ADD 함수는 N^2+1개의 연산을 수행한다.

    - 2단계 : 대장만 남긴다
    : T(n)이 다항식으로 표현된 경우, 최고차항의 차수가 Big-O가 된다. 따라서 다음과 같은 규칙 2가지를 적용한다.
    1) 영향력이 가장 큰 대표 항목만 남기고 삭제
    2) 상수는 무시한다(ex. 2N => N)

    ex. 빅오를 따져볼 항목을 분석한 결과 O(1+N+4*N^2+1)이 되었다면, ()안의 식중에서 가장 차수가 높은 4*N^2만 남기고 모두 삭제한다. 이어서 상수는 무시한다는 규칙이 있기 때문에 앞의 4는 무시한다.
    결과적으로 해당 식(T(n))의 빅오는 O(N^2)가 된다.


  • 대표적인 Big-O
    - O(1) : 상수형 빅-오라고 한다. 데이터 수에 상관없이 연산 횟수가 고정인 유형의 알고리즘을 뜻한다. 만약 3회 진행되는 알고리즘이라 하더라도 표기는 O(1)로 통일한다. 스택에서의 Push, Pop을 할때나 Hash Table을 이용해 접근하는 경우가 이와 같다.

    - O(logn) : 로그형 빅-오라고 한다. 데이터 수의 증가율에 비해서 연산횟수의 증가율이 훨씬 낮은 알고리즘을 뜻한다. 이진트리의 시간 복잡도가 이와 같다.

    - O(n) : 선형 빅-오라고 한다. 데이터의 수와 연산횟수가 비례하는 알고리즘을 뜻한다. for문, 트리순회, linked list 순회의 시간 복잡도가 이와 같다.

    - O(nlogn) : 선형로그형 빅-오라고 한다. 데이터 수가 두 배로 늘 때, 연산 횟수는 두 배를 조금 넘게 증가하는 알고리즘을 뜻한다. 퀵 정렬, 병합 정렬, 힙 정렬, 분할 정복 방식으로 설계된 알고리즘이 해당된다.

    - O(n^2) : 다항형 빅-오라고 한다. 데이터 수의 제곱에 해당하는 연산횟수를 요구하는 알고리즘을 뜻한다. 이중으로 중첩된 반복문, 삽입 정렬, 거품 정렬, 선택 정렬 내에서 발생한다.

    - O(2^n) : 지수형 빅-오라고 한다. 연산 횟수가 지수적으로 매우 빠르게 증가하는 알고리즘을 뜻한다. 피보나치 수열이 이와 같으며 성능적인 면을 따져보면 현실적으로 사용하기에 무리가 있다.


  • Big-O 표기의 성능(수행시간, 연산횟수) 대소 비교
    : O(1)-상수함수 < O(logn)-로그함수 <O(n)-선형함수 < O(n^2)-다항 함수 < O(2^n)-지수 함수)
카테고리
작성일
2022. 8. 23. 02:10
작성자
risehyun

C#과 유니티로 만드는 MMORPG 게임 개발 시리즈 강의를 수강하며 널러블 파트에서 배우고 느낀 점을 정리해보았다.


  • 왜 널러블이 필요한가
    - 아래의 예제를 통해 Null 값을 사용할 수 있는 것은 class 처럼 참조타입으로 동작하는 경우에 한정되어 있음을 알 수 있다.
   class Program
    {
     < null을 사용할 수 있는 경우 >
        class Monster
        { 
        
        }

        static Monster FindMonster(int id)
        {
            // for () // 몬스터를 범위 내에서 찾고
            // return monster; // 있다면 리턴
            return null; // 없다면 null을 리턴
        
        }

        static void Main(string[] args)
        {
            // Nullable -> Null + able

            Monster monster = FindMonster(101); // 반환값을 확인하여

            if (monster != null) // 원하는 몬스터가 있는지를 판단할 수 있음
                                 // 여기서 null은 해당 값이 있는지의 유무를 판단할 수 있게 해줌
                                 // 이때 null을 사용할 수 있는 이유는 monster가 클래스 타입,
                                 // 즉 참조 타입으로 동작하기 때문이다.                                
            { 
            
            }

        }

    }
    class Program
    {
    < null을 사용할 수 없는 경우 >
        static int Find()
        {

            return 0; // int는 null을 허용하지 않는 값 형식이므로 대신 0을 사용하게 되는데,
                      // 0만으로는 찾았는지 여부를 확실히 구분하기 어렵다.

        }

    }


    - 참조 타입으로 동작하지 않아 null을 사용할 수 없는 타입에서도 null을 사용할 수 있게끔 해주는 것이 바로 널러블이다.

 

  • 널러블 사용 예제
    class Program
    {

        static int Find()
        {

            return 0; // int는 null을 허용하지 않는 값 형식이므로 대신 0을 사용하게 되는데,
                      // 0만으로는 찾았는지 여부를 확실히 구분하기 어렵다.

        }


        static void Main(string[] args)
        {

            {
                int? number = null;
                // 형식 뒤에 ? 기호를 사용하면 해당 변수는 null도 될 수 있다는 뜻을 가지게 된다.

                number = 3; // null 이 아닌 일반 값도 문제없이 할당된다.

                //     int a = number; // 하지만 값을 꺼내올 때 nullable형식과
                // int형식은 다르므로 오류가 발생한다.

                int a = number.Value; // .Value를 사용하면 정상적으로 값을 꺼내올 수 있다.
                Console.WriteLine(a);
            }

        }

    }

널러블 변수에 할당된 3이 정상적으로 출력됨을 알 수 있다.

 

    - 단, 아래의 코드처럼 널러블로 선언된 변수에 새로운 값을 할당하지 않고 null로 둘 경우 오류가 발생한다.

        static void Main(string[] args)
        {

            {
                int? number = null;
                int a = number.Value; 
                Console.WriteLine(a);
            }

        }

널러블 오브젝트는 반드시 value를 가져야 한다.

 

    - 이러한 오류를 방지하기 위해 클래스 형식(참조 형식)에 접근하는 것과 마찬가지로 다음과 같이 null인지 아닌지를 체크해주는 코드를 작성해주거나, HasValue를 이용해 값을 확인해주어야 한다.

       static void Main(string[] args)
        {

            {
                int? number = null;


		// 1.
                if (number != null)
                { 
                    
                }

                // 2.
                if (number.HasValue)
                { 
                    
                }

                // 두 버전중 어떤 것을 사용하더라도 실행 결과에는 차이가 없다.

                int a = number.Value; 
                Console.WriteLine(a);
            }

        }

   - 상술한 코드처럼 직접 값을 체크하는 방법도 있지만, 이를 더욱 간소화한 방법이 존재한다. 바로 ?? 기호를 사용하는 것이다.

 

1. 변수의 값이 null이 아닌 경우 

       static void Main(string[] args)
        {

                int? number = 5;

                int b = number ?? 0; // number란 변수가 만약 null이 아니라면
                                     // number.Value 안에 들어있는 값을 뽑아와서 넣어주고,
                                     // 반대로 null 이라면 ?? 뒤에 있는 0이란 값을 넣어준다는 의미이다.

                Console.WriteLine(b); // null이 아니기 때문에 5가 출력된다.

        }

 

2. 변수의 값이 null인 경우

        static void Main(string[] args)
        {

                int? number = null;

                int b = number ?? 0; // number란 변수가 만약 null이 아니라면
                                     // number.Value 안에 들어있는 값을 뽑아와서 넣어주고,
                                     // 반대로 null 이라면 ?? 뒤에 있는 0이란 값을 넣어준다는 의미이다.

                Console.WriteLine(b); // null이기 때문에 0이 출력된다.

        }

 

    - 널러블은 자주 쓰이는 문법은 아니지만 DB 작업 등에 종종 사용된다.

    - 널러블은 3항 연산자와 비슷하게 작동된다. 

+) if문을 이용해 null 여부를 판단하는 경우를 약식으로 표현할 수 있는 방법이 있다.

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

        static void Main(string[] args)
        {
            Monster monster = null;

            if (monster != null)
            {
                int monsterId = monster.Id;
            }

            int? id = monster?.Id; // monster라는 변수가 null인지 여부를 체크하여
                                   // null이 아닌 경우 Id값을 가져와서 할당하고,
                                   // null인 경우 그냥 null을 할당하게 된다.

        }

 

[결론]

- 널러블 타입은 형식에 ? 기호를 붙인 형태를 가지고 있다.

- 널러블로 선언된 값은 기존 타입과 다르게 null 타입도 함께 가질 수 있게 된다.

- null이 사용가능해진다는 것은 다른 참조 타입 데이터와 동일하게 사용전에 값의 유무를 먼저 체크해야 한다는 것을 의미한다.

- 이 과정을 간소화하기 위해  ?? 문법과 int? id = monster?.id 처럼 해당 타입의 뒤에 ?을 붙이는 방식을 활용할 수 있다.

카테고리
작성일
2022. 8. 23. 01:33
작성자
risehyun

C#과 유니티로 만드는 MMORPG 게임 개발 시리즈 강의를 수강하며 리플렉션 파트에서 배우고 느낀 점을 정리해보았다.

 


  • 리플렉션이란
    - 클래스가 가지고 있는 모든 정보들을 Runtime중에 뜯어보고 분석해볼 수 있는, 일종의 X-Ray라고 할 수 있다.

  • 사용 예제
using System.Reflection;

namespace CSharpStudy
{
    class Program
    {
        class Monster
        {
            public int hp;
            protected int attack;
            private float speed;

            void Attack() { }
        }

        static void Main(string[] args)
        {
            Monster monster = new Monster();

            Type type = monster.GetType(); 
            // C#의 모든 객체들은 Object에서 파생되기 때문에 GetType이란걸 들고있다.
            // 이걸 이용하면 객체의 타입을 빼낼 수 있다.
            // 이 type을 사용하면 클래스의 이름부터 내부 변수, 함수까지 
            // 모든 정보를 알 수 있다.

            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";
                Console.WriteLine($"{access} {field.FieldType.Name} {field.Name}");
            }

        }

    }

}

실행결과. 참고로 여기서 나오는 Single은 float와 같은 의미이다.

이 밖에도 프로퍼티나 필드, 생성자 정보를 얻는 등 클래스 내부의 다양한 정보에 대해서 알 수 있다.

 

  • 애트리뷰트란?
    - 클래스나 메소드의 메타데이터를 기록할 수 있는 기능. 프로그래머끼리 정보를 전달할 수 있지만 컴퓨터는 사용할 수 없는 일반 주석과 달리 컴퓨터가 runtime중에도 체크하고 참고할 수 있는 힌트를 남길 수 있다.

  • 애트리뷰트 사용 예제
    class Program
    {
        class Important : System.Attribute
        {
            string massage;

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

        class Monster
        {

            // hp입니다. 중요한 정보입니다. -> 이렇게 기록해도 컴퓨터는 인식하지 않음
            [Important("Very Important")] // 애트리뷰트에서 파생된 클래스를 선언함으로서 컴퓨터가 체크하게 됨
            public int hp;
            protected int attack;
            private float speed;

            void Attack() { }
        }

        static void Main(string[] args)
        {
            Monster monster = new Monster();

            Type type = monster.GetType();

            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";

                var attributes = field.GetCustomAttributes();

                Console.WriteLine($"{access} {field.FieldType.Name} {field.Name}");
            }

        }

    }

애트리뷰트가 적용된 hp 변수에 대해서 massage가 나타남을 알 수 있다.

- 리플렉션과 애트리뷰트는  툴을 만들 때 특히 유용하다.

- 예를 들어서 유니티에서 public 상태로 변수를 입력하면 바로 인스펙터창에 해당 변수가 나타나는데, 이를 리플렉션 기능 없이 구현하려면 상당히 어려워질 것이다.

언리얼의 경우 C++에 리플렉션 기능이 없기 때문에 파일을 읽어와서 필드로 추정되는 값을 어딘가에 기억하도록 다른 파일을 이용해 간접적으로 관리하도록 만들게 되는데,

C#은 자체적으로 리플렉션을 지원하므로 필드의 값을 바로 체크하여 툴의 UI를 열어주게끔 구현할 수 있다.

 

엑셀을 참고해 무언가를 동적으로 구현할 때도 유용하게 쓸 수 있다.

 

- 유니티에서 private 변수의 값을 인스펙터에 나타내주는 SerializeField 키워드도 애트리뷰트인 일종인 주석 문법이다.

'C#' 카테고리의 다른 글

220823 Nullable (널러블)  (0) 2022.08.23
220822 Exception (예외 처리)  (0) 2022.08.22
220822 Event (이벤트)  (0) 2022.08.22
[C#] Delegate (대리자)  (0) 2022.08.21
220820 Property (프로퍼티) + 식 본문 멤버 (Expression-bodied members)  (0) 2022.08.20
카테고리
작성일
2022. 8. 22. 23:14
작성자
risehyun

C#과 유니티로 만드는 MMORPG 게임 개발 시리즈 강의를 수강하며 예외처리 파트에서 배우고 느낀 점을 정리해보았다.


  • 예외처리란?
    - 예상치 못한 예외적인 상황에 대한 처리를 뜻한다.
    - 게임 쪽에서는 잘 안쓰는 경향이 있다.
      -> 왜? 게임에서는 예외처리를 따로 하기보다는 크래쉬가 나도록 두고, 빠르게 수정하는 쪽으로 처리함. (한 사람이 예외가 발생한다는 건 다른 사람들도 모두 똑같이 예외가 발생한다는 것이므로 한 사람만 따로 예외처리하는 것이 의미가 없다.)
      -> 단, 심각한 예외가 아니거나 네트워크 관련 예외의 경우에는 catch해서 처리할 수 있다.

  • 예외처리 문법 (try ~ catch, finally)

        static void Main(string[] args)
        {
            try // 1. 먼저 try구문 안의 코드를 한 번 실행해보고
            {

                // 여기서 예외적인 상황이란?
                // : 프로그램이 정상적으로 실행되지 않는 상황으로 아래와 같은 예시가 있음
                // 1. 값을 0으로 나눌 때. 0으로 나누면 아무것도 나눠지지 않으므로 오류발생
                // 2. 잘못된 메모리를 참조하는 경우
                // (null || 캐스팅 오류로 잘못된 타입을 가지는 경우)
                // 3. 복사시 너무 많은 데이터를 복사하여 오버플로우가 발생한 경우
                // 4. 스택이나 힙 메모리를 잘못 건드렸을 때

                // 1번 예외를 활용한 예제
                int a = 10;
                int b = 0;
                int result = a / b; // 0으로 값을 나눴기 때문에 예외가 발생함
            }

            catch (DivideByZeroException e) // 세부예외처리 -> 0으로 나누는 경우.
            {

            }

            catch (Exception e) // 2. 실행도중 예외적인 결과가 발생한다면
                                // 해당 예외에 대한 처리를 여기서 실행함
                                // Exception의 경우 모든 예외를 커버할 수 있고,
                                // 그 밖에 세부적으로 예외를 처리할 수도 있다.
                                // 단, Exception이 전체 예외를 커버하기 때문에
                                // 세부 예외처리보다 Exception이
                                // 앞에 오면 오류가 발생하여 실행이 되지 않는다.
                                // 그러므로 이 부분이 DivideByZeroException의 뒤에 와야 한다.
            {

            }

            finally  // try 문에서 예외가 발생하면 바로 catch문으로 빠지기 때문에
                     // try 스코프 안에서  예외가 발생한 줄 아래에 있는 코드들은 실행되지 않는다.
                     // 따라서 try catch문과 상관없이 반드시 실행되어야 하는 경우에는
                     // 여기에 해당 코드를 입력해서 실행되도록 따로 빼주어야 한다.
                     // 이 키워드의 활용으로는 파일 입출력이나 데이터베이스 연결같은 작업을 할 때,
                     // try 스코프에서 접속 코드를, finally 스코프 안에서는
                     // 해당 DB, 파일 내용을 정리하는 부분을 넣는 방법이 있다.
            {

            }
        }

 

  • 직접 에러 Exception을 만드는 방법
    class Program
    {
        class TestException : Exception // 커스팀 예외처리 클래스를 만들기 위해서는
                                        // 반드시 Exception을 상속받아야 한다.
        { 
            
        }

        static void Main(string[] args)
        {
            try 
            {
                throw new TestException(); // 새로 만든 예외를 던지면 catch가 받아줌
                
            }

            catch (Exception e) 
            {

            }

        }

    }

 

[결론]

try~catch 구문을 사용하여 예외 상황에 대한 처리를 간편하게 할 수 있다.

다양한 상황에서 발생하는 예외에 대해서 대처할 수 있으며,

네트워크 통신 오류 등의 덜 치명적인 상황에 대해서도 catch해서 별도로 처리할 수 있다.

카테고리
작성일
2022. 8. 22. 22:11
작성자
risehyun

C#과 유니티로 만드는 MMORPG 게임 개발 시리즈 강의를 수강하며 이벤트 파트에서 배우고 느낀 점을 정리해보았다.

 


  • 이벤트를 사용하는 이유?
    - 델리게이트가 의도하지 않은 곳에서 멋대로 호출될 수 있다는 단점을 가지고 있으므로, 이를 통한 문제 발생을 막기 위해 델리게이트를 맵핑하기 위해서이다.
  • 예제
   // 구독자를 모집한 다음 특정 이벤트가 발생했을 때 구독자에게
    // 메시지를 뿌리는 방식을 옵저버 패턴이라고 한다.

    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)
            {
                // 모두에게 알려준다! -> 이 부분을 어떻게 만들 것인가?
                // 여기에 강제로 코드를 넣는 것은 
                // 다른 코드와 inputManager와의 의존성을
                // 증가시키기 때문에 비효율적인 방식.
                // -> 이를 해결하기 위해 사용하는 것이 콜백 방식.
                // -> 콜백을 구현하기 위해 사용하는 문법이 델리게이트


                            // A라는 키를 누르면
                InputKey(); // 함수가 실행되며 모두에게 알려준다!
                            // 이 이벤트에 관심이 있다면 구독신청을 하면 된다.


            }
        }

    }
   class Program 
    {
        static void OnInputTest()
        { 
            Console.WriteLine("Input Received!"); 
        }

        static void Main(string[] args)
        {
            InputManager inputManager = new InputManager();
            inputManager.InputKey += OnInputTest; // 해당 함수를 구독함
//            inputManager.InputKey -= OnInputTest; // 구독을 취소함.  

            while (true)
            {
                inputManager.Update(); // a키가 눌리면 정상적으로 함수가 호출됨
                                       // 즉 "Input Received!"가 출력됨
            }

          // 이벤트와 델리게이트의 차이점
          //  inputManager.InputKey(); // 이벤트는 델리게이트와 달리 멋대로 직접 호출할 수 없음.
                                     // 구독 신청 or 취소만 가능하며 실질적으로 호출하는 것은 불가능함.
                                     // 내부적으로는 큰 차이가 없으며 이벤트 또한 콜백 방식의 일종임. 
        }

    }

 

  • 이벤트와 델리게이트의 차이점과 공통점
    - 차이점 : 이벤트는 델리게이트와 달리 멋대로 직접 호출할 수 없다. 오직 구독 신청 or 취소만 가능하며 실질적으로 호출하는 것은 불가능하다.
    - 공통점 : 이벤트와 델리게이트는 내부적으로 큰 차이가 없으며 이벤트 또한 델리게이트 처럼 콜백 방식의 일종이다.