미니게임 제작 15
앞으로는 간단한 게임들을 만들어보며 추가적인 기능에 대해 공부를 해보려한다. 간단한 것부터 만들기 시작하여 만든 결과물을 레퍼런스 하여 덩치를 조금씩 키워나가는 과정을 밟아보자. 지식이 많지 않아 부족할 수 있지만, 하는데까지 해보겠다.
현재 구상 하고 있는 것은 예전에 잠깐 화제가 되었던 모바일 게임 매직 서바이벌 과 스팀에서 최근 붐이 일었던 게임 뱀파이어 서바이벌 의 장르를 본딴 생존 서바이벌 슈팅 게임이다.
오늘은 지금까지 구현한 기능들을 정리해보겠다.
조작
기본 조작 부분이다. 좌,우 대각선으로 이동할 때 애니메이션을 통일시키는 부분에 대해 생각할 점이 많았다.
랜덤 위치 적 생성 및 피격 이벤트
플레이어 주변의 랜덤한 위치에 적 개체를 생성되게 하고, 생성된 적 개체는 일정한 속도로 플레이어에게 다가가게 한다.
만약 적 개체가 플레이어에게 닿는데 성공한다면 플레이어에게 배정되어있는 Hp가 줄어들도록 설정했다.
플레이어 사망 및 게임 재시작과 점수
만약 플레이어의 Hp가 0이 된다면 게임은 종료되고, 현재까지 누적된 점수가 최고 점수일 경우 게임 데이터에 저장한다. 점수는 적을 처치했을 때, 일정 시간 버틸 때마다 오른다. ‘R’ 키를 누르면 게임을 재시작 할 수 있다.
공격 기능과 적 처치, 적 처치 시 점수 증가
플레이어는 현재 위치에서 가장 가까운 적의 방향으로 일정 시간마다 공격을 날린다. 공격에 적이 맞으면 적은 사라지며, 각 적 개체에 배정되어 있는 점수만큼 Score가 오른다. 이 시점에서 적 개체의 종류는 기존 1개에서 1개 더 추가하여 두 종류가 되었다.
무한 맵 이동
탑뷰에서의 맵 스크롤링을 구현하여 플레이어가 어느 방향으로 이동하던지 주어진 타일에서 벗어날 수 없게 하였다.
최종 게임 화면
이전 포스팅과 변경된 점은, 2번째 적 개체의 속도를 2배로 올린 것, 그리고 생성 주기를 약간 단축시켰다.
소스코드는 다음과 같다.
PlayerControl.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class PlayerControl : MonoBehaviour
{
// rigidbody 선언
Rigidbody2D playerRigidbody;
float H;
float V;
// 죽었는지 여부 확인할 변수
bool isDie = false;
public float speed = 8;
Animator anim;
GameManager instance;
void Start()
{
// rigidbody 컴포넌트 가져와 할당하기
playerRigidbody = GetComponent<Rigidbody2D>();
// Animator 컴포넌트 가져와 할당하기
anim = GetComponent<Animator>();
instance = GameManager.Instance;
}
void Update()
{
MoveSet();
}
void FixedUpdate()
{
Move();
}
void MoveSet()
{
H = Input.GetAxisRaw("Horizontal");
V = Input.GetAxisRaw("Vertical");
if (!isDie)
{
//Animation
if (anim.GetInteger("vAxisRaw") != V)
{
if (anim.GetInteger("hAxisRaw") != 0 && anim.GetInteger("vAxisRaw") == 0)
{
anim.SetInteger("hAxisRaw", (int)H);
anim.SetBool("isChange", true);
}
else
{
anim.SetInteger("vAxisRaw", (int)V);
anim.SetBool("isChange", true);
}
}
else if (anim.GetInteger("hAxisRaw") != H)
{
anim.SetInteger("hAxisRaw", (int)H);
anim.SetBool("isChange", true);
}
else
{
anim.SetBool("isChange", false);
}
}
}
public void die()
{
// 플레이어 사망 애니메이션 재생
anim.SetBool("isDie", true);
isDie = true;
// 조작 막기
speed = 0;
}
void Move()
{
Vector2 dirVec = new Vector2(H, V);
playerRigidbody.velocity = dirVec * speed;
}
void OnCollisionEnter2D(Collision2D other)
{
if (other.gameObject.tag == "Enemy")
{
instance.hpDown(other.gameObject.);
Destroy(other.gameObject);
}
}
}
플레이어 조작과 관련된 스크립트 파일이다.
Enemy.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Enemy : MonoBehaviour
{
// 플레이어 위치
public Transform playerTransform;
// Enemy 위치
Transform enemyTransform;
Rigidbody2D enemyRigidbody;
SpriteRenderer rend;
public float speed = 3f;
Vector2 vec;
bool rotation;
// 초기화 메서드
public void init(Transform playerTransform)
{
this.playerTransform = playerTransform;
}
void Start()
{
enemyTransform = GetComponent<Transform>();
enemyRigidbody = GetComponent<Rigidbody2D>();
rend = GetComponent<SpriteRenderer>();
}
void Update()
{
MoveSet();
}
void FixedUpdate()
{
Move();
}
void MoveSet()
{
// 플레이어로의 방향 벡터 구하기
vec = playerTransform.position - enemyTransform.position;
if (vec.x > 0)
{
rotation = true;
}
else
{
rotation = false;
}
// 벡터를 단위벡터화 시킨다.
vec.Normalize();
}
void Move()
{
enemyRigidbody.velocity = vec * speed;
if (!rotation)
{
rend.flipX = true;
}
else
{
rend.flipX = false;
}
}
}
적 개체가 가지고 있는 스크립트이다.
EnemySpawner.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class EnemySpawner : MonoBehaviour
{
// 몬스터
public GameObject[] enemys;
// 플레이어 위치
public Transform playerTransform;
// 최대거리
public float maxDist = 8f;
// 최소거리
public float minDist = 3f;
public float timeSpawnMax = 5f; // 최대 주기
public float timeSpawnMin = 2f; // 최소 주기
public float timeSpawn;
private float lastSpawn; // 가장 최근 생성
void Start()
{
// 생성 주기 최소와 최대 사이에서 랜덤하게 초기화
timeSpawn = Random.Range(timeSpawnMin, timeSpawnMax);
lastSpawn = 0;
}
void Update()
{
if (Time.time >= lastSpawn + timeSpawn)
{
// 최근 생성 시간 갱신
lastSpawn = Time.time;
// 생성주기 랜덤 변경
timeSpawn = Random.Range(timeSpawnMin, timeSpawnMax);
Spawn();
}
}
void Spawn()
{
Vector2 spawnPos = GetRandomPoint(playerTransform.position, maxDist);
GameObject selectedEnemy = enemys[Random.Range(0, enemys.Length)];
GameObject enemy = Instantiate(selectedEnemy, spawnPos, Quaternion.identity);
Enemy es = enemy.GetComponent<Enemy>();
es.init(playerTransform);
}
Vector2 GetRandomPoint(Vector2 center, float distance)
{
// 중앙을 중심으로 반지름이 maxDist인 원 안에서 랜덤 위치 저장
// Random.insideUnitCircle은 반지름이 distance에서
Vector2 randomPos = Random.insideUnitCircle * distance + center;
return randomPos;
}
public void gameover()
{
gameObject.SetActive(false);
}
}
플레이어 주변의 랜덤한 위치에 랜덤한 주기마다 적 개체를 생성시키는 스크립트이다.
Bullet.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Bullet : MonoBehaviour
{
GameManager instance;
void Start()
{
instance = GameManager.Instance;
// 3초 뒤에 자신의 게임 오브젝트 파괴
Destroy(gameObject, 3f);
}
private void OnTriggerEnter2D(Collider2D other)
{
if (other.gameObject.tag == "Enemy")
{
//빈번히 일어나는 작업에는 GetComponent를 사용하지 않는 것이 좋다고 알고있기 때문에,
//오브젝트의 이름으로 적 개체를 구분지으려고 한다.
instance.scoreUp(other.gameObject.name);
Destroy(other.gameObject);
Destroy(gameObject);
}
}
}
플레이어가 발사하는 총알이 가지는 스크립트이다.
BulletSpanwer.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BulletSpanwer : MonoBehaviour
{
List<GameObject> foundEnemys;
GameObject enemy;
public GameObject bullet;
public string tagName = "Enemy";
float minDist;
float speed = 3f;
void Start()
{
// 코루틴 시작
StartCoroutine("attack");
}
IEnumerator attack()
{
while (true)
{
// 1초에 한번씩 수행
attackBullet();
yield return new WaitForSeconds(1.0f);
}
}
void attackBullet()
{
foundEnemys = new List<GameObject>(GameObject.FindGameObjectsWithTag(tagName));
// Enemy가 있을 경우에만 수행
if (foundEnemys.Count != 0)
{
minDist = Vector2.Distance(gameObject.transform.position, foundEnemys[0].transform.position);
enemy = foundEnemys[0];
foreach (GameObject foundEnemy in foundEnemys)
{
// 현재 오브젝트의 위치와 적 개체의 위치를 통해 거리를 얻어오기
float dist = Vector2.Distance(gameObject.transform.position, foundEnemy.transform.position);
// 현재 저장된 최소 거리보다 지금 거리가 더 작다면
if (dist < minDist)
{
// 최소 거리 저장 및 최소 거리 객체 저장
enemy = foundEnemy;
minDist = dist;
}
}
// 플레이어로부터 적 까지의 벡터 구하기
Vector2 vec = enemy.transform.position - gameObject.transform.position;
// 벡터를 단위벡터화
vec.Normalize();
// 벡터로 각도 구하기
float angle = Vector3.SignedAngle(transform.up, vec, transform.forward);
// 소환 포지션 정하기
Vector3 spawnPos = gameObject.transform.position;
spawnPos.z++;
// 생성하기
GameObject attBullet = Instantiate(bullet, spawnPos, Quaternion.identity);
attBullet.transform.Rotate(0, 0, angle + 90);
attBullet.GetComponent<Rigidbody2D>().velocity = vec * speed;
}
}
}
일정시간 마다 플레이어로부터 총알을 발사시키는 스크립트이다.
GameManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
public class GameManager : MonoBehaviour
{
// 플레이어 Hp
int health = 4;
// 점수
float score;
// 게임 오버 체크 변수
bool gameover = false;
// Enemy 오브젝트 이름 저장할 변수
public Dictionary<string, int> EnemyScoreByName;
// hp Bar 객체
public HpBar hpBar;
// 플레이어 컨트롤 변수
public PlayerControl playerControl;
// EnemySpawner 변수
public EnemySpawner enemySpawner;
// 게임오버 텍스트 변수
public Text gameoverText;
// 실시간 점수 표시 텍스트
public Text scoreText;
// 최고 점수 표시 텍스트
public Text bestScoreText;
// Private 기본생성자 : sigleton
private GameManager() { }
// 인스턴스 변수 생성 : sigleton
private static GameManager instance = null;
void Awake()
{
// 게임 오브젝트 생성 시 객체생성.
if (instance == null)
{
instance = this;
}
}
void Start()
{
score = 0;
EnemyScoreByName = new Dictionary<string, int>();
EnemyScoreByName.Add("Enemy01(Clone)", 10);
EnemyScoreByName.Add("Enemy02(Clone)", 30);
}
public static GameManager Instance
{
get
{
if (instance == null)
{
return null;
}
return instance;
}
}
void Update()
{
if (gameover && Input.GetKeyDown(KeyCode.R))
{
restart();
}
else if (!gameover)
{
scoreUpdate();
}
}
public void hpDown()
{
if (health > 1)
{
// Hp 감소 및 hpBar 갱신
hpBar.setHp(--health);
}
else
{
// Hp 감소 및 hpBar 갱신
hpBar.setHp(--health);
endGame();
}
}
public void endGame()
{
// 플레이어의 사망 스크립트 수행(애니메이션 재생)
playerControl.die();
// EnemySpawner 정지
enemySpawner.gameover();
// 게임오버, 점수 UI 적용
gameoverText.gameObject.SetActive(true);
// 게임오버 체크 변수설정
gameover = true;
//"BestScore" 키로 저장되어있는 최고기록을 가져오기
float bestScore = PlayerPrefs.GetFloat("BestScore");
// 최고기록을 갱신했을 경우
if (score > bestScore)
{
// 최고기록 값을 현재 기록으로 변경
bestScore = score;
// "BestScore" 키로 최고기록을 저장
PlayerPrefs.SetFloat("BestScore", bestScore);
}
// 최고기록 표시
bestScoreText.text = "Best Score: " + (int)bestScore;
}
// 점수 갱신 메서드 : 시간에 따라 점수 증가 및 UI 표시
void scoreUpdate()
{
score += Time.deltaTime;
scoreText.text = "Score: " + (int)score;
}
// 게임 재시작 메서드 : 플레이어 사망 후 특정 조작을 통해 게임 재시작 : R키
void restart()
{
// Scene1 씬을 로드
SceneManager.LoadScene("Scene1");
}
// 적 개체에 따라서 점수 다르게 추가
public void scoreUp(string name)
{
score += EnemyScoreByName[name];
scoreText.text = "Score: " + (int)score;
}
}
플레이어 Hp 감소에 따 HpBar 감소, Hp가 0이 되었을 때 게임 오버 처리, 스코어 관리 와 같은 게임의 전반적인 관리를 하는 게임 매니저 스크립트이다.
HpBar.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class HpBar : MonoBehaviour
{
Image hpBar;
void Start()
{
hpBar = gameObject.GetComponent<Image>();
}
public void setHp(int health)
{
hpBar.fillAmount = health / 4f;
}
}
Hp가 감소함에 따라 Hp바(초록색)가 줄어들게끔 하는, Hpbar 스크립트이다.
BgScrolling.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
public class BgScrolling : MonoBehaviour
{
public GameObject[] tmp_tile; //임시 타일 public으로 받기
public GameObject[,] tile; //임시 타일을 3x3으로 바꿀 변수
private int currentIndex_i; //현재 나의 타일 인덱스 i
private int currentIndex_j; //현재 나의 타일 인덱스 j
private string tmpStringIndex; //오브젝트 이름(ex. 00 ~ 22)로 받을 변수
private void Start()
{
tile = new GameObject[3, 3]; // 3x3
int cnt = 0;
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
tile[i, j] = tmp_tile[cnt]; //타일을 2차원으로 변경
cnt += 1;
}
}
tmpStringIndex = this.name; //오브젝트 이름을 스트링으로 받아서 인덱스에 넣음
currentIndex_i = int.Parse(tmpStringIndex[0].ToString());
currentIndex_j = int.Parse(tmpStringIndex[1].ToString());
}
private void OnTriggerExit2D(Collider2D other) //충돌 Exit처리 -> 나가면 배경이 바뀌어야 함
{
if (other.tag != "Player") return; //다른 충돌이면 그냥 리턴
Vector3 dir = other.transform.position - transform.position; //플레이어의 위치 - 타일의 중심 =>>> 타일 중심에서의 벡터가 나옴
float angle = Vector3.Angle(transform.up, dir); //각도
int sign = Vector3.Cross(transform.up, dir).z < 0 ? -1 : 1;
angle *= sign;
Debug.Log(angle);
if (-45 <= angle && angle <= 45) // 위쪽
{
for (int i = 0; i < 3; i++) // 타일 3개를 옮겨야 함
{
Debug.Log(tile[currentIndex_i, currentIndex_j].gameObject.name);
if (currentIndex_i + 1 <= 2) // 배열에 대한 예외처리, 아래의 else문은 currentIndex_i가 2일때임
{
if (tile[currentIndex_i + 1, i].transform.localPosition.y - transform.localPosition.y == -16)
{
tile[currentIndex_i + 1, i].transform.localPosition += new Vector3(0, 16 * 3, 0); //위쪽으로 가므로 아래행을 옮김
}
}
else
{
if (tile[0, i].transform.localPosition.y - transform.localPosition.y == -16) //currentIndex_i가 2일때는 아래가 0인덱스
{
tile[0, i].transform.localPosition += new Vector3(0, 16 * 3, 0);
}
}
}
}
else if (45 <= angle && angle <= 135) // 왼쪽
{
for (int i = 0; i < 3; i++)
{
if (currentIndex_j + 1 <= 2)
{
if (tile[i, currentIndex_j + 1].transform.localPosition.x - transform.localPosition.x == 17)
{
tile[i, currentIndex_j + 1].transform.localPosition += new Vector3(-17 * 3, 0, 0);
}
}
else
{
if (tile[i, 0].transform.localPosition.x - transform.localPosition.x == 17)
{
tile[i, 0].transform.localPosition += new Vector3(-17 * 3, 0, 0);
}
}
}
}
else if (135 <= angle || -135 >= angle) // 아래쪽 -135 ~ -180 or 135 ~ 180
{
for (int i = 0; i < 3; i++)
{
if (currentIndex_i - 1 >= 0)
{
if (tile[currentIndex_i - 1, i].transform.localPosition.y - transform.localPosition.y == 16)
{
tile[currentIndex_i - 1, i].transform.localPosition += new Vector3(0, -16 * 3, 0);
}
}
else
{
if (tile[2, i].transform.localPosition.y - transform.localPosition.y == 16)
{
tile[2, i].transform.localPosition += new Vector3(0, -16 * 3, 0);
}
}
}
}
else if (-135 <= angle && angle <= -45) // 오른쪽
{
for (int i = 0; i < 3; i++)
{
if (currentIndex_j - 1 >= 0)
{
if (tile[i, currentIndex_j - 1].transform.localPosition.x - transform.localPosition.x == -17)
{
tile[i, currentIndex_j - 1].transform.localPosition += new Vector3(17 * 3, 0, 0);
}
}
else
{
if (tile[i, 2].transform.localPosition.x - transform.localPosition.x == -17)
{
tile[i, 2].transform.localPosition += new Vector3(17 * 3, 0, 0);
}
}
}
}
}
}
탑뷰에서의 무한 스크롤링을 할 수 있게 하는 스크립트이다.
지금까지 미니게임의 구현한 내용을 정리해보았다. 이제 이 결과물을 조금씩 다듬어가며 디테일한 부분을 수정해 나가보자.