오락실에 가면 항상 인기 게임 순위 안에 들어 있는 갤러그는 아군 비행기가 적비행기를 격추하면서 점수를 얻는 게임이다. 앞,뒤,좌,우를 움직이며 미사일을 발사하여 적기를 격추하는 단순한 게임이지만 쉬운 조작법과 간단한 인터페이스로 많은 사람들의 인기를 끌었다. 그래서 갤러그와 같은 2D 비행기 Shooting 게임을 만들어보고 싶었다.
2D 게임을 만들기 위해서는 먼저 Package manager에서 2D SPRITE를 다운로드(또는 업데이트) 해야된다. 임포트한 이미지 파일을 SPRITE (2D AND UI)로 설정한 후 SPRITE RENDRER에 이미지 삽입하면 된다.
모바일 버전으로 엑스포트해보기 위해 전체적인 게임 뷰를 6:10으로 맞춰놓고 제작을 시작했다.
2D SPRITE 이미지를 수정하기 위해서는 인스펙터 창의 2D SPRITE EDITOR 툴을 활용하면 된다. 에디터는 3D FBX의 전개를 2D로 펼쳐놓은 개념이라고 보면 된다. 원하는 비율로 SLICE해서 이미지를 분할하거나 Padding하여 이미지 사이에 약간의 간격을 두고 자를수도 있다. 이미지끼리 겹치면 에러가 뜨기도 하니 여유있게 패딩해주는게 좋겠다.
플레이어가 움직일 수 있는 범위를 지정해주기 위해 Box collider를 활용했다. 하이라키 창에서 Create Emty를 생성 후 Rename : Border에 계층화를 시켜 Top, Bottom, Right, Left 박스 콜라이더를 설정해 주었다. 그리고 비행기가 총알을 발사했을 때 총알이 자연스럽게 화면 너머로 움직이며 사라질 수 있게 BulletBorder를 따로 만들었고 기존의 border보다 더 넓은 범위로 확장되게 배치하였다. 추가적으로 border와 bulletborder에 rigidbody2D를 추가했고 Body Type은 Static으로 설정했다. 그리고 상단 Tag에 border라는 새로운 태그를 생성해 달아주었고, 콜라이더에서 isTrigger을 체크했다.
플레이어 비행기에 추가한 컴포넌트들이다. Sprite renderer에 구글에서 받은 비행기 이미지를 넣었고 Rigidbody2D와
Box Collider 2D를 추가했다. (MeshCollider를 넣어주면 오브젝트 모양에 맞춰 콜라이더가 자동으로 생성된다, 여기선 편의상 박스 콜라이더로 지정해주었다.) 여기서 Rigid body에서 바디타입을 Kinematic으로 변경해주었고 Box Collider에도 isTrigger을 체크해주었다.
2D에서 애니메이션을 생성 시 원하는 애니메이션을 하이라키 창의 플레이어에 드래그앤 드롭 해주면 된다. 애니메이션이 여러개 일 경우 묶어서 드롭해주면 된다. 기본 자세(idle), 오른쪽으로 이동(Right), 왼쪽으로 이동(Left) 다음 3가지의 애니메이션을 만들었다. 생성된 애니메이션은 인스펙터 창 하단의 preview에 플레이어를 드래그앤 드롭해서 정상작동하는지 확인하면 된다.
Anmiator Controller에서 idle/left/rigt 간에 서로 트랜지션을 연결해 주었다. Parmeter에서 인티저 형식으로 이름을 변경해 주었고 Input으로 이름을 설정했다. 각각의 트랜지션 컨디션마다 추가한 Input에 Equal 형식으로 숫자를 새로 입력해주었다. (1,0,-1) 해당 숫자는 마우스 좌클릭 시 왼쪽, 마우스 우클릭 시 오른쪽으로 이동하는 명령값에 반응하는 애니메이션을 할당한다. 그리고 has exit time을 체크 해제하여 무한 반복하도록 설정하였고 trasition duration도 0으로 변경했다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class player : MonoBehaviour
{
public float speed;
public float power;
public bool isTouchTop;
public bool isTouchBottom;
public bool isTouchRight;
public bool isTouchLeft;
public GameObject bulletObjA;
public GameObject bulletObjB;
public GameObject bulletObjC;
public float MaxShootDelay;
public float NowShootDelay;
//2D 비행기 꼬리 움직임
Animator anim;
private void Awake()
{
anim = GetComponent<Animator>();
}
void Update()
{
Move();
Fire();
Reload();
}
void Move()
{
float h = Input.GetAxisRaw("Horizontal");
if ((isTouchRight && h == 1) || (isTouchLeft && h == -1))
h = 0;
float v = Input.GetAxisRaw("Vertical");
if ((isTouchTop && v == 1) || (isTouchBottom && v == -1))
v = 0;
//위아래좌우 콜라이더에 닿으면 움직이지 않는다(떨림 방지)
Vector3 nowPos = transform.position;
Vector3 afterPos = new Vector3(h, v, 0) * speed * Time.deltaTime;
transform.position = nowPos + afterPos;
if (Input.GetButtonDown("Horizontal") || Input.GetButtonUp("Horizontal"))
{
anim.SetInteger("Input", (int)h);
}
}
void Fire()
{
if (!Input.GetButtonDown("Fire1"))
return;
//!(not) 선언 후 return을 반환하면 not이 중복되어 긍정의 의미, 즉 버튼을 누르면 발사되라는 뜻
if (NowShootDelay < MaxShootDelay)
return;
//재장전으로 총알을 발사하는 시간 간격 설정
switch(power)
{
case 1 :
GameObject bullet = Instantiate(bulletObjA, transform.position, transform.rotation);
Rigidbody2D rigid = bullet.GetComponent<Rigidbody2D>();
rigid.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
break;
case 2 :
GameObject bullet2 = Instantiate(bulletObjB, transform.position, transform.rotation);
Rigidbody2D rigid2 = bullet2.GetComponent<Rigidbody2D>();
rigid2.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
break;
case 3 :
GameObject bullet3 = Instantiate(bulletObjA, transform.position + Vector3.right*0.3f, transform.rotation);
GameObject bullet4 = Instantiate(bulletObjA, transform.position + Vector3.left*0.3f, transform.rotation);
Rigidbody2D rigid3 = bullet3.GetComponent<Rigidbody2D>();
rigid3.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
Rigidbody2D rigid4 = bullet4.GetComponent<Rigidbody2D>();
rigid4.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
break;
case 4 :
GameObject bullet5 = Instantiate(bulletObjC, transform.position + Vector3.right * 0.2f, transform.rotation);
GameObject bullet6 = Instantiate(bulletObjC, transform.position + Vector3.left * 0.2f, transform.rotation);
Rigidbody2D rigid5 = bullet5.GetComponent<Rigidbody2D>();
rigid5.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
Rigidbody2D rigid6 = bullet6.GetComponent<Rigidbody2D>();
rigid6.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
break;
case 5:
GameObject bullet7 = Instantiate(bulletObjA, transform.position + Vector3.right * 0.2f, transform.rotation);
GameObject bullet8 = Instantiate(bulletObjA, transform.position + Vector3.left * 0.2f, transform.rotation);
GameObject bullet9 = Instantiate(bulletObjC, transform.position + Vector3.right * 0.5f, transform.rotation);
GameObject bullet10 = Instantiate(bulletObjC, transform.position + Vector3.left * 0.5f, transform.rotation);
GameObject bullet11 = Instantiate(bulletObjB, transform.position + Vector3.right, transform.rotation);
GameObject bullet12 = Instantiate(bulletObjB, transform.position + Vector3.left, transform.rotation);
Rigidbody2D rigid7 = bullet7.GetComponent<Rigidbody2D>();
rigid7.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
Rigidbody2D rigid8 = bullet8.GetComponent<Rigidbody2D>();
rigid8.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
Rigidbody2D rigid9 = bullet9.GetComponent<Rigidbody2D>();
rigid9.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
Rigidbody2D rigid10 = bullet10.GetComponent<Rigidbody2D>();
rigid10.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
Rigidbody2D rigid11 = bullet11.GetComponent<Rigidbody2D>();
rigid11.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
Rigidbody2D rigid12 = bullet12.GetComponent<Rigidbody2D>();
rigid12.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
break;
}
NowShootDelay = 0;
}
void Reload()
{
NowShootDelay += Time.deltaTime;
}
void OnTriggerEnter2D(Collider2D collision)
{
if(collision.gameObject.tag == "Border")
{
switch(collision.gameObject.name)
{
case "Top":
isTouchTop = true;
break;
case "Bottom":
isTouchBottom = true;
break;
case "Right":
isTouchRight = true;
break;
case "Left":
isTouchLeft = true;
break;
}
}
}
void OnTriggerExit2D(Collider2D collision)
{
if (collision.gameObject.tag == "Border")
{
switch (collision.gameObject.name)
{
case "Top":
isTouchTop = false;
break;
case "Bottom":
isTouchBottom = false;
break;
case "Right":
isTouchRight = false;
break;
case "Left":
isTouchLeft = false;
break;
}
}
}
}
플레이어에 추가된 스크립트다. 여기서 Bool함수를 생성했는데 기존에 비행기가 벽면에 닿으면 미세한 떨림이 있었는데 bool함수를 통해 플레이어가 벽면에 닿으면 이동을 멈춰 떨림 버그가 발생하지 않도록 스크립트를 추가해주었다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Bullet : MonoBehaviour
{
public int dmg;
private void OnTriggerEnter2D(Collider2D collision)
{
if(collision.gameObject.tag == "Bulletborder")
{
Destroy(gameObject);
}
}
}
SPRITE EDITOR을 활용하여 Bullet Prefab을 제작했다. Bullet에도 Box Collider와 Rigid body 컴포넌트를 추가했고 Gravity Scale을 0으로 변경했다. 앞서 생성한 Bulletborder에 총알이 닿으면 Destroy되게 스크립트도 작성했다. Bullet에도 isTrigger을 체크해주었는데 만약 체크되어있지 않으면 총알이 발사(연사)될 때 매 프레임마다 Instantiate된 총알끼리 부딪혀 일직선으로 나가지 않기 때문이다. 완성된 Bullet Prefab을 player 인스펙터창에 선언된 objA에 넣어주기만 하면 된다. (Bullet프리팹을 만들때는 항상 transform을 reset해서 좌표값을 0,0,0으로 만들고 시작해야 한다)
배경음악을 찾는데 유용하게 쓰는 사이트이다. 다양한 장르와 느낌의 무료 배경음악이 많으니 게임에 맞는 음원을 서치하면 되겠다. 해당 음원 파일을 유니티에 임포트하고 Main Camera에 드래그앤 드롭하면 자동으로 배경음악이 생성되게 된다. 비슷하게 bullet이 발사 될 때 sound effect를 넣어주기 위해 bullet prefab에 원하는 사운드를 드래그앤 드롭해주기만 하면 되니 참고.
여기까지 기본 환경 세팅과 플레이어 및 총알 제작을 완료했다.
'게임 프로그래밍 > 유니티 프로젝트' 카테고리의 다른 글
유니티를 활용한 3D 적 피하기 게임 만들기(1) (0) | 2021.07.31 |
---|---|
[Unity Error]유니티 안드로이드 빌드 시 "Package Name has not been set up correctly" 오류 해결 방법 (0) | 2021.07.29 |
유니티를 활용한 2D Plane Shooting 게임 만들기(2) (0) | 2021.07.29 |
무료 3D 캐릭터 애니메이션 다운로드 사이트 - Mixamo 캐릭터 FBX 파일 Unity 임포트, 애니메이션 세팅 방법 (0) | 2021.07.24 |
Unity Navigation AI를 활용하여 미니 게임 만들기 (1) | 2021.07.21 |