안녕하세요. 인텔리원스튜디오(IntelliOneStudio)입니다.
오늘은 GPT-4, GPT-4o 활용해서 플레이어가 경사면을 오르는 방법을 구현해볼까해요.
[오늘의 목표] GPT-4, GPT-4o 활용해서 플레이어의 경사면 오르기 구현하기
1. 경사면 감지를 위한 스크립트 구현
2.실행하고 확인하기
※ ChatGPT 답변 내용 중 제가 별도로 표시한 파란색 내용을 기준으로 읽으시면 좀 더 이해가 쉽게 될거예요.
경사면을 감지하는 방법을 알아봐요.
경사면을 처리하는 방법은 아래 2가지 방법이 있어요.
[두 가지 방법—Raycast와 Cast (예를 들면, 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)입니다.
감사합니다.
'디지털노마드' 카테고리의 다른 글
[챗GPT게임개발33] GPT-4, GPT-4o 활용해서 급격한 지면 경사에서 자연스럽게 이동하기 (4) | 2024.05.23 |
---|---|
[챗GPT게임개발32] GPT-4, GPT-4o 활용해서 텔리포트 기능 구현하기 (71) | 2024.05.22 |
[챗GPT게임개발30] GPT-4, GPT-4o 활용해서 플레이어의 총알 발사 애니메이션 적용하기(1) (2) | 2024.05.18 |
[챗GPT게임개발29] GPT-4, GPT-4o 활용해서 산탄 효과(Particle System)를 플레이어에 적용하기 (76) | 2024.05.18 |
[챗GPT게임개발28] GPT-4, GPT-4o 활용 Particle System을 사용해서 산탄 효과 구현하기 (2) | 2024.05.18 |