SRP : Single Responsibility Principle(단일 책임 원칙)

단일 책임 원칙(SRP)는 <객체는 단 하나의 책임만 가져야 한다> 는 원칙을 말한다.

 

여기서 '책임'이라는 의미는 하나의 '기능 담당'으로 보면된다.

하나의 클래스는 하나의 기능을 담당하여 하나의 책임을 수행하는데 집중되어야 한다

 

 

현재 TopDownGame 프로젝트 수업을 듣는 중에 캐릭터의 이동을 담당하는 설계를 하는 중에 로직 순서를 간단하게라도 정리 해보았다.

1번은 생성자에서 함수를 event 함수에 추가 후, 동작하지 않는다.

2번은 PlayerInputController에서 InputValue(입력값을 받음)를 받아서 Move event함수를 호출하여 부모클래스로 접근한다.

3번은 부모에있는 TopDownController의 event Action에 등록된 함수를 Invoke()로 동작시키면

TopDownMoveMent에 있는 함수를 동작시키게 한다.

 

그 이후로, 입력값을 받을때마다 2 ~ 3번을 반복하게 된다.

 

이 로직은 상위 부모 클래스로 Controller로 선언 후 각 기능(이동,공격등등)마다 Class로 설계하여 SRP 원칙을 지켰고

event 함수를 사용하여 확장성을 챙겼다고 생각한다.

 

이렇게 구성하게 되면 

'기능 변경(수정)이 일어 났을때의 파급 효과가 최대한 적게 일어난다.

즉, 커플링의 영향이 적어진다고 볼 수 있는것이다. 

 

해당 구조를 UML로 표현하면 이렇게 된다.

 

기능을 분리하였고 TopDownController를 참조하여서 그 안에 사용할 메서드를 Event로 저장합니다.

그럼 PlayerInputController는 자신의 부모클래스에 저장된 event들을 호출하여 사용이 가능해집니다.

 

Unity에서 컴포넌트로 적용한 모습

PlayerInputController 부모 클래스에 해당 기능들을 저장시키고 inputController에서 호출한다.

 

SRP(단일 책임 원칙)에 대해서는 해당 블로그 글을 참고 하였습니다.

 

https://inpa.tistory.com/entry/OOP-%F0%9F%92%A0-%EC%95%84%EC%A3%BC-%EC%89%BD%EA%B2%8C-%EC%9D%B4%ED%95%B4%ED%95%98%EB%8A%94-SRP-%EB%8B%A8%EC%9D%BC-%EC%B1%85%EC%9E%84-%EC%9B%90%EC%B9%99

 

💠 완벽하게 이해하는 SRP (단일 책임 원칙)

단일 책임 원칙 - SRP (Single Responsibility Principle) 단일 책임 원칙(SRP)는 객체는 단 하나의 책임만 가져야 한다는 원칙을 말한다. 여기서 '책임' 이라는 의미는 하나의 '기능 담당'으로 보면 된다. 즉,

inpa.tistory.com

 

<InputManager>

Unity 작업시 기존부터 사용되어왔던 입력처리 패키지로 비교적 쉽게 사용가능하다는 장점이 있다.

Unity-ProjectSetting에 접근시 기본적으로 설치되어 있는 패키지 중 하나이다.

*Inspect를 통한 간단한 키매핑
    public KeyCode Up;
    public KeyCode Down;
    void Update()
    {
        movement = 0f;
        if(Input.GetKey(Up)) { movement += 1f; }
        if(Input.GetKey(Down)) { movement -= 1f; }
        rigidbody.velocity = new Vector2(0, movement * speed);    
    }

InputManager를 이용하여 키 맵핑과 오브젝트 조작까지 동작하게하는 코드이다.

 

위에 코드 적용시 Inspector에서 이런식으로 키맵핑이 가능해진다.

 

<InputManager 를 지양하게 된이유>

1.다양한 입력 장비 대응 : 현 게임시장은 다양한 플랫폼이 제공되고 있고 , 키맵핑도 매우 자유롭다.
하지만 InputManager는 해당 문제를 대응하기 부적절하다.
2.입력 처리와 실제 로직 실행주체의 분리의 목적
InputManager의 자체 문제는 아니지만, 기능 별로 class를 나누는 작업을 해야 유지 보수 및 확장성이 많이 상승한다.

-> OPP(객체지향프로그래밍) 에서 이런 설계 원칙을 단일책임원칙(SRP)라고 부름

 

위에 이렇게 단점이 있지만 실제로도 간단한 프로젝트를 만들때는 아직도 많이 사용된다.

 

<InputSystem>

위에 말한 InputManager의 단점을 극복하기 위해 새로 나온 패키지이다.

해당 패키지는 Unity 패키지에 포함되어있지 않기에 ProjectManager를 통해서 설치해야한다.

window - packageManager-inputsystem 설치 ( 현재 설치되있는상태라 Remove로 뜸)

 

InputSystem의 추가로 Unity에서 사용하는 모습

Input Action : 해당 동작에 대한 키를 맵핑 데이터를 모아두는 파일

Player Input 컴포넌트 : 자동으로 입력 행동을 처리하고 해당 게임 오브젝트에 메시지를 보내는 역할

 

Player Input의 Behaviour은 무엇인가?

해당 Input 입력 발생시 해당 게임 오브젝트에 메시지 보내는 여러 방식중 하나 라고 생각하면 된다.

https://docs.unity3d.com/Packages/com.unity.inputsystem@1.3/manual/Components.html#notification-behaviors

 

GameObject components for input | Input System | 1.3.0

GameObject components for input The Input System provides two MonoBehaviour components that simplify how you set up and work with input: Component Description PlayerInput Represents a single player, and that player's associated Input Actions. PlayerInputMa

docs.unity3d.com

해당 글음 참고하여 작성하였음

이름 특징
SendMessages  PlayerInput을 컴포넌트로 가지고있는 GameObject에 코드내용을 전달
MonoBehaviour 의 update,start와 같은 이벤트함수처럼 사용이 가능해짐
BroadCastMessages PlayerInput을 컴포넌트로 가지고있는 GameObject와 그 자식들에게 코드내용을 전달
성능적인 면에서 하자가 있기에 잘 사용되진 않음
InvokeUnityEvents UI의 button 컴포넌트처럼 Inspector 에서  Onclick() 이벤트 함수처럼 메서드를 추가해서 사용하는것 
Invoke CSharp Events 위의 기능과 비슷하지만 PlayerInput API에서 코드를 호출하여 사용이 가능하고 , inspector 표시되지 않고 오직 스크립트 코드로 동작한다.

onActionTriggered (collective event for all actions on the player)
onDeviceLost
onDeviceRegained 

해당 메서드들이 API에 등록되있다고 함

 

<InputSystem 사용>

Input Action Asset 에서 맵 추가하는 방법

 

 

그 외에도 기능이 많다. 예를들어 키를 홀드하고있는지 키를 몇번 더블 클릭하는지 설정이 가능하지만 기본적인것만 적어보았다.

Action Type 이름 : 입력을 어떻게 받을 것인지 설명
Value 일반적인 상태에 사용함 , 눌렀을 때, 누르고 있을때, 뗄 떼 등 다양한 상황에 대응이 가능해짐
Button 눌렀을때 발생하는 액션에 활용 되며 , Control 타입이 Button으로 고정 됨
Pass-Through 명확화(Disambiguation)을 거치지 않은 Value.
Value 쓰시면 됩니다.

입력이 들어오면 그 즉시 처리 되기에 중복 입력에 대한 필터링이 없음(액션 상태에 따른 시작,중지)가 이루어지지 않음

연속적인 입력 처리가 필요할때 주로 사용,동일한 입력에서 여러 프레임에 걸쳐 이벤트를 받을 수 있음

언제 사용하면 됨??
축(axis)기반 입력이나 마우스의 움직임 , 복잡한 키 조합
: 축적된 입력을 매 프레임마다 처리해야 하는 경우
ex)조이스틱 축, 마우스 이동 -> 연속적인 입력으로 매 프레임마다 값을 처리해야함
ex2) 멀티 터치 입력 -> 여러 입력을 동시에 수집하고 처리해야됨, 중복입력 필터링이 없기에 모든 입력을 받음

 

<InputSystem 예제>

using UnityEngine.InputSystem;

public void OnMove(InputValue value)
  {
      Debug.Log("OnMove" + value.ToString());
      Vector2 moveInput = value.Get<Vector2>().normalized;
      CallMoveEvent(moveInput);
      //실제 움직이는 처리는 여기서 작업하는게 아님 
      //PlayerMovement에서 작업을 하는것
  }

  public void OnLook(InputValue value)
  {
      //Debug.Log("OnLook" + value.ToString());
      Vector2 newAim = value.Get<Vector2>();
      //마우스 위치를 정규화 해버리면 제대로된 좌표가 반환되지 않음
      Vector2 worldPos = _camera.ScreenToWorldPoint(newAim);//화면 좌표계에 있기에 월드 좌표로 변환
      //벡터의 뺼셈으로 원하는 방향벡터를 계산
      newAim = (worldPos - (Vector2)transform.position).normalized;

      if (newAim.magnitude >= .9f)
      {
          CallLookeEvent(newAim);
      }
  }

 

수업 듣는 InputController의 구조를 살짝 떼왔다. Event Action으로 각 기능의 함수를 동작시키는 형태인데,

InputValue 타입의 변수 인자값을 받아 해당 동작에서 사용하는 간단한 예시이다.

 

하지만 게임 프로젝트 규모가 커질수록 Input Map들도 많아지게되며, map에 직접 접근해서 사용해야되는 경우가 존재한다.

 

ChatGPT - 참고 코드 : 사용시 꼭 읽어보고 사용할것 

using UnityEngine;
using UnityEngine.InputSystem;

public class ActionMapSwitcher : MonoBehaviour
{
    // InputActionAsset은 Unity의 Input Actions Asset 파일을 연결할 때 사용됩니다.
    public InputActionAsset inputActions;

    // Action Maps
    private InputActionMap playerActionMap;
    private InputActionMap uiActionMap;

    private void Awake()
    {
        // InputActionAsset에서 각 Action Map을 가져옴
        playerActionMap = inputActions.FindActionMap("Player");
        uiActionMap = inputActions.FindActionMap("UI");

        // Player Action Map에서 'Move' 액션을 가져와 이벤트 등록
        InputAction moveAction = playerActionMap.FindAction("Move");
        moveAction.started += OnMoveStarted;
        moveAction.performed += OnMovePerformed;
        moveAction.canceled += OnMoveCanceled;

        // UI Action Map에서 'Navigate' 액션을 가져와 이벤트 등록
        InputAction navigateAction = uiActionMap.FindAction("Navigate");
        navigateAction.started += OnNavigateStarted;
        navigateAction.performed += OnNavigatePerformed;
        navigateAction.canceled += OnNavigateCanceled;
    }

    private void OnEnable()
    {
        // 기본적으로 Player Action Map 활성화
        playerActionMap.Enable();
    }

    private void OnDisable()
    {
        // 비활성화 시 모든 액션맵을 비활성화
        playerActionMap.Disable();
        uiActionMap.Disable();
    }

    // 'Move' 액션 관련 콜백 함수들
    private void OnMoveStarted(InputAction.CallbackContext context)
    {
        Debug.Log("Move started");
    }

    private void OnMovePerformed(InputAction.CallbackContext context)
    {
        Vector2 moveInput = context.ReadValue<Vector2>();
        Debug.Log($"Move performed: {moveInput}");
    }

    private void OnMoveCanceled(InputAction.CallbackContext context)
    {
        Debug.Log("Move canceled");
    }

    // 'Navigate' 액션 관련 콜백 함수들
    private void OnNavigateStarted(InputAction.CallbackContext context)
    {
        Debug.Log("Navigate started");
    }

    private void OnNavigatePerformed(InputAction.CallbackContext context)
    {
        Vector2 navigateInput = context.ReadValue<Vector2>();
        Debug.Log($"Navigate performed: {navigateInput}");
    }

    private void OnNavigateCanceled(InputAction.CallbackContext context)
    {
        Debug.Log("Navigate canceled");
    }

    // Action Map 전환 예시
    public void SwitchToUI()
    {
        // Player Action Map 비활성화
        playerActionMap.Disable();

        // UI Action Map 활성화
        uiActionMap.Enable();
        Debug.Log("Switched to UI Action Map");
    }

    public void SwitchToPlayer()
    {
        // UI Action Map 비활성화
        uiActionMap.Disable();

        // Player Action Map 활성화
        playerActionMap.Enable();
        Debug.Log("Switched to Player Action Map");
    }

    private void Update()
    {
        // 예시: Escape 키를 누르면 UI로 전환, 다시 누르면 Player로 전환
        if (Keyboard.current.escapeKey.wasPressedThisFrame)
        {
            if (playerActionMap.enabled)
            {
                SwitchToUI(); // UI로 전환
            }
            else
            {
                SwitchToPlayer(); // Player로 전환
            }
        }
    }
}

 

해당 코드는 게임내에서의 Player와 UI창의 키 맵핑을 다르게 하고 사용하는 모습이다.

Esc 키를 누르면 UI로 전환 한번 더 누르면 Player로 전환하는 방식이다. 

 


결론 : 

1.InputManager는 간단한 프로젝트 진행시 사용하기 좋다.

 

2.InputSystem은 규모가 커지고 다양한 플랫폼에 제공 및 키 맵핑 변경을 한다면 좋은 선택지이다.

 

3.InputSystem의 핵심 개념은 , InputAction , Input Action Asset , Player Input 컴포넌트 이 세가지이다.

 

InputAction : 프로젝트 창에서 생성 가능한 Input 맵핑 데이터가 있는 파일

Input Action Asset : 여러 개의 입력 행동을 그룹화하는 방법, InputAction을 열면 설정창에서 ActionMaps,Actions,AcitonProperites를 수정할 수 있다.

Player Input 컴포넌트 : 입력 행동을 받아 해당 게임오브젝트에 메시지로 전달해주는 역할

인자값이 타입이 다름

Unity로 작업을 할때 충돌처리를 하기 위해서 많이 쓰던 메서드이다.

 

하지만, 수업을 듣는 도중 Unity 패키지의 자동완성으로 나오는 충돌처리 메서드의 인자값의 타입이 다르다는 말을 듣고나서 찾아보니 진짜 달랐다.

그 둘의 차이점을 알고싶어 따로 알아보았다.

 

<OnCollison~(Collison )>

using UnityEngine;

    private void OnCollisionEnter(Collision collision)
    {
        
    }

OnCollison~()메서드는 두개의 물체가 충돌 할때 충돌 지점,충돌한 물체,물리적 힘 등의 정보를 처리해야된다.

Collsion 객체는 충돌관 관련된 위의 정보를 담고 있기에 해당 타입을 사용하는 것이다.

 

Collision Class 내부 모습 , Impulse,Velocity 등등 물리와 관련된 변수가 보인다.

<OnTrigger~(Collider )>

using UnityEngine;

 	private void OnTriggerEnter(Collider other)
 	{
     
 	}

OnTrigger()는 객체들이 충돌 했을때 물리적 충돌은 발생하지 않고 , Collider로 들어온 객체의 Data를 인자값으로 들고온다. 즉 쉽게 말하기 위해 충돌이라고 하는거지 해당 트리거 영역에 객체가 들어왔을때 동작하는 메서드인것이다. 

하지만 물리동작을 하지 않는다고 Rigidbody를 빼놓으면 안된다.

(움직이는 쪽이 물리 시뮬레이션을 하면서 충돌체크를 하기 때문)

 

Collider Class의 내부 모습, 물리 충돌 변수가 존재하지 않는다 그 대신에 isTrigger 변수가 눈에 보인다.

 

결론 : OnCollison() 메서드와 OnTrigger()메서드의 자동완성 인자값이 다르기에 실제로 사용할때 주의하여 사용하자

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



+ Recent posts