์ค๋๋ง์ ๋ค์ ํฌ์คํ ์ ์ฌ๋ฆฌ๋ค์. ๊ทธ๋์ ๋ค๋ฅธ ๊ต์ก ํ๋ก๊ทธ๋จ์ ๋ฃ๋๋ค๊ณ ๋ฐ๋น ์ ์นด๋ก ๊ฐ๋ฐ์ ๋ชป ํ์ต๋๋ค.
FPS ๊ฒ์ ๊ฐ๋ฐ์ ์ฒ์ ์งํํด๋ดค๋๋ฐ, ์ด๋ฆด ๋ ์นด์ดํฐ์คํธ๋ผ์ดํฌ ์จ๋ผ์ธ ์ข๋น ์๋๋ฆฌ์ค ๋ชจ๋๋ฅผ ์ข์ํ๋ ๊ธฐ์ต์ ๋์ด๋ ค ์ฌ๋ฏธ์๊ฒ ๋ง๋ค์๋ ๊ฒ ๊ฐ์ต๋๋ค. ๋์ค์ ๊ธฐํ์ ์ ๋๋ก ํด์ ์ฌ์ด๋ ํ๋ก์ ํธ๋ก ํ๋ ์งํํด๋ณด๊ณ ์ถ๋ค์. ๋ฉํฐ ํ๋ ์ด๋ ๋๋ค๋ฉด ๊ธ์์ฒจํ๊ฒ ์ง๋ง ๊ทธ๊ฑฐ๊น์ง ํ๋ ค๋ฉด ๊ฐ ๊ธธ์ด ๊ต์ฅํ ๋ฉ ๊ฒ ๊ฐ์ต๋๋ค.ใ ใ ใ ...
์ก๋ด์ ์ฌ๊ธฐ๊น์ง ํ๋๋ก ํ๊ณ , ์ ๋ฒ ๊ธ์์๋ ์ด๋ ์ ๋๋ฉ์ด์ ์ฑํฌ ๋ง์ถ๋ ๊ฒ๊น์ง ํ์์ฃ ?
ํ์ง์์๋ ์๋ฌด๋ฐ ๋ฌธ์ ๊ฐ ์์์ง๋ง ๊ณ๋จ๊ณผ ๊ฐ์ ๊ฒฝ์ฌ ์งํ์ธ ๊ฒฝ์ฐ์๋ ์๋ง์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ์์ต๋๋ค. ๊ทธ๋์ ์์ ๋ด์ผํ ๋ด์ฉ๋ค์ด ์ข ๊ต์ฅํ ๋ง์์ด์. ๊ทธ๋๋ ํ๋ํ๋ ์ฐพ์๋ณด๋ฉฐ ๊ณต๋ถํ๊ธฐ์ ๋๋ฆ ์๋ฏธ์๋ ์๊ฐ์ด ์๋์๋ ์ถ์ต๋๋ค.
๋ฒ๊ทธ๋ฅผ ๊ฑฐ์ ๋ค ํด๊ฒฐํ๊ณ ๋ ๋ค์ ๋ ์๊ฐ์ด์ง๋ง, ์ด ๊ฒ์์์๋ Rigidbody๊ฐ ์๋๋ผ Character Controller๋ฅผ ์ฐ๋ ๊ฒ์ด ์ฐจ๋ผ๋ฆฌ ๋ ๋์์ ๊ฒ ๊ฐ๋ค๋ ์๊ฐ์ด ๋๋ค์...ใ ใ ,, ๊ทธ๋๋ ์ด์ ๋ง๋ ๊ฑฐ ๊ณ์ ํด๋ณด์ฃ .
๊ทธ๋ผ ์ด์ ๋ถํฐ ์ด๋ ํ ๋ฒ๊ทธ๋ค์ด ์์๋์ง ํ๋ํ๋์ฉ ์ดํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
โข๏ธ๋ฐ์ํ ๋ฌธ์ ๋ค
์ฒซ ๋ฒ์งธ | ํํํ ์ง๋ฉด์์ ๊ฒฝ์ฌ๋ก๋ก ์ค๋ฅผ ๋, ํค ์ ๋ ฅ์ ๋ผ๋ฉด ์๋ก ํ์ด์ค๋ฅด๋ ๋ฒ๊ทธ
์ด ๋ฌธ์ ๋ฅผ ์ผ๊ธฐํ ๊ฑด ๋ฐ๋ก ๋ค์ ์ฝ๋์์ต๋๋ค.
rigidBody.velocity = direction * currentMoveSpeed + Vector3.up * rigidBody.velocity.y;
๊ฒฝ์ฌ๋ก๋ฅผ ์ค๋ฅผ ๋์๋ rigidBody.velocity.y๊ฐ ์์๊ฐ ๋๊ธฐ ๋๋ฌธ์ ์ ์ชฝ์ผ๋ก ๋ฒกํฐ ํ์ด ๋ํด์ ธ์ ๋ํ๋ ๊ฒฐ๊ณผ๋ผ๊ณ ๋ณผ ์ ์์ง์.
๋ ๋ฒ์งธ | ์บ๋ฆญํฐ๊ฐ ๊ฒฝ์ฌ๋ก์์ ๋ด๋ ค ์ฌ ๋, ํํ ํ๊ธฐ๋ฉฐ ๋ด๋ ค์ค๋ ๋ฒ๊ทธ
์ด๊ฒ์ ์ด๋ ๋ฐฉํฅ ๋ฒกํฐ์ ์ค๋ ฅ ๋ฒกํฐ๊ฐ ๋ํด์ก๊ธฐ ๋๋ฌธ์ ๋ฐ์ํ ๊ฒฐ๊ณผ์ ๋๋ค. ์ด๋ํ ๋ฐฉํฅ์ ํ๋ฉด ์ชฝ์ผ๋ก ํฌ์์ํค๋ ๊ณผ์ ์ด ํ์ํ๊ฒ ์ต๋๋ค. ๋ฌผ๋ก ์ด์ ๊ฐ์ ๋ก์ง์ ์ํ๋ ๊ฒ์๋ ์์ ํ ์ง๋ง, ์ฐ๋ฆฌ ๊ฒ์์์๋ ์๋๊ธฐ์ ์์ ํด์ค์ผ ํฉ๋๋ค.
์ธ ๋ฒ์งธ | ์บ๋ฆญํฐ๊ฐ ๊ฒฝ์ฌ๋ก์ ๊ฐ๋งํ ์ ์์ ๋, ์์ํ ๋ฏธ๋๋ฌ์ ธ ๋ด๋ ค์ค๋ ๋ฒ๊ทธ
๋ง์ฐฐ๋ ฅ ๋ฌธ์ ๋ฅผ ์๊ฐํด๋ณผ ์ ์๊ฒ ์ผ๋, PhysicsMaterial์ Static Friction์ ์ต๋๋ก ์ค๋ด๋ ๋๊ฐ์ด ์ ์ฉ๋๋๋ผ๊ตฌ์.
์ด๊ฒ ์ญ์ ์ค๋ ฅ๊ณผ Vector3.up * rigidBody.velocity.y์ ํ์ ์ํด ๋ฐ์ํ๋ ๋ฒ๊ทธ๋ผ๊ณ ๋ณผ ์ ์๊ฒ ์ต๋๋ค.
๋ค ๋ฒ์งธ | ์บ๋ฆญํฐ๊ฐ ๊ฒฝ์ฌ๋ก์ ์ค๋ฅผ ๋์ ๋ด๋ ค์ฌ ๋ ์๋๊ฐ ๋ฌ๋ผ์ง๋ ๋ฒ๊ทธ
์ด๊ฒ์ ํ์ค ์ธ๊ณ์์๋ ๋น์ฐํ ๋ฒ์น์ด์ง๋ง, ๊ฒ์ ํ๋ ์ด๋ฅผ ํ ๋ ๋ถํธํ๊ฒ ์์ฉํ ์ ์์ผ๋ฏ๋ก ์์ ํด์ค์ผ ํ ๊ฒ ๊ฐ์ต๋๋ค.
๋ค์ฏ ๋ฒ์งธ | ์์์ ์ผ๋ก ์ฌ๋ผ๊ฐ ์ ์๋ ๊ฒฝ์ฌ ๊ฐ๋์ธ๋ฐ๋ ์บ๋ฆญํฐ๊ฐ ์ฌ๋ผ๊ฐ ์ ์์๋ ๋ฒ๊ทธ
์ฌ์ค์ ์ ์ผ ๋๊ฐํ ๋ฒ๊ทธ์์ต๋๋ค. ์ฌ์ค์ ์์ง๋ ์์ธ ํ์ ์ ์ ํํ๊ฒ๋ ํ์ง ๋ชปํ ๊ฒ ๊ฐ์ต๋๋ค.
์ด๊ฒ๋ ๋ฌธ์ ๊ฐ ๋ ์ ์์ผ๋, ๋ง์์ฃผ๋๋ก ํ๊ฒ ์ต๋๋ค. ๊ทธ๋ ๋ค๋ฉด ์ด์ ๋ฌธ์ ๋ฅผ ์ฐจ๊ทผ์ฐจ๊ทผ ํด๊ฒฐํด ๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
โ ํด๊ฒฐ ๊ณผ์
๊ฒฝ์ฌ(Slope) ์งํ ์ฒดํฌํ๊ธฐ
์บ๋ฆญํฐ๊ฐ ํ์ง์ ์๋์ง, ๊ฒฝ์ฌ๋ก์ ์๋์ง ํ๋จํ๋ ๊ฒ์ Raycast๋ฅผ ์๋๋ก ์์ ๋ถ๋ชํ ํ๋ฉด์ ๋ฒ์ ๋ฒกํฐ(Normal)์ ์์๋ด๊ณ , ์ด ๋ฒ์ ๋ฒกํฐ์ Vector3.up ๋ฒกํฐ ์ฌ์ด์ ๊ฐ๋๋ก ํ๋ณํ ์ ์์ต๋๋ค.
์ด ์๋ฆฌ๋ฅผ ์ ์ฉํ์ฌ ํ์ฌ ์บ๋ฆญํฐ๊ฐ ๊ฒฝ์ฌ ์งํ์ ์๋์ง ์๋์ง๋ฅผ ํ๋ณํด์ฃผ๋ ํจ์๋ฅผ ๋ง๋ค์์ต๋๋ค.
๊ฐ์ธ์ ์ผ๋ก Raycast์ ๊ฐ์ ๋ถ๋ฅ์ ๋ฉ์๋๋ค์ ์ฌ์ฉํ๊ธฐ ์ด๋ ค์ ๋๋ฐ, ์ด๋ฒ ๊ธฐํ์ ์ฐ์ต์ ์ข ํ ์ ์์๋ ๊ฒ ๊ฐ์ต๋๋ค.
private const float RAY_DISTANCE = 2f;
private RaycastHit slopeHit;
private int groundLayer = 1 << LayerMask.NameToLayer("Ground"); // ๋
(Ground) ๋ ์ด์ด๋ง ์ฒดํฌ
public bool IsOnSlope()
{
Ray ray = new Ray(transform.position, Vector3.down);
if(Physics.Raycast(ray, out slopeHit, RAY_DISTANCE, groundLayer))
{
var angle = Vector3.Angle(Vector3.up, slopeHit.normal);
return angle != 0f && angle < maxSlopeAngle;
}
return false;
}
๊ฒฝ์ฌ ์งํ ํ๋ฉด์ ๋ง๊ฒ ์ด๋ ๋ฐฉํฅ ๋ฒกํฐ ํฌ์ํ๊ธฐ
์ด์ ๊ฒฝ์ฌ ์งํ์ธ์ง ์๋์ง๋ฅผ ํ๋จํ ์ ์๊ฒ ๋์์ผ๋ ๋ง์ฝ ๊ฒฝ์ฌ ์งํ์ด๋ผ๋ฉด, ํ์ฌ ์บ๋ฆญํฐ๊ฐ ์ ์๋ ๊ฒฝ์ฌ ์งํ ํ๋ฉด ๋ฒกํฐ๋ก ์ด๋ ๋ฐฉํฅ ๋ฒกํฐ๋ฅผ ํฌ์ํ๋ ์์ ์ ํด์ค์ผ ํฉ๋๋ค. ๊ทธ๋์ผ ์บ๋ฆญํฐ๊ฐ ๊ฒฝ์ฌ ์งํ์์ ๋ด๋ ค๊ฐ ๋ ํตํต ํ์ง ์๊ณ ๋ด๋ ค๊ฐ ์ ์์ด์.
Vector3์์ ์ ๊ณตํ๋ ProjectOnPlane() ๋ฉ์๋๋ฅผ ํ์ฉํ๋ฉด ์ฝ๊ฒ ๊ตฌํ ์ ์์ต๋๋ค. ๋ํ, ํฌ์๋ ๋ฒกํฐ ๋ฐฉํฅ ์ ๋ณด๋ง ํ์ํ ๊ฒ์ด๋ฏ๋ก ์ ๊ทํ(normalized)๋ฅผ ํด์ฃผ์์ต๋๋ค.
protected Vector3 AdjustDirectionToSlope(Vector3 direction)
{
return Vector3.ProjectOnPlane(direction, slopeHit.normal).normalized;
}
์ด ๋๊น์ง ํ ๊ฒ๋ค์ ํตํด, ๊ฒฝ์ฌ ์งํ์์ ํตํต ํ๋ฉฐ ๋ด๋ ค์ค๋ ๋ฒ๊ทธ๋ฅผ ์์ ํด ์ฃผ์์ต๋๋ค.
protected void Move()
{
float currentMoveSpeed = player.MoveSpeed * CONVERT_UNIT_VALUE;
float animationPlaySpeed = DEFAULT_ANIMATION_PLAYSPEED +
GetAnimationSyncWithMovement(currentMoveSpeed);
// ---------์ถ๊ฐ-----------
bool isOnSlope = IsOnSlope();
Vector3 velocity = isOnSlope ? AdjustDirectionToSlope(Vector3 direction) : direction;
Vector3 gravity = isOnSlope ? Vector3.zero : Vector3.down * Mathf.Abs(rigidBody.velocity.y);
// ------------------------
LookAt();
rigidBody.velocity = velocity * currentMoveSpeed + gravity;
animator.SetFloat("Velocity", animationPlaySpeed);
}
์ถ๊ฐ๋ก, ์ฒซ ๋ฒ์งธ ๋ฒ๊ทธ์๋ ์งํ → ๊ฒฝ์ฌ ์งํ ์ด๋ ์ ์๋ก ํ๋ ๋ฒ๊ทธ๋ ์์ ํด์ฃผ์์ต๋๋ค.
Vector3 gravity = isOnSlope ? Vector3.zero : Vector3.down * Mathf.Abs(rigidBody.velocity.y);
์์ ์์ฑํ ์ฝ๋๋ค์ ์ด๋ ๊ฒ ์ฌ์ฉํ๋ค๋ ์์๋ฅผ ๋ณด์ฌ๋๋ฆฐ ๊ฒ์ผ ๋ฟ, ๋ชจ๋ ๋ฒ๊ทธ๋ค์ ํด๊ฒฐํ ์ฝ๋๊ฐ ์๋๋๋ค.
๋ค์์ผ๋ก ์ด๋ํ์ฌ ๋ ํ๋ํ๋ ์ฐจ๊ทผ์ฐจ๊ทผ ํด๊ฒฐํด ๋ณด๊ฒ ์ต๋๋ค.
๋ (Ground)์ ๋ถ์ด ์๋์ง ์ฒดํฌํ๊ธฐ
์ด์ ๊ฒฝ์ฌ ์งํ์ ์ ์์ ๋, ์์ํ ๋ฏธ๋๋ฌ์ ธ ๋ด๋ ค์ค๋ ๋ฒ๊ทธ๋ฅผ ์์ ํ ๊ฒ๋๋ค. ๊ฒฝ์ฌ ์งํ์ผ ๋์ ์๋ ๋๋ฅผ ๊ตฌ๋ณํ์ฌ ๋ฐ๋ก ์ ์ฉํด์ฃผ๋ฉด ๋๋๋ฐ, ๋ ๊ฐ์ง ๋ฐฉ๋ฒ์ด ์์ต๋๋ค.
- Rigidbody์ Freeze Position Y ์ ์ฒดํฌ/ํด์ ํ๋ ๋ฐฉ์
- Rigidbody์ Use Gravity ์ ์ฒดํฌ/ํด์ ํ๋ ๋ฐฉ์
์ ๋ ๋ ๋ฒ์งธ ๋ฐฉ๋ฒ์ ์ ํํ์ต๋๋ค. ๊ทธ๋ฐ๋ฐ ์ด๊ฑฐ๋ ๋ ์ ๋ถ์ด ์๋์ง ์ฒดํฌํ๋ ๊ฒ์ ๋ฌด์จ ์๊ด์ด๋๊ตฌ์? ์บ๋ฆญํฐ๊ฐ ๋ฐ๋ฅ๊ณผ ๊ฒฝ์ฌ ์งํ์ ๋ฐ๋ผ ์ด๋ํ๊ธฐ๋ง ํ๋ฉด ์ฌ์ค์ ํ์ ์์ต๋๋ค. ๋ฌธ์ ๋ ๊ณต์ค์์ ๋จ์ด์ง๋ ์ํฉ์ด์ง์.
์ด๋ฏธ ๊ฒฝ์ฌ ์งํ ์ฒดํฌ์ฉ์ผ๋ก ์ฌ์ฉํ๋ Raycast๋ฅผ ์ฌํ์ฉํ์๋, Raycast์ ๊ธธ์ด ๋๋ฌธ์ ๊ณต์ค์ ๋ฌ ์ํ๋ก ๋ค๋๊ฒ ๋๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํฉ๋๋ค. ์ฝ๋ผ์ด๋์ OnCollisionEnter() ๋ฉ์๋๋ ๊ฒฝ์ฌ ์งํ์์ ๊ทธ๋ฅ ์ข์ ๋ชจ์ต์ ๋ณด์ฌ์ฃผ์ง ๋ชปํ์ต๋๋ค.
๊ทธ๋์ ์๋ก์ด Raycast๋ฅผ ์ฐ์๋, Raycast๋ ํน์ ์ํฉ์์๋ ๋ ์ฒดํฌ๋ฅผ ํ์ง ๋ชปํฉ๋๋ค.
๊ทธ๋์ ์ ๋ ๋ถ๋ชํ ๋์์ ๋ํ ์ ๋ณด๋ ํ์์๊ณ , ๋จ์ง ๋์ ๋ฉด์ ์ผ๋ก ์ฒดํฌ๋ง ํจ์จ์ ์ผ๋ก ํ ์ ์์ผ๋ฉด ๋๋ Physics.CheckBox()๋ฅผ ์ด์ฉํ์ต๋๋ค. ์ฐ์ ์บ๋ฆญํฐ ์ค๋ธ์ ํธ์ ํ์ ๋น ์ค๋ธ์ ํธ๋ก "GroundCheck"๋ฅผ ๋ง๋ค์ด ์ฃผ์์ต๋๋ค.
๊ทธ๋ฆฌ๊ณ ์บ๋ฆญํฐ์ ํฌ๊ธฐ์ ๋ง์ถฐ์ CheckBox ํฌ๊ธฐ๊ฐ ์ ๋์ ์ผ๋ก ๋ณํ ์ ์๋, Y์ถ์ ์กฐ๊ธ ์๊ฒ ์ธํ ํด์ฃผ์์ต๋๋ค.
[SerializeField] Transform groundCheck;
public bool IsGrounded()
{
Vector3 boxSize = new Vector3(transform.lossyScale.x, 0.4f, transform.lossyScale.z);
return Physics.CheckBox(groundCheck.position, boxSize, Quaternion.identity, groundLayer);
}
// Quaternion.identity๋ ํ์ ๊ฐ์ด ์๋ค๋ ์๋ฏธ์
๋๋ค.
์๊ฐ์ ์ผ๋ก ๊ทธ๋ ค์ ํ ๋ฒ ๋ณด์ฌ๋๋ฆฌ๋ฉด ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
private void OnDrawGizmos()
{
Gizmos.color = Color.red;
Vector3 boxSize = new Vector3(transform.lossyScale.x, 0.4f, transform.lossyScale.z);
Gizmos.DrawWireCube(groundCheck.position, boxSize);
}
์ด์ ์ํํ ๋ ์ฒดํฌ๋ฅผ ํ ์ ์๊ฒ ๋์๋ค์. ์ด๊ฒ์ ํ ๋๋ก ์ฝ๋๋ฅผ ์์ ํ์ฌ ์ฃผ๋ฉด, ๊ฒฝ์ฌ ์งํ์์ ๋ฏธ๋๋ฌ์ ธ ๋ด๋ ค๊ฐ๋ ๋ฒ๊ทธ๋ฅผ ๊ณ ์น ์ ์์ต๋๋ค. ๊ฒฝ์ฌ ์งํ์์ ์ค๋ ฅ์ ๊บผ์ฃผ๋, ์ฌ๋ผ๊ฐ ๋์ ๋ด๋ ค๊ฐ ๋ ์๋๊ฐ ๋ฌ๋ผ์ง๋ ๋ฌธ์ ๋ ํด๊ฒฐ๋๊ฒ ๋ค์.
protected void Move()
{
float currentMoveSpeed = player.MoveSpeed * CONVERT_UNIT_VALUE;
float animationPlaySpeed = DEFAULT_ANIMATION_PLAYSPEED +
GetAnimationSyncWithMovement(currentMoveSpeed);
//-----------------์์ ----------------------------------
bool isOnSlope = IsOnSlope();
bool isGrounded = IsGrounded();
Vector3 velocity = direction;
Vector3 gravity = Vector3.down * Mathf.Abs(rigidBody.velocity.y);
if (isGrounded && isOnSlope) // ๊ฒฝ์ฌ๋ก์ ์์ ๋
{
velocity = AdjustDirectionToSlope(direction);
gravity = Vector3.zero;
rigidBody.useGravity = false;
}
else
{
rigidBody.useGravity = true;
}
//-------------------------------------------------------
LookAt();
rigidBody.velocity = velocity * currentMoveSpeed + gravity;
animator.SetFloat("Velocity", animationPlaySpeed);
}
๊ฐ ์ ์๋ ์งํ์ธ์ง ์๋์ง ์ฒดํฌํ๊ธฐ
์ด์ ๋ง์ง๋ง ๋ฒ๊ทธ๋ง ๋จ์๋ค์. ์ค๋ฅผ ์ ์๋ ๊ฒฝ์ฌ ์งํ์ธ๋ฐ๋ ๋ถ๊ตฌํ๊ณ ํค ์ ๋ ฅ์ ๊ณ์ ์ฃผ๋ฉด ์ฌ๋ผ๊ฐ๋ ๋ฒ๊ทธ์์ต๋๋ค.
์ค๋ ฅ๊ฐ์ ๋ ๋๋ฆฌ๋ฉด ํ์ด ๋ธ๋ ค์ ๋ชป ์ฌ๋ผ๊ฐ๊ธด ํ์ง๋ง, ๊ทธ๋ ๊ฒ ํ๋ฉด ์์ ๋ฐฉ์งํฑ ํ๋์กฐ์ฐจ ๋ชป ๋๋๋ผ๊ตฌ์.
๊ทธ๋์ ๋ค์ ํ๋ ์์ ์ด๋ํ ์์น๋ฅผ ๋จผ์ ๊ณ์ฐํ ํ, ๊ฑฐ๊ธฐ์์ ์๋ก์ด Raycast๋ฅผ ์๋๋ก ์์ ์ด๋ํ๊ธฐ ์ ์ ํ๋จํ๋ ๊ฑธ๋ก ํด๊ฒฐ ๋ฐฉ์์ ๋ง๋ จํ์ต๋๋ค.
์ฐ์ ์บ๋ฆญํฐ๊ฐ ๋ฐ๋ผ๋ณด๋ ์ ๋ฉด ์ชฝ์ผ๋ก, ์ฝ๋ผ์ด๋ ๊ฐ์ฅ ์ธ๊ณฝ์ "RaycastOrigin"์ด๋ผ๋ ๋น ์ค๋ธ์ ํธ๋ฅผ ์์์ผ๋ก ์์ฑํด์ฃผ์์ต๋๋ค. ์ด๋ํ๋ ์บ๋ฆญํฐ ์ค๋ธ์ ํธ ์ขํ๋ ์บ๋ฆญํฐ ์ค์ฌ์ชฝ์ด์ง๋ง, ์ฝ๋ผ์ด๋ ๋ฐ์ง๋ฆ์ ์๊ฐํด์ผ ํ๊ธฐ ๋๋ฌธ์ด์ฃ .
์ฌ๊ธฐ์์ ์ด๋ํ ๋ฐฉํฅ * ์ด๋์๋ * Time.fixedDeltaTime ๋ฒกํฐ๋ฅผ ๋ํด์ฃผ๋ฉด ๋ค์ ํ๋ ์์์์ ์บ๋ฆญํฐ ๋งจ ์ ์์น๊ฐ ๋๊ฒ ์ง์. ์ ๋ Move()๋ฅผ FixedUpdate() ํจ์์์ ๊ตฌ๋ ์ค์ด๊ธฐ ๋๋ฌธ์ Time.fixedDeltaTime์ ์ฌ์ฉํ์ต๋๋ค.
์๋ฌดํผ ๋ค์ ํ๋ ์ ์บ๋ฆญํฐ ์์น์์ ์๋๋ก Raycast๋ฅผ ์์, ๋ฟ์ ํ๋ฉด์ ๋ฒ์ ๋ฒกํฐ์ Vector3.up ๋ฒกํฐ ์ฌ์ด์ ๊ฐ๋๋ฅผ ๊ณ์ฐํฉ๋๋ค.
private float CalculateNextFrameGroundAngle(float moveSpeed)
{
// ๋ค์ ํ๋ ์ ์บ๋ฆญํฐ ์ ๋ถ๋ถ ์์น
var nextFramePlayerPosition =
raycastOrigin.position + direction * moveSpeed * Time.fixedDeltaTime;
if (Physics.Raycast(nextFramePlayerPosition, Vector3.down, out RaycastHit hitInfo,
RAY_DISTANCE, groundLayer))
return Vector3.Angle(Vector3.up, hitInfo.normal);
return 0f;
}
๊ทธ๋ฆฌ๊ณ ์ด๊ฒ maxSlopeAngle๋ก ์ค์ ํ ๊ฐ๋ณด๋ค ํด ๋๋ velocity๋ฅผ Vector3.zero๋ก ์ค์ ํ์ฌ ์ด๋ํ์ง ๋ชปํ๋๋ก ํ๊ฒ ํฉ๋๋ค.
protected void Move()
{
float currentMoveSpeed = player.MoveSpeed * CONVERT_UNIT_VALUE;
float animationPlaySpeed = DEFAULT_ANIMATION_PLAYSPEED +
GetAnimationSyncWithMovement(currentMoveSpeed);
bool isOnSlope = IsOnSlope();
bool isGrounded = IsGrounded();
//----------------์์ -----------------
Vector3 velocity = CalculateNextFrameGroundAngle(currentMoveSpeed) < maxSlopeAngle ?
direction : Vector3.zero;
//-------------------------------------
Vector3 gravity = Vector3.down * Mathf.Abs(rigidBody.velocity.y);
if (isGrounded && isOnSlope)
{
velocity = AdjustDirectionToSlope(direction);
gravity = Vector3.zero;
rigidBody.useGravity = false;
}
else
{
rigidBody.useGravity = true;
}
LookAt();
rigidBody.velocity = velocity * currentMoveSpeed + gravity;
animator.SetFloat("Velocity", animationPlaySpeed);
}
์ต๋ ๊ฒฝ์ฌ ๊ฐ๋(maxSlopeAngle)์ 40๋๋ก ํด๋๊ณ ํ ์คํธ ํด๋ณด๊ฒ ์ต๋๋ค.
ํ ์คํธ ํด๋ณด๋ ์ ์๋ํ๋ ๊ฒ์ ๋ณผ ์ ์์ต๋๋ค. ์ถ๊ฐ๋ก, ๋ฐ๋ฅ์ ์๋ ๋ฐฉ์งํฑ์ด๋ ๊ณ๋จ ๋์ด์ ๋ํด์๋ ์ค๋ฅผ ์ ์๋์ง ์๋์ง๋ฅผ ๊ตฌํํ ์ ์๋๋ฐ ์ ๋ ๊ฑฐ๊ธฐ๊น์ง๋ ํ์ ์์ ๊ฒ ๊ฐ์ ๊ตฌํํ์ง ์๊ฒ ์ต๋๋ค. ํ์ํ์๋ฉด ๐์ด ์์์ ์ฐธ๊ณ ํ๋ฉด ์ข์ ๊ฒ ๊ฐ๋ค์.
๋ง๋ฌด๋ฆฌ
3D ๋ฌผ๋ฆฌ๊ฐ ์ด๋ ๊ฒ๋ ํ๋ค๊ณ ์ด๋ ค์ด ์ค์ ๊ฟ์๋ ๋ชฐ๋์ต๋๋ค. ๊ฝค๋ ๊ณ ์์ ์ข ํ๋ค์... ์ด๊ฒ์ ๊ฒ ๋ง์ด ์ฐพ์๋ณด๋ฉฐ ๊ณต๋ถํ๋ ๊ฒ ๊ฐ์ต๋๋ค. ๋ค์์๋ ์ด๋ค ๋๊ด์ด ์ ๋ฅผ ๊ณ ํต์ค๋ฝ๊ฒ ํ ๊น์...ใ ใ ใ ๊ธด ๊ธ ์ฝ์ด์ฃผ์ ์ ๊ฐ์ฌํฉ๋๋ค. ์ ์ฒด ์์ค์ฝ๋ ํ์ํ์ ๋ถ์ ์๋์ ๋จ๊ฒจ ๋์๊ฒ์.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
[RequireComponent(typeof(Player))]
public class PlayerController : MonoBehaviour
{
#region #๊ธฐ๋ณธ ์ปดํฌ๋ํธ
public Vector3 direction { get; private set; }
protected Player player;
protected Rigidbody rigidBody;
protected Animator animator;
protected CapsuleCollider capsuleCollider;
#endregion
#region #์ด๋ ๊ด๋ จ ๋ณ์
protected const float CONVERT_UNIT_VALUE = 0.01f;
protected const float DEFAULT_CONVERT_MOVESPEED = 3f;
protected const float DEFAULT_ANIMATION_PLAYSPEED = 0.9f;
protected float frontGroundHeight;
#endregion
#region #๊ฒฝ์ฌ ์ฒดํฌ ๋ณ์
[Header("๊ฒฝ์ฌ ์งํ ๊ฒ์ฌ")]
[SerializeField, Tooltip("์บ๋ฆญํฐ๊ฐ ๋ฑ๋ฐํ ์ ์๋ ์ต๋ ๊ฒฝ์ฌ ๊ฐ๋์
๋๋ค.")]
float maxSlopeAngle;
[SerializeField, Tooltip("๊ฒฝ์ฌ ์งํ์ ์ฒดํฌํ Raycast ๋ฐ์ฌ ์์ ์ง์ ์
๋๋ค.")]
Transform raycastOrigin;
private const float RAY_DISTANCE = 2f;
private RaycastHit slopeHit;
#endregion
#region #๋ฐ๋ฅ ์ฒดํฌ ๋ณ์
[Header("๋
์ฒดํฌ")]
[SerializeField, Tooltip("์บ๋ฆญํฐ๊ฐ ๋
์ ๋ถ์ด ์๋์ง ํ์ธํ๊ธฐ ์ํ CheckBox ์์ ์ง์ ์
๋๋ค.")]
Transform groundCheck;
private int groundLayer;
#endregion
#region #UNITY_FUNCTIONS
void Start()
{
rigidBody = GetComponent<Rigidbody>();
animator = GetComponent<Animator>();
player = GetComponent<Player>();
capsuleCollider = GetComponent<CapsuleCollider>();
groundLayer = 1 << LayerMask.NameToLayer("Ground");
}
void FixedUpdate()
{
Move();
}
#endregion
private float CalculateNextFrameGroundAngle(float moveSpeed)
{
var nextFramePlayerPosition = raycastOrigin.position + direction * moveSpeed * Time.fixedDeltaTime; // ๋ค์ ํ๋ ์ ์บ๋ฆญํฐ ์ ๋ถ๋ถ ์์น
if (Physics.Raycast(nextFramePlayerPosition, Vector3.down, out RaycastHit hitInfo, RAY_DISTANCE, groundLayer))
{
return Vector3.Angle(Vector3.up, hitInfo.normal);
}
return 0f;
}
protected void Move()
{
float currentMoveSpeed = player.MoveSpeed * CONVERT_UNIT_VALUE;
float animationPlaySpeed = DEFAULT_ANIMATION_PLAYSPEED + GetAnimationSyncWithMovement(currentMoveSpeed);
bool isOnSlope = IsOnSlope();
bool isGrounded = IsGrounded();
Vector3 velocity = CalculateNextFrameGroundAngle(currentMoveSpeed) < maxSlopeAngle ? direction : Vector3.zero;
Vector3 gravity = Vector3.down * Mathf.Abs(rigidBody.velocity.y);
if (isGrounded && isOnSlope) // ๊ฒฝ์ฌ๋ก์ ์์ ๋
{
velocity = AdjustDirectionToSlope(direction);
gravity = Vector3.zero;
rigidBody.useGravity = false;
}
else
{
rigidBody.useGravity = true;
}
LookAt();
rigidBody.velocity = velocity * currentMoveSpeed + gravity;
animator.SetFloat("Velocity", animationPlaySpeed);
}
public bool IsGrounded()
{
Vector3 boxSize = new Vector3(transform.lossyScale.x, 0.4f, transform.lossyScale.z);
return Physics.CheckBox(groundCheck.position, boxSize, Quaternion.identity, groundLayer);
}
public bool IsOnSlope()
{
Ray ray = new Ray(transform.position, Vector3.down);
if(Physics.Raycast(ray, out slopeHit, RAY_DISTANCE, groundLayer))
{
var angle = Vector3.Angle(Vector3.up, slopeHit.normal);
return angle != 0f && angle < maxSlopeAngle;
}
return false;
}
public void OnMoveInput(InputAction.CallbackContext context)
{
Vector2 input = context.ReadValue<Vector2>();
direction = new Vector3(input.x, 0f, input.y);
}
protected void LookAt()
{
if (direction != Vector3.zero)
{
Quaternion targetAngle = Quaternion.LookRotation(direction);
rigidBody.rotation = targetAngle;
}
}
protected float GetAnimationSyncWithMovement(float changedMoveSpeed)
{
if (direction == Vector3.zero)
{
return -DEFAULT_ANIMATION_PLAYSPEED;
}
// (๋ฐ๋ ์ด๋ ์๋ - ๊ธฐ๋ณธ ์ด๋์๋) * 0.1f
return (changedMoveSpeed - DEFAULT_CONVERT_MOVESPEED) * 0.1f;
}
protected Vector3 AdjustDirectionToSlope(Vector3 direction)
{
Vector3 adjustVelocityDirection = Vector3.ProjectOnPlane(direction, slopeHit.normal).normalized;
return adjustVelocityDirection;
}
}