오늘은 두 벡터사이의 거리를 측정 할수있는 Atan2(y,x)메서드 Unity에서 다루는 회전에 대해 TIL을 작성했습니다.

 

https://01149.tistory.com/90

 

게임수학 :: 아크탄젠트( Atan2(y,x) 함수에 대해 ) , 두 객체 사이의 각도 알기

삼각함수에 아크(arc)는 삼각함수의 역함수를 나타내는 것이다.삼각함수는 각도 -> 변의 비율 을 알수 있다면역삼각함수는  변의 비율 -> 각도 를 알수 있다. 그럼 역삼각함수중 왜 아크탄젠트를

01149.tistory.com

https://01149.tistory.com/91

 

Unity :: 오일러각 vs 쿼터니언 , 쿼터니언과 벡터 곱셈

게임 개발을 하면 오브젝트를 회전시켜야 될 일이 정말 많다.회전을 시키면 두가지 개념을 보게 되는데오일러 각과 쿼터니언이다. 오일러 각(Euler Angle)-3차원 공간의 절대적 좌표를 기준으로 물

01149.tistory.com

 

오늘 정리한 내용도 많고 따로 찾아보기 위해 글 하나에 정리하는것보다

따로 분리하는게 나을것 같아 이렇게 작성했습니다. 

 


https://01149.tistory.com/84

 

Unity :: 충돌 처리,OnCollison~() , OnTrigger~()의 인자값 차이

Unity로 작업을 할때 충돌처리를 하기 위해서 많이 쓰던 메서드이다. 하지만, 수업을 듣는 도중 Unity 패키지의 자동완성으로 나오는 충돌처리 메서드의 인자값의 타입이 다르다는 말을 듣고나서

01149.tistory.com

https://01149.tistory.com/85

 

Unity :: Unity 입력처리, InputManager vs InputSystem

Unity 작업시 기존부터 사용되어왔던 입력처리 패키지로 비교적 쉽게 사용가능하다는 장점이 있다.*Inspect를 통한 간단한 키매핑 public KeyCode Up; public KeyCode Down; void Update() { movement = 0f; if(Input.GetKey(

01149.tistory.com

https://01149.tistory.com/86

 

설계 :: OPP(객체 지향 프로그래밍)에서 SRP(단일 책임 원칙) 설계하기

SRP : Single Responsibility Principle(단일 책임 원칙)단일 책임 원칙(SRP)는 는 원칙을 말한다. 여기서 '책임'이라는 의미는 하나의 '기능 담당'으로 보면된다.하나의 클래스는 하나의 기능을 담당하여 하

01149.tistory.com

https://01149.tistory.com/87

 

Unity2D :: 타일맵을 이용한 오브젝트 꾸미기

https://0x72.itch.io/dungeontileset-ii 16x16 DungeonTileset II by 0x72A free tileset + animated characters + weapons + others.0x72.itch.io타일맵 공부를 위해 해당 에셋으 사용하였습니다. Q.게임 월드에 타일맵을 어떡해 생성

01149.tistory.com

 

 

Q.delegate(델리게이트)가 뭐야?

A. 메서드(함수)에 대한 참조(주소값,포인터)를 캡슐화 하는 형식(타입)이다.

즉, 메서드를 가리키는 포인터(C,C++의 포인터 아님) , C++의 함수포인터라고 생각하면 쉽다.

Type-Safe(타입-안전)이며 , 객체 지향적으로 설계되있는것이 특징이다.

 

Q.delegate(델리게이트)를 왜 쓰는건데?

A.

1.유연성 : 메서드(함수)를 변수처럼 다룰 수 있다. 런타임 중 메서드를 선택 또는 변경 할수 있음
2.콜백 함수 : 비동기 작업이나 이벤트 처리 시 , 특정 시점에 실행할 메서드를 델리게이트로 전달 가능
3.함수형 프로그래밍 지원 : 람다 표현식과 함께 사용시 간결하고 효율적인 코드 작성 가능

Q.delegate(델리게이트)를 어떡해 쓰는데?

A.

예시 1)

public class Program
{
	// 반환 타입이 void이고, 매개변수가 없는 델리게이트
	public delegate void SimpleDelegate();

	// 반환 타입이 int이고, 두 개의 int 매개변수를 받는 델리게이트
	public delegate int OperationDelegate(int a, int b);

    // 델리게이트 메서드
    public static void SayHello()
    {
        Console.WriteLine("Hello!");
    }

    public static int Add(int x, int y)
    {
        return x + y;
    }

    public static void Main()
    {
        // SimpleDelegate 델리게이트 인스턴스 생성 및 메서드 할당
        SimpleDelegate simpleDel = new SimpleDelegate(SayHello);
        simpleDel(); // 출력: Hello!

        // OperationDelegate 델리게이트 인스턴스 생성 및 메서드 할당
        OperationDelegate opDel = new OperationDelegate(Add);
        int result = opDel(5, 3);
        Console.WriteLine(result); // 출력: 8
    }
}

 

예시2)

//델리게이트 안에 여러개의 메서드를 추가 할 수 있다.

public class Program
{
    public delegate void Notify(string message);

    public static void MethodA(string msg)
    {
        Console.WriteLine($"MethodA: {msg}");
    }

    public static void MethodB(string msg)
    {
        Console.WriteLine($"MethodB: {msg}");
    }

    public static void Main()
    {
        Notify notifyDel = MethodA;
        notifyDel += MethodB; // 델리게이트 체인에 MethodB 추가
        //+= 연산자로 메서드를 추가할수 있다.

        notifyDel("Hello Delegates!");
        // 출력:
        // MethodA: Hello Delegates!
        // MethodB: Hello Delegates!
    }
}

예시2) 의 사용은 += 연산자로 델리게이트 변수에 메서드를 추가하고 있으며,
주로 반환타입이 void인 델리게이트에서 사용된다. 
반환값이 있는 경우 체인에 연결된 마지막 메서드 반환값만 반환

 

Q.delegate(델리게이트)와 event(이벤트)와 관계가 있다는 무슨관계야?

A. 이벤트는 델리게이트를 기반으로 구현 된 함수포인터라고 생각하면된다.

하지만 event를 선언한 클래스 내부에서만 저장한 메서드들을 동작 시킬수 있다.

public class Publisher
{
    // 이벤트 선언
    public event Action OnChange;

    public void RaiseEvent()
    {
    	//이벤트 변수를 선언한 클래스 내부에서 Invoke()로 동작시키는 모습
        //변수 뒤에 ?는 해당 이벤트가 null 이면 동작을 막는 예외처리 키워드이다.
        OnChange?.Invoke();
    }
}

public class Subscriber
{
    public void Subscribe(Publisher publisher)
    {
    	//이벤트 변수에 메서드를 추가하고 있다.
        //이벤트는 외부에서도 추가/삭제가 가능하다.
        publisher.OnChange += Respond;
    }

    private void Respond()
    {
        Console.WriteLine("Event received and handled.");
    }
}

public class Program
{
    public static void Main()
    {
        Publisher publisher = new Publisher(); //이벤트 변수가 있는 클래스
        Subscriber subscriber = new Subscriber(); //이벤트 변수에 메서드를 추가하는 클래스

        subscriber.Subscribe(publisher);
        publisher.RaiseEvent(); // 출력: Event received and handled.
    }
}

subscriber 클래스를 보면 이벤트가 들어있는 변수(publisher)를 인자값으로 가져간 뒤 event에 메서드를 저장 한다.

그 이후 publisher에서 invoke()로 함수 동작시 subscriber의 함수가 동작하게 된다.

 

이렇게 외부 클래스의 함수를 특정 시점에 원할때 쓸 수있는것이 event 함수이다.

이 패턴은 옵저버 패턴과 매우 유사하다.

 

Q.C#에서 델리게이트 기반으로 된 함수포인터 변수는 event가 끝인거야?

A.물론 아니다.

보통 6개의 델리게이트를 많이 사용하게 될 것이다.

 

1. Delegate 기본 타입 : 모든 델리게이트의 기본 클래스.

2. Event 델리게이트 : 이벤트 처리에 사용되는 델리게이트.

3. Action 델리게이트 : 반환값이 없는 메서드를 참조.

Action action = () => Console.WriteLine("Hello");
action(); // 출력: Hello

Action<int> printNumber = (number) => Console.WriteLine(number);
printNumber(10); // 출력: 10


4. Func 델리게이트 : 반환값이 있는 메서드를 참조.

Func<int, int, int> add = (a, b) => a + b;
Console.WriteLine(add(3, 4)); // 출력: 7


5. Predicate 델리게이트 : bool 값을 반환하는 조건 검사를 위한 메서드 참조.

Predicate<int> isEven = (x) => x % 2 == 0;
Console.WriteLine(isEven(4)); // 출력: True
Console.WriteLine(isEven(3)); // 출력: False

이 델리게이트는 List 컨테이너의 Find 함수에 인자값으로 안내가 되어있다.


6. Comparison 델리게이트 : 두 객체를 비교하는 메서드 참조.

Comparison<int> comparison = (a, b) => a.CompareTo(b);
int[] numbers = { 3, 1, 4, 1, 5 };
Array.Sort(numbers, comparison);

주로 정렬에 사용된다.

 


오늘 TextRPG 팀프로젝트를 하고 발표를 하는 중 delegate를 사용한 팀이 있어서

생각해보니 정확한 개념을 모르는 것 같아 다시 한번 정리해 보았다.

Q.람다식? Predicate<T>?

A.람다식 일회용 메서드 또는 이름 없는 메서드라고 보면된다.

이름이 없기에 계속 호출해서 쓸수 없기에 event에 등록되있으면 계속 사용이 가능하거나

해당 로직에서 딱 한번만 간단하게 쓰일정도면 람다식을 쓰기도한다.

 

나는 주로 람다식을 event 함수 구현을 할때 주로 사용 했었다.

하지만 팀 프로젝트 중에 이렇게 간단히도 쓸 수 있구나 하는걸 알았다.

 

플레이어의 인벤토리 List<item> 에서 Find를 사용하는 함수이다.

 

빨간상자 쳐진 Predicate<T> 일반화된 델리게이트(delegate)로, 해당조건문을 동작하면 참,거짓 을 반환하는 함수 포인터이다.

 

Predicate<T> 자료형은 즉 함수 포인터이기에 함수를 만들고 해당 자료형에 함수를 초기화하고 사용 할 수있다.

하지만 이렇게 쓰는것보다 바로 람다식을 사용하여 간단하게 쓸수있다. 

 

 var matchedItem = PlayerInventory.Find(item => item.ItemNum == SelectTypeItemList[i].ItemNum);

즉 , 해당 함수는 ItemNum 을 비교하여 참,거짓 값을 반환하여 참이면 해당 데이터(Item)를 거짓이면 null 값을 반환한다.

(Find 함수의 반환값은 Item? 이기에 null값을 받을 수있다.)

해당 내용을 설명하기 전에 json 구조와 Class 구조를 미리 알고 가기 위해 사진을 먼저 올렸다.

 

Save와 Load시에 사용되는 .Json Data
Skill Class 구조

 

역직렬화에 되는 상속 클래스들


 

Q.Data를 사용하기 위해 역직렬화를 했는데 왜 Data에 Null 들어오는거야??

A.

직렬화는 접근제한자의 상관없이 작업을 할 수 있다.

하지만 역직렬화시에 필드(변수)가 private 및 protected으로 설정되어 있으면, 해당 필드에 접근할 수 없기 때문에 null 값이나 더미 데이터를 할당하게 됩니다.

이를 방지하기 위해 필드에 대해 JsonProperty 어트리뷰트를 사용하여 공개적으로 접근 가능하게 하거나, public 프로퍼티를 제공하여 역직렬화 시 데이터를 올바르게 설정할 수 있도록 해야 합니다.

 

역직렬화하는 Data들은 접근 제한자를 Public으로 선언해줘야 접근이 가능하다.

 

Skill의 Set 접근제한자도 public으로 선언하니 잘 적용되는 모습

 

 

Q.위에 SkillDeck을 역직렬화 하는데 자꾸 Data가 중복되서 나와요

index 0 ~ 2 dhk index 3 ~ 5는 같은 Data이다.

A.

이런 경우는 직렬화/역직렬화시에 .json Data를 잘못 불러오거나 쓸때 일어난다.

해당 데이터를 .json으로 직렬화 하였음

위에 .json 하고 구조가 좀 다른데 "$type" 이라는 문구가 추가 되어있다. 

$type이 작성되어 어떤 자료형인지 나타내고 있다.

그럼 저 "$type"은 어떡해 나온것인가??

var settings으로 선언하여 $type 필드를 기반으로 실제 객체타입으로 정확하게 역직렬화하는데 도움이 된다.

 

직렬화 메서드 호출시에 인자값으로 settings으로 호출하게되면 자료형의 실제 객체타입으로 정확하게 역직렬화하는데 도움이 된다.

여기서 도움상속되있는 자식클래스로 직렬화 하고 역직렬화로 부모 클래스로 하는경우데 도움이 된다.

 

역 직렬화시에도 해당 setting을 가져와서 사용 중이다.

하지만 여기서는 자식 클래스가 아닌 부모클래스에 해당 작업을 취하니 역직렬화시에 Data를 두번 만들게 되는것이다.

 

즉, 역직렬화의 저 setting값을 없애주면 정상적으로 동작이 잘된다.

역직렬화시에 seeting값을 없애주니 잘 적용 되었다.

 

결론:

직렬화는 접근 제한자에 구애받지 않지만, 역직렬화에서는 접근 제한자로 인해 데이터가 올바르게 설정되지 않을 수 있다.

Q. enum 으로 형변환이 가능해?

A. Enum.Parse()를 사용하면 변경이 가능하다. 

함수 오버로딩이 8가지 되어있다.

 

주로, 지금 표시되어있는 함수를 주로 사용하고있는데 

 internal class Program
 {
     internal enum EnemyType
     {
        None = 0,
        Skeleton = 1,
        Goblin,
        Orc,
        Crab,
        Turtle,
        End,
      }
 
     static void Main(string[] args)
     {
         string _str = "Skeleton";
         //Enum.Parse()의 반환값은 object 타입이기 때문에 언박싱을해줘야한다.
         //언박싱 : 사용하는 타입으로 형변환 하는것 
         EnemyType _enemyType =  (EnemyType)Enum.Parse(typeof(EnemyType), _str);
     }
 }

 

하지만 boxing/unboxing(박싱/언박싱)은 최적화에서는 지양해야 되는 기법중 하나이다.

 

Q.박싱과 언박싱을 왜 지양해야되는데?

A.

박싱과 언박싱은 내부적으로 상당한 오버헤드를 감수하고 사용

 

박싱

-박싱 순서-

1) 힙 영역에 새로운 메모리를 할당하고,

2) 스택의 값을 힙 메모리로 복사한 뒤

3) 힙 메모리의 주소 값을 갖는 새로운 스택 메모리를 할당하는 과정을 거친다.

박싱은 값 하나 옮기는데 메모리 참조를 많이 하는데, 이로 인해 시간적 오버헤드가 발생한다.

 

언박싱의 경우도 위와 비슷한 순서로 진행한다.

하지만 언박싱은 새로운 문제를 야기하는데, 바로 가비지를 생성한다는 것이다.
즉 언박싱은 그 자체로도 오버헤드가 있지만 가비지를 생성함으로 인해 GC를 동작시키는 잠재적 오버헤드까지 가진 셈이다.

 

Q.그럼 박식/언방식의 대안은 있어?

A. 제네릭을 사용하면 박싱 문제를 해결하여 사용 할 수있다.

 

p.s 박싱/언박싱 관련 정보는 해당 블로그를 참고하여 작성하였으며 , 해당 블로그의 글을 참고하는것이 좋다.

https://velog.io/@wjdgh9577/C-%EB%B0%95%EC%8B%B1Boxing%EA%B3%BC-%EC%96%B8%EB%B0%95%EC%8B%B1Unboxing%EC%9D%B4%EB%9E%80

 

[C#] 박싱(Boxing)과 언박싱(Unboxing)이란?

박싱(Boxing)과 언박싱(Unboxing)의 기본 개념과 사용법, 특징을 정리한다.

velog.io

 

Q. .csv파일을 어떡해 가지고 와?

A. 

해당 코드에 주석과 순서를 적었으니 참고하여 사용하면됨 

         //1.경로 설정
         string csvFilePath = @"..\..\..\Data\TextRPG_Quest.csv";

          // 2. 읽어올 데이터를 저장할 컨테이너 선언 및 초기화
          Dictionary<int, Quest> csvData = new Dictionary<int, Quest>();
          
          //3. CSV 파일을 UTF-8 인코딩으로 읽기
          using (var reader = new StreamReader(csvFilePath, Encoding.UTF8))
          {
          	  //첫줄은 보통 항목이기에 header라고 하여 읽어줌
              string headerLine = reader.ReadLine(); //카테고리
              string[] headers = headerLine.Split(','); //문자열 카테고리 분류
			  
              //그 다음 줄부터는 Data이기에 끝문자(\n)일때까지 읽기
              string line;
              while ((line = reader.ReadLine()) != null)
              {
                  string[] values = line.Split(',');
                  //TODO CODE : 배열로 들어간 데이터를 파싱해서 사용하는 코드 작성
              }

 

구현 (퀘스트 매니저)

더보기
  internal class QuestManager
  {
      StringBuilder _strbuilder = new StringBuilder(); //문자열 최적화를 위한 스트링빌더 선언
      Dictionary<int, Quest> _quests;
      public Dictionary<int, Quest> Quests { get { return _quests; } }

      Dictionary<int, Quest> _acceptedQuest; //플레이어가 수락한 퀘스트 
      public Dictionary<int, Quest> AcceptedQuest {  get { return _acceptedQuest; } }

      public QuestManager()
      {
          string csvFilePath = @"..\..\..\Data\TextRPG_Quest.csv";

          // 1. CSV 파일을 UTF-8 인코딩으로 읽기
          Dictionary<int, Quest> csvData = new Dictionary<int, Quest>();
          using (var reader = new StreamReader(csvFilePath, Encoding.UTF8))
          {
              string headerLine = reader.ReadLine(); //카테고리
              string[] headers = headerLine.Split(','); //문자열 카테고리 분류

              string line;
              while ((line = reader.ReadLine()) != null)
              {
                  string[] values = line.Split(',');

                  //0 퀘스트 ID
                  //1 퀘스트 이름
                  //2 퀘스트 내용
                  //3 퀘스트 목적
                  //4 퀘스트 현재 진행도 (최초 0)
                  //5 퀘스트 목표 진행도
                  //6 퀘스트 클리어 유무
                  //7 퀘스트 보상 이름
                  //8 퀘스트 보상 갯수
                  //9 퀘스트 보상 골드
                  //10 퀘스트 타입

                  _strbuilder.Clear();
                  
                  Quest quest = new Quest();
                  quest.Label = values[1];
                  _strbuilder.Append(ApplyEscapeCharacters(values[2]));
                  quest.Detail = _strbuilder.ToString();
                  quest.Purpose = values[3];
                  quest.CurProgressRequired = int.Parse(values[4]);
                  quest.EndProgressRequired = int.Parse(values[5]);
                  quest.IsFinish = bool.Parse(values[6]);
                  quest.RewardType = values[7];
                  quest.RewardValue = values[8];
                  quest.RewardGold = int.Parse(values[9]);
                  quest.Type = values[10];

                  csvData.Add(int.Parse(values[0]), quest);
              }

              if (csvData != null)
              {
                  _quests = csvData;
              }

              //플레이어가 수락한 퀘스트 리스트 생성 
              if(_acceptedQuest == null)
              {
                  _acceptedQuest = new Dictionary<int, Quest>();
              }
          }

      }

      private string ApplyEscapeCharacters(string input)
      {
          // 예시로, 줄바꿈 문자를 이스케이프 화시킴
          input = input.Replace("\\n", "\n");
          input = input.Replace("\\t", "\t");

          // 다른 이스케이프 문자도 필요에 따라 처리할 수 있다.
          return input;
      }



Q. C#에서 객체가 사라지는 타이밍?

A. 객체가 사용(호출)이 되지 않을때 GC(가비지컬렉터)에서 제거를 함

C/C++과는 다르게 C#은 객체를 만들면 프로그래머가 따로 건드리지 않아도 사용되지 않으면 자동으로 제거된다고한다.

 

Q. 직렬화 와 역직렬화의 차이점?

A.

직렬화 : 외부에서 들어온 데이터를 코드영역 안에서 사용할 수 있게 변환하는것

역직렬화 : 코드영역에 있는 데이터를 외부로 꺼낼수 있게 변환하는것

 

Q. .csv 와 .json의 공통점,차이점 ?

A.

공통점 : 외부에서 관리되는 데이터 파일형들

차이점 :

.csv는 ( , ) 로 문자열을 구분하여 데이터를 관리함 , csv는 엑셀 파일로 셀 한칸마다 데이터 칸이기에 

.json은 배열,colletion 에 있는 구조들을 구체화 하여 관리에 능동적임

 

C# System.Text 에 속한 Json 패키지

using System.Text.Json;
using System.Text.Json.Serialization;
 
 // 1. 객체를 생성하고 JSON으로 직렬화하여 파일에 저장
 List<Quest> quests = new List<Quest>
 {
     new Quest("퀘스트1"),
     new Quest("퀘스트2"),
     new Quest("퀘스트3")
 };

 // 2. 리스트를 JSON 문자열로 직렬화
 string jsonString = JsonSerializer.Serialize(quests, new JsonSerializerOptions { WriteIndented =true});

 //3. 파일 경로 설정
 string filePath = "Quest.json";

 //4. JSON 문자열을 파일로 저장
 File.WriteAllText(filePath, jsonString);
 Console.WriteLine($"Serialized JSON list saved to {filePath}");

 // 5. JSON 파일을 읽어와서 리스트로 역직렬화
 if (File.Exists(filePath))
 {
     string loadedJsonString = File.ReadAllText(filePath);
     List<Quest> loadedQuest = JsonSerializer.Deserialize<List<Quest>>(loadedJsonString);

     //6. 역직렬화된 리스트 출력
     foreach (Quest quest in loadedQuest)
     {
         Console.WriteLine($"Loaded Quest: Label = {quest.Label}, Detail = {quest.Detail}, Finish = {quest.IsFinish}");
     }

 }

 

+ Recent posts