

using UnityEngine;
using UnityEngine.InputSystem;
[RequireComponent(typeof(Rigidbody))]
public class PlayerController : MonoBehaviour
{
[Header("이동")]
[Range(1f, 20f)] public float moveSpeed = 6f;
[Range(1f, 3f)] public float runMultiplier = 1.5f;
[Range(0f, 1f)] public float airControl = 0.4f;
[Range(1f, 30f)] public float rotationSpeed = 10f;
[Header("카메라 좌우 회전 (A/D)")]
[Range(30f, 180f)][Tooltip("A/D 키로 카메라 yaw가 회전하는 속도 (도/초)")]
public float cameraYawSpeed = 90f;
[Header("점프")]
[Range(1f, 20f)] public float jumpForce = 7f;
[Range(1f, 5f)] public float fallMultiplier = 2.5f;
[Range(1f, 5f)] public float lowJumpMultiplier = 2f;
[Header("지면 감지")]
public LayerMask groundLayer = ~0;
[Range(0.05f, 0.5f)] public float groundCheckRadius = 0.1f;
[Range(0.01f, 0.5f)] public float groundCheckDistance = 0.2f;
public Vector3 groundCheckOriginOffset = new Vector3(0f, -0.9f, 0f);
Rigidbody _rb;
InputAction _moveAction;
InputAction _jumpAction;
InputAction _runAction;
bool _isGrounded;
bool _jumpQueued;
bool _isRunning;
float _forwardInput;
float _cameraYaw;
public float CameraYaw => _cameraYaw;
public Vector3 MoveDir { get; private set; }
public Vector3 FaceDir { get; private set; }
void Awake()
{
_rb = GetComponent<Rigidbody>();
_rb.freezeRotation = true;
_cameraYaw = transform.eulerAngles.y;
SetupInputActions();
}
void SetupInputActions()
{
_moveAction = new InputAction("Move", InputActionType.Value, expectedControlType: "Vector2");
_moveAction.AddCompositeBinding("2DVector")
.With("Up", "<Keyboard>/w")
.With("Down", "<Keyboard>/s")
.With("Left", "<Keyboard>/a")
.With("Right", "<Keyboard>/d");
_moveAction.AddCompositeBinding("2DVector")
.With("Up", "<Keyboard>/upArrow")
.With("Down", "<Keyboard>/downArrow")
.With("Left", "<Keyboard>/leftArrow")
.With("Right", "<Keyboard>/rightArrow");
_jumpAction = new InputAction("Jump", InputActionType.Button);
_jumpAction.AddBinding("<Keyboard>/space");
_jumpAction.started += ctx => { _jumpQueued = true; };
_runAction = new InputAction("Run", InputActionType.Button);
_runAction.AddBinding("<Keyboard>/leftShift");
_runAction.AddBinding("<Keyboard>/rightShift");
}
void OnEnable() { _moveAction.Enable(); _jumpAction.Enable(); _runAction.Enable(); }
void OnDisable() { _moveAction.Disable(); _jumpAction.Disable(); _runAction.Disable(); }
void Update()
{
ReadInput();
}
void FixedUpdate()
{
CheckGround();
if (_jumpQueued && _isGrounded)
{
DoJump();
_jumpQueued = false;
}
else if (!_isGrounded)
{
_jumpQueued = false;
}
Move();
ApplyBetterGravity();
}
void ReadInput()
{
Vector2 raw = _moveAction.ReadValue<Vector2>();
_isRunning = _runAction.IsPressed();
_cameraYaw += raw.x * cameraYawSpeed * Time.deltaTime;
_forwardInput = raw.y;
Vector3 camForward = Quaternion.Euler(0f, _cameraYaw, 0f) * Vector3.forward;
MoveDir = _forwardInput * camForward;
if (MoveDir.sqrMagnitude > 0.01f)
MoveDir = MoveDir.normalized;
FaceDir = raw.sqrMagnitude > 0.01f ? camForward : Vector3.zero;
}
void Move()
{
float speed = moveSpeed * (_isRunning ? runMultiplier : 1f);
float control = _isGrounded ? 1f : airControl;
Vector3 vel = _rb.linearVelocity;
vel.x = Mathf.Lerp(vel.x, MoveDir.x * speed * control, control);
vel.z = Mathf.Lerp(vel.z, MoveDir.z * speed * control, control);
_rb.linearVelocity = vel;
if (FaceDir.sqrMagnitude > 0.01f)
{
Quaternion targetRot = Quaternion.LookRotation(FaceDir);
transform.rotation = Quaternion.Slerp(
transform.rotation, targetRot, rotationSpeed * Time.fixedDeltaTime
);
}
}
void DoJump()
{
Vector3 vel = _rb.linearVelocity;
vel.y = jumpForce;
_rb.linearVelocity = vel;
}
void ApplyBetterGravity()
{
if (_rb.linearVelocity.y < 0f)
_rb.linearVelocity += Vector3.up * Physics.gravity.y * (fallMultiplier - 1f) * Time.fixedDeltaTime;
else if (_rb.linearVelocity.y > 0f && !_jumpAction.IsPressed())
_rb.linearVelocity += Vector3.up * Physics.gravity.y * (lowJumpMultiplier - 1f) * Time.fixedDeltaTime;
}
void CheckGround()
{
Vector3 origin = transform.position + groundCheckOriginOffset;
_isGrounded = Physics.SphereCast(
origin, groundCheckRadius, Vector3.down,
out _, groundCheckDistance, groundLayer,
QueryTriggerInteraction.Ignore
);
}
void OnDrawGizmosSelected()
{
Vector3 origin = transform.position + groundCheckOriginOffset;
Gizmos.color = _isGrounded ? Color.green : Color.red;
Gizmos.DrawWireSphere(origin, groundCheckRadius);
Gizmos.DrawWireSphere(origin + Vector3.down * groundCheckDistance, groundCheckRadius);
}
}
Inspector에서 Target으로 Player 오브젝트 설정하기
using UnityEngine;
// === FollowCamera ===
// PlayerController.CameraYaw 를 단일 기준으로 삼아 위치 계산
// 피드백 루프 없음 — Player 회전/속도 일절 미참조
public class FollowCamera : MonoBehaviour
{
[Header("타겟")]
public Transform target;
[Header("오프셋")]
[Range(2f, 15f)] public float distance = 5f;
[Range(0f, 10f)] public float height = 2.5f;
public Vector3 lookAtOffset = new Vector3(0f, 1f, 0f);
[Header("추적 속도")]
[Range(1f, 30f)] public float followSpeed = 8f;
[Range(1f, 30f)] public float rotationSpeed = 10f;
// ===
PlayerController _player;
void Start()
{
if (target == null) return;
_player = target.GetComponent<PlayerController>();
// 시작 위치 스냅
transform.position = CalcDesiredPosition(_player.CameraYaw);
}
void LateUpdate()
{
if (target == null || _player == null) return;
float yaw = _player.CameraYaw;
Vector3 desired = CalcDesiredPosition(yaw);
transform.position = Vector3.Lerp(transform.position, desired, followSpeed * Time.deltaTime);
Vector3 lookTarget = target.position + lookAtOffset;
Quaternion desiredRot = Quaternion.LookRotation(lookTarget - transform.position);
transform.rotation = Quaternion.Slerp(transform.rotation, desiredRot, rotationSpeed * Time.deltaTime);
}
Vector3 CalcDesiredPosition(float yaw)
{
// yaw 방향의 반대편 + 높이
Vector3 back = Quaternion.Euler(0f, yaw, 0f) * Vector3.back;
return target.position + back * distance + Vector3.up * height;
}
}