글로 설명하기가 힘들어 간단하게라도 구조를 그려보았다.

 

ItemStatusView 클래스에 EquipBtn 변수가 있고 해당 Button변수에 컨트롤러에 있는 데이터를 인자값으로 받아와 이벤트를 등록해놨는데 이때 인자값의 초기값은 null값으로 설정이 되있었다.

 

ItemSlot 클래스에 버튼을 눌러 Item 데이터를 컨트롤러로 보내 초기화를 하였기에 null값은 사라졌다고 생각했다

왜냐하면 Class는 참조값이기에 변경될것이기 때문이다),

 

그리고 실제로 콘솔창에서는 null값이 들어온다고 오류가 발생하였다.

 

원인은 item이 참조형 변수라고 해도, 이 참조가 유효하려면 메모리 상의 객체를 가리키고 있어야 하는데 이미 처음 초기값을 null로 설정 한 뒤였고, 해당 변수는 다른 클래스(ItemStatusView)로 넘어가 있는 상태였기에 아무리 Controller에서 item이 초기화가 되더라도 view에서 받는 이벤트의 인자값은 null이 들어오는 것이였다.

 

즉, 이 문제를 일으킨 이유는 

null 빈 데이터라고만 착각을 해버려서 발생한 문제였다. 

null은 빈데이터가 아닌 주소값이 없다라는 의미이기 떄문이다.

 

C++에서는 포인터 개념이 있기에 주소값 및 할당을 하게되면 무조건 해제를 해야만하지만 

C#을 계속 사용하다보니 주소값에 대해서 깜빡했던것 같다.

C#은 포인터 개념이 없기 때문에 자동으로 Heap영역에 알아서 할당이 되었기 때문에 간단히 생각을 했던것이다. 

 

 

대처법으로는 View의 버튼 변수를 프로퍼티로 선언한뒤 호출하여 Controller에서 이벤트를 등록하였다.

public class ItemStatusController : UIController
{
    public ItemSlot SelectItem;
    private ItemStatusModel itemStatusModel;
    private ItemStatusView itemStatusView;

    public override void Initialize(IUIBase view, UIModel model)
    {
        itemStatusModel = model as ItemStatusModel;
        itemStatusView = view as ItemStatusView;

        base.Initialize(itemStatusView, itemStatusModel);
        //아이템 장착 버튼 이벤트 함수 등록 (장착 , UI 출력)
        itemStatusView.EquipButton.onClick.AddListener(() => GameManager.Instance.player.EquipItem(SelectItem.item));
        itemStatusView.EquipButton.onClick.AddListener(() => OnShow());

        //아이템 장착해제 버튼 이벤트 함수 등록 (장착해제 , UI 출력)
        itemStatusView.DisEquipButton.onClick.AddListener(() => GameManager.Instance.player.DisEquipItem());
        itemStatusView.DisEquipButton.onClick.AddListener(() => OnShow());
    }
}

 

public class ItemStatusView : MonoBehaviour, IUIBase
{
    [SerializeField] private TextMeshProUGUI curUpgradeLevelText;
    [SerializeField] private TextMeshProUGUI maxUpgradeLeveText;
    [SerializeField] private TextMeshProUGUI UpgradeCostText;
    [SerializeField] private TextMeshProUGUI itemPassiveEffectText;
    [SerializeField] private TextMeshProUGUI itemEquipEffectText;
    [SerializeField] private Image ItemIcon;
    [SerializeField] private Button UpgradeBtn;
    [SerializeField] private Button EquipBtn;
    [SerializeField] private Button DisEquipBtn;


    public Button EquipButton { get => EquipBtn; }
    public Button DisEquipButton { get => DisEquipBtn; }
    public Button UpgradeButton { get => UpgradeBtn; }
}

 

이런식으로 사용하니, 우선 item은 null값이 들어가있는 상태에서 외부에서 데이터가 초기화 되더라도 

Controller 클래스 안에 멤버변수로 되어있으니 참조값을 제대로 활용 할 수 있게 되었다. 

면접 질문 모음

C# 문법

  1. 객체란 무엇인가요? 클래스와 어떤 연관이 있나요?
  2. 생성자에 대해 간단하게 설명해주세요.
  3. 접근제한자란 무엇이며, 각각 어떤 차이가 있는지 비교해서 설명해주세요.
  4. static 한정자에 대해 설명해주세요.
  5. SOLID 원칙에 대해 설명해주세요.
  6. 객체지향 프로그래밍의 속성 중 하나인 다형성과 이를 활용한 설계의 장점에 대해 설명해주세요.
  7. override와 overload에 대해 설명해주세요.
  8. 확장 메서드에 대해 설명하고 어떻게 활용했는지 알려주세요.
  9. 콜백이란 무엇인가요? 콜백을 사용해본 경험이 있을까요?
  10. 델리게이트(delegate; 대리자)란 무엇인가요?
  11. C#의 event란 무엇인가요?
  12. Unity에서 사용하는 델리게이트 혹은 이벤트에는 어떤 것이 있나요?
  13. 참조 형식과 값 형식에 대해 설명해주세요.
  14. 메모리에서 스택과 힙의 차이점에 대해 설명해주세요.
  15. 1번과 2번 질문의 답안을 기반으로 struct와 class의 차이점에 대해 설명해주세요.
  16. 얕은 복사와 깊은 복사의 차이점은 무엇인가요?
  17. 박싱과 언박싱이 일어나는 과정을 메모리 관점에서 설명해주세요.
  18. 클래스를 다른 클래스로 상속하기 위한 방법은 무엇인가요?
  19. 클래스 상속에서 다이아몬드 문제(diamond problem)가 발생하는 이유와 이를 해결하는 방법에 대해 설명해주세요.
  20. 인터페이스란 무엇인가요?
  21. 인터페이스와 추상클래스의 차이는 무엇인가요?
  22. 가비지 컬렉터란 무엇인가요?
  23. 가비지 컬렉터의 장점과 단점에 대해 설명해주세요.
  24. 가비지 컬렉터의 세대 개념에 대해 설명해주세요.
  25. 박싱, 언박싱을 사용할 때 주의해야 할 점은 무엇일까요?
  26. 오브젝트 풀을 사용하면 메모리 관리에 도움이 되는 이유가 무엇일까요?
  27. 제네릭이란 무엇인가요?
  28. 람다식(Lambda Expression)이 무엇인지 설명해주세요.
  29. LINQ란 무엇인가요?
  30. 리플렉션(Reflection)이 뭔지, 사용을 해봤다면 어떤 이유에서 사용했는지 설명해주세요.

Unity 문법

  1. Unity 생명주기(Unity Life Cycle)에 대해서 설명해주세요.
  2. MonoBehaviour 클래스의 주요 메서드와 그 기능에 대해 설명해주세요.
    • MonoBehaviour 클래스에서 Start와 Awake의 차이점은 무엇이며, 이를 적절히 사용하는 방법에 대해 설명해주세요.
  3. Update, FixedUpdate, LateUpdate의 차이점에 대해 설명해주세요.
  4. Time.deltaTime이란 무엇이며, 사용하는 이유에 대해 설명해주세요.
  5. 코루틴의 동작원리와 사용해본 예시를 함께 설명해주세요.
  6. Invoke와 코루틴의 차이에 대해 설명해주세요.
  7. 코루틴과 멀티쓰레딩은 어떤 차이가 있는지 설명해주세요.
  8. 유니티 최적화 기법은 어떤 것들이 있나요?
    • 최적화를 해본 적이 있나요? 없다면 어떤 최적화가 있는지 설명해주세요.
    • 최적화에서 가장 중요한 부분은 무엇인가요?
    • 최적화를 위해서 적용해본 텍스쳐 포맷이 있나요?
  9. 드로우콜에 대해서 설명하고, 최적화하는 방식에 대해 알고 있는 것이 있으면 설명하세요.
  10. Find 함수 사용을 자제해야 하는 이유에 대해 설명해주세요.
  11. Update에서 GetComponent와 그 계열의 캐싱을 지양해야하는 이유를 설명하세요.
  12. CSV/JSON 등 데이터 저장 포맷에 대해 설명하고, 활용에 적절한 상황을 설명해주세요.
  13. 특정 데이터를 JSON으로 활용하기 위해 해야하는 작업은 무엇인가요?
  14. Unity에서 필드를 직렬화하려면 어떻게 해야하는지 설명해주세요.
  15. Unity에서 멀티스레딩을 구현하기 위한 방법에 대해 설명해주세요.
  16. CPU와 GPU의 작동 방법은 어떤 차이가 있는지 설명해주세요.
  17. 월드 스페이스 (World Space) 와 로컬 스페이스 (Local Space)의 차이에 대해 설명해주세요.
  18. 벡터의 내적과 외적을 어느 상황에 사용할 수 있는지 설명해주세요.
  19. 쿼터니언을 사용하는 이유에 대해 설명해주세요.
  20. 네트워크 프로토콜 (IP, TCP, UDP)에 대해 설명해주세요.
    1. TCP와 UDP의 차이를 설명해주세요.
  21. 렌더링 파이프라인에 대해 설명해주세요.
  22. 3D 공간에 있는 오브젝트들이 화면에 표현되는 픽셀로 표시되기까지의 과정을 설명해보세요.
  23. 셰이더를 활용해본 경험이 있을까요? 어떻게 활용했는지 설명해주세요.

자료구조 & 알고리즘

  1. LinkedList의 특성을 설명해주세요.
    1. LinkedList는 언제 사용하면 좋은 자료구조인가요? 반대로 언제 사용하기 불리할까요?
    2. LinkedList를 본인의 프로젝트에 적용해본 경험을 말해주세요.
  2. Stack의 특성을 설명해주세요.
    1. Stack은 언제 사용하면 좋은 자료구조인가요? 반대로 언제 사용하기 불리할까요?
    2. Stack을 본인의 프로젝트에 적용해본 경험을 말해주세요.
  3. Queue의 특성을 설명해주세요.
    1. Queue는 언제 사용하면 좋은 자료구조인가요? 반대로 언제 사용하기 불리할까요?
    2. Queue를 본인의 프로젝트에 적용해본 경험을 말해주세요.
  4. Tree의 순회(Traversal) 방법에 대해 설명해주세요.
  5. DFS와 BFS에 대해 설명해주세요.
    1. DFS와 BFS를 본인의 프로젝트에 활용한 경험이 있다면 설명해주세요.
  6. 행동 트리 (Behaviour Tree) 에 대해 설명해주세요.
  7. 길찾기 알고리즘에 대해 알고 있는 것이 있나요?
  8. 각 길찾기 알고리즘의 차이점은 무엇인가요?
  9. A* 알고리즘에 대해 설명해주세요.
  10. 해당 알고리즘들을 프로젝트에 적용해본 경험이 있나요?

기타

  1. 게임 개발 학습 과정에서 자신이 가장 재미있었거나 자신 있게 설명할 수 있는 부분이 있다면 설명해주세요.
  2. 포트폴리오에서 본인이 담당한 부분이 무엇인가요?
  3. 만약 본인이 개발한 것을 다시 개발한다면, 어떻게 개선할 것인지 설명해주세요.
  4. 본인이 활용하는 디버그 방법을 설명해보세요.
  5. 협업하면서 가장 어려웠던 점이 무엇인가요?
  6. 구현하면서 기술적으로 어려웠던 부분을 어떻게 해결하였나요?
  7. Git Repository를 팀원들과 공동으로 작업하면서 발생했던 문제점이 있다면 무엇이었는지, 어떻게 해결하였는지 이야기해주세요.

'기술면접질문' 카테고리의 다른 글

기술 면접 질문과 답변(1)  (0) 2024.12.09

구현된 인벤토리

ItemDB.Json을 통하여 ItemSlot들을 생성하여 인벤토리를 구현하였고 , ItemSlot은 현재 생성/파괴를 하고있기에 , 오브젝트 풀링을 이용하여 리팩토링 해야된다. 

 

해당 구조도 MVP패턴을 이용하여 구현하였다.

 

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class InventoryController : UIController
{
    private InventoryModel inventoryModel;
    private InventoryView inventoryView;

    public InventoryModel Model { get => inventoryModel; set => inventoryModel = value; }
    public InventoryView View { get => inventoryView; set => inventoryView = value; }

    public override void Initialize(IUIBase view, UIModel model)
    {
        inventoryModel = model as InventoryModel;
        inventoryView = view as InventoryView;

        base.Initialize(inventoryView, inventoryModel);
    }

    public override void OnShow()
    {
        view.ShowUI();
        UpdateView();   // 초기 View 갱신
    }

    public override void OnHide()
    {
        view.HideUI();
    }

    public override void UpdateView()
    {
        // Model 데이터를 기반으로 View 갱신
        view.UpdateUI();
    }
}

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using UnityEngine.U2D;

public class InventoryModel : UIModel
{
    public List<Item> Items = new List<Item>(); // 소지하고있는 아이템 리스트 

    public void Initilaize()
    {
        foreach (ItemDB Data in DataManager.Instance.ItemDB.ItemsDict.Values)
        {
            Item itemObj = new Item();
            itemObj.Initialize(Data);
            Items.Add(itemObj);
        }
    }

    public void AddItem(string item)
    {

    }

    public void RemoveItem(string item)
    {

    }
}
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.InputSystem.XR;

public class InventoryView : MonoBehaviour, IUIBase
{
    [SerializeField] private Transform itemSlotParent;
    [SerializeField] private ItemSlot itemSlotPrefab;
    [SerializeField] private RectTransform itemSlotBoundary;
    public InventoryController Controller;

    private List<ItemSlot> itemSlots = new List<ItemSlot>();

    public void Initialize()
    {
        if (itemSlotPrefab == null)
        {
            itemSlotPrefab = Resources.Load<ItemSlot>("Prefabs/Item/ItemSlot");
        }

        for (int i = 0; i < Controller.Model.Items.Count; i++)
        {
            itemSlots.Add(Instantiate(itemSlotPrefab, itemSlotParent));

            itemSlots[i].Initiliaze(Controller.Model.Items[i]);
        }
    }

    public void ShowUI()
    {
        Vector2 size = itemSlotBoundary.sizeDelta;
        size.y = 135 * ((itemSlots.Count / 4) + 1);
        itemSlotBoundary.sizeDelta = size;
    }

    public void HideUI()
    {
    }

    public void UpdateUI()
    {
        Debug.LogAssertion("인벤토리 UI 업데이트");
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class UIInventory : MonoBehaviour
{
    public string UIKey;

    private InventoryModel model;
    [SerializeField] private InventoryView[] views;
    private InventoryController controller;

    private void Start()
    {
        //Model(Data) 초기화
        model = new InventoryModel();
        model.Initilaize();
        GameManager.Instance._player.Inventory = model;

        //컨트롤러  초기화 및 View 등록
        controller = new InventoryController();
        for (int i = 0; i < views.Length; i++) 
        {
            views[i].Controller = controller;
            controller.Initialize(views[i], model);
        }

        //UI매니저에 UI 등록
        UIManager.Instance.RegisterController(UIKey, controller);
        gameObject.SetActive(false);
    }

    public void UIShow()
    {
        controller.OnShow();
    }
    
}

 

현재 로직만 구현 중이고 , 아직 Player 캐릭터와는 연결이 되어있지 않다. ItemStatus UI와 연결후 PlayerStat과 연동해서 값을 변경 해야 될것 같다. 

https://01149.tistory.com/148

 

TIL : 2024-12-09(월) :: 최종프로젝트 개발 일지(11) - Scene을 로드하는경우 파괴된 Object에 접근되는 상

Enemy 및 Projectile 을 ObjectPool Class에 넣어 관리를 하는데,Scene을 로드하는 경우 ObjectPollManager는 DontDestroy로 파괴되지 않아 데이터가 유지되지만Enemy Object들은 Scene이 로드되면서 파괴되기에 ObjectPoolM

01149.tistory.com

해당 관련된 글의 트러블 슈팅이다.

트러블 슈팅의 대처법으로 

 

[ 결론은 Scene 재로드시에 ObjectPool을 비우는 구조를 설계를 해야만 한다.  ] 

 

였기에 , 우선 호출시점을 오브젝트 풀링을 셋팅하기 전으로 설계를 해야되고 , 각 클래스별로 오브젝트 풀링세팅하는것을 한 곳으로 모아서 작업을 하였으며 , 경로 같은 고정 문자열은 따로 Const 라는 고정 상수,문자열을 저장하는 클래스를 따로 만들어 관리하기로 하였다.

 

이렇게 한 이유는 

첫번째 : 오브젝트 풀링 세팅은 한 Scene에서 세팅 후에 호출 및 반납만하기에 한 곳에서 해도 된다고 판단하였고

Scene 로드 할때 처음으로 오브젝트 풀을 비우면 중복 생성이나 파괴된 오브젝트에 접근하지 않을거라고 판단하였다.

두번째 : 경로는 보통 문자열로 이루어져있고 수정이 되지 않아야되고 접근이 가능해야되기에 Static Class로 생성 한뒤 readonly 기능을 이용하어 수정을 막았다.

 

GameSceneTrigger.cs

using System;
using UnityEngine;

public class GameSceneTigger : MonoBehaviour
{
    private void Start()
    {
        ObjectPoolManager.Instance.ObjectPoolAllClear();

        PlayerObjectPoolSetting();
        EnemyObjectPoolSetting();
        InventoryObjectPoolSetting();

        GameManager.Instance.StartGame();
    }

    private void InventoryObjectPoolSetting()
    {
    }

    private void PlayerObjectPoolSetting()
    {
        ObjectPool playerProjectilePool = new ObjectPool(Const.POOL_KEY_PLAYERPROJECTILE, Const.PLAYER_INITIAL_POOL_SIZE, Const.PLAYER_PROJECTILE_ENERGYBOLT_PATH);
        ObjectPoolManager.Instance.AddPool(Const.PLAYER_PROJECTILE_ENERGYBOLT_KEY, playerProjectilePool);
    }

    private void EnemyObjectPoolSetting()
    {
        ObjectPool goblinPool = new ObjectPool(5000, 60, Const.ENEMY_PREFEB_GOBLIN_PATH);
        ObjectPool goblinMagicianPool = new ObjectPool(5001, 60, "Prefabs/Enemy/GoblinMagician");

        ObjectPool slashPool = new ObjectPool(6000, 60, "Prefabs/Enemy/Effects/Slash");
        ObjectPool energyBoltPool = new ObjectPool(6001, 60, "Prefabs/Enemy/Effects/EnergyBolt");
        ObjectPool slashBossPool = new ObjectPool(6002, 60, "Prefabs/Enemy/Effects/SlashBoss");
        ObjectPool skillBoss1Pool = new ObjectPool(6003, 10, "Prefabs/Enemy/Effects/SkillBoss1");

        ObjectPool goblinBossPool = new ObjectPool(5500, 3, "Prefabs/Enemy/GoblinBoss");

        ObjectPoolManager.Instance.AddPool(Const.ENEMY_POOL_KEY, goblinPool);
        ObjectPoolManager.Instance.AddPool(Const.ENEMY_POOL_KEY, goblinMagicianPool);

        ObjectPoolManager.Instance.AddPool(Const.ENEMY_EFFECT_POOL_KEY, slashPool);
        ObjectPoolManager.Instance.AddPool(Const.ENEMY_EFFECT_POOL_KEY, energyBoltPool);
        ObjectPoolManager.Instance.AddPool(Const.ENEMY_EFFECT_POOL_KEY, slashBossPool);
        ObjectPoolManager.Instance.AddPool(Const.ENEMY_EFFECT_POOL_KEY, skillBoss1Pool);

        ObjectPoolManager.Instance.AddPool(Const.ENEMY_BOSS_POOL_KEY, goblinBossPool);
    }
}

해당 코드의 기능은 GameScene을 로드시에 필요한 곳에서 ObjectPool을 세팅하는 Class입니다.

SceneManager의 SceneLoad를 사용해도 되지만, 세팅해야될 데이터가 많고 , 확장성을 대비해서 Class로 만든뒤 오브젝트에 컴포넌트로 적용시켜 사용 중입니다. 

Enemy 및 Projectile 을 ObjectPool Class에 넣어 관리를 하는데,

Scene을 로드하는 경우 ObjectPollManager는 DontDestroy로 파괴되지 않아 데이터가 유지되지만

Enemy Object들은 Scene이 로드되면서 파괴되기에 ObjectPoolManager에는 파괴된 Object만 가지고 있게 된다.

 

해당 트러블 슈팅을 겪고 있어 두가지 방법을 생각하고 있다.

1. Scene이 로드 될때마다 ObjectPool을 비우기 

이 방법을 사용하면 처리비용은 높지만 문제는 깔끔하게 처리가 될것이다. 

하지만 우리 프로젝트는 Stage마다 Scene을 재 로드 해서 사용하기로 했으나 , 이렇게되면

최적화에 좋지 않아 고민 중이다. 

 

2.Scene 로드 할때마다 ObjectPool을 비우지만 Stage 로드시에는 비우지 않기 

Scene로드 시에만 ObjectPool을 비우고 Stage 에서는 생성되어있는 ObjectPoll을 재활용 하여 다음 Stage로 넘어갈떄 비동기로 데이터 세팅을 하는싞으로 할지 고민 중이다.

 

결론은 Scene 재로드는 구현을 해야되기에 비우는 구조를 설계를 해야만 한다. 

면접관의 기술면접 의도
1.개념(정의) : 너가 이 주제에 대해 개념을 잘 알고 있느냐?
2.구현방식 : 물어본 주제가 어떤식으로 동작되는지 알고 있느냐?
3.장점과 단점 or 특징 : 해당 주제의 장단점을 알고 어떡해 사용되는지 알고 있느냐?
4.실용적인 예시 : 실제로 사용해본적이 있는가? 없다면 언제 사용해야되는지 알고 있는가?


1.객체란 무엇인가요? 클래스와 어떤 연관이 있나요? 

개념 : 객체는 오브젝트를 의미하며 런타임시 실제로 동작하게되는 하나의 단위라고 생각합니다.

구현 방식 : C++이나 C#에서는 new라는 키워드를 사용하여 동적 할당을 할 수 있고 , 자료형을 선언하여 정적으로도 생성하여 사용 할 수 있습니다.

장점과 단점 or 특징: 하나의 기능 및 동작을 추상화하여 객체 안에 작성하여 사용 함으로 하나의 객체로 수 십개의 똑같은 객체를 만들어 사용 할 수 있습니다.

실용적인 예시 : 클래스는 객체를 만드는 설계도로 , 해당 클래스에 구현 코드를 작성한 뒤 new 키워드나 정적으로 생성하여 어러 객체를 사용 할 수 있습니다. 

 

 

4.static 한정자에 대해 설명해주세요.

static은 전역변수의 위험성을 보완하기 위한 한정자입니다.

static은 클래스간 공유 상태를 관리 하거나 인스턴스(객체)에 의존하지 않는 멤버를 선언하는데

사용되는 한정자입니다.

static 한정자가 붙은 필드나 메서드는 컴파일시 제일 먼저 생성되며,

static 멤버는 프로그램 실행 시(즉, 클래스가 로드될 때) 메모리에 할당되며, 런타임에 클래스가 처음 참조되거나

로드 될때 초기화 됩니다.
어느 클래스에서든지 호출이 가능해집니다.

해당 클래스의 정적멤버로서 ,명시적으로 클래스 이름을 통해 접근하여 호출이 가능해짐 

 

static이 사용되는 주요 사례 -> 유틸리티메서드 , 싱글톤 패턴에 사용된다고 언급하기 

 

5.SOLID 원칙에 대해 설명해주세요

객체지향 프로그래밍(OOP) 및 설계의 다섯 가지 기본 원칙을 정리한것을 SOLID원칙이라고 합니다.

S는 SRP(single responsibley principle )로 , 단일 책임 원칙을 의미하며

하나의 클래스에 하나의 책임을 가진다는 의미입니다.

O는 OCP(Open-Close principle)로, 개방-폐쇄 원칙을 의미하며

확장에는 개방을 , 변경에는 폐쇠적으로 설계를 해야된다는 의미입니다.

L은 LSP(Liskov substitition principle)로, 리스코프 치환 원칙을 의미하며,

다형성에 관련된 원칙으로 상속,인터페이스 설계에 적용됩니다. 

또한 부모-자식의 상속관계에서 자식클래스는 언제나 부모클래스로 치환되어 사용이 가능해져야 합니다.

ISP : 인터페이스 분리 원칙 (Interface segregation principle)
인터페이스의 단일 책임 , SPR는 클래스의 단일책임, ISP는 인터페이스의 단일책임을 의미
SPR도 책임의 범위에 대해 분리기준이 있듯이 , 인터페이스를 분리하는 기준은 상황에 따라 다름, 
핵심은 관련 있는 기능끼리 하나의 인터페이스로 모으되 지나치게 커지지 않도로 크기에 제한을 두라는 의미
ISP는 SPR를 만족하면 성립되는가? -> 반드시 그렇다고 할수 없다 
ex)게시판 인터페이스로 글쓰기 읽기 수정 삭제 권한이 있을때 해당 인터페이스는 관리자에 상속해서 사용하지만
일반 사용자가 상속해서 사용하게되면 글삭제 권한이 생기기에 ISP를 만족 할수 없다.
인터페이스를 설계할 때는 클라이언트 중심으로 설계하여 변경 가능성을 최소화해야 한다" , 
나중에 수정사항이 생겨서 또 인터페이스를
분리하는 행위를 하지 말아야되는것이 ISP원칙의 주의해야 할 점이다. 

DIP 원칙이란 객체에서 어떤 Class를 참조해서 사용해야하는 상황이 생긴다면, 
그 Class를 직접 참조하는 것이 아니라 그 대상의 상위 요소(추상 클래스 or 인터페이스)로 참조하라는 원칙
이 때 객체들은 나름대로의 원칙을 갖고 정보를 주고 받아야 하는 약속이 있다. 
여기서 나름대로의 원칙이란 추상성이 낮은 클래스보다 추상성이 높은 클래스와 통신을 한다는 것을 의미하는데 이것이 DIP 원칙

 

 

 

'기술면접질문' 카테고리의 다른 글

기술 면접 리스트  (0) 2024.12.12

해당 Stage 진행도를 나타내주는 UI

 

Stage 진행도를 나타내주는 기능은 아무래도 동적으로 값이 계속 변하다보니 MVC 패턴을 이용하여 UIManager에 등록하고 사용하는 방식으로 진행 하였다., 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem.XR;
using UnityEngine.UI;

//Model : GameManager의 처치한 Enemy Count가 Model이됨
//View : UIStageProgressBarView
//Controller : UIStageProgressBarController

public class UIStageProgressBar : MonoBehaviour
{
    public string UIKey;

    private UIStageProgressBarModel model;
    [SerializeField]private UIStageProgressBarView view;
    private UIStageProgressBarController controller;

    private void Start()
    {
        model = new UIStageProgressBarModel();
        GameManager.Instance.StageProgressModel = model;
        controller = new UIStageProgressBarController();
        controller.Initialize(view, model);

        UIManager.Instance.RegisterController(UIKey, controller);
    }

}

해당 기능의 Model,View,Controller를 멤버변수로 가지고있어 초기화및 관리를 하고있다. 오브젝트에 컴포넌트로 적용하여 사용하는 클래스이다. 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
using DG.Tweening;

//Model : GameManager의 처치한 Enemy Count가 Model이됨
//View : UIStageProgressBarView
//Controller : UIStageProgressBarController

public class UIStageProgressBarView : MonoBehaviour, IUIBase
{
    [SerializeField] private RectTransform curStageProgress;

    public void Initialize()
    {
        if (curStageProgress == null)
        {
            curStageProgress = GetComponent<RectTransform>();
        }

        curStageProgress.localScale = new Vector3(0, 1, 1);

    }

    public void ShowUI()
    {
        //Boss 몬스터 등장 조건이 되지 않으면 UI 출력
        gameObject.SetActive(true);
    }

    public void HideUI()
    {
        //Boss 몬스터 등장 조건이 되면 사라지기
        Utils.StartFadeOut(this.GetComponent<CanvasGroup>(), Ease.OutBounce, 1.0f);
        gameObject.SetActive(false);
    }

    public void UpdateUI()
    {

    }

    public void UpdateUIProgree(float resultProgress)
    {
        curStageProgress.localScale = new Vector3(resultProgress, 1, 1);
    }
}

Controller로부터 데이터를 받아 Scene에 출력(업데이트)하는 클래스 영역이다. 

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

//Model : GameManager의 처치한 Enemy Count가 Model이됨
//View : UIStageProgressBarView
//Controller : UIStageProgressBarController

[System.Serializable]
public class UIStageProgressBarModel : UIModel
{
    public event Action OnEventCurEnemyAddCount;
    private int curEnemySlayerCount; //처지한 Enemy 카운트
    private int bossTriggerEnemySlayerCount; //Boss가 등장에 필요한 쓰러트린 Enemy 카운트

    public int CurEnemySlayerCount { get => curEnemySlayerCount; private set => curEnemySlayerCount = value; }
    public int BossTriggerEnemySlayerCount { get => bossTriggerEnemySlayerCount; private set => bossTriggerEnemySlayerCount = value; }

    public void Initialize(int bossTriggerCount)
    {
        bossTriggerEnemySlayerCount = bossTriggerCount;
        curEnemySlayerCount = 0;
    }

    public void CurCountDataClear()
    {
        curEnemySlayerCount = 0;
    }

    public void AddCurEnemyCount(int slayEnemyCount)
    {
        curEnemySlayerCount += slayEnemyCount;
        Debug.Log($"현재 처치한 적의 갯수 : {curEnemySlayerCount} \" {bossTriggerEnemySlayerCount}");
        OnEventCurEnemyAddCount?.Invoke();
    }


}

Data(Enemy처치 갯수)를 관리하는 곳으로 적을 처치할때마다 EnemyCount가 올라간다. 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

//Model : GameManager의 처치한 Enemy Count가 Model이됨
//View : UIStageProgressBarView
//Controller : UIStageProgressBarController

public class UIStageProgressBarController : UIController
{
    private UIStageProgressBarModel stageProgressBarModel;
    private UIStageProgressBarView stageProgressBarView;

    public override void Initialize(IUIBase view, UIModel model)
    {
        base.Initialize(view, model);

        stageProgressBarModel = model as UIStageProgressBarModel;
        stageProgressBarView = view as UIStageProgressBarView;

        stageProgressBarModel.OnEventCurEnemyAddCount += UpdateView;
        stageProgressBarModel.OnEventCurEnemyAddCount += BossTriggerCheck;
    }

    public override void OnShow()
    {
        view.ShowUI();
        UpdateView();   // 초기 View 갱신
    }

    public override void OnHide()
    {
        view.HideUI();
    }

    public override void UpdateView()
    {
        float resultProgress = (stageProgressBarModel.CurEnemySlayerCount / (float)stageProgressBarModel.BossTriggerEnemySlayerCount);
        resultProgress = Mathf.Min(1,resultProgress);

        // Model 데이터를 기반으로 View 갱신
        //view.UpdateUI();
        stageProgressBarView.UpdateUIProgree(resultProgress);
    }

    public void BossTriggerCheck()
    {
        if (stageProgressBarModel.CurEnemySlayerCount >= stageProgressBarModel.BossTriggerEnemySlayerCount)
        {
            Debug.Log($"보스 등장 조건을 만족 합니다.");
            GameManager.Instance.isTryBoss = true;
            OnHide();
        }
    }
}

Controller로 Data값이 등록되면 해당 Controller에 알려주고 Controller는 해당 데이터가 들어옴에 따라 로직대로 동작하게된다. 해당 로직은 현재 잡은 Enemy수 / 목표 Enemy 처치수를 하여 백분율로 나타내고 해당 데이터를 이용하여 Boss 등장 조건을 체크하는 Class 이다. 

Player,Enemy의 콜라이더를 IsTrigger 체크를 하여 사용하니 투사체에 있는 OnCollison 메서드가 동작하지 않았고

Collison의 ContactPoint를 OnTrigger 메서드 내에서 필요했다.

  protected void ProjectileCollison(Collision collision)
  {
      //Lock all axes movement and rotation
      rb.constraints = RigidbodyConstraints.FreezeAll;
      //speed = 0;
      if (lightSourse != null)
          lightSourse.enabled = false;
      col.enabled = false;
      projectilePS.Stop();
      projectilePS.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear);

      ContactPoint contact = collision.contacts[0];
      Quaternion rot = Quaternion.FromToRotation(Vector3.up, contact.normal);
      Vector3 pos = contact.point + contact.normal * hitOffset;
   	
    //.....
  }

해당 코드는 OnCollison 메서드 내부의 있는 메서드인데 해당 함수에서 Collison.contacts를 호출해서 접촉한 면의 각도와 방향을 가져와서 투사체가 사라질때 피격 효과를 내고있었다.

 

이 피격효과를 내기 위해서는 충돌하는 객체의 데이터가 필요했다. 

 

그래서 Collider의 Docs에서 찾아보니 ClosestPoint 프로퍼티를 사용하면

 트리거와 충돌하는 물체의 가장 가까운 지점을 계산해주는 기능이 있었다. 

https://docs.unity3d.com/ScriptReference/Collider.ClosestPoint.html

 

Unity - Scripting API: Collider.ClosestPoint

This method computes the point on the Collider that is closest to a 3D location in the world. In the example below closestPoint is the point on the Collider and location is the point in 3D space. If location is in the Collider the closestPoint is inside. I

docs.unity3d.com

 

그래서 해당 프로퍼티를 사용하여 충돌시 동작하는메서드를 변경하여 사용하였다. 

 protected void ProjectileCollison(Collider other)
 {
     //Lock all axes movement and rotation
     rb.constraints = RigidbodyConstraints.FreezeAll;
     //speed = 0;
     if (lightSourse != null)
         lightSourse.enabled = false;
     col.enabled = false;
     projectilePS.Stop();
     projectilePS.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear);

     Vector3 closetPoint = other.ClosestPoint(other.transform.position);

     //Spawn hit effect on collision
     if (hit != null)
     {
         //hit.transform.rotation = rot;
         hit.transform.position = closetPoint;
         if (UseFirePointRotation) { hit.transform.rotation = gameObject.transform.rotation * Quaternion.Euler(0, 180f, 0); }
         else if (rotationOffset != Vector3.zero) { hit.transform.rotation = Quaternion.Euler(rotationOffset); }
         //else { hit.transform.LookAt(contact.point + contact.normal); }
         else { hit.transform.LookAt(closetPoint); }
         hitPS.Play();
     }

     //Removing trail from the projectile on cillision enter or smooth removing. Detached elements must have "AutoDestroying script"
     foreach (var detachedPrefab in Detached)
     {
         if (detachedPrefab != null)
         {
             ParticleSystem detachedPS = detachedPrefab.GetComponent<ParticleSystem>();
             detachedPS.Stop();
         }
     }

     if (notDestroy)
         StartCoroutine(DisableTimer(hitPS.main.duration));
     else
     {
         gameObject.SetActive(false);
     }


     ObjectPoolManager.Instance.GetPool("playerProjectile", Utils.POOL_KEY_PLAYERPROJECTILE).GetObject();
 }

 

객체에 접촉하는 면의 데이터를 가져오는게 아니라 법선벡터라던가 각도를 제대로 측정하지는 못하지만

OnTrigger 메서드로 피격 효과의 좌표값 조정이 가능해졌다.

+ Recent posts