방법은 몬스터를 기준으로 자신의 전방에서 아래로 Raycast를 쏘고, 바닥이 없으면 방향을 전환하는 방식이다.
또한 애니메이션은 Flip으로 처리한 방식이다.
using System.Collections;
using UnityEngine;
public class EnemyController : MonoBehaviour
{
[SerializeField] private float moveSpeed;
[SerializeField] private LayerMask groundLayer;
private Rigidbody2D rigid;
private Animator animator;
private SpriteRenderer spriteRenderer;
private Vector2 patrolVec;
private bool isWaited;
private readonly int IDLE_HASH = Animator.StringToHash("Idle");
private readonly int PATROL_HASH = Animator.StringToHash("Patrol");
void Start()
{
rigid = GetComponent<Rigidbody2D>();
animator = GetComponent<Animator>();
spriteRenderer = GetComponent<SpriteRenderer>();
patrolVec = Vector2.left;
}
void Update()
{
Patrol();
}
private void Patrol()
{
Vector2 rayOrigin = transform.position + new Vector3(patrolVec.x, 0);
Debug.DrawRay(rayOrigin, Vector2.down * 3f, Color.red);
RaycastHit2D hit = Physics2D.Raycast(rayOrigin, Vector2.down, 3f, groundLayer);
if (hit.collider == null)
{
//돌아가는 로직.
StartCoroutine(CoTurnBack());
}
}
private IEnumerator CoTurnBack()
{
spriteRenderer.flipX = !spriteRenderer.flipX;
if (spriteRenderer.flipX)
{
patrolVec = Vector2.right;
}
else
{
patrolVec = Vector2.left;
}
animator.Play(IDLE_HASH);
isWaited = true;
rigid.velocity = Vector2.zero;
yield return new WaitForSeconds(2f);
isWaited = false;
animator.Play(PATROL_HASH);
}
private void FixedUpdate()
{
if (isWaited == false)
rigid.velocity = patrolVec * moveSpeed;
}
}
나는 Raycast 방식으론 비용이 다소 들 거란 생각이 들어 다른 방법으로, Monster 아래에 Trigger를 달아 Trigger 인식이 되지 않으면 방향을 돌리는 방식으로 구현하였다.
또한 Trigger 영역을 늘리지 않기 위해, Monster의 Rotation 자체를 돌리는 방법으로 생각했다.
using UnityEngine;
public class MonsterController : MonoBehaviour
{
private Animator animator;
private Rigidbody2D rigid;
private bool isWalking = false;
private int moveDir = 1;
[SerializeField] private int moveSpeed;
[SerializeField] MonsterTrigger monsterTrigger;
private void Start()
{
animator = GetComponent<Animator>();
rigid = GetComponent<Rigidbody2D>();
}
private void Update()
{
IsWalkable();
}
private void FixedUpdate()
{
MonsterMove();
}
private void MonsterMove()
{
animator.SetBool("IsWalk", isWalking);
isWalking = true;
rigid.velocity = Vector2.left * moveSpeed * moveDir;
}
private void IsWalkable()
{
if(!monsterTrigger.IsWalkable)
{
moveDir = -moveDir;
gameObject.transform.rotation = moveDir > 0? Quaternion.Euler(0, 0, 0) : Quaternion.Euler(0, 180, 0);
}
}
}
using UnityEngine;
public class MonsterTrigger : MonoBehaviour
{
private bool isWalkable = true;
public bool IsWalkable { get { return isWalkable; } }
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.CompareTag("Ground"))
{
isWalkable = true;
}
}
private void OnTriggerExit2D(Collider2D collision)
{
if(collision.CompareTag("Ground"))
{
isWalkable = false;
}
}
}
Monster의 대각선 아래에 이와 같이 트리거 영역을 만들었다.
맵과 레벨 디자인을 위해 타일맵을 생성해보자.
하이어라키 창에서 타일맵을 생성할 수 있다.
사각형 모양의 타일이나 육각형 모양의 타일, Isometric 등 다양한 타일맵 옵신열 선택할 수 있다.
이번에 사용할 에셋은 사각형 에셋이니 Rectangular로 생성해보자.
격자 무늬가 생기면서 Grid와 Tilemap이 생겼다. 여기서 Open Tile Palette로 타일맵을 생성해보자.
에셋스토어에서 받은 에셋이라면 기본적으로 팔레트 설정이 되어 있을 것이다. 외부 자료로 가져 온 경우라면 좌측 상단의 Create New Palette로 새 팔레트를 만들면 된다.
빈 팔레트에 이와 같이 넣을 스프라이트 이미지를 전부 선택한 다음에 옮겨주면 간단하게 팔레트를 만들 수 있다.
간단하게 버튼의 기능을 알아보고자 한다.
맨 왼쪽 버튼을 누르고 Grid를 선택해보면, 인스펙터로 현재 칸에 어떤 정보가 들어 있는지 확인할 수 있다.
빈칸으로 보이지만 룰 타일이 들어 있는 모습이다.
이곳을 통해 그리드 칸에 무엇이 들어있는지 확인하고, 지우는 것도 여기서 할 수 있다.
1번에서 사용한 Grid 선택으로 해당 Grid의 데이터를 옮겨줄 수 있다.
그림판처럼 그림을 그려줄 수 있다.
영역을 설정해주고 그림을 그릴 수 있다.
룰 타일의 경우에는 한 칸만 설정하여 그릴 수 있다.
그림판에서 색을 추출하는 것처럼 타일 정보를 골라내서 다시 그릴 수 있다.
지우개 기능이다.
지우개를 써보면 넓은 영역을 지우는 게 안 되는데, 이걸 위해선 이와 같이 세팅해야 한다.
타일맵의 충돌체를 만드는 방법은 아주 간단하다.
유니티에서는 이와 같이 Tilemap Collider 2D가 있으며 이를 사용하면 모든 타일에 자동적으로 충돌체가 생성된다.
Effecter를 사용해야 하는 충돌체가 아니라면, Tile Collider를 설정한 다음 부모 오브젝트에 Composite Collider 2D를 활용하는 것도 좋은 방법이다.
이와 같이 처리하면 타일맵의 충돌체 각각의 계산하는 양보다, 통째로 한 덩어리의 충돌체로 계산하는 게 최적화 면에서 좋기 때문이다. 맵이 작을 때에는 큰 차이가 없지만 타일의 개수가 많아지면 거기에서부터 성능 최적화의 효과를 볼 수 있다.
타일로 맵을 만들 수 있는 건 좋지만, 아무래도 하나하나 맵을 다 찍고 있으면 버겁다는 생각이 들 수 밖에 없다.
이때 Rule Tile을 설정하여 일종의 프리팹과 같은 형태로 만들 수도 있다.
프로젝트 창에서 Rule Tile을 만들어보자.
룰 타일은 사용할 타일의 종류를 리스트로 받고, 이후 타일 중심으로 8방향에 대한 조건을 입력하여 해당 타일이 어떤 형태를 유지하여야 하는지 세팅해주는, 규칙 타일을 만드는 것이다.
여기서 맵에 대한 룰 타일을 만들기 위해서 아래와 같이 타일 9개를 세팅하고, 조건을 걸었다.
초록색 화살표는 해당 타일 중심으로 해당 방향으로 타일이 존재해야 한다는 뜻이고, X방향은 타일이 없어야 한다는 뜻이다.
이 조건에 따라 생성되는 프리팹의 예시 사진도 볼 수 있다.
사각형으로 구성된 맵은 분명 맵 제작에서 자주 쓰일 것이다. 이제 이걸 사용하기 위해서 팔레트에서 세팅을 한다.
Default Sprite가 없어서 아무것도 보이지 않지만 이렇게 Rule Tile을 팔레트에 드래그 앤 드롭으로 하면 룰 타일을 생성할 수 있다.
해당 기능이 특히나 유용한 건 계속해서 룰을 지키는 구성 때문인지, 마음대로 만들었다가 지워도 타일이 알아서 룰에 맞게 교체된다.
꽤나 유용하게 사용할 수 있는 기능이라 예상되며, 필요 시에 룰 타일을 만들어서 맵 작업을 해야 할 것으로 보인다.
타일 브러쉬의 종류는 이와 같이 있다. 기본형인 Default Brush 말고 다른 Brush의 기능을 알아보자.
게임오브젝트를 마치 타일 같이 가져와서 배치할 수 있다. 그리고 이 오브젝트는 실제로 그 오브젝트의 기능이 모두 작동된다.
버그인지는 몰라도 실수로 해당 게임 오브젝트를 지우개로 지워버리면 오브젝트 자체가 날아가버리니 주의하자.
box로 지정하여 타일맵을 만드는 것과는 미묘하게 다르다
타일을 등록하고 해당 타일중 랜덤하게 뽑을 수 있다.
타일을 일렬로 그을 때 편리하다.
Virtual Camera에 Cinemachine Pixel Perfect를 달아보자.
타일이 많은 경우 카메라가 전환되면서 경계선이 깜빡거리는 듯한 현상을 볼 수 있는데 그걸 없애주는 기능이라고 생각하면 된다.
지금까지의 강의에서 강사님 기준으로는 애니메이션을 Flip하여 전환하는 방식을 취했다. 하지만 나는 이에 반해 오브젝트 자체를 180도 회전시키는 방식으로 좌우 이동을 구현했다.
이렇게 구현한 데에는 아래와 같은 이유가 있었다.
이와 같은 이유로 플레이어의 애니메이션을 오브젝트를 회전시키는 방식으로 변경했고, 카메라워킹 내용까지 포함해 아래와 같이 변경했다.
using Cinemachine;
using UnityEngine;
public class PlayerController : MonoBehaviour
{
private Animator animator;
private Rigidbody2D rigid;
private bool isJumped = false;
private bool isLanded;
private float inputX;
[SerializeField] CinemachineVirtualCamera virtualCamera;
[SerializeField] private float moveSpeed;
[SerializeField] private float jumpPow;
private readonly int IDLE_HASH = Animator.StringToHash("Idle");
private readonly int WALK_HASH = Animator.StringToHash("Walk");
private readonly int JUMP_HASH = Animator.StringToHash("Jump");
private void Start()
{
rigid = GetComponent<Rigidbody2D>();
animator = GetComponent<Animator>();
}
private void Update()
{
PlayerInput();
if (Input.GetKeyDown(KeyCode.Space))
{
isJumped = true;
}
}
private void FixedUpdate()
{
PlayerMove();
if (isJumped && isLanded) PlayerJump();
}
private void PlayerInput()
{
inputX = Input.GetAxis("Horizontal");
}
private void PlayerMove()
{
if(inputX == 0)
{
animator.Play(IDLE_HASH);
return;
}
rigid.velocity = new Vector2(inputX * moveSpeed, rigid.velocity.y);
animator.Play(WALK_HASH);
//spriteRenderer.flipX = inputX < 0;
//gameObject.transform.rotation = inputX > 0 ? Quaternion.Euler(0, 0, 0) : Quaternion.Euler(0, 180, 0);
if(inputX > 0)
{
gameObject.transform.rotation = Quaternion.Euler(0, 0, 0);
}
else
{
gameObject.transform.rotation = Quaternion.Euler(0, 180, 0);
}
}
private void PlayerJump()
{
rigid.AddForce(Vector2.up * jumpPow, ForceMode2D.Impulse);
animator.Play(JUMP_HASH);
isLanded = false;
}
private void OnCollisionEnter2D(Collision2D collision)
{
if (collision.gameObject.CompareTag("Ground"))
{
isJumped = false;
isLanded = true;
}
}
}
이는 최종 해결된 코드이긴 처음에 해당 방법대로 코드를 적용했을 때 아래와 같은 문제가 발생했었다.
또한 이전까지 고민하고 있던 카메라의 화면 이동 문제로, Outline으로 카메라 영역을 싸는 것에 대해 고민이 깊었다.
이 두 문제를 해결하기 위해서 꽤 머리를 싸맸는데, 결론적으로 고친 방법은 다음과 같다.
세팅은 아래와 같이 했다.
바운드 영역을 Trigger로 설정하면 카메라도 그대로 빠져나오는 거 아닌가 싶기는 했지만, 의외로 빠져나가지 않았다.