본문 바로가기
디지털노마드

[챗GPT게임개발31] GPT-4, GPT-4o 활용해서 플레이어의 경사면 오르기 구현하기

by 인텔리노마드라이프 2024. 5. 19.
728x90

안녕하세요. 인텔리원스튜디오(IntelliOneStudio)입니다.

오늘은 GPT-4, GPT-4o 활용해서 플레이어가 경사면을 오르는 방법을 구현해볼까해요.

[오늘의 목표] GPT-4, GPT-4o 활용해서 플레이어의 경사면 오르기 구현하기
1. 경사면 감지를 위한 스크립트 구현
2.실행하고 확인하기

 

 ChatGPT 답변 내용 중 제가 별도로 표시한 파란색 내용을 기준으로 읽으시면 좀 더 이해가 쉽게 될거예요.

경사면을 감지하는 방법을 알아봐요.

경사면을 처리하는 방법은 아래 2가지 방법이 있어요.

[두 가지 방법—RaycastCast (예를 들면, Rigidbody2D.Cast)—을 사용하여 경사면과 다른 물리적 상호작용을 감지하고 처리

Raycast 사용

  • 장점: 정밀한 위치와 거리 제어가 가능하며, 매우 특정한 방향으로의 감지가 가능합니다. 경사면 감지와 같은 민감한 상황에서 효과적입니다.
  • 단점: 여러 방향으로의 감지가 필요할 때 여러 Raycast 호출이 필요하며, 더 많은 계산을 요구할 수 있습니다.

Cast 사용 (예: Rigidbody2D.Cast)

  • 장점: 오브젝트의 현재 모양과 크기를 고려하여 범위 내의 모든 충돌을 감지합니다. 이는 플레이어의 실제 형태를 고려하여 보다 넓은 영역에서의 상호작용을 확인할 수 있게 해줍니다.
  • 단점: Raycast보다 덜 정밀할 수 있으며, 방향이나 특정 지점에 대한 민감한 반응을 조정하기 어려울 수 있습니다.

경사 처리를 위한 선택

경사면 처리를 위해 어느 것이 더 나은지는 게임의 특성과 필요에 따라 다릅니다. 예를 들어, 플레이어가 넓은 지면을 따라 움직이면서 여러 경사면과 상호작용해야 하는 경우, Rigidbody2D.Cast 같은 방법이 더 유리할 수 있습니다. 이는 플레이어의 전체적인 형태와 경사면 사이의 상호작용을 더 잘 처리할 수 있기 때문입니다.

반면, 매우 정밀하고 특정한 지점의 경사를 처리해야 하는 상황에서는 Raycast가 더 적합할 수 있습니다. 예를 들어, 플레이어의 발 아래의 특정 지점만을 검사하여 경사를 감지해야 할 때 사용됩니다.

결론

각각의 방법은 그에 맞는 상황과 필요가 있으며, 게임의 특성과 구현하고자 하는 상호작용의 유형에 따라 선택됩니다. 때로는 이 두 기술을 혼합하여 사용하기도 하며, 이는 개발자가 목표하는 게임 플레이 경험에 따라 달라질 수 있습니다. 따라서 경사 처리를 위한 최선의 방법을 결정할 때는 프로젝트의 요구사항을 면밀히 검토하고 적절한 테스트를 통해 최적의 결과를 도출하는 것이 중요합니다.

 

넓은 지면을 따라 움직이는 기능을 구현할 예정이라 Cast를 사용하는 게 좋을 것 같아요.

플레이어가 넓은 지면을 따라 움직이면서 여러 경사면과 상호작용해야 하는 경우, Rigidbody2D.Cast 같은 방법이 더 유리

 

그리고 Unity 에서 제공하는 Platfomer Microgame 에서도 Cast 를 사용하고 있어요. 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace Platformer.Mechanics
{
    /// <summary>
    /// Implements game physics for some in game entity.
    /// </summary>
    public class KinematicObject : MonoBehaviour
    {
        /// <summary>
        /// The minimum normal (dot product) considered suitable for the entity sit on.
        /// </summary>
        public float minGroundNormalY = .65f;

        /// <summary>
        /// A custom gravity coefficient applied to this entity.
        /// </summary>
        public float gravityModifier = 1f;

        /// <summary>
        /// The current velocity of the entity.
        /// </summary>
        public Vector2 velocity;

        /// <summary>
        /// Is the entity currently sitting on a surface?
        /// </summary>
        /// <value></value>
        public bool IsGrounded { get; private set; }

        protected Vector2 targetVelocity;
        protected Vector2 groundNormal;
        protected Rigidbody2D body;
        protected ContactFilter2D contactFilter;
        protected RaycastHit2D[] hitBuffer = new RaycastHit2D[16];

        protected const float minMoveDistance = 0.001f;
        protected const float shellRadius = 0.01f;


        /// <summary>
        /// Bounce the object's vertical velocity.
        /// </summary>
        /// <param name="value"></param>
        public void Bounce(float value)
        {
            velocity.y = value;
        }

        /// <summary>
        /// Bounce the objects velocity in a direction.
        /// </summary>
        /// <param name="dir"></param>
        public void Bounce(Vector2 dir)
        {
            velocity.y = dir.y;
            velocity.x = dir.x;
        }

        /// <summary>
        /// Teleport to some position.
        /// </summary>
        /// <param name="position"></param>
        public void Teleport(Vector3 position)
        {
            body.position = position;
            velocity *= 0;
            body.velocity *= 0;
        }

        protected virtual void OnEnable()
        {
            body = GetComponent<Rigidbody2D>();
            body.isKinematic = true;
        }

        protected virtual void OnDisable()
        {
            body.isKinematic = false;
        }

        protected virtual void Start()
        {
            contactFilter.useTriggers = false;
            contactFilter.SetLayerMask(Physics2D.GetLayerCollisionMask(gameObject.layer));
            contactFilter.useLayerMask = true;
        }

        protected virtual void Update()
        {
            targetVelocity = Vector2.zero;
            ComputeVelocity();
        }

        protected virtual void ComputeVelocity()
        {

        }

        protected virtual void FixedUpdate()
        {
            //if already falling, fall faster than the jump speed, otherwise use normal gravity.
            if (velocity.y < 0)
                velocity += gravityModifier * Physics2D.gravity * Time.deltaTime;
            else
                velocity += Physics2D.gravity * Time.deltaTime;

            velocity.x = targetVelocity.x;

            IsGrounded = false;

            var deltaPosition = velocity * Time.deltaTime;

            var moveAlongGround = new Vector2(groundNormal.y, -groundNormal.x);

            var move = moveAlongGround * deltaPosition.x;

            PerformMovement(move, false);

            move = Vector2.up * deltaPosition.y;

            PerformMovement(move, true);

        }

        void PerformMovement(Vector2 move, bool yMovement)
        {
            var distance = move.magnitude;

            if (distance > minMoveDistance)
            {
                //check if we hit anything in current direction of travel
                var count = body.Cast(move, contactFilter, hitBuffer, distance + shellRadius);
                for (var i = 0; i < count; i++)
                {
                    var currentNormal = hitBuffer[i].normal;

                    //is this surface flat enough to land on?
                    if (currentNormal.y > minGroundNormalY)
                    {
                        IsGrounded = true;
                        // if moving up, change the groundNormal to new surface normal.
                        if (yMovement)
                        {
                            groundNormal = currentNormal;
                            currentNormal.x = 0;
                        }
                    }
                    if (IsGrounded)
                    {
                        //how much of our velocity aligns with surface normal?
                        var projection = Vector2.Dot(velocity, currentNormal);
                        if (projection < 0)
                        {
                            //slower velocity if moving against the normal (up a hill).
                            velocity = velocity - projection * currentNormal;
                        }
                    }
                    else
                    {
                        //We are airborne, but hit something, so cancel vertical up and horizontal velocity.
                        velocity.x *= 0;
                        velocity.y = Mathf.Min(velocity.y, 0);
                    }
                    //remove shellDistance from actual move distance.
                    var modifiedDistance = hitBuffer[i].distance - shellRadius;
                    distance = modifiedDistance < distance ? modifiedDistance : distance;
                }
            }
            body.position = body.position + move.normalized * distance;
        }

    }
}

 

위 코드를 사용해서 실행해볼께요

아래 영상과 같이 경사면을 만들고 실행해보면 경사면에 따라 플레이어가 회전하지 않고 있어요.

[경사면에 따라 플레이어가 회전하지 않아요]

 

경사면을 따라 플레이어도 회전시켜볼께요.

플레이어의 경사면 오르기 구현하기

먼저 KinematicObject 를 학습해요.

이후 플레이어 경사면 오르기 구현하기 위해 경사면 판단과 플레이어 회전 메커니즘을 꼼꼼히 봐야해요. 

경사면 판단과 플레이어 회전 메커니즘

1. PerformMovement 메소드 개요: PerformMovement 메소드는 두 개의 주요 매개변수를 사용합니다: 이동 벡터(move)와 Y축 이동 여부를 나타내는 불린 값(yMovement). 이 메소드는 move 벡터에 따라 물체를 이동시키고, 이동 중에 충돌한 표면의 법선 벡터를 사용하여 플레이어의 상태를 업데이트합니다.


2.충돌 검사 및 법선 벡터 계산
:body.Cast 메소드는 물체가 이동할 때 충돌하는 모든 표면을 검사합니다. 충돌된 표면의 정보는 hitBuffer 배열에 저장됩니다.

var count = body.Cast(move, contactFilter, hitBuffer, distance + shellRadius);



3. 법선 벡터를 통한 경사면 판단:각 충돌 표면의 법선 벡터(currentNormal)를 통해 현재 표면이 바닥(ground)인지 판단합니다. 여기서 currentNormal.y > minGroundNormalY 조건을 통해 경사면이 충분히 평평한지 확인합니다. minGroundNormalY는 바닥으로 간주할 수 있는 최소한의 법선 벡터 Y 값을 나타냅니다.

var currentNormal = hitBuffer[i].normal;

if (currentNormal.y > minGroundNormalY)
{
    IsGrounded = true;
}



4. 플레이어 회전 계산
:

playerAngle: 현재 플레이어의 Z축 회전 각도입니다.

surfaceAngle: 현재 표면의 각도로, 법선 벡터를 사용하여 계산됩니다.

angleDifference: 현재 플레이어 각도와 표면 각도 사이의 차이입니다.

var playerAngle = transform.rotation.eulerAngles.z;
float surfaceAngle = Mathf.Atan2(currentNormal.y, currentNormal.x) * Mathf.Rad2Deg - 90;
float angleDifference = Mathf.Abs(Mathf.DeltaAngle(playerAngle, surfaceAngle));


5. 플레이어 회전 조정
:

currentNormal.y >= 0.99f: 표면이 거의 수평이면 플레이어의 회전을 초기화합니다.

angleDifference > 5: 표면 각도와 플레이어 각도 차이가 5도 이상이면 플레이어를 표면 각도에 맞게 회전시킵니다. Quaternion.Lerp를 사용하여 현재 회전에서 목표 회전으로 부드럽게 전환합니다.

if (currentNormal.y >= 0.99f)
{
    ResetPlayerRotation();
}
else if (angleDifference > 5) // 예를 들어, 각도 차이 임계값을 10도로 설정
{
    Quaternion currentRotation = transform.rotation;
    Quaternion targetRotation = Quaternion.Euler(0, 0, surfaceAngle);
    transform.rotation = Quaternion.Lerp(currentRotation, targetRotation, Time.deltaTime * 5); // 스무딩 적용
}


6. ResetPlayerRotation 메소드
:이 메소드는 플레이어의 회전을 초기화하는데 사용됩니다. Quaternion.Lerp를 사용하여 현재 회전에서 초기 회전(0도)으로 부드럽게 전환합니다.

void ResetPlayerRotation()
{
    Quaternion currentRotation = transform.rotation;
    Quaternion targetRotation = Quaternion.Euler(0, 0, 0);
    transform.rotation = Quaternion.Lerp(currentRotation, targetRotation, Time.deltaTime * 5);
}

 

 

위 내용을 바탕으로 아래 스크립트를 완성했어요.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace Platformer.Mechanics
{
    /// <summary>
    /// Implements game physics for some in game entity.
    /// </summary>
    public class KinematicObject : MonoBehaviour
    {
        /// <summary>
        /// The minimum normal (dot product) considered suitable for the entity sit on.
        /// </summary>
        public float minGroundNormalY = .65f;

        /// <summary>
        /// A custom gravity coefficient applied to this entity.
        /// </summary>
        public float gravityModifier = 1f;

        /// <summary>
        /// The current velocity of the entity.
        /// </summary>
        public Vector2 velocity;

        /// <summary>
        /// Is the entity currently sitting on a surface?
        /// </summary>
        /// <value></value>
        public bool IsGrounded { get; private set; }

        protected Vector2 targetVelocity;
        protected Vector2 groundNormal;
        protected Rigidbody2D body;
        protected ContactFilter2D contactFilter;
        protected RaycastHit2D[] hitBuffer = new RaycastHit2D[16];

        protected const float minMoveDistance = 0.001f;
        protected const float shellRadius = 0.01f;


        /// <summary>
        /// Bounce the object's vertical velocity.
        /// </summary>
        /// <param name="value"></param>
        public void Bounce(float value)
        {
            velocity.y = value;
        }

        /// <summary>
        /// Bounce the objects velocity in a direction.
        /// </summary>
        /// <param name="dir"></param>
        public void Bounce(Vector2 dir)
        {
            velocity.y = dir.y;
            velocity.x = dir.x;
        }

        /// <summary>
        /// Teleport to some position.
        /// </summary>
        /// <param name="position"></param>
        public void Teleport(Vector3 position)
        {
            body.position = position;
            velocity *= 0;
            body.velocity *= 0;
        }

        protected virtual void OnEnable()
        {
            body = GetComponent<Rigidbody2D>();
            body.isKinematic = true;
        }

        protected virtual void OnDisable()
        {
            body.isKinematic = false;
        }

        protected virtual void Start()
        {
            contactFilter.useTriggers = false;
            contactFilter.SetLayerMask(Physics2D.GetLayerCollisionMask(gameObject.layer));
            contactFilter.useLayerMask = true;
        }

        protected virtual void Update()
        {
            targetVelocity = Vector2.zero;
            ComputeVelocity();
        }

        protected virtual void ComputeVelocity()
        {

        }

        protected virtual void FixedUpdate()
        {
            if (velocity.y < 0)
                velocity += gravityModifier * Physics2D.gravity * Time.deltaTime;
            else
                velocity += Physics2D.gravity * Time.deltaTime;

            velocity.x = targetVelocity.x;

            IsGrounded = false;

            var deltaPosition = velocity * Time.deltaTime;

            var moveAlongGround = new Vector2(groundNormal.y, -groundNormal.x);

            var move = moveAlongGround * deltaPosition.x;

            PerformMovement(move, false);

            move = Vector2.up * deltaPosition.y;

            PerformMovement(move, true);
        }

        void PerformMovement(Vector2 move, bool yMovement)
        {
            var distance = move.magnitude;

            if (distance > minMoveDistance)
            {
                var count = body.Cast(move, contactFilter, hitBuffer, distance + shellRadius);
                for (var i = 0; i < count; i++)
                {
                    var currentNormal = hitBuffer[i].normal;

                    if (currentNormal.y > minGroundNormalY)
                    {
                        IsGrounded = true;

                        var playerAngle = transform.rotation.eulerAngles.z;
                        float surfaceAngle = Mathf.Atan2(currentNormal.y, currentNormal.x) * Mathf.Rad2Deg - 90;
                        float angleDifference = Mathf.Abs(Mathf.DeltaAngle(playerAngle, surfaceAngle));

                        if (currentNormal.y >= 0.99f)
                        {
                            ResetPlayerRotation();
                        }
                        else if (angleDifference > 5) // 예를 들어, 각도 차이 임계값을 10도로 설정
                        {
                            Quaternion currentRotation = transform.rotation;
                            Quaternion targetRotation = Quaternion.Euler(0, 0, surfaceAngle);
                            transform.rotation = Quaternion.Lerp(currentRotation, targetRotation, Time.deltaTime * 5); // 스무딩 적용
                        }

                        if (yMovement)
                        {
                            groundNormal = currentNormal;
                            currentNormal.x = 0;
                        }
                    }
                    if (IsGrounded)
                    {
                        var projection = Vector2.Dot(velocity, currentNormal);
                        if (projection < 0)
                        {
                            velocity = velocity - projection * currentNormal;
                        }
                    }
                    else
                    {
                        velocity.x *= 0;
                        velocity.y = Mathf.Min(velocity.y, 0);
                    }
                    var modifiedDistance = hitBuffer[i].distance - shellRadius;
                    distance = modifiedDistance < distance ? modifiedDistance : distance;
                }
            }
            body.position = body.position + move.normalized * distance;
        }

        void ResetPlayerRotation()
        {
            Quaternion currentRotation = transform.rotation;
            Quaternion targetRotation = Quaternion.Euler(0, 0, 0);
            transform.rotation = Quaternion.Lerp(currentRotation, targetRotation, Time.deltaTime * 5);
        }
    }
}

 

이제 수정 사항을 반영하고 실행해볼께요.

경사면을 오를 때 플레이어 동작이 잘 나오네요

플레이어가 경사면을 오르고 내릴 때 경사면의 기울기에 따라 플레이어의 회전도 잘 되요.

[경사면에 따라 플레이어가 회전해요]

 

오늘은 GPT-4, GPT-4o 활용해서 플레이어가 경사면을 오르는 방법을 구현해봤어요.

 

[인텔리노마드라이프와 함께 행복하세요 : 인텔리원스튜디오]

 

지금까지,

언제나 성장하는 인텔리원스튜디오(IntelliOneStudio)입니다.

감사합니다.

728x90