0. 들어가기에 앞서
이때까지 3D 환경에서의 게임 제작이 우선되었지만, 이번에는 2D 환경에서의 게임 제작을 해 볼 차례이다.
2D 환경으로 게임을 만들기 위한 방법은 두 가지가 있다.
기존 3D로 진행하던 프로젝트를 2D로 전환하는 것도 가능하다. 우선 아래와 같이 Package Manager를 통해서 2D를 다운 받아보자.
패키지를 다운받은 후 Project Setting을 통해서 모드를 변경해야 한다.
모드를 변경하고 저장한 다음 새로 씬을 생성하면 2D 씬 화면이 열린다.
2D 프로젝트에는 기본적으로 2D 전용 오브젝트가 따로 있다. 오브젝트를 생성하여 배치해보면 이와 같이 나온다.
물론 여기서 화면을 3D로 돌려보면 Z축도 없지는 않지만, 2D 환경에서는 기본적으로 Z축이 의미가 없다고 봐도 되지 싶다. 실제로 의문이 생겨 실험을 조금 해 보았다.
일부러 두 물체의 Z축을 다른 위치로 변경하고, Rigidbody2D와 각각의 2D 충돌체를 걸어보았다. 결과는 아래와 같이 작동하는 것을 확인할 수 있다.
3D 시점으론 부딪힌 물체가 아닌데도 2D 시점에서 상호작용이 발생하는 것을 확인할 수 있었다. Rigidbody2D 나 collider2D를 지원하는 것에서부터 물리 연산이 다르게 일어난다는 것을 알 수 있으며, Z축이 반영되지 않는다고 판단했다. 즉, 원근감이 없다고 봐도 되겠다.
물론 2D 프로젝트에서도 3D 오브젝트의 생성을 제한하지 않는다. 다만 3D 오브젝트를 생성하면 아래와 같이 출력된다.
Cylinder(원통)을 만들었는데 완전히 사각형으로 만들어지고, 완전히 검게 출력이 된다.
여기서 검게 출력되는 이유에 대해 설명하자면, 2D 씬 화면에서는 기본적으로 빛이 없기 때문이다.
이와 같이 빛을 추가하면 원통의 색깔이 기본색깔인 하얀색으로 나타나는 석을 확인할 수 있다.
다만 알아두어야 할 것은 2D 환경에서는 빛을 사용할 일이 잘 없을 것이란 점이다.
(안 쓰는 방향을 권장하나 금지는 아님)
최적화의 문제
빛 연산은 기본적으로 무거운 작업이다.
2D 프로젝트에서는 빛을 통해 물체의 빛을 표현하는 것보다, 스프라이트로 빛을 받은 물체와 빛을 받지 않은 물체로 따로 구별하는 편이 최적화에서 좋다.
이와 같은 특징을 기억해 보면서 2D 프로젝트에 대해 더 자세히 알아보자.
유니티2D에서 제공하는 Collider는 종류가 다양하다.
기본적으로 유니티3D에서 사용했던 Box Collider나 Capsule Collider나 Sphere Collider처럼, 2D 버전이 다 있다고 생각할 수도 있다.
여기에서 이런 Collider 외에 자주 쓰는 것은 3가지라 할 수 있다.
(Polygon Collider, Composite Collider, Tilemap Collier)
각 Collider의 특징을 알아보자.
쉽게 생각하면 유니티 3D의 Mesh Collider 2D 버전이라 생각할 수 있다.
Composite Collider2D는 하위 오브젝트의 모든 Collider 각각 처리하는 것이 아닌 하나로 보는 기능을 말한다.
이는 충돌체를 각각 계산하여 연산에 드는 비용을 줄이기 위한 용도라고 할 수 있다.
사용 방법은 부모 오브젝트에 Composite Collider 2D를 추가하는 것이다. 그러면 Rigidbody2D도 기본적으로 추가되는데, 이와 같이 한 후 하위 오브젝트에 Collider를 부여한다.
Used By Composite을 사용해야지 Composite Collider2D로 반영된다.
다만 버그사항인지 몰라도 이상하게 collider 중 box collider2D만 해당 항목이 뜨니 참고하도록 하자.
유니티의 2D 환경에서 맵을 만드는데 사용한다. 타일맵으로 맵을 구성하는 경우가 편하기 때문에 자주 쓰인다.
해당 영상을 참고하면 좋다.
쉽게 말하자면 벽이나 바닥 같은 충돌체를 선으로 표현할수 있는 Collider라고 생각하면 된다.
사용하기 어려운 것이라고 하며, 실제로 검색해 보아도 사용 사례를 찾아보기 어려웠다. 필요할 시에 공부해 보는 것도 나쁘지 않을 듯 하다.
플레이어를 구성해보자. 다소 보완해야 할 부분이 있는 스크립트지만, 간단하게 만들 수 있는 형태로 만들어 보았다.
using UnityEngine;
public class PlayerController : MonoBehaviour
{
private Rigidbody2D rigid;
private bool isJumped = false;
private bool isLanded;
private float inputX;
private CinemachineFramingTransposer cinemachine;
[SerializeField] private float moveSpeed;
[SerializeField] private float jumpPow;
private void Start()
{
rigid = GetComponent<Rigidbody2D>();
}
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)
{
return;
}
else
{
rigid.velocity = new Vector2(inputX * moveSpeed, rigid.velocity.y);
spriteRenderer.flipX = inputX < 0;
}
}
private void PlayerJump()
{
rigid.AddForce(Vector2.up * jumpPow, ForceMode2D.Impulse);
isLanded = false;
}
private void OnCollisionEnter2D(Collision2D collision)
{
if (collision.gameObject.CompareTag("Ground"))
{
isJumped = false;
isLanded = true;
}
}
}
점프 연속 방지를 위해 바닥과 충돌했을 때 다시 점프할 수 있도록 만들었으나, 좋은 매커니즘이라고 보기는 조금 어렵다.
다만 테스트상으로 진행한 것으로, 여기서 핵심은 PlayerController를 만들 때 Vector2를 사용한다는 것과 Z축이 사용되지 않는다는 점이다.
유니티2D에는 2D오브젝트에 부여할 수 있는 이펙터가 있다.
기본적으로 오브젝트에서 Used By Effector 기능이 켜져 있어야지 사용 가능하다.
각각의 기능을 알아보자.
흔히 플랫포머 게임에서 유용하게 쓰일 수 있을 기능이다. 한쪽 방향으로만 통과할 수 있게 하는 기능이다. 가장 익숙한 예시로는, 충돌체가 있는 발판과 겹쳐 있어도 괜찮으며, 뛰어서 위로 올라갈 수 있는 경우를 생각할 수 있다.
중력을 적용할 수 있는 기능이다. 음수값이 커질 수록 끌어당기는 힘이 강해지고, 양수값이 커질 수록 밀어내는 힘이 강해진다.
컨베이어벨트처럼 움직이게 할 수 있는 이펙터이다. 다만 현재의 플레이어 코드로는 움직일 때 이펙터의 효과가 무시되기 때문에, 이펙터가 플레이어의 물리에 반영되도록 하려면 추가적인 방법이 필요해 보인다.
Point Effecter와 유사하다고 볼 수 있다. 다만 영역을 통해서 힘을 가하는 방식이며, 힘의 방향과 각도를 조절할 수 있다.
Buoyancy는 부력이란 영어 단어로, 물 속에서 떠오르는 것처럼 효과를 주는 이펙터를 말한다.
시네머신을 설정하여 Virtual Camera를 만들고 Player를 Follow하게 만들면 플레이어를 계속 쫓아가며 시야를 확보할 것이다. 하지만 아래와 같이 플랫폼의 끝까지 카메라가 쫓아갈 것이다.
이를 방지하기 위해 카메라의 시야를 제한할 수 있는 방법을 알아보고자 한다.
Virtual Camera의 Extension으로 Cinemachine Confiner2D 를 적용해보자. 해당 Bounding Shape 2D에는 Polygon Collider나 Composite Collider만 들어갈 수 있다.
이를 위해 Bound라는 빈 오브젝트를 만들어 Composite Collider를 만들었다.
이와 같이 만들고 적용해보면 카메라가 박스의 영역 밖으로 나가지 못한다. 플레이어도 마찬가지로 가로막힌 것처럼 나가지 못하고, 카메라 워킹이 자연스럽게 나온다.
2D 무료 스프라이트 이미지를 찾아보면, 유용한 것들이 꽤 나온다. 개인적으로 유용하다 생각하는 사이트 하나를 링크를 걸어둔다.
스프라이트 이미지를 다운받으면 아래와 같이 파일이 나온다.
이걸 유니티에 가져와서 어떻게 사용할지 알아보자.
스프라이트 이미지를 그대로 Unity로 가져오면 아래와 같이 되어 있을 것이다.
유니티 에셋 같은 것을 다운 받아 보면 잘려져 있는 채인 경우도 있지만, 외부 스프라이트를 가져왔을 경우 이와 같이 애니메이션이 잘려 있지 않은 경우가 있다.
이때 우리는 이 이미지를 잘라서 쓸 수 있도록 변환해야 한다.
Sprite Editor를 누르면 아래와 같이 화면이 뜬다.
Automatic에 Pivot은 Bottom으로 설정해주고 Slice를 누르면 자동으로 잘라준다.
만약 자른 범위가 이상할 경우에 Type을 바꿔서 수정할 수도 있다.
플레이어를 누르고 애니메이션 만들기를 한다.
이와 같이 애니메이션 창이 열렸으면 이제 잘라둔 스프라이트를 전체 선택하여 가져와주면 된다.
그러면 사진이 전부 삽입되며, 적절하게 애니메이션 길이를 조절하면 완성이다.
이번엔 애니메이션을 조금 다른 방식으로 구현하고자 한다.
플레이어 코드를 아래와 같이 수정하였다.
using UnityEngine;
public class PlayerController : MonoBehaviour
{
private Animator animator;
private Rigidbody2D rigid;
private bool isJumped = false;
private bool isLanded;
private float inputX;
private SpriteRenderer spriteRenderer;
[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>();
spriteRenderer = GetComponent<SpriteRenderer>();
}
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;
}
else
{
rigid.velocity = new Vector2(inputX * moveSpeed, rigid.velocity.y);
animator.Play(WALK_HASH);
spriteRenderer.flipX = inputX < 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;
}
}
}
Animator.StringToHash()는 애니메이터로 관계를 일일히 연결하고 변수를 설정하는 대신, 그냥 스크립트 상으로 특정 조건에서 어떤 애니메이션이 재생되도록 하는 기능이다.
이와 같은 방식은 애니메이터로 관계를 설정하는 것보다 최적화의 효율이 있으며, 애니메이터를 아예 설정하지 않아도 애니메이션이 작동한다.
* 유의사항 - StringToHash 안에 들어가는 애니메이션 명은 위의 사진에서의 애니메이터 속 상태 이름과 일치해야 함.
다만 이와 같은 방식의 단점으로는, 애니메이션의 작동이 부자연스러울 수 있다는 점이 있다. (특히 3D에서 확연히 드러남)
실제로 이 방법을 적용했을 때 현재 캐릭터의 점프 모션이 제대로 적용되지 않는다.