일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- layer
- 기초
- Aseprite
- 포토샵
- COSMO
- 픽셀 아트
- 드로잉
- 모작
- 도트
- photoshop
- 도트공부
- 드로잉 연습
- 스마일게이트
- 노하우
- 연습
- 에이세프라이트
- 서포터즈
- 멋쟁이사자처럼
- 애니메이션
- 인디게임 개발
- Pixelart
- 개발
- 장학팀
- menu
- pixel art
- 자원순환보증금관리센터
- 채색
- 반환원정대
- TOOL
- 픽셀아트
- Today
- Total
소소한 나의 하루들
2d 종스크롤 슈팅(6) - 아이템과 필살기 구현하기 본문
출처: https://youtube.com/playlist?list=PLO-mt5Iu5TeYI4dbYwWP8JqZMC9iuUIW2&feature=shared
#1. 준비하기
오늘 만들어볼 아이템은 동전, 파워, 필살기 3가지이다. 아이템 sprite의 Pixels per Unit은 24, Filter Mode는 Point (no filter), Compression은 None으로 설정한다. 그리고 아이템 모두 애니메이션이 있는 아틀라스 이미지이기 때문에 Sprite Mode는 Multiple로 해준다.
동일한 크기의 spirte라면 Slice를 Slice By Cell Size, Pixel Size를 16x16으로, Padding을 x1 y1로 해주는데(이번 에셋의 경우) 아틀라스를 구성하는 sprite의 크기가 다르다면, Slice를 Automatic으로 하고, Padding은 Center로 설정해주면 된다.
반드시 Padding은 x1 y1로 설정해준다.
설정이 끝나면 Scene에다가 아이템들을 배치한다. 그리고 콜라이더 컴포넌트와 Rigidbody 2D 컴포넌트를 추가한다.
*Rigidbody 2D의 Gravity Scale은 0으로 설정하고, 아이템끼리는 서로 충돌하지 않게 콜라이더의 Is Trigger를 체크한다.
그리고 Item 스크립트를 생성해서, 각 아이템 오브젝트에 컴포넌트로 드래그 적용시킨다.
아이템에 필요한 로직은 무엇일까? 우선 이 아이템이 어떤 것인지 플레이어가 알아야하기 때문에 아이템 타입을 위한 변수를 추가한다. 그리고 아이템 속도도 추가하기위해 rigidbody.velocity를 사용한다.
아이템 타입은 이름 그대로 동전은 "Coin", 파워는 "Power", 필살기는 "Boom"이라고 한다. 이제 애니메이션을 적용하기 위해 필요한 프레임을 다중선택해서 오브젝트에 sprite 드래그해주고 애니메이션을 적용시킨다.
오브젝트 이동시키기
https://gamdekong.tistory.com/204
1) Transform.Translate(벡터)
벡터 값을 현재 위치에 더하는 함수.
*각 프레임마다 같은 값을 적용하기 위해 벡터에는 Time.deltaTime을 사용해야한다. (곱한다)
Transform은 이동 후에 물리연산을 수행한다. 이동할 때 물리연산을 수행하지 않는다. 따라서 이동한 위치에 다른 오브젝트가 있다면 갑자기 튕겨나온다.
2) Rigidbody.velocity = 벡터 참고
velocity는 위치이동을 하기 전에 먼저 물리연산을 해버린다. 따라서 오브젝트가 튕겨나오지 않고 붙어버린다. [등속운동]
3) Rigidbody.AddForce(벡터, 힘모드) 참고
AddForce()는 벡터 방향으로 힘을 가한다. 따라서 연속적으로 힘을 가해주면 계속 속도가 붙는다(가속). [등가속운동]
*2d에서는 ForceMode의 Impulse를 주로 사용한다. (2d에서는 Acceleration과 VelcotiyChange는 없다)
*기본적으로 벡터는 방향 * 힘 [방향은 단위벡터로 만들어야 제어하기 편하다. : GetAxisRaw()로 -1 / 0 / 1 값을 변수로 갖고오거나 nomalized 또는 normalize() 함수로 단위벡터화한다.]
※Rigidbody의 경우 기본적으로 중력의 영향을 받기 때문에 (중력에 영향을 가하지 않았다면) AddForce()라도 결국 중력에 의해 바닥으로 떨어질 것이다.
#2. 충돌 로직
플레이어가 아이템을 먹어야한다. OnTriggerEnter2D()를 사용하기 위해 Item 태그를 만들어주고 설정한다.
동전을 먹었을 때는 score가 크게 상승하고, 파워를 먹었을 때는 파워 모드가 올라간다. 그런데 파워 모드를 3개까지만 만들어놨기 때문에, maxPower라는 변수를 추가해서 제한을 두어야 한다. (파워는 최대값을 설정하여 구현)
생명도 최대값을 설정하여 이전의 UI 설정 로직을 수정해준다. (3→maxLife)
#3. 필살기
필살기는 쏘자마자 화면에 큰 폭탄이 발생하고, 날라왔던 적 비행기와 총알을 모두 삭제해야한다. 그리고 플레이어는 총알 회피가 가능하도록 한다. 주어진 필살기 spirte의 Pixels per Uinit은 15 정도로 작게 맞춘다. (sprite 크기가 512px인데, 1Unit 당 15px가 들어가는 거니까 화면의 꽤 넓은 면적을 차지할 것이다)
이 필살기는 적 비행기와 총알, 플레이어를 가리면 안된다. Boom 스프라이트가 적 비행기, 총알, 플레이어 아래에 오도록 Order in Layer를 조정한다. 그리고 애니메이션을 넣기 위해 sprite를 다중선택해서 넣어준다.
적절히 투명도도 맞춰주고 설정이 다 끝났으면 비활성화 시켜준다.
그리고 스크립트에서 필살기 아이템(Item Boom)을 먹었을 때 활성화시켜주기위해 필살기 오브젝트(boomEffect)를 가져온다. "Boom" 아이템을 먹으면 boomEffect 오브젝트를 활성화해준다. 그리고 필살기가 재생되면서 게임 화면 상의 적과 총알이 모두 제거되도록 한다.
GameObject.FindGameObjectsWithTag("태그명") : Scene에 있는 오브젝트들 중 입력한 태그의 모든 오브젝트를 추출한다.
*FineGameObjectWithTag()와 구분해야한다. : object에 's'가 붙음
GameObject[] enemies = GameObject.FindGameObjectsWithTag("Enemy");
만약 "Enemy" 태그를 가진 오브젝트가 Scene에 10개 있다면, enemies 배열의 크기는 10이 될 것이다.
어쨌거나 for문을 돌리기 위해서 enemies.Length로 크기를 구하는 것이 좋다.
else if (collision.gameObject.tag == "Item")
{
Item item = collision.gameObject.GetComponent<Item>();
switch (item.type)
{
case "Coin":
score += 1000;
break;
case "Power":
if (power == maxPower)
score += 500;
else
power++;
break;
case "Boom":
//Effect visible
boomEffect.SetActive(true);
Invoke("OffBoomEffect", 4.0f);
//Remove Enemy
GameObject[] enemies = GameObject.FindGameObjectsWithTag("Enemy");
for (int index = 0; index < enemies.Length; index++)
{
Enemy enemyLogic = enemies[index].GetComponent<Enemy>();
enemyLogic.OnHit(1000);
}
//Remove Enemy Bullet
GameObject[] bullets = GameObject.FindGameObjectsWithTag("EnemyBullet");
for (int index = 0; index < bullets.Length; index++)
{
Destroy(bullets[index]);
}
break;
}
Destroy(collision.gameObject);
}
}
void OffBoomEffect()
{
boomEffect.SetActive(false);
}
그리고 각자 enemy의 로직을 갖고온다. 그리고 오브젝트들을 그냥 지우는 것이 아니라, 큰 데미지를 줘서 피격되어 제거되도록 한다. 그리고 플레이어가 총알도 회피할 수 있도록 총알도 다 지운다.
필살기가 해야할 역할들을 다 수행하고 난 뒤에는 필살기를 비활성화시켜야한다. 필살기는 3초 뒤 비활성화시키도록 한다. 아이템을 먹은 뒤에는 맨 마지막에 아이템도 삭제해야한다.
void Boom()
{
if (!Input.GetButton("Fire2"))
return;
if (isBoomTime)
return;
if (curBoom == 0)
return;
curBoom--;
isBoomTime = true;
//Effect visible
boomEffect.SetActive(true);
Invoke("OffBoomEffect", 3.0f);
//Remove Enemy
GameObject[] enemies = GameObject.FindGameObjectsWithTag("Enemy");
for (int index = 0; index < enemies.Length; index++)
{
Enemy enemyLogic = enemies[index].GetComponent<Enemy>();
enemyLogic.OnHit(1000);
}
//Remove Enemy Bullet
GameObject[] bullets = GameObject.FindGameObjectsWithTag("EnemyBullet");
for (int index = 0; index < bullets.Length; index++)
{
Destroy(bullets[index]);
}
}
...
else if (collision.gameObject.tag == "Item")
{
Item item = collision.gameObject.GetComponent<Item>();
switch (item.type)
{
case "Coin":
score += 1000;
break;
case "Power":
if (power == maxPower)
score += 500;
else
power++;
break;
case "Boom":
if (curBoom == maxBoom)
score += 500;
else
curBoom++;
break;
}
Destroy(collision.gameObject);
}
void OffBoomEffect()
{
boomEffect.SetActive(false);
isBoomTime = false;
}
보통 슈팅게임은 아이템을 먹었다고 바로 필살기가 사용되는 것이 아니라, 필살기를 모아놨다가 필살기 버튼을 눌러야 사용하게 된다. 그래서 필살기의 최대값과 현재값(현재 개수)을 갖는 변수를 추가한다. (maxBoom, curBoom)
그리고 폭탄 사용을 관리할 함수도 만들어준다.
Fire2키 (마우스 우클릭/왼쪽 alt)를 누르면 폭탄을 터트리고, 만약 폭탄이 터지는 중이라면 폭탄을 발사하지 않도록 해야한다. 그리고 현재 갖고있는 필살기가 없다면 사용하지 않도록 한다.
아래에는 폭탄을 터트리는(필살기를 사용하는) 로직을 그대로 갖다 옮겨넣는다.
만약 필살기 아이템을 먹었다면 Power 아이템을 먹었을 때와 마찬가지로 로직을 작동시킨다. (필살기 아이템을 먹을때 피현재 필살기 개수를 가산시키고, 만약 최대 필살기 개수와 같다면 가산없이 점수를 주는 방식)
//GameManager
public void UpdateBoomUI(int boom)
{
//Boom UI Init Disable
for (int index = 0; index < playerLogic.maxBoom; index++)
{
boomImage[index].color = new Color(1, 1, 1, 0);
}
//Boom UI Active
for (int index = 0; index < boom; index++)
{
boomImage[index].color = new Color(1, 1, 1, 1);
}
}
//Player
void Boom()
{
if (!Input.GetButton("Fire2"))
return;
if (isBoomTime)
return;
if (curBoom == 0)
return;
curBoom--;
isBoomTime = true;
gameManager.UpdateBoomUI(curBoom);
//Effect visible
boomEffect.SetActive(true);
Invoke("OffBoomEffect", 3.0f);
//Remove Enemy
GameObject[] enemies = GameObject.FindGameObjectsWithTag("Enemy");
for (int index = 0; index < enemies.Length; index++)
{
Enemy enemyLogic = enemies[index].GetComponent<Enemy>();
enemyLogic.OnHit(1000);
}
//Remove Enemy Bullet
GameObject[] bullets = GameObject.FindGameObjectsWithTag("EnemyBullet");
for (int index = 0; index < bullets.Length; index++)
{
Destroy(bullets[index]);
}
}
...
case "Boom":
if (curBoom == maxBoom)
score += 500;
else
{
curBoom++;
gameManager.UpdateBoomUI(curBoom);
}
break;
플레이어가 현재 필살기 폭탄을 몇 개 가지고 있는지 표시해줄 수 있는 UI도 만들어본다.
필살기 폭탄을 관리해줄 수 있는 함도 GameManager에 작성한다. 이 함수는 (1)플레이어가 필살기 아이템을 먹었을 때, 혹은(2) 필살기를 사용했을 때 호출되어야한다.
*모두 다 아이템 개수가 업데이트된 후 호출되어야한다.
실행하기 전, Boom 효과 오브젝트의 투명도를 0으로 낮춘다. 그리고 GameManager에 필살기 폭탄 UI 오브젝트를 드래그 적용한다.
이제 이 아이템들이 언제 나타나느냐가 문제이다. 일반적으로 적 비행기를 물리쳤을 때 아이템이 드랍되는 경우가 있을 것이다. 그렇다면 지금 설정한 아이템 오브젝트들은 프리펩으로 지정을 한다. 그리고 프리펩으로 지정했다면 Scene에서 삭제를 해준다.
#3. 아이템 드랍
이제 아이템 프리펩들을 생성해준다. Enemy와 플레이어에서 총알 프리펩을 변수화시켰던 것처럼 Enemy 스크립트에서 아이템 프리펩들을 변수화시킨다.
: 아이템 프리펩을 저장할 변수 생성
//Enemy
//Random Ratio Item Drop
int ran = Random.Range(0, 10);
if(ran < 5) //Nothing 0.5
{
Debug.Log("Not Item");
}
else if (ran < 8) //Coin 0.3
{
Instantiate(itemCoin, transform.position, itemCoin.transform.rotation);
}
else if (ran < 9) //Power 0.1
{
Instantiate(itemPower, transform.position, itemPower.transform.rotation);
}
else if (ran < 10) //Boom 0.1
{
Instantiate(itemBoom, transform.position, itemBoom.transform.rotation);
}
적 비행기가 파괴되기 직전에 랜덤하게 아이템을 드랍하는(생성) 로직을 추가한다.
*회전만 아이템 프리펩의 회전 방향대로 설정한다.
실행하기 전, 변수에 프리펩들을 할당해줘야한다.
#4. 예외 처리
아이템이 2개가 동시에 소환되는 경우가 있다. 적 비행기가 Destroy가 되기 전에 OnHit()를 2번 맞았다.
: 아이템이 다중으로 만들어지지 않도록 예외처리
//Enemy
public void OnHit(int damage)
{
if (health <= 0)
return;
health -= damage;
spriteRenderer.sprite = sprites[1];
Invoke("ReturnSprite", 0.1f);
if (health <= 0)
{
Player playerLogic = player.GetComponent<Player>();
playerLogic.score += enemyScore;
//Random Ratio Item Drop
int ran = Random.Range(0, 10);
if(ran < 5) //Nothing 0.5
{
Debug.Log("Not Item");
}
else if (ran < 8) //Coin 0.3
{
Instantiate(itemCoin, transform.position, itemCoin.transform.rotation);
}
else if (ran < 9) //Power 0.1
{
Instantiate(itemPower, transform.position, itemPower.transform.rotation);
}
else if (ran < 10) //Boom 0.1
{
Instantiate(itemBoom, transform.position, itemBoom.transform.rotation);
}
Destroy(gameObject);
}
이전에 큰 적 비행기가 쏜 총알 2발을 동시에 맞았을 때 한번에 피 2가 깎이지 않도록 했던 예외처리처럼 처리해준다.
이렇게 플레이어 체력이 0 이하라면 진입을 하지 못하게 하거나 상태를 나타내는 플래그 변수를 활용할 수도 있다.
이제 우리가 알고있는 슈팅게임의 모습을 갖게되었다. 난이도 조절같은 부분만 신경써주면된다.
'개발 > 유니티' 카테고리의 다른 글
2d 종스크롤 슈팅(8) - 최적화의 기본, 오브젝트 풀링 (0) | 2024.02.22 |
---|---|
2d 종스크롤 슈팅(7) - 원근감있는 무한 배경 만들기 (0) | 2024.02.21 |
2d 종스크롤 슈팅(5) - UI 간단하게 완성하기 (0) | 2024.02.18 |
2d 종스크롤 슈팅(4) - 적 전투와 피격 이벤트 만들기 (0) | 2024.02.17 |
2d 종스크롤 슈팅(3) - 적 비행기 만들기 (0) | 2024.02.16 |