+ Best aligns every requirement with implementation.
- Stability bounds and momentum claims are oversimplified.
a Game Physics Logic Architect. I will provide you with a specific gameplay mechanic idea, and you will output the complete technical implementation l
| Category | Development › Coding |
|---|---|
| Tags | DraftingAnalyzingDeveloperCode |
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."
Used to design game physics logic such as collision, momentum, and input-response latency. It excludes world-building or dialogue, with a default example of a Unity C# grapple hook.
Claude is the most complete and physically coherent. Gemini double-counts forces despite its detail, while ChatGPT is fatally truncated.
+ Best aligns every requirement with implementation.
- Stability bounds and momentum claims are oversimplified.
+ Broadly connects formulas, states, and code.
- Double-counts centripetal force and mismatches release logic.
+ Correctly avoids adding centrifugal force twice.
- The truncated code is neither complete nor buildable.
| Criterion | ChatGPT | Claude | Gemini | Leader |
|---|---|---|---|---|
| Instruction following | 5 | 10 | 9 | Claude +11% |
| Accuracy | 6 | 8 | 6 | Claude +33% |
| Specificity | 8 | 10 | 9 | Claude +11% |
| Structure | 8 | 9 | 9 | Tie |
| Right length | 3 | 8 | 7 | Claude +14% |
Scored 1–10 by gpt-5.6-sol with model names hidden (2026-09-24). This is an AI review, not a measurement.
We gave three models the same input and copied their answers unedited. Each ran in its CLI (an agent harness), and answers in the ChatGPT or Claude apps or on the web may differ. Outputs are in Korean.
기준 환경은 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의 질량과 감쇠 계수를 함께 조정해야 합니다.
| 12-Month AI and Computer Vision Roadmap for Defense Applications | |
| 2046 Puzzle Game Challenge | |
| 21st.dev component prompt | |
| 3D FACTORY | |
| 3D FPS Game |