<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>jinyj_2008.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Wed, 31 Dec 2025 01:44:34 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>jinyj_2008.log</title>
            <url>https://velog.velcdn.com/images/jinyj_2008/profile/bfadd6ac-77e5-4321-8367-99f799713e62/social_profile.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. jinyj_2008.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/jinyj_2008" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Unity 실무 프로그래밍 #2 - MVP 패턴으로 UI 시스템 전략적으로 설계하기 + Interaction System]]></title>
            <link>https://velog.io/@jinyj_2008/Unity-%EC%8B%A4%EB%AC%B4-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-2-MVP-%ED%8C%A8%ED%84%B4%EC%9C%BC%EB%A1%9C-UI-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%A0%84%EB%9E%B5%EC%A0%81%EC%9C%BC%EB%A1%9C-%EC%84%A4%EA%B3%84%ED%95%98%EA%B8%B0-Interaction-System</link>
            <guid>https://velog.io/@jinyj_2008/Unity-%EC%8B%A4%EB%AC%B4-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-2-MVP-%ED%8C%A8%ED%84%B4%EC%9C%BC%EB%A1%9C-UI-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%A0%84%EB%9E%B5%EC%A0%81%EC%9C%BC%EB%A1%9C-%EC%84%A4%EA%B3%84%ED%95%98%EA%B8%B0-Interaction-System</guid>
            <pubDate>Wed, 31 Dec 2025 01:44:34 GMT</pubDate>
            <description><![CDATA[<h1 id="가-프로젝트-소개">가. 프로젝트 소개</h1>
<hr>
<p>개발하는 게임의 장르는 <strong>스테이지형 3D 어드벤처</strong>입니다.
Unity 및 C#을 기반으로 개발됩니다.</p>
<blockquote>
<p>Unity, C#, 객체 지향에 대한 기초적 이해가 있으신 분들을 위한 시리즈입니다!!</p>
</blockquote>
<p>Unity Engine 상에서의 개발은 자세히 다루지 않으며, 프로그래밍에 대한 통찰을 집중적으로 나누고자 합니다.</p>
<p>.
.</p>
<h1 id="나-개요">나. 개요</h1>
<hr>
<p>이전 게시글에서는 Facade 패턴을 활용하여 Player Controller System을 구현하였습니다.</p>
<p>이번에는 <strong>Model-View-Presenter (MVP) 패턴</strong>를 기반으로 UI를 개발하고,
상호작용 (Interaction) 시스템까지 구현해보겠습니다.</p>
<blockquote>
<p><strong>UI 프로그래밍은 프로젝트의 환경 변수를 정확하게 고려하여 개발해야 합니다.</strong></p>
</blockquote>
<p>구현하는 프로그램의 목적(도메인 및 요구 사항)과 제공하고자 하는 경험(기능 및 UX)에 따라 UI는 다르게 설계되고, 각 페이지 또는 UI 컴포넌트가 화면에 어떻게 구성되느냐에 따라 <strong>설계 방식은 얼마든지 변형될 수 있습니다.</strong></p>
<blockquote>
<p><strong>[팁!]</strong></p>
<ul>
<li>제가 이 프로그램을 왜 이렇게 작성했는지</li>
<li><strong>개발자님의 프로젝트에서는 UI 프로그램이 어떻게 설계될 수 있는지</strong>를</li>
</ul>
<p>생각하면서 읽어주시면 이 글을 흡수하는 데에 큰 도움이 될 것입니다.</p>
</blockquote>
<p>아래에서는 MVP에 대한 간략한 소개와 제가 작성한 UI 프로그램 예제가 소개됩니다.</p>
<p>.
.</p>
<h1 id="다-model-view-presenter-mvp에-대하여">다. Model-View-Presenter (MVP)에 대하여</h1>
<hr>
<h2 id="1-mvp-패턴이란">1) MVP 패턴이란?</h2>
<p><strong>Model-View-Presenter (MVP) 패턴</strong>은 구조적인 User Interface (UI) 프로그램을 구현하기 위해 자주 사용되는 디자인 패턴입니다.</p>
<p>이름에서도 알 수 있듯 <strong>Model, View, Presenter로 나눠진 클래스가 서로 다른 책임을 맡아 UI를 운영</strong>하도록 구조하는 패턴입니다. 각 클래스의 역할은 다음과 같습니다.</p>
<p>.
.</p>
<h3 id="ㄱ-model-view-presenter의-책임">ㄱ) Model, View, Presenter의 책임</h3>
<p><img src="https://velog.velcdn.com/images/jinyj_2008/post/c2b28499-14a1-4758-b555-19733b354a73/image.png" alt=""></p>
<ul>
<li><p><strong>Model: 데이터 저장소 겸 비즈니스 로직을 담당하는 부분입니다.</strong>
(실제 게임 규칙과 연결되어 있는 부분이지만 엄연히 UI 시스템에 속하며, 따라서 Model에 로직을 구현하기 보다는 이미 구현되어 있는 로직과 UI 시스템을 이어주는 징검다리(인터페이스)처럼 구현되는 경우가 많습니다. 아래에 예시가 있습니다.)</p>
</li>
<li><p><strong>View: 실제 플레이어에게 보여지는 프레젠테이션 계층입니다.</strong>
(User Interface로서 데이터 저장소나 로직(=Model)에 대한 의존 없이 홀로 완전할 수 있어야 합니다. Unity 프로그램에서는 주로 UI를 제어하기 위해 TextMeshProUGUI나 Button 등의 객체만 참조하며, 제공되는 데이터를 화면에 표시하는 책임만 갖습니다.)</p>
</li>
<li><p><strong>Presenter: Model과 View를 이어 UI 시스템을 완성하는 접착제와 같습니다.</strong>
(Model과 View는 Presenter를 알지 못하지만, Presenter는 Model과 View를 모두 참조합니다.)
(Model에서 제공하는 데이터와 이벤트(state-change events)를 바탕으로, View를 조작하여 UI 시스템의 반응을 구현합니다. 이를 통해 View와 Model은 서로를 알지 못하며 각자의 책임만 지킬 수 있게 됩니다. {단일 책임 원칙, SRP})</p>
</li>
</ul>
<p>.
.</p>
<h3 id="ㄴ-mvp-흐름-예시">ㄴ) MVP 흐름 예시</h3>
<p><strong>User Interface -&gt; Model 흐름</strong></p>
<pre><code class="language-bash">User                                            View
[ 회복 버튼 클릭 ]   -- &lt;Unity Game UI System&gt; --&gt;    [ OnHealAttempted.Invoke() ]


View                                            Presenter
[ OnHealAttempted.Invoke() ] -- &lt;Event 수신&gt; --&gt;    [ model.Heal() ]


Presenter                                        Model
[ model.Heal() ]          -- &lt;조작&gt; --&gt;            [ Hp += 100 ] </code></pre>
<p><strong>Model -&gt; UserInterface 흐름</strong></p>
<pre><code class="language-bash">Model                                            Presenter
[ OnHpUpdated.Invoke() ]   -- &lt;Event 수신&gt; --&gt;    [ view.UpdateHpBar(model.Hp) ]


Presenter                                        View
[ view.UpdateHpBar(model.Hp) ]   -- &lt;조작&gt; --&gt;    [ hpBar.text = hp ]


View                                            User
[ hpBar.text = hp ]      -- &lt;Unity UI&gt; --&gt;        [ HP 상태 갱신 완료 ]</code></pre>
<p>.
.</p>
<h1 id="라-상호작용-hud-및-지도-메뉴-개발">라. 상호작용 HUD 및 지도 메뉴 개발</h1>
<hr>
<h2 id="1-scripts-폴더-구조">1) Scripts 폴더 구조</h2>
<pre><code>Scripts
├── Components
│   ├── Interactable
│   │   └── IInteractable.cs
│   └── Player
│       └── Interaction
│           ├── IInteractionEventProvider.cs
│           └── PlayerInteractor.cs
└── UI
    ├── InteractionPrompt
    │   ├── Presenter
    │   │   └── InteractionPromptPresenter.cs
    │   └── View
    │       └── InteractionPromptView.cs
    └── Models
        ├── Interaction
            ├── IInteractionModel.cs
            └── Impl
                └── PlayerInteractionModel.cs</code></pre><h3 id="ㄱ-디렉토리-설명">ㄱ) 디렉토리 설명</h3>
<ul>
<li><p><strong>/Components : 특정 오브젝트에 기능을 부여하기 위한 Component들을 위한 디렉토리입니다.</strong>
(RPG 게임이라면 Player, Enemy, 자동차 게임이라면 Vehicle 등이 이 디렉토리에 포함될 수 있습니다.)</p>
</li>
<li><p><strong>/UI : 유저 인터페이스의 독립적인 계층을 보장하기 위한 디렉토리입니다. UI와 관련된 모든 스크립트가 포함됩니다.</strong>
(UI와 게임 시스템은 독립되어 있고, 앞서 소개한 Model을 통해서만 이루어지며, Model 외의 다른 클래스들은 게임 시스템과 연결될 수 없습니다.)</p>
</li>
</ul>
<p>.
.</p>
<h3 id="ㄴ-스크립트-간단-설명">ㄴ) 스크립트 간단 설명</h3>
<ul>
<li><p><strong>IInteractable</strong> : 이 인터페이스를 구현한 컴포넌트는 플레이어 캐릭터가 상호작용 가능 (Collider 필요)</p>
</li>
<li><p><strong>IInteractionEventProvider</strong> : 플레이어의 상호작용 행동을 이벤트로 제공하는 인터페이스</p>
</li>
<li><p><strong>PlayerInteractor</strong> : 플레이어 캐릭터가 <code>IInteractable</code>과 상호작용할 수 있게 하는 컴포넌트. <code>IInteractionEventProvider</code>를 구현했음</p>
</li>
<li><p><strong>InteractionPromptPresenter</strong> : <code>InteractionPromptView</code>와 Model을 연결하는 Presenter</p>
</li>
<li><p><strong>InteractionPromptView</strong> : 상호작용 Prompt HUD를 제어하는 Presenter</p>
</li>
<li><p><strong>IInteractionModel</strong> : 플레이어의 Interaction 정보를 제공하는 Model</p>
</li>
<li><p><strong>PlayerInteractionModel</strong> : <code>IInteractionEventProvider</code> 기반의 Interaction Model</p>
</li>
</ul>
<p>.
.</p>
<h2 id="2-playerinteractor--iinteractioneventprovider--iinteractable">2) PlayerInteractor &amp; IInteractionEventProvider &amp; IInteractable &amp;</h2>
<h3 id="ㄱ-playerinteractor">ㄱ) PlayerInteractor</h3>
<pre><code class="language-csharp">    public class PlayerInteractor : MonoBehaviour, IInteractionEventProvider
    {
        [SerializeField, RequireImplement(typeof(IPlayerInputProvider))]
        private Object _playerInputProvider;

        public float InteractionRange = 1;

        public LayerMask InteractionLayerMask;

        public event Action&lt;IInteractable&gt; OnHoverEntered;

        public event Action&lt;IInteractable&gt; OnHoverExited;

        public event Action&lt;IInteractable&gt; OnInteracted;

        private IInteractable _currentInteractable;

        private void Awake()
        {
            var inputProvider = _playerInputProvider as IPlayerInputProvider;
            if (inputProvider == null) return;

            inputProvider.OnInteracted += Interact;
        }

        private void Interact()
        {
            if (_currentInteractable != null)
            {
                _currentInteractable.Interact();
            }
        }

        private void Update()
        {
            Collider[] colliders = Physics.OverlapSphere(transform.position, InteractionRange, InteractionLayerMask);

            // order by distance to player
            IEnumerable&lt;Collider&gt; orderedColliders = colliders.OrderBy(c =&gt; Vector3.Distance(transform.position, c.transform.position));

            bool foundInteractable = false;
            bool stayingOnCurrent = false;
            foreach (var c in orderedColliders)
            {
                var interactable = c.GetComponentInParent&lt;IInteractable&gt;(true);
                if (interactable != null &amp;&amp; interactable.CanInteract)
                {
                    if (interactable == _currentInteractable)
                    {
                        stayingOnCurrent = true;
                        break;
                    }

                    foundInteractable = true;
                    if (_currentInteractable != null)
                    {
                        _currentInteractable.OnInteractionHoverExited();
                        OnHoverExited?.Invoke(_currentInteractable);
                    }
                    _currentInteractable = interactable;
                    _currentInteractable.OnInteractionHoverEntered();
                    OnHoverEntered?.Invoke(_currentInteractable);
                    break;
                }
            }

            if (!foundInteractable &amp;&amp; !stayingOnCurrent &amp;&amp; _currentInteractable != null)
            {
                _currentInteractable.OnInteractionHoverExited();
                OnHoverExited?.Invoke(_currentInteractable);
                _currentInteractable = null;
            }
        }
    }</code></pre>
<h3 id="ㄴ-iinteractioneventprovider">ㄴ) IInteractionEventProvider</h3>
<pre><code class="language-csharp">using System;
using Components.Interactable;

namespace Components.Player.Interaction
{

    public interface IInteractionEventProvider
    {
        event Action&lt;IInteractable&gt; OnHoverEntered;

        event Action&lt;IInteractable&gt; OnHoverExited;

        event Action&lt;IInteractable&gt; OnInteracted;
    }

}</code></pre>
<h3 id="ㄷ-iinteractable">ㄷ) IInteractable</h3>
<pre><code class="language-csharp">namespace Components.Interactable
{

    public interface IInteractable
    {
        bool CanInteract { get; }

        string PromptContent { get; }

        void OnInteractionHoverEntered();

        void OnInteractionHoverExited();

        void Interact();
    }

}</code></pre>
<h3 id="ㄹ-playerinteractor의-다중-책임-문제">ㄹ) PlayerInteractor의 다중 책임 문제</h3>
<p><code>PlayerInteractor</code>는 플레이어 캐릭터 오브젝트에 부착되는 컴포넌트(MonoBehaviour)이며, 아래 2개 기능을 제공합니다.</p>
<ul>
<li>플레이어 캐릭터의 Interaction 기능</li>
<li>Interaction 관련 이벤트를 외부에 제공 (OnHoverEntered, OnInteracted, etc.)</li>
</ul>
<p>이전 게시글에서도 언급했고 앞으로도 계속 강조할 내용이지만, <strong>하나의 클래스가 다양한 책임을 가지도록 개발하는 것은 기술 부채로 되돌아올 가능성이 큽니다.</strong> 따라서 <code>PlayerInteractor</code>는 본래 Interaction 기능을 제공하고, Interaction 관련 데이터를 제공하는 2개의 클래스로 분리되는 것이 마땅합니다.</p>
<h3 id="ㅁ-인터페이스를-통해-다중-책임-문제-완화">ㅁ) 인터페이스를 통해 다중 책임 문제 완화</h3>
<p>하지만 객체지향 관점을 지키려고 노력하다보면 개발이 점점 피로해지게 되며, 실제로 사용 중인 많은 소스 코드에서도 이미 여러 기능이 하나의 클래스에 구현되고 있습니다.</p>
<p>이러한 부분에는 타협이 필요합니다.</p>
<p>저는 소스 코드의 복잡성을 높이지 않고 기술 부채를 방지하기 위해 <strong>각 기능을 나타내는 인터페이스 (여기서는 <code>IInteractionEventProvider</code>)를 하나의 클래스가 구현하는 방식</strong>을 사용하여 위험을 완화하였습니다.</p>
<p>여전히 클래스는 여러 기능을 구현하고 있지만, <strong>외부로 기능이 제공될 때에는 인터페이스라는 경로를 통해 인터페이스에서 정의된 각 기능별로 나누어져 제공되게 됩니다.</strong></p>
<p>.
.</p>
<h2 id="3-iinteractionmodel--playerinteractionmodel">3) IInteractionModel &amp; PlayerInteractionModel</h2>
<h3 id="ㄱ-iinteractionmodel">ㄱ) IInteractionModel</h3>
<pre><code class="language-csharp">namespace UI.Models.Interaction
{

    public interface IInteractionModel
    {
        bool IsInteractable { get; }

        string PromptContent { get; }
    }

}</code></pre>
<h3 id="ㄴ-playerinteractionmodel">ㄴ) PlayerInteractionModel</h3>
<pre><code class="language-csharp">using Components.Interactable;
using Components.Player.Interaction;
using Core.Attributes;
using UnityEngine;

namespace UI.Models.Interaction.Impl
{

    public class PlayerInteractionModel : MonoBehaviour, IInteractionModel
    {
        [SerializeField, RequireImplement(typeof(IInteractionEventProvider))]
        private Object _interactionEventProvider;

        public bool IsInteractable { get; private set; }

        public string PromptContent { get; private set; }

        private void Start()
        {
            var interactionEventProvider = _interactionEventProvider as IInteractionEventProvider;
            if (interactionEventProvider == null) return;

            interactionEventProvider.OnHoverEntered += OnInteractableHoverEntered;
            interactionEventProvider.OnHoverExited += OnInteractableHoverExited;
        }

        private void OnInteractableHoverEntered(IInteractable interactable)
        {
            IsInteractable = true;
            PromptContent = interactable.PromptContent;
        }

        private void OnInteractableHoverExited(IInteractable interactable)
        {
            IsInteractable = false;
        }
    }

}</code></pre>
<h3 id="ㄷ-model이란">ㄷ) Model이란?</h3>
<p>위에서 MVP 패턴의 Model은 데이터 저장 및 비즈니스 로직을 담당한다고 했습니다.</p>
<p>다만 실제로는 유지보수성을 위해 Model 클래스에 비즈니스 로직을 구현하는 경우는 드물고, 이미 구현되어 있는 시스템과 UI 시스템을 연결하는 접착제 역할일 때가 많습니다. (Glue code)</p>
<h3 id="ㄹ-이-구조에서-model을-사용하는-방법">ㄹ) 이 구조에서 Model을 사용하는 방법</h3>
<p>간단한 게임을 개발할 때는 인벤토리, 스탯창과 같이 하나의 페이지에 하나의 시스템과 관련된 정보를 표시할 때가 많으므로 UI와 Model을 하나의 단위로 묶어 개발하는 경우도 있습니다.</p>
<blockquote>
<p><strong>[예시!]</strong>
인벤토리와 스탯창 메뉴가 있다고 하면</p>
</blockquote>
<ul>
<li>InventoryView ↔ InventoryPresenter ↔ InventoryModel</li>
<li>StatMenuView ↔ StatMenuPresenter ↔ StatMenuModel
와 같이 구현하는 식입니다.</li>
</ul>
<p>단 UI가 조금이라도 복잡해지면 여러 UI에 하나의 정보가 표시될 때가 많아집니다.</p>
<blockquote>
<p><strong>[예시!]</strong>
내가 가지고 있는 코인이나 아이템의 이미지는 여러 곳에서 로딩됩니다.</p>
<ul>
<li>코인은 인게임 HUD, 인벤토리, 장비 메뉴 등에서 모두 보여질 수도 있으며</li>
<li>아이템의 이미지 또한 인벤토리나 상점 메뉴 등, 여러 곳에서 보인다!
<img src="https://velog.velcdn.com/images/jinyj_2008/post/efef206e-f99c-4479-a16d-b7d36f17a3f6/image.png" alt=""><figcaption style="text-align:center; font-size:15px; color:#808080; margin-top:-10px; margin-bottom:40px">"호그와트 레거시"의 상점 메뉴. 상점 메뉴에 코인과 아이템 이미지가 보입니다.</figcaption>

</li>
</ul>
<p><img src="https://velog.velcdn.com/images/jinyj_2008/post/66c9f51b-b43e-4a92-80eb-5f33a7acf032/image.png" alt=""></p>
<figcaption style="text-align:center; font-size:15px; color:#808080; margin-top:-10px;">"호그와트 레거시"의 인벤토리. 상점 메뉴에서도 보였던 코인과 아이템 이미지가 보입니다!</figcaption>
</blockquote>
<p>따라서 현대 게임 UI에서는 Model을 <strong>기능별로 분리</strong>하는 경우가 많습니다.</p>
<p>UI 단위로 Model을 분리하면 Model에는 해당 UI에서 사용되는 모든 정보들을 게임 시스템으로부터 받아오는 코드가 작성되고, <strong>여러 Model에서 하나의 정보를 참조할 때는 같은 코드를 여러 번 작성하게 됩니다.</strong>
즉, 일관성 또는 클래스 관계 복잡에 영향을 미쳐 유지보수를 방해하는 기술 부채가 발생하게 됩니다.</p>
<p>따라서 기능별로 Model을 분리하고, Presenter가 각 Model의 데이터 및 이벤트를 참조하게 하면 이러한 문제를 깔끔하게 해결할 수 있습니다. MVP 패턴 기반 UI 시스템 개발에서 권장되는 방식입니다.</p>
<blockquote>
<p><strong>[UI별 Model 분리]</strong>
InventoryModel ↔ ItemDatabase, CoinProvider, PlayerStatusProvider...
→ Model이 여러 게임 시스템을 사용. 게임 시스템과 UI 시스템 사이의 연결이 매우 복잡해짐!</p>
</blockquote>
<blockquote>
<p><strong>[기능별 Model 분리]</strong>
InventoryPresenter ↔ LoadItemModel, LoadCoinModel, LoadStatusModel...
→ Model은 자신이 책임지는 게임 시스템과 연결됨. 게임 시스템과 UI 시스템과 사이의 연결이 깔끔해짐.</p>
</blockquote>
<p>.
.</p>
<h2 id="4-interactionpromptpresenter--interactionpromptview">4) InteractionPromptPresenter &amp; InteractionPromptView</h2>
<h3 id="ㄱ-interactionpromptpresenter">ㄱ) InteractionPromptPresenter</h3>
<pre><code class="language-csharp">using Core.Attributes;
using UI.InteractionPrompt.View;
using UI.Models.Interaction;
using UnityEngine;

namespace UI.InteractionPrompt.Presenter
{

    public class InteractionPromptPresenter : MonoBehaviour
    {
        [SerializeField]
        private InteractionPromptView _view;

        [SerializeField, RequireImplement(typeof(IInteractionModel))]
        private Object _interactionModel;

        private void Update()
        {
            var interactionModel = _interactionModel as IInteractionModel;
            if (interactionModel == null) return;

            if (interactionModel.IsInteractable != _view.IsVisible)
            {
                if (interactionModel.IsInteractable)
                {
                    _view.ShowPrompt(interactionModel.PromptContent);
                }
                else
                {
                    _view.HidePrompt();
                }
            }
        }
    }

}</code></pre>
<h3 id="ㄴ-interactionpromptview">ㄴ) InteractionPromptView</h3>
<pre><code class="language-csharp">using TMPro;
using UnityEngine;

namespace UI.InteractionPrompt.View
{

    public class InteractionPromptView : MonoBehaviour
    {
        [SerializeField]
        private TextMeshProUGUI _promptTmp;

        public bool IsVisible { get; private set; }

        public void ShowPrompt(string text)
        {
            IsVisible = true;
            _promptTmp.enabled = true;
            _promptTmp.text = text;
        }

        public void HidePrompt()
        {
            IsVisible = false;
            _promptTmp.enabled = false;
        }
    }

}</code></pre>
<h3 id="ㄷ-기능-개요">ㄷ) 기능 개요</h3>
<p><img src="https://velog.velcdn.com/images/jinyj_2008/post/6a555fa4-2cef-4697-88d6-d90948422455/image.png" alt=""></p>
<figcaption style="text-align:center; font-size:15px; color:#808080; margin-top:-30px; margin-bottom:40px">Interaction Prompot HUD는 오브젝트와의 상호작용을 안내하는 UI입니다.</figcaption>

<p>Interactable 객체와 상호작용할 수 있는 상태가 되면 해당 Interactable 객체가 제공하는 텍스트를 화면에 보여주는 UI 시스템을 구현하였습니다.</p>
<h3 id="ㄹ-게임-시스템의-변화가-ui에-영향을-미치는-방법">ㄹ) 게임 시스템의 변화가 UI에 영향을 미치는 방법</h3>
<p>UI 시스템과 게임 시스템은 단절되어 있습니다.</p>
<p>Model은 두 시스템을 이어주는 유일한 통로입니다. Presenter는 기능별로 존재하는 Model을 통해 게임 데이터와 이벤트를 알 수 있으며, <strong>프로그래머가 구현한 UI 규칙에 따라 View를 조작합니다.</strong></p>
<p>예시로, <code>InteractionPromptPresenter</code>에는 &quot;인터렉션이 가능한 상태에 Prompt를 활성화한다&quot;라는 규칙을 실현하기 위해, 플레이어의 상호작용 정보를 제공하는 <code>IInteractionModel</code>을 참조합니다.</p>
<h3 id="ㅁ-view-클래스의-책임">ㅁ) View 클래스의 책임</h3>
<p>게임 시스템은 Model을 통해 제공받고, UI 규칙은 Presenter에서 구현됩니다.
그렇다면 View의 책임은 무엇일까요?</p>
<p>View에는 그 어떤 로직도 구현할 수 없습니다.
View는 오직 UI 프레젠테이션에 대한 책임만 가지고 있기 때문에, UI를 플레이어에게 보여주는 기능(UI 요소 활성화/비활성화, 애니메이션, HP바 길이 조작, etc.)만 구현됩니다.</p>
<p>이 구조에서도 마찬가지로, <code>InteractionPromptView</code>에는 Prompt를 Show/Hide하는 기능만 구현되었습니다.</p>
<p>.
.</p>
<h2 id="5-최종-설정-unity">5) 최종 설정 (Unity)</h2>
<h3 id="ㄱ-hierarchy">ㄱ) Hierarchy</h3>
<p><img src="https://velog.velcdn.com/images/jinyj_2008/post/daafbaca-2677-4553-b90f-6dedc2ce8676/image.png" alt=""></p>
<figcaption style="text-align:center; font-size:15px; color:#808080; margin-top:-30px; margin-bottom:40px">오늘 개발한 UI는 "InteractionPrompt"입니다.</figcaption>

<p>루트 오브젝트인 Canvas 아래에 복수의 메뉴 또는 HUD를 조합해서 사용하고 있습니다.
개발이 진행되며 UI 오브젝트의 위치는 얼마든지 변경될 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/jinyj_2008/post/25ed4703-a7c5-4e55-a62f-f2e012060b7c/image.png" alt=""></p>
<figcaption style="text-align:center; font-size:15px; color:#808080; margin-top:-30px; margin-bottom:40px">오늘 개발한 Model은 "InteractionModel"입니다.</figcaption>

<p>루트 오브젝트인 Models 아래에 복수의 Model을 배치하여 사용하고 있습니다.</p>
<h3 id="ㄴ-inspector">ㄴ) Inspector</h3>
<p><img src="https://velog.velcdn.com/images/jinyj_2008/post/188cfc04-633c-40b2-b803-85fc1a33529f/image.png" alt=""></p>
<p>UI 오브젝트에는 Presenter와 View가 포함됩니다.
Presenter는 위 &quot;Models&quot; 오브젝트 하위의 Model들을 필요한 만큼 참조합니다.</p>
<h3 id="ㄷ-unity-시연">ㄷ) Unity 시연</h3>
<p>!youtube[X4zK65QVJ3I]</p>
<p>.
.</p>
<h1 id="마-마무리">마. 마무리</h1>
<hr>
<h2 id="1-꼭-기억해야-할-것들">1) 꼭 기억해야 할 것들</h2>
<ul>
<li><p><strong>Model-View-Presenter (MVP) 패턴</strong> : UI 시스템과 게임 시스템은 분리되어야 하며, UI 시스템의 계층을 Model, Presenter, View로 나누어 이를 실현할 수 있습니다.</p>
</li>
<li><p><strong>기능별 Model 분리</strong> : 자신이 담당한 기능만을 제공하는 각 Model을 필요에 따라 사용하는 구조를 통해 보일러 플레이트를 최소화할 수 있습니다.</p>
</li>
<li><p><strong>Presenter의 책임</strong> : Presenter에는 UI 규칙이 구현됩니다.</p>
</li>
</ul>
<h2 id="2-조언">2) 조언</h2>
<ul>
<li><p>Model-View-Presenter 패턴을 정석에 가깝게 사용하여 UI 시스템을 개발해 보았습니다.
하지만 프로젝트마다 개발 환경은 다르고, 따라서 필요하다면 디자인 패턴 또는 아키텍처를 과감하게 변형하는 유연한 태도도 필수입니다.</p>
</li>
<li><p>궁극적인 시스템 아키텍처는 결국 개발자님이 구조해야 합니다.
문제가 발생했을 때 다양한 디자인 패턴을 사용해본 경험을 바탕으로 이를 혼합하여 문제를 해결하는 능력이 프로그래머의 문제 해결 능력입니다. 은총알(Silver bullet)은 존재하지 않고, 정형화된 &#39;패턴&#39; 중에서는 더더욱 존재하지 않습니다.</p>
</li>
</ul>
<p>.
.
조금이나마 도움이 되었으면 좋겠습니다.
궁금하신 점이 있으면 댓글로 물어봐주세요. 답변해 드리겠습니다!</p>
<p><img src="https://velog.velcdn.com/images/jinyj_2008/post/4b8d1b50-a7e3-4531-9e60-48da24c0b748/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Unity 실무 프로그래밍 #1 - 프로젝트 소개 & 플레이어 기초 개발]]></title>
            <link>https://velog.io/@jinyj_2008/Unity-%EC%8B%A4%EB%AC%B4-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-1-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%86%8C%EA%B0%9C-%ED%94%8C%EB%A0%88%EC%9D%B4%EC%96%B4-%EA%B8%B0%EC%B4%88-%EA%B0%9C%EB%B0%9C</link>
            <guid>https://velog.io/@jinyj_2008/Unity-%EC%8B%A4%EB%AC%B4-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-1-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%86%8C%EA%B0%9C-%ED%94%8C%EB%A0%88%EC%9D%B4%EC%96%B4-%EA%B8%B0%EC%B4%88-%EA%B0%9C%EB%B0%9C</guid>
            <pubDate>Wed, 24 Dec 2025 17:20:10 GMT</pubDate>
            <description><![CDATA[<h1 id="가-프로젝트-소개">가. 프로젝트 소개</h1>
<hr>
<p>개발하는 게임의 장르는 <strong>스테이지형 3D 어드벤처</strong>입니다.
Unity 및 C#을 기반으로 개발됩니다.</p>
<blockquote>
<p>Unity, C#, 객체 지향에 대한 기초적 이해가 있으신 분들을 위한 시리즈입니다!!</p>
</blockquote>
<p>.
.</p>
<h1 id="나-프로젝트-목적">나. 프로젝트 목적</h1>
<hr>
<blockquote>
<p>다양한 시스템 아키텍처 및 디자인 패턴을 바탕으로,
<strong>실무적 효용성*을 구체적으로 고려한 코드를 생산</strong>하는 것이 목적입니다.</p>
</blockquote>
<p>*다만 실무에서는 프로젝트의 요구 사항, 제약 조건 등의 환경 변수가 작용하므로 본 프로젝트의 코드 또한  은총알(Silver bullet)은 아니라는 것을 초보자분들은 염두해 주시길 바랍니다.</p>
<blockquote>
<p>본 프로젝트에서 생산되는 모든 코드의 설계 근거를 자세하고 정성을 들여 설명하고자 합니다.</p>
</blockquote>
<blockquote>
<p>게임 개발의 심화 단계에 접어들며 공부에 참고할 자료의 양이 극히 줄어드는 문제를 겪는 분들이 많고 저 또한 그런 경험이 있어, 같은 문제를 겪고 계신 분들을 돕고자 시리즈를 진행합니다.</p>
</blockquote>
<p>.
.</p>
<h1 id="다-project-세팅-및-플레이어-기초-개발">다. Project 세팅 및 플레이어 기초 개발</h1>
<p>제가 생성한 Unity Project의 정보는 다음과 같습니다.
본 시리즈를 읽으실 정도의 개발자님이라면 이미 알고 계시겠지만, Project 버전은 <strong>프로그래밍</strong>과 크게 상관 없습니다.</p>
<ul>
<li>Unity 2022.3.62f3 (프로젝트 생성일{25.12.25} 기준 최신 버전*)</li>
<li>Universial Render Pipeline (URP)</li>
</ul>
<p>*Unity5 기준. (프로그래밍에만 집중하기 위해 사용)</p>
<h2 id="1-scripts-폴더-구조">1) Scripts 폴더 구조</h2>
<pre><code>Scripts
├── Components
│   └── Player
│       ├── InputProvider
│       │   ├── IPlayerInputProvider.cs
│       │   └── InputManagerBasedInputProvider.cs
│       ├── Locomotion
│       │   ├── ILocomotionStateProvider.cs
│       │   └── PlayerLocomotionManager.cs
│       ├── PlayerCameraManager.cs
│       └── PlayerRotationManager.cs
├── Core
│   └── Attributes
│       └── RequireImplementAttribute.cs
└── Editor
    └── PropertyDrawer
        └── RequireImplementDrawer.cs</code></pre><h3 id="ㄱ-디렉토리-설명">ㄱ) 디렉토리 설명</h3>
<ul>
<li><p><strong>/Components : 특정 오브젝트에 기능을 부여하기 위한 Component들을 위한 디렉토리입니다.</strong>
(디렉토리명의 &quot;Component&quot;는 Unity 개발의 컴포넌트(Component)와는 다른 개념입니다. 앞으로 Unity Engine의 컴포넌트는 &quot;Component&quot;, 모듈과 유사한 개념의 컴포넌트는 &quot;컴포넌트&quot;라고 작성하겠습니다.)
(RPG 게임이라면 Player, Enemy, 자동차 게임이라면 Vehicle 등이 이 디렉토리에 포함될 수 있습니다.)</p>
</li>
<li><p><strong>/Core : 프로젝트의 모든 코드가 사용할 수 있는 코드를 위한 디렉토리입니다.</strong>
(편의를 위한 기능을 제공하는 Utility 클래스 또는 기반 코드(ex. Singleton) 등이 이 디렉토리 포함될 수 있습니다.)</p>
</li>
<li><p><strong>/Editor : Unity Editor 환경에서만 사용될 코드를 위한 디렉토리입니다.</strong>
(Editor 폴더는 Unity의 특수 폴더 중 하나로, Editor 폴더의 모든 스크립트는 에디터 스크립트로 간주되어 런타임(실행 환경)에서 사용될 수 없습니다.)</p>
</li>
</ul>
<h3 id="ㄴ-스크립트-간단-설명">ㄴ) 스크립트 간단 설명</h3>
<ul>
<li><p><strong>IPlayerInputProvider</strong> : 입력 정보를 제공하는 인터페이스</p>
</li>
<li><p><strong>InputManagerBasedInputProvider</strong> : <code>IPlayerInputProvider</code>를 구현한 구현체(Input Manager를 기반으로 입력 정보를 제공)</p>
</li>
<li><p><strong>ILocomotionStateProvider</strong> : 플레이어 캐릭터의 이동 관련 상태를 제공하는 인터페이스</p>
</li>
<li><p><strong>PlayerLocomotionManager</strong> : 플레이어 캐릭터의 이동 기능을 제공. 또한, <code>ILocomotionStateProvider</code>를 구현.</p>
</li>
<li><p><strong>PlayerCameraManager</strong> : 플레이어 캐릭터를 중심으로 한 카메라 제어 기능을 제공</p>
</li>
<li><p><strong>PlayerRotationManager</strong> : 플레이어 캐릭터의 회전 제어 기능을 제공</p>
</li>
<li><p><strong>RequireImplementAttribute</strong> : 편의성 클래스. 본 Attribute를 통해 Inspector의 특정 Object 타입 필드에 인터페이스 구현을 강제할 수 있음</p>
</li>
<li><p><strong>RequireImplementDrawer</strong> : <code>RequireImplementAttribute</code>의 실제 로직 및 Inspector UI 제공 기능이 포함</p>
</li>
</ul>
<p>.
.</p>
<h2 id="2-iplayerinputprovider--inputmanagerbasedinputprovider">2) IPlayerInputProvider &amp; InputManagerBasedInputProvider</h2>
<h3 id="ㄱ-iplayeriputprovider">ㄱ) IPlayerIputProvider</h3>
<pre><code class="language-csharp">using System;
using UnityEngine;

public interface IPlayerInputProvider
{
    event Action OnRotated;

    event Action OnMoved;

    Vector2 RotationDirection { get; }

    Vector2 MoveDirection { get; }

    bool IsSprinting { get; }
}</code></pre>
<h3 id="ㄴ-inputmanagerbasedinputprovider">ㄴ) InputManagerBasedInputProvider</h3>
<pre><code class="language-csharp">using System;
using UnityEngine;

public class InputManagerBasedInputProvider : MonoBehaviour, IPlayerInputProvider
{
    public event Action OnRotated;

    public event Action OnMoved;

    public Vector2 RotationDirection { get; private set; }

    public Vector2 MoveDirection { get; private set; }

    public bool IsSprinting { get; private set; }

    private void UpdateRotationInput()
    {
        float x = Input.GetAxisRaw(&quot;Mouse X&quot;);
        float y = Input.GetAxisRaw(&quot;Mouse Y&quot;);
        RotationDirection = new Vector2(x, y);
        if (RotationDirection.magnitude != 0)
        {
            onRotated?.Invoke();
        }
    }

    private void UpdateMoveInput()
    { 
        float horiz = Input.GetAxisRaw(&quot;Horizontal&quot;);
        float vert = Input.GetAxisRaw(&quot;Vertical&quot;);
        MoveDirection = new Vector2(horiz, vert).normalized;
        if (MoveDirection.magnitude != 0)
        {
            onMoved?.Invoke();
        }
    }

    private void UpdateSprintInput()
    {
        IsSprinting = Input.GetKey(KeyCode.LeftShift) &amp;&amp; MoveDirection.magnitude != 0;
    }

    private void Update()
    {
        UpdateRotationInput();
        UpdateMoveInput();
        UpdateSprintInput();
    }
}</code></pre>
<h3 id="ㄷ-iplayerinputprovider는-왜-만들었는가">ㄷ) IPlayerInputProvider는 왜 만들었는가?</h3>
<ul>
<li>본 프로젝트는 빠른 개발을 위해 MVP* 위주로 개발되고 있습니다.</li>
<li>따라서 간편하게 사용할 수 있는 Input Manager를 사용하되, 이후 입력 시스템의 교체를 염두하여 인터페이스를 통해 입력 관련 코드를 추상화하였습니다. </li>
</ul>
<h3 id="ㄹ-facade-패턴퍼사드-패턴이란">ㄹ) Facade 패턴(퍼사드 패턴)이란?</h3>
<ul>
<li>기능을 인터페이스를 통해 추상화하고, 계약을 이행하는 구현체(여기서는 <code>InputManagerBasedInputProvider</code>)를 통해 계약한 기능을 실현하는 디자인 패턴을 Facade 패턴이라고 합니다.</li>
<li>이 패턴은 단순해 보이지만 <strong>기술 부채를 예방하기 위한 가장 중요한 전략</strong> 중 하나로, Hexagonal Architecture의 Port-Adapter 구조 등에서 응용되기도 합니다.</li>
</ul>
<p>.
.</p>
<h2 id="3-ilocomotionstateprovider--playerlocomotionmanager">3) ILocomotionStateProvider &amp; PlayerLocomotionManager</h2>
<h3 id="ㄱ-ilocomotionstateprovider">ㄱ) ILocomotionStateProvider</h3>
<pre><code class="language-csharp">using UnityEngine;

namespace Player.Components.Player
{

    public interface ILocomotionStateProvider
    {
        public bool IsMoving { get; }

        public Vector3 moveWorldDirection { get; }

        public bool isSprinting { get; }
    }

}</code></pre>
<h3 id="ㄴ-playerlocomotionmanager">ㄴ) PlayerLocomotionManager</h3>
<pre><code class="language-csharp">using Core.Attributes;
using UnityEngine;

namespace Player.Components.Player
{

    public class PlayerLocomotionManager : MonoBehaviour, ILocomotionStateProvider
    {
        public bool IsMoving { get; private set; }

        public Vector3 moveWorldDirection { get; private set; }

        public bool isSprinting { get; }

        [SerializeField, RequireImplement(typeof(IPlayerInputProvider))]
        private Object _inputProvider;

        [SerializeField]
        private Transform _rotationSource;

        public Transform RotationSource =&gt; _rotationSource;

        public float MoveSpeed = 2f;

        public float SprintingSpeedMultiplier = 2f;

        private void Update()
        {
            var inputProvider = _inputProvider as IPlayerInputProvider;
            if (inputProvider == null) return;

            if (_rotationSource == null) return;

            Vector3 inputToLocal = new Vector3(
                inputProvider.MoveDirection.x,
                0,
                inputProvider.MoveDirection.y
            );
            Vector3 parallelForward = _rotationSource.forward;
            parallelForward.y = 0;
            parallelForward.Normalize();
            moveWorldDirection = Quaternion.LookRotation(parallelForward) * inputToLocal;

            Vector3 moveVector = moveWorldDirection * (Time.deltaTime * MoveSpeed);
            if (inputProvider.IsSprinting) moveVector *= SprintingSpeedMultiplier;
            transform.position += moveVector;

            IsMoving = moveVector.magnitude != 0;
        }
    }

}</code></pre>
<h3 id="ㄷ-playerlocomotionmanager란-무엇인가">ㄷ) PlayerLocomotionManager란 무엇인가?</h3>
<ul>
<li><code>PlayerLocomotionManager</code>는 플레이어 캐릭터의 이동을 책임지는 클래스입니다. 걷기, 달리기 뿐만이 아니라, 점프나 기어가기 등의 기능의 일부*를 책임질 수 있습니다.</li>
</ul>
<p>*일부라고 설명한 이유는, 걷기 등의 기능에 이동(=위치 변화) 뿐만이 아니라 애니메이션, 콜라이더 제어 등의 조작이 필요할 수 있기 때문입니다.</p>
<h3 id="ㄹ-관계-1---iinputprivoder-인터페이스를-통한-추상화">ㄹ) 관계 #1 - IInputPrivoder (인터페이스를 통한 추상화)</h3>
<ul>
<li><p>본 클래스는 <code>RequireImplement</code> Attribute를 사용하여 <code>IInputProvider</code>의 구현체를 참조하고 있습니다.</p>
</li>
<li><p>인터페이스의 특성에 따라, <code>PlayerLocomotionManager</code>는 자신에게 입력 정보를 제공하는 클래스가 Input Manager를 사용하는지, New Input System을 사용하는지 등의 내부적인 기술 정보를 알지 못합니다.</p>
</li>
</ul>
<h3 id="ㅁ-관계-2---ilocomotionstateprivoder-다중-상속-시-클래스의-책임-범위는-어떻게-설정되는가">ㅁ) 관계 #2 - ILocomotionStatePrivoder (다중 상속 시 클래스의 책임 범위는 어떻게 설정되는가?)</h3>
<ul>
<li><p>본 클래스는 <code>ILocomotionStateProvider</code>를 구현합니다.</p>
</li>
<li><p>인터페이스의 가장 큰 장점 중 하나는 이미 특정 클래스를 상속한 클래스도 인터페이스를 구현할 수 있다는 것이며, 따라서 인터페이스는 <strong>하나의 클래스에 여러 책임을 설정할 수 있는 가장 합리적인 방법</strong>입니다.</p>
</li>
<li><p>이에 따라 &#39;플레이어 캐릭터의 이동&#39; 및 &#39;이동 상태 제공&#39;이라는 2가지 책임을 가집니다.</p>
</li>
</ul>
<p>.
.</p>
<h2 id="4-playercameramanager">4) PlayerCameraManager</h2>
<pre><code class="language-csharp">using Core.Attributes;
using UnityEngine;

namespace Player.Components.Player
{

    public class PlayerCameraManager : MonoBehaviour
    {
        [SerializeField, RequireImplement(typeof(IPlayerInputProvider))]
        private Object _inputProvider;

        [SerializeField]
        private Camera _camera;

        public Camera Camera =&gt; _camera;

        [SerializeField]
        private Transform _pivot;

        public Transform Pivot =&gt; _pivot;

        public float CameraDistance = 3f;

        public float RotationSpeed = 1f;

        private void Update()
        {
            var inputProvider = _inputProvider as IPlayerInputProvider;
            if (inputProvider == null) return;

            if (_camera == null) return;

            Vector3 rotation = new Vector3(
                -inputProvider.RotationDirection.y,
                inputProvider.RotationDirection.x
            ) * RotationSpeed;
            Vector3 angle = _camera.transform.eulerAngles + rotation;

            // Lock the Z axis rotation
            // And limit the X axis rotation between -89 and 89 degrees
            float xAngle = angle.x;
            if (xAngle is &gt; 89f and &lt; 180f)
            {
                xAngle = 89f;
            }
            else if (xAngle is &gt; 180f and &lt; 271f)
            {
                xAngle = 271f;
            }
            _camera.transform.rotation = Quaternion.Euler(
                xAngle,
                angle.y,
                0
            );

            if (_pivot == null) return;

            Vector3 position = _camera.transform.rotation * (Vector3.back * CameraDistance);
            _camera.transform.position = position + _pivot.position;
        }
    }

}</code></pre>
<p>.
.</p>
<h2 id="5-playerrotationmanager">5) PlayerRotationManager</h2>
<pre><code class="language-csharp">using Core.Attributes;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.Serialization;

namespace Player.Components.Player
{

    public class PlayerRotationManager : MonoBehaviour
    {
        [SerializeField, RequireImplement(typeof(ILocomotionStateProvider))]
        private Object _locomotionStateProvider;

        [SerializeField]
        private Transform _dampedRotationFollower;

        public Transform DampedRotationFollower =&gt; _dampedRotationFollower;

        public float DampingSpeed = 180f;

        [SerializeField, Header(&quot;Optional&quot;)]
        private Transform _rawRotationFollower;

        public Transform RawRotationFollower =&gt; _rawRotationFollower;

        public void Update()
        {
            var locomotionStateProvider = _locomotionStateProvider as ILocomotionStateProvider;
            if (locomotionStateProvider == null) return;

            if (_dampedRotationFollower == null) return;

            Vector3 parallelSourceDirection = locomotionStateProvider.moveWorldDirection;
            parallelSourceDirection.y = 0;
            parallelSourceDirection.Normalize();

            Vector3 parallelFollowerDirection = _dampedRotationFollower.forward;
            parallelFollowerDirection.y = 0;
            parallelFollowerDirection.Normalize();

            if (Vector3.Angle(parallelSourceDirection, parallelFollowerDirection) &gt; 0.1f)
            {
                Quaternion sourceRotation = Quaternion.LookRotation(parallelSourceDirection);
                Quaternion followerRotation = Quaternion.LookRotation(parallelFollowerDirection);

                _dampedRotationFollower.rotation = Quaternion.RotateTowards(
                    followerRotation,
                    sourceRotation,
                    DampingSpeed * Time.deltaTime
                );

                if (_rawRotationFollower != null)
                {
                    _rawRotationFollower.rotation = sourceRotation;
                }
            }
        }
    }

}</code></pre>
<p>.
.</p>
<h2 id="6-requireimplementattribute--requireimplementdrawer">6) RequireImplementAttribute &amp; RequireImplementDrawer</h2>
<h3 id="ㄱ-requireimplementattribute">ㄱ) RequireImplementAttribute</h3>
<pre><code class="language-csharp">using System;
using UnityEngine;

namespace Core.Attributes
{

    public class RequireImplementAttribute : PropertyAttribute
    {
        public Type type;

        public RequireImplementAttribute(Type type)
        {
            this.type = type;
        }
    }

}</code></pre>
<h3 id="ㄴ-requireimplementdrawer">ㄴ) RequireImplementDrawer</h3>
<pre><code class="language-csharp">using Core.Attributes;
using UnityEditor;
using UnityEngine;

namespace Editor.PropertyDrawer
{

    [CustomPropertyDrawer(typeof(RequireImplementAttribute))]
    public class RequireInterfaceDrawer : UnityEditor.PropertyDrawer
    {
        public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
        {
            var attr = (RequireImplementAttribute)attribute;
            var old = property.objectReferenceValue;

            EditorGUI.BeginProperty(position, label, property);

            var obj = EditorGUI.ObjectField(position, label, old, typeof(Object), true);

            if (obj == null)
            {
                property.objectReferenceValue = null;
            }
            else if (attr.type.IsAssignableFrom(obj.GetType()))
            {
                property.objectReferenceValue = obj;
            }
            else
            {
                property.objectReferenceValue = old;
                Debug.LogWarning($&quot;Object must implement {attr.type.Name}.&quot;);
            }

            EditorGUI.EndProperty();
        }
    }

}</code></pre>
<p>.
.</p>
<h2 id="7-player-설정">7) Player 설정</h2>
<h3 id="ㄱ-hierarchy-gameobject-설정">ㄱ) Hierarchy (GameObject 설정)</h3>
<p><img src="https://velog.velcdn.com/images/jinyj_2008/post/ce699ed1-dd28-4595-a4d1-2e4d199c62de/image.png" alt=""></p>
<h3 id="ㄴ-inspector-component별-설정">ㄴ) Inspector (Component별 설정)</h3>
<p><img src="https://velog.velcdn.com/images/jinyj_2008/post/ceafb6cd-4e16-4969-a566-d1d41afa4ae9/image.png" alt=""></p>
<h3 id="ㄷ-시연-영상">ㄷ) 시연 영상</h3>
<p><a href="https://www.youtube.com/watch?v=LqQL0iESjqg">Youtube URL</a>
!youtube[LqQL0iESjqg]</p>
<p>.
.</p>
<h1 id="라-마무리">라. 마무리</h1>
<h2 id="1-꼭-기억해야-할-것들">1) 꼭 기억해야 할 것들</h2>
<ul>
<li><p>Facade 패턴 : 기술 부채 방지를 위한 가장 효과적인 전략이라고 장담합니다.</p>
</li>
<li><p>인터페이스 사용 전략 : 기술 추상화, 다중 상속을 통한 책임 범위 관리 등의 전략은 매우 중요합니다!</p>
</li>
</ul>
<h2 id="2-조언">2) 조언</h2>
<ul>
<li><p>게임 프로그래밍 역량을 키우기 위해서는 다양한 디자인 패턴과 전략에 대한 이해가 필수입니다.</p>
</li>
<li><p>요구 사항과 제약 조건 등의 상황을 정확히 고려하고, 알고 있는 디자인 패턴을 적절히 적용하는 것이 좋은 설계의 핵심입니다.</p>
</li>
<li><p>넓고 깊은 경험만이 답입니다.
(저는 Untiy Engine 기반 게임 개발 및 VR 개발을 배운 뒤, 백엔드 개발 또한 전공하여 활발하게 프로젝트를 진행 중입니다.)
(<strong>저는 게임 개발에 대한 디자인 패턴을 어느 정도 숙련한 뒤 백엔드 공부를 통해 새로운 디자인 패턴들을 접하게 된 것이 성장에 큰 도움이 되었습니다. 백엔드 개발은 강추입니다.</strong>)</p>
</li>
</ul>
<p>.
.
조금이나마 도움이 되었으면 좋겠습니다.
궁금하신 점이 있으면 댓글로 물어봐주세요. 정성을 다해 답변해 드리겠습니다!</p>
]]></description>
        </item>
    </channel>
</rss>