+ 요구 요소를 실제 구현과 가장 치밀하게 맞췄다.
- 안정성 상한과 각운동량 보존 설명이 단순화됐다.
게임플레이 메커닉 아이디어와 언어를 넣으면 물리 공식, 상태 머신 다이어그램, 구현 코드 중심의 기술 설계를 받습니다.
| 분류 | 개발 › 코딩 |
|---|---|
| 태그 | 초안작성분석개발자코드 |
I want you to act as a Game Physics Logic Architect. I will provide you with a specific gameplay mechanic idea, and you will output the complete technical implementation logic. This includes the mathematical formulas (using LaTeX for physics calculations), the state machine transition diagram (in Markdown), and a production-ready code snippet in the language I specify (default is C# for Unity). Do not provide world-building, lore, or NPC dialogue. Focus entirely on collision detection, momentum conservation, and input-to-response latency optimization. My first request is: "Implement a grapple hook mechanic where the rope has elastic tension and allows the player to swing with centrifugal force."
충돌, 운동량, 입력 반응 지연 같은 게임 물리 로직을 설계할 때 쓰인다. 세계관이나 대사는 제외하고, 기본 예시는 Unity C# 그래플 훅 구현이다.
Claude가 완성도와 물리 일관성에서 가장 우수하다. Gemini는 상세하지만 힘을 중복 계산하고, ChatGPT는 코드 절단이 치명적이다.
+ 요구 요소를 실제 구현과 가장 치밀하게 맞췄다.
- 안정성 상한과 각운동량 보존 설명이 단순화됐다.
+ 수식·상태도·코드를 폭넓게 연결했다.
- 구심력 중복과 해제 수식 불일치가 크다.
+ 원심력을 중복 적용하지 않는 설명이 정확하다.
- 코드가 중간에 잘려 빌드 가능한 구현이 아니다.
| 기준 | ChatGPT | Claude | Gemini | 앞선 모델 |
|---|---|---|---|---|
| 지시 준수 | 5 | 10 | 9 | Claude +11% |
| 정확성 | 6 | 8 | 6 | Claude +33% |
| 구체성 | 8 | 10 | 9 | Claude +11% |
| 구성·가독성 | 8 | 9 | 9 | 동점 |
| 분량 적정성 | 3 | 8 | 7 | Claude +14% |
gpt-5.6-sol가 모델 이름을 가린 채 1~10점으로 채점했습니다 (2026-09-24). 측정값이 아니라 AI 심사 결과입니다.
같은 입력을 세 모델에 넣어 받은 답을 그대로 옮겼습니다. 각 CLI(에이전트 하네스)에서 실행했습니다. ChatGPT·Claude 앱이나 웹에서는 답이 다를 수 있습니다.
기준 환경은 Unity 3D + Rigidbody 기반 플레이어입니다. 로프는 다음 특성을 가집니다.
Update, 물리 처리는 FixedUpdate에서 수행플레이어 위치를 (\mathbf{x}), 그래플 지점을 (\mathbf{a})라 하면:
[ \mathbf{r} = \mathbf{x} - \mathbf{a} ]
[ d = |\mathbf{r}| ]
[ \mathbf{n} = \frac{\mathbf{r}}{d} ]
여기서 (\mathbf{n})은 그래플 지점에서 플레이어를 향하는 단위 벡터입니다.
로프의 기준 길이를 (L_0)라 하면:
[ \Delta L = \max(0, d - L_0) ]
로프가 기준 길이보다 짧으면 장력은 발생하지 않습니다.
Hooke의 법칙과 속도 감쇠를 적용합니다.
[ \mathbf{v}_r = \mathbf{v} \cdot \mathbf{n} ]
-\mathbf{n} \left( k\Delta L + c\max(0, \mathbf{v}_r) \right) ]
장력은 플레이어를 그래플 지점 방향으로 끌어당깁니다.
플레이어 속도를 로프 방향과 접선 방향으로 분해합니다.
[ \mathbf{v}_{radial} = \mathbf{n}(\mathbf{v}\cdot\mathbf{n}) ]
[ \mathbf{v}{tangent} = \mathbf{v} - \mathbf{v}{radial} ]
스윙 운동은 (\mathbf{v}_{tangent})를 보존해야 합니다. 그래플을 시작할 때 로프 방향으로 멀어지는 속도는 제한하고 접선 속도는 유지합니다.
[ \mathbf{v}' = \mathbf{v}_{tangent} + \mathbf{n}\min(0, \mathbf{v}\cdot\mathbf{n}) ]
게임 물리에서는 원심력을 별도의 힘으로 추가하지 않는 것이 안정적입니다. 플레이어가 접선 속도를 유지하면 필요한 구심력이 장력으로 자연스럽게 발생합니다.
필요한 구심력의 크기는:
[ F_c = m\frac{v_t^2}{r} ]
실제 시뮬레이션에서는 탄성 장력이 이 역할을 수행합니다. 따라서 인위적인 원심력을 추가하면 속도가 과도하게 증가할 수 있습니다.
다만 스윙 감각을 강화하기 위해 접선 방향으로 플레이어 입력을 추가할 수 있습니다.
\mathbf{t}{input} F{swing} ]
입력 방향은 로프 방향에 직교하도록 투영합니다.
\frac{ \mathbf{u} - \mathbf{n}(\mathbf{u}\cdot\mathbf{n}) }{ |\mathbf{u} - \mathbf{n}(\mathbf{u}\cdot\mathbf{n})| } ]
stateDiagram-v2
[*] --> Free
Free --> Aim: GrapplePressed
Aim --> Free: GrappleReleased
Aim --> Attached: ValidHit
Attached --> Swinging: TangentialVelocity > SwingThreshold
Attached --> Retracting: ReleasePressed
Attached --> Attached: RopeUnderRestLength
Swinging --> Swinging: RopeTensionActive
Swinging --> Attached: TangentialVelocity <= SwingThreshold
Swinging --> Retracting: ReleasePressed
Attached --> Retracting: InvalidAnchor
Swinging --> Retracting: InvalidAnchor
Retracting --> Free: RopeFullyReleased
Retracting --> Aim: GrapplePressed
상태 의미:
Free: 그래플이 연결되지 않은 일반 이동 상태Aim: 그래플 대상 탐색 상태Attached: 그래플 지점에 연결됐지만 스윙 속도가 낮은 상태Swinging: 접선 속도가 충분히 발생한 스윙 상태Retracting: 그래플 해제 과정에서 로프 제약을 제거하는 상태using UnityEngine;
[RequireComponent(typeof(Rigidbody))]
public sealed class ElasticGrapple : MonoBehaviour
{
private enum GrappleState
{
Free,
Aim,
Attached,
Swinging,
Retracting
}
[Header("References")]
[SerializeField] private Camera playerCamera;
[SerializeField] private Transform aimOrigin;
[Header("Grapple Detection")]
[SerializeField] private LayerMask grappleMask;
[SerializeField] private float maxGrappleDistance = 60f;
[SerializeField] private float grappleRadius = 0.12f;
[SerializeField] private QueryTriggerInteraction triggerInteraction =
QueryTriggerInteraction.Ignore;
[Header("Rope Physics")]
[SerializeField] private float ropeRestLength = 18f;
[SerializeField] private float ropeStiffness = 850f;
[SerializeField] private float ropeDamping = 55f;
[SerializeField] private float maximumTension = 18000f;
[Header("Swing Control")]
[SerializeField] private float swingAcceleration = 28f;
[SerializeField] private float radialVelocityLimit = 1.5f;
[SerializeField] private float minimumSwingSpeed = 0.5f;
[SerializeField] private float reelInSpeed = 8f;
[SerializeField] private float reelOutSpeed = 10f;
[SerializeField] private float minimumRopeLength = 3f;
[Header("Release")]
[SerializeField] private float releaseVelocityBoost = 1.05f;
private Rigidbody body;
private GrappleState state = GrappleState.Free;
private Vector3 anchorPoint;
private float currentRopeLength;
private bool grapplePressed;
private bool grappleHeld;
private bool releasePressed;
private Vector2 moveInput;
public bool IsGrappling => state == GrappleState.Attached ||
state == GrappleState.Swinging;
public Vector3 AnchorPoint => anchorPoint;
public float CurrentRopeLength => currentRopeLength;
private void Awake()
{
body = GetComponent<Rigidbody>();
body.interpolation = RigidbodyInterpolation.Interpolate;
body.collisionDetectionMode =
CollisionDetectionMode.ContinuousDynamic;
}
private void Update()
{
// 입력 샘플링은 프레임 단위로 수행해 입력 지연을 줄인다.
moveInput = new Vector2(
Input.GetAxisRaw("Horizontal"),
Input.GetAxisRaw("Vertical")
);
grapplePressed = Input.GetMouseButtonDown(0);
grappleHeld = Input.GetMouseButton(0);
releasePressed = Input.GetMouseButtonUp(0);
if (grapplePressed && state == GrappleState.Free)
{
state = GrappleState.Aim;
}
if (releasePressed && IsGrappling)
{
ReleaseGrapple();
}
}
private void FixedUpdate()
{
switch (state)
{
case GrappleState.Free:
SimulateFreeState();
break;
case GrappleState.Aim:
TryAttach();
break;
case GrappleState.Attached:
case GrappleState.Swinging:
SimulateGrapple();
break;
case GrappleState.Retracting:
SimulateRetracting();
break;
}
grapplePressed = false;
releasePressed = false;
}
private void SimulateFreeState()
{
if (!grappleHeld)
{
return;
}
}
private void TryAttach()
{
if (!grappleHeld)
{
state = GrappleState.Free;
return;
}
Ray ray = new Ray(
aimOrigin != null ? aimOrigin.position : playerCamera.transform.position,
aimOrigin != null ? aimOrigin.forward : playerCamera.transform.forward
);
bool hitSomething = Physics.SphereCast(
ray,
grappleRadius,
out RaycastHit hit,
maxGrappleDistance,
grappleMask,
triggerInteraction
);
if (!hitSomething)
{
state = GrappleState.Free;
return;
}
// 그래플 지점까지 직접 시야가 확보되는지 검증한다.
Vector3 origin = body.worldCenterOfMass;
Vector3 toAnchor = hit.point - origin;
float distance = toAnchor.magnitude;
if (distance <= 0.001f)
{
state = GrappleState.Free;
return;
}
if (Physics.Raycast(
origin,
toAnchor.normalized,
out RaycastHit obstruction,
distance,
grappleMask,
triggerInteraction))
{
// 첫 충돌 지점이 그래플 대상이 아니면 연결하지 않는다.
if (obstruction.collider != hit.collider)
{
state = GrappleState.Free;
return;
}
}
anchorPoint = hit.point;
currentRopeLength = Mathf.Min(ropeRestLength, distance);
RemoveOutwardRadialVelocity();
state = GrappleState.Attached;
}
private void SimulateGrapple()
{
if (!grappleHeld)
{
ReleaseGrapple();
return;
}
Vector3 offset = body.worldCenterOfMass - anchorPoint;
float distance = offset.magnitude;
if (distance <= 0.001f)
{
return;
}
Vector3 ropeDirection = offset / distance;
// 로프 길이 조절 입력
UpdateRopeLength();
// 플레이어가 앵커에서 너무 멀어졌는지 검사
ApplyElasticTension(
ropeDirection,
distance
);
ApplySwingInput(ropeDirection);
UpdateSwingState(ropeDirection);
}
private void ApplyElasticTension(
Vector3 ropeDirection,
float distance)
{
float extension = distance - currentRopeLength;
if (extension <= 0f)
{
return;
}
Vector3 velocity = body.GetPointVelocity(body.worldCenterOfMass);
float radialVelocity = Vector3.Dot(velocity, ropeDirection);
// 로프가 늘어나는 방향으로 움직일 때만 감쇠한다.
float dampingForce = Mathf.Max(0f, radialVelocity) * ropeDamping;
float tensionMagnitude =
ropeStiffness * extension + dampingForce;
tensionMagnitude = Mathf.Min(
tensionMagnitude,
maximumTension
);
Vector3 tensionForce = -ropeDirection * tensionMagnitude;
body.AddForce(
tensionForce,
ForceMode.Force
);
// 탄성력만으로 로프 길이 초과가 남는 경우를 방지하는
// 위치 기반 보정이다.
Vector3 correctedPosition =
anchorPoint + ropeDirection * currentRopeLength;
float penetration = distance - currentRopeLength;
if (penetration > 0f)
{
body.position = Vector3.Lerp(
body.position,
correctedPosition,
0.85f
);
// 앵커에서 멀어지는 속도 제거
velocity = body.linearVelocity;
float outwardSpeed = Vector3.Dot(
velocity,
ropeDirection
);
if (outwardSpeed > radialVelocityLimit)
{
body.linearVelocity =
velocity - ropeDirection *
(outwardSpeed - radialVelocityLimit);
}
}
}
private void ApplySwingInput(Vector3 ropeDirection)
{
Vector3 inputDirection = GetWorldInputDirection();
if (inputDirection.sqrMagnitude <= 0.0001f)
{
return;
}
// 입력 방향에서 로프 방향 성분을 제거해 접선 입력만 남긴다.
Vector3 tangentialInput =
Vector3.ProjectOnPlane(
inputDirection,
ropeDirection
);
if (tangentialInput.sqrMagnitude <= 0.0001f)
{
return;
}
tangentialInput.Normalize();
body.AddForce(
tangentialInput * swingAcceleration,
ForceMode.Acceleration
);
}
private void UpdateSwingState(Vector3 ropeDirection)
{
Vector3 velocity = body.GetPointVelocity(
body.worldCenterOfMass
);
Vector3 tangentialVelocity =
Vector3.ProjectOnPlane(
velocity,
ropeDirection
);
if (tangentialVelocity.magnitude >= minimumSwingSpeed)
{
state = GrappleState.Swinging;
}
else
{
state = GrappleState.Attached;
}
}
private void UpdateRopeLength()
{
float scroll = Input.mouseScrollDelta.y;
if (scroll > 0f)
{
currentRopeLength -= reelInSpeed *
Time.fixedDeltaTime;
}
else if (scroll < 0f)
{
currentRopeLength += reelOutSpeed *
Time.fixedDeltaTime;
}
currentRopeLength = Mathf.Clamp(
currentRopeLength,
minimumRopeLength,
maxGrappleDistance
);
}
private Vector3 GetWorldInputDirection()
{
if (playerCamera == null)
{
return new Vector3(
moveInput.x,
0f,
moveInput.y
);
}
Vector3 forward = playerCamera.transform.forward;
Vector3 right = playerCamera.transform.right;
forward.y = 0f;
right.y = 0f;
forward.Normalize();
right.Normalize();
Vector3 direction =
forward * moveInput.y +
right * moveInput.x;
return Vector3.ClampMagnitude(direction, 1f);
}
private void RemoveOutwardRadialVelocity()
{
Vector3 offset =
body.worldCenterOfMass - anchorPoint;
if (offset.sqrMagnitude <= 0.0001f)
{
return;
}
Vector3 ropeDirection = offset.normalized;
Vector3 velocity = body.linearVelocity;
float outwardSpeed = Vector3.Dot(
velocity,
ropeDirection
);
if (outwardSpeed > 0f)
{
body.linearVelocity =
velocity - ropeDirection * outwardSpeed;
}
}
private void ReleaseGrapple()
{
if (!IsGrappling)
{
state = GrappleState.Free;
return;
}
Vector3 offset =
body.worldCenterOfMass - anchorPoint;
if (offset.sqrMagnitude > 0.0001f)
{
Vector3 ropeDirection = offset.normalized;
Vector3 velocity = body.linearVelocity;
// 해제 순간 접선 속도는 유지하고,
// 바깥 방향 속도만 선택적으로 강화한다.
Vector3 tangentialVelocity =
Vector3.ProjectOnPlane(
velocity,
ropeDirection
);
Vector3 outwardVelocity =
Vector3.Project(
velocity,
ropeDirection
);
body.linearVelocity =
tangentialVelocity * releaseVelocityBoost +
outwardVelocity;
}
state = GrappleState.Retracting;
}
private void SimulateRetracting()
{
state = GrappleState.Free;
}
private void OnDrawGizmosSelected()
{
if (!IsGrappling)
{
return;
}
Gizmos.color = Color.yellow;
Gizmos.DrawLine(
transform.position,
anchorPoint
);
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(
anchorPoint,
0.15f
);
}
}
그래플 연결 검사:
SphereCastRaycastRigidbody와 Collider가 처리로프 자체가 월드에 걸려야 한다면 단일 앵커 방식보다 다음 구조가 필요합니다.
Player
└── Rope Segment 0
└── Rope Segment 1
└── Rope Segment 2
└── Anchor
각 세그먼트 사이에 ConfigurableJoint를 배치하고, 꺾이는 지점에서 새 세그먼트를 삽입합니다. 다만 이 방식은 물리 비용과 불안정성이 증가하므로, 일반적인 액션 게임에서는 현재 코드처럼 “직선 로프 + 앵커 직접 연결” 방식이 더 안정적입니다.
핵심 원칙은 다음과 같습니다.
Update()에서 입력을 즉시 샘플링FixedUpdate()에서 해당 입력을 물리 계산에 적용Rigidbody.interpolation = InterpolateContinuousDynamic 충돌 모드 사용FixedUpdate()마다 계산ForceMode.Acceleration으로 질량에 따른 조작감 편차 감소권장 초기값:
ropeRestLength = 18
ropeStiffness = 850
ropeDamping = 55
maximumTension = 18000
swingAcceleration = 28
radialVelocityLimit = 1.5
강성(ropeStiffness)을 지나치게 높이면 물리 진동이 발생하므로, 높은 값을 사용할 때는 Fixed Timestep을 낮추거나 Rigidbody의 질량과 감쇠 계수를 함께 조정해야 합니다.
| 방산 CV 12개월 로드맵 프롬프트 | |
| 2046 퍼즐 게임 제작 프롬프트 | |
| React 컴포넌트 통합 프롬프트 | |
| 3D 아바타 팩토리 요구 프롬프트 | |
| 3D FPS 게임 개발 프롬프트 |