<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>code_null.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Thu, 30 May 2024 15:30:30 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>code_null.log</title>
            <url>https://velog.velcdn.com/images/code_null/profile/d6cec6cc-97e1-434a-9bf0-abb4fcfe3230/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. code_null.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/code_null" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[개념] 공간의 수학]]></title>
            <link>https://velog.io/@code_null/%EA%B0%9C%EB%85%90-%EA%B3%B5%EA%B0%84%EC%9D%98-%EC%88%98%ED%95%99</link>
            <guid>https://velog.io/@code_null/%EA%B0%9C%EB%85%90-%EA%B3%B5%EA%B0%84%EC%9D%98-%EC%88%98%ED%95%99</guid>
            <pubDate>Thu, 30 May 2024 15:30:30 GMT</pubDate>
            <description><![CDATA[<h1 id="벡터-공간">벡터 공간</h1>
<p>게임을 사용하는 가상 공간의 본질</p>
<hr>
<h2 id="벡터와-스칼라">벡터와 스칼라</h2>
<h4 id="물리학에서의-정의">물리학에서의 정의</h4>
<ul>
<li>벡터: 크기와 방향을 가진 대상</li>
<li>스칼라: 크기만 있는 물리량</li>
</ul>
<h4 id="수학에서의-정의">수학에서의 정의</h4>
<ul>
<li>벡터: 벡터 공간의 원소</li>
<li>스칼라: 체 집합의 원소</li>
</ul>
<hr>
<h2 id="선형-변환-linear-trasformation">선형 변환 (Linear Trasformation)</h2>
<p>선형성을 가진 변환.</p>
<blockquote>
<p>선형 변환 과정을 통해 빠르게 공간을 변형하고, 변환된 공간도 원래대로 돌리는게 가능해진다.</p>
</blockquote>
<hr>
<h2 id="렌더링-파이프-라인">렌더링 파이프 라인</h2>
<p>모델링을 카메라가 바라보고, 바라본 공간을 모니터에 투영해주는 일련의 공간 변환 체제</p>
<p><img src="https://velog.velcdn.com/images/code_null/post/77808233-72b6-4f3b-822e-9e99a61a8072/image.png" alt=""></p>
<hr>
<h2 id="행렬-matrix">행렬 (Matrix)</h2>
<p>선형 변환을 수행하는 도구</p>
<blockquote>
<p>가상 공간을 빠르게 변환시키는 일종의 명령어</p>
</blockquote>
<p>렌더링 파이프라인에서 공간을 변환하는데 사용되는 행렬이 존재한다.
<img src="https://velog.velcdn.com/images/code_null/post/aa6ffc82-30ce-4cc1-b6c5-e471f111c6a7/image.png" alt=""></p>
<hr>
<h2 id="평면-방정식">평면 방정식</h2>
<p>여러개의 평면을 사용해서 공간안에 자신의 영역을 만들 수 있다.</p>
<pre><code>ax + by +cz + d = 0</code></pre><br>

<h3 id="평면-방정식의-예">평면 방정식의 예</h3>
<h4 id="절두체">절두체</h4>
<ul>
<li>카메라 보는 영역에 해당하는 절두체</li>
<li>6개의 평면으로 구성되어 보이는 물체만 그리도록 처리할 수 있다</li>
</ul>
<p><img src="https://velog.velcdn.com/images/code_null/post/692a1598-022a-4b86-b1d2-9444f931a25d/image.png" alt=""></p>
<hr>
<h2 id="중요-뽀인트">중요 뽀인트!</h2>
<ol>
<li><strong>수의 체계와 벡터 공간</strong></li>
<li><strong>선형 변환과 행렬</strong></li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity3D] 경사면에서 미끄러지기]]></title>
            <link>https://velog.io/@code_null/Unity3D-%EA%B2%BD%EC%82%AC%EB%A9%B4%EC%97%90%EC%84%9C-%EB%AF%B8%EB%81%84%EB%9F%AC%EC%A7%80%EA%B8%B0</link>
            <guid>https://velog.io/@code_null/Unity3D-%EA%B2%BD%EC%82%AC%EB%A9%B4%EC%97%90%EC%84%9C-%EB%AF%B8%EB%81%84%EB%9F%AC%EC%A7%80%EA%B8%B0</guid>
            <pubDate>Sun, 05 May 2024 13:16:26 GMT</pubDate>
            <description><![CDATA[<h1 id="경사면">경사면</h1>
<hr>
<p>경사가 가파른 지면을 플레이어가 연속해서 점프하여 위로 향하지 못하도록 경사가 가파르면 미끄러지도록 해보자.</p>
<h2 id="핵심-코드">핵심 코드</h2>
<p>핵심 코드만 보도록 하자.</p>
<pre><code class="language-csharp">// 미끄러지는지 여부 체크
private bool isSliding;
// 미끄러지는 속도
private Vector3 slopeSlideVelocity;

private void SetSlopeSlideVelocity()
{
    // transform.position + Vector3.up: 캐릭터와 가까운 지면을 놓치지 않기 위해 원점을 높임
    if (Physics.Raycast(transform.position + Vector3.up, Vector3.down, out RaycastHit hitInfo, 5))
    {
        // 경사 각도 결정
        float angle = Vector3.Angle(hitInfo.normal, Vector3.up);
        // Charater Contoller의 경사 각도와 비교
        if (angle &gt;= characterController.slopeLimit)
        {
            slopeSlideVelocity = Vector3.ProjectOnPlane(new Vector3(0, ySpeed, 0), hitInfo.normal);
            return;
        }
    }

    // 슬라이딩할 때 부드러운 감속을 주기 위해
    if (isSliding)
    {
        // 배속을 주기 위해 *3
        slopeSlideVelocity -= slopeSlideVelocity * Time.deltaTime * 3;

        // 크기가 1보다 작아지면 충분히 감속했다고 판단
        if (slopeSlideVelocity.magnitude &gt; 1)
        {
            return;
        }
    }

    slopeSlideVelocity = Vector3.zero;
}</code></pre>
<h3 id="vector3angle"><a href="https://docs.unity3d.com/ScriptReference/Vector3.Angle.html">Vector3.Angle</a></h3>
<p><strong>public static float Angle(Vector3 from, Vector3 to);</strong></p>
<p>두 벡터 A,B가 있을 때 <code>Vector3.Angle(A, B)</code>는 A와 B 사이의 각도를 반환한다.</p>
<blockquote>
<p>각도의 부호가 없으며, 항상 0도 이상 180도 미만의 값을 가진다.</p>
</blockquote>
<h3 id="vector3projectonplane"><a href="https://docs.unity3d.com/ScriptReference/Vector3.ProjectOnPlane.html">Vector3.ProjectOnPlane</a></h3>
<p><strong>public static Vector3 ProjectOnPlane(Vector3 vector, Vector3 planeNormal);</strong>
주어진 평면에 대해 벡터를 투영 시키는 역할을 한다. 벡터를 주어진 평면에 수직인 벡터롤 변환한다.</p>
<blockquote>
<p>ex) 구불한 바닥 평면에 방향 벡터를 투영하여 수직인 벡터를 얻음으로써, 평면 바닥을 따라 움직이는 벡터를 얻을 수 있다.</p>
</blockquote>
<h4 id="투영">투영</h4>
<p><img src="https://velog.velcdn.com/images/code_null/post/821ccbbf-fce3-4eec-958b-f6b12fec365c/image.png" alt="">
빨간 사각형으로 표시된 <code>벡터</code>는 녹색 <code>평면</code>에 투영되어 검은색 사각형으로 표시된 <code>벡터</code>가 된다.</p>
<h3 id="법선-벡터-normal">법선 벡터 (Normal)</h3>
<p><code>법선 벡터</code>는 특정 지점 또는 표면에서 수직으로 나아가는 벡터를 말한다. 표면의 각 점에서 법선 벡터는 해당 점에서 표면을 수직으로 가리키며, 이는 표면이 어떤 방향을 향하는지를 나타낸다.</p>
<hr>
<h2 id="코드-리뷰">코드 리뷰</h2>
<pre><code class="language-csharp">// 경사 각도 결정
float angle = Vector3.Angle(hitInfo.normal, Vector3.up);</code></pre>
<p><code>Raycast</code>를 통하여 충돌된 오브젝트의 정보를 얻고, 충돌 지점에서의 <code>법선 벡터</code>와 <code>Vector3.up</code> 이용하여 경사면의 각도를 구한다.</p>
<br>

<pre><code class="language-csharp">slopeSlideVelocity = Vector3.ProjectOnPlane(new Vector3(0, ySpeed, 0), hitInfo.normal);</code></pre>
<p><code>평면</code>을 <strong>충돌된 오브젝트의 법선벡터</strong>로 할당하고, <code>벡터</code>를 중력으로 감소되고 있는 <code>ySpeed</code>로 할당해 투영된 평면에 수직인 벡터를 얻음으로써, 경사면의 기울기대로 중력을 적용시킨다.</p>
<hr>
<h2 id="스크립트">스크립트</h2>
<p>경사면에서 미끄러지는 핵심 코드와 더불어 예외 처리를 적용한 <code>PlayerMovement</code>의 전체 코드를 보자.</p>
<pre><code class="language-csharp">using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerMovement : MonoBehaviour
{
    public float rotationSpeed = 720;

    public float jumpHeight = 2;
    public float gravityMultiplier = 1.5f;

    public float jumpHorizontalSpeed = 3;

    public float jumpButtonGracePeriod = 0.2f;

    public Transform cameraTransform;

    private CharacterController characterController;
    private Animator animator;

    private float ySpeed;
    private float originalStepOffset;

    private float? lastGroundedTime;
    private float? jumpButtonPressedTime;

    private bool isJumping;
    private bool isGrounded;

    // 미끄러지는지 여부 체크
    private bool isSliding;
    // 미끄러지는 속도
    private Vector3 slopeSlideVelocity;


    private void Start()
    {
        characterController = GetComponent&lt;CharacterController&gt;();
        animator = GetComponent&lt;Animator&gt;();
        originalStepOffset = characterController.stepOffset;
    }

    void Update()
    {
        float horizontalInput = Input.GetAxis(&quot;Horizontal&quot;);
        float verticalInput = Input.GetAxis(&quot;Vertical&quot;);

        Vector3 movementDirection = new Vector3(horizontalInput, 0, verticalInput);

        float inputMagnitude = Mathf.Clamp01(movementDirection.magnitude);

        if (Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift))
        {
            inputMagnitude *= 0.5f;
        }

        animator.SetFloat(&quot;Input Magnitude&quot;, inputMagnitude, 0.05f, Time.deltaTime);

        movementDirection = Quaternion.AngleAxis(cameraTransform.rotation.eulerAngles.y, Vector3.up) * movementDirection;
        movementDirection.Normalize();

        float gravity = Physics.gravity.y * gravityMultiplier;

        // 점프 시작, 위쪽 방향이동 체크, 점프 버튼을 더 이상 누르지 않는지
        // 세가지 조건이 충족되면 중력값을 두배로 늘린다.
        if(isJumping &amp;&amp; ySpeed &gt; 0 &amp;&amp; Input.GetButton(&quot;Jump&quot;) == false)
        {
            gravity *= 2;
        }

        ySpeed += gravity * Time.deltaTime;

        // 경사속도 설정
        SetSlopeSlideVelocity();
        // 경사면인지 검사
        if(slopeSlideVelocity == Vector3.zero)
        {
            isSliding = false;
        }

        if (characterController.isGrounded)
        {
            lastGroundedTime = Time.time;
        }
        if(Input.GetButtonDown(&quot;Jump&quot;))
        {
            jumpButtonPressedTime = Time.time;
        }

        if (Time.time - lastGroundedTime &lt;= jumpButtonGracePeriod)
        {
            if (slopeSlideVelocity != Vector3.zero)
            {
                isSliding = true;
            }

            characterController.stepOffset = originalStepOffset;

            // 슬라이딩이 아닌 경우만 실행
            if (isSliding == false)
            {
                ySpeed = -0.8f;
            }

            animator.SetBool(&quot;isGrounded&quot;, true);
            isGrounded = true;
            animator.SetBool(&quot;isJumping&quot;, false);
            isJumping = false;
            animator.SetBool(&quot;isFalling&quot;, false);

            // 슬라이딩이 아닌 경우에만 점프
            if (Time.time - jumpButtonPressedTime &lt;= jumpButtonGracePeriod &amp;&amp; isSliding == false)
            {
                ySpeed = Mathf.Sqrt(jumpHeight * -2 * gravity);

                animator.SetBool(&quot;isJumping&quot;, true);
                isJumping = true;

                lastGroundedTime = null;
                jumpButtonPressedTime = null;
            }
        }
        else
        {
            characterController.stepOffset = 0;

            animator.SetBool(&quot;isGrounded&quot;, false);
            isGrounded = false;

            if ((isJumping &amp;&amp; ySpeed &lt; 0) || (ySpeed &lt; -2.5f))
            {
                animator.SetBool(&quot;isFalling&quot;, true);
            }
        }

        if (movementDirection != Vector3.zero)
        {
            animator.SetBool(&quot;isMoving&quot;, true);
            Quaternion toRotation = Quaternion.LookRotation(movementDirection, Vector3.up);
            transform.rotation = Quaternion.RotateTowards(transform.rotation, toRotation, rotationSpeed * Time.deltaTime);
        }
        else
        {
            animator.SetBool(&quot;isMoving&quot;, false);
        }

        // 슬라이딩이 아닐때만 떨어짐
        if(isGrounded == false &amp;&amp; isSliding == false)
        {
            Vector3 velocity = movementDirection * inputMagnitude * jumpHorizontalSpeed;
            velocity.y = ySpeed;

            characterController.Move(velocity * Time.deltaTime);
        }

        // 슬라이딩 이동 처리
        if(isSliding)
        {
            Vector3 velocity = slopeSlideVelocity;
            velocity.y = ySpeed;

            characterController.Move(velocity * Time.deltaTime);
        }
    }

    private void SetSlopeSlideVelocity()
    {
        // transform.position + Vector3.up: 캐릭터와 가까운 지면을 놓치지 않기 위해 원점을 높임
        if (Physics.Raycast(transform.position + Vector3.up, Vector3.down, out RaycastHit hitInfo, 5))
        {
            // 경사 각도 결정
            float angle = Vector3.Angle(hitInfo.normal, Vector3.up);
            // Charater Contoller의 경사 각도와 비교
            if (angle &gt;= characterController.slopeLimit)
            {
                slopeSlideVelocity = Vector3.ProjectOnPlane(new Vector3(0, ySpeed, 0), hitInfo.normal);
                return;
            }
        }

        // 슬라이딩할 때 부드러운 감속을 주기 위해
        if(isSliding)
        {
            // 배속을 주기 위해 *3
            slopeSlideVelocity -= slopeSlideVelocity * Time.deltaTime * 3;

            // 크기가 1보다 작아지면 충분히 감속했다고 판단
            if (slopeSlideVelocity.magnitude &gt; 1)
            {
                return;
            }
        }

        slopeSlideVelocity = Vector3.zero;
    }

    private void OnAnimatorMove()
    {
        // 슬라이딩이 아닐때만 적용
        if(isGrounded &amp;&amp; isSliding == false)
        {
            Vector3 velocity = animator.deltaPosition;
            velocity.y = ySpeed * Time.deltaTime;

            characterController.Move(velocity);
        }
    }

    private void OnApplicationFocus(bool focus)
    {
        if (focus)
        {
            Cursor.lockState = CursorLockMode.Locked;
        }
        else
        {
            Cursor.lockState = CursorLockMode.None;
        }
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity3D] 일시중지]]></title>
            <link>https://velog.io/@code_null/Unity3D-%EC%9D%BC%EC%8B%9C%EC%A4%91%EC%A7%80</link>
            <guid>https://velog.io/@code_null/Unity3D-%EC%9D%BC%EC%8B%9C%EC%A4%91%EC%A7%80</guid>
            <pubDate>Sun, 05 May 2024 08:15:38 GMT</pubDate>
            <description><![CDATA[<h1 id="일시중지">일시중지</h1>
<hr>
<p>일시중지하는 간단한 방법들을 알아보자.</p>
<h2 id="timetimescale">Time.timeScale</h2>
<p>Time.timeScale을 이용한 스크립트를 작성해보자. </p>
<pre><code class="language-csharp">using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PauseController : MonoBehaviour
{
    private bool isPaused;

    private void Update()
    {
        if(Input.GetKeyDown(KeyCode.Escape))
        {
            isPaused = !isPaused;

            if(isPaused)
            {
                Time.timeScale = 0;
            }
            else
            {
                Time.timeScale = 1;
            }
        }
    }
}</code></pre>
<blockquote>
<p><strong>Time.timeScale</strong>을 이용한 일시중지는 간단하지만 <code>timeScale</code>이 0이 되더라도 스크립트 실행에서 <code>Update()</code> 메서드는 중지되지 않는다는 문제가 있다.</p>
</blockquote>
<ul>
<li>예를 들어 입력은 계속 처리될 것이다.</li>
</ul>
<h2 id="스크립트-비활성화">스크립트 비활성화</h2>
<p>유니티 이벤트를 사용하여 스크립트 활성/비활성화를 통해 위 문제를 해결해보자.</p>
<pre><code class="language-csharp">using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

public class PauseController : MonoBehaviour
{
    public UnityEvent GamePaused;
    public UnityEvent GameResumed;

    private bool isPaused;

    private void Update()
    {
        if(Input.GetKeyDown(KeyCode.Escape))
        {
            isPaused = !isPaused;

            if(isPaused)
            {
                Time.timeScale = 0;
                GamePaused.Invoke();
            }
            else
            {
                Time.timeScale = 1;
                GameResumed.Invoke();
            }
        }
    }
}</code></pre>
<p>유니티 이벤트를 추가하고 활성/비활성화할 스크립트를 연결한다.
<img src="https://velog.velcdn.com/images/code_null/post/8527df53-eb3f-423c-b232-d35af31328ba/image.png" alt=""></p>
<blockquote>
<p><strong>timeScale</strong>이 0으로 설정된 경우 <code>FixedUpdate()</code> 메서드는 절대로 호출되지 않는다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity3D] 커스텀 기즈모]]></title>
            <link>https://velog.io/@code_null/Unity3D-%EC%BB%A4%EC%8A%A4%ED%85%80-%EA%B8%B0%EC%A6%88%EB%AA%A8</link>
            <guid>https://velog.io/@code_null/Unity3D-%EC%BB%A4%EC%8A%A4%ED%85%80-%EA%B8%B0%EC%A6%88%EB%AA%A8</guid>
            <pubDate>Sun, 05 May 2024 07:54:18 GMT</pubDate>
            <description><![CDATA[<h1 id="커스텀-기즈모">커스텀 기즈모</h1>
<hr>
<p>커스텀 기즈모를 활용하는 방법을 간단히 알아보자.</p>
<h2 id="스크립트">스크립트</h2>
<p><a href="https://velog.io/@code_null/Unity3D-%EC%9B%A8%EC%9D%B4%ED%8F%AC%EC%9D%B8%ED%8A%B8-%ED%94%8C%EB%9E%AB%ED%8F%BC#%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8">웨이포인트 스크립트</a> 활용하여 기즈모를 그리는 코드를 추가해보자.</p>
<pre><code class="language-csharp">using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class WaypointPath : MonoBehaviour
{
    public Transform GetWaypoint(int waypointIndex)
    {
        return transform.GetChild(waypointIndex);
    }

    public int GetNextWaypointIndex(int currentWaypointIndex)
    {
        int nextWaypointIndex = currentWaypointIndex + 1;
        if(nextWaypointIndex == transform.childCount)
        {
            nextWaypointIndex = 0;
        }

        return nextWaypointIndex;
    }

    private void OnDrawGizmos()
    {
        for (int waypoinIndex = 0; waypoinIndex &lt; transform.childCount; waypoinIndex++)
        {
            // 웨이포인트 지점에 원 그리기
            var waypoint = GetWaypoint(waypoinIndex);

            Gizmos.color = Color.cyan;
            Gizmos.DrawSphere(waypoint.position, 0.2f);

            // 다음 웨이포인트까지 선그리기
            int nextWaypointIndex = GetNextWaypointIndex(waypoinIndex);
            var nextWaypoint = GetWaypoint(nextWaypointIndex);

            Gizmos.color = Color.yellow;
            Gizmos.DrawLine(waypoint.position, nextWaypoint.position);
        }
    }
}</code></pre>
<h2 id="씬-뷰">씬 뷰</h2>
<p><img src="https://velog.velcdn.com/images/code_null/post/ab5db7ae-03c8-4510-9b92-7f38c2d88b70/image.png" alt=""></p>
<h2 id="개선">개선</h2>
<h3 id="1-선택한-경우만">#1 선택한 경우만</h3>
<p>웨이포인트 오브젝트의 부모, 자식 오브젝트를 클릭했을 때만 기즈모가 나오도록 수정해보자.</p>
<pre><code class="language-csharp">// 네임스페이스 추가
using System.Linq;
using UnityEditor;

// 수정
private void OnDrawGizmos()
{
    if (IsWaypointSelected())
    {
        for (int waypoinIndex = 0; waypoinIndex &lt; transform.childCount; waypoinIndex++)
        {
            // 웨이포인트 지점에 원 그리기
            var waypoint = GetWaypoint(waypoinIndex);

            Gizmos.color = Color.cyan;
            Gizmos.DrawSphere(waypoint.position, 0.2f);

            // 다음 웨이포인트까지 선그리기
            int nextWaypointIndex = GetNextWaypointIndex(waypoinIndex);
            var nextWaypoint = GetWaypoint(nextWaypointIndex);

            Gizmos.color = Color.yellow;
            Gizmos.DrawLine(waypoint.position, nextWaypoint.position);
        }
    }
}

private bool IsWaypointSelected()
{
    // 현재 오브젝트를 선택했는지
    if (Selection.transforms.Contains(transform))
    {
        return true;
    }

    // 현재 오브젝트의 자식 오브젝트가 선택되었는지
    foreach (Transform child in transform)
    {
        if (Selection.transforms.Contains(child))
        {
            return true;
        }
    }

    return false;
}</code></pre>
<h3 id="2-관련-오브젝트가-선택된-경우">#2 관련 오브젝트가 선택된 경우</h3>
<p>실제로 움직이는 플랫폼 오브젝트를 선택했을 때도 웨이포인트 기즈모가 출력되도록 수정해보자.</p>
<p><code>WaypointPath</code> 스크립트 수정</p>
<pre><code class="language-csharp">private void OnDrawGizmos()
{
    if (IsWaypointSelected())
    {
        DrawWaypointGizmo();
    }
}

public void DrawWaypointGizmo()
{
    for (int waypoinIndex = 0; waypoinIndex &lt; transform.childCount; waypoinIndex++)
    {
        // 웨이포인트 지점에 원 그리기
        var waypoint = GetWaypoint(waypoinIndex);

        Gizmos.color = Color.cyan;
        Gizmos.DrawSphere(waypoint.position, 0.2f);

        // 다음 웨이포인트까지 선그리기
        int nextWaypointIndex = GetNextWaypointIndex(waypoinIndex);
        var nextWaypoint = GetWaypoint(nextWaypointIndex);

        Gizmos.color = Color.yellow;
        Gizmos.DrawLine(waypoint.position, nextWaypoint.position);
    }
}</code></pre>
<p>실제 오브젝트를 조작하는 스크립트 수정</p>
<pre><code class="language-csharp">using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MovingPlatform : MonoBehaviour
{
    public WaypointPath waypointPath;

    private void OnDrawGizmosSelected()
    {
        waypointPath.DrawWaypointGizmo();
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity3D] 웨이포인트 플랫폼]]></title>
            <link>https://velog.io/@code_null/Unity3D-%EC%9B%A8%EC%9D%B4%ED%8F%AC%EC%9D%B8%ED%8A%B8-%ED%94%8C%EB%9E%AB%ED%8F%BC</link>
            <guid>https://velog.io/@code_null/Unity3D-%EC%9B%A8%EC%9D%B4%ED%8F%AC%EC%9D%B8%ED%8A%B8-%ED%94%8C%EB%9E%AB%ED%8F%BC</guid>
            <pubDate>Sun, 05 May 2024 07:26:19 GMT</pubDate>
            <description><![CDATA[<h1 id="플랫폼">플랫폼</h1>
<hr>
<p>웨인포인트에 따라 움직이는 플랫폼을 만들고, 플레이어가 플랫폼에 탑승하도록 만들어보자.</p>
<h2 id="씬-구성">씬 구성</h2>
<p><code>MovingPlatform</code>이라는 프리팹을 씬에 구성하고, 빈오브젝트를 만들어 웨이포인트를 관리할 <code>WaypointPath1</code> 오브젝트와 하위 오브젝트로 실제 경유할 <code>Waypoin</code> 오브젝트 3개를 배치했다.</p>
<p><img src="https://velog.velcdn.com/images/code_null/post/71dbe260-d5f0-4fbc-8b8e-2b73b14aef4d/image.png" alt=""></p>
<h2 id="스크립트">스크립트</h2>
<p>Path를 관리할 오브젝트를 작성해보자. <code>WaypointPath1</code> 오브젝트에 부착.</p>
<pre><code class="language-csharp">using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class WaypointPath : MonoBehaviour
{
    public Transform GetWaypoint(int waypointIndex)
    {
        return transform.GetChild(waypointIndex);
    }

    public int GetNextWaypointIndex(int currentWaypointIndex)
    {
        int nextWaypointIndex = currentWaypointIndex + 1;
        if(nextWaypointIndex == transform.childCount)
        {
            nextWaypointIndex = 0;
        }

        return nextWaypointIndex;
    }
}</code></pre>
<p>플랫폼을 직접 움직이는 스크립트를 작성해보자. <code>MovingPlatform</code> 오브젝트에 부착.</p>
<pre><code class="language-csharp">using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MovingPlatform : MonoBehaviour
{
    public WaypointPath waypointPath;
    public float speed = 1;

    private int targetWaypointIndex;

    private Transform previousWaypoint;
    private Transform targetWaypoint;

    // 목표 경유지까지 걸리는 시간
    private float timeToWaypoint;
    // 경과된 시간
    private float elapsedTime;

    private void Start()
    {
        TargetNextWaypoint();
    }

    private void FixedUpdate()
    {
        elapsedTime += Time.deltaTime;

        // 경과된 시간을 목표 경유지까지 걸리는 시간으로 나누어 완료될 비율을 계산
        float elapsedPercentage = elapsedTime / timeToWaypoint;
        // 시작과 끝에 속도가 느려지게
        elapsedPercentage = Mathf.SmoothStep(0, 1, elapsedPercentage);

        transform.position = Vector3.Lerp(previousWaypoint.position, targetWaypoint.position, elapsedPercentage);
        // 회전값도 적용
        transform.rotation = Quaternion.Lerp(previousWaypoint.rotation, targetWaypoint.rotation, elapsedPercentage);

        // 목표 경유지까지 이동했는지 검사
        if (elapsedPercentage &gt;= 1)
        {
            TargetNextWaypoint();
        }
    }

    private void TargetNextWaypoint()
    {
        previousWaypoint = waypointPath.GetWaypoint(targetWaypointIndex);
        targetWaypointIndex = waypointPath.GetNextWaypointIndex(targetWaypointIndex);
        targetWaypoint = waypointPath.GetWaypoint(targetWaypointIndex);

        elapsedTime = 0f;

        float distanceToWaypoint = Vector3.Distance(previousWaypoint.position, targetWaypoint.position);

        // 거리를 속도로 나누어 해당거리에 도달하는 데 걸리는 시간 계산
        timeToWaypoint = distanceToWaypoint / speed;
    }

    private void OnTriggerEnter(Collider other)
    {
        other.transform.SetParent(transform);
    }

    private void OnTriggerExit(Collider other)
    {
        other.transform.SetParent(null);
    }
}</code></pre>
<blockquote>
<p><strong><a href="(https://docs.unity3d.com/ScriptReference/Mathf.SmoothStep.html)">Mathf.SmoothStep</a></strong>
public static float SmoothStep(float from, float to, float t);
보간 속도는 처음부터 점차 빨라지고 끝으로 갈수록 느려진다.
<code>from: 시작점</code>
<code>to: 종료점</code>
<code>t: 비율</code></p>
</blockquote>
<h2 id="문제-해결">문제 해결</h2>
<h3 id="1-플랫폼과-같이-움직이기">#1 플랫폼과 같이 움직이기</h3>
<p>플랫폼과 같이 움직이고 회전하도록 하기위해 <code>MovingPlatform</code> 스크립트에 <code>OnTrigger</code> 함수를 추가해 <code>Enter</code>일 때는 플랫폼의 자식으로, <code>Exit</code>일 때는 자식을 해제하므로써 플랫폼과 같은 움직임을 보이도록 수정하였다.</p>
<h3 id="2-버벅거리는-움직임">#2 버벅거리는 움직임</h3>
<p><code>Update</code>메서드에 제어되는 <strong>플랫폼</strong> 오브젝트에 실제로 올라타면 움직임이 버벅이는 것처럼 보인다. 이는 플레이어 오브젝트에 부착된 <code>Character Controller</code>와 관련이 있다. 이를 해결하기 위해 <strong>플랫폼</strong> 오브젝트가 <code>FixedUpdate</code>에서 제어되도록 수정하였다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity3D] Idle 모션의 다양화]]></title>
            <link>https://velog.io/@code_null/Unity3D-Idle-%EB%AA%A8%EC%85%98%EC%9D%98-%EB%8B%A4%EC%96%91%ED%99%94</link>
            <guid>https://velog.io/@code_null/Unity3D-Idle-%EB%AA%A8%EC%85%98%EC%9D%98-%EB%8B%A4%EC%96%91%ED%99%94</guid>
            <pubDate>Fri, 03 May 2024 13:38:07 GMT</pubDate>
            <description><![CDATA[<h1 id="idle">Idle</h1>
<hr>
<p>Idle 상태에서 일정시간이 지나면 지루한 모션등의 다른 모션을 취하도록 수정해보자.</p>
<h2 id="블렌드-트리-구성">블렌드 트리 구성</h2>
<p>일단 사용할 여러가지 Idle 모션을 블렌드 트리로 구성하고, 매개변수와 Threshord값을 설정한다.
<img src="https://velog.velcdn.com/images/code_null/post/828ca43a-77c9-4c83-9836-8eedb05f9dda/image.png" alt=""></p>
<h2 id="스크립트-추가">스크립트 추가</h2>
<p>애니메이터 State에서 스크립트를 생성한다.
<img src="https://velog.velcdn.com/images/code_null/post/034793b9-fe7c-4c1d-8262-14458b442736/image.png" alt=""></p>
<h2 id="스크립트">스크립트</h2>
<p>일정 시간을 Idle 상태에서 머물 경우 다른 Idle 모션으로 변경하는 스크립트를 작성해보자.</p>
<pre><code class="language-csharp">using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BoredBehaviour : StateMachineBehaviour
{
    // 지루해질 때까지의 시간
    public float timeUntilBored = 3;
    // 애니메이션 갯수
    public int numberOfBoredAnimations = 3;

    private bool isBored;
    // Idle 상태 시간 저장
    private float idleTime;

    // OnStateEnter is called when a transition starts and the state machine starts to evaluate this state
    override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        ResetIdle(animator);
    }

    // OnStateUpdate is called on each Update frame between OnStateEnter and OnStateExit callbacks
    override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        if(isBored == false)
        {
            idleTime += Time.deltaTime;

            if(idleTime &gt; timeUntilBored)
            {
                isBored = true;
                int boredAnimation = Random.Range(1, numberOfBoredAnimations + 1);

                animator.SetFloat(&quot;BoredAnimation&quot;, boredAnimation);
            }
        }
        // 상태 정보를 이용하여 애니메이션의 정규화된 시간을 확인하여
        // 지루한 애니메이션을 1회만 재생하도록 제어
        else if (stateInfo.normalizedTime % 1 &gt; 0.98f)
        {
            ResetIdle(animator);
        }
    }

    private void ResetIdle(Animator animator)
    {
        isBored = false;
        idleTime = 0;

        animator.SetFloat(&quot;BoredAnimation&quot;, 0);
    }
}</code></pre>
<blockquote>
<p><strong>(stateInfo.normalizedTime % 1 &gt; 0.98f) 사용 이유</strong>
Loop 애니메이션의 경우 normalizedTime이 0에서 시작해 계속 증가하기 때문에 0과 1사이의 값이 아니다. Loop가 계속 되면서 1이상으로 계속 늘어나게 된다.</p>
</blockquote>
<ul>
<li>% 나머지 연산자를 이용해 항상 애니메이션이 끝까지 재생되도록 한다.</li>
</ul>
<h2 id="문제-해결">문제 해결</h2>
<h3 id="1-idle-모션-끊김">#1 Idle 모션 끊김</h3>
<p>Idle 모션 재생 중 다른 Bored 모션으로 전환이 이루어진다. Idle 모션이 끝난 뒤 다른 Bored 모션으로 전환하도록 수정해보자.</p>
<pre><code class="language-csharp">// 조건을 추가하여 Loop가 시작 부분에 가까워졌는지 체크한다.
if(idleTime &gt; timeUntilBored &amp;&amp; stateInfo.normalizedTime % 1 &lt; 0.02f)</code></pre>
<h3 id="2-모션-전환">#2 모션 전환</h3>
<p>모션이 전환될 때 전환이 부드럽게 이루어지지 않는 부분을 수정해보자.</p>
<pre><code class="language-csharp">// 애니메이션 재생에 dampTime을 적용한다.
animator.SetFloat(&quot;BoredAnimation&quot;, boredAnimation, 0.2f, Time.deltaTime);</code></pre>
<h3 id="3-idle-모션으로-되돌아갈-때">#3 Idle 모션으로 되돌아갈 때</h3>
<p>기본 Idle 모션과 3가지 Bored 모션이 블렌드 트리로 묶여있기 때문에 Bored 모션 재생 후 Idle 모션으로 돌아갈 때 3가지 Bored 모션이 조금씩 표시되면서 부자연스러운 젼환을 보여준다.</p>
<p>이를 해결하기 위해 bored 모션 사이에 기본 Idle 모션을 배치하여 다른 애니메이션을 거치지 않도록 함으로써 자연스러운 전환을 이끌어낼 수 있다.
<img src="https://velog.velcdn.com/images/code_null/post/4820f118-48cb-40ee-88d3-573d450eaca0/image.png" alt=""></p>
<h2 id="스크립트-수정">스크립트 수정</h2>
<p>관련 문제를 해결한 전체 스크립트를 보자.</p>
<pre><code class="language-csharp">using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BoredBehaviour : StateMachineBehaviour
{
    public float timeUntilBored = 3;
    public int numberOfBoredAnimations = 3;

    private bool isBored;
    private float idleTime;

    private int boredAnimation;

    // OnStateEnter is called when a transition starts and the state machine starts to evaluate this state
    override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        ResetIdle();
    }

    // OnStateUpdate is called on each Update frame between OnStateEnter and OnStateExit callbacks
    override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        if(isBored == false)
        {
            idleTime += Time.deltaTime;

            // 모션이 처음에 가까운지도 같이 체크
            if(idleTime &gt; timeUntilBored &amp;&amp; stateInfo.normalizedTime % 1 &lt; 0.02f)
            {
                isBored = true;
                boredAnimation = Random.Range(1, numberOfBoredAnimations + 1);
                // 기본 Idle 모션 추가 후 올바른 bored 모션을 얻기 위한 공식
                boredAnimation = boredAnimation * 2 - 1;

                // 가장 가까운 기본 Idle 모션으로 전환
                animator.SetFloat(&quot;BoredAnimation&quot;, boredAnimation - 1);
            }
        }
        else if (stateInfo.normalizedTime % 1 &gt; 0.98f)
        {
            ResetIdle();
        }

        // damptime 추가
        animator.SetFloat(&quot;BoredAnimation&quot;, boredAnimation, 0.2f, Time.deltaTime);
    }

    private void ResetIdle()
    {
        // bored 모션이 실행 중이라면 가장 가까운 기본 idle 모션을 재생하도록 리셋
        if(isBored)
        {
            boredAnimation--;
        }

        isBored = false;
        idleTime = 0;
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity3D] 높은 점프와 낮은 점프]]></title>
            <link>https://velog.io/@code_null/Unity3D-%EB%86%92%EC%9D%80-%EC%A0%90%ED%94%84%EC%99%80-%EB%82%AE%EC%9D%80-%EC%A0%90%ED%94%84</link>
            <guid>https://velog.io/@code_null/Unity3D-%EB%86%92%EC%9D%80-%EC%A0%90%ED%94%84%EC%99%80-%EB%82%AE%EC%9D%80-%EC%A0%90%ED%94%84</guid>
            <pubDate>Fri, 03 May 2024 11:13:04 GMT</pubDate>
            <description><![CDATA[<h1 id="점프">점프</h1>
<hr>
<p>키를 누르는 시간에 따라 점프의 높이에 변화를 줘보자.</p>
<h2 id="스크립트">스크립트</h2>
<p><a href="https://velog.io/@code_null/Unity3D-%EC%A0%90%ED%94%84-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98#%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8">이전의 점프 스크립트</a>를 수정한다.</p>
<pre><code class="language-csharp">using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerMovement : MonoBehaviour
{
    // ...생략

    // 점프 높이
    public float jumpHeight = 2;
    // 작용하는 중력
    public float gravityMultiplier = 1.5f;

    // ...생략

    void Update()
    {
        // ...생략

        // 작용하는 중력 계산
        float gravity = Physics.gravity.y * gravityMultiplier;

        // 점프 시작, 위쪽 방향이동 체크, 점프 버튼을 더 이상 누르지 않는지
        // 세가지 조건이 충족되면 중력값을 두배로 늘린다.
        if(isJumping &amp;&amp; ySpeed &gt; 0 &amp;&amp; Input.GetButton(&quot;Jump&quot;) == false)
        {
            gravity *= 2;
        }

        ySpeed += gravity * Time.deltaTime;

        // ...생략

        if (Time.time - lastGroundedTime &lt;= jumpButtonGracePeriod)
        {
            characterController.stepOffset = originalStepOffset;
            ySpeed = -0.8f;

            animator.SetBool(&quot;isGrounded&quot;, true);
            isGrounded = true;
            animator.SetBool(&quot;isJumping&quot;, false);
            isJumping = false;
            animator.SetBool(&quot;isFalling&quot;, false);

            if (Time.time - jumpButtonPressedTime &lt;= jumpButtonGracePeriod)
            {
                // jumpHeight만큼 점프하도록하는 공식
                ySpeed = Mathf.Sqrt(-jumpHeight * 2 * gravity);

                animator.SetBool(&quot;isJumping&quot;, true);
                isJumping = true;

                lastGroundedTime = null;
                jumpButtonPressedTime = null;
            }
        }
        else
        {
            characterController.stepOffset = 0;

            animator.SetBool(&quot;isGrounded&quot;, false);
            isGrounded = false;

            if ((isJumping &amp;&amp; ySpeed &lt; 0) || (ySpeed &lt; -2.5f))
            {
                animator.SetBool(&quot;isFalling&quot;, true);
            }
        }

        // ...생략
    }

    // ...생략
}</code></pre>
<p><code>gravityMultiplier</code> 값에 따라 체공 시간이 달라진다</p>
<blockquote>
<p><strong>MathF.Sqrt</strong>
public static float Sqrt (float x);
제곱근을 반환하는 함수</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity3D] Cinemachine 카메라 정렬]]></title>
            <link>https://velog.io/@code_null/Unity3D-Cinemachine-%EC%B9%B4%EB%A9%94%EB%9D%BC-%EC%A0%95%EB%A0%AC</link>
            <guid>https://velog.io/@code_null/Unity3D-Cinemachine-%EC%B9%B4%EB%A9%94%EB%9D%BC-%EC%A0%95%EB%A0%AC</guid>
            <pubDate>Fri, 03 May 2024 10:17:05 GMT</pubDate>
            <description><![CDATA[<h1 id="카메라-정렬">카메라 정렬</h1>
<hr>
<p><a href="https://velog.io/@code_null/Unity3D-Cinemachine%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-3%EC%9D%B8%EC%B9%AD-%EC%B9%B4%EB%A9%94%EB%9D%BC#%EC%B9%B4%EB%A9%94%EB%9D%BC-%EC%A0%95%EB%A0%AC">카메라 정렬</a>을 <code>특정 키</code>가 눌리거나 게임패드의 <code>오른쪽 트리거</code>가 눌렸을 때 다시 중앙으로 돌아오도록 변경해보자.</p>
<h2 id="자동-중앙-조정">자동 중앙 조정</h2>
<ul>
<li>FreeLock 카메라의 <code>Recenter To Target Heading</code> 옵션 비활성화한다.</li>
<li>키가 눌렸을 때 바로 정렬이 될 수 있도록 <code>Wait Time</code>과 <code>Recentering Time</code>의 값을 수정한다.
<img src="https://velog.velcdn.com/images/code_null/post/212afadf-b60e-40a6-95af-84ce064efe1d/image.png" alt=""></li>
</ul>
<h2 id="키-할당">키 할당</h2>
<p>Project Settings-&gt;Input Manager에서 Axis를 하나 추가하여 Recenter에 사용할 키를 할당하자.
<img src="https://velog.velcdn.com/images/code_null/post/c69f7e0b-c176-40c3-9219-80b13cf8910a/image.png" alt=""></p>
<blockquote>
<p><code>Joystick Axis</code>의 <code>10th axis</code>는 오른쪽 트리거에 해당한다. 게임패드마다 설정이 다를수도 있으니 안되면 다른 <code>Axis</code>값을 찾아 설정하자.</p>
</blockquote>
<h2 id="스크립트">스크립트</h2>
<p>카메라 중앙 조정을 제어할 스크립트를 작성하고, FreeLock 오브젝트에 부착하여 테스트한다.</p>
<pre><code class="language-csharp">using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Cinemachine; // &lt;-- namespace 추가

public class CameraRecenter : MonoBehaviour
{
    private CinemachineFreeLook camera;

    private void Start()
    {
        camera = GetComponent&lt;CinemachineFreeLook&gt;();
    }

    private void Update()
    {
        // 값이 1이면 왼쪽 컨트롤을 눌렀거나 오른쪽 트리거를 눌렀다는 의미
        if(Input.GetAxis(&quot;CameraRecentre&quot;) == 1)
        {
            camera.m_RecenterToTargetHeading.m_enabled = true;
        }
        else
        {
            camera.m_RecenterToTargetHeading.m_enabled = false;
        }
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity3D] 점프 애니메이션]]></title>
            <link>https://velog.io/@code_null/Unity3D-%EC%A0%90%ED%94%84-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98</link>
            <guid>https://velog.io/@code_null/Unity3D-%EC%A0%90%ED%94%84-%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98</guid>
            <pubDate>Thu, 02 May 2024 12:14:45 GMT</pubDate>
            <description><![CDATA[<h1 id="점프-애니메이션">점프 애니메이션</h1>
<hr>
<h2 id="애니메이터-구성">애니메이터 구성</h2>
<p>점프 애니메이션에는 점프할 때, 점프 중, 착지할 때 3가지 애니메이션을 구성</p>
<p><img src="https://velog.velcdn.com/images/code_null/post/ccd33261-0695-4c35-ac27-fb809989864f/image.png" alt=""></p>
<h2 id="스크립트">스크립트</h2>
<p>전체 코드를 보자.</p>
<pre><code class="language-csharp">using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerMovement : MonoBehaviour
{
    public float rotationSpeed = 720;
    public float jumpSpeed = 5;

    public float jumpButtonGracePeriod = 0.2f;

    public Transform cameraTransform;

    private CharacterController characterController;
    private Animator animator;

    private float ySpeed;
    private float originalStepOffset;

    private float? lastGroundedTime;
    private float? jumpButtonPressedTime;

    private bool isJumping;
    private bool isGrounded;

    private void Start()
    {
        characterController = GetComponent&lt;CharacterController&gt;();
        animator = GetComponent&lt;Animator&gt;();
        originalStepOffset = characterController.stepOffset;
    }

    void Update()
    {
        float horizontalInput = Input.GetAxis(&quot;Horizontal&quot;);
        float verticalInput = Input.GetAxis(&quot;Vertical&quot;);

        Vector3 movementDirection = new Vector3(horizontalInput, 0, verticalInput);

        float inputMagnitude = Mathf.Clamp01(movementDirection.magnitude);

        if (Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift))
        {
            inputMagnitude *= 0.5f;
        }

        animator.SetFloat(&quot;Input Magnitude&quot;, inputMagnitude, 0.05f, Time.deltaTime);

        movementDirection = Quaternion.AngleAxis(cameraTransform.rotation.eulerAngles.y, Vector3.up) * movementDirection;
        movementDirection.Normalize();

        ySpeed += Physics.gravity.y * Time.deltaTime;

        if(characterController.isGrounded)
        {
            lastGroundedTime = Time.time;
        }
        if(Input.GetButtonDown(&quot;Jump&quot;))
        {
            jumpButtonPressedTime = Time.time;
        }

        if (Time.time - lastGroundedTime &lt;= jumpButtonGracePeriod)
        {
            characterController.stepOffset = originalStepOffset;
            ySpeed = -0.8f;

            animator.SetBool(&quot;isGrounded&quot;, true);
            isGrounded = true;
            animator.SetBool(&quot;isJumping&quot;, false);
            isJumping = false;
            animator.SetBool(&quot;isFalling&quot;, false);

            if (Time.time - jumpButtonPressedTime &lt;= jumpButtonGracePeriod)
            {
                ySpeed = jumpSpeed;

                animator.SetBool(&quot;isJumping&quot;, true);
                isJumping = true;

                lastGroundedTime = null;
                jumpButtonPressedTime = null;
            }
        }
        else
        {
            characterController.stepOffset = 0;

            animator.SetBool(&quot;isGrounded&quot;, false);
            isGrounded = false;

            /* 낙하 애니메이션 전환 처리 */
            if ((isJumping &amp;&amp; ySpeed &lt; 0) || (ySpeed &lt; -2))
            {
                animator.SetBool(&quot;isFalling&quot;, true);
            }
        }

        if (movementDirection != Vector3.zero)
        {
            animator.SetBool(&quot;isMoving&quot;, true);
            Quaternion toRotation = Quaternion.LookRotation(movementDirection, Vector3.up);
            transform.rotation = Quaternion.RotateTowards(transform.rotation, toRotation, rotationSpeed * Time.deltaTime);
        }
        else
        {
            animator.SetBool(&quot;isMoving&quot;, false);
        }
    }

    private void OnAnimatorMove()
    {
        Vector3 velocity = animator.deltaPosition;
        velocity.y = ySpeed * Time.deltaTime;

        characterController.Move(velocity);
    }

    private void OnApplicationFocus(bool focus)
    {
        if (focus)
        {
            Cursor.lockState = CursorLockMode.Locked;
        }
        else
        {
            Cursor.lockState = CursorLockMode.None;
        }
    }
}</code></pre>
<blockquote>
<p><strong>낙하 중 처리</strong></p>
</blockquote>
<pre><code class="language-csharp">if ((isJumping &amp;&amp; ySpeed &lt; 0) || (ySpeed &lt; -2))
{
    animator.SetBool(&quot;isFalling&quot;, true);
}</code></pre>
<ul>
<li><code>(isJumping &amp;&amp; ySpeed &lt; 0)</code> 처리의 의미는 점프의 최고 높이에 도달했을 때 낙하 애니메이션을 처리하기 위함.</li>
<li><code>(ySpeed &lt; -2)</code> 처리의 의미는 계단을 내려올 때나 경사를 내려올때도 낙하 애니메이션 처리가 되지 않도록 하기위한 체크</li>
</ul>
<h2 id="문제-상황">문제 상황</h2>
<h3 id="1-연속된-점프">#1 연속된 점프</h3>
<p>연속으로 점프를 누를 경우 착지 애니메이션 재생되면서 곧바로 점프 시작 애니메이션으로 바뀌지 않아 어색한 애니메이션이 연결된다.</p>
<p>이유는 
<img src="https://velog.velcdn.com/images/code_null/post/f074ba23-1dad-48ee-be23-140c3e86086d/image.png" alt=""></p>
<p>착지 후 Idle 모션으로 돌아갈 때 트랜지션 시간이 0.6로 설정되어 있기 때문이다. </p>
<p>이를 해결하기 위해서는 <strong>Interruption Source</strong>를 <code>Next State</code>로 설정하여 다음 상태의 전환이 현재 상태를 중단 시키도록 할 수 있다.
<img src="https://velog.velcdn.com/images/code_null/post/aa04d462-8c02-46a0-981e-07fb5274bc3c/image.png" alt=""></p>
<blockquote>
<p><strong>전환 흐름</strong></p>
</blockquote>
<ol>
<li>착지 -&gt; Idle 전환 중</li>
<li>점프 버튼이 눌린다면</li>
<li>착지 -&gt; Idle 전환 중단</li>
<li>Idle -&gt; 점프 전환 시작
<code>다음 상태</code>인 Idle의 상태 전환이 우선권을 가짐</li>
</ol>
<h3 id="2-점프-중-이동">#2 점프 중 이동</h3>
<p>현재 코드로는 점프 중 이동을 할 수 있다. 이를 해결해보자.</p>
<p>점프 중 이동이 되지 않는 이유는 루트 모션에서 움직임을 제어하기 때문이므로 지상에 있을 때는 루트 모션이 제어하도록하고, 점프 중에는 새로운 이동 제어를 추가해준다.</p>
<pre><code class="language-csharp">private void OnAnimatorMove()
{
    if (isGrounded)
    {
        Vector3 velocity = animator.deltaPosition;
        velocity.y = ySpeed * Time.deltaTime;

        characterController.Move(velocity);
    }
}</code></pre>
<blockquote>
<p>지상에서만 제어하도록 수정</p>
</blockquote>
<pre><code class="language-csharp">public float jumpHorizontalSpeed = 3;

void Update()
{
    // ...생략

    if (isGrounded == false)
    {
        Vector3 velocity = movementDirection * inputMagnitude * jumpHorizontalSpeed;
        velocity.y = ySpeed;

        characterController.Move(velocity * Time.deltaTime);
    }
}</code></pre>
<blockquote>
<p>Update() 메서드 하단에 점프 중 이동 추가</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity3D] Character Controller와 Root Motion]]></title>
            <link>https://velog.io/@code_null/Unity3D-Character-Controller%EC%99%80-Root-Motion</link>
            <guid>https://velog.io/@code_null/Unity3D-Character-Controller%EC%99%80-Root-Motion</guid>
            <pubDate>Thu, 02 May 2024 10:14:25 GMT</pubDate>
            <description><![CDATA[<h1 id="root-motion-적용">Root Motion 적용</h1>
<hr>
<h2 id="스크립트">스크립트</h2>
<p>y의 위치만 Character Controller가 제어하고 XZ의 움직임은 Root Motion에 맡겨야하는데, Root Motion의 동작과 Character Controller가 서로 경쟁을 벌임으로서 약간의 동작 결함이 발생할 수 있다.</p>
<p><code>OnAnimatorMove</code> 메서드를 활용해서 움직임을 Character Controller가 모두 조작하도록 통합해 볼 것이다.</p>
<pre><code class="language-csharp">using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerMovement : MonoBehaviour
{
    public float rotationSpeed = 720;
    public float jumpSpeed = 5;

    public Transform cameraTransform;

    private CharacterController characterController;
    private Animator animator;

    private float ySpeed;

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

    void Update()
    {
        float horizontalInput = Input.GetAxis(&quot;Horizontal&quot;);
        float verticalInput = Input.GetAxis(&quot;Vertical&quot;);

        Vector3 movementDirection = new Vector3(horizontalInput, 0, verticalInput);

        float inputMagnitude = Mathf.Clamp01(movementDirection.magnitude);

        if (Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift))
        {
            inputMagnitude *= 0.5f;
        }

        animator.SetFloat(&quot;Input Magnitude&quot;, inputMagnitude, 0.05f, Time.deltaTime);

        movementDirection = Quaternion.AngleAxis(cameraTransform.rotation.eulerAngles.y, Vector3.up) * movementDirection;
        movementDirection.Normalize();

        ySpeed += Physics.gravity.y * Time.deltaTime;

        if (movementDirection != Vector3.zero)
        {
            Quaternion toRotation = Quaternion.LookRotation(movementDirection, Vector3.up);
            transform.rotation = Quaternion.RotateTowards(transform.rotation, toRotation, rotationSpeed * Time.deltaTime);
        }
    }

    // 기본 루트모션의 동작을 재정의할 수 있다.
    private void OnAnimatorMove()
    {
        // movementDirection * speed 속도를 animator.deltaPosition으로 대체
        // animator.deltaPosition 이는 애니메이션에 의해 결정되는 위치 변경이다.
        Vector3 velocity = animator.deltaPosition;
        // 프레임마다의 위치 변경은 animator.deltaPosition이 담당하므로 ySpeed에 Time.deltaTime을 곱해준다.
        velocity.y = ySpeed * Time.deltaTime;

        characterController.Move(velocity);
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity3D] Character Controller 물체 충돌]]></title>
            <link>https://velog.io/@code_null/Unity3D-Character-Controller-%EB%AC%BC%EC%B2%B4-%EC%B6%A9%EB%8F%8C</link>
            <guid>https://velog.io/@code_null/Unity3D-Character-Controller-%EB%AC%BC%EC%B2%B4-%EC%B6%A9%EB%8F%8C</guid>
            <pubDate>Thu, 02 May 2024 09:29:02 GMT</pubDate>
            <description><![CDATA[<h1 id="장애물-밀기">장애물 밀기</h1>
<hr>
<p>Character Controller와 물리 시스템을 이용하여 장애물을 밀어내는 방법에 대해 알아보자.</p>
<h2 id="씬-구성">씬 구성</h2>
<p>적당한 상자에 RigidBody 컴포넌트를 추가하고 씬을 구성한다.
<img src="https://velog.velcdn.com/images/code_null/post/f53d6c12-7c40-4c25-9181-7d2ab7d5bdc6/image.png" alt=""></p>
<h2 id="스크립트">스크립트</h2>
<p>Character Controller의 충돌 감지 메서드를 활용하여 상자를 밀어보자.</p>
<pre><code class="language-csharp">using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ObstaclePush : MonoBehaviour
{
    public float forceMagnitude = 1;

    // Character Controller가 충돌하며 호출되는 메서드
    private void OnControllerColliderHit(ControllerColliderHit hit)
    {
        // 부착된 리지드바디 캐싱
        Rigidbody rigidbody = hit.collider.attachedRigidbody;

        if(rigidbody != null)
        {
            // 힘을 줄 방향은 캐릭터 정면
            Vector3 forceDirection = hit.gameObject.transform.position - transform.position;
            // y축은 배제
            forceDirection.y = 0;
            forceDirection.Normalize();

            // 특정 위치에서 힘을 가하는 메서드
            rigidbody.AddForceAtPosition(forceDirection * forceMagnitude, transform.position, ForceMode.Impulse);
        }
    }
}</code></pre>
<blockquote>
<p><strong>Rigidbody.AddForceAtPosition</strong>
public void AddForceAtPosition (Vector3 force, Vector3 position, ForceMode mode= ForceMode.Force);
지정된 위치에서 물리적인 힘을 가하는 함수</p>
</blockquote>
<blockquote>
<p><code>ForceMode.Force: 연속 + 질량 적용 : 현실적인 물리 현상</code>
<code>ForceMode.Accel: 연속 + 질량 무시 : 오브젝트 질량 관계 없이 가속</code>
<code>ForceMode.Impulse: 불연속 + 질량 적용 : 폭발이나 충돌과 같은 짧은 순간의 힘</code>
<code>ForceMode.VelocityChange: 불연속 + 질량 무시 : 질량이 다른 오브젝트들을 같은 속도로 움직이고 싶을 때</code></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity3D] Cinemachine을 이용한 3인칭 카메라]]></title>
            <link>https://velog.io/@code_null/Unity3D-Cinemachine%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-3%EC%9D%B8%EC%B9%AD-%EC%B9%B4%EB%A9%94%EB%9D%BC</link>
            <guid>https://velog.io/@code_null/Unity3D-Cinemachine%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-3%EC%9D%B8%EC%B9%AD-%EC%B9%B4%EB%A9%94%EB%9D%BC</guid>
            <pubDate>Thu, 02 May 2024 09:02:29 GMT</pubDate>
            <description><![CDATA[<h1 id="3인칭-카메라">3인칭 카메라</h1>
<hr>
<p>Cinemachine을 이용하여 3인칭 카메라를 만들어보자.</p>
<h2 id="freelock-카메라">FreeLock 카메라</h2>
<p>씬에 FreeLock 카메라를 추가한다.
<img src="https://velog.velcdn.com/images/code_null/post/3ba2158e-d76e-4821-8245-9c624dc7ebe1/image.png" alt=""></p>
<p>FreeLock 카메라에 Follow 타겟과 Look At 타겟을 지정한다.
<img src="https://velog.velcdn.com/images/code_null/post/da8c593d-a674-4748-a0ce-f04cd2e1cafb/image.png" alt=""></p>
<h2 id="orbits">Orbits</h2>
<p>FreeLock 카메라를 선택하고 씬 뷰를 보면 3개의 빨간색 원을 볼 수 있다.
<img src="https://velog.velcdn.com/images/code_null/post/4746c39e-f5d6-4b2e-ba9a-fae2d53d9025/image.png" alt=""></p>
<p>이는 카메라의 세가지 궤도 반경을 보여준다.
상단, 중단, 하단의 Rig의 높이와 반경을 변경하여 카메라 높이가 변경됨에 따라 캐릭터에 얼마나 가까이 위치하는지 설정할 수 있다.
<img src="https://velog.velcdn.com/images/code_null/post/d34bbe5d-9518-4078-989e-ff3ddd5d4659/image.png" alt=""></p>
<p>카메라가 높을수록 반경이 커지고, 카메라가 낮을수록 확대된다.
<img src="https://velog.velcdn.com/images/code_null/post/a391881a-2c08-4e63-acab-46d02e38d9db/image.png" alt=""></p>
<h2 id="스크립트">스크립트</h2>
<p>카메라가 바라보는 방향으로 움직이도록 스크립트를 작성해보자.</p>
<pre><code class="language-csharp">using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerMovement : MonoBehaviour
{
    public float maximumSpeed = 3;
    public float rotationSpeed = 720;
    public Transform cameraTransform;

    private CharacterController characterController;
    private Animator animator;

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

    void Update()
    {
        float horizontalInput = Input.GetAxis(&quot;Horizontal&quot;);
        float verticalInput = Input.GetAxis(&quot;Vertical&quot;);

        Vector3 movementDirection = new Vector3(horizontalInput, 0, verticalInput);

        float inputMagnitude = Mathf.Clamp01(movementDirection.magnitude);
        float speed = inputMagnitude * maximumSpeed;

        if (Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift))
        {
            inputMagnitude *= 0.5f;
        }

        animator.SetFloat(&quot;Input Magnitude&quot;, inputMagnitude, 0.05f, Time.deltaTime);

        // 카메라의 y회전만으로 새회전을 생성
        // 카메라의 y회전을 이동 방향에 적용
        movementDirection = Quaternion.AngleAxis(cameraTransform.rotation.eulerAngles.y, Vector3.up) * movementDirection;
        movementDirection.Normalize();

        Vector3 velocity = movementDirection * speed;

        characterController.Move(velocity * Time.deltaTime);

        if (movementDirection != Vector3.zero)
        {
            Quaternion toRotation = Quaternion.LookRotation(movementDirection, Vector3.up);
            transform.rotation = Quaternion.RotateTowards(transform.rotation, toRotation, rotationSpeed * Time.deltaTime);
        }
    }

    private void OnApplicationFocus(bool focus)
    {
        // 어플리케이션이 포커스를 받으면 커서를 숨긴다.
        if (focus)
        {
            Cursor.lockState = CursorLockMode.Locked;
        }
        else
        {
            Cursor.lockState = CursorLockMode.None;
        }
    }
}</code></pre>
<blockquote>
<p><strong>Quaternion.AngleAxis</strong>
public static Quaternion AngleAxis (float angle, Vector3 axis);
axis을 기준으로 angle만큼 회전시킨 회전량
<code>angle: 회전량</code>
<code>axis: 기준축</code></p>
</blockquote>
<h2 id="카메라-정렬">카메라 정렬</h2>
<p>캐릭터와 같은 방향을 향하도록 카메라를 자동으로 정렬시키는 기능을 추가해보자.</p>
<h3 id="binding-mode">Binding Mode</h3>
<p>FreeLock 카메라의 Binding Mode 값을 <code>Lock To Target On Assign</code>으로 변경한다.
<img src="https://velog.velcdn.com/images/code_null/post/00cb0255-8b10-444c-8817-a4647e625016/image.png" alt=""></p>
<h3 id="recenter-to-target-heading">Recenter to Target Heading</h3>
<p>다시 중심 맞추기의 <code>Enabled</code> 값을 활성화한다.
<img src="https://velog.velcdn.com/images/code_null/post/12a1e6bc-22e8-4b08-9275-a15a819d0e91/image.png" alt=""></p>
<h2 id="게임패드">게임패드</h2>
<p>게임패드의 오른쪽 썸스틱을 이용하여 마우스와 같은 기능을 수행하도록 해보자.</p>
<p>Edit-&gt;Project Settings-&gt;Input Manager로 들어가 <code>Mouse X</code>, <code>Mouse Y</code>를 복사하고 다음 설정을 적용한다.</p>
<p><img src="https://velog.velcdn.com/images/code_null/post/06ffba5b-3384-4def-b3d2-9477a3fc6c66/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity3D] 1D 블렌드 트리]]></title>
            <link>https://velog.io/@code_null/Unity3D-1D-%EB%B8%94%EB%A0%8C%EB%93%9C-%ED%8A%B8%EB%A6%AC</link>
            <guid>https://velog.io/@code_null/Unity3D-1D-%EB%B8%94%EB%A0%8C%EB%93%9C-%ED%8A%B8%EB%A6%AC</guid>
            <pubDate>Thu, 02 May 2024 07:48:50 GMT</pubDate>
            <description><![CDATA[<h1 id="1d-블렌드-트리">1D 블렌드 트리</h1>
<hr>
<p>블렌드 유형이 1D라면 블렌딩이 단일 매개변수롤 제어된다는 것을 의미한다. 보통 사용의 목적은 속도를 기준으로 블렌딩할 때이다.</p>
<p><img src="https://velog.velcdn.com/images/code_null/post/0a9d8758-2b77-4740-9dc3-2de24e758293/image.png" alt=""></p>
<p>이동 방향과 속도를 기준으로 블렌딩하려는 경우</p>
<ul>
<li>2D Simple Directional</li>
<li>2D Freeform Directional</li>
<li>2D Freeform Cartesian</li>
</ul>
<p>을 사용한다.</p>
<h2 id="블렌딩">블렌딩</h2>
<p>Input Magnitude 매개변수의 값에 따라 Idle, Walking, Running 애니메이션을 자연스럽게 블렌딩하도록 설정할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/code_null/post/259d81d1-a23f-45f0-9b87-b653632abaed/image.png" alt=""></p>
<h2 id="스크립트">스크립트</h2>
<p>Input Magnitude 매개변수를 조작하여 블렌딩 애니메이션을 조작해보자.
<a href="https://velog.io/@code_null/Unity3D-Character-Controller-%EC%A0%90%ED%94%84">점프 구현</a> 스크립트의 이동부분을 수정한다.</p>
<pre><code class="language-csharp">using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerMovement : MonoBehaviour
{
    // 최대 속도 정의
    public float maximumSpeed = 3;

    void Update()
    {
        // ...생략

        Vector3 movementDirection = new Vector3(horizontalInput, 0, verticalInput);
        // 입력 크기와 최대 속도에 따라 캐릭터가 움직이는 속도 정의
        float inputMagnitude = Mathf.Clamp01(movementDirection.magnitude);
        float speed = inputMagnitude * maximumSpeed;

        // 키보드에서의 걷기 행동 추가
        if(Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift))
        {
            inputMagnitude *= 0.5f;
        }

        animator.SetFloat(&quot;Input Magnitude&quot;, inputMagnitude);

        movementDirection.Normalize();

        // ...생략
    }
}</code></pre>
<h2 id="문제-해결">문제 해결</h2>
<p>걷기와 달리기 사이의 애니메이션이 스냅되는 문제를 해결해보자.</p>
<pre><code class="language-csharp">// 수정
animator.SetFloat(&quot;Input Magnitude&quot;, inputMagnitude, 0.05f, Time.deltaTime);</code></pre>
<blockquote>
<p>애니메이션이 너무 빨리 바뀌지않도록 감쇠하는 효과가 있도록 dampTime을 설정한다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[기술] Mono vs IL2CPP]]></title>
            <link>https://velog.io/@code_null/%EA%B8%B0%EC%88%A0-Mono-vs-IL2CPP</link>
            <guid>https://velog.io/@code_null/%EA%B8%B0%EC%88%A0-Mono-vs-IL2CPP</guid>
            <pubDate>Thu, 02 May 2024 07:13:30 GMT</pubDate>
            <description><![CDATA[<h1 id="mono-vs-il2cpp">Mono vs IL2CPP</h1>
<hr>
<p><strong>Mono</strong> :  JIT 컴파일을 사용하여 런타임 시점에 요청 시 코드를 컴파일한다.
<strong>IL2CPP</strong> : AOT 컴파일을 사용하여 실행 전에 전체 애플리케이션을 컴파일한다.</p>
<p><code>Mono</code>는 사용하기 쉽고 컴파일 시간이 빠르고, <code>IL2CPP</code>는 컴파일 시간이 느리지만 플랫폼에 맞는 최대 성능을 낼 수 있다.</p>
<blockquote>
<p><strong>JIT(Just-In-Time)</strong>
프로그램에서 코드가 실행되는 시점에 기계어로 번역하는 컴파일 기법
<code>동적 번역</code> <code>런타임 컴파일</code></p>
</blockquote>
<blockquote>
<p><strong>AOT(Ahead-Of-Time)</strong>
런타임 시 수행해야 할 작업량을 줄이기 위해 빌드 타임에서 미리 컴파일하는 기법</p>
</blockquote>
<hr>
<h2 id="mono">Mono</h2>
<p>c#으로 작성된 스크립트를 IL(Intermediate Language) 코드로 컴파일한 후 런타임 환경에서 실행된다. </p>
<p><code>AOT 컴파일을 지원하지 않는 플랫폼에 적합</code></p>
<hr>
<h2 id="il2cpp">IL2CPP</h2>
<p>IL(Intermediate Language) 2(to) CPP(C++)의 약자로 IL(Intermediate Language) 코드를 c++ 코드롤 전환하고, 타겟 플램폼에서 직접 실행될 수 잇는 적합한 네이티브 바이너리 파일로 컴파일한다.</p>
<p><code>Mono나 JIT 컴파일을 지원하지 않는 플랫폼에 적합</code></p>
<h3 id="il2cpp의-장점">IL2CPP의 장점</h3>
<ul>
<li>c++을 지원하는 플랫폼이 더 많기 때문에 다양한 플랫폼을 지원할 수 있다.</li>
<li>타겟 플랫폼에 최적화된 c++ 코드를 생성하기 때문에 성능이 향상된다.</li>
<li>역설계하기 어려운 컴파일된 c++로 대체되기 때문에 보안성이 개선된다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity3D] Cinemachine을 이용한 카메라 충돌 처리]]></title>
            <link>https://velog.io/@code_null/Unity3D-Cinemachine%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%B9%B4%EB%A9%94%EB%9D%BC-%EC%B6%A9%EB%8F%8C-%EC%B2%98%EB%A6%AC</link>
            <guid>https://velog.io/@code_null/Unity3D-Cinemachine%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%B9%B4%EB%A9%94%EB%9D%BC-%EC%B6%A9%EB%8F%8C-%EC%B2%98%EB%A6%AC</guid>
            <pubDate>Wed, 01 May 2024 08:09:40 GMT</pubDate>
            <description><![CDATA[<h1 id="시야-처리">시야 처리</h1>
<hr>
<p>장애물에 막힌 카메라의 충돌 처리를 Cinemachine으로 해결해보자.</p>
<h2 id="cinemachinecollider">CinemachineCollider</h2>
<p>버츄얼 카메라에서 확장 컴포넌트를 추가한다.
<img src="https://velog.velcdn.com/images/code_null/post/e58b1e86-f00d-40c1-ba03-c1faa9fa77bc/image.png" alt=""></p>
<h2 id="전략-선택">전략 선택</h2>
<p>충돌을 처리할 전략과 댐핑시간을 선택한다.</p>
<p><img src="https://velog.velcdn.com/images/code_null/post/ab48c60f-136c-4f5e-a206-f04eb8dee716/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/code_null/post/5e0f3f49-57f9-4c10-bda5-f2c15489dc70/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity3D] 점프 유예 시간]]></title>
            <link>https://velog.io/@code_null/Unity3D-%EC%A0%90%ED%94%84-%EC%9C%A0%EC%98%88-%EC%8B%9C%EA%B0%84</link>
            <guid>https://velog.io/@code_null/Unity3D-%EC%A0%90%ED%94%84-%EC%9C%A0%EC%98%88-%EC%8B%9C%EA%B0%84</guid>
            <pubDate>Wed, 01 May 2024 07:36:37 GMT</pubDate>
            <description><![CDATA[<h1 id="아이디어">아이디어</h1>
<hr>
<p>점프 후 착지한 후 곧바로 점프하려고 할 때 의도와는 다르게 입력이 씹히거나, 높은 곳에서 아래로 점프할 때 생각했던 타이밍에 점프가 되지 않아 불쾌한 조작 경험을 줄 수 있는 환경을 해결하기 위해 점프를 하는데에 유예 시간을 적용하여 좀 더 부드럽고, 불합리하다고 느끼지 않도록 개선해보도록 하겠다.</p>
<h2 id="스크립트-수정">스크립트 수정</h2>
<p><a href="https://velog.io/@code_null/Unity3D-Character-Controller-%EC%A0%90%ED%94%84">점프 구현</a> 스크립트를 수정한다.</p>
<pre><code class="language-csharp">/* 
 * 변수 추가
 */

// 점프버튼 유예 시간
public float jumpButtonGracePeriod = 0.2f;

// 마지막 땅 접지 시간
private float? lastGroundedTime;
// 점프 버튼을 누른 시간
private float? jumpButtonPressedTime;


/* 
 * 업데이트 메서드 수정 
 */

void Update()
{
    //...중략

    if (characterController.isGrounded)
    {
        lastGroundedTime = Time.time;
    }
    if (Input.GetButtonDown(&quot;Jump&quot;))
    {
        jumpButtonPressedTime = Time.time;
    }

    if (Time.time - lastGroundedTime &lt;= jumpButtonGracePeriod)
    {
        characterController.stepOffset = originalStepOffset;
        ySpeed = -0.8f;

        if (Time.time - jumpButtonPressedTime &lt;= jumpButtonGracePeriod)
        {
            ySpeed = jumpSpeed;

            lastGroundedTime = null;
            jumpButtonPressedTime = null;
        }
    }
    else
    {
        characterController.stepOffset = 0;
    }

    //...중략
}</code></pre>
<p><code>버튼을 누른 시점</code> <code>땅에 접지 한 시점</code> 을 저장하여, 유예 시간 전에 버튼이 눌렸다면 점프를 실행할 수 있도록 스크립트를 수정했다.</p>
<blockquote>
<p>Time.time - <code>Null 허용 값 형식</code>의 연산 결과는 Null이다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity3D] Character Controller 점프]]></title>
            <link>https://velog.io/@code_null/Unity3D-Character-Controller-%EC%A0%90%ED%94%84</link>
            <guid>https://velog.io/@code_null/Unity3D-Character-Controller-%EC%A0%90%ED%94%84</guid>
            <pubDate>Wed, 01 May 2024 07:28:09 GMT</pubDate>
            <description><![CDATA[<h1 id="점프">점프</h1>
<hr>
<p>Character Controller를 이용하여 점프를 구현해보자.</p>
<h2 id="스크립트">스크립트</h2>
<p><code>PlayerMovement</code> 스크립트를 이어서 사용한다.</p>
<pre><code class="language-csharp">using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerMovement : MonoBehaviour
{
    public float speed = 5;
    public float rotationSpeed = 720;
    // 점프 속도 설정
    public float jumpSpeed = 5;

    private CharacterController characterController;
    // 현재 적용받는 y의 속도값
    private float ySpeed;
    // CharacterController의 StepOffset 설정값
    private float originalStepOffset;

    private void Start()
    {
        characterController = GetComponent&lt;CharacterController&gt;();
        // 설정된 StepOffset값 저장
        originalStepOffset = characterController.stepOffset;
    }

    void Update()
    {
        //..증략

        // 중력에 의한 ySpeed 감소
        ySpeed += Physics.gravity.y * Time.deltaTime;

        // 지상에 있는 경우만 점프
        if(characterController.isGrounded)
        {
            // 기존 설정값으로 다시 설정
            characterController.stepOffset = originalStepOffset;
            // 안정된 점프를 위한 초기화
            ySpeed = -0.8f;

            // 점프
            if (Input.GetButtonDown(&quot;Jump&quot;))
            {
                ySpeed = jumpSpeed;
            }
        }
        else
        {
            // 점프 중에는 어딜 올라갈 필요가 없으니 0으로 설정
            characterController.stepOffset = 0;
        }

        // 점프 속도를 포함한 velocity 계산
        Vector3 velocity = movementDirection * magnitude;
        velocity.y = ySpeed;

        // Move() 메서드로 변경
        characterController.Move(velocity * Time.deltaTime);

        //...중략
    }
}</code></pre>
<blockquote>
<p><strong>CharacterController.Move</strong>
public CollisionFlags Move (Vector3 motion);
<code>Grounded 체크 가능</code>
<code>중력에 영향 받지 않음</code>
<code>Time.deltaTime은 내장 되어있지 않음</code></p>
</blockquote>
<h2 id="문제-해결">문제 해결</h2>
<h3 id="yspeed가-계속-감소하는-문제">ySpeed가 계속 감소하는 문제</h3>
<p>ySpeed가 계속 감소하기 때문에 높은 곳에서 떨어질 때 지면에 바로 스냅하는 문제가 발생한다. 이를 해결하기 위해 땅에 접지해있을 때는 ySpeed를 초기화해주도록 한다.</p>
<pre><code class="language-csharp">if(characterController.isGrounded)
{
    ySpeed = -0.8f;
}</code></pre>
<blockquote>
<p>여기서 ySpeed를 0이 아닌 -0.8로 초기화해주는 이유는 isGrounded 체크의 불안정성으로 ySpeed의 값이 더욱 감소했다가 0으로 초기화되는 문제를 해결하기 위함이다.</p>
</blockquote>
<h4 id="벽으로-점프했을-때-문제">벽으로 점프했을 때 문제</h4>
<p>벽으로 점프하게 되면 Character Controller의 Step Offset 속성으로 인해 계단을 타는 듯한 현상이 발생하게 된다. 이를 해결하기 위해 낙하 중일 때는 Step Offset의 값을 0으로 설정해준다.</p>
<pre><code class="language-csharp">if(characterController.isGrounded)
{
    characterController.stepOffset = originalStepOffset;
}
else
{
    characterController.stepOffset = 0;
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity3D] Follow Camera]]></title>
            <link>https://velog.io/@code_null/Unity3D-Follow-Camera</link>
            <guid>https://velog.io/@code_null/Unity3D-Follow-Camera</guid>
            <pubDate>Tue, 30 Apr 2024 12:37:11 GMT</pubDate>
            <description><![CDATA[<h1 id="cinemachine">Cinemachine</h1>
<p>시네머신을 이용하여 간단한 Follow Camera를 만들어보자.</p>
<h2 id="구성">구성</h2>
<p>Window-&gt;Package Manager를 통해 Cinemachine 설치하고 Virtual Camera 생성
<img src="https://velog.velcdn.com/images/code_null/post/6a0e75bd-9b66-4995-8659-86a3e339a5de/image.png" alt=""></p>
<h2 id="카메라-설정">카메라 설정</h2>
<p>팔로우 타겟 지정 (회전은 원하지 않으므로 <code>Look At</code> 타겟은 지정하지 않음)
<img src="https://velog.velcdn.com/images/code_null/post/abeaa7e3-93ce-472e-967a-fa814c0d53d4/image.png" alt=""></p>
<p>카메라를 움직이는 알고리즘 선택 (Framing Transposer)
<img src="https://velog.velcdn.com/images/code_null/post/19aab2b8-3f39-4c9b-aefd-b2f8d8cffbe5/image.png" alt=""></p>
<p>카메라의 x축 앵글을 45도로 수정</p>
<br>

<p><strong>최종 화면</strong>
<img src="https://velog.velcdn.com/images/code_null/post/4f791228-7063-4852-838d-638956cdbef2/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity3D] Character Controller를 이용한 이동]]></title>
            <link>https://velog.io/@code_null/Unity3D-Character-Controller%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%9D%B4%EB%8F%99</link>
            <guid>https://velog.io/@code_null/Unity3D-Character-Controller%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%9D%B4%EB%8F%99</guid>
            <pubDate>Tue, 30 Apr 2024 11:47:15 GMT</pubDate>
            <description><![CDATA[<h1 id="character-controller-이동">Character Controller 이동</h1>
<hr>
<p><strong>캐릭터 움직임의 기초</strong>를 제공하는 <code>Character Controller</code> 컴포넌트를 추가하여 장애물을 고려한 이동 처리를 구현해보자.</p>
<h2 id="씬-구성">씬 구성</h2>
<p>경사진 지역과 계단 지역을 만들어준다.
<img src="https://velog.velcdn.com/images/code_null/post/8c00ab4c-d804-419c-99ce-91518184b9ff/image.png" alt=""></p>
<h2 id="플레이어-오브젝트-구성">플레이어 오브젝트 구성</h2>
<p>플레이어 오브젝트에 Character Controller 컴포넌트를 부착한다.
<img src="https://velog.velcdn.com/images/code_null/post/bd8817ff-20bc-4b40-9c8b-71b8a9c5a0c3/image.png" alt=""></p>
<h2 id="스크립트-수정">스크립트 수정</h2>
<p><code>Character Controller</code> 컴포넌트를 이용하여 이동하도록 코드를 수정한다.</p>
<pre><code class="language-csharp">using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerMovement : MonoBehaviour
{
    //...중략

    private CharacterController characterController;

    private void Start()
    {
        characterController = GetComponent&lt;CharacterController&gt;();
    }

    void Update()
    {
        //...중략

        Vector3 movementDirection = new Vector3(horizontalInput, 0, verticalInput);
        float magnitude = Mathf.Clamp01(movementDirection.magnitude) * speed;
        movementDirection.Normalize();

        characterController.SimpleMove(movementDirection * magnitude);

        //...중략
    }
}</code></pre>
<blockquote>
<p><strong>CharacterController.SimpleMove</strong>
public bool SimpleMove (Vector3 speed);
<code>중력을 적용 받음</code>
<code>Grounded 체크 가능</code>
<code>y축의 속도는 무시됨</code>
<code>Time.deltaTime은 내장되어 있음</code></p>
</blockquote>
<h2 id="character-controller-속성">Character Controller 속성</h2>
<p><a href="https://docs.unity3d.com/kr/2021.1/Manual/class-CharacterController.html">유니티 레퍼런스</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Unity3D] 게임패드 이동 입력 문제]]></title>
            <link>https://velog.io/@code_null/Unity3D-%EA%B2%8C%EC%9E%84%ED%8C%A8%EB%93%9C-%EC%9D%B4%EB%8F%99-%EC%9E%85%EB%A0%A5-%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@code_null/Unity3D-%EA%B2%8C%EC%9E%84%ED%8C%A8%EB%93%9C-%EC%9D%B4%EB%8F%99-%EC%9E%85%EB%A0%A5-%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Tue, 30 Apr 2024 11:21:01 GMT</pubDate>
            <description><![CDATA[<h1 id="문제">문제</h1>
<hr>
<p>대각선으로 움직일 때 속도가 빨라지는 부분을 해결하기 위해 이동 벡터를 정규화시켜주었다. 이는 게임패드로 조작 시 왼쪽 썸스틱의 기울기와는 상관없이 일정한 움직임을 갖도록 했다. 이를 해결해보자.</p>
<blockquote>
<p>이동 벡터를 정규화하지 않고 게임패드로 움직여보면 썸스틱의 기울기에 따라 이동속도가 달라지는 걸 볼 수 있다.</p>
</blockquote>
<h2 id="해결책">해결책</h2>
<ol>
<li>이동 벡터를 정규화하기 전 이동 벡터의 크기를 구한다.<pre><code class="language-csharp">float magnitude = movementDirection.magnitude;</code></pre>
</li>
<li>이동 벡터의 크기가 1을 넘지 못하도록 처리한다.<pre><code class="language-csharp">magnitude = Mathf.Clamp01(magnitude);</code></pre>
</li>
<li>이동속도와 곱해준다.<pre><code class="language-csharp">transform.Translate(movementDirection * magnitude * speed * Time.deltaTime, Space.World);</code></pre>
<br>

</li>
</ol>
<p><strong><code>이동 벡터의 크기</code>를 이용하여 게임패드 왼쪽 썸스틱의 기울기에 따라 이동속도가 조절되도록 하여 문제를 해결할 수 있다.</strong></p>
]]></description>
        </item>
    </channel>
</rss>