UniRun 미니게임
20261020 Unity 2D 게임(UniRun 미니게임)
Package Manager
처음 프로젝트를 생성할 때 2D 혹은 3D로 설정을 하지만 이미 생성한 프로젝트라도 Package Manager에서 설정 수정을 통해서 변경이 가능하다. Unity 화면 상단의 Asset Store -> My Assets로 열 수 있다.
외부 Asset
이번에는 외부 Asset을 받아서 Unity에 적용한다. Asset을 받은 폴더의 파일들을 드래그 해서 Project View의 Assets폴더에 놓는것으로 적용이 가능하다.
폴더링으로 정리
Project 폴더 정리를 잘 해놓는것이 중요하다. 여러 파일들이 늘어갈 수록 필요한 파일을 찾기 힘들어진다. 이번에는 Sprites, Fonts, Audio, Scripts 폴더를 만들어서 파일들을 각각 맞는 위치에 저장해둔다.
.meta 파일
작업을 할 때에는 항상 이미지 원본을 보존해야 한다. 재사용이 가능해야 하기 때문. 그래서 이미지 원본이 그대로 사용되는 것이 아니라 Inspector에서 수정한 설정들이 meta파일에 저장 되고, 저장된 파일을 불러와서 사용한다. 이렇게 설정이 적용된 파일은 Library 폴더에 저장된다.
이미지 Inspector의 Compression 옵션
압축에는 두가지 종류가 있다.(무손실 압축, 손실압축) 일반적으로 게임의 데이터들 중 사운드와 이미지가 사이즈가 가장 크다. 압축 여부에 따라사 VRam 사용량이 달라지기 때문에 3D에서는 압축하는게 기본이다. 하지만 2D에서는 손실압축을 할 경우 퀄리티 손실이 많이 눈에 띄게 된다. 그래서 작은 사이즈의 이미지는 압축하지 않는다. 이미지의 Inspector View에서 Compression -> None으로 선택 후 Apply
- 참고: 이미지의 Inspector에서 설정을 수정할 경우 Apply까지 눌러야 적용이 된다.
이미지 Inspector의 Sprite Mode 옵션
이미지 하나를 통으로 사용할 경우 Sprite Mode설정을 Single, 하나의 이미지에서 부분적으로 여러부분을 쓸꺼면 Multiple로 선택한다.
이미지의 표시
2D로 보이지만 실제로는 화면을 돌려보면 3D렌더와 같다.
이미지의 충돌 처리
2D물리엔진과 3D물리엔진은 호환이 안된다. 즉 Rigidbody는 Rigidbody 2D나 Collider 2D와 호환이 안된다. 2D는 2D에 맞는 Rigidbody와 Collider를 사용해야 한다.
Player의 낙사 확인 방법 두가지
- Y값을 확인하여 일정 범위를 벗어나면 사망하도록 하는 것.
- 2D Collider의 Trigger로 확인해서 사망하도록 하는 것.
오늘 할 것은 2번째의 Trigger로 사망하는 방식을 사용한다.
게임의 Animation 처리하는 방법 두가지
- 캐릭터를 3D모델로 제작 후 그 모델을 움직임.
- Mesh는 그대로 두고 이미지만 교체하는 방식.
오늘 하는 것은 2번째의 이미지만 교체하는 방식을 사용한다.
Sprite Sheet와 Atlas
움직임 별로 여러 Sprite를 하나로 붙여놓은 것을 Sprite Sheet라고 한다. 게임에 사용하는 모든 sprite를 한곳에 모아 놓은것을 Atlas 라고 한다.
하나로 되어있는 Sprite를 나눠서 적용하기
Sprite 이미지를 선택한 상태로 Inspector View -> Open Sprite Editor를 연다. 화면 상단 Slice -> Type을 Grid By Cell Size로 선택하고 이미지의 사이즈에 맞게 Pixel Size를 수정하고 Slice를 실행한다.(현재 사용하는 Player의 Run 이미지는 총 512 * 64 사이즈로 8개 있으니 1장면당 64 * 64 pixel 이다.)
- 주의사항: 이미지를 GameObject로 만들면 Transform에서 Scale을 조절할 수 있는데 이미지는 Scale을 건드리지 말것. 이미지가 이상하게 보이거나 하는 등의 여러 문제가 발생할 수 있음. Inspector의 Sprite Mode의 Pixels Per Unit의 값을 조절하여 크기를 조절한다. Pixels Per Unit의 값이 의미하는 것은 해당 범위에 몇 Pixel로 그릴지를 선택하는 것이다. 따라서 한정된 범위 안에 들어가는 Pixel수가 많아지면, 즉 숫자가 커질수록 그림이 작아짐. 여기서는 Player의 이미지는 32로한다.
Filter Mode
Inspector View의 Filter Mode를 Point(no filter)로 변경한다. 변경 전 Bilinear 상태에서는 흐릿하게 보인다. 이유는 3D에서는 이미지가 각지면 안되기 때문에 Bilinear를 사용해서 각진 부분을 부드럽게 해줘야 하지만 현재 2D 에서는 도트 그래픽이기 때문에 이미지가 선명하게 보이도록 하기 위해서 Point(no filter)로 변경하는 것이다.
Animation 적용
유니티에서도 간단한 애니메이션 작업은 가능하다. 때문에 간단한 애니메이션은 유니티에서 진행하고 그게 아니면 에셋을 불러오듯이 다른곳에서 만든것을 가져와서 적용한다. 여기서는 유니티에서 하는 방법을 설명한다.
- Window -> Animation -> Animation(Ctrl + 6)
- Player GameObject 선택 후 Create -> Animation을 선택 후 저장할 폴더 선택 및 파일이름 저장
- Player GameObject에 Animator Component가 생성되고 Animation을 저장한 폴더에 Animator Controller와 Animation Clip이 생성됨.
- Animation 창에 Clip으로 만들 Sprite들을 모두 선택 후 끌어서 놓음.
- Animation 창 우측에 있는 시간표시 옆에 확장버튼(점 세개)을 눌러서 Show Sample Rate를 켜준다.
- Animation 창 좌상단의 Samples에 1초에 몇 프레임 보여줄 것인가가 표시됨.
- Run은 16, Jump와 Die는 6프레임으로 맞춘다.
Loop Time 설정
Loop Time은 Animation의 Clip 동작이 종료될 경우 처음으로 되돌아가서 다시 보여줄지에 대한 옵션이다. true면 처음으로 돌아가서 반복적으로 Animation이 진행되고 false면 Animation의 마지막 이미지 상태로 멈춘다. 따라서 Run은 Loop Time을 True로 설정하고 Jump와 Die는 false로 설정한다.
상태별 Animation 설정(유한 상태머신)
Entry와 Run, Jump를 연결하기
- Project View의 Player (Animator Controller) 더블클릭
- Scene View 옆에 Animator Tab이 생성된다. 이곳에서 상태별로 Animation 설정을 한다.
- 지금 생성되어 있는 Clip들이 표시된다. 상태를 변경하기 위해서는 Clip을 우클릭하고 Make Transition을 통해 화살표를 생성하여 이를 다른 Clip과 이어주어야 한다.
- Entry -> Run이 기본 상태로 되어있는데 이는 Run을 먼저 만들었기 때문이다. 만약 이를 변경하려면 변경할 Clip을 우클릭 후
Set as Layer Default State로 변경이 가능하다.
- Entry -> Run이 기본 상태로 되어있는데 이는 Run을 먼저 만들었기 때문이다. 만약 이를 변경하려면 변경할 Clip을 우클릭 후
- 화살표를 생성한 상태에서 상태변경에 대한 조건을 추가하기 위하여 화면 좌측에 보이는 Parametoers 탭을 선택한 후
+버튼으로 bool 형태의 Grounded를 추가한다. 이후 화살표를 클릭하여 Inspector View에 화살표의 정보를 표시하고 해당 방향으로 이동하기 위한 조건(Conditions)에 추가해준다.(Run -> Jump 로 가기 위해서는 Grounded가 false)
화살표의 옵션 설정
- Fixed Duration값을 0으로 수정한다.
- 일반적으로 3D는 동작과 동작을 이어주기 위하여 Animation간에 두 동작을 섞는 Blending이 필요하지만 2D Sprite는 Blending하면 두 동작을 섞을 방법이 없어서 이미지 전환만 늦어져버린다. 그래서 Fixed Duration값을 0으로 수정해야 한다.
- Has Exit Time을 false로 수정한다.
- Has Exit Time은 이미지의 Animation을 끝까지 재생한 후 다음 Animation으로 넘어갈지, 아니면 바로 넘어갈지를 선택하는 옵션이다. 지금은 바로 변경되어야 하기때문에 false로 설정한 것이다.
Any Stat와 Die를 연결하기
- Any State는 어떤상태에서든 변경될 수 있는 상태를 연결하는 것이다. Run중이든, Jump중이든 사망시 Die로 변경해야 하므로 Any State에서 Die로 연결해준다.
- 해당 변경 조건도 Parameters에 추가해 주어야 하는데 이번에는 Trigger로 Die를 추가해준다. Trigger는 함수처럼 호출할 수 있다. Trigger 호출을 통해 Die 상태로 변경할 것이다.
Prefab Overrides
작업을 하다보면 Project View의 Prefab과 Hierarchy View의 Prefab 설정이 달라지는 경우가 생긴다. Hierarchy View에서 작업한 내용을 Project View에 있는 Prefab에도 적용하고 싶다면 Hierarchy에서 해당 Prefab을 선택 한 후 Inspector View의 Prefab 옵션에서 Overrides를 선택 후 Apply All을 선택하면 된다. 만약 Project의 Prefab 상태로 되돌리고 싶다면 Revert All을 선택하면 된다.
Platform Prefab 만들기
Platform을 GameObject로 만든 후 해당 GameObject에 Obstacle을 자식으로 추가하고 Obstacle에 Box Collider 2D를 추가해준다. 이후 Prefab으로 만든다.
TMP(TextMeshPro)폰트 만들기
지금 예제용으로 받은 폰트를 그대로 쓸 수 없고 TMPpro용 Text Asset을 만들어야 함. Window -> TextMeshPro -> Font Asset Creator를 열고 Source Font 항목으로 드래그해서 넣은 다음 Generate Font Atlas로 해당 Font를 Atlas형식으로 만들어 준 뒤 Save까지 클릭하여 TMP Font를 생성한다.
배경추가
이동하는 배경을 추가할 예정이다. 같은 배경을 2개를 생성한 후 화면을 벗어날 만큼 일정거리를 이동하면 반대쪽으로 이동시켜서 이를 무한히 반복하도록 만든다.
이미지의 위치 및 길이
Transform은 Unity의 좌표거리를 따른다. 이미지의 경우 1024 pixel은 10.24 2048 pixel은 20.48의 길이를 가진다.
이미지에 대한 이동 및 배경의 Loop를 만들기
Create Empty 후 Sky 이미지를 2개 자식으로 넣어준다. 이름은 Background로 한다. 계속 이동하도록 하기 위하여 Script로 ScrollingObject를 만들어주고(background, platform등이 공통으로 사용)
// ScrollingObject 예시
public float speed = 10f;
private void Update()
{
transform.Translate(Vector3.left * speed * Time.deltaTime, Space.World);
}
배경은 자신의 가로 크기만큼 이동 후 원래의 위치로 옮겨주기 위하여 Script로 BackgroundLoop를 만들어 준 후에 Sky마다 각각 넣어준다.
// BackgroundLoop 예시
public float width = 20.48f; // 이미지의 가로 사이즈
private void Update()
{
if (transform.position.x <= -width)
{
transform.position += new Vector3(width * 2f, 0f, 0f);
}
}
- 주의사항
- Game View 화면 비율을 16:9로 해놓고 개발할 것. Free Aspect로 개발하면 Scene View의 좌우 길이에 따라서 화면 배율이 계속 변하기 때문에 정상적으로 개발하기 힘들다.
- 화면에 이미지를 렌더링 할 때 그리기 순서를 결정 해줘야 한다. 만약 렌더링 우선순위가 같다면 같은 순위끼리 무작위 순서로 그려져 버린다.
배경의 우선순위 설정
Sky GameObject의 Sprite Renderer -> Additional Settings -> Sorting Layer에 Add Sorting Layer를 통해 Layer를 추가하고 기존 Default 위로 옮겨준다. 먼저 그려져야 뒤에 그려지기 때문이다. 만약에 Sorting Layer가 같다면 둘 중에서는 Order In Layer 항목으로 먼저 그려줄 순위를 결정한다. 숫자가 낮을수록 먼저 그려진다.
카메라
카메라에는의 Projection 옵션에는 원근투영(Perspective) 직교투영(Orthographic)이 있다. 3D는 원근투영을 사용하고 2D는 직교투영을 사용한다. 원근투영은 원근비에의해서 멀리있는것은 작게 그린다. 직교투영은 원근비교 없이 그대로 그리며 세로가 기준이고 가로는 종횡비에의해서 자동으로 결정된다.
PlayerController 구현
Player의 입력 및 Animation, Jump 등 Player와 관련된 것을 처리하기 위해 PlayerController를 만든다.
점프 구현
RigidBody2D를 통하여 점프를 구현한다. jumpSpeed라는 변수를 생성하여 점프시 RigidBody2D의 linearVelocityY에 jumpSpeed를 할당한다. Jump상태나 횟수는 Platform과의 충돌을 확인하고 결정할 수 있다.
// PlayerController의 Jump 구현. 2단 점프가 가능하도록 구현해본다.
// 한번만 점프가 가능하게 하려면 int를 써도 되지만 bool을 사용해도 된다.
[Header("점프 기능")]
[SerializeField] Rigidbody2D rigid;
[SerializeField] [Range(0f, 20f)] float jumpSpeed;
[SerializeField] bool isGrounded;
[SerializeField] int jumpCount;
private void Update()
{
if(Input.GetButtonDown("Jump") &&
jumpCount <= 1) // bool을 통해 한번만 점프할 경우 이 자리에 isGrounded를 사용
{
Jump();
}
}
public void Jump()
{
rigid.linearVelocityY = jumpSpeed;
jumpSound.Play();
jumpCount++;
}
private void OnCollisionEnter2D(Collision2D collision)
{
if(collision.collider.tag == "Platform")
{
isGrounded = true;
jumpCount = 0;
}
}
private void OnCollisionExit2D(Collision2D collision)
{
if(collision.collider.tag == "Platform")
{
isGrounded = false;
}
}
중력 설정
기본적으로 Unity에는 지구의 중력과 비슷한 9.8의 중력 값이 정해져 있다. 하지만 일반적인 플랫폼 게임이나 점프게임들 에서는 보다 빠른 움직임을 위해 중력값을 약 25정도로 두고 사용한다.
중력값은 다음 위치에서 변경한다.
Project Settings -> Physics 2D -> Gravity항목의 Y값을 수정.(일반적으로 -25)
Animation 제어 및 죽음 처리
죽음에 대한 처리를 진행하고 위에서 상태별 Animation을 만들어 둔 것을 적용한다.
// Animation 및 죽음 적용 예시
[SerializeField] Animator animator;
private void Awake()
{
rigid = GetComponent<Rigidbody2D>();
animator = GetComponent<Animator>();
}
public void Die()
{
animator.SetTrigger("Die");
rigid.linearVelocity = Vector2.zero;
rigid.simulated = false;
}
private void OnCollisionEnter2D(Collision2D collision)
{
if(collision.collider.tag == "Platform")
{
isGrounded = true;
jumpCount = 0;
animator.SetBool("Grounded", true);
}
}
private void OnCollisionExit2D(Collision2D collision)
{
if(collision.collider.tag == "Platform")
{
isGrounded = false;
animator.SetBool("Grounded", false);
}
}
private void OnTriggerEnter2D(Collider2D collision)
{
if(collision.tag == "Dead")
{
Die();
}
}
OnTriggerEnter2D를 통하여 장애물과의 충돌, 낙사 등을 확인할 수 있으므로 해당 이벤트시 Die()함수를 통하여 Player의 죽음 상태를 적용한다.
OnCollisionEnter2D, OnCollisionExit2D를 통하여 현제 Platform에 있지 여부를 파악할 수 있으므로 해당 이벤트 발생시 Animator의 상태를 바꿔준다.
Audio 추가
각각의 상황에 맞는 사운드를 추가하여 Play로 소리가 나게할 수도 있다. 이때 Player GameObject에 직접 Audio Source를 추가해서 Inspector에 넣어줄 수도 있고 Player GameObject의 자식으로 Empty GameObject를 생성하여 해당 GameObject에 Audio Source를 추가 후 Inspector에 넣어줄 수도 있다.
[SerializeField] AudioSource jumpSound;
[SerializeField] AudioSource dieSound;
public void Jump()
{
jumpSound.Play();
}
public void Die()
{
dieSound.Play();
}
Hierarchy에 Empty GameObject를 만든 후 해당 Obejct에 Audio Source를 넣고 Play On Awake를 켜주면 게임이 실행될 때 함께 켜진다.(BGM 넣는 방법)
- 주의사항:Jump와 Die Sound는 호출 될 때만 실행할 것이기 때문에 Play On Awake 옵션은 꺼주도록 한다.



