Depth-First Search 방식을 적용해서 해결했다. 예전에 만든 Flood Fill 알고리즘과 검색을 통해 필요한 부분을 접목시켜보았다.
한 Depth마다 모든 부분을 검사하는 방식으로 동작하며, 최종적으로 MathF.Max를 통해 방문 가능한 가장 큰 값을 반환한다.
using System;
public class Solution {
public int solution(int k, int[,] dungeons) {
int answer = 0;
bool[] visited = new bool[dungeons.GetLength(0)];
answer = Search(answer,0,k,dungeons,visited);
return answer;
}
public int Search(int answer,int depth, int k, int[,] dungeons,bool[] visited)
{
for (int i = 0; i < dungeons.GetLength(0); i++)
{
if (!visited[i] && dungeons[i, 0] <= k)
{
visited[i] = true;
answer = Search(answer,depth + 1, k - dungeons[i, 1], dungeons,visited);
visited[i] = false;
}
}
answer = (int)MathF.Max(answer, depth);
return answer;
}
}
주의 : 이 프로젝트의 플레이어 이동은 RigidBody와 Collider를 대체해 Character Contoroller를 사용합니다. 일반적인 구현과 차이가 있습니다.
미끄러짐, 사라지는 발판, 넉백을 위한 함정 등 해당사항이 선결조건들은 모두 OnControllerColliderHit을 사용한다.
Character Controller는 RigidBody와 Collider가 존재하지 않는다. (대체하는 것들이 있긴 함). 그래서 일반적인 OnCollision이나 OnTrigger로는 검출할 수 없다.
접촉한 오브젝트의 레이어에 따라 미끄러질 수 있는 지를 먼저 체크한다.
private void OnControllerColliderHit(ControllerColliderHit hit)
{
if (1 << hit.gameObject.layer == 1 << LayerMask.NameToLayer("Slip"))
{
isslipped = true;
}
else isslipped = false;
}
그 후 실제 이동을 담당하는 스크립트에서 이동을 멈췄을 때 미끄러질 방향을 미리미리 저장해준다.
private void Move()
{
Vector3 movementDirection = GetMovementDirection();
if (movementDirection != Vector3.zero)
{
player.slideDir = movementDirection.x * Camera.main.transform.right;
}
Move(movementDirection);
}
그리고 이동이 멈췄을 때는 Character Contorller의 SimpleMove 메서드를 이용해 간단하게 이동을 구현한다.
private void Move(Vector3 movementDirection)
{
float movementSpeed = player.GetMoveSpeed;
Vector3 cameraRight = Camera.main.transform.right;
movementDirection = cameraRight * movementDirection.x;
Vector3 finalMovement = movementDirection * movementSpeed + player.ForceReceiver.Movement + player.knockbackDir;
if (player.slideDir != Vector3.zero && player.isslipped && Mathf.Approximately(finalMovement.magnitude, 0f) &&
!isSliding)
// 이동이 종료되고, 미끄러운 상태라면
{
isSliding = true;
slideTimer = 0f;
}
if (isSliding)
{
slideTimer += Time.deltaTime;
if (slideTimer >= slideTime)
{
isSliding = false;
player.slideDir = Vector3.zero;
return;
}
if (player.isslipped) //미끄러지지 않는 부분 들어갈 때 바로 멈출 수 있도록
controller.SimpleMove(player.slideDir * slidingSpeed);
return;
}
controller.Move(finalMovement * Time.deltaTime);
}
미끄러질 때는 남아있는 finalMovement Vector에 영향을 받지 않도록 return 해준다.
if (movementDirection != Vector3.zero)
{
player.slideDir = movementDirection.x * Camera.main.transform.right;
}
문제 현상 : 위 스크립트처럼 이동할 방향이 0이 아닐 때만 slideDir를 변경하도록 작성했는데 계속 0이 되는 현상이 발생했다.
문제 원인 : state들의 Base가 되는 BaseState의 클래스변수로 slideDir = Vector3.Zero라고 선언했었는데, 이 부분이 상태가 변경되면서 계속 호출되어 초기화 되었던 것으로 보인다.
문제 해결 : 따라서 위 값이 최초 한번 호출된 이후로는 초기화 되지 않도록 관리해주어야 한다.
처음에는 1번을 사용했으나, 더 고민한 결과 StateMachine이 Player와 역참조 관계에 있기에 Player 스크립트로 변수를 옮겨 사용하도록 했다.
문제 현상 : z축 이동에서는 미끄러지지 않는다.
문제 원인 : MovementDirection을 설정할 때 사용되는 MovementInput의 값이 vector2이기 때문에 x,y값만 존재하고, 이것을 Camera.main.transform.right를 사용하여 가공하기 때문에, 처음 slideDir에는 카메라에 대한 부분을 신경쓰지 못하였다.
문제 해결 : 현재처럼 MovementDirection의 x값에 카메라 보정을 곱해주어 미끄러지는 방향을 변경하도록 수정해주었다.
위의 충돌처리 방식을 사용하여 Player와 충돌했을 때 발판이 사라지는 연출을 시작한다.
hit.Point와 hit.Collider.bounds.max를 사용해서 발판 위를 밟았을 때만 사라지도록 작성되었다.
private void OnControllerColliderHit(ControllerColliderHit hit) //미끄러짐 구현에 필요한 메서드
{
if (hit.gameObject.TryGetComponent<DisappearBlock>(out DisappearBlock disappearBlock))
{
if (Mathf.Approximately(hit.point.y, hit.gameObject.GetComponent<Collider>().bounds.max.y))
disappearBlock.StartAnim();
}
그리고 애니메이션이 끝날 때 event를 사용하여 Reset메서드를 호출한다.
public void InvokeReset()
{
_collider.enabled = false;
_meshRenderer.enabled = false;
Invoke(nameof(Resetting),resettingTime);
}
Invoke를 활용해 일정 시간 후 다시 재생성 되도록 하는데, 사실 재생성이 아니라 콜라이더와 렌더러만 On/Off 하는 방식으로 작동한다.
문제 현상 : 오브젝트에 사용되는 Material의 알파값을 변경했을 때 투명도가 변경되지 않음.
문제 원인 : 렌더모드에 따라 투명도가 적용되는지의 여부가 다르다.
차이가 있긴 하지만 그렇게 큰 차이는 아닌 모양이다.
사라지는 시간 변경의 어려움
애니메이션 종료 시 재설정을 하도록(발판이 사라지도록) event를 걸어놨기 때문에 사라지는 시간을 변경하려면 애니메이션을 다시 세팅해줘야하는 번거로움이 생겼다. 아직 완성본이 아니기에 적절한 시간을 구하기 위해서는 다른 방법이 필요했다.
그래서 애니메이션을 1초정도 분량으로 잡고, 애니메이터의 속도를 1/사라지는 시간 으로 설정하여 번거로움을 해결해보았다.
추후 가능하다면 DOTween을 활용해볼까 한다.
RigidBody를 활용할 수 있다면 간단하게 Addforce로 처리할 수 있다.
하지만 안되기에 방법을 찾아야한다.
구상 단계에서 생각한 구현 방법으로는
1. PlayerHitState를 추가한다.
2-1. ForceReceiver에 추가 - 점프와 같이 물리적인 이동을 구현한다.
2-2. PlayerBaseState에 추가 - 이때까진 생각이 없었다.
먼저 함정에 닿았을 때 상태가 변경되도록 설정해주었다.
Player.cs
private void OnControllerColliderHit(ControllerColliderHit hit)
{
if (1 << hit.gameObject.layer == 1 << LayerMask.NameToLayer("Trap"))
{
if (!isKnockback)
{
isKnockback = true;
Vector3 knockback = (hit.point - hit.collider.bounds.center).normalized;
Vector3 cameraRight = Camera.main.transform.right;
knockbackDir = cameraRight.x == 0 ?
((knockback.z * cameraRight)+ knockback.y*Vector3.up)*knockbackPower :
((knockback.x * cameraRight)+ knockback.y*Vector3.up)*knockbackPower;
}
}
---------------------------------------------------------------------------
PlayerGround & AirState.cs
public override void Update()
{
base.Update();
if (player.isKnockback)
{
stateMachine.ChangeState(stateMachine.HitState);
return;
}
}
충돌했을 때 그 오브젝트의 중심과 내 캐릭터의 충돌 지점으로 방향벡터를 만들어서 넉백이 적용될 방향을 정해주었다.
그리고 PlayerHitState에서는 해당하는 애니메이션과 필요한 변수들을 변경해주었다.
public override void Enter()
{
base.Enter();
stateMachine.Player.moveSpeedModifier = 0f; // 이렇게하면 움직임 제한.
startdir = player.knockbackDir;
//StartAnimation(animData.HitParameterHash);
}
public override void Update()
{
base.Update();
player.knockbackDir = Vector3.Lerp(startdir, Vector3.zero, currenttime/needtime);
currenttime += Time.deltaTime;
if (currenttime >= needtime)
{
currenttime = 0f;
player.isKnockback = false;
player.moveSpeedModifier = 1f;
if (controller.velocity.y < 0)
{
stateMachine.ChangeState(stateMachine.FallState);
return;
}
stateMachine.ChangeState(stateMachine.IdleState);
}
}
현재는 시간으로 조건을 체크하지만, 애니메이션을 추가한 후에는 애니메이션의 진행도에 따라 체크하도록 변경할 것이다.
마지막으로 움직임을 구현하는 PlayerBaseState의 Move에서 finalMovement에 넉백방향을 더해주면 끝난다.
문제 원인 : handleinput 잠궈놔서 그 이전에 존재하던 MovementInput 벡터가 0이 되조 못하고 그대로 남아 finalMovement에 영향을 주던 것으로 확인.
문제 해결 : handleinput을 풀어주기만 해도 정상적으로 동작하지만, 넉백 시에는 조작할 수 없는 것이 보통이기에 이동에 관련있는 MovementModifier를 HitState가 될 때 0으로 변경하여 이동할 수 없도록 수정함.
-> 부딪힌 반대방향으로 밀려나야하지만 오히려 전진하는 현상
문제 원인 : Lerp를 사용할 때는 Vector3.Lerp(a,b,T);에서 a와 b의 값은 고정되어 있어야하고 T만 바뀌어야 한다. 문제가 발생한 원인은
player.knockbackDir = Vector3.Lerp(player.knockbackDir,Vector3.zero,currentTime/needTime);
이렇게 작성했기 때문에 a값이 고정되지 않아 선형이 아니게 되어 발생한 것이다.
문제 해결 : Lerp가 시작되기 전, State에 Enter할 때 knockbackDir을 변수로 따로 저장해 사용하면 값이 고정되어 문제가 해결된다.