일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 장학팀
- 도트
- 애니메이션
- 픽셀 아트
- TOOL
- photoshop
- 연습
- 스마일게이트
- COSMO
- 서포터즈
- menu
- 도트공부
- 포토샵
- 인디게임 개발
- 픽셀아트
- 반환원정대
- 에이세프라이트
- Pixelart
- pixel art
- 드로잉 연습
- 노하우
- 자원순환보증금관리센터
- 드로잉
- 개발
- 채색
- Aseprite
- layer
- 모작
- 멋쟁이사자처럼
- 기초
- Today
- Total
소소한 나의 하루들
2d 플랫포머(4) - 플레이어 점프 구현하기 본문
출처: https://youtube.com/playlist?list=PLO-mt5Iu5TeYI4dbYwWP8JqZMC9iuUIW2&feature=shared
#1. 물리 점프
단발적인 입력에 한해서는 Update에 작성하는 것이 좋다.
public float jumpPower;
private void Update()
{
//Jump By Button Control
if (Input.GetButton("vertical"))
rigid.AddForce(Vector2.up * jumpPower, ForceMode2D.Impulse);
---
}
실행해보면 제대로 작동한다.
그런데, 올라가는 것은 빠른데 낙하하는 것이 살짝 느리다. 왜냐하면, 전에 설정한 Linear Drag(공기저항) 값을 넣었기 때문이다. 그것도 그렇고 캐릭터가 지금 받는 중력값이 기본값이기 때문이다.
따라서 중력값을 키우면 캐릭터가 조금 더 빨리 떨어질 수 있을 것이다.
Edit 메뉴>Project Setting>Physics 2D에서 Y값 조작으로 중력 값을 설정할 수 있다. (기본값 y: -9.81)
만약 이 값을 건드리고 싶지 않으면, RigidBody 2D에서 Gravity Scale옵션을 높이면 된다.
Gravity Scale은 오브젝트에 적용되는 중력 비율이다. (현재 프로젝트에 설정되어있는 Gravity 값을 해당 오브젝트에 어느정도의 비율로 적용할 것인지 결정. 1=100% 0.5=50%)
RigidBody 2D 컴포넌트의 Gravity Scale과 Project Setting 설정창의 Jump Power를 조절하여 적절히 배분하면 원하는 모습으로 구현할 수 있다.
#2. 애니메이션
제자리에서 점프 시에는 아무런 애니메이션이 없다. 그리고 이동 중 점프 시에는 이동 애니메이션이 재생된다.
따라서 점프 시 애니메이션을 추가해본다.
Jump 애니메이션 프레임들을 Project에서 다중선택해서 Jump1 애니메이션을 생성한다. Jump1 애니메이션은 반복하면 안된다. (Project 창에서 Animation>Jump1 클릭 후 Loop Time 체크 해제)
걷는 중에도, 가만히 있는 중에도 점프를 할 수 있다.
따라서 Transition을 만들어서 각 애니메이션끼리 연결해준다. 그리고 ‘점프를 했다’라는 상태를 표시하는 Parameter도 만들어줘야한다.
Bool타입의 IsJump1 매개변수를 생성한다.
idle상태 → walk상태 : IsWalk (true)
walk상태 → idle상태 : IsWalk (false)
walk상태 → jump상태 : IsWalk (true) / IsJump (true)
jump상태 → walk상태 : IsWalk (true) / IsJump (false)
idle상태 → jump상태 : IsWalk (false) / IsJump (true)
jump상태 → idle상태 : IsWalk (false) / IsJump (false)
이렇게 Animation과 Animator 작업이 끝났다.
이제 스크립트에서 코드로 매개변수 값을 설정해줘야한다.
전에 Animator를 anime 변수로 선언, 초기화했었으니, anime를 그대로 활용한다.
이대로 저장하고 실행해보면, 가만히 있는 상태에서 점프하면 Jump 애니메이션이 잘 동작하지만 착지 후에는 Jump 애니메이션이 남아있는 상태로 고정이 된다.
착지할 때 Jump에서 Walk이나 Idle으로 다시 transition 화살표를 타고 애니메이션 state가 돌아와야한다.
#3. 레이 캐스트(RayCast)
RayCast는 눈에 안보여서 원래 파악하기가 어렵다.
3d 강좌에서는 OnCollitionEnter을 사용했었는데, 이번에는 RayCast를 사용해보겠다.
RayCast는 오브젝트 검색을 위해 Ray(빔)를 쏘는 방식이다.
RayCast는 빔을 맞은 오브젝트가 무엇인지 밝혀내는 빔이다.
이건 FixedUpdate에 작성한다.
//Landing Platform
Debug.DrawRay(rigid.position, Vector3.down, new Color(0, 1, 0));
DrawRay( ): 에디터 상에서만 Ray를 그려주는 함수
매개변수로 (빔이 시작되는 위치, 쏘는 방향 및 길이, 컬러값, 라인의 표시시간, 카메라에서 가까운 오브젝트에 의해 라인이 숨겨진 경우, 라인을 숨길지 여부)가 들어간다.
color(0, 1, 0)은 RGB에서 녹색이다.
DrawRay는 에디터 창에서 색상과 함께 Start 지점부터 Start 지점 + dir(방향과 길이) 까지 선을 그리는 역할을 한다.
저장하고 실행해보면, Scene 창에서 녹색 빔이 아래 방향으로 나타난 것을 확인할 수 있다. *DrawRay( )함수로 작성한 코드는 현재 Debug이기 때문에, 실제 Game에서는 보이지 않는다. Debug는 게임을 개발하는 동안 쉽게 디버깅하기 위한 목적의 클래스이다.
이제 빔을 활용해본다. (실제로 빔을 쏴보도록 한다)
RaycastHit2D는 Ray에 닿은 오브젝트를 말한다.
Raycast(): 들어가는 매개변수에 따라 빔을 쏘는 함수
*물리 기반의 빔이기 때문에 초기화할 때 Physics2D 자료형 사용한다.
*매개변수는 (빔 시작위치, 쏘는 방향, 거리, 레이어)이다. 이때 레이어는 int형의 값이 들어간다.
레이어 입력 시, GetMask() 함수를 사용한다.
GetMask(): 레이어 이름에 해당하는 정수값을 리턴하는 함수 [LayerMask 클래스에 들어있다]
rayHit은 빔을 쏘고, 빔에 맞은 오브젝트에 대한 정보이다.
따라서 빔에 맞았으면, rayHit은 초기화가 된 것이다. (값이 들어갔다)
rayHit은 물리 기반이기 때문에 collider가 들어간다. (빔에 맞으면, collider가 들어간다) 맞지 않았다면, 초기화가되지 않았기 때문에 collider도 없다. = '빔에 맞은 오브젝트의' collider 정보
맞지 않았을 때만 보면 의미가 없으니까, 맞았을 경우에만 살펴본다.
RaycastHit 변수의 collider로 검색 확인 가능하다.
맞았을 때 그 오브젝트가 누구인지 Debug.Log로 알아본다.
실행해보면 Player가 맞는다.
변수 rigid가 갖고있는, RigidBody는 player 기준의 가운데에서 빔을 쏘기 시작한다. 지금 Player의 Collider도 한번 관통했고 Platform의 Collider도 관통한 상태이다.
그런데 RayHit은 관통이 안된다. 한번 맞으면 거기서 끝이다.
그러면 Player에서 빔을 쏴서, Player를 감지하면 의미가 없다. 우리는 바닥 아래로 빔을 쏴서 바닥이 있는지 없는지만 감지할 것이다. 따라서 지금 로직이 'Player에서 빔을 쏴서 Player를 감지하는 것'은 의미가 없다.
해결방안
빔은 쏘는데, Player 오브젝트의 Collider(충돌박스)는 무시하고 싶다.
물리에서 필터 개념으로 들어간 것이 있다. 그것은 바로 LayerMask이다.
LayerMask: 물리 효과를 구분하는 정수값
3d 실습에서는 Tag로 했는데 (Tag는 문자열로된 간단한 식별자) : 나중에 알아봄
Layer 역시 식별자이지만, Layer는 특별하다.
Layer를 설정하면, 다른 Layer끼리는 부딪히지 않게 설정할 수 있다.
//Landing Platform
Debug.DrawRay(rigid.position, Vector3.down, new Color(0, 1, 0));
RaycastHit2D rayHit = Physics2D.Raycast(rigid.position, Vector3.down, 1, LayerMask.GetMask("Platform"));
if (rayHit.collider != null)
{
if (rayHit.distance < 0.5f)
Debug.Log(rayHit.collider.name);
}
이제 rayHit은 Platform 레이어의 오브젝트만을 스캔하게 되었고, 스캔한 오브젝트의 정보를 가져올 수 있게 되었다.
Player가 Platform과 완전히 밀착하여 착지되었다는 것을 알아내야하기 때문에, [빔에 지형 오브젝트가 맞아, rayHit의 값이 초기화되었을 때] 거리를 측정한다.
if(rayHit.distance < 0.5f)
Player의 크기는 1이고, 빔은 절반에서 시작했기 때문에, 거리가 0.5보다 작다는 것은 바닥에 닿았다는 것이다.
(1은 현재 Player 오브젝트의 크기를 Pixel per Unit에서 1Unit만큼 설정했기 때문에, 1은 Unit 1개만큼의 사이즈를 말하는 것이다)
물리적으로 닿았을 때 OnCollitionEnter 함수같은 이벤트 함수를 사용하는 것 외에도 이렇게 2D의 경우 RayCastHit으로 바닥에 닿았는지 감지할 수 있다.
//Landing Platform
if (rigid.velocity.y < 0)
{
Debug.DrawRay(rigid.position, Vector3.down, new Color(0, 1, 0));
RaycastHit2D rayHit = Physics2D.Raycast(rigid.position, Vector3.down, 1, LayerMask.GetMask("Platform"));
if (rayHit.collider != null)
{
if (rayHit.distance < 0.5f)
anime.SetBool("IsJump", false);
}
}
※점프 전, 처음에 캐릭터가 바닥에 닿은 상태이기 때문에 IsJump1이 false값이 되는 문제가 있다. 따라서 캐릭터가 점프 후 아래로 낙하하고있을 때만 빔을 쏘도록 해본다. 낙하 시 rigid.velocity.y값 -의 값을 갖고 있는 것이다.
y축의 속도를 보고, 그게 -값을 가지면 '내려가는 속도'라고 판단하여 (충돌 오브젝트를 감지할 수 있는) 빔을 쏘는 것이다. [착지]
이것이 RayCastHit이다.
다만 이제 해결해야할 것은 무한점프이다.
//Jump By Button Control
if (Input.GetButton("Jump") && !anime.GetBool("IsJump"))
{
rigid.AddForce(Vector2.up * jumpPower, ForceMode2D.Impulse);
anime.SetBool("IsJump", true);
}
점프 시 조건만 추가하면 된다. '지금 점프중이 아닐 때 점프할 수 있도록'
“지금 애니메이션이 점프하고있는 state인가”를 불러올 수 있다. (현재 점프하고 있는 상태라면, 더 점프할 수 없도록)
논리값을 설정하는 SetBool 함수가 있듯이, 현재 논리값이 무엇인지 불러오는 GetBool 함수를 이용한다.
GetBool() : 변수의 논리값이 무엇인지 불러오는 함수
이제 무한점프는 해결됐다.
문제상황 - 피드백
1. 로직에 문제가 없음에도 점프가 되질 않는다면 Mass, Gravity Scale, 스크립트에 입력받는 jumpPower, 공기저항값 Angular Drag 등을 확인해본다.
2. 캐릭터의 무한점프 방지를 위한 코드 입력 시 캐릭터가 점프하지 못하고, 점프키 입력 시 Idle 애니메이션과 Jump 애니메이션이 계속 왔다갔다하며 실행되는 문제
//Animation Transition
if (Mathf.Abs(rigid.velocity.x) < 0.3 && Mathf.Abs(rigid.velocity.y) < 0.1)
anime.SetBool("IsWalk", false);
else
anime.SetBool("IsWalk", true);
캐릭터의 x축 방향 속도가 0.3 이하일때만을 기준으로 IsWalk를 false로 설정하고, 그 외에는 true로 설정했었다.
이 부분 때문에 캐릭터가 점프 후 바닥에 착지할 때 속도가 매우 빠르게 0으로 수렴하면서 IsWalk가 false로 변경되어 Idle 애니메이션으로 전환된다. (Raycast.distance < 0.5f이므로 IsJump는 false가 되는 상황이니까)
점프 중일 때 IsWalk를 변경하지 않도록 Player의 y축 방향 속도의 절댓값이 0.1 이하일 때를 함께 조건에 걸면
점프 중 Idle 애니메이션과 Jump 애니메이션의 전환이 이루어지지 않는다.
public class Move : MonoBehaviour
{
public float maxSpeed;
public float jumpPower;
private bool isJumping = false;
Rigidbody2D rigid;
SpriteRenderer spriteRenderer;
Animator anime;
private void Awake()
{
rigid = GetComponent<Rigidbody2D>();
spriteRenderer = GetComponent<SpriteRenderer>();
anime = GetComponent<Animator>();
}
private void FixedUpdate()
{
//Move By Button Control
float h = Input.GetAxisRaw("Horizontal");
rigid.AddForce(Vector2.right * h, ForceMode2D.Impulse);
if (rigid.velocity.x > maxSpeed) //right max speed
rigid.velocity = new Vector2(maxSpeed, rigid.velocity.y);
else if (rigid.velocity.x < maxSpeed * (-1)) //left max speed
rigid.velocity = new Vector2(maxSpeed * (-1), rigid.velocity.y);
//Landing Platform
if (rigid.velocity.y < 0)
{
Debug.DrawRay(rigid.position, Vector3.down, new Color(0, 1, 0));
RaycastHit2D rayHit = Physics2D.Raycast(rigid.position, Vector3.down, 1, LayerMask.GetMask("Platform"));
if (rayHit.collider != null)
{
if (rayHit.distance < 0.5f)
{
anime.SetBool("IsJump", false);
isJumping = false;
}
}
}
}
private void Update()
{
//Jump By Button Control
if (Input.GetButtonDown("Jump") && !anime.GetBool("IsJump") && !isJumping)
{
rigid.AddForce(Vector2.up * jumpPower, ForceMode2D.Impulse);
anime.SetBool("IsJump", true);
isJumping = true;
}
//Stop Speed
if (Input.GetButtonUp("Horizontal"))
rigid.velocity = new Vector2(rigid.velocity.normalized.x * 0.5f, rigid.velocity.y);
//Direction Sprite
if (Input.GetButton("Horizontal"))
spriteRenderer.flipX = Input.GetAxisRaw("Horizontal") == -1;
//Animation Transition
if (Mathf.Abs(rigid.velocity.x) < 0.3 && Mathf.Abs(rigid.velocity.y) < 0.1 && !isJumping)
anime.SetBool("IsWalk", false);
else
anime.SetBool("IsWalk", true);
}
}
2-2. 이러면 점프할때 가장 높게 점프해서 다시 낙하하는 순간 x축 방향 속도는 0.3 이하 y축 방향 속도는 0.1 이하가 되어 순간적으로 Idle 애니메이션이 재생되는 문제가 있다. (점프 중 고점에서 idle 애니메이션 순간 전환 문제)
: isJumping 플래그 변수 도입해서 점프 중 상태 만들어서 해결
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Move : MonoBehaviour
{
public float maxSpeed;
public float jumpPower;
//private bool isJumping = false;
Rigidbody2D rigid;
SpriteRenderer spriteRenderer;
Animator anime;
private void Awake()
{
rigid = GetComponent<Rigidbody2D>();
spriteRenderer = GetComponent<SpriteRenderer>();
anime = GetComponent<Animator>();
}
private void FixedUpdate()
{
//Move By Button Control
float h = Input.GetAxisRaw("Horizontal");
rigid.AddForce(Vector2.right * h, ForceMode2D.Impulse);
if (rigid.velocity.x > maxSpeed) //right max speed
rigid.velocity = new Vector2(maxSpeed, rigid.velocity.y);
else if (rigid.velocity.x < maxSpeed * (-1)) //left max speed
rigid.velocity = new Vector2(maxSpeed * (-1), rigid.velocity.y);
//Landing Platform
if (rigid.velocity.y < 0)
{
Debug.DrawRay(rigid.position, Vector3.down, new Color(0, 1, 0));
RaycastHit2D rayHit = Physics2D.Raycast(rigid.position, Vector3.down, 1, LayerMask.GetMask("Platform"));
if (rayHit.collider != null)
{
if (rayHit.distance < 0.5f)
{
anime.SetBool("IsJump", false);
//isJumping = false;
}
}
}
}
private void Update()
{
//Jump By Button Control
if (Input.GetButton("Jump") && !anime.GetBool("IsJump"))
{
anime.SetBool("IsJump", true);
rigid.AddForce(Vector2.up * jumpPower, ForceMode2D.Impulse);
//isJumping = true;
}
//Stop Speed
if (Input.GetButtonUp("Horizontal"))
rigid.velocity = new Vector2(rigid.velocity.normalized.x * 0.5f, rigid.velocity.y);
//Direction Sprite
if (Input.GetButton("Horizontal"))
spriteRenderer.flipX = Input.GetAxisRaw("Horizontal") == -1;
//Animation Transition
if (Mathf.Abs(rigid.velocity.x) < 0.3 && Mathf.Abs(rigid.velocity.y) < 0.1 && !anime.GetBool("IsJump"))
anime.SetBool("IsWalk", false);
else
anime.SetBool("IsWalk", true);
}
}
->anime.IsJump 매개변수 상태만 갖고 isJumping 대체할 수 있다.
아직 점프 착지 직후 x방향으로 이동시키면 점프모션이 풀리지 않는 문제가 있다. [나중에 해결해보자]
골드메탈님의 유튜브 댓글 중에 나와 비슷한 문제를 겪으셨던 분의 댓글을 발견했다. 이를 참고해서 문제를 해결해보려고 했다.
//Raycast - Landing Platform
if (rigid.linearVelocity.y < 0) //y축 속도 < 0 : 낙하중
{
Debug.DrawRay(rigid.position, Vector3.down, new Color(0, 1, 0));
RaycastHit2D rayHit = Physics2D.Raycast(rigid.position, Vector3.down, 1, LayerMask.GetMask("Platform"));
Debug.Log(rayHit.distance);
if (rayHit.collider != null)
{
if (rayHit.distance < 0.5)
{
//Debug.Log(rayHit.collider.name);
anime.SetBool("IsJump", false);
isJumping_flag = false;
}
}
}
나는 Player 오브젝트에 0.5 Radius의 Circle Collider 2D 컴포넌트를 사용하고 있고, rayCast 빔에 맞은 오브젝트인 rayHit.distance < 0.5일 때 점프 모션을 중단하도록 코드를 작성했다.
이 부분에서 문제가 발생하는 것 같아서 Debug.log로 콘솔창에 거리를 찍어보았다.
착지 후에도 점프모션이 계속 재생되는 문제를 발생시켰을 때, 가장 최근에 착지했던 Raycast에 찍힌 거리가 0.50xxxxx로 찍히는 것을 보아, Platform레이어의 ground 오브젝트와의 거리가 0.5 이하가 되지 않아서 이런 문제가 발생한 것으로 보인다.
따라서 이 문제는 위 댓글 작성하신 분께서도 말씀하셨듯이 플레이어의 크기가 collider의 크기보다 컸다면 발생하지 않았을 문제였을 것 같고, 플레이어 collider와 바닥 플랫폼 collider의 충돌 거리를 줄이기 위해 collider의 크기를 줄여주면 될 것 같다.
Player 오브젝트의 Collider의 크기(radius)를 0.4 이렇게 하면 화면 상 플레이어가 바닥면에 파묻힌 것처럼 부자연스럽게 보이니, 0.47~0.49 정도로 player 콜라이더 크기를 조정했다.
그리고 예상대로 Player 오브젝트의 collider 크기를 0.48로 줄이니 착지 후에도 점프 모션이 중단되지 않고 재생되는 문제를 해결할 수 있었다.
거리도 찍어보면 마지막 platform과 player 간 거리가 0.48xxxxx로 찍힌 것도 확인할 수 있다.
이번에 하나 더 알게된건 콘솔 창에 log 찍어보는 것도, 실행 중에도 여러 설정값을 바꿔가며 테스트해보는 것도 문제해결과정에 큰 도움이 되는 것 같다.
'개발 > 유니티' 카테고리의 다른 글
2d 플랫포머(6) - 몬스터 AI 구현하기 (0) | 2024.01.24 |
---|---|
2d 플랫포머(5) - 타일맵으로 플랫폼 만들기 (0) | 2024.01.23 |
2d 플랫포머(3) - 플레이어 이동 구현하기 (0) | 2024.01.22 |
2d 플랫포머(2) - 아틀라스와 애니메이션 (0) | 2024.01.22 |
2d 플랫포머(1) - 프로젝트 준비 (0) | 2024.01.21 |