Control Game

용준·2024년 3월 5일
0

Project

목록 보기
5/6

1. 플레이어

1.1. 조이스틱

조이스틱의 패드와 컨트롤러 스프라이트로 원형 터치 패드를 제작하였습니다.

터치패드를 제어하기 위한 TouchPad 스크립트 일부입니다.

터치패드는 3차원 공간이 아닌 Canvas 내부에 존재하기 때문에 Transform 대신 Rect Transform을 사용해 위치 정보를 받아옵니다.

조이패드의 반지름, 클릭여부, 컨트롤러의 시작지점 등을 변수로 지정해주었습니다.

터치패드를 초기화해주고, 조이패드 시작위치에 터치패드의 위치를 받아옵니다. HandleInput은 PC환경에서 실행될 때 작성할 시작점을 인자값으로 건네주었습니다.

버튼업, 다운 함수는 터치패드에 이벤트 트리거 컴포넌트를 추가해 연동해주었습니다.

FixedUpdate에서 PC일때, 모바일일때 함수를 따로 구현하여 나누어주었습니다.

매 프레임마다 실행되는 Update 함수와 달리 FixedUpdate는 프레임과 독립적으로 같은 시간 간격(정기적)으로 호출됩니다.

HandleInput 함수입니다.

만약 입력 지점이 패드의 최대치보다 크다면, 방향 거리를 1로 설정하고 방향 컨트롤러를 최대치만큼 움직이게 합니다.
반면에, 입력 지점이 최대치보다 작거나 같다면 현재 입력 좌표에 따라 방향키를 이동시킵니다.

그리고 만일 _buttonPressed가 눌리지 않았을 때의 else문에서는 _touchPad(방향 컨트롤러)의 위치를 시작점으로 이동시킵니다.
이후 현재 컨트롤러와 기준 지점의 차이를 구하면서 방향을 유지한 채로, 해당 방향을 normDiff에 저장합니다.
마지막으로, _player(PlayerMovement)가 연결되어 있다면 OnStickChanged 함수에 방향을 전달해 줍니다.

거리를 측정하는 데에는 Distance, magnitude, sqrMagnitude라는 세 가지 옵션이 있습니다.

Distance와 magnitude는 과정 및 결과가 동일하나, 편의를 위해 Distance가 제공됩니다.
반면에 sqrMagnitude는 제곱 값을 반환하여 루트 연산을 하지 않아 연산 속도가 더 빠르며, 정확한 거리 측정은 필요하지 않을 때 단순 거리를 비교하는 데 사용됩니다.

PlayerMovement 클래스의 Update 함수입니다.

첫 번째 조건문인 !isDie는 플레이어가 아직 살아있을 때만 움직일 수 있도록 하는 조건입니다. 이는 죽은 상태에서 쓰러진 채로 움직이는 것을 방지하기 위해 추가되었습니다.

두 번째 playerAni는 해당 컴포넌트가 없는 경우에만 실행되는 조건입니다. 팀 프로젝트에서 혼자 작업하지만, 향후를 대비하여 안전장치를 마련한 것입니다.
playerAni.SetFloat("Speed", (h h + v v))은 단순히 애니메이터에 속도 값을 전달합니다.

세 번째 gameObject.layer == 11의 조건문은 레이어가 11일 때에만 동작하도록 하는 부분입니다. 플레이어가 피격을 당하면 일정 시간 동안 레이어가 변경되어 무적 판정이 생기는데, 이 조건을 통해 피격 상태나 레이어 변경 상태에서는 움직임이 막히게 되었습니다.

플레이어의 이동은 RigidBody의 velocity를 이용하며, 현재 velocity 값을 Vector speed로 받아와 x축과 z 축에 지정한 스피드를 곱하여 사용합니다.

마지막으로 (h!= 0f && v!= 0f)의 조건문은 받아온 좌표값 h와 v가 모두 0이 아닌 경우에만 방향 전환을 위해 transform의 rotation값에 해당 벡터 방향을 바라보는 회전상태를 반환하는 LookRotation을 이용해 Quaternion으로 변환된 값을 넣어줍니다. 이것은 캐릭터의 방향 전환을 즉시 이뤄지게 하고, 애니메이션에 의존하지 않고 자체적으로 처리하기 위함입니다.


1.2. HP

플레이어의 체력을 담당할 이미지 입니다.


for문에서는 0번째 인덱스부터 hearts 배열의 길이 직전까지 검사를 합니다.

만약 현재 인덱스 i가 heart (설정된 풀 하트 개수)보다 작다면, hearts[i] (해당 인덱스의 배열 스프라이트)는 fullHeart 스프라이트가 됩니다.

그렇지 않고, 현재 인덱스 i가 heart보다 크다면, hearts[i]는 emptyHeart 스프라이트가 됩니다. 이로써 배열 전체를 검사하면서 설정한 heart 값에 따라 각각의 하트가 full 또는 empty로 결정됩니다.

이후, 만약 현재 인덱스 i가 overHeart (최대 하트 개수)보다 작다면, 현재 배열은 enabled = true로 활성화되어 보이게 됩니다.
그렇지 않다면, 현재 배열은 enabled = false로 비활성화되어 안 보이게 됩니다.

이렇게 overHeart가 최대 개수를 나타내며, overHeart보다 heart가 작을 경우 heart 개수만큼 fullHeart 스프라이트가 생성되고, 나머지 개수는 emptyHeart로 생성됩니다.

마지막으로, 첫 번째 조건문인 if문에서 만약 heart의 개수가 overHeart의 개수보다 높다면 (이상한 상황이므로), heart에는 overHeart의 값을 할당합니다.

따라서 heart는 overHeart의 값을 넘길 수 없으며, 최대 하트 개수를 나타내는 overHeart와 풀 하트의 개수를 나타내는 heart가 정리되었습니다.


1.3. 펫

FoxState를 제어하기 위해 foxState라는 변수를 선언해 주었습니다.

FSM은 유한 상태 기계

foxSpeed(펫의 속도)
idleDis(Idle 상태로 전환될 거리)
moveDis(Move 상태로 전환될 거리)
notFindDis(플레이어를 못 찾았을 때 플레이어 위치로 이동시키기 위한 변수)
player(플레이어와 펫의 거리를 측정하기 위해 Transform으로 받아왔습니다.)

Start에서 Find로 플레이어 오브젝트를 검색해 player 변수에 위치좌표를 넣어주고, 펫의 초기 상태를 Idle로 설정해줍니다.

펫의 상태별 행동을 다루기 위해 Update에서 사용한 switch-case문입니다.

먼저 Idle(대기)상태인데요, 거리를 측정하기 위해 Distance 함수를 활용하였고, transform (펫)의 위치와 플레이어 간의 거리를 계산합니다.

그리고 이 거리가 moveDis 범위 안에 있다면, Idle을 true로 설정하여 Idle 애니메이션의 향하는 트랜지션 조건을 충족시켰습니다.
만약 해당 거리가 moveDis 범위보다 크다면, foxState 상태를 FoxState.Move로 전환합니다.

Move(이동) 상태입니다.

transform (펫)과 플레이어의 위치를 계산하여 moveDis 범위를 벗어나면 펫은 플레이어를 따라가도록 구현했습니다.

먼저, LookAt 함수를 사용하여 펫의 회전을 플레이어를 향하게 설정했습니다. 각 트랜지션 조건에 맞는 매개변수 변수를 변경하여 펫의 동작을 조절했습니다.

그 후, 미끄러짐 현상과 다른 애니메이션 중에 움직이는 현상을 방지하기 위해 isStopped을 true로 설정하여 이동을 멈추도록 하고, ResetPath 함수를 사용하여 목적지를 초기화하고, SetDestination 함수를 사용하여 새로운 목적지로 플레이어를 설정했습니다.

두 번째 조건문인 if문에서는 펫이 플레이어를 향해 이동 중에 moveDis 안에 들어왔을 때, "Move"를 false로 설정하여 이동 애니메이션을 멈추고, "Idle"을 true로 변경하여 대기 애니메이션을 실행하도록 했습니다.

Gizmos 클래스의 WireSphere를 사용해 눈으로 확인할 수 있게 노출시켰습니다. 테스트 환경에서 많이 편하단 느낌을 받았는데 앞으로도 비슷한 작업을 할 때에는 기즈모를 많이 사용해야할 것 같습니다.

펫 상태 코드는 전반적으로 잘 작동되지만 테스트를 해보던 중 두가지 문제가 있었습니다.

1. 구워지지 않은 지점을 점프로 넘어가면 펫이 넘어오지 못하는 현상

펫의 NavMesh를 껐다켜주는 것으로 해결했습니다.

Move 상태에서 if문으로 펫이 플레이어로부터의 위치가 notFindDis보다 클 경우,

enabled를 false로 꺼준 상태에서 펫의 위치를 플레이어의 위치를 옮겨주며,
다시 enable를 이용해 NavMeshAgent를 true로 활성화시켜 주었습니다.

이후 플레이어의 위치에 있는 펫은 Idle 상태로 돌아가게 됩니다.

2. 플레이어가 떨어졌을 때 펫 내비게이션이 고장나는 경우

높은 곳에서 낙하하거나 장애물에서 떨어질 때 펫이 플레이어의 위치로 가서 NavMeshAgent가 활성화되면 발생한 문제였습니다. 원하는 시점에 NavMeshAgent를 껐다 켜는 것이 어려워 문제가 발생했습니다.

플레이어가 점프 중이거나 Ground가 아닐 때는 코드가 실행되지 않도록 구현했지만 플레이어가 점프를 계속하면서 이동할 경우 펫이 자연스럽게 따라오지 못하는 어색한 상황이 연출됩니다.

아직 NavMeshAgent를 사용하면서 이 문제를 해결하는 방법을 찾지 못했지만 포기하지 않고 계속해서 해결책을 찾고자 합니다.

일단은 NavMeshAgent의 항목을 제거하고 Translate 함수를 사용하여 움직이도록 구현했습니다.

코드를 수정하였고 펫의 이동도 플레이어와 마찬가지로 deltaTime을 곱해 프레임간의 격차를 없애주었습니다.


다음은 펫 스킬로, 플레이어에게 도움을 줄 수 있도록

플레이어의 일정 범위 안에 있는 일정시간 동안 행동이 정지되는 효과로 기획했습니다.

Image Type의 Filled를 이용하여 두 겹으로 설정했습니다.
하나는 BackGround로, 다른 하나는 Button으로 쿨타임을 나타냅니다.

coolDown(0부터 값을 누적시킬 변수)
coolTime(실질적인 쿨타임)
waitTime(Invoke 함수를 기다렸다 실행시켜줄 시간)
cdImg(받아올 이미지)
cool(쿨타임인지 체크할 변수)
foxFsm(FoxFSM 클래스의 스크립트를 받아올 변수)
foxAttackFoot(스킬 시전시 발생할 이펙트)

스킬 버튼의 OnClick으로 실행될 public으로 선언한 함수인 OnClickBtn01 함수입니다.

중복 방지를 위해 cool이 false인 경우의 조건을 걸어주었으며 바로 이후 cool을 true로 바꿔줍니다.

다음으로 CoolDown 코루틴을 불러오고 foxFsm에 접근하여 FoxSkill01 (스킬 애니메이션)을 실행하게 됩니다.

마지막으로 Invoke로 AttackFoot 함수를 waitTime 이후에 실행시켜주어 원하는 애니메이션 모션에 맞춰 이펙트가 켜지도록 설정하였습니다.

코루틴 함수입니다. 스킬 쿨타임은 0부터 시작되기 때문에 초기에 cooldown을 0으로 초기화합니다.

while문에선 Time.deltaTime * 1 (1초씩 누적) 값을 더해주었습니다.
이렇게 함으로써 cdImg (버튼 이미지)의 fillAmount를 cooldown / coolTime의 값으로 설정하여 0.0f부터 1.0f까지의 값을 가지도록 하였고 동기화를 시켜주었습니다.

만일 cooldown이 coolTime보다 크거나 같다면 fillAmount는 0.0에서 1.0로 증가한 것을 의미합니다.
그 후에는 스킬을 다시 사용할 수 있도록 cool = false로 바꾸어주었으며, StopCoroutine을 이용하여 자기자신을 중단시킵니다.

OnClickBtn01 함수에서 Invoke로 실행되는 함수입니다.

펫에 붙어 있는 이펙트를 SetActive(true)를 사용하여 활성화시켜주었습니다.

그리고 생성된 이펙트 오브젝트는 자체적으로 비활성화되도록 스크립트를 만들어 인스펙터 창에서 시간을 조절하여 이펙트가 일정 시간이 지나면 자동으로 비활성화되도록 설정해주었습니다.

다음으로 펫 스킬을 오브젝트에 적용하는 기능을 만들었습니다.

펫 스킬을 사용하면 상대 오브젝트의 콜라이더를 검출하는 코드입니다.

listCols(검출한 콜라이더를 담을 List)
colExtractRadius(검출할 범위)
player(플레이어)

  • 여기서 플레이어에 접근해준 이유는 처음에는 펫을 중점으로 해당 범위 안에 있는 콜라이더를 검출하려고 했는데, 플레이어의 이동에 따라 뒤에 따라오는 펫의 중점이기에 범위가 일정하지 않은 문제가 있었습니다. 따라서 플레이어를 중점으로 받아오기 위한 선언입니다. Start 함수에서는 Null 연산자를 사용했습니다.
  player = GameObject.Find("Player")?.transform;

이렇게 구현함으로써 Find 함수의 결과가 null이 아니라면 transform을 받아옵니다.
이를 통해 player 변수에는 유효한 transform이 들어가게 됩니다.

FoxColExtract_ 함수는 콜라이더를 검출하는 기능을 담고 있습니다.

콜라이더의 검출은 OverlapSphere를 이용해 수행되었는데요, 이 함수의 인자로 중심, 범위, 그리고 비트 연산자를 사용하여 10번 레이어를 지정해 주었습니다.

여기서 10번 레이어는 고양이에게 할당된 레이어를 의미합니다.
따라서 플레이어 중심 + 범위 내에 있는 + 10번 레이어의 콜라이더를 검출하는 코드가 됩니다.

이 검출된 콜라이더들은 Collider[] cols로 받아왔습니다.
cols != null 조건을 확인하여 검출한 콜라이더가 비어있지 않다면 아래의 foreach 루프가 실행됩니다.

foreach는 cols를 검사하며 검사한 각 col들을 listCols에 Add하여 저장합니다.

테스트 과정에서 불필요한 오브젝트 검출을 줄이기 위해 태그 검색을 CatEnemy로만 하였는데,
실제로는, '적'과 같은 불특정다수가 집합되게끔 레이어 검색을 하는 편이 좋을 것입니다.

마지막으로 col의 게임 오브젝트와 0번 자식이 조건을 통해 꺼지거나 켜지게 됩니다.
activeSelf를 조건으로 사용한 이유는 적이 사라지거나 collider가 꺼진 상황을 대비한 것입니다.

col.GetComponent<CatFSM>().FoxSkill_CatNavStop()

바로 살펴볼 CatFSM의 스크립트 안에 구현된 함수입니다.

간단하게 이 함수는 코루틴을 이용하여 speed = 0에서 원래 스피드 값으로 바뀌도록 구현되어 있습니다. 이 스킬을 맞으면 멈춰 있는 동작을 나타내는 것입니다.

적 객체인 고양이 오브젝트입니다.

고양이가 이동 상태일 때 실행되는 Move 함수입니다.

transform(CatEnemy)가 originPos로부터의 거리가 FindPlayerRadius의 범위보다 크다면, catState의 상태는 Return으로 변경됩니다.

또한 초기 위치로부터의 거리뿐만 아니라 transform(CatEnemy)가 플레이어의 위치로부터 떨어져 있는 거리가 FindPlayerRadius의 범위보다 크다면 catState는 다시 Return 상태로 변경됩니다.

이로써 초기 위치로부터 지정된 범위가 아니더라도 플레이어와 일정 거리가 떨어져도 복귀하는 동작이 구현되었습니다.

Move 함수의 마지막 if문에서는 transform(CatEnemy)가 플레이어의 위치로부터 떨어져 있는 거리가 FindPlayerRadius의 범위 안에 있다면 catNav의 destination (목적지)를 플레이어로 지정해 주어 이동되게 합니다.

고양이가 복귀할 때 실행하는 Return 함수입니다.

자기 자신과 초기 위치를 검사하여 거리가 0.1보다 크다면 목적지를 originPos로 이동시키고,
멈춰야 할 거리인 stoppingDistance는 0으로 설정합니다.

반대로, 거리가 0.1f 이내라면 네비의 isStopped를 true로 설정하고,
ResetPath를 호출하여 경로를 초기화한 뒤 초기 위치와 회전 값을 설정하며 Idle로 돌아가게 됩니다.


1.4. 피격

Player의 충돌을 체크하는 부분은 OnCollisionEnter에서 구현되었습니다.

다양한 충돌체의 활용과 관리를 위해 각 충돌된 collision의 태그가 "CatEnemy", "Trap", "CubeRun", "CircleTrap" 중 하나일 때 실행되는 조건문이 있습니다.
이렇게 함으로써 충돌체의 쓰임새에 따른 넉백의 파워를 다르게 할 수 있고, 코드를 관리하기도 편하게 만들었습니다.

OnPlayerDamageLayer 함수의 실행과 피격(충돌) 시 애니메이션 재생, 그리고 PlayOneShot으로 사운드를 재생합니다.

마지막으로 PlayerHitHeart 함수를 실행하여 하트를 감소시키는 부분이 구현되어 있습니다.

isJump를 true로 바꿔준 이유는 피격 시 점프 판정을 갖게 해 주어 피격 시 점프가 안되게 구현하기 위함입니다.

OnCollisionEnter에서 충돌 시 실행되는 OnPlayerDamageLayer 함수에 대해 설명하겠습니다.

이 함수는 Transform pos를 인자로 받아, 현재 충돌된 collision의 transform을 넣어 줍니다.

가장 먼저, gameObject(플레이어)의 레이어를 12로 바꿔줍니다.
12번 레이어로 변경될 때는 CatEnemy의 태그와 충돌 체크를 하지 않도록 합니다.


플레이어가 12번 레이어로 설정되어 있는 동안에는 CatEnemy와의 충돌 체크가 일어나지 않습니다.

이후에 playerX와 playerZ를 선언하여 날아갈 방향을 정했습니다.

현재 player의 위치에서 - pos(충돌체)의 위치가 0보다 크다면 1을,
그렇지 않다면 -1의 방향 값을 3항 연산자로 정해주었는데요, 충돌체의 반대 방향으로 이동하게 됩니다.

플레이어에는 Rigidbody의 AddForce를 사용하였고, 순간적인 힘과 질량을 고려하여 ForceMode를 Impulse로 설정하였습니다. 방향은 x에 playerX와 playerY를 곱한 값을, y에 playerJumpSpeed 만큼 값이 들어가게 됩니다.

이후 Invoke 함수로 레이어를 원래 레이어(11)로 변경시킬 함수 OffPlayerDamageLayer를 2초 뒤에 실행하도록 했습니다.

이 2초는 PlayerRenderer 코루틴의 재생시간보다 큰 값이라 충돌 시 시각적인 효과를 잘 보여주게 됩니다.

이어서 PlayerRenderer 코루틴입니다.

처음에 위 코드를 작성할 때 반복 동작을 인지하고 for문으로 구현하려고 하였으나 마땅히 좋은 방법이 떠오르지 않아 하드코딩을 하고 넘겼었습니다.

현재는 훨씬 좋은 방법이 떠올라서 코드를 아래와 같이 수정하였습니다.

코드 개선사항

int blinkCount;

for (int i = 0; i < blinkCount; i++) 
{
  player.transform.GetComponent<MeshRenderer>().enabled = !player.transform.GetComponent<MeshRenderer>().enabled;
  yield return new WaitForSeconds(0.2f);
}


1.5. 사망


간단하게 스테이지를 구성한 뒤 플레이어가 맵을 이탈하여 추락하면 죽은 상태가 되도록 사망처리를 하는 코드를 작성하였습니다.

코루틴으로 함수 호출 시간(5초)을 지정하여 일정 시간 뒤에 부활하며 아래 코드를 통해 씬을 재시작하게 됩니다.

  SceneManager.LoadScene(SceneManager.GetActiveScene(). buildIndex);

코루틴에서 사용하는 PlayerDie 함수는 PlayerMovement(플레이어 이동 클래스)에서 받아옵니다.


2. 구성요소

2.1. 스크린 회전

disAway : 플레이어로부터 카메라의 거리
disUp : 플레이어로부터 카메라의 높이
camSpeed : 카메라의 회전 속도
cam_Axis : 카메라 중심 오브젝트
cam : 메인카메라

MainCamera가 직접 움직이지 않고 camAxis를 회전시키면 회전에 따른 MainCamera가 이동됩니다.

카메라 관련 함수는 LateUpdate에서 실행시켰습니다. 호출 순서에 의해 카메라가 떨리는 현상을 방지하기 위함입니다.

이 함수는 회전축을 x축만 회전할 것이므로 Mouse X의 값을 누적시킵니다.
Mouse X는 마우스의 X축 좌표를 -1.0f부터 0까지 1.0f까지의 값을 반환합니다.

이후 회전을 메인 카메라가 아닌 메인 카메라의 부모로 있는 cam_Axis를 Quaternion.Eular 값으로 받아옵니다. 주의할 점은 이동 값과 회전 값을 깜빡하지 않아야 한다는 것입니다.

예를 들어, 마우스의 x축 이동 방식은 회전 값의 y축과 같습니다.
또한, 상/하로 움직이는 이동 y축 방식은 회전 값의 x축이 회전되는 것과 같습니다.
한 가지 더, y축은 현재 쓰이지 않았지만 한국에서 생각하는 상/하의 값이 반대로 되어있는데, 이를 해결하려면 -1을 곱해주면 됩니다.

cam_Axis가 회전하면 자식으로 붙어있는 메인 카메라가 회전하는 효과가 나타납니다.

이 함수는 플레이어의 키보드 입력을 통한 이동을 구현합니다.

먼저, Horizontal과 Vertical을 이용하여 키보드 입력에 따른 방향을 movement에 할당합니다.
이후, 정규화를 통해 방향 벡터를 1로 만들어 균일한 이동이 가능하게 합니다.

첫 번째 조건문에서는 movement가 Vector.zero가 아닐 때, 즉 속도가 있을 때로 판단합니다.

PlayerAxis는 빈 오브젝트이고, player는 현재 보이는 렌더러를 갖고 있는 플레이어입니다.
스크립트는 PlayerAxis가 갖고 있기 때문에 transform은 Player가 아닌 PlayerAxis가 됩니다.
transform.rotation은 cam_Axis의 y축 회전 값과 Mouse X를 받아오는 코드입니다.

플레이어의 이동은 Translate 함수를 사용하여 이동시킵니다.

player.transform.localRotation = Quaternion.Slerp(player.transform.localRotation, Quaternion.LookRotation(movement), 6 * Time.deltaTime);

실질적인 회전은 위 코드를 통해 이루어집니다

Slerp 함수는 A에서 B까지 0.0 ~ 1.0 퍼센트 비율로 부드러운 회전을 제공합니다.
여기서, Quaternion.LookRotation(movement)는 현재 방향 벡터를 바라보는 회전을 생성합니다.

SetFloat의 Speed 값은 현재 방향의 magnitude(길이)로 설정됩니다.

마지막으로 PlayerMovement 클래스에서 cam_Axis의 위치를 플레이어의 위치로 설정하고, 플레이어의 이동이 가능한 레이어가 11일 때 PlayerMoveTest 테스트 함수를 실행하도록 구현하여 완성하였습니다.


2.2. 장애물

첫번째 장애물 큐브입니다.
생성되면 지정한 방향으로 회전하여 플레이어가 닿으면 피격 처리를 합니다.
rotationPower : 회전하는 힘
AddTime : 재생성되는 시간
OnEnable 함수는 해당 스크립트나 게임 오브젝트가 활성화될 때마다 호출되는 함수입니다.

Start는 최초 1회의 초기화에 주로 사용되고,
OnEnable은 활성화 시마다 반복적으로 수행되는 초기화 작업에 사용됩니다.

회전하는 힘과 큐브 크기를 점진적으로 늘려줍니다. (2.0f를 초과하지 못하게 if문으로 조건을 걸어주었습니다.)

AddTorque(Vector)는 Vector 방향축으로 회전력이 생겨 강체를 회전할 때 사용하는 함수입니다.
ForceMode.Acceleration은 무게를 무시하고 지속적인 힘을 가하는 기능입니다. [참고]

Ground 태그가 아닌 오브젝트 충돌 발생 시 코루틴 함수를 중지하고 rotationPower와 cubeScale을 초기 값으로 리셋한 뒤, 특정 시간과 랜덤한 시간을 기다린 후 AddCube 함수를 실행합니다.

회전값이 달라지는 문제가 있었는데요, AddForce나 AddTorque를 여러번 사용하게되면 이전 값이 누적해서 적용된다고 합니다. AddCube 함수에 스케일, 위치, 회전, 회전각을 초기화하는 코드를 넣어서 해결했습니다.

두번째 장애물 기둥입니다.
플레이어가 올라서면 몇 초 동안 깜빡거린 뒤 사라져 플레이어를 떨어트립니다.

애니메이터를 사용하여 오브젝트의 Mesh Renderer를 조절하였습니다.

기둥이 사라질 시간은 코루틴으로 조절하였습니다.

세번째 장애물 회전판입니다.
코루틴과 while문으로 회전하게 하였습니다.
실린더 오브젝트를 배치하고 태그와 레이어를 적대대상(Trap)으로 설정하여 플레이어가 피격되면 밀어냅니다.

네번째 장애물 골렘입니다.

findPlayerDis : 골렘의 플레이어 탐색 거리 (상태 변환을 위해)
attackStep : 골렘의 공격 단계 (변칙적인 연출을 위해 왼손과 오른손을 번갈아가며 사용합니다.)
curTime : 공격 딜레이를 조절할 변수
AttackDealyTime : 공격 딜레이
golemRotSpeed : 회전속도

골렘을 회전판 위에 배치할 것이기 때문에 같은 회전값을 설정해주었습니다.

Idle 함수는 골렘의 거리가 플레이어와 인접하다면 Attack 상태로 전환하고, Attack 상태가 되면 Attack 함수를 실행합니다.

Attack 함수는 다시 조건을 확인합니다.

먼저, 만약 여전히 골렘과 플레이어 간의 거리가 findPlayerDis 범위 내에 있다면 먼저 attackStep에 랜덤한 값을 할당합니다.

이 값은 0 또는 1이 되고 두가지 공격중 랜덤으로 실행됩니다.

플레이어가 근처에 없으면 Idle 상태로 돌아갑니다.


2.3. 플랫폼

인스펙터 창에서 지정한 방향(상,하 또는 좌,우)으로 이동하도록 조절할 수 있게 만든 발판입니다.
x, y, z : 어떤 값을 받을지 트리거 역할을 하는 변수
ground_X, Y, Z : 이동 방향 값을 사전에 설정한 변수
isArrive : 목적지 도착 여부 트리거
orizinPos, destiPos : 시작점, 도착점
dir : 이동 방향
playerToGround : 플레이어가 어느 방향에 위치해있는지 알려줄 값

orizinPos에 초기 위치 값을 할당하였으며, destiPos(목적지)의 위치는 new Vector3의 3항 연산자를 통해 지정됩니다.

불 변수가 체크되어 있으면 그라운드의 현재 포지션 값이 사용되고 불 변수가 체크되어 있지 않으면 지정된 값이 사용됩니다.

이를 통해 불 변수의 체크 여부에 따라 목적지가 변경되어 이동할 수 있습니다.

MoveTowards 함수를 사용해 시작점, 도착점으로 이동하도록 설정합니다.

OnCollisionStay 함수에서는 충돌체(collision)의 태그가 Player면서, 해당 collision의 RigidBody의 velocity(속도)가 0일 경우에 플레이어의 위치를 현재 위치 - playerToGround로 설정합니다.

여기서 playerToGround는 Update 함수에서 받아와 현재 그라운드의 위치 - 플레이어의 위치를 누적 저장하는 변수입니다.

이렇게 함으로써 현재 그라운드 위의 플레이어가 위치해 있는 방향 그대로 움직이게 만들어줍니다.

2.4. UI

플레이어 조작 클래스에서 사용한 점프 이미지와 코드입니다.

함수 내에서는 먼저 2회 이상의 중복 점프를 방지하기 위한 조건인 isJump를 체크합니다.
이를 통해 플레이어가 이미 점프 중인 경우 추가 점프를 막아줍니다.

그 후, 플레이어가 사망한 상태인지를 확인하는 부분은 isDie로 표시되어 있습니다.
이 부분은 나중에 테스트를 통해 추가되었는데, 플레이어가 죽어있는 상태에서도 점프 버튼이 작동하지 않도록 설정해주는 역할을 합니다.

중복 점프 방지를 위해 isJump를 즉시 true로 설정하고, 애니메이션의 트랜지션 조건값과 사운드를 재생하는 코드를 통해 점프 시 시각적 및 청각적 효과를 적절히 연출합니다.

더불어, 점프 동작에 역동적인 느낌을 부여하기 위해 RigidBody의 AddForce를 활용하였습니다. 이때 사용된 ForceMode는 Impulse로, 순간적인 힘을 가하면서도 무게를 고려하여 플레이어를 역동적으로 띄우게 됩니다.

이러한 ForceMode에는 Impulse와 VelocityChange뿐만 아니라 Force와 Acceleration이 있습니다. 각각은 순간적인 힘, 무게 무시, 연속적인 힘, 연속적인 가속을 나타냅니다.

이 중에서 순간적인 힘과 무게를 고려하기 위해 Impulse를 선택한 것이며, 이렇게 조합하여 코드를 작성함으로써 원하는 점프 동작을 얻을 수 있었습니다.

중복 점프 방지는 OnCollisionEnter에서 Ground 태그와 충돌했을 때 isJump를 false 상태가 되어야만 점프가 가능해집니다.

점프 애니메이션을 동작중이었다면 SetBool의 파라미터 값인 "JumpToGround"를 true로 바꿔 주어 점프 애니메이션이 재생되지 않게 해 주었습니다.

MinimapCam 클래스는 카메라의 위치를 조절하여 미니맵을 관리합니다.

x, y, z 축 이동 여부를 결정하는 3개의 bool 변수와 플레이어 정보를 담는 Manager의 싱글톤 패턴이 사용됩니다.

원형 이미지는 컴포넌트의 Mask를 이용해 나타내 주었습니다.

줌 인/아웃 효과는 fieldOfView 속성을 사용하여 구현했습니다. Mathf.Min과 Mathf.Max 함수를 활용하여 줌 값이 zoomMin과 zoomMax 범위 내에서 유지되도록 했습니다.

버튼이 눌릴 때마다 Input.GetButtonDown을 통해 해당 방향으로 줌 인 또는 줌 아웃을 수행하며, fieldOfView 값을 적절히 업데이트했습니다.


3. 마치며

velog는 영상 첨부가 불가능하여 이 곳에 들어가면 확인할 수 있습니다.

0개의 댓글

관련 채용 정보