<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>ju_hong.log</title>
        <link>https://velog.io/</link>
        <description>카페러버의 게임개발 도전기</description>
        <lastBuildDate>Mon, 02 Sep 2024 16:13:48 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>ju_hong.log</title>
            <url>https://velog.velcdn.com/images/ju_hong/profile/50ebd83d-e8f1-4982-805b-934d1d741923/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. ju_hong.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/ju_hong" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[리사이클 스크롤뷰]]></title>
            <link>https://velog.io/@ju_hong/%EB%A6%AC%EC%82%AC%EC%9D%B4%ED%81%B4-%EC%8A%A4%ED%81%AC%EB%A1%A4%EB%B7%B0</link>
            <guid>https://velog.io/@ju_hong/%EB%A6%AC%EC%82%AC%EC%9D%B4%ED%81%B4-%EC%8A%A4%ED%81%AC%EB%A1%A4%EB%B7%B0</guid>
            <pubDate>Mon, 02 Sep 2024 16:13:48 GMT</pubDate>
            <description><![CDATA[<h3 id="단순한-복기-공부용-문서-입니다">단순한 복기, 공부용 문서 입니다.</h3>
<h3 id="틀린-내용이-있을-수-있습니다">틀린 내용이 있을 수 있습니다.</h3>
<p><img src="https://velog.velcdn.com/images/ju_hong/post/2a12ca79-1fa2-42d8-81f8-ab64d415054a/image.png" alt="">_</p>
<blockquote>
<p>스크롤뷰가 뭔데?</p>
</blockquote>
<p>말 그대로 스크롤을 진행하여 오브젝트들을 볼 수 있는 유니티내의 컴포넌트 중 하나이다.
유니티를 제외하고도 많은 앱에서 사용하는 기능이다.</p>
<h3 id="유니티에서-제공해주는-컴포넌트면-그냥-사용하면-되는게-아닌가">유니티에서 제공해주는 컴포넌트면 그냥 사용하면 되는게 아닌가?</h3>
<p>맞다. 유니티에서 이미 제공해주는 컴포넌트도 이미 완성형이고 오브젝트만 추가해서 사용하면 된다.</p>
<p>하지만 스크롤뷰에서 많은 오브젝트를 사용하면 말이 달라진다.
유니티에서는 스크롤뷰에서 보이지 않는 오브젝트들도 렌더링을 진행하며 많은 오브젝트들을 사용하여 메모리,CPU에 부담이 가기 시작한다.
이를 보안해서 사용하는게 리사이클뷰이다.</p>
<h1 id="코드">코드</h1>
<pre><code>using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class ReusableScrollList : MonoBehaviour
{
    [SerializeField] private ScrollRect _scrollRect; // ScrollRect 컴포넌트
    [SerializeField] private GameObject _slotPrefab; // 아이템 프리팹
    [SerializeField] private Transform _content; // Content 트랜스폼
    [SerializeField] private int _itemCount = 3; // 아이템 개수
    [SerializeField] private int _bufferItems = 2; // 좌,우측에 끊김없이 보이기 위한 추가 아이템 개수 

    public LinkedList&lt;GameObject&gt; _itemList = new LinkedList&lt;GameObject&gt;();
    private RectTransform _contentRect;
    public float _itemWidth;
    public int _tmpfirstVisibleIndex;//임시 첫번째 인덱스
    public int _poolSize;//오브젝트의 총 개수(가로길이 계산 포함)

    public int padding = 0;  // 아이템 간 간격
    public int left = 0;     // 왼쪽 여백
    public int right = 0;    // 오른쪽 여백


    private void Start()
    {
        _contentRect = _content.GetComponent&lt;RectTransform&gt;();

        // 슬롯 너비
        _itemWidth = _slotPrefab.GetComponent&lt;RectTransform&gt;().rect.width;

        // content sizeDelta.x 크기 (컨텐츠의 총 길이는 아이템의 개수 + 좌/우 여백)
        float contentWidth = (_itemWidth * _itemCount) + left + right + padding * (_itemCount - 1);

        // 스크롤렉트 가로 길이, 추가 슬롯을 고려한 poolSize 계산
        _poolSize = (int)(_scrollRect.GetComponent&lt;RectTransform&gt;().sizeDelta.x / (_itemWidth + padding)) + _bufferItems * 2;

        // content 사이즈 조절 (여백 포함)
        _contentRect.sizeDelta = new Vector2(contentWidth, _contentRect.sizeDelta.y);

        // 슬롯 생성 및 리스트에 추가
        for (int i = 0; i &lt; _poolSize; i++)
        {
            GameObject item = Instantiate(_slotPrefab, _content);
            _itemList.AddLast(item);
        }

        // 슬롯 초기 설정
        SlotInit();

        // ScrollRect 자식의 위치가 변경되면 실행되도록 이벤트 등록
        _scrollRect.onValueChanged.AddListener(OnScroll);


    }

    /// &lt;summary&gt;슬롯의 위치를 변경하는 함수&lt;/summary&gt;
    private void OnScroll(Vector2 scrollPosition)
    {


        float contentX = _contentRect.anchoredPosition.x;

        // 현재 contentRect의 x값에 맞춰 최소 인덱스를 구한다.
        int firstVisibleIndex = Mathf.Max(0, (-1 * Mathf.FloorToInt((contentX - left) / (_itemWidth + padding))) - _bufferItems);

        // 인덱스 차이가 너무 큰 경우 방어 로직 추가
        if (Mathf.Abs(_tmpfirstVisibleIndex - firstVisibleIndex) &gt; _poolSize)
        {
            // 큰 변화가 발생한 경우 슬롯들을 리셋
            _tmpfirstVisibleIndex = firstVisibleIndex;
            SlotInit();
            return;
        }

        // 만약 이전 위치와 현재 위치가 달라졌다면?
        if (_tmpfirstVisibleIndex != firstVisibleIndex)
        {
            // 위치 인덱스의 차이를 구한다.
            int diffIndex = _tmpfirstVisibleIndex - firstVisibleIndex;

            // 현재 인덱스가 더 크다면 (스크롤이 오른쪽으로 이동)
            while (diffIndex &lt; 0)
            {
                // 우측 이동
                GameObject item = _itemList.First.Value;
                _itemList.RemoveFirst();
                _itemList.AddLast(item);

                // 해당 아이템의 새로운 위치 계산 (left 여백을 더해준다)
                int newIndex = firstVisibleIndex + _poolSize - 1;
                item.transform.localPosition = new Vector3(left + newIndex * (_itemWidth + padding), 0, 0);

                diffIndex++;
            }

            // 이전 인덱스가 더 크다면 (스크롤이 왼쪽으로 이동)
            while (diffIndex &gt; 0)
            {
                // 좌측 이동
                GameObject item = _itemList.Last.Value;
                _itemList.RemoveLast();
                _itemList.AddFirst(item);

                // 해당 아이템의 새로운 위치 계산 (left 여백을 더해준다)
                int newIndex = firstVisibleIndex;
                item.transform.localPosition = new Vector3(left + newIndex * (_itemWidth + padding), 0, 0);

                diffIndex--;
            }

            // 위치 인덱스를 갱신한다.
            _tmpfirstVisibleIndex = firstVisibleIndex;
        }
    }

    /// &lt;summary&gt;슬롯 초기 설정&lt;/summary&gt;
    private void SlotInit()
    {
        int i = 0;
        foreach (GameObject item in _itemList)
        {
            // 각 슬롯의 위치 설정 (left 여백 포함)
            item.transform.localPosition = new Vector3(left + i * (_itemWidth + padding), 0, 0);
            item.GetComponentInChildren&lt;Text&gt;().text = (i + 1).ToString();
            i++;
        }
    }
}</code></pre><p>위에 코드를 적용한 결과</p>
<p><img src="https://velog.velcdn.com/images/ju_hong/post/87a0b6b6-60fa-4a06-a8df-1fde8353db35/image.png" alt="">
<img src="https://velog.velcdn.com/images/ju_hong/post/26905aae-ae5f-43ec-8049-2ba40edda88d/image.png" alt=""></p>
<p>스크롤을 진행할시 재사용이 잘 진행되는 것을 확인할 수 있다. </p>
<blockquote>
<p>회고</p>
</blockquote>
<p>기능을 완성하고나면 아 이렇게 하면 쉬웠는데 왜 이 부분을 놓쳤을까 라는 아쉬움이 있다.
또 혼자만의 힘이 아니라 GPT를 이용해서 구현을 했다는 점이 많이 아쉬운 부분이다..
어떤 부분이 문제였을까?라고 생각을 하면서 퇴근을 했다.</p>
<p>결론은 내 코딩의 과정중에 있었다.
어 여기 넣으면 되지 않을까?라는 생각으로 때려넣는 정확한 이유가 없는 코딩을 진행하다보니 안그래도 약한 나의 멘탈도 금방 갈리고 나도 모르게 자신감이 사라지는 것 같다.
GPT사용을 최소화 하여 진행해야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[오브젝트 포물선 그리기]]></title>
            <link>https://velog.io/@ju_hong/%EC%98%A4%EB%B8%8C%EC%A0%9D%ED%8A%B8-%ED%8F%AC%EB%AC%BC%EC%84%A0-%EA%B7%B8%EB%A6%AC%EA%B8%B0</link>
            <guid>https://velog.io/@ju_hong/%EC%98%A4%EB%B8%8C%EC%A0%9D%ED%8A%B8-%ED%8F%AC%EB%AC%BC%EC%84%A0-%EA%B7%B8%EB%A6%AC%EA%B8%B0</guid>
            <pubDate>Thu, 22 Aug 2024 10:23:27 GMT</pubDate>
            <description><![CDATA[<h1 id="시작">시작</h1>
<p>우리는 게임을 플레이하다가 보면 종종 화살이나 폭탄이 포물선을 그리며 날라가는 장면을 심심치않게 확인할 수 있다. 
나도 언젠가는 구현을 해봐야겠다! 라는 생각을 가졌으나 세시간의 씨름 끝에 결국 쥐선생을 사용하여 구현완료 :)
구현으로 끝나는건 맞지 않으니 로직을 분석해서 해보자</p>
<h3 id="로직-목적">로직 목적</h3>
<blockquote>
<p>현재 생존하고있는 몬스터들 중에 가장 군집하고 있는 위치를 찾아 해당 포지션에 포물선을 그려 폭탄을 던진다.</p>
</blockquote>
<p>그렇다면 순서는
1.군집되어 있는 위치를 찾아 할당
2.해당 위치에 폭탄이 이동하는 로직
3.Physics를 통하여 데미지를 가하는 로직 
크게 이러한 플로우로 되어 있다.</p>
<blockquote>
<pre><code> private void OnEnable()
    {
        if (SpawnManager.instance.alive_monsters.Count &gt; 0)
        {
            Vector3 bestPosition = Vector3.zero; // 몬스터가 가장 많이 몰려 있는 곳의 위치
            float bestDensity = 0f; // 가장 많은 밀집도를 가진 구역의 밀집도</code></pre></blockquote>
<pre><code>        float searchRadius = 7f; // 플레이어 주변의 탐색 반경
        float gridCellSize = 2f; // 밀집도를 계산할 그리드 셀 크기

        Dictionary&lt;Vector3, int&gt; densityMap = new Dictionary&lt;Vector3, int&gt;(); // 위치별 몬스터 밀집도

        foreach (var monster in SpawnManager.instance.alive_monsters)
        {
            // 플레이어와 몬스터 간의 거리 계산
            float playerToMonsterDistance = Vector3.Distance(DB.instance.player.transform.position, monster.transform.position);

            // 플레이어와의 거리가 7 미만인 몬스터만 고려
            if (playerToMonsterDistance &lt; searchRadius)
            {
                // OnAi가 활성화된 몬스터만 고려
                if (!monster.OnAi || DB.instance.player.target == monster)
                {
                    continue; // OnAi가 활성화되지 않은 경우 건너뛰기
                }

                // 그리드 셀 위치 계산 (정규화된 위치)
                Vector3 gridPosition = new Vector3(
                    Mathf.Floor(monster.transform.position.x / gridCellSize) * gridCellSize,
                    Mathf.Floor(monster.transform.position.y / gridCellSize) * gridCellSize,
                    Mathf.Floor(monster.transform.position.z / gridCellSize) * gridCellSize
                );

                // 해당 그리드 셀에 몬스터 추가 (밀집도 계산)
                if (densityMap.ContainsKey(gridPosition))
                {
                    densityMap[gridPosition]++;
                }
                else
                {
                    densityMap[gridPosition] = 1;
                }

                // 가장 밀집된 구역을 업데이트
                if (densityMap[gridPosition] &gt; bestDensity)
                {
                    bestDensity = densityMap[gridPosition];
                    bestPosition = gridPosition;
                }
            }
        }

        // 가장 밀집된 구역의 중심 위치를 target에 할당
        if (bestDensity &gt; 0)
        {
            target = bestPosition; // target은 가장 많이 몰린 곳의 중심 위치
        }
    }

    StartMoveToTarget(target);
}</code></pre><p>기존에 내가 작성했던 코드와 쥐선생의 힘이 합쳐진 로직이다.
확인하면 몬스터가 가장 많이 몰려있는 곳을 저장하기 위한 변수 bestPosition이 가장 메인이다. </p>
<p>큰틀로 보면 소환되어 있는 몬스터에서 카메라 내의 범위인 7을 제한하고
밀집도를 계산하기 위한 Dicitionary를 Vector3타입으로 선언한다.
또한 카운팅을 하기 위해 int형으로 값을 할당할 수 있도록 하고.
상대의 위치를 계산할때 그리드를 이용해 계산한다. 여기서 그리드는 3D 공간에서 일정한 크기로 나누는것을 이야기한다.
우리는 이 3D공간의 일정한 크기인 그리드를 디셔너리에 저장하여 가장 많이 저장된 포지션을 사용할 것이다. 
이렇게보면 되게 간단한 로직이지만 아직 내 머릿속에서는 쉽지 않았나보다ㅠㅠ..</p>
<h3 id="오브젝트-포물선으로-움직이기">오브젝트 포물선으로 움직이기</h3>
<pre><code>IEnumerator MoveToPositionParabola(Vector3 startPosition, Vector3 targetPosition, float duration, float height)
{
    float elapsed = 0f;

    // 초기 위치 및 타겟 위치의 X 정보만 사용
    float startX = startPosition.x;
    float targetX = targetPosition.x;

    while (elapsed &lt; duration)
    {
        // 경과 시간 비율
        float t = elapsed / duration;

        // X 축에서 선형 이동
        float currentX = Mathf.Lerp(startX, targetX, t);

        // 포물선 형식으로 Z 좌표 계산 (포물선 공식 사용)
        float currentZ = Mathf.Lerp(startPosition.z, targetPosition.z, t);
        float parabolaZOffset = Mathf.Sin(t * Mathf.PI) * height; // 포물선 높이 계산

        // 현재 위치 업데이트 (X는 선형, Z는 포물선)
        transform.position = new Vector3(currentX, transform.position.y, currentZ + parabolaZOffset);

        // 시간 증가
        elapsed += Time.deltaTime;
        yield return null;
    }

    // 도착 위치를 마지막으로 설정 (정확히 타겟에 도달하도록)
    transform.position = new Vector3(targetPosition.x, transform.position.y, targetPosition.z);
}
</code></pre><p>여기서 우리는 <strong>선형보간</strong>에 대해서 먼저 알아야한다.
<strong>선형보간</strong>에서 선형은 Line을 의미하고 보간은 두점을 연결하는 방법이다.
<strong>Unity</strong> 에서는 선형보간을 제공해주는 메소드가 있는데 
바로 Mathf.Lerp다.
첫번째 인자로 시작위치 두번째 인자로 도착 위치를 넣어주고 세번째는 0~1의 값을 넣어 보간이 진행되도록 한다. 
그렇게 현재 X와Z축을 움직여 포물선을 그려야 하는 나는
지속된 시간/목표 시간 으로 현재 선형의 위치와 포지션을 넣어 해당 오브젝트가 이동하도록 구현했다.</p>
<p>그리고 수포자였던 나에게 가장 어려운 Mathf.Sin..
과거 취준생일때 3D 프로젝트를 처음 접했을때 사용해본 삼각함수이다.
삼각함수는 Cos,Sin,tan가 있다. 일단 여기서 Sin은 높이를 구하는 공식이다.
또한 Sin의 함수는 2π로 이루어져 있는데 우리는 포물선을 사용할거니 π만 사용한다고 보면 된다. 
여기서 Mathf.PI는 반원을 그리고 있으니 우리는 0~1값인 t를 곱해 포물선 높이를 구할 수 있다.
여기서 우리는 포물선을 Z축에서 그려나갈 것이므로 Z축에 선형보간된 벡터 + 삼각함수 Sin을 통해서 계산된 벡터를 더해 포물선을 그리는 것 처럼 표현할 수 있다.</p>
<p>지금 적으면서 느꼈는데 어느정도는 내가 작성할 수 있는 코드였던 것 같다. 하지만 내가 고민한다면 많은 시간을 소요할텐데 시간대비 과연 효율이 있을까 라는 의구심이 들기도 한다.
맥시멈 시간을 잡고 개발을 진행하다가 안되면 GPT를 이용하는 방법으로 진행해야겠다. </p>
]]></description>
        </item>
    </channel>
</rss>