<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>ye-seong.log</title>
        <link>https://velog.io/</link>
        <description>Unreal Engine &amp; Unity 게임 개발자</description>
        <lastBuildDate>Tue, 05 Aug 2025 11:37:24 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>ye-seong.log</title>
            <url>https://velog.velcdn.com/images/ye-seong/profile/5ccb055c-730b-4b50-a809-7c4cf7a8b49d/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. ye-seong.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/ye-seong" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Unity / C#] 세이브&로드, 빌드, 로그 뷰어 활용]]></title>
            <link>https://velog.io/@ye-seong/Unity-C-%EC%84%B8%EC%9D%B4%EB%B8%8C-%EB%A1%9C%EB%93%9C</link>
            <guid>https://velog.io/@ye-seong/Unity-C-%EC%84%B8%EC%9D%B4%EB%B8%8C-%EB%A1%9C%EB%93%9C</guid>
            <pubDate>Tue, 05 Aug 2025 11:37:24 GMT</pubDate>
            <description><![CDATA[<h2 id="📋-목차">📋 목차</h2>
<ol>
<li>메뉴화면</li>
<li>세이브</li>
<li>로드</li>
<li>빌드 앤 런</li>
<li>최종결과</li>
<li>오늘의 배운 점</li>
<li>다음 계획</li>
</ol>
<hr>
<h3 id="🖥️-메뉴화면">🖥️ 메뉴화면</h3>
<p><code>처음부터</code> <code>이어하기</code> <code>설정</code> 메뉴가 있는 <strong>메뉴화면</strong>을 제작합시다. <strong>MenuScene</strong>이라는 이름으로 Project 창에서 씬을 제작해주세요.</p>
<h4 id="1-ui-제작">1. UI 제작</h4>
<img src=https://velog.velcdn.com/images/ye-seong/post/ec67d1d4-ddb7-4bdb-830c-4cf230ba9875/image.png width="700">

<h4 id="2-씬전환">2. 씬전환</h4>
<p><strong>시작하기</strong>를 누르면 게임으로 들어가게 하겠습니다. <code>MenuManager</code> 스크립트와 빈 오브젝트를 생성하고, 오브젝트 컴포넌트로 넣습니다.</p>
<pre><code class="language-csharp">// MenuManager.cs

 public void StartGame()
 {
     SceneManager.LoadScene(&quot;MainScene&quot;);
 }</code></pre>
<p> 해당 버튼 오브젝트의 Inspector에서 Button의 On Click()에 MenuManager 오브젝트를 넣고, <code>StartGame</code> 함수로 설정해주세요.</p>
<p> 그리고 <strong>File</strong> -&gt; <strong>Build Settings</strong> 에서 메뉴와 메인씬을 <strong>Add Open Scenes</strong>로 추가합시다. 그러면 씬으로 등록되고, 전환이 가능하게 됩니다.</p>
 <img src=https://velog.velcdn.com/images/ye-seong/post/844b3d90-3d10-4d93-a482-64bc577e2dd5/image.png width="500">

<h4 id="3-로딩-중-표시">3. 로딩 중 표시</h4>
<p>게임에서 보면 시작하기를 한 후, <code>로딩 중...</code>이 뜨며 현재 몇 퍼센트인지 알 수 있습니다. 조금 더 구현해볼까요?
AsyncOperation 클래스를 이용하시면 됩니다. 해당 클래스에 대한 설명은 이 <a href="https://velog.io/@ye-seong/Unity-C-AsyncOperation">게시글</a>을 참고해주세요.</p>
<pre><code class="language-csharp">// MenuManager.cs

public GameObject loadingPanel;
public Text loadingText; 

public void StartGame()
{
    StartCoroutine(LoadGameScene());
}

IEnumerator LoadGameScene()
{
    loadingPanel.SetActive(true);

    AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(&quot;MainScene&quot;);

    while (!asyncLoad.isDone)
    {
        float progress = asyncLoad.progress;
        Debug.Log(loadingText);
        loadingText.text = $&quot;로딩 중... {progress * 100:F0}%&quot;; 
        yield return null;
    }
}</code></pre>
<h4 id="4-테스트">4. 테스트</h4>
<p>성공적으로 로딩이 완료됩니다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/bf147d7d-a2d9-45a0-9ef3-28581dc47b9b/image.gif width="700">

<hr>
<h3 id="✅-세이브">✅ 세이브</h3>
<p>게임을 저장하면 해당 데이터가 필요합니다. 저장할 데이터를 정리해봅시다. <code>SaveData</code> 스크립트를 만들어볼까요?</p>
<h4 id="1-savedata">1. SaveData</h4>
<p>일단 저장할 데이터를 간단하게 정리합시다. 플레이어 현재 상태 및 아이템, 그리고 맵 상태를 저장합시다.</p>
<pre><code class="language-csharp">// SaveData.cs

[System.Serializable]
public class SavePlayerData
{
    public PlayerStats playerStats;
    public Vector3 playerPosition;
    public Quaternion playerRotation;

    public SavedItemInstance[] inventoryItems;
    public SavedItemInstance[] equipmentItems;

    public bool[] unlockedItems;
    public int[] quickSlotIndexs;
    public int currentHandleItemIndex;

    public bool isOperate;
    public DateTime currentTime;
}

[System.Serializable]
public class SaveMapData
{
    public SavedItemInstance[] mapItems;
    public int semaphoreNumber;
}
</code></pre>
<h4 id="2-savemanager">2. SaveManager</h4>
<p>저장, 로드를 담당할 스크립트를 생성합니다.</p>
<pre><code class="language-csharp">// SaveManager.cs

public class SaveManager : MonoBehaviour
{
    [SerializeField] private PlayerState playerState;
    [SerializeField] private PlayerController playerController;
    [SerializeField] private GameManager gameManager;
    [SerializeField] private UIManager uiManager;
    private string savePlayerPath = &quot;savePlayerData.json&quot;;
    private string saveMapPath = &quot;saveMapData.json&quot;;


    public void GoMenuScene()
    {
        Debug.Log(&quot;메인 메뉴로 이동합니다.&quot;);
        Time.timeScale = 1f;
        playerController.isPaused = false;
        GameState.IsUIOpen = false;
        SceneManager.LoadScene(&quot;MenuScene&quot;);
    }
    public virtual void SaveGame()
    {
        Debug.Log(&quot;게임 저장 시작!&quot;);
        SavePlayerData savePlayerData = CollectAllPlayerData();
        SaveMapData saveMapData = CollectAllMapData();

        string json = JsonUtility.ToJson(savePlayerData, true);
        File.WriteAllText(savePlayerPath, json);
        json = JsonUtility.ToJson(saveMapData, true);
        File.WriteAllText(saveMapPath, json);

        Debug.Log(&quot;게임 저장 완료!&quot;);
    }

    private SavePlayerData CollectAllPlayerData()
    {
        SavePlayerData saveData = new SavePlayerData();
        if (playerState)
        {
            saveData.playerStats = playerState.stats;
            saveData.playerPosition = playerState.transform.position;
            saveData.playerRotation = playerState.transform.rotation;
            saveData.quickSlotIndexs = playerState.quickSlotIndexs;
            saveData.currentHandleItemIndex = uiManager.quickSlotPanel.GetComponent&lt;QuickPanel&gt;().currentSlotIndex;

            // equipmentItems 배열 초기화
            saveData.equipmentItems = new SavedItemInstance[playerState.equipmentItems.Length];
            for (int i = 0; i &lt; playerState.equipmentItems.Length; i++)
            {
                if (playerState.equipmentItems[i])
                {
                    saveData.equipmentItems[i] = playerState.equipmentItems[i].ToSaveData();
                }
                else
                {
                    saveData.equipmentItems[i] = null;
                }
            }

            saveData.inventoryItems = new SavedItemInstance[playerState.inventoryItems.Length];
            for (int i = 0; i &lt; playerState.inventoryItems.Length; i++) 
            {
                if (playerState.inventoryItems[i])
                {
                    saveData.inventoryItems[i] = playerState.inventoryItems[i].ToSaveData();
                }
                else
                {
                    saveData.inventoryItems[i] = null;
                }
            }

            saveData.unlockedItems = playerState.unlockedItems;
            saveData.isOperate = GameState.IsOperate;
            saveData.currentTime = System.DateTime.Now;
        }
        return saveData;
    }

    private SaveMapData CollectAllMapData()
    {
        SaveMapData saveData = new SaveMapData();
        List&lt;SavedItemInstance&gt; worldItems = new List&lt;SavedItemInstance&gt;();

        foreach (ItemInstance item in FindObjectsOfType&lt;ItemInstance&gt;())
        {
            if (item)
            {
                if (item.isWorldItem)
                {
                    worldItems.Add(item.ToSaveData());
                }
            }
        }
        saveData.mapItems = worldItems.ToArray();
        return saveData;
    }

    private ItemData FindItemDataByName(string itemName)
    {
        for (int i = 0; i &lt; gameManager.allItems.Length; i++)
        {
            if (gameManager.allItems[i].itemName == itemName)
            {
                return gameManager.allItems[i];
            }
        }
        return Resources.Load&lt;ItemData&gt;(&quot;Items/&quot; + itemName);
    }

}
</code></pre>
<h4 id="3-esc-후-이어하기-저장하기-나가기">3. Esc 후 이어하기, 저장하기, 나가기</h4>
<p>Canvas에 <code>이어하기</code>, <code>저장하기</code>, <code>나가기</code> 버튼을 추가합니다. 그리고 <strong>Esc</strong>키를 통해 메뉴가 열리고 게임이 멈추게 하겠습니다.</p>
<p><strong>(1) 이어하기</strong></p>
<pre><code class="language-csharp">// UIManager.cs

[HideInInspector] public bool isPaused = false;

public GameObject menuPanel;

public void SetEscMenu()
{
    if (!menuPanel) return;
    menuPanel.SetActive(!menuPanel.activeSelf);
    if (menuPanel.activeSelf)
    {
        Time.timeScale = 0f;
        playerController.isPaused = true;
    }
    else
    {
        Time.timeScale = 1f;
        playerController.isPaused = false;
    }
    SetMouseSate();
}

// 이어하기의 On Click()에 넣기
public void ClickContinueButton()
{
    if (!menuPanel) return;
    menuPanel.SetActive(false);
    Time.timeScale = 1f;
    playerController.isPaused = false;
    SetMouseSate();
}

// PlayerController.cs

void Update()
{
    HandleEsc();
    if (isPaused) return;
}

void HandleEsc()
{
    if (Input.GetKeyDown(KeyCode.Escape))
    {
        uiManager.SetEscMenu();
    }
}</code></pre>
<p><strong>(2) 저장하기</strong>
<code>저장하기 버튼</code>의 <code>On Click</code>에 <code>SaveManager</code>의 <strong>SaveGame()</strong>을 적용해주세요.
그러면 SignalHome 파일에 <code>savePlayerData.json</code>과 <code>saveMapData.json</code>라는 세이브 파일이 생깁니다!</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/ddfa081c-73b9-4a1e-aea3-b65cb6487356/image.png width="800">

<p><strong>(3) 나가기</strong></p>
<pre><code class="language-csharp">// SaveManager.cs

[SerializeField] private PlayerController playerController;

 public void GoMenuScene()
 {
     Debug.Log(&quot;메인 메뉴로 이동합니다.&quot;);
     Time.timeScale = 1f;
     playerController.isPaused = false;
     GameState.IsUIOpen = false;
     SceneManager.LoadScene(&quot;MenuScene&quot;);
 }</code></pre>
<hr>
<h3 id="🔁-로드">🔁 로드</h3>
<p> 저장까진 모두 해냈죠? 그럼 해당 Json파일을 게임 시작 전에 불러와 적용해야 합니다.</p>
<pre><code class="language-csharp"> // SaveManager.cs


public virtual void LoadGame()
{
    if (File.Exists(savePlayerPath))
    {
        string json = File.ReadAllText(savePlayerPath);
        SavePlayerData savePlayerData = JsonUtility.FromJson&lt;SavePlayerData&gt;(json);
        json = File.ReadAllText(saveMapPath);
        SaveMapData saveMapData = JsonUtility.FromJson&lt;SaveMapData&gt;(json);
        ApplyAllGameData(savePlayerData, saveMapData);
        Debug.Log(&quot;게임 불러오기 완료!&quot;);
    }
    else
    {
        Debug.LogWarning(&quot;저장된 게임 데이터가 없습니다. 새 게임을 시작합니다.&quot;);
    }
}

private void ApplyAllGameData(SavePlayerData savePlayerData, SaveMapData saveMapData)
{
    if (playerState)
    {
        playerState.quickSlotIndexs = new int[] { -1, -1, -1, -1, -1 };
        playerState.inventoryItems = new ItemInstance[GameConstants.MAX_INVENTORY_SIZE];
        playerState.stats = savePlayerData.playerStats;
        playerState.transform.position = savePlayerData.playerPosition;
        playerState.transform.rotation = savePlayerData.playerRotation;

        // inventoryItems 복원 (Create 메서드로 통일)
        for (int i = 0; i &lt; savePlayerData.inventoryItems.Length; i++)
        {
            if (savePlayerData.inventoryItems[i] != null)
            {
                ItemData itemData = FindItemDataByName(savePlayerData.inventoryItems[i].itemName);
                if (itemData != null)
                {
                    playerState.inventoryItems[i] = ItemInstance.Create(itemData, savePlayerData.inventoryItems[i]);
                    playerState.inventoryItems[i].gameObject.SetActive(false);
                }
            }
        }
        uiManager.inventory.items = playerState.inventoryItems;
        uiManager.UpdateItemUI();
        // equipmentItems 복원 (Create 메서드로 통일)
        playerState.equipmentItems = new ItemInstance[savePlayerData.equipmentItems.Length];
        for (int i = 0; i &lt; savePlayerData.equipmentItems.Length; i++)
        {
            if (savePlayerData.equipmentItems[i] != null)
            {
                ItemData itemData = FindItemDataByName(savePlayerData.equipmentItems[i].itemName);
                if (itemData != null)
                {
                    playerState.equipmentItems[i] = ItemInstance.Create(itemData, savePlayerData.equipmentItems[i]);
                    playerState.equipmentItems[i].gameObject.SetActive(false);
                }
            }
        }

        playerState.quickSlotIndexs = savePlayerData.quickSlotIndexs;
        uiManager.quickSlotPanel.GetComponent&lt;QuickPanel&gt;().currentSlotIndex = savePlayerData.currentHandleItemIndex;

        uiManager.RefreshQuickSlotItems(playerState.quickSlotIndexs);
        uiManager.RefreshEquipmentItems(playerState.equipmentItems);

        if (uiManager.quickSlotPanel.GetComponent&lt;QuickPanel&gt;().currentSlotIndex &gt;= 0)
        {
            if (playerState.inventoryItems[savePlayerData.currentHandleItemIndex])
            {
                int index = playerState.quickSlotIndexs[savePlayerData.currentHandleItemIndex];
                if (index &gt;= 0)
                {
                    playerState.playerItemHandler.currentItem = playerState.inventoryItems[index];
                    playerState.playerItemHandler.SetHoldingItem(playerState.playerItemHandler.currentItem, playerState.playerItemHandler.rightHandBone);
                }
            }
        }

        foreach (SavedItemInstance savedItem in saveMapData.mapItems)
        {
            if (savedItem != null)
            {
                ItemData itemData = FindItemDataByName(savedItem.itemName);
                if (itemData != null)
                {
                    ItemInstance worldItem = ItemInstance.Create(itemData, savedItem);
                    worldItem.isWorldItem = true;
                    worldItem.gameObject.SetActive(true);
                    worldItem.transform.position = savedItem.position;
                    worldItem.transform.rotation = savedItem.rotation;
                }
            }
        }

        playerState.unlockedItems = savePlayerData.unlockedItems;
        GameState.IsOperate = savePlayerData.isOperate;
        GameState.currentSemaphoreNumber = saveMapData.semaphoreNumber;
        playerState.RefreshUI();
    }
}

// GameManager.cs

private void Start()
{
    // 기존 로직...
     // 게임 데이터가 없을 경우 새로운 세이브 파일을 자동 생성
     if (!File.Exists(saveManager.savePlayerPath))
     {
         saveManager.SaveStart();
     }
     saveManager.LoadGame();
}</code></pre>
<hr>
<h3 id="🛠️-메인메뉴-수정">🛠️ 메인메뉴 수정</h3>
<p>메인메뉴를 더 수정합시다. 아래와 같이 처음부터, 이어하기, 설정, 나가기가 있는 겁니다.</p>
<p><strong>처음부터 :</strong> 현재 세이브 파일이 있는 경우, 새로운 게임을 할 것인지 물어보고 아니라면 바로 새게임을 시작합니다.
<strong>이어하기 :</strong> 현재 세이브 파일이 있는 경우에만 나타납니다.
<strong>설정 :</strong> 소리, 자막 크기 등을 설정할 수 있습니다.
<strong>나가기 :</strong> 게임을 끕니다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/fd29c1bf-d4cb-428c-9ccd-dbdde99ffeb5/image.png width="700">

<pre><code class="language-csharp">// MenuManager.cs

public class MenuManager : MonoBehaviour
{
    public GameObject loadingPanel;
    public GameObject warningPanel;
    public GameObject continueButton;
    public Text loadingText;

    private string savePlayerPath =&gt; Path.Combine(Application.persistentDataPath, &quot;savePlayerData.json&quot;);
    private string saveMapPath =&gt; Path.Combine(Application.persistentDataPath, &quot;saveMapData.json&quot;);
    private string originMapPath =&gt; Path.Combine(Application.streamingAssetsPath, &quot;originMapData.json&quot;);

    private void Start()
    {
        if (!File.Exists(savePlayerPath))
        {
            continueButton.SetActive(false);
        }
    }
    public void StartGame()
    {
        if (File.Exists(savePlayerPath))
        {
            warningPanel.SetActive(true);
        }
        else
        {
            File.Copy(originMapPath, saveMapPath, true);  
            StartCoroutine(LoadGameScene());
        }
    }

    public void ContinueGame()
    {
        StartCoroutine(LoadGameScene());
    }

    public void RestartGame(bool confirm)
    {
        if (confirm)
        {
            warningPanel.SetActive(false);
            File.Delete(savePlayerPath);
            File.Copy(originMapPath, saveMapPath, true);
            StartCoroutine(LoadGameScene());
        }
        else
        {
            warningPanel.SetActive(false);
        }
    }

    IEnumerator LoadGameScene()
    {
        loadingPanel.SetActive(true);

        AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(&quot;MainScene&quot;);

        while (!asyncLoad.isDone)
        {
            float progress = asyncLoad.progress;
            loadingText.text = $&quot;로딩 중... {progress * 100:F0}%&quot;; 
            yield return null;
        }
    }

    public void QuitGame()
    {
        Application.Quit();
    }
}</code></pre>
<hr>
<h3 id="▶️-빌드-앤-런">▶️ 빌드 앤 런</h3>
<p>이제 게임을 실행파일 (exe)로 한 번 뽑아볼까요? </p>
<h4 id="1-원본-맵-데이터">1. 원본 맵 데이터</h4>
<p>이미 <code>saveMapData.json</code>이 있기는 하나, 우리에게는 처음 맵 데이터가 필요합니다. 그것을 유니티 에디터에서 아이템을 배치한뒤, 한 번 세이브 파일이 생기게끔 합시다. 그 다음 해당 프로젝트의 파일탐색기의 <strong>Assets</strong>에 <strong>StreamingAssets</strong>라는 폴더를 생성합니다. <code>saveMapData.Json</code>파일의 이름을 <code>originMapData.json</code>으로 하고 <strong>File -&gt; Build And Run</strong>을 합니다.
전 <strong>SiganlHome_Build</strong>라는 파일을 따로 만들었습니다. 여기서 <code>exe</code>파일이 실행파일입니다!</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/c245d885-81e0-44d2-9822-9995ad00bff2/image.png width="600">

<h4 id="2-테스트-log-viewer-asset">2. 테스트 (Log Viewer Asset)</h4>
<p>테스트를 해보는데, 혹시 자신이 의도한 것과 다르게 나오는 것이 있나요? 저는 에디터에서와 다르게 실행파일에서는 안되는 부분이 있었습니다. 에디터에서는 <code>Debug.Log</code>로 문제점을 파악할 수 있었는데 실행파일에서는 어떻게 할까요?
바로 <a href="https://assetstore.unity.com/packages/tools/integration/log-viewer-12047">Log Viewer</a> 에셋 입니다. 해당 에셋을 다운 받은 후 상단바에 있는 <code>Reporter -&gt; Create</code>를 하여 Hierarchy에 추가해주세요.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/ac9f039d-7eda-486a-b4e5-8dd13538e60b/image.png width="600">

<p>위와 같이 뜰텐데요. 해당 에셋은 <strong>마우스 클릭을 한 채로 동그라미를 그릴 경우</strong> 로그창을 보여주는 에셋입니다. 오류, 경고, 로그가 모두 뜹니다.</p>
<h4 id="3-log-viewer-활용">3. Log Viewer 활용</h4>
<p><code>Build Settings</code>에서 <strong>Develpment Build, Deep Profiling Support, Script Debugging</strong>을 true로 하고 <strong>Compression Method</strong>를 LZ4 (빠른 디버깅을 위함) 로 합시다. 그리고 다시 Build And Run을 해주세요.
실행파일에서 마우스로 동그라미를 그려주면 로그가 뜹니다. 간단하죠?</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/352b5da1-79f6-4a29-a5b2-1521528830f5/image.gif width="700">

<p>해당 로그를 클릭하면 스크립트 오류 위치 등의 정보도 볼 수 있습니다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/7c9f7d7a-2bf7-4929-8ffa-896299e3cebf/image.png width="700">

<p><strong><em>저의 문제는...</em></strong></p>
<p><strong>- Animator가 null인 문제</strong></p>
<ol>
<li>Animator가 없을 경우 <code>GetComponent&lt;Animator&gt;()</code>을 하여 값을 할당함</li>
<li>적용 후 애니메이션이 보임</li>
</ol>
<p><strong>- PlayerState의 playerItemHandler가 null인 문제</strong></p>
<ol>
<li>playerItemHandler가 없을 경우 <code>GetComponent&lt;PlayerItemHandler&gt;()</code>을 하여 값을 할당함</li>
</ol>
<p><strong>- Inventory에 아이템이 할당되지 않은 문제</strong></p>
<ol>
<li>LoadGame을 할때 PlayerState의 inventoryItems값이 Inventory의 items에 제대로 할당되지 않음</li>
<li>items 변수를 삭제, 그리고 모두 PlayerState의 inventoryItems로 대체함 (얕은 복사 및 동기화 활용)</li>
</ol>
<p><em>모두 로드 전 해당 스크립트의 변수값이 할당되어 있지 않아 생긴 문제였습니다.</em></p>
<hr>
<h3 id="🎮-최종결과">🎮 최종결과</h3>
<p>오늘은 <strong>빌드</strong>하고 <strong>실행파일</strong>을 실행 후, <strong>세이브&amp;로드</strong>가 제대로 됐는지 확인하는 작업까지! 완료했네요. 실제 빌드파일이 나오니 정말 게임 같아졌습니다!</p>
<p><a href="https://drive.google.com/file/d/1boLqfjWqQDW_WKTp7lCBlCTcOB6vXqnc/view?usp=sharing">영상 링크 (구글 드라이브)</a></p>
<hr>
<h3 id="📚-오늘의-배운-점">📚 오늘의 배운 점</h3>
<ul>
<li>세이브&amp;로드 방법</li>
<li>빌드 후 exe파일 사용법</li>
<li>Log Viewer 활용</li>
</ul>
<hr>
<h3 id="🎯-다음-계획">🎯 다음 계획</h3>
<p>다음 글에서는:</p>
<ol>
<li>오프닝 연출</li>
<li>자막 생성</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[C#] 개념 - 코딩테스트 대비!]]></title>
            <link>https://velog.io/@ye-seong/C-%EA%B0%9C%EB%85%90-%EC%BD%94%EB%94%A9%ED%85%8C%EC%8A%A4%ED%8A%B8-%EB%8C%80%EB%B9%84</link>
            <guid>https://velog.io/@ye-seong/C-%EA%B0%9C%EB%85%90-%EC%BD%94%EB%94%A9%ED%85%8C%EC%8A%A4%ED%8A%B8-%EB%8C%80%EB%B9%84</guid>
            <pubDate>Fri, 01 Aug 2025 07:31:16 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/ye-seong/post/3f76aa24-b24a-49b1-8ee9-9a974d3b971a/image.png" alt=""></p>
<p>코딩테스트에서 <strong>함수를 잘 몰라서</strong> 혹은 <strong>응용법을 잘 몰라서</strong> 때문에 로직이 복잡해지는 경우가 있죠? 이때 알아두면 좋은 기초 함수를 가져왔습니다.</p>
<hr>
<h3 id="📊-math-수학-관련">📊 Math (수학 관련)</h3>
<pre><code class="language-csharp">// 최댓값, 최솟값
Math.Max(a, b)        // 둘 중 큰 값
Math.Min(a, b)        // 둘 중 작은 값
Math.Max(Math.Max(a, b), c)        // 3개 중 최댓값

// 절댓값, 제곱근
Math.Abs(-5)        // 5 (절댓값)
Math.Sqrt(16)        // 4.0 (제곱근)
Math.Pow(2, 3)        // 8.0 (2의 3제곱)

// 올림, 내림, 반올림
Math.Ceiling(3.2)    // 4.0 (올림)
Math.Floor(3.8)        // 3.0 (내림)
Math.Round(3.6)        // 4.0 (반올림)</code></pre>
<hr>
<h3 id="🔤-string-문자열">🔤 String (문자열)</h3>
<pre><code class="language-csharp">string str = &quot;Hello World&quot;;

// 기본 정보
str.Length                // 11 (길이)
str[0]                    // &#39;H&#39; (인덱스 접근)
str.Contains(&quot;ell&quot;)        // true (포함 여부)

// 변환
str.ToUpper()            // &quot;HELLO WORLD&quot;
str.ToLower()            // &quot;hello world&quot;
str.Replace(&quot;World&quot;, &quot;C#&quot;)        // &quot;Hello C#&quot;

// 분할/결합
str.Split(&#39; &#39;)            // [&quot;Hello&quot;, &quot;World&quot;]
string.Join(&quot;,&quot;, arr)    // 배열을 &quot;,&quot;로 연결

// 부분 문자열
str.Substring(0, 5)        // &quot;Hello&quot; (시작인덱스, 길이)
str.Substring(6)        // &quot;World&quot; (시작인덱스부터 끝까지)

// 찾기
str.IndexOf(&quot;o&quot;)        // 4 (첫 번째 &#39;o&#39;의 인덱스, 없으면 -1)
str.LastIndexOf(&quot;o&quot;)    // 7 (마지막 &#39;o&#39;의 인덱스)</code></pre>
<hr>
<h3 id="📋-array배열">📋 Array(배열)</h3>
<pre><code class="language-csharp">int [] arr = {3, 1, 4, 1, 5};

// 정렬
Array.Sort(arr)            // 오름차순 정렬
Array.Reverse(arr)        // 배열 뒤집기

// 찾기
Array.IndexOf(arr, 4)    // 2 (값 4의 인덱스, 없으면 -1)
Array.Exists(arr, x =&gt; x &gt; 3)    // true (조건 만족하는 요소 존재?)

// 복사
int[] copy = new int[arr.Length];
Array.Copy(arr, copy, arr.Length);</code></pre>
<hr>
<h3 id="📚-list-리스트">📚 List (리스트)</h3>
<pre><code class="language-csharp">List&lt;int&gt; list = new List&lt;int&gt;();

// 추가/삭제
list.Add(1)            // 끝에 추가
list.Insert(0, 5)    // 특정 위치에 삽입
list.Remove(1)         // 첫 번째로 발견되는 1 제거
list.RemoveAt(0)    // 0번 인덱스 제거
list.Clear(0)        // 모든 요소 제거

// 정보
list.Count            // 개수
list.Contains(5)    // 포함 여부
list[0]                // 인덱스 접근

// 정렬/변환
list.Sort()            // 오름차순 정렬
list.Reverse()        // 뒤집기
list.ToArray()        // 배열로 변환</code></pre>
<hr>
<h3 id="🗂️-dictionarykv-해시맵">🗂️ Dictionary&lt;K,V&gt; (해시맵)</h3>
<pre><code class="language-csharp">Dictionary&lt;string, int&gt; dict = new Dictionary&lt;string, int&gt;();

// 추가/수정
dict[&quot;apple&quot;] = 5        // 키-값 추가/수정
dict.Add(&quot;banana&quot;, 3)    // 추가 (키가 이미 있으면 예외)

// 접근/확인
dict[&quot;apple&quot;]            // 5 (값 접근)
dict.ContainsKey(&quot;apple&quot;)    // true (키 존재 여부)
dict.ContainsValue(5)        // true (값 존재 여부)
dict.TryGetValue(&quot;apple&quot;, out int value)    // 안전한 접근

// 정보
dict.Count                // 개수
dict.Keys                // 모든 키
dict.Values                // 모든 값</code></pre>
<hr>
<h3 id="🔄-queue--stack">🔄 Queue &amp; Stack</h3>
<pre><code class="language-csharp">// Queue (FIFO - 먼저 들어간 게 먼저 나옴)
Queue&lt;int&gt; queue = new Queue&lt;int&gt;();
queue.Enqueue(1)        // 큐에 추가
int first = queue.Dequeue()        // 첫 번째 요소 제거하고 반환
int peek = queue.Peek()            // 첫 번째 요소 확인만 (제거 안함)

// Stack (LIFO - 나중에 들어간 게 먼저 나옴)
Stack&lt;int&gt; stack = new Stack&lt;int&gt;();
stack.Push(1)            // 스택에 추가
int top = stack.Pop()    // 맨 위 요소 제거하고 반환
int peek = stack.Peek()    // 맨 위 요소 확인만</code></pre>
<hr>
<h3 id="🎯-linq">🎯 LINQ</h3>
<pre><code class="language-csharp">using System.Linq;

int[] numbers = {1, 2, 3, 4, 5, 6};

// 필터링
numbers.Where(x =&gt; x &gt; 3)        // {4, 5, 6}
numbers.Where(x =&gt; x %2 == 0)    // {2, 4, 6} (짝수만)

// 변환
numbers.Select(x =&gt; x * 2)        // {2, 4, 6, 8, 10, 12}
numbers.Select((x, i) =&gt; x + i)    // 값과 인덱스 사용

// 집계
numbers.Sum()                    // 21 (합계)
numbers.Max()                    // 6 (최댓값)
numbers.Min()                    // 1 (최솟값)
numbers.Average()                // 3.5 (평균)
numbers.Count(x =&gt; x &gt; 3)        // 3 (조건 만족하는 개수)

// 정렬
numbers.OrderBy(x =&gt; x)                // 오름차순
numbers.OrderByDescending(x =&gt; x)    // 내림차순

// 기타 유용한 것들
numbers.Take(3)                        // 처음 3개 {1, 2, 3}
numbers.Skip(2)                        // 처음 2개 건너뛰고 {3, 4, 5, 6}
numbers.First()                        // 1 (첫 번째, 없으면 예외)
numbers.FirstOrDefault()            // 1 (첫 번째, 없으면 기본값)
numbers.Last()                        // 6 (마지막)
numbers.Any(x =&gt; x &gt; 5)                // true (조건 만족하는 게 하나라도 있나?)
numbers.All(x =&gt; x &gt; 0)                // true (모두 조건 만족하나?)
numbers.Distinct()                    // 중복 제거</code></pre>
<hr>
<h3 id="🔢-형변환">🔢 형변환</h3>
<pre><code class="language-csharp">// 문자열 - 숫자
int Parse(&quot;123&quot;)        // 123 (문자열을 int로)
&quot;123&quot;.ToString()        // &quot;123&quot; (int를 문자열로)
Convert.ToInt32(&quot;123&quot;)    // 123 (다른 방법)

// 안전한 변환
int.TryParse(&quot;123&quot;, out int result)        // 변환 성공하면 true

// char - int
&#39;5&#39; - &#39;0&#39;                // 5 (문자를 숫자로)
5 + &#39;0&#39;                    // &#39;5&#39; (숫자를 문자로)</code></pre>
<hr>
<h3 id="🎲-자주-쓰는-패턴들">🎲 자주 쓰는 패턴들</h3>
<pre><code class="language-csharp">// 2차원 배열 순회
for (int i = 0; i &lt; arr.getLength(0); i++)        // 행
    for (int j = 0; j &lt; arr.GetLength(1); j++)    // 열

// 배열 초기화
int[] arr = new int[5];                // {0, 0, 0, 0, 0}
int[] arr = Enumerable.Repeat(1, 5).ToArray();    // {1, 1, 1, 1, 1}
int[] arr = Enumerable.Range(1, 5).ToArray();    // {1, 2, 3, 4, 5}

// 조건부 삼항 연산자
int result = condition ? valueIfTrue : valueIfFalse;

// null 체크 간단히
string result = str?.ToUpper() ?? &quot;DEFAULT&quot;;</code></pre>
<hr>
<p><strong>특히 꼭 외워두면 좋은 것들:</strong></p>
<ul>
<li><code>Math.Max/Min</code> - 비교문 대신 쓰면 훨씬 깔끔</li>
<li><code>LINQ</code>의 <code>Where, Select, Sum, Max</code> - 배열 처리가 한 줄로!</li>
<li><code>dictionary</code> - 해시 문제는 거의 이걸로 해결</li>
<li><code>Queue/Stack</code> = BFS/DFS 문제 필수</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[C#] 알고리즘 - BFS(너비 우선 탐색)]]></title>
            <link>https://velog.io/@ye-seong/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-C-BFS%EB%84%88%EB%B9%84-%EC%9A%B0%EC%84%A0-%ED%83%90%EC%83%89</link>
            <guid>https://velog.io/@ye-seong/%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-C-BFS%EB%84%88%EB%B9%84-%EC%9A%B0%EC%84%A0-%ED%83%90%EC%83%89</guid>
            <pubDate>Thu, 31 Jul 2025 08:36:39 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/ye-seong/post/789b1432-33b9-4694-8771-62814d29d65b/image.png" alt=""></p>
<h3 id="bfs란">BFS란?</h3>
<p><strong>BFS(Breadth-First Search, 너비 우선 탐색)</strong>는 그래프나 트리에서 <strong>가까운 노드부터 차례대로 탐색</strong>하는 알고리즘입니다. 마치 풀이 퍼져나가는 것처럼 <strong>동심원 형태</strong>로 탐색하기 때문에 <strong>최단거리 문제</strong>에 매우 유용합니다.</p>
<hr>
<h3 id="bfs를-사용하는-이유">BFS를 사용하는 이유</h3>
<p><strong>최단거리를 보장</strong>하는 이유입니다. </p>
<ul>
<li>거리 1인 모든 지점을 먼저 탐색</li>
<li>거리 2인 모든 지점을 그 다음에 탐색</li>
<li>거리 3인 모든 지점을 그 다음에 탐색 ...</li>
</ul>
<p>이런 식으로 진행하기 때문에 <strong>처음으로 목표지점에 도달한 순간이 바로 최단거리</strong>입니다!</p>
<hr>
<h3 id="bfs-vs-dfs-비교">BFS vs DFS 비교</h3>
<table>
<thead>
<tr>
<th>구분</th>
<th>BFS</th>
<th>DFS</th>
</tr>
</thead>
<tbody><tr>
<td>탐색 방식</td>
<td>너비 우선 (가까운 것부터)</td>
<td>깊이 우선 (끝까지 파고들기)</td>
</tr>
<tr>
<td>자료구조</td>
<td>Queue (FIFO)</td>
<td>Stack/재귀 (LIFO)</td>
</tr>
<tr>
<td>최단거리</td>
<td>보장</td>
<td>보장 안됨</td>
</tr>
<tr>
<td>메모리 사용량</td>
<td>많음</td>
<td>적음</td>
</tr>
<tr>
<td>사용 예시</td>
<td>최단거리, 레벨별 탐색</td>
<td>경로 존재 여부, 완전 탐색</td>
</tr>
</tbody></table>
<hr>
<h3 id="실전-예제-게임-맵-최단거리">실전 예제: 게임 맵 최단거리</h3>
<p>해당 문제는 <a href="https://school.programmers.co.kr/learn/challenges?order=recent&amp;levels=5%2C2">프로그래머스</a>에서 검색하여 푸시면 됩니다. 먼저 풀고 나서 보시는걸 추천 드립니다!</p>
<h4 id="1-핵심-구성-요소">1. 핵심 구성 요소</h4>
<p><strong>- Queue(큐)</strong></p>
<pre><code class="language-csharp">Queue&lt;(int x, int y, int distance)&gt; queue = new Queue&lt;(int, int, int)&gt;();</code></pre>
<ul>
<li>Enqueue: 뒤에 추가 (줄 서기)</li>
<li>Dequeue: 앞에서 제거 (줄에서 빠지기)</li>
<li>FIFO(First In First Out) 방식으로 동작</li>
</ul>
<p><strong>- Visited 배열</strong></p>
<pre><code class="language-csharp">bool [,] visited = new bool[행, 열];</code></pre>
<ul>
<li>이미 방문한 곳을 다시 방문하지 않도록 체크</li>
<li><strong>무한루프 방지</strong>의 핵심!</li>
</ul>
<p><strong>- 방향 배열</strong></p>
<pre><code class="language-csharp">int[] dx = {-1, 1, 0, 0};
int[] dy = {0, 0, -1, 1};</code></pre>
<ul>
<li>상하좌우 4방향 이동을 간단하게 처리</li>
</ul>
<h4 id="2-완성된-코드">2. 완성된 코드</h4>
<pre><code class="language-csharp">using System;
using System.Collections.Generic;

class Solution {
    public int solution(int[,] maps) {
        int n = maps.GetLength(0);  // 행의 개수
        int m = maps.GetLength(1);  // 열의 개수

        // 방문 체크 배열
        bool[,] visited = new bool[n, m];

        // BFS를 위한 큐: (행, 열, 거리)
        Queue&lt;(int, int, int)&gt; queue = new Queue&lt;(int, int, int)&gt;();

        // 상하좌우 이동을 위한 방향 배열
        int[] dx = {-1, 1, 0, 0};
        int[] dy = {0, 0, -1, 1};

        // 시작점 초기화
        queue.Enqueue((0, 0, 1));
        visited[0, 0] = true;

        while (queue.Count &gt; 0) {
            var (x, y, dist) = queue.Dequeue();

            // 목표지점 도달 체크
            if (x == n-1 &amp;&amp; y == m-1) {
                return dist;
            }

            // 4방향 탐색
            for (int i = 0; i &lt; 4; i++) {
                int nx = x + dx[i];
                int ny = y + dy[i];

                // 이동 가능 조건 체크
                if (nx &gt;= 0 &amp;&amp; nx &lt; n &amp;&amp; ny &gt;= 0 &amp;&amp; ny &lt; m
                   &amp;&amp; maps[nx, ny] == 1
                   &amp;&amp; !visited[nx, ny]) {

                    queue.Enqueue((nx, ny, dist + 1));
                    visited[nx, ny] = true;
                }
            }
        }

        return -1;  // 도달 불가능
    }
}</code></pre>
<hr>
<h3 id="bfs-활용-분야">BFS 활용 분야</h3>
<h4 id="1-최단거리-문제">1. 최단거리 문제</h4>
<ul>
<li>미로 찾기</li>
<li>게임 맵 탐색</li>
<li>네트워크 최단 경로<h4 id="2-레벨별-탐색">2. 레벨별 탐색</h4>
</li>
<li>트리의 레벨 순회</li>
<li>그래프의 깊이별 탐색<h4 id="3-연결성-문제">3. 연결성 문제</h4>
</li>
<li>섬의 개수 구하기</li>
<li>연결 요소 찾기</li>
</ul>
<hr>
<h3 id="마무리">마무리</h3>
<p>BFS는 <strong>최단거리가 보장</strong>되는 강력한 알고리즘입니다. 핵심은:</p>
<ol>
<li>Queue를 이용한 순차적 탐색</li>
<li>동심원 형태로 퍼져나가는 탐색</li>
<li>Visited 배열을 통한 중복 방지</li>
</ol>
<p>이 세가지만 확실히 이해하면 다양한 최단거리 문제를 해결할 수 있습니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity / C#] LINQ]]></title>
            <link>https://velog.io/@ye-seong/Unity-C-LINQ</link>
            <guid>https://velog.io/@ye-seong/Unity-C-LINQ</guid>
            <pubDate>Thu, 31 Jul 2025 08:14:22 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/ye-seong/post/4a8b71a1-f4e2-4d7b-ab31-0835a2e6df94/image.png" alt=""></p>
<p>유니티를 개발하다 보면 배열이나 리스트를 다루는 일이 많죠? GameObject들을 찾고, 필터링하고, 정렬하고... 이런 작업들을 반복문으로 하다 보면 코드가 길어지고 복잡해집니다.</p>
<p>그런데 LINQ를 알고 나면 이런 작업들이 놀라울 정도로 간단해져요! 오늘은 유니티 개발자 관점에서 LINQ를 어떻게 활용할 수 있는지 알아보겠습니다.</p>
<hr>
<h3 id="linq란">LINQ란?</h3>
<p>Language Intergrated Query의 약자로 C#에서 컬렉션 데이터를 쉽게 다룰 수 있게 해주는 기능입니다. SQL처럼 데이터를 쿼리할 수 있어서 코드가 훨씬 직관적이고 간결해져요.</p>
<pre><code class="language-csharp">using System.Linq; // 추가!!</code></pre>
<hr>
<h3 id="기본-활용법">기본 활용법</h3>
<p>기존 방식과 LINQ를 비교하며 공부해봅시다.</p>
<h4 id="1-select---데이터-변환">1. Select - 데이터 변환</h4>
<pre><code class="language-csharp">// Enemy 리스트에서 이름만 가져와 리스트로 만들기

// 기존 방식
List&lt;string&gt; enemyNames = new List&lt;string&gt;();
foreach (GameObject enemy in enemies)
{
    enemyNames.Add(enemy.name);
}

// LINQ 방식
var enemyNames = enemies.Select(enemy =&gt; enemy.name).ToList();</code></pre>
<h4 id="2-where---조건-필터링">2. Where - 조건 필터링</h4>
<pre><code class="language-csharp">// 활성화된 적들만 찾기

// 기존 방식
List&lt;GameObject&gt; activeEnemies = new List&lt;GameObject&gt;();
foreach(GameObject enemy in allEnemies)
{
    if(enemy.activeInHierarchy)
    {
        activeEnemies.Add(enemy);
    }
}

// LINQ 방식
var activeEnemies = allEnemies.Where(enemy =&gt; enemy.activeInHierarchy).ToList();

// 특정 거리 내의 적들 찾기 (LINQ 방식)
var nearbyEnemies = enemies.Where(enemy =&gt;
    Vector3.Distance(transform.position, enemy.transform.position) &lt; 10f
).ToList()</code></pre>
<h4 id="3-orderby---정렬">3. OrderBy - 정렬</h4>
<pre><code class="language-csharp">// 거리순으로 정렬
// 내림차순 정렬은 `OrderByDescending`
var sortedEnemies = enemies.OrderBy(enemy =&gt;
    Vector3.Distance(transform.position, enemy.transform.position)
).ToList();

// 가장 가까운 적 찾기
// 1. Distance 사용
var closestEnemy = enemies.OrderBy(enemy =&gt;
    Vector3.distance(transform.position, enemy.transform.position)
).FirstOrDefault();

// 2. sqrMagnitude 사용 (제곱근 계산 생략으로 성능 향상)
var closestEnemy = enemies.OrderBy(enemy =&gt;
    (transform.position - enemy.transform.position).sqrMagnitude
).FirstOrDefault();</code></pre>
<hr>
<h3 id="실전-유니티-활용-예시">실전 유니티 활용 예시</h3>
<h4 id="1-gameobject-관리">1. GameObject 관리</h4>
<pre><code class="language-csharp">// 모든 적 오브젝트 중 활성화되고 HP가 50 이상인 것들
var dangerousEnemies = GameObject.FindGameObjectsWithTag(&quot;Enemy&quot;)
    .Where(enemy =&gt; enemy.activeInHierarchy)
    .Where(enemy =&gt; enemy.GetComponent&lt;EnemyHealth&gt;().currentHP &gt;= 50)
    .OrderByDescending(enemy =&gt; enemy.Getcomponent&lt;EnemyHealth&gt;().currentHP)
    .ToList();

// 모든UI 버튼을 한 번에 비활성화
GetComponentsInChildren&lt;Button&gt;()
    .ToList()
    .ForEach(btn =&gt; btn.interactable = false);</code></pre>
<h4 id="2-인벤토리-시스템">2. 인벤토리 시스템</h4>
<pre><code class="language-csharp">// 아이템 타입별 개수 세기
var itemCounts = inventory
    .Where(item =&gt; item.itemType)
    .GroupBy(item =&gt; item.itemType)
    .ToDictionary(g =&gt; g.Key, g =&gt; g.Count());

// 판매 가능한 아이템들의 총 가치
var totalSellValue = inventory
    .Where(item =&gt; item.canSell)
    .Sum(item =&gt; item.sellPrice);

// 무기만 필터링해서 공격력 순으로 정렬
var sortedWeapons = inventory
    .Where(item =&gt; item.itemType == ItemType.Weapon)
    .OrderByDescending(Weapon =&gt; weapon.attackPower)
    .ToList();</code></pre>
<h4 id="3-점수-시스템">3. 점수 시스템</h4>
<pre><code class="language-csharp">// 리더보드 생성 (상위 10명)
var leaderboard = playerScores
    .OrderByDescending(score =&gt; score.points)
    .Take(10)
    .Select((score, index) =&gt; new {
        Rank = index + 1,
        Name = score.playerName,
        Points = score.points
    })
    .ToList();

// 평균 점수보다 높은 플레이어들
var averageScore = playerScores.Average(score =&gt; score.points);
var aboveAverageUsers = playerScores
    .Where(score =&gt; score.points &gt; averageScore)
    .ToList();</code></pre>
<h4 id="4-레벨-관리">4. 레벨 관리</h4>
<pre><code class="language-csharp">// 언락된 레벨들만 난이도순으로
var availableLevels = allLevels
    .Where(level =&gt; level.isUnlocked)
    .OrderBy(level =&gt; level.difficulty)
    .ToList();

// 클리어하지 못한 레벨 중 가장 쉬운 것
var nextLevel = allLevels
    .Where(level =&gt; level.isUnlocked &amp;&amp; !level.isCompleted)
    .OrderBy(level =&gt; level.difficulty)
    .FirstOrDefault();</code></pre>
<hr>
<h3 id="고급-활용법">고급 활용법</h3>
<h4 id="1-체이닝으로-복잡한-작업-한-번에">1. 체이닝으로 복잡한 작업 한 번에</h4>
<pre><code class="language-csharp">// 활성화된 적들 중에서 플레이어 근처에 있고,
// HP가 낮은 순으로 정렬해서 상위 3마리만 가져오기
var targetEnemies = GameObject.FindGameObjectsWithTag(&quot;Enemy&quot;)
    .Where(enemy =&gt; enemy.activeInHierarchy)
    .Where(enemy =&gt; Vector3.Distance(player.position, enemy.transform.position) &lt; 15f
    .Select(enemy =&gt; enemy.GetComponent&lt;Enemy&gt;())
    .Where(enemy =&gt; enemy != null)
    .OrderBy(enemy =&gt; enemy.currentHP)
    .Take(3)
    .ToList();</code></pre>
<h4 id="2-groupby로-데이터-분석">2. GroupBy로 데이터 분석</h4>
<pre><code class="language-csharp">// 적들을 타입별로 그룹화
var enemyGroups = enemies
    .GroupBy(enemy =&gt; enemy.GetComponent&lt;Enemy&gt;().enemyType)
    .ToDictionary(g = g.Key, g =&gt; g.ToList());

// 아이템을 등급별로 개수 세기
var itemsByRarity = inventory
    .Where(item =&gt; item != null)
    .GroupBy(item =&gt; item.rarity)
    .Select(g =&gt; new {
        Rarity = g.Key,
        Count = g.Count(),
        TotalValue = g.Sum(item =&gt; item.value)
    })
    .ToList();</code></pre>
<hr>
<h3 id="성능-고려사항">성능 고려사항</h3>
<h4 id="1-좋은-사용법">1. 좋은 사용법</h4>
<pre><code class="language-csharp">// 게임 시작 시나 필요할 때만
void Start() {
    enemies = GameObject.FindGameObjectsWithTag(&quot;Enemy&quot;)
        .Where(e =&gt; e.activeInHierarchy)
        .ToList();
}


// 이벤트 발생 시
void OnEnemyDeath() {
    var remainingEnemies = enemies.Where(e =&gt; e != null).ToList();
}</code></pre>
<h4 id="2-피해야-할-사용법">2. 피해야 할 사용법</h4>
<pre><code class="language-csharp">// Update에서 매 프레임마다 - 성능 저하!
void Update() {
    var nearbyEnemies = FindObjectsOfType&lt;Enemy&gt;()
        .Where(e =&gt; Vector3.Distance(transform.position, e.transform.position) &lt; 10f)
        .ToList();
}</code></pre>
<h4 id="3-성능-최적화-팁">3. 성능 최적화 팁</h4>
<ol>
<li>캐싱 활용 : 자주 사용하는 컬렉션은 미리 저장해두세요</li>
<li>ToList() 적절히 사용 : 지연 실행을 이해하고 필요할 때만 즉시 실행!</li>
<li>FindObjectsOfType 주의 : 비용이 크니 LINQ와 함께 사용할 때 특히 조심!<pre><code class="language-csharp">// 캐싱 예시
private List&lt;Enemy&gt; cachedEnemies;
</code></pre>
</li>
</ol>
<p>void Start() {
    RefreshEnemyCache();
}</p>
<p>void RefreshEnemyCache()
{
    cachedEnemies = FindObjectsOfType<Enemy>().ToList();
}</p>
<p>void UpdateEnemyLogic()
{
    var activeEnemies = cachedEnemies
        .Where(enemy =&gt; enemy != null &amp;&amp; enemy.gameObject.activeInHierarchy)
        .ToList();
}</p>
<pre><code>---

### 그 외
게임 개발뿐만 아니라 다른 부분에서도 LINQ는 정말 강력합니다.
```csharp
// 배열에서 중복 제거
var unique = numbers.Distinct().ToArray();

// 두 배열의 교집합
var common = arr1.Intersect(arr2).ToArray();

// 문자열에서 각 문자의 빈도수
var frequency = text.GroupBy(c =&gt; c)
    .ToDictionary(g =&gt; g.Key, g =&gt; g.Count());

// k번째로 큰 수
var kthLargest = numbers.OrderByDescending(x =&gt; x)
    .Distinct()
    .Skip(k - 1)
    .First();</code></pre><hr>
<h3 id="끝으로">끝으로</h3>
<p>LINQ를 익혀두면 유니티 개발이 정말 편해집니다. 복잡한 반복문 대신 한 줄로 해결되는 경우가 많아서 코드도 깔끔해지고 버그도 줄어들어요!
처음에는 낯설 수 있지만, 몇 번 써보면 없어서는 안 될 도구가 될 거예요. 특히 데이터 처리나 GameObject 관리할 때 정말 유용하니까 꼭 익혀두시길 추천합니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity / C#] AsyncOperation]]></title>
            <link>https://velog.io/@ye-seong/Unity-C-AsyncOperation</link>
            <guid>https://velog.io/@ye-seong/Unity-C-AsyncOperation</guid>
            <pubDate>Thu, 24 Jul 2025 06:25:44 GMT</pubDate>
            <description><![CDATA[<p>세이브 및 로딩 작업을 하다가 발견한 클래스, <strong>AsyncOperation</strong> 클래스입니다.</p>
<h3 id="asyncoperation란">AsyncOperation란?</h3>
<ul>
<li>유니티에서 <strong>비동기 작업</strong>을 처리하는 클래스</li>
<li>시간이 오래 걸리는 작업(씬 로딩, 에셋 로딩 등)을 백그라운드에서 처리</li>
<li>메인 스레드를 막지 않고 진행상황을 추적할 수 있음</li>
</ul>
<hr>
<h3 id="주요-속성들">주요 속성들</h3>
<pre><code class="language-csharp">AsyncOperation asyncLoad = SceneManager.LadSceneAsync(&quot;MainScene&quot;);

// 진행률 (0.0 ~ 1.0, 단 씬 로딩은 0.9까지만)
float progress = asyncLoad.progress;

// 작업 완료 여부
bool isFinished = asyncLoad.isDone;

// 씬 자동 활성화 여부
asyncLoad.allowSceneActivation = false;

// 우선순위 설정 (높을수록 빠름)
asyncLoad.priority = ThreadPriority.High;</code></pre>
<hr>
<h3 id="다른-유용한-함수들">다른 유용한 함수들</h3>
<h4 id="1-에셋-비동기-로딩">1. 에셋 비동기 로딩:</h4>
<pre><code class="language-csharp">ResourceRequest request = Resources.LoadAsync&lt;GameObject&gt;(&quot;PlayerPrefab&quot;);
while (!request.isDone)
{
    Debug.Log($&quot;에셋 로딩: {request.progress * 100}%&quot;);
    yield return null;
}
GameObject player = request.asset as GameObject;</code></pre>
<h4 id="2-씬-언로드">2. 씬 언로드:</h4>
<pre><code class="language-csharp">AsyncOperation unloadOp = SceneManager.UnloadSceneAsync(&quot;OldScene&quot;);</code></pre>
<h4 id="3-에셋번들-로딩">3. 에셋번들 로딩:</h4>
<pre><code class="language-csharp">AssetBundleCreateRequest bundleRequest = AssetBundle.LoadFromFileAsync(path);</code></pre>
<hr>
<h3 id="실제-활용-예시">실제 활용 예시</h3>
<pre><code class="language-csharp">IEnumerator LoadWithControl()
{
    AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(&quot;MainScene&quot;);
    asyncLoad.allowSceneActivation = false; // 수동 제어

    while (asyncLoad.progress &lt; 0.9f) // 0.9까지만 로딩
    {
        loadingText.text = $&quot;로딩: {asyncLoad.progress * 100:F0}%&quot;;
        yield return null;
    }

    loadingText.text = &quot;완료! 클릭하세요&quot;;

    // 사용자 입력 대기
    while (!Input.GetMouseButtonDown(0))
        yield return null;

    asyncLoad.allowSceneActivation = true; // 씬 활성화
}</code></pre>
<p>이 클래스는 <strong>비동기 작업의 상태를 추적하고 제어</strong>하는 핵심 클래스입니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity / C#] 신호기 구현]]></title>
            <link>https://velog.io/@ye-seong/Unity-C-%EC%8B%A0%ED%98%B8%EA%B8%B0-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@ye-seong/Unity-C-%EC%8B%A0%ED%98%B8%EA%B8%B0-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Tue, 22 Jul 2025 12:14:29 GMT</pubDate>
            <description><![CDATA[<h2 id="📋-목차">📋 목차</h2>
<ol>
<li>신호기 시스템</li>
<li>SemaphoreSystem</li>
<li>이펙트 추가</li>
<li>중간 결과</li>
<li>신호기 미션 완료</li>
<li>최종 결과</li>
<li>오늘의 배운 점</li>
<li>다음 계획</li>
</ol>
<hr>
<h3 id="📡-신호기-시스템">📡 신호기 시스템</h3>
<p>제작하고 있는 게임 <strong>SignalHome</strong>은 외계에 고립된 플레이어가 신호기를 발동시켜 지구에 도움을 요청하고, 탈출하는 게임 입니다. 그렇다면 맵에 신호기가 존재하고, 플레이어는 그것을 찾아 발동시켜야겠죠?
스토리 흐름은 다음과 같습니다.</p>
<h4 id="1-신호기-존재-사실의-발견">1. 신호기 존재 사실의 발견</h4>
<p>불탄 연구실과 홀로 생존한 플레이어. 곧 외계에서 지구로 돌아가야 하지만, 신호기가 박살난 상태. 신호기에 응축되어있던 에너지는 모두 빠져나갔으며, 여분도 없어져버렸다. 고민하던 찰나 연구실에서 신호기의 존재에 대한 pdf를 발견한다.</p>
<h4 id="2-외계-신호기">2. 외계 신호기</h4>
<img src=https://velog.velcdn.com/images/ye-seong/post/617ea01e-4475-41e8-ab37-8b0939be44ca/image.png width="700">

<p>이곳에서 처음 발견한 외계 문서에는 이 행성에 엄청난 에너지를 품고 있는 곳이 5곳이 있다고 한다. 우리는 외계 문서에 따라 그것을 &#39;신호기&#39;라고 부른다. 흡사 재단같이 생겼으며, 에너지가 농축된 특정 외계 보석을 넣으면 신호기가 발동된다.</p>
<h4 id="3-신호기-위치">3. 신호기 위치</h4>
<p>신호기 위치에 대한 설명은 단 한 곳밖에 존재하지 않는다. 신호기에 보석을 넣으면 다음 신호기의 위치가 드러난다. 보석을 돌린 순간, 외계 에너지가 급증하는 곳을 찾아가면 된다.</p>
<h4 id="4-에너지-보석-위치">4. 에너지 보석 위치</h4>
<p>해당 신호기 주변에 여러개 존재한다. </p>
<h4 id="5-발동-전과-발동-후">5. 발동 전과 발동 후</h4>
<img src=https://velog.velcdn.com/images/ye-seong/post/c074ff72-9ea9-4509-9083-1bf834e78b39/image.png width="500">

<hr>
<h3 id="📄-semaphoresystem">📄 SemaphoreSystem</h3>
<p>이제 스토리에서 중요한 신호기 스크립트를 짜봅시다. <code>SemaphoreSytstem</code> 스크립트를 생성합니다.</p>
<h4 id="1-현재-번호-저장">1. 현재 번호 저장</h4>
<p>신호기는 순번대로 진행됩니다. 그러니 현재 게임 진행상태에 어디까지의 신호기까지 진행했는지 저장해야만 합니다.</p>
<pre><code class="language-csharp">// GameState.cs

// 첫번째 신호기는 이미 열려있음
public static int currentSemaphoreNumber = 0;</code></pre>
<h4 id="2-활성화-코드">2. 활성화 코드</h4>
<p>미리 맵에 신호기 다섯개를 배치합니다. 그리고 GameManager의 Inspector에 넣습니다.</p>
<pre><code class="language-csharp">// GameManager.cs

[Header(&quot;Semaphore&quot;)]
public SemaphoreSystem[] semaphores;

private void Start()
{
    for (int i = 0; i &lt; GameState.currentSemaphoreNumber + 1; i++)
    {
        SetActiveSemaphore(i);
    }
}

public void SetActiveSemaphore(int num)
{
    if (num &lt; 0 || num &gt;= semaphores.Length) return;
    semaphores[num].gameObject.SetActive(true);
    if (num &lt; GameState.currentSemaphoreNumber)
    {
        semaphores[num].isUnlocked = true;
    }
}</code></pre>
<h4 id="3-신호기-기본-코드">3. 신호기 기본 코드</h4>
<pre><code class="language-csharp">// SemaphoreSystem.cs

[Header(&quot;Semaphore Settings&quot;)]
public int number;
public Transform keyCrystalPoint;
public GameObject keyCrystalPrefab;

private bool isUnlocked = false;

private void Start()
{
    if (isUnlocked)
    {
        SpawnKeyCrystal();
    }
}

private void SpawnKeyCrystal()
{
    if (keyCrystalPrefab &amp;&amp; keyCrystalPoint)
    {
        GameObject keyCrystal = Instantiate(keyCrystalPrefab, keyCrystalPoint.position, keyCrystalPoint.rotation);
        keyCrystal.transform.SetParent(keyCrystalPoint);
        keyCrystal.transform.localPosition = Vector3.zero;
        keyCrystal.transform.localRotation = Quaternion.identity;
    }
}</code></pre>
<h4 id="4-키-상호작용">4. 키 상호작용</h4>
<p><code>F</code>를 누르면 <code>KeyCrystal</code>이 삽입되게 합시다.</p>
<pre><code class="language-csharp">// InteractionDetector.cs

private void CheckInteraction()
{
    // 기존 코드...
    else if (hit.collider.CompareTag(&quot;Semaphore&quot;))
    {
        AddCrystalKey(hit.collider.gameObject);
    }
}

private void AddCrystalKey(GameObject hitObject)
{
    if (playerItemHandler.currentItem)
    {
        if (playerItemHandler.currentItem.itemData.itemID == 31)
        {
            SemaphoreSystem semaphore = hitObject.GetComponent&lt;SemaphoreSystem&gt;();
            if (semaphore)
            {
                semaphore.SpawnKeyCrystal();
                GameState.currentSemaphoreNumber++;
                gameManager.SetActiveSemaphore(GameState.currentSemaphoreNumber);
                inventory.RemoveItemFromInstance(playerItemHandler.currentItem);
            }
        }
    }
}</code></pre>
<hr>
<h3 id="✨-이펙트-추가">✨ 이펙트 추가</h3>
<p>보석을 넣은 후 2초 후, 이펙트가 활성화되게 해봅시다. 먼저 넣고 싶은 이펙트를 보석이 아닌 <strong>신호기</strong>에 붙여주세요!</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/309c713a-da4c-4399-8332-a47e36fbb786/image.png width="700">

<pre><code class="language-csharp">// SemaphoreSystem.cs

[Header(&quot;Particle Settings&quot;)]
public ParticleSystem particleSystem1;
public ParticleSystem particleSystem2;

private void Start()
{
    if (!isUnlocked)
    {
        particleSystem1.Stop();
        particleSystem2.Stop();
    }
    else
    {
        SpawnKeyCrystal();
    }
}

public void SpawnKeyCrystal()
{
    if (keyCrystalPrefab &amp;&amp; keyCrystalPoint)
    {
        // 기존 코드...

        Invoke(&quot;ShowPartical&quot;, 2f);   
    }
}

void ShowPartical()
{
    particleSystem1.Play();
    particleSystem2.Play();
}</code></pre>
<hr>
<h3 id="🎮-중간-결과">🎮 중간 결과</h3>
<p>첫 번째 신호기에 보석을 넣으면 2초 후에 이펙트가 일어납니다. 또한, 다음 신호기의 모습이 보이는 것 또한 확인할 수 있습니다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/1728e980-6001-49f4-ba21-f28cee6ec5dd/image.gif width="700">


<hr>
<h3 id="📡-신호기-미션-완료">📡 신호기 미션 완료</h3>
<p>모든 신호기에 보석을 놓게 되면 차례대로 빛이 나오더니 위를 향해 빛줄기를 발사하게 됩니다. 해당 빛줄기 파티클을 직접 만들어볼까요? 일단 신호기 프리팹으로 이동합니다.
<code>Hierarchy</code>에서 우클릭 후 <code>Effects</code> -&gt; <code>Line</code> 으로 라인 렌더를 생성해주세요.</p>
<h4 id="1-line-renderer">1. Line Renderer</h4>
<pre><code class="language-csharp">// SemaphoreSystem.cs

[Header(&quot;Line Setting&quot;)]
public LineRenderer lineRenderer;

private void Awake()
{
    lineRenderer.useWorldSpace = false;
    lineRenderer.positionCount = 2;
    lineRenderer.SetPosition(0, Vector3.zero);      // 오브젝트 중심
    lineRenderer.SetPosition(1, Vector3.up * 50f);  // 위로 50m
}</code></pre>
<h4 id="2-머티리얼-적용">2. 머티리얼 적용</h4>
<p><code>Project</code>에서 <code>Create</code> -&gt; <code>Material</code> 를 생성합니다.</p>
<ul>
<li>Shader : Standard</li>
<li>Albedo : 배경색</li>
<li>Emission 체크 후 Color : 그라데이션 시작할 색</li>
</ul>
<p>설정을 한 후 <code>SemaphoreSystem</code> 스크립트에서 Awake에 <code>lineRenderer.material = beamMaterial</code> 이라고 타이핑 해주세요.</p>
<h4 id="3-신호기-빔-발사-시점">3. 신호기 빔 발사 시점</h4>
<p>모든 신호기가 가동되면 동시에 빔이 쏘아올려집니다. 그렇다면 게임 상태에서 현재 신호기가 모두 발동됐다는 신호를 보내야겠죠?</p>
<pre><code class="language-csharp">// GameState.cs

public static bool IsOperate = false;

// GameManager.cs

public void SetOperateSemaphore()
{
    if (!GameState.IsOperate) return;
    foreach (SemaphoreSystem semaphore in semaphores)
    {
        semaphore.lineRenderer.enabled = true;
    }
}

// SemaphoreSystem.cs

void ShowPartical()
{
    particleSystem1.Play();
    particleSystem2.Play();

    // 마지막 신호기에 보석을 넣었을때만 실행됨
    if (number &gt;= 4)
    {
        GameState.IsOperate = true;
        gameManager.SetOperateSemaphore();
    }
}</code></pre>
<hr>
<h3 id="🎮-최종-결과">🎮 최종 결과</h3>
<p>이제 파티클이 생성된 후, 모든 신호기에서 빔이 나오는 것을 확인할 수 있습니다.
<img src=https://velog.velcdn.com/images/ye-seong/post/f05b4735-cc26-4578-b2ec-c9f6f5f6f08e/image.gif width="800"></p>
<hr>
<h3 id="📚-오늘의-배운-점">📚 오늘의 배운 점</h3>
<ul>
<li>파티클 제작</li>
<li>머티리얼 제작</li>
</ul>
<hr>
<h3 id="🎯-다음-계획">🎯 다음 계획</h3>
<p>다음 글에서는:</p>
<ol>
<li>세이브 &amp; 로드</li>
<li>빌드 후 실행파일 테스트</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity / C#] 방어구 구현 #3]]></title>
            <link>https://velog.io/@ye-seong/Unity-C-%EB%B0%A9%EC%96%B4%EA%B5%AC-%EA%B5%AC%ED%98%84-3</link>
            <guid>https://velog.io/@ye-seong/Unity-C-%EB%B0%A9%EC%96%B4%EA%B5%AC-%EA%B5%AC%ED%98%84-3</guid>
            <pubDate>Fri, 18 Jul 2025 09:27:44 GMT</pubDate>
            <description><![CDATA[<h2 id="📋-목차">📋 목차</h2>
<ol>
<li>공속 모듈</li>
<li>중간 결과</li>
<li>체력 자동 회복 모듈</li>
<li>투명화 모듈</li>
<li>최종 결과</li>
<li>오늘의 배운 점</li>
<li>다음 계획</li>
</ol>
<hr>
<h3 id="⚔️-공속-모듈">⚔️ 공속 모듈</h3>
<h4 id="1-함수-셋팅">1. 함수 셋팅</h4>
<p>공속을 올리려면 <code>ItemData</code>에서 <code>attackSpeed</code>를 들고 와야겠죠? 쿨타임일 경우에는 시행하지 않는단 코드를 일단 짜봅니다.</p>
<pre><code class="language-csharp"> // ItemData.cs

 public float GetAttackSpeed(float amount)
{
    if (itemType != ItemType.Weapon) return 0f;
    return attackSpeed * amount;
}

// UseItem.cs

private float attackCooldown = 0f;
private bool useWeapon = false;

private void Update()
{
    if (playerItemHandler.currentItem)
    {
        ReloadCharge(playerItemHandler.currentItem);
        if (useWeapon)
        {
            WeaponAttackCooldown();
        }   
    }
    // 기존 코드...
}

private void UseKnife(float damage)
{
    useWeapon = true;
    if (attackCooldown &gt; 0f)
    {
        Debug.Log(&quot;쿰타임이 돌아오지 않았습니다!&quot;);
    }
    // 기존 코드...
}

private void WeaponAttackCooldown()
{
    if (playerItemHandler.currentItem.itemData.itemType == ItemType.Weapon)
    {
        if (attackCooldown &lt;= playerItemHandler.currentItem.itemData.attackSpeed)
        {
            attackCooldown += Time.deltaTime;
        }
        else
        {
            Debug.Log(&quot;쿨타임이 돌아왔습니다!&quot;);
            attackCooldown = 0f;
            useWeapon = false;
        }
    }
}</code></pre>
<h4 id="2-공속-변경">2. 공속 변경</h4>
<p>만약 공속 증가 모듈이 있을 경우, 공속이 빨라지게 합니다.</p>
<pre><code class="language-csharp">// PlayerItemHandler.cs

public void UseItem()
{
    if (!currentItem) return;

    if (useItem)
    {
        if (currentItem.itemData.itemType == ItemType.Weapon)
        {
            SetMaxAttackCooldown(useItem);
        }
        useItem.UseItems(currentItem);
    }
}

private void SetMaxAttackCooldown()
{
    ItemInstance armor = playerState.equipmentItems[1];
    if (armor)
    {
        ItemInstance attackSpeedModule = armor.Get&lt;ItemInstance[]&gt;(&quot;armorModules&quot;)[2];
        if (attackSpeedModule)
        {
            ArmorSkill armorSkill = attackSpeedModule.GetComponent&lt;ArmorSkill&gt;();
            useItem.maxAttackCooldown = currentItem.itemData.attackSpeed * (1 - armorSkill.attackSpeed);
        }
        else
        {
            useItem.maxAttackCooldown = currentItem.itemData.attackSpeed;
        }
    }
}

// UseItem.cs

[HideInInspector] public float maxAttackCooldown;
private bool useWeapon = false;

// 쿨타임 변수를 maxAttackCooldown으로 변경
private void WeaponAttackCooldown()
{
    if (playerItemHandler.currentItem.itemData.itemType == ItemType.Weapon)
    {
        if (attackCooldown &lt;= maxAttackCooldown)
        {
            attackCooldown += Time.deltaTime;
        }
        else
        {
            Debug.Log(&quot;쿨타임이 돌아왔습니다!&quot;);
            attackCooldown = 0f;
            useWeapon = false;
        }
    }
}</code></pre>
<h4 id="3-리셋">3. 리셋</h4>
<p>방어구를 중간에 벗게 되면 공속 또한 원래대로 돌아와야 합니다.</p>
<pre><code class="language-csharp">// PlayerItemHandler.cs

public void ResetWeapon()
{
    useItem.maxAttackCooldown = currentItem.itemData.attackSpeed;
}

// PlayerState.cs

public void ResetModuleEffect()
{
    if (isInHotZone)
    {
        ExitFromHotZone();
        EnterInHotZone();
    }
    playerController.SetMoveSpeed(5f); 
    if (playerItemHandler.currentItem)
    {
        if (playerItemHandler.currentItem.itemData.itemType == ItemType.Weapon)
        {
            playerItemHandler.ResetWeapon();
        }
    }
}</code></pre>
<hr>
<h3 id="🎮-중간-결과">🎮 중간 결과</h3>
<ol>
<li>이속 모듈 적용</li>
<li>공속 모듈 적용</li>
<li>모듈이 없는 상태</li>
</ol>
<img src=https://velog.velcdn.com/images/ye-seong/post/05ce99c0-6152-4558-822b-7babc6624391/image.gif width="800">

<hr>
<h3 id="❤️-체력-자동-회복-모듈">❤️ 체력 자동 회복 모듈</h3>
<p>체력이 일정 피 이하가 되면 자동으로 차게끔 하는 모듈을 구현해보겠습니다.</p>
<h4 id="1-체력-감지">1. 체력 감지</h4>
<p><code>PlayerState</code>에서 체력을 처리하는 함수에 현재 체력이 얼마인지 검사하는 로직을 짜면 되겠죠?</p>
<pre><code class="language-csharp">// PlayerState.cs

private bool isAutoHeal;

public void ModifyHealth(float amount)
{
    // 기존 로직...
    isAutoHeal = SetIsAutoHeal();
    // 기존 로직...
}

private bool SetIsAutoHeal()
{
    if (!equipmentItems[1]) return false;

    ItemInstance skillModule = equipmentItems[1].Get&lt;ItemInstance[]&gt;(&quot;armorModules&quot;)[2];
    if (!skillModule) return false;

    ArmorSkill armorSkill = skillModule.GetComponent&lt;ArmorSkill&gt;();
    if (ArmorSkillType.AutoHeal != armorSkill.skillType) return false;

    // 마지막에 한 번 더 AutoHeal하는 것을 막기 위해 현재 체력 + 힐량과 비교함
    return stats.health + autoHealAmount &lt; GameConstants.MAX_HEALTH * armorSkill.healthRatio;
}</code></pre>
<h4 id="2-체력-회복">2. 체력 회복</h4>
<pre><code class="language-csharp">// PlayerState.cs

private float autoHealAmount = 0f;
private float autoHealTime = 5f;

private void Update()
{
    if (isAutoHeal)
    {
        autoHealTime -= Time.deltaTime;
        if (autoHealTime &lt;= 0f)
        {
            autoHealTime = 5f;
            ModifyHealth(autoHealAmount);
        }
    }
}

public void ResetModuleEffect()
{
    // 기존 로직...
    isAutoHeal = false;
    autoHealTime = 5f;
}

private void SetSkillModule(ItemInstance item)
{
    // 기존 로직...
    isAutoHeal = SetIsAutoHeal();
}</code></pre>
<hr>
<h3 id="👻-투명화-모듈">👻 투명화 모듈</h3>
<p>투명화는 패시브가 아니라 <strong>액티브</strong> 스킬로 <strong>Q</strong>를 누르면 스킬을 쓸 수 있게 할겁니다. 투명화는 지속시간이 있고, 쿨타임도 존재합니다.</p>
<h4 id="1-q키-누를때-활성화">1. Q키 누를때 활성화</h4>
<pre><code class="language-csharp">// ArmorSkill.cs

[Header(&quot;Skill Properties&quot;)]
public float skillCooldown;
public float skillDuration; 

[HideInInspector] public float skillCooldownTimer;
[HideInInspector] public float skillDurationTimer;
[HideInInspector] public bool isSkill = false;
[HideInInspector] public bool isCooldown = false;

private PlayerController cachedController;

public void SetSkill(PlayerController playerController)
{
    if (isCooldown) return;

    if (!cachedController)
    {
        cachedController = playerController;
    }
    isSkill = true;
    switch(skillType)
    {
        // 기존 로직...
        case ArmorSkillType.Invisibility:
            ApplyInvisibility();
            break;
        // 기존 로직...
    }
}

public void ResetSkill()
{
    if (!cachedController) return;
    switch (skillType)
    {
        case ArmorSkillType.SpeedBoost:
            cachedController.SetMoveSpeed(5f);
            break;
        case ArmorSkillType.Invisibility:
            cachedController.SetPlayerInvisibility(false);
            break;
        default:
            break;
    }
}

private void ApplyInvisibility()
{
    cachedController.SetPlayerInvisibility(true);
}

// PlayerController.cs

void Update()
{
    // 기존 로직...
    if (!GameState.IsUIOpen)
    {
        HandleModuleSkill();
    }
    SkillTimerController();
}

void SkillTimerController()
{
    if (!currentSkill) return;
    if (currentSkill.isSkill)
    {
        currentSkill.skillDurationTimer -= Time.deltaTime;
        if (currentSkill.skillDurationTimer &lt;= 0)
        {
            currentSkill.isSkill = false;
            currentSkill.isCooldown = true;
            currentSkill.ResetSkill();
            currentSkill.skillDurationTimer = currentSkill.skillDuration;
        }
    }

    if (currentSkill.isCooldown)
    {
        currentSkill.skillCooldownTimer -= Time.deltaTime;
        if (currentSkill.skillCooldownTimer &lt;= 0)
        {
            currentSkill.isCooldown = false;
            currentSkill.skillCooldownTimer = currentSkill.skillCooldown;
        }
    }
}

void HandleModuleSkill()
{
    if (!playerState.equipmentItems[1]) return;
    ItemInstance moduleManager = playerState.equipmentItems[1];
    ItemInstance skillModule = moduleManager.Get&lt;ItemInstance[]&gt;(&quot;armorModules&quot;)[2];
    ArmorSkill skill = skillModule.GetComponent&lt;ArmorSkill&gt;();

    if (Input.GetKeyDown(KeyCode.Q))
    {
        skill.SetSkill(this);
        currentSkill = skill;
        skill.isSkill = true;
    }
}

public void SetPlayerInvisibility(bool invisible)
{
    playerRenderer.enabled = !invisible;
}</code></pre>
<h4 id="2-제작-후-초기화">2. 제작 후 초기화</h4>
<p>비활성화일땐 Start함수가 실행되지 않습니다. 하지만 제작대에서 생성하게 되면 비활성화로 들어오죠. 그러기 위해 초기화를 합시다.</p>
<pre><code class="language-csharp">// ArmorSkill.cs

public void Initialize()
{
    if (skillCooldownTimer == 0f) skillCooldownTimer = skillCooldown;
    if (skillDurationTimer == 0f) skillDurationTimer = skillDuration;
}

public void SetSkill(PlayerController playerController)
{
    Initialize();
    // 기존 로직...
}</code></pre>
<hr>
<h3 id="🎮-최종-결과">🎮 최종 결과</h3>
<p>체력 자동 회복, 투명화 모두 되는 모습입니다. 체력 회복은 현재 모듈 설정을 <code>0.8</code>로 했기 때문에 체력이 80%이상이라면 더이상 회복하지 않습니다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/e07f3eda-8328-418b-bcc2-faa961b986e9/image.gif width="800">

<img src=https://velog.velcdn.com/images/ye-seong/post/30488233-379f-4cee-8b3a-44def0b29f7c/image.gif width="800">


<hr>
<h3 id="📚-오늘의-배운-점">📚 오늘의 배운 점</h3>
<ul>
<li>Mesh 비활성화로 투명화</li>
</ul>
<hr>
<h3 id="🎯-다음-계획">🎯 다음 계획</h3>
<p>다음 글에서는:</p>
<ol>
<li>신호기 시스템 구현</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity / C#] 방어구 구현 #2]]></title>
            <link>https://velog.io/@ye-seong/Unity-C-%EB%B0%A9%EC%96%B4%EA%B5%AC-%EA%B5%AC%ED%98%84-2</link>
            <guid>https://velog.io/@ye-seong/Unity-C-%EB%B0%A9%EC%96%B4%EA%B5%AC-%EA%B5%AC%ED%98%84-2</guid>
            <pubDate>Wed, 16 Jul 2025 11:23:16 GMT</pubDate>
            <description><![CDATA[<h2 id="📋-목차">📋 목차</h2>
<ol>
<li>대미지 감소 모듈</li>
<li>중간 결과 1</li>
<li>환경 적응 모듈</li>
<li>UI에 적용</li>
<li>모듈 적용</li>
<li>중간 결과 2</li>
<li>스킬 모듈</li>
<li>최종 결과</li>
<li>오늘의 배운 점</li>
<li>다음 계획</li>
</ol>
<hr>
<h3 id="🔑-대미지-감소-모듈">🔑 대미지 감소 모듈</h3>
<p>방어구의 기본 효과, <strong>대미지 감소 효과</strong>를 구현하겠습니다. <code>ModuleEffect</code> 스크립트를 생성합니다.</p>
<h4 id="1-대미지-감소-로직">1. 대미지 감소 로직</h4>
<pre><code class="language-csharp">// ModuleEffect.cs

using UnityEngine;

public class ModuleEffect : MonoBehaviour
{
    public ArmorModuleType moduleType;

    [ShowIf(&quot;moduleType&quot;, ArmorModuleType.Defense)]
    public float defensePower;

    [ShowIf(&quot;moduleType&quot;, ArmorModuleType.Resistance)]
    public EnvironmentType environmentType;

    [ShowIf(&quot;moduleType&quot;, ArmorModuleType.Skill)]
    public ArmorSkillType skillType;

    private void Start()
    {

    }

    public void ApplyArmorModule()
    {

    }
    public float ApplyDefenseEffect(float damage)
    {
        float reducedDamage = damage * (1 - defensePower);
        Debug.Log(defensePower * 100 + &quot;%의 대미지를 감소시켜 &quot; + reducedDamage + &quot;의 대미지를 입었습니다.&quot;);
        return reducedDamage;
    }
}</code></pre>
<h4 id="2-체력-감소-로직에-적용">2. 체력 감소 로직에 적용</h4>
<pre><code class="language-csharp">// PlayerState.cs

public void ModifyHealth(float amout)
{
    if (isGameOver) return;

    if (amount &lt; 0f &amp;&amp; equipmentItems[1] != null)
    {
        ItemInstance defenseModule = equipmentItems[1].Get&lt;ItemInstance[]&gt;(&quot;armorModules&quot;)[0];
        if (defenseModule)
        {
            ModuleEffect moduleEffect = defenseModule.GetComponent&lt;ModuleEffect&gt;();
            if (moduleEffect)
            {
                amount = moduleEffect.ApplyDefenseEffect(amount);
            }
        }
    }

    // 기존 로직...
}</code></pre>
<hr>
<h3 id="🎮-중간-결과-1">🎮 중간 결과 1</h3>
<p>기존 10 대미지였던 것이 방어구 모듈 효과를 얻어 5만 감소하는 것을 볼 수 있습니다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/c99dd66b-63e3-496c-829c-27a742824350/image.png width="700">

<hr>
<h3 id="🔑-환경-적응-모듈">🔑 환경 적응 모듈</h3>
<p>이곳에는 다양한 환경이 있습니다. 물지형, 사막지형, 눈지형 등이 있죠. 각각 <code>산소, 더움, 추움</code>을 넣으면 좋겠죠? 먼저 플레이어가 해당 지형에 있다면 각 스텟이 감소하는 로직을 짜보겠습니다.</p>
<h4 id="1-ui-생성">1. UI 생성</h4>
<p>일단은 산소와 더움만 추가해보겠습니다. HUD_Panel에 <strong>HotBar</strong>와 <strong>BreathingBar</strong>를 넣어주세요. 다른 Bar와 같이 Image Type을 <strong>Filled</strong>로 하여 게이지를 표현합니다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/602c76f3-8a58-4a08-a613-418a0e590885/image.png width="700">

<h4 id="2-플레이어의-현재-장소-인식">2. 플레이어의 현재 장소 인식</h4>
<p><strong>더움</strong>은 더운 지역에 있을 때 증가하고, <strong>산소</strong>는 물 안에 들어가 있을 때 감소해야만 합니다. 그러니 플레이어가 현재 있는 장소에 대해 알 수 있어야겠죠? 현재 Zone중에 <code>Desert</code>가 있으니 더움은 해당 지역에 있을 때 증가하는 것으로 합시다.
일단 Desert 오브젝트에 그 지역의 범위만큼 Collision을 넣습니다. 그리고 IsTrigger를 체크해주세요.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/889f953e-076d-4c3b-9612-c76a8b3cd6e8/image.png width="600">

<p>그리고 아래 코드를 작성하여 Zone안에 들어갔을시 로그가 뜨면 감지 성공입니다.</p>
<pre><code class="language-csharp">// ZoneController.cs

private void OnTriggerEnter(Collider other)
{
    if (other.CompareTag(&quot;Player&quot;))
    {
        switch(environmentType)
        {
            case EnvironmentType.Desert:
                Debug.Log(&quot;Player가 Desert Zone에 들어왔습니다.&quot;);
                break;
            default:
                break;
        }
    }
}</code></pre>
<h4 id="3-게이지-증감">3. 게이지 증감</h4>
<p>들어가면 Hot이 증가하고, 나가면 서서히 감소하는 로직을 구성해야만 합니다.</p>
<pre><code class="language-csharp">// ZoneController.cs

private void OnTriggerEnter(Collider other)
{
    if (other.CompareTag(&quot;Player&quot;))
    {
        PlayerState playerState = other.GetComponent&lt;PlayerState&gt;();
        switch(environmentType)
        {
            case EnvironmentType.Desert:
                playerState.EnterInHotZone();
                Debug.Log(&quot;Player가 Desert Zone에 들어왔습니다.&quot;);
                break;
            default:
                break;
        }
    }
}

private void OnTriggerExit(Collider other)
{
    if (other.CompareTag(&quot;Player&quot;))
    {
        PlayerState playerState = other.GetComponent&lt;PlayerState&gt;();
        switch (environmentType)
        {
            case EnvironmentType.Desert:
                playerState.ExitFromHotZone();
                Debug.Log(&quot;Player가 Desert Zone을 나갔습니다.&quot;);
                break;
            default:
                break;
        }
    }
}

// PlayerState.cs

// PlayerState의 생성자도 변경해줍시다!
public static class GameConstants
{
    public const float MAX_HEALTH = 100f;
    public const float MAX_SATIETY = 100f;
    public const float MAX_HYDRATION = 100f;
    public const float MAX_HOT_GAUGE = 100f;
    public const float MAX_BREATHING_GAUGE = 100f;
}

public class PlayerState : MonoBeHaviour
{
    private float hotGaugeTime = 1f;
    private float breathingGaugeTime = 1f;

    private void Update()
    {
        Debug.Log($&quot;Hot:{stats.hotGauge}, Breathing:{stats.breathingGauge}&quot;);
    }
    public void EnterInHotZone()
    {
        CancelInvoke(&quot;DecreaseHotGaugeByTime&quot;); 
        InvokeRepeating(&quot;IncreaseHotGaugeByTime&quot;, hotGaugeTime, hotGaugeTime); 
    }

    public void ExitFromHotZone()
    {
        CancelInvoke(&quot;IncreaseHotGaugeByTime&quot;);
        CancelInvoke(&quot;DecreaseHealthByHotGauge&quot;);
        InvokeRepeating(&quot;DecreaseHotGaugeByTime&quot;, hotGaugeTime + 1f, hotGaugeTime + 1f); // 온도 게이지 감소 함수 호출
    }

    public void EnterInWaterZone()
    {
        CancelInvoke(&quot;IncreaseHotGaugeByTime&quot;);
        InvokeRepeating(&quot;DecreaseBreathingGaugeByTime&quot;, breathingGaugeTime, breathingGaugeTime); // 호흡 게이지 감소 함수 호출
    }

    public void ExitFromWaterZone()
    {
        CancelInvoke(&quot;DecreaseBreathingGaugeByTime&quot;);
        CancelInvoke(&quot;DecreaseHealthByBreathingGauge&quot;);
        InvokeRepeating(&quot;IncreaseBreathingGaugeByTime&quot;, breathingGaugeTime + 1f, breathingGaugeTime + 1f); // 호흡 게이지 증가 함수 호출
    }

    // 온도 게이지 증가 함수
    private void IncreaseHotGaugeByTime()
    {
        ModifyHotGauge(1f);
        if (stats.hotGauge &gt;= GameConstants.MAX_HOT_GAUGE)
        {
            InvokeRepeating(&quot;DecreaseHealthByHotGauge&quot;, 1f, 1f);
            CancelInvoke(&quot;IncreaseHotGaugeByTime&quot;);
        }
    }

    // 온도 게이지 감소 함수
    private void DecreaseHotGaugeByTime()
    {
        ModifyHotGauge(-1f);
        if (stats.hotGauge &lt;= 0f)
        {
            CancelInvoke(&quot;DecreaseHealthByHotGauge&quot;);
            CancelInvoke(&quot;DecreaseHotGaugeByTime&quot;);
        }
    }

    // 호흡 게이지 증가 함수
    private void IncreaseBreathingGaugeByTime()
    {
        ModifyBreathingGauge(1f);
        if (stats.breathingGauge &gt;= GameConstants.MAX_BREATHING_GAUGE)
        {
            CancelInvoke(&quot;DecreaseBreathingGaugeByTime&quot;);
            CancelInvoke(&quot;DecreaseHealthByBreathingGauge&quot;);
        }
    }

    // 호흡 게이지 감소 함수
    private void DecreaseBreathingGaugeByTime()
    {
        ModifyBreathingGauge(-1f);
        if (stats.breathingGauge &lt;= 0f)
        {
            InvokeRepeating(&quot;DecreaseHealthByBreathingGauge&quot;, 1f, 1f);
            CancelInvoke(&quot;DecreaseBreathingGaugeByTime&quot;);
        }
    }

    // 온도 게이지 수정 함수
    public void ModifyHotGauge(float amount)
    {
        stats.hotGauge += amount;

        if (stats.hotGauge &gt; GameConstants.MAX_HOT_GAUGE)
        {
            stats.hotGauge = GameConstants.MAX_HOT_GAUGE;
        }
        else if (stats.hotGauge &lt; 0f)
        {
            stats.hotGauge = 0f;
        }

        OnStatsChanged?.Invoke(stats);
    }

    // 호흡 게이지 수정 함수
    public void ModifyBreathingGauge(float amount)
    {
        stats.breathingGauge += amount;

        if (stats.breathingGauge &gt; GameConstants.MAX_BREATHING_GAUGE)
        {
            stats.breathingGauge = GameConstants.MAX_BREATHING_GAUGE;
        }

        else if (stats.breathingGauge &lt; 0f)
        {
            stats.breathingGauge = 0f;
        }

        OnStatsChanged?.Invoke(stats);
    }
}</code></pre>
<p>코드가 길어보이지만 로직 구조는 거의 같은 편입니다. 어차피 PlayerState는 Player의 현재상태 변화와 같은 코드만 넣을 것이기 때문에, 보기 쉽게 세분화하는 것이 좋습니다. 이제 ZoneController에서 Invoke실행과 종료를 하게끔 함수를 넣습니다.</p>
<pre><code class="language-csharp">// ZoneController.cs

private void OnTriggerEnter(Collider other)
{
    if (other.CompareTag(&quot;Player&quot;))
    {
        PlayerState playerState = other.GetComponent&lt;PlayerState&gt;();
        switch(environmentType)
        {
            case EnvironmentType.Desert:
                playerState.EnterInHotZone();
                Debug.Log(&quot;Player가 Desert Zone에 들어왔습니다.&quot;);
                break;
            default:
                break;
        }
    }
}

private void OnTriggerExit(Collider other)
{
    if (other.CompareTag(&quot;Player&quot;))
    {
        PlayerState playerState = other.GetComponent&lt;PlayerState&gt;();
        switch (environmentType)
        {
            case EnvironmentType.Desert:
                playerState.ExitFromHotZone();
                Debug.Log(&quot;Player가 Desert Zone을 나갔습니다.&quot;);
                break;
            default:
                break;
        }
    }
}</code></pre>
<h4 id="4-결과-확인">4. 결과 확인</h4>
<p>영역 안에 들어가면 Hot 게이지가 오르고, 나가면 올라가는 속도 보다 1초 느리게 감소합니다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/ae28274e-90ef-4b2e-af66-142e1a1d9e35/image.gif width="700">

<hr>
<h3 id="🎨-ui에-적용">🎨 UI에 적용</h3>
<p>이제 시각화 해봅시다! 본래 있던 함수에 넣기만 하면 끝입니다.</p>
<h4 id="1-hud-update">1. HUD Update</h4>
<pre><code class="language-csharp">// UIManager.cs

[Header(&quot;Bar&quot;)]
public Image hotGaugeBar;
public Image breathingGaugeBar;

private void UpdateUI(PlayerStats stats)
{
    if (healthBar &amp;&amp; satietyBar &amp;&amp; hydrationBar)
    {
        healthBar.fillAmount = stats.health / GameConstants.MAX_HEALTH;
        satietyBar.fillAmount = stats.satiety / GameConstants.MAX_SATIETY;
        hydrationBar.fillAmount = stats.hydration / GameConstants.MAX_HYDRATION;
        hotGaugeBar.fillAmount = stats.hotGauge / GameConstants.MAX_HOT_GAUGE;
        breathingGaugeBar.fillAmount = stats.breathingGauge / GameConstants.MAX_BREATHING_GAUGE;
    }

    if (healthText &amp;&amp; satietyText &amp;&amp; hydrationText)
    {
        healthText.text = stats.health.ToString(&quot;F0&quot;);  
        satietyText.text = stats.satiety.ToString(&quot;F0&quot;);
        hydrationText.text = stats.hydration.ToString(&quot;F0&quot;);
    }
}</code></pre>
<h4 id="2-결과-확인">2. 결과 확인</h4>
<p>Desert Zone안에 있으면 게이지가 오르고, 100이 된다면 체력도 줄어드는 모습입니다. (빠른 결과를 보여주기 위해 time을 0.1로 대폭 줄인 상태입니다)</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/9447ffe1-1a27-496a-b740-dd66e78621a7/image.gif width="700">


<hr>
<h3 id="🔑-모듈-적용">🔑 모듈 적용</h3>
<p>이제 모듈을 적용합시다! 더운 곳에 있을 경우, 해당 게이지가 천천히 오르게 하는 모듈입니다.</p>
<pre><code class="language-csharp">// ModuleEffect.cs

[ShowIf(&quot;moduleType&quot;, ArmorModuleType.Resistance)]
public EnvironmentType environmentType;
[ShowIf(&quot;moduleType&quot;, ArmorModuleType.Resistance)]
public float resistancePower;

public float ApplyResistanceEffect(float time)
{
    float reducedTime = time + time * resistancePower;
    Debug.Log($&quot;time:{reducedTime}&quot;);
    return reducedTime;
}

// PlayerState.cs

[HideInInspector] public bool isInHotZone;

public void EnterInHotZone()
{
    CancelInvoke(&quot;DecreaseHotGaugeByTime&quot;);
    float resultTime = hotGaugeTime;
    if (equipmentItems[1] != null)
    {
        ItemInstance resistanceModule = equipmentItems[1].Get&lt;ItemInstance[]&gt;(&quot;armorModules&quot;)[1];
        if (resistanceModule)
        {
            ModuleEffect moduleEffect = resistanceModule.GetComponent&lt;ModuleEffect&gt;();
            if (moduleEffect &amp;&amp; moduleEffect.environmentType == EnvironmentType.Desert)
            {
                resultTime = moduleEffect.ApplyResistanceEffect(resultTime);
            }
        }
    }
    isInHotZone = true;
    InvokeRepeating(&quot;IncreaseHotGaugeByTime&quot;, resultTime, resultTime); 
}

// ExitFromHotZone 함수에는 isInHotZone을 False로 바꿉시다</code></pre>
<p>만약 구역 안에서 방어구를 벗거나 입었을때, 감소 시간의 수치가 그대로면 안되니 그때의 경우에도 로직을 넣어줍시다.</p>
<pre><code class="language-csharp">// EquipmentSlots.cs

private void RemoveItem(EquipmentType type)
{
    // 기존 로직...
    if (index == 1)
    {
        if (playerState.isInHotZone)
        {
            playerState.ExitFromHotZone();
            playerState.EnterInHotZone();
        }
    }
    // 기존 로직...
}

// UIManager.cs

private void UpdateEquipmentUI(EquipmentType type, ItemInstance item)
{
    switch(type)
    {
        // 기존 로직...
        case EquipmentType.Body:
            equipmentSlots[1].SetEquipmentSlot(item);
            if (playerState.isInHotZone)
            {
                playerState.ExitFromHotZone();    
                playerState.EnterInHotZone();
            }
            break;
        // 기존 로직...
}</code></pre>
<hr>
<h3 id="🎮-중간-결과-2">🎮 중간 결과 2</h3>
<p>기본 감소 시간은 0.5초로 했고, 각 방어구가 가진 power는 0.5와 1인 상태입니다.
그러니 0.5인 모듈은 0.75초마다 게이지가 오르고, 1인 모듈은 1초마다 게이지가 오르죠.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/dc2d59cb-3719-4300-9ab7-175d9816b29a/image.gif width="800">

<hr>
<h3 id="🔑-스킬-모듈">🔑 스킬 모듈</h3>
<p>현재 스킬은 <code>이속, 공속, 체력 자동 회복, 투명화</code>가 있는데 여기선 이속만 구현하겠습니다. 이속은 간단합니다! <code>PlayerController</code>에 있는 <code>moveSpeed</code>만 조절하면 돼요.</p>
<h4 id="1-속도-변경">1. 속도 변경</h4>
<pre><code class="language-csharp">// PlayerController.cs

public void SetMoveSpeed(float speed)
{
    moveSpeed = speed;
}

// ArmorSkill.cs

public class ArmorSkill : MonoBehaviour
{
    public ArmorSkillType skillType;

    [ShowIf(&quot;skillType&quot;, ArmorSkillType.SpeedBoost)]
    public float speed;

    public void SetSkill(PlayerController playerController)
    {
        switch(skillType)
        {
            case ArmorSkillType.SpeedBoost:
                ApplySpeedBoost(playerController);
                break;
            case ArmorSkillType.Invisibility:
                ApplyInvisibility();
                break;
            default:
                Debug.LogWarning(&quot;No skill type set.&quot;);
                break;
        }
    }

    private void ApplySpeedBoost(PlayerController controller)
    {
        controller.SetMoveSpeed(speed);
    }

    private void ApplyInvisibility()
    {
        Debug.Log(&quot;투명화!&quot;);
    }
}

public enum ArmorSkillType
{
    None,
    SpeedBoost,
    Invisibility
}</code></pre>
<h4 id="2-장착시-적용">2. 장착시 적용</h4>
<p>스킬이 적용되면 플레이어의 현재 상태가 변경되어야하죠. 그것을 담당하는 <code>PlayerState</code> 스크립트를 수정합시다.</p>
<pre><code class="language-csharp">//PlayerState.cs

private void SetEquipmentItems(int index, ItemInstance item)
{
    if (equipmentItems[index])
    {
        inventory.AddItem(equipmentItems[index]);
        uiManager.UpdateItemUI();
    }
    equipmentItems[index] = item;

    SetSkillModule(item);
}

private void SetSkillModule(ItemInstance item)
{
    ItemInstance skillModule = item.Get&lt;ItemInstance[]&gt;(&quot;armorModules&quot;)[2];
    if (!skillModule) return;

    skillModule.GetComponent&lt;ArmorSkill&gt;().SetSkill(playerController);
}

// 방어구가 바뀔시 이전에 착용했던 모든 효과를 초기화
public void ResetModuleEffect()
{
    if (isInHotZone)
    {
        ExitFromHotZone();
        EnterInHotZone();
    }
    playerController.SetMoveSpeed(5f); 
}</code></pre>
<hr>
<h3 id="🎮-최종-결과">🎮 최종 결과</h3>
<p><code>대미지 감소, 열 저항력 증가, 이속 증가</code> 모듈을 모두 장착한 장면입니다. <strong>대미지 감소</strong> 모듈은 업그레이드가 가능한 형태로 해야겠습니다. 하나만 있으면 재미가 없으니까요!
일단 그건 나중에...</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/a24e26b6-a484-4adc-85f3-05e41a25e0de/image.gif width="700">



<hr>
<h3 id="📚-오늘의-배운-점">📚 오늘의 배운 점</h3>
<ul>
<li>받는 대미지 감소</li>
<li>Trigger로 지역 나누기</li>
<li>이속 조정</li>
</ul>
<hr>
<h3 id="🎯-다음-계획">🎯 다음 계획</h3>
<p>다음 글에서는:</p>
<ol>
<li>방어구 스킬 구현<ul>
<li>공속 증가</li>
<li>체력 자동 회복</li>
<li>투명화</li>
</ul>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity / C#] 방어구 구현 #1]]></title>
            <link>https://velog.io/@ye-seong/Unity-C-%EB%B0%A9%EC%96%B4%EA%B5%AC-%EA%B5%AC%ED%98%84-1</link>
            <guid>https://velog.io/@ye-seong/Unity-C-%EB%B0%A9%EC%96%B4%EA%B5%AC-%EA%B5%AC%ED%98%84-1</guid>
            <pubDate>Tue, 15 Jul 2025 08:07:53 GMT</pubDate>
            <description><![CDATA[<h2 id="📋-목차">📋 목차</h2>
<ol>
<li>모듈형 방어구</li>
<li>모듈 적용</li>
<li>최종 결과</li>
<li>오늘의 배운 점</li>
<li>다음 계획</li>
</ol>
<hr>
<h3 id="🛡️-모듈형-방어구">🛡️ 모듈형 방어구</h3>
<p>이 게임에서는 단순히 방어구를 입는 것이 아니라, 기존 방어구에서 <strong>모듈</strong>을 추가하는 형식으로 할겁니다. 슬롯이 3개인 방어구 모듈판이 있고, 각각 <code>방어력, 환경 저항, 스킬</code>로 구성되어 있습니다.
먼저 모듈 타입을 만들까요?</p>
<h4 id="1-모듈-아이템-생성">1. 모듈 아이템 생성</h4>
<p>아래 코드를 작성하고, Defense, Resistance, Skill이란 Module 데이터를 만든 후, Prefab도 생성해주세요.</p>
<pre><code class="language-csharp">// ItemInstance.cs

void IntializeProperties()
{
    // 기존 코드...

    switch (itemdata.itemType)
    {
        case ItemType.Armor:
            properties[&quot;durability&quot;] = 100f;
            properties[&quot;armorModules&quot;] = new ItemInstance[3];
            break;
        default:
            break;
    }
}

// ItemData.cs

[ShowIf(&quot;itemType&quot;, ItemType.ArmorMoudle)]
public ArmorModuleType armorModuleType;

[ShowIf(&quot;armorModuleType&quot;, ArmorModuleType.Defense)]
public float defensePower;

[ShowIf(&quot;armorModuleType&quot;, ArmorModuleType.Resistance)]
public EnvironmentType environmentType;

[ShowIf(&quot;armorModuleType&quot;, ArmorModuleType.Skill)]
public ArmorSkill armorSkill;

public enum ArmorModuletype
{
    Defense,
    Resistance,
    Skill
}</code></pre>
<h4 id="2-ui생성">2. UI생성</h4>
<p>기존에 있던 슬롯 아무거나 하나를 복제해서, 모듈 교체시 나오는 UI를 제작합니다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/1ef904c5-aa14-481f-9d41-6bdc782c5030/image.png width="700">

<h4 id="3-슬롯-우클릭">3. 슬롯 우클릭</h4>
<p>인벤토리 슬롯안에서 갑옷을 우클릭하면 위에 Module UI가 나오게 할겁니다.</p>
<pre><code class="language-csharp">// InventorySlot.cs

public void OnPointerClick(PointerEventData eventData)
{
    if (!inventory.items[slotIndex]) return;

    if (eventData.button == PointerEventData.InputButton.Left &amp;&amp; eventData.clickCount == 2)
    {
        uiManager.OnInventorySlotDoubleClick(slotIndex);
    }

    if (eventData.button == PointerEventData.InputButton.Right)
    {
        uiManager.OnInventorySlotRightClick(slotIndex);
    }
}

// UIManager.cs

// moduleSlotPanel은 InventoryPanel의 자식에 놓음
public GameObject moduleSlotPanel;
private ItemInstance currentArmor;

public void OnInventorySlotRightClick(int slotIndex)
{
    currentArmor = inventory.items[slotIndex];
    if (!currentArmor) return;

    ItemType type = currentArmor.itemData.itemType;
    if (type != ItemType.Armor) return;

    moduleSlotPanel.SetActive(true);

    ModuleSlot[] moduleSlots = moduleSlotPanel.GetComponentsInChildren&lt;ModuleSlot&gt;();
    ItemInstance[] armorModules = currentArmor.Get&lt;ItemInstance[]&gt;(&quot;armorModules&quot;);

    if (moduleSlots.Length &lt;= 0 || armorModules.Length &lt;= 0) return;
    for (int i = 0; i &lt; moduleSlots.Length; i++)
    {
        moduleSlots[i].SetModuleSlots(armorModules[i], currentArmor);
    }
}</code></pre>
<hr>
<h3 id="🔑-모듈-적용">🔑 모듈 적용</h3>
<p>이제 UI를 생성하는 것까진 됐죠? 그렇다면 <code>ModuleSlot</code> 스크립트를 생성해서 슬롯 프리팹에 적용하고 방어구에 모듈이 들어가게끔 해봅시다.</p>
<h4 id="1-현재-모듈-세팅">1. 현재 모듈 세팅</h4>
<pre><code class="language-csharp">// ModuleSlot.cs

public void SetModuleSlots(ItemInstance module, ItemInstance armor)
{
    int index = -1;
    switch(moduleType)
    {
        case ArmorModuleType.Defense:
            index = 0;
            break;
        case ArmorModuleType.Resistance:
            index = 1;
            break;
        case ArmorModuleType.Skill:
            index = 2;
            break;
        default:
            return;
    }

    if (module)
    {
        itemIcon.sprite = module.itemData.icon;
        itemIcon.color = Color.white;
    }
    else
    {
        itemIcon.sprite = slotImage;
        itemIcon.color = new Color(1, 1, 1, 0.5f);
    }
}</code></pre>
<p>인벤토리를 열어 갑옷을 우클릭하면 상단에 모듈창이 뜨는 모습입니다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/4cd6c108-7721-4c01-97da-601c1b341647/image.png width="700">

<h4 id="2-모듈장착하기">2. 모듈장착하기</h4>
<p>인벤토리창에서 모듈을 더블클릭하면 해당 방어구의 모듈로 들어가고, 모듈창에서 더블클릭 한다면 인벤토리 창으로 해당 모듈이 들어가는 것을 구현하겠습니다.</p>
<pre><code class="language-csharp">// ArmorModuleManager.cs

// 현재 모듈이 바뀔때마다 부름
public static event System.Action&lt;int, ItemInstance, ItemInstance&gt; OnModuleChanged;

public void AddModule(Inventory inventory, ItemInstance module, ItemInstance armor)
{
    if (!inventory || !module) return;

    if (module.itemData.itemType != ItemType.ArmorMoudle) return;

    int index = -1;
    switch(module.itemData.armorModuleType)
    {
        case ArmorModuleType.Defense:
            index = 0;
            break;
        case ArmorModuleType.Resistance:
            index = 1;
            break;
        case ArmorModuleType.Skill:
            index = 2;
            break;
        default:
            return;
    }
    AddModuleInIndex(inventory, module, armor, index);
}

private void AddModuleInIndex(Inventory inventory, ItemInstance module, ItemInstance armor, int index)
{
    OnModuleChanged?.Invoke(index, module, armor);
    ItemInstance[] armorModules = armor.Get&lt;ItemInstance[]&gt;(&quot;armorModules&quot;);
    if (!armorModules[index])
    {
        armorModules[index] = module;
        inventory.RemoveItemFromInstance(module);
    }
    else
    {
        inventory.AddItem(armorModules[index]);
        armorModules[index] = module;
        inventory.RemoveItemFromInstance(module);
    }
}

// ModuleSlot.cs

void OnEnable()
{
    ArmorModuleManager.OnModuleChanged += OnModuleUpdated;
}

void OnDisable()
{
    ArmorModuleManager.OnModuleChanged -= OnModuleUpdated;
}

public void SetModuleSlots(ItemInstance module, ItemInstance armor)
{
    if (module)
    {
        itemIcon.sprite = module.itemData.icon;
        itemIcon.color = Color.white;
    }
    else
    {
        itemIcon.sprite = slotImage;
        itemIcon.color = new Color(1, 1, 1, 0.5f);
    }
}</code></pre>
<h4 id="3-모듈해제하기">3. 모듈해제하기</h4>
<p>이번엔 모듈창에서 더블클릭하면 모듈이 해제되게 할겁니다. 먼저 <code>currentArmor</code>라는 변수를 생성하고 <code>setModuleSlots</code>함수에서 <code>currentArmor = armor</code>를 작성해주세요.</p>
<pre><code class="language-csharp">// ModuleSlot.cs

public void OnPointerClick(PointerEventData eventData)
{
    if (eventData.clickCount == 2)
    {
        RemoveItem(moduleType);
    }
}

private void RemoveItem(ArmorModuleType type)
{

    int index = -1;
    if (type == ArmorModuleType.Defense) index = 0;
    else if (type == ArmorModuleType.Resistance) index = 1;
    else if (type == ArmorModuleType.Skill) index = 2;
    else return;

    ItemInstance[] armorModules = currentArmor.Get&lt;ItemInstance[]&gt;(&quot;armorModules&quot;);
    if (!armorModules[index]) return;

    inventory.AddItem(armorModules[index]);
    armorModules[index] = null;

    currentArmor.Set(&quot;armorModules&quot;, armorModules);
    SetModuleSlots(null, currentArmor);

    uiManager.UpdateItemUI();
}</code></pre>
<h4 id="4-그-외-예외처리">4. 그 외 예외처리</h4>
<p>항상 그렇듯... 한 시스템을 만들면 엄청난 예외처리가 필요합니다. 그건 이곳에서 따로 코드를 보여드리진 않겠습니다. 대략적으로 말씀드리자면,</p>
<ol>
<li>인벤토리 창을 닫을 때 모듈 창이 꺼지게 함</li>
<li>인벤토리의 다른 슬롯을 건들면 모듈 창이 꺼지게 함</li>
<li>다른 방어구를 우클릭했을 경우, 해당 방어구의 모듈을 보여줌</li>
</ol>
<p>등등이 있겠죠?</p>
<hr>
<h3 id="🎮-최종-결과">🎮 최종 결과</h3>
<p>각 모듈이 탈부착 가능하며, 각각 방어구마다 다른 모듈을 보여주는 모습입니다. 인벤토리 슬롯 인덱스가 바껴도 창은 그대로 입니다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/e7b1f711-87df-41b0-ab4a-4887c4e0543c/image.gif width="800">

<hr>
<h3 id="📚-오늘의-배운-점">📚 오늘의 배운 점</h3>
<ul>
<li>모듈 탈부착 구현</li>
</ul>
<hr>
<h3 id="🎯-다음-계획">🎯 다음 계획</h3>
<p>다음 글에서는:</p>
<ol>
<li>방어구 효과 적용<ul>
<li>받는 대미지 감소</li>
<li>환경에 따른 영향 감소</li>
<li>스킬 적용</li>
</ul>
</li>
<li>스킬 구현<ul>
<li>이속 증가</li>
<li>공속 증가</li>
<li>체력 자동 회복</li>
</ul>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity / C#] 손전등 기능 및 아이템 모션 구현]]></title>
            <link>https://velog.io/@ye-seong/Unity-C-%EC%86%90%EC%A0%84%EB%93%B1-%EA%B8%B0%EB%8A%A5-%EB%B0%8F-%EC%95%84%EC%9D%B4%ED%85%9C-%EB%AA%A8%EC%85%98-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@ye-seong/Unity-C-%EC%86%90%EC%A0%84%EB%93%B1-%EA%B8%B0%EB%8A%A5-%EB%B0%8F-%EC%95%84%EC%9D%B4%ED%85%9C-%EB%AA%A8%EC%85%98-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Mon, 14 Jul 2025 11:26:38 GMT</pubDate>
            <description><![CDATA[<h2 id="📋-목차">📋 목차</h2>
<ol>
<li>손전등 구현</li>
<li>중간 결과</li>
<li>애니메이션</li>
<li>스크립트에서 애니메이션 제어</li>
<li>오브젝트 맞추기</li>
<li>최종 결과</li>
<li>오늘의 배운 점</li>
<li>다음 계획</li>
</ol>
<hr>
<h3 id="🔦-손전등-구현">🔦 손전등 구현</h3>
<h4 id="1-빛-추가">1. 빛 추가</h4>
<p>손전등 구현은 이미 <strong>Scanner</strong>에서 대부분 구현했기 때문에, 간단하게 하고 넘어가겠습니다. 먼저 손전등 프리팹에서 <code>PointLight</code>를 자식으로 추가하고, 설정을 맞춰주세요.</p>
<img src = https://velog.velcdn.com/images/ye-seong/post/a99ae01e-8747-4fec-8566-8a21051b6bfc/image.png width="600">

<h4 id="2-입력으로-끄고-켜기">2. 입력으로 끄고 켜기</h4>
<p>지금 현재 손전등이 켜져있는지 꺼져있는지의 변수를 추가하고, 빛을 끄고 켜고를 표현합시다.</p>
<pre><code class="language-csharp">// ItemInstance.cs

 void IntializeProperties()
 {
     // 기존 코드...

    if (itemData.itemName = &quot;손전등&quot;)
    {
        properties[&quot;IsFlashlightOn&quot;] = false;
    }
}

public void SetFlashLightOn(bool isOn)
{
    if (itemData.itemName != &quot;손전등&quot;) return;
    properties[&quot;isFlashlightOn&quot;] = isOn;
    GetComponentInChildren&lt;Light&gt;().enabled = isOn;
}

// UseItem.cs

private void Update()
{
    if (playerItemHandler.currentItem)
    {
        ReloadCharge(playerItemHandler.currentItem);
    }
    UseScannerInUpdate();
    UseFlashlightInUpdate();
}

public void UseItems(ItemInstance item)
{
    switch(item.itemData.itemName)
    {
        case &quot;스캐너&quot;:
            if (Input.GetMouseButtonDown(1)) UseScanner();
            break;
        case &quot;나이프&quot;:
            if (Input.GetMouseButtonDown(0)) UseKnife(item.itemData.attackPower);
            break;
        case &quot;손전등&quot;:
            if (Input.GetMouseButtonDown(1)) UseFlashlight(item);
            break;
        default:
            return;
    }
}

private void UseFlashlightInUpdate()
{
    if (!playerItemHandler.currentItem) return;
    if (playerItemHandler.currentItem.itemData.itemName != &quot;손전등&quot;) return;

    bool isBattery = CanUseBatteryProduct();

    // 켜져있는중 배터리가 모두 소모하면 자동으로 꺼
    if (!isBattery)
    {
        playerItemHandler.currentItem.gameObject.GetComponentInChildren&lt;Light&gt;().enabled = false;
        return;
    }

    if (playerItemHandler.currentItem.Get&lt;bool&gt;(&quot;isFlashlightOn&quot;))
    {
        UseBattery();
    }
}
private void UseFlashlight(ItemInstance item)
{
    if (!inventory || !item || chargeUIOpen || GameState.IsUIOpen || !CanUseBatteryProduct()) return;
    if (item.gameObject.GetComponentInChildren&lt;Light&gt;().enabled)
    {
        Debug.Log(&quot;손전등을 끕니다.&quot;);

    }
    else
    {
        Debug.Log(&quot;손전등을 켭니다.&quot;);
    }
    item.gameObject.GetComponentInChildren&lt;Light&gt;().enabled = !item.gameObject.GetComponentInChildren&lt;Light&gt;().enabled;
    item.Set&lt;bool&gt;(&quot;isFlashlightOn&quot;, item.gameObject.GetComponentInChildren&lt;Light&gt;().enabled);
}</code></pre>
<h4 id="3-ui-업데이트">3. UI 업데이트</h4>
<p><code>R</code>키로 배터리 교체창을 만들었었는데, 실시간으로 몇%인지 업데이트 하는 로직도 넣습니다.</p>
<pre><code class="language-csharp">// UseItem.cs

private void UseBattery()
{
    ItemInstance battery = playerItemHandler.currentItem.Get&lt;ItemInstance&gt;(&quot;Battery&quot;);
    float rate = battery.Get&lt;float&gt;(&quot;batteryUsageRate&quot;);
    rate -= Time.deltaTime;
    battery.Set&lt;float&gt;(&quot;batteryUsageRate&quot;, rate);
    uiManager.SetChargeText();
}

// UIManager.cs

public void SetChargeText()
{
    ItemInstance item = playerItemHandler.currentItem.GetComponent&lt;ItemInstance&gt;();
    ItemInstance charge = null;
    switch (item.itemData.productType)
    {
        case ProductType.BatteryUsing:
            charge = item.Get&lt;ItemInstance&gt;(&quot;Battery&quot;);
            break;
        case ProductType.FuelUsing:
            charge = item.Get&lt;ItemInstance&gt;(&quot;Fuel&quot;);
            break;
        default:
            return;
    }

    int displayCharge = Mathf.Max(0, (int)charge.Get&lt;float&gt;(&quot;batteryUsageRate&quot;));

    if (!chargeSlotPanel) return;
    // 현재 쓰고 있는 배터리는 무조건 0 인덱스로 처음 것만 값이 변하도록 함
    if (!chargeSlotPanel.GetComponentInChildren&lt;TextMeshProUGUI&gt;()) return;
    chargeSlotPanel.GetComponentInChildren&lt;TextMeshProUGUI&gt;().text = displayCharge.ToString() + &quot;%&quot;;
}</code></pre>
<hr>
<h3 id="🎮-중간-결과">🎮 중간 결과</h3>
<p>우클릭으로 껐다 켰다 할 수 있으며 배터리 교체도 가능합니다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/4722d6cc-a70f-4144-a711-ca1fb4098c6d/image.gif width="700">

<hr>
<h3 id="🎞️-애니메이션">🎞️ 애니메이션</h3>
<p>물건을 사용하고 쥐고 있을때의 애니메이션을 넣겠습니다. 전 이미 만들어진 애니메이션 에셋을 수정하여 사용했습니다.</p>
<h4 id="1-만들기">1. 만들기</h4>
<p><code>Window</code> -&gt; <code>Animation</code> -&gt; <code>Animation</code>을 들어가면 Animation창이 뜹니다. 거기서 드롭다운을 선택, <strong>Create New Clip</strong>으로 새로운 Anim파일을 만듭니다.
왼족 상단에 녹화버튼이 있죠? 그 버튼을 누르면 이제 편집할 수가 있게 됩니다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/6f3a570d-1adf-48d4-91e9-9cf991a6164f/image.png width="300">

<h4 id="2-편집">2. 편집</h4>
<p>편집은 간단하지만 매우 번거롭습니다. 만들고 싶은 애니메이션을 직접 제작하는 것인데요. <strong>무언가를 쥐고 있는 애니메이션</strong>을 만들기 위해 Player의 스켈레톤을 직접 조작합니다. 원하는 프레임에서 스켈레톤을 바꾸면 Key가 생겨 해당 지점에서는 그 위치로 옮겨가는 애니메이션을 진행합니다.
먼저 칼을 들고, 칼을 쓰는 애니메이션을 보여드리겠습니다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/1fda6854-0b50-468d-9a15-e6fdfa5aa053/image.gif width="600"> 

<h4 id="3-animator-controller">3. Animator Controller</h4>
<p>Project창에서 <code>Create</code> -&gt; <code>Animator Controller</code>을 하고 이름을 <strong>PlayerAnimator</strong>로 합시다. 들어가면 그래프같은 창이 뜰겁니다.
이 각 노드들은 현재 캐릭터가 취하고 있는 행동을 말합니다. 지금은 아무것도 없으니, 캐릭터가 <strong>아무것도 하지 않는 가만히 있는 상태</strong>를 뜻하는 <strong>Idle</strong>를 추가합시다. 해당 창에서 <code>create State</code> -&gt; <code>Empty</code>를 선택해주세요.
생성된 New State의 이름을 Idle로 바꾸고, Motion에는 가만히 서 있는 포즈 애니메이션을 넣습니다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/850e1f47-8221-47e0-9a94-d4d961e11bab/image.png width="500">

<p>물건을 드는 것과 쓰고 있는 것 또한 <code>HoldingItem</code>과 <code>OnUseKnife</code>로 이름을 설정하여 Empty 두 개를 생성합니다. 그리고 똑같이 Motion에 만든 애니메이션을 넣습니다.</p>
<h4 id="4-노드-연결하기">4. 노드 연결하기</h4>
<p>이젠 여기선 다음을 연결하기 위한 조건이 필요합니다.
물건을 들게 되면 Idle에서 HoldingItem으로 바뀌고, 나이프를 쓰면 HoldingItem에서 OnUseKnife로 행동이 바뀌겠죠?
그 반대로도 행동이 바껴야하니 노드에서 우클릭을 하고 <strong>Make Transition</strong>을 선택하여 나온 화살표를 다음에 행동할 노드에 연결합시다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/4844f72b-3089-4686-8bdc-891f88b4c372/image.png width="500">

<h4 id="5-조건-걸기">5. 조건 걸기</h4>
<p>이제 조건을 추가합시다. Idle에서 HodingItem을 가려면 <strong>현재 아이템을 들고 있다는 Bool값</strong>이 필요하고, HodingItem에서 OnUseKnife를 하려면 <strong>나이프를 들고 있으며 현재 쓰는 중 이라는 Trigger</strong>가 필요합니다.
Trigger는 단발성 행동에 좋습니다. 한 번만 하고 끝내거든요.</p>
<p>왼쪽의 Parameters창에서 +버튼을 눌러 Bool과 Trigger 파라미터를 추가합시다.</p>
<img src= http://velog.velcdn.com/images/ye-seong/post/406f3edd-f060-47e3-8410-a8de105ac46e/image.png width = "400">

<p>그 다음 Idle-&gt;HoldingItem 화살표를 클릭하여 Conditions에 <strong>Holding을 True</strong>로 설정하여 추가합니다.
HoldingItem-&gt;OnUseKnife는 <strong>DoUseKnife</strong>를 추가해주세요.
HoldingItem-&gt;Idle은 <strong>Holding을 False</strong>로 설정하여 추가합니다.</p>
<p>각 화살표의 Inspector에 <strong>Has Exit Time</strong>이 있는데요. 이것은 애니메이션 전환 타이밍을 결정하는 중요한 설정입니다.</p>
<ul>
<li>체크 시: 애니메이션이 일정 시간 재생된 후에 전환하고 Exit Time까지 기다린 후 조건 확인함. 애니메이션이 자연스럽게 끝나고 전환. 공격, 스킬, 점프 등 <strong>완료되어야 하는</strong> 애니메이션에 사용.</li>
<li>체크 해제 시: 조건이 맞으면 즉시 전환하며 애니메이션 중간에도 바로 전환 가능. 이동, 방향 전환 등 <strong>즉시 반응</strong>해야 하는 애니메이션에 사용.</li>
</ul>
<p>OnUseKnife -&gt; HoldingItem 의 Has Exit Time만 체크하고 나머지는 해제해주세요.</p>
<hr>
<h3 id="🎞️-스크립트에서-애니메이션-제어">🎞️ 스크립트에서 애니메이션 제어</h3>
<p>이제 우리에겐 <strong>아이템을 들었다</strong>와 <strong>아이템을 쓴다</strong>라는 코드를 찾아가는 일만 남았습니다.</p>
<pre><code class="language-csharp">// PlayerItemHandler.cs

private Animator animator;

private void Start()
{
    animator = GetComponent&lt;Animator&gt;();
}

public void ClearHolding()
{
    // 기존 코드...
    animator.SetBool(&quot;Holding&quot;, false);
}

private void SetHoldingItem(ItemInstance item, Transform transform)
{
    // 기존 코드...
    animator.SetBool(&quot;Holding&quot;, true);
    // 기존 코드...
}

// UseItem.cs

private void UseKnife(float damage)
{
    playerController.animator.SetTrigger(&quot;DoUseKnife&quot;);
    // 기존 코드...
}</code></pre>
<hr>
<h3 id="🛠️-오브젝트-맞추기">🛠️ 오브젝트 맞추기</h3>
<p>이대로 그냥 실행하게 되면 물건이 어색하게 손에 쥐어집니다. 그러니 물건을 쥐고 있는 Player를 <code>Scene</code>창에서 자연스럽게 쥐게끔 해당 오브젝트를 조정해줍니다.
그리고 조정 후, Inspector에 있는 Transform정보를 Prefab에 적용하면 자연스럽게 쥐게 됩니다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/9c10dcc9-65bf-4744-bc16-cca8c6ca91f7/image.png width="600">

<p>다른 방법이 있기는 하나...!! 전 이게 더 자연스러워서 이 방법으로 했습니다.</p>
<hr>
<h3 id="🎮-최종-결과">🎮 최종 결과</h3>
<p>이제 물건을 쥐고 사용해보세요! 다른 물건들도 만들어 보세요.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/e8b670e0-a7b0-430b-b0cb-69e6d1702e30/image.gif width="700">

<img src=https://velog.velcdn.com/images/ye-seong/post/c6128511-1487-41d4-8c7a-47826fc565bd/image.gif width="700">

<hr>
<h3 id="📚-오늘의-배운-점">📚 오늘의 배운 점</h3>
<ul>
<li>손전등 구현</li>
<li>Animation 및 Animator Controller 활용</li>
</ul>
<hr>
<h3 id="🎯-다음-계획">🎯 다음 계획</h3>
<p>다음 글에서는:</p>
<ol>
<li>모듈형 방어구 구현</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity / C#] 적 만들기 #5]]></title>
            <link>https://velog.io/@ye-seong/Unity-C-%EC%A0%81-%EB%A7%8C%EB%93%A4%EA%B8%B0-5</link>
            <guid>https://velog.io/@ye-seong/Unity-C-%EC%A0%81-%EB%A7%8C%EB%93%A4%EA%B8%B0-5</guid>
            <pubDate>Fri, 11 Jul 2025 11:52:43 GMT</pubDate>
            <description><![CDATA[<h2 id="📋-목차">📋 목차</h2>
<ol>
<li>날아다니는 AI</li>
<li>중간 결과</li>
<li>유도 미사일</li>
<li>최종 결과</li>
<li>오늘의 배운 점</li>
<li>다음 계획</li>
</ol>
<hr>
<h3 id="🪽-날아다니는-ai">🪽 날아다니는 AI</h3>
<p>지상을 다니는 AI는 <strong>NavMesh</strong>를 이용했지만 공중 AI는 보통 NavMesh를 쓰지 않습니다.
이 <a href="https://velog.io/@ye-seong/Unity-C-%EC%A0%81-%EB%A7%8C%EB%93%A4%EA%B8%B0-1">게시글</a>처럼 <strong>Transform을 직접 조작</strong>하는 것으로 AI를 구현하게 됩니다.</p>
<h4 id="1-타입-구분">1. 타입 구분</h4>
<pre><code class="language-csharp">//Enemy.cs

public AIType aiType;

public enum AIType
{
    None,
    Ground,
    Flying,
    Swimming
}</code></pre>
<h4 id="2-이동-구현">2. 이동 구현</h4>
<pre><code class="language-csharp">// Enemy.cs

public void PerformPatrol()
{
    switch(aiType)
    {
        case AIType.Ground:
            patrolCoroutine = StartCoroutine(WaitForPatrolCoroutine());
            break;
        case AIType.Flying:
            patrolCoroutine = StartCoroutine(WaitForPatrolCoroutineFlying());
            break;
        case AIType.Swimming:
            break;
        default:
            break;
    }
}

IEnumerator WaitForPatrolCoroutineFlying()
{
    while (true)
    {
        // 현재 목표 위치
        Vector3 targetPosition = SetNewRandomPosition();

        while (Vector3.Distance(transform.position, targetPosition) &gt; 0.5f)
        {
            // 목표 위치와 현재 위치를 연결하는 방향
            Vector3 direction = (targetPosition - transform.position).normalized;
            if (direction != Vector3.zero)
            {
                // 해당 방향을 바라보게됨
                Quaternion targetRotation = Quaternion.LookRotation(direction);
                // Slerp로 부드럽게 바라봄
                transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, enemyData.rotationSpeed * Time.deltaTime);
            }

            // MoveTowards는 (a,b,c)일때, a부터 b까지 c의 속도로 이동해주는 함수
            transform.position = Vector3.MoveTowards(transform.position, targetPosition, enemyData.moveSpeed * Time.deltaTime);
            yield return null;
        }

        // 도착할 경우 2초 대기
        yield return new WaitForSeconds(2f);
    }
}

private Vector3 SetNewRandomPosition()
{
    Vector3 randomDirection = UnityRandom.insideUnitSphere * patrolRadius;
    randomDirection += centerPoint;
    return randomDirection;
}</code></pre>
<h4 id="3-chasestate-수정">3. ChaseState 수정</h4>
<pre><code class="language-csharp">// ChaseState.cs

private void Moving()
{
    if (enemy.IsInCenter(enemy.transform))
    {
        switch(enemy.aiType)
        {
            case AIType.Ground:
                enemy.agent.destination = enemy.target.transform.position;
                break;
            case AIType.Flying:
                FlyingTypeMoving();
                break;
            case AIType.Swimming:
                break;
            default:
                break;
        }
    }
}

private void FlyingTypeMoving()
{
    Vector3 direction = (enemy.target.transform.position - enemy.transform.position).normalized;
    if (direction != Vector3.zero)
    {
        Quaternion targetRotation = Quaternion.LookRotation(direction);
        enemy.transform.rotation = Quaternion.Slerp(enemy.transform.rotation, targetRotation, enemy.enemyData.rotationSpeed * Time.deltaTime);
        enemy.transform.position = Vector3.MoveTowards(enemy.transform.position, enemy.target.transform.position, enemy.enemyData.moveSpeed * Time.deltaTime);
    }
}</code></pre>
<h4 id="4-attackstate-수정">4. AttackState 수정</h4>
<p><code>Attackstate</code>에선 <code>agent.isStopped</code>를 GroundType만 적용하게 바꿔주세요. 그리고 <code>EnemyAttackSystem</code>도 수정합시다.</p>
<pre><code class="language-csharp">// EnemyAttackSystem.cs

public void PerformAttackByName(string name)
{
    switch(name)
    {
        case &quot;Spitter&quot;:
            ThrowProjectile(enemy.attackTransform);
            break;
        case &quot;Bomber&quot;:
            DropBomb(enemy.attackTransform);
            break;
        default:
            break;
    }
}

private void DropBomb(Transform attackTransform)
{
    Debug.Log(&quot;폭탄 투하!!!&quot;);
}

public void LookAtPlayer()
{
    Vector3 direction = (enemy.target.transform.position - transform.position).normalized;
    direction.y = 0; 
    Quaternion lookRotation = Quaternion.LookRotation(direction);
    switch(enemy.aiType)
    {
        case AIType.Ground:
            transform.rotation = Quaternion.Slerp(transform.rotation, lookRotation, enemy.agent.angularSpeed * Time.deltaTime);
            break;
        case AIType.Flying:
            transform.rotation = Quaternion.Slerp(enemy.transform.rotation, lookRotation, enemy.enemyData.rotationSpeed * Time.deltaTime);
            break;
        case AIType.Swimming:
            break;
        default:
            break;
    }
}</code></pre>
<hr>
<h3 id="🎮-중간-결과">🎮 중간 결과</h3>
<p>순찰 중일때 공중에서 자유롭게 다닙니다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/594cb1a7-f7c3-4303-9cd7-2abd1cbcf6fa/image.gif width="800">

<p>공격 범위내에 있으면 폭탄을 투하하고, 범위에서 벗어날땐 <strong>ChaseState</strong>인 것을 볼 수 있습니다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/d77b39e9-5878-4e33-b022-be439f82e7ae/image.gif width="800">

<hr>
<h3 id="💣-유도-미사일">💣 유도 미사일</h3>
<p>게임에 자주 나오는 <strong>유도</strong> 기능입니다. 탄환이 플레이어를 쫓아가고, 일정 시간이 지나면 공중에서 터지거나 플레이어에 닿으면 데미지를 입고 터지죠. 구현해볼까요?
다른 오브젝트에도 쓰일 수도 있으니, 함수명을 <code>ThrowGuidedMissile</code>로 바꿉시다.</p>
<pre><code class="language-csharp">// EnemyAttackSystem.cs

public void PerformAttackByName(string name)
{
    switch(name)
    {
        case &quot;Spitter&quot;:
            ThrowProjectile(enemy.attackTransform);
            break;
        case &quot;Bomber&quot;:
            // 이미 발사한 미사일이 있다면 사라질때까지 스폰하지 않음
            if (!bomberMissile)
            {
                ThrowGuidedMissile(enemy.attackTransform);
            }
            break;
        default:
            break;
    }
}

private void ThrowGuidedMissile(Transform attackTransform)
{
    Debug.Log(&quot;폭탄 투하!!!&quot;);

    bomberMissile = Instantiate(enemy.projectilePrefab, attackTransform.position, attackTransform.rotation);
    ProjectileManager projectileManager = bomberMissile.GetComponent&lt;ProjectileManager&gt;();
    if (projectileManager)
    {
        projectileManager.damage = enemy.enemyData.damage;
        projectileManager.startFollow = true;
        projectileManager.enemy = enemy; 
    }
}

// ProjectileManager.cs

// 총알 유지 시간
public float lifeTime;

[HideInInspector] public float damage = 10f;
[HideInInspector] public bool startFollow = false;
[HideInInspector] public Enemy enemy;

private float currentTime = 0f;
private void Update()
{
    if (startFollow)
    {
        currentTime += Time.deltaTime;
        if (currentTime &gt;= lifeTime)
        {
            Destroy(gameObject);
            return;
        }
        FollowTarget();
    }
}

public void FollowTarget()
{
    Vector3 direction = (enemy.target.transform.position - transform.position).normalized;

    if (direction != Vector3.zero &amp;&amp; enemy)
    {
        Quaternion targetRotation = Quaternion.LookRotation(direction);
        transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, enemy.projectileSpeed * Time.deltaTime);
        // 유도탄이 발밑으로 가지 않기 위해  + Vector3.up * 1.5f 를 해줌
        transform.position = Vector3.MoveTowards(transform.position, enemy.target.transform.position + Vector3.up * 1.5f, enemy.projectileSpeed * Time.deltaTime);
    }
}</code></pre>
<hr>
<h3 id="🎮-최종-결과">🎮 최종 결과</h3>
<p>공격 범위내에 들어오면 비교적 느린 속도로 유도탄이 날아오고, 일정 시간이 지나면 제자리에서 터지게끔 사라집니다.
나중에 에셋을 적용시키고 더 느리게 한 후, 비틀거리는 애니메이션을 추가한다면 더 보기좋을것 같네요!</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/47a9e419-d7e1-4d36-a2cc-858392618073/image.gif width="700">

<hr>
<h3 id="📚-오늘의-배운-점">📚 오늘의 배운 점</h3>
<ul>
<li><strong>MoveTowards</strong>를 이용한 공중 이동</li>
<li>유도탄 구현</li>
</ul>
<hr>
<h3 id="🎯-다음-계획">🎯 다음 계획</h3>
<p>다음 글에서는:</p>
<ol>
<li>손전등 구현</li>
<li>아이템을 든 모션 구현<ul>
<li>손전등</li>
<li>나이프</li>
</ul>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity / C#] 적 만들기 #4]]></title>
            <link>https://velog.io/@ye-seong/Unity-C-%EC%A0%81-%EB%A7%8C%EB%93%A4%EA%B8%B0-4</link>
            <guid>https://velog.io/@ye-seong/Unity-C-%EC%A0%81-%EB%A7%8C%EB%93%A4%EA%B8%B0-4</guid>
            <pubDate>Fri, 11 Jul 2025 09:17:50 GMT</pubDate>
            <description><![CDATA[<h2 id="📋-목차">📋 목차</h2>
<ol>
<li>투사체 발사</li>
<li>표적을 향해 발사</li>
<li>데미지 주기</li>
<li>최종 결과</li>
<li>오늘의 배운 점</li>
<li>다음 계획</li>
</ol>
<hr>
<h3 id="🔫-투사체-발사">🔫 투사체 발사</h3>
<p>이번에는 적의 공격특성과 능력을 본격적으로 만들어보도록 합니다. 만들기로 계획한 몹 중 <code>투사체 발사</code>가 있는데요. 해당 로직을 따로 따로 만들어 세분화하고, 나중에 재활용하기 쉽게 합시다.</p>
<p>투사체는 본래 있던 <strong>Attack Point</strong>에서 공격 탄환을 발사하게 할 겁니다. 먼저 <strong>Spitter</strong>의 능력을 살펴봅시다. 해당 몹은 <strong>산성 탄환 발사</strong>를 하므로 마치 대포처럼 쏘는 게 좋겠죠?</p>
<p>적의 공격 시스템을 담당할 <code>EnemyAttackSystem</code> 스크립트를 생성합니다.</p>
<h4 id="1-물체를-앞으로-던지기">1. 물체를 앞으로 던지기</h4>
<pre><code class="language-csharp">// EnemyAttackSystem

private Enemy enemy;
private void Awake()
{
    enemy = GetComponent&lt;Enemy&gt;();
}

// 적이 공격을 수행하는 로직
public void PerformAttackByName(string name)
{
    switch(name)
    {
        case &quot;Spitter&quot;:
            ThrowProjectile(enemy.attackTransform);
            break;
        default:
            break;
    }
}    
// AttackTransform를 기점으로 투사체를 던지는 로직
private void ThrowProjectile(Transform attackTransform)
{
    // 투사체를 생성하고 초기화하는 로직
    GameObject projectile = Instantiate(enemy.projectilePrefab, attackTransform.position, attackTransform.rotation);
    Rigidbody rb = projectile.GetComponent&lt;Rigidbody&gt;();
    if (rb != null)
    {
        rb.AddForce(attackTransform.forward * enemy.projectileSpeed, ForceMode.VelocityChange);
    }
}</code></pre>
<h4 id="2-쿨타임마다-공격-시행">2. 쿨타임마다 공격 시행</h4>
<pre><code class="language-csharp">[Header(&quot;Projectile Settings&quot;)] 
public GameObject projectilePrefab;
public float projectileSpeed = 10f;

private EnemyAttackSystem attackSystem;

protected override void Start()
{
    attackSystem = GetComponent&lt;EnemyAttackSystem&gt;();
}

public void PerformAttack()
{
    if (!attackSystem) return;
    StartCoroutine(AttackWithCooldown());
}
IEnumerator AttackWithCooldown()
{
    Debug.Log(&quot;공격합니다~&quot;);
    if (attackSystem)
    {
        attackSystem.PerformAttackByName(enemyData.enemyName);
    }
    yield return new WaitForSeconds(enemyData.attackCooldown);
}</code></pre>
<h4 id="3-실행">3. 실행</h4>
<img src=https://velog.velcdn.com/images/ye-seong/post/aa9ef289-5efa-484d-8e7e-95ec6b8edf7e/image.gif width="700">

<hr>
<h3 id="🎯-표적을-향해-발사">🎯 표적을 향해 발사</h3>
<h4 id="1-방향-설정">1. 방향 설정</h4>
<p>현재 <code>forward</code>를 이용하므로, 적이 바라보는 방향으로만 구체가 날아갑니다. 공격을 시행했을때는 플레이어를 보는 것으로 계속 고정시킵시다.</p>
<pre><code class="language-csharp">//EnemyAttackSystem.cs

private void Update()
{
    if (enemy.currentState.CurrentStateType == StateType.Attack)
    {
        LookAtPlayer();
    }
}

public void LookAtPlayer()
{
    Vector3 direction = (enemy.target.transform.position - transform.position).normalized;
    direction.y = 0; // Y축 회전을 방지하기 위해 Y값을 0으로 설정
    Quaternion lookRotation = Quaternion.LookRotation(direction);
    transform.rotation = Quaternion.Slerp(transform.rotation, lookRotation, Time.deltaTime * enemy.agent.angularSpeed);
}</code></pre>
<h4 id="2-state-조건-수정">2. State 조건 수정</h4>
<p>계속해서 따라오는 것을 고치기 위해 <code>ChaseState</code>일때 자신의 위치가 추적범위 이상에서 멀어지면 움직이지 못하는 것으로 합니다.
이렇게 하면 공격 범위내에 플레이어가 있을시, 공격은 하나 더이상 쫓아오진 않습니다.</p>
<pre><code class="language-csharp">// ChaseState.cs

public override void Update()
{

    if (!enemy.IsInDistance(enemy.target.transform))
    {
        enemy.ChangeState(new PatrolState(enemy));
    }
    else if (enemy.IsInAttackRange(enemy.target.transform))
    {
        enemy.ChangeState(new AttackState(enemy));
    }
    Moving();
}

private void Moving()
{
    if (enemy.IsInCenter(enemy.transform))
    {
        enemy.agent.destination = enemy.target.transform.position;
    }
}</code></pre>
<h4 id="3-실행-1">3. 실행</h4>
<p>플레이어 포착하면 몸을 바로 돌려 플레이어를 바라보고, 쫓아옵니다. 또한 공격범위내에 들어오면 공격합니다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/81737b41-df85-4208-b917-ce4eb8b1cf99/image.gif width="700">

<hr>
<h3 id="💔-데미지-주기">💔 데미지 주기</h3>
<p>부딪히면 <strong>사라지고</strong> 플레이어의 <strong>생명력을 깎는</strong> 로직을 짜봅시다. <code>ProjectileManager</code> 스크립트를 생성해주세요.</p>
<pre><code class="language-csharp">// ProjectileManager.cs

public class ProjectileManager : MonoBehaviour
{
    [HideInInspector] public float damage = 10f;

    private void OnTriggerEnter(Collider other)
    {
        if (other.gameObject.CompareTag(&quot;Player&quot;))
        {
            PlayerState playerState = other.gameObject.GetComponent&lt;PlayerState&gt;();
            if (playerState != null)
            {
                playerState.ModifyHealth(-damage);
            }
            Destroy(gameObject); 
        }
        else if (other.gameObject.CompareTag(&quot;Ground&quot;))
        {
            Destroy(gameObject);
        }
    }
}

// EnemyAttackSystem.cs

private void ThrowProjectile(Transform attackTransform)
{
    GameObject projectile = Instantiate(enemy.projectilePrefab, attackTransform.position, attackTransform.rotation);
    Rigidbody rb = projectile.GetComponent&lt;Rigidbody&gt;();
    ProjectileManager projectileManager = projectile.GetComponent&lt;ProjectileManager&gt;();
    if (rb &amp;&amp; projectileManager)
    {
        projectileManager.damage = enemy.enemyData.damage;
        rb.AddForce(attackTransform.forward * enemy.projectileSpeed, ForceMode.VelocityChange);
    }
}</code></pre>
<hr>
<h3 id="🎮-최종-결과">🎮 최종 결과</h3>
<p>데미지를 입으며 탄환이 플레이어의 몸이나 땅에 닿으면 사라지는 모습입니다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/918b64ca-ca41-4de0-a8e9-4b1afc22c9f3/image.gif width="800">

<img src=https://velog.velcdn.com/images/ye-seong/post/ac94ebd3-a459-4a6a-ab20-0e9ca4ebe63a/image.gif width="800">

<hr>
<h3 id="📚-오늘의-배운-점">📚 오늘의 배운 점</h3>
<ul>
<li>투사체 생성 및 발사</li>
<li>TriggetEnter시 투사체 삭제</li>
</ul>
<hr>
<h3 id="🎯-다음-계획">🎯 다음 계획</h3>
<p>다음 글에서는:</p>
<ol>
<li>날아다니는 적 구현</li>
<li>위에서 떨어지는 투사체 공격</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity / C#] 적 만들기 #3]]></title>
            <link>https://velog.io/@ye-seong/Unity-C-%EC%A0%81-%EB%A7%8C%EB%93%A4%EA%B8%B0-3</link>
            <guid>https://velog.io/@ye-seong/Unity-C-%EC%A0%81-%EB%A7%8C%EB%93%A4%EA%B8%B0-3</guid>
            <pubDate>Thu, 10 Jul 2025 09:55:51 GMT</pubDate>
            <description><![CDATA[<h2 id="📋-목차">📋 목차</h2>
<ol>
<li>몹 데이터 설정</li>
<li>적 생성</li>
<li>Enemy 설정</li>
<li>중간 결과</li>
<li>여러 적 스폰</li>
<li>최종 결과</li>
<li>오늘의 배운 점</li>
<li>다음 계획</li>
</ol>
<hr>
<h3 id="👾-몹-데이터-설정">👾 몹 데이터 설정</h3>
<p><code>EnemyData</code>를 담아두는 <code>Resources -&gt; Enemys</code>에 특성에 따른 폴더를 제작할겁니다. 전 <strong>Aerial(비행), Ground(대지), Special(특이)</strong> 로 구분할겁니다. 일단 예시로 몇개만 생성해볼까요?</p>
<h4 id="aerial">Aerial</h4>
<table>
<thead>
<tr>
<th>이름</th>
<th>유형</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>Bomber</td>
<td>폭격형</td>
<td>상공에서 폭탄 투하</td>
</tr>
<tr>
<td>Drone</td>
<td>정찰형</td>
<td>플레이어 추적 및 다른 적 소환</td>
</tr>
<tr>
<td>Swarm</td>
<td>떼공격형</td>
<td>작지만 수십 마리가 몰려옴</td>
</tr>
</tbody></table>
<h4 id="ground">Ground</h4>
<table>
<thead>
<tr>
<th>이름</th>
<th>유형</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>Brute</td>
<td>거대형</td>
<td>느리지만 강력한 공격력</td>
</tr>
<tr>
<td>Crawler</td>
<td>기어다니는 벌레형</td>
<td>빠르고 약하며 떼로 몰려옴</td>
</tr>
<tr>
<td>Spitter</td>
<td>원거리형</td>
<td>산성 탄환 발사</td>
</tr>
<tr>
<td>Stalker</td>
<td>은밀형</td>
<td>투명화 능력, 뒤에서 기습</td>
</tr>
</tbody></table>
<blockquote>
<p>공격력 및 체력 등 자세한 것은 임의로 설정했으므로 생략합니다.
Special은 나중에 다루겠습니다!</p>
</blockquote>
<hr>
<h3 id="👾-적-생성">👾 적 생성</h3>
<p>이전에 계속 시험했던 Enemy는 <code>Brute</code>로 설정하고 진행하겠습니다. 이 몹은 <strong>Forest(숲지형)</strong> 타입으로 숲에서만 스폰하게 할 겁니다. 그렇다면 만들었던 <code>ZoneManager</code>에서 숲지형의 Transform을 넣고 그곳에서만 생성되게 해야겠죠?</p>
<pre><code class="language-csharp">// ZoneManager.cs

[Header(&quot;Forest Zone&quot;)]
public Transform[] forestTransforms;</code></pre>
<p>숲 지형이며 몹이 스폰되는 곳은 여러곳일테니 <strong>Transform형식의 배열</strong>로 선언합니다. 그리고 Hierarchy에서 Map의 자식에 빈 오브젝트의 <strong>Forest</strong>를 넣고 스폰을 원하는 위치로 옮겨주세요. 그리고 ZoneManager의 Inspector에 집어넣습니다.</p>
<p>이제 영역은 지정했으니 스폰 로직이 필요합니다. <code>LivingSpawner</code> 스크립트를 생성해주세요.</p>
<pre><code class="language-csharp">// LivingSpawner.cs

using UnityEngine;
using UnityEngine.AI;
using UnityRandom = UnityEngine.Random;

public class LivingSpawner : MonoBehaviour
{
    [Header(&quot;Spawner Zone&quot;)]
    public ZoneManager zoneManager;

    [Header(&quot;Spawn Settings&quot;)]
    private float spawnRadius = 10f;

    public void SpawnEnemy(Enemy enemy)
    {
        if (!zoneManager || !enemy) return;
        GameObject enemyObject = Instantiate(enemy.enemyData.enemyPrefab);
        Vector3 randomDirection = UnityRandom.insideUnitSphere * spawnRadius;
        randomDirection += enemy.centerPoint;
        NavMeshHit hit;
        NavMesh.SamplePosition(randomDirection, out hit, spawnRadius, NavMesh.AllAreas
    }
}

// GameManager.cs

[Header(&quot;Scripts&quot;)]
public LivingSpawner livingSpawner;

// 테스트용으로 Inspector에서 Enemy를 하나만 추가합시다
public EnemyData[] allEnemies;

private void Start()
{
    if (allEnemies.Length &gt; 0 &amp;&amp; livingSpawner)
    {
        Enemy enemy = Enemy.Create(allEnemies[0]);
        livingSpawner.SpawnEnemy(enemy);
    }
}

// Enemy.cs

// 스폰된 후 Inspector에서 넣은 AttackPoint Collider가 None일 경우를 대비함
private void Awake()
{
    if (!attackPoint)
    {
        attackPoint = GetComponentInChildren&lt;Collider&gt;();
        if (attackPoint)
        {
            attackPoint.enabled = false; // 공격 콜라이더 비활성화
        }
        else
        {
            Debug.LogError(&quot;Attack Point Collider가 설정되지 않았습니다!&quot;);
        }
    }
}
</code></pre>
<hr>
<h3 id="⚙️-enemy-설정">⚙️ Enemy 설정</h3>
<p>전에 맵을 설치하면서 Enemy AI가 약간씩 고장나는 것을 확인할 수 있죠. 그 이유는 플레이어의 벽 오르기 버그를 해결하기 위해 임시 벽 Collider를 설치했는데, Enemy가 그곳을 지나려고 할때 발생하는 문제입니다.</p>
<h4 id="1-layer-설정">1. Layer 설정</h4>
<p>Enemy는 NavMesh의 영역에서만 이동하는데, 그것을 Collider가 막아버리면 Enemy는 지나가지 못합니다. 그러니 Enemy Prefab의 Inspector -&gt; <code>Layer</code>에 들어가 <strong>Add Layer</strong>를 합니다. 그 안에 <strong>Enemy</strong>와 <strong>Wall</strong>을 추가합니다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/ec381980-988b-4cef-8c4d-f8dcf3018663/image.png width="400">
Enemy는 Enemy를, 구조물들의 Collider에는 Wall로 Layer를 설정합니다.

<h4 id="2-layer-collision-matrix-설정">2. Layer Collision Matrix 설정</h4>
<p><code>Edit -&gt; Project Settings -&gt; Physics -&gt; Layer Collision Matrix</code>에 들어갑니다. 그렇다면 Layer들이 있는 체크박스를 볼 수가 있습니다. 거기서 <strong>Wall / Enemy</strong> 체크박스를 해제해주세요.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/fa180bf2-9e63-400e-a0dd-ded741237a49/image.png width="400">

<h4 id="3-충돌-제거">3. 충돌 제거</h4>
<p>Enemy는 AI이기 때문에 물리 판정이 대체로 필요가 없습니다. 그러니 해당 프리팹의 Inspector에서 Rigidbody의 <strong>Is Kinematic</strong>을 <strong>true</strong>로 변경합시다.</p>
<hr>
<h3 id="🎮-중간-결과">🎮 중간 결과</h3>
<p>확인을 위해 코드를 약간 변경하여 적들이 여러개 스폰되게 했습니다. 특정 지역을 중심으로 랜덤한 곳에서 스폰되고, 각자 이동하는 것을 확인할 수 있습니다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/7b4d99c7-9bdc-4494-b7f2-b417498f6654/image.png width="700">

<hr>
<h3 id="👾-여러-적-스폰">👾 여러 적 스폰</h3>
<p>게임에는 한가지의 적만 있지는 않죠. 여러 적들이 있을 겁니다.
이전에 아이템 정보를 배열에 담았듯이 적도 그럴 필요가 있습니다. 그래서 <code>GameManager</code>에서 <code>enemyData</code> 배열을 만들었죠!! EnemyData에서도 <code>environmentType</code>이라는 해당 적이 사는 지역에 대한 정보도 담았습니다. 그것을 기반으로 해볼까요?</p>
<h4 id="1-gamemanager에-enemy-종류-저장">1. GameManager에 Enemy 종류 저장</h4>
<pre><code class="language-csharp">// EnvironmentType.cs

public enum EnvironmentType
{
    None,    
    Forest,    // 숲
    Desert, // 사막
    Cave,    // 동굴
    Beach,    // 해변
}

// GameManager.cs

public ZoneManager zoneManager;

public EnemyData[] allEnemies;

private void Start()
{
    GetAllEnemies();
    zoneManager.SpawnEnemyByZone(allEnemies);
}

private void GetAllEnemies()
{
    allEnemies = Resources.LoadAll&lt;EnemyData&gt;(&quot;Enemies&quot;);
    Array.Sort(allEnemies, (a, b) =&gt; a.enemyID - b.enemyID);
}</code></pre>
<h4 id="2-zone별-enemy-스폰">2. Zone별 Enemy 스폰</h4>
<pre><code class="language-csharp">// zoneManager.cs

using UnityEngine;

public class ZoneManager : MonoBehaviour
{
    [Header(&quot;Scripts&quot;)]
    public LivingSpawner livingSpawner;

    [Header(&quot;Forest Zone&quot;)]
    public GameObject[] forestZones;

    [Header(&quot;Desert Zone&quot;)]
    public GameObject[] desertZones;

    [Header(&quot;Cave Zone&quot;)]
    public GameObject[] caveZones;

    [Header(&quot;Beach Zone&quot;)]
    public GameObject[] beachZones;


    public void SpawnEnemyByZone(EnemyData[] datas)
    {
        if (datas.Length &lt;= 0 || !livingSpawner) return;

        int count = 0;
        Vector3 point = Vector3.zero;

        foreach (EnemyData data in datas)
        {
            if (data.environmentType == EnvironmentType.Forest)
            {
                foreach (GameObject zone in forestZones)
                {
                    count = zone.GetComponent&lt;ZoneController&gt;().SpawnCount(EnvironmentType.Forest);
                    for (int i = 0; i &lt; count; i++)
                    {
                        Debug.Log(&quot;Forest 에서 생성!&quot;);
                        Enemy enemy = Enemy.Create(data);
                        point = zone.GetComponent&lt;Transform&gt;().position;
                        livingSpawner.SpawnEnemyRandomPosition(enemy, point);
                    }
                }
            }
            else if (data.environmentType == EnvironmentType.Desert)
            {
                foreach (GameObject zone in desertZones)
                {
                    count = zone.GetComponent&lt;ZoneController&gt;().SpawnCount(EnvironmentType.Desert);
                    for (int i = 0; i &lt; count; i++)
                    {
                        Debug.Log(&quot;Desert 에서 생성!&quot;);
                        Enemy enemy = Enemy.Create(data);
                        point = zone.GetComponent&lt;Transform&gt;().position;
                        livingSpawner.SpawnEnemyRandomPosition(enemy, point);
                    }
                }
            }
            else if (data.environmentType == EnvironmentType.Cave)
            {
                foreach (GameObject zone in caveZones)
                {
                    count = zone.GetComponent&lt;ZoneController&gt;().SpawnCount(EnvironmentType.Cave);
                    for (int i = 0; i &lt; count; i++)
                    {
                        Enemy enemy = Enemy.Create(data);
                        point = zone.GetComponent&lt;Transform&gt;().position;
                        livingSpawner.SpawnEnemyRandomPosition(enemy, point);
                    }
                }
            }
            else if (data.environmentType == EnvironmentType.Beach)
            {
                foreach (GameObject zone in beachZones)
                {
                    count = zone.GetComponent&lt;ZoneController&gt;().SpawnCount(EnvironmentType.Beach);
                    for (int i = 0; i &lt; count; i++)
                    {
                        Enemy enemy = Enemy.Create(data);
                        point = zone.GetComponent&lt;Transform&gt;().position;
                        livingSpawner.SpawnEnemyRandomPosition(enemy, point);
                    }
                }
            }
        }
    }
}</code></pre>
<h4 id="4-enemy-데이터-및-오브젝트-생성">4. Enemy 데이터 및 오브젝트 생성</h4>
<pre><code class="language-csharp">// Enemy.cs

public static Enemy Create(EnemyData data)
{
    GameObject obj = Instantiate(data.enemyPrefab);
    Enemy enemy = obj.GetComponent&lt;Enemy&gt;();
    enemy.enemyData = data;
    return enemy;
}

// LivingSpawner.cs

[Header(&quot;Spawner Zone&quot;)]
public ZoneManager zoneManager;

[Header(&quot;Spawn Settings&quot;)]
private float spawnRadius = 10f;

public void SpawnEnemyRandomPosition(Enemy enemy, Vector3 centerPoint)
{
    if (!zoneManager || !enemy) return;

    Vector3 randomDirection = UnityRandom.insideUnitSphere * spawnRadius;
    randomDirection += centerPoint;
    enemy.centerPoint = centerPoint;
    NavMeshHit hit;

    if(NavMesh.SamplePosition(randomDirection, out hit, spawnRadius, NavMesh.AllAreas))
    {
        enemy.transform.position = hit.position;
    }
}</code></pre>
<hr>
<h3 id="🎮-최종-결과">🎮 최종 결과</h3>
<p>맵에 각 지형의 위치를 빈 오브젝트로 설정하고, EnemyData에 사는 지형을 지정했습니다. 그럼 해당 Enemy가 Min~Max까지의 수로, 해당 지역 주위에 랜덤으로 스폰되는 모습입니다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/c0bde3e6-9757-4d02-bd8e-49ecf12cf316/image.gif width="700">

<hr>
<h3 id="📚-오늘의-배운-점">📚 오늘의 배운 점</h3>
<ul>
<li><strong>Layer</strong> 활용</li>
<li>조건에 따른 랜덤 스폰</li>
</ul>
<hr>
<h3 id="🎯-다음-계획">🎯 다음 계획</h3>
<p>다음 글에서는:</p>
<ol>
<li>투사체 공격 구현</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity / C#] 맵세팅]]></title>
            <link>https://velog.io/@ye-seong/Unity-C-%EB%A7%B5%EC%84%B8%ED%8C%85</link>
            <guid>https://velog.io/@ye-seong/Unity-C-%EB%A7%B5%EC%84%B8%ED%8C%85</guid>
            <pubDate>Wed, 09 Jul 2025 11:21:23 GMT</pubDate>
            <description><![CDATA[<h2 id="📋-목차">📋 목차</h2>
<ol>
<li>맵설치</li>
<li>Physics Material</li>
<li>벽과 바닥, 계단</li>
<li>오브젝트 렌더링</li>
<li>오늘의 배운 점</li>
<li>다음 계획. 다음 계획</li>
</ol>
<hr>
<h3 id="🧱-맵설치">🧱 맵설치</h3>
<p>1인 개발로 3D 모델링까지 하기에는 부담이 컸습니다. 그러니 Unity에서 제공하는 무료 에셋을 이용해봅시다. <a href="https://assetstore.unity.com/packages/templates/tutorials/unity-learn-3d-game-kit-115747">이곳</a>에서 다운받아 임포트 해주세요.</p>
<h4 id="1-지형-설치">1. 지형 설치</h4>
<p>아직 장황하게 맵을 설치할 필요는 없습니다. 해당 에셋을 다운받았나요? 그러면 빈 오브젝트를 설치하여 <strong>Mesh Renderer, Mesh Filter, Mesh Collider, DefaultGroundMaterial</strong>을 추가하여 기본 바닥을 생성합시다. <code>3DGameKit-&gt;Art-&gt;Models-&gt;Terrains</code>에서 입맛대로 골라주세요.
어느정도의 장애물은 필요하니 <code>Models-&gt;Environment-&gt;Rock</code>에서 돌을 몇개 설치합시다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/4272ef87-c93a-4099-b098-d236c94ccca9/image.png width="500">


<h4 id="2-navmeshsurface">2. NavMeshSurface</h4>
<p>해당 Ground가 있는 오브젝트를 Ground라는 빈오브젝트의 자식들로 둡니다. 그리고 Ground에 <strong>NaveMeshSurface</strong> 컴포넌트를 추가한뒤 CollectObjects를 <strong>Children</strong>으로 하고 Bake합니다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/4f5969e5-9a3a-4cf6-8fec-e4ed74d17ef3/image.png width="400">

<h4 id="3-테스트">3. 테스트</h4>
<img src=https://velog.velcdn.com/images/ye-seong/post/cdb70815-0d04-4c68-a1c9-8ad3425b71ec/image.gif width="700">

<hr>
<h3 id="🥎-physics-material">🥎 Physics Material</h3>
<p>맵과 플레이어가 충돌하면 빗겨가지 않고 그자리에서 버티는 경우가 있습니다. 예를 들면 <code>점프하는 도중 벽에 충돌할때, W키를 누르면 공중에서 버티는 것</code>이 있죠.
그것을 해결하기 위해 <strong>Physics Material</strong>을 Project창에서 생성합시다.
그리고 플레이어, 바닥, 벽에 적용할 해당 Physics Material의 값을 알맞게 설정해주세요.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/4f1ed8bb-0826-407a-ba7e-294b681c9744/image.png width="400">
<img src=https://velog.velcdn.com/images/ye-seong/post/10a74304-e1ae-4a94-a349-057031640740/image.png width="400">
<img src=https://velog.velcdn.com/images/ye-seong/post/a2693937-915e-4cfe-8cd0-0706c470c60e/image.png width="400">

<h4 id="1-dynamic-friction-동적-마찰력">1. Dynamic Friction (동적 마찰력)</h4>
<ul>
<li>움직이는 물체 간의 마찰력</li>
<li>값이 클수록 미끄러짐이 적음</li>
</ul>
<h4 id="2-static-friction-정적-마찰력">2. Static Friction (정적 마찰력)</h4>
<ul>
<li>정지한 물체가 움직이기 시작할 때의 저항력</li>
<li>값이 클 수록 움직이기 어려움</li>
<li>보통 Dynamic Friction보다 높게 설정</li>
</ul>
<h4 id="3-bounciness-탄성력">3. Bounciness (탄성력)</h4>
<ul>
<li>물체가 튀어오르는 정도</li>
</ul>
<h4 id="4-friction-combine-마찰력-결합-방식">4. Friction Combine (마찰력 결합 방식)</h4>
<ul>
<li>두 물체의 마찰력을 어떻게 계산할지<ul>
<li><strong>Average:</strong> 평균값 사용</li>
<li><strong>Minimum:</strong> 더 낮은 값 사용</li>
<li><strong>Maximum:</strong> 더 높은 값 사용</li>
<li><strong>Multiply:</strong> 곱셈 사용</li>
</ul>
</li>
</ul>
<h4 id="5-bounce-combine-탄성력-결합-방식">5. Bounce Combine (탄성력 결합 방식)</h4>
<ul>
<li>두 물체의 탄성력 계산 방식</li>
<li>설정은 위와 동일</li>
</ul>
<hr>
<h3 id="🪨-벽과-바닥-계단">🪨 벽과 바닥, 계단</h3>
<p>플레이어가 자연스럽게 이동하도록 환경에서 <strong>벽</strong>모양대로 Collider를 생성해 <code>Material</code>에 WallPhysicsMat를 넣습니다. 계단에서는 미끄러지지 않게 하기 위해 마찰력이 높은 GroundPhysicsMat를 넣어주세요.</p>
<h4 id="테스트">테스트</h4>
<img src=https://velog.velcdn.com/images/ye-seong/post/973ae9d5-a289-4f14-8db8-a4837db168c1/image.gif width="700">

<hr>
<h3 id="📦️-오브젝트-렌더링">📦️ 오브젝트 렌더링</h3>
<p>간혹가다 벽에 <strong>너무</strong> 가까이에 있으면 렌더링되지 않아 해당 오브젝트를 뚫고 바깥이 보이게 됩니다. 그때는 Player에 있는 <strong>Main Camera</strong>의 Inspector에서 <strong>Clipping Planes -&gt; Near</strong>값을 <code>0.1 또는 0.01</code> 정도로 줄입시다.
그러면 정상적으로 렌더링 될겁니다!!</p>
<hr>
<h3 id="📚-오늘의-배운-점">📚 오늘의 배운 점</h3>
<ul>
<li>Physics Material</li>
<li>Collider로 벽과 계단 판정 나누기</li>
</ul>
<hr>
<h3 id="🎯-다음-계획">🎯 다음 계획</h3>
<p>다음 글에서는:</p>
<ol>
<li>EnemyData 여러개 생성</li>
<li>구역에 따른 적 랜덤 스폰</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity / C#] 적 만들기 #2]]></title>
            <link>https://velog.io/@ye-seong/Unity-C-%EC%A0%81-%EB%A7%8C%EB%93%A4%EA%B8%B0-2</link>
            <guid>https://velog.io/@ye-seong/Unity-C-%EC%A0%81-%EB%A7%8C%EB%93%A4%EA%B8%B0-2</guid>
            <pubDate>Mon, 07 Jul 2025 10:17:53 GMT</pubDate>
            <description><![CDATA[<h2 id="📋-목차">📋 목차</h2>
<ol>
<li>NavMesh</li>
<li>중간 결과</li>
<li>순찰 상태</li>
<li>랜덤으로 이동</li>
<li>최종 결과</li>
<li>오늘의 배운 점</li>
<li>다음 계획</li>
</ol>
<hr>
<h3 id="🧭-navmesh">🧭 NavMesh</h3>
<p>Unity에서는 AI 시스템이 존재합니다. 이것으로 간편하게 AI몹들이 움직이게 할 수 있습니다. 장애물을 알아서 피하게 할 수 있고, 속도나 회전을 모두 설정할 수 있습니다.
자세한 설명은 이 <a href="https://velog.io/@ye-seong/Unity-C-NavMesh-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0">게시글</a>을 확인해주세요!</p>
<h4 id="1-패키지-다운">1. 패키지 다운</h4>
<p><strong>Package Manager</strong>에서 <strong>Unity Registry</strong> -&gt; <code>AI Navigation</code>을 설치합니다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/13af85b6-6770-4b2e-ba68-b95f28201795/image.png width="500">

<h4 id="2-inspector에-추가">2. Inspector에 추가</h4>
<p>자식이 아닌 최상위 부모. 즉 지형지물을 담은 <strong>Map</strong>에서 <code>NavMeshSurface</code> 컴포넌트를 추가합니다. 그리고 <strong>Bake</strong> 해주세요. 그러면 <strong>Nav Mesh Data</strong>에 데이터가 생길겁니다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/b15096d0-678b-4fec-a226-cd6ea5598fe3/image.png width="500">

<p><strong>Bake</strong>를 하고 나면 이러한 파란색이 영역이 뜹니다. 뜨지 않을 경우 오른쪽 상단 gizmo를 클릭해주세요.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/5d711218-4956-4ced-9f45-828c51c5044d/image.png width="600">

<h4 id="3-몹에-적용">3. 몹에 적용</h4>
<p><strong>적 만들기 #1</strong> 에서 플레이어를 따라가는 코드를 작성했죠? 그것은 이제 모두 필요없습니다!
적 프리팹에 <code>Nav Mesh Agent</code> 컴포넌트를 추가합시다. 또 스크립트를 수정합시다.</p>
<pre><code class="language-csharp">// Enemy.cs

private NavMeshAgent agent;

protected override void Start()
{
    agent = GetComponent&lt;NavMeshAgent&gt;();
}

// ChaseState.cs

private void Moving()
{
    // 기존 이동, 회전 코드 모두 지움
    // 현재 이 AI의 목표는 enemy.target임
    enemy.agent.destination = enemy.target.transform.position;
}</code></pre>
<p>그리고 <code>AttackState</code>와 <code>IdleState</code>에서 각각 <strong>Enter()</strong>에는 <code>enemy.agent.isStoppend = true</code>를 넣고, <strong>Exit()</strong>에는 <code>enemy.agent.isStoppend = false</code>를 넣어주시면 됩니다.
true면 이동을 멈추고 false면 이동을 다시 시작합니다.</p>
<hr>
<h3 id="🎮-중간-결과">🎮 중간 결과</h3>
<p>몹이 알아서 지형지물을 피하고 플레이어를 따라가는 모습입니다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/cc89427a-d5a5-485d-a947-4d53c654d115/image.gif width="700">

<hr>
<h3 id="🔍-순찰-상태">🔍 순찰 상태</h3>
<p>이제 플레이어를 감지하지 않고 있을 때 가만히만 있으면 어색하잖아요? 그러니 <code>Idle</code>상태가 아니라 홀로 걸어다니는 <strong>PatrolSate</strong>를 만들 생각입니다.</p>
<h4 id="1-코루틴">1. 코루틴</h4>
<p>각 적들은 순찰을 하는 Waypoint가 존재하며, 그 중 랜덤으로 이동하게 할 겁니다. 그렇다면 NavMesh를 이용해 목표지점으로 이동하고 다음 지점을 변경한뒤 기다리는 로직이 필요합니다.</p>
<pre><code class="language-csharp">//Enemy.cs

public Transform[] waypoints;

// PatrolState에서 코루틴을 종료시키기 위함
[HideInInspector] public Coroutine patrolCoroutine;

public void PerformPatrol()
{
    patrolCoroutine = StartCoroutine(WaitForPatrolCoroutine());
}

IEnumerator WaitForPatrolCoroutine()
{
    // 무한 반복
    while (true)
    {
        // 배열 중 랜덤으로 하나 선택
        int randomIndex = Range(0, waypoints.Length);
        // 해당 포지션으로 이동
        agent.SetDestination(waypoints[randomIndex].position);

        // 도착 전까지는 리턴
        while (agent.pathRending || agent.remainingDistance &gt; 0.5f)
            yield return null;

        // 도착 후 2초 기다림
        yield return new WaitForSeconds(2f);
    }
}

// PatrolState.cs

public override void Enter()
{
    Debug.Log(&quot;적이 순찰 상태로 진입&quot;);
    enemy.PerformPatrol();
}

public override void Update()
{
    if (enemy.StartChase())
    {
        enemy.ChangeState(new ChaseState(enemy));
    }
}

public override void Exit()
{
    Debug.Log(&quot;적이 순찰 행동을 벗어남&quot;);
    enemy.StopCoroutine(enemy.patrolCoroutine);
}</code></pre>
<h4 id="2-transform-적용">2. Transform 적용</h4>
<p>임시로 Empty 오브젝트를 맵에 설치하여 적 오브젝트에 적용시킵니다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/c3f9862f-410f-4c74-bb4d-c4fb23ba0b75/image.png width="300">

<img src=https://velog.velcdn.com/images/ye-seong/post/8fdc5995-dd91-4ce4-9f0b-f6dc74756dc6/image.png width="300">

<h4 id="3-적용-후-확인">3. 적용 후 확인</h4>
<p>이때 적이 생성되면 <strong>PatrolState</strong>로 시작됩니다. 돌아다니다가 플레이어를 만나면 <strong>ChaseState</strong>로 바뀌고 플레이어가 일정 거리를 벗어나면 다시 <strong>PatrolState</strong>로 바뀝니다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/12fa61e4-2a23-479b-a8d2-1664c05e9985/image.gif width="700">

<hr>
<h3 id="🎲-랜덤으로-이동">🎲 랜덤으로 이동</h3>
<p>제가 설계한 게임은 <strong>오픈월드</strong>이기에 AI몹의 포인트를 맵에 지정해주는 것이 힘들뿐더러, 이미 정해져 있으면 부자연스러워 보입니다. 그렇기 때문에 <strong>Random함수</strong>를 이용하여 AI가 이동하게 할 겁니다.</p>
<h4 id="1-순찰-함수-변경">1. 순찰 함수 변경</h4>
<pre><code class="language-csharp">// Enemy.cs

// 하지 않으면 오류가 떠서 명시적 선언을 함
using UnityRandom = UnityEngine.Random;

private Vector3 centerPoint;

protected override void start()
{
    // 처음 스폰된 위치를 이동 반경의 중심으로 설정
    centerPoint = transform.position;
}

IEnumerator WaitForPatrolCoroutine()
{
    while (true)
    {
        Debug.Log(&quot;코루틴 실행&quot;);

        SetNewRandomDestination();

        while (agent.pathPending || agent.remainingDistance &gt; 0.5f)
            yield return null;

        yield return new WaitForSeconds(2f);
    }
}

void SetNewRandomDestination()
{
    // 구형식의 랜덤 좌표
    Vector3 randomDirection = UnityRandom.insideUnitSphere * patrolRadius;

    // 처음 스폰된 위치에 랜덤 좌표를 더함
    randomDirection += centerPoint;

    NavMeshHit hit;

    // 갈 수 있는 근처 NavMesh 좌표로 이동
    if (NavMesh.SamplePosition(randomDirection, out hit, patrolRadius, NavMesh.AllAreas))
    {
        agent.SetDestination(hit.position);
    }
}</code></pre>
<h4 id="2-추적-함수-변경">2. 추적 함수 변경</h4>
<p>만약 중심점에서 플레이어가 일정 반경 이상 멀어지면 더이상 쫓아가지 않게 해야 합니다.</p>
<pre><code class="language-csharp">// Enemy.cs

public bool IsInCenter(Transform target)
{
    return Vector3.Distance(centerPoint, target.position) &lt;= patrolRadius;
}

// ChaseState.cs

public override void Update()
{
    if (enemy.IsInAttackRange(enemy.target.transform))
    {
        enemy.ChangeState(new AttackState(enemy));
    }
    else if (!enemy.IsInDistance(enemy.target.transform) || !enemy.IsInCenter(enemy.target.transform))
    {
        enemy.ChangeState(new PatrolState(enemy));
    }
    Moving();
}</code></pre>
<hr>
<h3 id="🎮-최종-결과">🎮 최종 결과</h3>
<p>순찰을 하다가 플레이어를 감지하면 추적을 하나, 플레이어가 일정 영역을 벗어나면 더이상 쫓지 않습니다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/8217f625-5437-44c4-9d40-8f695df2c2aa/image.gif width="700">

<hr>
<h3 id="📚-오늘의-배운-점">📚 오늘의 배운 점</h3>
<ul>
<li><strong>NavMesh</strong> 활용</li>
<li>랜덤을 이용한 순찰 이동</li>
<li>일정 반경 인식</li>
</ul>
<hr>
<h3 id="🎯-다음-계획">🎯 다음 계획</h3>
<p>다음 글에서는:</p>
<ol>
<li>맵세팅</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity / C#] NavMesh 이해하기]]></title>
            <link>https://velog.io/@ye-seong/Unity-C-NavMesh-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ye-seong/Unity-C-NavMesh-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 07 Jul 2025 09:01:44 GMT</pubDate>
            <description><![CDATA[<p>Unity에서 <strong>AI</strong>를 쓴다면 꼭! 써야할 기능인 <strong>NavMesh</strong>입니다.
이걸 잘 몰라서 여러번 헤맸는데... 정리겸 개념을 알려드리기 위해 글을 씁니다.</p>
<h1 id="navmesh">NavMesh</h1>
<p>Unity에서는 별도의 AI 시스템이 존재합니다. 이것으로 간편하게 AI몹들이 움직이게 할 수 있습니다. 장애물을 알아서 피하게 할 수 있고, 속도나 회전을 모두 설정할 수 있습니다.</p>
<h4 id="1-패키지-다운">1. 패키지 다운</h4>
<p><strong>Package Manager</strong>에서 <strong>Unity Registry</strong> -&gt; <code>AI Navigation</code>을 설치합니다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/13af85b6-6770-4b2e-ba68-b95f28201795/image.png width="500">

<h4 id="2-navmeshsurface-컴포넌트-추가">2. NavMeshSurface 컴포넌트 추가</h4>
<p>자식이 아닌 최상위 부모. 즉 지형지물을 담은 <strong>Map</strong>에서 <code>NavMeshSurface</code> 컴포넌트를 추가합니다. 그리고 <strong>Bake</strong> 해주세요. 그러면 <strong>Nav Mesh Data</strong>에 데이터가 생길겁니다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/b15096d0-678b-4fec-a226-cd6ea5598fe3/image.png width="500">

<p><strong>Bake</strong>를 하고 나면 이러한 파란색이 영역이 뜹니다. 뜨지 않을 경우 오른쪽 상단 gizmo를 클릭해주세요.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/5d711218-4956-4ced-9f45-828c51c5044d/image.png width="600">

<h4 id="3-navmesh-agent-컴포넌트-추가">3. NavMesh Agent 컴포넌트 추가</h4>
<p>움직일 AI 캐릭터에 <code>NavMesh Agent</code> 컴포넌트를 추가합니다.</p>
<hr>
<h2 id="navigation-설정">Navigation 설정</h2>
<p>각 <strong>Agent Type</strong>마다 AI 행동반경을 조정할 수 있는데요. 에디터 상단바에서 <code>Window-&gt;AI-&gt;Navigation</code>을 들어가줍니다.
여러가지가 뜨겠죠? 이것들을 설명해봅시다.</p>
<p><strong>- Name (이름)</strong>
Agent 타입의 이름으로 여러 타입의 AI가 있을 때 구분용입니다. 코드에서 <code>특정 Agent 타입을 지정</code>할 때 사용됩니다.</p>
<p><strong>- Radius (반지름)</strong>
AI 캐릭터의 두께로 <code>측면 충돌 범위</code>를 지정합니다. 좁은 통로를 지날 수 있는지 결정합니다. 작을수록 좁은 곳을 통과 가능하지만, 벽에 파묻힐 위험이 있습니다. 클수록 안전하지만 좁은 곳을 통과하는 것이 불가능합니다.</p>
<p><strong>- Height (높이)</strong>
AI 캐릭터의 키로 <code>수직 충돌 범위</code>를 지정합니다. 낮은 천장 아래 지날 수 있는지 결정합니다. 작을수록 낮은 곳을 통과 가능하고, 클수록 높은 장애물을 감지합니다.</p>
<p><strong>- Step Height (계단 높이)</strong>
<code>한 번에 올라갈 수 있는 계단/단차 높이</code>입니다. 이 값 이하의 단차는 부드럽게 올라가고 이 값을 초과하면 장애물로 인식합니다.</p>
<p><strong>- Max Slope (최대 경사)</strong>
<code>올라갈 수 있는 최대 경사각</code> (도 단위)입니다. 이 각도 이하의 경사는 걸어 올라가고 이 각도를 초과하면 벽으로 인식합니다.</p>
<p><strong>- Drop Height (낙하 높이)</strong>
<code>떨어져도 괜찮은 최대 높이</code>입니다. 이 높이 이하면 뛰어내려서 이동하고 이 높이를 초과하면 다른 경로를 찾습니다.</p>
<p><strong>- Jump Distance (점프 거리)</strong>
<code>수평으로 뛸 수 있는 최대 거리</code>입니다. 작은 틈을 뛰어넘을 수 있는 거리를 알려줍니다. 0이면 점프가 불가능합니다.재시도Claude는 실수를 할 수 있습니다. 응답을 반드시 다시 확인해 주세요.</p>
<hr>
<h2 id="navmeshsurface-설정">NavMeshSurface 설정</h2>
<p>Map의 컴포넌트인 <code>NavMeshSurface</code> 설명입니다.</p>
<p><strong>- Agent Type (에이전트 타입)</strong>
어떤 크기/특성의 AI가 사용할 NavMesh인지 지정합니다. <code>Navigation → Agents에서 정의한 타입들</code> 중 선택하며, 서로 다른 크기의 AI가 다른 경로를 가질 수 있게 합니다.</p>
<p><strong>- Default Area (기본 영역)</strong>
NavMesh의 <code>기본 영역 타입</code>을 설정합니다. AI가 이동할 때의 비용을 결정하며, 여러 영역이 있을 때 AI는 비용이 낮은 곳을 선호합니다. Walkable, Not Walkable, Jump 등이 있습니다.</p>
<p><strong>- Generate Links (링크 생성)</strong>
떨어진 NavMesh 영역들을 <code>자동으로 연결할지</code> 결정합니다. 체크하면 작은 틈이나 낙하지점을 자동으로 점프 연결하고, 체크 해제하면 수동으로 NavMesh Link를 추가해야 합니다.</p>
<p><strong>- Use Geometry (지오메트리 사용)</strong>
NavMesh 생성 시 <code>어떤 지오메트리를 사용할지</code> 선택합니다. Render Meshes는 보이는 모든 지형/건물을 포함하고, Physics Colliders는 Collider가 있는 오브젝트들만 사용합니다.</p>
<p><strong>- Collect Objects (오브젝트 수집)</strong>
NavMesh 생성 시 <code>어떤 범위의 오브젝트를 포함할지</code> 설정합니다. All Game Objects는 Scene의 모든 오브젝트를 검사하고, Children은 현재 오브젝트의 자식들만, Volume은 특정 범위 안의 오브젝트만 검사합니다.</p>
<p><strong>- Include Layers (포함 레이어)</strong>
<code>어떤 레이어의 오브젝트들을 NavMesh에 포함할지</code> 레이어 마스크로 선택합니다. Default, Ground는 지형만 포함하고, Everything은 모든 레이어를 포함합니다.</p>
<p><strong>- NavMesh Data (NavMesh 데이터)</strong>
<code>생성된 NavMesh 정보가 저장되는 곳</code>입니다. 여러 Scene에서 같은 NavMesh를 공유하거나, NavMesh 데이터를 에셋으로 저장할 때 사용합니다.</p>
<hr>
<h2 id="navmesh-agent-설정">NavMesh Agent 설정</h2>
<p>AI캐릭터에 있을 <code>NavMesh Agent</code> 설명입니다.</p>
<p><strong>- Agent Type (에이전트 타입)</strong>
<code>사용할 Agent 타입을 지정</code>합니다. Navigation → Agents에서 정의한 타입 중 선택하며, 각 타입은 서로 다른 이동 특성(크기, 속도, 점프능력 등)을 가집니다.</p>
<p><strong>- Base Offset (기본 오프셋)</strong>
Agent의 <code>기준점에서 실제 바닥까지의 거리</code>를 설정합니다. 캐릭터가 바닥에 정확히 서있도록 조정하는 값으로, 양수는 위로, 음수는 아래로 이동시킵니다.</p>
<p><strong>- Speed (속도)</strong>
Agent가 <code>이동할 때의 최대 속도</code>입니다. 목적지까지 이동하는 기본 속력을 결정하며, 코드에서 동적으로 변경 가능합니다.</p>
<p><strong>- Angular Speed (회전 속도)</strong>
Agent가 <code>방향을 바꿀 때의 회전 속도</code> (도/초)입니다. 값이 클수록 빠르게 회전하고, 값이 작으면 천천히 부드럽게 회전합니다.</p>
<p><strong>- Acceleration (가속도)</strong>
Agent가 <code>속도를 변경할 때의 가속도</code>입니다. 정지 상태에서 최대 속도까지 도달하는 시간과 급정거 시 멈추는 시간을 결정합니다.</p>
<p><strong>- Stopping Distance (정지 거리)</strong>
목<code>적지에 도달했다고 판단하는 거리</code>입니다. 이 거리 내에 들어오면 Agent가 목적지에 도착한 것으로 간주하고 이동을 멈춥니다.</p>
<p><strong>- Auto Braking (자동 제동)</strong>
목적지 근처에서 <code>자동으로 감속</code>할지 결정합니다. 체크하면 목적지 근처에서 부드럽게 멈추고, 체크 해제하면 일정 속도로 이동합니다.</p>
<p><strong>- Radius (반지름)</strong>
Agent의 <code>충돌 반지름</code>을 설정합니다. 다른 Agent나 장애물과의 거리를 유지하며, 값이 클수록 다른 객체들과 멀리 떨어져서 이동합니다.</p>
<p><strong>- Height (높이)</strong>
Agent의 <code>충돌 높이</code>를 설정합니다. 낮은 천장이나 장애물 아래를 지날 수 있는지 판단하는 기준이 됩니다.</p>
<p><strong>- Quality (품질)</strong>
<code>경로 계산의 정밀도</code>를 설정합니다. High Quality는 정확하지만 무겁고, Low Quality는 빠르지만 부정확할 수 있습니다.</p>
<p><strong>- Priority (우선순위)</strong>
다른 Agent들과 <code>충돌 회피 시의 우선순위</code>입니다. 낮은 값일수록 높은 우선순위를 가지며, 다른 Agent들이 이 Agent를 피해서 이동합니다.</p>
<p><strong>- Obstacle Avoidance (장애물 회피)</strong>
<code>동적 장애물 회피 기능을 사용할지</code> 결정합니다. 체크하면 움직이는 장애물이나 다른 Agent들을 실시간으로 피해서 이동합니다.</p>
<p><strong>- Auto Traverse Off Mesh Link (자동 오프메시 링크 횡단)</strong>
NavMesh 간의 <code>연결고리를 자동으로 건드릴지</code> 결정합니다. 체크하면 점프나 낙하 지점을 자동으로 통과하고, 체크 해제하면 Off Mesh Link에 도달했을 때 수동으로 처리해야 합니다.</p>
<p><strong>- Auto Repath (자동 경로 재계산)</strong>
경로가 <code>무효화되었을 때 자동으로 새 경로를 계산할지</code> 결정합니다. 체크하면 장애물이 생기거나 목적지가 변경될 때 자동으로 경로를 다시 찾고, 체크 해제하면 수동으로 경로를 재계산해야 합니다.</p>
<p><strong>- Area Mask (영역 마스크)</strong>
Agent가 <code>이동 가능한 NavMesh 영역을 제한</code>합니다. 선택된 영역에만 경로를 생성하며, 특정 영역을 금지구역으로 설정하거나 특별한 경로만 사용하도록 제한할 수 있습니다.</p>
<hr>
<h1 id="unity-navmesh-주요-함수-가이드">Unity NavMesh 주요 함수 가이드</h1>
<h2 id="navmeshagent-기본-함수">NavMeshAgent 기본 함수</h2>
<h3 id="이동-관련">이동 관련</h3>
<pre><code class="language-csharp">// 목표 지점으로 이동
NavMeshAgent agent = GetComponent&lt;NavMeshAgent&gt;();
agent.SetDestination(targetPosition);

// 이동 중인지 확인
if (agent.pathPending || agent.remainingDistance &gt; 0.1f)
{
    // 아직 이동 중
}

// 목표 지점에 도달했는지 확인
if (!agent.pathPending &amp;&amp; agent.remainingDistance &lt; 0.1f)
{
    // 목표 지점 도달
}

// 이동 중지
agent.Stop();
agent.isStopped = true;

// 이동 재개
agent.isStopped = false;
agent.Resume();</code></pre>
<h3 id="속도-및-회전-제어">속도 및 회전 제어</h3>
<pre><code class="language-csharp">// 이동 속도 설정
agent.speed = 5f;

// 회전 속도 설정
agent.angularSpeed = 120f;

// 가속도 설정
agent.acceleration = 8f;

// 정지 거리 설정
agent.stoppingDistance = 2f;

// 자동 회전 끄기/켜기
agent.updateRotation = false;
agent.updateRotation = true;</code></pre>
<h3 id="경로-관련">경로 관련</h3>
<pre><code class="language-csharp">// 경로 계산
NavMeshPath path = new NavMeshPath();
agent.CalculatePath(targetPosition, path);

// 경로 상태 확인
if (path.status == NavMeshPathStatus.PathComplete)
{
    // 경로 완성됨
}

// 경로 설정
agent.SetPath(path);

// 현재 경로 정보
Vector3[] pathCorners = agent.path.corners;
float pathLength = agent.path.GetCornersNonAlloc(pathCorners);</code></pre>
<hr>
<h2 id="navmesh-유틸리티-함수">NavMesh 유틸리티 함수</h2>
<h3 id="위치-검색">위치 검색</h3>
<pre><code class="language-csharp">// 가장 가까운 NavMesh 포인트 찾기
NavMeshHit hit;
if (NavMesh.SamplePosition(transform.position, out hit, 10f, NavMesh.AllAreas))
{
    Vector3 closestPoint = hit.position;
}

// 특정 영역에서 랜덤 포인트 찾기
Vector3 randomPoint = Random.insideUnitSphere * 10f;
randomPoint += transform.position;
if (NavMesh.SamplePosition(randomPoint, out hit, 10f, NavMesh.AllAreas))
{
    Vector3 validRandomPoint = hit.position;
}</code></pre>
<h3 id="경로-찾기">경로 찾기</h3>
<pre><code class="language-csharp">// 두 점 사이의 경로 계산
NavMeshPath path = new NavMeshPath();
bool pathFound = NavMesh.CalculatePath(startPos, endPos, NavMesh.AllAreas, path);

if (pathFound &amp;&amp; path.status == NavMeshPathStatus.PathComplete)
{
    // 유효한 경로 존재
    Vector3[] corners = path.corners;
}</code></pre>
<h3 id="레이캐스트">레이캐스트</h3>
<pre><code class="language-csharp">// NavMesh 레이캐스트 (장애물 확인)
NavMeshHit hit;
bool blocked = NavMesh.Raycast(transform.position, targetPosition, out hit, NavMesh.AllAreas);

if (blocked)
{
    // 장애물 있음
    Vector3 hitPoint = hit.position;
}</code></pre>
<hr>
<h2 id="navmesh-영역-관리">NavMesh 영역 관리</h2>
<h3 id="영역-설정">영역 설정</h3>
<pre><code class="language-csharp">// 특정 영역만 사용
int walkableArea = 1 &lt;&lt; NavMesh.GetAreaFromName(&quot;Walkable&quot;);
int jumpArea = 1 &lt;&lt; NavMesh.GetAreaFromName(&quot;Jump&quot;);
int combinedAreas = walkableArea | jumpArea;

agent.areaMask = combinedAreas;

// 모든 영역 사용
agent.areaMask = NavMesh.AllAreas;</code></pre>
<h3 id="영역-비용-설정">영역 비용 설정</h3>
<pre><code class="language-csharp">// 영역별 이동 비용 설정
NavMeshAgent agent = GetComponent&lt;NavMeshAgent&gt;();
agent.SetAreaCost(NavMesh.GetAreaFromName(&quot;Water&quot;), 3f);
agent.SetAreaCost(NavMesh.GetAreaFromName(&quot;Mud&quot;), 2f);</code></pre>
<hr>
<h2 id="고급-기능">고급 기능</h2>
<h3 id="off-mesh-link-처리">Off-Mesh Link 처리</h3>
<pre><code class="language-csharp">// Off-Mesh Link 사용 중인지 확인
if (agent.isOnOffMeshLink)
{
    // 수동으로 Off-Mesh Link 완료
    agent.CompleteOffMeshLink();
}

// Off-Mesh Link 자동 처리 끄기
agent.autoTraverseOffMeshLink = false;</code></pre>
<h3 id="장애물-회피">장애물 회피</h3>
<pre><code class="language-csharp">// 장애물 회피 설정
agent.obstacleAvoidanceType = ObstacleAvoidanceType.HighQualityObstacleAvoidance;
agent.avoidancePriority = 50; // 0(높음) ~ 99(낮음)
agent.radius = 0.5f;
agent.height = 2f;</code></pre>
<h3 id="동적-navmesh-업데이트">동적 NavMesh 업데이트</h3>
<pre><code class="language-csharp">// NavMesh 베이킹 (런타임)
NavMeshBuilder.BuildNavMesh();

// NavMesh 데이터 업데이트
NavMeshHit hit;
if (NavMesh.SamplePosition(transform.position, out hit, 1f, NavMesh.AllAreas))
{
    // 새로운 NavMesh 데이터 사용
}</code></pre>
<hr>
<h2 id="유용한-속성들">유용한 속성들</h2>
<h3 id="상태-확인">상태 확인</h3>
<pre><code class="language-csharp">// 현재 상태 확인
bool isMoving = agent.velocity.magnitude &gt; 0.1f;
bool hasPath = agent.hasPath;
bool isOnNavMesh = agent.isOnNavMesh;
bool pathPending = agent.pathPending;

// 거리 정보
float remainingDistance = agent.remainingDistance;
float stoppingDistance = agent.stoppingDistance;</code></pre>
<h3 id="위치-정보">위치 정보</h3>
<pre><code class="language-csharp">// 현재 위치 정보
Vector3 currentPosition = agent.transform.position;
Vector3 destination = agent.destination;
Vector3 nextPosition = agent.nextPosition;
Vector3 velocity = agent.velocity;</code></pre>
<hr>
<h2 id="실전-예제">실전 예제</h2>
<h3 id="기본-적-ai-이동">기본 적 AI 이동</h3>
<pre><code class="language-csharp">public class EnemyAI : MonoBehaviour
{
    private NavMeshAgent agent;
    private Transform target;

    void Start()
    {
        agent = GetComponent&lt;NavMeshAgent&gt;();
        target = GameObject.FindGameObjectWithTag(&quot;Player&quot;).transform;
    }

    void Update()
    {
        if (target != null)
        {
            agent.SetDestination(target.position);
        }
    }
}</code></pre>
<h3 id="순찰-ai">순찰 AI</h3>
<pre><code class="language-csharp">public class PatrolAI : MonoBehaviour
{
    private NavMeshAgent agent;
    public Transform[] waypoints;
    private int currentWaypoint = 0;

    void Start()
    {
        agent = GetComponent&lt;NavMeshAgent&gt;();
        if (waypoints.Length &gt; 0)
        {
            agent.SetDestination(waypoints[0].position);
        }
    }

    void Update()
    {
        if (!agent.pathPending &amp;&amp; agent.remainingDistance &lt; 0.5f)
        {
            currentWaypoint = (currentWaypoint + 1) % waypoints.Length;
            agent.SetDestination(waypoints[currentWaypoint].position);
        }
    }
}</code></pre>
<h3 id="랜덤-이동">랜덤 이동</h3>
<pre><code class="language-csharp">public class RandomMovement : MonoBehaviour
{
    private NavMeshAgent agent;
    public float wanderRadius = 10f;

    void Start()
    {
        agent = GetComponent&lt;NavMeshAgent&gt;();
        SetRandomDestination();
    }

    void Update()
    {
        if (!agent.pathPending &amp;&amp; agent.remainingDistance &lt; 0.5f)
        {
            SetRandomDestination();
        }
    }

    void SetRandomDestination()
    {
        Vector3 randomDirection = Random.insideUnitSphere * wanderRadius;
        randomDirection += transform.position;

        NavMeshHit hit;
        if (NavMesh.SamplePosition(randomDirection, out hit, wanderRadius, NavMesh.AllAreas))
        {
            agent.SetDestination(hit.position);
        }
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity / C#] 적 만들기 #1]]></title>
            <link>https://velog.io/@ye-seong/Unity-C-%EC%A0%81-%EB%A7%8C%EB%93%A4%EA%B8%B0-1</link>
            <guid>https://velog.io/@ye-seong/Unity-C-%EC%A0%81-%EB%A7%8C%EB%93%A4%EA%B8%B0-1</guid>
            <pubDate>Thu, 03 Jul 2025 09:09:39 GMT</pubDate>
            <description><![CDATA[<h2 id="📋-목차">📋 목차</h2>
<ol>
<li>AI 상태 분류</li>
<li>플레이어 감지</li>
<li>중간 결과 1</li>
<li>추적 상태</li>
<li>중간 결과 2</li>
<li>공격 판정</li>
<li>최종 결과</li>
</ol>
<hr>
<h3 id="🤖-ai-상태-분류">🤖 AI 상태 분류</h3>
<p>AI에게는 여러 행동들이 있습니다. 일단 행동들의 부모가 될 추상 클래스 <code>EnemyState</code>스크립트와 <code>IdleState(대기), PatrolState(순찰), ChaseState(추적), AttackState(공격)</code>스크립트를 생성합니다.</p>
<pre><code class="language-csharp">// EnemyState.cs

public abstract class EnemyState
{
    protected Enemy enemy;

    public EnemyState(Enemy enemy)
    {
        this.enemy = enemy;
    }

    public abstract void Enter();
    public abstract void Update();
    public abstract void Exit();
}

// IdleState.cs

public class IdleState : EnemyState
{
    // 생성자 문법
    // public 생성자(매개변수) : 부모생성자호출 {생성자 내용}
    // 생성자 내용이 빈 이유는 부모 생성자가 이미 다 해줘서
    public IdleState(Enemy enemy) : base(enemy) { }
    public override void Enter()
    {
        Debug.Log(&quot;적이 대기 상태로 진입&quot;);
    }

    public override void Update()
    {
        Debug.Log(&quot;적이 대기 행동을 하는 중&quot;);
    }

    public override void Exit()
    {
        Debug.Log(&quot;적이 대기 행동을 벗어남&quot;);
    }
}

// PatrolState.cs

public class PatrolState : EnemyState
{
    public PatrolState(Enemy enemy) : base(enemy) { }
    public override void Enter()
    {
        Debug.Log(&quot;적이 순찰 상태로 진입&quot;);
    }

    public override void Update()
    {
        Debug.Log(&quot;적이 순찰 행동을 하는 중&quot;);
    }

    public override void Exit()
    {
        Debug.Log(&quot;적이 순찰 행동을 벗어남&quot;);
    }
}

// ChaseState.cs

public class ChaseState : EnemyState
{
    public ChaseState(Enemy enemy) : base(enemy) { }
    public override void Enter()
    {
        Debug.Log(&quot;적이 추적 상태로 진입&quot;);
    }

    public override void Update()
    {
        Debug.Log(&quot;적이 추적 행동을 하는 중&quot;);
    }

    public override void Exit()
    {
        Debug.Log(&quot;적이 추적 행동을 벗어남&quot;);
    }
}

// AttackState.cs

public class AttackState : EnemyState
{
    public AttackState(Enemy enemy) : base(enemy) { }
    public override void Enter()
    {
        Debug.Log(&quot;적이 공격 상태로 진입&quot;);
    }

    public override void Update()
    {
        Debug.Log(&quot;적이 공격 행동을 하는 중&quot;);
    }

    public override void Exit()
    {
        Debug.Log(&quot;적이 공격 행동을 벗어남&quot;);
    }
}

// Enemy.cs

private EnemyState currentState;

protected override void Start()
{
    // 처음은 대기상태
    ChangeState(new IdleState(this));
    base.Start();
}

private void Update()
{
    currentState?.Update();
}

// EnemyState를 바꿔주는 함수
public void ChangeState(EnemyState newState)
{
    currentState?.Exit();        // 현재 상태에서 벗어나고
    currentState = newState;    // 새로운 상태를 할당
    currentState?.Enter();        // 새로운 상태에 진입
}</code></pre>
<img src=https://velog.velcdn.com/images/ye-seong/post/4a285ef7-0087-4ee5-b790-2503bdd61697/image.png width="700">

<p>처음 생성될땐 대기 상태로 진입했다는 로그를 볼 수 있습니다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/d96d1ef1-62e6-4354-971f-83984b17c87d/image.png width="400">

<p>임의로 2초 이상이 흐를 경우 대기 행동을 벗어나도록 했습니다. 그러면 2초간 <code>Update</code>함수가 호출되다가 <code>Exit</code>하게 됩니다.</p>
<hr>
<h3 id="👾-플레이어-감지">👾 플레이어 감지</h3>
<p>플레이어가 <strong>일정범위</strong>안에 들어가고, 적이 볼 수 있는 <strong>시야(각도)</strong>내에 들어와야 합니다. 또한, 장애물이 있을 경우 감지하지 않은 것으로 처리될겁니다.</p>
<pre><code class="language-csharp">// Enemy.cs

private GameObject player;

private float fieldOfViewAngle = 120f;
public float sightRange = 10f;    
public float attackRange = 2f;

protected override void Start()
{
    player = GameObject.Find(&quot;Player&quot;);
    maxHealth = enemyData.maxHealth;
    ChangeState(new IdleState(this));
    base.Start();
}
private void Update()
{
    if (CanSeePosition(player.transform) &amp;&amp; !IsObstacleBetween(player.transform) &amp;&amp; IsInDistance(player.transform))
    {
        Debug.Log(&quot;플레이어 발견!!&quot;);
    }
    else
    {
        Debug.Log(&quot;찾는 중.....&quot;);
    }
}

// 플레이어를 볼 수 있는 각도인지
public bool CanSeeAngle(Transform target)
{
    Vector3 directionToTarget = (target.position - transform.position).normalized;
    float angleToTarget = Vector3.Angle(transform.forward, directionToTarget);
    // 각도는 좌우 대칭이므로 나누기 2
    return angleToTarget &lt;= fieldOfViewAngle / 2f;
}

// Raycast에 닿은 것이 플레이어인지 사물인지 (벽이나 다른 사물이라면 true)
public bool IsObstacleBetween(Transform target)
{
    Vector3 direction = (target.position - transform.position).normalized;
    float distance = Vector3.Distance(transform.position, target.position); 

    if (Physics.Raycast(transform.position, direction, out RaycastHit hit, distance))
    {
        return hit.transform != target;
    }
    return false;
}

// 감지 가능 거리 안에 있는지
public bool IsInDistance(Transform target)
{
    return Vector3.Distance(transform.position, target.position) &lt;= sightRange; 
}</code></pre>
<hr>
<h3 id="🎮-중간-결과-1">🎮 중간 결과 1</h3>
<p><strong>각도, 거리, 충돌</strong>에 따라 감지하는 로그가 띄워집니다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/44bfb06d-49bb-4170-89df-faf45d760a39/image.gif width="700">

<hr>
<h3 id="🏃-추적-상태">🏃 추적 상태</h3>
<p>적의 시야내에 플레이어가 온다면 추적 상태로 바꾸는 로직을 짜겠습니다.</p>
<h4 id="1-changestate">1. ChangeState</h4>
<pre><code class="language-csharp">// Enemy.cs

private bool isChase = false;

private void Update()
{
    if (CanSeeAngle(player.transform) &amp;&amp; !IsObstacleBetween(player.transform) &amp;&amp; IsInDistance(player.transform))
    {
        if (!isChase)
        {
            // 시야내에 있다면 ChaseState로 변경
            ChangeState(new ChaseState(this));
            isChase = true;
        }
    }
    else
    {
        // 추적 중 사라진다면 IdleState로 변경
        if (isChase)
        {
            ChangeState(new IdleState(this));
            isChase = false;
        }
    }

    // isChase가 true일때 isChase의 Update를 실행함
    if (isChase)
    {
        currentState.Update();
    }
}</code></pre>
<img src=https://velog.velcdn.com/images/ye-seong/post/3fa45cc2-7e91-4279-93c7-eb4e868b77c1/image.gif width="600">

<img src=https://velog.velcdn.com/images/ye-seong/post/b98d11db-cedb-49ef-b464-03b89188867c/image.png width="350">

<img src=https://velog.velcdn.com/images/ye-seong/post/259d04a7-f9f7-44cd-85d1-3a1dc6d3eb9c/image.png width="350">

<h4 id="2-이동">2. 이동</h4>
<p>플레이어를 향해 회전하고 이동하는 코드를 짜봅시다.</p>
<pre><code class="language-csharp">// ChaseState.cs

private void Moving()
{
    Vector3 direction = (enemy.target.transform.position - enemy.transform.position).normalized;
    // y는 0으로 해서 수직상승을 하지 않게 함
    enemy.rb.MovePosition(enemy.transform.position + new Vector3(direction.x, 0, direction.z) * enemy.enemyData.moveSpeed);
    if (direction != Vector3.zero)
    {
        // y는 0으로 하여 돌아가지 않게 함
        Quaternion targetRotation = Quaternion.LookRotation(new Vector3(direction.x, 0, direction.z));
        enemy.transform.rotation = Quaternion.Slerp(enemy.transform.rotation, targetRotation, enemy.enemyData.rotationSpeed * Time.deltaTime);
    }
}

// Enemy.cs

// 중간에 Enemy가 회전하지 않기 위한 설정
protected override void Start()
{
    rb = GetComponent&lt;Rigidbody&gt;();
    // 질량 증가 - 다른 물체에게 밀리지 않도록 무겁게 만듦
    rb.mass = 10f;                    
    // 공기 저항 - 이동 시 점점 느려지게 해서 부드러운 정지
    rb.drag = 10f;                    
    // 회전 저항 - 회전 시 빠르게 멈추게 해서 흔들림 방지
    rb.angularDrag = 20f;             
    // X, Z축 회전 고정 - 앞뒤/좌우로 넘어지지 않게 (Y축 회전만 허용)
    rb.constraints = RigidbodyConstraints.FreezeRotationX | RigidbodyConstraints.FreezeRotationZ;  
    // 무게중심을 아래쪽으로 이동 - 바닥에 가깝게 해서 안정성 증가                 
    rb.centerOfMass = new Vector3(0, -0.5f, 0);                   
}</code></pre>
<h4 id="3-상태-전환">3. 상태 전환</h4>
<p>적 상태를 효율적으로 관리하기 위해 <code>StateType</code>을 생성합시다.</p>
<pre><code class="language-csharp">// StateType.cs

public enum StateType
{
    Idle,
    Chase,
    Attack,
    Patrol,
    Dead
}

// EnemyState.cs

public abstract StateType CurrentStateType { get; }

// IdleState.cs

public override StateType CurrentStateType =&gt; StateType.Idle;

// PatrolState.cs

public override StateType CurrentStateType =&gt; StateType.Patrol;

// ChaseState.cs

public override StateType CurrentStateType =&gt; StateType.Chase;

// AttackState.cs

public override StateType CurrentStateType =&gt; StateType.Attack;</code></pre>
<pre><code class="language-csharp">// Enemy.cs

private void Update()
{
    switch(currentState.CurrentStateType)
    {
        case StateType.Idle:
            DoIdle();
            break;
        case StateType.Patrol:
            DoPatrol();
            break;
        case StateType.Chase:
            DoChase();
            break;
        case StateType.Attack:
            DoAttack();
            break;
        case StateType.Dead:
            break;
        default:
            break;
    }
    currentState.Update();
}

// Idle일때 목표를 포착하면 Chase로
private void DoIdle()
{
    if (CanSeeAngle(target.transform) &amp;&amp; !IsObstacleBetween(target.transform) &amp;&amp; IsInDistance(target.transform))
    {
        ChangeState(new ChaseState(this));
    }
}

// Patrol일때 목표를 포착하면 chase로
private void DoPatrol()
{
    if (CanSeeAngle(target.transform) &amp;&amp; !IsObstacleBetween(target.transform) &amp;&amp; IsInDistance(target.transform))
    {
        ChangeState(new ChaseState(this));
    }
}

// Chase일때 공격범위 내에 들어오면 Attack으로, 나간다면 Idle로
private void DoChase()
{
    if (IsInAttackRange(target.transform))
    {
        ChangeState(new AttackState(this));
    }
    else if (!IsInDistance(target.transform))
    {
        ChangeState(new IdleState(this));
    }

}

// Attack일때 공격범위에서 나간다면 Idle로
private void DoAttack()
{
    if (!IsInAttackRange(target.transform))
    {
        ChangeState(new IdleState(this));
    }
}</code></pre>
<hr>
<h3 id="🎮-중간-결과-2">🎮 중간 결과 2</h3>
<p>플레이어를 포착하면 따라오고, 공격범위 내에 들어온다면 멈춰서 공격하는 모습입니다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/07ea609b-4e32-40b5-a8ee-35461dd0a64e/image.gif width="700">

<hr>
<h3 id="🗡️-공격-판정">🗡️ 공격 판정</h3>
<p>실제로 공격을 하고 데미지를 받는 것을 처리해봅시다. 여러가지 방법이 있지만, 전 <code>OnTriggerEnter</code>을 이용했습니다.</p>
<h4 id="1-attackpoint">1. AttackPoint</h4>
<p>적이 공격하는 범위를 설정합니다.
<code>AttackPoint</code>라는 빈 오브젝트를 만들어 <code>Collider</code>를 넣고 <strong>IsTrigger</strong>를 체크합니다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/809c3915-a141-4074-9b61-feeecc856861/image.png width="600">

<h4 id="2-attackcollider">2. AttackCollider</h4>
<p>충돌을 관리할 <code>AttackCollider</code> 스크립트를 생성합시다.</p>
<pre><code class="language-csharp">// AttackCollider.cs

public class AttackCollider : MonoBehaviour
{
    private Enemy enemy;

    void Start()
    {
        enemy = GetComponentInParent&lt;Enemy&gt;();

        // 시작할 때는 콜라이더 비활성화
        GetComponent&lt;Collider&gt;().enabled = false;
    }

    void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag(&quot;Player&quot;))
        {
            other.gameObject.GetComponent&lt;PlayerState&gt;().ModifyHealth(-enemy.enemyData.damage);
            Debug.Log($&quot;플레이어 체력 {enemy.enemyData.damage} 감소!&quot;);
        }
    }
}</code></pre>
<h4 id="2-쿨타임을-적용한-공격">2. 쿨타임을 적용한 공격</h4>
<p>만약 <strong>공격범위내</strong>에 있다면 적은 공격을 <strong>시도</strong>하게 됩니다. 그 시도는 <code>Collider</code>를 <strong>활성화</strong> 하는 것으로 적용됩니다.</p>
<pre><code class="language-csharp">// AttackState.cs

private float currentCooldown;

public override void Enter()
{
    currentCooldown = enemy.enemyData.attackCooldown;
}

public override void Update()
{
    if (CanAttack())
    {
        enemy.PerformAttack();
    }
}

private bool CanAttack()
{
    currentCooldown -= Time.deltaTime;
    if (currentCooldown &lt;= 0)
    {
        Debug.Log(&quot;공격!&quot;);
        currentCooldown = enemy.enemyData.attackCooldown;
        return true;
    }
    return false;
}</code></pre>
<hr>
<h3 id="🎮-최종-결과">🎮 최종 결과</h3>
<p>투명한 원 부분이 적의 공격 범위 입니다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/dd2ef4ae-e12a-4377-8e5f-17c737b64830/image.gif width="700">

<hr>
<h3 id="📚-오늘의-배운-점">📚 오늘의 배운 점</h3>
<ul>
<li><strong>State</strong> 변환으로 몹 상태 변경</li>
<li>AI 추격 시스템</li>
<li><strong>Collider</strong>를 이용한 공격 판정</li>
</ul>
<hr>
<h3 id="🎯-다음-계획">🎯 다음 계획</h3>
<p>다음 글에서는:</p>
<ol>
<li>다양한 몹 제작</li>
<li>AI시스템 추가 및 구체화, 수정</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity / C#] 장착형 아이템 탈착하기]]></title>
            <link>https://velog.io/@ye-seong/Unity-C-%EC%9E%A5%EC%B0%A9%ED%98%95-%EC%95%84%EC%9D%B4%ED%85%9C-%ED%83%88%EC%B0%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@ye-seong/Unity-C-%EC%9E%A5%EC%B0%A9%ED%98%95-%EC%95%84%EC%9D%B4%ED%85%9C-%ED%83%88%EC%B0%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 02 Jul 2025 07:36:16 GMT</pubDate>
            <description><![CDATA[<h2 id="📋-목차">📋 목차</h2>
<ol>
<li>UI 생성</li>
<li>데이터 옮기기</li>
<li>Equipment 슬롯 설정</li>
<li>최종 결과</li>
<li>오늘의 배운 점</li>
<li>다음 계획</li>
</ol>
<hr>
<h3 id="🎨-ui-생성">🎨 UI 생성</h3>
<p>일단 탈착을 위해서는 별도의 UI창이 필요합니다. <strong>Inventory_Panel</strong>에서 <strong>EquipmentSlots</strong>라는 빈 오브젝트를 생성합시다.
그리고 본래 있는 Slot Prefab을 복사하여 <code>EquipmentSlot</code>이란 프리팹을 만듭시다. UI예시는 다음과 같습니다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/77275ca5-cda7-466f-b1ad-dcdf1047f1c5/image.png width="500">

<table>
<thead>
<tr>
<th>슬롯</th>
<th>위치</th>
<th>장착아이템</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>얼굴</td>
<td>호흡기 등</td>
</tr>
<tr>
<td>2</td>
<td>몸</td>
<td>수트</td>
</tr>
<tr>
<td>3</td>
<td>등</td>
<td>제트팩, 가방 등</td>
</tr>
</tbody></table>
<hr>
<h3 id="🚗-데이터-옮기기">🚗 데이터 옮기기</h3>
<h4 id="1-itemdata변경">1. ItemData변경</h4>
<p>현재 아이템이 탈착이 가능한 아이템인지 아닌지를 판별해야합니다.</p>
<pre><code class="language-csharp">// ItemData.cs

public EquipmentType equipmentType;

public enum EquipmentType
{
    None,
    Face,
    Body,
    Back
}</code></pre>
<h4 id="2-배열에-추가">2. 배열에 추가</h4>
<p>인벤토리 창에서 탈착이 가능한 아이템을 <strong>더블클릭</strong>하면 아이템이 옮겨지게 할 겁니다.
<code>PlayerState</code>에 새 배열 변수가 필요할 겁니다.</p>
<pre><code class="language-csharp">// PlayerState.cs

// 에디터의 Inspector에서 배열 3개 추가
public ItemInstance[] equipmentItems;

public void AddEquipmentItem(ItemInstance item)
{
    switch (item.itemData.equipmentType)
    {
        case EquipmentType.Face:
            SetEquipmentItems(0, item);
            break;
        case EquipmentType.Body:
            SetEquipmentItems(1, item);
            break;
        case EquipmentType.Back:
            SetEquipmentItems(2, item);
            break;
        default:
            break;
    }
}

private void SetEquipmentItems(int index, ItemInstance item)
{
    if (equipmentItems[index])
    {
        inventory.AddItem(equipmentItems[index]);
        uiManager.UpdateItemUI();
    }
    equipmentItems[index] = item;
}</code></pre>
<h4 id="3-더블클릭-처리">3. 더블클릭 처리</h4>
<p>기존 함수에 추가합니다.</p>
<pre><code class="language-csharp">// UIManager.cs

public void OnInventorySlotDoubleClick(int slotIndex)
{
    if (!inventory.items[slotIndex]) return;

    // 장착 아이템
    if (inventory.items[slotIndex].itemData.equipmentType != EquipmentType.None)
    {
        playerState.AddEquipmentItem(inventory.items[slotIndex]);
        inventory.RemoveItem(slotIndex);
        UpdateItemUI();
        return;
    }

    // 물, 식량, 힐팩 아이템
    if (inventory.items[slotIndex].itemData.itemType == ItemType.BioState &amp;&amp; inventory.UseItem(slotIndex))
    {
        Destroy(inventory.items[slotIndex].gameObject);
        inventory.RemoveItem(slotIndex);
        UpdateItemUI();
        return;
    }
}</code></pre>
<hr>
<h3 id="🔁-equipment-슬롯-설정">🔁 Equipment 슬롯 설정</h3>
<h4 id="1-스크립트-제작">1. 스크립트 제작</h4>
<p><code>EquipmentSlot</code> 스크립트를 추가하여 관리합시다.</p>
<pre><code class="language-csharp">// EquipmentSlot.cs

using UnityEngine;
using UnityEngine.UI;

public class EquipmentSlot : MonoBehaviour
{
    // 비어있을때 아이콘
    [SerializeField] private Sprite slotImage;

    // 현재 슬롯의 타입 (얼굴, 몸, 등)
    public EquipmentType equipmentType;

    // 변경할 아이템 이미지 위치
    private Image itemIcon;

    private void Start()
    {
        itemIcon = transform.Find(&quot;ItemIcon&quot;).GetComponent&lt;Image&gt;();
    }

    public void SetEquipmentSlot(ItemInstance item)
    {
        if (item)
        {
            itemIcon.sprite = item.itemData.icon;
            itemIcon.color = Color.white;
        }
        else
        {
            itemIcon.sprite = slotImage;
            itemIcon.color = new Color(255, 255, 255, 100);
        }
    }
}</code></pre>
<h4 id="2-장착할때">2. 장착할때</h4>
<p>인벤토리에서 더블클릭하면 이동한 것이 보이게 합시다.</p>
<pre><code class="language-csharp">// UIManager.cs

public GameObject equipmentSlotPanel;

private EquipmentSlot[] equipmentSlots;

void Start()
{    
    if (equipmentSlotPanel != null)
    {
        equipmentSlots = equipmentSlotPanel.GetComponentsInChildren&lt;EquipmentSlot&gt;();
    }
}

// 이 함수를 OnInventorySlotDoubleClick 함수에 추가!
private void UpdateEquipmentUI(EquipmentType type, ItemInstance item)
{
    switch(type)
    {
        case EquipmentType.Face:
            equipmentSlots[0].SetEquipmentSlot(item);
            break;
        case EquipmentType.Body:
            equipmentSlots[1].SetEquipmentSlot(item);
            break;
        case EquipmentType.Back:
            equipmentSlots[2].SetEquipmentSlot(item);
            playerItemHandler.SetBackSocket(item);
            break;
        default:
            break;
    }
}</code></pre>
<pre><code class="language-csharp">// PlayerItemHandler.cs

public void SetBackSocket(ItemInstance item)
{
    if (!item || item == backSocketItem) return;
    backSocketItem = item;
    SetHoldingItem(backSocketItem, backSocket);
    // 더블클릭한 아이템이 현재 들고 있는 아이템일 경우에만 null로 할당!
    if (item == currentItem)
    {
        currentItem = null;
    }
}</code></pre>
<h4 id="3-떼어낼때">3. 떼어낼때</h4>
<p>탈착형 아이템은 착용을 하고 떼어내는 것도 필요합니다. 그것은 <strong>EquipmentSlot</strong>에서 더블클릭 이벤트로 합시다.</p>
<pre><code class="language-csharp">// EquipmentSlot.cs

public void OnPointerClick(PointerEventData eventData)
{
    if (eventData.clickCount == 2)
    {
        RemoveItem(equipmentType);
    }
}

public void SetEquipmentSlot(ItemInstance item)
{
    if (item)
    {
        itemIcon.sprite = item.itemData.icon;
        itemIcon.color = Color.white;
    }
    else
    {
        itemIcon.sprite = slotImage;
        itemIcon.color = new Color(1, 1, 1, 0.5f);
    }
}

private void RemoveItem(EquipmentType type)
{
    int index = -1;
    if (type == EquipmentType.Face) index = 0;
    else if (type == EquipmentType.Body) index = 1;
    else if (type == EquipmentType.Back) index = 2;
    else return;

    if (!inventory.AddItem(playerState.equipmentItems[index]) || !playerItemHandler) return;

    if (index == 0) return;
    else if (index == 1) return;
    else if (index == 2) playerItemHandler.ClearBackSocketHolding();

    playerState.equipmentItems[index] = null;
    SetEquipmentSlot(null);

    uiManager.UpdateItemUI();
}</code></pre>
<pre><code class="language-csharp">// PlayerItemHandler.cs

public void ClearBackSocketHolding()
{
    if (!backSocketItem) return;
    backSocketItem.gameObject.SetActive(false);
    backSocketItem = null;
}</code></pre>
<hr>
<h3 id="🎮-최종-결과">🎮 최종 결과</h3>
<p>인벤토리를 열고 장착하면 UI가 뜨며 아이템을 사용할 수 있는 모습입니다.
만약에 연료가 0%가 된다면 아이템은 사용할 수 없으며, 충전이 되어 있는 연료통으로 갈아끼울시 다시 사용 가능합니다. <strong>(모듈형)</strong></p>
<img src=https://velog.velcdn.com/images/ye-seong/post/e83c647a-d5fb-4bcb-a9e4-52a6b721aef5/image.gif width="700">

<hr>
<h3 id="📚-오늘의-배운-점">📚 오늘의 배운 점</h3>
<ul>
<li>더블클릭으로 아이템 탈착</li>
</ul>
<hr>
<h3 id="🎯-다음-계획">🎯 다음 계획</h3>
<p>다음 글에서는:</p>
<ol>
<li>몹 (Enemy) 제작<ul>
<li>AI 행동 패턴 설계</li>
</ul>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity / C#] 아이템 기능 만들기 #2 - 제트팩]]></title>
            <link>https://velog.io/@ye-seong/Unity-C-%EC%95%84%EC%9D%B4%ED%85%9C-%EA%B8%B0%EB%8A%A5-%EB%A7%8C%EB%93%A4%EA%B8%B0-2-%EC%A0%9C%ED%8A%B8%ED%8C%A9</link>
            <guid>https://velog.io/@ye-seong/Unity-C-%EC%95%84%EC%9D%B4%ED%85%9C-%EA%B8%B0%EB%8A%A5-%EB%A7%8C%EB%93%A4%EA%B8%B0-2-%EC%A0%9C%ED%8A%B8%ED%8C%A9</guid>
            <pubDate>Wed, 02 Jul 2025 02:58:51 GMT</pubDate>
            <description><![CDATA[<h2 id="📋-목차">📋 목차</h2>
<ol>
<li>제트팩 장착</li>
<li>날아오르기</li>
<li>중간 결과</li>
<li>UI 표시</li>
<li>최종 결과</li>
<li>오늘의 배운 점</li>
<li>다음 계획</li>
</ol>
<hr>
<h3 id="🚀-제트팩-장착">🚀 제트팩 장착</h3>
<p>제트팩은 등 뒤에 장착하겠습니다. 
Player에게 <strong>BackSocket</strong>이라는 빈 오브젝트를 등에 달아줍니다. 그리고 <code>PlayerItemHandler</code>에서 관리합시다.</p>
<pre><code class="language-csharp">// PlayerItemHandler.cs

[HideInInspector] public ItemInstance backSocketItem;

[Header(&quot;Socket&quot;)]
public Transform backSocket;

public void SetBackSocket(ItemInstance item)
{
    if (!item || item == backSocketItem) return;
    backSocketItem = item;
    SetActiveCurrentItem(backSocketItem, backSocket);
}

// 본래 있던 함수를 변형함
// 다른 Bone에도 붙일 수 있게끔 item과 해당 위치의 transform을 인수에 둠
private void SetHoldingItem(ItemInstance item, Transform transform)
{
    if (!item) return;

    item.transform.position = transform.position;
    item.transform.rotation = transform.rotation;

    item.transform.SetParent(transform);
    item.transform.localPosition = Vector3.zero;
    item.transform.localRotation = Quaternion.identity;

    Rigidbody rb = item.GetComponent&lt;Rigidbody&gt;();
    if (rb) rb.isKinematic = true;

    Collider[] colliders = item.GetComponents&lt;Collider&gt;();
    foreach (Collider collider in colliders)
    {
        collider.enabled = false;
    }

    item.gameObject.SetActive(true);
}</code></pre>
<pre><code class="language-csharp">// UseItem.cs

public void UseItems(ItemInstance item)
{
    switch(item.itemData.itemName)
    {
        case &quot;스캐너&quot;:
            if (Input.GetMouseButtonDown(1)) UseScanner();
            break;
        case &quot;나이프&quot;:
            if (Input.GetMouseButtonDown(0)) UseKnife(item.itemData.attackPower);
            break;
        case &quot;제트팩&quot;:
            if (Input.GetMouseButtonDown(1)) UseJetPack(item);
            break;
        default:
            return;
    }
}

private void UseJetPack(ItemInstance item)
{
    if (chargeUIOpen) return;
    playerItemHandler.SetBackSocket(item);
}</code></pre>
<blockquote>
<p>우클릭을 누르면 플레이어의 등에 장착하게 됩니다.</p>
</blockquote>
<img src=https://velog.velcdn.com/images/ye-seong/post/c8e38f63-6fbe-4863-9d85-545a459b0d16/image.png width="600">

<hr>
<h3 id="🚀-날아오르기">🚀 날아오르기</h3>
<p>이 게임에서 제트팩의 효과는 <strong>점프가 더 높이</strong>되며, <strong>내려올때 천천히</strong> 내려와 낙하데미지를 입지 않는 것입니다. <code>JetpackController</code> 스크립트를 생성합시다.</p>
<pre><code class="language-csharp">// JetpackController.cs

using UnityEngine;

public class JetpackController : MonoBehaviour
{
    [Header(&quot;Jetpack Settings&quot;)]
    public float jetpackForce = 15f;        // 제트팩 추진력
    public float maxAltitude = 50f;         // 최대 고도
    public float gravityScale = 1f;         // 중력 영향

    private ItemInstance jetPack;
    private Rigidbody rb;
    private float groundLevel;              // 지면 높이
    private bool isUsingJetpack;
    private GameObject player;

    private float foolPowerTime = 5f;        // 허공에 머물러 있을 수 있는 시간
    private float currentPowerTime;

    void Start()
    {
        player = GameObject.Find(&quot;Player&quot;);
        jetPack = GetComponent&lt;ItemInstance&gt;(); 
        rb = player.GetComponent&lt;Rigidbody&gt;();
        groundLevel = player.transform.position.y;  // 시작 지점을 지면으로 설정
        currentPowerTime = foolPowerTime;
    }

    void Update()
    {
        Debug.Log($&quot;현재 제트팩 가능 게이지 : {currentPowerTime}&quot;);
        bool isCan = false;
        if (Input.GetKey(KeyCode.Space))
        {
            // 허공에 있을 수 있는 게이지가 모두 닳아버릴 경우 천천히 떨어짐
            if (currentPowerTime &gt; 0)
            {
                currentPowerTime -= Time.deltaTime; // 누르고 있을 때는 게이지가 줄어듬
                isCan = CanUseFuelProduct(jetPack);
            }
            isUsingJetpack = isCan;
        }
        else // 누르고 있지 않을 때는 게이지가 느리게 채워짐
        {
            if (currentPowerTime &lt;= foolPowerTime)
            {
                currentPowerTime += Time.deltaTime / 3;
            }
            isUsingJetpack = false;
        }
    }

    private void FixedUpdate()
    {
        if (isUsingJetpack)
        {
            UseJetpack();
        }
    }

    private bool CanUseFuelProduct(ItemInstance jetPack)
    {
        ItemInstance fuel = jetPack.Get&lt;ItemInstance&gt;(&quot;Fuel&quot;);
        if (!fuel)
        {
            Debug.Log(&quot;현재 연료통이 존재하지 않습니다!&quot;);
            return false;
        }
        float rate = fuel.Get&lt;float&gt;(&quot;fuelUsageRate&quot;);
        if (rate &lt;= 0)
        {
            Debug.Log(&quot;연료가 모두 소진되었습니다!&quot;);
            return false;
        }
        rate -= Time.deltaTime;
        fuel.Set&lt;float&gt;(&quot;fuelUsageRate&quot;, rate);
        return true;
    }

    void UseJetpack()
    {
        // 현재 고도
        float currentAltitude = player.transform.position.y - groundLevel;

        if (currentAltitude &lt; maxAltitude)
        {
            // 만약 있을 수 있는 고도면 위로 날 수 있게
            rb.AddForce(Vector3.up * jetpackForce, ForceMode.Force);
        }
        else
        {
            // 아니라면 더이상 올라가지 못함
            rb.AddForce(Vector3.up * (jetpackForce * 0.3f), ForceMode.Acceleration);
        }
    }
}</code></pre>
<hr>
<h3 id="🎮-중간-결과">🎮 중간 결과</h3>
<p>플레이어가 공중에서 잠시 뜰 수 있게 됩니다. 고도 제한과 시간 등의 설정은 나중에 밸런스를 맞추도록 합시다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/54c9154a-cc4b-4c76-87cb-08d0d4111b6e/image.gif width="700">

<hr>
<h3 id="🎨-ui표시">🎨 UI표시</h3>
<p>제트팩은 현재 연료량과 날 수 있는 제한 시간에 대한 UI 표시가 필요합니다. <strong>HUD_Panel</strong>에서 <strong>JetpackBar</strong>의 빈 오브젝트를 생성하고, 다른 HUD와 비슷하게 설정할겁니다.
<code>Background_Bar(Image), Fill_Bar(Image, FuelRate(Text)</code>란 자식 오브젝트를 생성합시다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/750eb43c-b77f-4164-907e-76f1c912b0bb/image.png width="500">

<p><code>UIManager</code>에서 편집하도록 합시다.</p>
<pre><code class="language-csharp">// UIManager.cs

[Header(&quot;Bar&quot;)]
public GameObject jetpackBar;
public Image jetpackGaugeBar;

[Header(&quot;Text&quot;)]
public Text jetPackRateText;

private UseItem useItem;

void Start()
{
    // 기존 코드...
    if (player)
    {
        useItem = player.GetComponent&lt;UseItem&gt;();
    }
    // 기존 코드...
}

public void UpdateJetpackUI(ItemInstance jetpack, float time, float foolTime)
{
    if (!jetpackBar || !jetpackGaugeBar || !jetpackRateText || !jetpack) return;

    JetpackController jetpackController = jetpack.GetComponent&lt;JetpackController&gt;();
    jetpackBar.SetActive(true);

    jetpackGaugeBar.fillAmount = time / foolTime;

    ItemInstance fuel = jetpack.Get&lt;ItemInstance&gt;(&quot;Fuel&quot;);
    int displayRate = 0;

    if (fuel)
    {
        displayRate = Mathf.Max(0, (int)fuel.Get&lt;float&gt;(&quot;fuelUsageRate&quot;));
        Debug.Log(jetpack.Get&lt;float&gt;(&quot;fuelUsageRate&quot;));
        jetpackRateText.text = displayRate.ToString() + &quot;%&quot;;
    }
    else
    {
        jetpackRateText.text = &quot;None&quot;;
    }
}</code></pre>
<pre><code class="language-csharp">// JetpackController.cs

void Update()
{
    // 기존 코드...
    if (uiManager)
    {
        uiManager.UpdateJetpackUI(jetPack, currentPowerTime, foolPowerTime);
    }
    // 기존 코드...
}</code></pre>
<hr>
<h3 id="🎮-최종-결과">🎮 최종 결과</h3>
<img src=https://velog.velcdn.com/images/ye-seong/post/317b44ac-ba31-459f-8126-c83426e644b5/image.gif width="700">

<hr>
<h3 id="📚-오늘의-배운-점">📚 오늘의 배운 점</h3>
<ul>
<li><strong>Rigidbody</strong>를 이용한 비행</li>
<li>플레이어의 몸에 아이템 장착</li>
</ul>
<hr>
<h3 id="🎯-다음-계획">🎯 다음 계획</h3>
<p>다음 글에서는:</p>
<ol>
<li>현재 장착 아이템을 인벤토리에 표시</li>
<li>더블클릭으로 탈착 구현</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity / C#] 배터리, 연료 사용 아이템 제어]]></title>
            <link>https://velog.io/@ye-seong/Unity-C-%EB%B0%B0%ED%84%B0%EB%A6%AC-%EC%97%B0%EB%A3%8C-%EC%82%AC%EC%9A%A9-%EC%95%84%EC%9D%B4%ED%85%9C-%EC%A0%9C%EC%96%B4</link>
            <guid>https://velog.io/@ye-seong/Unity-C-%EB%B0%B0%ED%84%B0%EB%A6%AC-%EC%97%B0%EB%A3%8C-%EC%82%AC%EC%9A%A9-%EC%95%84%EC%9D%B4%ED%85%9C-%EC%A0%9C%EC%96%B4</guid>
            <pubDate>Tue, 01 Jul 2025 07:44:57 GMT</pubDate>
            <description><![CDATA[<h2 id="📋-목차">📋 목차</h2>
<ol>
<li>배터리 교체 UI</li>
<li>배터리 관리</li>
<li>배터리 사용 아이템 조건 추가</li>
<li>중간 결과</li>
<li>연료 교체 및 관리</li>
<li>연료 사용</li>
<li>최종 결과</li>
<li>오늘의 배운 점</li>
<li>다음 계획</li>
</ol>
<hr>
<h3 id="🔋-배터리-교체-ui">🔋 배터리 교체 UI</h3>
<p>배터리를 넣거나 바꾸려면 UI가 필요합니다. <strong>HUD_Panel</strong>에 <strong>BatterySlots</strong>로 칸을 관리해줍니다.</p>
<pre><code class="language-csharp">// UIManager.cs

public GameObject batterySlotPrefab;
public GameObject batterySlotPanel;

// 현재 인벤토리에 있는 배터리 아이템을 보여줌
public void ShowBatterySlots(ItemInstance[] batterys, ItemInstance currentBattery)
{
    batterySlotPanel.SetActive(true);
    for (int i = 0; i &lt; batterys.Length; i++)
    {
        GameObject slot = Instantiate(batterySlotPrefab, batterySlotPanel.transform);
        Image itemImage = slot.transform.Find(&quot;ItemIcon&quot;).GetComponent&lt;Image&gt;();
        TextMeshProUGUI batteryRate = slot.transform.Find(&quot;BatteryRate&quot;).GetComponent&lt;TextMeshProUGUI&gt;();

        // 배터리가 없다면 None이라는 빈칸을 띄움
        if (!batterys[i])
        {
            itemImage.color = Color.clear;
            batteryRate.text = &quot;None&quot;;
            continue;
        }

        itemImage.sprite = batterys[i].itemData.icon;
        int displayBattery = Mathf.Max(0, (int)batterys[i].Get&lt;float&gt;(&quot;batteryUsageRate&quot;));
        // 현재 남은 배터리량 표시
        batteryRate.text = displayBattery.ToString() + &quot;%&quot;;

        // 현재 들어있는 배터리는 불투명하게 표시
        if (currentBattery == batterys[i])
        {
            itemImage.color = Color.white;
            continue;
        }

        // 현재 선택되어 있지 않은 배터리들은 반투명하게 표시
        itemImage.color = new Color (1, 1, 1, 0.5f);
    }
}

// 배터리 교체 창 닫기 전 초기화
public void ClearBatterySlots()
{
    for (int i = batterySlotPanel.transform.childCount - 1; i &gt;= 0; i--)
    {
        Destroy(batterySlotPanel.transform.GetChild(i).gameObject);
    }
}

public void SwitchBatterySlot(int index)
{
    for (int i = 0; i &lt; batterySlotPanel.transform.childCount; i++)
    {
        GameObject slot = batterySlotPanel.transform.GetChild(i).gameObject;
        Image itemImage = slot.transform.Find(&quot;ItemIcon&quot;).GetComponent&lt;Image&gt;();

        // 현재 선택되어 있는 배터리
        if (index == i)
        {
            itemImage.color = Color.white;
        }
        // 그 외
        else
        {
            itemImage.color = new Color(1, 1, 1, 0.5f);
        }
    }
}</code></pre>
<hr>
<h3 id="🔋-배터리-관리">🔋 배터리 관리</h3>
<p>배터리를 전담할 스크립트로 <code>BatteryManager</code> 스크립트를 생성합시다.</p>
<pre><code class="language-csharp">// BatteryManager.cs

public class BatteryManager : MonoBehaviour
{
    // 현재 들고 있는 아이템 (스캐너 등)
    private ItemInstance thisItem;
    // 아이템에 들어있는 배터리
    private ItemInstance currentBattery;

    // 현재 선택하고 있는 배터리 선택 창 인덱스
    private int currentIndex;
    private UIManager uiManager;
    private Inventory inventory;

    private void Start()
    {
        thisItem = GetComponent&lt;ItemInstance&gt;();
        currentBattery = thisItem.Get&lt;ItemInstance&gt;(&quot;Battery&quot;);
        uiManager = GameObject.Find(&quot;UIManager&quot;).GetComponent&lt;UIManager&gt;();
        inventory = GameObject.Find(&quot;Player&quot;).GetComponent&lt;Inventory&gt;();
    }

    public void SetBatterySlots()
    {
        currentBattery = thisItem.Get&lt;ItemInstance&gt;(&quot;Battery&quot;);
        uiManager.ShowBatterySlots(BatteryArray(inventory.items), currentBattery);
        uiManager.SwitchBatterySlot(currentIndex);
    }

    // 현재 보여줄 배터리 창의 인스턴스 배열
    public ItemInstance[] BatteryArray(ItemInstance[] items)
    {
        List&lt;ItemInstance&gt; list = new List&lt;ItemInstance&gt;();

        // 만약 현재 장착하고 있는 배터리가 있다면, 현재 배터리도 표시하기 위해 list에 추가함
        // 왜냐하면 장착한 배터리는 인벤토리에서 삭제할 것이기 때문
        if (currentBattery) list.Add(currentBattery);

        // 없다면 현재 배터리는 null상태임을 표시하기 위해 null을 추가함
        else list.Add(null);

        // 인벤토리에서 순차적으로 배터리만 꺼내 list에 넣음
        for (int i = 0; i &lt; items.Length; i++)
        {
            if (!items[i]) continue;
            if (items[i].itemData.productType == ProductType.Battery)
            {
                list.Add(items[i]);
            }
        }
        // 배터리를 아예 빼내는 선택창도 필요하기 때문에 현재 배터리가 있다면 마지막에 null도 추가함
        if (currentBattery) list.Add(null);

        return list.ToArray();
    }

    // 현재 배터리를 설정함
    public void SetCurrentBattery(ItemInstance[] items)
    {
        // 설정 전 본래 배터리를 저장
        ItemInstance defaultBattery = currentBattery;
        currentBattery = BatteryArray(items)[currentIndex];
        // 현재 장착된 배터리를 선택한 배터리로 변경
        thisItem.Set&lt;ItemInstance&gt;(&quot;Battery&quot;, currentBattery);
        // 장착한 배터리를 인벤토리에서 삭제
        inventory.RemoveItemFromInstance(currentBattery);
        if (defaultBattery)
        {
            // 새로 장착한 배터리라면
            if (defaultBattery != currentBattery)
            {    
                // 본래 배터리는 인벤토리에 추가하고 UI 업데이트
                inventory.AddItem(defaultBattery);
                uiManager.UpdateItemUI();
            }
        }
        currentIndex = 0;
    }

    // 마우스의 왼클릭 우클릭으로 배터리를 선택함
    public void SetCurrentBatteryIndex(string button)
    {
        switch(button)
        {
            case &quot;left&quot;:
                if (currentIndex &gt; 0) currentIndex--;
                break;
            case &quot;right&quot;:
                if (currentIndex &lt; BatteryArray(inventory.items).Length - 1) currentIndex++;
                break;
            default:
                break;
        }
        uiManager.SwitchBatterySlot(currentIndex);
    }
}</code></pre>
<hr>
<h3 id="🔋-배터리-사용-아이템-조건-추가">🔋 배터리 사용 아이템 조건 추가</h3>
<p>현재까지 만든 기능에는 <strong>Scanner</strong>가 배터리를 사용하고 있습니다. 그곳에 배터리가 있으며, 0% 가 초과할 때만 사용할 수 있게 조건을 추가합니다.</p>
<pre><code class="language-csharp">// UseItem.cs

private bool batteryUIOpen = false;

private void UseScannerInUpdate()
{
    if (!playerItemHandler.currentItem)
    {
        scanTime = 0f;
        isScan = false;
        uiManager.scanBar.SetActive(false);
        return;
    }

    ReloadBattery(playerItemHandler.currentItem);

    bool isCan = false;
    if (isScan &amp;&amp; Input.GetMouseButton(1) &amp;&amp; !batteryUIOpen)
    {
        // 현재 장착한 배터리가 있거나, 쓸 수 있는지 판별
        isCan = CanUseBatteryProduct();
        if (isCan)
        {
            scanTime += Time.deltaTime;

            if (scanTime &gt;= 2f)
            {
                isScan = false;
                scanTime = 0f;
                playerState.AddUnlockedItems(scanItem.itemData);
                uiManager.scanBar.SetActive(false);
            }
            else
            {
                if (uiManager)
                {
                    uiManager.UpdateScanUI(scanTime);
                }
            }
        }
    }

    if ((isScan &amp;&amp; Input.GetMouseButtonUp(1) &amp;&amp; scanTime &gt; 0f) || !interactionDetector.GetCurrentTarget() || 
        playerItemHandler.currentItem.itemData.itemName != &quot;스캐너&quot; || !isCan)
    {
        scanTime = 0f;
        isScan = false;
        uiManager.scanBar.SetActive(false);
    }
}

// 배터리 사용 아이템을 들고 있는 채로 R키를 누른다면 배터리 선택창이 뜸
private void ReloadBattery(ItemInstance item)
{
    if (!inventory) return;
    BatteryManager batteryManager = item.GetComponent&lt;BatteryManager&gt;();

    if (Input.GetKeyDown(KeyCode.R) &amp;&amp; batteryManager)
    {
        if (!batteryUIOpen)
        { 
            batteryManager.SetBatterySlots();
            batteryUIOpen = true;
        }
        else
        {
            batteryManager.SetCurrentBattery(inventory.items);
            uiManager.ClearBatterySlots();
            uiManager.batterySlotPanel.SetActive(false);
            batteryUIOpen = false;
        }
    }
    // 열려있는 채로 마우스를 누르면 배터리를 선택할 수 있음
    if (batteryUIOpen)
    {
        if (Input.GetMouseButtonDown(0))
        {
            batteryManager.SetCurrentBatteryIndex(&quot;left&quot;);
        }
        else if (Input.GetMouseButtonDown(1))
        {
            batteryManager.SetCurrentBatteryIndex(&quot;right&quot;);
        }
    }
}

private bool CanUseBatteryProduct()
{
    ItemInstance battery = playerItemHandler.currentItem.Get&lt;ItemInstance&gt;(&quot;Battery&quot;);
    // 배터리가 없을 경우
    if (!battery)
    {
        Debug.Log(&quot;배터리를 장착하세요!&quot;);
        return false;
    }
    float rate = battery.Get&lt;float&gt;(&quot;batteryUsageRate&quot;);
    // 현재 배터리 충전량이 0이하일 경우
    if (rate &lt;= 0)
    {
        Debug.Log(&quot;배터리가 모두 소진되었습니다.&quot;);
        return false;
    }
    // Update에서 배터리의 batteryUsageRate가 아이템을 사용하는 동안 점차 줄어듬
    rate -= Time.deltaTime;
    battery.Set&lt;float&gt;(&quot;batteryUsageRate&quot;, rate);
    Debug.Log(rate);
    return true;
}</code></pre>
<hr>
<h3 id="🎮-중간-결과">🎮 중간 결과</h3>
<p><code>R</code>키를 이용해 배터리 선택 창을 열고, 마우스 <code>왼클릭, 우클릭</code>으로 장착할 배터리를 고릅니다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/107aff7a-b193-4acb-a09f-c9147c25a0b6/image.gif width="600">

<p>현재 장착한 배터리가 없다면 쓰지 못하고, 장착한 배터리가 있으며 해당 배터리에 충전량이 있을 때만 사용할 수 있습니다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/99f43fc5-611e-4911-8130-a5e9a1cab775/image.gif width="600">

<img src=https://velog.velcdn.com/images/ye-seong/post/42dc34d0-507a-4c3e-b532-18803aefc42d/image.gif width="600">

<hr>
<h3 id="🛢️-연료-교체-및-관리">🛢️ 연료 교체 및 관리</h3>
<p>연료는 배터리와 같은 형식으로 관리됩니다. 그러니 쓰는 UI도 로직도 모두 같습니다.
이럴 경우에는 다른 함수를 만드는 것이 아닌, 같은 함수에서 조건만 나누어 관리하는 것이 훨씬 효율적입니다.
<code>BatteryManager</code>를 <code>ChargeManager</code>로 바꾸어 진행합시다.</p>
<pre><code class="language-csharp">// 제트팩과 스캐너에 넣어 에디어의 인스펙터에서 설정합니다
public Charge Type chargeType;

public enum ChargeType
{
    None,
    Battery,
    Fuel
}</code></pre>
<p>위의 타입을 추가하고, Batter를 관리했던 모든 함수에 <code>chargeType</code>에 따른 조건부를 추가합니다.</p>
<hr>
<h3 id="🛢️-연료-사용">🛢️ 연료 사용</h3>
<p>아직 제트팩 로직은 완성되지 않았으나, 시험용으로 <code>UseItem</code>에 코드를 추가합시다.</p>
<pre><code class="language-csharp">public void UseItems(ItemInstance item)
{
    switch(item.itemData.itemName)
    {
        case &quot;스캐너&quot;:
            if (Input.GetMouseButtonDown(1)) UseScanner();
            break;
        case &quot;나이프&quot;:
            if (Input.GetMouseButtonDown(0)) UseKnife(item.itemData.attackPower);
            break;
        case &quot;제트팩&quot;:
            if (Input.GetMouseButtonDown(1)) UseJetPack();
            break;
        default:
            return;
    }
}

private void UseJetPack()
{
    if (chargeUIOpen) return;
    bool isCan = CanUseFuelProduct();
}

private bool CanUseFuelProduct()
{
    ItemInstance fuel = playerItemHandler.currentItem.Get&lt;ItemInstance&gt;(&quot;Fuel&quot;);
    if (!fuel)
    {
        Debug.Log(&quot;현재 연료통이 존재하지 않습니다!&quot;);
        return false;
    }
    float rate = fuel.Get&lt;float&gt;(&quot;fuelUsageRate&quot;);
    if (rate &lt;= 0)
    {
        Debug.Log(&quot;연료가 모두 소진되었습니다!&quot;);
        return false;
    }
    rate -= Time.deltaTime;
    fuel.Set&lt;float&gt;(&quot;fuelUsageRate&quot;, rate);
    Debug.Log(&quot;연료가 소진되는 중입니다...&quot;);
    return true;
}</code></pre>
<hr>
<h3 id="🎮-최종-결과">🎮 최종 결과</h3>
<p>배터리와 연료통 모두 교체 및 사용이 가능해집니다.</p>
<img src=https://velog.velcdn.com/images/ye-seong/post/b62154ae-5643-49b9-8287-7941e4fac5a0/image.gif width="700">

<hr>
<h3 id="📚-오늘의-배운-점">📚 오늘의 배운 점</h3>
<ul>
<li>제네릭을 이용한 Get, Set 활용</li>
<li>오브젝트 교체</li>
</ul>
<hr>
<h3 id="🎯-다음-계획">🎯 다음 계획</h3>
<p>다음 글에서는:</p>
<ol>
<li>제트팩 구현</li>
</ol>
]]></description>
        </item>
    </channel>
</rss>