<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>yj_621.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Tue, 27 Jan 2026 05:47:59 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>yj_621.log</title>
            <url>https://velog.velcdn.com/images/yj_621/profile/0a62dae3-eda2-42a9-840f-f25b2f192345/social_profile.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. yj_621.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/yj_621" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[유니티로 만드는 3D 농사게임 - 2일차]]></title>
            <link>https://velog.io/@yj_621/%EC%9C%A0%EB%8B%88%ED%8B%B0%EB%A1%9C-%EB%A7%8C%EB%93%9C%EB%8A%94-3D-%EB%86%8D%EC%82%AC%EA%B2%8C%EC%9E%84-2%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@yj_621/%EC%9C%A0%EB%8B%88%ED%8B%B0%EB%A1%9C-%EB%A7%8C%EB%93%9C%EB%8A%94-3D-%EB%86%8D%EC%82%AC%EA%B2%8C%EC%9E%84-2%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Tue, 27 Jan 2026 05:47:59 GMT</pubDate>
            <description><![CDATA[<p>1월 19일에 개발한걸 이제야 씀니다..</p>
<h1 id="1-초기-설정">1. 초기 설정</h1>
<p>오늘은 밭을 내마음대로 배치했다면 그 위에 씨앗을 심을 예정입니다</p>
<p>씨앗 - 새싹 - 열매 - 성체 순으로 점점 커지는 느낌으로 아래 에셋을 다운 받아요</p>
<p><a href="https://assetstore.unity.com/packages/3d/environments/industrial/lite-farm-pack-low-poly-3d-art-by-gridness-243315">https://assetstore.unity.com/packages/3d/environments/industrial/lite-farm-pack-low-poly-3d-art-by-gridness-243315</a></p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/ec8eaf07-f998-47af-9106-100ab1eb0dc0/image.png" alt=""></p>
<p>일단 버튼으로 밭-토마토 중 하나를 골라서 심어야하니까 TmpText 먼저 만들어주기
<img src="https://velog.velcdn.com/images/yj_621/post/e25dca2e-1572-488c-b632-8ba5fd1f2f03/image.png" alt=""></p>
<pre><code>32-126,44032-55203,12593-12643,8200-9900</code></pre><p>저번에 한 밭을 투명하게 한 것 처럼 씨앗도 이렇게 해줌
<img src="https://velog.velcdn.com/images/yj_621/post/53b69197-0e2d-48d5-9b29-a048cc2cda1a/image.png" alt=""></p>
<h1 id="2-결과">2. 결과</h1>
<p>씬뷰 확대하니까 도트같아서 귀여운데 ㅋ
<img src="https://velog.velcdn.com/images/yj_621/post/b03a0ef9-cd5c-4db5-a4d5-2acbb8305e8c/image.png" alt="">
일단 요런식으로 밭 - 토마토 버튼이 있어서 무조건 밭 위에 심을 수 있게 하려고 함
<img src="https://velog.velcdn.com/images/yj_621/post/a31e45c8-093c-4323-9fb5-1f699749cd37/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/63f768c8-0845-481a-9bdf-a810b88a2997/image.gif" alt=""></p>
<h1 id="3-작물-설정">3. 작물 설정</h1>
<p>Crop이라는 빈 오브젝트를 만들어주고
<img src="https://velog.velcdn.com/images/yj_621/post/1a9ce442-1ccb-4085-87c5-c92aa0a98630/image.png" alt=""></p>
<pre><code class="language-csharp">using UnityEngine;

public class Crop : MonoBehaviour
{
    private CropData data;
    private int currentState = 0;
    private GameObject currentModel;

    public void Initialize(CropData cropData)
    {
        data = cropData;
        currentState = 0; // 0단계부터 시작 보장

        // 데이터가 정상인지 검사
        if (data == null || data.growthStagePrefabs == null || data.growthStagePrefabs.Length == 0)
        {
            Debug.LogError(&quot;CropData 또는 성장 프리팹 배열이 비어있습니다.&quot;);
            return;
        }

        UpdateModel();

        // Invoke 지연 시간 재확인 (0이면 실행 안됨)
        float interval = data.timeBetweenStages;
        if (interval &lt;= 0) interval = 2.0f; // 기본값 방어 코드

        // 기존 예약된 Grow가 있다면 취소 후 재등록
        CancelInvoke(nameof(Grow));
        InvokeRepeating(nameof(Grow), interval, interval);

        Debug.Log($&quot;{data.cropName} 성장 시작. 주기: {interval}초&quot;);
    }

    void Grow()
    {
        if (currentState &lt; data.growthStagePrefabs.Length - 1)
        {
            currentState++;
            UpdateModel();
            Debug.Log($&quot;{gameObject.name}가 {currentState}단계로 성장했습니다.&quot;);
        }
        else
        {
            CancelInvoke(&quot;Grow&quot;);
        }
    }

    void UpdateModel()
    {
        if(currentModel != null) Destroy(currentModel);

        // 자식으로 생성하되, 생성된 모델의 Local Position을 0으로 맞춤
        currentModel = Instantiate(data.growthStagePrefabs[currentState], transform);
        currentModel.transform.localPosition = Vector3.zero;
        currentModel.transform.localRotation = Quaternion.identity;
    }
}
</code></pre>
<pre><code class="language-csharp">using UnityEngine;

[CreateAssetMenu(fileName = &quot;CropData&quot;, menuName = &quot;Scriptable Objects/CropData&quot;)]
public class CropData : ScriptableObject
{
    public string cropName;
    public GameObject[] growthStagePrefabs; // 4단계 프리팹(씨앗-새싹-성장-수확)
    public float timeBetweenStages; //단계별 성장 시간
}
</code></pre>
<p>Scriptable Objects로 CropData도 만들어줌
<img src="https://velog.velcdn.com/images/yj_621/post/704727a9-de3f-4775-b623-e45b51a0fe32/image.png" alt="">
GridManager 스크립트 수정</p>
<pre><code class="language-csharp">using UnityEngine;

public enum TileType { Empty, Mud, Crop };

/// &lt;summary&gt;
/// 농장의 물리적인 공간 데이터를 관리하는 클래스
/// 어떤 타일이 비어있고, 어떤 타일이 점유되었는지를 2차원 배열로 저장함
/// &lt;/summary&gt;
public class GridManager : MonoBehaviour
{
    [Header(&quot;농장 크기 설정&quot;)]
    public int width = 10;  // 가로 타일 개수
    public int height = 10; // 세로 타일 개수

    private TileType[,] tileTypes; // bool 대신 TileType 사용

    /// &lt;summary&gt;
    /// 게임 시작 시 설정된 크기에 맞춰 배열 메모리를 할당
    /// &lt;/summary&gt;
    private void Awake()
    {
        tileTypes = new TileType[width, height];
    }

    /// &lt;summary&gt;
    /// 특정 좌표(x, z)에 오브젝트를 설치할 수 있는지 검사
    /// &lt;/summary&gt;
    /// &lt;param name=&quot;x&quot;&gt;검사할 그리드 X 인덱스&lt;/param&gt;
    /// &lt;param name=&quot;z&quot;&gt;검사할 그리드 Z 인덱스&lt;/param&gt;
    /// &lt;returns&gt;해당 칸이 Empty일때만 true, 불가능하면 false&lt;/returns&gt;
    public bool CanPlace(int x, int z)
    {
        // 맵 범위 체크
        if (x &lt; 0 || x &gt;= width || z &lt; 0 || z &gt;= height)
        {
            return false;
        }

        // 해당 칸이 Empty 상태일 때만 true 반환
        // (isOccupied 배열을 삭제했으므로 tileTypes만 확인하면 됩니다)
        return tileTypes[x, z] == TileType.Empty;
    }

    /// &lt;summary&gt;
    /// 오브젝트 설치가 확정되었을 때 데이터 갱신
    /// &lt;/summary&gt;
    public void PlaceObject(int x, int z, TileType type)
    {
        // 맵 범위 안일 때만 실행
        if (x &gt;= 0 &amp;&amp; x &lt; width &amp;&amp; z &gt;= 0 &amp;&amp; z &lt; height)
        {
            tileTypes[x, z] = type;
            Debug.Log($&quot;&lt;color=yellow&gt;데이터 갱신:&lt;/color&gt; [{x},{z}] 번지 타일이 {type} 상태가 되었습니다.&quot;);
        }
    }

    /// &lt;summary&gt;
    /// 현재 타일의 타입을 반환하는 함수
    /// &lt;/summary&gt;
    public TileType GetTileType(int x, int z)
    {
        if(x &lt; 0 || x &gt;= width || z &lt; 0 || z&gt;=height) return TileType.Empty;
        return tileTypes[x, z];
    }
}
</code></pre>
<p>GridSystem 수정</p>
<pre><code class="language-csharp">using System;
using UnityEditor.ShaderGraph;
using UnityEngine;

/// &lt;summary&gt;
/// 마우스 입력을 받아 그리드 상에 오브젝트를 배치하고 
/// 배치 가능 여부를 시각적으로 보여주는 클래스
/// &lt;/summary&gt;
public class GridSystem : MonoBehaviour
{
    [Header(&quot;설정&quot;)]
    [SerializeField] private float cellSize = 10f;       // 그리드 한 칸의 크기 (기본 Plane 10x10에 맞춤)
    [SerializeField] private LayerMask groundLayer;      // 레이캐스트가 감지할 바닥 레이어

    [Header(&quot;밭 설정&quot;)]
    [SerializeField] private GameObject mudPreviewPrefab;   // 설치 전 보여줄 투명한 미리보기용 프리팹
    [SerializeField] private GameObject mudPrefab;      // 실제로 설치될 완성된 오브젝트 프리팹

    [Header(&quot;작물 설정&quot;)]
    [SerializeField] private GameObject previewCropPrefab;
    [SerializeField] private GameObject cropPrefab;    // Crop 스크립트가 붙어있는 빈 오브젝트 프리팹

    [Header(&quot;선택된 상태&quot;)]
    [SerializeField] private bool isMudSelect = false; // 기본적으로 아무것도 선택X
    [SerializeField] private CropData currentSelectedCrop; // 선택된 씨앗 데이터

    private GridManager gridManager;   // 설치 데이터를 관리하는 GridManager 참조
    private GameObject previewInstance; // 씬에 생성되어 따라다닐 미리보기 인스턴스

    void Start()
    {
       /* // 미리보기 인스턴스를 생성하고 초기 설정 진행
        if (mudPreviewPrefab != null)
        {
            previewInstance = Instantiate(mudPreviewPrefab);
            mudPreviewPrefab.SetActive(false);

            // TryGetComponent: GameObject에 존재하는 경우 지정된 유형의 컴포넌트를 검색하려고 시도하고,
            // 발견되면 true, 발견되지 않으면 false를 반환한다.
            //public bool TryGetComponent&lt;T&gt;(out T component) where T : Component;
            *//* T: 가져오려는 컴포넌트의 타입
            component: 컴포넌트를 가져올 때 사용되는 out 매개변수 *//*

            // 미리보기 오브젝트가 마우스 레이캐스트를 방해하지 않도록 콜라이더 비활성화
            if (previewInstance.TryGetComponent&lt;Collider&gt;(out Collider col)) col.enabled = false;
        }*/
        // 씬에 존재하는 GridManager를 찾아 참조 연결
        if (gridManager == null) gridManager = FindAnyObjectByType&lt;GridManager&gt;();
    }
    void Update()
    {
        // 마우스 위치로부터 화면 안쪽으로 레이(광선)를 쏜다.
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        RaycastHit hit;

        //Ray가 어떤 Collider에 맞으면 true를 반환하고, 그 충돌 정보는 hit에 담긴다.

        // 지정된 groundLayer(바닥)에 레이가 맞았을 때만 로직 실행
        if (Physics.Raycast(ray, out hit, Mathf.Infinity, groundLayer))
        {
            // [좌표 계산] 맞은 지점의 월드 좌표를 그리드 인덱스(0, 1, 2...)로 변환
            int xIdx = Mathf.FloorToInt(hit.point.x / cellSize);
            int zIdx = Mathf.FloorToInt(hit.point.z / cellSize);

            // [스냅 좌표] 바닥이 그리드 정중앙에 오도록 좌표 보정 (+cellSize/2)
            Vector3 snapPos = new Vector3(xIdx * cellSize + (cellSize / 2), 0, zIdx * cellSize + (cellSize / 2));

            // 모드에 따른 높이 결정
            // 밭 설치 모드면 0, 작물 설치 모드면 0.16f
            float targetY = isMudSelect ? 0f : 0.16f;

            // 프리뷰 처리 (targetY 반영)
            HandlePreview(snapPos, targetY, xIdx, zIdx);

            // 마우스 왼쪽 클릭 시 해당 위치에 실제 설치 시도
            if (Input.GetMouseButtonDown(0))
            {
                PlaceAt(snapPos, xIdx, targetY, zIdx);
            }
        }
        else
        {
            // 바닥을 벗어나면 미리보기를 숨김
            if (previewInstance != null)
            {
                previewInstance.SetActive(false);
            }
        }
    }

    /// &lt;summary&gt;
    /// 선택된 모드에 맞춰 미리보기(Preview) 모델을 교체합니다.
    /// &lt;/summary&gt;
    void UpdatePreviewInstance(GameObject newPrefab)
    {
        // 기존 미리보기가 있다면 삭제
        if(previewInstance != null)
        {
            Destroy(previewInstance); 
        }
        //OnClickSelectCrop 새로운 프리팹으로 생성
        if (newPrefab != null)
        {
            // 원본 프리팹을 복제하여 previewInstance 변수에 할당
            previewInstance = Instantiate(newPrefab);
            previewInstance.SetActive(false);

            // 레이캐스트 방해 방지 (콜라이더 끄기)
            if (previewInstance.TryGetComponent&lt;Collider&gt;(out Collider col))
                col.enabled = false;
        }
    }

    /// &lt;summary&gt;
    /// 밭 선택 버튼
    /// &lt;/summary&gt;
    public void OnClickSelectMud()
    {
        isMudSelect = true;
        currentSelectedCrop = null;
        // 미리보기를 밭용으로 교체
        UpdatePreviewInstance(mudPreviewPrefab);
        Debug.Log(&quot;밭 선택 모드&quot;);
    }

    public void OnClickSelectCrop(CropData cropdata)
    {
        isMudSelect = false;
        currentSelectedCrop = cropdata;
        // 미리보기를 작물용으로 교체
        UpdatePreviewInstance(previewCropPrefab);
        Debug.Log($&quot;{cropdata.cropName} 씨앗 선택&quot;);
    }

    /// &lt;summary&gt;
    /// 미리보기 오브젝트를 마우스 위치로 이동시키고, 설치 가능 여부에 따라 색상을 변경하는 메서드
    /// &lt;/summary&gt;
    void HandlePreview(Vector3 pos, float y, int x, int z)
    {
        if (previewInstance == null)
        {
            return;
        }

        // 아무 모드도 선택되지 않았다면 프리뷰를 끄고 리턴
        if (!isMudSelect &amp;&amp; currentSelectedCrop == null)
        {
            previewInstance.SetActive(false);
            return;
        }
        pos.y = y;
        previewInstance.SetActive(true);
        previewInstance.transform.position = pos;

        if (gridManager == null) return;

        bool canPlaceVisually = false;
        if (isMudSelect)
        {
            //밭 설치 모드 : 해당 칸이 Empty여야 초록색
            canPlaceVisually = gridManager.CanPlace(x, z);
        }
        else if (currentSelectedCrop != null)
        {
            //씨앗 심기 모드 : 해당 칸이 Mud 여야 초록색(이미 Crop인 곳은 false가 됨)
            canPlaceVisually = (gridManager.GetTileType(x, z) == TileType.Mud);
        }

        // 시각적 피드백: 가능하면 초록색(Green), 불가능하면 빨간색(Red) 반투명 처리
        MeshRenderer mr = previewInstance.GetComponentInChildren&lt;MeshRenderer&gt;();
        if (mr != null)
        {
            mr.material.color = canPlaceVisually ? new Color(0, 1, 0, 0.5f) : new Color(1, 0, 0, 0.5f);
        }
    }

    /// &lt;summary&gt;
    /// 최종적으로 GridManager의 승인을 받아 실제 오브젝트를 생성하고 점유 데이터를 기록하는 메서드
    /// &lt;/summary&gt;
    void PlaceAt(Vector3 pos, int x, float y, int z)
    {
        // 밭 설치 모드일 때
        if (isMudSelect &amp;&amp; gridManager.CanPlace(x, z))
        {
            pos.y = 0f; // 밭은 무조건 바닥에 설치
            Instantiate(mudPrefab, pos, Quaternion.identity);
            gridManager.PlaceObject(x, z, TileType.Mud);
            Debug.Log($&quot;[{x}, {z}] 위치에 밭 설치 성공&quot;);
        }
        // 작물 설치 모드 (현재 칸이 정확히 Mud 상태여야만 함)
        // 만약 이미 작물을 심어서 TileType.Crop으로 변했다면, GetTileType은 Mud가 아니게 됨
        else if (!isMudSelect &amp;&amp; currentSelectedCrop != null)
        {
            TileType currentTile = gridManager.GetTileType(x, z);
            if (gridManager.GetTileType(x, z) == TileType.Mud)
            {
                pos.y = 0.16f;
                PlantCrop(pos, x, z);
            }
            else if (currentTile == TileType.Crop)
            {
                Debug.Log(&quot;여기는 이미 작물이 자라고 있습니다!&quot;); // 이 로그가 찍히는지 확인하세요.
            }
            else
            {
                Debug.Log(&quot;밭이 아닌 곳에는 심을 수 없습니다.&quot;);
            }
        }
    }

    void PlantCrop(Vector3 pos, int x, int z)
    {
        // 작물 프리팹 생성
        GameObject newCrop = Instantiate(cropPrefab, pos, Quaternion.identity);

        gridManager.PlaceObject(x, z, TileType.Crop);

        // 생성된 작물에 Crop 스크립트를 가져와 데이터 전달
        if (newCrop.TryGetComponent&lt;Crop&gt;(out Crop crop))
        {
            crop.Initialize(currentSelectedCrop);
            Debug.Log($&quot;{currentSelectedCrop.cropName}을 심었습니다/&quot;);
        }

        else
        {
            Debug.LogError($&quot;cropPrefab 루트에 Crop 컴포넌트가 없습니다! 프리팹을 확인하세요: {newCrop.name}&quot;);
            gridManager.PlaceObject(x, z, TileType.Mud);
            Destroy(newCrop);
        }
    }
}
</code></pre>
<h1 id="4-스크립트-역할-정리">4. 스크립트 역할 정리</h1>
<h3 id="①-gridsystemcs-입력-및-로직-제어기">① <code>GridSystem.cs</code> (입력 및 로직 제어기)</h3>
<ul>
<li><strong>역할:</strong> 플레이어의 마우스 입력을 게임 내 좌표로 변환하고, 현재 무엇을 들고 있는지(밭 or 씨앗)에 따라 적절한 행동을 지시</li>
<li><strong>핵심 기능:</strong><ul>
<li><strong>Raycasting:</strong> 3D 공간의 바닥을 감지하여 그리드 좌표를 산출</li>
<li><strong>State Management:</strong> <code>isMudSelect</code>와 <code>currentSelectedCrop</code> 변수를 통해 현재 모드를 관리함</li>
<li><strong>Preview Switching:</strong> 모드 변경 시 기존 미리보기를 삭제하고 새로운 모델을 생성하여 시각적 피드백을 제공</li>
</ul>
</li>
</ul>
<h3 id="②-gridmanagercs-데이터-관리자">② <code>GridManager.cs</code> (데이터 관리자)</h3>
<ul>
<li><strong>역할:</strong> 농장의 모든 타일 상태를 2차원 배열(<code>TileType[,]</code>)로 저장하는 데이터 장부</li>
<li><strong>핵심 기능:</strong><ul>
<li><strong>Data Integrity:</strong> <code>PlaceObject</code>를 통해 설치된 객체의 타입을 기록하여 중복 설치를 방지하고 작물 설치 조건을 검사</li>
<li><strong>Encapsulation:</strong> 데이터를 직접 수정하지 않고 <code>CanPlace</code>, <code>GetTileType</code> 등의 함수를 통해서만 접근하게 하여 버그를 최소화</li>
</ul>
</li>
</ul>
<h3 id="③-cropcs--cropdatacs-객체-및-데이터-구조">③ <code>Crop.cs</code> &amp; <code>CropData.cs</code> (객체 및 데이터 구조)</h3>
<ul>
<li><strong>역할:</strong> <code>CropData</code>는 작물의 정보를 담는 설계도이며, <code>Crop</code>은 실제 월드에서 시간에 따라 자라나는 식물임</li>
<li><strong>핵심 기능:</strong><ul>
<li><strong>Data-Driven Design:</strong> <code>ScriptableObject</code>를 사용하여 기획 데이터(성장 시간, 메쉬 등)를 코드와 분리</li>
<li><strong>Recursive Update:</strong> <code>InvokeRepeating</code>을 사용하여 정해진 주기마다 성장 단계를 갱신하고 모델을 교체</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[유니티로 만드는 3D 농사게임 - 1일차]]></title>
            <link>https://velog.io/@yj_621/%EC%9C%A0%EB%8B%88%ED%8B%B0%EB%A1%9C-%EB%A7%8C%EB%93%9C%EB%8A%94-3D-%EB%86%8D%EC%82%AC%EA%B2%8C%EC%9E%84-1%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@yj_621/%EC%9C%A0%EB%8B%88%ED%8B%B0%EB%A1%9C-%EB%A7%8C%EB%93%9C%EB%8A%94-3D-%EB%86%8D%EC%82%AC%EA%B2%8C%EC%9E%84-1%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Tue, 20 Jan 2026 07:41:45 GMT</pubDate>
            <description><![CDATA[<p>안녕하신가요
오늘(사실 1월 16일)부터 시작한 3D 농사게임을 만들어보려고 해요</p>
<p>제미나이와 함께한 + 제 포트폴리오에 3D가 없기에 + 기술적 향상을 위한 의미있는 프로젝트인데 기록겸 남깁니당</p>
<h1 id="1-초기-설정">1. 초기 설정</h1>
<p>일단 아이소메트릭뷰로 만들거기 때문에 Camera를 자식으로 둔 Camera Pivot 오브젝트 생성 후 Rotation 30, 45, 0으로 설정해줘요
<img src="https://velog.velcdn.com/images/yj_621/post/9236e39e-278c-41dd-83e7-578ab4a197cf/image.png" alt=""></p>
<p>Main Camera는 Orthographic으로 설정 후 Size 및 Clipping Planes 조절
<img src="https://velog.velcdn.com/images/yj_621/post/70eb7ac6-a78e-4d3f-88dc-c56ee03c45f4/image.png" alt=""></p>
<p>Plane 생성 후 Position(10, 0, 10) Scale(2, 2, 2)로 변경
<img src="https://velog.velcdn.com/images/yj_621/post/41cecab5-ecca-4716-8d3e-7b75dce3db34/image.png" alt=""></p>
<p>농사게임에 일단 밭을 만들어야하니까 
이 에셋 다운/임포트 후</p>
<p><a href="https://assetstore.unity.com/packages/3d/environments/industrial/low-poly-farm-pack-lite-188100">https://assetstore.unity.com/packages/3d/environments/industrial/low-poly-farm-pack-lite-188100</a></p>
<p>그리드 크기에 맞춰서 작게 만들어줍니다(원본/투명 둘 다)
<img src="https://velog.velcdn.com/images/yj_621/post/783e5cf4-fcc4-4a79-ae2b-04f71b6cb33e/image.png" alt="">
<img src="https://velog.velcdn.com/images/yj_621/post/77975663-2cb7-4c34-89cf-08ad2b41cce4/image.png" alt=""></p>
<p>투명 오브젝트는 어케 만드냐?
메테리얼을 생성하고 - 인스펙터 창에서 Surface Type을 Transparent로 변경합니다
<img src="https://velog.velcdn.com/images/yj_621/post/5b9f9289-fddd-4dc2-9954-8f5f63f92315/image.png" alt="">
Blending Mode를 Alpha로 설정하고 만든 머티리얼을 Preview Prefab의 Mesh Renderer에 할당하면 끝!</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/0110748e-62f6-40e2-bd3e-0964a969edf2/image.png" alt=""></p>
<h1 id="2-그리드-시스템의-데이터와-로직-분리">2. 그리드 시스템의 데이터와 로직 분리</h1>
<p>아이소메트릭뷰에 배치 시스템이 있으면 그리드 시스템이 기본으로 깔리겟져?</p>
<p>GridSystem 스크립트 생성 후 Plane에 부착해줘요</p>
<pre><code class="language-csharp">using System;
using UnityEngine;

/// &lt;summary&gt;
/// 마우스 입력을 받아 그리드 상에 오브젝트를 배치하고 
/// 배치 가능 여부를 시각적으로 보여주는 클래스
/// &lt;/summary&gt;
public class GridSystem : MonoBehaviour
{
    [Header(&quot;설정&quot;)]
    public float cellSize = 10f;       // 그리드 한 칸의 크기 (기본 Plane 10x10에 맞춤)
    public LayerMask groundLayer;      // 레이캐스트가 감지할 바닥 레이어

    [Header(&quot;프리팹&quot;)]
    public GameObject previewPrefab;   // 설치 전 보여줄 투명한 미리보기용 프리팹
    public GameObject realPrefab;      // 실제로 설치될 완성된 오브젝트 프리팹

    private GridManager gridManager;   // 설치 데이터를 관리하는 GridManager 참조
    private GameObject previewInstance; // 씬에 생성되어 따라다닐 미리보기 인스턴스

    void Start()
    {
        // 미리보기 인스턴스를 생성하고 초기 설정 진행
        if (previewPrefab != null)
        {
            previewInstance = Instantiate(previewPrefab);
            previewPrefab.SetActive(false);

            // TryGetComponent: GameObject에 존재하는 경우 지정된 유형의 컴포넌트를 검색하려고 시도하고,
            // 발견되면 true, 발견되지 않으면 false를 반환한다.
            //public bool TryGetComponent&lt;T&gt;(out T component) where T : Component;
            /* T: 가져오려는 컴포넌트의 타입
            component: 컴포넌트를 가져올 때 사용되는 out 매개변수 */

            // 미리보기 오브젝트가 마우스 레이캐스트를 방해하지 않도록 콜라이더 비활성화
            if (previewInstance.TryGetComponent&lt;Collider&gt;(out Collider col)) col.enabled = false;
        }
        // 씬에 존재하는 GridManager를 찾아 참조 연결
        if (gridManager == null) gridManager = FindAnyObjectByType&lt;GridManager&gt;();
    }
    void Update()
    {
        // 마우스 위치로부터 화면 안쪽으로 레이(광선)를 쏜다.
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        RaycastHit hit;

        //Ray가 어떤 Collider에 맞으면 true를 반환하고, 그 충돌 정보는 hit에 담긴다.

        // 지정된 groundLayer(바닥)에 레이가 맞았을 때만 로직 실행
        if (Physics.Raycast(ray, out hit, Mathf.Infinity, groundLayer))
        {
            // [좌표 계산] 맞은 지점의 월드 좌표를 그리드 인덱스(0, 1, 2...)로 변환
            int xIdx = Mathf.FloorToInt(hit.point.x / cellSize);
            int zIdx = Mathf.FloorToInt(hit.point.z / cellSize);

            // [스냅 좌표] 오브젝트가 그리드 정중앙에 오도록 좌표 보정 (+cellSize/2)
            Vector3 snapPos = new Vector3(xIdx * cellSize + (cellSize / 2), 0, zIdx * cellSize + (cellSize / 2));

            // 미리보기 위치 업데이트 및 색상 변경 함수 호출
            HandlePreview(snapPos, xIdx, zIdx);

            // 마우스 왼쪽 클릭 시 해당 위치에 실제 설치 시도
            if (Input.GetMouseButtonDown(0))
            {
                PlaceAt(snapPos, xIdx, zIdx);
            }
        }
        else
        {
            // 바닥을 벗어나면 미리보기를 숨김
            if (previewInstance != null)
            {
                previewInstance.SetActive(false);
            }
        }

        /// &lt;summary&gt;
        /// 미리보기 오브젝트를 마우스 위치로 이동시키고, 설치 가능 여부에 따라 색상을 변경하는 메서드
        /// &lt;/summary&gt;
        void HandlePreview(Vector3 pos, int x, int z)
        {
            if (previewInstance == null)
            {
                return;
            }
            previewInstance.SetActive(true);
            previewInstance.transform.position = pos;

            // GridManager에게 현재 칸이 비어있는지 확인 요청
            bool canPlace = gridManager.CanPlace(x, z);

            // 시각적 피드백: 가능하면 초록색(Green), 불가능하면 빨간색(Red) 반투명 처리
            MeshRenderer mr = previewInstance.GetComponentInChildren&lt;MeshRenderer&gt;();
            if(mr != null)
            {
                mr.material.color = canPlace ? new Color(0, 1, 0, 0.5f) : new Color(1, 0, 0, 0.5f);
            }
        }

        /// &lt;summary&gt;
        /// 최종적으로 GridManager의 승인을 받아 실제 오브젝트를 생성하고 점유 데이터를 기록하는 메서드
        /// &lt;/summary&gt;
        void PlaceAt(Vector3 pos, int x, int z)
        {
            if (gridManager.CanPlace(x, z))
            {
                // 실제 오브젝트 생성
                Instantiate(realPrefab, pos, Quaternion.identity);

                // GridManager에 해당 좌표가 점유되었음을 기록 (중복 설치 방지)
                gridManager.PlaceObject(x, z);

                Debug.Log($&quot;[{x}, {z}] 위치에 설치 성공&quot;);
            }
            else
            {
                Debug.Log(&quot;설치 불가 지역&quot;);
            }
        }
    }
}
</code></pre>
<p>GridManager 스크립트 생성 후 GridManager 오브젝트에 부착</p>
<pre><code class="language-csharp">using UnityEngine;

/// &lt;summary&gt;
/// 농장의 물리적인 공간 데이터를 관리하는 클래스
/// 어떤 타일이 비어있고, 어떤 타일이 점유되었는지를 2차원 배열로 저장함
/// &lt;/summary&gt;
public class GridManager : MonoBehaviour
{
    [Header(&quot;농장 크기 설정&quot;)]
    public int width = 10;  // 가로 타일 개수
    public int height = 10; // 세로 타일 개수

    // 타일의 점유 상태를 저장하는 2차원 배열 데이터 구조
    // false : 비어있음 (설치 가능)
    // true  : 이미 무언가 설치됨 (설치 불가)
    private bool[,] isOccupied;

    /// &lt;summary&gt;
    /// 게임 시작 시 설정된 크기에 맞춰 배열 메모리를 할당
    /// &lt;/summary&gt;
    private void Awake()
    {
        // width x height 크기의 바둑판 모양 데이터를 생성
        isOccupied = new bool[width, height];
    }

    /// &lt;summary&gt;
    /// 특정 좌표(x, z)에 오브젝트를 설치할 수 있는지 검사
    /// &lt;/summary&gt;
    /// &lt;param name=&quot;x&quot;&gt;검사할 그리드 X 인덱스&lt;/param&gt;
    /// &lt;param name=&quot;z&quot;&gt;검사할 그리드 Z 인덱스&lt;/param&gt;
    /// &lt;returns&gt;설치 가능하면 true, 불가능하면 false&lt;/returns&gt;
    public bool CanPlace(int x, int z)
    {
        // 인덱스가 맵의 범위를 벗어났는지 먼저 체크 (배열 오류 방지)
        if (x &lt; 0 || x &gt;= width || z &lt; 0 || z &gt;= height)
        {
            return false; // 맵 밖은 설치 불가
        }

        // 해당 칸의 점유 상태를 반전시켜 반환
        // !isOccupied[x, z] 의 의미:
        // 점유(true)면 설치불가(false) 반환, 비었으면(false) 설치가능(true) 반환
        return !isOccupied[x, z];
    }

    /// &lt;summary&gt;
    /// 오브젝트 설치가 확정되었을 때 해당 칸의 상태를 &#39;점유됨&#39;으로 변경
    /// &lt;/summary&gt;
    /// &lt;param name=&quot;x&quot;&gt;점유할 그리드 X 인덱스&lt;/param&gt;
    /// &lt;param name=&quot;z&quot;&gt;점유할 그리드 Z 인덱스&lt;/param&gt;
    public void PlaceObject(int x, int z)
    {
        // 맵 범위 안일 때만 실행
        if (x &gt;= 0 &amp;&amp; x &lt; width &amp;&amp; z &gt;= 0 &amp;&amp; z &lt; height)
        {
            isOccupied[x, z] = true;
            Debug.Log($&quot;데이터 업데이트: [{x}, {z}] 지점이 점유되었습니다.&quot;);
        }
    }
}
</code></pre>
<p>농장 크기는 Plane에 그리드 개수만큼 해줌(저는 2,2,2 크기로해서 20, 20이 나옴)
<img src="https://velog.velcdn.com/images/yj_621/post/a4470293-a53f-4bda-b468-96784deb371a/image.png" alt="">
<img src="https://velog.velcdn.com/images/yj_621/post/bbcceade-be10-4f8b-86a4-0aff972c138b/image.png" alt=""></p>
<p>0,0,0 위치가 맨 아래가도록 배치해주고 Plane의 Layer를 Ground로 설정해주면 끝 
왜 이럿케 하느냐
x,z 좌표가 양수인 부분만 배치가 가능하게 해둠</p>
<p>왜&quot;???</p>
<blockquote>
<p>가장 큰 이유는 배열(GridManager의 tileTypes)의 인덱스는 음수가 될 수 없기 때문입니다.</p>
<p>배열의 한계: GridManager에서 사용하는 new TileType[width, height]는 0부터 시작하는 &#39;방 번호&#39;를 가집니다. 컴퓨터 과학에서 배열 인덱스는 항상 0 또는 양수입니다.
좌표 변환 방식: 현재 GridSystem에서 인덱스를 계산할 때 아래 공식을 사용합니다.
int xIdx = Mathf.FloorToInt(hit.point.x / cellSize);</p>
</blockquote>
<p>만약 hit.point.x가 -5.0이고 cellSize가 10이라면, 결과는 -1이 됩니다.
코드의 방어막: GridManager의 CanPlace 함수에는 아래와 같은 조건문이 있습니다.
if (x &lt; 0 || x &gt;= width ...)
여기서 음수 인덱스(-1)가 들어오면, 배열 오류(IndexOutOfRangeException)를 막기 위해 무조건 false(배치 불가)를 반환하도록 설계되어 있습니다.</p>
<p>라는 제미나이의 답변,, 요거는 좀 더 공부를 해바야겟음
<img src="https://velog.velcdn.com/images/yj_621/post/f8a33d45-3348-4558-8bd5-d9c9d102baca/image.png" alt=""></p>
<h1 id="3-결과">3. 결과</h1>
<p><img src="https://velog.velcdn.com/images/yj_621/post/7fcbf819-08d0-4a0f-a7a0-7b56784a310f/image.png" alt="">
클릭하면 밭이 배치되고, 배치할 수 없는(이미 점유된)곳에 배치하려고 하면 빨간색으로 표시,</p>
<p>가능한 곳은 초록색으로 표시되게 함
<img src="https://velog.velcdn.com/images/yj_621/post/44cc06fb-44a4-47ee-a99c-38b63856582f/image.gif" alt=""></p>
<h1 id="4-지식-">4. 지식 +</h1>
<h3 id="🔷-trygetcomponent">🔷 TryGetComponent?</h3>
<blockquote>
<p>💡 GameObject에 존재하는 경우 지정된 유형의 컴포넌트를 검색하려고 시도하고, 발견되면 true, 발견되지 않으면 false를 반환한다.</p>
</blockquote>
<pre><code>public bool TryGetComponent&lt;T&gt;(out T component) where T : Component;</code></pre><blockquote>
<p>T: 가져오려는 컴포넌트의 타입
component: 컴포넌트를 가져올 때 사용되는 out 매개변수</p>
</blockquote>
<h3 id="🔷physicsraycast">🔷Physics.Raycast</h3>
<blockquote>
<p>💡 Ray가 어떤 Collider에 맞으면 true를 반환하고, 그 충돌 정보는 hit에 담긴다.</p>
</blockquote>
<pre><code>Physics.Raycast(ray, out hit, Mathf.Infinity, groundLayer)</code></pre><blockquote>
</blockquote>
<p>: groundLayer에 맞으면 hit에 정보가 담기고 길이는 무한</p>
<h2 id="💡-기술적-포인트"><strong>💡 기술적 포인트</strong></h2>
<ol>
<li><strong>데이터 무결성 (Data Integrity)</strong>:</li>
</ol>
<ul>
<li>시각적인 오브젝트(GameObject)는 파괴되거나 오류가 날 수 있지만, <code>GridManager</code>의 <code>bool</code> 배열은 현재 농장 상태의 <strong>절대적인 기준</strong>이 됨</li>
</ul>
<ol start="2">
<li><strong>연산 효율성</strong>:</li>
</ol>
<ul>
<li>이미 설치된 게 있나?를 알기 위해 <code>Physics.OverlapBox</code> 같은 무거운 물리 연산을 쓰지 않고, 단순한 배열 인덱스 참조(O(1))만으로 판별하기 때문에 매우 빠름</li>
</ul>
<ol>
<li><strong>예외 처리 능력</strong>:</li>
</ol>
<ul>
<li><code>if (x &lt; 0 || x &gt;= width ...)</code>와 같은 조건문을 통해 배열 인덱스 초과 에러(IndexOutOfRangeException)를 사전에 방지</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[UniTask를 이용한 비동기 처리]]></title>
            <link>https://velog.io/@yj_621/UniTask%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%B2%98%EB%A6%AC</link>
            <guid>https://velog.io/@yj_621/UniTask%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%B2%98%EB%A6%AC</guid>
            <pubDate>Wed, 14 Jan 2026 06:58:01 GMT</pubDate>
            <description><![CDATA[<h2 id="🔷-unitask---유니티-최적화-라이브러리">🔷 UniTask - 유니티 최적화 라이브러리</h2>
<p>기존 C#의 Task는 무겁기도 하고 GC 할당이 계속 발생해서 UniTask를 사용했습니다.</p>
<p>구조체(struct) 기반이라 메모리 할당이 거의 없어요!</p>
<p>일단 패키지부터 설치해야겠죠?</p>
<p>Window -&gt; Package Manager -&gt; Add package from git URL 누르고 아래 주소 입력!</p>
<p><a href="https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask">https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask</a>
<img src="https://velog.velcdn.com/images/yj_621/post/302fd349-0886-4e98-bc02-52e80b7453cd/image.png" alt=""></p>
<p>아래 링크 넣어서 다운로드</p>
<pre><code>https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask</code></pre><h2 id="📄-1-네트워크-통신-리팩토링-coroutine-→-unitask">📄 1. 네트워크 통신 리팩토링 (Coroutine → UniTask)</h2>
<p>원래 Coroutine이랑 Action(콜백) 써서 비동기 통신을 구현했었는데, 이게 가독성이 진짜 안 좋아서 UniTask로 싹 바꿔버렸습니다. 그리고 공부할겸! 활용해봤습니다~</p>
<h3 id="①-통신-매니저-pythonconnectmanagercs">① 통신 매니저 (PythonConnectManager.cs)</h3>
<p>** [Before] 기존 코드: 코루틴 + 콜백 **</p>
<p>결과값을 바로 return 못하고 callback으로 넘겨줘야 하는게 제일 불편함 ㅠㅠ</p>
<pre><code>// IEnumerator 반환, 결과는 Action 콜백으로 넘겨줌
public IEnumerator MostSimilarty(string inputWord, int num, Action&lt;List&lt;string&gt;&gt; callback)
{
    // ... (중략) ...

    using (UnityWebRequest request = new UnityWebRequest(url, &quot;POST&quot;))
    {
        // [기존 방식] 여기서 yield return으로 대기
        yield return request.SendWebRequest();

        if (request.result == UnityWebRequest.Result.Success)
        {
            // ... 데이터 파싱 ...
            // [단점] 값을 직접 리턴 못하고 콜백 함수 호출해야 함
            callback(responseData.result);
        }
        else
        {
            callback(new List&lt;string&gt; { &quot;요청 실패&quot; });
        }
    }
}</code></pre><p>** [After] 개선된 코드: UniTask + Async/Await **</p>
<p>async 키워드 붙이고 결과값을 바로 return 받을 수 있어서 완전 직관적임! </p>
<pre><code>using Cysharp.Threading.Tasks; // 필수!

// async 키워드 사용, List&lt;string&gt;을 직접 반환!
public async UniTask&lt;List&lt;string&gt;&gt; MostSimilarty(string inputWord, int num)
{
    // ... (중략) ...

    using (UnityWebRequest request = new UnityWebRequest(url, &quot;POST&quot;))
    {
        // [개선] await로 기다림
        await request.SendWebRequest();

        if (request.result == UnityWebRequest.Result.Success)
        {
             // ... 데이터 파싱 ...
            // [장점] 그냥 바로 return 때리면 됨
            return responseData.result;
        }
        else
        {
            return new List&lt;string&gt; { &quot;요청 실패&quot; };
        }
    }
}</code></pre><h3 id="②-호출하는-곳-algorithmcallcs">② 호출하는 곳 (AlgorithmCall.cs)</h3>
<p><strong>[Before] 기존: 콜백 지옥 (계단식 코드)</strong></p>
<p>StartCoroutine 안에 람다식 (result) =&gt; { ... } 들어가니까 들여쓰기가 계속 깊어짐;;</p>
<pre><code>public void OnShowSimilarWord()
{
    // ... 생략 ...

    // [단점] StartCoroutine + 람다식 콜백 = 가독성 떨어짐
    StartCoroutine(pythonConnectManager.MostSimilarty(answerWord, 5, (result) =&gt;
    {
        loading?.StopAnim();

        // --- 여기서부터 결과 처리 로직 ---
        if (result == null) { ... }

        // 로직이 길어질수록 계속 들여쓰기 해야함 ㅠㅠ
        resultText.text = $&quot;관련 단어 : {result[idx]}&quot;;
    }));
}</code></pre><p><strong>[After] 개선: 선형적인 코드</strong></p>
<pre><code>// async void로 변경 (유니티 이벤트 함수용)
public async void OnShowSimilarWord()
{
    // ... 생략 ...

    // [장점] await로 결과값 바로 받아옴! (코루틴 삭제)
    List&lt;string&gt; result = await pythonConnectManager.MostSimilarty(answerWord, 5);

    loading?.StopAnim();

    // --- 들여쓰기 없이 바로 로직 작성 가능 ---
    if (result == null) { ... }

    resultText.text = $&quot;관련 단어 : {result[idx]}&quot;;
}</code></pre><h2 id="📄-2-비동기-씬-로드">📄 2. 비동기 씬 로드</h2>
<p>네트워크만 바꾸긴 아쉬워서 씬 로딩도 바꿨습니당.
보통 로딩바 구현할 때 StartCoroutine이랑 yield return null 쓰잖아요? 근데 yield return null이 미세하게 가비지를 만든다는 사실.. UniTask 쓰면 이것도 해결된다고 하더라구요</p>
<p>**[Before] **</p>
<pre><code>IEnumerator LoadRoutine(string targetScene)
{
    AsyncOperation op = SceneManager.LoadSceneAsync(targetScene);
    op.allowSceneActivation = false;

    while (!op.isDone)
    {
        yield return null; // 매 프레임 가비지 생성 가능성 있음
        // ... 진행률 계산 로직 ...
    }
}</code></pre><p>**[After] **</p>
<pre><code>// Start에서 바로 비동기 함수 호출 가능!
async void Start()
{
    await LoadSceneAsync(&quot;TargetScene&quot;);
}

async UniTask LoadSceneAsync(string targetScene)
{
    await UniTask.Yield(); // 첫 프레임 대기

    AsyncOperation op = SceneManager.LoadSceneAsync(targetScene);
    op.allowSceneActivation = false;

    while (!op.isDone)
    {
        // [핵심] struct 기반이라 메모리 할당 없음 (Zero Allocation)
        await UniTask.Yield(); 

        // ... 진행률 계산 (기존과 동일) ...
        float raw = Mathf.Clamp01(op.progress / 0.9f);
        loadingUI?.SetProgress(raw);

        if (raw &gt;= 1f)
        {
             op.allowSceneActivation = true;
             break;
        }
    }
}</code></pre><p>Start()를 async void로 바꾸고 UniTask.Yield()를 쓰면 끗</p>
<p>💡 UniTask의 효과!</p>
<ul>
<li><p>가독성 떡상: 콜백 지옥 탈출하고 코드가 깔끔해짐</p>
</li>
<li><p>메모리 절약: yield return null 대신 UniTask.Yield() 써서 GC 방어</p>
</li>
<li><p>예외 처리: 코루틴에선 안되던 try-catch가 먹힘</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[유니티 셰이더로 이미지 흑백 처리하기]]></title>
            <link>https://velog.io/@yj_621/%EC%9C%A0%EB%8B%88%ED%8B%B0-%EC%85%B0%EC%9D%B4%EB%8D%94%EB%A1%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-%ED%9D%91%EB%B0%B1-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@yj_621/%EC%9C%A0%EB%8B%88%ED%8B%B0-%EC%85%B0%EC%9D%B4%EB%8D%94%EB%A1%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-%ED%9D%91%EB%B0%B1-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 08 Jan 2026 06:51:52 GMT</pubDate>
            <description><![CDATA[<p>오늘도 제미나이에게 도움을..</p>
<p>왼쪽 일반적인 스프라이트를 흑백처리하는 방법입니당</p>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/yj_621/post/50a2199f-4246-4cf8-9b0f-c7871f2733b0/image.png" alt=""></th>
<th><img src="https://velog.velcdn.com/images/yj_621/post/6d6f5b36-824c-4102-957b-24dc3f0bdbb7/image.png" alt=""></th>
</tr>
</thead>
</table>
<p>셰이더말고 답이 없더라고요,, 일단 이렇게 하면되는데 하나씩 차근차근 알려드릴게엽
<img src="https://velog.velcdn.com/images/yj_621/post/4b7af5fe-fc61-4561-a9f8-db73bc692211/image.png" alt=""></p>
<h2 id="ui-canvas-shader-graph-사용-유니티-6-권장"><strong>UI Canvas Shader Graph 사용 (유니티 6 권장)</strong></h2>
<h3 id="1-셰이더-그래프-생성">1. 셰이더 그래프 생성</h3>
<ol>
<li><code>Project</code> 창 우클릭 -&gt; <code>Create</code> -&gt; <code>Shader Graph</code> -&gt; <code>UI</code> -&gt; <code>Canvas Shader Graph</code>를 선택</li>
</ol>
<p><img src="https://velog.velcdn.com/images/yj_621/post/19d23b2e-4481-42a4-a635-082314f54982/image.png" alt=""></p>
<ol start="2">
<li>이름을 짓고 더블 클릭하여 열면</li>
</ol>
<h3 id="2-필수-속성-만들기">2. 필수 속성 만들기</h3>
<p>좌측 <code>Blackboard</code>에서 다음 두 가지를 추가</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/b91e4020-0c07-4810-a484-784972177a26/image.png" alt=""></p>
<ol>
<li><strong>Texture2D</strong> 추가 -&gt; 이름을 반드시 <code>_MainTex</code>로 변경<ul>
<li><em>주의: Reference 이름도 <code>_MainTex</code>여야 UI Image 컴포넌트의 소스 이미지를 자동으로 받아옴</em></li>
</ul>
</li>
<li><strong>Float</strong> 추가 -&gt; 이름을 <code>_GrayscaleAmount</code>로 짓고 Default Value는 1로</li>
</ol>
<p>_MainText는 아무것도 안 건드림</p>
<table>
<thead>
<tr>
<th><img src="https://velog.velcdn.com/images/yj_621/post/92226fd8-4ec4-4f92-b0ac-5111f6fb5875/image.png" alt=""></th>
<th><img src="https://velog.velcdn.com/images/yj_621/post/fc9c3e13-ec9f-496e-889d-3698fdc851d7/image.png" alt=""></th>
</tr>
</thead>
<tbody><tr>
<td>### 3. 노드 연결하기</td>
<td></td>
</tr>
</tbody></table>
<p>빈 공간에 우클릭하여 노드를 추가(<code>Create Node</code>)하고 아래 순서대로 연결하면 되는데 좀 복잡함 ㅠㅠ</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/32f091d0-67ef-4231-9c7d-92633c95f6e8/image.png" alt=""></p>
<ol>
<li><strong><code>Sample Texture 2D</code> 노드 생성</strong><br> <img src="https://velog.velcdn.com/images/yj_621/post/0d733e15-0408-45ea-8c9b-23beeb5b9b35/image.png" alt=""></li>
</ol>
<pre><code>- Blackboard의 _MainTex를 끌어와서 Texture 슬롯에 연결
- RGBA 출력값을 사용</code></pre><p><img src="https://velog.velcdn.com/images/yj_621/post/a1e8096d-c594-4a73-877e-26ae336854e6/image.png" alt=""></p>
<ol start="2">
<li><strong><code>Saturation</code> 노드 생성</strong><br><img src="https://velog.velcdn.com/images/yj_621/post/b8887153-1d36-4b4d-a3ce-ab42c2fa8292/image.png" alt=""><ul>
<li>Sample Texture 2D의 출력을 In에 연결</li>
<li>Saturation값에는 0을 입력 (흑백)
<img src="https://velog.velcdn.com/images/yj_621/post/8580b2f8-612e-427e-b01b-7e3c7f278d97/image.png" alt=""></li>
</ul>
</li>
</ol>
<ol start="3">
<li><p><strong><code>Lerp</code> 노드 생성</strong> (원본과 흑백을 섞기 위함)
 <img src="https://velog.velcdn.com/images/yj_621/post/5f7e3bab-eff1-4962-8bc5-884898531a03/image.png" alt=""></p>
<ul>
<li>A: Sample Texture 2D의 출력 (원본 색)<ul>
<li>B: Saturation 노드의 출력 (흑백 색)</li>
<li>T: Blackboard의 _GrayscaleAmount 연결
<img src="https://velog.velcdn.com/images/yj_621/post/46254135-ce67-408a-9856-e2814f19936d/image.png" alt=""></li>
</ul>
</li>
</ul>
</li>
<li><p><strong><code>Vertex Color</code> 노드 생성</strong> (투명도 조절을 위해 필수)<br><img src="https://velog.velcdn.com/images/yj_621/post/3f11ef80-83d5-4e88-ab89-848265d39ea4/image.png" alt=""></p>
</li>
<li><p><strong><code>Multiply</code> 노드 생성</strong>
<img src="https://velog.velcdn.com/images/yj_621/post/8e6bdf30-dd28-4dde-bfc5-06431cb7c3cf/image.png" alt="">
 -A :  Lerp의 결과
 -B :  Vertex Color 노드
 <img src="https://velog.velcdn.com/images/yj_621/post/2f9ee067-2630-4bf9-a2e3-eefba4ba359d/image.png" alt=""></p>
</li>
<li><p><strong>최종 연결</strong></p>
<ul>
<li><p><code>Multiply</code>의 결과를 <strong>Master Stack</strong>의 <code>Base Color</code>와 <code>Alpha</code>에 연결</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/e89294d5-57dd-4484-8c8d-b26268365a27/image.png" alt=""></p>
</li>
</ul>
</li>
</ol>
<h3 id="4-머터리얼-적용">4. 머터리얼 적용</h3>
<p>저장 후 만들어진 셰이더 파일 우클릭 -&gt; <code>Create</code> -&gt; <code>Material</code></p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/648fe76f-886b-4e43-9061-4a883660d37e/image.png" alt=""></p>
<p>Material을 눌러보면 Grayscale Amount 속성을 1로 바꿔주면 됨니다(0 :원본, 1 :흑백)</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/40068865-8018-4f6e-aa8f-6dec082b9f70/image.png" alt=""></p>
<p>만들어진 Matrial를 해당 Material 자리에 넣어주면 끝!</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/0a8a8afc-be49-4eed-a84c-89288a3713e2/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[유니티에서 버튼을 선택했을때 스프라이트 교체]]></title>
            <link>https://velog.io/@yj_621/%EC%9C%A0%EB%8B%88%ED%8B%B0%EC%97%90%EC%84%9C-%EB%B2%84%ED%8A%BC%EC%9D%84-%EC%84%A0%ED%83%9D%ED%96%88%EC%9D%84%EB%95%8C-%EC%8A%A4%ED%94%84%EB%9D%BC%EC%9D%B4%ED%8A%B8-%EA%B5%90%EC%B2%B4</link>
            <guid>https://velog.io/@yj_621/%EC%9C%A0%EB%8B%88%ED%8B%B0%EC%97%90%EC%84%9C-%EB%B2%84%ED%8A%BC%EC%9D%84-%EC%84%A0%ED%83%9D%ED%96%88%EC%9D%84%EB%95%8C-%EC%8A%A4%ED%94%84%EB%9D%BC%EC%9D%B4%ED%8A%B8-%EA%B5%90%EC%B2%B4</guid>
            <pubDate>Thu, 08 Jan 2026 06:26:16 GMT</pubDate>
            <description><![CDATA[<p>오늘은 유니티에서 버튼에 있는 Sprite Swap기능으로 스프라이트 교체가 되는것이 아닌 방법을 소개하겠습니다</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/231f1c56-e36c-4a13-86bc-f1d873e5efdf/image.png" alt=""></p>
<p>이 방법을 사용하면 다른 곳을 누르면 바로 풀리는 문제가 있어서 제미나이한테 물어봐서 해결했습니당</p>
<p>일단 이 코드가 제일 중요한데 버튼의 interactable 기능을 이용했습니다! </p>
<pre><code class="language-csharp">private void Switch(JamoDefsType type)
    {
        _current = type;

        // 선택된 탭은 클릭이 안 되게(interactable = false) 만들어서
        // 인스펙터에서 설정한 &#39;Disabled Sprite(흰 테두리)&#39;가 보이도록 함
        if (btnConsonant != null) 
            btnConsonant.interactable = (type != JamoDefsType.Consonant);

        if (btnVowel != null) 
            btnVowel.interactable = (type != JamoDefsType.Vowel);

        RebuildGrid(type == JamoDefsType.Consonant ? JamoDefs.Consonants : JamoDefs.Vowels);
    }</code></pre>
<p>interactable가 꺼져있으면 버튼 상호작용이 불가능하게 되고, 켜져있으면 버튼과 상호작용이 가능한 기능인데 버튼을 누르는 순간 interactable이 false가 되면서 Disabled Sprite를 보여주는 것임 </p>
<ul>
<li>Disabled Sprite : 버튼이 비활성화되었을때 보여줄 스프라이트</li>
</ul>
<p><img src="https://velog.velcdn.com/images/yj_621/post/6b4afd72-8784-40f7-9213-f00bcd6f6174/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[유니티에서 Google AdMob를 사용해서 광고 삽입하기]]></title>
            <link>https://velog.io/@yj_621/%EC%9C%A0%EB%8B%88%ED%8B%B0%EC%97%90%EC%84%9C-Google-AdMob%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%84%9C-%EA%B4%91%EA%B3%A0-%EC%82%BD%EC%9E%85%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@yj_621/%EC%9C%A0%EB%8B%88%ED%8B%B0%EC%97%90%EC%84%9C-Google-AdMob%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%84%9C-%EA%B4%91%EA%B3%A0-%EC%82%BD%EC%9E%85%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 19 Dec 2025 08:51:36 GMT</pubDate>
            <description><![CDATA[<p><a href="https://code-piggy.tistory.com/entry/Unity-Google-AdMob-%EA%B4%91%EA%B3%A0-%EC%82%BD%EC%9E%85%ED%95%98%EB%8A%94-%EB%B2%95%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8-%EB%8B%A4%EC%9A%B4%EB%A1%9C%EB%93%9C-%EA%B4%91%EA%B3%A0-%EC%BD%94%EB%93%9C#google_vignette">Unity - Google AdMob 광고 삽입하는 법(플러그인 다운로드, 광고 코드)</a>
이 게시물을 참고했습니다 감사합니다(__)</p>
<p><a href="https://github.com/googleads/googleads-mobile-unity/releases/tag/v10.5.0">https://github.com/googleads/googleads-mobile-unity/releases/tag/v10.5.0
</a></p>
<p>이 링크에서 </p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/82579d4e-ae65-4054-92a8-e85fcc02c8f6/image.png" alt=""></p>
<p>패키지 다운 후 드래그</p>
<p>Asset - Google ~ - Settings</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/fd2ffd87-9210-4383-b802-dad76671fb84/image.png" alt=""></p>
<p>Enable 선택 후</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/aeb3969a-b64b-4159-a1ef-d44805d8c77a/image.png" alt=""></p>
<p>여기서 Android 에 ID 입력</p>
<p><a href="https://admob.google.com/intl/ko/home/">Google AdMob: 모바일 앱 수익 창출</a></p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/949dad20-9355-4b0c-9b05-21a182b38468/image.png" alt=""></p>
<p>뭔진 모르겠지만 있어야하는듯함</p>
<pre><code class="language-csharp">using GoogleMobileAds;
using GoogleMobileAds.Api;
using UnityEngine;

public class GoogleMobileAdsDemoScript : MonoBehaviour
{
    public void Awake()
    {
        // Initialize Google Mobile Ads SDK.
        MobileAds.Initialize((InitializationStatus initStatus) =&gt;
        {
            // This callback is called once the MobileAds SDK is initialized.
        });
    }

}</code></pre>
<p>겜매에 두개를 넣어줌</p>
<p>인스펙터의 저 아이디는 광고 단위에서 추가한 아이디임</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/12971a82-506e-4328-a125-21824745995a/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/a5f7b444-fa2b-4001-958c-2b79cf0db82a/image.png" alt=""></p>
<p>AdsManager.cs</p>
<pre><code class="language-csharp">using System;
using GoogleMobileAds;
using GoogleMobileAds.Api;
using UnityEngine;

public class AdsManager : MonoBehaviour
{
    public static AdsManager I { get; private set; }

#if UNITY_ANDROID
    [SerializeField] private string rewardedAdRevivalId = &quot;ca-app-pub-1881501262849586/3221896109&quot;; // 테스트용
#elif UNITY_IOS
    [SerializeField] private string rewardedAdUnitId = &quot;ca-app-pub-3940256099942544/1712485313&quot;; // 테스트용
#else
    [SerializeField] private string rewardedAdUnitId = &quot;unused&quot;;
#endif

    private RewardedAd _rewardedAd;
    private bool _initializing;

    private void Awake()
    {
        if (I != null) { Destroy(gameObject); return; }
        I = this;
        DontDestroyOnLoad(gameObject);
    }

    private void Start()
    {
        InitializeSdkIfNeeded(() =&gt; PreloadRewarded());
    }

    public void InitializeSdkIfNeeded(Action onDone = null)
    {
        if (_initializing) return;
        _initializing = true;

        MobileAds.Initialize((InitializationStatus status) =&gt;
        {
            _initializing = false;
            onDone?.Invoke();
        });
    }

    // 미리 로드
    public void PreloadRewarded()
    {
        if (string.IsNullOrEmpty(rewardedAdRevivalId)) return;

        if (_rewardedAd != null)
        {
            _rewardedAd.Destroy();
            _rewardedAd = null;
        }

        var request = new AdRequest();
        RewardedAd.Load(rewardedAdRevivalId, request, (RewardedAd ad, LoadAdError error) =&gt;
        {
            if (error != null)
            {
                Debug.LogWarning($&quot;[Ads] Rewarded load failed: {error.GetMessage()}&quot;);
                return;
            }

            _rewardedAd = ad;
            HookRewardedEvents(_rewardedAd);
            Debug.Log(&quot;[Ads] Rewarded loaded.&quot;);
        });
    }

    private void HookRewardedEvents(RewardedAd ad)
    {
        ad.OnAdFullScreenContentOpened += () =&gt;
        {
            Debug.Log(&quot;[Ads] Rewarded opened.&quot;);
        };
        ad.OnAdFullScreenContentClosed += () =&gt;
        {
            Debug.Log(&quot;[Ads] Rewarded closed.&quot;);
            // 닫히면 다음 광고 미리 로드
            PreloadRewarded();
        };
        ad.OnAdFullScreenContentFailed += (AdError err) =&gt;
        {
            Debug.LogWarning($&quot;[Ads] Rewarded open failed: {err.GetMessage()}&quot;);
            PreloadRewarded();
        };
    }

    /// &lt;summary&gt;
    /// 공용: 리워드 광고 표시 (보상 콜백/미준비 콜백)
    /// &lt;/summary&gt;
    public void ShowRewarded(Action onRewardEarned, Action onUnavailable = null)
    {
        if (_rewardedAd != null &amp;&amp; _rewardedAd.CanShowAd())
        {
            _rewardedAd.Show((Reward reward) =&gt;
            {
                onRewardEarned?.Invoke();
            });
        }
        else
        {
            Debug.LogWarning(&quot;[Ads] Rewarded not ready.&quot;);
            onUnavailable?.Invoke();
            PreloadRewarded();
        }
    }

    /// &lt;summary&gt;
    /// 부활 전용 진입점
    /// &lt;/summary&gt;
    public void ShowReviveAd(Action onRevive, Action onFail = null)
    {
        ShowRewarded(
            onRewardEarned: onRevive,
            onUnavailable: onFail
        );
    }
}
</code></pre>
<p>GameReviveSystem.cs</p>
<pre><code class="language-csharp">using System;
using UnityEngine;
using WordEater.Core;

[Serializable]
public class WordEaterCheckpoint
{
    public Vector3 Position;
    public int BatteryPercent;
    public int TurnsLeft;
    public int MistakesLeft;
    public GrowthStage Stage;
    public string CurrentAnswer;
}

public class GameReviveSystem : MonoBehaviour
{
    public static GameReviveSystem I { get; private set; }
    [SerializeField] private RevivePopup revivePopup;

    private WordEaterCheckpoint _cp;
    private bool _reviveOffered;

    void Awake() =&gt; I = this;

    public void SaveCheckpoint(WordEater.Core.WordEater we, int batteryPercent)
    {
        _cp = new WordEaterCheckpoint
        {
            Position = we.transform.position,
            BatteryPercent = Mathf.Clamp(batteryPercent, 0, 100),
            TurnsLeft = we.GetTurnsLeft(),         //
            MistakesLeft = we.GetMistakesLeft(),
            Stage = we.ReturnStage(),
            CurrentAnswer = we.CurrentAnswer
        };
    }
    public void OnPlayerDied(Action onGiveUp)
    {
        if (_reviveOffered) return;
        _reviveOffered = true;

        if (revivePopup == null)
        {
            Debug.LogWarning(&quot;[Revive] revivePopup 미할당&quot;);
            _reviveOffered = false;
            onGiveUp?.Invoke();
            return;
        }

        // ▶ 게임 정지: UI는 Unscaled로 뜨게
        Time.timeScale = 0f;

        revivePopup.Show(
            onAccept: () =&gt;
            {
                ReviveFromCheckpoint();
                Debug.Log(&quot;[WordEater] 턴 고갈 → 사망 처리&quot;);
                _reviveOffered = false;
                Time.timeScale = 1f;  // ▶ 재개
            },
            onDecline: () =&gt;
            {
                _reviveOffered = false;
                Time.timeScale = 1f;  // ▶ 재개
                onGiveUp?.Invoke();   // ▶ EndingController(1) 호출됨
            }
        );
    }

    public void ReviveFromCheckpoint()
    {
        var player = FindFirstObjectByType&lt;WordEater.Core.WordEater&gt;();
        if (player == null) { Debug.LogWarning(&quot;[Revive] WordEater 없음&quot;); return; }
        if (_cp == null) { Debug.LogWarning(&quot;[Revive] 체크포인트 없음&quot;); return; }

        // 위치/배터리 복원
        player.transform.position = _cp.Position;
        var battery = player.GetComponent&lt;WordEater.Systems.BatterySystem&gt;()
                     ?? FindFirstObjectByType&lt;WordEater.Systems.BatterySystem&gt;();
        if (battery != null) battery.SetBatteryPercent(Mathf.Max(_cp.BatteryPercent, 50));

        // 턴/오답/정답/단계 복원
        player.RestoreTurns(_cp.TurnsLeft, _cp.MistakesLeft);
        player.RestoreAnswer(_cp.CurrentAnswer, _cp.Stage);

        // 최종 활성화
        player.Reactivate();

        Debug.Log(&quot;[Revive] 체크포인트로 완전 복원 완료&quot;);
    }
}
</code></pre>
<p>얘는 초기 알파값을 0으로 일단 해두고</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/2547d22c-68e3-4107-a23e-4a926f66014c/image.png" alt=""></p>
<p>RevivePopup.cs</p>
<pre><code class="language-csharp">using System;
using UnityEngine;
using UnityEngine.UI;

public class RevivePopup : MonoBehaviour
{
    [Header(&quot;Refs&quot;)]
    [SerializeField] private CanvasGroup canvasGroup;
    [SerializeField] private Button watchAdButton;
    [SerializeField] private Button noThanksButton;
    [SerializeField] private GameObject spinner; // 로딩 표시용

    private Action _onAccept;
    private Action _onDecline;
    private bool _visible;

    private void Awake()
    {
        if (watchAdButton != null) watchAdButton.onClick.AddListener(OnClickWatchAd);
        if (noThanksButton != null) noThanksButton.onClick.AddListener(OnClickNoThanks);
        HideImmediate();
    }

    public void Show(Action onAccept, Action onDecline)
    {
        _onAccept = onAccept;
        _onDecline = onDecline;
        _visible = true;

        if (spinner != null) spinner.SetActive(false);

        // 팝업은 UnscaledTime 기반으로 동작 (애니메이션/타이머 쓴다면)
        var cg = canvasGroup;
        if (cg != null)
        {
            cg.ignoreParentGroups = true;
            cg.blocksRaycasts = true;
            cg.interactable = true;
        }

        // 최상단 보장(없다면 추가)
        var canvas = GetComponentInParent&lt;Canvas&gt;();
        if (canvas != null)
        {
            canvas.overrideSorting = true;
            canvas.sortingOrder = 10000;
        }

        SetCanvas(true);
    }

    public void Hide()
    {
        _visible = false;
        SetCanvas(false);
    }

    private void HideImmediate()
    {
        _visible = false;
        SetCanvas(false, immediate: true);
    }

    private void SetCanvas(bool show, bool immediate = false)
    {
        if (canvasGroup == null) return;
        canvasGroup.alpha = show ? 1f : 0f;
        canvasGroup.blocksRaycasts = show;
        canvasGroup.interactable = show;
        gameObject.SetActive(show);
    }

    private void OnClickWatchAd()
    {
        if (!_visible) return;

        if (spinner != null) spinner.SetActive(true);
        watchAdButton.interactable = false;
        noThanksButton.interactable = false;

        // 광고 표시
        AdsManager.I.ShowReviveAd(
            onRevive: () =&gt;
            {
                // 광고 보상 수령 → 부활 승인
                _onAccept?.Invoke();
                Hide();
                ResetButtons();
            },
            onFail: () =&gt;
            {
                // 광고가 준비 안됨/실패 → 안내 후 버튼 복구
                if (spinner != null) spinner.SetActive(false);
                watchAdButton.interactable = true;
                noThanksButton.interactable = true;
                Debug.LogWarning(&quot;[Revive] 광고가 준비되지 않았습니다. 잠시 후 다시 시도해주세요.&quot;);
            }
        );
    }

    private void OnClickNoThanks()
    {
        _onDecline?.Invoke();
        Hide();
        ResetButtons();
    }

    private void ResetButtons()
    {
        if (spinner != null) spinner.SetActive(false);
        watchAdButton.interactable = true;
        noThanksButton.interactable = true;
    }
}
</code></pre>
<p>워드이터가 죽었을때 띄우게 하려고 일단 전체 코드</p>
<pre><code class="language-csharp">using System;
using System.IO;
using UnityEngine;
using WordEater.Data;
using WordEater.Services;
using WordEater.Systems;
using static UnityEngine.EventSystems.EventTrigger;

namespace WordEater.Core
{
    /// &lt;summary&gt;
    /// 한 마리 워드 이터의 생애 주기를 관리하는 상태 머신
    /// &lt;/summary&gt;
    public class WordEater : MonoBehaviour
    {
        [Header(&quot;할당&quot;)]
        [SerializeField] private GrowthConfig growthConfig;          // 단계 규칙 SO
        [SerializeField] private WordAssignmentService wordService;  // 단어 배정 
        [SerializeField] private BatterySystem battery;
        [SerializeField] private SubmitManager submitmanager;
        [SerializeField] private GameManager gamemanager;
        [SerializeField] private GalleryUIManager galleryUIManager;

        [SerializeField] private Sprite BitImg;
        [SerializeField] private Sprite ByteImg;
        [SerializeField] private Sprite WordImg;
        [SerializeField] private bool isDead = false;

        [Header(&quot;Runtime (read-only)&quot;)]
        [SerializeField] private GrowthStage stage = GrowthStage.Bit; // 현재 단계
        [SerializeField] private string currentAnswer;                // 현재 정답(프로토타입용 노출)

        private TurnController turn;   // 턴/오답 관리자
        private WordEntry currentEntry; // 현재 단어 데이터(주제/연관어 포함)

        private string pendingEvoId; // Bit/Byte 동안 쓸 임시 키=

        private void Awake()
        {
            turn = new TurnController(growthConfig);
        }

        /// &lt;summary&gt;
        /// 단계 시작(턴/오답 초기화 + 단어 배정)
        /// &lt;/summary&gt;
        public void BeginStage(GrowthStage s, bool initial = false)
        {

            turn.StartStage(s);

            //처음 (다시시작이나 게임 클리어 포함)
            if (initial)
            {
                //BIT상태로 변경
                stage = GrowthStage.Bit;
                var sr = GetComponent&lt;SpriteRenderer&gt;();
                if (sr != null)
                {
                    sr.sprite = BitImg;
                }

                // 현재 단어 선택
                currentEntry = wordService.PickInitialWord();

                // ✅ 임시 키 생성 (한 생애 내내 고정)
                pendingEvoId = $&quot;evo_{System.DateTime.UtcNow.Ticks}&quot;;

                // ✅ Bit 썸네일을 임시 키로 저장
                if (sr != null)
                    GalleryCapture.SaveSpriteThumb(sr, $&quot;thumb_{pendingEvoId}_s0&quot;, 256);

                submitmanager.OnRelevantButton();
            }
            else
            {
                var sr = GetComponent&lt;SpriteRenderer&gt;();
                if (s == GrowthStage.Byte)
                {
                    if (sr != null) sr.sprite = ByteImg;
                    // ✅ Byte 썸네일을 임시 키로 저장
                    if (sr != null)
                        GalleryCapture.SaveSpriteThumb(sr, $&quot;thumb_{pendingEvoId}_s1&quot;, 256);
                }
                else if (s == GrowthStage.Word)
                {
                    if (sr != null) sr.sprite = WordImg;
                }
            }

            // BeginStage 끝부분(초기 진입 포함)
            if (GameReviveSystem.I != null &amp;&amp; battery != null)
            {
                GameReviveSystem.I.SaveCheckpoint(this, battery.CurrentPercent);
            }

            // EvolveOrFinish에서 다음 단계 단어 배정 직후
            if (GameReviveSystem.I != null &amp;&amp; battery != null)
            {
                GameReviveSystem.I.SaveCheckpoint(this, battery.CurrentPercent);
            }

            currentAnswer = currentEntry.word;
            GameEvents.OnNewWordAssigned?.Invoke(currentAnswer); // UI: &quot;새 단어 등장&quot; (정답 직접 노출 대신 디버그/프로토타입용)
        }

        /// &lt;summary&gt;
        /// 데이터 주입(정답/오답 판정, 다음 문제 배정, 진화 체크)
        /// &lt;/summary&gt;
        public void DoFeedData(string userInput)
        {

            if (!battery.TryConsume(ActionType.FeedData))
                return; // OnActionBlockedLowBattery 이벤트로 HUD/토스트 띄우기

            // 턴 소모(FeedData는 1턴)
            if (!turn.ConsumeTurn(ActionType.FeedData))
            {
                WordEaterDie();
                return;
            }

            if (turn.TurnsLeft &lt;= 0) { WordEaterDie(); return; }
            // 정답 판정(v1 : 완전 일치, v2 : 오타/의미 유사도 확정 예정)
            bool ok = IsCorrect(userInput, currentAnswer);
            GameEvents.OnFeedResult?.Invoke(userInput, ok);

            if (ok)
            {
                EvolveOrFinish();
            }
            else
            {
                if (!turn.RegisterMistake()) { WordEaterDie(); return; }
            }

            // 턴 바닥나면 사망
            if (turn.TurnsLeft &lt;= 0) { WordEaterDie(); return; }
        }

        /// &lt;summary&gt;
        /// 미니게임/힌트(턴 1 소모)
        /// &lt;/summary&gt;
        public void DoOptimizeAlgo() // 미니게임 자리(힌트/버프 지급)
        {
            if (isDead) return;
            if (!turn.ConsumeTurn(ActionType.OptimizeAlgo))
            {
                WordEaterDie(); return;
            }
            // TODO: 힌트 토큰 +1, 상성 버프 스택 등
        }

        /// &lt;summary&gt;
        /// 노이즈 제거(턴 2 소모, 배율/버프 예정)
        /// &lt;/summary&gt;
        public void DoCleanNoise() // 2턴 소모, 배율/보상 증가 버프
        {
            if (isDead) return;
            if (!turn.ConsumeTurn(ActionType.CleanNoise))
            {
                WordEaterDie(); return;
            }
            // TODO: 배율 스택 += 1
        }

        // === 내부 로직 =======================================================

        private bool IsCorrect(string input, string answer)
        {
            // v1: 완전 일치. v2: 유사도(레벤슈타인/임베딩) 도입.
            return string.Equals(input.Trim(), answer.Trim(), System.StringComparison.Ordinal);
        }

        /// &lt;summary&gt;
        /// 단계 종료 처리(다음 단계 or 성체)
        /// - 현재 단계가 최종 단계(Word)이면: 도감에 등록하고 엔딩 처리
        /// - 아니면: 다음 단계로 진화
        /// &lt;/summary&gt;
        private void EvolveOrFinish()
        {
            if (stage == GrowthStage.Word)
            {
                // 성체 달성 → UI/연출 등 외부 구독자에게 알림
                GameEvents.OnEvolved?.Invoke(stage);

                // 성체가 되었으므로 &quot;도감&quot;에 현재 개체를 등록
                RegisterToGallery();

                // 게임 클리어 처리(엔딩 등)
                gamemanager.EndingController(2);
                galleryUIManager.Refresh();
                return;
            }

            // (성체가 아니라면) 다음 단계 단어 배정 및 진화
            currentEntry = wordService.PickNextLinkedWord(currentEntry, stage);
            currentAnswer = currentEntry.word;
            GameEvents.OnNewWordAssigned?.Invoke(currentAnswer);

            stage = (GrowthStage)((int)stage + 1);
            GameEvents.OnEvolved?.Invoke(stage);

            BeginStage(stage);
        }

        /// &lt;summary&gt;
        /// 사망 처리(휴지통/광고 보상 등 훅)
        /// &lt;/summary&gt;
        private void WordEaterDie()
        {
            if (isDead) return; 
            isDead = true;
            GameEvents.OnDied?.Invoke();

            enabled = false;

            // 광고 팝업 띄우고, 거절하면 그때 엔딩
            if (GameReviveSystem.I != null)
            {
                Debug.Log(&quot;광고 팝업 띄울겨&quot;);
                GameReviveSystem.I.OnPlayerDied(onGiveUp: () =&gt;
                {
                    // 정말 포기한 경우에만 게임오버 연출로 이동
                    gamemanager.EndingController(1);
                });
            }
            else
            {
                // 시스템이 없으면 안전하게 기존 흐름 유지
                gamemanager.EndingController(1);
            }

        }

        // 부활시
        public void Reactivate()
        {
            isDead = false;       // 죽은 상태 해제
            enabled = true;       // 다시 동작
                                  // 필요하면 무적 타이머/상태 초기화/입력언락 등을 여기서 처리
        }

        public int GetTurnsLeft() =&gt; turn.TurnsLeft;
        public int GetMistakesLeft() =&gt; turn.MistakesLeft; // TurnController에 프로퍼티 노출 필요

        public void RestoreTurns(int turnsLeft, int mistakesLeft)
        {
            turn.ForceRestore(turnsLeft, mistakesLeft); // TurnController에 강제 복원 API 추가
        }

        public void RestoreAnswer(string answer, GrowthStage s)
        {
            stage = s;
            currentAnswer = answer;
            GameEvents.OnNewWordAssigned?.Invoke(currentAnswer);

            // 스프라이트 동기화
            var sr = GetComponent&lt;SpriteRenderer&gt;();
            if (sr != null)
            {
                if (stage == GrowthStage.Bit) sr.sprite = BitImg;
                if (stage == GrowthStage.Byte) sr.sprite = ByteImg;
                if (stage == GrowthStage.Word) sr.sprite = WordImg;
            }
        }

        public WordEntry returnCurrentEnrty()
        {
            return currentEntry;
        }

        public string CurrentAnswer =&gt; currentAnswer;

        public GrowthStage ReturnStage()
        {
            return stage;
        }

        /// &lt;summary&gt;
        /// [도감 등록] 성체 달성 시 현재 워드이터를 도감 JSON에 등록한다.
        /// 동작 순서:
        /// 1) 현재 단어/단계를 이용해 &quot;고유 ID&quot; 생성 (충돌 방지용)
        /// 2) UI에 표시할 제목/카테고리(설명) 텍스트 구성
        /// 3) 현재 SpriteRenderer의 스프라이트를 썸네일 PNG로 저장
        /// 4) GalleryStore(싱글톤)로 Upsert → gallery.json에 반영
        /// &lt;/summary&gt;
        private void RegisterToGallery()
        {
            var entry = currentEntry;

            // ✅ 최종 키: Word 단계의 MakeStableId (예: &quot;2-수학&quot;)
            string finalId = MakeStableId(entry);

            // 임시 경로/최종 경로
            string baseDir = Application.persistentDataPath;
            string tmpS0 = Path.Combine(baseDir, $&quot;thumb_{pendingEvoId}_s0.png&quot;);
            string tmpS1 = Path.Combine(baseDir, $&quot;thumb_{pendingEvoId}_s1.png&quot;);
            string finS0 = Path.Combine(baseDir, $&quot;thumb_{finalId}_s0.png&quot;);
            string finS1 = Path.Combine(baseDir, $&quot;thumb_{finalId}_s1.png&quot;);

            // ✅ Bit/Byte 파일을 최종 키 이름으로 이동(있을 때만)
            MoveIfExists(tmpS0, finS0);
            MoveIfExists(tmpS1, finS1);

            // ✅ Word 썸네일은 최종 키로 저장
            var sr = GetComponent&lt;SpriteRenderer&gt;();
            string finS2 = Path.Combine(baseDir, $&quot;thumb_{finalId}_s2.png&quot;);
            GalleryCapture.SaveSpriteThumb(sr, $&quot;thumb_{finalId}_s2&quot;, 256);

            // 도감 등록: 대표 썸네일은 Word
            var item = new GalleryItem
            {
                id = finalId,
                displayName = entry.word,
                desc = GetTopicForDisplay(entry),
                thumbPath = finS2,
                dateCaught = System.DateTime.Now.ToString(&quot;yyyy-MM-dd&quot;)
            };
            GalleryStore.Instance.Upsert(item);
        }

        static void MoveIfExists(string src, string dst)
        {
            try
            {
                if (File.Exists(src))
                {
                    // 덮어쓰기 방지: 기존 있으면 삭제
                    if (File.Exists(dst)) File.Delete(dst);
                    File.Move(src, dst);
                }
            }
            catch (System.Exception ex)
            {
                Debug.LogWarning($&quot;[Gallery] Move fail {src} -&gt; {dst} : {ex.Message}&quot;);
            }
        }
        /// &lt;summary&gt;
        /// [도감 고유키 생성] 단어가 중복될 수 있으므로 &quot;단계-단어&quot; 형태로 고정 키를 만든다.
        /// 예: stage=2, word=&quot;수학&quot; → &quot;2-수학&quot;
        /// &lt;/summary&gt;
        static string MakeStableId(WordEntry e)
        {
            string slug = e.word.Trim().Replace(&quot; &quot;, &quot;&quot;); // 공백 제거 등 최소 정규화
            return $&quot;{e.stage}-{slug}&quot;;
        }

        /// &lt;summary&gt;
        /// [표시용 카테고리 텍스트] 데이터 구조상 topic 필드는 없으므로 다음 규칙 적용:
        /// - Word(2단계)는 상위 개념이므로 &quot;자기 자신&quot;을 카테고리로 사용
        /// - Bit/Byte는 related[] 첫 번째 항목을 대표 카테고리로 사용
        /// - 없을 경우 &quot;기타&quot;
        /// &lt;/summary&gt;
        static string GetTopicForDisplay(WordEntry e)
        {
            if (e.stage == 2) return e.word;                     // 최상위 카테고리
            if (e.related != null &amp;&amp; e.related.Length &gt; 0)
                return e.related[0];                              // 대표 카테고리 하나만 표시
            return &quot;기타&quot;;
        }

    }
}
</code></pre>
<p>길다 길어</p>
<p>여기만 보면됨</p>
<pre><code class="language-csharp">
        /// &lt;summary&gt;
        /// 사망 처리(휴지통/광고 보상 등 훅)
        /// &lt;/summary&gt;
        private void WordEaterDie()
        {
            if (isDead) return; 
            isDead = true;
            GameEvents.OnDied?.Invoke();

            enabled = false;

            // 광고 팝업 띄우고, 거절하면 그때 엔딩
            if (GameReviveSystem.I != null)
            {
                Debug.Log(&quot;광고 팝업 띄울겨&quot;);
                GameReviveSystem.I.OnPlayerDied(onGiveUp: () =&gt;
                {
                    // 정말 포기한 경우에만 게임오버 연출로 이동
                    gamemanager.EndingController(1);
                });
            }
            else
            {
                // 시스템이 없으면 안전하게 기존 흐름 유지
                gamemanager.EndingController(1);
            }

        }

        // 부활시
        public void Reactivate()
        {
            isDead = false;       // 죽은 상태 해제
            enabled = true;       // 다시 동작
                                  // 필요하면 무적 타이머/상태 초기화/입력언락 등을 여기서 처리
        }

        public int GetTurnsLeft() =&gt; turn.TurnsLeft;
        public int GetMistakesLeft() =&gt; turn.MistakesLeft; // TurnController에 프로퍼티 노출 필요

        public void RestoreTurns(int turnsLeft, int mistakesLeft)
        {
            turn.ForceRestore(turnsLeft, mistakesLeft); // TurnController에 강제 복원 API 추가
        }

        public void RestoreAnswer(string answer, GrowthStage s)
        {
            stage = s;
            currentAnswer = answer;
            GameEvents.OnNewWordAssigned?.Invoke(currentAnswer);

            // 스프라이트 동기화
            var sr = GetComponent&lt;SpriteRenderer&gt;();
            if (sr != null)
            {
                if (stage == GrowthStage.Bit) sr.sprite = BitImg;
                if (stage == GrowthStage.Byte) sr.sprite = ByteImg;
                if (stage == GrowthStage.Word) sr.sprite = WordImg;
            }
        }
</code></pre>
<p>TurnController.cs 변경점 (강제 복원 API 추가)</p>
<pre><code class="language-csharp">using UnityEngine;
using WordEater.Core;
using WordEater.Data;

namespace WordEater.Core
{
    /// &lt;summary&gt;
    /// 현재 단계(StageConfig)에 따라 &#39;턴/오답&#39; 수를 관리하는 컨트롤러
    /// &lt;/summary&gt;

    public class TurnController
    {
        private readonly GrowthConfig _growth;
        public int TurnsLeft { get; private set; }
        public int MistakesLeft { get; private set; }

        public TurnController(GrowthConfig growth) =&gt; _growth = growth;

        /// &lt;summary&gt;
        /// 단계 시작 시 규칙 초기화
        /// &lt;/summary&gt;
        public void StartStage(GrowthStage stage)
        {
            var cfg = _growth.Get(stage);
            TurnsLeft = cfg.turnsPerStage;
            MistakesLeft = cfg.maxMistakes;
            GameEvents.OnStageStarted?.Invoke(stage, TurnsLeft, MistakesLeft);
        }

        /// &lt;summary&gt;
        /// 액션 수행 시 턴 차감 (Clean은 2턴)
        /// &lt;/summary&gt;
        public bool ConsumeTurn(ActionType action)
        {
            int cost = action == ActionType.CleanNoise ? 2 : 1;
            TurnsLeft -= cost;

            // (OnTurnsChanged는 기존대로 두되, 필요시 RaiseTurnsChanged로 교체 가능)
            GameEvents.OnTurnsChanged?.Invoke(TurnsLeft);
            return TurnsLeft &gt; 0;
        }

        /// &lt;summary&gt;
        /// 오답 1회 등록
        /// &lt;/summary&gt;
        public bool RegisterMistake()
        {
            MistakesLeft -= 1;
            // 래퍼로 호출
            GameEvents.RaiseMistakesChanged(MistakesLeft);
            return MistakesLeft &gt;= 0;
        }

        /// &lt;summary&gt;
        /// ✅ 부활 복원을 위한 강제 복원 API (권장: stage 넘겨서 룰에 맞게 Clamp)
        /// &lt;/summary&gt;
        public void ForceRestore(int turnsLeft, int mistakesLeft, GrowthStage stage)
        {
            var cfg = _growth.Get(stage);
            TurnsLeft = Mathf.Clamp(turnsLeft, 0, cfg.turnsPerStage);
            MistakesLeft = Mathf.Clamp(mistakesLeft, 0, cfg.maxMistakes);

            GameEvents.OnStageStarted?.Invoke(stage, TurnsLeft, MistakesLeft);
            GameEvents.OnTurnsChanged?.Invoke(TurnsLeft);
            GameEvents.RaiseMistakesChanged(MistakesLeft);
        }

        /// &lt;summary&gt;
        /// (옵션) stage 없이 그대로 복원 – 기존 호출부 호환용
        /// &lt;/summary&gt;
        public void ForceRestore(int turnsLeft, int mistakesLeft)
        {
            TurnsLeft = turnsLeft;
            MistakesLeft = mistakesLeft;

            GameEvents.OnTurnsChanged?.Invoke(TurnsLeft);
            GameEvents.RaiseMistakesChanged(MistakesLeft);
        }
    }

}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[유니티 모바일 키보드가 인풋필드를 가리는 문제]]></title>
            <link>https://velog.io/@yj_621/%EC%9C%A0%EB%8B%88%ED%8B%B0-%EB%AA%A8%EB%B0%94%EC%9D%BC-%ED%82%A4%EB%B3%B4%EB%93%9C%EA%B0%80-%EC%9D%B8%ED%92%8B%ED%95%84%EB%93%9C%EB%A5%BC-%EA%B0%80%EB%A6%AC%EB%8A%94-%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@yj_621/%EC%9C%A0%EB%8B%88%ED%8B%B0-%EB%AA%A8%EB%B0%94%EC%9D%BC-%ED%82%A4%EB%B3%B4%EB%93%9C%EA%B0%80-%EC%9D%B8%ED%92%8B%ED%95%84%EB%93%9C%EB%A5%BC-%EA%B0%80%EB%A6%AC%EB%8A%94-%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Fri, 19 Dec 2025 08:46:32 GMT</pubDate>
            <description><![CDATA[<p>문제 영상</p>
<img src="https://velog.velcdn.com/images/yj_621/post/62955b9b-baf2-4a00-af52-34e59fbe68a3/image.gif" width="30%" height="30%">

<p>인풋필드에 타자를 치려하면 키보드가 가리는 문제가 생김</p>
<p>KeyboardAvoider 라는 스크립트를 생성하고</p>
<pre><code class="language-csharp">using UnityEngine;

/// &lt;summary&gt;
/// 모바일(안드로이드/IOS)에서 소프트 키보드가 올라올 때,
/// 화면 하단에 위치한 Input_Group(입력창 + 전송 버튼)을
/// 키보드 높이에 맞춰 위로 올려주는 스크립트
///
/// 목적:
/// - 키보드가 InputField를 가리는 현상을 방지
/// 
/// 원리:
/// 1) 네이티브 키보드 높이를 픽셀 단위로 가져온다
/// 2) Canvas Scaler의 scaleFactor로 나눠 &#39;UI 좌표&#39;로 변환
/// 3) 입력창의 anchoredPosition.y 를 그만큼 위로 올린다
/// 
/// 이 스크립트는 오직 target(Input_Group)만 움직이며,
/// 메시지 스크롤뷰 등 다른 UI는 그대로 둔다
/// (채팅 앱에서 하단 입력창만 올라가는 방식)
/// &lt;/summary&gt;
public class KeyboardAvoider : MonoBehaviour
{
    [Header(&quot;키보드에 맞춰 올릴 RectTransform (Input_Group)&quot;)]
    [SerializeField] private RectTransform target;

    [Header(&quot;키보드 높이에 곱해줄 값 (1.0 = 그대로, 0.9 = 살짝 덜 올리기)&quot;)]
    [SerializeField] private float heightMultiplier = 1.05f;

    [Header(&quot;추가로 미세하게 조절할 오프셋 (UI px 단위, 음수면 아래로)&quot;)]
    [SerializeField] private float extraOffset = -50f;

    // 현재 캔버스 (CanvasScaler의 scaleFactor 읽기 위함)
    private Canvas rootCanvas;

    // target의 원래 anchoredPosition 값 (키보드 닫으면 원래 위치로 복귀)
    private Vector2 originalAnchoredPos;

    // 이전 프레임 키보드 높이 (같은 값일 경우 연산 안 하려고 저장)
    private float currentKeyboardHeight;

    void Awake()
    {
        // 타겟이 비어 있으면 자기 자신 RectTransform 참조
        if (target == null)
            target = GetComponent&lt;RectTransform&gt;();

        // 가장 가까운 부모 Canvas 찾기
        rootCanvas = target.GetComponentInParent&lt;Canvas&gt;();

        // 초기 anchoredPosition 저장
        originalAnchoredPos = target.anchoredPosition;
    }

    void OnEnable()
    {
        // 씬 다시 열리거나 활성화될 때 위치 리셋
        if (target != null)
            target.anchoredPosition = originalAnchoredPos;

        currentKeyboardHeight = 0f;
    }

    void Update()
    {
#if UNITY_ANDROID || UNITY_IOS
        // 네이티브로 실시간 키보드 높이 가져오기 (스크린 px 단위)
        float keyboardHeight = GetNativeKeyboardHeight();

        // 키보드 높이가 변경되었을 때만 UI 갱신
        if (!Mathf.Approximately(currentKeyboardHeight, keyboardHeight))
        {
            currentKeyboardHeight = keyboardHeight;

            // Canvas Scaler 고려 → 스크린 픽셀 → UI 픽셀로 변환
            float uiKeyboardHeight = keyboardHeight / rootCanvas.scaleFactor;

            // multiplier &amp; 추가 offset 적용
            float finalY = uiKeyboardHeight * heightMultiplier + extraOffset;

            // Input_Group을 위로 이동
            target.anchoredPosition =
                originalAnchoredPos + new Vector2(0f, finalY);
        }
#endif
    }

    /// &lt;summary&gt;
    /// 실제 모바일 환경에서 &quot;진짜&quot; 키보드 높이를 가져오는 함수
    /// 
    /// 에디터(PC)에서는 0만 반환
    /// 안드로이드에서는 TouchScreenKeyboard.area → 부족하면 네이티브 수동 계산
    /// iOS는 TouchScreenKeyboard.area가 정확하게 동작함
    /// 
    /// 반환값: 키보드 높이 (스크린 픽셀 기준)
    /// &lt;/summary&gt;
    private float GetNativeKeyboardHeight()
    {
#if UNITY_EDITOR
        // 에디터에선 실제 키보드 없음
        return 0f;

#elif UNITY_ANDROID
        // 1차 시도: Unity 내장 TouchScreenKeyboard 값 사용
        if (TouchScreenKeyboard.visible &amp;&amp; TouchScreenKeyboard.area.height &gt; 0)
            return TouchScreenKeyboard.area.height;

        // 2차 시도: 안드로이드 네이티브 코드로 높이 계산
        using (var unityPlayer = new AndroidJavaClass(&quot;com.unity3d.player.UnityPlayer&quot;))
        {
            var currentActivity = unityPlayer.GetStatic&lt;AndroidJavaObject&gt;(&quot;currentActivity&quot;);
            var rootView = currentActivity.Call&lt;AndroidJavaObject&gt;(&quot;getWindow&quot;)
                                          .Call&lt;AndroidJavaObject&gt;(&quot;getDecorView&quot;);
            var visibleRect = new AndroidJavaObject(&quot;android.graphics.Rect&quot;);

            // 현재 화면에서 키보드가 차지하지 않는 영역을 가져옴
            rootView.Call(&quot;getWindowVisibleDisplayFrame&quot;, visibleRect);

            int screenHeight = rootView.Call&lt;int&gt;(&quot;getHeight&quot;);      // 전체 화면 높이
            int visibleHeight = visibleRect.Call&lt;int&gt;(&quot;height&quot;);     // 키보드 제외 보이는 영역
            int keyboardHeight = screenHeight - visibleHeight;       // 차이 = 키보드 높이

            // 값이 너무 작으면 네비게이션 바로 오판할 수 있으니 제외
            if (keyboardHeight &lt; screenHeight * 0.15f)
                return 0;

            return keyboardHeight;
        }

#elif UNITY_IOS
        // iOS에서는 TouchScreenKeyboard.area 값이 정확함
        if (TouchScreenKeyboard.visible)
            return TouchScreenKeyboard.area.height;
        return 0f;

#else
        // 그 외 플랫폼은 키보드가 없다고 간주
        return 0f;
#endif
    }
}
</code></pre>
<h3 id="이-스크립트가-하는-일">이 스크립트가 하는 일</h3>
<ul>
<li><p>모바일에서 키보드가 올라올 때</p>
</li>
<li><p>Input_Group(InputField + 전송 버튼 포함한 UI)을</p>
</li>
<li><p>키보드 높이만큼 위로 들어 올려서</p>
<p>  <strong>키보드가 입력창을 가리지 않게 만드는 기능</strong></p>
</li>
</ul>
<p>target : 키보드와 함께 올라갈 오브젝트</p>
<p>extraOffset : 너무 내려감 → 양수 
너무 올라감 → 음수로 조절하는 변수</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/81769f20-5a9a-46c7-b4a0-38d3d78d71d6/image.png" alt=""></p>
<h1 id="동작-원리">동작 원리</h1>
<ol>
<li>모바일에서 키보드가 올라오면 OS가 “키보드 영역” 만큼 화면을 가린다.</li>
<li>Android/iOS 네이티브 API를 사용하여 <strong>키보드의 실제 높이(px)</strong> 를 가져온다.</li>
<li>Canvas Scaler 값에 따라 UI 좌표계로 변환한다.</li>
<li>Input_Group에 <code>anchoredPosition.y</code> 만큼 값을 더해준다.</li>
<li>키보드가 내려가면 originalAnchoredPos로 원위치.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[유니티에서 AWS를 사용해 파이썬 프로그램 실행하기]]></title>
            <link>https://velog.io/@yj_621/%EC%9C%A0%EB%8B%88%ED%8B%B0%EC%97%90%EC%84%9C-AWS%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4-%ED%8C%8C%EC%9D%B4%EC%8D%AC-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8-%EC%8B%A4%ED%96%89%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@yj_621/%EC%9C%A0%EB%8B%88%ED%8B%B0%EC%97%90%EC%84%9C-AWS%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4-%ED%8C%8C%EC%9D%B4%EC%8D%AC-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8-%EC%8B%A4%ED%96%89%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 19 Dec 2025 08:41:37 GMT</pubDate>
            <description><![CDATA[<h1 id="1단계--aws-ec2-인스턴스-생성-서버-만들기">1단계 : AWS EC2 인스턴스 생성 (서버 만들기)</h1>
<p>일단 로긘을 해야겠지?</p>
<p>난 아이디도 없어서 회원가입부터함</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/699b5cab-1a04-4d9c-ba39-f4c50a4e9c50/image.png" alt=""></p>
<p>쭉 진행하다 보면 카드 정보 입력하고 어디사는지 입력하라고 뜨는데 그거까지 다 해주고 로그인 ㄱㄱ</p>
<p><a href="https://aws.amazon.com/ko/">https://aws.amazon.com/ko/</a></p>
<p>다음에 이 사이트를 들어가서 EC2를 검색(로그인 돼있으면 사진처럼)</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/540ab329-22e3-4d66-aaa3-92d8708cc4c5/image.png" alt=""></p>
<p>인스턴스를 누르고 </p>
<p>오른쪽 위 인스턴스 시작 ㄱㄱ</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/677b0033-a2b0-4dc7-8835-e252f650749b/image.png" alt=""></p>
<ul>
<li><strong>이름 : 프로젝트 이름</strong>으로 암거나하고</li>
<li><strong>OS 이미지(AMI):</strong> <strong>Ubuntu Server 22.04 LTS</strong> (프리 티어 사용 가능 확인)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/yj_621/post/9a985cd7-1e6d-4fe8-bb33-5b71ef978241/image.png" alt=""></p>
<p>첫번째 프리티어 사용 가능이니까 그거 선택</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/5d5e5a7a-4791-4a30-aee5-c120cec9447c/image.png" alt=""></p>
<ul>
<li><strong>인스턴스 유형:</strong> <strong>t2.micro</strong> 또는 <strong>t3.micro</strong> (지역에 따라 &#39;프리 티어 사용 가능&#39;이라고 표시된 것 선택)</li>
<li><strong>키 페어:</strong> &#39;새 키 페어 생성&#39;을 눌러 <code>.pem</code> 파일을 다운로드
(<strong>중요:</strong> 이 파일은 서버 접속 시 꼭 필요하며 다시 받을 수 없으니 안전한 곳에 보관해야댐)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/yj_621/post/a7ffb82b-5401-4bff-8bd7-98d972bda85b/image.png" alt=""></p>
<p>키 페어 이름도 프로젝트 이름으로 해줬음
여기서 ppk로 하는게 좋을듯 어차피 나중에 ppk도 필요해서(근데 난 pem으로 함)</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/e69928a4-bada-4273-94be-1297c60436ce/image.png" alt=""></p>
<p>인스턴스 시작하면 만들어집니당</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/2c978af0-9ff9-49f9-a96c-f684a636ac46/image.png" alt=""></p>
<h1 id="2단계-서버-보안-설정-포트-5000-열기"><strong>2단계: 서버 보안 설정 (포트 5000 열기)</strong></h1>
<p>다시 대시보드로 돌아와서 인스턴스ID를 클릭하면(사진에서 가려놔서 안 보이는거지 클릭할 수 있게 돼있음)</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/8abfbb4e-7021-4349-bf93-4e995095c72a/image.png" alt=""></p>
<p>가릴거 참 많다,, 여기서 보안 들어가면</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/ad908f68-739d-42ea-8fc1-8e576b526d64/image.png" alt=""></p>
<p>보안 그룹이라는게 보이는데 이걸 누르삼</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/0aee6fc9-ac6e-411e-9a39-b5f46dc223ca/image.png" alt=""></p>
<ul>
<li><strong>인바운드 규칙 편집</strong>을 클릭</li>
</ul>
<p><img src="https://velog.velcdn.com/images/yj_621/post/c5f0312c-e352-497b-a1d3-21bebd120c6c/image.png" alt=""></p>
<ul>
<li>원래 있던 두가지 유형을 이렇게 편집하면 됨<ul>
<li><strong>유형:</strong> 사용자 지정 TCP</li>
<li><strong>포트 범위:</strong> <code>5000</code></li>
<li><strong>소스:</strong> Anywhere-IPv4 (<code>0.0.0.0/0</code>) — <em>테스트용</em></li>
</ul>
</li>
<li><strong>규칙 저장</strong>을 누르면 됨</li>
</ul>
<p><img src="https://velog.velcdn.com/images/yj_621/post/59463629-da5c-4d6b-95e3-075cac6a783f/image.png" alt=""></p>
<h1 id="3단계-서버에-파일-업로드-및-환경-구축"><strong>3단계: 서버에 파일 업로드 및 환경 구축</strong></h1>
<p>얘가 귀찮음</p>
<p><a href="https://winscp.net/eng/download.php">https://winscp.net/eng/download.php</a></p>
<p>여기서 얘 다운 받고 키면 로그인하라는 창이 뜨는데 여기다 파이썬 모듈을 옮겨줄거임</p>
<ul>
<li><strong>호스트 이름(Host Name):</strong> 아까 확인한 AWS의 <strong>퍼블릭 IPv4 주소</strong> (예: <code>3.34.xxx.xxx</code>)</li>
</ul>
<p>아까 그 대시보드에서 이 부분을 복사해오면 됨
<img src="https://velog.velcdn.com/images/yj_621/post/2f8f76f0-886e-46e7-9a00-ba673b5c2ff5/image.png" alt=""></p>
<ul>
<li><strong>사용자 이름(User Name):</strong> <code>ubuntu</code> (반드시 소문자)</li>
<li><strong>비밀번호:</strong> 입력X</li>
</ul>
<p><img src="https://velog.velcdn.com/images/yj_621/post/84ccb3a7-a67e-4928-958e-13bce0807690/image.png" alt="">
여기서 냅다 로그인하면 안되고 고급에서 </p>
<ul>
<li>로그인 창의 <strong>[고급(D)]</strong> 버튼을 클릭</li>
<li>왼쪽 목록에서 <strong>SSH</strong> → 인증을 클릭
<img src="https://velog.velcdn.com/images/yj_621/post/15dcc049-6863-4092-ad45-dd72d9109303/image.png" alt=""></li>
</ul>
<ul>
<li><strong>개인 키 파일(Private key file)</strong> 칸의 <code>...</code> 버튼을 눌러 내 컴퓨터에 있는 <code>.pem</code> 파일을 선택<ul>
<li>파일 탐색기 창이 뜨면 오른쪽 하단 파일 형식을 모든 파일 (<em>.</em>)로 바꿔야 <code>.pem</code> 파일이 보임
<img src="https://velog.velcdn.com/images/yj_621/post/8d23d356-46e6-44b5-a28b-920d4ffcab5c/image.png" alt=""></li>
</ul>
</li>
</ul>
<p>만약에 ppk로 했으면 그거 넣고 pem으로 했으면 이렇게 넣어줌
<img src="https://velog.velcdn.com/images/yj_621/post/75604175-d61b-4903-b605-bc92cf0cbc5a/image.png" alt=""></p>
<p>그럼 이런 창이 뜨면서 확인 누르면 됨</p>
<p>만약 안 뜬다!! 하면 공개 키 표시 버튼을 눌러보세요(난 이렇게 해결함 왜인진 모름)</p>
<p>이제 내 컴퓨터에 있는 아래 4개 파일을 마우스로 잡아서 <strong>오른쪽 칸으로 드래그</strong>하면 끝</p>
<h1 id="4단계-서버-실행">4단계: 서버 실행</h1>
<p>일단 내가 만든 서버에 접속해야되니까 이거부터 해줌</p>
<ol>
<li><p><strong>서버 접속 (SSH):</strong> 터미널이나 PuTTY를 통해 서버에 접속</p>
<p> <code>ssh -i &quot;키파일이름.pem&quot; ubuntu@내-EC2-IP주소</code></p>
<p> 키파일이름은 아까 만든 이름으로 하면 되고</p>
<p> 내 EC2 IP 주소는 아래 사진에 보이는 퍼블릭 IPv4 주소를 쓰면댐
 <img src="https://velog.velcdn.com/images/yj_621/post/ccb24c85-7c85-4c86-8b69-0ba9f50259d4/image.png" alt=""></p>
</li>
</ol>
<ol start="2">
<li><p><strong>파이썬 환경 설치</strong></p>
<p> <code>sudo apt update
 sudo apt install python3-pip -y
 pip3 install flask gensim</code></p>
</li>
</ol>
<ul>
<li><strong>패키지 목록 업데이트</strong></li>
</ul>
<pre><code>`sudo apt update`</code></pre><ul>
<li><strong>파이썬 설치 확인 및 pip(설치 도구) 설치</strong></li>
</ul>
<pre><code>`sudo apt install python3-pip -y`</code></pre><ul>
<li><strong>필요한 라이브러리 설치</strong></li>
</ul>
<pre><code>`pip3 install flask gensim`</code></pre><ol start="3">
<li><strong>실행 테스트</strong></li>
</ol>
<pre><code>`python3 run.py`

(에러 없이 `Complety Loading Model`이 뜨는지 확인!!)</code></pre><ol start="2">
<li><p><strong>백그라운드 실행</strong></p>
<p> <code>nohup python3 run.py &gt; output.log 2&gt;&amp;1 &amp;</code></p>
<ul>
<li>이제 터미널을 종료해도 서버는 계속 작동함</li>
<li>서버를 끄고 싶을 때는 <code>ps -ef | grep run.py</code>로 프로세스 ID를 찾아 <code>kill</code> 명령어를 사용</li>
</ul>
</li>
</ol>
<h3 id="⚠️-오류-모음">⚠️ 오류 모음</h3>
<p>만약</p>
<pre><code class="language-csharp">Loading FastText...

Killed</code></pre>
<p>라는 오류가 떴다면 메모리 부족으로 뜨는 오류로 다음과 같이 순서대로 진행</p>
<p><strong>1. 2GB 크기의 스왑 파일 생성</strong></p>
<p><code>sudo fallocate -l 2G /swapfile</code></p>
<p><strong>2. 파일 권한 설정 (보안)</strong></p>
<p><code>sudo chmod 600 /swapfile</code></p>
<p><strong>3. 파일을 스왑 공간으로 포맷</strong></p>
<p><code>sudo mkswap /swapfile</code></p>
<p><strong>4. 스왑 공간 활성화</strong></p>
<p><code>sudo swapon /swapfile</code></p>
<p><strong>5. 설정이 잘 되었는지 확인</strong></p>
<p><code>free -h</code></p>
<blockquote>
<p>결과 확인: Swap: 부분에 2.0Gi라고 나오면 성공입니다!</p>
</blockquote>
<p>이후에 다시 <code>python3 [run.py](http://run.py)</code> 를 입력해서 실행</p>
<hr>
<h1 id="5단계-유니티-코드-수정">5단계: 유니티 코드 수정</h1>
<p>이제 유니티의 <code>PythonConnectManager.cs</code> 파일에서 URL만 변경하면 끝!</p>
<pre><code class="language-csharp">using UnityEngine;
using UnityEngine.Networking;
using System.Collections;
using System.Text;
using Newtonsoft.Json;
using System.Collections.Generic;
using System;

[System.Serializable]
public class ResultData
{
    public List&lt;string&gt; result;
}

public class ResultData2
{
    public float? result;
}

public class PythonConnectManager : MonoBehaviour
{
    private string serverIP = &quot;본인 퍼블릭 IPv4 주소&quot;;

    }

    //단어와 몇개의 유사한 단어를 가져올 것인지 입력
    public IEnumerator MostSimilarty(string inputWord, int num, Action&lt;List&lt;string&gt;&gt; callback)
    {
        string url = $&quot;http://{serverIP}:5000/most_similarty&quot;;

        var data = new { word = inputWord, num = num };
        string jsonData = JsonConvert.SerializeObject(data);

        using (UnityWebRequest request = new UnityWebRequest(url, &quot;POST&quot;))
        {
            byte[] bodyRaw = Encoding.UTF8.GetBytes(jsonData);
            request.uploadHandler = new UploadHandlerRaw(bodyRaw);
            request.downloadHandler = new DownloadHandlerBuffer();
            request.SetRequestHeader(&quot;Content-Type&quot;, &quot;application/json&quot;);

            yield return request.SendWebRequest();

            if (request.result == UnityWebRequest.Result.Success)
            {
                string jsonResponse = request.downloadHandler.text;
                Debug.Log(&quot;서버 응답: &quot; + jsonResponse);
                ResultData responseData = JsonConvert.DeserializeObject&lt;ResultData&gt;(jsonResponse);

                if (responseData.result != null &amp;&amp; responseData.result.Count &gt; 0)
                {
                    callback(responseData.result);
                }
                else
                {
                    callback(new List&lt;string&gt; { &quot;부정확한 단어&quot; });
                }
            }
            else
            {
                Debug.LogError(&quot;요청 실패: &quot; + request.error);
                callback(new List&lt;string&gt; { &quot;요청 실패&quot; });
            }
        }
    }

    //두 단어 사이의 유사도
    public IEnumerator SimilartyTwoWord(string inputWord, string inputWord2, Action&lt;float?&gt; callback)
    {
        string url = $&quot;http://{serverIP}:5000/similarity&quot;;

        var data = new { word1 = inputWord, word2 = inputWord2 };
        string jsonData = JsonConvert.SerializeObject(data);

        using (UnityWebRequest request = new UnityWebRequest(url, &quot;POST&quot;))
        {
            byte[] bodyRaw = Encoding.UTF8.GetBytes(jsonData);
            request.uploadHandler = new UploadHandlerRaw(bodyRaw);
            request.downloadHandler = new DownloadHandlerBuffer();
            request.SetRequestHeader(&quot;Content-Type&quot;, &quot;application/json&quot;);

            yield return request.SendWebRequest();

            if (request.result == UnityWebRequest.Result.Success)
            {
                string jsonResponse = request.downloadHandler.text;
                ResultData2 responseData = JsonConvert.DeserializeObject&lt;ResultData2&gt;(jsonResponse);

                if (responseData != null &amp;&amp; responseData.result.HasValue)
                {
                    callback?.Invoke(responseData.result.Value);
                }
                else
                {
                    Debug.Log(&quot;부정확한 단어&quot;);
                    callback?.Invoke(null);
                }
            }
            else
            {
                Debug.LogError(&quot;요청 실패: &quot; + request.error);
                callback?.Invoke(null);
            }
        }
    }
}
</code></pre>
<h3 id="💡-만약-서버를-끄고-싶다면">💡 만약 서버를 끄고 싶다면?</h3>
<p>나중에 서버를 업데이트하거나 끄고 싶을 때는 아래 명령어를 터미널에 입력하면 됩니당~</p>
<p><code>pkill -f run.py</code></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[유니티 ait 파일 생성, 앱인토스 ait 파일 빌드, 유니티에서 만든 게임을 앱인토스에 올리기]]></title>
            <link>https://velog.io/@yj_621/%EC%9C%A0%EB%8B%88%ED%8B%B0-ait-%ED%8C%8C%EC%9D%BC-%EC%83%9D%EC%84%B1-%EC%95%B1%EC%9D%B8%ED%86%A0%EC%8A%A4-ait-%ED%8C%8C%EC%9D%BC-%EB%B9%8C%EB%93%9C-%EC%9C%A0%EB%8B%88%ED%8B%B0%EC%97%90%EC%84%9C-%EB%A7%8C%EB%93%A0-%EA%B2%8C%EC%9E%84%EC%9D%84-%EC%95%B1%EC%9D%B8%ED%86%A0%EC%8A%A4%EC%97%90-%EC%98%AC%EB%A6%AC%EA%B8%B0</link>
            <guid>https://velog.io/@yj_621/%EC%9C%A0%EB%8B%88%ED%8B%B0-ait-%ED%8C%8C%EC%9D%BC-%EC%83%9D%EC%84%B1-%EC%95%B1%EC%9D%B8%ED%86%A0%EC%8A%A4-ait-%ED%8C%8C%EC%9D%BC-%EB%B9%8C%EB%93%9C-%EC%9C%A0%EB%8B%88%ED%8B%B0%EC%97%90%EC%84%9C-%EB%A7%8C%EB%93%A0-%EA%B2%8C%EC%9E%84%EC%9D%84-%EC%95%B1%EC%9D%B8%ED%86%A0%EC%8A%A4%EC%97%90-%EC%98%AC%EB%A6%AC%EA%B8%B0</guid>
            <pubDate>Fri, 01 Aug 2025 08:06:00 GMT</pubDate>
            <description><![CDATA[<p><a href="https://developers-apps-in-toss.toss.im/prepare/console-workspace.html">앱인토스 개발자센터</a></p>
<p>이 글은 앱인토스 ait 빌드, 리더보드,, 어쩌고 기능을 어떻게 써!! 하시는 분들에게 바칩니다.</p>
<p>앱인토스로 게임을 만드는 분들께 도움이 되길 바랍니다 … ⭐</p>
<p>일단 제일 중요한 ❗사업자 등록❗이 필요합니다.</p>
<p>이게 없으면 앱인토스에 올릴 수가 없어요</p>
<p>있다고 가정하고, 게임까지 다 만들었다(아예 완성X, 80%?)라고 가정한 후 이 단계를 따라오시면 됩니다.</p>
<p>먼저, 게임을 다 만든후 WebGL로 빌드해주세요</p>
<h1 id="앱인토스-시작하기">앱인토스 시작하기</h1>
<p><a href="https://apps-in-toss.toss.im/workspace">앱인토스 콘솔</a></p>
<p>여기 들어가서 회원가입 후 - 워크스페이스에 초대를(본인이 사업자 등록자라면 본인이 만들기) 받은 후 들어갑니다.</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/005d0f6e-59ea-4bbb-a27e-085bfb57d8c9/image.png" alt=""></p>
<p>워크스페이스(회사명)을 입력한 후, 안에 여러 게임을 동시에 등록할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/4fc39dbc-7512-4819-b8ae-0198ae7be12b/image.png" alt=""></p>
<p>만들게되면 이런 화면이 뜨는데 “앱 등록하기” 버튼을 누르면</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/74fb7f09-377e-40e7-be28-2554f65fdba7/image.png" alt="">
앱 정보(이름, 한국어, 영어,,,, ,, 등등)를 쓸 수 있는 칸이 나오는데 다 채워주시면 됩니다.</p>
<p>여기서 미리 준비해야할 것이</p>
<p>⭐ 앱 로고(600px * 600px)</p>
<p>⭐ 정방형 썸네일(1000px * 1000px)</p>
<p>⭐ 가로형 썸네일(1932px * 828px)</p>
<p>이 세가지입니다!!</p>
<p>이런식으로 들어갈 거고, 저는 미리캔버스 사용해서 만들었습니다</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/f5ab4e24-fcec-4eb6-83b2-110d28ee9dcf/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/db1bfa85-26cb-42bb-86c6-29f13489f2fd/image.png" alt="">
다 만들었다면 카테고리를 작성해야하는데 앱 설명은 </p>
<p>이런식으로 옆에 뜨는 문구입니다.</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/2a0b79bb-e2a3-49e1-993e-2385d96717e2/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/6af92e54-100a-4aa5-80e9-0e2b4f058e70/image.png" alt="">
리더보드의 점수는 이렇게 뜬다고 예시보기 누르면 이 사진이 뜨게됩니다~!</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/978b84ce-16e0-43cf-94fd-206f7d725539/image.png" alt="">
저는 아래 등급정보에 스토어 링크가 없으니 itch.io에 WebGL로 빌드한 후, 업로드하고 그 링크를 넣어줬어요</p>
<p>자 이제 50%는 준비가 완료됐습니다. 검토 요청 후 토스내에서 검토가 완료되면 워크스페이스가 만들어질 것이고, 저희가 이제부터 할 것은 ait 파일을 빌드하는 방법이에요</p>
<h1 id="ait-파일-빌드하기">AIT 파일 빌드하기</h1>
<p><a href="https://nodejs.org/ko">Node.js — Run JavaScript Everywhere</a></p>
<p>먼저, node js를 다운받아줍니다. 다운 받는 방법은 여러 게시물이 있으니 생략하겠습니다.</p>
<p>⚠️ WebGL로 빌드한 폴더와 경로가 같아야합니다. (ex. desktop - Build - 빌드한 폴더 라면, Build에서 진행)</p>
<pre><code>npm create vite@latest 폴더명</code></pre><p>이렇게 한 후** 프레임워크는 Vanilla**
<strong>배리언트는 JavaScript</strong>
<img src="https://velog.velcdn.com/images/yj_621/post/67aadcca-70fe-4928-9223-ef34cf204512/image.png" alt=""></p>
<p>만들어진 폴더로 이동한 후</p>
<pre><code>npm install</code></pre><p>후에 ! 아래부터 차근차근 따라가주세요</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/14c498f0-16d8-42cb-b5ce-0a3f2263eab6/image.png" alt=""></p>
<pre><code>npm install @apps-in-toss/web-framework
</code></pre><p><img src="https://velog.velcdn.com/images/yj_621/post/b6ea6493-c589-4699-968b-457dd9e682a5/image.png" alt=""></p>
<pre><code>npx ait init</code></pre><p><img src="https://velog.velcdn.com/images/yj_621/post/18d58d1d-5e25-49a5-8308-aa9020d27af6/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/86bd4105-afc7-43fb-8bb5-5eac453e0787/image.png" alt=""></p>
<p>여기까지 했다면 아래 이것저것 입력하라고 할텐데 사진대로 하면 됩니당 </p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/313a6876-b163-4275-b928-c85816d0f9d3/image.png" alt="">
React Native로 해주고, app-name(본인 게임 이름 소문자로)를 입력해줍니다.</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/85ff7bb5-c5da-4700-ac11-7245ec54e785/image.png" alt=""></p>
<p>이때 빌드한 폴더의 index.html파일을 만들어진 unity-web-wrapper에 덮어씌우기 한 후</p>
<p>유니티 빌드 폴더의 &quot;Build&quot;폴더를 public 폴더 안에 복사
<img src="https://velog.velcdn.com/images/yj_621/post/7b8f7686-d601-4cd0-b59b-bfe79da7a0aa/image.png" alt=""></p>
<p>복사를 다 한 후에 ait파일을</p>
<pre><code>npm run build</code></pre><p>여기까지하면 ait파일이 빌드가 됩니다.</p>
<p>그 전에, bedrock파일에</p>
<pre><code class="language-csharp">import { defineConfig } from &#39;@apps-in-toss/web-framework/config&#39;;

export default defineConfig({
  appName: &#39;crayonfish&#39;,
  brand: {
    displayName: &#39;크레용 피쉬&#39;, // 화면에 노출될 앱의 한글 이름으로 바꿔주세요.
    primaryColor: &#39;#3182F6&#39;, // 화면에 노출될 앱의 기본 색상으로 바꿔주세요.
    icon: &quot;이미지주소링크(위에서 말한것)&quot;, // 화면에 노출될 앱의 아이콘 이미지 주소로 바꿔주세요.
    bridgeColorMode: &#39;inverted&#39;,
  },
  web: {
    host: &#39;localhost&#39;,
    port: 5173,
    commands: {
      dev: &#39;vite&#39;,
      build: &#39;vite build&#39;,
    },
  },
  permissions: [],
  outdir: &#39;dist&#39;,
  webViewProps: {
    type: &#39;game&#39;,
  },
});
</code></pre>
<p>bridgeColorMode 는 inverted로,</p>
<p>type은 game으로 바꿔주셔야합니다!</p>
<p>여기서 icon은 
<img src="https://velog.velcdn.com/images/yj_621/post/20ae660f-8231-4164-8d45-bcec3a370d70/image.png" alt="">
아까 만든 워크스페이스에 개발 - 앱정보 들어가서 “수정하기”버튼을 누릅니다.
<img src="https://velog.velcdn.com/images/yj_621/post/8a81fc2e-ba0d-4520-9aa2-b66ed18c3c6f/image.png" alt="">
여기서 앱로고 링크를 복사해 bedrock의 icon : null 에 null대신 넣어주면 됩니다.</p>
<p>리더보드 기능은 index.html과 toss.js파일을 만들고 유니티 에디터에서 연결할 버튼 스크립트 짜주고 bedrock 수정해주고 등등 다시 파일을 바꾸고 그 버전으로 ait파일을 빌드하려면</p>
<p> <code>npm run build</code> 를 해주셔야합니다.</p>
<h1 id="ait-파일-업로드하기">AIT 파일 업로드하기</h1>
<p><img src="https://velog.velcdn.com/images/yj_621/post/a9ee0a76-2346-4fc5-98e3-93f4b28a0644/image.png" alt=""></p>
<p>ait 빌드 후 오른쪽 위에 버전 등록을 누르고
 <img src="https://velog.velcdn.com/images/yj_621/post/9194b6ba-1077-4f4e-b2c1-cf45ae1ff29c/image.png" alt="">
아까 만든 ait파일과 출시노트(~수정, ~추가 버전)을 작성하고 등록해주면 목록에 버전, 상태, 출시노트, 생성일, 테스트하기 / 검토요청하기 버튼이 뜹니다</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/85610148-4520-4e30-ae43-9bfb23082f42/image.png" alt=""></p>
<p>테스트하기 버튼을 누르면 이런 화면이 뜨고, 큐알을 찍거나 링크를 복사해 모바일로 실행하면 토스앱 내에서 테스트할 수 있게 됩니다.</p>
<p>푸시발송하기 누르면 워크스페이스 내 멤버 모두가 토스 알람을 받게됩니다.</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/65fd6a15-9af9-440f-8fa6-fb6359bce67c/image.png" alt="">
검토 요청을 누르게되면 체크리스트 체크 후 검토 요청하기를 누르면 반려사유보기 or 출시하기 버튼이 활성화되며 검토가 완료된 버전은 출시가 가능합니다. (토스 내 게임 앱에서 바로 출시가 됩니다!)</p>
<p>중간에 까먹은 명령어와 리더보드는 빠른 시일내에 올리도록 하겠습니다 ㅠ,ㅠ</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[유니티 Photon Voice2 보이스 채팅 기능]]></title>
            <link>https://velog.io/@yj_621/%EC%9C%A0%EB%8B%88%ED%8B%B0-Photon-Voice2-%EB%B3%B4%EC%9D%B4%EC%8A%A4-%EC%B1%84%ED%8C%85-%EA%B8%B0%EB%8A%A5</link>
            <guid>https://velog.io/@yj_621/%EC%9C%A0%EB%8B%88%ED%8B%B0-Photon-Voice2-%EB%B3%B4%EC%9D%B4%EC%8A%A4-%EC%B1%84%ED%8C%85-%EA%B8%B0%EB%8A%A5</guid>
            <pubDate>Tue, 25 Mar 2025 07:12:41 GMT</pubDate>
            <description><![CDATA[<p>이 글을,,, 보이스를 구현하려는 모든 사람에게 바칩니다. 유니티 6버전 기준으로 만들어졌습니다@@!</p>
<p>포톤 보이스에 대한 기본적인 기능은 있는데 음소거 ON/OFF, 말하는 사람 아이콘 활성화 등의 기능을 다룬 게시물이 없어서 씀</p>
<p>근데 나도 야매로 한거라 약간의 하자가 있습니다.</p>
<hr>
<p>먼저, Photon Voice2를 임포트하고
기본 세팅은 다른 곳에서 찾아보면 알 수 있듯이, Voice Manager라는 오브젝트를 만들어줌</p>
<p>아래처럼 Recorder 오브젝트와 Pun Voice Client를 부착
<img src="https://velog.velcdn.com/images/yj_621/post/e1ed8508-f2aa-43d4-a644-c9592bbfb57c/image.png" alt=""></p>
<p>플레이어에 Speaker와 Photon Voice View 컴포넌트를 부착해주면 준비 끝
<img src="https://velog.velcdn.com/images/yj_621/post/c91836a5-1b4b-4e36-b805-8a4287bcab0a/image.png" alt=""></p>
<p>이때, Recorder에 Debug Echo를 체크하면 내 목소리가 들려서 테스트가 가능하다
<img src="https://velog.velcdn.com/images/yj_621/post/5ac546f3-3ac7-4530-b569-0492a985cc7a/image.png" alt=""></p>
<p>조금 복잡하게 생겼지만 내 게임의 구조는 PlayerGroup이라는 오브젝트 아래 Player1,2,3,4 자식으로 각각 플레이어가 생성될 것이고
<img src="https://velog.velcdn.com/images/yj_621/post/dbc9a21e-6af4-45f2-b371-e05657d4b6de/image.png" alt=""></p>
<p>VoicePanel은이렇게 생겼따.
<img src="blob:https://velog.io/820d803c-950f-4b8e-b088-a19b477ff88b" alt="업로드중.."></p>
<p>내 코드인것,,. 정말정말 길고 복잡하고 막 짠거(아님 ㅠㅠ 열심히 한거야)같지만 최선이였따.</p>
<pre><code>using Photon.Pun;
using Photon.Realtime;
using Photon.Voice.Unity;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using static TotalMultiManager;
using System.Collections;

public class VoiceManager : MonoBehaviourPunCallbacks
{
    public static VoiceManager Instance { get; private set; } // Singleton 인스턴스

    [SerializeField] private Transform playerGroup; // PlayerGroup의 Transform

    [SerializeField] private GameObject[] players;  // 각 플레이어 GameObject
    [SerializeField] private TextMeshProUGUI[] playerTexts;  // 각 플레이어의 TextMeshProUGUI 배열

    [SerializeField] private Sprite speakImage;  // 말하는 이미지
    [SerializeField] private Sprite defaultImage;  // 기본 이미지
    [SerializeField] private Sprite muteImage;  // 기본 이미지

    [SerializeField] private GameObject speakerPanel;

    private Speaker[] speakers;  // Speaker 컴포넌트를 담을 배열
    private bool[] isMuted = new bool[4];  // 각 플레이어의 음소거 상태

    private Recorder recorder;
    // 로컬 플레이어의 자체 음소거 상태 변수
    private bool selfMuted = false;

    private void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
        }
        else
        {
            Destroy(Instance.gameObject);
            Instance = this;
        }
    }

    private void Start()
    {
        StartCoroutine(UpdatePlayerText());
    }

    /// &lt;summary&gt;
    /// 매 프레임의 후반부에 호출되는 함수
    /// 스피커 목록을 최신 상태로 유지하기 위해 업데이트 호출
    /// &lt;/summary&gt;
    private void LateUpdate()
    {
        StartCoroutine(UpdatePlayerText());
        CheckIsPlaying();
    }


    public void OnClickSpeakerPanel()
    {
        speakerPanel.SetActive(!speakerPanel.activeSelf);
    }

    /// &lt;summary&gt;
    /// 각 스피커의 재생 상태에 따라 플레이어 UI를 업데이트하는 함수
    /// &lt;/summary&gt;
    private void CheckIsPlaying()
    {
        // 모든 플레이어 UI를 비활성화
        for (int i = 0; i &lt; players.Length; i++)
        {
            players[i].SetActive(false);
        }

        speakers = playerGroup.GetComponentsInChildren&lt;Speaker&gt;(true);

        foreach (var speaker in speakers)
        {
            PhotonView pv = speaker.GetComponent&lt;PhotonView&gt;();
            if (pv == null) continue;

            int index = pv.OwnerActorNr - 1;
            if (index &lt; 0 || index &gt;= players.Length) continue;

            players[index].SetActive(true);

            Image img = playerTexts[index].GetComponentInChildren&lt;Image&gt;();


            // 로컬 플레이어 처리
            if (pv.IsMine)
            {
                // 로컬 플레이어의 AudioSource 가져오기
                AudioSource audioSource = speaker.GetComponent&lt;AudioSource&gt;();
                if (audioSource != null)
                {
                    // 본인이 말할 때는 볼륨을 0, 그렇지 않을 때는 1로 설정
                    audioSource.volume = speaker.IsPlaying ? 0f : 1f;
                }
                // 로컬 플레이어의 음소거 여부 및 말하는 상태에 따른 이미지 설정
                img.sprite = selfMuted ? muteImage : (speaker.IsPlaying ? speakImage : defaultImage);
            }
            else
            {
                // 원격 플레이어 처리: 음소거 상태 및 말하는 상태에 따른 이미지 설정
                img.sprite = isMuted[index]
                    ? muteImage
                    : (speaker.IsPlaying ? speakImage : defaultImage);
            }
            // 플레이어 닉네임 업데이트
            playerTexts[index].text = pv.Owner.NickName;
        }
    }

    /// &lt;summary&gt;
    /// 모든 Speaker 컴포넌트를 가진 플레이어를 확인하고 playerTexts를 업데이트
    /// &lt;/summary&gt;
    private IEnumerator UpdatePlayerText()
    {
        while (!AllhasTag(&quot;HasInfo&quot;))
        {
            yield return null; // 모든 플레이어의 CustomProperties가 준비될 때까지 대기
        }

        speakers = playerGroup.GetComponentsInChildren&lt;Speaker&gt;(true);

        foreach (var speaker in speakers)
        {
            PhotonView pv = speaker.GetComponent&lt;PhotonView&gt;();
            if (pv == null) continue;

            int index = pv.OwnerActorNr - 1;
            if (index &lt; 0 || index &gt;= playerTexts.Length) continue;

            players[index].SetActive(true);
            playerTexts[index].text = pv.Owner.NickName;
        }
    }

    /// &lt;summary&gt;
    /// 방에 입장했을 때 호출되는 콜백 함수
    /// 로컬 플레이어의 CustomProperties에 &quot;HasInfo&quot;를 설정
    /// &lt;/summary&gt;
    public override void OnJoinedRoom()
    {
        base.OnJoinedRoom();

        // 로컬 플레이어의 정보가 준비되었음을 표시하는 프로퍼티 설정
        ExitGames.Client.Photon.Hashtable props = new ExitGames.Client.Photon.Hashtable
        {
            { &quot;HasInfo&quot;, true }
        };

        PhotonNetwork.LocalPlayer.SetCustomProperties(props);
    }

    // 방에 들어왔을때
    public override void OnPlayerEnteredRoom(Player newPlayer)
    {
        // 입장한 플레이어의 ActorNumber를 인덱스로 사용
        int index = newPlayer.ActorNumber - 1;

        // 인덱스가 유효하면 UI를 활성화하고 닉네임을 업데이트
        if (index &gt;= 0 &amp;&amp; index &lt; players.Length)
        {
            players[index].SetActive(true);
            playerTexts[index].text = newPlayer.NickName;
        }
    }

    /// &lt;summary&gt;
    /// 플레이어가 방을 떠났을 때 호출되는 콜백 함수
    /// 해당 플레이어의 UI를 비활성화(또는 닉네임 삭제)하고 스피커 목록을 업데이트
    /// &lt;/summary&gt;
    public override void OnPlayerLeftRoom(Player otherPlayer)
    {
        // 나간 플레이어의 ActorNumber를 인덱스로 사용
        int index = otherPlayer.ActorNumber - 1;

        // 인덱스가 유효하면 UI를 비활성화(또는 닉네임을 지움)하여 표시하지 않음
        if (index &gt;= 0 &amp;&amp; index &lt; players.Length)
        {
            players[index].SetActive(true);
            playerTexts[index].text = &quot;&quot;;
        }
    }


    /// &lt;summary&gt;
    /// 특정 ActorNumber에 해당하는 플레이어의 음소거 상태를 토글하는 함수
    /// - 로컬 플레이어인 경우 Recorder를 토글하여 자신의 목소리 전송 여부 제어
    /// - 원격 플레이어인 경우 해당 Speaker 컴포넌트의 활성화를 토글하여 클라이언트에서만 음소거 처리
    /// &lt;/summary&gt;
    /// &lt;param name=&quot;actorNumber&quot;&gt;음소거할 플레이어의 ActorNumber&lt;/param&gt;
    public void ToggleSpeaker(int actorNumber)
    {
        if (actorNumber == PhotonNetwork.LocalPlayer.ActorNumber)
        {
            ToggleSelfMute();
            return;
        }

        // ActorNumber를 인덱스로 변환 (배열은 0부터 시작)
        int index = actorNumber - 1;
        foreach (var speaker in speakers)
        {
            PhotonView pv = speaker.GetComponent&lt;PhotonView&gt;();
            // 해당 Speaker의 소유자와 전달된 ActorNumber가 일치하면 음소거 상태를 토글
            if (pv != null &amp;&amp; pv.OwnerActorNr == actorNumber)
            {
                // Speaker 컴포넌트의 활성화 여부를 반전시킴 (비활성화되면 음소거)
                speaker.enabled = !speaker.enabled;
                // isMuted 배열에도 반영 (speaker가 비활성화이면 음소거 상태)
                isMuted[index] = !speaker.enabled;

                // UI의 이미지도 즉시 업데이트하여 음소거 상태를 표시
                Image img = playerTexts[index].GetComponentInChildren&lt;Image&gt;();
                img.sprite = isMuted[index] ? muteImage : (speaker.IsPlaying ? speakImage : defaultImage);
                return;
            }
        }
    }
    /// &lt;summary&gt;
    /// 로컬 플레이어의 음소거를 토글
    /// &lt;/summary&gt;
    private void ToggleSelfMute()
    {
        if (recorder == null)
        {
            Debug.LogWarning(&quot;Recorder is not assigned!&quot;);
            return;
        }

        // selfMuted 상태 반전
        selfMuted = !selfMuted;
        // 음소거 상태이면 전송하지 않음, 아니면 전송
        // TransmitEnabled : 시작하자마자 말하기가 가능함(눌러서 말하기 제어 가능)
        recorder.TransmitEnabled = !selfMuted;

        // UI 업데이트: 로컬 플레이어 인덱스에 해당하는 이미지 변경
        int index = PhotonNetwork.LocalPlayer.ActorNumber - 1;
        Image img = playerTexts[index].GetComponentInChildren&lt;Image&gt;();

        img.sprite = selfMuted ? muteImage : defaultImage;
        Debug.Log(img);
        Debug.Log(&quot;Self mute toggled: &quot; + (selfMuted ? &quot;Muted&quot; : &quot;Unmuted&quot;));
    }
}
</code></pre><p>자, 하나씩 살펴보면 전체적인 구조는 Player가 Speaker 컴포넌트를 가지고 있으니 PlayerGroup에서 speaker컴포넌트를 가진 애가 들어올때마다 위의 VoicePanel에 닉네임과 아이콘을 활성화시켜주는것임</p>
<p>여기서 ActorNumber란 무엇인가!!
플레이어를 구분할때 사용하는 고유한 넘버인데, 내 코드에서는 닉네임 배열의 인덱스로 활용함</p>
<p>그리고 누군가가 말하고 있으면 CheckIsPlaying 함수를 사용해 말하는 아이콘을 활성화해줌
이때 Recorder에 Debug Echo를 체크를 한 상태로 진행했는데, 어째서인지 체크를 해제하면 상대방에게도 내 목소리가 들리지 않은 버그가 생겼었음</p>
<p>그래서 아래의 코드로 내 목소리는 들리지 않도록 처리를 해줬음</p>
<pre><code>// 본인이 말할 때는 볼륨을 0, 그렇지 않을 때는 1로 설정
audioSource.volume = speaker.IsPlaying ? 0f : 1f;</code></pre><p>on/off는 ToggleSelfMute함수로 해줄건데 내가 상대방의 목소리를 끌 수도 있고, 내 목소리를 끌 수도 있음</p>
<p>내 코드에서 로컬과 원격을 분리한 이유는 자신의 음성 전송과 관련된 직접적인 제어가 필요하고, 원격 플레이어는 단순히 해당 클라이언트에서 음성이 재생되는지 여부에 따라 UI와 음소거 상태를 관리해야하기 때문에 두가지를 나눠 구현했다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[객체지향 5대 원칙/인터페이스 분리 원칙(ISP)]]></title>
            <link>https://velog.io/@yj_621/%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5-5%EB%8C%80-%EC%9B%90%EC%B9%99%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4-%EB%B6%84%EB%A6%AC-%EC%9B%90%EC%B9%99ISP</link>
            <guid>https://velog.io/@yj_621/%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5-5%EB%8C%80-%EC%9B%90%EC%B9%99%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4-%EB%B6%84%EB%A6%AC-%EC%9B%90%EC%B9%99ISP</guid>
            <pubDate>Sun, 17 Nov 2024 05:40:28 GMT</pubDate>
            <description><![CDATA[<h1 id="인터페이스-분리-원칙isp">인터페이스 분리 원칙(ISP)</h1>
<p>인터페이스 분리 원칙이란, 객체는 자신이 호출하지 않는 메소드에 의존하지 않아야한다는 원칙이다.</p>
<h2 id="5가지-solid-원칙">5가지 SOLID 원칙</h2>
<blockquote>
<p>객체지향 설계에서 지켜줘야 할 5개의 소프트웨어 개발 원칙</p>
</blockquote>
<ul>
<li>단일 책임 (SRP)</li>
<li>개방 폐쇄 (OCP)</li>
<li>리스코프 치환 (LSP)</li>
<li>인터페이스 분리 (ISP)</li>
<li>종속성 역전 (DIP)</li>
</ul>
<p>이 5가지를 SOLID원칙이라고 한다.</p>
<h3 id="단일-책임-원칙single-responsibility">단일 책임 원칙(Single Responsibility)</h3>
<ul>
<li>컴포넌트도 클래스도 함수도 하나만 책임진다.</li>
<li>클래스가 너무 많은 일을 책임진다면 일을 나눈다.</li>
</ul>
<p>*<em>단일 책임을 무시한다면 *</em>
<img src="https://velog.velcdn.com/images/yj_621/post/ae304bb0-8d01-4b65-9544-82d4921ef8d4/image.png" alt="">
<img src="https://velog.velcdn.com/images/yj_621/post/d421a44e-5c02-4a79-a87b-cbffffe241d5/image.png" alt="">
이런식으로 클래스를 분할한다.</p>
<h3 id="개방-폐쇄-원칙open--losed">개방 폐쇄 원칙(Open- losed)</h3>
<ul>
<li>확장에는 개방적이되, 수정에는 폐쇄적일 것</li>
<li>새로운 요소를 추가할 때마다 기존의 코드도 수정되는 상황을 피할 것</li>
</ul>
<p>각각 도형이 추가될때마다 추가한 클래스에 기능을 추가(기존 클래스를 수정하는 방식X)
<img src="https://velog.velcdn.com/images/yj_621/post/11dd1a7c-1c04-457f-a0c9-972e13ee70ab/image.png" alt=""></p>
<h3 id="리스코프-치환-원칙liskov-substitution">리스코프 치환 원칙(Liskov Substitution)</h3>
<ul>
<li>자식 클래스는 부모 클래스로 완전히 대체할 수 있다.</li>
<li>자식은 부모의 기능을 제거하지 않도록, 부모는 너무 많은 로직을 가지지 않도록</li>
<li>자식 클래스가 기능을 추가할땐 가능한한 인터페이스를 사용한다.</li>
</ul>
<h3 id="인터페이스-분리-원칙interface-scgregation">인터페이스 분리 원칙(Interface Scgregation)</h3>
<ul>
<li>객체는 자신이 호출하지 않는 메서드에 의존하지 않아야한다는 원칙</li>
<li>특정 클래스가 사용하지도 않을 약속에 얽매여서는 안된다.</li>
<li>하나의 인터페이스에 너무 많은 약속이 있으면 곤란</li>
</ul>
<p><img src="https://velog.velcdn.com/images/yj_621/post/807411ea-8777-4468-84cc-7404a2485d90/image.png" alt="">
규모가 너무 큰 객체를 상속했을 때 방생하는 문제와 이를 인터페이스로 분리하여 해결하는 방법이다.</p>
<h3 id="의존-역전-원칙dependency-inversion">의존 역전 원칙(Dependency Inversion)</h3>
<ul>
<li>클래스 간 종속성을 최소화한다.</li>
<li>상위 모듈이 하위 모듈의 정보나 기능에 의존하면 안된다.</li>
<li>구제촤된 클래스에 의존하기보다는 추상 클래스나 인터페이스에 의존해야 한다는 의미.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[C# 백준 1120 문자열
(나는 이제 실버다 ㅋㅋ)]]></title>
            <link>https://velog.io/@yj_621/C-%EB%B0%B1%EC%A4%80-1120-%EB%AC%B8%EC%9E%90%EC%97%B4%EB%82%98%EB%8A%94-%EC%9D%B4%EC%A0%9C-%EC%8B%A4%EB%B2%84%EB%8B%A4-%E3%85%8B%E3%85%8B</link>
            <guid>https://velog.io/@yj_621/C-%EB%B0%B1%EC%A4%80-1120-%EB%AC%B8%EC%9E%90%EC%97%B4%EB%82%98%EB%8A%94-%EC%9D%B4%EC%A0%9C-%EC%8B%A4%EB%B2%84%EB%8B%A4-%E3%85%8B%E3%85%8B</guid>
            <pubDate>Thu, 24 Oct 2024 07:38:58 GMT</pubDate>
            <description><![CDATA[<h2 id="❓문제">❓문제</h2>
<p>길이가 N으로 같은 문자열 X와 Y가 있을 때, 두 문자열 X와 Y의 차이는 X[i] ≠ Y[i]인 i의 개수이다. 예를 들어, X=”jimin”, Y=”minji”이면, 둘의 차이는 4이다.</p>
<p>두 문자열 A와 B가 주어진다. 이때, A의 길이는 B의 길이보다 작거나 같다. 이제 A의 길이가 B의 길이와 같아질 때 까지 다음과 같은 연산을 할 수 있다.</p>
<p>A의 앞에 아무 알파벳이나 추가한다.
A의 뒤에 아무 알파벳이나 추가한다.
이때, A와 B의 길이가 같으면서, A와 B의 차이를 최소로 하는 프로그램을 작성하시오.</p>
<hr>
<h2 id="✏️입력">✏️입력</h2>
<p>첫째 줄에 A와 B가 주어진다. A와 B의 길이는 최대 50이고, A의 길이는 B의 길이보다 작거나 같고, 알파벳 소문자로만 이루어져 있다.</p>
<h2 id="⌨️출력">⌨️출력</h2>
<p>A와 B의 길이가 같으면서, A와 B의 차이를 최소가 되도록 했을 때, 그 차이를 출력하시오.</p>
<hr>
<h3 id="예제-입력1">예제 입력1</h3>
<p>adaabc aababbc</p>
<h3 id="예제-출력1">예제 출력1</h3>
<p>2</p>
<hr>
<h3 id="예제-입력2">예제 입력2</h3>
<p>hello xello</p>
<h3 id="예제-출력2">예제 출력2</h3>
<p>1</p>
<hr>
<h2 id="💻코드">💻코드</h2>
<pre><code class="language-csharp">using System;

namespace Baekjoon
{
    class Program
    {
        static void Main(string[] args)
        {
            string[] input = Console.ReadLine().Split();
            string A = input[0];
            string B = input[1];
            int min = 50;

            for (int i = 0; i &lt;= B.Length - A.Length ; i++)
            {
                int count = 0;
                for (int j= 0; j &lt; A.Length ; j++)
                {
                    if (A[j] != B[i+j])
                    {
                        count++;
                    }
                }
                min = Math.Min(min, count);
            }
            Console.WriteLine(min);
        }
    }
}</code></pre>
<pre><code></code></pre><h2 id="✍️풀이">✍️풀이</h2>
<p>input에 띄어쓰기로 입력을 두 개 받고, input[0]은  문자열 A, input[0]은  문자열 B로 선언해준다.
여기서 *<em>A 길이 &lt;= B길이 *</em> 이므로 B-A만큼 반복문을 돌려준다.
A와 B의 차이를 최소화할 것이기 때문에 아래처럼 B문자열을 순회함.
<img src="https://velog.velcdn.com/images/yj_621/post/4743a5f6-6ff7-4052-b243-b58660d2fdff/image.png" alt="">
이때 차이가 빈 곳을 제외하고 2개 차이가 나므로 2가 된다.
<img src="https://velog.velcdn.com/images/yj_621/post/51ca9a2b-e971-427e-85eb-9025c29a45b4/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[멋쟁이사자처럼 부트캠프 TIL] 멋쟁이사자처럼 유니티게임스쿨 9일차]]></title>
            <link>https://velog.io/@yj_621/%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-TIL-%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-%EC%9C%A0%EB%8B%88%ED%8B%B0%EA%B2%8C%EC%9E%84%EC%8A%A4%EC%BF%A8-9%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@yj_621/%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-TIL-%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-%EC%9C%A0%EB%8B%88%ED%8B%B0%EA%B2%8C%EC%9E%84%EC%8A%A4%EC%BF%A8-9%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Mon, 14 Oct 2024 08:03:01 GMT</pubDate>
            <description><![CDATA[<h3 id="애니메이션">애니메이션</h3>
<p>달리다가 바로 멈췄으면 좋겠다 → Has Exit Time 없애기</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/0dfe4f89-742b-4f5c-9ec5-6126bac750ec/image.png" alt=""></p>
<blockquote>
<p>Nav Mesh Agent 컴포넌트의 Angular Speed : 초당 돌아볼 수 있는 각도</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/yj_621/post/9a61d09d-793d-400e-8abe-81fc148e4046/image.png" alt=""></p>
<h3 id="애니메이션에-따른-코드-추가">애니메이션에 따른 코드 추가</h3>
<pre><code class="language-csharp">using UnityEngine;
using UnityEngine.AI;

public class Enemy : MonoBehaviour
{
    enum State
    {
        idle,
        Follow,
        Attack
    }

    GameObject player;
    NavMeshAgent agent;
    Animator animator;

    State state;
    float currentStateTime;

    public float timeForNextState = 2;

    void Start()
    {
        animator = GetComponent&lt;Animator&gt;();
        player = GameObject.FindWithTag(&quot;Player&quot;);
        agent = GetComponent&lt;NavMeshAgent&gt;();

        state = State.idle;

        currentStateTime = timeForNextState;
    }

    void Update()
    {
        switch (state)
        {
            case State.idle:
                currentStateTime -= Time.deltaTime;
                if(currentStateTime &lt; 0)
                {
                    float distance = (player.transform.position - transform.position).magnitude;
                    if(distance &lt; 1.5f)
                    {
                        StartAttack();
                    }
                    else
                    {
                        StartFollow();
                    }
                }
                break;
            case State.Follow: 
                if(agent.remainingDistance &lt; 1.5f || !agent.hasPath)
                //agent.hasPath : 갈 수 있는 길
                {
                    StartIdle();
                }
                break;
            case State.Attack:
                currentStateTime -= Time.deltaTime;
                if(currentStateTime &lt; 0)
                {
                    StartIdle();
                }
                break;
        }
    }
    void StartIdle()
    {
        state = State.idle;
        currentStateTime = timeForNextState;
        agent.isStopped = true;
        animator.SetTrigger(&quot;Idle&quot;);
    }

    void StartFollow()
    {
        state = State.Follow;
        agent.destination = player.transform.position;
        agent.isStopped = false;
        animator.SetTrigger(&quot;Run&quot;);
    }

    void StartAttack()
    {
        state = State.Attack;
        currentStateTime = timeForNextState;
        animator.SetTrigger(&quot;Attack&quot;);

    }    
    public void OnDie()
    {
        Debug.Log(&quot;주금&quot;);
    }
}
</code></pre>
<h3 id="인터페이스">인터페이스</h3>
<pre><code class="language-csharp">using UnityEngine;
using UnityEngine.AI;

public class Enemy : MonoBehaviour, Health.IHealthListener
//Enemy 클래스는 Health.IHealthListener인터페이스를 구현해야한다.
//C#은 상속을 1개밖에 받지 못해 그 단점을 인터페이스로 보완
{
</code></pre>
<pre><code class="language-csharp">public class Health : MonoBehaviour
{
    public float hp = 10;
    public float maxHp = 10;
    public IHealthListener healthListener;

    void Start()
    {
        healthListener = GetComponent&lt;IHealthListener&gt;();
    }
    public void Damage(float damage)
    {
        if(hp&gt;0)
        {
            hp -= damage;
            if(hp &lt;= 0 )
            {
                //죽음
                Debug.Log(&quot;Dead&quot;);
               if(healthListener !=null)
                {
                    healthListener.OnDie();
                }
            }
            else
            {
                //다침
                Debug.Log(&quot;다침&quot;);
            }
        }
    }

    public interface IHealthListener
    {
        void OnDie();
    }
}
</code></pre>
<blockquote>
<p>interface : 여러 클래스가 공통된 기능을 제공하면서도 서로 다른 방식으로 그 기능을 구현할 수 있도록 하기 위해서</p>
</blockquote>
<pre><code class="language-csharp">// 1. 인터페이스 정의
public interface IAnimal
{
    void Speak();  // 동물들이 소리를 낼 수 있는 능력을 정의
}

// 2. Dog 클래스에서 IAnimal 인터페이스 구현
public class Dog : IAnimal
{
    public void Speak()
    {
        Console.WriteLine(&quot;멍멍&quot;);
    }
}

// 3. Cat 클래스에서 IAnimal 인터페이스 구현
public class Cat : IAnimal
{
    public void Speak()
    {
        Console.WriteLine(&quot;야옹&quot;);
    }
}

// 4. 인터페이스를 사용하는 코드
public class Program
{
    public static void Main()
    {
        IAnimal dog = new Dog();
        IAnimal cat = new Cat();

        dog.Speak();  // 출력: 멍멍
        cat.Speak();  // 출력: 야옹
    }
}
</code></pre>
<h3 id="죽고나서-없어지기">죽고나서 없어지기</h3>
<pre><code class="language-csharp">    public void OnDie()
    {
        state = State.Die;
        agent.isStopped = true;
        animator.SetTrigger(&quot;Die&quot;);
        Invoke(&quot;DestroyThis&quot;, 2);
    }
    void DestroyThis()
    {
        Destroy(gameObject);
    }</code></pre>
<h3 id="펀치-콜라이더-설정">펀치 콜라이더 설정</h3>
<p><img src="https://velog.velcdn.com/images/yj_621/post/f9b05223-df4f-4e9b-af34-bd8700dda17f/image.png" alt=""></p>
<p>손 쪽에 콜라이더를 넣어준 후, 펀치 애니메이션이 나갈때쯤 콜라이더를 켜주고, 끝날때 쯤 콜라이더를 꺼준다.</p>
<h3 id="무적시간">무적시간</h3>
<pre><code class="language-csharp">using UnityEngine;

public class Health : MonoBehaviour
{
    public float hp = 10;
    public float maxHp = 10;
    public float invincibleTime; //무적시간

    public IHealthListener healthListener;
    float lastDamageTime;

    void Start()
    {
        healthListener = GetComponent&lt;IHealthListener&gt;();
    }
    public void Damage(float damage)
    {
        //마지막으로 얻어 맞은 시간 + 무적 시간이 현재 시간보다 적으면
        if(hp&gt;0 &amp;&amp; lastDamageTime+invincibleTime &lt; Time.time)
        {
            hp -= damage;
            lastDamageTime = Time.time;

            if (hp &lt;= 0 )
            {
                //죽음
                Debug.Log(&quot;Dead&quot;);
               if(healthListener !=null)
                {
                    healthListener.OnDie();
                }
            }
            else
            {
                //다침
                Debug.Log(&quot;다침&quot;);
            }
        }
    }

    public interface IHealthListener
    {
        void OnDie();
    }
    void Update()
    {

    }
}
</code></pre>
<h3 id="hp-구현">Hp 구현</h3>
<p><img src="https://velog.velcdn.com/images/yj_621/post/c6030f6b-a4b7-467b-be7d-f110e9ecda89/image.png" alt=""></p>
<p>filed → Horizontal</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/778546aa-b947-4b86-830c-b7211c2b42c7/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/f7441679-c1e0-48d6-a16a-820ce5fa4d47/image.png" alt=""></p>
<p>맞으면 피가 줄어들게</p>
<pre><code class="language-csharp">public class Health : MonoBehaviour
{

    public void Damage(float damage)
    {
        //마지막으로 얻어 맞은 시간 + 무적 시간이 현재 시간보다 적으면
        if (hp &gt; 0 &amp;&amp; lastDamageTime + invincibleTime &lt; Time.time)
        {
            hp -= damage;

            if (hpGauge != null)
            {
                hpGauge.fillAmount = hp / maxHp;
            }
            //마지막으로 피해를 입은 시간을 기록
            lastDamageTime = Time.time;

            if (hp &lt;= 0)
            {
                //죽음
                Debug.Log(&quot;Dead&quot;);
                if (healthListener != null)
                {
                    healthListener.OnDie();
                }
            }
            else
            {
                //다침
                Debug.Log(&quot;다침&quot;);
            }
        }
    }
}
</code></pre>
<p>HP 게이지의 Amount값에 따라 색 바뀌도록</p>
<pre><code class="language-csharp">using UnityEngine;
using UnityEngine.UI;

public class GaugeColor : MonoBehaviour
{
    void Update()
    {
        Image image = GetComponent&lt;Image&gt;();

        image.color = Color.HSVToRGB(image.fillAmount / 3, 1.0f, 1.0f);
    }
}
</code></pre>
<p><img src="https://velog.velcdn.com/images/yj_621/post/4916c4eb-dd61-4d47-830b-4bd863cb94a2/image.png" alt="">
무서워 아저씨. ..</p>
<h3 id="render-mode">Render Mode</h3>
<p>Screen Space - Camera 일때</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/f725c577-8fc0-4348-8348-682759aaffa7/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/848b405a-654a-4966-9d0e-303c7606b53b/image.png" alt="">
거리를 설정하면 거리에 따라 캔버스가 뒤로 가보이게 됨
<img src="https://velog.velcdn.com/images/yj_621/post/f63d98d1-9635-4bc2-9c5a-a3c3725de332/image.png" alt="">
World Space 일때
<img src="https://velog.velcdn.com/images/yj_621/post/bad3b977-9d3f-4481-a887-37b9ee982dc1/image.png" alt=""></p>
<h3 id="enemy-hp-구현">Enemy HP 구현</h3>
<p>Enemy아래 캔버스를 달아주고, Hp를 똑같이 해준다.</p>
<p>여기서 Canvas는 world space로 해줌</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/25443ada-155c-4619-9c49-e6a770c0bcc4/image.png" alt=""></p>
<pre><code class="language-csharp">using UnityEngine;
using UnityEngine.UI;

public class Health : MonoBehaviour
{
    public float hp = 10;
    public float maxHp = 10;
    public float invincibleTime; //무적시간

    public Image hpGauge;

    public IHealthListener healthListener;
    float lastDamageTime;

    void Start()
    {
        healthListener = GetComponent&lt;IHealthListener&gt;();
    }
    public void Damage(float damage)
    {
        //마지막으로 얻어 맞은 시간 + 무적 시간이 현재 시간보다 적으면
        if (hp &gt; 0 &amp;&amp; lastDamageTime + invincibleTime &lt; Time.time)
        {
            hp -= damage;

            if (hpGauge != null)
            {
                hpGauge.fillAmount = hp / maxHp;
            }

            //마지막으로 피해를 입은 시간을 기록
            lastDamageTime = Time.time;

            if (hp &lt;= 0)
            {
                //죽음
                Debug.Log(&quot;Dead&quot;);
                if (healthListener != null)
                {
                    healthListener.OnDie();
                }
            }
            else
            {
                //다침
                Debug.Log(&quot;다침&quot;);
            }
        }
    }

    public interface IHealthListener
    {
        void OnDie();
    }
    void Update()
    {

    }
}
</code></pre>
<p>저 게이지가 나를 보도록</p>
<pre><code class="language-csharp">using UnityEngine;

public class LookCamera : MonoBehaviour
{
    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        transform.LookAt(transform.position + Camera.main.transform.forward);
        //HP 게이지가 나를 보도록
    }
}
</code></pre>
<p>위 코드 사용X(게이지를 보세용)</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/a1f2dc30-05d9-45d5-8609-7bd12d595fc2/image.png" alt=""></p>
<p>사용</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/1ef788c5-ae5c-4685-81d3-6262d3ffb5f6/image.png" alt=""></p>
<h3 id="player-죽음">Player 죽음</h3>
<p><img src="https://velog.velcdn.com/images/yj_621/post/08fb6bf8-29b3-4a53-8fbc-9a879ccc636e/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/da09cfff-f7a0-4754-978e-08c7a58fcedb/image.png" alt=""></p>
<p>플레이어가 쓰러지면서 화면이 어두워지도록 만들어줌</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/fe708e69-e5e4-4b25-9d5f-1d87d433a04f/image.png" alt=""></p>
<p>이렇게하면 Player가 안 움직이는데 Apply Root Motion을 켜주면 움직인다.</p>
<blockquote>
<p>Apply Root Motion : 오브젝트의 위치와 회전을 애니메이션이 제어하도록 할 것이냐를 결정</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/yj_621/post/9e91c6aa-73a8-4562-ae86-eb105b6a1e7d/image.png" alt=""></p>
<p>죽었을때 조작 불가능하도록</p>
<pre><code class="language-csharp">public class PlayerController : MonoBehaviour, Health.IHealthListener
{
......
    void Update()
    {
        if (GameManager.Instance.isPlaying)
        {
            Vector3 moveVector = moveAction.ReadValue&lt;Vector2&gt;(); //move 입력 감지
            Vector3 move = new Vector3(moveVector.x, 0, moveVector.y);
........

    public void OnDie()
    {
        GetComponent&lt;Animator&gt;().SetTrigger(&quot;Die&quot;);
        GameManager.Instance.PlayerDie();
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/yj_621/post/314bba16-4ad7-4389-9e79-fa4672439637/image.png" alt="">
<img src="https://velog.velcdn.com/images/yj_621/post/bf8597c0-aaef-4619-bfb7-c454488ba933/image.png" alt="">
Sort Order가 2여야함</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[멋쟁이사자처럼 부트캠프 TIL] 멋쟁이사자처럼 유니티게임스쿨 8일차]]></title>
            <link>https://velog.io/@yj_621/%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-TIL-%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-%EC%9C%A0%EB%8B%88%ED%8B%B0%EA%B2%8C%EC%9E%84%EC%8A%A4%EC%BF%A8-8%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@yj_621/%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-TIL-%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-%EC%9C%A0%EB%8B%88%ED%8B%B0%EA%B2%8C%EC%9E%84%EC%8A%A4%EC%BF%A8-8%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Mon, 14 Oct 2024 07:23:57 GMT</pubDate>
            <description><![CDATA[<p>조퇴로 인한 지금 업로드 ,, 크흠 ^_^</p>
<h3 id="코루틴으로-총알-흔적-없애기">코루틴으로 총알 흔적 없애기</h3>
<pre><code class="language-csharp">void RayCastFire()
{
    ...

    GameObject go = Instantiate(trailPrefab);
    Vector3[] pos = new Vector3[] { firingPosition.position, hitPosition };
    go.GetComponent&lt;LineRenderer&gt;().SetPositions(pos);

    //방법 1 Invoke()
    //방법 2
    StartCoroutine(DestroyTrail(go));        
    //방법 3
    Destroy(go, 0.1f);
}
IEnumerator DestroyTrail(GameObject obj)
    {
    yield return new WaitForSeconds(0.5f);        
    Destroy(obj);
   }
}</code></pre>
<p><strong>사용 이유</strong></p>
<p>비동기 작업을 처리하면서도 게임 루프의 제어를 유지할 수 있기 때문에 사용한다.</p>
<p><strong>사용 방법</strong></p>
<blockquote>
<p>IEnumerator 메소드이름()
{
  yield return new WaitForSeconds(초);
}
StartCoroutine(메소드이름());</p>
</blockquote>
<h3 id="총알이-맞은-지점-나타내기">총알이 맞은 지점 나타내기</h3>
<p><img src="https://velog.velcdn.com/images/yj_621/post/ca30454f-250a-4d1f-81b6-3006a868d03a/image.png" alt=""></p>
<p>파티클이 끝난 후 어떤걸 불러올지 정할 수 있음(None, Disable, Destroy, Callback(함수 불러오기))</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/475fe422-783d-4ee3-9523-15b358f7bf65/image.png" alt=""></p>
<p>총알이 맞은 방향 수직으로 파티클이 튀도록</p>
<pre><code class="language-csharp">    void RayCastFire()
    {
        Camera cam = Camera.main;

        RaycastHit hit;
        Ray r = cam.ViewportPointToRay(Vector3.one / 2); //카메라 정 중앙으로 발사

        Vector3 hitPosition = r.origin + r.direction * 200; //아무곳에도 부딪히지 않았다면 카메라 방향에서 200정도 떨어져있는 곳

        if (Physics.Raycast(r, out hit, 1000)) //어딘가에 빛이 부딪혔으면 true
        { //레이 r의 정보를 가지고 1000의 최대거리 만큼 레이를 쏜다. out : 변수를 넣었을때 변수가 변할 수 있다는 것(out을 안 쓰면 오류남)
            hitPosition = hit.point; //빛이 부딪힌 부분이 총알의 종착점

            //총알이 부딪힌 지점의 수직으로 파티클이 튀도록
            GameObject particle = Instantiate(particlePrefab);
            particle.transform.position = hitPosition;
            particle.transform.forward = hit.point;
        }</code></pre>
<h3 id="수류탄">수류탄</h3>
<p>총과 같은 위치에 수류탄 에셋을 가져와서 두기</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/449d6e4b-bbcb-4568-b05b-255679523be1/image.png" alt=""></p>
<p>상속</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/358e1e71-f8bd-4213-afad-8f5e8c6a4ae6/image.png" alt=""></p>
<p>0,0,0에 수류탄을 하나 만들어서 콜라이더를 넣어줌 </p>
<p>얘가 projectilePrefab이 될 예정</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/6b874ff3-3177-4e48-9504-0d364b0a4bc2/image.png" alt=""></p>
<p>상속되어서 인스펙터창에 다 나옴</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/7c0a7f68-46ed-4ed2-8c49-ade28add14dc/image.png" alt="">
각각 알맞은 곳에 넣어준다</p>
<pre><code class="language-csharp">using UnityEngine;

public class ProjectileWeapon : Weapon
{
    public GameObject projectilePrefab;
    public float projectileAngle = 30;
    public float projectileForce = 10;
    public float projectileTime = 5;

    protected override void Fire()
    {
        ProjectileFire();
    }
    public void ProjectileFire()
    {
        //30도 올린 각도 만들기
        Camera cam =Camera.main;

        Vector3 forward = cam.transform.forward; //카메라가 주시하는 방향 벡터
        Vector3 up = cam.transform.up; //y축 벡터

        Vector3 direction = forward + up * Mathf.Tan(projectileAngle * Mathf.Deg2Rad);
        //주시방향+(y축 * 라디안으로 치환한 발사각의 탄젠트값)

        direction.Normalize(); //벡터값 정규화
        direction *= projectileForce; //발사 방향에 발사 힘 곱해서

        GameObject go = Instantiate(projectilePrefab); //수류탄 인스턴스 생성
        go.transform.position = firingPosition.position; //생성한 수류탄 초기 위치 지정
        go.GetComponent&lt;Rigidbody&gt;().AddForce(direction, ForceMode.Impulse); //생성한 수류탄을 지정한 벡터로 투척
    }
}
</code></pre>
<pre><code class="language-csharp">public class Weapon : MonoBehaviour
{
    public void FireWeapon()
    {
        if (animator != null)
        {
            if (animator.GetCurrentAnimatorStateInfo(0).IsName(&quot;Idle&quot;))
            {
                animator.SetTrigger(&quot;Fire&quot;);
                Fire();
            }
        }
        else
        {
            Fire();
        }

    }

    protected virtual void Fire()
    {
        RayCastFire();
    }</code></pre>
<h3 id="폭발">폭발</h3>
<p><img src="https://velog.velcdn.com/images/yj_621/post/a293dd22-c0a7-42a6-9df4-06f5eef278af/image.png" alt=""></p>
<p>폭발하는 애니메이션 생성</p>
<pre><code class="language-csharp">using UnityEngine;

public class Bomb : MonoBehaviour
{
    public float time;

    private void Update()
    {
        time -= Time.deltaTime;
        if (time &lt; 0)
        {
            GetComponent&lt;Animator&gt;().SetTrigger(&quot;Explode&quot;);
            Destroy(gameObject, 2f);
        }
    }
}
</code></pre>
<pre><code class="language-csharp">using UnityEngine;

public class Bomb : MonoBehaviour
{
    public float time;

    private void Update()
    {
        time -= Time.deltaTime;
        if (time &lt; 0)
        {
            GetComponent&lt;Animator&gt;().SetTrigger(&quot;Explode&quot;);
            Destroy(gameObject, 2f);
        }
    }
}
</code></pre>
<p>수류탄 프리팹에 추가</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/ee1608e6-6e62-451c-9c00-9523577cde9f/image.png" alt=""></p>
<pre><code class="language-csharp">public class ProjectileWeapon : Weapon
{
    public float projectileTime = 5;

    public void ProjectileFire()
    {  
        go.GetComponent&lt;Bomb&gt;().time = projectileTime;
    }
}</code></pre>
<h3 id="무기-변경">무기 변경</h3>
<p>인풋 추가</p>
<p>Listen누르고 원하는 버튼 누르면 쉽게 가능
<img src="https://velog.velcdn.com/images/yj_621/post/f862f626-584d-46c0-84d3-41ef1c118298/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/8d99335b-5caa-419a-8609-f252b61b0f22/image.png" alt="">
PlayerController 스크립트 수정</p>
<pre><code class="language-csharp">public List&lt;Weapon&gt; weapons;
int currentWeaponIndex;

    void OnChangeWeapon()
    {
        weapons[currentWeaponIndex].gameObject.SetActive(false);

        currentWeaponIndex++;
        if(currentWeaponIndex &gt; weapons.Count-1)
        {
            currentWeaponIndex = 0;
        }
        weapons[currentWeaponIndex].gameObject.SetActive(true);
    }</code></pre>
<pre><code class="language-csharp">public class Weapon : MonoBehaviour
{
    public GameObject trailPrefab;
    public Transform firingPosition;
    public GameObject particlePrefab;
    public TextMeshProUGUI bulletText;

    public int currentBullet = 8;
    public int totalBullet = 32;
    public int maxBulletMagazine = 8;

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

    void Update()
    {
        bulletText.text = currentBullet + &quot;/&quot; + totalBullet;
    }
    public void FireWeapon()
    {
        if (currentBullet &gt; 0) //현재 잔탄이 남아있을때
        {
            if (animator != null)
            {
                if (animator.GetCurrentAnimatorStateInfo(0).IsName(&quot;Idle&quot;))
                {
                    animator.SetTrigger(&quot;Fire&quot;);
                    currentBullet--;
                    Fire();
                }
            }
            else
            {
                currentBullet--;
                Fire();
            }
        }

    }

    protected virtual void Fire()
    {
        RayCastFire();
    }

    public void ReloadWeapon()
    {
        if (totalBullet &gt; 0) //탄약이 있으면
        {
            if (animator != null)
            {
                if (animator.GetCurrentAnimatorStateInfo(0).IsName(&quot;Idle&quot;))
                {
                    animator.SetTrigger(&quot;Reload&quot;);
                    Reload();
                }
            }
            else
            {
                Reload();
            }

        }
    }
    void Reload()
    {
        if (totalBullet &gt;= maxBulletMagazine - currentBullet)
            //탄약수가 (탄창 탄약 - 잔탄)보다 같거나 많으면
        {
            //꽉 채우기
            totalBullet -= maxBulletMagazine - currentBullet;
            currentBullet = maxBulletMagazine;
            //잔탄을 탄창 탄약으로 설정
        }
        else
            //탄약수가 (탄창 탄약 - 잔탄)보다 적으면
        {
            //남은 탄창 + 채울거
            currentBullet += totalBullet;
            totalBullet = 0; //탄약을 비움
        }
    }
</code></pre>
<h3 id="적-ai-만들기">적 Ai 만들기</h3>
<p>적으로 지정해줘서 콜라이더 넣어주기</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/7a68823b-e763-423a-abe6-504cf8b4b204/image.png" alt="">
Nav Mesh Surface 추가후 Bake
<img src="https://velog.velcdn.com/images/yj_621/post/9c0210c4-df08-4af5-be5d-9fa3029179fa/image.png" alt="">
Enemy에 Nav Mesh Agent를 추가</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/b2b20de5-2360-4512-84d2-67499e97821b/image.png" alt=""></p>
<pre><code class="language-csharp">using UnityEngine;
using UnityEngine.AI;

public class Enemy : MonoBehaviour
{
    GameObject player;
    NavMeshAgent agent;

    void Start()
    {
        player = GameObject.FindWithTag(&quot;Player&quot;);
        agent = GetComponent&lt;NavMeshAgent&gt;(); 
    }

    void Update()
    {
        agent.destination = player.transform.position;
    }
}</code></pre>
<p>내가 가있던 자리로 쫓기</p>
<pre><code class="language-csharp">using UnityEngine;
using UnityEngine.AI;

public class Enemy : MonoBehaviour
{
    GameObject player;
    NavMeshAgent agent;

    void Start()
    {
        player = GameObject.FindWithTag(&quot;Player&quot;);
        agent = GetComponent&lt;NavMeshAgent&gt;(); 
    }

    void Update()
    {
        //destination까지 남은 거리(흔적쫓기)
        if (agent.remainingDistance&lt;1f)
        {
            agent.destination = player.transform.position;
        }
    }
}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[멋쟁이사자처럼 부트캠프 TIL] 멋쟁이사자처럼 유니티게임스쿨 7일차]]></title>
            <link>https://velog.io/@yj_621/%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-TIL-%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-%EC%9C%A0%EB%8B%88%ED%8B%B0%EA%B2%8C%EC%9E%84%EC%8A%A4%EC%BF%A8-7%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@yj_621/%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-TIL-%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-%EC%9C%A0%EB%8B%88%ED%8B%B0%EA%B2%8C%EC%9E%84%EC%8A%A4%EC%BF%A8-7%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Thu, 10 Oct 2024 08:51:22 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/yj_621/post/8056b007-e7b9-4fb5-bb3a-93b6db9f977f/image.png" alt=""></p>
<blockquote>
<p>Slope Limit : 올라갈수 있는 경사로의 한계</p>
<p>Step Offset : 올라갈 수 있는 계단의 단차 등을 설정할 수 있다</p>
</blockquote>
<h3 id="중력">중력</h3>
<pre><code class="language-csharp">public class PlayerController : MonoBehaviour
{
    public float WalkSpeed = 7;
    public float mouseSens = 1;
    public Transform cameraTransform; 

    public float gravity = 10;
    public float terminalSpeed = 20; //떨어지는 물체가 최대 속도를 유지하게

    float horizontalAngle;
    float verticalAngle;
    float verticalSpeed; //낙하 속력

    InputAction moveAction;
    InputAction lookAction;

    CharacterController characterController;

    ...생략
    void Update(){
        //중력
        verticalSpeed -=  gravity * Time.deltaTime; 
        //gravity값에 타임을 곱해 verticalSpeed를 뺴주면 점점 더 빠르게 아래로 떨어짐

        if(verticalSpeed &lt; -terminalSpeed) 
        //verticalSpeed가 최대 낙하속도(terminalSpeed)를 넘어가면 **-**최대 낙하속도로 고정
        {
            verticalSpeed = -terminalSpeed;
        }
        Vector3 verticalMove = new Vector3(0, verticalSpeed, 0); //떨어지는 Vector 생성 
        verticalMove *= Time.deltaTime; //Vector에 deltaTime 적용

        CollisionFlags flag = characterController.Move(verticalMove); //캐릭터에게 중력속도 적용

        if ((flag &amp; (CollisionFlags.Below | CollisionFlags.Above)) !=0) 
        //flag중에 Bellow 비트가 없으면 / 떨어지는 상태가 아니면(바닥에 땅을 디딛고 있냐)
        {
            verticalSpeed = 0; //하강속도를 0으로 초기화
        }
     }</code></pre>
<p>땅에 붙어있는지 0.5초 뒤에 인지하도록</p>
<pre><code class="language-csharp">public class PlayerController : MonoBehaviour
{
    public float WalkSpeed = 7;
    public float mouseSens = 1;
    public Transform cameraTransform;

    public float gravity = 10;
    public float terminalSpeed = 20; //떨어지는 물체가 최대 속도를 유지하게

    float horizontalAngle;
    float verticalAngle;
    float verticalSpeed; //낙하 속력
    bool isGrounded;
    float groundedTimer;

    InputAction moveAction;
    InputAction lookAction;

    CharacterController characterController;

    void Start()
    {
        Cursor.lockState = CursorLockMode.Locked; //게임화면 내 커서 잠금       
        Cursor.visible = false; //커서 숨기기

        InputActionAsset inputActions = GetComponent&lt;PlayerInput&gt;().actions;
        moveAction = inputActions.FindAction(&quot;Move&quot;); //Move에 해당하는 입력값 가져오기
        lookAction = inputActions.FindAction(&quot;Look&quot;);

        characterController = GetComponent&lt;CharacterController&gt;();

        horizontalAngle = transform.localEulerAngles.y; //바라보는 각도에 얼마나 돌아야하는지 세팅
        verticalAngle = 0;
        verticalSpeed = 0;
        isGrounded = true;
        groundedTimer = 0;
    }

   void Update()
   {  
        Vector3 verticalMove = new Vector3(0, verticalSpeed, 0); //떨어지는 Vector 생성 
        verticalMove *= Time.deltaTime; //Vector에 deltaTime 적용

        CollisionFlags flag = characterController.Move(verticalMove); //캐릭터에게 중력속도 적용

        if ((flag &amp; CollisionFlags.Below) !=0)
         //flag중에 Bellow 비트가 없으면 / 떨어지는 상태가 아니면(바닥에 땅을 디딛고 있냐)
        {
            verticalSpeed = 0; //하강속도를 0으로 초기화
        }
        if(!characterController.isGrounded) //characterController가 땅에 안 붙어있다고함
        { 
            if(isGrounded) //안 붙어있다면
            {
                groundedTimer += Time.deltaTime; //timer를 움직이게하고
                if(groundedTimer &gt; 0.3f) //0.5초뒤에
                {
                    isGrounded = false; //false로
                }
            }
        }
        else //땅에 붙어있다면
        { 
            isGrounded = true; //true
            groundedTimer = 0;
        }
   }</code></pre>
<h3 id="점프">점프</h3>
<p>뉴인풋시스템에 있는 Jump를 사용하기 위해 OnJump()함수 사용
<img src="https://velog.velcdn.com/images/yj_621/post/811338b3-bd6c-43e0-a3f5-2e4729ac73cc/image.png" alt=""></p>
<pre><code class="language-csharp">void OnJump()
{
    if(isGrounded)
    {
        verticalSpeed = jumpSpeed;
        isGrounded = false; //무한점프 방지
    }
}</code></pre>
<h3 id="총만들기">총만들기</h3>
<p>총 에셋 가져오기</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/917d42cb-d5d8-4394-9816-deae236aca19/image.png" alt=""></p>
<p>애니메이션 생성</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/d2ac250c-b3ec-4262-bdb7-0e13c5c16ca2/image.png" alt=""></p>
<p>재장전 애니메이션은 Mag와 Mag_Full을 사용</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/fa20f8e7-d783-4362-89d0-609e22198eee/image.png" alt=""></p>
<h3 id="애니메이션-연결">애니메이션 연결</h3>
<p>저 이름으로 할거면 스크립트도 저 이름으로 해야댐</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/8da7436c-6483-45bf-8f3c-dc61209e573e/image.png" alt=""></p>
<p>발사할땐 Has Exit TIme없이(바로 애니메이션이 실행될 수 있도록)
<img src="https://velog.velcdn.com/images/yj_621/post/6811b313-3180-4528-aabf-5a79bc5cd9e7/image.png" alt=""></p>
<p>Trigger 설정</p>
<hr>
<p>Idle로 돌아올땐(애니메이션이 다 끝나고) Has Exit TIme 체크</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/83bbdc28-6a63-43a5-a947-8b7279166322/image.png" alt=""></p>
<p>인풋 이름 바꿔서
<img src="https://velog.velcdn.com/images/yj_621/post/48d16b2a-115b-4f73-9463-8c7923bc5988/image.png" alt=""></p>
<pre><code class="language-csharp">InputAction fireAction;

    void Start()
    {

        fireAction = inputActions.FindAction(&quot;Fire&quot;);
    }
    void Updat()
    {

        if (fireAction.WasPressedThisFrame())
        {

            weapon.FireWeapon();
        }
    }</code></pre>
<p>이렇게 갖다 쓸 수 있다.</p>
<pre><code class="language-csharp">public class Weapon : MonoBehaviour
{
    }
    public void FireWeapon()
    {
        Debug.Log(&quot;Fire&quot;);
    }
    public void ReloadWeapon()
    {

    }
}</code></pre>
<p>Player Input 컴포넌트를 보면 아래 저 함수들을 사용할 수 있다고 보여줌
<img src="https://velog.velcdn.com/images/yj_621/post/71a06ff1-389a-45f8-bc25-8220c2d4618c/image.png" alt=""></p>
<pre><code class="language-csharp">public class PlayerController : MonoBehaviour
{
   void Update()
        {
        if (fireAction.WasPressedThisFrame())
        {
            weapon.FireWeapon();
        }
        if (reloadAction.WasPressedThisFrame()) //이번 프레임에 눌렸는가
        {
            weapon.ReloadWeapon();
        }
    } 
 }</code></pre>
<pre><code class="language-csharp">public class Weapon : MonoBehaviour
{
    Animator animator;
    void Start()
    {
        animator = GetComponent&lt;Animator&gt;();
    }
    public void FireWeapon()
    {
        if (animator.GetCurrentAnimatorStateInfo(0).IsName(&quot;Idle&quot;))
        {
            animator.SetTrigger(&quot;Fire&quot;);
        }
    }
    public void ReloadWeapon()
    {
        if (animator.GetCurrentAnimatorStateInfo(0).IsName(&quot;Idle&quot;))
        {
            animator.SetTrigger(&quot;Reload&quot;);
        }
    }
}</code></pre>
<p>인풋시스템 수동 추가</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/eb03c5b3-f701-4690-8a21-5de412367615/image.png" alt=""></p>
<p>없는건 이렇게 만들 수 있음</p>
<h3 id="조준선">조준선</h3>
<p><img src="https://velog.velcdn.com/images/yj_621/post/3980099f-3c48-4395-958c-0addcf37f842/image.png" alt="">
<img src="https://velog.velcdn.com/images/yj_621/post/fc4c58c2-6119-41cd-a3d1-c3a2e86c9708/image.png" alt="">
이렇게 추가 된 모습</p>
<h3 id="총구-위치-추가">총구 위치 추가</h3>
<p>빈 오브젝트를 총구 앞으로 설정
<img src="https://velog.velcdn.com/images/yj_621/post/b8cff798-8d25-4d28-bb8a-6c718706fe98/image.png" alt=""></p>
<p>Effect - Line 추가 </p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/77c4abf8-03ca-4d35-bbb4-72a51d04462d/image.png" alt=""></p>
<p>TrailPrefab으로 이름을 바꿔주고 프리팹으로 만들어줌</p>
<p>0,0,0으로 설정</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/bde88ff6-e261-4627-b7d0-8b3a4770aff0/image.png" alt=""></p>
<pre><code class="language-csharp">using UnityEngine;

public class Weapon : MonoBehaviour
{
    public GameObject trailPrefab;
    public Transform firingPosition;

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

    void Update()
    {

    }
    public void FireWeapon()
    {
        if (animator.GetCurrentAnimatorStateInfo(0).IsName(&quot;Idle&quot;))
        {
            animator.SetTrigger(&quot;Fire&quot;);
            RayCastFire();
        }
    }
    public void ReloadWeapon()
    {
        if (animator.GetCurrentAnimatorStateInfo(0).IsName(&quot;Idle&quot;))
        {
            animator.SetTrigger(&quot;Reload&quot;);
        }
    }

    void RayCastFire()
    {
        Camera cam = Camera.main;

        RaycastHit hit;
        Ray r = cam.ViewportPointToRay(Vector3.one / 2); //카메라 정 중앙으로 발사

        Vector3 hitPosition = r.origin + r.direction * 200; //아무곳에도 부딪히지 않았다면 카메라 방향에서 200정도 떨어져있는 곳

        if(Physics.Raycast(r, out hit, 1000)) //어딘가에 빛이 부딪혔으면 true
        { //레이 r의 정보를 가지고 1000의 최대거리 만큼 레이를 쏜다. out : 변수를 넣었을때 변수가 변할 수 있다는 것(out을 안 쓰면 오류남)
            hitPosition = hit.point; //빛이 부딪힌 부분이 총알의 종착점
        }

        GameObject go = Instantiate(trailPrefab);
        Vector3[] pos = new Vector3[] { firingPosition.position, hitPosition };
        go.GetComponent&lt;LineRenderer&gt;().SetPositions(pos);
    }
}
</code></pre>
<blockquote>
<p><strong>Physics.Raycast(Vector3 origin, Vector3 direction, out RaycastHit hitInfo, float maxDistance) :</strong>
<code>시작점(Origin)</code>과 <code>방향(Direction)</code>으로 <code>최대거리(maxDistance)</code>만큼 레이를 쏘는 함수입니다. 최대거리 안에서 충돌이 되면 <code>true</code>를 반환하고 <code>RaycastHit</code>로 충돌정보를 넘겨줍니다.</p>
</blockquote>
<blockquote>
<p><strong>Physics.Raycast(r, out hit, 1000) :</strong>
레이 r의 정보를 가지고 1000의 최대거리 만큼 레이를 쏜다. out : 변수를 넣었을때 변수가 변할 수 있다는 것(out을 안 쓰면 오류남)</p>
</blockquote>
<p>쏘면 이렇게 됨
<img src="https://velog.velcdn.com/images/yj_621/post/7256fc7c-aeab-494c-9e40-e9a92c6f3043/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[멋쟁이사자처럼 부트캠프 TIL] 멋쟁이사자처럼 유니티게임스쿨 6일차 - 3D 프로젝트 시작]]></title>
            <link>https://velog.io/@yj_621/%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-TIL-%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-%EC%9C%A0%EB%8B%88%ED%8B%B0%EA%B2%8C%EC%9E%84%EC%8A%A4%EC%BF%A8-6%EC%9D%BC%EC%B0%A8-3D-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%8B%9C%EC%9E%91</link>
            <guid>https://velog.io/@yj_621/%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-TIL-%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-%EC%9C%A0%EB%8B%88%ED%8B%B0%EA%B2%8C%EC%9E%84%EC%8A%A4%EC%BF%A8-6%EC%9D%BC%EC%B0%A8-3D-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%8B%9C%EC%9E%91</guid>
            <pubDate>Tue, 08 Oct 2024 08:25:38 GMT</pubDate>
            <description><![CDATA[<p>사용 에셋</p>
<p><a href="https://pixellated.itch.io/survival-city-lowpoly-pack">Survival city lowpoly pack</a></p>
<p><a href="https://doctor-sci3nce.itch.io/psx-misc-gun-pack">PSX Misc. Gun Pack</a></p>
<p><a href="https://id.unity.com/ko/conversations/e5d92605-6e49-4946-bcac-0b1d7e3dd12400df"></a></p>
<p>3D 오브젝트가 마젠타 색으로 변했을때 전부다 체크하고 컨벌트</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/4a5db264-4c17-4adc-8844-ca820852d312/image.png" alt=""></p>
<p>Window - Rendering - Render Pipeline Convert로 들어가서 싹다 선택후 and Convert버튼 누르기</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/0fdbb95b-632e-4cf5-8692-3b119255b830/image.png" alt="">
Player 자식에 카메라를두고 Character Controller 컴포넌트 부착(1인칭 효과)</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/ed5f79b8-f410-4939-8fb4-e972c7403dbd/image.png" alt=""></p>
<p>Player에 아래 컴포넌트들 부착</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/d61bb5d0-3599-48e4-b4b9-fa6dc3f7efc6/image.png" alt="">
옛날 입력 시스템+최근거 다 쓸 수 있음(Both)</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/f5c4042a-1edf-4ff4-90c2-87d4f21ea839/image.png" alt="">
[최근거]</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/313ef41d-601c-4cfb-8417-0cb7d00fb5c9/image.png" alt=""></p>
<h3 id="마우스-커서-조절">마우스 커서 조절</h3>
<pre><code class="language-csharp">using UnityEngine;

public class PlayerController : MonoBehaviour
{
    void Start()
    {
        Cursor.lockState = CursorLockMode.Locked;        
        Cursor.visible = false; //그냥 추가
    }
}
</code></pre>
<p>이렇게하면 마우스 커서가 안 보이게 된다</p>
<h3 id="플레이어-input">플레이어 input</h3>
<pre><code class="language-csharp">using UnityEngine;
using UnityEngine.InputSystem;

public class PlayerController : MonoBehaviour
{
    public float WalkSpeed = 7;
    InputAction moveAction;

    void Start()
    {
        Cursor.lockState = CursorLockMode.Locked;        
        Cursor.visible = false;

        InputActionAsset inputActions = GetComponent&lt;PlayerInput&gt;().actions;

        moveAction = inputActions.FindAction(&quot;Move&quot;);

    }
 }
</code></pre>
<p>뉴 인풋 시스템 사용</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/7e78f696-35d0-4439-b286-90a5738a701e/image.png" alt=""></p>
<pre><code class="language-csharp">using UnityEngine;
using UnityEngine.InputSystem;

public class PlayerController : MonoBehaviour
{
    public float WalkSpeed = 7; //움직이는 속도
    public float mouseSens = 1; //마우스 민감도
    public Transform cameraTransform; //카메라가 바라보고 있는 방향

    float horizontalAngle; //가로 각도(마우스 움직일때 돌아가는거)
    float verticalAngle;  //세로 각도

    InputAction moveAction; //움직일때 input
    InputAction lookAction; //마우스 움직일때 보이는거

    CharacterController characterController; //계속 인스턴스로 갖고오기 귀찮아서

    void Start()
    {
        Cursor.lockState = CursorLockMode.Locked; //게임화면 내 커서 잠금       
        Cursor.visible = false; //커서 숨기기

        InputActionAsset inputActions = GetComponent&lt;PlayerInput&gt;().actions; 
        //인풋시스템에서 할당
        moveAction = inputActions.FindAction(&quot;Move&quot;); //Move에 해당하는 입력값 가져오기
        lookAction = inputActions.FindAction(&quot;Look&quot;); //Look &quot;

        characterController = GetComponent&lt;CharacterController&gt;();

        horizontalAngle = transform.localEulerAngles.y; //바라보는 각도에 얼마나 돌아야하는지 세팅
        verticalAngle = 0; //초기값 설정(상하 각도)

    }

    void Update()
    {
        Vector3 moveVector = moveAction.ReadValue&lt;Vector2&gt;(); //move 입력 감지
        Vector3 move = new Vector3(moveVector.x, 0, moveVector.y);

        //이동벡터가 1보다 크다면 1로
        if(move.magnitude &gt;1) //Vector크기 감지(대각선 그거 어쩌구)
        {
            move.Normalize(); //아래 그림으로 설명함
        }
        move = move * WalkSpeed * Time.deltaTime;
        move = transform.TransformDirection(move); //현재 gameObjct의 방향으로 벡터를 돌린다.
        characterController.Move(move); //characterController컴포넌트에 접근해 움직여준다.

        Vector2 look = lookAction.ReadValue&lt;Vector2&gt;(); //look 액션 벡터를 가져오고

        float turnPlayer = look.x * mouseSens; //마우스 감도를 적용하고

        horizontalAngle += turnPlayer; //현재 각도에 더한다

        if (horizontalAngle &gt;= 360) horizontalAngle -= 360; //변화된 각도를 넣는다.
        if (horizontalAngle &lt;0) horizontalAngle += 360;

        Vector3 currentAngle = transform.localEulerAngles; //currentAngle 는 현재 y회전값
        currentAngle.y = horizontalAngle; 
        transform.localEulerAngles = currentAngle;

        //마우스 상하
        float turnCam = look.y * mouseSens;
        verticalAngle -= turnCam; //상하이동은 카메라에서 얻어온 verticalAngle에 더함
        verticalAngle = Mathf.Clamp(verticalAngle, -89f, 89f); //90도 이상 꺾이지 않도록 구간 지정
        currentAngle = cameraTransform.localEulerAngles; //카메라의 EulerAngles의 x값을 회전량을 적용한 verticalAngle로 바꿈
        currentAngle.x = verticalAngle;
        cameraTransform.localEulerAngles = currentAngle;
    }
}
</code></pre>
<blockquote>
<p>eulerAngles : 오일러앵글이라고 하며 x,y,z축 중심으로 회전한다는 의미
eulerAngles .x 라고하면 x축 회전</p>
</blockquote>
<blockquote>
<p>localEulerAngles :  부모에 대한 상대좌표로 부모가 없다면 월드좌표 공간이 기준(부모의 상대적인 회전값을 Vector3으로 접근)</p>
</blockquote>
<p><code>//이동벡터가 1보다 크다면 1로</code> :</p>
<p>아래 대각선을 1로 맞춰주는거
<img src="https://velog.velcdn.com/images/yj_621/post/359ff8ea-f159-4bc5-8ef1-ad886465d09f/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[멋쟁이사자처럼 부트캠프 TIL] 멋쟁이사자처럼 유니티게임스쿨 6일차]]></title>
            <link>https://velog.io/@yj_621/%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-TIL-%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-%EC%9C%A0%EB%8B%88%ED%8B%B0%EA%B2%8C%EC%9E%84%EC%8A%A4%EC%BF%A8-6%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@yj_621/%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-TIL-%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-%EC%9C%A0%EB%8B%88%ED%8B%B0%EA%B2%8C%EC%9E%84%EC%8A%A4%EC%BF%A8-6%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Tue, 08 Oct 2024 08:23:25 GMT</pubDate>
            <description><![CDATA[<h3 id="프리팹을-만들어서-중요-오브젝트를-한-번에-관리">프리팹을 만들어서 중요 오브젝트를 한 번에 관리</h3>
<p>Enemy와 Player를 프리팹으로 만든다.</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/285a22eb-7e81-4f17-85cc-9941a3bde369/image.png" alt=""></p>
<p>Level Data라는 빈 오브젝트 하위에 다른 난이도의 맵을 만들때 사용할 프리팹으로 생성</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/2f6d11e9-0534-4424-9d22-92a6fe4b8d51/image.png" alt=""></p>
<p>프리팹으로 만들었기 때문에 프리팹을 지웠다 다시 올리면 Missing이 뜸
<img src="https://velog.velcdn.com/images/yj_621/post/0eb9b80f-bf53-4592-b4a2-9c8d5f19eae8/image.png" alt="">
해결 방법!
<img src="https://velog.velcdn.com/images/yj_621/post/91574caf-5a39-4cee-bd49-66903c35149f/image.png" alt="">
LevelLoader 스크립트에서 Missing이 뜨는 오브젝트를 다시 불러와준다</p>
<pre><code class="language-csharp">using UnityEngine;

public class LevelLoader : MonoBehaviour
{
    public PlayerController Player;
    public GameObject Cinemachine;
    private void Start()
    {
        GameManager.Instance.Player = Player; 
        GameManager.Instance.CinemaCamera = Cinemachine;
    }

}
</code></pre>
<p><strong>Start</strong>가 아니라 <strong>Awake</strong>로 하려니까 GameManager에 Awake에서 Instance로 만들어주는게 우선순위가 더 높아서 오류가 뜸<code>(인스턴스를 만들고 찾아야하는데 찾고 만들려니까 안 찾아짐)</code></p>
<hr>
<p><strong>다른 방법(Scripts Execution Order)</strong>
<img src="https://velog.velcdn.com/images/yj_621/post/976aec2d-04fa-4863-b4fb-ca0234561b2a/image.png" alt=""></p>
<p>우선순위를 정해주는 설정창으로 </p>
<p>[ Project Settings - Scripts Execution Order - +버튼에서 내 스크립트 할당 ]하면 어떤 스크립트가 먼저 실행될지 정할 수 있다.</p>
<hr>
<p>유니티는 기본적으로 아래와 같은 순서로 실행된다
<img src="https://velog.velcdn.com/images/yj_621/post/2b3e4c51-1e9e-4c29-a87f-ea8594530bf9/image.png" alt=""></p>
<p>Enemy를 새로 만들때마다 Terrain만 계속 추가해주어야해서 스크립트에서 할당해주자.</p>
<p><img src="https://prod-files-secure.s3.us-west-2.amazonaws.com/e1d9a61c-ab12-481e-9094-8916efe0a9d3/99644f4f-5ca8-4979-9cb1-912739463647/image.png" alt="image.png"></p>
<pre><code class="language-csharp">using UnityEngine;

public class EnemyController : MonoBehaviour
{
    public int Hp = 3;
    public float Speed = 3;
    public CompositeCollider2D TerrainCollider;
    public Collider2D FrontCollider;
    public Collider2D FrontBottomCollider;

    Vector2 vx;

    private void Awake()
    {
        GameObject.FindGameObjectWithTag(&quot;Terrain&quot;).GetComponent&lt;CompositeCollider2D&gt;();
    }</code></pre>
<p><code>FindGameObjectWithTag</code> 를 사용해서 태그로 찾아 할당해주기</p>
<h3 id="레벨-선택창-만들기">레벨 선택창 만들기</h3>
<p>LevelSelect 씬을 만들고 캔버스를 만들어준다</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/b19b9e14-96f7-4ad5-94c4-37041150d40f/image.png" alt=""></p>
<p>이런식으로 Panel - Scroll View 순서로 만들어주고, 아래처럼 설정</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/e12b3cc4-3a77-45d3-95ab-bf65986e3fd2/image.png" alt="">
가로로 스크롤할거기 때문에 Vertical 체크 해제</p>
<p>Level Manager를 싱글톤으로 작성</p>
<blockquote>
<p>싱글톤 : 무조건 한번만 만들어야함
이런식으로 instance가 있으면(null이 아니면) 삭제해야함</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/158226c6-f2bf-41d9-87d9-abbf75e4ffad/image.png" alt=""></p>
</blockquote>
<p>LevelManager위에 class를 만들어주고 list를 만들어줌
<img src="https://velog.velcdn.com/images/yj_621/post/f5d34c4c-d90a-4c78-888c-9ca0d0f08a8c/image.png" alt=""></p>
<pre><code class="language-csharp">using System;
using System.Collections.Generic;
using UnityEngine;

[Serializable]
public class LevelInfo
{
    public string LevelName;
    public Sprite LevelThum;
    public GameObject LevelPrefab;

}

public class LevelManager : MonoBehaviour
{
    public List&lt;LevelInfo&gt; levels;

    private static LevelManager instance;  
    public static LevelManager Instance
    {
         get { return instance; }
        private set
        {
            instance = value;
        }
    }

    private void Awake()
    {
        if (instance == null)
        {
            instance = this;
            DontDestroyOnLoad(gameObject);
        }
        else
        {
            Destroy(gameObject);
        }
    }
}</code></pre>
<p>Class를 Serializable로 해주어서 인스펙터창에 나타나게 된다.</p>
<blockquote>
<p>Serializable : 구조체나 클래스에서만 사용하는 인스펙터에서 접근할 수 있도록 하는 키워드</p>
</blockquote>
<p>맵 프리팹을 다르게 두개 만들어서 각각에 할당해주면 된다.</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/0d34ea3f-a609-4b30-b461-110d423c140f/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/bea901b3-a2fb-4c3e-af61-5b214ef95459/image.png" alt=""></p>
<p>public List<LevelInfo> levels;에는 Name, Thum, Prefab 각각 할당해준 것들이 들어가게 된다.</p>
<p>가로로 정렬하는 Horizontal Layout Group 생성
  <img src="https://velog.velcdn.com/images/yj_621/post/b7820adc-3156-44ee-84b9-7f9ff400a4b9/image.png" alt="">
content size fitter : 컨텐츠의 크기에 따라 자동으로 컨텐트의 길이가 달라지도록(크기가 작으면 스크롤이 그 크기밖에 안됨)
  <img src="https://velog.velcdn.com/images/yj_621/post/50a43bd1-0d32-4749-9374-10539771b97b/image.png" alt="">
<img src="https://velog.velcdn.com/images/yj_621/post/7799d778-92c8-4b7e-ae32-c6717e267afb/image.png" alt=""></p>
<p>  LevelPanel 스크립트 생성
  <img src="https://velog.velcdn.com/images/yj_621/post/7794d14d-807f-4038-a991-f60dc9a29175/image.png" alt=""></p>
<pre><code class="language-csharp">using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class LevelPanel : MonoBehaviour
{
    int StageIndex;
    public Image StageThum;
    public TextMeshProUGUI TextTitle;

    public void SetLevelInfomation(int stageIndex, Sprite thumnail, string title)
    {
        StageThum.sprite = thumnail;
        this.StageIndex = stageIndex;
        TextTitle.text = title;
    }

}
</code></pre>
<p>위에 LevelTitle + 사진 + 버튼 이 들어가있는 패널을 컨트롤하는 스크립트</p>
<p>패널들의 내용들을 넣어주는 스크립트</p>
<pre><code class="language-csharp">using UnityEngine;

public class LevelSelectManager : MonoBehaviour
{
    public GameObject LevelPanelPrefab;
    public GameObject ScrollViewContent;

    private void Start()
    {
        for(int i = 0;i&lt;LevelManager.Instance.levels.Count;i++) 
        {
            LevelInfo Info = LevelManager.Instance.levels[i];
            GameObject go = Instantiate(LevelPanelPrefab, ScrollViewContent.transform);
            go.GetComponent&lt;LevelPanel&gt;().SetLevelInfomation(i, Info.LevelThum, Info.LevelName);
        }
    }
}</code></pre>
<p>  <img src="https://velog.velcdn.com/images/yj_621/post/8e65fe1b-0416-4ee6-a2b1-4b8307a2d11e/image.png" alt=""></p>
<p>  <strong>LevelSelectManager 설명</strong></p>
<pre><code class="language-csharp">    for(int i = 0;i&lt;LevelManager.Instance.levels.Count;i++)</code></pre>
<p>▲ Name, Thum, Prefab 각각 할당해준 것들(몇개 지정해주었는지)의 수까지 반복문을 돌린다.</p>
<pre><code class="language-csharp">        LevelInfo Info = LevelManager.Instance.levels[i];</code></pre>
<p>▲ class에 넣어준다.</p>
<pre><code class="language-csharp">         GameObject go = Instantiate(LevelPanelPrefab, ScrollViewContent.transform);</code></pre>
<p>▲ LevelPanelPrefab을 ScrollViewContent 자식에 clone해준다.</p>
<pre><code class="language-csharp">         go.GetComponent&lt;LevelPanel&gt;().SetLevelInfomation(i, Info.LevelThum, Info.LevelName);</code></pre>
<p>▲ LevelPanel스크립트의 SetLevelInfomation함수로 인자 전달 (LevelThum(이미지)와 Name)</p>
<p>LevelPanel.cs</p>
<pre><code class="language-csharp">public void SetLevelInfomation(int stageIndex, Sprite thumnail, string title)
{
    StageThum.sprite = thumnail;
    this.StageIndex = stageIndex;
    TextTitle.text = title;
}</code></pre>
<p>각각 전달받은 인자들 대입</p>
<h3 id="난이도에-따른-플레이-버튼-설정">난이도에 따른 플레이 버튼 설정</h3>
<pre><code class="language-csharp">using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

[Serializable]
public class LevelInfo
{
    public string LevelName;
    public Sprite LevelThum;
    public GameObject LevelPrefab;
}

public class LevelManager : MonoBehaviour
{
    public List&lt;LevelInfo&gt; levels;

    private static LevelManager instance;
    public GameObject SelectedPrefab;
    public static LevelManager Instance
    {
         get { return instance; }
        private set
        {
            instance = value;
        }
    }
...생략

    public void StartLevel(int index)
    {
        SelectedPrefab = levels[index].LevelPrefab;
        SceneManager.LoadScene(&quot;GameScene&quot;);
    }

}
</code></pre>
<p>GameManager.cs</p>
<pre><code class="language-csharp">    void Start()
    {
        Instantiate(LevelManager.Instance.SelectedPrefab);
        life = 3;
        **LifeDisplayerInstance.SetLives(life);**
    }
</code></pre>
<pre><code class="language-csharp">using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class LevelPanel : MonoBehaviour
{
    int StageIndex;
    public Image StageThum;
    public TextMeshProUGUI TextTitle;

    public void SetLevelInfomation(int stageIndex, Sprite thumnail, string title)
    {
        StageThum.sprite = thumnail;
        this.StageIndex = stageIndex;
        TextTitle.text = title;
    }

    public void StageStart()
    {
        LevelManager.Instance.StartLevel(StageIndex);
    }
}
</code></pre>
<p>StageStart를 Level Prefab의 버튼에 할당!
  <img src="https://velog.velcdn.com/images/yj_621/post/1a21516c-ce61-4883-8841-ecb35c36c8ac/image.png" alt="">
그럼 이제 버튼을 누르면 다른 난이도의 맵들을 볼 수 있답니다~</p>
<h3 id="빌드-세팅아이콘-이름-회사이름-등등">빌드 세팅(아이콘, 이름, 회사이름, 등등)</h3>
<p><img src="https://velog.velcdn.com/images/yj_621/post/e00237e7-d088-4d8d-a10a-1f7da40263ab/image.png" alt=""></p>
<p>게임 시작하자마자 나오는 화면</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/64a7e1e8-591c-4e1e-87fd-f82566cc73ab/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/a332f809-08f7-46dc-af0a-0c8c11593913/image.png" alt=""></p>
<p>끝~</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[멋쟁이사자처럼 부트캠프 TIL] 멋쟁이사자처럼 유니티게임스쿨 5일차]]></title>
            <link>https://velog.io/@yj_621/%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-TIL-%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-%EC%9C%A0%EB%8B%88%ED%8B%B0%EA%B2%8C%EC%9E%84%EC%8A%A4%EC%BF%A8-5%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@yj_621/%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-TIL-%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-%EC%9C%A0%EB%8B%88%ED%8B%B0%EA%B2%8C%EC%9E%84%EC%8A%A4%EC%BF%A8-5%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Mon, 07 Oct 2024 08:47:57 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>[SerializeField] : private으로 선언했지만 인스펙터에서 접근할 수 있도록 함</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/yj_621/post/05b91b30-e7da-401f-9c28-f881c25cc095/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/3ce43557-095f-4766-bcc9-537516e0d739/image.png" alt=""></p>
<pre><code class="language-csharp">    scoreLebel.text = GameManager.Instance.TimeLimit.ToString(&quot;#.##&quot;);</code></pre>
<p>▲ 소수점 아래 자리 제한 (#.##은 두번째자리까지 표시하겠다는 의미)</p>
<pre><code class="language-csharp">    private void OnEnable()
    {
        Time.timeScale = 0f;
        if (GameManager.Instance.IsCleared)
        {
            resultTitle.text = &quot;Clear!&quot;;
            scoreLabel.text = GameManager.Instance.TimeLimit.ToString(&quot;#.##&quot;);
            SaveHighScore();
        }
        else
        {
            resultTitle.text = &quot;Game Over&quot;;
            scoreLabel.text = &quot;&quot;;
        }
        scoreLabel.text = GameManager.Instance.TimeLimit.ToString(&quot;#.##&quot;);
    }

    public void TryAgainButton()
    {
        SceneManager.LoadScene(&quot;GameScene&quot;);
        Time.timeScale = 1f;
    }</code></pre>
<p>OnEnable 함수는 오브젝트가 켜질때 호출됨</p>
<blockquote>
<p>timeScale : 모든 것을 다 멈추게한다(애니메이션, 플레이어 등) deltatIme에도 영향을 받는다</p>
</blockquote>
<h3 id="게임-클리어">게임 클리어</h3>
<pre><code class="language-csharp">using TMPro;
using Unity.Cinemachine;
using UnityEngine;

public class GameManager : MonoBehaviour
{
..생략
    private bool isCleared;
    public bool IsCleared
    {
        get { return isCleared; }
    }

    void GameOver()
    {
        isCleared = false;
        popUpCanvas.SetActive(true);
    }
    public void GameClear()
    {
        isCleared = true;
        popUpCanvas.SetActive(true);
    }
}</code></pre>
<p>GameClear 함수로 popup을 띄워주고 isCleared의 bool값에 따라 text를 다르게 출력해준다.</p>
<pre><code class="language-csharp">public class PopUpCanvas : MonoBehaviour
{
    [SerializeField]
    private TMP_Text resultTitle;
    [SerializeField]
    private TMP_Text scoreLabel;
    [SerializeField]
    GameObject highScoreObject;

    private void OnEnable()
    {
        Time.timeScale = 0f;
        if (GameManager.Instance.IsCleared)
        {
            resultTitle.text = &quot;Clear!&quot;;
            scoreLabel.text = GameManager.Instance.TimeLimit.ToString(&quot;#.##&quot;);
            SaveHighScore();
        }
        else
        {
            resultTitle.text = &quot;Game Over&quot;;
            scoreLabel.text = &quot;&quot;;
        }
        scoreLabel.text = GameManager.Instance.TimeLimit.ToString(&quot;#.##&quot;);
    }

  }</code></pre>
<p>클리어했을 경우
<img src="https://velog.velcdn.com/images/yj_621/post/3bf7ed86-4074-449d-8fe7-baa5cf18b3f7/image.png" alt="">
게임 오버시
<img src="https://velog.velcdn.com/images/yj_621/post/b7af0fc7-27a5-4526-8a5f-c21def503893/image.png" alt=""></p>
<h3 id="최고기록">최고기록</h3>
<pre><code class="language-csharp">public class ResultCanvas : MonoBehaviour
{
 void SaveHighScore()
    {
        float highScore = PlayerPrefs.GetFloat(&quot;highscore&quot;, 0);

        if(GameManager.Instance.TimeLimit &gt; highScore)
        {
            highScoreObject.SetActive(true);
            PlayerPrefs.SetFloat(&quot;highscore&quot;, GameManager.Instance.TimeLimit);
            PlayerPrefs.Save();
        }
    }
 }</code></pre>
<p>highScoreObject가 활성화되면서 가장 높은 점수일때 표시된다</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/1c00cc77-8919-4cef-a237-6efa6893e813/image.png" alt=""></p>
<pre><code class="language-csharp">    string cureentScoreString = score.ToString(&quot;#.##&quot;);
    string savedScoreString = PlayerPrefs.GetString(&quot;HighScores&quot;, &quot;&quot;);

    if (savedScoreString == &quot;&quot;)
    {
        PlayerPrefs.SetString(&quot;HighScores&quot;, cureentScoreString);
    }
    else
    {
        string[] scoreArray = savedScoreString.Split(&#39;,&#39;);
    }
</code></pre>
<p>배열을 만들어서 , 로 분류한다.</p>
<h3 id="점수-저장법">점수 저장법</h3>
<pre><code class="language-csharp">public class ResultCanvas : MonoBehaviour
{
    [SerializeField]
    private TMP_Text resultTitle;
    [SerializeField]
    private TMP_Text scoreLabel;
    [SerializeField]
    GameObject highScoreObject;
    [SerializeField]
    GameObject highScoreCanvas;

    private void OnEnable()
    {
        Time.timeScale = 0f;
        if (GameManager.Instance.IsCleared)
        {
            resultTitle.text = &quot;Clear!&quot;;
            scoreLabel.text = GameManager.Instance.TimeLimit.ToString(&quot;#.##&quot;);
            SaveHighScore();
        }
        else
        {
            resultTitle.text = &quot;Game Over&quot;;
            scoreLabel.text = &quot;&quot;;
        }
        scoreLabel.text = GameManager.Instance.TimeLimit.ToString(&quot;#.##&quot;);
    }

    void SaveHighScore()
    {
        float score = GameManager.Instance.TimeLimit;
        float highScore = PlayerPrefs.GetFloat(&quot;highscore&quot;, 0);

        if (GameManager.Instance.TimeLimit &gt; highScore)
        {
            highScoreObject.SetActive(true);
            PlayerPrefs.SetFloat(&quot;highscore&quot;, score);
            PlayerPrefs.Save();
        }
        else
        {
            highScoreObject.SetActive(false);
        }

        string cureentScoreString = score.ToString(&quot;#.##&quot;);
        string savedScoreString = PlayerPrefs.GetString(&quot;HighScores&quot;, &quot;&quot;);

        if (savedScoreString == &quot;&quot;)
        {
            PlayerPrefs.SetString(&quot;HighScores&quot;, cureentScoreString);
        }
        else
        {
            string[] scoreArray = savedScoreString.Split(&#39;,&#39;);  
            List&lt;string&gt; scoreList = new List&lt;string&gt;(scoreArray);
            for(int i = 0; i&lt; scoreList.Count; i++) //적절한 위치에 새 스코어 넣기
            { 
                float savedScore = float.Parse(scoreList[i]); 
                if(savedScore &lt; score) // 나보다 낮은 점수가 들어온다면
                {
                    scoreList.Insert(i, cureentScoreString); // 뒤로 밀어버린다
                    break;
                }
            }
            if (scoreArray.Length == scoreList.Count) // 적절한 위치를 못 찾았다면 넌 꼴찌
            {
                scoreList.Add(cureentScoreString);
            }
            if(scoreList.Count&gt;10) // 10개 넘으면 맨 끝 빼기
            {
                scoreList.RemoveAt(10);
            }

            string result = string.Join(&quot;,&quot;, scoreList); // 리스트를 하나의 스트링으로 합치기
            Debug.Log(result);
            PlayerPrefs.SetString(&quot;HighScores&quot;, result);

        }

    }
    public void TryAgainButton()
    {
        SceneManager.LoadScene(&quot;GameScene&quot;);
        Time.timeScale = 1f;
    }

    public void Quit()
    {
        Application.Quit();
    }

    public void OnHighScoreCanvas()
    {
        highScoreCanvas.SetActive(true);
    }
}
</code></pre>
<blockquote>
<p>score : 현재 남은 시간
highScore : 저장된 highscore (저장된게 없으면 0)</p>
<pre><code class="language-csharp">    void SaveHighScore()
    {
        float score = GameManager.Instance.TimeLimit;
        float highScore = PlayerPrefs.GetFloat(&quot;highscore&quot;, 0);
</code></pre>
<p>현재 TimeLimit (남은시간)이 저장된 highScore보다 크면 
=(최고기록을 세우면)</p>
<p>highscore을 다시 저장함</p>
<pre><code class="language-csharp"> if (GameManager.Instance.TimeLimit &gt; highScore)
        {
            highScoreObject.SetActive(true);
            PlayerPrefs.SetFloat(&quot;highscore&quot;, score);
            PlayerPrefs.Save();
        }</code></pre>
<p>cureentScoreString  : 현재 점수 </p>
<p>savedScoreString  : 저장된 점수(처음에는 빈 스트링)</p>
<pre><code class="language-csharp">        string cureentScoreString = score.ToString(&quot;#.##&quot;);
        string savedScoreString = PlayerPrefs.GetString(&quot;HighScores&quot;, &quot;&quot;);</code></pre>
<p>저장된 점수가 빈 스트링이면 현재 스코어를 저장함</p>
<pre><code class="language-csharp">if (savedScoreString == &quot;&quot;)
        {
            PlayerPrefs.SetString(&quot;HighScores&quot;, cureentScoreString);
        }</code></pre>
<p>scoreList(string)을 float로 형변환해서  savedScore 변수에 넣어준다</p>
<p>비교해서 낮은 점수가 들어오면 적절한 위치에 넣거나 맨 끝에 넣음</p>
<pre><code class="language-csharp">float savedScore = float.Parse(scoreList[i]); 
if(savedScore &lt; score) // 나보다 낮은 점수가 들어온다면
 {
  scoreList.Insert(i, cureentScoreString); // 뒤로 밀어버린다
   break;
}  

if (scoreArray.Length == scoreList.Count) // 적절한 위치를 못 찾았다면 넌 꼴찌
  {
      scoreList.Add(cureentScoreString);
  }</code></pre>
<p>10개가 넘으면 맨 끝을 제거하고, result를 하나로 합친 후 저장</p>
<pre><code class="language-csharp">if(scoreList.Count&gt;10) // 10개 넘으면 맨 끝 빼기
{
    scoreList.RemoveAt(10);
}

string result = string.Join(&quot;,&quot;, scoreList); // 리스트를 하나의 스트링으로 합치기
Debug.Log(result);
PlayerPrefs.SetString(&quot;HighScores&quot;, result);</code></pre>
</blockquote>
<h3 id="최고-기록-나열">최고 기록 나열</h3>
<pre><code class="language-csharp">using TMPro;
using UnityEngine;

public class HighscoreCanvas : MonoBehaviour
{
    public TMP_Text ScoreLabel;

    private void OnEnable()
    {
        string[] scores = PlayerPrefs.GetString(&quot;HighScores&quot;, &quot;&quot;).Split(&#39;,&#39;);
        string result = &quot;&quot;;

        for(int i=0; i&lt;scores.Length; i++)
        {
            result += (i + 1) + &quot;. &quot; + scores[i] + &quot;\n&quot;;
        }
        ScoreLabel.text = result;
    }
    public void ClosedPopUp()
    {
        gameObject.SetActive(false);
    }
}
</code></pre>
<p>아래의 패널이 생기면 실행되는 함수로 저장된 최고기록들을 scores라는 배열에 저장한다.</p>
<p>최고기록들을 1. <del>~ 2. ~</del>로 출력하게 해준다.</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/0b24f8f5-119c-404b-a9d6-35a710577e62/image.png" alt=""></p>
<h3 id="총알-만들기">총알 만들기</h3>
<pre><code class="language-csharp">using UnityEngine;

public class Bullet : MonoBehaviour
{
    public Vector2 Velocity = new Vector2(10, 0); 
    void Start()
    {

    }
    private void FixedUpdate()
    {
        transform.Translate(Velocity * Time.fixedDeltaTime);
    }

    void Update()
    {

    }
    private void OnTriggerEnter2D(Collider2D collision)
    {
        if(collision.gameObject.tag == &quot;Terrain&quot;)
        {
            Destroy(gameObject);
        }
        else if (collision.gameObject.tag == &quot;Enemy&quot;)
        {
            Destroy(gameObject);
            collision.GetComponent&lt;EnemyController&gt;().Hit(1);
        }
    }
}
</code></pre>
<p>프리팹으로 만들어주고 isTrigger, rigd2D를 Kinematic으로 설정</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/af5eec03-f826-4648-9dba-e04fc5c3c831/image.png" alt=""></p>
<h3 id="적-체력--죽게-만들기">적 체력 + 죽게 만들기</h3>
<pre><code class="language-csharp">using UnityEngine;

public class EnemyController : MonoBehaviour
{
    public int Hp = 3;
      ...생략

    private void FixedUpdate()
    {
       // GetComponent&lt;Rigidbody2D&gt;().MovePosition
        transform.Translate(vx*Time.fixedDeltaTime);
    }

    public void Hit(int damage)
    {
        Hp -= damage;
        if(Hp &lt;= 0 )
        {
            GetComponent&lt;Rigidbody2D&gt;().constraints = RigidbodyConstraints2D.None; //Freeze OFF
            GetComponent&lt;Rigidbody2D&gt;().angularDamping = 720; //빙글빙글
            GetComponent&lt;Rigidbody2D&gt;().AddForce(new Vector2(0, 10), ForceMode2D.Impulse);//한번에 힘을 주기
            GetComponent&lt;BoxCollider2D&gt;().enabled = false;

            Invoke(&quot;DestroyThis&quot;, 2f);
        }
    }
    void DestroyThis()
    {
        Destroy(gameObject);
    }
}
</code></pre>
<p>플레이어가 Die하듯이 적도 똑같이 지정해준다.</p>
<h3 id="제한시간이-넘어가면-게임-종료하기">제한시간이 넘어가면 게임 종료하기</h3>
<pre><code class="language-csharp">using TMPro;
using Unity.Cinemachine;
using UnityEngine;

public class GameManager : MonoBehaviour
{
   ..생략
    void Update()
    {
        TimeLimit -= Time.deltaTime;
        TimeLimitLabel.text = &quot;Time Left &quot; + ((int)TimeLimit);
        if(TimeLimit&lt;0)
        {
            GameOver();
        }
    }</code></pre>
<p><code>if(TimeLimit&lt;0)</code></p>
<p>간단하게 이 조건으로 완료!</p>
<h3 id="instantiate-줄이기오브젝트-풀링">instantiate 줄이기(오브젝트 풀링)</h3>
<p>아래와 같이 게임 루프에 instantiate가 많으면 렉이 발생할 확률이 높아진다.
<img src="https://velog.velcdn.com/images/yj_621/post/3383e37c-4b0e-4d10-8b5b-cc27dda647df/image.png" alt=""></p>
<p>ObjectPool 스크립트 생성</p>
<pre><code class="language-csharp">public class ObjectPool : MonoBehaviour
{

    public GameObject Prefab;
    public int InitialObjectNumber = 30;

    List&lt;GameObject&gt; objs;

    private void Start()
    {
        objs = new List&lt;GameObject&gt;();

        for (int i = 0; i &lt; InitialObjectNumber; i++)
        {
            GameObject go = Instantiate(Prefab, transform); //부모 지정
            go.SetActive(false);
            objs.Add(go);
        }
    }
    public GameObject GetObject()
    {
        foreach (GameObject go in objs)
        {
            if (!go.activeSelf)
            {
                go.SetActive(true);
                return go;
            }
        }
        //준비해둔 30개를 다 쓰면 인스턴스로 생성 
        GameObject obj = Instantiate(Prefab, transform);
        objs.Add(obj);
        return obj;
    }
}</code></pre>
<blockquote>
<p>Instantiate :</p>
<pre><code class="language-csharp">GameObject Object.Instantiate&lt;GameObject&gt;GameObject original, Vector3 position, Quaternion rotation</code></pre>
<p>라고 사용</p>
</blockquote>
<p>objs에 go(bullet : Prefab으로 지정한 오브젝트) 추가</p>
<pre><code class="language-csharp">using UnityEngine;

public class Bullet : MonoBehaviour
{
    public Vector2 Velocity = new Vector2(10, 0); 
    void Start()
    {

    }
    private void FixedUpdate()
    {
        transform.Translate(Velocity * Time.fixedDeltaTime);
    }

    void Update()
    {
        if(!GetComponent&lt;SpriteRenderer&gt;().isVisible)
        {
            gameObject.SetActive(false);
        }
    }
    private void OnTriggerEnter2D(Collider2D collision)
    {
        if(collision.gameObject.tag == &quot;Terrain&quot;)
        {
            gameObject.SetActive(false);
        }
        else if (collision.gameObject.tag == &quot;Enemy&quot;)
        {
            gameObject.SetActive(false);
            collision.GetComponent&lt;EnemyController&gt;().Hit(1);
        }
    }
}
</code></pre>
<p><code>gameObject.SetActive(false);</code>으로 수정</p>
<p>화면 밖을 나가면 비활성화된다</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/3765315f-b503-4ec4-a820-93f16daeea82/image.png" alt=""></p>
<pre><code>    if(!GetComponent&lt;SpriteRenderer&gt;().isVisible)
    {
        gameObject.SetActive(false);
    }</code></pre><p>isVisible : 화면에 보이는지의 여부</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[멋쟁이사자처럼 부트캠프 TIL] 멋쟁이사자처럼 유니티게임스쿨 4일차]]></title>
            <link>https://velog.io/@yj_621/%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-TIL-%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-%EC%9C%A0%EB%8B%88%ED%8B%B0%EA%B2%8C%EC%9E%84%EC%8A%A4%EC%BF%A8-4%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@yj_621/%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-TIL-%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-%EC%9C%A0%EB%8B%88%ED%8B%B0%EA%B2%8C%EC%9E%84%EC%8A%A4%EC%BF%A8-4%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Fri, 04 Oct 2024 07:52:12 GMT</pubDate>
            <description><![CDATA[<h2 id="endpoint로-골인지점과-애니메이션-콜라이더-추가">EndPoint로 골인지점과 애니메이션, 콜라이더 추가</h2>
<p><img src="https://velog.velcdn.com/images/yj_621/post/32571835-71b2-4226-92f9-2833a2b4670f/image.png" alt=""></p>
<ul>
<li>Collider - isTrigger</li>
<li>Animation 추가</li>
<li>스크립트 추가</li>
</ul>
<pre><code class="language-csharp">public class EndPoint : MonoBehaviour
{

    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.gameObject.tag == &quot;Player&quot;)
        {
            Debug.Log(&quot;End&quot;);
        }
    }
}</code></pre>
<h2 id="itemmelon과-애니메이션-콜라이더-추가">Item(Melon)과 애니메이션, 콜라이더 추가</h2>
<p><img src="https://velog.velcdn.com/images/yj_621/post/e91fe26d-3e8f-4115-85f7-9b036da098a9/image.png" alt=""></p>
<ul>
<li>Collider - isTrigger</li>
<li>Animation 추가 (+Eaten)</li>
<li>스크립트 추가</li>
</ul>
<p>먹으면 먹는 애니메이션(Eaten)과 함께 과일이 사라지도록함
<img src="https://velog.velcdn.com/images/yj_621/post/f9c9a10b-05ca-41fd-b847-2d76cdcdbced/image.png" alt=""></p>
<pre><code class="language-csharp">public class Fruits : MonoBehaviour
{
    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.gameObject.tag == &quot;Player&quot;)
        {
            GetComponent&lt;Animator&gt;().SetTrigger(&quot;Eaten&quot;);
        }
    }
}</code></pre>
<h2 id="gamemanger-생성">Gamemanger 생성</h2>
<pre><code class="language-csharp">public class GameManager : MonoBehaviour
{
    private static GameManager instance;
    public static GameManager Instance
    {
        get { return instance; }
    }
    public TMP_Text timeLimitLabel;

    public float TimeLimit = 30;

    private void Awake()
    {
        instance = this;
    }

    void Update()
    {
        TimeLimit -= Time.deltaTime;
        timeLimitLabel.text = &quot;Time Left&quot; + ((int)TimeLimit);
    }
}
</code></pre>
<ul>
<li>남은시간 표시</li>
</ul>
<p><img src="https://velog.velcdn.com/images/yj_621/post/9f6c41c7-b0d3-469b-bc5e-378548460120/image.png" alt=""></p>
<ul>
<li>캔버스 추가</li>
<li>남은 시간 텍스트 추가</li>
</ul>
<p>아이템 먹고 사라지게 하기</p>
<pre><code class="language-csharp">public class Fruits : MonoBehaviour
{
    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.gameObject.tag == &quot;Player&quot;)
        {
            GetComponent&lt;Animator&gt;().SetTrigger(&quot;Eaten&quot;);
            GetComponent&lt;Collider2D&gt;().enabled = false;
            Invoke(&quot;DestroyThis&quot;, 0.6f);
        }
    }
    void DestroyThis()
    {
        Destroy(gameObject);
    }
}</code></pre>
<ul>
<li><code>Invoke(&quot;DestroyThis&quot;, 0.6f);</code> 없이도 원하는 시기에 함수 호출 가능</li>
</ul>
<p>Add event로 이벤트 추가 후 </p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/6282174f-9977-4529-af8d-bec70442c710/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/cc221495-1a77-423a-8199-578bbc78f8a5/image.png" alt="">
원하는 함수 입력 가능</p>
<h2 id="시간-늘리기">시간 늘리기</h2>
<pre><code class="language-csharp">public class GameManager : MonoBehaviour
{
    private static GameManager instance;
    public static GameManager Instance
    {
        get { return instance; }
    }
    public TMP_Text TimeLimitLabel;

    public float TimeLimit = 30;

    private void Awake()
    {
        instance = this;
    }

    void Start()
    {

    }


    void Update()
    {
        TimeLimit -= Time.deltaTime;
        TimeLimitLabel.text = &quot;Time Left &quot; + ((int)TimeLimit);
    }
    public void AddTime(float time)
    {
        TimeLimit += time;
    }
}
</code></pre>
<p>AddTime 함수를 통해 아이템을 먹으면 시간이 늘어나도록 설정</p>
<pre><code class="language-csharp">public class Fruits : MonoBehaviour
{
    public float TimeAdd = 5;
    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.gameObject.tag == &quot;Player&quot;)
        {
            GameManager.Instance.AddTime(TimeAdd);
            GetComponent&lt;Animator&gt;().SetTrigger(&quot;Eaten&quot;);
            GetComponent&lt;Collider2D&gt;().enabled = false;
        }
    }
    void DestroyThis()
    {
        Destroy(gameObject);
    }
}</code></pre>
<p><code>GameManager.Instance.AddTime(TimeAdd);</code> Fruits 스크립트에서 함수 호출</p>
<ul>
<li>아이템을 먹으면 5초씩 늘어남</li>
</ul>
<h2 id="프리팹-설정">프리팹 설정</h2>
<p><img src="https://velog.velcdn.com/images/yj_621/post/6e8a27d0-3052-4fe2-9890-9d1ee9e8865c/image.png" alt=""></p>
<blockquote>
<p>Revert All : 리셋(프리팹대로 돌리겠다)
Apply All : 적용(이 설정대로 프리팹을 수정하겠다)</p>
</blockquote>
<h2 id="죽었을때">죽었을때</h2>
<pre><code class="language-csharp">public class DeadZone : MonoBehaviour
{
    private void OnTriggerEnter2D(Collider2D collision)
    {
        if(collision.gameObject.tag == &quot;Player&quot;)
        {
            GameManager.Instance.Die();
            Debug.Log(&quot;Dead&quot;);
        }
    }
}
</code></pre>
<p>죽었는데도 플레이어를 움직이면 카메라가 움직이는 것 방지하기</p>
<pre><code class="language-csharp">public class GameManager : MonoBehaviour
{
    public GameObject CinemaCamera;

    private static GameManager instance;
    public static GameManager Instance
    {
        get { return instance; }
    }
    public TMP_Text TimeLimitLabel;

    public float TimeLimit = 30;

    private void Awake()
    {
        instance = this;
    }

    void Update()
    {
        TimeLimit -= Time.deltaTime;
        TimeLimitLabel.text = &quot;Time Left &quot; + ((int)TimeLimit);
    }
    public void AddTime(float time)
    {
        TimeLimit += time;
    }

    public void Die()
    {
        CinemaCamera.SetActive(false);
    }
}</code></pre>
<p><code>CinemaCamera.SetActive(false);</code> 로 시네마 카메라를 비활성화 해준다.</p>
<h2 id="콜라이더의-차이">콜라이더의 차이</h2>
<pre><code class="language-csharp">public Collider2D BottomCollider;
public CompositeCollider2D TerrainCollder;</code></pre>
<p><img src="https://velog.velcdn.com/images/yj_621/post/c3375bec-437d-48e8-bc69-0130a5ee5e35/image.png" alt=""></p>
<p>▲ 위에서 아래로 찾기 때문에 그냥 Collider2D 라고하면 Tilemap 콜라이더를 가져온다.
<img src="https://velog.velcdn.com/images/yj_621/post/2725cdb1-dc1a-45c5-83da-2617fae39097/image.png" alt=""></p>
<h2 id="목숨-ui">목숨 UI</h2>
<p><img src="https://velog.velcdn.com/images/yj_621/post/19967c0f-6f9f-4a24-8f2a-49d43cd7ee8e/image.png" alt=""></p>
<ul>
<li>horizontal layout group으로 가로로 정렬해준다</li>
</ul>
<pre><code class="language-csharp">public class LifeDisplayer : MonoBehaviour
{
    public List&lt;GameObject&gt; lifeImage;

    public void SetLives(int life)
    {
        for(int i = 0; i&lt;lifeImage.Count; i++)
        {
            if(i&lt;life)
            {
                lifeImage[i].SetActive(true);
            }
            else
            {
                lifeImage[i].SetActive(false);
            }
        }
    }
}
</code></pre>
<hr>
<p>🚨 오류가 생겼던 부분</p>
<p>이 코드에서 false ↔ true를 바꿔버림</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/e8c29027-c746-472c-8730-3e1689963212/image.png" alt=""></p>
<hr>
<pre><code class="language-csharp">public class GameManager : MonoBehaviour
{
    public GameObject CinemaCamera;

    private static GameManager instance;
    public static GameManager Instance
    {
        get { return instance; }
    }
    public TMP_Text TimeLimitLabel;

    public float TimeLimit = 30;

    private void Awake()
    {
        instance = this;
    }

    void Start()
    {
        life = 3;
    }


    void Update()
    {
        TimeLimit -= Time.deltaTime;
        TimeLimitLabel.text = &quot;Time Left &quot; + ((int)TimeLimit);
    }
    public void AddTime(float time)
    {
        TimeLimit += time;
    }

    public LifeDisplayer LifeDisplayerInstance;
    int life = 3;

    public void Die()
    {
        CinemaCamera.SetActive(false);
        life--;
        LifeDisplayerInstance.SetLives(life);
    }
}</code></pre>
<p>이제 떨어지면 dead존(아래 떨어지면 Collider)에 맞고 life가 하나 깎인다</p>
<h2 id="죽었을때-위치-초기화">죽었을때 위치 초기화</h2>
<pre><code class="language-csharp">public class PlayerController : MonoBehaviour
{
    public float Speed = 5;
    public float JumpSpeed = 5;
    public Collider2D BottomCollider;
    public CompositeCollider2D TerrainCollder;

    float prevVx = 0;
    float vx = 0;
    bool isGround;

    Vector2 originalPosition;

    public void Restart()
    {
        transform.position = originalPosition;
        GetComponent&lt;Rigidbody2D&gt;().linearVelocity = Vector2.zero;//떨어지고 있을때의 속도 초기화
    }
...생략

}
</code></pre>
<p><code>GetComponent&lt;Rigidbody2D&gt;().linearVelocity = Vector2.zero</code></p>
<p>떨어지는 속도를 초기화해준다.</p>
<pre><code class="language-csharp">public class GameManager : MonoBehaviour
{
    public GameObject CinemaCamera;
    public PlayerController Player;

    private static GameManager instance;
    public static GameManager Instance
    {
        get { return instance; }
    }
    public TMP_Text TimeLimitLabel;

    public float TimeLimit = 30;

    ..생략

    public LifeDisplayer LifeDisplayerInstance;
    int life = 3;

    public void Die()
    {
        CinemaCamera.SetActive(false);
        life--;
        LifeDisplayerInstance.SetLives(life);

        Invoke(&quot;Restart&quot;, 2);
    }

    void Restart()
    {
        if(life &gt; 0)
        {
            CinemaCamera.SetActive(true);
            Player.Restart();
        }
        else
        {
            GameOver();
        }
    }

    void GameOver()
    {
        Debug.Log(&quot;Game OVer&quot;);
    }
}</code></pre>
<p>Die,  Restart, GameOver 함수를 추가해준다.</p>
<h2 id="적-만들기">적 만들기</h2>
<p><img src="https://velog.velcdn.com/images/yj_621/post/04cd0739-1839-4d9e-81ce-d699386a2404/image.png" alt="">
<img src="https://velog.velcdn.com/images/yj_621/post/5801895b-0a55-4412-85b8-48dba5f0c920/image.png" alt=""></p>
<ul>
<li>sorting layer는 player로</li>
<li>Collider 추가</li>
<li>Animation 추가</li>
<li>스크립트 추가</li>
</ul>
<pre><code class="language-csharp">public class EnemyConteroller : MonoBehaviour
{
    public float Speed;
    Vector2 vx;

    void Start()
    {
        vx = Vector2.right * Speed;
    }

    private void FixedUpdate()
    {
       // GetComponent&lt;Rigidbody2D&gt;().MovePosition
        transform.Translate(vx*Time.fixedDeltaTime);
    }
}
</code></pre>
<p><code>transform.Translate(vx*Time.fixedDeltaTime);</code>
이 방법은 추천하진 않지만 이런 방법이 있다.</p>
<p>❓ 이유는</p>
<ul>
<li>순간이동하는 방식이라 속도가 빠르고 콜라이더가 얇으면 뚫을수도 있음</li>
<li>rigd로 사용하면 천천히 이동</li>
</ul>
<p>벽과 절벽등을 감지하는 collider를 만들어줌
<img src="https://velog.velcdn.com/images/yj_621/post/9e007aa9-e73f-426b-9242-fa9652168c4d/image.png" alt=""></p>
<blockquote>
<p>Front : 앞에 벽이 있는지</p>
<p>Front Bottom : 땅을 밟고 있는지</p>
</blockquote>
<pre><code class="language-csharp">void Update()
{
        if(FrontCollider.IsTouching(TerrainCollider) || !FrontBottomCollider.IsTouching(TerrainCollider)) //벽이 있거나 || (절벽이 있는 경우) 바닥이 없는 경우
    {
        vx = -vx; //좌우반전
       transform.localScale = new Vector2(-transform.localScale.x, 1);

    }
}
</code></pre>
<p>좌우반전</p>
<pre><code class="language-csharp">        transform.localScale = new Vector2(-transform.localScale.x, 1);</code></pre>
<p>적과 부딪히면 죽도록</p>
<pre><code class="language-csharp">    private void OnCollisionEnter2D(Collision2D collision)
    {
        if(collision.gameObject.tag == &quot;Enemy&quot;)
        {
            Die();
        }
    }

    void Die()
    {
        GameManager.Instance.Die();
    }</code></pre>
<pre><code class="language-csharp">using UnityEngine;

public class PlayerController : MonoBehaviour
{
    enum State{
        Playing,
        Dead
    }

//생략

    Vector2 originalPosition;
    State state;

    void Start()
    {
        originalPosition = transform.position;
        State state = State.Playing;
    }

    public void Restart()
    {
        //원상복구
        GetComponent&lt;Rigidbody2D&gt;().constraints = RigidbodyConstraints2D.FreezeRotation; 
        GetComponent&lt;Rigidbody2D&gt;().angularVelocity = 0; 
        GetComponent&lt;BoxCollider2D&gt;().enabled = true;

        transform.position = originalPosition;
        GetComponent&lt;Rigidbody2D&gt;().linearVelocity = Vector2.zero;//떨어지고 있을때의 속도 초기화
        state = State.Playing;
    }

   //생략

    private void OnCollisionEnter2D(Collision2D collision)
    {
        if(collision.gameObject.tag == &quot;Enemy&quot;)
        {
            Die();
        }
    }

    void Die()
    {
        state = State.Dead;
        GetComponent&lt;Rigidbody2D&gt;().constraints = RigidbodyConstraints2D.None; //Freeze OFF
        GetComponent&lt;Rigidbody2D&gt;().angularDamping = 720; //빙글빙글
        GetComponent&lt;Rigidbody2D&gt;().AddForce(new Vector2(0, 10), ForceMode2D.Impulse);//한번에 힘을 주기
        GetComponent&lt;BoxCollider2D&gt;().enabled = false;

        GameManager.Instance.Die();
    }
}
</code></pre>
<p>Rigidbody2D컴포넌트에서 여러 설정을 수정해서 </p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/1ba6cee4-dd43-4ef9-a5de-a99a7e1dbfa6/image.png" alt="">
▲ 이런식으로 죽게 만들기</p>
<pre><code class="language-csharp">enum State{
    Playing,
    Dead
}</code></pre>
<p>현재 상태를 나타내는 State를 만들어주고, </p>
<pre><code class="language-csharp">State state;

void Start()
{
    originalPosition = transform.position;
    State state = State.Playing;
}</code></pre>
<p>처음에는 Playing으로 초기화,</p>
<pre><code class="language-csharp">    void Die()
    {
        state = State.Dead;
        GetComponent&lt;Rigidbody2D&gt;().constraints = RigidbodyConstraints2D.None; //Freeze OFF
        GetComponent&lt;Rigidbody2D&gt;().angularDamping = 720; //빙글빙글
        GetComponent&lt;Rigidbody2D&gt;().AddForce(new Vector2(0, 10), ForceMode2D.Impulse);//한번에 힘을 주기
        GetComponent&lt;BoxCollider2D&gt;().enabled = false;

        GameManager.Instance.Die();
    }</code></pre>
<p>죽었을때는 Dead로 설정해주고, 캐릭터가 움직이지 못하고 떨어지는 효과를 주기 위해 </p>
<p>Rigdbody의 여러 상태들을 바꿔준다.</p>
<pre><code class="language-csharp">    public void Restart()
    {
        //원상복구
        GetComponent&lt;Rigidbody2D&gt;().constraints = RigidbodyConstraints2D.FreezeRotation; 
        GetComponent&lt;Rigidbody2D&gt;().angularVelocity = 0; 
        GetComponent&lt;BoxCollider2D&gt;().enabled = true;

        transform.position = originalPosition;
        GetComponent&lt;Rigidbody2D&gt;().linearVelocity = Vector2.zero;//떨어지고 있을때의 속도 초기화
        state = State.Playing;
    }</code></pre>
<p>Restart(재시작)이 될 경우에는 원상복구를 해준다.</p>
<p>게임 오버 팝업창을 만들어준다.</p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/3680152b-8449-4df5-9dd9-c1eec31435f1/image.png" alt=""></p>
<p>Sliced는 안 깨지고 크기가 늘어나게
<img src="https://velog.velcdn.com/images/yj_621/post/873d42a2-418f-42bc-b098-662b59e0fd1f/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[멋쟁이사자처럼 부트캠프 TIL] 멋쟁이사자처럼유니티게임스쿨 3일차]]></title>
            <link>https://velog.io/@yj_621/%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-TIL-%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC%EC%9C%A0%EB%8B%88%ED%8B%B0%EA%B2%8C%EC%9E%84%EC%8A%A4%EC%BF%A8-1%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@yj_621/%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC-%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-TIL-%EB%A9%8B%EC%9F%81%EC%9D%B4%EC%82%AC%EC%9E%90%EC%B2%98%EB%9F%BC%EC%9C%A0%EB%8B%88%ED%8B%B0%EA%B2%8C%EC%9E%84%EC%8A%A4%EC%BF%A8-1%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Mon, 30 Sep 2024 07:40:05 GMT</pubDate>
            <description><![CDATA[<h1 id="타일맵-만들기">타일맵 만들기</h1>
<p>사용 에셋</p>
<p><a href="https://assetstore.unity.com/packages/2d/characters/pixel-adventure-1-155360">Pixel Adventure 1</a></p>
<p><a href="https://anokolisa.itch.io/basic-140-tiles-grassland-and-mines">Free - Adventure Pack - Grassland</a></p>
<h2 id="tilemap-종류">TileMap 종류</h2>
<blockquote>
<p>rectangular : 사각형(기본)
hexagonal : 육각형
isometic : 2.5D? 메이플2 같은 뷰</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/yj_621/post/d3af49d1-1924-4a38-9ef1-7fc2523f6ed0/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yj_621/post/c0931a7c-cd7d-470f-98c2-5dfdae0b0e78/image.png" alt="">
▲ hexagonal
<img src="https://velog.velcdn.com/images/yj_621/post/0f9b898d-d175-4013-873d-3976363aab25/image.png" alt="">
▲ isometic</p>
<h2 id="tilemap-설정하기">TileMap 설정하기</h2>
<p>가져온 타일맵의 pixel per unit을 25로 하고 grid크기를 0.64(16/25)로 해준다
<img src="https://velog.velcdn.com/images/yj_621/post/59581480-56fa-48d0-96c5-c0587d0c9af9/image.png" alt="">
<img src="https://velog.velcdn.com/images/yj_621/post/147fc58e-6218-40aa-9866-83e2ec2be6fb/image.png" alt="">
pixel per unit을 16으로하고 grid크기를 1,1로 하는거랑 뭐가 다른지?</p>
<p>→ 한 영역에 담기는 그리드 개수가 달라짐. 25, 0.64로하면 더 많은 그리드가 생김</p>
<h3 id="콜라이더-설정">콜라이더 설정</h3>
<p>만든 Tile Map이 생성된 자식 오브젝트 이름을 Terrain로 설정한 후, composite collider 컴포넌트(+rigd가 동시에 생김)를 달아준다.</p>
<blockquote>
<p>*<em>composite collider *</em>
Tilemap Collider를 넣었을때 타일 하나하나가 Collider 적용이 되기 때문에 composite collider로 하나의 콜라이더로 통합해준다.
<img src="https://velog.velcdn.com/images/yj_621/post/5a3df26c-a71b-4280-b703-2f03ec6c94fe/image.png" alt="">
<img src="https://velog.velcdn.com/images/yj_621/post/78f24aa5-d3b6-487f-b23f-761201b64990/image.png" alt=""></p>
</blockquote>
<blockquote>
<p>타일 맵의 땅이 떨어지는걸 막기 위해 static으로 바꿔줌
<img src="https://velog.velcdn.com/images/yj_621/post/ddf8fa3c-f44a-4fd8-8515-012fd7198c9d/image.png" alt=""></p>
</blockquote>
<blockquote>
<p>Player도 rigd와 collider추가
<img src="https://velog.velcdn.com/images/yj_621/post/36b29634-4a04-4d41-9dfc-50c3063f997f/image.png" alt=""></p>
</blockquote>
<h2 id="player">Player</h2>
<blockquote>
<p>GetAxis : 부드러운 중간값도 출력
GetAxisRaw : 키를 누르자마자 반응</p>
</blockquote>
<h3 id="player-이동">Player 이동</h3>
<pre><code class="language-csharp">using UnityEngine;

public class PlayerController : MonoBehaviour
{
    public float Speed = 5;
    float vx = 0;
    void Start()
    {

    }

    void Update()
    {
        vx = Input.GetAxisRaw(&quot;Horizontal&quot;)*Speed;
        GetComponent&lt;Rigidbody2D&gt;().linearVelocity = new Vector2(vx,0);
    }
}
</code></pre>
<p>linearVelocity랑 velocity랑 같은 것</p>
<p>버전이 바뀌면서 velocity → linearVelocity로 바뀜</p>
<h3 id="player를-바라보는-방향으로-보게하기">Player를 바라보는 방향으로 보게하기</h3>
<pre><code class="language-csharp">using UnityEngine;

public class PlayerController : MonoBehaviour
{
    public float Speed = 5;
    float vx = 0;
    void Start()
    {

    }

    void Update()
    {
        vx = Input.GetAxisRaw(&quot;Horizontal&quot;)*Speed;

        if(vx&lt;0)
        {
            GetComponent&lt;SpriteRenderer&gt;().flipX = true;
        }
        if (vx &gt; 0)
        {
            GetComponent&lt;SpriteRenderer&gt;().flipX = false;
        }

        GetComponent&lt;Rigidbody2D&gt;().linearVelocity = new Vector2(vx,0);
    }
}
</code></pre>
<h3 id="player-점프-구현">Player 점프 구현</h3>
<pre><code class="language-csharp">using UnityEngine;

public class PlayerController : MonoBehaviour
{
    public float Speed = 5;
    public float JumpSpeed = 5;

    float vx = 0;

    void Start()
    {

    }

    void Update()
    {
        vx = Input.GetAxisRaw(&quot;Horizontal&quot;)*Speed;
        float vy = GetComponent&lt;Rigidbody2D&gt;().linearVelocityY;

        if(vx&lt;0)
        {
            GetComponent&lt;SpriteRenderer&gt;().flipX = true;
        }
        if (vx &gt; 0)
        {
            GetComponent&lt;SpriteRenderer&gt;().flipX = false;
        }
        if(Input.GetButtonDown(&quot;Jump&quot;))
        {
            vy = JumpSpeed;
        }

        GetComponent&lt;Rigidbody2D&gt;().linearVelocity = new Vector2(vx,vy);
    }
}</code></pre>
<p><code>float vy = GetComponent&lt;Rigidbody2D&gt;().linearVelocityY;</code>
위 Y 값을 주고 <code>GetComponent&lt;Rigidbody2D&gt;().linearVelocity = new Vector2(vx,vy);</code> 로 수정해준다
<img src="https://velog.velcdn.com/images/yj_621/post/f1f4324e-88c2-4cb3-905d-068ffb41a7b6/image.png" alt=""></p>
<blockquote>
<p>Discrete : 빠른 계산
Continuous : 정밀한 계산</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/yj_621/post/d1785559-df2d-4e1b-86ed-20cbd21c82aa/image.png" alt=""></p>
<h3 id="player가-벽타는-현상마찰력-줄이기">Player가 벽타는 현상(마찰력 줄이기)</h3>
<p><img src="https://velog.velcdn.com/images/yj_621/post/91df8065-0e7f-4e59-95b6-9aff4e42d4e2/image.png" alt="">
PhysicsMaterial 컴포넌트를 만들고 Player의 Rigidbody에 붙여주면 벽에 붙는 현상이 없어진다.
<img src="https://velog.velcdn.com/images/yj_621/post/b702dc2c-87af-448a-9607-2a2f4204560c/image.png" alt=""></p>
<p>Discrete로 설정하면 높이 뛰었다가 내려올때 collider를 뚫고 내려갈 수 있기 때문에 Continuous로 바꿔준다
<img src="https://velog.velcdn.com/images/yj_621/post/3dc25e7b-b17e-46b7-9a0e-09d9bb5e4463/image.png" alt=""></p>
<h2 id="무한-점프-방지">무한 점프 방지</h2>
<pre><code class="language-csharp">using UnityEngine;

public class PlayerController : MonoBehaviour
{
    public float Speed = 5;
    public float JumpSpeed = 5;
    public Collider2D BottomCollider;
    public CompositeCollider2D TerrainCollder;

    float vx = 0;

    void Start()
    {

    }

    void Update()
    {
        if(BottomCollider.IsTouching(TerrainCollder))
        {
            Debug.Log(&quot;Grounded&quot;);
        }
        else
        {
            Debug.Log(&quot;NOT&quot;);
        }

        vx = Input.GetAxisRaw(&quot;Horizontal&quot;)*Speed;
        float vy = GetComponent&lt;Rigidbody2D&gt;().linearVelocityY;

        if(vx&lt;0)
        {
            GetComponent&lt;SpriteRenderer&gt;().flipX = true;
        }
        if (vx &gt; 0)
        {
            GetComponent&lt;SpriteRenderer&gt;().flipX = false;
        }
        if(Input.GetButtonDown(&quot;Jump&quot;))
        {
            vy = JumpSpeed;
        }

        GetComponent&lt;Rigidbody2D&gt;().linearVelocity = new Vector2(vx,vy);
    }

}
</code></pre>
<p>   <code>public Collider2D BottomCollider;
    public CompositeCollider2D TerrainCollder;</code> </p>
<p>콜라이더를 선언하고 넣어준 후,   <code>if(BottomCollider.IsTouching(TerrainCollder))</code>로 </p>
<blockquote>
<p>IsTouching :  2D 물리 시스템에서 <code>Collider2D</code> 컴포넌트 간의 충돌 여부를 확인할 때 사용
나의 콜라이더.IsTouching(닿을 콜라이더)
로 사용한다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/yj_621/post/9648cea7-d7bf-42cb-881a-497769b343c4/image.png" alt=""></p>
<pre><code class="language-csharp">
        if (BottomCollider.IsTouching(TerrainCollder))
        {
            isGround = true;
        }
        else
        {
            isGround = false;
        }</code></pre>
<p>이렇게 한줄로 요약 가능</p>
<pre><code class="language-csharp">isGround = BottomCollider.IsTouching(TerrainCollder);</code></pre>
<pre><code class="language-csharp">using UnityEngine;

public class PlayerController : MonoBehaviour
{
    public float Speed = 5;
    public float JumpSpeed = 5;
    public Collider2D BottomCollider;
    public CompositeCollider2D TerrainCollder;

    float vx = 0;
    bool isGround;

    void Start()
    {

    }

    void Update()
    {
        vx = Input.GetAxisRaw(&quot;Horizontal&quot;)*Speed;
        float vy = GetComponent&lt;Rigidbody2D&gt;().linearVelocityY;

        if(vx&lt;0)
        {
            GetComponent&lt;SpriteRenderer&gt;().flipX = true;
        }
        if (vx &gt; 0)
        {
            GetComponent&lt;SpriteRenderer&gt;().flipX = false;
        }

        isGround = BottomCollider.IsTouching(TerrainCollder);

        if (Input.GetButtonDown(&quot;Jump&quot;) &amp;&amp; isGround == true)
        {
            vy = JumpSpeed;
        }

        GetComponent&lt;Rigidbody2D&gt;().linearVelocity = new Vector2(vx,vy);
    }

}
</code></pre>
<h2 id="카메라가-따라오도록-하기">카메라가 따라오도록 하기</h2>
<p>시네머신 카메라의 활용 : 카트라이더에 뒤에서 비추는 카메라 구현도 가능</p>
<p>Unity Registry에서 시네머신 import
cinemachine camera 추가 후, follow 컴포넌트와 target을 player로 지정하면 따라오게 한다
<img src="https://velog.velcdn.com/images/yj_621/post/95655b00-b435-43df-a6ef-6a87f30debd9/image.png" alt="">
Border(화면 밖으로 나가지 못하게하는 경계선)을 collider로 만들고, 시네마 카메라에 confiner 2D를 추가한 후 Border을 할당해준다
<img src="https://velog.velcdn.com/images/yj_621/post/8c69322e-6c6c-4da6-a1c3-f717ab54d1d7/image.png" alt="">
BoxCollider은 꼭짓점 정보를 안 줌 그래서 Edge Collider로 사용</p>
<p>🚨 보더 크기가 카메라보다 훨씬 커야 오류가 안 뜸(작으면 플레이어를 안 따라옴)</p>
<h2 id="player-애니메이션">Player 애니메이션</h2>
<p><img src="https://velog.velcdn.com/images/yj_621/post/abce5a09-5f7c-4186-ad37-f5cc21f5d724/image.png" alt="">
2D에는 다 0,0 으로 세팅</p>
<p>다른 애들도 다 Can Transition To Self를 켜준다</p>
<pre><code class="language-csharp">using UnityEngine;

public class PlayerController : MonoBehaviour
{
    public float Speed = 5;
    public float JumpSpeed = 5;
    public Collider2D BottomCollider;
    public CompositeCollider2D TerrainCollder;

    float prevVx = 0;
    float vx = 0;
    bool isGround;


    void Start()
    {

    }

    void Update()
    {
        vx = Input.GetAxisRaw(&quot;Horizontal&quot;)*Speed;
        float vy = GetComponent&lt;Rigidbody2D&gt;().linearVelocityY;

        if(vx&lt;0)
        {
            GetComponent&lt;SpriteRenderer&gt;().flipX = true;
        }
        if (vx &gt; 0)
        {
            GetComponent&lt;SpriteRenderer&gt;().flipX = false;
        }

        if (BottomCollider.IsTouching(TerrainCollder))
        {
            if (!isGround)
            {
                if(vx == 0) //속도가 0일때, 가만히 있는다면
                {
                    GetComponent&lt;Animator&gt;().SetTrigger(&quot;Idle&quot;);
                }
                else
                {
                    GetComponent&lt;Animator&gt;().SetTrigger(&quot;Run&quot;);
                }
            }
            else
            {
                if (vx != prevVx)
                {
                    if (vx == 0)
                    {
                        GetComponent&lt;Animator&gt;().SetTrigger(&quot;Idle&quot;);
                    }
                    else
                    {
                        GetComponent&lt;Animator&gt;().SetTrigger(&quot;Run&quot;);
                    }
                }
            }

        }
        else
        {
            if(isGround)
            {
                GetComponent&lt;Animator&gt;().SetTrigger(&quot;Jump&quot;);
            }
        }

        isGround = BottomCollider.IsTouching(TerrainCollder); //콜라이더들이 부딪히면 true를 isGround에 

        if (Input.GetButtonDown(&quot;Jump&quot;) &amp;&amp; isGround == true)
        {
            vy = JumpSpeed;
        }

        GetComponent&lt;Rigidbody2D&gt;().linearVelocity = new Vector2(vx,vy);
    }

}
</code></pre>
<p>각 조건마다 애니메이션을 다르게 실행하도록 해준다</p>
]]></description>
        </item>
    </channel>
</rss>