유니티 다크소울 따라만들기 ch_7 낙하
이 글은 유튜브 Sebastian Graves Create Dark Souls를 보고 따라 만들면서 헷갈리는 부분을 정리한 글입니다.
구현 개요
이번엔 플레이어 행동 중 하나인 낙하를 구현한다.
레이캐스트를 통해 땅을 인식하여 낙하를 구현하며,
구현 내용
대략적인 구현 내용은 다음과 같다.
- 레이케스트를 통해서 바닥에 닿아있는지를 계속 체크한다 (레이케스트는 길지 않아 바로 아래 바닥만 감지가능).
- 바닥이 감지될 경우 그 바닥의 위치로 플레이어의 위치를 설정한다. (플레이어 높이를 바닥에 맞춰줌)
-
바닥이 감지되지 않을 경우 떨어진다. 이때 아래로 떨어지며 떨어지기 직전의 이동방향으로도 조금씩 이동한다. (떨어지면서 이동 방향 변경은 불가능)
-
떨어지는 시간이 0.5초 이상일 경우 착지 애니메이션 실행
초기 설정
- 애니메이션 추가
- 스크립트들 내용 추가
애니메이션 추가
떨어지는 애니메이션과 착지 애니메이션이 필요.
착지 애니메이션
낙하 애니메이션
Scripts 내용 추가
PlayerManager과 PlayerLocomotion이 수정됨.
PlayerManager
PlayerManager
public class PlayerManager : MonoBehaviour
{
InputHandler inputHandler;
Animator anim;
CameraHandler cameraHandler;
PlayerLocomotion playerLocomotion;
public bool isInteracting;
[Header("Player Flags")]
public bool isSprinting;
public bool isInAir;
public bool isGrounded;
void Start()
{
inputHandler = GetComponent<InputHandler>();
anim = GetComponentInChildren<Animator>();
playerLocomotion = GetComponent<PlayerLocomotion>();
}
private void Awake()
{
cameraHandler = CameraHandler.singleton;
}
void Update()
{
float delta = Time.deltaTime;
isInteracting = anim.GetBool("isInteracting");
inputHandler.TickInput(delta);
playerLocomotion.HandleMovement(delta);
playerLocomotion.HandleRollingAndSprinting(delta);
playerLocomotion.HandleFalling(delta, playerLocomotion.moveDirection);
}
private void FixedUpdate()
{
float delta = Time.fixedDeltaTime;
if (cameraHandler != null)
{
cameraHandler.FollowTarget(delta);
cameraHandler.HandleCameraRotation(delta, inputHandler.mouseX, inputHandler.mouseY);
}
}
private void LateUpdate()
{
inputHandler.rollFlag = false;
inputHandler.sprintFlag = false;
isSprinting = inputHandler.b_Input;
if (isInAir)
{
playerLocomotion.inAirTimer = playerLocomotion.inAirTimer + Time.deltaTime;
}
}
}
추가된 부분
isInAir 변수가 추가됐으며,
떨어진지 얼마나 지났는지를 나타내는 PlayerLocomotion의 inAirTimer 변수를 갱신해준다
public bool isInAir;
private void LateUpdate()
{
...
if (isInAir)
{
playerLocomotion.inAirTimer = playerLocomotion.inAirTimer + Time.deltaTime;
}
}
PlayerLocomotion
PlayerLocomotion
public class PlayerLocomotion : MonoBehaviour
{
private PlayerManager playerManager;
Transform cameraObject;
InputHandler inputHandler;
public Vector3 moveDirection;
[HideInInspector]
public Transform myTransform;
[HideInInspector]
public AnimatorHandler animatorHandler;
public new Rigidbody rigidbody;
public GameObject normalCamera;
[Header("Ground & Air Detection Stats")]
[SerializeField] private float groundDetectionRayStartPoint = 0.5f;
[SerializeField] private float minimumDistanceNeededToBeginFall = 1f;
[SerializeField]
private float groundDirectionRayDistance = 0.2f;
private LayerMask ignoreForGroundCheck;
public float inAirTimer;
[Header("Movement Status")]
[SerializeField] private float movementSpeed = 5;
[SerializeField] private float sprintSpeed = 7;
[SerializeField] private float rotationSpeed = 10;
[SerializeField] private float fallingSpeed = 45;
void Start()
{
playerManager = GetComponent<PlayerManager>();
rigidbody = GetComponent<Rigidbody>();
inputHandler = GetComponent<InputHandler>();
animatorHandler = GetComponentInChildren<AnimatorHandler>();
cameraObject = Camera.main.transform;
myTransform = transform;
animatorHandler.Initialized();
playerManager.isGrounded = true;
ignoreForGroundCheck = ~(1 << 8 | 1 << 11);
}
#region Movement
Vector3 normalVector;
Vector3 targetPosition;
private void HandleRotation(float delta)
{
Vector3 targetDir = Vector3.zero;
float moveOverride = inputHandler.moveAmount;
targetDir = cameraObject.forward * inputHandler.vertical;
targetDir += cameraObject.right * inputHandler.horizontal;
targetDir.Normalize();
targetDir.y = 0;
if (targetDir == Vector3.zero)
targetDir = myTransform.forward;
float rs = rotationSpeed;
Quaternion tr = Quaternion.LookRotation(targetDir);
Quaternion targetRotation = Quaternion.Slerp(myTransform.rotation, tr, rs * delta);
myTransform.rotation = targetRotation;
}
public void HandleMovement(float delta)
{
// 특정 애니메이션 도중 이동 x
if (inputHandler.rollFlag || playerManager.isInteracting)
return;
moveDirection = cameraObject.forward * inputHandler.vertical;
moveDirection += cameraObject.right * inputHandler.horizontal;
moveDirection.Normalize();
moveDirection.y = 0;
float speed = movementSpeed;
if (inputHandler.sprintFlag)
{
speed = sprintSpeed;
playerManager.isSprinting = true;
moveDirection *= speed;
}
else
{
moveDirection *= speed;
}
Vector3 projectedVelocity = Vector3.ProjectOnPlane(moveDirection, normalVector);
rigidbody.velocity = projectedVelocity;
animatorHandler.UpdateAnimatorValues(inputHandler.moveAmount, 0, playerManager.isSprinting);
if (animatorHandler.canRotate)
{
HandleRotation(delta);
}
}
public void HandleRollingAndSprinting(float delta)
{
// 한 번 실행하고 애니메이션이 끝날 때 까지 다시 실행 불가능
if (animatorHandler.anim.GetBool("isInteracting"))
return;
if (inputHandler.rollFlag)
{
moveDirection = cameraObject.forward * inputHandler.vertical;
moveDirection += cameraObject.right * inputHandler.horizontal;
if (inputHandler.moveAmount > 0)
{
animatorHandler.PlayTargetAnimation("Rolling", true);
moveDirection.y = 0;
Quaternion rollRotation = Quaternion.LookRotation(moveDirection);
myTransform.rotation = rollRotation;
}
else
{
animatorHandler.PlayTargetAnimation("BackStep", true);
}
}
}
public void HandleFalling(float delta, Vector3 moveDirection)
{
playerManager.isGrounded = false;
RaycastHit hit;
Vector3 origin = myTransform.position;
origin.y += groundDetectionRayStartPoint;
// 앞에 벽이 있으면 못감
if (Physics.Raycast(origin, myTransform.forward, out hit, 0.4f))
{
moveDirection = Vector3.zero;
}
if (playerManager.isInAir)
{
rigidbody.AddForce(-Vector3.up * fallingSpeed);
rigidbody.AddForce(moveDirection * fallingSpeed / 7f);
}
//Vector3 dir = moveDirection;
//dir.Normalize();
//origin = origin + dir * groundDirectionRayDistance;
targetPosition = myTransform.position;
Debug.DrawRay(origin, -Vector3.up * minimumDistanceNeededToBeginFall, Color.red, 0.1f, false);
// 발 아래 땅이 있을 때
if (Physics.Raycast(origin, -Vector3.up, out hit, minimumDistanceNeededToBeginFall, ignoreForGroundCheck))
{
normalVector = hit.normal;
Vector3 tp = hit.point;
playerManager.isGrounded = true;
targetPosition.y = tp.y;
// 공중에서 낙하도중 땋에 닿은 것이라면
if (playerManager.isInAir)
{
// 많이 떨어지던 중 땋에 닿음 -> 착지 애니메이션 실행
if (inAirTimer > 0.5f)
{
Debug.Log("You were in the air for " + inAirTimer);
animatorHandler.PlayTargetAnimation("Land", true);
inAirTimer = 0;
}
// 그냥 조그만 난간에서 떨어짐 -> 이동 애니메이션
else
{
animatorHandler.PlayTargetAnimation("Locomotion", false);
inAirTimer = 0;
}
playerManager.isInAir = false;
}
}
// 공중에 떠있을 경우
else
{
// 땅에 있음 변수를 false로 바꿈
if (playerManager.isGrounded)
{
playerManager.isGrounded = false;
}
// 공중으로 떨어지기 시작할 때 첫 한번 실행
if (playerManager.isInAir == false)
{
// 다른 애니메이션 실행중이 아니라면 낙하 애니메이션 실행
if (playerManager.isInteracting == false)
{
animatorHandler.PlayTargetAnimation("Falling", true);
}
// 가던 방향으로 살짝 느리게 감
Vector3 vel = rigidbody.velocity;
vel.Normalize();
rigidbody.velocity = vel * (movementSpeed / 2);
playerManager.isInAir = true;
}
}
// 땅에 있을 때 발의 위치를 재조정함
if (playerManager.isGrounded)
{
if (playerManager.isInteracting || inputHandler.moveAmount > 0)
{
myTransform.position = Vector3.Lerp(myTransform.position, targetPosition, Time.deltaTime);
}
else
{
myTransform.position = targetPosition;
}
}
}
#endregion
}
추가된 부분
HandleFalling 함수와 몇가지 변수가 추가됐다.
[Header("Ground & Air Detection Stats")]
// 레이케스트가 시작되는 좌표와 플레이어 좌표가 가질 간격
[SerializeField] private float groundDetectionRayStartPoint = 0.5f;
// 낙하가 되는 높이의 최소거리 (레이캐스트의 길이)
[SerializeField] private float minimumDistanceNeededToBeginFall = 1f;
//버그가 있어서 비활성화 해둠
//[SerializeField] private float groundDirectionRayDistance = 0.2f;
private LayerMask ignoreForGroundCheck;
public float inAirTimer;
public void HandleFalling(float delta, Vector3 moveDirection)
{
playerManager.isGrounded = false;
RaycastHit hit;
Vector3 origin = myTransform.position;
origin.y += groundDetectionRayStartPoint;
// 앞에 벽이 있으면 못감
if (Physics.Raycast(origin, myTransform.forward, out hit, 0.4f))
{
moveDirection = Vector3.zero;
}
if (playerManager.isInAir)
{
rigidbody.AddForce(-Vector3.up * fallingSpeed);
rigidbody.AddForce(moveDirection * fallingSpeed / 7f);
}
//Vector3 dir = moveDirection;
//dir.Normalize();
//origin = origin + dir * groundDirectionRayDistance;
targetPosition = myTransform.position;
Debug.DrawRay(origin, -Vector3.up * minimumDistanceNeededToBeginFall, Color.red, 0.1f, false);
// 발 아래 땅이 있을 때
if (Physics.Raycast(origin, -Vector3.up, out hit, minimumDistanceNeededToBeginFall, ignoreForGroundCheck))
{
normalVector = hit.normal;
Vector3 tp = hit.point;
playerManager.isGrounded = true;
targetPosition.y = tp.y; // 플레이어가 위치해야할 좌표를 저장해둠
// 공중에서 낙하도중 땋에 닿은 것이라면
if (playerManager.isInAir)
{
// 많이 떨어지던 중 땋에 닿음 -> 착지 애니메이션 실행
if (inAirTimer > 0.5f)
{
Debug.Log("You were in the air for " + inAirTimer);
animatorHandler.PlayTargetAnimation("Land", true);
inAirTimer = 0;
}
// 그냥 조그만 난간에서 떨어짐 -> 이동 애니메이션
else
{
animatorHandler.PlayTargetAnimation("Locomotion", false);
inAirTimer = 0;
}
playerManager.isInAir = false;
}
}
// 공중에 떠있을 경우
else
{
// 땅에 있음 변수를 false로 바꿈
if (playerManager.isGrounded)
{
playerManager.isGrounded = false;
}
// 공중으로 떨어지기 시작할 때 첫 한번 실행
if (playerManager.isInAir == false)
{
// 다른 애니메이션 실행중이 아니라면 낙하 애니메이션 실행
if (playerManager.isInteracting == false)
{
animatorHandler.PlayTargetAnimation("Falling", true);
}
// 가던 방향으로 살짝 느리게 감
Vector3 vel = rigidbody.velocity;
vel.Normalize();
rigidbody.velocity = vel * (movementSpeed / 2);
playerManager.isInAir = true;
}
}
// 땅에 있을 때 발의 위치를 재조정함
if (playerManager.isGrounded)
{
if (playerManager.isInteracting || inputHandler.moveAmount > 0)
{
myTransform.position = Vector3.Lerp(myTransform.position, targetPosition, Time.deltaTime);
}
else
{
myTransform.position = targetPosition;
}
}
}
Raycast를 통해 땅감지가 됨
땅을 다음과 같이 감지한다.
if (Physics.Raycast(origin, -Vector3.up, out hit, minimumDistanceNeededToBeginFall, ignoreForGroundCheck))
감지될 경우 착지 상태로 바꾸고 땅의 위치를 저장해둔다.
normalVector = hit.normal;
Vector3 tp = hit.point;
playerManager.isGrounded = true;
targetPosition.y = tp.y;
그 다음 착지 애니메이션을 재생할지 여부를 계산한다.
// 공중에서 낙하도중 땋에 닿은 것이라면
if (playerManager.isInAir)
{
// 많이 떨어지던 중 땋에 닿음 -> 착지 애니메이션 실행
if (inAirTimer > 0.5f)
{
Debug.Log("You were in the air for " + inAirTimer);
animatorHandler.PlayTargetAnimation("Land", true);
inAirTimer = 0;
}
// 그냥 조그만 난간에서 떨어짐 -> 이동 애니메이션
else
{
animatorHandler.PlayTargetAnimation("Locomotion" , false);
inAirTimer = 0;
}
playerManager.isInAir = false;
}
땅 감지가 안됨
이 경우엔 땅에 있다가 떨어진 경우 낙하 애니메이션을 실행
// 공중에 떠있을 경우
else
{
// 땅에 있음 변수를 false로 바꿈
if (playerManager.isGrounded)
{
playerManager.isGrounded = false;
}
// 공중으로 떨어지기 시작할 때 첫 한번 실행
if (playerManager.isInAir == false)
{
// 다른 애니메이션 실행중이 아니라면 낙하 애니메이션 실행
if (playerManager.isInteracting == false)
{
animatorHandler.PlayTargetAnimation("Falling", true);
}
// 가던 방향으로 살짝 느리게 감
Vector3 vel = rigidbody.velocity;
vel.Normalize();
rigidbody.velocity = vel * (movementSpeed / 2);
playerManager.isInAir = true;
}
}
플레이어 높이를 땅과 맞춤
아까 저장해둔 땅의 위치로 플레이어를 자연스럽게 이동시킴
// 땅에 있을 때 발의 위치를 재조정함
if(playerManager.isGrounded)
{
if(playerManager.isInteracting || inputHandler.moveAmount > 0)
{
myTransform.position = Vector3.Lerp(myTransform.position, targetPosition,Time.deltaTime);
}
else
{
myTransform.position = targetPosition;
}
}
댓글남기기