https://youtu.be/YlcZZPR1jqc
영상의 0:08 ~ 0:18 구간 10초 분량의 인트로 영상을 만드는 과정입니다.
스프라이트를 분할하고 영상을 참고하며 어떻게 만들면 좋을지 구상합니다.
한 컷에 진행하는 것보단 파트를 나눠서 작업한 뒤 이어붙이는 방식이 효과적일 것 같습니다.
스프라이트를 준비하고 프리팹으로 만들어줍니다.
게임오브젝트로 받아와 SetActive와 코루틴을 이용해 적절한 시간에 맞춰 전환시키는 연출을 합니다.
인트로 연출 중 Skip 효과는 키 입력 신호를 받아 바로 다음 씬으로 넘어가게 하였습니다.
메탈슬러그는 플레이어가 움직이면서 총도 쏘고 시선도 바뀌고 하기때문에 상, 하체 스프라이트가 나뉘어져 있습니다.
상체, 하체 애니메이션을 만든 뒤 한 오브젝트 안에 붙여넣는 작업을 했습니다.
주의할 점은 하체와 결합했을 시 상체의 중심축이 맞지 않는 스프라이트가 몇 개 있는데,
스프라이트 에디터로 Split 하는 과정에서 플레이어 상체 스프라이트의 피벗을 좌측 하단으로 당겨서 해결할 수 있었습니다.
상체 - 대기 애니메이션
상체 - 전방 발사 애니메이션
상체 - 하단 발사 애니메이션
상체 - 상단 발사 애니메이션
상체 - 칼 공격 애니메이션
하체 - 대기 애니메이션
하체 - 앞으로 점프 애니메이션
하체 - 이동 애니메이션
플레이어의 이동을 담당할 스크립트.
isJump는 이중점프를 막기 위한 트리거 역할입니다.
Start 함수에서 자식 오브젝트 컴포넌트를 받아왔습니다.
플레이어 빈 오브젝트 안에 상체, 하체 오브젝트를 넣어놨기 때문에 자식 오브젝트의 정보를 전달하는 GetChild 함수를 사용했습니다.
Update 함수에선 플레이어의 간단한 좌,우 이동과 점프 기능을 만들었고
머무른 상태일 때 매개변수가 전달되는 OnTriggerStay를 이용하여 맞닿은 오브젝트의 태그가 Ground일때 isJump를 비활성화 해주었습니다.
플레이어가 바닥에 닿고있으니 점프 상태가 아닌 판정을 해주는 것인데요,
이 코드로 이중 점프의 제어 역할을 해주면 isJump가 비활성화 일때만 Update 함수에서 if문이 작동하게 됩니다.
switch-case문으로 하체 애니메이션을 제어할 것인데,
h는 Input.GetAxisRow("Horizontal")을, 즉 수평 움직임을 의미합니다.
이 값이 0이 아니라면 어떠한 영향을 받아서 움직이고 있다는 것으로 처리됩니다.
그래서 0이 아닐 때만 플레이어의 상태는 Move로 변경해주며,
다시 switch를 통해 두번째 case인 Move 상태가 되면 if문의 조건에 맞을 때 아래의 명령을 실행하게 됩니다.
여기서 if문의 조건은 and가 3번 사용됐으므로 1가지 조건으로 통용되는데
1. 움직이는 상태
2. 점프 신호가 없을 때 (점프는 다른 애니메이션을 하기 때문)
3. 강체(rigidbody)의 y값(수직 속도)이 0.01보다 작거나 같을 때
모두 만족중인 상황에서 다음 명령 실행 중인 애니메이션을 모두 취소하고 하체이동 애니메이션을 재생되는 것입니다.
h(수평움직임) 값이 0이고 점프 상태가 아니라면 다시 대기상태 및 애니메이션으로 전환됩니다.
상체 애니메이터입니다. 트랜지션, 키입력에 따라 연결해주고 간단한 이동 로직을 완성했습니다.
총알 애니메이션을 만들고 적이 총알에 피격될 수 있게 Box Collider를 추가하였습니다.
공격 로직입니다.
플레이어의 보는 방향에 따라 총알이 생성되며 나아가는 위치도 동일하게 정해주었습니다.
fire(총알 오브젝트)의 transform.rotation을 조절하는 이유는 회전값을 정해주지 않는다면 총알이 사진처럼 발사되는 문제가 생깁니다. 그렇기 때문에 발사 방향에 따라 회전을 시켜주어야 합니다.
총을 발사한 뒤 플레이어가 점프를 뛰면 생성된 총알이 플레이어랑 똑같이 위치가 변경되기 때문에 SetParent에 null을 넣어서 총알의 부모 오브젝트가 없어지게 설정했습니다.
유니티에서 회전은 Vector로 하지 못하기 때문에 오일러각을 대입해줍니다.
마지막으로 총알이 발사된채로 남아있으면 안되므로 생명주기를 조절하여 일정시간 지나면 사라지게 해줍니다.
스프라이트를 이어붙여 애니메이션을 만들고
첫 스프라이트와 마지막 스프라이트의 Position.y 값을 조절하여 위에서 아래로 내려오게 만들어줍니다.
그리고 애니메이션이 종료될 때 OnEnterFinish 함수를 호출하는 이벤트를 추가하여 등장 애니메이션이 끝나고 다음 행동을 취하게 할 것입니다.
public class MarcoEnter : MonoBehaviour
{
MarcoControl control;
void Start()
{
control = transform.parent.GetComponent<MarcoControl>();
}
void OnEnterFinish()
{
if (control != null)
{
control.OnEnterFinish();
}
}
}
OnEnterFinish 함수는 null 체크를 하는 용도이고,
핵심적인 기능은 MarcoEnter(플레이어 조작)에 있는 OnEnterFinish 함수에 있습니다.
public void OnEnterFinish()
{
//자식객체(상체, 하체, 등장, 플레이어번호) 찾기
GameObject upperBody = transform.Find("상체").gameObject;
GameObject lowerBody = transform.Find("하체").gameObject;
GameObject enterObj = transform.Find("등장").gameObject;
GameObject playerNum = transform.Find("플레이어번호").gameObject;
//마르코 상체/하체/플레이어번호 자식객체 활성화
upperBody.SetActive(true);
lowerBody.SetActive(true);
playerNum.SetActive(true);
//마르코 등장 자식객체 비활성화
enterObj.SetActive(false);
//마르코 상태를 play 상태로 전이
GameManger manager = GameManger.Instance;
manager.MarcoState = MarcoState.play;
이 함수가 실행될 때 상체, 하체, 등장의 오브젝트를 활성화시키고 자신(등장 애니메이션)은 비활성화 시킵니다.
메탈슬러그는 등장 애니메이션이 종료된 후에 플레이어가 움직일 수 있습니다.
하이라이키 창에서 플레이어의 상체, 하체의 초기 활성 상태는 꺼두고 플레이어의 상태를 State.play로 변경해줍니다.
플레이어가 처음 생성되거나 리스폰 될 때는 바로 죽을 수도 있기 때문에 무적 효과를 만들어줄 것입니다.
플레이어 조작 스크립트로 돌아가
//마르코 무적 여부
bool invincible = true; //등장 이후 깜빡임이 끝나면 무적해제
변수의 초기 값을 true로 두고
//마르코 무적 깜빡임 코루틴
IEnumerator Blink()
{
SpriteRenderer upperBodyRender = transform.Find("상체").GetComponent<SpriteRenderer>();
SpriteRenderer lowerBodyRender = transform.Find("하체").GetComponent<SpriteRenderer>();
for(int i = 0; i < 20; i++)
{
//스프라이트 숨기기
upperBodyRender.enabled = false;
lowerBodyRender.enabled = false;
//숨기기 대기시간
yield return new WaitForSeconds(0.05f);
//스프라이트 보이기
upperBodyRender.enabled = true;
lowerBodyRender.enabled = true;
//보이기 대기시간
yield return new WaitForSeconds(0.1f);
}
invincible = false; //무적모드 종료 => 적들의 피해를 받음
}
Blink 코루틴에서 0.05~0.1초 간격으로 스프라이트 렌더러를 껐다 켰다 하면서 무적상태일 때 캐릭터가 깜빡거리게 만들어 줍니다.
반복문으로 텀을 정해놓았고 i가 20이 되면 for문을 빠져나와 invincible 변수의 값을 false로 만들어 무적 상태가 끝남을 알립니다.
//마르코 등장이 끝났음을 알려주는 함수
public void OnEnterFinish()
{
//자식객체(상체, 하체, 등장, 플레이어번호) 찾기
GameObject upperBody = transform.Find("상체").gameObject;
GameObject lowerBody = transform.Find("하체").gameObject;
GameObject enterObj = transform.Find("등장").gameObject;
GameObject playerNum = transform.Find("플레이어번호").gameObject;
//마르코 상체/하체/플레이어번호 자식객체 활성화
upperBody.SetActive(true);
lowerBody.SetActive(true);
playerNum.SetActive(true);
//마르코 등장 자식객체 비활성화
enterObj.SetActive(false);
//마르코 상태를 play 상태로 전이
GameManger manager = GameManger.Instance;
manager.MarcoState = MarcoState.play;
//마르코 깜빡임 시작
StartCoroutine(Blink());
}
플레이어 등장 애니메이션을 만들 때 사용한 OnEnterFinish 함수에서
등장 애니메이션이 끝나고 Blink 코루틴을 재생해서 첫 생성때도 무적 효과를 주었습니다.
그런데 아직까지는 실제로 플레이어가 무적은 아닙니다.
아직은 적의 공격을 구현하지 않았기 때문입니다.
적 오브젝트의 공격을 OnTriggerEnter의 콜라이더 검출로 감지할 것이고 공격 오브젝트와 검출한 콜라이더의 태그가 일치하다면 플레이어의 HP를 감소하는 식으로 피격 처리를 할 것입니다.
그리고 OnTriggerEnter 함수의 메소드의 상단에 'invincible가 false일 때' 라는 조건을 걸어주면 무적 처리가 됩니다.
//마르코 번호 숨기기 코루틴
IEnumerator HideNum()
{
//번호 보이기 대기시간
yield return new WaitForSeconds(3.0f);
//번호 숨기기
GameObject playerNum = transform.Find("플레이어번호").gameObject;
playerNum.SetActive(false);
}
플레이어 조작 스크립트에 HideNum 코루틴을 작성하였습니다.
이 애니메이션은 3초간 재생되고 오브젝트는 비활성화되는 간단한 코드인데요,
이 코루틴은 아래의 OnEnterFinish 함수에서 사용됩니다.
//마르코 등장이 끝났음을 알려주는 함수
public void OnEnterFinish()
{
//자식객체(상체, 하체, 등장, 플레이어번호) 찾기
GameObject upperBody = transform.Find("상체").gameObject;
GameObject lowerBody = transform.Find("하체").gameObject;
GameObject enterObj = transform.Find("등장").gameObject;
GameObject playerNum = transform.Find("플레이어번호").gameObject;
//마르코 상체/하체/플레이어번호 자식객체 활성화
upperBody.SetActive(true);
lowerBody.SetActive(true);
playerNum.SetActive(true);
//마르코 등장 자식객체 비활성화
enterObj.SetActive(false);
//마르코 상태를 play 상태로 전이
GameManger manager = GameManger.Instance;
manager.MarcoState = MarcoState.play;
//마르코 깜빡임 시작, 번호숨기기 코루틴 시작
StartCoroutine(Blink());
StartCoroutine(HideNum());
}
게임을 시작하면 무적 효과와 동시에 플레이어의 머리 위에 P1 오브젝트가 표기될 것입니다.
하단의 스프라이트들은 모두 동일한 것으로 보이지만 자세히보면 각기 다른 스프라이트였습니다.
이를 이어붙여서 애니메이션으로 만들면 물이 찰랑거리는 연출을 하게 됩니다.
분할한 스프라이트들을 모두 이어붙여 하나의 긴 맵을 만들었습니다.
오브젝트를 옮기는 작업을 할 때 Vertex Snapping 기능을 이용하면 훨씬 편하고 효율적인 작업이 가능하다고 합니다.
지형은 플레이어가 지나다니는 발판이 되기 때문에 박스 콜라이더를 입혀주었습니다.
충돌체크를 하기 위해 두 오브젝트 모두 Trigger가 활성화 되어있으면 안되며, 한 쪽에라도 Rigidbody가 입혀져 있어야 합니다. (굉장히 중요하지만 자주 까먹어서 왜 안되지 싶을때가 한 번씩 있습니다.)
Box Collider를 입히기 어려운 지형은 Edge Collider를 이용했습니다.
스테이지를 만들고 테스트하며 빠트린 기능을 추가했습니다.
메탈슬러그는 왼쪽에서 오른쪽으로 진행하는 횡스크롤 게임이고 플레이어가 한 번 지나간 길은 다시 되돌아오지 못합니다.
그러한 고증을 살리기 위해 플레이어가 왼쪽으로 움직이면 뒷걸음질 칠 수 없도록 Eular각으로 회전을 시켜준다. (flipX는 상체만 회전되어 사용X)
또 마찰에 관한 문제가 있었습니다. 영상처럼 플레이어가 점프를 하여 콜라이더의 가장자리에 부딪히면 멈춰버리게 됩니다.
이 부분인데 Rigidbody의 마찰을 없애서 해결했습니다.
프로젝트 창 우클릭 - Create - Physics Material 2D 생성 - Friction(마찰), Bounciness(탄력) 두 값을 모두 0으로 설정하고
플레이어의 Rigidbody - Material에 등록해주면 됩니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class Pig : MonoBehaviour
{
public GameObject popupScore;
float hp = 100;
Animator anim;
BoxCollider2D col;
void Start()
{
anim = GetComponent<Animator>();
col = GetComponent<BoxCollider2D>();
}
public void DoDamage(float damage)
{
hp -= damage;
if (hp <= 0) Die();
}
void Die()
{
//돼지 사망 애니메이션
anim.SetTrigger("die"); //사망 애니메이션 플레이
col.enabled = false; //불필요한 충돌체 동작 중지하기
Destroy(gameObject, 1); //사망 애니메이션 이후 1초 후 제거
//스코어 상승
}
void OnTriggerEnter2D(Collider2D collision)
{
string tag = collision.tag;
if (tag == "마르코총알")
{
hp -= 50;
if (hp <= 0) Die();
}
}
}
돼지 오브젝트 입니다.
플레이어가 돼지를 처치할 때 게임 스코어가 상승합니다.
돼지의 최대 체력은 100이며 총알에 맞을 때마다 체력이 50씩 감소하고,
0이 되면 사망 애니메이션을 재생, 1초 후 오브젝트가 삭제됩니다.
1초라는 대기 시간에도 플레이어의 공격에 피격될 수 있기 때문에 Collider의 활성화 상태를 미리 false로 바꿔주어 불필요한 충돌을 방지하였습니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Bird : MonoBehaviour
{
public float speed = 50;
public float angle = 150;
public float lifeTime = 5;
void Update()
{
Quaternion rot = Quaternion.Euler(0, 0, angle);
Vector2 dir = rot * Vector2.right;
transform.Translate(dir * speed * Time.deltaTime);
Destroy(gameObject, lifeTime); //lifeTime 이후에 객체 제거
}
}
새 오브젝트 입니다.
새는 특정 구역에서 스폰되고 날아가는 배경 효과 연출을 합니다.
정해놓은 벡터를 따라 이동하며 5초 뒤에 오브젝트가 제거되는 코드입니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BulletBox : MonoBehaviour
{
public GameObject effect;
//마르코의 레이캐스트에 의해서 피해를 받음
public void DoDamage()
{
Explode();
}
void OnTriggerEnter2D(Collider2D collision)
{
string tag = collision.tag;
if (tag == "마르코총알") Explode();
}
void Explode()
{
//TODO : (1)총알 갯수 늘리기
// (2)박스 제거
// (3)박스 제거 애니메이션 효과
// (4)마르코 총알 제거
// (5)마르코 총알 폭발 효과
//총알 갯수 늘리기//
GameManger manager = GameManger.Instance;
manager.BulletCount += 100;
//박스제거 + 제거 애니메이션
Vector3 P = transform.position;
Quaternion Q = Quaternion.identity;
Destroy(gameObject);
Instantiate(effect, P, Q);
}
}
총알 박스 오브젝트 입니다.
파괴될 시 플레이어의 현재 탄약수를 증가시킵니다.
총알은 게임 매니저에서 관리하고, 플레이어가 공격했을 때 히트한 오브젝트의 태그가 총알박스라면 Explode 함수를 호출시키는 코드입니다.
메탈슬러그는 미션 시작 효과도 빼놓을 수 없어 만들어주었습니다.
스프라이트를 분할하고 미리 배치를 해둡니다.
public class MSLetter : MonoBehaviour
{
enum State {wait = 0, move = 1, exit=2 };
public float movSpeed = 100; //이동속도
public float rotSpeed = 90; //회전속도
public float waitTime = 0; //대기시간
public float exitTime = 2; //글자 퍼져서 나가는 시간
public float exitAngle = 0; //글자 퍼져서 나가는 각도
Vector3 orgPos; //글자의 원래 위치
Vector3 startPos; //글자의 시작 위치
SpriteRenderer spriteRenderer;
State state;
IEnumerator TimeOut()
{
//대기시간
yield return new WaitForSeconds(waitTime);
//이동으로 상태변경
state = State.move;
//나가는 시간동안 wait를 함 (exitTime)
yield return new WaitForSeconds(exitTime - waitTime);
//나가는 상태변경
state = State.exit;
}
void Start()
{
//이동의 원래 위치 저장
orgPos = transform.position;
//시작점 위치 찾아서 이동, y축을 중심으로 180도 회전
startPos = GameObject.Find("미션시작/시작점").transform.position;
transform.position = startPos;
transform.rotation = Quaternion.Euler(0, 180, 0);
//Color 조정
spriteRenderer = GetComponent<SpriteRenderer>();
spriteRenderer.color = new Color(0.0f, 0.0f, 1.0f);
//상태 변수 초기화
state = State.wait;
//코루틴 시작
StartCoroutine(TimeOut());
}
}
문자의 State를 Wait, Move, Exit 3가지로 나누어놓고
문자의 이동속도와 회전속도, 대기시간, 퍼지는 시간, 퍼지는 각도를 하나하나의 문자마다 정해줄 것입니다.
영상에서는 화면 밖에서 알파벳이 날아오는데 화면 밖의 특정 지점을 시작점으로 빈 오브젝트를 만들어주었습니다.
Find를 통해 시작점의 위치값을 저장하였고,
알파벳은 회전하면서 날아오기 때문에 Rotation Y값도 180도 돌리는 작업과 Sprite Renderer를 불러와 색상 또한 파란색 RGB(0,0,1)로 맞춰주었습니다.
이후 게임을 시작하면 알파벳이 화면 밖에서 정렬되는데요,
이때 까지는 State.Wait이며 이후 TimeOut 코루틴을 재생해 State.Move가 됩니다.
void Update()
{
switch (state)
{
case State.wait:
break;
case State.move:
//원래 위치로 이동
Vector3 currPos = transform.position; //현재위치
Vector3 destPos = orgPos; //목적지
Vector3 movePos = Vector3.MoveTowards(currPos, destPos, movSpeed * Time.deltaTime);
transform.position = movePos;
//목적지 도착 판단
Vector3 v = destPos - movePos; //현재위치에서 목적지 까지 가는 벡터
float dist = v.magnitude; //현재위치에서 목적지까지의 남은거리
if (dist <= 0.5) //남은거리가 0.5 이하면 도착으로 판단
{
//현재 회전된 각도 구하기
float angle = transform.rotation.eulerAngles.y; //0 ~ 360
// x축 위는 0~180도, x축 아래는 -180~0도 변경
if (180 < angle && angle <= 360)
{
angle = angle - 360; // -180 ~ 0
}
if (angle > 0)
{
transform.Rotate(-Vector3.up * rotSpeed * Time.deltaTime);
if (angle <= 90)
{
spriteRenderer.color = new Color(1.0f, 1.0f, 1.0f); //원래 색으로 복구
}
}
else
{
transform.rotation = Quaternion.Euler(0, 0, 0);
}
}
break;
case State.exit:
Quaternion Q = Quaternion.Euler(0, 0, exitAngle);
transform.Translate(Q * Vector3.right * movSpeed * Time.deltaTime);
break;
}
}
Update 함수에서는 switch-case문을 이용해 알파벳의 현재 상태에 따라 다른 동작을 실행할 수 있도록 나눠주었습니다.
State.move가 되면 MoveTowards 함수로 시작점으로부터 최종 위치로 이동하게 해주었고
최종 위치에서 현재 위치까지 Vector3 magnitude(남은 벡터의 길이)가 0.5 이하면 도착인 것으로 판단하여 y축의 회전 및 색상을 원상복구 시키는 코드입니다.
State.exit가 되면 인스펙터 창에서 알파벳 별로 설정한 exitAngle에 따라 해당하는 각으로 퍼지며 종료됩니다.
플로팅 텍스트란 코드 내부에서 특정한 데이터가 연산되는 것과 별개로 게이머에게 해당 데이터를 노출시켜 몰입도를 높이는 역할을 합니다.
일반적으로 데미지나 점수와 관련된 것들을 플로팅 텍스트로 주로 사용합니다.
여기서는 돼지를 처치하고 게임 스코어가 상승될 때 플로팅 텍스트 효과를 주려고 합니다.
플로팅 텍스트 역할을 해줄 오브젝트를 하나 만들고 프리팹화 합니다.
public GameObject popupScore;
void Die()
{
//돼지 사망 애니메이션
anim.SetTrigger("die"); //사망 애니메이션 플레이
col.enabled = false; //불필요한 충돌체 동작 중지하기
Destroy(gameObject, 1); //사망 애니메이션 이후 1초 후 제거
//팝업 점수 생성 + 게임매니저 점수 증가
Vector3 offset = new Vector3(0, 10, 0);
Vector3 P = transform.position;
Quaternion Q = Quaternion.identity;
GameObject scoreObj = Instantiate(popupScore, P + offset, Q);
Text scoreText = scoreObj.transform.Find("Text").GetComponent<Text>();
scoreText.text = "10";
//게임매니저 점수 증가
GameManger manger = GameManger.Instance;
manger.Score += 10;
}
Die 함수는 돼지가 사망할 때 실행되는데요,
오브젝트는 Instantiate될 때 자신의 위치에서 y가 10인 벡터를 더해줘 수직으로 상승하게 되고
자식 오브젝트에 있는 text 값을 10으로, 게임매니저에 플레이어의 점수도 10점을 올려주게 됩니다.
숫자 스프라이트를 재활용하여 목숨을 나타내는 UI를 만들었습니다.
public class PlayerCount : MonoBehaviour
{
//숫자 스프라이트
public Sprite[] num;
Transform countObj;
Image numImg;
void Start()
{
countObj = transform.Find("갯수");
numImg = countObj.GetComponent<Image>();
}
public void SetNumber(int x)
{
numImg.sprite = num[x];
}
}
Sprite[] num은 목숨 갯수를 바꿔치기 해줄 이미지를 넣는 배열
countObj는 바꿀 오브젝트의 위치
numImg는 countObj 오브젝트의 Image 컴포넌트 정보
SetNumber(int x) 함수는 매개변수로 받아온 목숨 갯수 x의 값에 따라 numImg의 스프라이트를 num 배열의 숫자로 변경시켜줍니다.
void Update()
{
//플레이어 매니저..가져오기//
GameManger manager = GameManger.Instance;
//플레이어 갯수 UI 출력하기
PlayerCount playerCount = transform.Find("플레이어갯수").GetComponent<PlayerCount>();
playerCount.SetNumber(manager.PlayerCount);
GameManager에서 받아온 목숨 개수를 UIManager 스크립트에서 연동시켜줍니다.
SetNumber 함수에 목숨 개수를 넣어서 초기화 시켜주었습니다.
public class GameManger : MonoBehaviour
{
static GameManger instance = null;
void Awake() //호출순서 1번
{
//게임 기본값 지정하기
playerCount = 3;
if (instance == null) //첫번째 게임 매니저인지 확인
{
instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject); //첫번째가 아닌 게임매니저는 생성과 함께 삭제
}
}
게임매니저는 싱글톤 패턴을 사용하여 다른 스크립트에서 접근할 수 있게 해주었고,
//플레이어 및 게임전체 관련 변수
int playerCount; //플레이어 갯수
int playerLevel; //플레이어 레벨
float playerHP; //플레이어 체력
int score; //게임점수
int bulletCount; //총알갯수
int bombCount; //폭탄갯수
void SetDefaultData()
{
playerCount = 3; //플레이어 갯수 초기화
playerLevel = 2; //플레이어 레벨 초기화
playerHP = 100; //플레이어 체력 초기화
score = 0; //게임 점수
bulletCount = 50; //총알 갯수
bombCount = 0; //폭탄 갯수
}
게임에 필요한 데이터들을 변수로 만들어서 관리하고
게임 불러오기 기능을 통해 기본값이 담겨진 함수도 미리 만들어줍니다.
//playerCount Get/Set 프로퍼티//
public int PlayerCount { get { return playerCount; } set { playerCount = value; } }
PlayerCount(목숨 개수)는 데이터를 가져오거나 지정하기 편하게 프로퍼티로 만들어줍니다.
//마르코 리스폰하기
public void RespawnPlayer(Vector3 P)
{
//플레이어 갯수 감수
playerCount--;
if (playerCount > 0)
{
//마르코 스폰(Instantiate)
Quaternion Q = Quaternion.identity;
Instantiate(player, P, Q);
//플레이어 체력 복구
playerHP = 100;
}
else
{
Debug.Log("게임 오버");
}
}
이 함수는 리스폰 역할을 합니다.
플레이어가 죽을 때마다 이 함수를 호출시켜주면 소지한 목숨 개수를 감소시키고 리스폰하여 체력을 초기화 해줍니다.
만약 목숨 개수가 부족할 경우 게임 오버로 전환됩니다.
public static Object Instantiate(Object original, Vector3 position, Quaternion rotation);
Instantiate 오버로드 중 오브젝트를 인스턴스하기 위해 필요한 매개변수는 오브젝트, 위치값, 회전값이 있는데 위치값은 생성위치를 나타냅니다.
플레이어가 죽었을 때의 위치를 받아오기 위해서 RespawnPlayer 함수는 매개변수로 Vector3 값을 받아옵니다.
회전은 필요없으니 Quaternion.identity (회전 없음을 나타내는 속성)을 적어주었습니다.
게임 실행 시 목숨 개수가 기본값인 3으로 설정됩니다.
사진처럼 점수와 체력, 총알과 폭탄 소지수를 나타내는 UI를 만들 것입니다.
public class UINumer : MonoBehaviour
{
public Sprite[] num;
Image num1Img;
Image num2Img;
Image num3Img;
void Start()
{
num1Img = transform.Find("숫자1").GetComponent<Image>();
num2Img = transform.Find("숫자2").GetComponent<Image>();
num3Img = transform.Find("숫자3").GetComponent<Image>();
}
public void SetNumber(int n)
{
int num1 = n % 10;
int num2 = (n / 10) % 10;
int num3 = n / 100;
num1Img.sprite = num[num1];
num2Img.sprite = num[num2];
num3Img.sprite = num[num3];
}
}
타이머를 만들때 한 것처럼 숫자 0 ~ 9의 스프라이트를 저장할 배열을 만들고 매개변수로 넘겨받은 n의 값을 각 자리별로 분할하여 스프라이트를 변경시켜줍니다.
이 스크립트는 점수 표기와 더불어 보유한 총알, 폭탄의 소지수를 표기하는데 같은 스크립트를 사용하게 됩니다.
//플레이어 점수 출력하기
UINumer score = transform.Find("점수").GetComponent<UINumer>();
score.SetNumber(manager.Score);
//총알 갯수 출력하기
UINumer bulletCount = transform.Find("총알갯수").GetComponent<UINumer>();
bulletCount.SetNumber(manager.BulletCount);
//폭탄 갯수 출력하기
UINumer bombCount = transform.Find("폭탄갯수").GetComponent<UINumer>();
bombCount.SetNumber(manager.BombCount);
UI Manager에서 위에서 만든 UINumber 클래스로 3가지 변수를 만들어주고
SetNumber 함수의 매개변수에는 게임매니저에서 받아온 점수, 총알개수, 폭탄개수를 전달합니다.
void SetDefaultData()
{
playerCount = 3; //플레이어 갯수 초기화
playerLevel = 2; //플레이어 레벨 초기화
playerHP = 100; //플레이어 체력 초기화
score = 0; //게임 점수
bulletCount = 50; //총알 갯수
bombCount = 0; //폭탄 갯수
}
void Explode()
{
//TODO : (1)총알 갯수 늘리기
// (2)박스 제거
// (3)박스 제거 애니메이션 효과
// (4)마르코 총알 제거
// (5)마르코 총알 폭발 효과
//총알 갯수 늘리기//
GameManger manager = GameManger.Instance;
manager.BulletCount += 100;
점수와 총알 개수, 폭탄 개수는 전에 작성한 게임매니저의 SetDefaultData (기본값 설정) 함수의 영향을 받습니다.
Explode 함수는 BulletBox 스크립트인데 BulletBox라는 게임 내 오브젝트를 파괴할 시 플레이어의 총알소지수가 100이 상승하는 코드이며,
돼지 오브젝트의 코드에도 게임매니저를 호출하여 사망 시 점수를 올려주는 기능을 추가했습니다.
//score Get/Set 프로퍼티//
public int Score { get { return score; } set { score = value; } }
//bulletCount Get/Set 프로퍼티//
public int BulletCount { get { return bulletCount; }
set {
//bulletCount = (value > 999) ? 999 : value;
bulletCount = Mathf.Min(value, 999);
/*
bulletCount = value;
if (bulletCount > 999)
{
bulletCount = 999;
}
*/
}
}
//bombCount Get/Set 프로퍼티//
public int BombCount { get { return bombCount; } set { bombCount = value; } }
Get Set 프로퍼티를 사용하면 변수의 데이터를 은닉함과 동시에 가져오거나(Get) 설정하는데(Set) 편리함이 있습니다.
스프라이트를 조립하여 캡과 배경, 막대를 만들어줍니다.
이미지 컴포넌트의 이미지 타입을 Filled로 변경하면
Fill Amount가 슬라이더로 바뀌게 되는데,
사진처럼 체력을 표현할 때 사용할 수 있습니다.
public class PlayerHPBar : MonoBehaviour
{
Image barImg;
void Start()
{
barImg = transform.Find("막대").GetComponent<Image>();
}
public void SetFill(float amount)
{
barImg.fillAmount = amount;
}
}
매개변수로 받아온 amount에 따라 fill Amount를 조절하는 함수를 만들어주고
//플레이어 체력 UI 출력하기
float amount=manager.PlayerHP / 100;
PlayerHPBar playerHPBar = transform.Find("체력바").GetComponent<PlayerHPBar>();
playerHPBar.SetFill(amount);
//playerHP Get/Set 프로퍼티//
public float PlayerHP { get { return playerHP; } set { playerHP = value; } }
UI Manager에서 초기화를 해주고 플레이어의 체력을 받아와 100으로 나눈 값을 등록(SetFill)했습니다.
게임에서 플레이어 체력의 최대값은 100인데 Fill Amount는 최소값이 0, 최대값이 1이라 둘이 맞춰주기 위해서 플레이어의 체력을 100으로 나눠줘야 합니다.
게임매니저에는 정보 은닉과 동시에 외부에 의한 값의 변경을 위해 프로퍼티도 만들어줍니다.
체력은 기본 값(SetDefaultData) 함수에서 100으로 초기화를 했습니다.
숫자 스프라이트를 사용하여 타이머를 제작합니다.
public class PlayTimer : MonoBehaviour
{
public Sprite[] num;
//시간의 초 출력 이미지
Image num1Img;
Image num2Img;
//시간의 분 출력 이미지
Image num3Img;
Image num4Img;
void Start()
{
//출력 이미지 가져오기
num1Img = transform.Find("숫자1").GetComponent<Image>();
num2Img = transform.Find("숫자2").GetComponent<Image>();
num3Img = transform.Find("숫자3").GetComponent<Image>();
num4Img = transform.Find("숫자4").GetComponent<Image>();
}
public void SetTime(int time)
{
int sec = time % 60;
int min = time / 60;
int num1 = sec % 10;
int num2 = sec / 10;
int num3 = min % 10;
int num4 = min / 10;
num1Img.sprite = num[num1]; //숫자1의 이미지
num2Img.sprite = num[num2]; //숫자2의 이미지
num3Img.sprite = num[num3]; //숫자3의 이미지
num4Img.sprite = num[num4]; //숫자4의 이미지
}
}
숫자 스프라이트의 저장 공간 역할을 하는 Sprite 배열을 만들어줍니다.
mm:ss는 10의자리 분을 num4, 1의자리 분을 num3, 10의자리 초를 num2, 1의자리 초를 num1로 바꾸어놓으면 43:21이 되어서 타이머에 대한 구조를 쉽게 이해할 수 있습니다.
num1은 초를 10으로 나눈 나머지
num2는 초를 10으로 나눈 몫 (int형이기 때문에 소수점은 버려짐)
초가 20이라면 2, 0로 분할이 됩니다.
num3은 분을 10으로 나눈 나머지
num4는 분을 10으로 나눈 몫 (int형이기 때문에 소수점은 버려짐)
분도 초와 마찬가지로 작동합니다.
float startTime;
void Start()
{
startTime = Time.time; //UIManager가 시작될때 까지 걸린 시간
}
void Update()
{
//게임 타이머
int time =(int)(Time.time - startTime); //실수==>정수로 변환
PlayTimer playTimer = transform.Find("타이머").GetComponent<PlayTimer>();
playTimer.SetTime(time);
}
매개변수의 time은 아래와 같습니다.
Time.time = 게임을 시작한 이후 경과한 시간의 양을 가리키며 꾸준히 증가
Time.time에서 startTime을 빼면 타이머가 작동할 때에 무조건 0초부터 재생되는데요,
타이머가 작동해야할 시간 전까지 흘러간 무의미한 시간(startTime)을 빼주는 것입니다.
이 값을 SetTime(time)에 넣어주면 타이머가 작동하게 됩니다.
횡스크롤 게임은 스테이지가 x축으로 뻗어있기 때문에 카메라가 가만히 있고, 플레이어가 움직이다보면 화면 밖을 벗어나게 되는 일이 일어납니다.
따라서 카메라가 플레이어를 추적해야 할 필요가 있습니다.
카메라 스크립트 입니다.
게임오브젝트의 Find 함수로 플레이어를 받아온 후,
플레이어의 좌표를 cx, cy, cz로 재구성하여 카메라가 그 값을 추적하게 했습니다.
처음엔 카메라를 플레이어의 자식 오브젝트로 넣어 코드도 안쓰고 쉽게 해결하려 했었지만
테스트 과정에서 플레이어가 사망하고 inactive되는 경우 게임 화면이 아무것도 보이지 않게 되는 문제가 있었습니다.
이것을 방지하기 위해 두 개 이상의 카메라를 사용하는 것은 배보다 배꼽이 더 큰 상황이기 때문에 플레이어의 중심축을 기준으로 카메라를 관리하기로 하였습니다.
또진 사진처럼 배경이 카메라 사이즈에 맞게 구성된 경우도 있는데 이럴 때 플레이어가 양 벽의 가장자리로 붙어버리면 게임화면에 배경 밖의 빈공간이 노출되서 게임의 질이 낮아질 수 있어,
스크립트로 카메라가 배경 밖을 넘어가지 못하도록 경계를 그어놓는 최소 x, y값을 지정해주며 이 보정된 값이 cx, cy, cz가 됩니다.
프로펠러 애니메이션과 기체의 애니메이션을 만들었습니다.
헬기의 이동 경로 위치값을 전달할 수 있게 빈 오브젝트를 여러개 생성하여 위치 좌표로 사용할 것입니다.
노란색 기즈모 : 헬기의 이동 경로
빨간색 기즈모 : 헬기의 공격 오브젝트 생성 위치
기즈모 함수
public class DrawGizmo : MonoBehaviour { public float radius = 10; public Color color = new Color(1, 0, 0, 1); void OnDrawGizmos() { Gizmos.color = color; Vector3 center = transform.position; Gizmos.DrawWireSphere(center, radius); } }
기즈모를 만들어두면 씬 뷰에서 빈 오브젝트 등을 관리할 때 편리하게 쓸 수 있습니다.
여기에서 Gizmos.DrawWireSphere 함수는 Wireframe 구체를 표기하는 역할을 합니다.
Gizmos 클래스는 구체 외에도 다양한 정적 함수를 제공합니다.
public class HeliBody : MonoBehaviour
{
//헬기 상태 열거형
enum State { hide=0, move=1, attack=2 }
public Color hitColor;
public float hp = 100;
public GameObject effect;
public GameObject popupScore;
public GameObject bomb;
public Transform[] points;
public float speed = 50;
int pointIdx = 0;
SpriteRenderer spriteRenderer;
State state; //헬기 상태 변수
bool isAttacking; //현재 공격중인지를 판단하는 변수
헬기도 플레이어처럼 3가지 상태가 있어서 열거형을 작성해줍니다.
hitColor 변수는 헬기가 플레이어의 공격에 맞았을 때 피격을 당했다는 시각 효과를 주기 위해 Color 클래스로 색상을 받아와 spriteRenderer의 color를 변경시켜줄 것입니다.
3가지 게임오브젝트를 받아오는데 이 중 두가지는 기체가 폭파되는 오브젝트(effect)와 기체의 공격 패턴인 폭탄을 사용했을 때의 오브젝트(bomb), 하나는 기체 폭파 시 화면에 노출될 플로팅 텍스트(popupScore)입니다.
Transfrom[] points는 헬기의 9가지 이동 경로(위치값)를 저장하기 위한 배열,
int pointIdx는 이동을 어디까지 했는지 체크하기 위한 변수,
bool isAttacking은 패턴의 중복실행을 방지하기 위한 변수입니다.
void Start()
{
spriteRenderer = GetComponent<SpriteRenderer>();
//헬기 초기 상태
state = State.hide;
isAttacking = false;
}
void Update()
{
switch (state)
{
case State.hide:
break;
case State.move:
//이동 포인트로 이동하기
Vector3 currPos = transform.position;
Vector3 destPos = points[pointIdx].position;
transform.position = Vector3.MoveTowards(currPos, destPos, speed * Time.deltaTime);
float dist = Vector3.Distance(currPos, destPos);
if (dist <= 0.5)
{
pointIdx++; //다음번 목적지 포인트 인덱스
if (pointIdx >= points.Length) //마지막 포인트에서는 다시 0번 부터 시작하게함
{
pointIdx = 0;
}
if (pointIdx == 0 || pointIdx == 1 || pointIdx == 2 || pointIdx == 3)
{
//이동지점에서 폭탄 투하
state = State.attack;
}
}
break;
case State.attack:
if (isAttacking == false) //현재 공격중이 아닌지를 체크
{
StartCoroutine(BombDrop()); //폭탄 투하 공격 코루틴 시작
isAttacking = true; //현재 공격중임을 표시
}
break;
}
}
Start 함수에서 Sprite Renderer 초기화하고
헬기의 초기 상태를 State.hide로, 공격 상태는 false로 둡니다.
(hide는 플레이어와 접촉이 되지 않은 상태)
Update 함수에서는 헬기의 상태별 행동을 구현합니다.
State.move 작동 원리:
![]()
- 목적지(destPos)는 points[pointIdx].position입니다.
(points[]는 사전에 지정한 노란 기즈모들의 위치 값)- pointIdx의 초기값이 0이기 때문에 헬기가 points[0].position로 MoveTowards 함수를 작동합니다.
- 현재 위치에서 목적지까지의 위치가 0.5 미만이면 pointIdx의 크기를 1 증가시킵니다.
- 목적지(destPos)또한 points[1].position로 변경되며 그 다음 위치로 이동합니다.
- 3 ~ 4번을 계속 반복하며, 만약 pointIdx의 값이 배열의 최대 길이(9)보다 커지게 되면 (9번 반복한 상황) pointIdx의 값을 0으로 초기화합니다.
- 그와 동시에 pointIdx가 points[0], [1], [2], [3]의 위치에 있다면 공격 상태(State.attack)로 전환합니다.
- 공격 중인지 체크를 하고, 아니라면 BombDrop() 공격 코루틴을 실행합니다.
IEnumerator BombDrop()
{
Transform dropPos = transform.Find("폭탄투하위치");
Vector3 P = dropPos.position;
Quaternion Q = Quaternion.identity;
Vector3[] offsets = { new Vector3(-5, 0, 0), new Vector3(0, 0, 0), new Vector3(5, 0, 0) };
for (int i = 0; i < 3; i++)
{
//offset 랜덤선택
int j = Random.Range(0, 3);
//폭탄 생성
Instantiate(bomb, P + offsets[j] , Q);
//잠시 대기
yield return new WaitForSeconds(0.09f);
}
//테스트용 잠시대기
yield return new WaitForSeconds(1);
isAttacking = false; //폭탄 투하 공격 끝
state = State.move; //이동 상태로 변경함
}
State.move에서 State.attack으로 넘어오면 실행되는 공격 코루틴입니다.
폭탄투하 위치(빨간 기즈모)의 위치값을 가져온 후,
빨간 기즈모의 x좌표로부터 -5, +0, +5 중 랜덤하게 증가된 위치에서 폭탄을 생성합니다.
그리고 for문으로 반복시켜 총 3번 투하하게 합니다.
폭탄 투하를 마치면 1초 뒤 공격은 비활성화 상태로,
헬기 상태는 move 상태로 변경시켜 이 패턴을 헬기가 플레이어를 감지한 직후 부숴지기 전까지 무한히 반복하게 됩니다.
void OnTriggerEnter2D(Collider2D collision)
{
string tag = collision.tag;
if (tag == "마르코총알")
{
StartCoroutine(HitColor()); //총알 히트 색변경 효과 코루틴
//총알 데미지
hp = hp - 10;
if (hp <= 0)
{
//헬기 폭발 코루틴
StartCoroutine(Explode());
}
}
}
헬기는 충돌 체크로 플레이어의 총알 태그를 가진 오브젝트에만 피해를 입으며,
각 탄환당 hp가 10씩 감소되어 총 10발에 맞으면 헬기 폭발(Explode) 코루틴을 호출하게 하였습니다.
//헬기 폭발 효과 코루틴
IEnumerator Explode()
{
float[] radius = { 10 , 15 , 20 , 25 , 30 };
int[] num = { 2 , 4 , 5 , 6 , 8 };
float[] time = { 0.05f, 0.05f, 0.1f , 0.1f , 0.15f };
Vector3 offset = new Vector3(0, -20, 0);
Vector3 P = transform.position + offset;
Quaternion Q = Quaternion.identity;
for (int i = 0; i < 5; i++)
{
//헬기 폭발
for (int j = 0; j < num[i] ; j++)
{
Vector3 V = Random.insideUnitCircle * radius[i];
Instantiate(effect, P + V, Q);
}
//잠시 대기
yield return new WaitForSeconds(time[i]);
}
//팝업 점수 객체 생성하기
GameObject scoreObj = Instantiate(popupScore, P, Q);
Text scoreText = scoreObj.transform.Find("Text").GetComponent<Text>();
scoreText.text = "50";
//게임매니저 점수 올리기
GameManger manger = GameManger.Instance;
manger.Score += 50;
//헬기 제거(부모객체를 찾아서 전체를 제거)
Destroy(transform.parent.gameObject);
}
Random.insideUnitCircle 함수는 원 안에 랜덤 포인트를 반환하는데,
radius[i] 를 곱하게 되면 반경 radius[i]만큼의 원 안에서 반환하게 됩니다.
radius, num, time에 배열로 값을 지정해둔 이유는 헬기가 터질때 생성되는 effect 오브젝트의 랜덤성을 강조하기 위함입니다.
for문을 5번 반복시키며
(5번인 이유는 각 배열의 인덱스의 개수가 공통적으로 5개이기 때문에)
effect의 발생빈도를 num[]번인 2, 4, 5, 6, 8 번 출현되게 하고 이펙트가 실행되는 크기인 Randun.insideUnitCircle * radius에서 radius의 값도 10, 15, 20, 25, 30씩 순차적으로 커지는 원 안에서의 랜덤 포인트를 반환합니다.
쉽게말해 for문이 첫번째 실행될때는
반경 10의 원에서 2번 폭발, 대기시간은 0.05초
두번째 실행될때는
반경 15의 원에서 4번 폭발, 대기시간은 0.05초
이런 식으로 점차 폭발 빈도가 느는 것입니다.
같은 헬기라도 폭발 시 이펙트가 다르게 연출됩니다.
//총알 맞은 효과 코루틴
IEnumerator HitColor()
{
//총알 맞은 효과 - 이미지에 색깔 추가하기
spriteRenderer.color = hitColor;
//잠시대기
yield return new WaitForSeconds(0.1f);
//원래 색으로 복구하기
spriteRenderer.color = new Color(1, 1, 1, 1);
}
헬기 피격 효과 코드입니다.
hitColor 변수는 접근제한자를 public으로 선언했기 때문에 인스펙터 창에서 직접 수정할 수 있습니다.
//플레이어(마르코) 접근을 알려주는 함수//
public void OnDetectPlayer()
{
//헬기가 이동상태 전이
state = State.move;
}
헬기는 플레이어랑 떨어져 있을때 화면 밖에선 스스로 움직이지 하지않기 위해
평상시는 hide 상태에 놓고, 플레이어가 인접했을 때만 State.move로 작동하게 하기 위한 함수를 작성합니다.
public class DetectPlayer : MonoBehaviour
{
HeliBody heliBody;
void Start()
{
//헬기 바디 스크립트 컴포넌트찾기
heliBody = transform.Find("헬기몸체").GetComponent<HeliBody>();
}
void OnTriggerEnter2D(Collider2D collision)
{
string tag = collision.tag;
if (tag == "마르코")
{
heliBody.OnDetectPlayer();
}
}
}
DetectPlayer(플레이어 추적) 스크립트 입니다.
콜라이더에 충돌된 오브젝트의 태그가 플레이어라면 이동 상태로 변경해주는 OnDetectPlayer 함수를 실행합니다.
헬기 부모 오브젝트에 콜라이더와 DetectPlayer 스크립트를 입혀주고
플레이더의 충돌을 감지하는 콜라이더의 크기를 설정해줍니다.
폭발 효과 애니메이션 입니다.
public class BombEffect : MonoBehaviour
{
CircleCollider2D circleCol;
void Start()
{
circleCol = GetComponent<CircleCollider2D>();
}
void OnAnimationEnd()
{
Destroy(gameObject);
}
void OnDisableTrigger()
{
circleCol.enabled = false; //컴포넌트..사용중지
}
}
OnAnimationEnd는 자신의 오브젝트를 삭제하는 함수입니다.
OnDisableTrigger는 오브젝트의 Circle Collider 컴포넌트를 받아오고 콜라이더 컴포넌트를 비활성화하는 함수입니다.
비활성화되면 is Trigger가 작동하지 않게 됩니다.
그리고 애니메이션의 중간 지점에 OnDisableTrigger 함수를 Add Event하여 불꽃이 꺼질 때는 피격되지 않는 효과를 주며,
종료되는 부분에는 OnAnimationEnd 함수를 Add Event하여 해당 오브젝트가 스스로 삭제되게 합니다.
마지막으로 해당 오브젝트를 프리팹화 합니다.
헬기의 State.attack 상태에서 사용될 폭탄 오브젝트
public class Bomb : MonoBehaviour
{
public GameObject effect;
public float speed = 100;
void Update()
{
transform.Translate(-Vector2.up * speed * Time.deltaTime);
}
void OnTriggerEnter2D(Collider2D collision)
{
string tag = collision.tag;
if (tag == "배경")
{
//폭탄 폭발 효과
Vector3 P = transform.position;
Quaternion Q = Quaternion.identity;
Vector3 offset = new Vector3(0, -20, 0);
Instantiate(effect, P + offset, Q);
//폭탄 객체 제거
Destroy(gameObject);
}
}
}
앞서 작성한 폭발 효과를 게임 오브젝트 effect로 받아왔습니다.
다른 오브젝트는 접근제한자를 private으로 두고 Find 함수를 이용해 오브젝트를 검출하였으나,
Find 함수는 씬 뷰에 있는 (=하이라이키 창에 있는) 오브젝트만 검출하기 때문에,
사전에 추가해놓지 않은 프리팹 오브젝트는 검출될 수 없습니다.
그래서 public으로 직접 프리팹을 등록해주었습니다.
프리팹도 꺼내놓으면 Find로 찾을 수 있지만 폭발 효과는 사전에 추가할 필요가 없이 공격 패턴에만 꺼내오면 되는 오브젝트라서 효율적인 메모리 관리를 위해 상황에 따라 유용한 방법을 사용했습니다.
PlayerPrefs는 간단한 데이터 저장을 위한 클래스입니다.
public void SaveData()
{
//기존저장값을 모두 삭제하고 저장할 필요가 있는 값들만 저장하기 위해서 기본 저장값을 모두 삭제
PlayerPrefs.DeleteAll(); //저장 리셋 후 필요한 값들만 저장
//저장시작 표시
PlayerPrefs.SetString("스테이지", "저장시작");
PlayerPrefs.SetInt("playerLevel", playerLevel);
PlayerPrefs.SetFloat("playerHP" , playerHP);
PlayerPrefs.SetInt("score" , score);
PlayerPrefs.SetInt("bulletCount", bulletCount);
PlayerPrefs.SetInt("bombCount" , bombCount);
//저장완료 표시
PlayerPrefs.SetString("스테이지", "저장완료");
Debug.Log("게임저장완료");
}
DeleteAll() : 모든 키 값을 삭제합니다.
SetString : 지정한 Key로 string형 데이터 저장합니다.
SetFloat : 지정한 Key로 float형 데이터 저장합니다.
SetInt : 지정한 Key로 int형 데이터 저장합니다.
SaveData 함수는 플레이어 목숨, 체력, 점수 등의 데이터를 저장하기 위한 함수입니다.
게임 매니저 스크립트에서 데이터 저장 코드를 작성해줍니다.
//씬 저장,로드
if (Input.GetKey(KeyCode.Escape) == true)
{
//씬 이동전 현재 게임상태를 저장
manager.SaveData();
//새로운 씬으로 이동
SceneManager.LoadScene(0); //씬 이름 또는 번호로 로드
}
스테이지 씬의 UI Manager 스크립트의 Update문에서는 ESC키에 신호를 받으면 데이터를 저장을 한 뒤 인트로 씬으로 전환되는 코드를 추가해줍니다.
LoadScene() 함수에는 매개변수로 int 혹은 string 값을 받을 수 있습니다.
씬 이름 대신 번호를 적었는데 이 번호는 Build Setting에서 위 사진처럼 각 씬의 이름 옆 우측에서 확인할 수 있습니다.
그림처럼 간단하게 씬을 전환할 수 있게 됩니다.
void OnEnable()
{
SceneManager.sceneLoaded += SceneLoaded;
}
//씬 로드 델리게이트 함수
void SceneLoaded(Scene scene, LoadSceneMode mode)
{
if (scene.name == "스테이지")
{
if (PlayerPrefs.HasKey("스테이지") == true)
{
string saved = PlayerPrefs.GetString("스테이지");
if (saved == "저장완료")
{
//저장에 성공할때
playerLevel = PlayerPrefs.GetInt("playerLevel");
playerHP = PlayerPrefs.GetFloat("playerHP");
score = PlayerPrefs.GetInt("score");
bulletCount = 100;
bombCount = PlayerPrefs.GetInt("bombCount");
}
else
{
//저장값이 없을때
SetDefaultData(); //기본값 지정
}
}
else
{
//저장값이 없음
SetDefaultData(); //기본값 지정
}
}
}
씬 로드시 데이터를 불러오는 코드입니다. 씬이 로드될 때 자동적으로 데이터를 가져옵니다.
HasKey(string Key) : Key가 존재하는지 확인합니다.
GetString(string Key) : Key값으로 저장된 string형 데이터를 불러옵니다.
GetInt(string Key) : Key값으로 저장된 int형 데이터를 불러옵니다.
GetFloat(string Key) : Key값으로 저장된 float형 데이터를 불러ㅏ옵니다.
(출처) SaveData가 호출된 당시에 기록된 데이터를 로드해주며 만약 데이터가 없다면 아래의 초기값을 넘겨주는 코드가 실행됩니다.
void SetDefaultData()
{
playerCount = 3; //플레이어 목숨 초기화
playerLevel = 2; //플레이어 레벨 초기화
playerHP = 100; //플레이어 체력 초기화
score = 0; //게임 점수
bulletCount = 50; //총알 개수
bombCount = 0; //폭탄 개수
}
돼지를 처치해서 10점을 올린 채로 재시작하여도 110점이 유지됩니다. 또한 감소된 체력도 초기화되지 않았습니다. 데이터가 제대로 저장된 것입니다!!
데이터는 어디에 저장되는가?
PlayerPrefs에서 저장된 데이터는 개인의 하드웨어에 저장되며,HKEY_CURRENT_USER\SOFTWARE\Unity\UnityEditor\DefaultCompany\프로젝트이름
Regedit(레지스트리 편집기)에서 확인할 수 있습니다.
다만 이렇게 직접 확인할 수 있다는 것은 보안에 취약하다는 얘기입니다.
그래서 온라인 게임의 경우 중요한 데이터는 암호화와 서버를 통해 관리하며,
비교적 중요도가 낮은 데이터는 서버에 부하를 주지 않기 위해 PlayerPrefs를 사용해 데이터를 관리한다고 합니다.
Ctrl+Shift+B로 Build Settings에 들어가 씬 넘버를 확인합니다.
지금은 두 개의 씬밖에 없지만, 가장 먼저 0번 씬이 재생되니 큰 프로젝트에서 다수의 씬을 작업한다면 이 순서에 항상 유의해야할 것 같습니다.
Player Settings에 들어가면 회사명, 게임명, 게임버전, 게임아이콘, 번들ID 등 다양한 설정이 가능합니다. 게임을 상품화하거나 출시할 때 다뤄야 하는 부분이 많은 것 같아서 제대로 살펴보지는 않았습니다.
빌드 후 유니티 프로젝트 경로에 게임을 실행할 수 있는 exe 파일이 생겼습니다.
exe 파일과 dll 파일은 같이 있어야 실행이 되는데요, 하나의 exe 파일로 합치는 방법도 있다고도 합니다.
빌드된 게임은 실행 시 Unity 로고가 나옵니다.
무료 버전 Unity에서는 로고를 제거할 수 없습니다.
만든 게임으로 수익 창출이 되면 정품 Unity를 필수적으로 이용해야 하며 그제야 로고가 제거된다고 합니다.
velog는 영상 첨부가 불가능하여 이 곳에 들어가면 확인할 수 있습니다.
혹시 저 스프라이트 리소스를 어디서 구하셨는지 알 수 있을까요?